diff --git a/.github/workflows/publish_npm_scoped_x402_fastify.yml b/.github/workflows/publish_npm_scoped_x402_fastify.yml new file mode 100644 index 0000000000..c794464be0 --- /dev/null +++ b/.github/workflows/publish_npm_scoped_x402_fastify.yml @@ -0,0 +1,56 @@ +name: Publish @x402/fastify package to NPM + +on: + workflow_dispatch: + +jobs: + publish-npm-x402-fastify: + runs-on: ubuntu-latest + environment: ${{ github.ref == 'refs/heads/main' && 'npm' || '' }} + permissions: + contents: read + id-token: write + steps: + - uses: actions/checkout@v4 + + - name: Setup pnpm + uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 + with: + version: 10.7.0 + + - uses: actions/setup-node@v4 + with: + node-version: "24" + registry-url: "https://registry.npmjs.org" + cache: "pnpm" + cache-dependency-path: ./typescript + + - name: Update npm for OIDC trusted publishing + run: npm install -g npm@latest + + - name: Configure npm for trusted publishing + run: npm config delete always-auth 2>/dev/null || true + + - name: Install and build + working-directory: ./typescript + run: | + pnpm install --frozen-lockfile + pnpm -r --filter=@x402/core --filter=@x402/extensions --filter=@x402/fastify run build + + - name: Publish @x402/fastify package + working-directory: ./typescript/packages/http/fastify + run: | + # Get package information directly + PACKAGE_NAME=$(node -p "require('./package.json').name") + PACKAGE_VERSION=$(node -p "require('./package.json').version") + + echo "Package: $PACKAGE_NAME@$PACKAGE_VERSION" + + # Check if running on main branch + if [[ "${{ github.ref }}" == "refs/heads/main" ]]; then + echo "Publishing to NPM (main branch)" + pnpm publish --provenance --access public + else + echo "Dry run only (non-main branch: ${{ github.ref }})" + pnpm publish --dry-run --no-git-checks + fi diff --git a/.github/workflows/publish_npm_scoped_x402_stellar.yml b/.github/workflows/publish_npm_scoped_x402_stellar.yml new file mode 100644 index 0000000000..92d09e057b --- /dev/null +++ b/.github/workflows/publish_npm_scoped_x402_stellar.yml @@ -0,0 +1,56 @@ +name: Publish @x402/stellar package to NPM + +on: + workflow_dispatch: + +jobs: + publish-npm-x402-stellar: + runs-on: ubuntu-latest + environment: ${{ github.ref == 'refs/heads/main' && 'npm' || '' }} + permissions: + contents: read + id-token: write + steps: + - uses: actions/checkout@v4 + + - name: Setup pnpm + uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 + with: + version: 10.7.0 + + - uses: actions/setup-node@v4 + with: + node-version: "24" + registry-url: "https://registry.npmjs.org" + cache: "pnpm" + cache-dependency-path: ./typescript + + - name: Update npm for OIDC trusted publishing + run: npm install -g npm@latest + + - name: Configure npm for trusted publishing + run: npm config delete always-auth 2>/dev/null || true + + - name: Install and build + working-directory: ./typescript + run: | + pnpm install --frozen-lockfile + pnpm -r --filter=@x402/core --filter=@x402/stellar run build + + - name: Publish @x402/stellar package + working-directory: ./typescript/packages/mechanisms/stellar + run: | + # Get package information directly + PACKAGE_NAME=$(node -p "require('./package.json').name") + PACKAGE_VERSION=$(node -p "require('./package.json').version") + + echo "Package: $PACKAGE_NAME@$PACKAGE_VERSION" + + # Check if running on main branch + if [[ "${{ github.ref }}" == "refs/heads/main" ]]; then + echo "Publishing to NPM (main branch)" + pnpm publish --provenance --access public + else + echo "Dry run only (non-main branch: ${{ github.ref }})" + pnpm publish --dry-run --no-git-checks + fi diff --git a/.gitignore b/.gitignore index 52308c896f..91f46e3f18 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ .env +.env.local node_modules/ dist/ .turbo/ @@ -20,12 +21,17 @@ e2e/clients/mcp-go/mcp-client e2e/clients/mcp-go/mcp-go e2e/facilitators/go/facilitator e2e/facilitators/go/go +e2e/servers/echo/echo e2e/servers/gin/gin e2e/servers/gin/server +e2e/servers/nethttp/nethttp e2e/servers/mcp-go/mcp-go e2e/servers/mcp-go/mcp-server e2e/legacy/servers/gin/gin +# Example build artifacts +examples/go/servers/upto/upto + # Agent artifacts TASK.md .claude/ diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 6c17d67b53..683297dae1 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -135,6 +135,18 @@ x402 aims to be chain-agnostic. New chain implementations are welcome. Because different chains have different best practices, a scheme may have a different mechanism on a new chain than it does on EVM. If the scheme mechanism varies from the reference implementation, the x402 Foundation will re-audit the scheme for that chain before accepting. +### Adding a Default Asset for an EVM Chain + +If your chain is EVM-compatible and you want to add a default stablecoin for +dollar-string pricing (`"$0.10"`), you don't need the full 3-PR workflow below. See: + +- [Go: DEFAULT_ASSET.md](go/mechanisms/evm/DEFAULT_ASSET.md) +- [TypeScript: DEFAULT_ASSET.md](typescript/packages/mechanisms/evm/src/exact/server/DEFAULT_ASSET.md) + +These guides include a cross-SDK checklist of every file to update. + +### Adding a New Chain Family + ### PR 1: Specification Only Open a PR with specs for one payment scheme implementation. diff --git a/README.md b/README.md index 51ef38a7bc..efcdc0790a 100644 --- a/README.md +++ b/README.md @@ -51,9 +51,9 @@ go get github.com/coinbase/x402/go - **Open standard:** x402 is an open standard, freely accessible and usable by anyone. It will never force reliance on a single party. - **HTTP / Transport Native:** x402 is meant to seamlessly complement existing data transportation. It should whenever possible not mandate additional requests outside the scope of a typical client / server flow. - **Network, token, and currency agnostic:** we welcome contributions that add support for new networks (both crypto and fiat), signing standards, or schemes, so long as they meet our acceptance criteria laid out in [CONTRIBUTING.md](https://github.com/coinbase/x402/blob/main/CONTRIBUTING.md). x402 may extend support to fiat based networks, but will never deprioritize onchain payments in favor of fiat payments. -- **Backwards Compatible:** x402 will not deprecate support for any existing networks unless such removal is deemed necessary for the security of the standard +- **Backwards Compatible:** x402 will not deprecate support for any existing networks unless such removal is deemed necessary for the security of the standard. Whenever possible, x402 will aim for backwards compatibility for non-major version changes. - **Trust minimizing:** all payment schemes must not allow for the facilitator or resource server to move funds, other than in accordance with client intentions -- **Easy to use:** x402 needs to be 10x better than existing ways to pay on the internet. This means abstracting as many details of crypto as possible away from the client and resource server, and into the facilitator. This means the client/server should not need to think about gas, rpc, etc. +- **Easy to use:** It is the goal of the x402 community to improve ease of use relative to other forms of payment on the Internet. This means abstracting as many details of crypto as possible away from the client and resource server, and into the facilitator. This means the client/server should not need to think about gas, rpc, etc. ## Ecosystem diff --git a/contracts/evm/README.md b/contracts/evm/README.md index e744fb71b1..ddbad6a791 100644 --- a/contracts/evm/README.md +++ b/contracts/evm/README.md @@ -18,14 +18,23 @@ Both contracts: - Support both standard Permit2 and EIP-2612 flows - Deploy to the **same address on all EVM chains** via CREATE2 -## Deployed Addresses +## Canonical Addresses -### Base Sepolia (Testnet) +| Contract | Address | +|----------|---------| +| x402ExactPermit2Proxy | `0x402085c248EeA27D92E8b30b2C58ed07f9E20001` | +| x402UptoPermit2Proxy | `0x4020a4f3b7b90CCA423b9FabCC0CE57c6c240002` | -| Contract | Address | Verified | -|----------|---------|----------| -| x402ExactPermit2Proxy | [`0x4020cd856c882d5fb903d99ce35316a085bb0001`](https://sepolia.basescan.org/address/0x4020cd856c882d5fb903d99ce35316a085bb0001) | ✓ | -| x402UptoPermit2Proxy | [`0x40204513ec14919adfd30d77c0a991371b420002`](https://sepolia.basescan.org/address/0x40204513ec14919adfd30d77c0a991371b420002) | ✓ | +### Current Deployments + +| Chain | Exact | Upto | +|-------|-------|------| +| Base Mainnet | [Deployed](https://basescan.org/address/0x402085c248EeA27D92E8b30b2C58ed07f9E20001) | — | +| Base Sepolia | [Deployed](https://sepolia.basescan.org/address/0x402085c248EeA27D92E8b30b2C58ed07f9E20001) | [Legacy\*](https://sepolia.basescan.org/address/0x402039b3d6E6BEC5A02c2C9fd937ac17A6940002) | + +> \*The Base Sepolia Upto deployment at `0x4020...0002` predates the deterministic build fix +> and uses a different bytecode (with CBOR metadata). The canonical Upto address for all +> new deployments is `0x4020a4f3...0002`. ## Prerequisites @@ -34,13 +43,87 @@ Both contracts: ## Installation ```bash -# Install dependencies forge install - -# Build contracts forge build ``` +## Deploying to a New EVM Chain + +Anyone can deploy both contracts to their canonical addresses on any EVM chain. +No special build environment, private key, or permission is required—only gas on the target chain. + +### How it works + +Both contracts are deployed via [Arachnid's deterministic CREATE2 deployer](https://github.com/Arachnid/deterministic-deployment-proxy) +(`0x4e59b44847b379578588920cA78FbF26c0B4956C`), which exists at the same address on +virtually every EVM chain. The CREATE2 address depends only on the deployer, a salt, +and `keccak256(initCode)`—not on who sends the transaction. + +| Contract | Bytecode source | Why | +|----------|----------------|-----| +| **Exact** | Pre-built initCode in `script/data/exact-proxy-initcode.hex` | The original build included Solidity CBOR metadata (an IPFS hash that varies per build environment). The committed hex file is the exact initCode from the original deployment, ensuring the same address everywhere. | +| **Upto** | Compiled from source (`forge build`) | Built with `cbor_metadata = false` so the bytecode is identical on every machine at the same git commit. | + +### Step-by-step + +1. **Clone and build** + ```bash + cd contracts/evm + forge install + forge build + ``` + +2. **Verify expected addresses** (optional, no RPC needed) + ```bash + forge script script/ComputeAddress.s.sol + ``` + You should see: + - Exact → `0x402085c248EeA27D92E8b30b2C58ed07f9E20001` + - Upto → `0x4020a4f3b7b90CCA423b9FabCC0CE57c6c240002` + +3. **Check prerequisites on the target chain** + - [Permit2](https://github.com/Uniswap/permit2) must be deployed at `0x000000000022D473030F116dDEE9F6B43aC78BA3` + - The CREATE2 deployer must exist at `0x4e59b44847b379578588920cA78FbF26c0B4956C` + - Your wallet needs enough native gas to pay for deployment (~300k gas per contract) + +4. **Deploy** + ```bash + export PRIVATE_KEY="your_private_key" + + forge script script/Deploy.s.sol \ + --rpc-url \ + --broadcast \ + --verify + ``` + + The script automatically: + - Loads the pre-built initCode for Exact and compiler-derived initCode for Upto + - Skips any contract already deployed at the expected address + - Verifies `PERMIT2()` returns the correct address after deployment + +5. **Verify on Etherscan** (if `--verify` didn't work automatically) + ```bash + forge verify-contract x402UptoPermit2Proxy \ + --rpc-url \ + --constructor-args $(cast abi-encode "constructor(address)" 0x000000000022D473030F116dDEE9F6B43aC78BA3) + ``` + + For the Exact proxy, verification may require matching the original compiler metadata. + The verified source on Base Sepolia / Base Mainnet can be used as a reference. + +### Overriding Permit2 address + +If the target chain has Permit2 at a non-canonical address: + +```bash +export PERMIT2_ADDRESS="0x..." +forge script script/Deploy.s.sol --rpc-url --broadcast +``` + +> **Warning:** Overriding the Permit2 address changes the initCode for the Upto contract +> and will produce a different deployment address. The Exact contract's pre-built initCode +> already encodes the canonical Permit2 address and cannot be overridden. + ## Testing ```bash @@ -71,90 +154,66 @@ forge test --match-contract Invariants Fork tests run against real Permit2 on Base Sepolia: ```bash -# Set up environment export BASE_SEPOLIA_RPC_URL="https://sepolia.base.org" -# Run fork tests for Exact variant forge test --match-contract X402ExactPermit2ProxyForkTest --fork-url $BASE_SEPOLIA_RPC_URL - -# Run fork tests for Upto variant forge test --match-contract X402UptoPermit2ProxyForkTest --fork-url $BASE_SEPOLIA_RPC_URL ``` -## Deployment - -### Compute Expected Addresses +## Vanity Address Mining -```bash -forge script script/ComputeAddress.s.sol -``` +Both contracts use vanity addresses with prefix `0x4020` and suffix `0001` (Exact) or `0002` (Upto). -### Deploy to Testnet +The vanity miner is only needed if the contract source code changes (which changes the +initCodeHash and invalidates existing salts). To re-mine: ```bash -# Set environment variables -export PRIVATE_KEY="your_private_key" -export BASE_SEPOLIA_RPC_URL="https://sepolia.base.org" -export BASESCAN_API_KEY="your_api_key" - -# Deploy both contracts with verification -forge script script/Deploy.s.sol \ - --rpc-url $BASE_SEPOLIA_RPC_URL \ - --broadcast \ - --verify -``` - -### Deploy to Mainnet +cd vanity-miner -```bash -export BASE_RPC_URL="https://mainnet.base.org" +# Mine both contracts +cargo run --release -forge script script/Deploy.s.sol \ - --rpc-url $BASE_RPC_URL \ - --broadcast \ - --verify +# Mine only one +cargo run --release -- exact +cargo run --release -- upto ``` -## Vanity Address Mining +After mining, update the salt constants in `script/Deploy.s.sol` and `script/ComputeAddress.s.sol`, +and the init code hashes in `vanity-miner/src/main.rs`. -The deployment uses vanity addresses starting with `0x4020`. To mine new salts: +## Deterministic Build Configuration -```bash -cd vanity-miner -cargo run --release +The `foundry.toml` includes two settings that ensure bytecode reproducibility: + +```toml +cbor_metadata = false +bytecode_hash = "none" ``` -The Rust miner uses parallel processing for efficient address generation. Update the init code hashes in `vanity-miner/src/main.rs` if the contract bytecode changes. +Without these, the Solidity compiler appends a CBOR-encoded IPFS hash of the contract +metadata to the bytecode. This hash varies across build environments (even with identical +source code and compiler version), breaking CREATE2 address determinism. + +The `x402ExactPermit2Proxy` was deployed before this fix was in place, which is why it +uses a committed initCode hex file instead of compiler-derived bytecode. ## Contract Architecture ``` src/ +├── x402BasePermit2Proxy.sol # Shared settlement logic and Permit2 interaction ├── x402ExactPermit2Proxy.sol # Exact amount transfers (EIP-3009-like) ├── x402UptoPermit2Proxy.sol # Flexible amount transfers (up to permitted) └── interfaces/ └── ISignatureTransfer.sol # Permit2 SignatureTransfer interface -test/ -├── x402ExactPermit2Proxy.t.sol # Exact variant unit tests -├── x402ExactPermit2Proxy.fork.t.sol # Exact variant fork tests -├── x402UptoPermit2Proxy.t.sol # Upto variant unit tests -├── x402UptoPermit2Proxy.fork.t.sol # Upto variant fork tests -├── invariants/ -│ ├── X402ExactInvariants.t.sol # Exact variant invariant tests -│ └── X402UptoInvariants.t.sol # Upto variant invariant tests -└── mocks/ - ├── MockERC20.sol - ├── MockERC20Permit.sol - ├── MockPermit2.sol - ├── MaliciousReentrantExact.sol - └── MaliciousReentrantUpto.sol - script/ -├── Deploy.s.sol # CREATE2 deployment for both contracts -└── ComputeAddress.s.sol # Address computation for both contracts +├── Deploy.s.sol # CREATE2 deployment for both contracts +├── ComputeAddress.s.sol # Address computation (no RPC needed) +└── data/ + └── exact-proxy-initcode.hex # Pre-built initCode for Exact proxy -vanity-miner/ # Rust-based vanity address miner +vanity-miner/ # Rust-based vanity address miner └── src/main.rs ``` @@ -194,11 +253,11 @@ The function signatures follow the same pattern as `settle()` for each variant. ## Security -- **Immutable:** No upgrade mechanism, one-time initialization +- **Immutable:** No upgrade mechanism, no owner, no admin functions - **No custody:** Contracts never hold tokens - **Destination locked:** Witness pattern enforces payTo address - **Reentrancy protected:** Uses OpenZeppelin's ReentrancyGuard -- **Deterministic:** Same address on all chains via CREATE2 (Permit2 set via initialize) +- **Deterministic:** Same address on all chains via CREATE2 ## Coverage @@ -213,10 +272,7 @@ forge coverage --no-match-coverage "(test|script)/.*" --offline ## Gas Snapshots ```bash -# Create snapshot forge snapshot - -# Compare against baseline forge snapshot --diff ``` diff --git a/contracts/evm/foundry.toml b/contracts/evm/foundry.toml index 21e56bb1b0..69e3e75e9b 100644 --- a/contracts/evm/foundry.toml +++ b/contracts/evm/foundry.toml @@ -8,6 +8,8 @@ solc = "0.8.28" optimizer = true optimizer_runs = 200 via_ir = false +cbor_metadata = false +bytecode_hash = "none" ffi = false fs_permissions = [{ access = "read", path = "./" }] gas_reports = ["x402ExactPermit2Proxy", "x402UptoPermit2Proxy"] diff --git a/contracts/evm/script/ComputeAddress.s.sol b/contracts/evm/script/ComputeAddress.s.sol index be92590f2e..602ef3d13b 100644 --- a/contracts/evm/script/ComputeAddress.s.sol +++ b/contracts/evm/script/ComputeAddress.s.sol @@ -10,9 +10,11 @@ import {x402UptoPermit2Proxy} from "../src/x402UptoPermit2Proxy.sol"; * @title ComputeAddress * @notice Compute the deterministic CREATE2 addresses for x402 Permit2 Proxies * - * @dev The Permit2 address is a constructor argument. Since the canonical Permit2 - * address is the same on all EVM chains, the initCode is identical everywhere, - * preserving uniform CREATE2 addresses. + * @dev x402ExactPermit2Proxy uses a pre-built initCode (script/data/exact-proxy-initcode.hex) + * because the original build included non-deterministic CBOR metadata. + * + * x402UptoPermit2Proxy uses compiler-derived creationCode, which is deterministic + * thanks to cbor_metadata = false in foundry.toml. * * @dev Run with default salts: * forge script script/ComputeAddress.s.sol @@ -28,12 +30,15 @@ contract ComputeAddress is Script { address constant CANONICAL_PERMIT2 = 0x000000000022D473030F116dDEE9F6B43aC78BA3; /// @notice Default salt for x402ExactPermit2Proxy - /// @dev Vanity mined for address 0x4020cd856c882d5fb903d99ce35316a085bb0001 - bytes32 constant DEFAULT_EXACT_SALT = 0x0000000000000000000000000000000000000000000000002c00000003c30a30; + /// @dev Vanity mined for address 0x402085c248eea27d92e8b30b2c58ed07f9e20001 + bytes32 constant DEFAULT_EXACT_SALT = 0x0000000000000000000000000000000000000000000000003000000007263b0e; /// @notice Default salt for x402UptoPermit2Proxy - /// @dev Vanity mined for address 0x40204513ec14919adfd30d77c0a991371b420002 - bytes32 constant DEFAULT_UPTO_SALT = 0x00000000000000000000000000000000000000000000000084000000275d7dbb; + /// @dev Vanity mined for address 0x4020a4f3b7b90cca423b9fabcc0ce57c6c240002 + bytes32 constant DEFAULT_UPTO_SALT = 0x000000000000000000000000000000000000000000000000b000000001db633d; + + /// @notice Expected initCodeHash for x402ExactPermit2Proxy (pre-built, includes CBOR metadata) + bytes32 constant EXACT_INIT_CODE_HASH = 0xe774d1d5a07218946ab54efe010b300481478b86861bb17d69c98a57f68a604c; /** * @notice Computes the CREATE2 addresses using the default salts @@ -59,15 +64,17 @@ contract ComputeAddress is Script { console2.log(" Permit2 (ctor arg): ", CANONICAL_PERMIT2); console2.log(""); - // Compute x402ExactPermit2Proxy address + // x402ExactPermit2Proxy — uses pre-built initCode from hex file { - bytes memory initCode = - abi.encodePacked(type(x402ExactPermit2Proxy).creationCode, abi.encode(CANONICAL_PERMIT2)); + bytes memory initCode = vm.parseBytes(vm.readFile("script/data/exact-proxy-initcode.hex")); bytes32 initCodeHash = keccak256(initCode); + + require(initCodeHash == EXACT_INIT_CODE_HASH, "Exact initCode hash mismatch - hex file may be corrupted"); + address expectedAddress = _computeCreate2Addr(exactSalt, initCodeHash, CREATE2_DEPLOYER); console2.log("------------------------------------------------------------"); - console2.log(" x402ExactPermit2Proxy"); + console2.log(" x402ExactPermit2Proxy (pre-built initCode)"); console2.log("------------------------------------------------------------"); console2.log(" Salt: ", vm.toString(exactSalt)); console2.log(" Init Code Hash: ", vm.toString(initCodeHash)); @@ -81,7 +88,7 @@ contract ComputeAddress is Script { console2.log(""); } - // Compute x402UptoPermit2Proxy address + // x402UptoPermit2Proxy — uses compiler-derived creationCode (deterministic) { bytes memory initCode = abi.encodePacked(type(x402UptoPermit2Proxy).creationCode, abi.encode(CANONICAL_PERMIT2)); @@ -89,7 +96,7 @@ contract ComputeAddress is Script { address expectedAddress = _computeCreate2Addr(uptoSalt, initCodeHash, CREATE2_DEPLOYER); console2.log("------------------------------------------------------------"); - console2.log(" x402UptoPermit2Proxy"); + console2.log(" x402UptoPermit2Proxy (deterministic build)"); console2.log("------------------------------------------------------------"); console2.log(" Salt: ", vm.toString(uptoSalt)); console2.log(" Init Code Hash: ", vm.toString(initCodeHash)); diff --git a/contracts/evm/script/Deploy.s.sol b/contracts/evm/script/Deploy.s.sol index ee758bea09..9f3713bc00 100644 --- a/contracts/evm/script/Deploy.s.sol +++ b/contracts/evm/script/Deploy.s.sol @@ -11,14 +11,26 @@ import {ISignatureTransfer} from "../src/interfaces/ISignatureTransfer.sol"; * @notice Deployment script for x402 Permit2 Proxy contracts using CREATE2 * @dev Run with: forge script script/Deploy.s.sol --rpc-url $RPC_URL --broadcast --verify * - * The Permit2 address is passed as a constructor argument. Since Permit2 is - * deployed via a deterministic CREATE2 deployer, its canonical address - * (0x000000000022D473030F116dDEE9F6B43aC78BA3) is the same on all EVM chains. - * Using the same constructor argument on every chain keeps the initCode identical, - * preserving a uniform CREATE2 address for these proxies across all chains. + * ## Deployment Strategy * - * If Permit2 has not yet been deployed on the target chain, deploy it first - * using the canonical Permit2 deployment method before running this script. + * **x402ExactPermit2Proxy** — Uses a pre-built initCode blob stored at + * `script/data/exact-proxy-initcode.hex`. The original build included + * Solidity CBOR metadata (an IPFS hash that varies per build + * environment), so recompiling from source produces a *different* + * initCodeHash and therefore a different CREATE2 address. By shipping + * the exact initCode that was used for the first deployment, anyone can + * deploy to the canonical address on any chain without needing the + * original build environment. + * + * **x402UptoPermit2Proxy** — Built from source with deterministic + * bytecode (`cbor_metadata = false` in foundry.toml). Any machine + * compiling at the same git commit will produce the same initCode and + * therefore the same CREATE2 address. + * + * Both contracts use the canonical Permit2 address + * (0x000000000022D473030F116dDEE9F6B43aC78BA3) as a constructor + * argument. If Permit2 has not yet been deployed on the target chain, + * deploy it first. */ contract DeployX402Proxies is Script { /// @notice Canonical Permit2 address (Uniswap's official deployment) @@ -29,15 +41,17 @@ contract DeployX402Proxies is Script { address constant CREATE2_DEPLOYER = 0x4e59b44847b379578588920cA78FbF26c0B4956C; /// @notice Salt for x402ExactPermit2Proxy deterministic deployment - /// @dev Vanity mined for address 0x4020cd856c882d5fb903d99ce35316a085bb0001 - bytes32 constant EXACT_SALT = 0x0000000000000000000000000000000000000000000000002c00000003c30a30; + /// @dev Vanity mined for address 0x402085c248eea27d92e8b30b2c58ed07f9e20001 + bytes32 constant EXACT_SALT = 0x0000000000000000000000000000000000000000000000003000000007263b0e; /// @notice Salt for x402UptoPermit2Proxy deterministic deployment - /// @dev Vanity mined for address 0x40204513ec14919adfd30d77c0a991371b420002 - bytes32 constant UPTO_SALT = 0x00000000000000000000000000000000000000000000000084000000275d7dbb; + /// @dev Vanity mined for address 0x4020a4f3b7b90cca423b9fabcc0ce57c6c240002 + bytes32 constant UPTO_SALT = 0x000000000000000000000000000000000000000000000000b000000001db633d; + + /// @notice Expected initCodeHash for x402ExactPermit2Proxy (pre-built, includes CBOR metadata) + bytes32 constant EXACT_INIT_CODE_HASH = 0xe774d1d5a07218946ab54efe010b300481478b86861bb17d69c98a57f68a604c; function run() public { - // Allow override of Permit2 address for chains with non-canonical deployments address permit2 = vm.envOr("PERMIT2_ADDRESS", CANONICAL_PERMIT2); console2.log(""); @@ -46,13 +60,11 @@ contract DeployX402Proxies is Script { console2.log("============================================================"); console2.log(""); - // Log configuration console2.log("Network: chainId", block.chainid); console2.log("Permit2:", permit2); console2.log("CREATE2 Deployer:", CREATE2_DEPLOYER); console2.log(""); - // Verify Permit2 exists (skip for local networks) if (block.chainid != 31_337 && block.chainid != 1337) { require(permit2.code.length > 0, "Permit2 not found on this network"); console2.log("Permit2 verified"); @@ -61,7 +73,6 @@ contract DeployX402Proxies is Script { console2.log("CREATE2 deployer verified"); } - // Deploy both contracts (Permit2 address is a constructor arg, no initialization needed) _deployExact(permit2); _deployUpto(permit2); @@ -78,8 +89,16 @@ contract DeployX402Proxies is Script { console2.log(" Deploying x402ExactPermit2Proxy"); console2.log("------------------------------------------------------------"); - // Constructor arg is the canonical Permit2 address (same on all chains) - bytes memory initCode = abi.encodePacked(type(x402ExactPermit2Proxy).creationCode, abi.encode(permit2)); + bytes memory initCode; + + if (block.chainid == 31_337 || block.chainid == 1337) { + initCode = abi.encodePacked(type(x402ExactPermit2Proxy).creationCode, abi.encode(permit2)); + } else { + initCode = vm.parseBytes(vm.readFile("script/data/exact-proxy-initcode.hex")); + bytes32 actualHash = keccak256(initCode); + require(actualHash == EXACT_INIT_CODE_HASH, "Exact initCode hash mismatch - hex file may be corrupted"); + } + bytes32 initCodeHash = keccak256(initCode); address expectedAddress = _computeCreate2Addr(EXACT_SALT, initCodeHash, CREATE2_DEPLOYER); @@ -127,7 +146,6 @@ contract DeployX402Proxies is Script { console2.log(" Deploying x402UptoPermit2Proxy"); console2.log("------------------------------------------------------------"); - // Constructor arg is the canonical Permit2 address (same on all chains) bytes memory initCode = abi.encodePacked(type(x402UptoPermit2Proxy).creationCode, abi.encode(permit2)); bytes32 initCodeHash = keccak256(initCode); address expectedAddress = _computeCreate2Addr(UPTO_SALT, initCodeHash, CREATE2_DEPLOYER); diff --git a/contracts/evm/script/data/exact-proxy-initcode.hex b/contracts/evm/script/data/exact-proxy-initcode.hex new file mode 100644 index 0000000000..cfeedd607c --- /dev/null +++ b/contracts/evm/script/data/exact-proxy-initcode.hex @@ -0,0 +1 @@ +0x60a060405234801561000f575f5ffd5b50604051610c3f380380610c3f83398101604081905261002e9161008c565b8060017f9b779b17422d0df92223018b32b4d1fa46e071723d6817e2486d003becc55f00556001600160a01b03811661007a576040516359106c8960e01b815260040160405180910390fd5b6001600160a01b0316608052506100b9565b5f6020828403121561009c575f5ffd5b81516001600160a01b03811681146100b2575f5ffd5b9392505050565b608051610b616100de5f395f818160c6015281816103fc01526104a90152610b615ff3fe608060405234801561000f575f5ffd5b5060043610610055575f3560e01c806313cd3b5314610059578063156e21521461006e5780632c03ae6a1461008c5780636afdd850146100c1578063fa34037814610100575b5f5ffd5b61006c61006736600461076c565b610113565b005b610076610211565b604051610083919061080e565b60405180910390f35b6100b37fd97b3239a7f32295517bd14cb074edfdd188dfe5eb42f802bb26d4fd1eb12c3781565b604051908152602001610083565b6100e87f000000000000000000000000000000000000000000000000000000000000000081565b6040516001600160a01b039091168152602001610083565b61006c61010e366004610827565b61022d565b61011b610322565b5f7fd97b3239a7f32295517bd14cb074edfdd188dfe5eb42f802bb26d4fd1eb12c3761014a60208601866108b5565b604080516020818101949094526001600160a01b039092169082015290850135606082015260800160408051601f19818403018152919052805160209182012091506101cb908790818101359088906101a5908901896108b5565b8860200135866040518060a0016040528060648152602001610aa8606491398a8a610340565b6040517f97088ec3606cfe8cc112180570d03fcde05f9b8e1bfef8e27784eaf5dd5691b6905f90a15061020a60015f516020610b0c5f395f51905f5255565b5050505050565b6040518060a0016040528060648152602001610aa86064913981565b610235610322565b61025161024560208701876108b5565b85886020890135610476565b5f7fd97b3239a7f32295517bd14cb074edfdd188dfe5eb42f802bb26d4fd1eb12c3761028060208601866108b5565b604080516020818101949094526001600160a01b039092169082015290850135606082015260800160408051601f19818403018152919052805160209182012091506102db908790818101359088906101a5908901896108b5565b6040517fde5b89d10fc800c459329c382fabfcad0be0ed7e5328e01fae04e507b09ef5d8905f90a15061031a60015f516020610b0c5f395f51905f5255565b505050505050565b61032a6106b5565b60025f516020610b0c5f395f51905f5255565b90565b875f036103605760405163162908e360e11b815260040160405180910390fd5b6001600160a01b038716610387576040516349e27cff60e01b815260040160405180910390fd5b6001600160a01b0386166103ae5760405163ac6b05f560e01b815260040160405180910390fd5b844210156103cf5760405163532a9cfd60e11b815260040160405180910390fd5b6040805180820182526001600160a01b038089168252602082018b905291516309be14ff60e11b815290917f0000000000000000000000000000000000000000000000000000000000000000169063137c29fe9061043d908d9085908d908b908b908b908b906004016108f6565b5f604051808303815f87803b158015610454575f5ffd5b505af1158015610466573d5f5f3e3d5ffd5b5050505050505050505050505050565b813581146104975760405163050cda4960e01b815260040160405180910390fd5b6001600160a01b03841663d505accf847f0000000000000000000000000000000000000000000000000000000000000000853560208701356104df60a0890160808a01610995565b604080516001600160e01b031960e089901b1681526001600160a01b0396871660048201529590941660248601526044850192909252606484015260ff16608483015285013560a4820152606085013560c482015260e4015f604051808303815f87803b15801561054e575f5ffd5b505af192505050801561055f575060015b6106af5761056b6109b5565b806308c379a0036105db575061057f610a06565b8061058a575061063d565b836001600160a01b0316856001600160a01b03167f0fea7e5a0e14e9f700b9b0666ffd524bacb9ceb9e6234ea2bfb8e1d0ee2365e7836040516105cd919061080e565b60405180910390a3506106af565b634e487b710361063d576105ed610a8a565b906105f8575061063d565b836001600160a01b0316856001600160a01b03167f278207c426558a8c6ef49e9a3c6f87d257ca5c1852f0c988ad5caf894d197623836040516105cd91815260200190565b3d808015610666576040519150601f19603f3d011682016040523d82523d5f602084013e61066b565b606091505b50836001600160a01b0316856001600160a01b03167f41d715076dfd9c1fd3135fbb7675020aab7fb8e82349a0cd5aaa970dcfb816fe836040516105cd919061080e565b50505050565b5f516020610b0c5f395f51905f52546002036106e457604051633ee5aeb560e01b815260040160405180910390fd5b565b5f608082840312156106f6575f5ffd5b50919050565b80356001600160a01b0381168114610712575f5ffd5b919050565b5f604082840312156106f6575f5ffd5b5f5f83601f840112610737575f5ffd5b50813567ffffffffffffffff81111561074e575f5ffd5b602083019150836020828501011115610765575f5ffd5b9250929050565b5f5f5f5f5f6101008688031215610781575f5ffd5b61078b87876106e6565b9450610799608087016106fc565b93506107a88760a08801610717565b925060e086013567ffffffffffffffff8111156107c3575f5ffd5b6107cf88828901610727565b969995985093965092949392505050565b5f81518084528060208401602086015e5f602082860101526020601f19601f83011685010191505092915050565b602081525f61082060208301846107e0565b9392505050565b5f5f5f5f5f5f8688036101a081121561083e575f5ffd5b60a081121561084b575f5ffd5b5086955061085c8860a089016106e6565b945061086b61012088016106fc565b935061087b886101408901610717565b925061018087013567ffffffffffffffff811115610897575f5ffd5b6108a389828a01610727565b979a9699509497509295939492505050565b5f602082840312156108c5575f5ffd5b610820826106fc565b81835281816020850137505f828201602090810191909152601f909101601f19169091010190565b6001600160a01b03610907896106fc565b168152602088810135818301526040808a0135908301526060808a01359083015287516001600160a01b0316608083015287015160a08201525f61095660c08301886001600160a01b03169052565b8560e08301526101406101008301526109736101408301866107e0565b8281036101208401526109878185876108ce565b9a9950505050505050505050565b5f602082840312156109a5575f5ffd5b813560ff81168114610820575f5ffd5b5f60033d111561033d5760045f5f3e505f5160e01c90565b601f8201601f1916810167ffffffffffffffff811182821017156109ff57634e487b7160e01b5f52604160045260245ffd5b6040525050565b5f60443d1015610a135790565b6040513d600319016004823e80513d602482011167ffffffffffffffff82111715610a3d57505090565b808201805167ffffffffffffffff811115610a59575050505090565b3d8401600319018282016020011115610a73575050505090565b610a82602082850101856109cd565b509392505050565b5f5f60233d1115610aa357602060045f3e50505f516001905b909156fe5769746e657373207769746e65737329546f6b656e5065726d697373696f6e73286164647265737320746f6b656e2c75696e7432353620616d6f756e74295769746e657373286164647265737320746f2c75696e743235362076616c69644166746572299b779b17422d0df92223018b32b4d1fa46e071723d6817e2486d003becc55f00a264697066735822122027467754a9b59749d429ab315c3464fc316abae10e53d8b86e8fa8ae8a4a0e1f64736f6c634300081c0033000000000000000000000000000000000022d473030f116ddee9f6b43ac78ba3 \ No newline at end of file diff --git a/contracts/evm/src/x402BasePermit2Proxy.sol b/contracts/evm/src/x402BasePermit2Proxy.sol index c180bd7a9f..46c6c8863a 100644 --- a/contracts/evm/src/x402BasePermit2Proxy.sol +++ b/contracts/evm/src/x402BasePermit2Proxy.sol @@ -28,15 +28,6 @@ abstract contract x402BasePermit2Proxy is ReentrancyGuard { /// @notice The Permit2 contract address (set once at construction, immutable) ISignatureTransfer public immutable PERMIT2; - /// @notice EIP-712 type string for witness data - /// @dev Must match the exact format expected by Permit2 - /// Types must be in ALPHABETICAL order after the primary type (TokenPermissions < Witness) - string public constant WITNESS_TYPE_STRING = - "Witness witness)TokenPermissions(address token,uint256 amount)Witness(address to,address facilitator,uint256 validAfter)"; - - /// @notice EIP-712 typehash for witness struct - bytes32 public constant WITNESS_TYPEHASH = keccak256("Witness(address to,address facilitator,uint256 validAfter)"); - /// @notice Emitted when settle() completes successfully event Settled(); @@ -79,24 +70,6 @@ abstract contract x402BasePermit2Proxy is ReentrancyGuard { /// @notice Thrown when EIP-2612 permit value doesn't match Permit2 permitted amount error Permit2612AmountMismatch(); - /// @notice Thrown when msg.sender does not match the facilitator in the witness - error UnauthorizedFacilitator(); - - /** - * @notice Witness data structure for payment authorization - * @param to Destination address (immutable once signed) - * @param facilitator Address authorized to settle this payment (must be msg.sender) - * @param validAfter Earliest timestamp when payment can be settled - * @dev The upper time bound is enforced by Permit2's deadline field. - * The facilitator field prevents frontrunning/griefing by binding the - * settlement caller to the payer's signature. - */ - struct Witness { - address to; - address facilitator; - uint256 validAfter; - } - /** * @notice EIP-2612 permit parameters grouped to reduce stack depth * @param value Approval amount for Permit2 @@ -129,43 +102,37 @@ abstract contract x402BasePermit2Proxy is ReentrancyGuard { /** * @notice Internal settlement logic shared by all settlement functions - * @dev Validates all parameters and executes the Permit2 transfer + * @dev Validates common parameters and executes the Permit2 witness transfer. + * Each child contract computes its own witnessHash and witnessTypeString + * based on its Witness struct definition. * @param permit The Permit2 transfer authorization * @param settlementAmount The actual amount to transfer (may be <= permit.permitted.amount) * @param owner The token owner (payer) - * @param witness The witness data containing destination and validity window + * @param to The destination address for the transfer + * @param validAfter Earliest timestamp when payment can be settled + * @param witnessHash The EIP-712 hash of the child's witness struct + * @param witnessTypeString The EIP-712 type string for the child's witness * @param signature The payer's signature */ function _settle( ISignatureTransfer.PermitTransferFrom calldata permit, uint256 settlementAmount, address owner, - Witness calldata witness, + address to, + uint256 validAfter, + bytes32 witnessHash, + string memory witnessTypeString, bytes calldata signature ) internal { - // Validate amount is non-zero to prevent no-op settlements that consume nonces if (settlementAmount == 0) revert InvalidAmount(); - - // Validate addresses if (owner == address(0)) revert InvalidOwner(); - if (witness.to == address(0)) revert InvalidDestination(); - - // Validate caller is the authorized facilitator signed over by the payer - if (msg.sender != witness.facilitator) revert UnauthorizedFacilitator(); + if (to == address(0)) revert InvalidDestination(); + if (block.timestamp < validAfter) revert PaymentTooEarly(); - // Validate time window (upper bound enforced by Permit2's deadline) - if (block.timestamp < witness.validAfter) revert PaymentTooEarly(); - - // Prepare transfer details with destination from witness ISignatureTransfer.SignatureTransferDetails memory transferDetails = - ISignatureTransfer.SignatureTransferDetails({to: witness.to, requestedAmount: settlementAmount}); - - // Reconstruct witness hash to enforce integrity - bytes32 witnessHash = - keccak256(abi.encode(WITNESS_TYPEHASH, witness.to, witness.facilitator, witness.validAfter)); + ISignatureTransfer.SignatureTransferDetails({to: to, requestedAmount: settlementAmount}); - // Execute transfer via Permit2 - PERMIT2.permitWitnessTransferFrom(permit, transferDetails, owner, witnessHash, WITNESS_TYPE_STRING, signature); + PERMIT2.permitWitnessTransferFrom(permit, transferDetails, owner, witnessHash, witnessTypeString, signature); } /** @@ -184,21 +151,19 @@ abstract contract x402BasePermit2Proxy is ReentrancyGuard { EIP2612Permit calldata permit2612, uint256 permittedAmount ) internal { - if (permit2612.value != permittedAmount) revert Permit2612AmountMismatch(); + if (permit2612.value != permittedAmount) { + revert Permit2612AmountMismatch(); + } try IERC20Permit(token).permit( owner, address(PERMIT2), permit2612.value, permit2612.deadline, permit2612.v, permit2612.r, permit2612.s ) { // EIP-2612 permit succeeded } catch Error(string memory reason) { - // Legacy revert(string) or require(condition, string) — e.g. older token implementations emit EIP2612PermitFailedWithReason(token, owner, reason); } catch Panic(uint256 errorCode) { - // Solidity panic — e.g. arithmetic overflow in non-standard implementations emit EIP2612PermitFailedWithPanic(token, owner, errorCode); } catch (bytes memory data) { - // Custom errors (e.g. OZ v5 ERC2612ExpiredSignature, ERC2612InvalidSigner), - // empty reverts, or out-of-gas from non-EIP-2612 tokens emit EIP2612PermitFailedWithData(token, owner, data); } } diff --git a/contracts/evm/src/x402ExactPermit2Proxy.sol b/contracts/evm/src/x402ExactPermit2Proxy.sol index 44083c1992..56e0e5a4a1 100644 --- a/contracts/evm/src/x402ExactPermit2Proxy.sol +++ b/contracts/evm/src/x402ExactPermit2Proxy.sol @@ -12,12 +12,29 @@ import {ISignatureTransfer} from "./interfaces/ISignatureTransfer.sol"; * It uses the "witness" pattern to cryptographically bind the payment destination, * preventing facilitators from redirecting funds. * - * Unlike x402UptoPermit2Proxy, this contract always transfers the EXACT permitted - * amount, similar to EIP-3009's transferWithAuthorization behavior. + * This contract always transfers the EXACT permitted amount, similar to + * EIP-3009's transferWithAuthorization behavior. * * @author x402 Protocol */ contract x402ExactPermit2Proxy is x402BasePermit2Proxy { + /// @notice EIP-712 type string for witness data + string public constant WITNESS_TYPE_STRING = + "Witness witness)TokenPermissions(address token,uint256 amount)Witness(address to,uint256 validAfter)"; + + /// @notice EIP-712 typehash for witness struct + bytes32 public constant WITNESS_TYPEHASH = keccak256("Witness(address to,uint256 validAfter)"); + + /** + * @notice Witness data structure for payment authorization + * @param to Destination address (immutable once signed) + * @param validAfter Earliest timestamp when payment can be settled + */ + struct Witness { + address to; + uint256 validAfter; + } + constructor( address _permit2 ) x402BasePermit2Proxy(_permit2) {} @@ -37,7 +54,17 @@ contract x402ExactPermit2Proxy is x402BasePermit2Proxy { Witness calldata witness, bytes calldata signature ) external nonReentrant { - _settle(permit, permit.permitted.amount, owner, witness, signature); + bytes32 witnessHash = keccak256(abi.encode(WITNESS_TYPEHASH, witness.to, witness.validAfter)); + _settle( + permit, + permit.permitted.amount, + owner, + witness.to, + witness.validAfter, + witnessHash, + WITNESS_TYPE_STRING, + signature + ); emit Settled(); } @@ -63,7 +90,17 @@ contract x402ExactPermit2Proxy is x402BasePermit2Proxy { bytes calldata signature ) external nonReentrant { _executePermit(permit.permitted.token, owner, permit2612, permit.permitted.amount); - _settle(permit, permit.permitted.amount, owner, witness, signature); + bytes32 witnessHash = keccak256(abi.encode(WITNESS_TYPEHASH, witness.to, witness.validAfter)); + _settle( + permit, + permit.permitted.amount, + owner, + witness.to, + witness.validAfter, + witnessHash, + WITNESS_TYPE_STRING, + signature + ); emit SettledWithPermit(); } } diff --git a/contracts/evm/src/x402UptoPermit2Proxy.sol b/contracts/evm/src/x402UptoPermit2Proxy.sol index fc03b23a06..236e9e858d 100644 --- a/contracts/evm/src/x402UptoPermit2Proxy.sol +++ b/contracts/evm/src/x402UptoPermit2Proxy.sol @@ -12,20 +12,42 @@ import {ISignatureTransfer} from "./interfaces/ISignatureTransfer.sol"; * It uses the "witness" pattern to cryptographically bind the payment destination, * preventing facilitators from redirecting funds. * - * Unlike x402ExactPermit2Proxy, this contract allows the facilitator to specify - * how much to transfer (up to the permitted amount), useful for scenarios where - * the actual amount is determined at settlement time. + * This contract allows the facilitator to transfer up to the permitted amount. + * The witness includes a facilitator field so only the authorized caller can + * choose the final settlement amount. * * @author x402 Protocol */ contract x402UptoPermit2Proxy is x402BasePermit2Proxy { - constructor( - address _permit2 - ) x402BasePermit2Proxy(_permit2) {} + /// @notice EIP-712 type string for witness data + string public constant WITNESS_TYPE_STRING = + "Witness witness)TokenPermissions(address token,uint256 amount)Witness(address to,address facilitator,uint256 validAfter)"; + + /// @notice EIP-712 typehash for witness struct + bytes32 public constant WITNESS_TYPEHASH = keccak256("Witness(address to,address facilitator,uint256 validAfter)"); + + /// @notice Thrown when msg.sender does not match the facilitator in the witness + error UnauthorizedFacilitator(); /// @notice Thrown when requested amount exceeds permitted amount error AmountExceedsPermitted(); + /** + * @notice Witness data structure for payment authorization + * @param to Destination address (immutable once signed) + * @param facilitator Address authorized to settle this payment (must be msg.sender) + * @param validAfter Earliest timestamp when payment can be settled + */ + struct Witness { + address to; + address facilitator; + uint256 validAfter; + } + + constructor( + address _permit2 + ) x402BasePermit2Proxy(_permit2) {} + /** * @notice Settles a payment using a Permit2 signature * @dev This is the standard settlement path when user has already approved Permit2 @@ -43,7 +65,10 @@ contract x402UptoPermit2Proxy is x402BasePermit2Proxy { bytes calldata signature ) external nonReentrant { if (amount > permit.permitted.amount) revert AmountExceedsPermitted(); - _settle(permit, amount, owner, witness, signature); + if (msg.sender != witness.facilitator) revert UnauthorizedFacilitator(); + bytes32 witnessHash = + keccak256(abi.encode(WITNESS_TYPEHASH, witness.to, witness.facilitator, witness.validAfter)); + _settle(permit, amount, owner, witness.to, witness.validAfter, witnessHash, WITNESS_TYPE_STRING, signature); emit Settled(); } @@ -70,8 +95,11 @@ contract x402UptoPermit2Proxy is x402BasePermit2Proxy { bytes calldata signature ) external nonReentrant { if (amount > permit.permitted.amount) revert AmountExceedsPermitted(); + if (msg.sender != witness.facilitator) revert UnauthorizedFacilitator(); _executePermit(permit.permitted.token, owner, permit2612, permit.permitted.amount); - _settle(permit, amount, owner, witness, signature); + bytes32 witnessHash = + keccak256(abi.encode(WITNESS_TYPEHASH, witness.to, witness.facilitator, witness.validAfter)); + _settle(permit, amount, owner, witness.to, witness.validAfter, witnessHash, WITNESS_TYPE_STRING, signature); emit SettledWithPermit(); } } diff --git a/contracts/evm/test/invariants/X402ExactInvariants.t.sol b/contracts/evm/test/invariants/X402ExactInvariants.t.sol index 74c6407bd9..9c20f7133f 100644 --- a/contracts/evm/test/invariants/X402ExactInvariants.t.sol +++ b/contracts/evm/test/invariants/X402ExactInvariants.t.sol @@ -45,12 +45,11 @@ contract X402ExactHandler is Test { deadline: t + 3600 }); - x402BasePermit2Proxy.Witness memory witness = - x402BasePermit2Proxy.Witness({to: recipient, facilitator: address(this), validAfter: t > 60 ? t - 60 : 0}); + x402ExactPermit2Proxy.Witness memory witness = + x402ExactPermit2Proxy.Witness({to: recipient, validAfter: t > 60 ? t - 60 : 0}); bytes memory sig = abi.encodePacked(bytes32(uint256(1)), bytes32(uint256(2)), uint8(27)); - // Exact variant has no amount parameter - always transfers the full permitted amount try proxy.settle(permit, payer, witness, sig) { totalSettled += amount; settleCallCount++; diff --git a/contracts/evm/test/invariants/X402UptoInvariants.t.sol b/contracts/evm/test/invariants/X402UptoInvariants.t.sol index 534fa8c96c..038d820126 100644 --- a/contracts/evm/test/invariants/X402UptoInvariants.t.sol +++ b/contracts/evm/test/invariants/X402UptoInvariants.t.sol @@ -45,8 +45,8 @@ contract X402UptoHandler is Test { deadline: t + 3600 }); - x402BasePermit2Proxy.Witness memory witness = - x402BasePermit2Proxy.Witness({to: recipient, facilitator: address(this), validAfter: t > 60 ? t - 60 : 0}); + x402UptoPermit2Proxy.Witness memory witness = + x402UptoPermit2Proxy.Witness({to: recipient, facilitator: address(this), validAfter: t > 60 ? t - 60 : 0}); bytes memory sig = abi.encodePacked(bytes32(uint256(1)), bytes32(uint256(2)), uint8(27)); diff --git a/contracts/evm/test/mocks/MaliciousReentrantExact.sol b/contracts/evm/test/mocks/MaliciousReentrantExact.sol index 1d44f8ad61..8d9d522d32 100644 --- a/contracts/evm/test/mocks/MaliciousReentrantExact.sol +++ b/contracts/evm/test/mocks/MaliciousReentrantExact.sol @@ -3,7 +3,6 @@ pragma solidity ^0.8.20; import {ISignatureTransfer} from "../../src/interfaces/ISignatureTransfer.sol"; import {x402ExactPermit2Proxy} from "../../src/x402ExactPermit2Proxy.sol"; -import {x402BasePermit2Proxy} from "../../src/x402BasePermit2Proxy.sol"; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; contract MaliciousReentrantExact is ISignatureTransfer { @@ -12,7 +11,7 @@ contract MaliciousReentrantExact is ISignatureTransfer { ISignatureTransfer.PermitTransferFrom public storedPermit; address public storedOwner; - x402BasePermit2Proxy.Witness public storedWitness; + x402ExactPermit2Proxy.Witness public storedWitness; bytes public storedSignature; mapping(address => mapping(uint256 => uint256)) public nonceBitmapStorage; @@ -32,7 +31,7 @@ contract MaliciousReentrantExact is ISignatureTransfer { function setAttackParams( ISignatureTransfer.PermitTransferFrom calldata permit, address owner, - x402BasePermit2Proxy.Witness calldata witness, + x402ExactPermit2Proxy.Witness calldata witness, bytes calldata signature ) external { storedPermit = permit; @@ -67,7 +66,6 @@ contract MaliciousReentrantExact is ISignatureTransfer { nonceBitmapStorage[owner][wordPos] |= (1 << bitPos); if (attemptReentry && address(target) != address(0)) { - // Exact variant has no amount parameter target.settle(storedPermit, storedOwner, storedWitness, storedSignature); } diff --git a/contracts/evm/test/mocks/MaliciousReentrantUpto.sol b/contracts/evm/test/mocks/MaliciousReentrantUpto.sol index 38a4d2f019..73e6310687 100644 --- a/contracts/evm/test/mocks/MaliciousReentrantUpto.sol +++ b/contracts/evm/test/mocks/MaliciousReentrantUpto.sol @@ -13,7 +13,7 @@ contract MaliciousReentrantUpto is ISignatureTransfer { ISignatureTransfer.PermitTransferFrom public storedPermit; uint256 public storedAmount; address public storedOwner; - x402BasePermit2Proxy.Witness public storedWitness; + x402UptoPermit2Proxy.Witness public storedWitness; bytes public storedSignature; mapping(address => mapping(uint256 => uint256)) public nonceBitmapStorage; @@ -34,7 +34,7 @@ contract MaliciousReentrantUpto is ISignatureTransfer { ISignatureTransfer.PermitTransferFrom calldata permit, uint256 amount, address owner, - x402BasePermit2Proxy.Witness calldata witness, + x402UptoPermit2Proxy.Witness calldata witness, bytes calldata signature ) external { storedPermit = permit; diff --git a/contracts/evm/test/x402ExactPermit2Proxy.fork.t.sol b/contracts/evm/test/x402ExactPermit2Proxy.fork.t.sol index 66a3e367ff..9a599e12eb 100644 --- a/contracts/evm/test/x402ExactPermit2Proxy.fork.t.sol +++ b/contracts/evm/test/x402ExactPermit2Proxy.fork.t.sol @@ -15,7 +15,7 @@ contract X402ExactPermit2ProxyForkTest is Test { bytes32 constant DOMAIN_TYPEHASH = keccak256("EIP712Domain(string name,uint256 chainId,address verifyingContract)"); bytes32 constant PERMIT_TYPEHASH = keccak256( - "PermitWitnessTransferFrom(TokenPermissions permitted,address spender,uint256 nonce,uint256 deadline,Witness witness)TokenPermissions(address token,uint256 amount)Witness(address to,address facilitator,uint256 validAfter)" + "PermitWitnessTransferFrom(TokenPermissions permitted,address spender,uint256 nonce,uint256 deadline,Witness witness)TokenPermissions(address token,uint256 amount)Witness(address to,uint256 validAfter)" ); bytes32 constant TOKEN_PERMISSIONS_TYPEHASH = keccak256("TokenPermissions(address token,uint256 amount)"); @@ -35,7 +35,6 @@ contract X402ExactPermit2ProxyForkTest is Test { if (block.chainid == 31_337) return; require(PERMIT2.code.length > 0, "Permit2 not deployed"); - // Use a key that produces an EOA (not a deployed contract) on the fork payerKey = uint256(keccak256("x402-test-payer")); payer = vm.addr(payerKey); recipient = makeAddr("recipient"); @@ -68,11 +67,9 @@ contract X402ExactPermit2ProxyForkTest is Test { uint256 amount, uint256 nonce, uint256 deadline, - x402BasePermit2Proxy.Witness memory witness + x402ExactPermit2Proxy.Witness memory witness ) internal view returns (bytes memory) { - // Must match contract's witness hash computation order - bytes32 witnessHash = - keccak256(abi.encode(proxy.WITNESS_TYPEHASH(), witness.to, witness.facilitator, witness.validAfter)); + bytes32 witnessHash = keccak256(abi.encode(proxy.WITNESS_TYPEHASH(), witness.to, witness.validAfter)); bytes32 tokenHash = keccak256(abi.encode(TOKEN_PERMISSIONS_TYPEHASH, tokenAddr, amount)); @@ -90,8 +87,8 @@ contract X402ExactPermit2ProxyForkTest is Test { uint256 nonce = _nonce(1); uint256 deadline = t + 3600; - x402BasePermit2Proxy.Witness memory witness = - x402BasePermit2Proxy.Witness({to: recipient, facilitator: address(this), validAfter: t - 60}); + x402ExactPermit2Proxy.Witness memory witness = + x402ExactPermit2Proxy.Witness({to: recipient, validAfter: t - 60}); bytes memory sig = _sign(address(token), TRANSFER_AMOUNT, nonce, deadline, witness); @@ -115,8 +112,8 @@ contract X402ExactPermit2ProxyForkTest is Test { uint256 t = block.timestamp; uint256 nonce = _nonce(2); - x402BasePermit2Proxy.Witness memory witness = - x402BasePermit2Proxy.Witness({to: recipient, facilitator: address(this), validAfter: t - 60}); + x402ExactPermit2Proxy.Witness memory witness = + x402ExactPermit2Proxy.Witness({to: recipient, validAfter: t - 60}); ISignatureTransfer.PermitTransferFrom memory permit = ISignatureTransfer.PermitTransferFrom({ permitted: ISignatureTransfer.TokenPermissions({token: address(token), amount: TRANSFER_AMOUNT}), @@ -135,12 +132,11 @@ contract X402ExactPermit2ProxyForkTest is Test { uint256 nonce = _nonce(3); uint256 deadline = t + 3600; - x402BasePermit2Proxy.Witness memory witness = - x402BasePermit2Proxy.Witness({to: recipient, facilitator: address(this), validAfter: t - 60}); + x402ExactPermit2Proxy.Witness memory witness = + x402ExactPermit2Proxy.Witness({to: recipient, validAfter: t - 60}); uint256 wrongKey = 0xdeadbeef; - bytes32 witnessHash = - keccak256(abi.encode(proxy.WITNESS_TYPEHASH(), witness.to, witness.facilitator, witness.validAfter)); + bytes32 witnessHash = keccak256(abi.encode(proxy.WITNESS_TYPEHASH(), witness.to, witness.validAfter)); bytes32 tokenHash = keccak256(abi.encode(TOKEN_PERMISSIONS_TYPEHASH, address(token), TRANSFER_AMOUNT)); bytes32 structHash = keccak256(abi.encode(PERMIT_TYPEHASH, tokenHash, address(proxy), nonce, deadline, witnessHash)); @@ -163,8 +159,8 @@ contract X402ExactPermit2ProxyForkTest is Test { uint256 nonce = _nonce(4); uint256 deadline = t + 3600; - x402BasePermit2Proxy.Witness memory witness = - x402BasePermit2Proxy.Witness({to: recipient, facilitator: address(this), validAfter: t - 60}); + x402ExactPermit2Proxy.Witness memory witness = + x402ExactPermit2Proxy.Witness({to: recipient, validAfter: t - 60}); bytes memory sig = _sign(address(token), TRANSFER_AMOUNT, nonce, deadline, witness); @@ -185,8 +181,8 @@ contract X402ExactPermit2ProxyForkTest is Test { uint256 nonce = _nonce(5); uint256 deadline = t - 60; // expired (Permit2's deadline enforces the upper bound) - x402BasePermit2Proxy.Witness memory witness = - x402BasePermit2Proxy.Witness({to: recipient, facilitator: address(this), validAfter: t - 120}); + x402ExactPermit2Proxy.Witness memory witness = + x402ExactPermit2Proxy.Witness({to: recipient, validAfter: t - 120}); bytes memory sig = _sign(address(token), TRANSFER_AMOUNT, nonce, deadline, witness); @@ -207,8 +203,8 @@ contract X402ExactPermit2ProxyForkTest is Test { address attacker = makeAddr("attacker"); - x402BasePermit2Proxy.Witness memory signedWitness = - x402BasePermit2Proxy.Witness({to: recipient, facilitator: address(this), validAfter: t - 60}); + x402ExactPermit2Proxy.Witness memory signedWitness = + x402ExactPermit2Proxy.Witness({to: recipient, validAfter: t - 60}); bytes memory sig = _sign(address(token), TRANSFER_AMOUNT, nonce, deadline, signedWitness); @@ -218,11 +214,8 @@ contract X402ExactPermit2ProxyForkTest is Test { deadline: deadline }); - x402BasePermit2Proxy.Witness memory tamperedWitness = x402BasePermit2Proxy.Witness({ - to: attacker, - facilitator: signedWitness.facilitator, - validAfter: signedWitness.validAfter - }); + x402ExactPermit2Proxy.Witness memory tamperedWitness = + x402ExactPermit2Proxy.Witness({to: attacker, validAfter: signedWitness.validAfter}); vm.expectRevert(); proxy.settle(permit, payer, tamperedWitness, sig); diff --git a/contracts/evm/test/x402ExactPermit2Proxy.t.sol b/contracts/evm/test/x402ExactPermit2Proxy.t.sol index 3e2e58e3db..5a6039663f 100644 --- a/contracts/evm/test/x402ExactPermit2Proxy.t.sol +++ b/contracts/evm/test/x402ExactPermit2Proxy.t.sol @@ -56,12 +56,8 @@ contract X402ExactPermit2ProxyTest is Test { }); } - function _witness( - address to, - address facilitator, - uint256 validAfter - ) internal pure returns (x402BasePermit2Proxy.Witness memory) { - return x402BasePermit2Proxy.Witness({to: to, facilitator: facilitator, validAfter: validAfter}); + function _witness(address to, uint256 validAfter) internal pure returns (x402ExactPermit2Proxy.Witness memory) { + return x402ExactPermit2Proxy.Witness({to: to, validAfter: validAfter}); } function _sig() internal pure returns (bytes memory) { @@ -84,35 +80,25 @@ contract X402ExactPermit2ProxyTest is Test { function test_settle_revertsOnZeroOwner() public { uint256 t = block.timestamp; vm.expectRevert(x402BasePermit2Proxy.InvalidOwner.selector); - proxy.settle( - _permit(TRANSFER_AMOUNT, 0, t + 3600), address(0), _witness(recipient, address(this), t - 60), _sig() - ); + proxy.settle(_permit(TRANSFER_AMOUNT, 0, t + 3600), address(0), _witness(recipient, t - 60), _sig()); } function test_settle_revertsOnZeroDestination() public { uint256 t = block.timestamp; vm.expectRevert(x402BasePermit2Proxy.InvalidDestination.selector); - proxy.settle(_permit(TRANSFER_AMOUNT, 0, t + 3600), payer, _witness(address(0), address(this), t - 60), _sig()); + proxy.settle(_permit(TRANSFER_AMOUNT, 0, t + 3600), payer, _witness(address(0), t - 60), _sig()); } function test_settle_revertsBeforeValidAfter() public { uint256 t = block.timestamp; vm.expectRevert(x402BasePermit2Proxy.PaymentTooEarly.selector); - proxy.settle(_permit(TRANSFER_AMOUNT, 0, t + 3600), payer, _witness(recipient, address(this), t + 60), _sig()); + proxy.settle(_permit(TRANSFER_AMOUNT, 0, t + 3600), payer, _witness(recipient, t + 60), _sig()); } function test_settle_revertsOnZeroAmount() public { uint256 t = block.timestamp; vm.expectRevert(x402BasePermit2Proxy.InvalidAmount.selector); - proxy.settle(_permit(0, 0, t + 3600), payer, _witness(recipient, address(this), t - 60), _sig()); - } - - function test_settle_revertsOnUnauthorizedFacilitator() public { - uint256 t = block.timestamp; - address attacker = makeAddr("attacker"); - vm.prank(attacker); - vm.expectRevert(x402BasePermit2Proxy.UnauthorizedFacilitator.selector); - proxy.settle(_permit(TRANSFER_AMOUNT, 0, t + 3600), payer, _witness(recipient, address(this), t - 60), _sig()); + proxy.settle(_permit(0, 0, t + 3600), payer, _witness(recipient, t - 60), _sig()); } // Note: validBefore was removed - upper time bound is enforced by Permit2's deadline @@ -123,7 +109,7 @@ contract X402ExactPermit2ProxyTest is Test { uint256 t = block.timestamp; uint256 balanceBefore = token.balanceOf(recipient); - proxy.settle(_permit(TRANSFER_AMOUNT, 0, t + 3600), payer, _witness(recipient, address(this), t - 60), _sig()); + proxy.settle(_permit(TRANSFER_AMOUNT, 0, t + 3600), payer, _witness(recipient, t - 60), _sig()); assertEq(token.balanceOf(recipient) - balanceBefore, TRANSFER_AMOUNT); } @@ -134,12 +120,24 @@ contract X402ExactPermit2ProxyTest is Test { vm.expectEmit(false, false, false, false); emit Settled(); - proxy.settle(_permit(TRANSFER_AMOUNT, 0, t + 3600), payer, _witness(recipient, address(this), t - 60), _sig()); + proxy.settle(_permit(TRANSFER_AMOUNT, 0, t + 3600), payer, _witness(recipient, t - 60), _sig()); } function test_settle_atExactValidAfter() public { uint256 t = block.timestamp; - proxy.settle(_permit(TRANSFER_AMOUNT, 0, t + 3600), payer, _witness(recipient, address(this), t), _sig()); + proxy.settle(_permit(TRANSFER_AMOUNT, 0, t + 3600), payer, _witness(recipient, t), _sig()); + assertEq(token.balanceOf(recipient), TRANSFER_AMOUNT); + } + + // --- Security: Permissionless settlement --- + + function test_settle_anyoneCanSettle() public { + uint256 t = block.timestamp; + address anyone = makeAddr("anyone"); + + vm.prank(anyone); + proxy.settle(_permit(TRANSFER_AMOUNT, 0, t + 3600), payer, _witness(recipient, t - 60), _sig()); + assertEq(token.balanceOf(recipient), TRANSFER_AMOUNT); } @@ -161,7 +159,7 @@ contract X402ExactPermit2ProxyTest is Test { nonce: 0, deadline: t + 3600 }); - x402ExactPermit2Proxy.Witness memory witness = _witness(recipient, address(this), t - 60); + x402ExactPermit2Proxy.Witness memory witness = _witness(recipient, t - 60); maliciousPermit2.setAttemptReentry(true); maliciousPermit2.setAttackParams(permit, payer, witness, _sig()); @@ -174,7 +172,7 @@ contract X402ExactPermit2ProxyTest is Test { function test_settle_proxyNeverHoldsTokens() public { uint256 t = block.timestamp; - proxy.settle(_permit(TRANSFER_AMOUNT, 0, t + 3600), payer, _witness(recipient, address(this), t - 60), _sig()); + proxy.settle(_permit(TRANSFER_AMOUNT, 0, t + 3600), payer, _witness(recipient, t - 60), _sig()); assertEq(token.balanceOf(address(proxy)), 0); } @@ -204,7 +202,7 @@ contract X402ExactPermit2ProxyTest is Test { vm.expectEmit(false, false, false, false); emit SettledWithPermit(); - proxy.settleWithPermit(permit2612, permit, payer, _witness(recipient, address(this), t - 60), _sig()); + proxy.settleWithPermit(permit2612, permit, payer, _witness(recipient, t - 60), _sig()); assertEq(permitToken.balanceOf(recipient), TRANSFER_AMOUNT); } @@ -232,7 +230,7 @@ contract X402ExactPermit2ProxyTest is Test { s: bytes32(uint256(2)) }); - proxy.settleWithPermit(permit2612, permit, payer, _witness(recipient, address(this), t - 60), _sig()); + proxy.settleWithPermit(permit2612, permit, payer, _witness(recipient, t - 60), _sig()); assertEq(permitToken.balanceOf(recipient), TRANSFER_AMOUNT); } @@ -260,7 +258,7 @@ contract X402ExactPermit2ProxyTest is Test { vm.expectEmit(true, true, false, true); emit EIP2612PermitFailedWithReason(address(permitToken), payer, "Permit failed"); - proxy.settleWithPermit(permit2612, permit, payer, _witness(recipient, address(this), t - 60), _sig()); + proxy.settleWithPermit(permit2612, permit, payer, _witness(recipient, t - 60), _sig()); } function test_settleWithPermit_emitsPermitFailedWithPanic() public { @@ -286,7 +284,7 @@ contract X402ExactPermit2ProxyTest is Test { vm.expectEmit(true, true, false, true); emit EIP2612PermitFailedWithPanic(address(permitToken), payer, 0x12); - proxy.settleWithPermit(permit2612, permit, payer, _witness(recipient, address(this), t - 60), _sig()); + proxy.settleWithPermit(permit2612, permit, payer, _witness(recipient, t - 60), _sig()); } function test_settleWithPermit_emitsPermitFailedWithData() public { @@ -312,7 +310,7 @@ contract X402ExactPermit2ProxyTest is Test { vm.expectEmit(true, true, false, false); emit EIP2612PermitFailedWithData(address(permitToken), payer, ""); - proxy.settleWithPermit(permit2612, permit, payer, _witness(recipient, address(this), t - 60), _sig()); + proxy.settleWithPermit(permit2612, permit, payer, _witness(recipient, t - 60), _sig()); } function test_settleWithPermit_doesNotEmitPermitFailedOnSuccess() public { @@ -336,7 +334,7 @@ contract X402ExactPermit2ProxyTest is Test { }); vm.recordLogs(); - proxy.settleWithPermit(permit2612, permit, payer, _witness(recipient, address(this), t - 60), _sig()); + proxy.settleWithPermit(permit2612, permit, payer, _witness(recipient, t - 60), _sig()); VmSafe.Log[] memory entries = vm.getRecordedLogs(); bytes32 reasonSig = keccak256("EIP2612PermitFailedWithReason(address,address,string)"); @@ -373,7 +371,7 @@ contract X402ExactPermit2ProxyTest is Test { }); vm.expectRevert(x402BasePermit2Proxy.InvalidAmount.selector); - proxy.settleWithPermit(permit2612, permit, payer, _witness(recipient, address(this), t - 60), _sig()); + proxy.settleWithPermit(permit2612, permit, payer, _witness(recipient, t - 60), _sig()); } function test_settleWithPermit_revertsWhenPermit2612ValueTooSmall() public { @@ -396,7 +394,7 @@ contract X402ExactPermit2ProxyTest is Test { }); vm.expectRevert(x402BasePermit2Proxy.Permit2612AmountMismatch.selector); - proxy.settleWithPermit(permit2612, permit, payer, _witness(recipient, address(this), t - 60), _sig()); + proxy.settleWithPermit(permit2612, permit, payer, _witness(recipient, t - 60), _sig()); } function test_settleWithPermit_revertsWhenPermit2612ValueTooLarge() public { @@ -419,7 +417,7 @@ contract X402ExactPermit2ProxyTest is Test { }); vm.expectRevert(x402BasePermit2Proxy.Permit2612AmountMismatch.selector); - proxy.settleWithPermit(permit2612, permit, payer, _witness(recipient, address(this), t - 60), _sig()); + proxy.settleWithPermit(permit2612, permit, payer, _witness(recipient, t - 60), _sig()); } // --- Fuzz: Time window --- @@ -430,12 +428,7 @@ contract X402ExactPermit2ProxyTest is Test { vm.warp(currentTime); - proxy.settle( - _permit(TRANSFER_AMOUNT, 0, currentTime + 3600), - payer, - _witness(recipient, address(this), validAfter), - _sig() - ); + proxy.settle(_permit(TRANSFER_AMOUNT, 0, currentTime + 3600), payer, _witness(recipient, validAfter), _sig()); assertEq(token.balanceOf(recipient), TRANSFER_AMOUNT); } @@ -447,12 +440,7 @@ contract X402ExactPermit2ProxyTest is Test { vm.warp(currentTime); vm.expectRevert(); - proxy.settle( - _permit(TRANSFER_AMOUNT, 0, currentTime + 3600), - payer, - _witness(recipient, address(this), validAfter), - _sig() - ); + proxy.settle(_permit(TRANSFER_AMOUNT, 0, currentTime + 3600), payer, _witness(recipient, validAfter), _sig()); } // --- Fuzz: Amount (exact always transfers full permitted amount) --- @@ -464,7 +452,7 @@ contract X402ExactPermit2ProxyTest is Test { uint256 t = block.timestamp; - proxy.settle(_permit(permitted, 0, t + 3600), payer, _witness(recipient, address(this), t - 60), _sig()); + proxy.settle(_permit(permitted, 0, t + 3600), payer, _witness(recipient, t - 60), _sig()); assertEq(token.balanceOf(recipient), permitted); } diff --git a/contracts/evm/test/x402UptoPermit2Proxy.fork.t.sol b/contracts/evm/test/x402UptoPermit2Proxy.fork.t.sol index 44364e2fda..a065e19d2a 100644 --- a/contracts/evm/test/x402UptoPermit2Proxy.fork.t.sol +++ b/contracts/evm/test/x402UptoPermit2Proxy.fork.t.sol @@ -68,7 +68,7 @@ contract X402UptoPermit2ProxyForkTest is Test { uint256 amount, uint256 nonce, uint256 deadline, - x402BasePermit2Proxy.Witness memory witness + x402UptoPermit2Proxy.Witness memory witness ) internal view returns (bytes memory) { // Must match contract's witness hash computation order bytes32 witnessHash = @@ -90,8 +90,8 @@ contract X402UptoPermit2ProxyForkTest is Test { uint256 nonce = _nonce(1); uint256 deadline = t + 3600; - x402BasePermit2Proxy.Witness memory witness = - x402BasePermit2Proxy.Witness({to: recipient, facilitator: address(this), validAfter: t - 60}); + x402UptoPermit2Proxy.Witness memory witness = + x402UptoPermit2Proxy.Witness({to: recipient, facilitator: address(this), validAfter: t - 60}); bytes memory sig = _sign(address(token), TRANSFER_AMOUNT, nonce, deadline, witness); @@ -115,8 +115,8 @@ contract X402UptoPermit2ProxyForkTest is Test { uint256 t = block.timestamp; uint256 nonce = _nonce(2); - x402BasePermit2Proxy.Witness memory witness = - x402BasePermit2Proxy.Witness({to: recipient, facilitator: address(this), validAfter: t - 60}); + x402UptoPermit2Proxy.Witness memory witness = + x402UptoPermit2Proxy.Witness({to: recipient, facilitator: address(this), validAfter: t - 60}); ISignatureTransfer.PermitTransferFrom memory permit = ISignatureTransfer.PermitTransferFrom({ permitted: ISignatureTransfer.TokenPermissions({token: address(token), amount: TRANSFER_AMOUNT}), @@ -135,8 +135,8 @@ contract X402UptoPermit2ProxyForkTest is Test { uint256 nonce = _nonce(3); uint256 deadline = t + 3600; - x402BasePermit2Proxy.Witness memory witness = - x402BasePermit2Proxy.Witness({to: recipient, facilitator: address(this), validAfter: t - 60}); + x402UptoPermit2Proxy.Witness memory witness = + x402UptoPermit2Proxy.Witness({to: recipient, facilitator: address(this), validAfter: t - 60}); uint256 wrongKey = 0xdeadbeef; bytes32 witnessHash = @@ -163,8 +163,8 @@ contract X402UptoPermit2ProxyForkTest is Test { uint256 nonce = _nonce(4); uint256 deadline = t + 3600; - x402BasePermit2Proxy.Witness memory witness = - x402BasePermit2Proxy.Witness({to: recipient, facilitator: address(this), validAfter: t - 60}); + x402UptoPermit2Proxy.Witness memory witness = + x402UptoPermit2Proxy.Witness({to: recipient, facilitator: address(this), validAfter: t - 60}); bytes memory sig = _sign(address(token), TRANSFER_AMOUNT, nonce, deadline, witness); @@ -185,8 +185,8 @@ contract X402UptoPermit2ProxyForkTest is Test { uint256 nonce = _nonce(5); uint256 deadline = t - 60; // expired (Permit2's deadline enforces the upper bound) - x402BasePermit2Proxy.Witness memory witness = - x402BasePermit2Proxy.Witness({to: recipient, facilitator: address(this), validAfter: t - 120}); + x402UptoPermit2Proxy.Witness memory witness = + x402UptoPermit2Proxy.Witness({to: recipient, facilitator: address(this), validAfter: t - 120}); bytes memory sig = _sign(address(token), TRANSFER_AMOUNT, nonce, deadline, witness); @@ -207,8 +207,8 @@ contract X402UptoPermit2ProxyForkTest is Test { address attacker = makeAddr("attacker"); - x402BasePermit2Proxy.Witness memory signedWitness = - x402BasePermit2Proxy.Witness({to: recipient, facilitator: address(this), validAfter: t - 60}); + x402UptoPermit2Proxy.Witness memory signedWitness = + x402UptoPermit2Proxy.Witness({to: recipient, facilitator: address(this), validAfter: t - 60}); bytes memory sig = _sign(address(token), TRANSFER_AMOUNT, nonce, deadline, signedWitness); @@ -218,7 +218,7 @@ contract X402UptoPermit2ProxyForkTest is Test { deadline: deadline }); - x402BasePermit2Proxy.Witness memory tamperedWitness = x402BasePermit2Proxy.Witness({ + x402UptoPermit2Proxy.Witness memory tamperedWitness = x402UptoPermit2Proxy.Witness({ to: attacker, facilitator: signedWitness.facilitator, validAfter: signedWitness.validAfter @@ -235,8 +235,8 @@ contract X402UptoPermit2ProxyForkTest is Test { uint256 permitted = TRANSFER_AMOUNT; uint256 requested = permitted / 2; - x402BasePermit2Proxy.Witness memory witness = - x402BasePermit2Proxy.Witness({to: recipient, facilitator: address(this), validAfter: t - 60}); + x402UptoPermit2Proxy.Witness memory witness = + x402UptoPermit2Proxy.Witness({to: recipient, facilitator: address(this), validAfter: t - 60}); bytes memory sig = _sign(address(token), permitted, nonce, deadline, witness); diff --git a/contracts/evm/test/x402UptoPermit2Proxy.t.sol b/contracts/evm/test/x402UptoPermit2Proxy.t.sol index c90a99d3b2..8d41928340 100644 --- a/contracts/evm/test/x402UptoPermit2Proxy.t.sol +++ b/contracts/evm/test/x402UptoPermit2Proxy.t.sol @@ -60,8 +60,8 @@ contract X402UptoPermit2ProxyTest is Test { address to, address facilitator, uint256 validAfter - ) internal pure returns (x402BasePermit2Proxy.Witness memory) { - return x402BasePermit2Proxy.Witness({to: to, facilitator: facilitator, validAfter: validAfter}); + ) internal pure returns (x402UptoPermit2Proxy.Witness memory) { + return x402UptoPermit2Proxy.Witness({to: to, facilitator: facilitator, validAfter: validAfter}); } function _sig() internal pure returns (bytes memory) { @@ -131,7 +131,7 @@ contract X402UptoPermit2ProxyTest is Test { uint256 t = block.timestamp; address attacker = makeAddr("attacker"); vm.prank(attacker); - vm.expectRevert(x402BasePermit2Proxy.UnauthorizedFacilitator.selector); + vm.expectRevert(x402UptoPermit2Proxy.UnauthorizedFacilitator.selector); proxy.settle( _permit(TRANSFER_AMOUNT, 0, t + 3600), TRANSFER_AMOUNT, diff --git a/contracts/evm/vanity-miner/src/main.rs b/contracts/evm/vanity-miner/src/main.rs index f5b37ae319..e9fb5d10bf 100644 --- a/contracts/evm/vanity-miner/src/main.rs +++ b/contracts/evm/vanity-miner/src/main.rs @@ -13,14 +13,19 @@ const PREFIX: [u8; 2] = [0x40, 0x20]; // 0x4020 const EXACT_SUFFIX: [u8; 2] = [0x00, 0x01]; // ...0001 const UPTO_SUFFIX: [u8; 2] = [0x00, 0x02]; // ...0002 -// Init code hashes (computed from contracts - no constructor args for chain portability) -// Run `forge script script/ComputeAddress.s.sol` to verify these match -// x402ExactPermit2Proxy +// Init code hashes: keccak256(creationCode ++ abi.encode(PERMIT2)) +// Run `forge script script/ComputeAddress.s.sol` to verify these match. +// +// IMPORTANT: The Exact hash is from the ORIGINAL build (with CBOR metadata enabled). +// Since that bytecode is already deployed, we preserve it via script/data/exact-proxy-initcode.hex. +// The Upto hash is from the current build (cbor_metadata = false, bytecode_hash = "none"). +// +// x402ExactPermit2Proxy (pre-built initCode, includes CBOR metadata) const EXACT_INIT_CODE_HASH: [u8; 32] = - hex_literal::hex!("61f007aac96be95995d250c70b750b0c239f3c8cbc28b7b0e89761f84bc0c2bb"); -// x402UptoPermit2Proxy + hex_literal::hex!("e774d1d5a07218946ab54efe010b300481478b86861bb17d69c98a57f68a604c"); +// x402UptoPermit2Proxy (deterministic build, no CBOR metadata) const UPTO_INIT_CODE_HASH: [u8; 32] = - hex_literal::hex!("6bc5ae76d294a4e82cf7857326e018e5d9cd6e306ccfd1ff1300c08697eed7b2"); + hex_literal::hex!("74f7a29cbc3c55f87cdef7f7c551643189e8bb62eed9de67753aebc402b83797"); fn compute_create2_address(salt: &[u8; 32], init_code_hash: &[u8; 32]) -> [u8; 20] { let mut hasher = Keccak::v256(); @@ -115,23 +120,37 @@ fn mine_vanity( } fn main() { + let args: Vec = std::env::args().collect(); + let filter = args.get(1).map(|s| s.as_str()); + + let mine_exact = matches!(filter, None | Some("exact")); + let mine_upto = matches!(filter, None | Some("upto")); + println!("\n🔍 x402 Vanity Address Miner (Rust)"); println!(" Prefix: 0x{}", hex::encode(PREFIX)); - println!(" Exact suffix: 0x{}", hex::encode(EXACT_SUFFIX)); - println!(" Upto suffix: 0x{}", hex::encode(UPTO_SUFFIX)); + if mine_exact { + println!(" Exact suffix: 0x{}", hex::encode(EXACT_SUFFIX)); + } + if mine_upto { + println!(" Upto suffix: 0x{}", hex::encode(UPTO_SUFFIX)); + } println!(" CREATE2 Deployer: 0x{}", hex::encode(CREATE2_DEPLOYER)); - // Get number of threads let num_threads = rayon::current_num_threads(); println!(" Using {} threads", num_threads); - // Mine for Exact contract - let exact_result = mine_vanity("x402ExactPermit2Proxy", &EXACT_INIT_CODE_HASH, &PREFIX, &EXACT_SUFFIX); + let exact_result = if mine_exact { + mine_vanity("x402ExactPermit2Proxy", &EXACT_INIT_CODE_HASH, &PREFIX, &EXACT_SUFFIX) + } else { + None + }; - // Mine for Upto contract - let upto_result = mine_vanity("x402UptoPermit2Proxy", &UPTO_INIT_CODE_HASH, &PREFIX, &UPTO_SUFFIX); + let upto_result = if mine_upto { + mine_vanity("x402UptoPermit2Proxy", &UPTO_INIT_CODE_HASH, &PREFIX, &UPTO_SUFFIX) + } else { + None + }; - // Summary println!("\n{}", "=".repeat(60)); println!("SUMMARY"); println!("{}", "=".repeat(60)); @@ -148,12 +167,13 @@ fn main() { println!(" Address: 0x{}", hex::encode(addr)); } - if exact_result.is_some() && upto_result.is_some() { - let (exact_salt, _) = exact_result.unwrap(); - let (upto_salt, _) = upto_result.unwrap(); + if let (Some((exact_salt, _)), Some((upto_salt, _))) = (exact_result, upto_result) { println!("\n// Update Deploy.s.sol with these values:"); println!("bytes32 constant EXACT_SALT = 0x{};", hex::encode(exact_salt)); println!("bytes32 constant UPTO_SALT = 0x{};", hex::encode(upto_salt)); + } else if let Some((salt, _)) = exact_result.or(upto_result) { + println!("\n// Update Deploy.s.sol:"); + println!("bytes32 constant SALT = 0x{};", hex::encode(salt)); } } diff --git a/docs/advanced-concepts/lifecycle-hooks.mdx b/docs/advanced-concepts/lifecycle-hooks.mdx index cb8ccd8eb5..c37bab531e 100644 --- a/docs/advanced-concepts/lifecycle-hooks.mdx +++ b/docs/advanced-concepts/lifecycle-hooks.mdx @@ -118,7 +118,31 @@ Register hooks for HTTP-specific request handling before payment processing. Use *Coming soon.* - *Coming soon.* + ```go + import ( + "context" + x402 "github.com/coinbase/x402/go" + x402http "github.com/coinbase/x402/go/http" + ) + + // Create resource server + resourceServer := x402.Newx402ResourceServer( + x402.WithFacilitatorClient(facilitatorClient), + ) + + // Wrap with HTTP server and register hook + httpServer := x402http.Wrappedx402HTTPResourceServer(routes, resourceServer). + OnProtectedRequest(func(ctx context.Context, reqCtx x402http.HTTPRequestContext, routeConfig x402http.RouteConfig) (*x402http.ProtectedRequestHookResult, error) { + apiKey := reqCtx.Adapter.GetHeader("X-API-Key") + + if apiKey != "" && isValidApiKey(apiKey) { + return &x402http.ProtectedRequestHookResult{GrantAccess: true}, nil + } + + // No valid API key — continue to payment flow + return nil, nil + }) + ``` diff --git a/docs/core-concepts/client-server.md b/docs/core-concepts/client-server.md index b914c52f99..83c3478a13 100644 --- a/docs/core-concepts/client-server.md +++ b/docs/core-concepts/client-server.md @@ -48,6 +48,19 @@ Servers can include: Servers do not need to manage client identities or maintain session state. Verification and settlement are handled per request. +#### Duplicate Settlement on Solana + +If your server settles payments directly on Solana (without delegating to a facilitator), be aware of a race condition: the same signed payment transaction can be submitted multiple times before the first submission is confirmed on-chain. Solana's RPC will return "success" for each submission, since the network deduplicates at the consensus level. A malicious client can exploit this to obtain access to multiple resources while only paying once. + +To mitigate this, servers that settle Solana payments themselves **must** maintain a short-lived, in-memory cache of transaction payloads currently being settled: + +1. After verification succeeds, derive a cache key from the transaction payload (e.g., the base64-encoded transaction string). +2. If the key is already present in the cache, reject the settlement with a `"duplicate_settlement"` error. +3. If the key is not present, insert it into the cache and proceed with settlement. +4. Evict entries older than 120 seconds (approximately twice the Solana blockhash lifetime). + +If you are using a facilitator, the x402 SVM libraries already include built-in duplicate settlement protection via a `SettlementCache`. See the [Exact SVM Scheme Specification](https://github.com/coinbase/x402/blob/main/specs/schemes/exact/scheme_exact_svm.md) for full details. + ### Communication Flow The typical flow between a client and a server in the x402 protocol is as follows: diff --git a/docs/core-concepts/facilitator.md b/docs/core-concepts/facilitator.md index 854d85f54f..4c96cba36f 100644 --- a/docs/core-concepts/facilitator.md +++ b/docs/core-concepts/facilitator.md @@ -51,6 +51,16 @@ Multiple facilitators are live in production, supporting various networks includ 11. `Facilitator server` returns a `Payment Execution Response` to the resource server. 12. `Resource server` returns a response to the `Client` with a `PAYMENT-RESPONSE` header containing the `Settlement Response` as Base64-encoded JSON. On success, this is a `200 OK` with the requested resource. On failure, this is a `402 Payment Required` with error details. +### Duplicate Settlement (Solana) + +On Solana, a race condition can occur when the same payment transaction is submitted to a facilitator's `/settle` endpoint multiple times before the first submission is confirmed on-chain. Because Solana's RPC returns "success" for duplicate submissions (the network deduplicates at the consensus level), the facilitator may return a successful settlement response for each call. A malicious client could exploit this to access multiple resources while only paying once. + +To mitigate this, the x402 SVM mechanism packages include a built-in `SettlementCache` — a short-lived, in-memory cache that detects and rejects duplicate settlement attempts for the same transaction payload. The cache requires no external storage and entries are automatically evicted after 120 seconds (approximately twice the Solana blockhash lifetime). + +This protection is enabled by default when using the standard SVM facilitator registration helpers in TypeScript and Python. In Go, a shared `SettlementCache` instance should be passed to both V1 and V2 SVM facilitator schemes during registration. + +**If you are a merchant settling payments directly (without a facilitator), you must implement equivalent duplicate detection yourself.** See the [Exact SVM Scheme Specification](https://github.com/coinbase/x402/blob/main/specs/schemes/exact/scheme_exact_svm.md) for the full specification. + ### Summary The facilitator acts as an independent verification and settlement layer within the x402 protocol. It helps servers confirm payments and submit transactions onchain without requiring direct blockchain infrastructure. diff --git a/docs/core-concepts/network-and-token-support.mdx b/docs/core-concepts/network-and-token-support.mdx index da14a7d96e..ffab259f2a 100644 --- a/docs/core-concepts/network-and-token-support.mdx +++ b/docs/core-concepts/network-and-token-support.mdx @@ -17,6 +17,8 @@ x402 V2 uses [CAIP-2](https://chainagnostic.org/CAIPs/caip-2) standard network i | `solana-devnet` | `solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1` | - | Solana Devnet | | - | `aptos:1` | 1 | Aptos Mainnet | | - | `aptos:2` | 2 | Aptos Testnet | +| - | `stellar:pubnet` | - | Stellar Mainnet | +| - | `stellar:testnet` | - | Stellar Testnet | | `avalanche` | `eip155:43114` | 43114 | Avalanche C-Chain mainnet | | `avalanche-fuji` | `eip155:43113` | 43113 | Avalanche Fuji testnet | | `polygon` | `eip155:137` | 137 | Polygon mainnet | @@ -29,6 +31,7 @@ x402 V2 uses [CAIP-2](https://chainagnostic.org/CAIPs/caip-2) standard network i ### Format Explanation - **EVM networks**: `eip155:` where chainId is the numeric chain identifier - **Solana**: `solana:` where genesisHash is the first 32 bytes of the genesis block hash +- **Stellar**: `stellar:` where network is `pubnet` (mainnet) or `testnet` - **Aptos**: `aptos:` where chainId is the numeric chain identifier ## Overview @@ -50,34 +53,46 @@ Multiple production-ready facilitators are available supporting various networks ### Token Support -x402 supports tokens on EVM, Solana, and Aptos networks: +x402 supports tokens on EVM, Solana, Stellar, and Aptos networks: -* **EVM**: Any ERC-20 token that implements the EIP-3009 standard +* **EVM**: Any ERC-20 token (via EIP-3009 or Permit2) * **Solana**: Any SPL or token-2022 token +* **Stellar**: Any Soroban token implementing SEP-41 * **Aptos**: Any fungible asset using Aptos's native fungible asset framework -**Important**: Facilitators support networks, not specific tokens — any EIP-3009 compatible token works on EVM networks, any SPL/token-2022 token works on Solana, and any fungible asset works on Aptos, for the facilitators that support those networks. +**Important**: Facilitators support networks, not specific tokens — any ERC-20 token works on EVM networks (via EIP-3009 or Permit2), any SPL/token-2022 token works on Solana, any SEP-41 token works on Stellar, and any fungible asset works on Aptos, for the facilitators that support those networks. -#### EVM: EIP-3009 Requirement +#### EVM: Asset Transfer Methods -Tokens must implement the `transferWithAuthorization` function from the EIP-3009 standard. This enables: +x402 supports two asset transfer methods on EVM, selected automatically based on token capabilities: -* **Gasless transfers**: The facilitator sponsors gas fees +| Method | When Used | How It Works | +|--------|-----------|--------------| +| **EIP-3009** | Tokens with `transferWithAuthorization` (e.g., USDC) | Single off-chain signature, simplest flow | +| **Permit2** | Any ERC-20 token | Uses Uniswap's [Permit2](https://docs.uniswap.org/contracts/v4/deployments) contract + `x402ExactPermit2Proxy` | + +Both methods provide: + +* **Gasless transfers**: The facilitator sponsors gas for the payment itself. For Permit2, the one-time approval step can also be made gasless via optional [gas sponsoring extensions](/extensions/eip2612-gas-sponsoring) * **Signature-based authorization**: Users sign transfer authorizations off-chain * **Secure payments**: Transfers are authorized by cryptographic signatures +EIP-3009 is preferred when available (simpler, no approval step). Permit2 serves as the universal fallback, enabling any ERC-20 token to work with x402. + +**Permit2 approval**: Permit2 requires a one-time on-chain approval of the Permit2 contract. x402 supports two gas sponsoring extensions that make this step gasless — see [EIP-2612 Gas Sponsoring](/extensions/eip2612-gas-sponsoring) and [ERC-20 Approval Gas Sponsoring](/extensions/erc20-approval-gas-sponsoring). + #### Specifying Payment Amounts When configuring payment requirements, you have two options: 1. **Price String** (e.g., `"$0.01"`) - The system infers USDC as the token -2. **TokenAmount** - Specify exact atomic units of any EIP-3009 token +2. **TokenAmount** - Specify exact atomic units of any ERC-20 token -#### Using Custom EIP-3009 Tokens +#### Using Custom ERC-20 Tokens -To use a custom EIP-3009 token, you need three key pieces of information: +To use a custom ERC-20 token, you need three key pieces of information: -1. **Token Address**: The contract address of your EIP-3009 token +1. **Token Address**: The contract address of your ERC-20 token 2. **EIP-712 Name**: The token's name for EIP-712 signatures 3. **EIP-712 Version**: The token's version for EIP-712 signatures @@ -103,23 +118,25 @@ These values are used in the `eip712` nested object when configuring TokenAmount On Solana, x402 supports all SPL tokens and Token 2022 tokens. When using facilitators that support Solana or Solana Devnet, payments can be made in any SPL/token-2022 token, including USDC (SPL). No EIP-712 configuration is required on Solana. +#### Stellar: Soroban Tokens + +On Stellar, x402 supports all Soroban tokens implementing [SEP-41](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0041.md). Payments use the `transfer(from, to, amount)` function. The TypeScript SDK supports sponsored transactions where facilitators pay gas fees on behalf of clients. Stellar uses ledger-based expiration (default ~12 ledgers ≈ 60 seconds) instead of timestamps. + #### Aptos: Fungible Assets On Aptos, x402 supports all fungible assets using Aptos's native fungible asset framework. Payments use the `primary_fungible_store::transfer` function for automatic store creation and management. The TypeScript SDK supports sponsored transactions where facilitators pay gas fees on behalf of clients. -#### USDC - The Default Token +#### Default Tokens -* **Status**: Supported by default across all networks -* **Why**: USDC implements EIP-3009 and is widely available -* **Networks**: Available on `eip155:8453` (Base), `eip155:84532` (Base Sepolia), and all supported networks +Each network defines its own default token. When you use a price string (e.g., `"$0.01"`), the system uses the network's configured default. USDC is the default on most EVM networks because it implements EIP-3009 (simplest flow) and is widely available, but other networks may define different defaults. -#### Why EIP-3009? +#### Why EIP-3009 + Permit2? -The EIP-3009 standard is essential for x402 because it enables: +These two transfer methods together give x402 full ERC-20 coverage on EVM: 1. **Gas abstraction**: Buyers don't need native tokens (ETH, MATIC, etc.) for gas -2. **One-step payments**: No separate approval transactions required -3. **Universal facilitator support**: Any EIP-3009 token works with any facilitator +2. **EIP-3009**: One-step payments with no approval needed — ideal for tokens like USDC +3. **Permit2**: Universal fallback for any ERC-20 token, with optional gas-sponsored approval via [extensions](/extensions/eip2612-gas-sponsoring) ### Quick Reference @@ -129,7 +146,7 @@ The EIP-3009 standard is essential for x402 because it enables: | [Production Facilitators](https://www.x402.org/ecosystem?filter=facilitators) | Various (Base, Solana, Polygon, Avalanche, etc.) | Yes | Varies | | Self-hosted | Any EVM network (CAIP-2 format) | Yes | Technical setup | -**Note**: On EVM networks, facilitators support any EIP-3009 compatible token; on Solana, facilitators support any SPL/Token-2022 token. +**Note**: On EVM networks, facilitators support any ERC-20 token (via EIP-3009 or Permit2); on Solana, facilitators support any SPL/Token-2022 token. ### Adding Support for New Networks @@ -255,7 +272,7 @@ Key takeaways: * Base (`eip155:8453`), Base Sepolia (`eip155:84532`), Solana (`solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp`), and Solana Devnet (`solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1`) have the best out-of-the-box support * Any EVM network can be supported with a custom facilitator using CAIP-2 format -* Any EIP-3009 token (with `transferWithAuthorization`) works on any facilitator +* Any ERC-20 token works on any facilitator (via EIP-3009 or Permit2) * Use price strings for USDC or TokenAmount for custom tokens * Network choice affects gas costs and payment economics * V2 uses CAIP-2 network identifiers for unambiguous cross-chain support diff --git a/docs/docs.json b/docs/docs.json index ad1a0c407b..90d8e39cb0 100644 --- a/docs/docs.json +++ b/docs/docs.json @@ -65,9 +65,13 @@ { "group": "Extensions", "pages": [ + "extensions/overview", "extensions/bazaar", + "extensions/payment-identifier", "extensions/sign-in-with-x", - "extensions/payment-identifier" + "extensions/offer-receipt", + "extensions/eip2612-gas-sponsoring", + "extensions/erc20-approval-gas-sponsoring" ] }, { diff --git a/docs/extensions/bazaar.mdx b/docs/extensions/bazaar.mdx index 5d4e9b5dce..e84dbf09bd 100644 --- a/docs/extensions/bazaar.mdx +++ b/docs/extensions/bazaar.mdx @@ -21,6 +21,43 @@ Facilitators that support the Bazaar extension may provide a `/discovery/resourc **Note:** The spec for marketplace items is open and part of the x402 scheme, meaning any facilitator can implement their own discovery layer. +#### Settlement Response Header + +After processing a payment that includes the `bazaar` extension, facilitators **may** return an `EXTENSION-RESPONSES` HTTP header to communicate extension-specific outcomes to the client. + +**Header name:** `EXTENSION-RESPONSES` + +**Header value:** A base64-encoded JSON object keyed by extension name. The `bazaar` key contains the bazaar extension's response: + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `bazaar.status` | string | Yes | One of `"success"`, `"processing"`, or `"rejected"` | +| `bazaar.rejectedReason` | string | No | Human-readable explanation. Only present when `status` is `"rejected"` | + +**Status values:** + +| Value | Meaning | +|-------|---------| +| `"success"` | The discovery info was validated and successfully cataloged | +| `"processing"` | The discovery info was accepted and is being cataloged asynchronously | +| `"rejected"` | The discovery info was rejected (e.g., failed schema validation). See `rejectedReason` for details | + +**Example (success):** + +``` +EXTENSION-RESPONSES: eyJiYXphYXIiOnsic3RhdHVzIjoic3VjY2VzcyJ9fQ== +``` +*(base64 of `{"bazaar":{"status":"success"}}`)* + +**Example (rejected):** + +``` +EXTENSION-RESPONSES: eyJiYXphYXIiOnsic3RhdHVzIjoicmVqZWN0ZWQiLCJyZWplY3RlZFJlYXNvbiI6ImluZm8gZmFpbGVkIHNjaGVtYSB2YWxpZGF0aW9uIn19 +``` +*(base64 of `{"bazaar":{"status":"rejected","rejectedReason":"info failed schema validation"}}`)* + +Clients that understand the `bazaar` extension should read the `bazaar` key of this header to confirm cataloging succeeded and surface any rejection reason for debugging. + #### Basic Flow 1. **Discovery**: Clients query the `/discovery/resources` endpoint to find available services @@ -132,6 +169,55 @@ Fetch the list of available x402 services using the facilitator client: ); ``` + + ```go + package main + + import ( + "context" + "fmt" + + "github.com/coinbase/x402/go/extensions/bazaar" + x402http "github.com/coinbase/x402/go/http" + ) + + func main() { + ctx := context.Background() + + // Create facilitator client and extend with bazaar + facilitatorClient := x402http.NewHTTPFacilitatorClient(&x402http.FacilitatorConfig{ + URL: "https://x402.org/facilitator", + }) + client := bazaar.WithBazaar(facilitatorClient) + + // List discovery resources + response, err := client.ListDiscoveryResources(ctx, &bazaar.ListDiscoveryResourcesParams{ + Type: "http", + Limit: 20, + Offset: 0, + }) + if err != nil { + panic(err) + } + + // Filter services under $0.10 + usdcAsset := "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913" + maxPrice := 100000 // $0.10 in USDC atomic units (6 decimals) + + for _, item := range response.Items { + for _, paymentReq := range item.Accepts { + // Parse asset and maxAmountRequired from payment requirements + if asset, ok := paymentReq["asset"].(string); ok && asset == usdcAsset { + if maxAmount, ok := paymentReq["maxAmountRequired"].(string); ok { + // Convert and compare price + fmt.Printf("Found service: %s\n", item.Resource) + } + } + } + } + } + ``` + ```python from x402.http import FacilitatorConfig, HTTPFacilitatorClient @@ -249,6 +335,18 @@ Add the bazaar extension to your route configuration to make your API or MCP too - **HTTP Endpoints** - Standard REST APIs (GET, POST, PUT, PATCH, DELETE, HEAD) - **MCP Tools** - Model Context Protocol tools for AI agent integration +#### Dynamic Routes + +The Bazaar extension supports parameterized routes (e.g., `/users/[userId]`, `/weather/[country]/[city]`). When you use route parameters: + +- The extension automatically extracts concrete parameter values into `pathParams` (e.g., `{ "userId": "123" }`) +- A `routeTemplate` field is added using `:param` syntax (e.g., `/users/:userId`) +- Facilitators use `routeTemplate` as the catalog key, consolidating all requests to the same route pattern into a single discovery entry + +This means `/users/123`, `/users/456`, and `/users/789` all map to the same catalog entry: `/users/:userId`. + +**Note:** Wildcard segments (`*`) are automatically converted to named parameters (`:var1`, `:var2`, etc.) for catalog normalization. + #### Adding Metadata To enhance your listing with descriptions and schemas, include them when setting up your x402 middleware. **You should include descriptions for each parameter to make it clear for agents to call your endpoints**: diff --git a/docs/extensions/eip2612-gas-sponsoring.mdx b/docs/extensions/eip2612-gas-sponsoring.mdx new file mode 100644 index 0000000000..7d90ad8a9d --- /dev/null +++ b/docs/extensions/eip2612-gas-sponsoring.mdx @@ -0,0 +1,142 @@ +--- +title: "EIP-2612 Gas Sponsoring" +description: "Gasless Permit2 approval for ERC-20 tokens that implement EIP-2612. The facilitator submits the permit on-chain, so the client never pays gas for the approval step." +--- + +The EIP-2612 gas sponsoring extension enables gasless Permit2 approval for tokens that implement the [EIP-2612](https://eips.ethereum.org/EIPS/eip-2612) `permit()` function (e.g., USDC). The client signs an off-chain permit authorizing the Permit2 contract, and the facilitator submits it atomically during settlement via `x402ExactPermit2Proxy.settleWithPermit`. + +## Overview + +When using the Permit2 asset transfer method, the user must first approve the Permit2 contract to spend their tokens. For tokens that support EIP-2612, this approval can be done entirely off-chain: + +* **For Buyers**: No gas needed — sign a permit off-chain and the facilitator handles the rest +* **For Sellers**: Advertise this extension so clients using EIP-2612 tokens get a seamless experience +* **For Facilitators**: Accept the permit signature and call `settleWithPermit` to atomically approve + settle in one transaction + +## How It Works + +1. **Server** advertises `eip2612GasSponsoring` in the `PaymentRequired` response extensions +2. **Client** checks if Permit2 allowance is insufficient; if so, signs an EIP-2612 permit off-chain +3. **Client** includes the permit data in the `extensions.eip2612GasSponsoring.info` field of the payment payload +4. **Facilitator** calls `x402ExactPermit2Proxy.settleWithPermit()`, which atomically submits the EIP-2612 permit and executes the Permit2 transfer in a single transaction + +## Server Usage + +Advertise support for this extension in your route configuration: + + + + ```typescript + import { declareEip2612GasSponsoringExtension } from "@x402/extensions/eip2612-gas-sponsoring"; + + const routes = { + "GET /api/data": { + accepts: [{ + scheme: "exact", + network: "eip155:84532", + price: "$0.01", + payTo: "0xYourAddress", + }], + extensions: { + ...declareEip2612GasSponsoringExtension(), + }, + }, + }; + ``` + + + ```go + import ( + "github.com/coinbase/x402/go/extensions/eip2612gassponsor" + ) + + extensions := eip2612gassponsor.DeclareEip2612GasSponsoringExtension() + // Include in your route's Extensions field + ``` + + + ```python + from x402.extensions.eip2612_gas_sponsoring import declare_eip2612_gas_sponsoring_extension + + routes = { + "GET /api/data": { + "accepts": { + "scheme": "exact", + "network": "eip155:84532", + "price": "$0.01", + "payTo": "0xYourAddress", + }, + "extensions": { + **declare_eip2612_gas_sponsoring_extension(), + }, + }, + } + ``` + + + +## Client Usage + +The `ExactEvmScheme` handles EIP-2612 gas sponsoring automatically. When the server advertises this extension and the client's Permit2 allowance is insufficient, the scheme signs the EIP-2612 permit and includes it in the payment payload. + + + + ```typescript + import { ExactEvmScheme } from "@x402/evm/exact/client"; + + // readContract capability is required for EIP-2612 (to check allowance and nonce) + const scheme = new ExactEvmScheme(signer); + client.register("eip155:*", scheme); + + // The scheme automatically signs an EIP-2612 permit when: + // 1. The server advertises eip2612GasSponsoring + // 2. The asset transfer method is "permit2" + // 3. The client's Permit2 allowance is insufficient + ``` + + + ```go + import ( + evm "github.com/coinbase/x402/go/mechanisms/evm/exact/client" + ) + + scheme := evm.NewExactEvmScheme(signer) + client.Register("eip155:*", scheme) + + // EIP-2612 permit signing is handled automatically + // when the server advertises the extension + ``` + + + ```python + from x402 import x402Client + from x402.mechanisms.evm import EthAccountSignerWithRPC + from x402.mechanisms.evm.exact import register_exact_evm_client + + account = Account.from_key("0xYourPrivateKey") + signer = EthAccountSignerWithRPC(account, rpc_url="https://sepolia.base.org") + + client = x402Client() + register_exact_evm_client(client, signer) + # Automatically signs EIP-2612 permit when needed + ``` + + + +## When to Use + +Use this extension when: + +- Your payment token implements EIP-2612 (has a `permit()` function) +- You want fully gasless Permit2 onboarding for your users +- You're using the `permit2` asset transfer method + +Common EIP-2612 tokens include USDC, DAI, and many modern ERC-20 tokens. + +## SDK Support + +| SDK | Supported | +|-----|-----------| +| TypeScript | ✅ | +| Go | ✅ | +| Python | ✅ | diff --git a/docs/extensions/erc20-approval-gas-sponsoring.mdx b/docs/extensions/erc20-approval-gas-sponsoring.mdx new file mode 100644 index 0000000000..63ddbfadcd --- /dev/null +++ b/docs/extensions/erc20-approval-gas-sponsoring.mdx @@ -0,0 +1,150 @@ +--- +title: "ERC-20 Approval Gas Sponsoring" +description: "Gasless Permit2 approval for any ERC-20 token, including those without EIP-2612. The facilitator sponsors the gas for the approval transaction." +--- + +The ERC-20 approval gas sponsoring extension enables gasless Permit2 approval for **any ERC-20 token**, including those that do not implement EIP-2612. The client signs (but does not broadcast) a standard `approve(Permit2, MaxUint256)` transaction, and the facilitator broadcasts it atomically before settling the Permit2 payment. + +## Overview + +This is the universal fallback for gasless Permit2 onboarding. While [EIP-2612 gas sponsoring](/extensions/eip2612-gas-sponsoring) is preferred for tokens that support it, this extension works with every ERC-20 token: + +* **For Buyers**: No gas needed — sign an approval transaction off-chain and the facilitator broadcasts it +* **For Sellers**: Advertise this extension to support the widest range of ERC-20 tokens +* **For Facilitators**: Broadcast the pre-signed approval and settle in an atomic batch, optionally funding the client's gas if needed + +## How It Works + +1. **Server** advertises `erc20ApprovalGasSponsoring` in the `PaymentRequired` response extensions +2. **Client** checks if Permit2 allowance is insufficient; if so, signs a raw `approve(Permit2, MaxUint256)` transaction without broadcasting it +3. **Client** includes the signed transaction in `extensions.erc20ApprovalGasSponsoring.info.signedTransaction` +4. **Facilitator** executes an atomic batch: + - Funds the client's wallet with gas (if needed) + - Broadcasts the client's signed approval transaction + - Calls `x402ExactPermit2Proxy.settle()` to complete the payment + +The atomic batch ensures the approval and settlement happen together — there is no window for front-running between the approval and the payment. + +## Server Usage + +Advertise support for this extension in your route configuration: + + + + ```typescript + import { declareErc20ApprovalGasSponsoringExtension } from "@x402/extensions/erc20-approval-gas-sponsoring"; + + const routes = { + "GET /api/data": { + accepts: [{ + scheme: "exact", + network: "eip155:84532", + price: "$0.01", + payTo: "0xYourAddress", + }], + extensions: { + ...declareErc20ApprovalGasSponsoringExtension(), + }, + }, + }; + ``` + + + ```go + import ( + "github.com/coinbase/x402/go/extensions/erc20approvalgassponsor" + ) + + extensions := erc20approvalgassponsor.DeclareExtension() + // Include in your route's Extensions field + ``` + + + ```python + from x402.extensions.erc20_approval_gas_sponsoring import ( + declare_erc20_approval_gas_sponsoring_extension, + ) + + routes = { + "GET /api/data": { + "accepts": { + "scheme": "exact", + "network": "eip155:84532", + "price": "$0.01", + "payTo": "0xYourAddress", + }, + "extensions": { + **declare_erc20_approval_gas_sponsoring_extension(), + }, + }, + } + ``` + + + +## Client Usage + +The `ExactEvmScheme` handles ERC-20 approval gas sponsoring automatically as a fallback when EIP-2612 is not available. + + + + ```typescript + import { ExactEvmScheme } from "@x402/evm/exact/client"; + + // signTransaction and getTransactionCount capabilities are required + const scheme = new ExactEvmScheme(signer); + client.register("eip155:*", scheme); + + // The scheme automatically signs an ERC-20 approval when: + // 1. The server advertises erc20ApprovalGasSponsoring + // 2. The asset transfer method is "permit2" + // 3. The client's Permit2 allowance is insufficient + // 4. EIP-2612 gas sponsoring is not available or not supported by the token + ``` + + + ```go + import ( + evm "github.com/coinbase/x402/go/mechanisms/evm/exact/client" + ) + + scheme := evm.NewExactEvmScheme(signer) + client.Register("eip155:*", scheme) + + // ERC-20 approval gas sponsoring is handled automatically + // as a fallback when EIP-2612 is not available + ``` + + + ```python + from x402 import x402Client + from x402.mechanisms.evm import EthAccountSignerWithRPC + from x402.mechanisms.evm.exact import register_exact_evm_client + + account = Account.from_key("0xYourPrivateKey") + signer = EthAccountSignerWithRPC(account, rpc_url="https://sepolia.base.org") + + client = x402Client() + register_exact_evm_client(client, signer) + # Automatically signs ERC-20 approval transaction when needed + ``` + + + +## When to Use + +Use this extension when: + +- You want to support any ERC-20 token, not just those with EIP-2612 +- You're using the `permit2` asset transfer method +- You want fully gasless onboarding for your users regardless of the token + +This extension is typically advertised alongside [EIP-2612 gas sponsoring](/extensions/eip2612-gas-sponsoring). The client automatically selects the best option: EIP-2612 if the token supports it, ERC-20 approval otherwise. + +## SDK Support + +| SDK | Supported | +|-----|-----------| +| TypeScript | ✅ | +| Go | ✅ | +| Python | ✅ | diff --git a/docs/extensions/offer-receipt.mdx b/docs/extensions/offer-receipt.mdx new file mode 100644 index 0000000000..b7d116e837 --- /dev/null +++ b/docs/extensions/offer-receipt.mdx @@ -0,0 +1,424 @@ +--- +title: "Signed Offers & Receipts" +description: "Sign offers on 402 responses and receipts on 200 responses, producing cryptographic proof-of-interaction artifacts that clients can use for reputation, auditing, or dispute resolution." +--- + +The Offer & Receipt extension adds cryptographic proof-of-interaction to x402 payment flows. When enabled, your server automatically signs an **offer** on every `402` response (committing to payment terms) and a **receipt** on every `200` response (confirming service delivery). No changes to your business logic. + +## Why Enable Offer & Receipt Signing? + +Signed offers and receipts are portable, verifiable artifacts that any third party can check. They enable: + +* **Reputation systems** — Clients can attach receipts to on-chain attestations as proof they actually paid for and received a service. This is the "Verified Purchase" equivalent for the open web. +* **Dispute resolution** — Offers prove the server committed to specific terms; receipts prove delivery. If either party disputes a transaction, the signed artifacts provide evidence. +* **Auditing** — Receipts create a verifiable trail of service delivery without exposing transaction details (the transaction hash is optional). +* **Client confidence** — Services with verifiable proof-of-interaction build stronger trust signals, making new clients more likely to use the service. + +## Prerequisites + +- An existing x402 resource server (or a new Express.js project) +- Node.js 18+ +- A facilitator URL (see [Quickstart for Sellers](/getting-started/quickstart-for-sellers)) + +## Installation + +```bash +npm install @x402/express @x402/extensions @x402/evm @x402/core viem +``` + +## Signing Formats + +The extension supports two signature formats. Choose based on your key management setup: + +| Format | Key Type | Identity | Best For | +|--------|----------|----------|----------| +| **EIP-712** | secp256k1 (Ethereum) | `did:pkh` (address recovered from signature) | Wallet-based signing. Simpler setup, especially with managed wallet providers. | +| **JWS** | Any asymmetric key (EC P-256, Ed25519, secp256k1) | `did:web` (resolved via `/.well-known/did.json`) | Server-side signing with KMS/HSM. Also supports Solana keys (Ed25519), so if your infrastructure is Solana-native, JWS may be the more natural fit. | + +Both formats produce equivalent proof artifacts. Clients and verifiers handle both transparently. + +## Quick Start: EIP-712 with Environment Variables + +This example uses EIP-712 signing with a raw private key from an environment variable. This is the simplest way to get started. + +> **Not for Production:** Storing private keys in environment variables is acceptable for local development and testing. For production deployments, use a key management service (KMS), hardware security module (HSM), or a managed wallet provider. See [Production Key Management](#production-key-management) below. + +> **Signing Key ≠ Payment Address:** The signing key used for offers and receipts should be a dedicated signing key, not the wallet that receives payments (`payTo`). Separating signing from payment receipt limits exposure if the signing key is compromised. + +### Environment Variables + +Create a `.env` file: + +```bash +# Wallet address that receives payments +EVM_ADDRESS=0xYourPaymentWalletAddress + +# Private key for signing offers and receipts (EIP-712) +# This should be a DEDICATED SIGNING KEY, not the payment wallet's key +# For production deployments, do not store private keys in an environment variable +SIGNING_PRIVATE_KEY=0xYourDedicatedSigningPrivateKey + +# x402 facilitator URL +FACILITATOR_URL=https://facilitator.x402.org +``` + +### Server Setup (EIP-712) + +```typescript +import { config } from "dotenv"; +import express from "express"; +import { paymentMiddleware, x402ResourceServer } from "@x402/express"; +import { ExactEvmScheme } from "@x402/evm/exact/server"; +import { HTTPFacilitatorClient } from "@x402/core/server"; +import { + createOfferReceiptExtension, + createEIP712OfferReceiptIssuer, + declareOfferReceiptExtension, +} from "@x402/extensions/offer-receipt"; +import { privateKeyToAccount } from "viem/accounts"; + +config(); + +const evmAddress = process.env.EVM_ADDRESS as `0x${string}`; +const signingPrivateKey = process.env.SIGNING_PRIVATE_KEY as `0x${string}`; // not for production +const facilitatorUrl = process.env.FACILITATOR_URL!; + +// Create EIP-712 signer from the dedicated signing key +const signingAccount = privateKeyToAccount(signingPrivateKey); +const kid = `did:pkh:eip155:1:${signingAccount.address}#key-1`; + +const offerReceiptIssuer = createEIP712OfferReceiptIssuer( + kid, + signingAccount.signTypedData.bind(signingAccount), +); + +// Set up the resource server with the extension +const facilitatorClient = new HTTPFacilitatorClient({ url: facilitatorUrl }); +const resourceServer = new x402ResourceServer(facilitatorClient) + .register("eip155:84532", new ExactEvmScheme()) + .registerExtension(createOfferReceiptExtension(offerReceiptIssuer)); + +const app = express(); + +// Configure payment routes with offer-receipt enabled +app.use( + paymentMiddleware( + { + "GET /api/data": { + accepts: [ + { + scheme: "exact", + price: "$0.001", + network: "eip155:84532", + payTo: evmAddress, // Payment goes here (different from signing key) + }, + ], + description: "Premium data endpoint", + mimeType: "application/json", + extensions: { + ...declareOfferReceiptExtension({ includeTxHash: false }), + }, + }, + }, + resourceServer, + ), +); + +// Your business logic — unchanged +app.get("/api/data", (req, res) => { + res.json({ data: "your premium content" }); +}); + +app.listen(4021, () => { + console.log("Server listening on http://localhost:4021"); + console.log("Offer-receipt extension enabled (EIP-712)"); +}); +``` + +### What Happens Automatically + +Once configured, the extension hooks into the x402 payment flow: + +1. **On `402` responses**: The extension signs an offer for each entry in `accepts[]` and includes them in the response's `extensions` field. Each offer contains the payment terms (`scheme`, `network`, `amount`, `payTo`) and a `validUntil` timestamp. + +2. **On `200` responses** (after successful payment): The extension signs a receipt containing the `resourceUrl`, `payer` address, `network`, and `issuedAt` timestamp. The receipt is included in the `PAYMENT-RESPONSE` header's `extensions` field. + +No changes to your route handlers are needed. The extension is composable middleware. + +## Alternative: JWS Signing with `did:web` + +JWS signing uses a `did:web` identifier, which means your server must host a DID document at `/.well-known/did.json`. Clients and verifiers resolve this document to find your public key so they can verify the signature. + +JWS supports a wider range of key types than EIP-712 (secp256k1 only), including secp256r1 (EC P-256), Ed25519, and secp256k1 (ES256K). If your infrastructure is enterprise-oriented or Solana-native (Ed25519), JWS lets you use your existing key infrastructure. + +### Environment Variables + +```bash +EVM_ADDRESS=0xYourPaymentWalletAddress +FACILITATOR_URL=https://facilitator.x402.org + +# Base64-encoded PKCS#8 private key (EC P-256) +# For production deployments, do not store private keys in an environment variable +SIGNING_PRIVATE_KEY=base64EncodedPrivateKey + +# Your server's domain (URL-encoded for did:web) +# e.g., "api.example.com" or "localhost%3A4021" for local dev +SERVER_DOMAIN=api.example.com +``` + +### Server Setup (JWS) + +```typescript +import * as crypto from "crypto"; +import { + createOfferReceiptExtension, + createJWSOfferReceiptIssuer, + declareOfferReceiptExtension, + type JWSSigner, +} from "@x402/extensions/offer-receipt"; + +const serverDomain = process.env.SERVER_DOMAIN!; +const signingPrivateKey = process.env.SIGNING_PRIVATE_KEY!; // not for production + +const did = `did:web:${serverDomain}`; +const kid = `${did}#key-1`; + +// Create JWS signer from PKCS#8 private key +const privateKeyPem = `-----BEGIN PRIVATE KEY-----\n${signingPrivateKey}\n-----END PRIVATE KEY-----`; +const keyObject = crypto.createPrivateKey(privateKeyPem); +const publicKeyJwk = keyObject.export({ format: "jwk" }); +delete (publicKeyJwk as Record).d; // Remove private component + +const jwsSigner: JWSSigner = { + kid, + format: "jws", + algorithm: "ES256", + async sign(payload: Uint8Array): Promise { + const sign = crypto.createSign("SHA256"); + sign.update(payload); + const signature = sign.sign(privateKeyPem); + return Buffer.from(derToRaw(signature)).toString("base64url"); + }, +}; + +const offerReceiptIssuer = createJWSOfferReceiptIssuer(kid, jwsSigner); + +// Register with x402ResourceServer the same way as the EIP-712 example: +// resourceServer.registerExtension(createOfferReceiptExtension(offerReceiptIssuer)); +``` + +### Hosting the DID Document + +For JWS verification, clients resolve your `did:web` to find the public key. Serve the DID document at `/.well-known/did.json`: + +```typescript +app.get("/.well-known/did.json", (req, res) => { + res.setHeader("Content-Type", "application/did+json"); + res.json({ + "@context": [ + "https://www.w3.org/ns/did/v1", + "https://w3id.org/security/suites/jws-2020/v1", + ], + id: did, + verificationMethod: [ + { + id: kid, + type: "JsonWebKey2020", + controller: did, + publicKeyJwk, + }, + ], + assertionMethod: [kid], + }); +}); +``` + +## Configuration + +The `declareOfferReceiptExtension` function accepts an optional configuration object: + +```typescript +declareOfferReceiptExtension({ + // Include the blockchain transaction hash in receipts. + // Default: false (for privacy — the payer address is still included). + // Set to true if verifiability is more important than privacy. + includeTxHash: false, + + // How long offers remain valid, in seconds. + // Default: 300 (5 minutes). Falls back to the route's maxTimeoutSeconds. + offerValiditySeconds: 300, +}); +``` + +Configuration is per-route — different endpoints can have different settings. + + +## What Gets Signed + +### Offer Payload + +Each offer is signed when the server returns a `402 Payment Required` response: + +| Field | Description | +|-------|-------------| +| `resourceUrl` | The URL the client is requesting | +| `offerType` | The payment scheme (e.g., `exact`) | +| `network` | The blockchain network (e.g., `eip155:84532`) | +| `amount` | The payment amount in the token's smallest unit | +| `payTo` | The server's payment address | +| `validUntil` | Unix timestamp after which the offer expires | + +### Receipt Payload + +Each receipt is signed when the server returns a `200` response after successful payment: + +| Field | Description | +|-------|-------------| +| `resourceUrl` | The URL the client requested | +| `payer` | The client's wallet address (from the payment) | +| `network` | The blockchain network used for payment | +| `issuedAt` | Unix timestamp when the receipt was issued | +| `txHash` | *(optional)* The blockchain transaction hash, included only if `includeTxHash: true` | + +Both payloads are signed using the format configured on the server (EIP-712 or JWS). The signed artifacts are self-contained — a verifier only needs the artifact and the signer's public key to verify. + +## Production Key Management + +> The examples above use environment variables for signing keys. This is fine for development but not for production. Private keys in environment variables can leak through process inspection, logging, crash dumps, and container metadata endpoints. + +For production, use a signing backend that keeps keys in secure hardware or managed infrastructure. The extension's signer interface is pluggable — you only need to implement the `sign()` function (for JWS) or `signTypedData()` function (for EIP-712) using your provider's SDK. The `OfferReceiptIssuer` interface handles the rest. + +When using a managed wallet provider, you won't have access to the raw private key. Instead, you call the provider's signing API. Here's what the EIP-712 setup looks like with a server wallet (conceptual example): + +```typescript +import { + createOfferReceiptExtension, + createEIP712OfferReceiptIssuer, +} from "@x402/extensions/offer-receipt"; + +// The provider's SDK gives you a signTypedData function +// that calls their API — the private key never leaves their infrastructure +const signerAddress = "0xYourServerWalletAddress"; +const kid = `did:pkh:eip155:1:${signerAddress}#key-1`; + +const offerReceiptIssuer = createEIP712OfferReceiptIssuer(kid, async (params) => { + // Call your wallet provider's signing API + return await yourWalletProvider.signTypedData({ + domain: params.domain, + types: params.types, + primaryType: params.primaryType, + message: params.message, + }); +}); + +// Register as usual +resourceServer.registerExtension(createOfferReceiptExtension(offerReceiptIssuer)); +``` + +The key difference from the environment variable example: you never construct a `privateKeyToAccount` — instead, you pass a function that delegates signing to the provider's API. Any managed wallet provider that supports `signTypedData` (for EIP-712) or raw signing (for JWS) works as a drop-in replacement. + +## Binding Your Signing Key to Your Service Identity + +Signing offers and receipts is only half the story. For verifiers to trust that your signatures are legitimate, they need to confirm that your signing key is authorized to act on behalf of your service's identity (`did:web:yourdomain.com`). + +### DID Document (`did.json`) + +If you're using JWS signing, you're already hosting a DID document at `/.well-known/did.json` (see [JWS setup above](#hosting-the-did-document)). This document declares which keys are authorized for your `did:web` identity. Verifiers resolve your DID and check that the signing key is listed in `verificationMethod`. + +If you're using EIP-712 signing, you can host a `did.json` as well — list your EIP-712 signing address as a `verificationMethod` so verifiers can confirm the key is authorized for your domain. + +This is a W3C standard mechanism and is sufficient for many use cases. However, the DID document is mutable — if you remove the key later, verifiers checking at that point won't find it. + +### Additional Binding Mechanisms + +For production services that need stronger guarantees — immutable on-chain attestations, DNS-based verification, temporal anchoring, or key lifecycle management (expiration, rotation, revocation) — ecosystem partners offer additional trust layers on top of `did.json`. See the [Infrastructure & Tooling category](https://www.x402.org/ecosystem?filter=ecosystem-infrastructure) on the ecosystem page for reputation and identity services that integrate with the offer-receipt extension. + +## Client-Side: Extracting Offers and Receipts + +The `@x402/extensions` package provides client utilities for extracting and verifying the signed artifacts your server produces. + +### Extract Offers from a `402` Response + +```typescript +import { + extractOffersFromPaymentRequired, + decodeSignedOffers, + verifyOfferSignatureJWS, + verifyOfferSignatureEIP712, + isJWSSignedOffer, +} from "@x402/extensions/offer-receipt"; + +// After receiving a 402 response: +const paymentRequiredBody = await response.json(); +const signedOffers = extractOffersFromPaymentRequired(paymentRequiredBody); +const decodedOffers = decodeSignedOffers(signedOffers); + +// Verify an offer signature +for (const decoded of decodedOffers) { + if (isJWSSignedOffer(decoded.signedOffer)) { + await verifyOfferSignatureJWS(decoded.signedOffer); + } else { + await verifyOfferSignatureEIP712(decoded.signedOffer); + } +} +``` + +### Extract a Receipt from a `200` Response + +```typescript +import { + extractReceiptFromResponse, + verifyReceiptMatchesOffer, + verifyReceiptSignatureJWS, + verifyReceiptSignatureEIP712, + isJWSSignedReceipt, +} from "@x402/extensions/offer-receipt"; + +// After a successful payment response: +const signedReceipt = extractReceiptFromResponse(paidResponse); + +// Verify the receipt signature +if (isJWSSignedReceipt(signedReceipt)) { + await verifyReceiptSignatureJWS(signedReceipt); +} else { + await verifyReceiptSignatureEIP712(signedReceipt); +} + +// Verify the receipt matches the offer you accepted +const verified = verifyReceiptMatchesOffer( + signedReceipt, + selectedOffer, + [yourWalletAddress], +); +``` + +`verifyReceiptMatchesOffer` checks that: +- `resourceUrl` matches the offer +- `network` matches the offer +- `payer` matches one of your wallet addresses +- `issuedAt` is recent (within 1 hour by default) + +### What Can Clients Do with These Artifacts? + +Signed offers and receipts are portable, verifiable artifacts. Clients can: + +- **Attach them to reputation attestations** as proof-of-interaction (e.g., "Verified Purchase" reviews) +- **Store them for auditing** — receipts create a verifiable trail of service delivery +- **Use them in dispute resolution** — offers prove the server committed to terms; receipts prove delivery +- **Share them with aggregators** — trust scoring engines can verify the signatures independently + +Ecosystem partners (see the [Infrastructure & Tooling category](https://www.x402.org/ecosystem?filter=ecosystem-infrastructure) on the ecosystem page) build on these artifacts to provide reputation systems, trust scoring, and other value-added services. + +## Working Examples + +Complete working examples are available in the x402 repository: + +- [Server Example (Express.js)](https://github.com/coinbase/x402/tree/main/examples/typescript/servers/offer-receipt) — Resource server with offer-receipt enabled, showing both EIP-712 and JWS configurations +- [Client Example](https://github.com/coinbase/x402/tree/main/examples/typescript/clients/offer-receipt) — Complete client flow: offer extraction, payment, receipt capture, and verification + +## Further Reading + +- [Extensions Overview](./overview) — How the x402 extension system works +- [Offer & Receipt Extension Specification](https://github.com/coinbase/x402/blob/main/specs/extensions/extension-offer-and-receipt.md) — Full protocol spec with payload schemas, EIP-712 types, verification rules, and wire format examples +- [@x402/extensions package](https://github.com/coinbase/x402/tree/main/typescript/packages/extensions/src/offer-receipt) — TypeScript implementation source +- [SDK Features](/sdk-features) — Extension support across TypeScript, Go, and Python diff --git a/docs/extensions/overview.mdx b/docs/extensions/overview.mdx new file mode 100644 index 0000000000..a6f7ea9454 --- /dev/null +++ b/docs/extensions/overview.mdx @@ -0,0 +1,160 @@ +--- +title: "Extensions Overview" +description: "x402 extensions are composable, optional capabilities that plug into the payment lifecycle. They enrich 402 responses, settlement responses, or both — without changing your business logic." +--- + +Extensions are the composable layer on top of x402's core payment protocol. They let resource servers, facilitators, and clients add optional capabilities — discovery, authentication, receipts, gas sponsoring — without modifying the core payment flow. + +## How Extensions Work + +x402 has two extension points that serve different roles in the payment flow: + +### Resource Server Extensions + +These run on the resource server (the service accepting payments) and hook into the HTTP payment lifecycle. A `ResourceServerExtension` can intervene at three points: + +1. **Declaration** (`enrichDeclaration`) — Called at route registration time. The extension can modify or narrow the route's extension declaration based on transport context (e.g., Bazaar narrows the HTTP method). +2. **402 Response** (`enrichPaymentRequiredResponse`) — Called when the server returns `402 Payment Required`. The extension can add data to the response (e.g., signed offers, discovery metadata). +3. **Settlement Response** (`enrichSettlementResponse`) — Called after successful payment. The extension can add data to the `PAYMENT-RESPONSE` header (e.g., signed receipts, payment identifiers). + +All three hooks are optional. Most extensions use one or two — not all three. + +### Facilitator Extensions + +These run on the facilitator (the service that verifies and settles payments on behalf of the resource server). A `FacilitatorExtension` provides a `key` and is stored for use by mechanism implementations during verification and settlement. Gas sponsoring extensions are the primary example — they inject batch signing capabilities into the settlement flow so the facilitator can sponsor gas on behalf of the payer. + +## Registering an Extension (Server) + +Extensions implement the `ResourceServerExtension` interface and are registered via `registerExtension`: + +```typescript +import { x402ResourceServer } from "@x402/express"; + +const resourceServer = new x402ResourceServer(facilitatorClient) + .register("eip155:84532", new ExactEvmScheme()) + .registerExtension(myExtension) // Add one extension + .registerExtension(anotherExtension); // Stack another +``` + +Each extension has a unique `key` that identifies it in route declarations and response payloads. + +## The ResourceServerExtension Interface + +```typescript +interface ResourceServerExtension { + /** Unique identifier for this extension */ + key: string; + + /** Enrich the extension declaration at route registration time */ + enrichDeclaration?: (declaration: unknown, transportContext: unknown) => unknown; + + /** Add data to the 402 PaymentRequired response */ + enrichPaymentRequiredResponse?: ( + declaration: unknown, + context: PaymentRequiredContext, + ) => Promise; + + /** Add data to the settlement response after successful payment */ + enrichSettlementResponse?: ( + declaration: unknown, + context: SettleResultContext, + ) => Promise; +} +``` + +## Declaring Extensions on Routes + +Extensions are declared per-route in the payment middleware configuration. Each extension's declaration goes under `extensions` keyed by the extension's `key`: + +```typescript +app.use( + paymentMiddleware( + { + "GET /api/data": { + accepts: [{ scheme: "exact", price: "$0.01", network: "eip155:84532", payTo: address }], + description: "Premium data", + mimeType: "application/json", + extensions: { + // Each key matches a registered extension + "offer-receipt": { includeTxHash: false }, + "bazaar": { /* bazaar config */ }, + }, + }, + }, + resourceServer, + ), +); +``` + +If an extension is declared on a route but not registered on the server, it is silently ignored. + +## Which Hooks Do Extensions Use? + +Not all extensions use the same hooks. Here's how the built-in extensions map to the extension points: + +| Extension | `enrichDeclaration` | `enrichPaymentRequiredResponse` | `enrichSettlementResponse` | Facilitator | +|-----------|:---:|:---:|:---:|:---:| +| Bazaar | ✅ (narrows HTTP method) | — | — | ✅ (discovery cataloging) | +| EIP-2612 Gas Sponsoring | — | — | — | ✅ (batch signing) | +| ERC-20 Approval Gas Sponsoring | — | — | — | ✅ (batch signing) | +| Payment Identifier | — | ✅ | ✅ | — | +| Sign-In-With-X | — | — | — | — | +| Signed Offers & Receipts | — | ✅ (signs offers) | ✅ (signs receipts) | — | + +Bazaar is unique in that it spans both sides: the resource server extension enriches declarations, while the facilitator component handles discovery cataloging and validation. Sign-In-With-X manages its own session lifecycle outside the standard hooks. + +## Available Extensions + + +| Extension | Type | Description | SDK Support | +|-----------|------|-------------|-------------| +| [Bazaar](./bazaar) | Server + Facilitator | Discovery layer for x402 endpoints and MCP tools. Makes your services findable by AI agents and developers. | TypeScript, Go, Python | +| [EIP-2612 Gas Sponsoring](./eip2612-gas-sponsoring) | Facilitator | Sponsors gas for EIP-2612 permit-based token transfers. | TypeScript, Go, Python | +| [ERC-20 Approval Gas Sponsoring](./erc20-approval-gas-sponsoring) | Facilitator | Sponsors gas for ERC-20 approval-based token transfers. | TypeScript, Go, Python | +| [Payment Identifier](./payment-identifier) | Server + Client | Attaches a unique identifier to each payment for tracking, reconciliation, and idempotency. | TypeScript, Go, Python | +| [Sign-In-With-X](./sign-in-with-x) | Server + Client | CAIP-122 wallet authentication. Lets clients prove wallet ownership to access previously purchased content without repaying. | TypeScript | +| [Signed Offers & Receipts](./offer-receipt) | Server + Client | Signs offers on 402 responses and receipts on 200 responses, producing cryptographic proof-of-interaction artifacts. | TypeScript | + +## Building a Custom Extension + +To create your own extension: + +1. **Define the extension object** implementing `ResourceServerExtension` +2. **Choose a unique key** — this identifies your extension in route declarations and response payloads +3. **Implement the hooks you need** — `enrichDeclaration`, `enrichPaymentRequiredResponse`, `enrichSettlementResponse` +4. **Create a declare function** — a helper that returns the route-level configuration for your extension +5. **Register it** on the `x402ResourceServer` via `registerExtension` +6. **Submit a pull request** to [coinbase/x402](https://github.com/coinbase/x402) — extensions must be reviewed and approved by the x402 maintainers before they are included in the SDK + +Here's a minimal example: + +```typescript +import type { ResourceServerExtension } from "@x402/core"; + +const myExtension: ResourceServerExtension = { + key: "my-extension", + + enrichPaymentRequiredResponse: async (declaration, context) => { + // Add custom data to the 402 response + return { customField: "value", timestamp: Date.now() }; + }, + + enrichSettlementResponse: async (declaration, context) => { + // Add custom data after successful payment + return { settled: true, processedAt: Date.now() }; + }, +}; + +// Declare helper for route config +function declareMyExtension(config: { customOption: boolean }) { + return { "my-extension": config }; +} +``` + +The data returned from each hook is included in the response under `extensions["my-extension"]`. + +## Further Reading + +- [x402 SDK Features](/sdk-features) — Extension support across TypeScript, Go, and Python +- [Extension Specs](https://github.com/coinbase/x402/tree/main/specs/extensions) — Protocol-level extension specifications +- [@x402/extensions package](https://github.com/coinbase/x402/tree/main/typescript/packages/extensions) — TypeScript implementation source diff --git a/docs/extensions/payment-identifier.mdx b/docs/extensions/payment-identifier.mdx index 3d72890086..e5ea85f00e 100644 --- a/docs/extensions/payment-identifier.mdx +++ b/docs/extensions/payment-identifier.mdx @@ -33,6 +33,10 @@ import { generatePaymentId } from "@x402/extensions/payment-identifier"; const paymentId = generatePaymentId(); // Example: "pay_7d5d747be160e280504c099d984bcfe0" + +// Custom prefix +const orderId = generatePaymentId("order_"); +// Example: "order_7d5d747be160e280504c099d984bcfe0" ``` ### Step 2: Add Payment ID to Extensions @@ -41,7 +45,10 @@ Hook into the payment flow to add the payment ID before payload creation: ```typescript import { x402Client, wrapFetchWithPayment } from "@x402/fetch"; -import { appendPaymentIdentifierToExtensions } from "@x402/extensions/payment-identifier"; +import { + appendPaymentIdentifierToExtensions, + generatePaymentId, +} from "@x402/extensions/payment-identifier"; const client = new x402Client(); // ... register schemes ... @@ -51,10 +58,10 @@ const paymentId = generatePaymentId(); // Hook into payment flow to add the payment ID client.onBeforePaymentCreation(async ({ paymentRequired }) => { - if (!paymentRequired.extensions) { - paymentRequired.extensions = {}; + if (paymentRequired.extensions) { + // Only appends if server declared the extension + appendPaymentIdentifierToExtensions(paymentRequired.extensions, paymentId); } - appendPaymentIdentifierToExtensions(paymentRequired.extensions, paymentId); }); const fetchWithPayment = wrapFetchWithPayment(fetch, client); @@ -78,6 +85,10 @@ from x402.extensions.payment_identifier import generate_payment_id payment_id = generate_payment_id() # Example: "pay_7d5d747be160e280504c099d984bcfe0" + +# Custom prefix +order_id = generate_payment_id("order_") +# Example: "order_7d5d747be160e280504c099d984bcfe0" ``` ### Step 2: Add Payment ID to Extensions @@ -86,8 +97,12 @@ Hook into the payment flow to add the payment ID before payload creation: ```python from x402 import x402Client -from x402.extensions.payment_identifier import append_payment_identifier_to_extensions +from x402.extensions.payment_identifier import ( + append_payment_identifier_to_extensions, + generate_payment_id, +) from x402.http.clients import x402HttpxClient +from x402.schemas import PaymentCreationContext client = x402Client() # ... register schemes ... @@ -96,9 +111,10 @@ client = x402Client() payment_id = generate_payment_id() # Hook into payment flow to add the payment ID -async def before_payment_creation(context): +async def before_payment_creation(context: PaymentCreationContext) -> None: extensions = context.payment_required.extensions if extensions is not None: + # Only appends if server declared the extension append_payment_identifier_to_extensions(extensions, payment_id) client.on_before_payment_creation(before_payment_creation) @@ -106,19 +122,81 @@ client.on_before_payment_creation(before_payment_creation) async with x402HttpxClient(client) as http: # First request - payment is processed response1 = await http.get(url) - + # Retry with same payment ID - cached response returned (no payment) response2 = await http.get(url) ``` + + + +### Step 1: Generate a Payment ID + +Use the `GeneratePaymentID()` utility to create a unique identifier: + +```go +import "github.com/coinbase/x402/go/extensions/paymentidentifier" + +// Generate with default prefix "pay_" +paymentID := paymentidentifier.GeneratePaymentID("") +// Example: "pay_7d5d747be160e280504c099d984bcfe0" + +// Generate with custom prefix +paymentID = paymentidentifier.GeneratePaymentID("order_") +// Example: "order_7d5d747be160e280504c099d984bcfe0" +``` + +### Step 2: Add Payment ID to Extensions + +Hook into the payment flow to add the payment ID before payload creation: + +```go +import ( + x402 "github.com/coinbase/x402/go" + "github.com/coinbase/x402/go/extensions/paymentidentifier" +) + +client := x402.Newx402Client() +// ... register schemes ... + +// Generate a unique payment ID for this logical request +paymentID := paymentidentifier.GeneratePaymentID("") + +// Hook into payment flow to add the payment ID +client.OnBeforePaymentCreation(func(ctx x402.PaymentCreationContext) (*x402.BeforePaymentCreationHookResult, error) { + if ctx.Extensions == nil { + return nil, nil + } + + // Only add if server declared the extension + if ctx.Extensions[paymentidentifier.PAYMENT_IDENTIFIER] == nil { + return nil, nil + } + + err := paymentidentifier.AppendPaymentIdentifierToExtensions(ctx.Extensions, paymentID) + if err != nil { + return nil, err + } + + return nil, nil +}) + +// First request - payment is processed +response1, err := client.MakeRequest(url) + +// Retry with same payment ID - cached response returned (no payment) +response2, err := client.MakeRequest(url) +``` + + ### Best Practices 1. **Generate payment IDs at the logical request level**, not per retry 2. **Persist payment IDs** for long-running operations so they survive restarts -3. **Use descriptive prefixes** (e.g., `order_`, `sub_`) to identify payment types +3. **Use descriptive prefixes** (e.g., `generatePaymentId("order_")`) to identify payment types 4. **Don't reuse payment IDs** across different logical requests ## Quickstart for Sellers (Servers) @@ -131,17 +209,26 @@ async with x402HttpxClient(client) as http: Declare the payment-identifier extension in your route configuration: ```typescript -import { paymentMiddlewareFromHTTPServer, x402ResourceServer, x402HTTPResourceServer } from "@x402/express"; -import { declarePaymentIdentifierExtension, PAYMENT_IDENTIFIER } from "@x402/extensions/payment-identifier"; +import { + paymentMiddlewareFromHTTPServer, + x402ResourceServer, + x402HTTPResourceServer, +} from "@x402/express"; +import { + declarePaymentIdentifierExtension, + PAYMENT_IDENTIFIER, +} from "@x402/extensions/payment-identifier"; const routes = { "GET /weather": { - accepts: { - scheme: "exact", - price: "$0.001", - network: "eip155:84532", - payTo: address - }, + accepts: [ + { + scheme: "exact", + price: "$0.001", + network: "eip155:84532", + payTo: address, + }, + ], extensions: { [PAYMENT_IDENTIFIER]: declarePaymentIdentifierExtension(false), // optional }, @@ -159,9 +246,9 @@ declarePaymentIdentifierExtension(false) declarePaymentIdentifierExtension(true) ``` -### Step 2: Implement Idempotency Cache +### Step 2: Cache Responses After Settlement -Use the `extractPaymentIdentifier()` utility to check for cached responses: +Store responses after successful payment settlement: ```typescript import { extractPaymentIdentifier } from "@x402/extensions/payment-identifier"; @@ -170,35 +257,42 @@ import { extractPaymentIdentifier } from "@x402/extensions/payment-identifier"; const idempotencyCache = new Map(); const CACHE_TTL_MS = 60 * 60 * 1000; // 1 hour -const httpServer = new x402HTTPResourceServer(resourceServer, routes) - .onProtectedRequest(async (context) => { - // Check if payment ID is in cache - const paymentPayload = JSON.parse(Buffer.from(context.paymentHeader, "base64").toString()); +const resourceServer = new x402ResourceServer(facilitatorClient) + .register("eip155:84532", new ExactEvmScheme()) + .onAfterSettle(async ({ paymentPayload }) => { const paymentId = extractPaymentIdentifier(paymentPayload); - if (paymentId) { - const cached = idempotencyCache.get(paymentId); - if (cached && Date.now() - cached.timestamp < CACHE_TTL_MS) { - return { grantAccess: true }; // Skip payment, grant access - } + idempotencyCache.set(paymentId, { + timestamp: Date.now(), + response: { /* your response data */ }, + }); } }); ``` -### Step 3: Cache Responses After Settlement +### Step 3: Check Cache Before Payment -Store responses after successful payment settlement: +Use the `onProtectedRequest` hook to return cached responses and skip payment processing: ```typescript -const resourceServer = new x402ResourceServer(facilitatorClient) - .register("eip155:84532", new ExactEvmScheme()) - .onAfterSettle(async ({ paymentPayload }) => { - const paymentId = extractPaymentIdentifier(paymentPayload); - if (paymentId) { - idempotencyCache.set(paymentId, { - timestamp: Date.now(), - response: { /* your response data */ } - }); +const httpServer = new x402HTTPResourceServer(resourceServer, routes) + .onProtectedRequest(async (context) => { + if (!context.paymentHeader) return; + + try { + const paymentPayload = JSON.parse( + Buffer.from(context.paymentHeader, "base64").toString("utf-8"), + ); + const paymentId = extractPaymentIdentifier(paymentPayload); + + if (paymentId) { + const cached = idempotencyCache.get(paymentId); + if (cached && Date.now() - cached.timestamp < CACHE_TTL_MS) { + return { grantAccess: true }; // Skip payment, serve from cache + } + } + } catch { + // Invalid payment header, continue to normal payment flow } }); ``` @@ -211,24 +305,30 @@ const resourceServer = new x402ResourceServer(facilitatorClient) Declare the payment-identifier extension in your route configuration: ```python -from x402.http.servers.fastapi import x402FastAPIResourceServer +from x402.server import x402ResourceServer +from x402.http import FacilitatorConfig, HTTPFacilitatorClient, PaymentOption +from x402.http.middleware.fastapi import PaymentMiddlewareASGI +from x402.http.types import RouteConfig +from x402.mechanisms.evm.exact import ExactEvmServerScheme from x402.extensions.payment_identifier import ( declare_payment_identifier_extension, PAYMENT_IDENTIFIER, ) routes = { - "GET /weather": { - "accepts": { - "scheme": "exact", - "price": "$0.001", - "network": "eip155:84532", - "payTo": address, + "GET /weather": RouteConfig( + accepts=[ + PaymentOption( + scheme="exact", + price="$0.001", + network="eip155:84532", + pay_to=address, + ), + ], + extensions={ + PAYMENT_IDENTIFIER: declare_payment_identifier_extension(required=False), # optional }, - "extensions": { - PAYMENT_IDENTIFIER: declare_payment_identifier_extension(False), # optional - }, - }, + ), } ``` @@ -236,62 +336,148 @@ routes = { ```python # Payment ID is optional (clients can omit it) -declare_payment_identifier_extension(False) +declare_payment_identifier_extension(required=False) # Payment ID is required (clients must provide it or receive 400 Bad Request) -declare_payment_identifier_extension(True) +declare_payment_identifier_extension(required=True) ``` -### Step 2: Implement Idempotency Cache +### Step 2: Cache Responses After Settlement -Use the `extract_payment_identifier()` utility to check for cached responses: +Store responses after successful payment settlement: ```python -from x402.extensions.payment_identifier import extract_payment_identifier import time +from x402.schemas import SettleContext +from x402.extensions.payment_identifier import extract_payment_identifier # In-memory cache (use Redis in production) -idempotency_cache = {} -CACHE_TTL_MS = 60 * 60 * 1000 # 1 hour - -async def on_protected_request(context): - # Check if payment ID is in cache - payment_payload = context.payment_payload - payment_id = extract_payment_identifier(payment_payload) - +idempotency_cache: dict = {} +CACHE_TTL_SECONDS = 60 * 60 # 1 hour + +async def after_settle(ctx: SettleContext) -> None: + payment_id = extract_payment_identifier(ctx.payment_payload) if payment_id: - cached = idempotency_cache.get(payment_id) - if cached and (time.time() * 1000 - cached["timestamp"]) < CACHE_TTL_MS: - return {"grant_access": True} # Skip payment, grant access + idempotency_cache[payment_id] = { + "timestamp": time.time(), + "response": {}, # your response data + } -http_server = x402FastAPIResourceServer(resource_server, routes) -http_server.on_protected_request(on_protected_request) +server = x402ResourceServer(facilitator) +server.register("eip155:84532", ExactEvmServerScheme()) +server.on_after_settle(after_settle) ``` -### Step 3: Cache Responses After Settlement +### Step 3: Check Cache Before Payment -Store responses after successful payment settlement: +Use FastAPI middleware to check the cache before the payment middleware processes the request: ```python -from x402 import x402ResourceServer -from x402.mechanisms.evm import ExactEvmScheme +import base64 +import json +from fastapi import Request, Response +from x402.schemas import PaymentPayload + +@app.middleware("http") +async def idempotency_middleware(request: Request, call_next): + payment_header = request.headers.get("X-Payment") + if payment_header: + try: + payment_data = json.loads(base64.b64decode(payment_header)) + payment_payload = PaymentPayload.model_validate(payment_data) + payment_id = extract_payment_identifier(payment_payload) + + if payment_id: + cached = idempotency_cache.get(payment_id) + if cached and time.time() - cached["timestamp"] < CACHE_TTL_SECONDS: + return Response( + content=json.dumps(cached["response"]), + media_type="application/json", + ) + except Exception: + pass # Invalid payment header, continue to normal flow + + return await call_next(request) + +# Add payment middleware AFTER idempotency middleware +app.add_middleware(PaymentMiddlewareASGI, routes=routes, server=server) +``` -async def after_settle(context): - payment_id = extract_payment_identifier(context.payment_payload) - if payment_id: - idempotency_cache[payment_id] = { - "timestamp": time.time() * 1000, - "response": {} # your response data - } + + + +### Step 1: Advertise Extension Support + +Declare the payment-identifier extension in your route configuration: -resource_server = x402ResourceServer(facilitator_client) -resource_server.register("eip155:84532", ExactEvmScheme()) -resource_server.on_after_settle(after_settle) +```go +import ( + x402http "github.com/coinbase/x402/go/http" + "github.com/coinbase/x402/go/extensions/paymentidentifier" +) + +// Optional or required payment identifier (pick one) +paymentIdExtension := paymentidentifier.DeclarePaymentIdentifierExtension(false) // optional +// paymentIdExtension = paymentidentifier.DeclarePaymentIdentifierExtension(true) // required + +routes := x402http.RoutesConfig{ + "GET /weather": { + Accepts: []x402http.PaymentOption{ + { + Scheme: "exact", + Price: "$0.001", + Network: "eip155:84532", + PayTo: address, + }, + }, + Extensions: map[string]interface{}{ + paymentidentifier.PAYMENT_IDENTIFIER: paymentIdExtension, + }, + }, +} +``` + +### Step 2: Extract Payment ID + +Use the `ExtractPaymentIdentifier()` utility to get the payment ID from the payload: + +```go +import "github.com/coinbase/x402/go/extensions/paymentidentifier" + +// In your handler +payload := c.MustGet("x402_payload").(x402.PaymentPayload) + +paymentID, err := paymentidentifier.ExtractPaymentIdentifier(payload, true) +if err != nil { + // Handle invalid payment ID + c.JSON(400, gin.H{"error": err.Error()}) + return +} + +// Check for duplicate +if existingResponse, found := processedPayments[paymentID]; found { + // Return cached response + c.JSON(200, existingResponse) + return +} +``` + +### Step 3: Cache Responses + +Store responses after successful payment processing: + +```go +// In-memory cache (use Redis in production) +var processedPayments = make(map[string]interface{}) + +// After processing payment +processedPayments[paymentID] = responseData ``` + ### Idempotency Behavior | Scenario | Server Response | @@ -305,7 +491,7 @@ resource_server.on_after_settle(after_settle) #### Cache TTL -Adjust `CACHE_TTL_MS` based on your use case: +Adjust `CACHE_TTL_MS` (TypeScript/Go) or `CACHE_TTL_SECONDS` (Python) based on your use case: - Short TTL (5-15 min): For time-sensitive resources - Long TTL (1-24 hours): For static or infrequently changing resources @@ -323,7 +509,7 @@ Adjust `CACHE_TTL_MS` based on your use case: ### Client Functions -#### `generatePaymentId()` +#### `generatePaymentId(prefix?)` Generates a cryptographically secure unique payment identifier. @@ -332,18 +518,21 @@ import { generatePaymentId } from "@x402/extensions/payment-identifier"; const paymentId = generatePaymentId(); // Returns: "pay_<32-character-hex-string>" + +const orderId = generatePaymentId("order_"); +// Returns: "order_<32-character-hex-string>" ``` -#### `appendPaymentIdentifierToExtensions(extensions, paymentId?)` +#### `appendPaymentIdentifierToExtensions(extensions, id?)` -Adds a payment identifier to the extensions object. If no payment ID is provided, one is generated automatically. +Adds a payment identifier to the extensions object. Only modifies extensions if the server declared support for the extension. If no payment ID is provided, one is generated automatically. ```typescript import { appendPaymentIdentifierToExtensions } from "@x402/extensions/payment-identifier"; -const extensions = {}; -appendPaymentIdentifierToExtensions(extensions, "pay_custom_id"); -// extensions now contains the payment-identifier extension +const extensions = paymentRequired.extensions ?? {}; +appendPaymentIdentifierToExtensions(extensions, "pay_custom_id_1234567890abcdef"); +// extensions now contains the payment-identifier extension (only if server declared it) ``` #### `isValidPaymentId(id)` @@ -353,21 +542,21 @@ Validates a payment identifier format. ```typescript import { isValidPaymentId } from "@x402/extensions/payment-identifier"; -isValidPaymentId("pay_7d5d747be160e280"); // true -isValidPaymentId("invalid"); // false +isValidPaymentId("pay_7d5d747be160e280504c099d984bcfe0"); // true +isValidPaymentId("invalid"); // false (too short) ``` ### Server Functions -#### `declarePaymentIdentifierExtension(required)` +#### `declarePaymentIdentifierExtension(required?)` Creates a payment-identifier extension declaration for resource servers. ```typescript import { declarePaymentIdentifierExtension } from "@x402/extensions/payment-identifier"; -// Optional payment ID -const extension = declarePaymentIdentifierExtension(false); +// Optional payment ID (default) +const extension = declarePaymentIdentifierExtension(); // Required payment ID const extensionRequired = declarePaymentIdentifierExtension(true); @@ -386,16 +575,17 @@ if (paymentId) { } ``` -#### `validatePaymentIdentifier(paymentPayload)` +#### `validatePaymentIdentifier(extension)` -Validates the payment identifier in a payment payload. +Validates the payment identifier extension object structure and ID format. ```typescript import { validatePaymentIdentifier } from "@x402/extensions/payment-identifier"; -const result = validatePaymentIdentifier(paymentPayload); +const extension = paymentPayload.extensions?.["payment-identifier"]; +const result = validatePaymentIdentifier(extension); if (!result.valid) { - console.error(result.error); + console.error(result.errors); } ``` @@ -415,7 +605,7 @@ import { ### Client Functions -#### `generate_payment_id()` +#### `generate_payment_id(prefix="pay_")` Generates a cryptographically secure unique payment identifier. @@ -424,18 +614,21 @@ from x402.extensions.payment_identifier import generate_payment_id payment_id = generate_payment_id() # Returns: "pay_<32-character-hex-string>" + +order_id = generate_payment_id("order_") +# Returns: "order_<32-character-hex-string>" ``` #### `append_payment_identifier_to_extensions(extensions, id=None)` -Adds a payment identifier to the extensions object. If no payment ID is provided, one is generated automatically. +Adds a payment identifier to the extensions object. Only modifies extensions if the server declared support for the extension. If no payment ID is provided, one is generated automatically. ```python from x402.extensions.payment_identifier import append_payment_identifier_to_extensions -extensions = {} -append_payment_identifier_to_extensions(extensions, "pay_custom_id") -# extensions now contains the payment-identifier extension +extensions = payment_required.extensions or {} +append_payment_identifier_to_extensions(extensions, "pay_custom_id_1234567890abcdef") +# extensions now contains the payment-identifier extension (only if server declared it) ``` #### `is_valid_payment_id(id)` @@ -445,8 +638,8 @@ Validates a payment identifier format. ```python from x402.extensions.payment_identifier import is_valid_payment_id -is_valid_payment_id("pay_7d5d747be160e280") # True -is_valid_payment_id("invalid") # False +is_valid_payment_id("pay_7d5d747be160e280504c099d984bcfe0") # True +is_valid_payment_id("invalid") # False (too short) ``` ### Server Functions @@ -458,11 +651,11 @@ Creates a payment-identifier extension declaration for resource servers. ```python from x402.extensions.payment_identifier import declare_payment_identifier_extension -# Optional payment ID -extension = declare_payment_identifier_extension(False) +# Optional payment ID (default) +extension = declare_payment_identifier_extension() # Required payment ID -extension_required = declare_payment_identifier_extension(True) +extension_required = declare_payment_identifier_extension(required=True) ``` #### `extract_payment_identifier(payment_payload)` @@ -478,16 +671,17 @@ if payment_id: pass ``` -#### `validate_payment_identifier(payment_payload)` +#### `validate_payment_identifier(extension)` -Validates the payment identifier in a payment payload. +Validates the payment identifier extension object structure and ID format. ```python from x402.extensions.payment_identifier import validate_payment_identifier -result = validate_payment_identifier(payment_payload) +extension = payment_payload.extensions.get("payment-identifier") +result = validate_payment_identifier(extension) if not result.valid: - print(result.error) + print(result.errors) ``` ### Constants @@ -497,8 +691,113 @@ from x402.extensions.payment_identifier import ( PAYMENT_IDENTIFIER, # "payment-identifier" PAYMENT_ID_MIN_LENGTH, # 16 PAYMENT_ID_MAX_LENGTH, # 128 - PAYMENT_ID_PATTERN, # r"^[a-zA-Z0-9_-]+$" + PAYMENT_ID_PATTERN, # re.compile(r"^[a-zA-Z0-9_-]+$") +) +``` + + + + +### Client Functions + +#### `GeneratePaymentID(prefix string)` + +Generates a cryptographically secure unique payment identifier. + +```go +import "github.com/coinbase/x402/go/extensions/paymentidentifier" + +// Generate with default prefix "pay_" +paymentID := paymentidentifier.GeneratePaymentID("") +// Returns: "pay_<32-character-hex-string>" + +// Generate with custom prefix +paymentID = paymentidentifier.GeneratePaymentID("order_") +// Returns: "order_<32-character-hex-string>" +``` + +#### `AppendPaymentIdentifierToExtensions(extensions map[string]interface{}, id string) error` + +Adds a payment identifier to the extensions object. Only modifies extensions if the server declared support for the extension. Pass an empty string to auto-generate an ID. + +```go +import "github.com/coinbase/x402/go/extensions/paymentidentifier" + +extensions := make(map[string]interface{}) +err := paymentidentifier.AppendPaymentIdentifierToExtensions(extensions, "pay_custom_id_1234567890abcdef") +// extensions now contains the payment-identifier extension (only if server declared it) +``` + +#### `IsValidPaymentID(id string) bool` + +Validates a payment identifier format. + +```go +import "github.com/coinbase/x402/go/extensions/paymentidentifier" + +valid := paymentidentifier.IsValidPaymentID("pay_7d5d747be160e280504c099d984bcfe0") // true +valid = paymentidentifier.IsValidPaymentID("invalid") // false (too short) +``` + +### Server Functions + +#### `DeclarePaymentIdentifierExtension(required bool) PaymentIdentifierExtension` + +Creates a payment-identifier extension declaration for resource servers. + +```go +import "github.com/coinbase/x402/go/extensions/paymentidentifier" + +// Optional payment ID +extension := paymentidentifier.DeclarePaymentIdentifierExtension(false) + +// Required payment ID +extensionRequired := paymentidentifier.DeclarePaymentIdentifierExtension(true) +``` + +#### `ExtractPaymentIdentifier(payload PaymentPayload, validate bool) (string, error)` + +Extracts the payment identifier from a payment payload. + +```go +import "github.com/coinbase/x402/go/extensions/paymentidentifier" + +paymentID, err := paymentidentifier.ExtractPaymentIdentifier(payload, true) +if err != nil { + // Handle error +} +if paymentID != "" { + // Check cache, implement idempotency logic +} +``` + +#### `ValidatePaymentIdentifier(extension interface{}) ValidationResult` + +Validates the payment identifier extension object structure and ID format. + +```go +import "github.com/coinbase/x402/go/extensions/paymentidentifier" + +extension := payload.Extensions[paymentidentifier.PAYMENT_IDENTIFIER] +result := paymentidentifier.ValidatePaymentIdentifier(extension) +if !result.Valid { + // Handle validation errors + fmt.Println(result.Errors) +} +``` + +### Constants + +```go +import "github.com/coinbase/x402/go/extensions/paymentidentifier" + +const ( + PAYMENT_IDENTIFIER = "payment-identifier" + PAYMENT_ID_MIN_LENGTH = 16 + PAYMENT_ID_MAX_LENGTH = 128 ) + +var PAYMENT_ID_PATTERN = regexp.MustCompile(`^[a-zA-Z0-9_-]+$`) ``` @@ -508,15 +807,17 @@ from x402.extensions.payment_identifier import ( Full working examples are available in the x402 repository: +**TypeScript:** - [TypeScript Client Example](https://github.com/coinbase/x402/tree/main/examples/typescript/clients/payment-identifier) - [TypeScript Server Example](https://github.com/coinbase/x402/tree/main/examples/typescript/servers/payment-identifier) + +**Python:** - [Python Client Example](https://github.com/coinbase/x402/tree/main/examples/python/clients/payment-identifier) - [Python Server Example](https://github.com/coinbase/x402/tree/main/examples/python/servers/payment-identifier) -## Support - -- **GitHub**: [github.com/coinbase/x402](https://github.com/coinbase/x402) -- **Discord**: [Join #x402 channel](https://discord.com/invite/cdp) +**Go:** +- [Go Client Example](https://github.com/coinbase/x402/tree/main/examples/go/clients/payment-identifier) +- [Go Server Example](https://github.com/coinbase/x402/tree/main/examples/go/servers/payment-identifier) ## FAQ @@ -530,4 +831,4 @@ A: This is configurable by the server. Typical TTLs range from 5 minutes to 24 h A: Payment IDs must be 16-128 characters, alphanumeric with hyphens and underscores allowed. Use `isValidPaymentId()` to validate custom IDs. **Q: What if the server doesn't support payment-identifier?** -A: The extension is optional. If the server doesn't advertise support, clients can still make payments normally without idempotency. \ No newline at end of file +A: The extension is optional. If the server doesn't advertise support, clients can still make payments normally without idempotency. diff --git a/docs/extensions/sign-in-with-x.mdx b/docs/extensions/sign-in-with-x.mdx index 22257a775d..0344207164 100644 --- a/docs/extensions/sign-in-with-x.mdx +++ b/docs/extensions/sign-in-with-x.mdx @@ -7,10 +7,14 @@ The Sign-In-With-X (SIWX) extension implements [CAIP-122](https://chainagnostic. ## Overview -SIWX solves a key problem in x402: **repeat access to purchased content**. Without SIWX, clients must pay every time they request a resource. With SIWX: +SIWX solves two key problems in x402: -* **For Buyers**: Sign in with your wallet to access content you've already paid for -* **For Sellers**: Grant access to returning customers without requiring repayment +1. **Repeat access to purchased content**: Without SIWX, clients must pay every time they request a resource. With SIWX, sign in with your wallet to access content you've already paid for. +2. **Auth-only routes**: Protect resources with wallet authentication alone, without requiring payment. + +**Key Features:** +* **For Buyers**: Sign in with your wallet to access content you've already paid for, or authenticate to access wallet-gated resources +* **For Sellers**: Grant access to returning customers without requiring repayment, or create auth-only routes that require wallet signatures but no payment * **Chain-Agnostic**: Works with EVM (Ethereum, Base, etc.) and Solana wallets * **Standards-Based**: Built on CAIP-122, EIP-4361 (SIWE), and Sign-In-With-Solana @@ -19,7 +23,9 @@ SIWX solves a key problem in x402: **repeat access to purchased content**. Witho 1. **Server** returns 402 with `sign-in-with-x` extension containing challenge parameters 2. **Client** signs the CAIP-122 message with their wallet 3. **Client** sends signed proof in `SIGN-IN-WITH-X` HTTP header -4. **Server** verifies signature and grants access if wallet has previous payment +4. **Server** verifies signature and grants access either because: + - The route is auth-only (requires signature but no payment), or + - The wallet has previously paid for the resource This is a **Server ↔ Client** extension. The Facilitator is not involved in the authentication flow. @@ -73,11 +79,19 @@ The easiest way to implement SIWX is using the provided hooks, which handle all statement: 'Sign in to access your purchased content', }), }, + 'GET /profile': { + accepts: [], // Auth-only route: no payment required + extensions: declareSIWxExtension({ + network: NETWORK, // Required for auth-only routes (cannot be inferred from accepts) + statement: 'Sign in to view your profile', + expirationSeconds: 300, + }), + }, }; // 3. Verify incoming SIWX proofs const httpServer = new x402HTTPResourceServer(resourceServer, routes) - .onProtectedRequest(createSIWxRequestHook({ storage })); // Grants access if paid + .onProtectedRequest(createSIWxRequestHook({ storage })); // Grants access for auth-only or paid routes ``` @@ -85,7 +99,9 @@ The easiest way to implement SIWX is using the provided hooks, which handle all The hooks automatically: - **siwxResourceServerExtension**: Derives `network` from `accepts`, `domain`/`uri` from request URL, refreshes `nonce`/`issuedAt`/`expirationTime` per request - **createSIWxSettleHook**: Records payment when settlement succeeds -- **createSIWxRequestHook**: Validates and verifies SIWX proofs, grants access if wallet has paid +- **createSIWxRequestHook**: Validates and verifies SIWX proofs, grants access for auth-only routes (`accepts: []`) or when wallet has paid + +**Auth-only routes** (declared with `accepts: []`) grant access based on a valid SIWX signature alone, without requiring payment. This is useful for wallet-gated content that doesn't need micropayments. ### Smart Wallet Support (EIP-1271 / EIP-6492) @@ -166,10 +182,11 @@ For custom implementations, you can use the low-level functions directly: } // verification.address is the verified wallet - // Check if this wallet has paid before + // Grant access for auth-only routes or if wallet has paid + const isAuthOnly = await checkIfAuthOnlyRoute(request); const hasPaid = await checkPaymentHistory(verification.address); - if (hasPaid) { - // Grant access without payment + if (isAuthOnly || hasPaid) { + // Grant access } } ``` @@ -326,6 +343,8 @@ declareSIWxExtension({ }) ``` +**Note for auth-only routes:** When using `accepts: []`, the `network` parameter cannot be inferred from payment requirements and must be provided explicitly. + ### `parseSIWxHeader(header)` Parses a base64-encoded SIGN-IN-WITH-X header into a payload object. diff --git a/docs/faq.md b/docs/faq.md index 4648d881b7..83c80990f8 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -63,8 +63,8 @@ Yes. x402 handles the _payment execution_. You can still meter usage, aggregate | Network | CAIP-2 ID | Asset | Fees\* | Status | | -------------- | --------- | ----- | -------- | ----------- | -| Base | `eip155:8453` | Any EIP-3009 token | fee-free | **Mainnet** | -| Base Sepolia | `eip155:84532` | Any EIP-3009 token | fee-free | **Testnet** | +| Base | `eip155:8453` | Any ERC-20 token | fee-free | **Mainnet** | +| Base Sepolia | `eip155:84532` | Any ERC-20 token | fee-free | **Testnet** | | Solana | `solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp` | Any SPL token or Token-2022 token | fee-free | **Mainnet** | | Solana Devnet | `solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1` | Any SPL token or Token-2022 | fee-free | **Testnet** | @@ -119,7 +119,7 @@ Yes. Programmatic wallets (e.g., **CDP Wallet API**, **viem**, **ethers‑v6** H Tracked in public GitHub issues + community RFCs. Major themes: * Multi‑asset support -* Additional schemes (`upto`, `stream`, `permit2`) +* Additional schemes (`upto`, `stream`) * Discovery layer for service search & reputation **Why is x402 hosted in the Coinbase GitHub?** @@ -131,7 +131,7 @@ We acknowledge that the repo is primarily under Coinbase ownership today. This i #### I keep getting `402 Payment Required`, even after attaching `PAYMENT-SIGNATURE`. Why? 1. Signature is invalid (wrong chain ID or payload fields). -2. Payment amount is less than the required `amount` in the payment requirements. +2. Payment amount does not exactly match the required `amount` in the payment requirements (the exact scheme requires strict equality - no overpayment or underpayment). 3. Address has insufficient USDC or was flagged by KYT.\ Check the `error` field in the server's JSON response for details. diff --git a/docs/getting-started/quickstart-for-buyers.mdx b/docs/getting-started/quickstart-for-buyers.mdx index b95796ab83..003524dc8a 100644 --- a/docs/getting-started/quickstart-for-buyers.mdx +++ b/docs/getting-started/quickstart-for-buyers.mdx @@ -32,6 +32,9 @@ There are pre-configured [examples available in the x402 repo](https://github.co # For Aptos support, also add: npm install @x402/aptos + + # For Stellar support, also add: + npm install @x402/stellar ``` @@ -135,6 +138,20 @@ const privateKey = new Ed25519PrivateKey(process.env.APTOS_PRIVATE_KEY!); const aptosSigner = Account.fromPrivateKey({ privateKey }); ``` +#### Stellar + +Use the Stellar SDK to instantiate a signer: + +```typescript +import { createEd25519Signer } from "@x402/stellar"; + +// Create signer from private key (S... format) +const stellarSigner = createEd25519Signer( + process.env.STELLAR_PRIVATE_KEY!, + "stellar:testnet" +); +``` + ### 3. Make Paid Requests Automatically @@ -387,6 +404,15 @@ You can register multiple payment schemes to handle different networks: const aptosPrivateKey = new Ed25519PrivateKey(process.env.APTOS_PRIVATE_KEY!); const aptosSigner = Account.fromPrivateKey({ privateKey: aptosPrivateKey }); client.register("aptos:*", new ExactAptosScheme(aptosSigner)); + + // For Stellar support, also add: + import { ExactStellarScheme, createEd25519Signer } from "@x402/stellar"; + + const stellarSigner = createEd25519Signer( + process.env.STELLAR_PRIVATE_KEY!, + "stellar:testnet" + ); + client.register("stellar:*", new ExactStellarScheme(stellarSigner)); ``` diff --git a/docs/getting-started/quickstart-for-sellers.mdx b/docs/getting-started/quickstart-for-sellers.mdx index 3a304d78da..789d6097e8 100644 --- a/docs/getting-started/quickstart-for-sellers.mdx +++ b/docs/getting-started/quickstart-for-sellers.mdx @@ -40,6 +40,13 @@ There are pre-configured examples available in the x402 repo for both [Node.js]( npm install @x402/hono @x402/core @x402/evm @x402/svm ``` + + Install the [x402 Fastify middleware package](https://www.npmjs.com/package/@x402/fastify). + + ```bash + npm install @x402/fastify @x402/core @x402/evm @x402/svm + ``` + Add the x402 Go module to your project: @@ -299,6 +306,63 @@ Integrate the payment middleware into your application. You will need to provide serve({ fetch: app.fetch, port: 4021 }); ``` + + Full example in the repo [here](https://github.com/coinbase/x402/tree/main/examples/typescript/servers/fastify). + + ```typescript + import Fastify from "fastify"; + import { paymentMiddleware, x402ResourceServer } from "@x402/fastify"; + import { ExactEvmScheme } from "@x402/evm/exact/server"; + import { ExactSvmScheme } from "@x402/svm/exact/server"; + import { HTTPFacilitatorClient } from "@x402/core/server"; + + const app = Fastify(); + const evmAddress = "0xYourEvmAddress"; + const svmAddress = "YourSolanaAddress"; + + const facilitatorClient = new HTTPFacilitatorClient({ + url: "https://x402.org/facilitator" + }); + + paymentMiddleware( + app, + { + "GET /weather": { + accepts: [ + { + scheme: "exact", + price: "$0.001", + network: "eip155:84532", // Base Sepolia + payTo: evmAddress, + }, + { + scheme: "exact", + price: "$0.001", + network: "solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1", // Solana Devnet + payTo: svmAddress, + }, + ], + description: "Weather data", + mimeType: "application/json", + }, + }, + new x402ResourceServer(facilitatorClient) + .register("eip155:84532", new ExactEvmScheme()) + .register("solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1", new ExactSvmScheme()), + ); + + app.get("/weather", async () => { + return { + report: { + weather: "sunny", + temperature: 70, + }, + }; + }); + + app.listen({ port: 4021 }); + ``` + Full example in the repo [here](https://github.com/coinbase/x402/tree/main/examples/go/servers/gin). @@ -372,6 +436,85 @@ Integrate the payment middleware into your application. You will need to provide } ``` + + Full example in the repo [here](https://github.com/coinbase/x402/tree/main/examples/go/servers/nethttp). + + ```go + package main + + import ( + "encoding/json" + "net/http" + "time" + + x402 "github.com/coinbase/x402/go" + x402http "github.com/coinbase/x402/go/http" + nethttpmw "github.com/coinbase/x402/go/http/nethttp" + evm "github.com/coinbase/x402/go/mechanisms/evm/exact/server" + svm "github.com/coinbase/x402/go/mechanisms/svm/exact/server" + ) + + func main() { + evmAddress := "0xYourEvmAddress" + svmAddress := "YourSolanaAddress" + evmNetwork := x402.Network("eip155:84532") // Base Sepolia + svmNetwork := x402.Network("solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1") // Solana Devnet + + // Create facilitator client + facilitatorClient := x402http.NewHTTPFacilitatorClient(&x402http.FacilitatorConfig{ + URL: "https://x402.org/facilitator", + }) + + // Configure routes + routes := x402http.RoutesConfig{ + "GET /weather": { + Accepts: x402http.PaymentOptions{ + { + Scheme: "exact", + Price: "$0.001", + Network: "eip155:84532", + PayTo: evmAddress, + }, + { + Scheme: "exact", + Price: "$0.001", + Network: "solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1", + PayTo: svmAddress, + }, + }, + Description: "Get weather data for a city", + MimeType: "application/json", + }, + } + + // Create ServeMux and register handlers + mux := http.NewServeMux() + + // Protected endpoint + mux.HandleFunc("GET /weather", func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + json.NewEncoder(w).Encode(map[string]interface{}{ + "weather": "sunny", + "temperature": 70, + }) + }) + + // Apply x402 payment middleware + handler := nethttpmw.X402Payment(nethttpmw.Config{ + Routes: routes, + Facilitator: facilitatorClient, + Schemes: []nethttpmw.SchemeConfig{ + {Network: evmNetwork, Server: evm.NewExactEvmScheme()}, + {Network: svmNetwork, Server: svm.NewExactSvmScheme()}, + }, + Timeout: 30 * time.Second, + })(mux) + + http.ListenAndServe(":4021", handler) + } + ``` + Full example in the repo [here](https://github.com/coinbase/x402/tree/main/examples/python/servers/fastapi). @@ -686,7 +829,7 @@ Change from testnet to mainnet network identifiers: For multi-network support, register both EVM and SVM schemes: - + ```typescript import { ExactEvmScheme } from "@x402/evm/exact/server"; import { ExactSvmScheme } from "@x402/svm/exact/server"; diff --git a/docs/introduction.md b/docs/introduction.md index 708ac07994..86d16a34c1 100644 --- a/docs/introduction.md +++ b/docs/introduction.md @@ -15,16 +15,16 @@ With x402, any web service can require payment before serving a response, using ### Why Use x402? -x402 offers key advantages over traditional payment systems: +x402 offers: -* **Low fees and minimal friction** compared to traditional credit cards and fiat payment processors -* **Native support for machine-to-machine payments**, enabling seamless use by AI agents -* **Built-in micropayment support**, making it easy to monetize usage-based services +- **No fees and minimal friction** x402 as a standard has 0 fees built in. +- **Native support for machine-to-machine payments**, enabling seamless use by AI agents +- **Built-in micropayment support**, making it easy to monetize usage-based services ### Who is x402 for? -* **Sellers:** Service providers who want to monetize their APIs or content. x402 enables direct, programmatic payments from clients with minimal setup. -* **Buyers:** Human developers and AI agents seeking to access paid services without accounts or manual payment flows. +- **Sellers:** Service providers who want to monetize their APIs or content. x402 enables direct, programmatic payments from clients with minimal setup. +- **Buyers:** Human developers and AI agents seeking to access paid services without accounts or manual payment flows. Both sellers and buyers interact directly through HTTP requests, with payment handled transparently through the protocol. @@ -32,11 +32,11 @@ Both sellers and buyers interact directly through HTTP requests, with payment ha x402 enables a range of use cases, including: -* API services paid per request -* AI agents that autonomously pay for API access -* [Paywalls](https://x.com/MurrLincoln/status/1935406976881803601) for digital content -* Microservices and tooling monetized via microtransactions -* Proxy services that aggregate and resell API capabilities +- API services paid per request +- AI agents that autonomously pay for API access +- [Paywalls](https://x.com/MurrLincoln/status/1935406976881803601) for digital content +- Microservices and tooling monetized via microtransactions +- Proxy services that aggregate and resell API capabilities ### How Does It Work? @@ -50,9 +50,9 @@ At a high level, the flow is simple: For more detail, see: -* [Client / Server](/core-concepts/client-server) -* [Facilitator](/core-concepts/facilitator) -* [HTTP 402](/core-concepts/http-402) +- [Client / Server](/core-concepts/client-server) +- [Facilitator](/core-concepts/facilitator) +- [HTTP 402](/core-concepts/http-402) The goal is to make programmatic commerce accessible, permissionless, and developer-friendly. @@ -60,7 +60,7 @@ The goal is to make programmatic commerce accessible, permissionless, and develo Ready to build? Start here: -* [Quickstart for Sellers](/getting-started/quickstart-for-sellers) -* [Quickstart for Buyers](/getting-started/quickstart-for-buyers) -* [Explore Core Concepts](/core-concepts/http-402) -* [Join our community on Discord](https://discord.gg/invite/cdp) +- [Quickstart for Sellers](/getting-started/quickstart-for-sellers) +- [Quickstart for Buyers](/getting-started/quickstart-for-buyers) +- [Explore Core Concepts](/core-concepts/http-402) +- [Join our community on Discord](https://discord.gg/invite/cdp) diff --git a/docs/sdk-features.md b/docs/sdk-features.md index b63ede970e..1d94740a15 100644 --- a/docs/sdk-features.md +++ b/docs/sdk-features.md @@ -19,7 +19,7 @@ This page tracks which features are implemented in each SDK (TypeScript, Go, Pyt | Role | TypeScript | Go | Python | |------|------------|-----|--------| -| Server | Express, Hono, Next.js | Gin | FastAPI, Flask | +| Server | Express, Hono, Next.js | Gin, net/http | FastAPI, Flask | | Client | Fetch, Axios | net/http | httpx, requests | ## Networks @@ -28,6 +28,7 @@ This page tracks which features are implemented in each SDK (TypeScript, Go, Pyt |---------|------------|-----|--------| | evm (EIP-155) | ✅ | ✅ | ✅ | | svm (Solana) | ✅ | ✅ | ✅ | +| stellar | ✅ | ❌ | ❌ | | aptos | ✅ | ❌ | ❌ | ## Mechanisms @@ -35,16 +36,23 @@ This page tracks which features are implemented in each SDK (TypeScript, Go, Pyt | Mechanism | TypeScript | Go | Python | |-----------|------------|-----|--------| | exact/evm (EIP-3009) | ✅ | ✅ | ✅ | +| exact/evm (Permit2) | ✅ | ✅ | ✅ | | exact/svm (SPL) | ✅ | ✅ | ✅ | +| exact/stellar (Soroban) | ✅ | ❌ | ❌ | | exact/aptos (Fungible Assets) | ✅ | ❌ | ❌ | +| upto/evm (Permit2) | ✅ | ✅ | ❌ | ## Extensions | Extension | TypeScript | Go | Python | |-----------|------------|-----|--------| -| bazaar | ✅ | ✅ | ✅ | +| bazaar (server) | ✅ | ✅ | ✅ | +| bazaar (facilitator client) | ✅ | ✅ | ✅ | | sign-in-with-x | ✅ | ❌ | ❌ | -| payment-identifier | ✅ | ❌ | ✅ | +| payment-identifier | ✅ | ✅ | ✅ | +| offer-receipt | ✅ | ❌ | ❌ | +| eip2612-gas-sponsoring | ✅ | ✅ | ✅ | +| erc20-approval-gas-sponsoring | ✅ | ✅ | ✅ | ## Client Hooks @@ -65,7 +73,7 @@ This page tracks which features are implemented in each SDK (TypeScript, Go, Pyt | onBeforeSettle | ✅ | ✅ | ✅ | | onAfterSettle | ✅ | ✅ | ✅ | | onSettleFailure | ✅ | ✅ | ✅ | -| onProtectedRequest (HTTP) | ✅ | ❌ | ❌ | +| onProtectedRequest (HTTP) | ✅ | ✅ | ❌ | ## Facilitator Hooks diff --git a/e2e/.env-local b/e2e/.env-local index d5cfb089e4..77e443bb11 100644 --- a/e2e/.env-local +++ b/e2e/.env-local @@ -1,7 +1,10 @@ # E2E Test Configuration SERVER_EVM_ADDRESS= SERVER_SVM_ADDRESS= +SERVER_STELLAR_ADDRESS= CLIENT_EVM_PRIVATE_KEY= CLIENT_SVM_PRIVATE_KEY= +CLIENT_STELLAR_PRIVATE_KEY= FACILITATOR_EVM_PRIVATE_KEY= FACILITATOR_SVM_PRIVATE_KEY= +FACILITATOR_STELLAR_PRIVATE_KEY= diff --git a/e2e/README.md b/e2e/README.md index 3b0a6fe4d4..9a54295429 100644 --- a/e2e/README.md +++ b/e2e/README.md @@ -85,27 +85,62 @@ Add the `-v` flag to any command for verbose output: Useful for debugging test failures or understanding the payment flow. +## Wallet Safety Warning + +**Use dedicated test wallets only. Do NOT use wallets that hold real funds.** + +The test suite moves ETH between the configured wallets during a run. Funds stay +within the set of wallets defined in `.env`, but individual wallet balances will +change unpredictably: + +- **ETH is transferred** from the facilitator wallet to the client wallet so the + client can pay gas for granting and revoking Permit2 approvals between tests. +- **ETH is swept** from the client wallet back to the facilitator after revocation + to create a zero-balance state, which is required to exercise the facilitator's + gasless funding step. +- **Token approvals are granted and revoked** on the client wallet as part of + normal test flow. + +While no funds leave the configured wallet set, the client wallet's ETH balance +will be drained to near-zero between tests. Do not rely on any particular wallet +having a stable balance during or after a run. + ## Environment Variables Required environment variables (set in `.env` file): ```bash -# Client wallets +# Client wallets (⚠️ TEST WALLETS ONLY — balances will be swept during runs) CLIENT_EVM_PRIVATE_KEY=0x... # EVM private key for client payments CLIENT_SVM_PRIVATE_KEY=... # Solana private key for client payments CLIENT_APTOS_PRIVATE_KEY=... # Aptos private key for client payments (hex string) +CLIENT_STELLAR_PRIVATE_KEY=... # Stellar private key for client payments # Server payment addresses SERVER_EVM_ADDRESS=0x... # Where servers receive EVM payments SERVER_SVM_ADDRESS=... # Where servers receive Solana payments SERVER_APTOS_ADDRESS=0x... # Where servers receive Aptos payments +SERVER_STELLAR_ADDRESS=... # Where servers receive Stellar payments -# Facilitator wallets (for payment verification/settlement) +# Facilitator wallets (⚠️ TEST WALLETS ONLY — used to fund/drain client between tests) FACILITATOR_EVM_PRIVATE_KEY=0x... # EVM private key for facilitator FACILITATOR_SVM_PRIVATE_KEY=... # Solana private key for facilitator FACILITATOR_APTOS_PRIVATE_KEY=... # Aptos private key for facilitator (hex string) +FACILITATOR_STELLAR_PRIVATE_KEY=... # Stellar private key for facilitator ``` +### Account Setup Instructions + +#### Stellar Testnet + +You need **three separate Stellar accounts** for e2e tests (client, server, facilitator): + +1. Go to [Stellar Laboratory](https://lab.stellar.org/account/create) ➡️ Generate keypair ➡️ Fund account with Friendbot, then copy the `Secret` and `Public` keys so you can use them. +2. Add USDC trustline (required for client and server): go to [Fund Account](https://lab.stellar.org/account/fund) ➡️ Paste your `Public Key` ➡️ Add USDC Trustline ➡️ paste your `Secret key` ➡️ Sign transaction ➡️ Add Trustline. +3. Get testnet USDC from [Circle Faucet](https://faucet.circle.com/) (select Stellar network). + +> **Note:** The facilitator account only needs XLM (step 1). Client and server accounts need all three steps. + ## Example Session ```bash @@ -118,7 +153,7 @@ $ pnpm test --min ✔ Select servers › express, hono, legacy-express ✔ Select clients › axios, fetch, httpx ✔ Select extensions › bazaar -✔ Select protocol families › EVM, SVM, Aptos +✔ Select protocol families › EVM, SVM, Aptos, Stellar 📊 Coverage-Based Minimization Total scenarios: 156 diff --git a/e2e/clients/axios/.env-local b/e2e/clients/axios/.env-local index 73a01182c4..910973a20d 100644 --- a/e2e/clients/axios/.env-local +++ b/e2e/clients/axios/.env-local @@ -3,3 +3,4 @@ ENDPOINT_PATH=/weather EVM_PRIVATE_KEY= SVM_PRIVATE_KEY= APTOS_PRIVATE_KEY= +STELLAR_PRIVATE_KEY= diff --git a/e2e/clients/axios/README.md b/e2e/clients/axios/README.md index 463fb72b1f..8da87a9794 100644 --- a/e2e/clients/axios/README.md +++ b/e2e/clients/axios/README.md @@ -1,13 +1,13 @@ -# E2E Test Client: TypeScript Fetch +# E2E Test Client: TypeScript Axios -This client demonstrates and tests the `@x402/fetch` package with both EVM and SVM payment support. +This client demonstrates and tests the `@x402/axios` package with EVM, SVM, and Stellar payment support. ## What It Tests ### Core Functionality - ✅ **V2 Protocol** - Modern x402 protocol with CAIP-2 networks - ✅ **V1 Protocol** - Legacy x402 protocol with simple network names -- ✅ **Multi-chain Support** - Both EVM and SVM in a single client +- ✅ **Multi-chain Support** - EVM, SVM, and (optional) Stellar in a single client - ✅ **Automatic Payment Handling** - Transparent 402 response handling - ✅ **Payment Response Decoding** - Extracts settlement information from headers @@ -16,40 +16,44 @@ This client demonstrates and tests the `@x402/fetch` package with both EVM and S - ✅ **EVM V1** - `base-sepolia` and `base` networks - ✅ **SVM V2** - `solana:*` wildcard scheme - ✅ **SVM V1** - `solana-devnet` and `solana` networks +- ✅ **Stellar V2** - `stellar:*` wildcard scheme (optional) ## What It Demonstrates ### Usage Pattern ```typescript -import { wrapFetchWithPayment } from "@x402/fetch"; +import axios from "axios"; +import { wrapAxiosWithPayment } from "@x402/axios"; import { x402Client } from "@x402/core/client"; import { ExactEvmClient } from "@x402/evm"; import { ExactEvmClientV1 } from "@x402/evm/v1"; import { ExactSvmClient } from "@x402/svm"; import { ExactSvmClientV1 } from "@x402/svm/v1"; +import { ExactStellarClient } from "@x402/stellar"; // Build x402 client with direct registration const client = new x402Client() .register("eip155:*", new ExactEvmClient(evmAccount)) .register("solana:*", new ExactSvmClient(svmSigner)) + .register("stellar:*", new ExactStellarClient(stellarSigner)) .registerV1("base-sepolia", new ExactEvmClientV1(evmAccount)) .registerV1("base", new ExactEvmClientV1(evmAccount)) .registerV1("solana-devnet", new ExactSvmClientV1(svmSigner)) .registerV1("solana", new ExactSvmClientV1(svmSigner)); -// Wrap fetch with payment handling -const fetchWithPayment = wrapFetchWithPayment(fetch, client); +// Wrap axios with payment handling +const axiosWithPayment = wrapAxiosWithPayment(axios.create(), client); // Make request - 402 responses handled automatically -const response = await fetchWithPayment(url, { method: "GET" }); +const response = await axiosWithPayment.get(url); ``` ### Key Concepts Shown 1. **Builder Pattern** - Fluent API for registering multiple schemes 2. **Multi-Version Support** - V1 and V2 protocols side-by-side -3. **Multi-Chain Support** - EVM and SVM in one client +3. **Multi-Chain Support** - EVM, SVM, and (optional) Stellar in one client 4. **Network Flexibility** - Wildcards for V2, specific networks for V1 5. **Transparent Payment** - No manual 402 handling needed @@ -58,8 +62,8 @@ const response = await fetchWithPayment(url, { method: "GET" }); This client is tested against: - **Servers:** Express (TypeScript), Gin (Go) - **Facilitators:** TypeScript, Go -- **Endpoints:** `/protected` (EVM), `/protected-svm` (SVM) -- **Networks:** Base Sepolia (EVM), Solana Devnet (SVM) +- **Endpoints:** `/protected` (EVM), `/protected-svm` (SVM), `/protected-stellar` (Stellar) +- **Networks:** Base Sepolia (EVM), Solana Devnet (SVM), Stellar Testnet (Stellar) ### Success Criteria - ✅ Request succeeds with 200 status @@ -72,24 +76,29 @@ This client is tested against: ```bash # Via e2e test suite cd e2e -pnpm test --client=fetch +pnpm test --client=axios # Direct execution (requires environment variables) -cd e2e/clients/fetch +cd e2e/clients/axios export RESOURCE_SERVER_URL="http://localhost:4022" export ENDPOINT_PATH="/protected" export EVM_PRIVATE_KEY="0x..." export SVM_PRIVATE_KEY="..." +export STELLAR_PRIVATE_KEY="S..." # optional pnpm start ``` ## Environment Variables +### Required - `RESOURCE_SERVER_URL` - Server base URL - `ENDPOINT_PATH` - Path to protected endpoint - `EVM_PRIVATE_KEY` - Ethereum private key (hex with 0x prefix) - `SVM_PRIVATE_KEY` - Solana private key (base58 encoded) +### Optional +- `STELLAR_PRIVATE_KEY` - Stellar private key (S... format) - enables Stellar support + ## Output Format ```json @@ -108,11 +117,12 @@ pnpm start ## Package Dependencies -- `@x402/fetch` - HTTP wrapper with payment handling +- `@x402/axios` - Axios wrapper with payment handling - `@x402/core` - Core x402 client and types - `@x402/evm` - EVM payment mechanisms (V2) - `@x402/evm/v1` - EVM payment mechanisms (V1) - `@x402/svm` - SVM payment mechanisms (V2) - `@x402/svm/v1` - SVM payment mechanisms (V1) +- `@x402/stellar` - Stellar payment mechanisms (V2) - `viem` - Ethereum library for account creation - `@solana/kit` - Solana keypair utilities diff --git a/e2e/clients/axios/index.ts b/e2e/clients/axios/index.ts index cee6f944e5..31f00ddfca 100644 --- a/e2e/clients/axios/index.ts +++ b/e2e/clients/axios/index.ts @@ -3,14 +3,17 @@ import axios from "axios"; import { wrapAxiosWithPayment, decodePaymentResponseHeader } from "@x402/axios"; import { createPublicClient, http } from "viem"; import { privateKeyToAccount } from "viem/accounts"; -import { baseSepolia } from "viem/chains"; -import { ExactEvmScheme } from "@x402/evm/exact/client"; +import { base, baseSepolia } from "viem/chains"; +import { ExactEvmScheme, type ExactEvmSchemeOptions } from "@x402/evm/exact/client"; +import { UptoEvmScheme as UptoEvmClientScheme, type UptoEvmSchemeOptions } from "@x402/evm/upto/client"; import { ExactEvmSchemeV1 } from "@x402/evm/v1"; import { toClientEvmSigner } from "@x402/evm"; import { ExactSvmScheme } from "@x402/svm/exact/client"; import { ExactSvmSchemeV1 } from "@x402/svm/v1"; import { ExactAptosScheme } from "@x402/aptos/exact/client"; import { Account, Ed25519PrivateKey, PrivateKey, PrivateKeyVariants } from "@aptos-labs/ts-sdk"; +import { ExactStellarScheme } from "@x402/stellar/exact/client"; +import { createEd25519Signer, type Ed25519Signer } from "@x402/stellar"; import { base58 } from "@scure/base"; import { createKeyPairSignerFromBytes } from "@solana/kit"; import { x402Client } from "@x402/core/client"; @@ -25,23 +28,45 @@ const svmSigner = await createKeyPairSignerFromBytes( base58.decode(process.env.SVM_PRIVATE_KEY as string), ); +const evmNetwork = process.env.EVM_NETWORK || "eip155:84532"; +const evmRpcUrl = process.env.EVM_RPC_URL; +const evmChain = evmNetwork === "eip155:8453" ? base : baseSepolia; + const publicClient = createPublicClient({ - chain: baseSepolia, - transport: http(), + chain: evmChain, + transport: http(evmRpcUrl), }); const evmSigner = toClientEvmSigner(evmAccount, publicClient); +const evmSchemeOptions: ExactEvmSchemeOptions | undefined = process.env.EVM_RPC_URL + ? { rpcUrl: process.env.EVM_RPC_URL } + : undefined; + +const uptoSchemeOptions: UptoEvmSchemeOptions | undefined = process.env.EVM_RPC_URL + ? { rpcUrl: process.env.EVM_RPC_URL } + : undefined; + // Initialize Aptos signer if key is provided let aptosAccount: Account | undefined; if (process.env.APTOS_PRIVATE_KEY) { - const formattedKey = PrivateKey.formatPrivateKey(process.env.APTOS_PRIVATE_KEY, PrivateKeyVariants.Ed25519); + const formattedKey = PrivateKey.formatPrivateKey( + process.env.APTOS_PRIVATE_KEY, + PrivateKeyVariants.Ed25519, + ); const aptosPrivateKey = new Ed25519PrivateKey(formattedKey); aptosAccount = Account.fromPrivateKey({ privateKey: aptosPrivateKey }); } +// Initialize Stellar signer if key is provided +let stellarSigner: Ed25519Signer | undefined; +if (process.env.STELLAR_PRIVATE_KEY) { + stellarSigner = createEd25519Signer(process.env.STELLAR_PRIVATE_KEY); +} + const client = new x402Client() - .register("eip155:*", new ExactEvmScheme(evmSigner)) + .register("eip155:*", new ExactEvmScheme(evmSigner, evmSchemeOptions)) + .register("eip155:*", new UptoEvmClientScheme(evmSigner, uptoSchemeOptions)) .registerV1("base-sepolia", new ExactEvmSchemeV1(evmSigner)) .registerV1("base", new ExactEvmSchemeV1(evmSigner)) .register("solana:*", new ExactSvmScheme(svmSigner)) @@ -50,6 +75,9 @@ const client = new x402Client() if (aptosAccount) { client.register("aptos:*", new ExactAptosScheme(aptosAccount)); } +if (stellarSigner) { + client.register("stellar:*", new ExactStellarScheme(stellarSigner)); +} const axiosWithPayment = wrapAxiosWithPayment(axios.create(), client); diff --git a/e2e/clients/axios/package.json b/e2e/clients/axios/package.json index 32a92fdf10..6c97ae132c 100644 --- a/e2e/clients/axios/package.json +++ b/e2e/clients/axios/package.json @@ -17,6 +17,7 @@ "@x402/axios": "workspace:*", "@x402/core": "workspace:*", "@x402/evm": "workspace:*", + "@x402/stellar": "workspace:*", "@x402/svm": "workspace:*", "axios": "^1.7.9", "dotenv": "^16.4.7", diff --git a/e2e/clients/axios/test.config.json b/e2e/clients/axios/test.config.json index 321b192c54..ff7b7accbf 100644 --- a/e2e/clients/axios/test.config.json +++ b/e2e/clients/axios/test.config.json @@ -5,7 +5,8 @@ "protocolFamilies": [ "evm", "svm", - "aptos" + "aptos", + "stellar" ], "x402Versions": [ 1, @@ -14,7 +15,8 @@ "evm": { "transferMethods": [ "eip3009", - "permit2" + "permit2", + "upto" ] }, "extensions": [ @@ -29,7 +31,8 @@ "ENDPOINT_PATH" ], "optional": [ - "APTOS_PRIVATE_KEY" + "APTOS_PRIVATE_KEY", + "STELLAR_PRIVATE_KEY" ] } } \ No newline at end of file diff --git a/e2e/clients/fetch/README.md b/e2e/clients/fetch/README.md index 463fb72b1f..56ba7ef7eb 100644 --- a/e2e/clients/fetch/README.md +++ b/e2e/clients/fetch/README.md @@ -1,13 +1,13 @@ # E2E Test Client: TypeScript Fetch -This client demonstrates and tests the `@x402/fetch` package with both EVM and SVM payment support. +This client demonstrates and tests the `@x402/fetch` package with EVM, SVM, and Stellar payment support. ## What It Tests ### Core Functionality - ✅ **V2 Protocol** - Modern x402 protocol with CAIP-2 networks - ✅ **V1 Protocol** - Legacy x402 protocol with simple network names -- ✅ **Multi-chain Support** - Both EVM and SVM in a single client +- ✅ **Multi-chain Support** - EVM, SVM, and (optional) Stellar in a single client - ✅ **Automatic Payment Handling** - Transparent 402 response handling - ✅ **Payment Response Decoding** - Extracts settlement information from headers @@ -16,6 +16,7 @@ This client demonstrates and tests the `@x402/fetch` package with both EVM and S - ✅ **EVM V1** - `base-sepolia` and `base` networks - ✅ **SVM V2** - `solana:*` wildcard scheme - ✅ **SVM V1** - `solana-devnet` and `solana` networks +- ✅ **Stellar V2** - `stellar:*` wildcard scheme (optional) ## What It Demonstrates @@ -28,11 +29,13 @@ import { ExactEvmClient } from "@x402/evm"; import { ExactEvmClientV1 } from "@x402/evm/v1"; import { ExactSvmClient } from "@x402/svm"; import { ExactSvmClientV1 } from "@x402/svm/v1"; +import { ExactStellarClient } from "@x402/stellar"; // Build x402 client with direct registration const client = new x402Client() .register("eip155:*", new ExactEvmClient(evmAccount)) .register("solana:*", new ExactSvmClient(svmSigner)) + .register("stellar:*", new ExactStellarClient(stellarSigner)) .registerV1("base-sepolia", new ExactEvmClientV1(evmAccount)) .registerV1("base", new ExactEvmClientV1(evmAccount)) .registerV1("solana-devnet", new ExactSvmClientV1(svmSigner)) @@ -49,7 +52,7 @@ const response = await fetchWithPayment(url, { method: "GET" }); 1. **Builder Pattern** - Fluent API for registering multiple schemes 2. **Multi-Version Support** - V1 and V2 protocols side-by-side -3. **Multi-Chain Support** - EVM and SVM in one client +3. **Multi-Chain Support** - EVM, SVM, and (optional) Stellar in one client 4. **Network Flexibility** - Wildcards for V2, specific networks for V1 5. **Transparent Payment** - No manual 402 handling needed @@ -58,8 +61,8 @@ const response = await fetchWithPayment(url, { method: "GET" }); This client is tested against: - **Servers:** Express (TypeScript), Gin (Go) - **Facilitators:** TypeScript, Go -- **Endpoints:** `/protected` (EVM), `/protected-svm` (SVM) -- **Networks:** Base Sepolia (EVM), Solana Devnet (SVM) +- **Endpoints:** `/protected` (EVM), `/protected-svm` (SVM), `/protected-stellar` (Stellar) +- **Networks:** Base Sepolia (EVM), Solana Devnet (SVM), Stellar Testnet (Stellar) ### Success Criteria - ✅ Request succeeds with 200 status @@ -80,16 +83,21 @@ export RESOURCE_SERVER_URL="http://localhost:4022" export ENDPOINT_PATH="/protected" export EVM_PRIVATE_KEY="0x..." export SVM_PRIVATE_KEY="..." +export STELLAR_PRIVATE_KEY="S..." # optional pnpm start ``` ## Environment Variables +### Required - `RESOURCE_SERVER_URL` - Server base URL - `ENDPOINT_PATH` - Path to protected endpoint - `EVM_PRIVATE_KEY` - Ethereum private key (hex with 0x prefix) - `SVM_PRIVATE_KEY` - Solana private key (base58 encoded) +### Optional +- `STELLAR_PRIVATE_KEY` - Stellar private key (S... format) - enables Stellar support + ## Output Format ```json @@ -114,5 +122,6 @@ pnpm start - `@x402/evm/v1` - EVM payment mechanisms (V1) - `@x402/svm` - SVM payment mechanisms (V2) - `@x402/svm/v1` - SVM payment mechanisms (V1) +- `@x402/stellar` - Stellar payment mechanisms (V2) - `viem` - Ethereum library for account creation - `@solana/kit` - Solana keypair utilities diff --git a/e2e/clients/fetch/index.ts b/e2e/clients/fetch/index.ts index 9544bb3f68..e88320d3d5 100644 --- a/e2e/clients/fetch/index.ts +++ b/e2e/clients/fetch/index.ts @@ -1,15 +1,18 @@ import { config } from "dotenv"; -import { wrapFetchWithPayment, decodePaymentResponseHeader } from "@x402/fetch"; +import { wrapFetchWithPayment } from "@x402/fetch"; import { createPublicClient, http } from "viem"; import { privateKeyToAccount } from "viem/accounts"; -import { baseSepolia } from "viem/chains"; -import { ExactEvmScheme } from "@x402/evm/exact/client"; +import { base, baseSepolia } from "viem/chains"; +import { ExactEvmScheme, type ExactEvmSchemeOptions } from "@x402/evm/exact/client"; +import { UptoEvmScheme as UptoEvmClientScheme, type UptoEvmSchemeOptions } from "@x402/evm/upto/client"; import { ExactEvmSchemeV1 } from "@x402/evm/v1"; import { toClientEvmSigner } from "@x402/evm"; import { ExactSvmScheme } from "@x402/svm/exact/client"; import { ExactSvmSchemeV1 } from "@x402/svm/v1"; import { ExactAptosScheme } from "@x402/aptos/exact/client"; import { Account, Ed25519PrivateKey, PrivateKey, PrivateKeyVariants } from "@aptos-labs/ts-sdk"; +import { ExactStellarScheme } from "@x402/stellar/exact/client"; +import { createEd25519Signer, Ed25519Signer } from "@x402/stellar"; import { base58 } from "@scure/base"; import { createKeyPairSignerFromBytes } from "@solana/kit"; import { x402Client, x402HTTPClient } from "@x402/core/client"; @@ -22,13 +25,25 @@ const url = `${baseURL}${endpointPath}`; const evmAccount = privateKeyToAccount(process.env.EVM_PRIVATE_KEY as `0x${string}`); const svmSigner = await createKeyPairSignerFromBytes(base58.decode(process.env.SVM_PRIVATE_KEY as string)); +const evmNetwork = process.env.EVM_NETWORK || "eip155:84532"; +const evmRpcUrl = process.env.EVM_RPC_URL; +const evmChain = evmNetwork === "eip155:8453" ? base : baseSepolia; + const publicClient = createPublicClient({ - chain: baseSepolia, - transport: http(), + chain: evmChain, + transport: http(evmRpcUrl), }); const evmSigner = toClientEvmSigner(evmAccount, publicClient); +const evmSchemeOptions: ExactEvmSchemeOptions | undefined = process.env.EVM_RPC_URL + ? { rpcUrl: process.env.EVM_RPC_URL } + : undefined; + +const uptoSchemeOptions: UptoEvmSchemeOptions | undefined = process.env.EVM_RPC_URL + ? { rpcUrl: process.env.EVM_RPC_URL } + : undefined; + // Initialize Aptos signer if key is provided let aptosAccount: Account | undefined; if (process.env.APTOS_PRIVATE_KEY) { @@ -37,8 +52,15 @@ if (process.env.APTOS_PRIVATE_KEY) { aptosAccount = Account.fromPrivateKey({ privateKey: aptosPrivateKey }); } +// Initialize Stellar signer if key is provided +let stellarSigner: Ed25519Signer | undefined; +if (process.env.STELLAR_PRIVATE_KEY) { + stellarSigner = createEd25519Signer(process.env.STELLAR_PRIVATE_KEY); +} + const client = new x402Client() - .register("eip155:*", new ExactEvmScheme(evmSigner)) + .register("eip155:*", new ExactEvmScheme(evmSigner, evmSchemeOptions)) + .register("eip155:*", new UptoEvmClientScheme(evmSigner, uptoSchemeOptions)) .registerV1("base-sepolia", new ExactEvmSchemeV1(evmSigner)) .registerV1("base", new ExactEvmSchemeV1(evmSigner)) .register("solana:*", new ExactSvmScheme(svmSigner)) @@ -47,6 +69,9 @@ const client = new x402Client() if (aptosAccount) { client.register("aptos:*", new ExactAptosScheme(aptosAccount)); } +if (stellarSigner) { + client.register("stellar:*", new ExactStellarScheme(stellarSigner)); +} const fetchWithPayment = wrapFetchWithPayment(fetch, client); diff --git a/e2e/clients/fetch/package.json b/e2e/clients/fetch/package.json index 1a051cc038..ae2c29cc60 100644 --- a/e2e/clients/fetch/package.json +++ b/e2e/clients/fetch/package.json @@ -17,6 +17,7 @@ "@x402/core": "workspace:*", "@x402/evm": "workspace:*", "@x402/fetch": "workspace:*", + "@x402/stellar": "workspace:*", "@x402/svm": "workspace:*", "axios": "^1.7.9", "dotenv": "^16.4.7", diff --git a/e2e/clients/fetch/test.config.json b/e2e/clients/fetch/test.config.json index 2c49b460d1..42b9be92a8 100644 --- a/e2e/clients/fetch/test.config.json +++ b/e2e/clients/fetch/test.config.json @@ -5,7 +5,8 @@ "protocolFamilies": [ "evm", "svm", - "aptos" + "aptos", + "stellar" ], "x402Versions": [ 1, @@ -14,7 +15,8 @@ "evm": { "transferMethods": [ "eip3009", - "permit2" + "permit2", + "upto" ] }, "extensions": [ @@ -29,7 +31,8 @@ "ENDPOINT_PATH" ], "optional": [ - "APTOS_PRIVATE_KEY" + "APTOS_PRIVATE_KEY", + "STELLAR_PRIVATE_KEY" ] } } diff --git a/e2e/clients/go-http/go.mod b/e2e/clients/go-http/go.mod index edae46db65..be39d8a56e 100644 --- a/e2e/clients/go-http/go.mod +++ b/e2e/clients/go-http/go.mod @@ -36,7 +36,7 @@ require ( github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/compress v1.16.0 // indirect github.com/logrusorgru/aurora v2.0.3+incompatible // indirect - github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mitchellh/go-testing-interface v1.14.1 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect @@ -54,11 +54,11 @@ require ( go.uber.org/multierr v1.6.0 // indirect go.uber.org/ratelimit v0.2.0 // indirect go.uber.org/zap v1.21.0 // indirect - golang.org/x/crypto v0.41.0 // indirect - golang.org/x/sync v0.16.0 // indirect - golang.org/x/sys v0.36.0 // indirect - golang.org/x/term v0.34.0 // indirect - golang.org/x/time v0.9.0 // indirect + golang.org/x/crypto v0.46.0 // indirect + golang.org/x/sync v0.19.0 // indirect + golang.org/x/sys v0.39.0 // indirect + golang.org/x/term v0.38.0 // indirect + golang.org/x/time v0.14.0 // indirect ) replace github.com/coinbase/x402/go => ../../../go diff --git a/e2e/clients/go-http/go.sum b/e2e/clients/go-http/go.sum index 8235c8df27..f02dcff073 100644 --- a/e2e/clients/go-http/go.sum +++ b/e2e/clients/go-http/go.sum @@ -137,9 +137,8 @@ github.com/leanovate/gopter v0.2.11 h1:vRjThO1EKPb/1NsDXuDrzldR28RLkBflWYcU9CvzW github.com/leanovate/gopter v0.2.11/go.mod h1:aK3tzZP/C+p1m3SPRE4SYZFGP7jjkuSI4f7Xvpt0S9c= github.com/logrusorgru/aurora v2.0.3+incompatible h1:tOpm7WcpBTn4fjmVfgpQq0EfczGlG91VSDkswnjF5A8= github.com/logrusorgru/aurora v2.0.3+incompatible/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4= -github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= -github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= -github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= +github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU= @@ -227,6 +226,12 @@ github.com/urfave/cli/v2 v2.27.5/go.mod h1:3Sevf16NykTbInEnD0yKkjDAeZDS0A6bzhBH5 github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4= github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM= +github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f h1:J9EGpcZtP0E/raorCMxlFGSTBrsSlaDGf3jU/qvAE2c= +github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= +github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0= +github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= +github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74= +github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4= github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM= github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA= @@ -253,8 +258,8 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20220214200702-86341886e292/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4= -golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc= +golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU= +golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0= golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df h1:UA2aFVmmsIlefxMk29Dp2juaUSth8Pyn3Tq5Y5mJGME= golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc= golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= @@ -267,13 +272,13 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE= -golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg= +golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU= +golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= -golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= +golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -284,27 +289,26 @@ golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k= -golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= +golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.34.0 h1:O/2T7POpk0ZZ7MAzMeWFSg6S5IpWd/RXDlM9hgM3DR4= -golang.org/x/term v0.34.0/go.mod h1:5jC53AEywhIVebHgPVeg0mj8OD3VO9OzclacVrqpaAw= +golang.org/x/term v0.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q= +golang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng= -golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU= -golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY= -golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU= +golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY= +golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= +golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= diff --git a/e2e/clients/go-http/main.go b/e2e/clients/go-http/main.go index c912626ddf..96ef06ab3f 100644 --- a/e2e/clients/go-http/main.go +++ b/e2e/clients/go-http/main.go @@ -12,8 +12,9 @@ import ( x402 "github.com/coinbase/x402/go" x402http "github.com/coinbase/x402/go/http" - evm "github.com/coinbase/x402/go/mechanisms/evm/exact/client" - evmv1 "github.com/coinbase/x402/go/mechanisms/evm/exact/v1/client" + exactevm "github.com/coinbase/x402/go/mechanisms/evm/exact/client" + exactevmv1 "github.com/coinbase/x402/go/mechanisms/evm/exact/v1/client" + uptoevm "github.com/coinbase/x402/go/mechanisms/evm/upto/client" svm "github.com/coinbase/x402/go/mechanisms/svm/exact/client" svmv1 "github.com/coinbase/x402/go/mechanisms/svm/exact/v1/client" evmsigners "github.com/coinbase/x402/go/signers/evm" @@ -74,14 +75,22 @@ func main() { return } - // Create x402 client with fluent API - // EIP-2612 gas sponsoring is handled internally by the EVM scheme - // when the server advertises support - no separate extension registration needed. + var evmConfig *exactevm.ExactEvmSchemeConfig + if evmRpcURL != "" { + evmConfig = &exactevm.ExactEvmSchemeConfig{RPCURL: evmRpcURL} + } + + var uptoConfig *uptoevm.UptoEvmSchemeConfig + if evmRpcURL != "" { + uptoConfig = &uptoevm.UptoEvmSchemeConfig{RPCURL: evmRpcURL} + } + x402Client := x402.Newx402Client(). - Register("eip155:*", evm.NewExactEvmScheme(evmSigner)). + Register("eip155:*", exactevm.NewExactEvmScheme(evmSigner, evmConfig)). + Register("eip155:*", uptoevm.NewUptoEvmScheme(evmSigner, uptoConfig)). Register("solana:*", svm.NewExactSvmScheme(svmSigner)). - RegisterV1("base-sepolia", evmv1.NewExactEvmSchemeV1(evmSigner)). - RegisterV1("base", evmv1.NewExactEvmSchemeV1(evmSigner)). + RegisterV1("base-sepolia", exactevmv1.NewExactEvmSchemeV1(evmSigner)). + RegisterV1("base", exactevmv1.NewExactEvmSchemeV1(evmSigner)). RegisterV1("solana-devnet", svmv1.NewExactSvmSchemeV1(svmSigner)). RegisterV1("solana", svmv1.NewExactSvmSchemeV1(svmSigner)) diff --git a/e2e/clients/go-http/test.config.json b/e2e/clients/go-http/test.config.json index 8c6b600592..07d5f8cb4d 100644 --- a/e2e/clients/go-http/test.config.json +++ b/e2e/clients/go-http/test.config.json @@ -10,10 +10,12 @@ 1, 2 ], + "schemes": ["exact", "upto"], "evm": { "transferMethods": [ "eip3009", - "permit2" + "permit2", + "upto" ] }, "extensions": [ diff --git a/e2e/clients/httpx/build.sh b/e2e/clients/httpx/build.sh index f5fbe5e8f8..c1bb071bbe 100755 --- a/e2e/clients/httpx/build.sh +++ b/e2e/clients/httpx/build.sh @@ -1,5 +1,5 @@ #!/bin/bash -# Python doesn't require a build step -# This file is intentionally empty -exit 0 +set -e +# Rebuild the local x402 editable dependency so the venv reflects source changes +uv sync --reinstall-package x402 diff --git a/e2e/clients/httpx/main.py b/e2e/clients/httpx/main.py index fdf976316a..424f0530ae 100644 --- a/e2e/clients/httpx/main.py +++ b/e2e/clients/httpx/main.py @@ -1,16 +1,20 @@ """httpx e2e test client using x402 v2 SDK.""" +import logging import os import json import asyncio from dotenv import load_dotenv from eth_account import Account -# Import from new x402 package +logging.basicConfig(level=logging.INFO, format="%(name)s %(levelname)s: %(message)s", stream=__import__('sys').stderr) +logging.getLogger("x402.signers").setLevel(logging.DEBUG) +logging.getLogger("x402.permit2").setLevel(logging.DEBUG) + from x402 import x402Client from x402.http import decode_payment_response_header from x402.http.clients import x402_httpx_transport -from x402.mechanisms.evm import EthAccountSigner +from x402.mechanisms.evm import EthAccountSignerWithRPC from x402.mechanisms.evm.exact import register_exact_evm_client from x402.mechanisms.svm import KeypairSigner from x402.mechanisms.svm.exact import register_exact_svm_client @@ -22,6 +26,7 @@ # Get environment variables evm_private_key = os.getenv("EVM_PRIVATE_KEY") svm_private_key = os.getenv("SVM_PRIVATE_KEY") +evm_rpc_url = os.getenv("EVM_RPC_URL", "https://sepolia.base.org") base_url = os.getenv("RESOURCE_SERVER_URL") endpoint_path = os.getenv("ENDPOINT_PATH") @@ -45,8 +50,8 @@ async def main(): # Register EVM exact scheme if private key is available if evm_private_key: - account = Account.from_key(evm_private_key) - evm_signer = EthAccountSigner(account) + evm_account = Account.from_key(evm_private_key) + evm_signer = EthAccountSignerWithRPC(evm_account, rpc_url=evm_rpc_url) register_exact_evm_client(client, evm_signer) # Register SVM exact scheme if private key is available diff --git a/e2e/clients/httpx/run.sh b/e2e/clients/httpx/run.sh index 31c1f93486..c653d856a9 100755 --- a/e2e/clients/httpx/run.sh +++ b/e2e/clients/httpx/run.sh @@ -1,4 +1,3 @@ #!/bin/bash -# Ensure dependencies are synced before running -uv sync --quiet +uv sync --reinstall-package x402 --quiet uv run python main.py diff --git a/e2e/clients/httpx/test.config.json b/e2e/clients/httpx/test.config.json index fcdeb61f96..26fecdf809 100644 --- a/e2e/clients/httpx/test.config.json +++ b/e2e/clients/httpx/test.config.json @@ -11,8 +11,12 @@ 2 ], "evm": { - "transferMethods": ["eip3009"] + "transferMethods": ["eip3009", "permit2"] }, + "extensions": [ + "eip2612GasSponsoring", + "erc20ApprovalGasSponsoring" + ], "description": "Python httpx client with x402 v2 payment hooks", "environment": { "required": [ @@ -21,7 +25,8 @@ ], "optional": [ "EVM_PRIVATE_KEY", - "SVM_PRIVATE_KEY" + "SVM_PRIVATE_KEY", + "EVM_RPC_URL" ] } -} \ No newline at end of file +} diff --git a/e2e/clients/httpx/uv.lock b/e2e/clients/httpx/uv.lock index 2121a54895..1d634e6156 100644 --- a/e2e/clients/httpx/uv.lock +++ b/e2e/clients/httpx/uv.lock @@ -1,5 +1,5 @@ version = 1 -revision = 3 +revision = 2 requires-python = ">=3.10" [[package]] @@ -1907,7 +1907,7 @@ wheels = [ [[package]] name = "x402" -version = "2.2.0" +version = "2.5.0" source = { editable = "../../../python/x402" } dependencies = [ { name = "nest-asyncio" }, @@ -1937,7 +1937,7 @@ svm = [ [package.metadata] requires-dist = [ { name = "eth-abi", marker = "extra == 'evm'", specifier = ">=5.0.0" }, - { name = "eth-account", marker = "extra == 'evm'", specifier = ">=0.12.0" }, + { name = "eth-account", marker = "extra == 'evm'", specifier = ">=0.13.0" }, { name = "eth-keys", marker = "extra == 'evm'", specifier = ">=0.5.0" }, { name = "eth-utils", marker = "extra == 'evm'", specifier = ">=4.0.0" }, { name = "fastapi", extras = ["standard"], marker = "extra == 'fastapi'", specifier = ">=0.115.0" }, @@ -1964,7 +1964,7 @@ provides-extras = ["httpx", "requests", "flask", "fastapi", "evm", "svm", "mcp", dev = [ { name = "black", specifier = ">=23.0.0" }, { name = "eth-abi", specifier = ">=5.0.0" }, - { name = "eth-account", specifier = ">=0.12.0" }, + { name = "eth-account", specifier = ">=0.13.0" }, { name = "eth-keys", specifier = ">=0.5.0" }, { name = "eth-utils", specifier = ">=4.0.0" }, { name = "fastapi", extras = ["standard"], specifier = ">=0.115.0" }, diff --git a/e2e/clients/mcp-go/go.mod b/e2e/clients/mcp-go/go.mod index c439a05d14..3899ac9b0c 100644 --- a/e2e/clients/mcp-go/go.mod +++ b/e2e/clients/mcp-go/go.mod @@ -10,23 +10,31 @@ require ( ) require ( + github.com/Microsoft/go-winio v0.6.2 // indirect github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20251001021608-1fe7b43fc4d6 // indirect + github.com/StackExchange/wmi v1.2.1 // indirect github.com/bits-and-blooms/bitset v1.20.0 // indirect github.com/consensys/gnark-crypto v0.18.0 // indirect github.com/crate-crypto/go-eth-kzg v1.4.0 // indirect github.com/crate-crypto/go-ipa v0.0.0-20240724233137-53bbb0ceb27a // indirect + github.com/deckarep/golang-set/v2 v2.6.0 // indirect github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 // indirect github.com/ethereum/c-kzg-4844/v2 v2.1.5 // indirect github.com/ethereum/go-ethereum v1.16.7 // indirect github.com/ethereum/go-verkle v0.2.2 // indirect + github.com/go-ole/go-ole v1.3.0 // indirect github.com/google/jsonschema-go v0.4.2 // indirect + github.com/gorilla/websocket v1.4.2 // indirect github.com/holiman/uint256 v1.3.2 // indirect + github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible // indirect github.com/supranational/blst v0.3.16-0.20250831170142-f48500c1fdbe // indirect + github.com/tklauser/go-sysconf v0.3.12 // indirect + github.com/tklauser/numcpus v0.6.1 // indirect github.com/yosida95/uritemplate/v3 v3.0.2 // indirect - golang.org/x/crypto v0.41.0 // indirect + golang.org/x/crypto v0.46.0 // indirect golang.org/x/oauth2 v0.30.0 // indirect - golang.org/x/sync v0.16.0 // indirect - golang.org/x/sys v0.36.0 // indirect + golang.org/x/sync v0.19.0 // indirect + golang.org/x/sys v0.39.0 // indirect ) replace github.com/coinbase/x402/go => ../../../go diff --git a/e2e/clients/mcp-go/go.sum b/e2e/clients/mcp-go/go.sum index f897f19fa1..917e66bca7 100644 --- a/e2e/clients/mcp-go/go.sum +++ b/e2e/clients/mcp-go/go.sum @@ -1,17 +1,45 @@ +github.com/DataDog/zstd v1.4.5 h1:EndNeuB0l9syBZhut0wns3gV1hL8zX8LIu6ZiVHWLIQ= +github.com/DataDog/zstd v1.4.5/go.mod h1:1jcaCB/ufaK+sKp1NBhlGmpz41jOoPQ35bpF36t7BBo= +github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= +github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20251001021608-1fe7b43fc4d6 h1:1zYrtlhrZ6/b6SAjLSfKzWtdgqK0U+HtH/VcBWh1BaU= github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20251001021608-1fe7b43fc4d6/go.mod h1:ioLG6R+5bUSO1oeGSDxOV3FADARuMoytZCSX6MEMQkI= github.com/StackExchange/wmi v1.2.1 h1:VIkavFPXSjcnS+O8yTq7NI32k0R5Aj+v39y29VYDOSA= github.com/StackExchange/wmi v1.2.1/go.mod h1:rcmrprowKIVzvc+NUiLncP2uuArMWLCbu9SBzvHz7e8= +github.com/VictoriaMetrics/fastcache v1.13.0 h1:AW4mheMR5Vd9FkAPUv+NH6Nhw+fmbTMGMsNAoA/+4G0= +github.com/VictoriaMetrics/fastcache v1.13.0/go.mod h1:hHXhl4DA2fTL2HTZDJFXWgW0LNjo6B+4aj2Wmng3TjU= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/bits-and-blooms/bitset v1.20.0 h1:2F+rfL86jE2d/bmw7OhqUg2Sj/1rURkBn3MdfoPyRVU= github.com/bits-and-blooms/bitset v1.20.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cockroachdb/errors v1.11.3 h1:5bA+k2Y6r+oz/6Z/RFlNeVCesGARKuC6YymtcDrbC/I= +github.com/cockroachdb/errors v1.11.3/go.mod h1:m4UIW4CDjx+R5cybPsNrRbreomiFqt8o1h1wUVazSd8= +github.com/cockroachdb/fifo v0.0.0-20240606204812-0bbfbd93a7ce h1:giXvy4KSc/6g/esnpM7Geqxka4WSqI1SZc7sMJFd3y4= +github.com/cockroachdb/fifo v0.0.0-20240606204812-0bbfbd93a7ce/go.mod h1:9/y3cnZ5GKakj/H4y9r9GTjCvAFta7KLgSHPJJYc52M= +github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b h1:r6VH0faHjZeQy818SGhaone5OnYfxFR/+AzdY3sf5aE= +github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b/go.mod h1:Vz9DsVWQQhf3vs21MhPMZpMGSht7O/2vFW2xusFUVOs= +github.com/cockroachdb/pebble v1.1.5 h1:5AAWCBWbat0uE0blr8qzufZP5tBjkRyy/jWe1QWLnvw= +github.com/cockroachdb/pebble v1.1.5/go.mod h1:17wO9el1YEigxkP/YtV8NtCivQDgoCyBg5c4VR/eOWo= +github.com/cockroachdb/redact v1.1.5 h1:u1PMllDkdFfPWaNGMyLD1+so+aq3uUItthCFqzwPJ30= +github.com/cockroachdb/redact v1.1.5/go.mod h1:BVNblN9mBWFyMyqK1k3AAiSxhvhfK2oOZZ2lK+dpvRg= +github.com/cockroachdb/tokenbucket v0.0.0-20230807174530-cc333fc44b06 h1:zuQyyAKVxetITBuuhv3BI9cMrmStnpT18zmgmTxunpo= +github.com/cockroachdb/tokenbucket v0.0.0-20230807174530-cc333fc44b06/go.mod h1:7nc4anLGjupUW/PeY5qiNYsdNXj7zopG+eqsS7To5IQ= github.com/consensys/gnark-crypto v0.18.0 h1:vIye/FqI50VeAr0B3dx+YjeIvmc3LWz4yEfbWBpTUf0= github.com/consensys/gnark-crypto v0.18.0/go.mod h1:L3mXGFTe1ZN+RSJ+CLjUt9x7PNdx8ubaYfDROyp2Z8c= +github.com/cpuguy83/go-md2man/v2 v2.0.5 h1:ZtcqGrnekaHpVLArFSe4HK5DoKx1T0rq2DwVB0alcyc= +github.com/cpuguy83/go-md2man/v2 v2.0.5/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/crate-crypto/go-eth-kzg v1.4.0 h1:WzDGjHk4gFg6YzV0rJOAsTK4z3Qkz5jd4RE3DAvPFkg= github.com/crate-crypto/go-eth-kzg v1.4.0/go.mod h1:J9/u5sWfznSObptgfa92Jq8rTswn6ahQWEuiLHOjCUI= github.com/crate-crypto/go-ipa v0.0.0-20240724233137-53bbb0ceb27a h1:W8mUrRp6NOVl3J+MYp5kPMoUZPp7aOYHtaua31lwRHg= github.com/crate-crypto/go-ipa v0.0.0-20240724233137-53bbb0ceb27a/go.mod h1:sTwzHBvIzm2RfVCGNEBZgRyjwK40bVoun3ZnGOCafNM= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dchest/siphash v1.2.3 h1:QXwFc8cFOR2dSa/gE6o/HokBMWtLUaNDVd+22aKHeEA= +github.com/dchest/siphash v1.2.3/go.mod h1:0NvQU092bT0ipiFN++/rXm69QG9tVxLAlQHIXMPAkHc= +github.com/deckarep/golang-set/v2 v2.6.0 h1:XfcQbWM1LlMB8BsJ8N9vW5ehnnPVIw0je80NsVHagjM= +github.com/deckarep/golang-set/v2 v2.6.0/go.mod h1:VAky9rY/yGXJOLEDv3OMci+7wtDpOF4IN+y82NBOac4= github.com/decred/dcrd/crypto/blake256 v1.0.0 h1:/8DMNYp9SGi5f0w7uCm6d6M4OU2rGFK09Y2A4Xv7EE0= github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 h1:YLtO71vCjJRCBcrPMtQ9nqBsqpA1m5sE92cU+pd5Mcc= @@ -20,18 +48,29 @@ github.com/emicklei/dot v1.6.2 h1:08GN+DD79cy/tzN6uLCT84+2Wk9u+wvqP+Hkx/dIR8A= github.com/emicklei/dot v1.6.2/go.mod h1:DeV7GvQtIw4h2u73RKBkkFdvVAz0D9fzeJrgPW6gy/s= github.com/ethereum/c-kzg-4844/v2 v2.1.5 h1:aVtoLK5xwJ6c5RiqO8g8ptJ5KU+2Hdquf6G3aXiHh5s= github.com/ethereum/c-kzg-4844/v2 v2.1.5/go.mod h1:u59hRTTah4Co6i9fDWtiCjTrblJv0UwsqZKCc0GfgUs= +github.com/ethereum/go-bigmodexpfix v0.0.0-20250911101455-f9e208c548ab h1:rvv6MJhy07IMfEKuARQ9TKojGqLVNxQajaXEp/BoqSk= +github.com/ethereum/go-bigmodexpfix v0.0.0-20250911101455-f9e208c548ab/go.mod h1:IuLm4IsPipXKF7CW5Lzf68PIbZ5yl7FFd74l/E0o9A8= github.com/ethereum/go-ethereum v1.16.7 h1:qeM4TvbrWK0UC0tgkZ7NiRsmBGwsjqc64BHo20U59UQ= github.com/ethereum/go-ethereum v1.16.7/go.mod h1:Fs6QebQbavneQTYcA39PEKv2+zIjX7rPUZ14DER46wk= github.com/ethereum/go-verkle v0.2.2 h1:I2W0WjnrFUIzzVPwm8ykY+7pL2d4VhlsePn4j7cnFk8= github.com/ethereum/go-verkle v0.2.2/go.mod h1:M3b90YRnzqKyyzBEWJGqj8Qff4IDeXnzFw0P9bFw3uk= github.com/ferranbt/fastssz v0.1.4 h1:OCDB+dYDEQDvAgtAGnTSidK1Pe2tW3nFV40XyMkTeDY= github.com/ferranbt/fastssz v0.1.4/go.mod h1:Ea3+oeoRGGLGm5shYAeDgu6PGUlcvQhE2fILyD9+tGg= +github.com/getsentry/sentry-go v0.27.0 h1:Pv98CIbtB3LkMWmXi4Joa5OOcwbmnX88sF5qbK3r3Ps= +github.com/getsentry/sentry-go v0.27.0/go.mod h1:lc76E2QywIyW8WuBnwl8Lc4bkmQH4+w1gwTf25trprY= +github.com/go-ole/go-ole v1.2.5/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= github.com/gofrs/flock v0.12.1 h1:MTLVXXHf8ekldpJk3AKicLij9MdwOWkZ+a/jHHZby9E= github.com/gofrs/flock v0.12.1/go.mod h1:9zxTsyu5xtJ9DK+1tFZyibEV7y3uwDxPPfbxeeHCoD0= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI= +github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8= github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs= github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= @@ -40,50 +79,122 @@ github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/jsonschema-go v0.4.2 h1:tmrUohrwoLZZS/P3x7ex0WAVknEkBZM46iALbcqoRA8= github.com/google/jsonschema-go v0.4.2/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE= +github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= +github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/hashicorp/go-bexpr v0.1.10 h1:9kuI5PFotCboP3dkDYFr/wi0gg0QVbSNz5oFRpxn4uE= +github.com/hashicorp/go-bexpr v0.1.10/go.mod h1:oxlubA2vC/gFVfX1A6JGp7ls7uCDlfJn732ehYYg+g0= +github.com/holiman/billy v0.0.0-20250707135307-f2f9b9aae7db h1:IZUYC/xb3giYwBLMnr8d0TGTzPKFGNTCGgGLoyeX330= +github.com/holiman/billy v0.0.0-20250707135307-f2f9b9aae7db/go.mod h1:xTEYN9KCHxuYHs+NmrmzFcnvHMzLLNiGFafCb1n3Mfg= +github.com/holiman/bloomfilter/v2 v2.0.3 h1:73e0e/V0tCydx14a0SCYS/EWCxgwLZ18CZcZKVu0fao= +github.com/holiman/bloomfilter/v2 v2.0.3/go.mod h1:zpoh+gs7qcpqrHr3dB55AMiJwo0iURXE7ZOP9L9hSkA= github.com/holiman/uint256 v1.3.2 h1:a9EgMPSC1AAaj1SZL5zIQD3WbwTuHrMGOerLjGmM/TA= github.com/holiman/uint256 v1.3.2/go.mod h1:EOMSn4q6Nyt9P6efbI3bueV4e1b3dGlUCXeiRV4ng7E= +github.com/huin/goupnp v1.3.0 h1:UvLUlWDNpoUdYzb2TCn+MuTWtcjXKSza2n6CBdQ0xXc= +github.com/huin/goupnp v1.3.0/go.mod h1:gnGPsThkYa7bFi/KWmEysQRf48l2dvR5bxr2OFckNX8= +github.com/jackpal/go-nat-pmp v1.0.2 h1:KzKSgb7qkJvOUTqYl9/Hg/me3pWgBmERKrTGD7BdWus= +github.com/jackpal/go-nat-pmp v1.0.2/go.mod h1:QPH045xvCAeXUZOxsnwmrtiCoxIr9eob+4orBN1SBKc= +github.com/klauspost/compress v1.16.0 h1:iULayQNOReoYUe+1qtKOqw9CwJv3aNQu8ivo7lw1HU4= +github.com/klauspost/compress v1.16.0/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/leanovate/gopter v0.2.11 h1:vRjThO1EKPb/1NsDXuDrzldR28RLkBflWYcU9CvzWu4= github.com/leanovate/gopter v0.2.11/go.mod h1:aK3tzZP/C+p1m3SPRE4SYZFGP7jjkuSI4f7Xvpt0S9c= +github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= +github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU= github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo= +github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= github.com/minio/sha256-simd v1.0.0 h1:v1ta+49hkWZyvaKwrQB8elexRqm6Y0aMLjCNsrYxo6g= github.com/minio/sha256-simd v1.0.0/go.mod h1:OuYzVNI5vcoYIAmbIvHPl3N3jUzVedXbKy5RFepssQM= github.com/mitchellh/mapstructure v1.4.1 h1:CpVNEelQCZBooIPDn+AR3NpivK/TIKU8bDxdASFVQag= github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/mitchellh/pointerstructure v1.2.0 h1:O+i9nHnXS3l/9Wu7r4NrEdwA2VFTicjUEN1uBnDo34A= +github.com/mitchellh/pointerstructure v1.2.0/go.mod h1:BRAsLI5zgXmw97Lf6s25bs8ohIXc3tViBH44KcwB2g4= github.com/modelcontextprotocol/go-sdk v1.3.0 h1:gMfZkv3DzQF5q/DcQePo5rahEY+sguyPfXDfNBcT0Zs= github.com/modelcontextprotocol/go-sdk v1.3.0/go.mod h1:AnQ//Qc6+4nIyyrB4cxBU7UW9VibK4iOZBeyP/rF1IE= github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= +github.com/pion/dtls/v2 v2.2.7 h1:cSUBsETxepsCSFSxC3mc/aDo14qQLMSL+O6IjG28yV8= +github.com/pion/dtls/v2 v2.2.7/go.mod h1:8WiMkebSHFD0T+dIU+UeBaoV7kDhOW5oDCzZ7WZ/F9s= +github.com/pion/logging v0.2.2 h1:M9+AIj/+pxNsDfAT64+MAVgJO0rsyLnoJKCqf//DoeY= +github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms= +github.com/pion/stun/v2 v2.0.0 h1:A5+wXKLAypxQri59+tmQKVs7+l6mMM+3d+eER9ifRU0= +github.com/pion/stun/v2 v2.0.0/go.mod h1:22qRSh08fSEttYUmJZGlriq9+03jtVmXNODgLccj8GQ= +github.com/pion/transport/v2 v2.2.1 h1:7qYnCBlpgSJNYMbLCKuSY9KbQdBFoETvPNETv0y4N7c= +github.com/pion/transport/v2 v2.2.1/go.mod h1:cXXWavvCnFF6McHTft3DWS9iic2Mftcz1Aq29pGcU5g= +github.com/pion/transport/v3 v3.0.1 h1:gDTlPJwROfSfz6QfSi0ZmeCSkFcnWWiiR9ES0ouANiM= +github.com/pion/transport/v3 v3.0.1/go.mod h1:UY7kiITrlMv7/IKgd5eTUcaahZx5oUN3l9SzK5f5xE0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v1.15.0 h1:5fCgGYogn0hFdhyhLbw7hEsWxufKtY9klyvdNfFlFhM= +github.com/prometheus/client_golang v1.15.0/go.mod h1:e9yaBhRPU2pPNsZwE+JdQl0KEt1N9XgF6zxWmaC0xOk= +github.com/prometheus/client_model v0.3.0 h1:UBgGFHqYdG/TPFD1B1ogZywDqEkwp3fBMvqdiQ7Xew4= +github.com/prometheus/client_model v0.3.0/go.mod h1:LDGWKZIo7rky3hgvBe+caln+Dr3dPggB5dvjtD7w9+w= +github.com/prometheus/common v0.42.0 h1:EKsfXEYo4JpWMHH5cg+KOUWeuJSov1Id8zGR8eeI1YM= +github.com/prometheus/common v0.42.0/go.mod h1:xBwqVerjNdUDjgODMpudtOMwlOwf2SaTr1yjz4b7Zbc= +github.com/prometheus/procfs v0.9.0 h1:wzCHvIvM5SxWqYvwgVL7yJY8Lz3PKn49KQtpgMYJfhI= +github.com/prometheus/procfs v0.9.0/go.mod h1:+pB4zwohETzFnmlpe6yd2lSc+0/46IYZRB/chUwxUZY= github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= +github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= +github.com/rs/cors v1.7.0 h1:+88SsELBHx5r+hZ8TCkggzSstaWNbDvThkVK8H6f9ik= +github.com/rs/cors v1.7.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU= +github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible h1:Bn1aCHHRnjv4Bl16T8rcaFjYSrGrIZvpiGO6P3Q4GpU= github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/supranational/blst v0.3.16-0.20250831170142-f48500c1fdbe h1:nbdqkIGOGfUAD54q1s2YBcBz/WcsxCO9HUQ4aGV5hUw= github.com/supranational/blst v0.3.16-0.20250831170142-f48500c1fdbe/go.mod h1:jZJtfjgudtNl4en1tzwPIV3KjUnQUvG3/j+w+fVonLw= +github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 h1:epCh84lMvA70Z7CTTCmYQn2CKbY8j86K7/FAIr141uY= +github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45IWZaF22tdD+VEXcAWRA037jwmWEB5VWYORlTpc= github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU= github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI= github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk= github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY= +github.com/urfave/cli/v2 v2.27.5 h1:WoHEJLdsXr6dDWoJgMq/CboDmyY/8HMMH1fTECbih+w= +github.com/urfave/cli/v2 v2.27.5/go.mod h1:3Sevf16NykTbInEnD0yKkjDAeZDS0A6bzhBH5hrMvTQ= +github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4= +github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM= github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4= github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4= -golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4= -golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc= +golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU= +golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0= +golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df h1:UA2aFVmmsIlefxMk29Dp2juaUSth8Pyn3Tq5Y5mJGME= +golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc= golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI= golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU= -golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= -golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= -golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k= -golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= -golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg= -golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s= +golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= +golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= +golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU= +golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY= +golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= +golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= +golang.org/x/tools v0.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ= +golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ= +google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw= +google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= +gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc= +gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/e2e/clients/mcp-go/main.go b/e2e/clients/mcp-go/main.go index 52cd2a41b2..4befd7e31d 100644 --- a/e2e/clients/mcp-go/main.go +++ b/e2e/clients/mcp-go/main.go @@ -69,9 +69,13 @@ func main() { } defer session.Close() - // Use X402MCPClient - payment is transparent in CallTool + var evmConfig *evm.ExactEvmSchemeConfig + if rpcURL := os.Getenv("EVM_RPC_URL"); rpcURL != "" { + evmConfig = &evm.ExactEvmSchemeConfig{RPCURL: rpcURL} + } + paymentClient := x402.Newx402Client() - paymentClient.Register("eip155:*", evm.NewExactEvmScheme(evmSigner)) + paymentClient.Register("eip155:*", evm.NewExactEvmScheme(evmSigner, evmConfig)) x402Mcp := mcp402.NewX402MCPClient(session, paymentClient, mcp402.Options{AutoPayment: mcp402.BoolPtr(true)}) result, err := x402Mcp.CallTool(ctx, endpointPath, map[string]any{ diff --git a/e2e/clients/mcp-python/uv.lock b/e2e/clients/mcp-python/uv.lock index a1964f57e4..d4768a6ad5 100644 --- a/e2e/clients/mcp-python/uv.lock +++ b/e2e/clients/mcp-python/uv.lock @@ -1,5 +1,5 @@ version = 1 -revision = 3 +revision = 2 requires-python = ">=3.10" [[package]] @@ -2137,7 +2137,7 @@ wheels = [ [[package]] name = "x402" -version = "2.1.0" +version = "2.5.0" source = { editable = "../../../python/x402" } dependencies = [ { name = "nest-asyncio" }, @@ -2160,7 +2160,7 @@ mcp = [ [package.metadata] requires-dist = [ { name = "eth-abi", marker = "extra == 'evm'", specifier = ">=5.0.0" }, - { name = "eth-account", marker = "extra == 'evm'", specifier = ">=0.12.0" }, + { name = "eth-account", marker = "extra == 'evm'", specifier = ">=0.13.0" }, { name = "eth-keys", marker = "extra == 'evm'", specifier = ">=0.5.0" }, { name = "eth-utils", marker = "extra == 'evm'", specifier = ">=4.0.0" }, { name = "fastapi", extras = ["standard"], marker = "extra == 'fastapi'", specifier = ">=0.115.0" }, @@ -2187,7 +2187,7 @@ provides-extras = ["httpx", "requests", "flask", "fastapi", "evm", "svm", "mcp", dev = [ { name = "black", specifier = ">=23.0.0" }, { name = "eth-abi", specifier = ">=5.0.0" }, - { name = "eth-account", specifier = ">=0.12.0" }, + { name = "eth-account", specifier = ">=0.13.0" }, { name = "eth-keys", specifier = ">=0.5.0" }, { name = "eth-utils", specifier = ">=4.0.0" }, { name = "fastapi", extras = ["standard"], specifier = ">=0.115.0" }, diff --git a/e2e/clients/mcp-typescript/index.ts b/e2e/clients/mcp-typescript/index.ts index 10d2d2c35b..9029b796a5 100644 --- a/e2e/clients/mcp-typescript/index.ts +++ b/e2e/clients/mcp-typescript/index.ts @@ -8,7 +8,7 @@ */ import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js"; -import { ExactEvmScheme } from "@x402/evm/exact/client"; +import { ExactEvmScheme, type ExactEvmSchemeOptions } from "@x402/evm/exact/client"; import { createx402MCPClient } from "@x402/mcp"; import { privateKeyToAccount } from "viem/accounts"; @@ -35,12 +35,14 @@ if (!serverUrl || !endpointPath || !evmPrivateKey) { async function main(): Promise { const evmSigner = privateKeyToAccount(evmPrivateKey); + const evmSchemeOptions: ExactEvmSchemeOptions | undefined = process.env.EVM_RPC_URL + ? { rpcUrl: process.env.EVM_RPC_URL } + : undefined; - // Create x402 MCP client with auto-payment enabled const x402Mcp = createx402MCPClient({ name: "x402-mcp-e2e-client", version: "1.0.0", - schemes: [{ network: "eip155:84532", client: new ExactEvmScheme(evmSigner) }], + schemes: [{ network: "eip155:84532", client: new ExactEvmScheme(evmSigner, evmSchemeOptions) }], autoPayment: true, onPaymentRequested: async () => true, // Auto-approve all payments for e2e }); diff --git a/e2e/clients/requests/build.sh b/e2e/clients/requests/build.sh index f5fbe5e8f8..c1bb071bbe 100755 --- a/e2e/clients/requests/build.sh +++ b/e2e/clients/requests/build.sh @@ -1,5 +1,5 @@ #!/bin/bash -# Python doesn't require a build step -# This file is intentionally empty -exit 0 +set -e +# Rebuild the local x402 editable dependency so the venv reflects source changes +uv sync --reinstall-package x402 diff --git a/e2e/clients/requests/main.py b/e2e/clients/requests/main.py index e1bcef0b6b..c5eb3b291d 100644 --- a/e2e/clients/requests/main.py +++ b/e2e/clients/requests/main.py @@ -5,11 +5,10 @@ from dotenv import load_dotenv from eth_account import Account -# Import from new x402 package (sync variant for requests) from x402 import x402ClientSync from x402.http import decode_payment_response_header from x402.http.clients import x402_requests -from x402.mechanisms.evm import EthAccountSigner +from x402.mechanisms.evm import EthAccountSignerWithRPC from x402.mechanisms.evm.exact import register_exact_evm_client from x402.mechanisms.svm import KeypairSigner from x402.mechanisms.svm.exact import register_exact_svm_client @@ -20,6 +19,7 @@ # Get environment variables evm_private_key = os.getenv("EVM_PRIVATE_KEY") svm_private_key = os.getenv("SVM_PRIVATE_KEY") +evm_rpc_url = os.getenv("EVM_RPC_URL", "https://sepolia.base.org") base_url = os.getenv("RESOURCE_SERVER_URL") endpoint_path = os.getenv("ENDPOINT_PATH") @@ -43,8 +43,8 @@ def main(): # Register EVM exact scheme if private key is available if evm_private_key: - account = Account.from_key(evm_private_key) - evm_signer = EthAccountSigner(account) + evm_account = Account.from_key(evm_private_key) + evm_signer = EthAccountSignerWithRPC(evm_account, rpc_url=evm_rpc_url) register_exact_evm_client(client, evm_signer) # Register SVM exact scheme if private key is available diff --git a/e2e/clients/requests/run.sh b/e2e/clients/requests/run.sh index 31c1f93486..c653d856a9 100644 --- a/e2e/clients/requests/run.sh +++ b/e2e/clients/requests/run.sh @@ -1,4 +1,3 @@ #!/bin/bash -# Ensure dependencies are synced before running -uv sync --quiet +uv sync --reinstall-package x402 --quiet uv run python main.py diff --git a/e2e/clients/requests/test.config.json b/e2e/clients/requests/test.config.json index 009e3eb0d1..783ba8467f 100644 --- a/e2e/clients/requests/test.config.json +++ b/e2e/clients/requests/test.config.json @@ -11,8 +11,12 @@ 2 ], "evm": { - "transferMethods": ["eip3009"] + "transferMethods": ["eip3009", "permit2"] }, + "extensions": [ + "eip2612GasSponsoring", + "erc20ApprovalGasSponsoring" + ], "description": "Python requests client with x402 v2 HTTP adapter", "environment": { "required": [ @@ -21,7 +25,8 @@ ], "optional": [ "EVM_PRIVATE_KEY", - "SVM_PRIVATE_KEY" + "SVM_PRIVATE_KEY", + "EVM_RPC_URL" ] } -} \ No newline at end of file +} diff --git a/e2e/clients/requests/uv.lock b/e2e/clients/requests/uv.lock index 11e3e49ebb..3fa9f0e1c2 100644 --- a/e2e/clients/requests/uv.lock +++ b/e2e/clients/requests/uv.lock @@ -1,5 +1,5 @@ version = 1 -revision = 3 +revision = 2 requires-python = ">=3.10" [[package]] @@ -1907,7 +1907,7 @@ wheels = [ [[package]] name = "x402" -version = "2.2.0" +version = "2.5.0" source = { editable = "../../../python/x402" } dependencies = [ { name = "nest-asyncio" }, @@ -1937,7 +1937,7 @@ svm = [ [package.metadata] requires-dist = [ { name = "eth-abi", marker = "extra == 'evm'", specifier = ">=5.0.0" }, - { name = "eth-account", marker = "extra == 'evm'", specifier = ">=0.12.0" }, + { name = "eth-account", marker = "extra == 'evm'", specifier = ">=0.13.0" }, { name = "eth-keys", marker = "extra == 'evm'", specifier = ">=0.5.0" }, { name = "eth-utils", marker = "extra == 'evm'", specifier = ">=4.0.0" }, { name = "fastapi", extras = ["standard"], marker = "extra == 'fastapi'", specifier = ">=0.115.0" }, @@ -1964,7 +1964,7 @@ provides-extras = ["httpx", "requests", "flask", "fastapi", "evm", "svm", "mcp", dev = [ { name = "black", specifier = ">=23.0.0" }, { name = "eth-abi", specifier = ">=5.0.0" }, - { name = "eth-account", specifier = ">=0.12.0" }, + { name = "eth-account", specifier = ">=0.13.0" }, { name = "eth-keys", specifier = ">=0.5.0" }, { name = "eth-utils", specifier = ">=4.0.0" }, { name = "fastapi", extras = ["standard"], specifier = ">=0.115.0" }, diff --git a/e2e/facilitators/go/bazaar.go b/e2e/facilitators/go/bazaar.go index a0b4313ee2..45d9e762b7 100644 --- a/e2e/facilitators/go/bazaar.go +++ b/e2e/facilitators/go/bazaar.go @@ -15,6 +15,7 @@ type DiscoveredResource struct { X402Version int `json:"x402Version"` Accepts []x402.PaymentRequirements `json:"accepts"` DiscoveryInfo *exttypes.DiscoveryInfo `json:"discoveryInfo,omitempty"` + RouteTemplate string `json:"routeTemplate,omitempty"` LastUpdated string `json:"lastUpdated"` Metadata map[string]interface{} `json:"metadata,omitempty"` } @@ -37,10 +38,14 @@ func (c *BazaarCatalog) CatalogResource( x402Version int, discoveryInfo *exttypes.DiscoveryInfo, paymentRequirements x402.PaymentRequirements, + routeTemplate string, ) { log.Printf("📝 Discovered resource: %s", resourceURL) log.Printf(" Method: %s", method) log.Printf(" x402 Version: %d", x402Version) + if routeTemplate != "" { + log.Printf(" Route template: %s", routeTemplate) + } c.mutex.Lock() defer c.mutex.Unlock() @@ -51,6 +56,7 @@ func (c *BazaarCatalog) CatalogResource( X402Version: x402Version, Accepts: []x402.PaymentRequirements{paymentRequirements}, DiscoveryInfo: discoveryInfo, + RouteTemplate: routeTemplate, LastUpdated: time.Now().Format(time.RFC3339), Metadata: make(map[string]interface{}), } diff --git a/e2e/facilitators/go/go.mod b/e2e/facilitators/go/go.mod index 889ac08171..439612494a 100644 --- a/e2e/facilitators/go/go.mod +++ b/e2e/facilitators/go/go.mod @@ -49,7 +49,7 @@ require ( github.com/klauspost/cpuid/v2 v2.3.0 // indirect github.com/leodido/go-urn v1.4.0 // indirect github.com/logrusorgru/aurora v2.0.3+incompatible // indirect - github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mitchellh/go-testing-interface v1.14.1 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect @@ -75,16 +75,16 @@ require ( go.uber.org/ratelimit v0.2.0 // indirect go.uber.org/zap v1.21.0 // indirect golang.org/x/arch v0.20.0 // indirect - golang.org/x/crypto v0.41.0 // indirect + golang.org/x/crypto v0.46.0 // indirect golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect - golang.org/x/mod v0.27.0 // indirect - golang.org/x/net v0.43.0 // indirect - golang.org/x/sync v0.16.0 // indirect - golang.org/x/sys v0.36.0 // indirect - golang.org/x/term v0.34.0 // indirect - golang.org/x/text v0.28.0 // indirect - golang.org/x/time v0.9.0 // indirect - golang.org/x/tools v0.36.0 // indirect + golang.org/x/mod v0.30.0 // indirect + golang.org/x/net v0.48.0 // indirect + golang.org/x/sync v0.19.0 // indirect + golang.org/x/sys v0.39.0 // indirect + golang.org/x/term v0.38.0 // indirect + golang.org/x/text v0.32.0 // indirect + golang.org/x/time v0.14.0 // indirect + golang.org/x/tools v0.39.0 // indirect google.golang.org/protobuf v1.36.9 // indirect ) diff --git a/e2e/facilitators/go/go.sum b/e2e/facilitators/go/go.sum index da343d1029..0fca838fd6 100644 --- a/e2e/facilitators/go/go.sum +++ b/e2e/facilitators/go/go.sum @@ -161,9 +161,8 @@ github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= github.com/logrusorgru/aurora v2.0.3+incompatible h1:tOpm7WcpBTn4fjmVfgpQq0EfczGlG91VSDkswnjF5A8= github.com/logrusorgru/aurora v2.0.3+incompatible/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4= -github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= -github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= -github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= +github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU= @@ -299,15 +298,15 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20220214200702-86341886e292/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4= -golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc= +golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU= +golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0= golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g= golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k= golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ= -golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc= +golang.org/x/mod v0.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk= +golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= @@ -315,13 +314,13 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE= -golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg= +golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU= +golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= -golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= +golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -332,34 +331,33 @@ golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k= -golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= +golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.34.0 h1:O/2T7POpk0ZZ7MAzMeWFSg6S5IpWd/RXDlM9hgM3DR4= -golang.org/x/term v0.34.0/go.mod h1:5jC53AEywhIVebHgPVeg0mj8OD3VO9OzclacVrqpaAw= +golang.org/x/term v0.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q= +golang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng= -golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU= -golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY= -golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU= +golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY= +golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= +golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg= -golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s= +golang.org/x/tools v0.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ= +golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/e2e/facilitators/go/main.go b/e2e/facilitators/go/main.go index 99ed985563..b9d7ad2f59 100644 --- a/e2e/facilitators/go/main.go +++ b/e2e/facilitators/go/main.go @@ -22,8 +22,9 @@ import ( "github.com/coinbase/x402/go/extensions/erc20approvalgassponsor" exttypes "github.com/coinbase/x402/go/extensions/types" evmmech "github.com/coinbase/x402/go/mechanisms/evm" - evm "github.com/coinbase/x402/go/mechanisms/evm/exact/facilitator" - evmv1 "github.com/coinbase/x402/go/mechanisms/evm/exact/v1/facilitator" + exactevm "github.com/coinbase/x402/go/mechanisms/evm/exact/facilitator" + exactevmv1 "github.com/coinbase/x402/go/mechanisms/evm/exact/v1/facilitator" + uptoevm "github.com/coinbase/x402/go/mechanisms/evm/upto/facilitator" svmmech "github.com/coinbase/x402/go/mechanisms/svm" svm "github.com/coinbase/x402/go/mechanisms/svm/exact/facilitator" svmv1 "github.com/coinbase/x402/go/mechanisms/svm/exact/v1/facilitator" @@ -42,11 +43,6 @@ import ( "github.com/gin-gonic/gin" ) -// NOTE: Facilitator signer helpers (go/signers/evm and go/signers/svm) are not yet implemented. -// When available, this will reduce 300+ lines of facilitator signer code to just a few lines. -// For now, facilitator signers still require manual implementation. -// See PROPOSAL_SIGNER_HELPERS.md for the planned facilitator signer helpers. - const ( DefaultPort = "4022" ) @@ -212,56 +208,37 @@ func (s *realFacilitatorEvmSigner) ReadContract( return nil, fmt.Errorf("failed to parse ABI: %w", err) } + methodObj, exists := contractABI.Methods[method] + if !exists { + return nil, fmt.Errorf("method %s not found in ABI", method) + } + // Pack the method call data, err := contractABI.Pack(method, args...) if err != nil { return nil, fmt.Errorf("failed to pack method call: %w", err) } - // Make the call + // Set From to the facilitator address — required by the upto proxy which enforces + // msg.sender == witness.facilitator in settle(). to := common.HexToAddress(contractAddress) - - // Check if contract exists at this address - code, err := s.client.CodeAt(ctx, to, nil) - if err != nil { - log.Printf("Failed to check contract code: contract=%s, error=%v", contractAddress, err) - } else if len(code) == 0 { - log.Printf("WARNING: No contract code at address %s", contractAddress) - } - msg := ethereum.CallMsg{ + From: s.address, To: &to, Data: data, } result, err := s.client.CallContract(ctx, msg, nil) if err != nil { - log.Printf("Contract call failed: method=%s, contract=%s, error=%v", method, contractAddress, err) return nil, fmt.Errorf("failed to call contract: %w", err) } - log.Printf("Contract call: method=%s, contract=%s, dataLen=%d, resultLen=%d, result=%x", method, contractAddress, len(data), len(result), result) - - // Handle empty result (some contract calls return nothing or revert) - if len(result) == 0 { - // For authorizationState, empty means false (nonce not used) - if method == "authorizationState" { - return false, nil - } - // For balanceOf or allowance, empty might mean 0 - if method == "balanceOf" || method == "allowance" { - return big.NewInt(0), nil - } - return nil, fmt.Errorf("empty result from contract call") + if len(methodObj.Outputs) == 0 { + return nil, nil } // Unpack the result based on method - method_obj, exists := contractABI.Methods[method] - if !exists { - return nil, fmt.Errorf("method %s not found in ABI", method) - } - - output, err := method_obj.Outputs.Unpack(result) + output, err := methodObj.Outputs.Unpack(result) if err != nil { return nil, fmt.Errorf("failed to unpack result: %w", err) } @@ -429,24 +406,106 @@ func (s *realFacilitatorEvmSigner) GetCode(ctx context.Context, address string) return code, nil } -func (s *realFacilitatorEvmSigner) SendRawTransaction(ctx context.Context, signedTx string) (string, error) { - txBytes, err := hexutil.Decode(signedTx) +func (s *realFacilitatorEvmSigner) decodeRawTransaction(serialized string) (*types.Transaction, error) { + txBytes, err := hexutil.Decode(serialized) if err != nil { - return "", fmt.Errorf("failed to decode signed transaction: %w", err) + return nil, fmt.Errorf("failed to decode signed transaction: %w", err) } - tx := new(types.Transaction) if err := tx.UnmarshalBinary(txBytes); err != nil { - return "", fmt.Errorf("failed to unmarshal transaction: %w", err) + return nil, fmt.Errorf("failed to unmarshal transaction: %w", err) } + return tx, nil +} +func (s *realFacilitatorEvmSigner) sendRawTransaction(ctx context.Context, tx *types.Transaction) (string, error) { if err := s.client.SendTransaction(ctx, tx); err != nil { return "", fmt.Errorf("failed to send raw transaction: %w", err) } - return tx.Hash().Hex(), nil } +func (s *realFacilitatorEvmSigner) fundPayerGasIfNeeded(ctx context.Context, decodedTx *types.Transaction) error { + chainSigner := types.LatestSignerForChainID(s.chainID) + payerAddr, err := types.Sender(chainSigner, decodedTx) + if err != nil { + return fmt.Errorf("failed to recover sender: %w", err) + } + + gasFeeCap := decodedTx.GasFeeCap() + if gasFeeCap == nil { + gasFeeCap = decodedTx.GasPrice() + } + gasCost := new(big.Int).Mul(new(big.Int).SetUint64(decodedTx.Gas()), gasFeeCap) + + payerBalance, err := s.client.BalanceAt(ctx, payerAddr, nil) + if err != nil { + return fmt.Errorf("failed to get payer balance: %w", err) + } + if payerBalance.Cmp(gasCost) >= 0 { + return nil + } + + deficit := new(big.Int).Sub(gasCost, payerBalance) + log.Printf("⛽ Funding payer %s with %s wei for gas", payerAddr.Hex(), deficit.String()) + + fundNonce, err := s.client.PendingNonceAt(ctx, s.address) + if err != nil { + return fmt.Errorf("failed to get funding nonce: %w", err) + } + fundGasPrice, err := s.client.SuggestGasPrice(ctx) + if err != nil { + return fmt.Errorf("failed to get gas price: %w", err) + } + + fundTx := types.NewTransaction(fundNonce, payerAddr, deficit, 21000, fundGasPrice, nil) + signedFundTx, err := types.SignTx(fundTx, chainSigner, s.privateKey) + if err != nil { + return fmt.Errorf("failed to sign funding tx: %w", err) + } + if err := s.client.SendTransaction(ctx, signedFundTx); err != nil { + return fmt.Errorf("failed to send funding tx: %w", err) + } + + fundReceipt, err := s.WaitForTransactionReceipt(ctx, signedFundTx.Hash().Hex()) + if err != nil || fundReceipt.Status != evmmech.TxStatusSuccess { + return fmt.Errorf("gas funding failed: %s", signedFundTx.Hash().Hex()) + } + log.Printf("⛽ Gas funding confirmed: %s", signedFundTx.Hash().Hex()) + return nil +} + +func (s *realFacilitatorEvmSigner) SendTransactions(ctx context.Context, transactions []erc20approvalgassponsor.TransactionRequest) ([]string, error) { + var hashes []string + for _, tx := range transactions { + var hash string + var err error + if tx.Serialized != "" { + decodedTx, decErr := s.decodeRawTransaction(tx.Serialized) + if decErr != nil { + return hashes, fmt.Errorf("transaction_failed: %w", decErr) + } + if fundErr := s.fundPayerGasIfNeeded(ctx, decodedTx); fundErr != nil { + return hashes, fmt.Errorf("transaction_failed: %w", fundErr) + } + hash, err = s.sendRawTransaction(ctx, decodedTx) + } else if tx.Call != nil { + hash, err = s.WriteContract(ctx, tx.Call.Address, tx.Call.ABI, tx.Call.Function, tx.Call.Args...) + } else { + return hashes, fmt.Errorf("transaction_failed: empty transaction request") + } + if err != nil { + return hashes, fmt.Errorf("transaction_failed: %w", err) + } + receipt, err := s.WaitForTransactionReceipt(ctx, hash) + if err != nil || receipt.Status != evmmech.TxStatusSuccess { + return hashes, fmt.Errorf("transaction_failed: %s", hash) + } + hashes = append(hashes, hash) + } + return hashes, nil +} + // Helper functions for type conversion func getStringFromInterface(v interface{}) string { if v == nil { @@ -491,6 +550,11 @@ func createPaymentHash(paymentPayload x402.PaymentPayload) string { return hex.EncodeToString(hash[:]) } +func hashBytes(data []byte) string { + hash := sha256.Sum256(data) + return hex.EncodeToString(hash[:]) +} + // Real SVM facilitator signer type realFacilitatorSvmSigner struct { privateKey solana.PrivateKey @@ -773,16 +837,20 @@ func main() { // Register EVM schemes with dynamic network // Enable smart wallet deployment via EIP-6492 - evmConfig := &evm.ExactEvmSchemeConfig{ + evmConfig := &exactevm.ExactEvmSchemeConfig{ DeployERC4337WithEIP6492: true, } - evmFacilitatorScheme := evm.NewExactEvmScheme(evmSigner, evmConfig) + evmFacilitatorScheme := exactevm.NewExactEvmScheme(evmSigner, evmConfig) facilitator.Register([]x402.Network{x402.Network(evmNetwork)}, evmFacilitatorScheme) - evmV1Config := &evmv1.ExactEvmSchemeV1Config{ + // Register upto EVM scheme + uptoEvmFacilitatorScheme := uptoevm.NewUptoEvmScheme(evmSigner, nil) + facilitator.Register([]x402.Network{x402.Network(evmNetwork)}, uptoEvmFacilitatorScheme) + + evmV1Config := &exactevmv1.ExactEvmSchemeV1Config{ DeployERC4337WithEIP6492: true, } - evmFacilitatorV1Scheme := evmv1.NewExactEvmSchemeV1(evmSigner, evmV1Config) + evmFacilitatorV1Scheme := exactevmv1.NewExactEvmSchemeV1(evmSigner, evmV1Config) facilitator.RegisterV1([]x402.Network{x402.Network(getV1EvmNetwork(evmNetwork))}, evmFacilitatorV1Scheme) // Register SVM schemes with dynamic network @@ -807,11 +875,7 @@ func main() { OnAfterVerify(func(ctx x402.FacilitatorVerifyResultContext) error { // Hook 1: Track verified payment for verify→settle flow validation if ctx.Result.IsValid { - // Hooks now use view interfaces - create hash from payload view - paymentHash := fmt.Sprintf("v%d-%s-%s", - ctx.Payload.GetVersion(), - ctx.Payload.GetScheme(), - ctx.Payload.GetNetwork()) + paymentHash := hashBytes(ctx.PayloadBytes) verificationMutex.Lock() verifiedPayments[paymentHash] = time.Now().Unix() verificationMutex.Unlock() @@ -840,6 +904,7 @@ func main() { version, discovered.DiscoveryInfo, requirements, + discovered.RouteTemplate, ) } } else if version == 1 { @@ -861,6 +926,7 @@ func main() { version, discovered.DiscoveryInfo, requirements, + discovered.RouteTemplate, ) } } @@ -870,10 +936,7 @@ func main() { }). OnBeforeSettle(func(ctx x402.FacilitatorSettleContext) (*x402.FacilitatorBeforeHookResult, error) { // Hook 3: Validate payment was previously verified - paymentHash := fmt.Sprintf("v%d-%s-%s", - ctx.Payload.GetVersion(), - ctx.Payload.GetScheme(), - ctx.Payload.GetNetwork()) + paymentHash := hashBytes(ctx.PayloadBytes) verificationMutex.RLock() verificationTimestamp, verified := verifiedPayments[paymentHash] verificationMutex.RUnlock() @@ -902,10 +965,7 @@ func main() { }). OnAfterSettle(func(ctx x402.FacilitatorSettleResultContext) error { // Hook 4: Clean up verified payment tracking after successful settlement - paymentHash := fmt.Sprintf("v%d-%s-%s", - ctx.Payload.GetVersion(), - ctx.Payload.GetScheme(), - ctx.Payload.GetNetwork()) + paymentHash := hashBytes(ctx.PayloadBytes) verificationMutex.Lock() delete(verifiedPayments, paymentHash) verificationMutex.Unlock() @@ -917,10 +977,7 @@ func main() { }). OnSettleFailure(func(ctx x402.FacilitatorSettleFailureContext) (*x402.FacilitatorSettleFailureHookResult, error) { // Hook 5: Clean up verified payment tracking on failure too - paymentHash := fmt.Sprintf("v%d-%s-%s", - ctx.Payload.GetVersion(), - ctx.Payload.GetScheme(), - ctx.Payload.GetNetwork()) + paymentHash := hashBytes(ctx.PayloadBytes) verificationMutex.Lock() delete(verifiedPayments, paymentHash) verificationMutex.Unlock() @@ -937,12 +994,6 @@ func main() { // POST /verify - Verify a payment against requirements // Note: Payment tracking and bazaar discovery are handled by lifecycle hooks router.POST("/verify", func(c *gin.Context) { - // First, peek at the version to determine which struct to use - var versionCheck struct { - X402Version int `json:"x402Version"` - } - - // Read body into buffer so we can parse it twice bodyBytes, err := c.GetRawData() if err != nil { c.JSON(http.StatusBadRequest, gin.H{ @@ -951,14 +1002,6 @@ func main() { return } - // Parse version - if err := json.Unmarshal(bodyBytes, &versionCheck); err != nil { - c.JSON(http.StatusBadRequest, gin.H{ - "error": fmt.Sprintf("Failed to parse version: %v", err), - }) - return - } - var req VerifyRequest if err := json.Unmarshal(bodyBytes, &req); err != nil { c.JSON(http.StatusBadRequest, gin.H{ @@ -992,12 +1035,6 @@ func main() { // POST /settle - Settle a payment on-chain // Note: Verification validation and cleanup are handled by lifecycle hooks router.POST("/settle", func(c *gin.Context) { - // First, peek at the version to determine which struct to use - var versionCheck struct { - X402Version int `json:"x402Version"` - } - - // Read body into buffer so we can parse it twice bodyBytes, err := c.GetRawData() if err != nil { c.JSON(http.StatusBadRequest, gin.H{ @@ -1006,17 +1043,6 @@ func main() { return } - // Debug: Log raw request body - log.Printf("🔍 [FACILITATOR SETTLE] Received raw body: %s", string(bodyBytes)) - - // Parse version - if err := json.Unmarshal(bodyBytes, &versionCheck); err != nil { - c.JSON(http.StatusBadRequest, gin.H{ - "error": fmt.Sprintf("Failed to parse version: %v", err), - }) - return - } - var req SettleRequest if err := json.Unmarshal(bodyBytes, &req); err != nil { c.JSON(http.StatusBadRequest, gin.H{ @@ -1033,9 +1059,6 @@ func main() { []byte(req.PaymentRequirements), ) - // Debug: Log response - log.Printf("🔍 [FACILITATOR SETTLE] Response: %+v", response) - log.Printf("🔍 [FACILITATOR SETTLE] Error: %v", err) if err != nil { log.Printf("Settle error: %v", err) diff --git a/e2e/facilitators/go/test.config.json b/e2e/facilitators/go/test.config.json index c549d55ae1..55ec7fc5f7 100644 --- a/e2e/facilitators/go/test.config.json +++ b/e2e/facilitators/go/test.config.json @@ -15,6 +15,7 @@ "eip2612GasSponsoring", "erc20ApprovalGasSponsoring" ], + "schemes": ["exact", "upto"], "evm": { "transferMethods": ["eip3009", "permit2"] }, diff --git a/e2e/facilitators/python/bazaar.py b/e2e/facilitators/python/bazaar.py index cc5accf6ba..4c7c466d55 100644 --- a/e2e/facilitators/python/bazaar.py +++ b/e2e/facilitators/python/bazaar.py @@ -17,6 +17,7 @@ def __init__( x402_version: int, accepts: list[dict[str, Any]], discovery_info: dict[str, Any] | None = None, + route_template: str | None = None, metadata: dict[str, Any] | None = None, ) -> None: self.resource = resource @@ -24,6 +25,7 @@ def __init__( self.x402_version = x402_version self.accepts = accepts self.discovery_info = discovery_info + self.route_template = route_template self.last_updated = datetime.now().isoformat() self.metadata = metadata or {} @@ -39,6 +41,8 @@ def to_dict(self) -> dict[str, Any]: } if self.discovery_info: result["discoveryInfo"] = self.discovery_info + if self.route_template: + result["routeTemplate"] = self.route_template return result @@ -55,6 +59,7 @@ def catalog_resource( x402_version: int, discovery_info: dict[str, Any] | None, payment_requirements: dict[str, Any], + route_template: str | None = None, ) -> None: """Add a discovered resource to the catalog. @@ -64,10 +69,13 @@ def catalog_resource( x402_version: The x402 protocol version. discovery_info: Optional discovery metadata. payment_requirements: The payment requirements for this resource. + route_template: Optional route template for dynamic routes. """ print(f"📝 Discovered resource: {resource_url}") print(f" Method: {method}") print(f" x402 Version: {x402_version}") + if route_template: + print(f" Route template: {route_template}") self._resources[resource_url] = DiscoveredResource( resource=resource_url, @@ -75,6 +83,7 @@ def catalog_resource( x402_version=x402_version, accepts=[payment_requirements], discovery_info=discovery_info, + route_template=route_template, metadata={}, ) diff --git a/e2e/facilitators/python/build.sh b/e2e/facilitators/python/build.sh index 9889f505ad..c1bb071bbe 100755 --- a/e2e/facilitators/python/build.sh +++ b/e2e/facilitators/python/build.sh @@ -1,5 +1,5 @@ #!/bin/bash -# Python build - no compilation needed -# This file exists for consistency with the e2e setup process -exit 0 +set -e +# Rebuild the local x402 editable dependency so the venv reflects source changes +uv sync --reinstall-package x402 diff --git a/e2e/facilitators/python/main.py b/e2e/facilitators/python/main.py index 9160860027..97d55e8f0b 100644 --- a/e2e/facilitators/python/main.py +++ b/e2e/facilitators/python/main.py @@ -7,15 +7,22 @@ - EVM networks (Base Sepolia) via web3.py - SVM networks (Solana Devnet) via solders - Bazaar discovery extension for resource cataloging +- EIP-2612 gas sponsoring extension (gasless Permit2 approval via permit) +- ERC-20 approval gas sponsoring extension (gasless Permit2 via signed tx relay) - V1 and V2 protocol versions Run with: uv run uvicorn main:app --port 4022 """ +import logging import os import sys from typing import Any +logging.basicConfig(level=logging.INFO, format="%(name)s %(levelname)s: %(message)s") +logging.getLogger("x402.permit2").setLevel(logging.DEBUG) +logging.getLogger("x402.signers").setLevel(logging.DEBUG) + from dotenv import load_dotenv from fastapi import FastAPI, HTTPException from pydantic import BaseModel @@ -23,8 +30,15 @@ from x402 import x402Facilitator from x402.extensions.bazaar import extract_discovery_info +from x402.extensions.eip2612_gas_sponsoring import EIP2612_GAS_SPONSORING +from x402.extensions.erc20_approval_gas_sponsoring import ( + Erc20ApprovalFacilitatorExtension, + WriteContractCall, +) from x402.mechanisms.evm import FacilitatorWeb3Signer +from x402.mechanisms.evm.constants import TX_STATUS_SUCCESS from x402.mechanisms.evm.exact import register_exact_evm_facilitator +from x402.mechanisms.evm.types import TransactionReceipt from x402.mechanisms.svm import FacilitatorKeypairSigner from x402.mechanisms.svm.exact import register_exact_svm_facilitator @@ -62,6 +76,71 @@ print(f"SVM Facilitator account: {svm_signer.get_addresses()[0]}") +class Erc20ApprovalSigner: + """Wraps FacilitatorWeb3Signer with send_transactions for ERC-20 approval sponsoring. + + Broadcasts pre-signed approval txs and settles via the proxy contract, + matching the Go/TS facilitator pattern. + """ + + def __init__(self, base_signer: FacilitatorWeb3Signer): + self._signer = base_signer + + def send_transactions(self, transactions: list) -> list[str]: + hashes: list[str] = [] + for tx in transactions: + if isinstance(tx, str): + raw_bytes = bytes.fromhex(tx[2:] if tx.startswith("0x") else tx) + w3 = self._signer._w3 + + payer_address = w3.eth.account.recover_transaction(tx) + # Use the same gas constants as the library's approve tx builder + gas_cost = 70_000 * 1_000_000_000 # ERC20_APPROVE_GAS_LIMIT * DEFAULT_MAX_FEE_PER_GAS + + payer_balance = w3.eth.get_balance(payer_address) + if payer_balance < gas_cost: + deficit = gas_cost - payer_balance + print(f"⛽ Funding payer {payer_address} with {deficit} wei for gas") + fund_tx = { + "to": payer_address, + "value": deficit, + "gas": 21000, + "gasPrice": w3.eth.gas_price, + "nonce": w3.eth.get_transaction_count(self._signer._account.address), + "chainId": w3.eth.chain_id, + } + signed_fund = self._signer._account.sign_transaction(fund_tx) + fund_hash = w3.eth.send_raw_transaction(signed_fund.raw_transaction).hex() + fund_receipt = w3.eth.wait_for_transaction_receipt(fund_hash) + if fund_receipt["status"] != 1: + raise RuntimeError(f"gas_funding_failed: {fund_hash}") + print(f"⛽ Gas funding confirmed: {fund_hash}") + + tx_hash = w3.eth.send_raw_transaction(raw_bytes).hex() + elif isinstance(tx, dict) or isinstance(tx, WriteContractCall): + if isinstance(tx, dict): + call = WriteContractCall(**tx) + else: + call = tx + tx_hash = self._signer.write_contract( + call.address, call.abi, call.function, *call.args + ) + else: + raise ValueError(f"Unsupported transaction type: {type(tx)}") + + receipt = self._signer.wait_for_transaction_receipt(tx_hash) + if receipt.status != TX_STATUS_SUCCESS: + raise RuntimeError(f"transaction_failed: {tx_hash}") + hashes.append(tx_hash) + return hashes + + def wait_for_transaction_receipt(self, tx_hash: str) -> TransactionReceipt: + return self._signer.wait_for_transaction_receipt(tx_hash) + + +erc20_approval_signer = Erc20ApprovalSigner(evm_signer) + + def _handle_after_verify(ctx: Any) -> None: """Handle after verify hook - extract discovery info and catalog.""" print("✅ Payment verified") @@ -97,6 +176,7 @@ def _handle_after_verify(ctx: Any) -> None: payment_requirements=ctx.requirements.model_dump(by_alias=True) if hasattr(ctx.requirements, "model_dump") else ctx.requirements, + route_template=getattr(discovered, "route_template", None), ) print(" ✅ Added to bazaar catalog") except Exception as err: @@ -129,6 +209,12 @@ def _handle_after_verify(ctx: Any) -> None: networks="solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1", # Devnet ) +# Register gas sponsoring extensions +facilitator.register_extension(EIP2612_GAS_SPONSORING) +facilitator.register_extension( + Erc20ApprovalFacilitatorExtension(signer=erc20_approval_signer) +) + # Pydantic models for request/response class VerifyRequest(BaseModel): @@ -179,13 +265,14 @@ async def verify(request: VerifyRequest): # - Extract and catalog discovery info (on_after_verify) response = await facilitator.verify(payload, requirements) - return { - "isValid": response.is_valid, - "payer": response.payer, - "invalidReason": response.invalid_reason, - } + if not response.is_valid: + print(f" ❌ Verify rejected: {response.invalid_reason} (payer={response.payer})") + + return response.model_dump(by_alias=True, exclude_none=True) except Exception as e: + import traceback print(f"Verify error: {e}") + traceback.print_exc() raise HTTPException(status_code=500, detail=str(e)) @@ -216,28 +303,23 @@ async def settle(request: SettleRequest): # - Clean up tracking (on_after_settle / on_settle_failure) response = await facilitator.settle(payload, requirements) - return { - "success": response.success, - "transaction": response.transaction, - "network": response.network, - "payer": response.payer, - "errorReason": response.error_reason, - } + return response.model_dump(by_alias=True, exclude_none=True) except Exception as e: print(f"Settle error: {e}") # Check if this was an abort from hook if "aborted" in str(e).lower() or "Settlement aborted" in str(e): - # Return a proper SettleResponse instead of 500 error - return { - "success": False, - "errorReason": str(e).replace("Settlement aborted: ", ""), - "network": request.paymentPayload.get("accepted", {}).get( + from x402.schemas import SettleResponse + + abort = SettleResponse( + success=False, + error_reason=str(e).replace("Settlement aborted: ", ""), + network=request.paymentPayload.get("accepted", {}).get( "network", "unknown" ), - "transaction": "", - "payer": None, - } + transaction="", + ) + return abort.model_dump(by_alias=True, exclude_none=True) raise HTTPException(status_code=500, detail=str(e)) @@ -253,15 +335,7 @@ async def supported(): response = facilitator.get_supported() return { - "kinds": [ - { - "x402Version": k.x402_version, - "scheme": k.scheme, - "network": k.network, - "extra": k.extra, - } - for k in response.kinds - ], + "kinds": [k.model_dump(by_alias=True, exclude_none=True) for k in response.kinds], "extensions": response.extensions, "signers": response.signers, } @@ -292,7 +366,7 @@ async def health(): "network": "eip155:84532", "facilitator": "python", "version": "2.0.0", - "extensions": ["bazaar"], + "extensions": facilitator.get_extensions(), "discoveredResources": bazaar_catalog.get_count(), } @@ -322,7 +396,7 @@ async def shutdown(): ║ Server: http://localhost:{PORT} ║ ║ Network: eip155:84532 ║ ║ Address: {evm_signer.get_addresses()[0]} ║ -║ Extensions: bazaar ║ +║ Extensions: bazaar, eip2612, erc20approval ║ ║ ║ ║ Endpoints: ║ ║ • POST /verify (verify payment) ║ diff --git a/e2e/facilitators/python/run.sh b/e2e/facilitators/python/run.sh index d3a07c0c9e..c653d856a9 100755 --- a/e2e/facilitators/python/run.sh +++ b/e2e/facilitators/python/run.sh @@ -1,3 +1,3 @@ #!/bin/bash +uv sync --reinstall-package x402 --quiet uv run python main.py - diff --git a/e2e/facilitators/python/test.config.json b/e2e/facilitators/python/test.config.json index 11ced9d07d..ab0ea28eb9 100644 --- a/e2e/facilitators/python/test.config.json +++ b/e2e/facilitators/python/test.config.json @@ -11,10 +11,12 @@ 2 ], "extensions": [ - "bazaar" + "bazaar", + "eip2612GasSponsoring", + "erc20ApprovalGasSponsoring" ], "evm": { - "transferMethods": ["eip3009"] + "transferMethods": ["eip3009", "permit2"] }, "environment": { "required": [ @@ -29,4 +31,3 @@ ] } } - diff --git a/e2e/facilitators/python/uv.lock b/e2e/facilitators/python/uv.lock index 569073a81f..e3a4299b04 100644 --- a/e2e/facilitators/python/uv.lock +++ b/e2e/facilitators/python/uv.lock @@ -2846,7 +2846,7 @@ wheels = [ [[package]] name = "x402" -version = "2.2.0" +version = "2.5.0" source = { editable = "../../../python/x402" } dependencies = [ { name = "nest-asyncio" }, @@ -2877,7 +2877,7 @@ svm = [ [package.metadata] requires-dist = [ { name = "eth-abi", marker = "extra == 'evm'", specifier = ">=5.0.0" }, - { name = "eth-account", marker = "extra == 'evm'", specifier = ">=0.12.0" }, + { name = "eth-account", marker = "extra == 'evm'", specifier = ">=0.13.0" }, { name = "eth-keys", marker = "extra == 'evm'", specifier = ">=0.5.0" }, { name = "eth-utils", marker = "extra == 'evm'", specifier = ">=4.0.0" }, { name = "fastapi", extras = ["standard"], marker = "extra == 'fastapi'", specifier = ">=0.115.0" }, @@ -2904,7 +2904,7 @@ provides-extras = ["httpx", "requests", "flask", "fastapi", "evm", "svm", "mcp", dev = [ { name = "black", specifier = ">=23.0.0" }, { name = "eth-abi", specifier = ">=5.0.0" }, - { name = "eth-account", specifier = ">=0.12.0" }, + { name = "eth-account", specifier = ">=0.13.0" }, { name = "eth-keys", specifier = ">=0.5.0" }, { name = "eth-utils", specifier = ">=4.0.0" }, { name = "fastapi", extras = ["standard"], specifier = ">=0.115.0" }, diff --git a/e2e/facilitators/typescript/README.md b/e2e/facilitators/typescript/README.md index c7064b9f3c..a87bbcade5 100644 --- a/e2e/facilitators/typescript/README.md +++ b/e2e/facilitators/typescript/README.md @@ -1,6 +1,6 @@ # E2E Test Facilitator: TypeScript -This facilitator demonstrates and tests the TypeScript x402 facilitator implementation with both EVM and SVM payment verification and settlement. +This facilitator demonstrates and tests the TypeScript x402 facilitator implementation with EVM, SVM, and optional Stellar payment verification and settlement. ## What It Tests @@ -9,7 +9,7 @@ This facilitator demonstrates and tests the TypeScript x402 facilitator implemen - ✅ **V1 Protocol** - Legacy x402 facilitator protocol - ✅ **Payment Verification** - Validates payment payloads off-chain - ✅ **Payment Settlement** - Executes transactions on-chain -- ✅ **Multi-chain Support** - EVM and SVM mechanisms +- ✅ **Multi-chain Support** - EVM, SVM, and (optional) Stellar mechanisms - ✅ **HTTP API** - Express.js server exposing facilitator endpoints ### Facilitator Endpoints @@ -27,6 +27,7 @@ This e2e facilitator showcases **production-ready lifecycle hook patterns**: ```typescript const facilitator = new x402Facilitator() .register("eip155:*", new ExactEvmFacilitator(evmSigner)) + .register("stellar:*", new ExactStellarScheme([stellarSigner])) .registerExtension(BAZAAR) // Hook 1: Track verified payments + extract discovery info .onAfterVerify(async (context) => { @@ -75,6 +76,7 @@ import { ExactEvmFacilitator } from "@x402/evm"; import { ExactEvmFacilitatorV1, NETWORKS as EVM_NETWORKS } from "@x402/evm/v1"; import { ExactSvmFacilitator } from "@x402/svm"; import { ExactSvmFacilitatorV1, NETWORKS as SVM_NETWORKS } from "@x402/svm/v1"; +import { ExactStellarFacilitator } from "@x402/stellar"; // Create facilitator with bazaar extension const facilitator = new x402Facilitator() @@ -95,6 +97,8 @@ EVM_NETWORKS.forEach(network => { }); // Register SVM schemes similarly... + +// Register Stellar (v2) schemes similarly... ``` ### HTTP Server @@ -118,7 +122,7 @@ app.listen(port, () => { 1. **Extension Registration** - Bazaar discovery 2. **Comprehensive Network Support** - All EVM V1 networks, all SVM V1 networks -3. **Wildcard Schemes** - Efficient V2 registration with `eip155:*` and `solana:*` +3. **Wildcard Schemes** - Efficient V2 registration with `eip155:*`, `solana:*`, and `stellar:*` 4. **HTTP Router Integration** - `@x402/server/facilitator` for Express 5. **Real Signers** - Actual blockchain transaction submission 6. **Multi-Protocol** - V1 and V2 side-by-side @@ -128,12 +132,13 @@ app.listen(port, () => { This facilitator is tested with: - **Clients:** TypeScript Fetch, Go HTTP - **Servers:** Express (TypeScript), Gin (Go) -- **Networks:** Base Sepolia (EVM), Solana Devnet (SVM) -- **Test Cases:** +- **Networks:** Base Sepolia (EVM), Solana Devnet (SVM), Stellar Testnet (Stellar) +- **Test Cases:** - V1 EVM payments - V2 EVM payments - V1 SVM payments - V2 SVM payments + - V2 Stellar payments (optional) ### Success Criteria - ✅ Verification returns valid status @@ -152,16 +157,24 @@ pnpm test --facilitator=typescript cd e2e/facilitators/typescript export EVM_PRIVATE_KEY="0x..." export SVM_PRIVATE_KEY="..." +export STELLAR_PRIVATE_KEY="S..." # optional export PORT=4025 pnpm start ``` ## Environment Variables +### Required - `PORT` - HTTP server port - `EVM_PRIVATE_KEY` - Ethereum private key (hex with 0x prefix) - `SVM_PRIVATE_KEY` - Solana private key (base58 encoded) +### Optional +- `STELLAR_PRIVATE_KEY` - Stellar private key (S... format) - enables Stellar support +- `EVM_NETWORK` - EVM network (default: eip155:84532) +- `SVM_NETWORK` - SVM network (default: solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1) +- `STELLAR_NETWORK` - Stellar network (default: stellar:testnet) + ## Package Dependencies - `@x402/core` - Core facilitator @@ -170,6 +183,7 @@ pnpm start - `@x402/evm/v1` - EVM facilitator (V1) + NETWORKS - `@x402/svm` - SVM facilitator (V2) - `@x402/svm/v1` - SVM facilitator (V1) + NETWORKS +- `@x402/stellar` - Stellar facilitator (V2, SEP-41) - `express` - HTTP server - `viem` - Ethereum transactions - `@solana/web3.js` - Solana transactions diff --git a/e2e/facilitators/typescript/bazaar.ts b/e2e/facilitators/typescript/bazaar.ts index a09f29fcdb..c9b8cefaba 100644 --- a/e2e/facilitators/typescript/bazaar.ts +++ b/e2e/facilitators/typescript/bazaar.ts @@ -7,6 +7,7 @@ export interface DiscoveredResource { x402Version: number; accepts: PaymentRequirements[]; discoveryInfo?: DiscoveryInfo; + routeTemplate?: string; lastUpdated: string; metadata?: Record; } @@ -20,10 +21,14 @@ export class BazaarCatalog { x402Version: number, discoveryInfo: DiscoveryInfo, paymentRequirements: PaymentRequirements, + routeTemplate?: string, ): void { console.log(`📝 Discovered resource: ${resourceUrl}`); console.log(` Method: ${method}`); console.log(` x402 Version: ${x402Version}`); + if (routeTemplate) { + console.log(` Route template: ${routeTemplate}`); + } this.discoveredResources.set(resourceUrl, { resource: resourceUrl, @@ -31,6 +36,7 @@ export class BazaarCatalog { x402Version, accepts: [paymentRequirements], discoveryInfo, + routeTemplate, lastUpdated: new Date().toISOString(), metadata: {}, }); diff --git a/e2e/facilitators/typescript/index.ts b/e2e/facilitators/typescript/index.ts index a8218f7fe8..ad3a406bfa 100644 --- a/e2e/facilitators/typescript/index.ts +++ b/e2e/facilitators/typescript/index.ts @@ -31,6 +31,7 @@ import { } from "@x402/core/types"; import { toFacilitatorEvmSigner } from "@x402/evm"; import { ExactEvmScheme } from "@x402/evm/exact/facilitator"; +import { UptoEvmScheme } from "@x402/evm/upto/facilitator"; import { ExactEvmSchemeV1 } from "@x402/evm/exact/v1/facilitator"; import { NETWORKS as EVM_V1_NETWORKS } from "@x402/evm/v1"; import { BAZAAR, extractDiscoveryInfo } from "@x402/extensions/bazaar"; @@ -42,10 +43,12 @@ import { toFacilitatorSvmSigner } from "@x402/svm"; import { ExactSvmScheme } from "@x402/svm/exact/facilitator"; import { ExactSvmSchemeV1 } from "@x402/svm/exact/v1/facilitator"; import { NETWORKS as SVM_V1_NETWORKS } from "@x402/svm/v1"; +import { createEd25519Signer, type FacilitatorStellarSigner } from "@x402/stellar"; +import { ExactStellarScheme } from "@x402/stellar/exact/facilitator"; import crypto from "crypto"; import dotenv from "dotenv"; import express from "express"; -import { createWalletClient, http, publicActions, Chain } from "viem"; +import { createWalletClient, http, publicActions, Chain, parseTransaction, recoverTransactionAddress } from "viem"; import { privateKeyToAccount } from "viem/accounts"; import { baseSepolia, base } from "viem/chains"; import { BazaarCatalog } from "./bazaar.js"; @@ -58,9 +61,11 @@ const EVM_NETWORK = process.env.EVM_NETWORK || "eip155:84532"; const SVM_NETWORK = process.env.SVM_NETWORK || "solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1"; const APTOS_NETWORK = process.env.APTOS_NETWORK || "aptos:2"; +const STELLAR_NETWORK = process.env.STELLAR_NETWORK || "stellar:testnet"; const EVM_RPC_URL = process.env.EVM_RPC_URL; const SVM_RPC_URL = process.env.SVM_RPC_URL; const APTOS_RPC_URL = process.env.APTOS_RPC_URL; +const STELLAR_RPC_URL = process.env.STELLAR_RPC_URL; // Map CAIP-2 network IDs to viem chains function getEvmChain(network: string): Chain { @@ -76,9 +81,11 @@ function getEvmChain(network: string): Chain { console.log(`🌐 EVM Network: ${EVM_NETWORK}`); console.log(`🌐 SVM Network: ${SVM_NETWORK}`); console.log(`🌐 Aptos Network: ${APTOS_NETWORK}`); +console.log(`🌐 Stellar Network: ${STELLAR_NETWORK}`); if (EVM_RPC_URL) console.log(`🌐 EVM RPC URL: ${EVM_RPC_URL}`); if (SVM_RPC_URL) console.log(`🌐 SVM RPC URL: ${SVM_RPC_URL}`); if (APTOS_RPC_URL) console.log(`🌐 Aptos RPC URL: ${APTOS_RPC_URL}`); +if (STELLAR_RPC_URL) console.log(`🌐 Stellar RPC URL: ${STELLAR_RPC_URL}`); // Validate required environment variables if (!process.env.EVM_PRIVATE_KEY) { @@ -117,6 +124,13 @@ if (process.env.APTOS_PRIVATE_KEY) { ); } +// Initialize the Stellar signer from private key (optional) +let stellarSigner: FacilitatorStellarSigner | undefined; +if (process.env.STELLAR_PRIVATE_KEY) { + stellarSigner = createEd25519Signer(process.env.STELLAR_PRIVATE_KEY as string, STELLAR_NETWORK as Network); + console.info(`Stellar Facilitator account: ${stellarSigner.address}`); +} + // Create a Viem client with both wallet and public capabilities const evmChain = getEvmChain(EVM_NETWORK); const viemClient = createWalletClient({ @@ -152,10 +166,12 @@ const evmSigner = toFacilitatorEvmSigner({ abi: readonly unknown[]; functionName: string; args: readonly unknown[]; + gas?: bigint; }) => viemClient.writeContract({ ...args, args: args.args || [], + gas: args.gas, }), sendTransaction: (args: { to: `0x${string}`; data: `0x${string}` }) => viemClient.sendTransaction(args), @@ -195,6 +211,7 @@ const facilitator = new x402Facilitator(); // Register EVM, SVM, and Aptos schemes (v2 + v1) facilitator .register(EVM_NETWORK as Network, new ExactEvmScheme(evmSigner)) + .register(EVM_NETWORK as Network, new UptoEvmScheme(evmSigner)) .registerV1(EVM_V1_NETWORKS as Network[], new ExactEvmSchemeV1(evmSigner)) .register(SVM_NETWORK as Network, new ExactSvmScheme(svmSigner)) .registerV1(SVM_V1_NETWORKS as Network[], new ExactSvmSchemeV1(svmSigner)); @@ -204,11 +221,74 @@ if (aptosSigner) { new ExactAptosScheme(aptosSigner), ); } +if (stellarSigner) { + facilitator.register(STELLAR_NETWORK as Network, new ExactStellarScheme([stellarSigner])); +} + +const PERMIT2_ADDRESS = "0x000000000022D473030F116dDEE9F6B43aC78BA3" as const; +const erc20AllowanceAbi = [ + { + inputs: [ + { name: "owner", type: "address" }, + { name: "spender", type: "address" }, + ], + name: "allowance", + outputs: [{ name: "", type: "uint256" }], + stateMutability: "view", + type: "function", + }, +] as const; + +const erc20ApprovalSigner = { + ...evmSigner, + sendTransactions: async ( + transactions: (`0x${string}` | { to: `0x${string}`; data: `0x${string}`; gas?: bigint })[], + ): Promise<`0x${string}`[]> => { + const hashes: `0x${string}`[] = []; + for (const tx of transactions) { + let hash: `0x${string}`; + if (typeof tx === "string") { + // Parse the raw tx to extract sender and gas params for potential gas funding + const parsed = parseTransaction(tx); + const payerAddress = await recoverTransactionAddress({ serializedTransaction: tx }); + const gas = parsed.gas ?? 70_000n; + const maxFeePerGas = parsed.maxFeePerGas ?? 1_000_000_000n; + const gasCost = gas * maxFeePerGas; + + // Check if the payer has enough ETH for gas + const payerBalance = await viemClient.getBalance({ address: payerAddress }); + if (payerBalance < gasCost) { + const deficit = gasCost - payerBalance; + console.log(`⛽ Funding payer ${payerAddress} with ${deficit} wei for gas`); + const fundHash = await viemClient.sendTransaction({ + to: payerAddress, + value: deficit, + }); + const fundReceipt = await viemClient.waitForTransactionReceipt({ hash: fundHash }); + if (fundReceipt.status !== "success") { + throw new Error(`gas_funding_failed: ${fundHash}`); + } + console.log(`⛽ Gas funding confirmed: ${fundHash}`); + } + + hash = await viemClient.sendRawTransaction({ serializedTransaction: tx }); + } else { + hash = await viemClient.sendTransaction(tx); + } + const receipt = await viemClient.waitForTransactionReceipt({ hash }); + if (receipt.status !== "success") { + throw new Error(`transaction_failed: ${hash}`); + } + hashes.push(hash); + } + return hashes; + }, +}; facilitator .registerExtension(BAZAAR) .registerExtension(EIP2612_GAS_SPONSORING) - .registerExtension(createErc20ApprovalGasSponsoringExtension(evmSigner, viemClient)) + .registerExtension(createErc20ApprovalGasSponsoringExtension(erc20ApprovalSigner)) // Lifecycle hooks for payment tracking and discovery .onAfterVerify(async (context) => { // Hook 1: Track verified payment for verify→settle flow validation @@ -228,6 +308,7 @@ facilitator discovered.x402Version, discovered.discoveryInfo, context.requirements, + discovered.routeTemplate, ); console.log( `📦 Discovered resource: ${discovered.method} ${discovered.resourceUrl}`, @@ -403,6 +484,7 @@ app.get("/health", (req, res) => { evmNetwork: EVM_NETWORK, svmNetwork: SVM_NETWORK, aptosNetwork: aptosAccount ? APTOS_NETWORK : "(not configured)", + stellarNetwork: stellarSigner ? STELLAR_NETWORK : "(not configured)", facilitator: "typescript", version: "2.0.0", extensions: [BAZAAR.key], @@ -436,6 +518,7 @@ app.listen(parseInt(PORT), () => { ║ Aptos Network: ${APTOS_NETWORK} ║ ║ EVM Address: ${evmAccount.address} ║ ║ Aptos Address: ${aptosAccount ? aptosAccount.accountAddress.toStringLong().slice(0, 20) + "..." : "(not configured)"} +║ Stellar Address: ${stellarSigner ? stellarSigner.address : "(not configured)"} ║ ║ Extensions: bazaar ║ ║ ║ ║ Endpoints: ║ diff --git a/e2e/facilitators/typescript/package.json b/e2e/facilitators/typescript/package.json index 6ce5e4d256..b3e6922b0a 100644 --- a/e2e/facilitators/typescript/package.json +++ b/e2e/facilitators/typescript/package.json @@ -19,6 +19,7 @@ "@x402/core": "workspace:*", "@x402/evm": "workspace:*", "@x402/extensions": "workspace:*", + "@x402/stellar": "workspace:*", "@x402/svm": "workspace:*", "dotenv": "^16.4.5", "express": "^4.19.2", diff --git a/e2e/facilitators/typescript/test.config.json b/e2e/facilitators/typescript/test.config.json index 31f0a5d5ee..d4a6137e50 100644 --- a/e2e/facilitators/typescript/test.config.json +++ b/e2e/facilitators/typescript/test.config.json @@ -5,7 +5,8 @@ "protocolFamilies": [ "evm", "svm", - "aptos" + "aptos", + "stellar" ], "x402Versions": [ 1, @@ -17,7 +18,7 @@ "erc20ApprovalGasSponsoring" ], "evm": { - "transferMethods": ["eip3009", "permit2"] + "transferMethods": ["eip3009", "permit2", "upto"] }, "environment": { "required": [ @@ -27,9 +28,11 @@ ], "optional": [ "APTOS_PRIVATE_KEY", + "STELLAR_PRIVATE_KEY", "EVM_NETWORK", "SVM_NETWORK", - "APTOS_NETWORK" + "APTOS_NETWORK", + "STELLAR_NETWORK" ] } } diff --git a/e2e/mock-facilitator/index.ts b/e2e/mock-facilitator/index.ts new file mode 100644 index 0000000000..024cdd896f --- /dev/null +++ b/e2e/mock-facilitator/index.ts @@ -0,0 +1,112 @@ +import http from "node:http"; + +/** + * Mock facilitator that claims to support all schemes/networks but errors + * if verify or settle are actually called. Used as a fallback facilitator + * during e2e testing so that servers with routes unsupported by the real + * facilitator (e.g. "upto" on Go/Python facilitators) can still start. + * + * The real facilitator is always first in the client array and handles + * all actual operations. This mock only fills validation gaps at startup. + */ + +const PORT = parseInt(process.env.PORT || "4099", 10); +const EVM_NETWORK = process.env.EVM_NETWORK || "eip155:84532"; +const SVM_NETWORK = process.env.SVM_NETWORK || "solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1"; +const APTOS_NETWORK = process.env.APTOS_NETWORK || "aptos:2"; +const STELLAR_NETWORK = process.env.STELLAR_NETWORK || "stellar:testnet"; + +const DUMMY_EVM_SIGNER = "0x0000000000000000000000000000000000000001"; +const DUMMY_SVM_SIGNER = "11111111111111111111111111111111"; +const DUMMY_APTOS_SIGNER = + "0x0000000000000000000000000000000000000000000000000000000000000001"; +const DUMMY_STELLAR_SIGNER = "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF"; + +function buildSupportedResponse() { + const evmSchemes = ["exact", "upto"]; + const otherSchemes = ["exact"]; + const versions = [1, 2]; + + const kinds: Array<{ + x402Version: number; + scheme: string; + network: string; + }> = []; + + for (const version of versions) { + for (const scheme of evmSchemes) { + kinds.push({ x402Version: version, scheme, network: EVM_NETWORK }); + } + for (const scheme of otherSchemes) { + kinds.push({ x402Version: version, scheme, network: SVM_NETWORK }); + } + if (APTOS_NETWORK) { + for (const scheme of otherSchemes) { + kinds.push({ x402Version: version, scheme, network: APTOS_NETWORK }); + } + } + if (STELLAR_NETWORK) { + for (const scheme of otherSchemes) { + kinds.push({ x402Version: version, scheme, network: STELLAR_NETWORK }); + } + } + } + + const signers: Record = { + "eip155:*": [DUMMY_EVM_SIGNER], + "solana:*": [DUMMY_SVM_SIGNER], + }; + if (APTOS_NETWORK) { + signers["aptos:*"] = [DUMMY_APTOS_SIGNER]; + } + if (STELLAR_NETWORK) { + signers["stellar:*"] = [DUMMY_STELLAR_SIGNER]; + } + + return { kinds, extensions: [], signers }; +} + +function sendJson(res: http.ServerResponse, statusCode: number, body: unknown) { + const json = JSON.stringify(body); + res.writeHead(statusCode, { "Content-Type": "application/json" }); + res.end(json); +} + +const supportedResponse = buildSupportedResponse(); + +const server = http.createServer((req, res) => { + const url = new URL(req.url || "/", `http://localhost:${PORT}`); + + if (req.method === "GET" && url.pathname === "/supported") { + sendJson(res, 200, supportedResponse); + return; + } + + if (req.method === "GET" && url.pathname === "/health") { + sendJson(res, 200, { status: "ok" }); + return; + } + + if (req.method === "POST" && url.pathname === "/verify") { + sendJson(res, 500, { + error: "Mock facilitator: /verify should never be called. " + + "The real facilitator should handle all verification.", + }); + return; + } + + if (req.method === "POST" && url.pathname === "/settle") { + sendJson(res, 500, { + error: "Mock facilitator: /settle should never be called. " + + "The real facilitator should handle all settlement.", + }); + return; + } + + sendJson(res, 404, { error: "Not found" }); +}); + +server.listen(PORT, () => { + console.log(`Mock facilitator listening on port ${PORT}`); + console.log("Facilitator listening"); +}); diff --git a/e2e/mock-facilitator/run.sh b/e2e/mock-facilitator/run.sh new file mode 100755 index 0000000000..68d48fcdae --- /dev/null +++ b/e2e/mock-facilitator/run.sh @@ -0,0 +1 @@ +npx tsx index.ts diff --git a/e2e/pnpm-lock.yaml b/e2e/pnpm-lock.yaml index 6e0ce33d8f..40353cd865 100644 --- a/e2e/pnpm-lock.yaml +++ b/e2e/pnpm-lock.yaml @@ -94,6 +94,9 @@ importers: ../typescript/packages/extensions: dependencies: + '@noble/curves': + specifier: ^1.9.0 + version: 1.9.7 '@scure/base': specifier: ^1.2.6 version: 1.2.6 @@ -103,6 +106,9 @@ importers: ajv: specifier: ^8.17.1 version: 8.17.1 + jose: + specifier: ^5.9.6 + version: 5.10.0 siwe: specifier: ^2.3.2 version: 2.3.2(ethers@6.16.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)) @@ -222,12 +228,6 @@ importers: ../typescript/packages/http/express: dependencies: - '@coinbase/cdp-sdk': - specifier: ^1.22.0 - version: 1.38.4(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3)(utf-8-validate@5.0.10)(ws@8.19.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)) - '@solana/kit': - specifier: ^6.1.0 - version: 6.1.0(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3)(utf-8-validate@5.0.10) '@x402/core': specifier: workspace:~ version: link:../../core @@ -235,7 +235,7 @@ importers: specifier: workspace:~ version: link:../../extensions '@x402/paywall': - specifier: workspace:* + specifier: workspace:^ version: link:../paywall viem: specifier: ^2.39.3 @@ -296,6 +296,67 @@ importers: specifier: ^3.0.5 version: 3.2.4(@types/debug@4.1.12)(@types/node@22.16.0)(jiti@1.21.7)(jsdom@26.1.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(tsx@4.20.3)(yaml@2.8.1) + ../typescript/packages/http/fastify: + dependencies: + '@x402/core': + specifier: workspace:~ + version: link:../../core + '@x402/extensions': + specifier: workspace:~ + version: link:../../extensions + '@x402/paywall': + specifier: workspace:* + version: link:../paywall + devDependencies: + '@eslint/js': + specifier: ^9.24.0 + version: 9.38.0 + '@types/node': + specifier: ^22.13.4 + version: 22.16.0 + '@typescript-eslint/eslint-plugin': + specifier: ^8.29.1 + version: 8.48.0(@typescript-eslint/parser@8.48.0(eslint@9.38.0(jiti@1.21.7))(typescript@5.8.3))(eslint@9.38.0(jiti@1.21.7))(typescript@5.8.3) + '@typescript-eslint/parser': + specifier: ^8.29.1 + version: 8.48.0(eslint@9.38.0(jiti@1.21.7))(typescript@5.8.3) + eslint: + specifier: ^9.24.0 + version: 9.38.0(jiti@1.21.7) + eslint-plugin-import: + specifier: ^2.31.0 + version: 2.32.0(@typescript-eslint/parser@8.48.0(eslint@9.38.0(jiti@1.21.7))(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.38.0(jiti@1.21.7)) + eslint-plugin-jsdoc: + specifier: ^50.6.9 + version: 50.8.0(eslint@9.38.0(jiti@1.21.7)) + eslint-plugin-prettier: + specifier: ^5.2.6 + version: 5.5.4(eslint@9.38.0(jiti@1.21.7))(prettier@3.5.2) + fastify: + specifier: ^5.0.0 + version: 5.8.2 + prettier: + specifier: 3.5.2 + version: 3.5.2 + tsup: + specifier: ^8.4.0 + version: 8.5.0(jiti@1.21.7)(postcss@8.5.6)(tsx@4.20.3)(typescript@5.8.3)(yaml@2.8.1) + tsx: + specifier: ^4.19.2 + version: 4.20.3 + typescript: + specifier: ^5.7.3 + version: 5.8.3 + vite: + specifier: ^6.2.6 + version: 6.4.1(@types/node@22.16.0)(jiti@1.21.7)(tsx@4.20.3)(yaml@2.8.1) + vite-tsconfig-paths: + specifier: ^5.1.4 + version: 5.1.4(typescript@5.8.3)(vite@6.4.1(@types/node@22.16.0)(jiti@1.21.7)(tsx@4.20.3)(yaml@2.8.1)) + vitest: + specifier: ^3.0.5 + version: 3.2.4(@types/debug@4.1.12)(@types/node@22.16.0)(jiti@1.21.7)(jsdom@26.1.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(tsx@4.20.3)(yaml@2.8.1) + ../typescript/packages/http/fetch: dependencies: '@x402/core': @@ -363,7 +424,7 @@ importers: specifier: workspace:~ version: link:../../extensions '@x402/paywall': - specifier: workspace:* + specifier: workspace:^ version: link:../paywall zod: specifier: ^3.24.2 @@ -420,9 +481,6 @@ importers: ../typescript/packages/http/next: dependencies: - '@coinbase/cdp-sdk': - specifier: ^1.22.0 - version: 1.38.4(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3)(utf-8-validate@5.0.10)(ws@8.19.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)) '@x402/core': specifier: workspace:~ version: link:../../core @@ -430,7 +488,7 @@ importers: specifier: workspace:~ version: link:../../extensions '@x402/paywall': - specifier: workspace:* + specifier: workspace:^ version: link:../paywall next: specifier: ^16.0.10 @@ -504,7 +562,7 @@ importers: version: 6.1.0(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3)(utf-8-validate@5.0.10) '@solana/transaction-confirmation': specifier: ^2.1.1 - version: 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3)(ws@8.19.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + version: 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)) '@solana/wallet-standard-features': specifier: ^1.3.0 version: 1.3.0 @@ -923,10 +981,10 @@ importers: dependencies: '@coinbase/cdp-sdk': specifier: ^1.22.0 - version: 1.38.4(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3)(utf-8-validate@5.0.10)(ws@8.19.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + version: 1.38.4(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3)(utf-8-validate@5.0.10)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10)) '@solana/kit': specifier: ^5.0.0 - version: 5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3)(ws@8.19.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + version: 5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10)) hono: specifier: ^4.7.1 version: 4.10.2 @@ -1180,9 +1238,6 @@ importers: '@x402/core': specifier: workspace:~ version: link:../../core - '@x402/extensions': - specifier: workspace:~ - version: link:../../extensions viem: specifier: ^2.39.3 version: 2.40.3(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.25.71) @@ -1236,6 +1291,61 @@ importers: specifier: ^3.0.5 version: 3.2.4(@types/debug@4.1.12)(@types/node@22.16.0)(jiti@1.21.7)(jsdom@26.1.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(tsx@4.20.3)(yaml@2.8.1) + ../typescript/packages/mechanisms/stellar: + dependencies: + '@stellar/stellar-sdk': + specifier: ^14.6.1 + version: 14.6.1 + '@x402/core': + specifier: workspace:* + version: link:../../core + devDependencies: + '@eslint/js': + specifier: ^9.24.0 + version: 9.38.0 + '@types/node': + specifier: ^22.13.4 + version: 22.16.0 + '@typescript-eslint/eslint-plugin': + specifier: ^8.29.1 + version: 8.48.0(@typescript-eslint/parser@8.48.0(eslint@9.38.0(jiti@1.21.7))(typescript@5.8.3))(eslint@9.38.0(jiti@1.21.7))(typescript@5.8.3) + '@typescript-eslint/parser': + specifier: ^8.29.1 + version: 8.48.0(eslint@9.38.0(jiti@1.21.7))(typescript@5.8.3) + eslint: + specifier: ^9.24.0 + version: 9.38.0(jiti@1.21.7) + eslint-plugin-import: + specifier: ^2.31.0 + version: 2.32.0(@typescript-eslint/parser@8.48.0(eslint@9.38.0(jiti@1.21.7))(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.38.0(jiti@1.21.7)) + eslint-plugin-jsdoc: + specifier: ^50.6.9 + version: 50.8.0(eslint@9.38.0(jiti@1.21.7)) + eslint-plugin-prettier: + specifier: ^5.2.6 + version: 5.5.4(eslint@9.38.0(jiti@1.21.7))(prettier@3.5.2) + prettier: + specifier: 3.5.2 + version: 3.5.2 + tsup: + specifier: ^8.4.0 + version: 8.5.0(jiti@1.21.7)(postcss@8.5.6)(tsx@4.20.3)(typescript@5.8.3)(yaml@2.8.1) + tsx: + specifier: ^4.19.2 + version: 4.20.3 + typescript: + specifier: ^5.7.3 + version: 5.8.3 + vite: + specifier: ^6.2.6 + version: 6.4.1(@types/node@22.16.0)(jiti@1.21.7)(tsx@4.20.3)(yaml@2.8.1) + vite-tsconfig-paths: + specifier: ^5.1.4 + version: 5.1.4(typescript@5.8.3)(vite@6.4.1(@types/node@22.16.0)(jiti@1.21.7)(tsx@4.20.3)(yaml@2.8.1)) + vitest: + specifier: ^3.0.5 + version: 3.2.4(@types/debug@4.1.12)(@types/node@22.16.0)(jiti@1.21.7)(jsdom@26.1.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(tsx@4.20.3)(yaml@2.8.1) + ../typescript/packages/mechanisms/svm: dependencies: '@solana-program/compute-budget': @@ -1326,6 +1436,9 @@ importers: '@x402/evm': specifier: workspace:* version: link:../../../typescript/packages/mechanisms/evm + '@x402/stellar': + specifier: workspace:* + version: link:../../../typescript/packages/mechanisms/stellar '@x402/svm': specifier: workspace:* version: link:../../../typescript/packages/mechanisms/svm @@ -1393,6 +1506,9 @@ importers: '@x402/fetch': specifier: workspace:* version: link:../../../typescript/packages/http/fetch + '@x402/stellar': + specifier: workspace:* + version: link:../../../typescript/packages/mechanisms/stellar '@x402/svm': specifier: workspace:* version: link:../../../typescript/packages/mechanisms/svm @@ -1509,6 +1625,9 @@ importers: '@x402/extensions': specifier: workspace:* version: link:../../../typescript/packages/extensions + '@x402/stellar': + specifier: workspace:* + version: link:../../../typescript/packages/mechanisms/stellar '@x402/svm': specifier: workspace:* version: link:../../../typescript/packages/mechanisms/svm @@ -1827,6 +1946,9 @@ importers: '@x402/extensions': specifier: workspace:* version: link:../../../typescript/packages/extensions + '@x402/stellar': + specifier: workspace:* + version: link:../../../typescript/packages/mechanisms/stellar '@x402/svm': specifier: workspace:* version: link:../../../typescript/packages/mechanisms/svm @@ -1874,6 +1996,70 @@ importers: specifier: ^5.3.0 version: 5.8.3 + servers/fastify: + dependencies: + '@x402/aptos': + specifier: workspace:* + version: link:../../../typescript/packages/mechanisms/aptos + '@x402/core': + specifier: workspace:* + version: link:../../../typescript/packages/core + '@x402/evm': + specifier: workspace:* + version: link:../../../typescript/packages/mechanisms/evm + '@x402/extensions': + specifier: workspace:* + version: link:../../../typescript/packages/extensions + '@x402/fastify': + specifier: workspace:* + version: link:../../../typescript/packages/http/fastify + '@x402/stellar': + specifier: workspace:* + version: link:../../../typescript/packages/mechanisms/stellar + '@x402/svm': + specifier: workspace:* + version: link:../../../typescript/packages/mechanisms/svm + dotenv: + specifier: ^16.6.1 + version: 16.6.1 + fastify: + specifier: ^5.3.3 + version: 5.8.2 + devDependencies: + '@eslint/js': + specifier: ^9.24.0 + version: 9.38.0 + '@typescript-eslint/eslint-plugin': + specifier: ^8.29.1 + version: 8.48.0(@typescript-eslint/parser@8.48.0(eslint@9.38.0(jiti@1.21.7))(typescript@5.8.3))(eslint@9.38.0(jiti@1.21.7))(typescript@5.8.3) + '@typescript-eslint/parser': + specifier: ^8.29.1 + version: 8.48.0(eslint@9.38.0(jiti@1.21.7))(typescript@5.8.3) + eslint: + specifier: ^9.24.0 + version: 9.38.0(jiti@1.21.7) + eslint-plugin-import: + specifier: ^2.31.0 + version: 2.32.0(@typescript-eslint/parser@8.48.0(eslint@9.38.0(jiti@1.21.7))(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.38.0(jiti@1.21.7)) + eslint-plugin-jsdoc: + specifier: ^50.6.9 + version: 50.8.0(eslint@9.38.0(jiti@1.21.7)) + eslint-plugin-prettier: + specifier: ^5.2.6 + version: 5.5.4(eslint@9.38.0(jiti@1.21.7))(prettier@3.5.2) + prettier: + specifier: 3.5.2 + version: 3.5.2 + tsup: + specifier: ^7.2.0 + version: 7.3.0(postcss@8.5.6)(typescript@5.8.3) + tsx: + specifier: ^4.7.0 + version: 4.20.3 + typescript: + specifier: ^5.3.0 + version: 5.8.3 + servers/hono: dependencies: '@hono/node-server': @@ -1894,6 +2080,9 @@ importers: '@x402/hono': specifier: workspace:* version: link:../../../typescript/packages/http/hono + '@x402/stellar': + specifier: workspace:* + version: link:../../../typescript/packages/mechanisms/stellar '@x402/svm': specifier: workspace:* version: link:../../../typescript/packages/mechanisms/svm @@ -2019,6 +2208,9 @@ importers: '@x402/next': specifier: workspace:* version: link:../../../typescript/packages/http/next + '@x402/stellar': + specifier: workspace:* + version: link:../../../typescript/packages/mechanisms/stellar '@x402/svm': specifier: workspace:* version: link:../../../typescript/packages/mechanisms/svm @@ -3114,6 +3306,24 @@ packages: peerDependencies: typescript: 5.8.3 + '@fastify/ajv-compiler@4.0.5': + resolution: {integrity: sha512-KoWKW+MhvfTRWL4qrhUwAAZoaChluo0m0vbiJlGMt2GXvL4LVPQEjt8kSpHI3IBq5Rez8fg+XeH3cneztq+C7A==} + + '@fastify/error@4.2.0': + resolution: {integrity: sha512-RSo3sVDXfHskiBZKBPRgnQTtIqpi/7zhJOEmAxCiBcM7d0uwdGdxLlsCaLzGs8v8NnxIRlfG0N51p5yFaOentQ==} + + '@fastify/fast-json-stringify-compiler@5.0.3': + resolution: {integrity: sha512-uik7yYHkLr6fxd8hJSZ8c+xF4WafPK+XzneQDPU+D10r5X19GW8lJcom2YijX2+qtFF1ENJlHXKFM9ouXNJYgQ==} + + '@fastify/forwarded@3.0.1': + resolution: {integrity: sha512-JqDochHFqXs3C3Ml3gOY58zM7OqO9ENqPo0UqAjAjH8L01fRZqwX9iLeX34//kiJubF7r2ZQHtBRU36vONbLlw==} + + '@fastify/merge-json-schemas@0.2.1': + resolution: {integrity: sha512-OA3KGBCy6KtIvLf8DINC5880o5iBlDX4SxzLQS8HorJAbqluzLRn80UXU0bxZn7UOFhFgpRJDasfwn9nG4FG4A==} + + '@fastify/proxy-addr@5.1.0': + resolution: {integrity: sha512-INS+6gh91cLUjB+PVHfu1UqcB76Sqtpyp7bnL+FYojhjygvOPA9ctiD/JDKsyD9Xgu4hUhCSJBPig/w7duNajw==} + '@gemini-wallet/core@0.2.0': resolution: {integrity: sha512-vv9aozWnKrrPWQ3vIFcWk7yta4hQW1Ie0fsNNPeXnjAxkbXr2hqMagEptLuMxpEP2W3mnRu05VDNKzcvAuuZDw==} peerDependencies: @@ -3584,6 +3794,9 @@ packages: resolution: {integrity: sha512-IHnV6A+zxU7XwmKFinmYjUcwlyK9+xkG3/s9KcQhI9BjQKycrJ1JRO+FbNYPwZiPKW3je/DR0k7w8/gLa5eaxQ==} deprecated: 'The package is now available as "qr": npm install qr' + '@pinojs/redact@0.4.0': + resolution: {integrity: sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==} + '@pkgjs/parseargs@0.11.0': resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} engines: {node: '>=14'} @@ -5085,6 +5298,18 @@ packages: '@stablelib/wipe@1.0.1': resolution: {integrity: sha512-WfqfX/eXGiAd3RJe4VU2snh/ZPwtSjLG4ynQ/vYzvghTh7dHFcI1wl+nrkWG6lGhukOxOsUHfv8dUXr58D0ayg==} + '@stellar/js-xdr@3.1.2': + resolution: {integrity: sha512-VVolPL5goVEIsvuGqDc5uiKxV03lzfWdvYg1KikvwheDmTBO68CKDji3bAZ/kppZrx5iTA8z3Ld5yuytcvhvOQ==} + + '@stellar/stellar-base@14.1.0': + resolution: {integrity: sha512-A8kFli6QGy22SRF45IjgPAJfUNGjnI+R7g4DF5NZYVsD1kGf7B4ITyc4OPclLV9tqNI4/lXxafGEw0JEUbHixw==} + engines: {node: '>=20.0.0'} + + '@stellar/stellar-sdk@14.6.1': + resolution: {integrity: sha512-A1rQWDLdUasXkMXnYSuhgep+3ZZzyuXJKdt5/KAIc0gkmSp906HTvUpbT4pu+bVr41tu0+J4Ugz9J4BQAGGytg==} + engines: {node: '>=20.0.0'} + hasBin: true + '@svgr/babel-plugin-add-jsx-attribute@8.0.0': resolution: {integrity: sha512-b9MIk7yhdS1pMCZM8VeNfUlSKVRhsHZNMl5O9SfaX0l0t5wjdgu4IDzGB8bpnGBBOjGST3rRFVsaaEtI4W6f7g==} engines: {node: '>=14'} @@ -5713,6 +5938,9 @@ packages: zod: optional: true + abstract-logging@2.0.1: + resolution: {integrity: sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA==} + accepts@1.3.8: resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==} engines: {node: '>= 0.6'} @@ -5863,6 +6091,9 @@ packages: resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} engines: {node: '>= 0.4'} + avvio@9.2.0: + resolution: {integrity: sha512-2t/sy01ArdHHE0vRH5Hsay+RtCZt3dLPji7W7/MMOCEgze5b7SNDC4j5H6FnVgPkI1MTNFGzHdHrVXDDl7QSSQ==} + axe-core@4.11.0: resolution: {integrity: sha512-ilYanEU8vxxBexpJd8cWM4ElSQq4QctCLKih0TSfjIfCQTeyH/6zVrmIJfLPrKTKJRbiG+cfnZbQIjAlJmF1jQ==} engines: {node: '>=4'} @@ -5878,6 +6109,9 @@ packages: axios@1.12.2: resolution: {integrity: sha512-vMJzPewAlRyOgxV2dU0Cuz2O8zzzx9VYtbJOaBgXFeLc4IV/Eg50n4LowmehOOR61S8ZMpc2K5Sa7g6A4jfkUw==} + axios@1.13.4: + resolution: {integrity: sha512-1wVkUaAO6WyaYtCkcYCOx12ZgpGf9Zif+qXa4n+oYzK558YryKqiL6UWwd5DqiH3VRW0GYhTZQ/vlgJrCoNQlg==} + axobject-query@4.1.0: resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==} engines: {node: '>= 0.4'} @@ -5906,6 +6140,10 @@ packages: base-x@5.0.1: resolution: {integrity: sha512-M7uio8Zt++eg3jPj+rHMfCC+IuygQHHCOU+IYsVtik6FWjuYpVt/+MRKcgsAMHh8mMFAwnB+Bs+mTrFiXjMzKg==} + base32.js@0.1.0: + resolution: {integrity: sha512-n3TkB02ixgBOhTvANakDb4xaMXnYUVkNoRFJjQflcqMQhyEKxEHdj3E6N8t8sUQ0mjH/3/JxzlXuz3ul/J90pQ==} + engines: {node: '>=0.12.0'} + base64-js@1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} @@ -5916,6 +6154,9 @@ packages: big.js@6.2.2: resolution: {integrity: sha512-y/ie+Faknx7sZA5MfGA2xKlu0GDv8RWrXGsmlteyJQ2lvoKv9GBK/fpRMc2qlSoBAgNxrixICFCBefIq8WCQpQ==} + bignumber.js@9.3.1: + resolution: {integrity: sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==} + binary-extensions@2.3.0: resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} engines: {node: '>=8'} @@ -6189,6 +6430,10 @@ packages: resolution: {integrity: sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==} engines: {node: '>= 0.6'} + cookie@1.1.1: + resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==} + engines: {node: '>=18'} + core-js-compat@3.46.0: resolution: {integrity: sha512-p9hObIIEENxSV8xIu+V68JjSeARg6UVMG5mR+JEUguG3sI6MsiS1njz2jHmyJDvA+8jX/sytkBHup6kxhM9law==} @@ -6386,6 +6631,10 @@ packages: resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} engines: {node: '>= 0.8'} + dequal@2.0.3: + resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} + engines: {node: '>=6'} + derive-valtio@0.1.0: resolution: {integrity: sha512-OCg2UsLbXK7GmmpzMXhYkdO64vhJ1ROUUGaTFyHjVwEdMEcTTRj7W1TxLbSBxdY8QLBPCcp66MTyaSy0RpO17A==} peerDependencies: @@ -6771,6 +7020,10 @@ packages: resolution: {integrity: sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==} engines: {node: '>=18.0.0'} + eventsource@2.0.2: + resolution: {integrity: sha512-IzUmBGPR3+oUG9dUeXynyNmf91/3zUSJg1lCktzKw47OXuhco54U3r9B7O4XX+Rb1Itm9OZ2b0RkTs10bICOxA==} + engines: {node: '>=12.0.0'} + eventsource@3.0.7: resolution: {integrity: sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==} engines: {node: '>=18.0.0'} @@ -6808,6 +7061,9 @@ packages: resolution: {integrity: sha512-GipyPsXO1anza0AOZdy69Im7hGFCNB7Y/NGjDlZGJ3GJJLtwNSb2vrzYrTYJRrRloVx7pl+bhUaTB8yiccPvFQ==} engines: {node: '> 0.1.90'} + fast-decode-uri-component@1.0.1: + resolution: {integrity: sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg==} + fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} @@ -6825,9 +7081,15 @@ packages: fast-json-stable-stringify@2.1.0: resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + fast-json-stringify@6.3.0: + resolution: {integrity: sha512-oRCntNDY/329HJPlmdNLIdogNtt6Vyjb1WuT01Soss3slIdyUp8kAcDU3saQTOquEK8KFVfwIIF7FebxUAu+yA==} + fast-levenshtein@2.0.6: resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + fast-querystring@1.1.2: + resolution: {integrity: sha512-g6KuKWmFXc0fID8WWH0jit4g0AGBoJhCkJMb1RmbsSEUNvQ+ZC8D6CUZ+GtF8nMzSPXnhiePyyqqipzNNEnHjg==} + fast-redact@3.5.0: resolution: {integrity: sha512-dwsoQlS7h9hMeYUq1W++23NDcBLV4KqONnITDV9DjfS3q1SgDGVrBdvvTLUotWtPSD7asWDV9/CmsZPy8Hf70A==} engines: {node: '>=6'} @@ -6844,6 +7106,9 @@ packages: fastestsmallesttextencoderdecoder@1.0.22: resolution: {integrity: sha512-Pb8d48e+oIuY4MaM64Cd7OW1gt4nxCHs7/ddPPZ/Ic3sg8yVGM7O9wDvZ7us6ScaUupzM+pfBolwtYhN1IxBIw==} + fastify@5.8.2: + resolution: {integrity: sha512-lZmt3navvZG915IE+f7/TIVamxIwmBd+OMB+O9WBzcpIwOo6F0LTh0sluoMFk5VkrKTvvrwIaoJPkir4Z+jtAg==} + fastq@1.19.1: resolution: {integrity: sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==} @@ -6856,6 +7121,9 @@ packages: picomatch: optional: true + feaxios@0.0.23: + resolution: {integrity: sha512-eghR0A21fvbkcQBgZuMfQhrXxJzC0GNUGC9fXhBge33D+mFDTwl0aJ35zoQQn575BhyjQitRc5N4f+L4cP708g==} + file-entry-cache@8.0.0: resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} engines: {node: '>=16.0.0'} @@ -6876,6 +7144,10 @@ packages: resolution: {integrity: sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==} engines: {node: '>= 18.0.0'} + find-my-way@9.5.0: + resolution: {integrity: sha512-VW2RfnmscZO5KgBY5XVyKREMW5nMZcxDy+buTOsL+zIPnBlbKm+00sgzoQzq1EVh4aALZLfKdwv6atBGcjvjrQ==} + engines: {node: '>=20'} + find-up@4.1.0: resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} engines: {node: '>=8'} @@ -7168,6 +7440,10 @@ packages: resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} engines: {node: '>= 0.10'} + ipaddr.js@2.3.0: + resolution: {integrity: sha512-Zv/pA+ciVFbCSBBjGfaKUya/CcGmUHzTydLMaTwrUUEM2DIEO3iZvueGxmacvmN50fGpGVKeTXpb2LcYQxeVdg==} + engines: {node: '>= 10'} + iron-webcrypto@1.2.1: resolution: {integrity: sha512-feOM6FaSr6rEABp/eDfVseKyTMDt+KGpeB35SkVn9Tyn0CqvVsY3EwI0v5i8nMHyJnzCIQf7nsy3p41TPkJZhg==} @@ -7270,6 +7546,10 @@ packages: resolution: {integrity: sha512-XVm7LOeLpTW4jV19QSH38vkswxoLud8sQ57YwJVTPWdiaI9I8keEhGFpBlslyVsgdQy4Opg8QOLb8YRgsyZiQg==} engines: {node: '>=10'} + is-retry-allowed@3.0.0: + resolution: {integrity: sha512-9xH0xvoggby+u0uGF7cZXdrutWiBiaFG8ZT4YFPXL8NzkyAwX3AKGLeFQLvzDpM430+nDFBZ1LHkie/8ocL06A==} + engines: {node: '>=12'} + is-set@2.0.3: resolution: {integrity: sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==} engines: {node: '>= 0.4'} @@ -7400,6 +7680,9 @@ packages: json-rpc-random-id@1.0.1: resolution: {integrity: sha512-RJ9YYNCkhVDBuP4zN5BBtYAzEl03yq/jIIsyif0JY9qyJuQQZNeDK7anAPKKlyEtLSj2s8h6hNh2F8zO5q7ScA==} + json-schema-ref-resolver@3.0.0: + resolution: {integrity: sha512-hOrZIVL5jyYFjzk7+y7n5JDzGlU8rfWDuYyHwGa2WA8/pcmMHezp2xsVwxrebD/Q9t8Nc5DboieySDpCp4WG4A==} + json-schema-traverse@0.4.1: resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} @@ -7457,6 +7740,9 @@ packages: resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} engines: {node: '>= 0.8.0'} + light-my-request@6.6.0: + resolution: {integrity: sha512-CHYbu8RtboSIoVsHZ6Ye4cj4Aw/yg2oAFimlF7mNvfDV192LR7nDiKtSIfCuLT7KokPSTn/9kfVLm5OGN0A28A==} + lilconfig@3.1.3: resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==} engines: {node: '>=14'} @@ -7806,6 +8092,10 @@ packages: on-exit-leak-free@0.2.0: resolution: {integrity: sha512-dqaz3u44QbRXQooZLTUKU41ZrzYrcvLISVgbrzbyCMxpmSLJvZ3ZamIJIZ29P6OhZIkNIQKosdeM6t1LYbA9hg==} + on-exit-leak-free@2.1.2: + resolution: {integrity: sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==} + engines: {node: '>=14.0.0'} + on-finished@2.4.1: resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} engines: {node: '>= 0.8'} @@ -8001,9 +8291,19 @@ packages: pino-abstract-transport@0.5.0: resolution: {integrity: sha512-+KAgmVeqXYbTtU2FScx1XS3kNyfZ5TrXY07V96QnUSFqo2gAqlvmaxH67Lj7SWazqsMabf+58ctdTcBgnOLUOQ==} + pino-abstract-transport@3.0.0: + resolution: {integrity: sha512-wlfUczU+n7Hy/Ha5j9a/gZNy7We5+cXp8YL+X+PG8S0KXxw7n/JXA3c46Y0zQznIJ83URJiwy7Lh56WLokNuxg==} + pino-std-serializers@4.0.0: resolution: {integrity: sha512-cK0pekc1Kjy5w9V2/n+8MkZwusa6EyyxfeQCB799CQRhRt/CqYKiWs5adeu8Shve2ZNffvfC/7J64A2PJo1W/Q==} + pino-std-serializers@7.1.0: + resolution: {integrity: sha512-BndPH67/JxGExRgiX1dX0w1FvZck5Wa4aal9198SrRhZjH3GxKQUKIBnYJTdj2HDN3UQAS06HlfcSbQj2OHmaw==} + + pino@10.3.1: + resolution: {integrity: sha512-r34yH/GlQpKZbU1BvFFqOjhISRo1MNx1tWYsYvmj6KIRHSPMT2+yHOEb1SG6NMvRoHRF0a07kCOox/9yakl1vg==} + hasBin: true + pino@7.11.0: resolution: {integrity: sha512-dMACeu63HtRLmCG8VKdy4cShCPKaYDR4youZqoSWLxl5Gu99HUw8bw75thbPv9Nip+H+QYX8o3ZJbTdVZZ2TVg==} hasBin: true @@ -8142,6 +8442,12 @@ packages: process-warning@1.0.0: resolution: {integrity: sha512-du4wfLyj4yCZq1VupnVSZmRsPJsNuxoDQFdCFHLaYiEbFBD7QE0a+I4D7hOxrVnh78QE/YipFAj9lXHiXocV+Q==} + process-warning@4.0.1: + resolution: {integrity: sha512-3c2LzQ3rY9d0hc1emcsHhfT9Jwz0cChib/QN89oME2R451w5fy3f0afAhERFZAwrbDU43wk12d0ORBpDVME50Q==} + + process-warning@5.0.0: + resolution: {integrity: sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA==} + prompts@2.4.2: resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==} engines: {node: '>= 6'} @@ -8274,6 +8580,10 @@ packages: resolution: {integrity: sha512-r/H9MzAWtrv8aSVjPCMFpDMl5q66GqtmmRkRjpHTsp4zBAa+snZyiQNlMONiUmEJcsnaw0wCauJ2GWODr/aFkg==} engines: {node: '>= 12.13.0'} + real-require@0.2.0: + resolution: {integrity: sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==} + engines: {node: '>= 12.13.0'} + reflect.getprototypeof@1.0.10: resolution: {integrity: sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==} engines: {node: '>= 0.4'} @@ -8337,10 +8647,17 @@ packages: responselike@2.0.1: resolution: {integrity: sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw==} + ret@0.5.0: + resolution: {integrity: sha512-I1XxrZSQ+oErkRR4jYbAyEEu2I0avBvvMM5JN+6EBprOGRCs63ENqZ3vjavq8fBw2+62G5LF5XelKwuJpcvcxw==} + engines: {node: '>=10'} + reusify@1.1.0: resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + rfdc@1.4.1: + resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==} + ripemd160@2.0.3: resolution: {integrity: sha512-5Di9UC0+8h1L6ZD2d7awM7E/T4uA1fJRlx6zk/NvdCCVEoAnFqvHmCuNeIKoCeIixBX/q8uM+6ycDvF8woqosA==} engines: {node: '>= 0.8'} @@ -8381,6 +8698,10 @@ packages: resolution: {integrity: sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==} engines: {node: '>= 0.4'} + safe-regex2@5.1.0: + resolution: {integrity: sha512-pNHAuBW7TrcleFHsxBr5QMi/Iyp0ENjUKz7GCcX1UO7cMh+NmVK6HxQckNL1tJp1XAJVjG6B8OKIPqodqj9rtw==} + hasBin: true + safe-stable-stringify@2.5.0: resolution: {integrity: sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==} engines: {node: '>=10'} @@ -8395,6 +8716,9 @@ packages: scheduler@0.27.0: resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} + secure-json-parse@4.1.0: + resolution: {integrity: sha512-l4KnYfEyqYJxDwlNVyRfO2E4NTHfMKAWdUuA8J0yve2Dz/E/PdBepY03RvyJpssIpRFwJoCD55wA+mEDs6ByWA==} + semver@6.3.1: resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} hasBin: true @@ -8423,6 +8747,9 @@ packages: set-blocking@2.0.0: resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==} + set-cookie-parser@2.7.2: + resolution: {integrity: sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==} + set-function-length@1.2.2: resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} engines: {node: '>= 0.4'} @@ -8507,6 +8834,9 @@ packages: sonic-boom@2.8.0: resolution: {integrity: sha512-kuonw1YOYYNOve5iHdSahXPOK49GqwA+LZhI6Wz/l0rP57iKyXXIHaRagOBHAPmGwJC6od2Z9zgvZ5loSgMlVg==} + sonic-boom@4.2.1: + resolution: {integrity: sha512-w6AxtubXa2wTXAUsZMMWERrsIRAdrK0Sc+FUytWvYAhBJLyuI4llrMIC1DtlNSdI99EI86KZum2MMq3EAZlF9Q==} + source-map-js@1.2.1: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} @@ -8700,6 +9030,10 @@ packages: thread-stream@0.15.2: resolution: {integrity: sha512-UkEhKIg2pD+fjkHQKyJO3yoIvAP3N6RlNFt2dUhcS1FGvCD1cQa1M/PGknCLFIyZdtJOWQjejp7bdNqmN7zwdA==} + thread-stream@4.0.0: + resolution: {integrity: sha512-4iMVL6HAINXWf1ZKZjIPcz5wYaOdPhtO8ATvZ+Xqp3BTdaqtAwQkNmKORqcIo5YkQqGXq5cwfswDwMqqQNrpJA==} + engines: {node: '>=20'} + tinybench@2.9.0: resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} @@ -8737,10 +9071,17 @@ packages: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} + toad-cache@3.7.0: + resolution: {integrity: sha512-/m8M+2BJUpoJdgAHoG+baCwBT+tf2VraSfkBgl0Y00qIWt41DJ8R5B8nsEw0I58YwF5IZH6z24/2TobDKnqSWw==} + engines: {node: '>=12'} + toidentifier@1.0.1: resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} engines: {node: '>=0.6'} + toml@3.0.0: + resolution: {integrity: sha512-y/mWCZinnvxjTKYhJ+pYxwD0mRLVvOtdS2Awbgxln6iEnt4rk0yBxeSBHkGJcPucRiG0e55mwWp+g/05rsrd6w==} + tough-cookie@5.1.2: resolution: {integrity: sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==} engines: {node: '>=16'} @@ -8993,6 +9334,9 @@ packages: uri-js@4.4.1: resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + urijs@1.19.11: + resolution: {integrity: sha512-HXgFDgDommxn5/bIv0cnQZsPhHDA90NPHD6+c/v21U5+Sx5hoP8+dP9IZXBU1gIfvdRfhG8cel9QNPeionfcCQ==} + use-sync-external-store@1.2.0: resolution: {integrity: sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==} peerDependencies: @@ -10254,6 +10598,29 @@ snapshots: - utf-8-validate - zod + '@coinbase/cdp-sdk@1.38.4(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3)(utf-8-validate@5.0.10)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10))': + dependencies: + '@solana-program/system': 0.8.1(@solana/kit@3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10))) + '@solana-program/token': 0.6.0(@solana/kit@3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10))) + '@solana/kit': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + '@solana/web3.js': 1.98.4(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10) + abitype: 1.0.6(typescript@5.8.3)(zod@3.25.71) + axios: 1.13.4 + axios-retry: 4.5.0(axios@1.13.4) + jose: 6.1.3 + md5: 2.3.0 + uncrypto: 0.1.3 + viem: 2.45.1(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.25.71) + zod: 3.25.71 + transitivePeerDependencies: + - bufferutil + - debug + - encoding + - fastestsmallesttextencoderdecoder + - typescript + - utf-8-validate + - ws + '@coinbase/cdp-sdk@1.38.4(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3)(utf-8-validate@5.0.10)(ws@8.19.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))': dependencies: '@solana-program/system': 0.8.1(@solana/kit@3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3)(ws@8.19.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))) @@ -10261,8 +10628,8 @@ snapshots: '@solana/kit': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3)(ws@8.19.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)) '@solana/web3.js': 1.98.4(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10) abitype: 1.0.6(typescript@5.8.3)(zod@3.25.71) - axios: 1.12.2 - axios-retry: 4.5.0(axios@1.12.2) + axios: 1.13.4 + axios-retry: 4.5.0(axios@1.13.4) jose: 6.1.3 md5: 2.3.0 uncrypto: 0.1.3 @@ -10704,6 +11071,29 @@ snapshots: typescript: 5.8.3 zod: 3.25.71 + '@fastify/ajv-compiler@4.0.5': + dependencies: + ajv: 8.17.1 + ajv-formats: 3.0.1(ajv@8.17.1) + fast-uri: 3.1.0 + + '@fastify/error@4.2.0': {} + + '@fastify/fast-json-stringify-compiler@5.0.3': + dependencies: + fast-json-stringify: 6.3.0 + + '@fastify/forwarded@3.0.1': {} + + '@fastify/merge-json-schemas@0.2.1': + dependencies: + dequal: 2.0.3 + + '@fastify/proxy-addr@5.1.0': + dependencies: + '@fastify/forwarded': 3.0.1 + ipaddr.js: 2.3.0 + '@gemini-wallet/core@0.2.0(viem@2.31.6(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10)(zod@3.25.71))': dependencies: '@metamask/rpc-errors': 7.0.2 @@ -11232,6 +11622,8 @@ snapshots: '@paulmillr/qr@0.2.1': {} + '@pinojs/redact@0.4.0': {} + '@pkgjs/parseargs@0.11.0': optional: true @@ -11867,6 +12259,10 @@ snapshots: dependencies: '@solana/kit': 6.1.0(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3)(utf-8-validate@5.0.10) + '@solana-program/system@0.8.1(@solana/kit@3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10)))': + dependencies: + '@solana/kit': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + '@solana-program/system@0.8.1(@solana/kit@3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3)(ws@8.19.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)))': dependencies: '@solana/kit': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3)(ws@8.19.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)) @@ -11890,6 +12286,10 @@ snapshots: dependencies: '@solana/kit': 6.1.0(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3)(utf-8-validate@5.0.10) + '@solana-program/token@0.6.0(@solana/kit@3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10)))': + dependencies: + '@solana/kit': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + '@solana-program/token@0.6.0(@solana/kit@3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3)(ws@8.19.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)))': dependencies: '@solana/kit': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3)(ws@8.19.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)) @@ -12422,6 +12822,32 @@ snapshots: transitivePeerDependencies: - fastestsmallesttextencoderdecoder + '@solana/kit@3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10))': + dependencies: + '@solana/accounts': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3) + '@solana/addresses': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3) + '@solana/codecs': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3) + '@solana/errors': 3.0.3(typescript@5.8.3) + '@solana/functional': 3.0.3(typescript@5.8.3) + '@solana/instruction-plans': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3) + '@solana/instructions': 3.0.3(typescript@5.8.3) + '@solana/keys': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3) + '@solana/programs': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3) + '@solana/rpc': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3) + '@solana/rpc-parsed-types': 3.0.3(typescript@5.8.3) + '@solana/rpc-spec-types': 3.0.3(typescript@5.8.3) + '@solana/rpc-subscriptions': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + '@solana/rpc-types': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3) + '@solana/signers': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3) + '@solana/sysvars': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3) + '@solana/transaction-confirmation': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + '@solana/transaction-messages': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3) + '@solana/transactions': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3) + typescript: 5.8.3 + transitivePeerDependencies: + - fastestsmallesttextencoderdecoder + - ws + '@solana/kit@3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3)(ws@8.19.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))': dependencies: '@solana/accounts': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3) @@ -12448,6 +12874,32 @@ snapshots: - fastestsmallesttextencoderdecoder - ws + '@solana/kit@5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10))': + dependencies: + '@solana/accounts': 5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3) + '@solana/addresses': 5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3) + '@solana/codecs': 5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3) + '@solana/errors': 5.0.0(typescript@5.8.3) + '@solana/functional': 5.0.0(typescript@5.8.3) + '@solana/instruction-plans': 5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3) + '@solana/instructions': 5.0.0(typescript@5.8.3) + '@solana/keys': 5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3) + '@solana/programs': 5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3) + '@solana/rpc': 5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3) + '@solana/rpc-parsed-types': 5.0.0(typescript@5.8.3) + '@solana/rpc-spec-types': 5.0.0(typescript@5.8.3) + '@solana/rpc-subscriptions': 5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + '@solana/rpc-types': 5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3) + '@solana/signers': 5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3) + '@solana/sysvars': 5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3) + '@solana/transaction-confirmation': 5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + '@solana/transaction-messages': 5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3) + '@solana/transactions': 5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3) + typescript: 5.8.3 + transitivePeerDependencies: + - fastestsmallesttextencoderdecoder + - ws + '@solana/kit@5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3)(ws@8.19.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))': dependencies: '@solana/accounts': 5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3) @@ -12945,14 +13397,23 @@ snapshots: transitivePeerDependencies: - fastestsmallesttextencoderdecoder - '@solana/rpc-subscriptions-channel-websocket@2.3.0(typescript@5.8.3)(ws@8.19.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))': + '@solana/rpc-subscriptions-channel-websocket@2.3.0(typescript@5.8.3)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))': dependencies: '@solana/errors': 2.3.0(typescript@5.8.3) '@solana/functional': 2.3.0(typescript@5.8.3) '@solana/rpc-subscriptions-spec': 2.3.0(typescript@5.8.3) '@solana/subscribable': 2.3.0(typescript@5.8.3) typescript: 5.8.3 - ws: 8.19.0(bufferutil@4.0.9)(utf-8-validate@5.0.10) + ws: 8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10) + + '@solana/rpc-subscriptions-channel-websocket@3.0.3(typescript@5.8.3)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10))': + dependencies: + '@solana/errors': 3.0.3(typescript@5.8.3) + '@solana/functional': 3.0.3(typescript@5.8.3) + '@solana/rpc-subscriptions-spec': 3.0.3(typescript@5.8.3) + '@solana/subscribable': 3.0.3(typescript@5.8.3) + typescript: 5.8.3 + ws: 7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10) '@solana/rpc-subscriptions-channel-websocket@3.0.3(typescript@5.8.3)(ws@8.19.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))': dependencies: @@ -12963,6 +13424,15 @@ snapshots: typescript: 5.8.3 ws: 8.19.0(bufferutil@4.0.9)(utf-8-validate@5.0.10) + '@solana/rpc-subscriptions-channel-websocket@5.0.0(typescript@5.8.3)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10))': + dependencies: + '@solana/errors': 5.0.0(typescript@5.8.3) + '@solana/functional': 5.0.0(typescript@5.8.3) + '@solana/rpc-subscriptions-spec': 5.0.0(typescript@5.8.3) + '@solana/subscribable': 5.0.0(typescript@5.8.3) + typescript: 5.8.3 + ws: 7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10) + '@solana/rpc-subscriptions-channel-websocket@5.0.0(typescript@5.8.3)(ws@8.19.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))': dependencies: '@solana/errors': 5.0.0(typescript@5.8.3) @@ -13038,7 +13508,7 @@ snapshots: optionalDependencies: typescript: 5.8.3 - '@solana/rpc-subscriptions@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3)(ws@8.19.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))': + '@solana/rpc-subscriptions@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))': dependencies: '@solana/errors': 2.3.0(typescript@5.8.3) '@solana/fast-stable-stringify': 2.3.0(typescript@5.8.3) @@ -13046,7 +13516,7 @@ snapshots: '@solana/promises': 2.3.0(typescript@5.8.3) '@solana/rpc-spec-types': 2.3.0(typescript@5.8.3) '@solana/rpc-subscriptions-api': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3) - '@solana/rpc-subscriptions-channel-websocket': 2.3.0(typescript@5.8.3)(ws@8.19.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + '@solana/rpc-subscriptions-channel-websocket': 2.3.0(typescript@5.8.3)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)) '@solana/rpc-subscriptions-spec': 2.3.0(typescript@5.8.3) '@solana/rpc-transformers': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3) '@solana/rpc-types': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3) @@ -13056,6 +13526,24 @@ snapshots: - fastestsmallesttextencoderdecoder - ws + '@solana/rpc-subscriptions@3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10))': + dependencies: + '@solana/errors': 3.0.3(typescript@5.8.3) + '@solana/fast-stable-stringify': 3.0.3(typescript@5.8.3) + '@solana/functional': 3.0.3(typescript@5.8.3) + '@solana/promises': 3.0.3(typescript@5.8.3) + '@solana/rpc-spec-types': 3.0.3(typescript@5.8.3) + '@solana/rpc-subscriptions-api': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3) + '@solana/rpc-subscriptions-channel-websocket': 3.0.3(typescript@5.8.3)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + '@solana/rpc-subscriptions-spec': 3.0.3(typescript@5.8.3) + '@solana/rpc-transformers': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3) + '@solana/rpc-types': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3) + '@solana/subscribable': 3.0.3(typescript@5.8.3) + typescript: 5.8.3 + transitivePeerDependencies: + - fastestsmallesttextencoderdecoder + - ws + '@solana/rpc-subscriptions@3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3)(ws@8.19.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))': dependencies: '@solana/errors': 3.0.3(typescript@5.8.3) @@ -13074,6 +13562,24 @@ snapshots: - fastestsmallesttextencoderdecoder - ws + '@solana/rpc-subscriptions@5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10))': + dependencies: + '@solana/errors': 5.0.0(typescript@5.8.3) + '@solana/fast-stable-stringify': 5.0.0(typescript@5.8.3) + '@solana/functional': 5.0.0(typescript@5.8.3) + '@solana/promises': 5.0.0(typescript@5.8.3) + '@solana/rpc-spec-types': 5.0.0(typescript@5.8.3) + '@solana/rpc-subscriptions-api': 5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3) + '@solana/rpc-subscriptions-channel-websocket': 5.0.0(typescript@5.8.3)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + '@solana/rpc-subscriptions-spec': 5.0.0(typescript@5.8.3) + '@solana/rpc-transformers': 5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3) + '@solana/rpc-types': 5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3) + '@solana/subscribable': 5.0.0(typescript@5.8.3) + typescript: 5.8.3 + transitivePeerDependencies: + - fastestsmallesttextencoderdecoder + - ws + '@solana/rpc-subscriptions@5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3)(ws@8.19.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))': dependencies: '@solana/errors': 5.0.0(typescript@5.8.3) @@ -13201,7 +13707,7 @@ snapshots: '@solana/rpc-spec': 3.0.3(typescript@5.8.3) '@solana/rpc-spec-types': 3.0.3(typescript@5.8.3) typescript: 5.8.3 - undici-types: 7.16.0 + undici-types: 7.22.0 '@solana/rpc-transport-http@5.0.0(typescript@5.8.3)': dependencies: @@ -13491,7 +13997,7 @@ snapshots: transitivePeerDependencies: - fastestsmallesttextencoderdecoder - '@solana/transaction-confirmation@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3)(ws@8.19.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))': + '@solana/transaction-confirmation@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))': dependencies: '@solana/addresses': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3) '@solana/codecs-strings': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3) @@ -13499,7 +14005,7 @@ snapshots: '@solana/keys': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3) '@solana/promises': 2.3.0(typescript@5.8.3) '@solana/rpc': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3) - '@solana/rpc-subscriptions': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3)(ws@8.19.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + '@solana/rpc-subscriptions': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)) '@solana/rpc-types': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3) '@solana/transaction-messages': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3) '@solana/transactions': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3) @@ -13508,6 +14014,23 @@ snapshots: - fastestsmallesttextencoderdecoder - ws + '@solana/transaction-confirmation@3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10))': + dependencies: + '@solana/addresses': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3) + '@solana/codecs-strings': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3) + '@solana/errors': 3.0.3(typescript@5.8.3) + '@solana/keys': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3) + '@solana/promises': 3.0.3(typescript@5.8.3) + '@solana/rpc': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3) + '@solana/rpc-subscriptions': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + '@solana/rpc-types': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3) + '@solana/transaction-messages': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3) + '@solana/transactions': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3) + typescript: 5.8.3 + transitivePeerDependencies: + - fastestsmallesttextencoderdecoder + - ws + '@solana/transaction-confirmation@3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3)(ws@8.19.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))': dependencies: '@solana/addresses': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3) @@ -13525,6 +14048,23 @@ snapshots: - fastestsmallesttextencoderdecoder - ws + '@solana/transaction-confirmation@5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10))': + dependencies: + '@solana/addresses': 5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3) + '@solana/codecs-strings': 5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3) + '@solana/errors': 5.0.0(typescript@5.8.3) + '@solana/keys': 5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3) + '@solana/promises': 5.0.0(typescript@5.8.3) + '@solana/rpc': 5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3) + '@solana/rpc-subscriptions': 5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + '@solana/rpc-types': 5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3) + '@solana/transaction-messages': 5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3) + '@solana/transactions': 5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3) + typescript: 5.8.3 + transitivePeerDependencies: + - fastestsmallesttextencoderdecoder + - ws + '@solana/transaction-confirmation@5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3)(ws@8.19.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))': dependencies: '@solana/addresses': 5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.8.3) @@ -13794,6 +14334,31 @@ snapshots: '@stablelib/wipe@1.0.1': {} + '@stellar/js-xdr@3.1.2': {} + + '@stellar/stellar-base@14.1.0': + dependencies: + '@noble/curves': 1.9.7 + '@stellar/js-xdr': 3.1.2 + base32.js: 0.1.0 + bignumber.js: 9.3.1 + buffer: 6.0.3 + sha.js: 2.4.12 + + '@stellar/stellar-sdk@14.6.1': + dependencies: + '@stellar/stellar-base': 14.1.0 + axios: 1.13.4 + bignumber.js: 9.3.1 + commander: 14.0.3 + eventsource: 2.0.2 + feaxios: 0.0.23 + randombytes: 2.1.0 + toml: 3.0.0 + urijs: 1.19.11 + transitivePeerDependencies: + - debug + '@svgr/babel-plugin-add-jsx-attribute@8.0.0(@babel/core@7.28.4)': dependencies: '@babel/core': 7.28.4 @@ -15239,6 +15804,8 @@ snapshots: typescript: 5.8.3 zod: 4.1.12 + abstract-logging@2.0.1: {} + accepts@1.3.8: dependencies: mime-types: 2.1.35 @@ -15403,11 +15970,16 @@ snapshots: dependencies: possible-typed-array-names: 1.1.0 + avvio@9.2.0: + dependencies: + '@fastify/error': 4.2.0 + fastq: 1.19.1 + axe-core@4.11.0: {} - axios-retry@4.5.0(axios@1.12.2): + axios-retry@4.5.0(axios@1.13.4): dependencies: - axios: 1.12.2 + axios: 1.13.4 is-retry-allowed: 2.2.0 axios@1.10.0: @@ -15426,6 +15998,14 @@ snapshots: transitivePeerDependencies: - debug + axios@1.13.4: + dependencies: + follow-redirects: 1.15.9 + form-data: 4.0.4 + proxy-from-env: 1.1.0 + transitivePeerDependencies: + - debug + axobject-query@4.1.0: {} babel-plugin-polyfill-corejs2@0.4.14(@babel/core@7.28.4): @@ -15460,12 +16040,16 @@ snapshots: base-x@5.0.1: {} + base32.js@0.1.0: {} + base64-js@1.5.1: {} baseline-browser-mapping@2.8.19: {} big.js@6.2.2: {} + bignumber.js@9.3.1: {} + binary-extensions@2.3.0: {} bn.js@4.12.2: {} @@ -15759,6 +16343,8 @@ snapshots: cookie@0.7.1: {} + cookie@1.1.1: {} + core-js-compat@3.46.0: dependencies: browserslist: 4.27.0 @@ -15960,6 +16546,8 @@ snapshots: depd@2.0.0: {} + dequal@2.0.3: {} + derive-valtio@0.1.0(valtio@1.13.2(@types/react@19.2.2)(react@19.2.0)): dependencies: valtio: 1.13.2(@types/react@19.2.2)(react@19.2.0) @@ -16623,6 +17211,8 @@ snapshots: eventsource-parser@3.0.6: {} + eventsource@2.0.2: {} + eventsource@3.0.7: dependencies: eventsource-parser: 3.0.6 @@ -16726,6 +17316,8 @@ snapshots: eyes@0.1.8: {} + fast-decode-uri-component@1.0.1: {} + fast-deep-equal@3.1.3: {} fast-diff@1.3.0: {} @@ -16748,8 +17340,21 @@ snapshots: fast-json-stable-stringify@2.1.0: {} + fast-json-stringify@6.3.0: + dependencies: + '@fastify/merge-json-schemas': 0.2.1 + ajv: 8.17.1 + ajv-formats: 3.0.1(ajv@8.17.1) + fast-uri: 3.1.0 + json-schema-ref-resolver: 3.0.0 + rfdc: 1.4.1 + fast-levenshtein@2.0.6: {} + fast-querystring@1.1.2: + dependencies: + fast-decode-uri-component: 1.0.1 + fast-redact@3.5.0: {} fast-safe-stringify@2.1.1: {} @@ -16760,6 +17365,24 @@ snapshots: fastestsmallesttextencoderdecoder@1.0.22: {} + fastify@5.8.2: + dependencies: + '@fastify/ajv-compiler': 4.0.5 + '@fastify/error': 4.2.0 + '@fastify/fast-json-stringify-compiler': 5.0.3 + '@fastify/proxy-addr': 5.1.0 + abstract-logging: 2.0.1 + avvio: 9.2.0 + fast-json-stringify: 6.3.0 + find-my-way: 9.5.0 + light-my-request: 6.6.0 + pino: 10.3.1 + process-warning: 5.0.0 + rfdc: 1.4.1 + secure-json-parse: 4.1.0 + semver: 7.7.3 + toad-cache: 3.7.0 + fastq@1.19.1: dependencies: reusify: 1.1.0 @@ -16768,6 +17391,10 @@ snapshots: optionalDependencies: picomatch: 4.0.3 + feaxios@0.0.23: + dependencies: + is-retry-allowed: 3.0.0 + file-entry-cache@8.0.0: dependencies: flat-cache: 4.0.1 @@ -16801,6 +17428,12 @@ snapshots: transitivePeerDependencies: - supports-color + find-my-way@9.5.0: + dependencies: + fast-deep-equal: 3.1.3 + fast-querystring: 1.1.2 + safe-regex2: 5.1.0 + find-up@4.1.0: dependencies: locate-path: 5.0.0 @@ -17129,6 +17762,8 @@ snapshots: ipaddr.js@1.9.1: {} + ipaddr.js@2.3.0: {} + iron-webcrypto@1.2.1: {} is-arguments@1.2.0: @@ -17232,6 +17867,8 @@ snapshots: is-retry-allowed@2.2.0: {} + is-retry-allowed@3.0.0: {} + is-set@2.0.3: {} is-shared-array-buffer@1.0.4: @@ -17381,6 +18018,10 @@ snapshots: json-rpc-random-id@1.0.1: {} + json-schema-ref-resolver@3.0.0: + dependencies: + dequal: 2.0.3 + json-schema-traverse@0.4.1: {} json-schema-traverse@1.0.0: {} @@ -17431,6 +18072,12 @@ snapshots: prelude-ls: 1.2.1 type-check: 0.4.0 + light-my-request@6.6.0: + dependencies: + cookie: 1.1.1 + process-warning: 4.0.1 + set-cookie-parser: 2.7.2 + lilconfig@3.1.3: {} lines-and-columns@1.2.4: {} @@ -17744,6 +18391,8 @@ snapshots: on-exit-leak-free@0.2.0: {} + on-exit-leak-free@2.1.2: {} + on-finished@2.4.1: dependencies: ee-first: 1.1.1 @@ -18024,8 +18673,28 @@ snapshots: duplexify: 4.1.3 split2: 4.2.0 + pino-abstract-transport@3.0.0: + dependencies: + split2: 4.2.0 + pino-std-serializers@4.0.0: {} + pino-std-serializers@7.1.0: {} + + pino@10.3.1: + dependencies: + '@pinojs/redact': 0.4.0 + atomic-sleep: 1.0.0 + on-exit-leak-free: 2.1.2 + pino-abstract-transport: 3.0.0 + pino-std-serializers: 7.1.0 + process-warning: 5.0.0 + quick-format-unescaped: 4.0.4 + real-require: 0.2.0 + safe-stable-stringify: 2.5.0 + sonic-boom: 4.2.1 + thread-stream: 4.0.0 + pino@7.11.0: dependencies: atomic-sleep: 1.0.0 @@ -18186,6 +18855,10 @@ snapshots: process-warning@1.0.0: {} + process-warning@4.0.1: {} + + process-warning@5.0.0: {} + prompts@2.4.2: dependencies: kleur: 3.0.3 @@ -18334,6 +19007,8 @@ snapshots: real-require@0.1.0: {} + real-require@0.2.0: {} + reflect.getprototypeof@1.0.10: dependencies: call-bind: 1.0.8 @@ -18405,8 +19080,12 @@ snapshots: dependencies: lowercase-keys: 2.0.0 + ret@0.5.0: {} + reusify@1.1.0: {} + rfdc@1.4.1: {} + ripemd160@2.0.3: dependencies: hash-base: 3.1.2 @@ -18458,7 +19137,7 @@ snapshots: buffer: 6.0.3 eventemitter3: 5.0.1 uuid: 8.3.2 - ws: 8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10) + ws: 8.19.0(bufferutil@4.0.9)(utf-8-validate@5.0.10) optionalDependencies: bufferutil: 4.0.9 utf-8-validate: 5.0.10 @@ -18492,6 +19171,10 @@ snapshots: es-errors: 1.3.0 is-regex: 1.2.1 + safe-regex2@5.1.0: + dependencies: + ret: 0.5.0 + safe-stable-stringify@2.5.0: {} safer-buffer@2.1.2: {} @@ -18502,6 +19185,8 @@ snapshots: scheduler@0.27.0: {} + secure-json-parse@4.1.0: {} + semver@6.3.1: {} semver@7.7.3: {} @@ -18560,6 +19245,8 @@ snapshots: set-blocking@2.0.0: {} + set-cookie-parser@2.7.2: {} + set-function-length@1.2.2: dependencies: define-data-property: 1.1.4 @@ -18699,6 +19386,10 @@ snapshots: dependencies: atomic-sleep: 1.0.0 + sonic-boom@4.2.1: + dependencies: + atomic-sleep: 1.0.0 + source-map-js@1.2.1: {} source-map@0.8.0-beta.0: @@ -18925,6 +19616,10 @@ snapshots: dependencies: real-require: 0.1.0 + thread-stream@4.0.0: + dependencies: + real-require: 0.2.0 + tinybench@2.9.0: {} tinyexec@0.3.2: {} @@ -18956,8 +19651,12 @@ snapshots: dependencies: is-number: 7.0.0 + toad-cache@3.7.0: {} + toidentifier@1.0.1: {} + toml@3.0.0: {} + tough-cookie@5.1.2: dependencies: tldts: 6.1.86 @@ -19201,6 +19900,8 @@ snapshots: dependencies: punycode: 2.3.1 + urijs@1.19.11: {} + use-sync-external-store@1.2.0(react@19.2.0): dependencies: react: 19.2.0 diff --git a/e2e/scripts/permit2-approval.ts b/e2e/scripts/permit2-approval.ts index bf19b93d6b..9e3ef2e6f0 100644 --- a/e2e/scripts/permit2-approval.ts +++ b/e2e/scripts/permit2-approval.ts @@ -5,8 +5,10 @@ * It can grant unlimited approval or revoke existing approval. * * Usage: - * pnpm tsx scripts/permit2-approval.ts approve # Check and approve if needed - * pnpm tsx scripts/permit2-approval.ts revoke # Revoke Permit2 approval (set allowance to 0) + * pnpm tsx scripts/permit2-approval.ts approve [tokenAddress] + * pnpm tsx scripts/permit2-approval.ts revoke [tokenAddress] + * + * If tokenAddress is not provided, processes all known tokens. * * Environment variables required: * CLIENT_EVM_PRIVATE_KEY - Private key of the client wallet @@ -19,18 +21,44 @@ import { http, parseAbi, formatUnits, + getAddress, } from 'viem'; import { privateKeyToAccount } from 'viem/accounts'; -import { baseSepolia } from 'viem/chains'; +import { base, baseSepolia } from 'viem/chains'; config(); // Permit2 canonical address (same on all EVM chains) const PERMIT2_ADDRESS = '0x000000000022D473030F116dDEE9F6B43aC78BA3'; -// Base Sepolia USDC -const USDC_ADDRESS = '0x036CbD53842c5426634e7929541eC2318f3dCF7e'; -const USDC_DECIMALS = 6; +const evmNetwork = process.env.EVM_NETWORK || 'eip155:84532'; +const evmRpcUrl = process.env.EVM_RPC_URL; +const evmChain = evmNetwork === 'eip155:8453' ? base : baseSepolia; +const isMainnet = evmNetwork === 'eip155:8453'; + +const TOKENS_BY_NETWORK: Record> = { + 'eip155:84532': { + USDC: { + address: '0x036CbD53842c5426634e7929541eC2318f3dCF7e', + decimals: 6, + name: 'USDC', + }, + MockERC20: { + address: '0xeED520980fC7C7B4eB379B96d61CEdea2423005a', + decimals: 6, + name: 'MockERC20', + }, + }, + 'eip155:8453': { + USDC: { + address: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913', + decimals: 6, + name: 'USDC', + }, + }, +}; + +const TOKENS = TOKENS_BY_NETWORK[evmNetwork] || TOKENS_BY_NETWORK['eip155:84532']; // Maximum uint256 for unlimited approval const MAX_UINT256 = 2n ** 256n - 1n; @@ -44,14 +72,18 @@ const erc20Abi = parseAbi([ async function main() { const action = process.argv[2]; + const tokenAddressArg = process.argv[3]; + const filterAddress = tokenAddressArg ? (getAddress(tokenAddressArg) as `0x${string}`) : undefined; if (!action || (action !== 'approve' && action !== 'revoke')) { console.log(` Permit2 Approval Script Usage: - pnpm tsx scripts/permit2-approval.ts approve # Check and approve Permit2 if needed - pnpm tsx scripts/permit2-approval.ts revoke # Revoke Permit2 approval (set allowance to 0) + pnpm tsx scripts/permit2-approval.ts approve [tokenAddress] + pnpm tsx scripts/permit2-approval.ts revoke [tokenAddress] + +If tokenAddress is not provided, processes all known tokens (USDC and MockERC20). Environment variables required: CLIENT_EVM_PRIVATE_KEY - Private key of the client wallet @@ -68,105 +100,104 @@ Environment variables required: const account = privateKeyToAccount(privateKey as `0x${string}`); const publicClient = createPublicClient({ - chain: baseSepolia, - transport: http(), + chain: evmChain, + transport: http(evmRpcUrl), }); const walletClient = createWalletClient({ account, - chain: baseSepolia, - transport: http(), + chain: evmChain, + transport: http(evmRpcUrl), }); console.log(`\n🔑 Wallet: ${account.address}`); - console.log(`📍 Network: Base Sepolia`); - console.log(`💰 Token: USDC (${USDC_ADDRESS})`); + console.log(`📍 Network: ${evmChain.name} (${evmNetwork})`); console.log(`🔐 Permit2: ${PERMIT2_ADDRESS}\n`); - // Check current balance - const balance = await publicClient.readContract({ - address: USDC_ADDRESS, - abi: erc20Abi, - functionName: 'balanceOf', - args: [account.address], - }); - console.log(`💵 USDC Balance: ${formatUnits(balance, USDC_DECIMALS)} USDC`); - - // Check current allowance - const currentAllowance = await publicClient.readContract({ - address: USDC_ADDRESS, - abi: erc20Abi, - functionName: 'allowance', - args: [account.address, PERMIT2_ADDRESS], - }); - - const formattedAllowance = - currentAllowance === MAX_UINT256 - ? 'unlimited' - : `${formatUnits(currentAllowance, USDC_DECIMALS)} USDC`; - console.log(`📋 Current Permit2 Allowance: ${formattedAllowance}\n`); - - if (action === 'revoke') { - // Revoke approval by setting allowance to 0 - if (currentAllowance === 0n) { - console.log('✅ Permit2 approval is already revoked (allowance is 0)'); - process.exit(0); - } + // Display balance and allowance for all known tokens + const tokenStates: { name: string; address: `0x${string}`; decimals: number; balance: bigint; allowance: bigint }[] = []; - console.log('🔄 Revoking Permit2 approval (setting allowance to 0)...'); + for (const token of Object.values(TOKENS)) { + const balance = await publicClient.readContract({ + address: token.address, + abi: erc20Abi, + functionName: 'balanceOf', + args: [account.address], + }); - const hash = await walletClient.writeContract({ - address: USDC_ADDRESS, + const allowance = await publicClient.readContract({ + address: token.address, abi: erc20Abi, - functionName: 'approve', - args: [PERMIT2_ADDRESS, 0n], + functionName: 'allowance', + args: [account.address, PERMIT2_ADDRESS], }); - console.log(`📝 Transaction submitted: ${hash}`); - console.log('⏳ Waiting for confirmation...'); + tokenStates.push({ ...token, balance, allowance }); - const receipt = await publicClient.waitForTransactionReceipt({ hash }); + const formattedBalance = `${formatUnits(balance, token.decimals)} ${token.name}`; + const formattedAllowance = + allowance === MAX_UINT256 + ? 'unlimited' + : `${formatUnits(allowance, token.decimals)} ${token.name}`; - if (receipt.status === 'success') { - console.log(`\n✅ Permit2 approval revoked successfully!`); - console.log(` Block: ${receipt.blockNumber}`); - console.log(` Gas used: ${receipt.gasUsed}`); - } else { - console.error(`\n❌ Revoke transaction failed`); - process.exit(1); - } - return; + console.log(`💰 ${token.name} (${token.address})`); + console.log(` 💵 Balance: ${formattedBalance}`); + console.log(` 📋 Permit2 Allowance: ${formattedAllowance}`); } + console.log(); - // action === 'approve' - // Check if approval already exists - if (currentAllowance === MAX_UINT256) { - console.log('✅ Permit2 already has unlimited approval'); - process.exit(0); + const tokensToProcess = filterAddress + ? tokenStates.filter((t) => getAddress(t.address) === filterAddress) + : tokenStates; + + if (tokensToProcess.length === 0) { + const addr = filterAddress ?? 'none'; + console.error(`❌ No matching token found for address ${addr}`); + process.exit(1); } - // Grant unlimited approval - console.log('🔄 Granting unlimited Permit2 approval...'); + let nonce = await publicClient.getTransactionCount({ address: account.address }); - const hash = await walletClient.writeContract({ - address: USDC_ADDRESS, - abi: erc20Abi, - functionName: 'approve', - args: [PERMIT2_ADDRESS, MAX_UINT256], - }); + if (action === 'revoke') { + for (const token of tokensToProcess) { + if (token.allowance === 0n) { + console.log(`✅ ${token.name}: Permit2 approval already revoked (allowance is 0)`); + continue; + } + + console.log(`🔄 ${token.name}: Revoking Permit2 approval...`); + + const hash = await walletClient.writeContract({ + address: token.address, + abi: erc20Abi, + functionName: 'approve', + args: [PERMIT2_ADDRESS, 0n], + nonce: nonce++, + }); + + console.log(` ✅ Revoke submitted (tx: ${hash})`); + } + return; + } - console.log(`📝 Transaction submitted: ${hash}`); - console.log('⏳ Waiting for confirmation...'); + // action === 'approve' + for (const token of tokensToProcess) { + if (token.allowance === MAX_UINT256) { + console.log(`✅ ${token.name}: Permit2 already has unlimited approval`); + continue; + } - const receipt = await publicClient.waitForTransactionReceipt({ hash }); + console.log(`🔄 ${token.name}: Granting unlimited Permit2 approval...`); - if (receipt.status === 'success') { - console.log(`\n✅ Permit2 approval granted successfully!`); - console.log(` Block: ${receipt.blockNumber}`); - console.log(` Gas used: ${receipt.gasUsed}`); - } else { - console.error(`\n❌ Transaction failed`); - process.exit(1); + const hash = await walletClient.writeContract({ + address: token.address, + abi: erc20Abi, + functionName: 'approve', + args: [PERMIT2_ADDRESS, MAX_UINT256], + nonce: nonce++, + }); + + console.log(` ✅ Approve submitted (tx: ${hash})`); } } diff --git a/e2e/servers/echo/README.md b/e2e/servers/echo/README.md new file mode 100644 index 0000000000..0a1fae0ee0 --- /dev/null +++ b/e2e/servers/echo/README.md @@ -0,0 +1,203 @@ +# E2E Test Server: Echo (Go) + +This server demonstrates and tests the x402 Echo middleware with both EVM and SVM payment protection. + +## What It Tests + +### Core Functionality +- ✅ **V2 Protocol** - Modern x402 server middleware +- ✅ **Payment Protection** - Middleware protecting specific routes +- ✅ **Multi-chain Support** - EVM and SVM payment acceptance +- ✅ **Facilitator Integration** - HTTP communication with facilitator +- ✅ **Extension Support** - Bazaar discovery metadata +- ✅ **Settlement Handling** - Payment verification and confirmation + +### Protected Endpoints +- ✅ `GET /protected` - Requires EVM payment (USDC on Base Sepolia) +- ✅ `GET /protected-svm` - Requires SVM payment (USDC on Solana Devnet) + +## What It Demonstrates + +### Server Setup + +```go +import ( + x402 "github.com/coinbase/x402/go" + x402http "github.com/coinbase/x402/go/http" + echomw "github.com/coinbase/x402/go/http/echo" + evm "github.com/coinbase/x402/go/mechanisms/evm/exact/server" + svm "github.com/coinbase/x402/go/mechanisms/svm/exact/server" + "github.com/coinbase/x402/go/extensions/bazaar" + "github.com/labstack/echo/v4" +) + +// Create Echo instance +e := echo.New() + +// Define payment routes +routes := x402http.RoutesConfig{ + "GET /protected": { + Accepts: x402http.PaymentOptions{ + { + Scheme: "exact", + Network: "eip155:84532", + PayTo: evmPayeeAddress, + Price: "$0.001", + }, + }, + Extensions: map[string]interface{}{ + "bazaar": discoveryExtension, + }, + }, + "GET /protected-svm": { + Accepts: x402http.PaymentOptions{ + { + Scheme: "exact", + Network: "solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1", + PayTo: svmPayeeAddress, + Price: "$0.001", + }, + }, + Extensions: map[string]interface{}{ + "bazaar": discoveryExtension, + }, + }, +} + +// Apply payment middleware +e.Use(echomw.X402Payment(echomw.Config{ + Routes: routes, + Facilitator: facilitatorClient, + Schemes: []echomw.SchemeConfig{ + {Network: "eip155:84532", Server: evm.NewExactEvmScheme()}, + {Network: "solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1", Server: svm.NewExactSvmScheme()}, + }, + Timeout: 30 * time.Second, +})) + +// Define protected endpoints +e.GET("/protected", func(c echo.Context) error { + return c.JSON(200, map[string]string{"message": "EVM payment successful!"}) +}) + +e.GET("/protected-svm", func(c echo.Context) error { + return c.JSON(200, map[string]string{"message": "SVM payment successful!"}) +}) +``` + +### Key Concepts Shown + +1. **Route Configuration** - Map of route → payment requirements +2. **Multi-Chain Services** - Different services for EVM vs SVM +3. **Facilitator Client** - HTTP client for verification/settlement +4. **Middleware Options** - Functional options pattern +5. **Extension Integration** - Bazaar discovery declarations +6. **Automatic Initialization** - Service initialization on startup + +## Test Scenarios + +This server is tested with: +- **Clients:** TypeScript Fetch, Go HTTP +- **Facilitators:** TypeScript, Go +- **Payment Types:** EVM (Base Sepolia), SVM (Solana Devnet) +- **Protocols:** V2 (primary), V1 (via client negotiation) + +### Request Flow +1. Client makes initial request (no payment) +2. Middleware returns 402 with `PAYMENT-REQUIRED` header +3. Client creates payment payload +4. Client retries with `PAYMENT-SIGNATURE` header +5. Middleware forwards to facilitator for verification +6. Middleware returns protected content + `PAYMENT-RESPONSE` header + +## Running + +```bash +# Via e2e test suite +cd e2e +pnpm test --server=echo + +# Direct execution +cd e2e/servers/echo +export FACILITATOR_URL="http://localhost:4024" +export EVM_PAYEE_ADDRESS="0x..." +export SVM_PAYEE_ADDRESS="..." +export PORT=4023 +./echo +``` + +## Environment Variables + +- `PORT` - HTTP server port (default: 4021) +- `FACILITATOR_URL` - Facilitator endpoint URL +- `EVM_PAYEE_ADDRESS` - Ethereum address to receive payments +- `SVM_PAYEE_ADDRESS` - Solana address to receive payments +- `EVM_NETWORK` - EVM network (default: eip155:84532) +- `SVM_NETWORK` - SVM network (default: solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1) + +## Response Examples + +### 402 Payment Required + +``` +HTTP/1.1 402 Payment Required +PAYMENT-REQUIRED: +Content-Type: application/json + +{ + "error": "Payment required", + "x402Version": 2, + "accepts": [...], + "resource": {...}, + "extensions": { + "bazaar": { + "method": "GET", + "outputExample": {...} + } + } +} +``` + +### 200 Success (After Payment) + +``` +HTTP/1.1 200 OK +PAYMENT-RESPONSE: +Content-Type: application/json + +{ + "message": "Protected endpoint accessed successfully", + "timestamp": "2024-01-01T00:00:00Z" +} +``` + +## Dependencies + +- `github.com/coinbase/x402/go` - Core x402 +- `github.com/coinbase/x402/go/http` - HTTP integration +- `github.com/coinbase/x402/go/http/echo` - Echo middleware +- `github.com/coinbase/x402/go/mechanisms/evm` - EVM server +- `github.com/coinbase/x402/go/mechanisms/svm` - SVM server +- `github.com/coinbase/x402/go/extensions/bazaar` - Discovery extension +- `github.com/labstack/echo/v4` - HTTP framework + +## Implementation Highlights + +### Middleware Features +- **Route Matching** - Pattern-based route configuration +- **Payment Requirement Building** - Automatic 402 response generation +- **Facilitator Communication** - HTTP client for verification +- **Settlement Callbacks** - Optional handlers for payment events +- **Extension Support** - Bazaar metadata in responses +- **Timeout Handling** - Configurable facilitator timeouts + +### Service Integration +- **EVM Server** - Base Sepolia USDC +- **SVM Server** - Solana Devnet USDC +- **Initialization** - Fetches supported kinds from facilitator +- **Price Parsing** - Dollar strings → token amounts + +### Bazaar Extension +- **Method Declaration** - GET with output schema +- **Example Output** - Response structure preview +- **Schema Definition** - JSON Schema for validation diff --git a/e2e/servers/echo/build.sh b/e2e/servers/echo/build.sh new file mode 100755 index 0000000000..dd569a0dd8 --- /dev/null +++ b/e2e/servers/echo/build.sh @@ -0,0 +1,6 @@ +#!/bin/bash +set -e + +echo "Building Echo server..." +go build -o echo . +echo "✅ Build completed: echo" diff --git a/e2e/servers/echo/go.mod b/e2e/servers/echo/go.mod new file mode 100644 index 0000000000..dc75cd9323 --- /dev/null +++ b/e2e/servers/echo/go.mod @@ -0,0 +1,73 @@ +module github.com/coinbase/x402/e2e/servers/echo + +go 1.24.0 + +toolchain go1.24.1 + +require ( + github.com/coinbase/x402/go v0.0.0 + github.com/joho/godotenv v1.5.1 + github.com/labstack/echo/v4 v4.15.1 +) + +require ( + filippo.io/edwards25519 v1.0.0-rc.1 // indirect + github.com/Microsoft/go-winio v0.6.2 // indirect + github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20251001021608-1fe7b43fc4d6 // indirect + github.com/StackExchange/wmi v1.2.1 // indirect + github.com/andres-erbsen/clock v0.0.0-20160526145045-9e14626cd129 // indirect + github.com/bits-and-blooms/bitset v1.20.0 // indirect + github.com/blendle/zapdriver v1.3.1 // indirect + github.com/consensys/gnark-crypto v0.18.0 // indirect + github.com/crate-crypto/go-eth-kzg v1.4.0 // indirect + github.com/crate-crypto/go-ipa v0.0.0-20240724233137-53bbb0ceb27a // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/deckarep/golang-set/v2 v2.6.0 // indirect + github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 // indirect + github.com/ethereum/c-kzg-4844/v2 v2.1.5 // indirect + github.com/ethereum/go-ethereum v1.16.7 // indirect + github.com/ethereum/go-verkle v0.2.2 // indirect + github.com/fatih/color v1.16.0 // indirect + github.com/gagliardetto/binary v0.8.0 // indirect + github.com/gagliardetto/solana-go v1.14.0 // indirect + github.com/gagliardetto/treeout v0.1.4 // indirect + github.com/go-ole/go-ole v1.3.0 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/gorilla/websocket v1.4.2 // indirect + github.com/holiman/uint256 v1.3.2 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/compress v1.16.0 // indirect + github.com/labstack/gommon v0.4.2 // indirect + github.com/logrusorgru/aurora v2.0.3+incompatible // indirect + github.com/mattn/go-colorable v0.1.14 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mitchellh/go-testing-interface v1.14.1 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/mostynb/zstdpool-freelist v0.0.0-20201229113212-927304c0c3b1 // indirect + github.com/mr-tron/base58 v1.2.0 // indirect + github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible // indirect + github.com/streamingfast/logging v0.0.0-20230608130331-f22c91403091 // indirect + github.com/supranational/blst v0.3.16-0.20250831170142-f48500c1fdbe // indirect + github.com/tklauser/go-sysconf v0.3.12 // indirect + github.com/tklauser/numcpus v0.6.1 // indirect + github.com/valyala/bytebufferpool v1.0.0 // indirect + github.com/valyala/fasttemplate v1.2.2 // indirect + github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f // indirect + github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect + github.com/xeipuuv/gojsonschema v1.2.0 // indirect + go.mongodb.org/mongo-driver v1.12.2 // indirect + go.uber.org/atomic v1.7.0 // indirect + go.uber.org/multierr v1.6.0 // indirect + go.uber.org/ratelimit v0.2.0 // indirect + go.uber.org/zap v1.21.0 // indirect + golang.org/x/crypto v0.46.0 // indirect + golang.org/x/net v0.48.0 // indirect + golang.org/x/sync v0.19.0 // indirect + golang.org/x/sys v0.39.0 // indirect + golang.org/x/term v0.38.0 // indirect + golang.org/x/text v0.32.0 // indirect + golang.org/x/time v0.14.0 // indirect +) + +replace github.com/coinbase/x402/go => ../../../go diff --git a/e2e/servers/echo/go.sum b/e2e/servers/echo/go.sum new file mode 100644 index 0000000000..02788fc579 --- /dev/null +++ b/e2e/servers/echo/go.sum @@ -0,0 +1,342 @@ +filippo.io/edwards25519 v1.0.0-rc.1 h1:m0VOOB23frXZvAOK44usCgLWvtsxIoMCTBGJZlpmGfU= +filippo.io/edwards25519 v1.0.0-rc.1/go.mod h1:N1IkdkCkiLB6tki+MYJoSx2JTY9NUlxZE7eHn5EwJns= +github.com/AlekSi/pointer v1.1.0 h1:SSDMPcXD9jSl8FPy9cRzoRaMJtm9g9ggGTxecRUbQoI= +github.com/AlekSi/pointer v1.1.0/go.mod h1:y7BvfRI3wXPWKXEBhU71nbnIEEZX0QTSB2Bj48UJIZE= +github.com/DataDog/zstd v1.4.5 h1:EndNeuB0l9syBZhut0wns3gV1hL8zX8LIu6ZiVHWLIQ= +github.com/DataDog/zstd v1.4.5/go.mod h1:1jcaCB/ufaK+sKp1NBhlGmpz41jOoPQ35bpF36t7BBo= +github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= +github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= +github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20251001021608-1fe7b43fc4d6 h1:1zYrtlhrZ6/b6SAjLSfKzWtdgqK0U+HtH/VcBWh1BaU= +github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20251001021608-1fe7b43fc4d6/go.mod h1:ioLG6R+5bUSO1oeGSDxOV3FADARuMoytZCSX6MEMQkI= +github.com/StackExchange/wmi v1.2.1 h1:VIkavFPXSjcnS+O8yTq7NI32k0R5Aj+v39y29VYDOSA= +github.com/StackExchange/wmi v1.2.1/go.mod h1:rcmrprowKIVzvc+NUiLncP2uuArMWLCbu9SBzvHz7e8= +github.com/VictoriaMetrics/fastcache v1.13.0 h1:AW4mheMR5Vd9FkAPUv+NH6Nhw+fmbTMGMsNAoA/+4G0= +github.com/VictoriaMetrics/fastcache v1.13.0/go.mod h1:hHXhl4DA2fTL2HTZDJFXWgW0LNjo6B+4aj2Wmng3TjU= +github.com/andres-erbsen/clock v0.0.0-20160526145045-9e14626cd129 h1:MzBOUgng9orim59UnfUTLRjMpd09C5uEVQ6RPGeCaVI= +github.com/andres-erbsen/clock v0.0.0-20160526145045-9e14626cd129/go.mod h1:rFgpPQZYZ8vdbc+48xibu8ALc3yeyd64IhHS+PU6Yyg= +github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8= +github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/bits-and-blooms/bitset v1.20.0 h1:2F+rfL86jE2d/bmw7OhqUg2Sj/1rURkBn3MdfoPyRVU= +github.com/bits-and-blooms/bitset v1.20.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8= +github.com/blendle/zapdriver v1.3.1 h1:C3dydBOWYRiOk+B8X9IVZ5IOe+7cl+tGOexN4QqHfpE= +github.com/blendle/zapdriver v1.3.1/go.mod h1:mdXfREi6u5MArG4j9fewC+FGnXaBR+T4Ox4J2u4eHCc= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cockroachdb/errors v1.11.3 h1:5bA+k2Y6r+oz/6Z/RFlNeVCesGARKuC6YymtcDrbC/I= +github.com/cockroachdb/errors v1.11.3/go.mod h1:m4UIW4CDjx+R5cybPsNrRbreomiFqt8o1h1wUVazSd8= +github.com/cockroachdb/fifo v0.0.0-20240606204812-0bbfbd93a7ce h1:giXvy4KSc/6g/esnpM7Geqxka4WSqI1SZc7sMJFd3y4= +github.com/cockroachdb/fifo v0.0.0-20240606204812-0bbfbd93a7ce/go.mod h1:9/y3cnZ5GKakj/H4y9r9GTjCvAFta7KLgSHPJJYc52M= +github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b h1:r6VH0faHjZeQy818SGhaone5OnYfxFR/+AzdY3sf5aE= +github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b/go.mod h1:Vz9DsVWQQhf3vs21MhPMZpMGSht7O/2vFW2xusFUVOs= +github.com/cockroachdb/pebble v1.1.5 h1:5AAWCBWbat0uE0blr8qzufZP5tBjkRyy/jWe1QWLnvw= +github.com/cockroachdb/pebble v1.1.5/go.mod h1:17wO9el1YEigxkP/YtV8NtCivQDgoCyBg5c4VR/eOWo= +github.com/cockroachdb/redact v1.1.5 h1:u1PMllDkdFfPWaNGMyLD1+so+aq3uUItthCFqzwPJ30= +github.com/cockroachdb/redact v1.1.5/go.mod h1:BVNblN9mBWFyMyqK1k3AAiSxhvhfK2oOZZ2lK+dpvRg= +github.com/cockroachdb/tokenbucket v0.0.0-20230807174530-cc333fc44b06 h1:zuQyyAKVxetITBuuhv3BI9cMrmStnpT18zmgmTxunpo= +github.com/cockroachdb/tokenbucket v0.0.0-20230807174530-cc333fc44b06/go.mod h1:7nc4anLGjupUW/PeY5qiNYsdNXj7zopG+eqsS7To5IQ= +github.com/consensys/gnark-crypto v0.18.0 h1:vIye/FqI50VeAr0B3dx+YjeIvmc3LWz4yEfbWBpTUf0= +github.com/consensys/gnark-crypto v0.18.0/go.mod h1:L3mXGFTe1ZN+RSJ+CLjUt9x7PNdx8ubaYfDROyp2Z8c= +github.com/cpuguy83/go-md2man/v2 v2.0.5 h1:ZtcqGrnekaHpVLArFSe4HK5DoKx1T0rq2DwVB0alcyc= +github.com/cpuguy83/go-md2man/v2 v2.0.5/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/crate-crypto/go-eth-kzg v1.4.0 h1:WzDGjHk4gFg6YzV0rJOAsTK4z3Qkz5jd4RE3DAvPFkg= +github.com/crate-crypto/go-eth-kzg v1.4.0/go.mod h1:J9/u5sWfznSObptgfa92Jq8rTswn6ahQWEuiLHOjCUI= +github.com/crate-crypto/go-ipa v0.0.0-20240724233137-53bbb0ceb27a h1:W8mUrRp6NOVl3J+MYp5kPMoUZPp7aOYHtaua31lwRHg= +github.com/crate-crypto/go-ipa v0.0.0-20240724233137-53bbb0ceb27a/go.mod h1:sTwzHBvIzm2RfVCGNEBZgRyjwK40bVoun3ZnGOCafNM= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dchest/siphash v1.2.3 h1:QXwFc8cFOR2dSa/gE6o/HokBMWtLUaNDVd+22aKHeEA= +github.com/dchest/siphash v1.2.3/go.mod h1:0NvQU092bT0ipiFN++/rXm69QG9tVxLAlQHIXMPAkHc= +github.com/deckarep/golang-set/v2 v2.6.0 h1:XfcQbWM1LlMB8BsJ8N9vW5ehnnPVIw0je80NsVHagjM= +github.com/deckarep/golang-set/v2 v2.6.0/go.mod h1:VAky9rY/yGXJOLEDv3OMci+7wtDpOF4IN+y82NBOac4= +github.com/decred/dcrd/crypto/blake256 v1.0.0 h1:/8DMNYp9SGi5f0w7uCm6d6M4OU2rGFK09Y2A4Xv7EE0= +github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 h1:YLtO71vCjJRCBcrPMtQ9nqBsqpA1m5sE92cU+pd5Mcc= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1/go.mod h1:hyedUtir6IdtD/7lIxGeCxkaw7y45JueMRL4DIyJDKs= +github.com/emicklei/dot v1.6.2 h1:08GN+DD79cy/tzN6uLCT84+2Wk9u+wvqP+Hkx/dIR8A= +github.com/emicklei/dot v1.6.2/go.mod h1:DeV7GvQtIw4h2u73RKBkkFdvVAz0D9fzeJrgPW6gy/s= +github.com/ethereum/c-kzg-4844/v2 v2.1.5 h1:aVtoLK5xwJ6c5RiqO8g8ptJ5KU+2Hdquf6G3aXiHh5s= +github.com/ethereum/c-kzg-4844/v2 v2.1.5/go.mod h1:u59hRTTah4Co6i9fDWtiCjTrblJv0UwsqZKCc0GfgUs= +github.com/ethereum/go-bigmodexpfix v0.0.0-20250911101455-f9e208c548ab h1:rvv6MJhy07IMfEKuARQ9TKojGqLVNxQajaXEp/BoqSk= +github.com/ethereum/go-bigmodexpfix v0.0.0-20250911101455-f9e208c548ab/go.mod h1:IuLm4IsPipXKF7CW5Lzf68PIbZ5yl7FFd74l/E0o9A8= +github.com/ethereum/go-ethereum v1.16.7 h1:qeM4TvbrWK0UC0tgkZ7NiRsmBGwsjqc64BHo20U59UQ= +github.com/ethereum/go-ethereum v1.16.7/go.mod h1:Fs6QebQbavneQTYcA39PEKv2+zIjX7rPUZ14DER46wk= +github.com/ethereum/go-verkle v0.2.2 h1:I2W0WjnrFUIzzVPwm8ykY+7pL2d4VhlsePn4j7cnFk8= +github.com/ethereum/go-verkle v0.2.2/go.mod h1:M3b90YRnzqKyyzBEWJGqj8Qff4IDeXnzFw0P9bFw3uk= +github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= +github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= +github.com/ferranbt/fastssz v0.1.4 h1:OCDB+dYDEQDvAgtAGnTSidK1Pe2tW3nFV40XyMkTeDY= +github.com/ferranbt/fastssz v0.1.4/go.mod h1:Ea3+oeoRGGLGm5shYAeDgu6PGUlcvQhE2fILyD9+tGg= +github.com/gagliardetto/binary v0.8.0 h1:U9ahc45v9HW0d15LoN++vIXSJyqR/pWw8DDlhd7zvxg= +github.com/gagliardetto/binary v0.8.0/go.mod h1:2tfj51g5o9dnvsc+fL3Jxr22MuWzYXwx9wEoN0XQ7/c= +github.com/gagliardetto/gofuzz v1.2.2 h1:XL/8qDMzcgvR4+CyRQW9UGdwPRPMHVJfqQ/uMvSUuQw= +github.com/gagliardetto/gofuzz v1.2.2/go.mod h1:bkH/3hYLZrMLbfYWA0pWzXmi5TTRZnu4pMGZBkqMKvY= +github.com/gagliardetto/solana-go v1.14.0 h1:3WfAi70jOOjAJ0deFMjdhFYlLXATF4tOQXsDNWJtOLw= +github.com/gagliardetto/solana-go v1.14.0/go.mod h1:l/qqqIN6qJJPtxW/G1PF4JtcE3Zg2vD2EliZrr9Gn5k= +github.com/gagliardetto/treeout v0.1.4 h1:ozeYerrLCmCubo1TcIjFiOWTTGteOOHND1twdFpgwaw= +github.com/gagliardetto/treeout v0.1.4/go.mod h1:loUefvXTrlRG5rYmJmExNryyBRh8f89VZhmMOyCyqok= +github.com/getsentry/sentry-go v0.27.0 h1:Pv98CIbtB3LkMWmXi4Joa5OOcwbmnX88sF5qbK3r3Ps= +github.com/getsentry/sentry-go v0.27.0/go.mod h1:lc76E2QywIyW8WuBnwl8Lc4bkmQH4+w1gwTf25trprY= +github.com/go-ole/go-ole v1.2.5/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= +github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= +github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= +github.com/gofrs/flock v0.12.1 h1:MTLVXXHf8ekldpJk3AKicLij9MdwOWkZ+a/jHHZby9E= +github.com/gofrs/flock v0.12.1/go.mod h1:9zxTsyu5xtJ9DK+1tFZyibEV7y3uwDxPPfbxeeHCoD0= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI= +github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs= +github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/go-cmp v0.5.2 h1:X2ev0eStA3AbceY54o37/0PQ/UWqKEiiO2dKL5OPaFM= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= +github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= +github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/hashicorp/go-bexpr v0.1.10 h1:9kuI5PFotCboP3dkDYFr/wi0gg0QVbSNz5oFRpxn4uE= +github.com/hashicorp/go-bexpr v0.1.10/go.mod h1:oxlubA2vC/gFVfX1A6JGp7ls7uCDlfJn732ehYYg+g0= +github.com/holiman/billy v0.0.0-20250707135307-f2f9b9aae7db h1:IZUYC/xb3giYwBLMnr8d0TGTzPKFGNTCGgGLoyeX330= +github.com/holiman/billy v0.0.0-20250707135307-f2f9b9aae7db/go.mod h1:xTEYN9KCHxuYHs+NmrmzFcnvHMzLLNiGFafCb1n3Mfg= +github.com/holiman/bloomfilter/v2 v2.0.3 h1:73e0e/V0tCydx14a0SCYS/EWCxgwLZ18CZcZKVu0fao= +github.com/holiman/bloomfilter/v2 v2.0.3/go.mod h1:zpoh+gs7qcpqrHr3dB55AMiJwo0iURXE7ZOP9L9hSkA= +github.com/holiman/uint256 v1.3.2 h1:a9EgMPSC1AAaj1SZL5zIQD3WbwTuHrMGOerLjGmM/TA= +github.com/holiman/uint256 v1.3.2/go.mod h1:EOMSn4q6Nyt9P6efbI3bueV4e1b3dGlUCXeiRV4ng7E= +github.com/huin/goupnp v1.3.0 h1:UvLUlWDNpoUdYzb2TCn+MuTWtcjXKSza2n6CBdQ0xXc= +github.com/huin/goupnp v1.3.0/go.mod h1:gnGPsThkYa7bFi/KWmEysQRf48l2dvR5bxr2OFckNX8= +github.com/jackpal/go-nat-pmp v1.0.2 h1:KzKSgb7qkJvOUTqYl9/Hg/me3pWgBmERKrTGD7BdWus= +github.com/jackpal/go-nat-pmp v1.0.2/go.mod h1:QPH045xvCAeXUZOxsnwmrtiCoxIr9eob+4orBN1SBKc= +github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= +github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/klauspost/compress v1.11.4/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= +github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= +github.com/klauspost/compress v1.16.0 h1:iULayQNOReoYUe+1qtKOqw9CwJv3aNQu8ivo7lw1HU4= +github.com/klauspost/compress v1.16.0/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= +github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= +github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/labstack/echo/v4 v4.15.1 h1:S9keusg26gZpjMmPqB5hOEvNKnmd1lNmcHrbbH2lnFs= +github.com/labstack/echo/v4 v4.15.1/go.mod h1:xmw1clThob0BSVRX1CRQkGQ/vjwcpOMjQZSZa9fKA/c= +github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0= +github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU= +github.com/leanovate/gopter v0.2.11 h1:vRjThO1EKPb/1NsDXuDrzldR28RLkBflWYcU9CvzWu4= +github.com/leanovate/gopter v0.2.11/go.mod h1:aK3tzZP/C+p1m3SPRE4SYZFGP7jjkuSI4f7Xvpt0S9c= +github.com/logrusorgru/aurora v2.0.3+incompatible h1:tOpm7WcpBTn4fjmVfgpQq0EfczGlG91VSDkswnjF5A8= +github.com/logrusorgru/aurora v2.0.3+incompatible/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4= +github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= +github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU= +github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo= +github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= +github.com/minio/sha256-simd v1.0.0 h1:v1ta+49hkWZyvaKwrQB8elexRqm6Y0aMLjCNsrYxo6g= +github.com/minio/sha256-simd v1.0.0/go.mod h1:OuYzVNI5vcoYIAmbIvHPl3N3jUzVedXbKy5RFepssQM= +github.com/mitchellh/go-testing-interface v1.14.1 h1:jrgshOhYAUVNMAJiKbEu7EqAwgJJ2JqpQmpLJOu07cU= +github.com/mitchellh/go-testing-interface v1.14.1/go.mod h1:gfgS7OtZj6MA4U1UrDRp04twqAjfvlZyCfX3sDjEym8= +github.com/mitchellh/mapstructure v1.4.1 h1:CpVNEelQCZBooIPDn+AR3NpivK/TIKU8bDxdASFVQag= +github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/mitchellh/pointerstructure v1.2.0 h1:O+i9nHnXS3l/9Wu7r4NrEdwA2VFTicjUEN1uBnDo34A= +github.com/mitchellh/pointerstructure v1.2.0/go.mod h1:BRAsLI5zgXmw97Lf6s25bs8ohIXc3tViBH44KcwB2g4= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc= +github.com/mostynb/zstdpool-freelist v0.0.0-20201229113212-927304c0c3b1 h1:mPMvm6X6tf4w8y7j9YIt6V9jfWhL6QlbEc7CCmeQlWk= +github.com/mostynb/zstdpool-freelist v0.0.0-20201229113212-927304c0c3b1/go.mod h1:ye2e/VUEtE2BHE+G/QcKkcLQVAEJoYRFj5VUOQatCRE= +github.com/mr-tron/base58 v1.2.0 h1:T/HDJBh4ZCPbU39/+c3rRvE0uKBQlU27+QI8LJ4t64o= +github.com/mr-tron/base58 v1.2.0/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc= +github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= +github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= +github.com/onsi/gomega v1.10.1 h1:o0+MgICZLuZ7xjH7Vx6zS/zcu93/BEp1VwkIW1mEXCE= +github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= +github.com/pion/dtls/v2 v2.2.7 h1:cSUBsETxepsCSFSxC3mc/aDo14qQLMSL+O6IjG28yV8= +github.com/pion/dtls/v2 v2.2.7/go.mod h1:8WiMkebSHFD0T+dIU+UeBaoV7kDhOW5oDCzZ7WZ/F9s= +github.com/pion/logging v0.2.2 h1:M9+AIj/+pxNsDfAT64+MAVgJO0rsyLnoJKCqf//DoeY= +github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms= +github.com/pion/stun/v2 v2.0.0 h1:A5+wXKLAypxQri59+tmQKVs7+l6mMM+3d+eER9ifRU0= +github.com/pion/stun/v2 v2.0.0/go.mod h1:22qRSh08fSEttYUmJZGlriq9+03jtVmXNODgLccj8GQ= +github.com/pion/transport/v2 v2.2.1 h1:7qYnCBlpgSJNYMbLCKuSY9KbQdBFoETvPNETv0y4N7c= +github.com/pion/transport/v2 v2.2.1/go.mod h1:cXXWavvCnFF6McHTft3DWS9iic2Mftcz1Aq29pGcU5g= +github.com/pion/transport/v3 v3.0.1 h1:gDTlPJwROfSfz6QfSi0ZmeCSkFcnWWiiR9ES0ouANiM= +github.com/pion/transport/v3 v3.0.1/go.mod h1:UY7kiITrlMv7/IKgd5eTUcaahZx5oUN3l9SzK5f5xE0= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v1.15.0 h1:5fCgGYogn0hFdhyhLbw7hEsWxufKtY9klyvdNfFlFhM= +github.com/prometheus/client_golang v1.15.0/go.mod h1:e9yaBhRPU2pPNsZwE+JdQl0KEt1N9XgF6zxWmaC0xOk= +github.com/prometheus/client_model v0.3.0 h1:UBgGFHqYdG/TPFD1B1ogZywDqEkwp3fBMvqdiQ7Xew4= +github.com/prometheus/client_model v0.3.0/go.mod h1:LDGWKZIo7rky3hgvBe+caln+Dr3dPggB5dvjtD7w9+w= +github.com/prometheus/common v0.42.0 h1:EKsfXEYo4JpWMHH5cg+KOUWeuJSov1Id8zGR8eeI1YM= +github.com/prometheus/common v0.42.0/go.mod h1:xBwqVerjNdUDjgODMpudtOMwlOwf2SaTr1yjz4b7Zbc= +github.com/prometheus/procfs v0.9.0 h1:wzCHvIvM5SxWqYvwgVL7yJY8Lz3PKn49KQtpgMYJfhI= +github.com/prometheus/procfs v0.9.0/go.mod h1:+pB4zwohETzFnmlpe6yd2lSc+0/46IYZRB/chUwxUZY= +github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= +github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= +github.com/rs/cors v1.7.0 h1:+88SsELBHx5r+hZ8TCkggzSstaWNbDvThkVK8H6f9ik= +github.com/rs/cors v1.7.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU= +github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible h1:Bn1aCHHRnjv4Bl16T8rcaFjYSrGrIZvpiGO6P3Q4GpU= +github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA= +github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8= +github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= +github.com/streamingfast/logging v0.0.0-20230608130331-f22c91403091 h1:RN5mrigyirb8anBEtdjtHFIufXdacyTi6i4KBfeNXeo= +github.com/streamingfast/logging v0.0.0-20230608130331-f22c91403091/go.mod h1:VlduQ80JcGJSargkRU4Sg9Xo63wZD/l8A5NC/Uo1/uU= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/supranational/blst v0.3.16-0.20250831170142-f48500c1fdbe h1:nbdqkIGOGfUAD54q1s2YBcBz/WcsxCO9HUQ4aGV5hUw= +github.com/supranational/blst v0.3.16-0.20250831170142-f48500c1fdbe/go.mod h1:jZJtfjgudtNl4en1tzwPIV3KjUnQUvG3/j+w+fVonLw= +github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 h1:epCh84lMvA70Z7CTTCmYQn2CKbY8j86K7/FAIr141uY= +github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45IWZaF22tdD+VEXcAWRA037jwmWEB5VWYORlTpc= +github.com/test-go/testify v1.1.4 h1:Tf9lntrKUMHiXQ07qBScBTSA0dhYQlu83hswqelv1iE= +github.com/test-go/testify v1.1.4/go.mod h1:rH7cfJo/47vWGdi4GPj16x3/t1xGOj2YxzmNQzk2ghU= +github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU= +github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI= +github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk= +github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY= +github.com/urfave/cli/v2 v2.27.5 h1:WoHEJLdsXr6dDWoJgMq/CboDmyY/8HMMH1fTECbih+w= +github.com/urfave/cli/v2 v2.27.5/go.mod h1:3Sevf16NykTbInEnD0yKkjDAeZDS0A6bzhBH5hrMvTQ= +github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= +github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= +github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo= +github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= +github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= +github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4= +github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM= +github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f h1:J9EGpcZtP0E/raorCMxlFGSTBrsSlaDGf3jU/qvAE2c= +github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= +github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0= +github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= +github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74= +github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= +github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4= +github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM= +github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA= +github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +go.mongodb.org/mongo-driver v1.12.2 h1:gbWY1bJkkmUB9jjZzcdhOL8O85N9H+Vvsf2yFN0RDws= +go.mongodb.org/mongo-driver v1.12.2/go.mod h1:/rGBTebI3XYboVmgz+Wv3Bcbl3aD0QF9zl6kDDw18rQ= +go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw= +go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/goleak v1.1.11/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= +go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4= +go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= +go.uber.org/ratelimit v0.2.0 h1:UQE2Bgi7p2B85uP5dC2bbRtig0C+OeNRnNEafLjsLPA= +go.uber.org/ratelimit v0.2.0/go.mod h1:YYBV4e4naJvhpitQrWJu1vCpgB7CboMe0qhltKt6mUg= +go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= +go.uber.org/zap v1.21.0 h1:WefMeulhovoZ2sYXz7st6K0sLj7bBhpiFaud4r4zST8= +go.uber.org/zap v1.21.0/go.mod h1:wjWOCqI0f2ZZrJF/UufIOkiC8ii6tm1iqIsLo76RfJw= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20220214200702-86341886e292/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU= +golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0= +golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df h1:UA2aFVmmsIlefxMk29Dp2juaUSth8Pyn3Tq5Y5mJGME= +golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU= +golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= +golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= +golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q= +golang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU= +golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY= +golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= +golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw= +google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc= +gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/e2e/servers/echo/install.sh b/e2e/servers/echo/install.sh new file mode 100755 index 0000000000..9928a06918 --- /dev/null +++ b/e2e/servers/echo/install.sh @@ -0,0 +1,6 @@ +#!/bin/bash +set -e + +echo "Installing Go dependencies for Echo server..." +go mod tidy +echo "✅ Dependencies installed" diff --git a/e2e/servers/echo/main.go b/e2e/servers/echo/main.go new file mode 100644 index 0000000000..0269f74880 --- /dev/null +++ b/e2e/servers/echo/main.go @@ -0,0 +1,443 @@ +package main + +import ( + "fmt" + "net/http" + "os" + "os/signal" + "syscall" + "time" + + x402 "github.com/coinbase/x402/go" + "github.com/coinbase/x402/go/extensions/bazaar" + "github.com/coinbase/x402/go/extensions/eip2612gassponsor" + "github.com/coinbase/x402/go/extensions/erc20approvalgassponsor" + "github.com/coinbase/x402/go/extensions/types" + x402http "github.com/coinbase/x402/go/http" + echomw "github.com/coinbase/x402/go/http/echo" + exactevm "github.com/coinbase/x402/go/mechanisms/evm/exact/server" + uptoevm "github.com/coinbase/x402/go/mechanisms/evm/upto/server" + svm "github.com/coinbase/x402/go/mechanisms/svm/exact/server" + "github.com/joho/godotenv" + "github.com/labstack/echo/v4" +) + +var shutdownRequested bool + +// Echo E2E Test Server with x402 v2 Payment Middleware +// +// This server demonstrates how to integrate x402 v2 payment middleware +// with an Echo application for end-to-end testing. + +func main() { + // Load .env file if it exists + if err := godotenv.Load(); err != nil { + fmt.Println("Warning: .env file not found. Using environment variables.") + } + + // Get configuration from environment + port := os.Getenv("PORT") + if port == "" { + port = "4021" + } + + evmPayeeAddress := os.Getenv("EVM_PAYEE_ADDRESS") + if evmPayeeAddress == "" { + fmt.Println("❌ EVM_PAYEE_ADDRESS environment variable is required") + os.Exit(1) + } + + svmPayeeAddress := os.Getenv("SVM_PAYEE_ADDRESS") + if svmPayeeAddress == "" { + fmt.Println("❌ SVM_PAYEE_ADDRESS environment variable is required") + os.Exit(1) + } + + facilitatorURL := os.Getenv("FACILITATOR_URL") + if facilitatorURL == "" { + fmt.Println("❌ FACILITATOR_URL environment variable is required") + os.Exit(1) + } + + // Network configurations (from env or defaults) + evmNetworkStr := os.Getenv("EVM_NETWORK") + if evmNetworkStr == "" { + evmNetworkStr = "eip155:84532" // Default: Base Sepolia + } + svmNetworkStr := os.Getenv("SVM_NETWORK") + if svmNetworkStr == "" { + svmNetworkStr = "solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1" // Default: Solana Devnet + } + evmNetwork := x402.Network(evmNetworkStr) + svmNetwork := x402.Network(svmNetworkStr) + + evmPermit2Asset := os.Getenv("EVM_PERMIT2_ASSET") + if evmPermit2Asset == "" { + evmPermit2Asset = "0x036CbD53842c5426634e7929541eC2318f3dCF7e" + } + + fmt.Printf("EVM Payee address: %s\n", evmPayeeAddress) + fmt.Printf("SVM Payee address: %s\n", svmPayeeAddress) + fmt.Printf("Using remote facilitator at: %s\n", facilitatorURL) + + // Create Echo instance + e := echo.New() + e.HideBanner = true + + // Create HTTP facilitator client + facilitatorClient := x402http.NewHTTPFacilitatorClient(&x402http.FacilitatorConfig{ + URL: facilitatorURL, + }) + + // Configure x402 payment middleware + + // Declare bazaar discovery extension for GET endpoints + discoveryExtension, err := bazaar.DeclareDiscoveryExtension( + bazaar.MethodGET, + nil, // No query params + nil, // No input schema + "", // No body type (GET method) + &types.OutputConfig{ + Example: map[string]interface{}{ + "message": "Protected endpoint accessed successfully", + "timestamp": "2024-01-01T00:00:00Z", + }, + Schema: types.JSONSchema{ + "properties": map[string]interface{}{ + "message": map[string]interface{}{"type": "string"}, + "timestamp": map[string]interface{}{"type": "string"}, + }, + "required": []string{"message", "timestamp"}, + }, + }, + ) + if err != nil { + fmt.Printf("Warning: Failed to create bazaar extension: %v\n", err) + } + + routes := x402http.RoutesConfig{ + "GET /exact/evm/eip3009": { + Accepts: x402http.PaymentOptions{ + { + Scheme: "exact", + PayTo: evmPayeeAddress, + Price: "$0.001", + Network: evmNetwork, + }, + }, + Extensions: map[string]interface{}{ + types.BAZAAR.Key(): discoveryExtension, + }, + }, + "GET /exact/svm": { + Accepts: x402http.PaymentOptions{ + { + Scheme: "exact", + PayTo: svmPayeeAddress, + Price: "$0.001", + Network: svmNetwork, + }, + }, + Extensions: map[string]interface{}{ + types.BAZAAR.Key(): discoveryExtension, + }, + }, + // Permit2 direct endpoint - standard settle, no gas sponsoring (client must pre-approve Permit2) + "GET /exact/evm/permit2": { + Accepts: x402http.PaymentOptions{ + { + Scheme: "exact", + PayTo: evmPayeeAddress, + Network: evmNetwork, + Price: map[string]interface{}{ + "amount": "1000", + "asset": "0x036CbD53842c5426634e7929541eC2318f3dCF7e", + "extra": map[string]interface{}{ + "assetTransferMethod": "permit2", + }, + }, + }, + }, + Extensions: map[string]interface{}{ + types.BAZAAR.Key(): discoveryExtension, + }, + }, + "GET /exact/evm/permit2-eip2612GasSponsoring": { + Accepts: x402http.PaymentOptions{ + { + Scheme: "exact", + PayTo: evmPayeeAddress, + Network: evmNetwork, + Price: map[string]interface{}{ + "amount": "1000", + "asset": evmPermit2Asset, + "extra": func() map[string]interface{} { + name := "USD Coin" + if evmNetworkStr == "eip155:84532" { + name = "USDC" + } + return map[string]interface{}{ + "assetTransferMethod": "permit2", + "name": name, + "version": "2", + } + }(), + }, + }, + }, + Extensions: func() map[string]interface{} { + ext := map[string]interface{}{ + types.BAZAAR.Key(): discoveryExtension, + } + for k, v := range eip2612gassponsor.DeclareEip2612GasSponsoringExtension() { + ext[k] = v + } + return ext + }(), + }, + "GET /upto/evm/permit2": { + Accepts: x402http.PaymentOptions{ + { + Scheme: "upto", + PayTo: evmPayeeAddress, + Network: evmNetwork, + Price: map[string]interface{}{ + "amount": "2000", + "asset": evmPermit2Asset, + "extra": map[string]interface{}{ + "assetTransferMethod": "permit2", + "name": "USDC", + "version": "2", + }, + }, + }, + }, + Extensions: func() map[string]interface{} { + ext := map[string]interface{}{ + types.BAZAAR.Key(): discoveryExtension, + } + for k, v := range eip2612gassponsor.DeclareEip2612GasSponsoringExtension() { + ext[k] = v + } + return ext + }(), + }, + "GET /exact/evm/permit2-erc20ApprovalGasSponsoring": { + Accepts: x402http.PaymentOptions{ + { + Scheme: "exact", + PayTo: evmPayeeAddress, + Network: evmNetwork, + Price: map[string]interface{}{ + "amount": "1000", + "asset": evmPermit2Asset, + "extra": map[string]interface{}{ + "assetTransferMethod": "permit2", + }, + }, + }, + }, + Extensions: func() map[string]interface{} { + ext := map[string]interface{}{ + types.BAZAAR.Key(): discoveryExtension, + } + for k, v := range erc20approvalgassponsor.DeclareExtension() { + ext[k] = v + } + return ext + }(), + }, + } + + // Apply payment middleware with detailed error logging + e.Use(echomw.X402Payment(echomw.Config{ + Routes: routes, + Facilitator: facilitatorClient, + Schemes: []echomw.SchemeConfig{ + {Network: evmNetwork, Server: exactevm.NewExactEvmScheme()}, + {Network: evmNetwork, Server: uptoevm.NewUptoEvmScheme()}, + {Network: svmNetwork, Server: svm.NewExactSvmScheme()}, + }, + SyncFacilitatorOnStart: true, + Timeout: 30 * time.Second, + ErrorHandler: func(c echo.Context, err error) { + // Log detailed error information for debugging + fmt.Printf("❌ [E2E SERVER ERROR] Payment error occurred\n") + fmt.Printf(" Path: %s\n", c.Request().URL.Path) + fmt.Printf(" Method: %s\n", c.Request().Method) + fmt.Printf(" Error: %v\n", err) + fmt.Printf(" Headers: %v\n", c.Request().Header) + + // Default error response + c.JSON(http.StatusPaymentRequired, map[string]interface{}{ + "error": err.Error(), + }) + }, + SettlementHandler: func(c echo.Context, settleResp *x402.SettleResponse) { + // Log successful settlement + fmt.Printf("✅ [E2E SERVER SUCCESS] Payment settled\n") + fmt.Printf(" Path: %s\n", c.Request().URL.Path) + fmt.Printf(" Transaction: %s\n", settleResp.Transaction) + fmt.Printf(" Network: %s\n", settleResp.Network) + fmt.Printf(" Payer: %s\n", settleResp.Payer) + }, + })) + + // Protected endpoint - requires payment to access + e.GET("/exact/evm/eip3009", func(c echo.Context) error { + if shutdownRequested { + return c.JSON(http.StatusServiceUnavailable, map[string]interface{}{ + "error": "Server shutting down", + }) + } + + return c.JSON(http.StatusOK, map[string]interface{}{ + "message": "Protected endpoint accessed successfully (EVM)", + "timestamp": time.Now().Format(time.RFC3339), + "network": "eip155:84532", + }) + }) + + // Protected SVM endpoint - requires payment to access + e.GET("/exact/svm", func(c echo.Context) error { + if shutdownRequested { + return c.JSON(http.StatusServiceUnavailable, map[string]interface{}{ + "error": "Server shutting down", + }) + } + + return c.JSON(http.StatusOK, map[string]interface{}{ + "message": "Protected endpoint accessed successfully (SVM)", + "timestamp": time.Now().Format(time.RFC3339), + "network": "solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1", + }) + }) + + // Protected Permit2 direct endpoint - standard settle (no gas sponsoring) + e.GET("/exact/evm/permit2", func(c echo.Context) error { + if shutdownRequested { + return c.JSON(http.StatusServiceUnavailable, map[string]interface{}{ + "error": "Server shutting down", + }) + } + + return c.JSON(http.StatusOK, map[string]interface{}{ + "message": "Permit2 endpoint accessed successfully", + "timestamp": time.Now().Format(time.RFC3339), + "method": "permit2", + }) + }) + + // Protected Permit2 EIP-2612 endpoint - Permit2 with gas sponsoring + e.GET("/exact/evm/permit2-eip2612GasSponsoring", func(c echo.Context) error { + if shutdownRequested { + return c.JSON(http.StatusServiceUnavailable, map[string]interface{}{ + "error": "Server shutting down", + }) + } + + return c.JSON(http.StatusOK, map[string]interface{}{ + "message": "Permit2 EIP-2612 endpoint accessed successfully", + "timestamp": time.Now().Format(time.RFC3339), + "method": "permit2-eip2612", + }) + }) + + // Protected Permit2 ERC-20 approval endpoint + e.GET("/exact/evm/permit2-erc20ApprovalGasSponsoring", func(c echo.Context) error { + if shutdownRequested { + return c.JSON(http.StatusServiceUnavailable, map[string]interface{}{ + "error": "Server shutting down", + }) + } + + return c.JSON(http.StatusOK, map[string]interface{}{ + "message": "Permit2 ERC-20 approval endpoint accessed successfully", + "timestamp": time.Now().Format(time.RFC3339), + "method": "permit2-erc20-approval", + }) + }) + + e.GET("/upto/evm/permit2", func(c echo.Context) error { + if shutdownRequested { + return c.JSON(http.StatusServiceUnavailable, map[string]interface{}{ + "error": "Server shutting down", + }) + } + + echomw.SetSettlementOverrides(c, &x402.SettlementOverrides{Amount: "1000"}) + + return c.JSON(http.StatusOK, map[string]interface{}{ + "message": "Upto Permit2 endpoint accessed successfully", + "timestamp": time.Now().Format(time.RFC3339), + "method": "upto-permit2", + }) + }) + + // Health check endpoint - no payment required + e.GET("/health", func(c echo.Context) error { + return c.JSON(http.StatusOK, map[string]interface{}{ + "status": "ok", + "version": "2.0.0", + "evm_network": string(evmNetwork), + "evm_payee": evmPayeeAddress, + "svm_network": string(svmNetwork), + "svm_payee": svmPayeeAddress, + }) + }) + + // Shutdown endpoint - used by e2e tests + e.POST("/close", func(c echo.Context) error { + shutdownRequested = true + + fmt.Println("Received shutdown request") + + // Schedule server shutdown after response + go func() { + time.Sleep(100 * time.Millisecond) + os.Exit(0) + }() + + return c.JSON(http.StatusOK, map[string]interface{}{ + "message": "Server shutting down gracefully", + }) + }) + + // Set up graceful shutdown + quit := make(chan os.Signal, 1) + signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) + + go func() { + <-quit + fmt.Println("Received shutdown signal, exiting...") + os.Exit(0) + }() + + // Print startup banner + fmt.Printf(` +╔════════════════════════════════════════════════════════╗ +║ x402 Echo E2E Test Server ║ +╠════════════════════════════════════════════════════════╣ +║ Server: http://localhost:%-29s ║ +║ EVM Network: %-40s ║ +║ EVM Payee: %-40s ║ +║ SVM Network: %-40s ║ +║ SVM Payee: %-40s ║ +║ ║ +║ Endpoints: ║ +║ • GET /exact/evm/eip3009 (EVM EIP-3009) ║ +║ • GET /exact/evm/permit2 (Permit2) ║ +║ • GET /exact/evm/permit2-eip2612GasSponsoring ║ +║ • GET /exact/evm/permit2-erc20ApprovalGasSponsoring ║ +║ • GET /exact/svm (SVM) ║ +║ • GET /upto/evm/permit2 (Upto Permit2) ║ +║ • GET /health (no payment required) ║ +║ • POST /close (shutdown server) ║ +╚════════════════════════════════════════════════════════╝ +`, port, evmNetwork, evmPayeeAddress, svmNetwork, svmPayeeAddress) + + if err := e.Start(":" + port); err != nil && err != http.ErrServerClosed { + fmt.Printf("Error starting server: %v\n", err) + os.Exit(1) + } +} diff --git a/e2e/servers/echo/run.sh b/e2e/servers/echo/run.sh new file mode 100755 index 0000000000..5f2f5f8501 --- /dev/null +++ b/e2e/servers/echo/run.sh @@ -0,0 +1,2 @@ +#!/bin/bash +go run main.go diff --git a/e2e/servers/echo/test.config.json b/e2e/servers/echo/test.config.json new file mode 100644 index 0000000000..74682bc629 --- /dev/null +++ b/e2e/servers/echo/test.config.json @@ -0,0 +1,87 @@ +{ + "name": "echo", + "type": "server", + "language": "go", + "x402Version": 2, + "extensions": [ + "bazaar", + "eip2612GasSponsoring", + "erc20ApprovalGasSponsoring" + ], + "description": "Go Echo server with x402 v2 payment middleware", + "endpoints": [ + { + "path": "/exact/evm/eip3009", + "method": "GET", + "description": "Protected endpoint requiring EIP-3009 payment", + "requiresPayment": true, + "protocolFamily": "evm", + "transferMethod": "eip3009" + }, + { + "path": "/exact/evm/permit2", + "method": "GET", + "description": "Protected endpoint requiring Permit2 payment (standard settle, no gas sponsoring)", + "requiresPayment": true, + "protocolFamily": "evm", + "transferMethod": "permit2", + "permit2Direct": true + }, + { + "path": "/exact/evm/permit2-eip2612GasSponsoring", + "method": "GET", + "description": "Protected endpoint requiring Permit2 payment with EIP-2612 gas sponsoring", + "requiresPayment": true, + "protocolFamily": "evm", + "transferMethod": "permit2", + "coldstart": true + }, + { + "path": "/exact/evm/permit2-erc20ApprovalGasSponsoring", + "method": "GET", + "description": "Protected endpoint requiring Permit2 payment with ERC-20 approval gas sponsoring", + "requiresPayment": true, + "protocolFamily": "evm", + "transferMethod": "permit2", + "extensions": ["erc20ApprovalGasSponsoring"], + "coldstart": true + }, + { + "path": "/upto/evm/permit2", + "method": "GET", + "description": "Protected endpoint requiring upto Permit2 payment (usage-based settlement)", + "requiresPayment": true, + "protocolFamily": "evm", + "transferMethod": "upto", + "permit2Direct": true + }, + { + "path": "/exact/svm", + "method": "GET", + "description": "Protected endpoint requiring payment (SVM)", + "requiresPayment": true, + "protocolFamily": "svm" + }, + { + "path": "/health", + "method": "GET", + "description": "Health check endpoint", + "health": true + }, + { + "path": "/close", + "method": "POST", + "description": "Graceful shutdown endpoint", + "close": true + } + ], + "environment": { + "required": [ + "PORT", + "EVM_PAYEE_ADDRESS", + "SVM_PAYEE_ADDRESS", + "FACILITATOR_URL" + ], + "optional": [] + } +} diff --git a/e2e/servers/express/README.md b/e2e/servers/express/README.md index c28f19f801..0e0d0aaf44 100644 --- a/e2e/servers/express/README.md +++ b/e2e/servers/express/README.md @@ -1,13 +1,13 @@ # E2E Test Server: Express (TypeScript) -This server demonstrates and tests the x402 Express.js middleware with both EVM and SVM payment protection. +This server demonstrates and tests the x402 Express.js middleware with EVM, SVM, and optional Stellar payment protection. ## What It Tests ### Core Functionality - ✅ **V2 Protocol** - Modern x402 server middleware - ✅ **Payment Protection** - Middleware protecting specific routes -- ✅ **Multi-chain Support** - EVM and SVM payment acceptance +- ✅ **Multi-chain Support** - EVM, SVM, and (optional) Stellar payment acceptance - ✅ **Facilitator Integration** - HTTP communication with facilitator - ✅ **Extension Support** - Bazaar discovery metadata - ✅ **Settlement Handling** - Payment verification and confirmation @@ -15,6 +15,7 @@ This server demonstrates and tests the x402 Express.js middleware with both EVM ### Protected Endpoints - ✅ `GET /protected` - Requires EVM payment (USDC on Base Sepolia) - ✅ `GET /protected-svm` - Requires SVM payment (USDC on Solana Devnet) +- ✅ `GET /protected-stellar` - Requires Stellar payment (USDC on Stellar Testnet) ## What It Demonstrates @@ -24,7 +25,8 @@ This server demonstrates and tests the x402 Express.js middleware with both EVM import express from "express"; import { x402Middleware } from "@x402/server/express"; import { ExactEvmServer } from "@x402/evm"; -import { ExactEvmServer } from "@x402/svm"; +import { ExactSvmServer } from "@x402/svm"; +import { ExactStellarServer } from "@x402/stellar"; const app = express(); @@ -47,16 +49,26 @@ const routes = { extensions: { bazaar: discoveryMetadata } + }, + "GET /protected-stellar": { + scheme: "exact", + network: "stellar:testnet", + payTo: "YourStellarAddress", + price: "$0.001", + extensions: { + bazaar: discoveryMetadata + } } }; -// Apply x402 middleware with EVM and SVM servers +// Apply x402 middleware with EVM, SVM, and Stellar servers app.use(x402Middleware({ routes, facilitatorUrl: "http://localhost:4023", servers: { "eip155:84532": new ExactEvmServer(), - "solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1": new ExactSvmServer() + "solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1": new ExactSvmServer(), + "stellar:testnet": new ExactStellarServer() } })); @@ -68,6 +80,10 @@ app.get("/protected", (req, res) => { app.get("/protected-svm", (req, res) => { res.json({ message: "SVM payment successful!" }); }); + +app.get("/protected-stellar", (req, res) => { + res.json({ message: "Stellar payment successful!" }); +}); ``` ### Key Concepts Shown @@ -84,7 +100,7 @@ app.get("/protected-svm", (req, res) => { This server is tested with: - **Clients:** TypeScript Fetch, Go HTTP - **Facilitators:** TypeScript, Go -- **Payment Types:** EVM (Base Sepolia), SVM (Solana Devnet) +- **Payment Types:** EVM (Base Sepolia), SVM (Solana Devnet), Stellar (Stellar Testnet) - **Protocols:** V2 (primary), V1 (via client negotiation) ### Request Flow @@ -107,18 +123,24 @@ cd e2e/servers/express export FACILITATOR_URL="http://localhost:4023" export EVM_PAYEE_ADDRESS="0x..." export SVM_PAYEE_ADDRESS="..." +export STELLAR_PAYEE_ADDRESS="G..." # optional export PORT=4022 pnpm start ``` ## Environment Variables +### Required - `PORT` - HTTP server port (default: 4022) - `FACILITATOR_URL` - Facilitator endpoint URL - `EVM_PAYEE_ADDRESS` - Ethereum address to receive payments - `SVM_PAYEE_ADDRESS` - Solana address to receive payments + +### Optional +- `STELLAR_PAYEE_ADDRESS` - Stellar address to receive payments - enables Stellar endpoint - `EVM_NETWORK` - EVM network (default: eip155:84532) - `SVM_NETWORK` - SVM network (default: solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1) +- `STELLAR_NETWORK` - Stellar network (default: stellar:testnet) ## Response Examples @@ -151,6 +173,7 @@ PAYMENT-RESPONSE: - `@x402/server` - Express middleware - `@x402/evm` - EVM server - `@x402/svm` - SVM server +- `@x402/stellar` - Stellar server - `@x402/extensions/bazaar` - Discovery extension - `express` - HTTP server framework @@ -166,5 +189,6 @@ PAYMENT-RESPONSE: ### Service Integration - **EVM Server** - Handles Base Sepolia USDC payments - **SVM Server** - Handles Solana Devnet USDC payments +- **Stellar Server** - Handles Stellar Testnet USDC contract payments (SEP-41) - **Price Conversion** - "$0.001" → token amounts with decimals - **Asset Resolution** - Automatic USDC contract/mint lookup diff --git a/e2e/servers/express/index.ts b/e2e/servers/express/index.ts index 364a1ca7dd..90591c19c9 100644 --- a/e2e/servers/express/index.ts +++ b/e2e/servers/express/index.ts @@ -1,9 +1,11 @@ import express from "express"; -import { paymentMiddleware } from "@x402/express"; +import { paymentMiddleware, setSettlementOverrides } from "@x402/express"; import { x402ResourceServer, HTTPFacilitatorClient } from "@x402/core/server"; import { ExactEvmScheme } from "@x402/evm/exact/server"; +import { UptoEvmScheme } from "@x402/evm/upto/server"; import { ExactSvmScheme } from "@x402/svm/exact/server"; import { ExactAptosScheme } from "@x402/aptos/exact/server"; +import { ExactStellarScheme } from "@x402/stellar/exact/server"; import { bazaarResourceServerExtension, declareDiscoveryExtension } from "@x402/extensions/bazaar"; import { declareEip2612GasSponsoringExtension, @@ -22,11 +24,15 @@ dotenv.config(); const PORT = process.env.PORT || "4021"; const EVM_NETWORK = (process.env.EVM_NETWORK || "eip155:84532") as `${string}:${string}`; -const SVM_NETWORK = (process.env.SVM_NETWORK || "solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1") as `${string}:${string}`; +const SVM_NETWORK = (process.env.SVM_NETWORK || + "solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1") as `${string}:${string}`; const APTOS_NETWORK = (process.env.APTOS_NETWORK || "aptos:2") as `${string}:${string}`; +const STELLAR_NETWORK = (process.env.STELLAR_NETWORK || "stellar:testnet") as `${string}:${string}`; const EVM_PAYEE_ADDRESS = process.env.EVM_PAYEE_ADDRESS as `0x${string}`; const SVM_PAYEE_ADDRESS = process.env.SVM_PAYEE_ADDRESS as string; +const EVM_PERMIT2_ASSET = process.env.EVM_PERMIT2_ASSET as `0x${string}`; const APTOS_PAYEE_ADDRESS = process.env.APTOS_PAYEE_ADDRESS as string; +const STELLAR_PAYEE_ADDRESS = process.env.STELLAR_PAYEE_ADDRESS as string | undefined; const facilitatorUrl = process.env.FACILITATOR_URL; if (!EVM_PAYEE_ADDRESS) { @@ -39,7 +45,6 @@ if (!SVM_PAYEE_ADDRESS) { process.exit(1); } - if (!facilitatorUrl) { console.error("❌ FACILITATOR_URL environment variable is required"); process.exit(1); @@ -48,30 +53,40 @@ if (!facilitatorUrl) { // Initialize Express app const app = express(); -// Create HTTP facilitator client -const facilitatorClient = new HTTPFacilitatorClient({ url: facilitatorUrl }); +// Create facilitator clients (mock facilitator as fallback for startup validation) +const facilitatorClients = [new HTTPFacilitatorClient({ url: facilitatorUrl })]; +const mockFacilitatorUrl = process.env.MOCK_FACILITATOR_URL; +if (mockFacilitatorUrl) { + facilitatorClients.push(new HTTPFacilitatorClient({ url: mockFacilitatorUrl })); +} // Create x402 resource server -const server = new x402ResourceServer(facilitatorClient); +const server = new x402ResourceServer(facilitatorClients); // Register server schemes server.register("eip155:*", new ExactEvmScheme()); +server.register("eip155:*", new UptoEvmScheme()); server.register("solana:*", new ExactSvmScheme()); if (APTOS_PAYEE_ADDRESS) { server.register("aptos:*", new ExactAptosScheme()); } +if (STELLAR_PAYEE_ADDRESS) { + server.register("stellar:*", new ExactStellarScheme()); +} // Register Bazaar discovery extension server.registerExtension(bazaarResourceServerExtension); -console.log(`Facilitator account: ${process.env.EVM_PRIVATE_KEY ? process.env.EVM_PRIVATE_KEY.substring(0, 10) + '...' : 'not configured'}`); +console.log( + `Facilitator account: ${process.env.EVM_PRIVATE_KEY ? process.env.EVM_PRIVATE_KEY.substring(0, 10) + "..." : "not configured"}`, +); console.log(`Using remote facilitator at: ${facilitatorUrl}`); /** * Pre-middleware guard for optional Aptos endpoint * Returns 501 Not Implemented if Aptos is not configured */ -app.get("/protected-aptos", (req, res, next) => { +app.get("/exact/aptos", (req, res, next) => { if (!APTOS_PAYEE_ADDRESS) { return res.status(501).json({ error: "Aptos payments not configured", @@ -81,17 +96,31 @@ app.get("/protected-aptos", (req, res, next) => { next(); }); +/** + * Pre-middleware guard for optional Stellar endpoint + * Returns 501 Not Implemented if Stellar is not configured + */ +app.use("/exact/stellar", (req, res, next) => { + if (!STELLAR_PAYEE_ADDRESS) { + return res.status(501).json({ + error: "Stellar payments not configured", + message: "STELLAR_PAYEE_ADDRESS environment variable is not set", + }); + } + next(); +}); + /** * Configure x402 payment middleware using builder pattern * * This middleware protects endpoints with $0.001 USDC payment requirements - * on Base Sepolia, Solana Devnet, and Aptos Testnet with bazaar discovery extension. + * on Base Sepolia, Solana Devnet, Aptos Testnet, and Stellar Testnet with bazaar discovery extension. */ app.use( paymentMiddleware( { // Route-specific payment configuration - "GET /protected": { + "GET /exact/evm/eip3009": { accepts: { payTo: EVM_PAYEE_ADDRESS, scheme: "exact", @@ -116,7 +145,7 @@ app.use( }), }, }, - "GET /protected-svm": { + "GET /exact/svm": { accepts: { payTo: SVM_PAYEE_ADDRESS, scheme: "exact", @@ -143,45 +172,44 @@ app.use( }, ...(APTOS_PAYEE_ADDRESS ? { - "GET /protected-aptos": { - accepts: { - payTo: APTOS_PAYEE_ADDRESS, - scheme: "exact", - price: "$0.001", - network: APTOS_NETWORK, - }, - extensions: { - ...declareDiscoveryExtension({ - output: { - example: { - message: "Protected endpoint accessed successfully", - timestamp: "2024-01-01T00:00:00Z", - }, - schema: { - properties: { - message: { type: "string" }, - timestamp: { type: "string" }, - }, - required: ["message", "timestamp"], + "GET /exact/aptos": { + accepts: { + payTo: APTOS_PAYEE_ADDRESS, + scheme: "exact", + price: "$0.001", + network: APTOS_NETWORK, + }, + extensions: { + ...declareDiscoveryExtension({ + output: { + example: { + message: "Protected endpoint accessed successfully", + timestamp: "2024-01-01T00:00:00Z", + }, + schema: { + properties: { + message: { type: "string" }, + timestamp: { type: "string" }, }, + required: ["message", "timestamp"], }, - }), - }, + }, + }), }, - } + }, + } : {}), - // Permit2 endpoint for generic ERC-20 tokens (no EIP-2612, uses raw approve tx) - "GET /protected-permit2-erc20": { + // Permit2 endpoint for ERC-20 approval gas sponsoring (no EIP-2612) + "GET /exact/evm/permit2-erc20ApprovalGasSponsoring": { accepts: { payTo: EVM_PAYEE_ADDRESS, scheme: "exact", network: EVM_NETWORK, price: { amount: "1000", - asset: "0xeED520980fC7C7B4eB379B96d61CEdea2423005a", // Generic MockERC20 token (no EIP-2612) + asset: EVM_PERMIT2_ASSET, extra: { assetTransferMethod: "permit2", - // No name/version - generic ERC-20 without EIP-2612 }, }, }, @@ -189,19 +217,18 @@ app.use( ...declareErc20ApprovalGasSponsoringExtension(), }, }, - // Permit2 endpoint - explicitly requires Permit2 flow instead of EIP-3009 - "GET /protected-permit2": { + // Permit2 standard/direct endpoint - no gas sponsoring, client must pre-approve Permit2 + "GET /exact/evm/permit2": { accepts: { payTo: EVM_PAYEE_ADDRESS, scheme: "exact", network: EVM_NETWORK, - // Use pre-parsed price with assetTransferMethod to force Permit2 price: { - amount: "1000", // 0.001 USDC (6 decimals) - asset: "0x036CbD53842c5426634e7929541eC2318f3dCF7e", // Base Sepolia USDC + amount: "1000", + asset: EVM_PERMIT2_ASSET, extra: { assetTransferMethod: "permit2", - name: "USDC", + name: EVM_NETWORK == "eip155:84532" ? "USDC" : "USD Coin", version: "2", }, }, @@ -224,9 +251,125 @@ app.use( }, }, }), + }, + }, + // Permit2 endpoint with EIP-2612 gas sponsoring + "GET /exact/evm/permit2-eip2612GasSponsoring": { + accepts: { + payTo: EVM_PAYEE_ADDRESS, + scheme: "exact", + network: EVM_NETWORK, + price: "$0.001", + extra: { assetTransferMethod: "permit2" }, + }, + extensions: { + ...declareDiscoveryExtension({ + output: { + example: { + message: "Permit2 EIP-2612 endpoint accessed successfully", + timestamp: "2024-01-01T00:00:00Z", + method: "permit2-eip2612", + }, + schema: { + properties: { + message: { type: "string" }, + timestamp: { type: "string" }, + method: { type: "string" }, + }, + required: ["message", "timestamp", "method"], + }, + }, + }), ...declareEip2612GasSponsoringExtension(), }, }, + // Upto Permit2 standard/direct endpoint - no gas sponsoring, client must pre-approve Permit2 + // Authorizes up to 2000 atomic units, settles 1000 (partial settlement) + "GET /upto/evm/permit2": { + accepts: { + payTo: EVM_PAYEE_ADDRESS, + scheme: "upto", + network: EVM_NETWORK, + price: { + amount: "2000", + asset: EVM_PERMIT2_ASSET, + extra: { + assetTransferMethod: "permit2", + name: EVM_NETWORK == "eip155:84532" ? "USDC" : "USD Coin", + version: "2", + }, + }, + }, + }, + // Upto Permit2 endpoint with EIP-2612 gas sponsoring + // Authorizes up to 2000 atomic units, settles 1000 (partial settlement) + "GET /upto/evm/permit2-eip2612GasSponsoring": { + accepts: { + payTo: EVM_PAYEE_ADDRESS, + scheme: "upto", + network: EVM_NETWORK, + price: { + amount: "2000", + asset: EVM_PERMIT2_ASSET, + extra: { + assetTransferMethod: "permit2", + name: EVM_NETWORK == "eip155:84532" ? "USDC" : "USD Coin", + version: "2", + }, + }, + }, + extensions: { + ...declareEip2612GasSponsoringExtension(), + }, + }, + // Upto Permit2 endpoint for ERC-20 approval gas sponsoring (no EIP-2612) + // Authorizes up to 2000 atomic units, settles 1000 (partial settlement) + "GET /upto/evm/permit2-erc20ApprovalGasSponsoring": { + accepts: { + payTo: EVM_PAYEE_ADDRESS, + scheme: "upto", + network: EVM_NETWORK, + price: { + amount: "2000", + asset: EVM_PERMIT2_ASSET, + extra: { + assetTransferMethod: "permit2", + }, + }, + }, + extensions: { + ...declareErc20ApprovalGasSponsoringExtension(), + }, + }, + ...(STELLAR_PAYEE_ADDRESS + ? { + "GET /exact/stellar": { + accepts: { + payTo: STELLAR_PAYEE_ADDRESS!, + scheme: "exact", + price: "$0.001", + network: STELLAR_NETWORK, + }, + extensions: { + ...declareDiscoveryExtension({ + output: { + example: { + message: "Protected Stellar endpoint accessed successfully", + timestamp: "2024-01-01T00:00:00Z", + }, + schema: { + properties: { + message: { type: "string" }, + timestamp: { type: "string" }, + }, + required: ["message", "timestamp"], + }, + }, + }), + }, + }, + } + : {}), }, server, // Pass pre-configured server instance ), @@ -238,7 +381,7 @@ app.use( * This endpoint demonstrates a resource protected by x402 payment middleware. * Clients must provide a valid payment signature to access this endpoint. */ -app.get("/protected", (req, res) => { +app.get("/exact/evm/eip3009", (req, res) => { res.json({ message: "Protected endpoint accessed successfully", timestamp: new Date().toISOString(), @@ -251,7 +394,7 @@ app.get("/protected", (req, res) => { * This endpoint demonstrates a resource protected by x402 payment middleware for SVM. * Clients must provide a valid payment signature to access this endpoint. */ -app.get("/protected-svm", (req, res) => { +app.get("/exact/svm", (req, res) => { res.json({ message: "Protected endpoint accessed successfully", timestamp: new Date().toISOString(), @@ -265,7 +408,7 @@ app.get("/protected-svm", (req, res) => { * Clients must provide a valid payment signature to access this endpoint. * Note: 501 check is handled by pre-middleware guard above. */ -app.get("/protected-aptos", (req, res) => { +app.get("/exact/aptos", (req, res) => { res.json({ message: "Protected endpoint accessed successfully", timestamp: new Date().toISOString(), @@ -279,7 +422,7 @@ app.get("/protected-aptos", (req, res) => { * that do NOT implement EIP-2612. The facilitator broadcasts the pre-signed * approve() transaction on the client's behalf before settling. */ -app.get("/protected-permit2-erc20", (req, res) => { +app.get("/exact/evm/permit2-erc20ApprovalGasSponsoring", (req, res) => { res.json({ message: "Permit2 ERC-20 approval endpoint accessed successfully", timestamp: new Date().toISOString(), @@ -293,7 +436,7 @@ app.get("/protected-permit2-erc20", (req, res) => { * This endpoint demonstrates the Permit2 payment flow. * Clients must have approved Permit2 to spend their USDC before accessing. */ -app.get("/protected-permit2", (req, res) => { +app.get("/exact/evm/permit2", (req, res) => { res.json({ message: "Permit2 endpoint accessed successfully", timestamp: new Date().toISOString(), @@ -301,6 +444,62 @@ app.get("/protected-permit2", (req, res) => { }); }); +/** + * Protected Permit2 EIP-2612 endpoint - requires payment via Permit2 with gas sponsoring + * + * Uses EIP-2612 permit atomically in settleWithPermit. No pre-approval needed. + */ +app.get("/exact/evm/permit2-eip2612GasSponsoring", (req, res) => { + res.json({ + message: "Permit2 EIP-2612 endpoint accessed successfully", + timestamp: new Date().toISOString(), + method: "permit2-eip2612", + }); +}); + +app.get("/upto/evm/permit2", (req, res) => { + setSettlementOverrides(res, { amount: "1000" }); + res.json({ + message: "Upto Permit2 endpoint accessed successfully", + timestamp: new Date().toISOString(), + method: "upto-permit2", + }); +}); + +app.get("/upto/evm/permit2-eip2612GasSponsoring", (req, res) => { + setSettlementOverrides(res, { amount: "1000" }); + res.json({ + message: "Upto Permit2 EIP-2612 endpoint accessed successfully", + timestamp: new Date().toISOString(), + method: "upto-permit2-eip2612", + }); +}); + +app.get("/upto/evm/permit2-erc20ApprovalGasSponsoring", (req, res) => { + setSettlementOverrides(res, { amount: "1000" }); + res.json({ + message: "Upto Permit2 ERC-20 approval endpoint accessed successfully", + timestamp: new Date().toISOString(), + method: "upto-permit2-erc20-approval", + }); +}); + +/** + * Protected Stellar endpoint - requires payment to access + * + * This endpoint demonstrates a resource protected by x402 payment middleware for Stellar. + * Clients must provide a valid payment signature to access this endpoint. + * Note: 501 check is handled by pre-middleware guard above. + */ +if (STELLAR_PAYEE_ADDRESS) { + app.get("/exact/stellar", (req, res) => { + res.json({ + message: "Protected Stellar endpoint accessed successfully", + timestamp: new Date().toISOString(), + }); + }); +} + /** * Health check endpoint - no payment required * @@ -340,16 +539,20 @@ app.listen(parseInt(PORT), () => { ║ EVM Network: ${EVM_NETWORK} ║ ║ SVM Network: ${SVM_NETWORK} ║ ║ Aptos Network: ${APTOS_NETWORK} ║ +║ Stellar Network: ${STELLAR_NETWORK}║ ║ EVM Payee: ${EVM_PAYEE_ADDRESS} ║ ║ SVM Payee: ${SVM_PAYEE_ADDRESS} ║ ║ Aptos Payee: ${APTOS_PAYEE_ADDRESS || "(not configured)"} +║ Stellar Payee: ${STELLAR_PAYEE_ADDRESS || "(not configured)"} ║ ║ ║ Endpoints: ║ -║ • GET /protected (EIP-3009 payment - EVM) ║ -║ • GET /protected-svm (SVM payment) ║ -║ • GET /protected-aptos (Aptos payment) ║ -║ • GET /protected-permit2 (Permit2 payment - EVM) ║ -║ • GET /protected-permit2-erc20 (Permit2 + ERC-20 approval) ║ +║ • GET /exact/evm/eip3009 (EVM EIP-3009) ║ +║ • GET /exact/evm/permit2 (Permit2) ║ +║ • GET /exact/evm/permit2-eip2612GasSponsoring ║ +║ • GET /exact/evm/permit2-erc20ApprovalGasSponsoring ║ +║ • GET /exact/svm (SVM) ║ +║ • GET /exact/aptos (Aptos) ║ +║ • GET /exact/stellar (Stellar) ║ ║ • GET /health (no payment required) ║ ║ • POST /close (shutdown server) ║ ╚════════════════════════════════════════════════════════╝ diff --git a/e2e/servers/express/package.json b/e2e/servers/express/package.json index eee39400e0..4756535ea6 100644 --- a/e2e/servers/express/package.json +++ b/e2e/servers/express/package.json @@ -15,6 +15,7 @@ "@x402/express": "workspace:*", "@x402/evm": "workspace:*", "@x402/extensions": "workspace:*", + "@x402/stellar": "workspace:*", "@x402/svm": "workspace:*", "dotenv": "^16.6.1", "express": "^4.18.2" @@ -33,4 +34,4 @@ "tsx": "^4.7.0", "typescript": "^5.3.0" } -} \ No newline at end of file +} diff --git a/e2e/servers/express/test.config.json b/e2e/servers/express/test.config.json index 52deba0120..188ab91bbf 100644 --- a/e2e/servers/express/test.config.json +++ b/e2e/servers/express/test.config.json @@ -3,14 +3,11 @@ "type": "server", "language": "typescript", "x402Version": 2, - "extensions": [ - "bazaar", - "eip2612GasSponsoring", - "erc20ApprovalGasSponsoring" - ], + "extensions": ["bazaar", "eip2612GasSponsoring", "erc20ApprovalGasSponsoring"], + "endpoints": [ { - "path": "/protected", + "path": "/exact/evm/eip3009", "method": "GET", "description": "Protected endpoint requiring EIP-3009 payment", "requiresPayment": true, @@ -18,36 +15,82 @@ "transferMethod": "eip3009" }, { - "path": "/protected-permit2", + "path": "/exact/evm/permit2", + "method": "GET", + "description": "Protected endpoint requiring Permit2 payment (standard settle, no gas sponsoring)", + "requiresPayment": true, + "protocolFamily": "evm", + "transferMethod": "permit2", + "permit2Direct": true + }, + { + "path": "/exact/evm/permit2-eip2612GasSponsoring", "method": "GET", - "description": "Protected endpoint requiring Permit2 payment", + "description": "Protected endpoint requiring Permit2 payment with EIP-2612 gas sponsoring", "requiresPayment": true, "protocolFamily": "evm", - "transferMethod": "permit2" + "transferMethod": "permit2", + "coldstart": true }, { - "path": "/protected-permit2-erc20", + "path": "/exact/evm/permit2-erc20ApprovalGasSponsoring", "method": "GET", "description": "Protected endpoint requiring Permit2 payment with ERC-20 approval gas sponsoring", "requiresPayment": true, "protocolFamily": "evm", "transferMethod": "permit2", - "extensions": ["erc20ApprovalGasSponsoring"] + "extensions": ["erc20ApprovalGasSponsoring"], + "coldstart": true }, { - "path": "/protected-svm", + "path": "/upto/evm/permit2", + "method": "GET", + "description": "Protected endpoint requiring Upto Permit2 payment (standard settle, no gas sponsoring)", + "requiresPayment": true, + "protocolFamily": "evm", + "transferMethod": "upto", + "permit2Direct": true + }, + { + "path": "/upto/evm/permit2-eip2612GasSponsoring", + "method": "GET", + "description": "Protected endpoint requiring Upto Permit2 payment with EIP-2612 gas sponsoring", + "requiresPayment": true, + "protocolFamily": "evm", + "transferMethod": "upto", + "coldstart": true + }, + { + "path": "/upto/evm/permit2-erc20ApprovalGasSponsoring", + "method": "GET", + "description": "Protected endpoint requiring Upto Permit2 payment with ERC-20 approval gas sponsoring", + "requiresPayment": true, + "protocolFamily": "evm", + "transferMethod": "upto", + "extensions": ["erc20ApprovalGasSponsoring"], + "coldstart": true + }, + { + "path": "/exact/svm", "method": "GET", "description": "Protected endpoint requiring payment on SVM network", "requiresPayment": true, "protocolFamily": "svm" }, { - "path": "/protected-aptos", + "path": "/exact/aptos", "method": "GET", "description": "Protected endpoint requiring payment on Aptos network", "requiresPayment": true, "protocolFamily": "aptos" }, + { + "path": "/exact/stellar", + "method": "GET", + "description": "Protected endpoint requiring payment on Stellar network", + "requiresPayment": true, + "protocolFamily": "stellar" + }, { "path": "/health", "method": "GET", @@ -62,14 +105,7 @@ } ], "environment": { - "required": [ - "PORT", - "EVM_PAYEE_ADDRESS", - "SVM_PAYEE_ADDRESS", - "FACILITATOR_URL" - ], - "optional": [ - "APTOS_PAYEE_ADDRESS" - ] + "required": ["PORT", "EVM_PAYEE_ADDRESS", "SVM_PAYEE_ADDRESS", "FACILITATOR_URL"], + "optional": ["APTOS_PAYEE_ADDRESS", "STELLAR_PAYEE_ADDRESS"] } } diff --git a/e2e/servers/fastapi/build.sh b/e2e/servers/fastapi/build.sh index f5fbe5e8f8..c1bb071bbe 100755 --- a/e2e/servers/fastapi/build.sh +++ b/e2e/servers/fastapi/build.sh @@ -1,5 +1,5 @@ #!/bin/bash -# Python doesn't require a build step -# This file is intentionally empty -exit 0 +set -e +# Rebuild the local x402 editable dependency so the venv reflects source changes +uv sync --reinstall-package x402 diff --git a/e2e/servers/fastapi/main.py b/e2e/servers/fastapi/main.py index 1d582a8823..1cfce7bedf 100644 --- a/e2e/servers/fastapi/main.py +++ b/e2e/servers/fastapi/main.py @@ -22,6 +22,10 @@ declare_discovery_extension, OutputConfig, ) +from x402.extensions.eip2612_gas_sponsoring import declare_eip2612_gas_sponsoring_extension +from x402.extensions.erc20_approval_gas_sponsoring import ( + declare_erc20_approval_gas_sponsoring_extension, +) # Load environment variables load_dotenv() @@ -31,6 +35,9 @@ SVM_ADDRESS = os.getenv("SVM_PAYEE_ADDRESS") PORT = int(os.getenv("PORT", "4021")) FACILITATOR_URL = os.getenv("FACILITATOR_URL") +EVM_PERMIT2_ASSET = os.getenv( + "EVM_PERMIT2_ASSET", "0x036CbD53842c5426634e7929541eC2318f3dCF7e" +) if not EVM_ADDRESS: print("Error: Missing required environment variable EVM_PAYEE_ADDRESS") @@ -67,7 +74,7 @@ # Define routes with payment requirements routes = { - "GET /protected": { + "GET /exact/evm/eip3009": { "accepts": { "scheme": "exact", "payTo": EVM_ADDRESS, @@ -92,18 +99,18 @@ ), }, }, - "GET /protected-2": { + "GET /exact/svm": { "accepts": { "scheme": "exact", - "payTo": EVM_ADDRESS, - "price": "$0.001", # 0.001 USDC - "network": EVM_NETWORK, + "payTo": SVM_ADDRESS, + "price": "$0.001", + "network": SVM_NETWORK, }, "extensions": { **declare_discovery_extension( output=OutputConfig( example={ - "message": "Access granted to protected resource #2", + "message": "Access granted to SVM protected resource", "timestamp": "2024-01-01T00:00:00Z", }, schema={ @@ -117,29 +124,55 @@ ), }, }, - "GET /protected-svm": { + "GET /exact/evm/permit2-eip2612GasSponsoring": { "accepts": { "scheme": "exact", - "payTo": SVM_ADDRESS, - "price": "$0.001", - "network": SVM_NETWORK, + "payTo": EVM_ADDRESS, + "network": EVM_NETWORK, + "price": { + "amount": "1000", + "asset": EVM_PERMIT2_ASSET, + "extra": { + "assetTransferMethod": "permit2", + "name": "USDC", + "version": "2", + }, + }, }, "extensions": { **declare_discovery_extension( output=OutputConfig( example={ - "message": "Access granted to SVM protected resource", + "message": "Permit2 endpoint accessed successfully", "timestamp": "2024-01-01T00:00:00Z", + "method": "permit2", }, schema={ "properties": { "message": {"type": "string"}, "timestamp": {"type": "string"}, + "method": {"type": "string"}, }, "required": ["message", "timestamp"], }, ) ), + **declare_eip2612_gas_sponsoring_extension(), + }, + }, + "GET /exact/evm/permit2-erc20ApprovalGasSponsoring": { + "accepts": { + "scheme": "exact", + "payTo": EVM_ADDRESS, + "network": EVM_NETWORK, + "price": { + "amount": "1000", + "asset": EVM_PERMIT2_ASSET, + "extra": {"assetTransferMethod": "permit2"}, + }, + }, + "extensions": { + **declare_erc20_approval_gas_sponsoring_extension(), }, }, } @@ -155,7 +188,7 @@ async def x402_payment_middleware(request, call_next): shutdown_requested = False -@app.get("/protected") +@app.get("/exact/evm/eip3009") async def protected_endpoint() -> Dict[str, Any]: """Protected endpoint that requires payment.""" if shutdown_requested: @@ -167,27 +200,41 @@ async def protected_endpoint() -> Dict[str, Any]: } -@app.get("/protected-2") -async def protected_endpoint_2() -> Dict[str, Any]: - """Protected endpoint that requires ERC20 payment.""" +@app.get("/exact/svm") +async def protected_svm_endpoint() -> Dict[str, Any]: + """Protected endpoint that requires SVM (Solana) payment.""" if shutdown_requested: raise HTTPException(status_code=503, detail="Server shutting down") return { - "message": "Access granted to protected resource #2", + "message": "Access granted to SVM protected resource", "timestamp": "2024-01-01T00:00:00Z", } -@app.get("/protected-svm") -async def protected_svm_endpoint() -> Dict[str, Any]: - """Protected endpoint that requires SVM (Solana) payment.""" +@app.get("/exact/evm/permit2-eip2612GasSponsoring") +async def protected_permit2_endpoint() -> Dict[str, Any]: + """Protected endpoint that requires Permit2 payment.""" if shutdown_requested: raise HTTPException(status_code=503, detail="Server shutting down") return { - "message": "Access granted to SVM protected resource", + "message": "Permit2 endpoint accessed successfully", + "timestamp": "2024-01-01T00:00:00Z", + "method": "permit2", + } + + +@app.get("/exact/evm/permit2-erc20ApprovalGasSponsoring") +async def protected_permit2_erc20_endpoint() -> Dict[str, Any]: + """Protected endpoint that requires Permit2 payment with ERC-20 approval sponsoring.""" + if shutdown_requested: + raise HTTPException(status_code=503, detail="Server shutting down") + + return { + "message": "Permit2+ERC20Approval endpoint accessed successfully", "timestamp": "2024-01-01T00:00:00Z", + "method": "permit2+erc20approval", } diff --git a/e2e/servers/fastapi/run.sh b/e2e/servers/fastapi/run.sh index 31c1f93486..c653d856a9 100644 --- a/e2e/servers/fastapi/run.sh +++ b/e2e/servers/fastapi/run.sh @@ -1,4 +1,3 @@ #!/bin/bash -# Ensure dependencies are synced before running -uv sync --quiet +uv sync --reinstall-package x402 --quiet uv run python main.py diff --git a/e2e/servers/fastapi/test.config.json b/e2e/servers/fastapi/test.config.json index 6a532a64f7..98be7db4af 100644 --- a/e2e/servers/fastapi/test.config.json +++ b/e2e/servers/fastapi/test.config.json @@ -4,32 +4,47 @@ "language": "python", "x402Version": 2, "extensions": [ - "bazaar" + "bazaar", + "eip2612GasSponsoring", + "erc20ApprovalGasSponsoring" ], + "evm": { "transferMethods": ["eip3009", "permit2"] }, "description": "Python FastAPI server with x402 v2 payment middleware", "endpoints": [ { - "path": "/protected", + "path": "/exact/evm/eip3009", "method": "GET", - "description": "Protected endpoint requiring payment", + "description": "Protected endpoint requiring EIP-3009 payment", "requiresPayment": true, "protocolFamily": "evm", "transferMethod": "eip3009" }, { - "path": "/protected-2", + "path": "/exact/svm", "method": "GET", - "description": "Protected endpoint requiring ERC20 payment", + "description": "Protected endpoint requiring SVM (Solana) payment", + "requiresPayment": true, + "protocolFamily": "svm" + }, + { + "path": "/exact/evm/permit2-eip2612GasSponsoring", + "method": "GET", + "description": "Protected endpoint requiring Permit2 payment with EIP-2612 gas sponsoring", "requiresPayment": true, "protocolFamily": "evm", - "transferMethod": "eip3009" + "transferMethod": "permit2", + "extensions": ["eip2612GasSponsoring"], + "coldstart": true }, { - "path": "/protected-svm", + "path": "/exact/evm/permit2-erc20ApprovalGasSponsoring", "method": "GET", - "description": "Protected endpoint requiring SVM (Solana) payment", + "description": "Protected endpoint requiring Permit2 payment with ERC-20 approval gas sponsoring", "requiresPayment": true, - "protocolFamily": "svm" + "protocolFamily": "evm", + "transferMethod": "permit2", + "extensions": ["erc20ApprovalGasSponsoring"], + "coldstart": true }, { "path": "/health", @@ -51,7 +66,8 @@ ], "optional": [ "PORT", - "FACILITATOR_URL" + "FACILITATOR_URL", + "EVM_PERMIT2_ASSET" ] } -} \ No newline at end of file +} diff --git a/e2e/servers/fastapi/uv.lock b/e2e/servers/fastapi/uv.lock index fc87dfbe5b..d3fe5a45b0 100644 --- a/e2e/servers/fastapi/uv.lock +++ b/e2e/servers/fastapi/uv.lock @@ -1,5 +1,5 @@ version = 1 -revision = 3 +revision = 2 requires-python = ">=3.10" [[package]] @@ -2841,7 +2841,7 @@ wheels = [ [[package]] name = "x402" -version = "2.2.0" +version = "2.5.0" source = { editable = "../../../python/x402" } dependencies = [ { name = "nest-asyncio" }, @@ -2872,7 +2872,7 @@ svm = [ [package.metadata] requires-dist = [ { name = "eth-abi", marker = "extra == 'evm'", specifier = ">=5.0.0" }, - { name = "eth-account", marker = "extra == 'evm'", specifier = ">=0.12.0" }, + { name = "eth-account", marker = "extra == 'evm'", specifier = ">=0.13.0" }, { name = "eth-keys", marker = "extra == 'evm'", specifier = ">=0.5.0" }, { name = "eth-utils", marker = "extra == 'evm'", specifier = ">=4.0.0" }, { name = "fastapi", extras = ["standard"], marker = "extra == 'fastapi'", specifier = ">=0.115.0" }, @@ -2899,7 +2899,7 @@ provides-extras = ["httpx", "requests", "flask", "fastapi", "evm", "svm", "mcp", dev = [ { name = "black", specifier = ">=23.0.0" }, { name = "eth-abi", specifier = ">=5.0.0" }, - { name = "eth-account", specifier = ">=0.12.0" }, + { name = "eth-account", specifier = ">=0.13.0" }, { name = "eth-keys", specifier = ">=0.5.0" }, { name = "eth-utils", specifier = ">=4.0.0" }, { name = "fastapi", extras = ["standard"], specifier = ">=0.115.0" }, diff --git a/e2e/servers/fastify/.prettierignore b/e2e/servers/fastify/.prettierignore new file mode 100644 index 0000000000..5bd240ba90 --- /dev/null +++ b/e2e/servers/fastify/.prettierignore @@ -0,0 +1,8 @@ +docs/ +dist/ +node_modules/ +coverage/ +.github/ +src/client +**/**/*.json +*.md \ No newline at end of file diff --git a/e2e/servers/fastify/.prettierrc b/e2e/servers/fastify/.prettierrc new file mode 100644 index 0000000000..ffb416b74b --- /dev/null +++ b/e2e/servers/fastify/.prettierrc @@ -0,0 +1,11 @@ +{ + "tabWidth": 2, + "useTabs": false, + "semi": true, + "singleQuote": false, + "trailingComma": "all", + "bracketSpacing": true, + "arrowParens": "avoid", + "printWidth": 100, + "proseWrap": "never" +} diff --git a/e2e/servers/fastify/build.sh b/e2e/servers/fastify/build.sh new file mode 100755 index 0000000000..b61bea4a5b --- /dev/null +++ b/e2e/servers/fastify/build.sh @@ -0,0 +1,4 @@ +#!/bin/bash +# TypeScript build handled by pnpm at root level +# This file is intentionally empty +exit 0 diff --git a/e2e/servers/fastify/eslint.config.js b/e2e/servers/fastify/eslint.config.js new file mode 100644 index 0000000000..e2fde7b3b8 --- /dev/null +++ b/e2e/servers/fastify/eslint.config.js @@ -0,0 +1,73 @@ +import js from "@eslint/js"; +import ts from "@typescript-eslint/eslint-plugin"; +import tsParser from "@typescript-eslint/parser"; +import prettier from "eslint-plugin-prettier"; +import jsdoc from "eslint-plugin-jsdoc"; +import importPlugin from "eslint-plugin-import"; + +export default [ + { + ignores: ["dist/**", "node_modules/**"], + }, + { + files: ["**/*.ts"], + languageOptions: { + parser: tsParser, + sourceType: "module", + ecmaVersion: 2020, + globals: { + process: "readonly", + __dirname: "readonly", + module: "readonly", + require: "readonly", + Buffer: "readonly", + console: "readonly", + exports: "readonly", + setTimeout: "readonly", + clearTimeout: "readonly", + setInterval: "readonly", + clearInterval: "readonly", + }, + }, + plugins: { + "@typescript-eslint": ts, + prettier: prettier, + jsdoc: jsdoc, + import: importPlugin, + }, + rules: { + ...ts.configs.recommended.rules, + "import/first": "error", + "prettier/prettier": "error", + "@typescript-eslint/member-ordering": "error", + "@typescript-eslint/no-unused-vars": ["error", { argsIgnorePattern: "^_$" }], + "jsdoc/tag-lines": ["error", "any", { startLines: 1 }], + "jsdoc/check-alignment": "error", + "jsdoc/no-undefined-types": "off", + "jsdoc/check-param-names": "error", + "jsdoc/check-tag-names": "error", + "jsdoc/check-types": "error", + "jsdoc/implements-on-classes": "error", + "jsdoc/require-description": "error", + "jsdoc/require-jsdoc": [ + "error", + { + require: { + FunctionDeclaration: true, + MethodDefinition: true, + ClassDeclaration: true, + ArrowFunctionExpression: false, + FunctionExpression: false, + }, + }, + ], + "jsdoc/require-param": "error", + "jsdoc/require-param-description": "error", + "jsdoc/require-param-type": "off", + "jsdoc/require-returns": "error", + "jsdoc/require-returns-description": "error", + "jsdoc/require-returns-type": "off", + "jsdoc/require-hyphen-before-param-description": ["error", "always"], + }, + }, +]; diff --git a/e2e/servers/fastify/index.ts b/e2e/servers/fastify/index.ts new file mode 100644 index 0000000000..4ba0718ed2 --- /dev/null +++ b/e2e/servers/fastify/index.ts @@ -0,0 +1,552 @@ +import Fastify from "fastify"; +import { paymentMiddleware, setSettlementOverrides } from "@x402/fastify"; +import { x402ResourceServer, HTTPFacilitatorClient } from "@x402/core/server"; +import { ExactEvmScheme } from "@x402/evm/exact/server"; +import { UptoEvmScheme } from "@x402/evm/upto/server"; +import { ExactSvmScheme } from "@x402/svm/exact/server"; +import { ExactAptosScheme } from "@x402/aptos/exact/server"; +import { ExactStellarScheme } from "@x402/stellar/exact/server"; +import { bazaarResourceServerExtension, declareDiscoveryExtension } from "@x402/extensions/bazaar"; +import { + declareEip2612GasSponsoringExtension, + declareErc20ApprovalGasSponsoringExtension, +} from "@x402/extensions"; +import dotenv from "dotenv"; + +dotenv.config(); + +/** + * Fastify E2E Test Server with x402 Payment Middleware + * + * This server demonstrates how to integrate x402 payment middleware + * with a Fastify application for end-to-end testing. + */ + +const PORT = process.env.PORT || "4024"; +const EVM_NETWORK = (process.env.EVM_NETWORK || "eip155:84532") as `${string}:${string}`; +const SVM_NETWORK = (process.env.SVM_NETWORK || + "solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1") as `${string}:${string}`; +const APTOS_NETWORK = (process.env.APTOS_NETWORK || "aptos:2") as `${string}:${string}`; +const STELLAR_NETWORK = (process.env.STELLAR_NETWORK || "stellar:testnet") as `${string}:${string}`; +const EVM_PAYEE_ADDRESS = process.env.EVM_PAYEE_ADDRESS as `0x${string}`; +const SVM_PAYEE_ADDRESS = process.env.SVM_PAYEE_ADDRESS as string; +const EVM_PERMIT2_ASSET = process.env.EVM_PERMIT2_ASSET as `0x${string}`; +const APTOS_PAYEE_ADDRESS = process.env.APTOS_PAYEE_ADDRESS as string; +const STELLAR_PAYEE_ADDRESS = process.env.STELLAR_PAYEE_ADDRESS as string | undefined; +const facilitatorUrl = process.env.FACILITATOR_URL; + +if (!EVM_PAYEE_ADDRESS) { + console.error("❌ EVM_PAYEE_ADDRESS environment variable is required"); + process.exit(1); +} + +if (!SVM_PAYEE_ADDRESS) { + console.error("❌ SVM_PAYEE_ADDRESS environment variable is required"); + process.exit(1); +} + +if (!facilitatorUrl) { + console.error("❌ FACILITATOR_URL environment variable is required"); + process.exit(1); +} + +// Initialize Fastify app +const app = Fastify(); + +// Create HTTP facilitator client +const facilitatorClient = new HTTPFacilitatorClient({ url: facilitatorUrl }); + +// Create x402 resource server +const server = new x402ResourceServer(facilitatorClient); + +// Register server schemes +server.register("eip155:*", new ExactEvmScheme()); +server.register("eip155:*", new UptoEvmScheme()); +server.register("solana:*", new ExactSvmScheme()); +if (APTOS_PAYEE_ADDRESS) { + server.register("aptos:*", new ExactAptosScheme()); +} +if (STELLAR_PAYEE_ADDRESS) { + server.register("stellar:*", new ExactStellarScheme()); +} + +// Register Bazaar discovery extension +server.registerExtension(bazaarResourceServerExtension); + +console.log( + `Facilitator account: ${process.env.EVM_PRIVATE_KEY ? process.env.EVM_PRIVATE_KEY.substring(0, 10) + "..." : "not configured"}`, +); +console.log(`Using remote facilitator at: ${facilitatorUrl}`); + +/** + * Pre-middleware guard for optional Aptos / Stellar endpoints + * Returns 501 Not Implemented if not configured + */ +app.addHook("onRequest", async (request, reply) => { + const path = request.url.split("?")[0]; + if (path === "/exact/aptos" && !APTOS_PAYEE_ADDRESS) { + return reply.status(501).send({ + error: "Aptos payments not configured", + message: "APTOS_PAYEE_ADDRESS environment variable is not set", + }); + } + if (path.startsWith("/exact/stellar") && !STELLAR_PAYEE_ADDRESS) { + return reply.status(501).send({ + error: "Stellar payments not configured", + message: "STELLAR_PAYEE_ADDRESS environment variable is not set", + }); + } +}); + +/** + * Configure x402 payment middleware using builder pattern + * + * This middleware protects endpoints with $0.001 USDC payment requirements + * on Base Sepolia, Solana Devnet, Aptos Testnet, and Stellar Testnet with bazaar discovery extension. + */ +paymentMiddleware( + app, + { + // Route-specific payment configuration + "GET /exact/evm/eip3009": { + accepts: { + payTo: EVM_PAYEE_ADDRESS, + scheme: "exact", + price: "$0.001", + network: EVM_NETWORK, + }, + extensions: { + ...declareDiscoveryExtension({ + output: { + example: { + message: "Protected endpoint accessed successfully", + timestamp: "2024-01-01T00:00:00Z", + }, + schema: { + properties: { + message: { type: "string" }, + timestamp: { type: "string" }, + }, + required: ["message", "timestamp"], + }, + }, + }), + }, + }, + "GET /exact/svm": { + accepts: { + payTo: SVM_PAYEE_ADDRESS, + scheme: "exact", + price: "$0.001", + network: SVM_NETWORK, + }, + extensions: { + ...declareDiscoveryExtension({ + output: { + example: { + message: "Protected endpoint accessed successfully", + timestamp: "2024-01-01T00:00:00Z", + }, + schema: { + properties: { + message: { type: "string" }, + timestamp: { type: "string" }, + }, + required: ["message", "timestamp"], + }, + }, + }), + }, + }, + ...(APTOS_PAYEE_ADDRESS + ? { + "GET /exact/aptos": { + accepts: { + payTo: APTOS_PAYEE_ADDRESS, + scheme: "exact", + price: "$0.001", + network: APTOS_NETWORK, + }, + extensions: { + ...declareDiscoveryExtension({ + output: { + example: { + message: "Protected endpoint accessed successfully", + timestamp: "2024-01-01T00:00:00Z", + }, + schema: { + properties: { + message: { type: "string" }, + timestamp: { type: "string" }, + }, + required: ["message", "timestamp"], + }, + }, + }), + }, + }, + } + : {}), + // Permit2 standard/direct endpoint - no gas sponsoring, client must pre-approve Permit2 + "GET /exact/evm/permit2": { + accepts: { + payTo: EVM_PAYEE_ADDRESS, + scheme: "exact", + network: EVM_NETWORK, + price: { + amount: "1000", + asset: EVM_PERMIT2_ASSET, + extra: { + assetTransferMethod: "permit2", + name: EVM_NETWORK == "eip155:84532" ? "USDC" : "USD Coin", + version: "2", + }, + }, + }, + extensions: { + ...declareDiscoveryExtension({ + output: { + example: { + message: "Permit2 endpoint accessed successfully", + timestamp: "2024-01-01T00:00:00Z", + method: "permit2", + }, + schema: { + properties: { + message: { type: "string" }, + timestamp: { type: "string" }, + method: { type: "string" }, + }, + required: ["message", "timestamp", "method"], + }, + }, + }), + }, + }, + // Permit2 endpoint with EIP-2612 gas sponsoring + "GET /exact/evm/permit2-eip2612GasSponsoring": { + accepts: { + payTo: EVM_PAYEE_ADDRESS, + scheme: "exact", + network: EVM_NETWORK, + price: "$0.001", + extra: { assetTransferMethod: "permit2" }, + }, + extensions: { + ...declareDiscoveryExtension({ + output: { + example: { + message: "Permit2 EIP-2612 endpoint accessed successfully", + timestamp: "2024-01-01T00:00:00Z", + method: "permit2-eip2612", + }, + schema: { + properties: { + message: { type: "string" }, + timestamp: { type: "string" }, + method: { type: "string" }, + }, + required: ["message", "timestamp", "method"], + }, + }, + }), + ...declareEip2612GasSponsoringExtension(), + }, + }, + // Permit2 endpoint for ERC-20 approval gas sponsoring (no EIP-2612) + "GET /exact/evm/permit2-erc20ApprovalGasSponsoring": { + accepts: { + payTo: EVM_PAYEE_ADDRESS, + scheme: "exact", + network: EVM_NETWORK, + price: { + amount: "1000", + asset: EVM_PERMIT2_ASSET, + extra: { + assetTransferMethod: "permit2", + }, + }, + }, + extensions: { + ...declareErc20ApprovalGasSponsoringExtension(), + }, + }, + // Upto Permit2 direct endpoint - client must have Permit2 pre-approved + "GET /upto/evm/permit2": { + accepts: { + payTo: EVM_PAYEE_ADDRESS, + scheme: "upto", + network: EVM_NETWORK, + price: { + amount: "2000", + asset: EVM_PERMIT2_ASSET, + extra: { + assetTransferMethod: "permit2", + name: EVM_NETWORK == "eip155:84532" ? "USDC" : "USD Coin", + version: "2", + }, + }, + }, + }, + // Upto Permit2 endpoint with EIP-2612 gas sponsoring + "GET /upto/evm/permit2-eip2612GasSponsoring": { + accepts: { + payTo: EVM_PAYEE_ADDRESS, + scheme: "upto", + network: EVM_NETWORK, + price: { + amount: "2000", + asset: EVM_PERMIT2_ASSET, + extra: { + assetTransferMethod: "permit2", + name: EVM_NETWORK == "eip155:84532" ? "USDC" : "USD Coin", + version: "2", + }, + }, + }, + extensions: { + ...declareEip2612GasSponsoringExtension(), + }, + }, + // Upto Permit2 endpoint for ERC-20 approval gas sponsoring + "GET /upto/evm/permit2-erc20ApprovalGasSponsoring": { + accepts: { + payTo: EVM_PAYEE_ADDRESS, + scheme: "upto", + network: EVM_NETWORK, + price: { + amount: "2000", + asset: EVM_PERMIT2_ASSET, + extra: { + assetTransferMethod: "permit2", + }, + }, + }, + extensions: { + ...declareErc20ApprovalGasSponsoringExtension(), + }, + }, + ...(STELLAR_PAYEE_ADDRESS + ? { + "GET /exact/stellar": { + accepts: { + payTo: STELLAR_PAYEE_ADDRESS!, + scheme: "exact", + price: "$0.001", + network: STELLAR_NETWORK, + }, + extensions: { + ...declareDiscoveryExtension({ + output: { + example: { + message: "Protected Stellar endpoint accessed successfully", + timestamp: "2024-01-01T00:00:00Z", + }, + schema: { + properties: { + message: { type: "string" }, + timestamp: { type: "string" }, + }, + required: ["message", "timestamp"], + }, + }, + }), + }, + }, + } + : {}), + }, + server, // Pass pre-configured server instance +); + +/** + * Protected endpoint - requires payment to access + * + * This endpoint demonstrates a resource protected by x402 payment middleware. + * Clients must provide a valid payment signature to access this endpoint. + */ +app.get("/exact/evm/eip3009", async () => { + return { + message: "Protected endpoint accessed successfully", + timestamp: new Date().toISOString(), + }; +}); + +/** + * Protected SVM endpoint - requires payment to access + * + * This endpoint demonstrates a resource protected by x402 payment middleware for SVM. + * Clients must provide a valid payment signature to access this endpoint. + */ +app.get("/exact/svm", async () => { + return { + message: "Protected endpoint accessed successfully", + timestamp: new Date().toISOString(), + }; +}); + +/** + * Protected Aptos endpoint - requires payment to access + * + * This endpoint demonstrates a resource protected by x402 payment middleware for Aptos. + * Clients must provide a valid payment signature to access this endpoint. + * Note: 501 check is handled by pre-middleware guard above. + */ +app.get("/exact/aptos", async () => { + return { + message: "Protected endpoint accessed successfully", + timestamp: new Date().toISOString(), + }; +}); + +/** + * Protected Permit2 endpoint - standard settle (no gas sponsoring) + */ +app.get("/exact/evm/permit2", async () => { + return { + message: "Permit2 endpoint accessed successfully", + timestamp: new Date().toISOString(), + method: "permit2", + }; +}); + +/** + * Protected Permit2 EIP-2612 endpoint - requires Permit2 with gas sponsoring + */ +app.get("/exact/evm/permit2-eip2612GasSponsoring", async () => { + return { + message: "Permit2 EIP-2612 endpoint accessed successfully", + timestamp: new Date().toISOString(), + method: "permit2-eip2612", + }; +}); + +/** + * Protected Permit2 ERC-20 endpoint - requires payment via Permit2 flow with ERC-20 approval + * + * This endpoint demonstrates the ERC-20 approval gas sponsoring flow for tokens + * that do NOT implement EIP-2612. The facilitator broadcasts the pre-signed + * approve() transaction on the client's behalf before settling. + */ +app.get("/exact/evm/permit2-erc20ApprovalGasSponsoring", async () => { + return { + message: "Permit2 ERC-20 approval endpoint accessed successfully", + timestamp: new Date().toISOString(), + method: "permit2-erc20-approval", + }; +}); + +/** + * Upto Permit2 direct endpoint - upto scheme, client must pre-approve Permit2 + */ +app.get("/upto/evm/permit2", async (_request, reply) => { + setSettlementOverrides(reply, { amount: "1000" }); + return { + message: "Upto Permit2 endpoint accessed successfully", + timestamp: new Date().toISOString(), + method: "upto-permit2", + }; +}); + +/** + * Upto Permit2 EIP-2612 endpoint - upto scheme with gas sponsoring + */ +app.get("/upto/evm/permit2-eip2612GasSponsoring", async (_request, reply) => { + setSettlementOverrides(reply, { amount: "1000" }); + return { + message: "Upto Permit2 EIP-2612 endpoint accessed successfully", + timestamp: new Date().toISOString(), + method: "upto-permit2-eip2612", + }; +}); + +/** + * Upto Permit2 ERC-20 approval endpoint - upto scheme with ERC-20 gas sponsoring + */ +app.get("/upto/evm/permit2-erc20ApprovalGasSponsoring", async (_request, reply) => { + setSettlementOverrides(reply, { amount: "1000" }); + return { + message: "Upto Permit2 ERC-20 approval endpoint accessed successfully", + timestamp: new Date().toISOString(), + method: "upto-permit2-erc20-approval", + }; +}); + +/** + * Protected Stellar endpoint - requires payment to access + * + * This endpoint demonstrates a resource protected by x402 payment middleware for Stellar. + * Clients must provide a valid payment signature to access this endpoint. + * Note: 501 check is handled by pre-middleware guard above. + */ +if (STELLAR_PAYEE_ADDRESS) { + app.get("/exact/stellar", async () => { + return { + message: "Protected Stellar endpoint accessed successfully", + timestamp: new Date().toISOString(), + }; + }); +} + +/** + * Health check endpoint - no payment required + * + * Used to verify the server is running and responsive. + */ +app.get("/health", async () => { + return { + status: "ok", + network: EVM_NETWORK, + payee: EVM_PAYEE_ADDRESS, + version: "2.0.0", + }; +}); + +/** + * Shutdown endpoint - used by e2e tests + * + * Allows graceful shutdown of the server during testing. + */ +app.post("/close", async (request, reply) => { + reply.send({ message: "Server shutting down gracefully" }); + console.log("Received shutdown request"); + + // Give time for response to be sent + setTimeout(() => { + process.exit(0); + }, 100); +}); + +// Start the server +app.listen({ port: parseInt(PORT) }, (err, address) => { + if (err) { + console.error(err); + process.exit(1); + } + console.log(` +╔════════════════════════════════════════════════════════╗ +║ x402 Fastify E2E Test Server ║ +╠════════════════════════════════════════════════════════╣ +║ Server: ${address} ║ +║ EVM Network: ${EVM_NETWORK} ║ +║ SVM Network: ${SVM_NETWORK} ║ +║ Aptos Network: ${APTOS_NETWORK} ║ +║ Stellar Network: ${STELLAR_NETWORK}║ +║ EVM Payee: ${EVM_PAYEE_ADDRESS} ║ +║ SVM Payee: ${SVM_PAYEE_ADDRESS} ║ +║ Aptos Payee: ${APTOS_PAYEE_ADDRESS || "(not configured)"} +║ Stellar Payee: ${STELLAR_PAYEE_ADDRESS || "(not configured)"} +║ ║ +║ Endpoints: ║ +║ • GET /exact/evm/eip3009 (EVM EIP-3009) ║ +║ • GET /exact/evm/permit2 (Permit2) ║ +║ • GET /exact/evm/permit2-eip2612GasSponsoring ║ +║ • GET /exact/evm/permit2-erc20ApprovalGasSponsoring ║ +║ • GET /exact/svm (SVM) ║ +║ • GET /exact/aptos (Aptos) ║ +║ • GET /exact/stellar (Stellar) ║ +║ • GET /health (no payment required) ║ +║ • POST /close (shutdown server) ║ +╚════════════════════════════════════════════════════════╝ + `); +}); diff --git a/e2e/servers/fastify/install.sh b/e2e/servers/fastify/install.sh new file mode 100755 index 0000000000..084d4490b3 --- /dev/null +++ b/e2e/servers/fastify/install.sh @@ -0,0 +1,4 @@ +#!/bin/bash +# TypeScript dependencies handled by pnpm install at root level +# This file is intentionally empty +exit 0 diff --git a/e2e/servers/fastify/package.json b/e2e/servers/fastify/package.json new file mode 100644 index 0000000000..690c1727e1 --- /dev/null +++ b/e2e/servers/fastify/package.json @@ -0,0 +1,36 @@ +{ + "name": "@x402/fastify-e2e", + "private": true, + "type": "module", + "scripts": { + "dev": "tsx index.ts", + "format": "prettier -c .prettierrc --write \"**/*.{ts,js,cjs,json,md}\"", + "format:check": "prettier -c .prettierrc --check \"**/*.{ts,js,cjs,json,md}\"", + "lint": "eslint . --ext .ts --fix", + "lint:check": "eslint . --ext .ts" + }, + "dependencies": { + "@x402/aptos": "workspace:*", + "@x402/core": "workspace:*", + "@x402/fastify": "workspace:*", + "@x402/evm": "workspace:*", + "@x402/extensions": "workspace:*", + "@x402/stellar": "workspace:*", + "@x402/svm": "workspace:*", + "dotenv": "^16.6.1", + "fastify": "^5.3.3" + }, + "devDependencies": { + "@eslint/js": "^9.24.0", + "@typescript-eslint/eslint-plugin": "^8.29.1", + "@typescript-eslint/parser": "^8.29.1", + "eslint": "^9.24.0", + "eslint-plugin-import": "^2.31.0", + "eslint-plugin-jsdoc": "^50.6.9", + "eslint-plugin-prettier": "^5.2.6", + "prettier": "3.5.2", + "tsup": "^7.2.0", + "tsx": "^4.7.0", + "typescript": "^5.3.0" + } +} diff --git a/e2e/servers/fastify/run.sh b/e2e/servers/fastify/run.sh new file mode 100755 index 0000000000..2cc84b562a --- /dev/null +++ b/e2e/servers/fastify/run.sh @@ -0,0 +1,2 @@ +#!/bin/bash +pnpm dev diff --git a/e2e/servers/fastify/test.config.json b/e2e/servers/fastify/test.config.json new file mode 100644 index 0000000000..5575e3681b --- /dev/null +++ b/e2e/servers/fastify/test.config.json @@ -0,0 +1,109 @@ +{ + "name": "fastify", + "type": "server", + "language": "typescript", + "x402Version": 2, + "extensions": ["bazaar", "eip2612GasSponsoring", "erc20ApprovalGasSponsoring"], + + "endpoints": [ + { + "path": "/exact/evm/eip3009", + "method": "GET", + "description": "Protected endpoint requiring EIP-3009 payment", + "requiresPayment": true, + "protocolFamily": "evm", + "transferMethod": "eip3009" + }, + { + "path": "/exact/evm/permit2", + "method": "GET", + "description": "Protected endpoint requiring Permit2 payment (standard settle, no gas sponsoring)", + "requiresPayment": true, + "protocolFamily": "evm", + "transferMethod": "permit2", + "permit2Direct": true + }, + { + "path": "/exact/evm/permit2-eip2612GasSponsoring", + "method": "GET", + "description": "Protected endpoint requiring Permit2 payment with EIP-2612 gas sponsoring", + "requiresPayment": true, + "protocolFamily": "evm", + "transferMethod": "permit2", + "coldstart": true + }, + { + "path": "/exact/evm/permit2-erc20ApprovalGasSponsoring", + "method": "GET", + "description": "Protected endpoint requiring Permit2 payment with ERC-20 approval gas sponsoring", + "requiresPayment": true, + "protocolFamily": "evm", + "transferMethod": "permit2", + "extensions": ["erc20ApprovalGasSponsoring"], + "coldstart": true + }, + { + "path": "/upto/evm/permit2", + "method": "GET", + "description": "Protected endpoint requiring upto Permit2 payment (direct, client must pre-approve)", + "requiresPayment": true, + "protocolFamily": "evm", + "transferMethod": "upto", + "permit2Direct": true + }, + { + "path": "/upto/evm/permit2-eip2612GasSponsoring", + "method": "GET", + "description": "Protected endpoint requiring upto Permit2 payment with EIP-2612 gas sponsoring", + "requiresPayment": true, + "protocolFamily": "evm", + "transferMethod": "upto" + }, + { + "path": "/upto/evm/permit2-erc20ApprovalGasSponsoring", + "method": "GET", + "description": "Protected endpoint requiring upto Permit2 payment with ERC-20 approval gas sponsoring", + "requiresPayment": true, + "protocolFamily": "evm", + "transferMethod": "upto", + "extensions": ["erc20ApprovalGasSponsoring"] + }, + { + "path": "/exact/svm", + "method": "GET", + "description": "Protected endpoint requiring payment on SVM network", + "requiresPayment": true, + "protocolFamily": "svm" + }, + { + "path": "/exact/aptos", + "method": "GET", + "description": "Protected endpoint requiring payment on Aptos network", + "requiresPayment": true, + "protocolFamily": "aptos" + }, + { + "path": "/exact/stellar", + "method": "GET", + "description": "Protected endpoint requiring payment on Stellar network", + "requiresPayment": true, + "protocolFamily": "stellar" + }, + { + "path": "/health", + "method": "GET", + "description": "Health check endpoint", + "health": true + }, + { + "path": "/close", + "method": "POST", + "description": "Graceful shutdown endpoint", + "close": true + } + ], + "environment": { + "required": ["PORT", "EVM_PAYEE_ADDRESS", "SVM_PAYEE_ADDRESS", "FACILITATOR_URL"], + "optional": ["APTOS_PAYEE_ADDRESS", "STELLAR_PAYEE_ADDRESS"] + } +} diff --git a/e2e/servers/fastify/tsconfig.json b/e2e/servers/fastify/tsconfig.json new file mode 100644 index 0000000000..78f9479b1b --- /dev/null +++ b/e2e/servers/fastify/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "ES2020", + "moduleResolution": "bundler", + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "skipLibCheck": true, + "strict": true, + "resolveJsonModule": true, + "baseUrl": ".", + "types": ["node"] + }, + "include": ["index.ts"] +} diff --git a/e2e/servers/flask/build.sh b/e2e/servers/flask/build.sh index f5fbe5e8f8..c1bb071bbe 100755 --- a/e2e/servers/flask/build.sh +++ b/e2e/servers/flask/build.sh @@ -1,5 +1,5 @@ #!/bin/bash -# Python doesn't require a build step -# This file is intentionally empty -exit 0 +set -e +# Rebuild the local x402 editable dependency so the venv reflects source changes +uv sync --reinstall-package x402 diff --git a/e2e/servers/flask/main.py b/e2e/servers/flask/main.py index 09b1b99dc7..6657dbe4ab 100644 --- a/e2e/servers/flask/main.py +++ b/e2e/servers/flask/main.py @@ -19,6 +19,10 @@ declare_discovery_extension, OutputConfig, ) +from x402.extensions.eip2612_gas_sponsoring import declare_eip2612_gas_sponsoring_extension +from x402.extensions.erc20_approval_gas_sponsoring import ( + declare_erc20_approval_gas_sponsoring_extension, +) # Configure logging to reduce verbosity logging.getLogger("werkzeug").setLevel(logging.ERROR) @@ -32,6 +36,9 @@ SVM_ADDRESS = os.getenv("SVM_PAYEE_ADDRESS") PORT = int(os.getenv("PORT", "4021")) FACILITATOR_URL = os.getenv("FACILITATOR_URL") +EVM_PERMIT2_ASSET = os.getenv( + "EVM_PERMIT2_ASSET", "0x036CbD53842c5426634e7929541eC2318f3dCF7e" +) if not EVM_ADDRESS: print("Error: Missing required environment variable EVM_PAYEE_ADDRESS") @@ -68,7 +75,7 @@ # Define routes with payment requirements routes = { - "GET /protected": { + "GET /exact/evm/eip3009": { "accepts": { "scheme": "exact", "payTo": EVM_ADDRESS, @@ -95,7 +102,7 @@ ), }, }, - "GET /protected-svm": { + "GET /exact/svm": { "accepts": { "scheme": "exact", "payTo": SVM_ADDRESS, @@ -120,6 +127,57 @@ ), }, }, + "GET /exact/evm/permit2-eip2612GasSponsoring": { + "accepts": { + "scheme": "exact", + "payTo": EVM_ADDRESS, + "network": EVM_NETWORK, + "price": { + "amount": "1000", + "asset": EVM_PERMIT2_ASSET, + "extra": { + "assetTransferMethod": "permit2", + "name": "USDC", + "version": "2", + }, + }, + }, + "extensions": { + **declare_discovery_extension( + output=OutputConfig( + example={ + "message": "Permit2 endpoint accessed successfully", + "timestamp": "2024-01-01T00:00:00Z", + "method": "permit2", + }, + schema={ + "properties": { + "message": {"type": "string"}, + "timestamp": {"type": "string"}, + "method": {"type": "string"}, + }, + "required": ["message", "timestamp"], + }, + ) + ), + **declare_eip2612_gas_sponsoring_extension(), + }, + }, + "GET /exact/evm/permit2-erc20ApprovalGasSponsoring": { + "accepts": { + "scheme": "exact", + "payTo": EVM_ADDRESS, + "network": EVM_NETWORK, + "price": { + "amount": "1000", + "asset": EVM_PERMIT2_ASSET, + "extra": {"assetTransferMethod": "permit2"}, + }, + }, + "extensions": { + **declare_erc20_approval_gas_sponsoring_extension(), + }, + }, } # Apply payment middleware @@ -129,7 +187,7 @@ shutdown_requested = False -@app.route("/protected") +@app.route("/exact/evm/eip3009") def protected_endpoint(): """Protected endpoint that requires payment.""" if shutdown_requested: @@ -144,7 +202,7 @@ def protected_endpoint(): ) -@app.route("/protected-svm") +@app.route("/exact/svm") def protected_svm_endpoint(): """Protected endpoint that requires SVM (Solana) payment.""" if shutdown_requested: @@ -158,6 +216,34 @@ def protected_svm_endpoint(): ) +@app.route("/exact/evm/permit2-eip2612GasSponsoring") +def protected_permit2_endpoint(): + """Protected endpoint that requires Permit2 payment.""" + if shutdown_requested: + return jsonify({"error": "Server shutting down"}), 503 + return jsonify( + { + "message": "Permit2 endpoint accessed successfully", + "timestamp": "2024-01-01T00:00:00Z", + "method": "permit2", + } + ) + + +@app.route("/exact/evm/permit2-erc20ApprovalGasSponsoring") +def protected_permit2_erc20_endpoint(): + """Protected endpoint that requires Permit2 payment with ERC-20 approval sponsoring.""" + if shutdown_requested: + return jsonify({"error": "Server shutting down"}), 503 + return jsonify( + { + "message": "Permit2+ERC20Approval endpoint accessed successfully", + "timestamp": "2024-01-01T00:00:00Z", + "method": "permit2+erc20approval", + } + ) + + @app.route("/health") def health_check(): """Health check endpoint.""" diff --git a/e2e/servers/flask/run.sh b/e2e/servers/flask/run.sh index 31c1f93486..c653d856a9 100644 --- a/e2e/servers/flask/run.sh +++ b/e2e/servers/flask/run.sh @@ -1,4 +1,3 @@ #!/bin/bash -# Ensure dependencies are synced before running -uv sync --quiet +uv sync --reinstall-package x402 --quiet uv run python main.py diff --git a/e2e/servers/flask/test.config.json b/e2e/servers/flask/test.config.json index 4c73225cf1..3ba02fea37 100644 --- a/e2e/servers/flask/test.config.json +++ b/e2e/servers/flask/test.config.json @@ -4,25 +4,48 @@ "language": "python", "x402Version": 2, "extensions": [ - "bazaar" + "bazaar", + "eip2612GasSponsoring", + "erc20ApprovalGasSponsoring" ], + "evm": { "transferMethods": ["eip3009", "permit2"] }, "description": "Python Flask server with x402 v2 payment middleware", "endpoints": [ { - "path": "/protected", + "path": "/exact/evm/eip3009", "method": "GET", - "description": "Protected endpoint requiring payment", + "description": "Protected endpoint requiring EIP-3009 payment", "requiresPayment": true, "protocolFamily": "evm", "transferMethod": "eip3009" }, { - "path": "/protected-svm", + "path": "/exact/svm", "method": "GET", "description": "Protected endpoint requiring SVM (Solana) payment", "requiresPayment": true, "protocolFamily": "svm" }, + { + "path": "/exact/evm/permit2-eip2612GasSponsoring", + "method": "GET", + "description": "Protected endpoint requiring Permit2 payment with EIP-2612 gas sponsoring", + "requiresPayment": true, + "protocolFamily": "evm", + "transferMethod": "permit2", + "extensions": ["eip2612GasSponsoring"], + "coldstart": true + }, + { + "path": "/exact/evm/permit2-erc20ApprovalGasSponsoring", + "method": "GET", + "description": "Protected endpoint requiring Permit2 payment with ERC-20 approval gas sponsoring", + "requiresPayment": true, + "protocolFamily": "evm", + "transferMethod": "permit2", + "extensions": ["erc20ApprovalGasSponsoring"], + "coldstart": true + }, { "path": "/health", "method": "GET", @@ -43,7 +66,8 @@ ], "optional": [ "PORT", - "FACILITATOR_URL" + "FACILITATOR_URL", + "EVM_PERMIT2_ASSET" ] } } diff --git a/e2e/servers/flask/uv.lock b/e2e/servers/flask/uv.lock index 34f4b66b19..bec13acc9d 100644 --- a/e2e/servers/flask/uv.lock +++ b/e2e/servers/flask/uv.lock @@ -1,5 +1,5 @@ version = 1 -revision = 3 +revision = 2 requires-python = ">=3.10" [[package]] @@ -2072,7 +2072,7 @@ wheels = [ [[package]] name = "x402" -version = "2.2.0" +version = "2.5.0" source = { editable = "../../../python/x402" } dependencies = [ { name = "nest-asyncio" }, @@ -2102,7 +2102,7 @@ svm = [ [package.metadata] requires-dist = [ { name = "eth-abi", marker = "extra == 'evm'", specifier = ">=5.0.0" }, - { name = "eth-account", marker = "extra == 'evm'", specifier = ">=0.12.0" }, + { name = "eth-account", marker = "extra == 'evm'", specifier = ">=0.13.0" }, { name = "eth-keys", marker = "extra == 'evm'", specifier = ">=0.5.0" }, { name = "eth-utils", marker = "extra == 'evm'", specifier = ">=4.0.0" }, { name = "fastapi", extras = ["standard"], marker = "extra == 'fastapi'", specifier = ">=0.115.0" }, @@ -2129,7 +2129,7 @@ provides-extras = ["httpx", "requests", "flask", "fastapi", "evm", "svm", "mcp", dev = [ { name = "black", specifier = ">=23.0.0" }, { name = "eth-abi", specifier = ">=5.0.0" }, - { name = "eth-account", specifier = ">=0.12.0" }, + { name = "eth-account", specifier = ">=0.13.0" }, { name = "eth-keys", specifier = ">=0.5.0" }, { name = "eth-utils", specifier = ">=4.0.0" }, { name = "fastapi", extras = ["standard"], specifier = ">=0.115.0" }, diff --git a/e2e/servers/gin/go.mod b/e2e/servers/gin/go.mod index 74749dd523..393edfdb4f 100644 --- a/e2e/servers/gin/go.mod +++ b/e2e/servers/gin/go.mod @@ -12,7 +12,9 @@ require ( require ( filippo.io/edwards25519 v1.0.0-rc.1 // indirect + github.com/Microsoft/go-winio v0.6.2 // indirect github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20251001021608-1fe7b43fc4d6 // indirect + github.com/StackExchange/wmi v1.2.1 // indirect github.com/andres-erbsen/clock v0.0.0-20160526145045-9e14626cd129 // indirect github.com/bits-and-blooms/bitset v1.20.0 // indirect github.com/blendle/zapdriver v1.3.1 // indirect @@ -23,6 +25,7 @@ require ( github.com/crate-crypto/go-eth-kzg v1.4.0 // indirect github.com/crate-crypto/go-ipa v0.0.0-20240724233137-53bbb0ceb27a // indirect github.com/davecgh/go-spew v1.1.1 // indirect + github.com/deckarep/golang-set/v2 v2.6.0 // indirect github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 // indirect github.com/ethereum/c-kzg-4844/v2 v2.1.5 // indirect github.com/ethereum/go-ethereum v1.16.7 // indirect @@ -33,19 +36,21 @@ require ( github.com/gagliardetto/solana-go v1.14.0 // indirect github.com/gagliardetto/treeout v0.1.4 // indirect github.com/gin-contrib/sse v1.1.0 // indirect + github.com/go-ole/go-ole v1.3.0 // indirect github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-playground/validator/v10 v10.27.0 // indirect github.com/goccy/go-json v0.10.5 // indirect github.com/goccy/go-yaml v1.18.0 // indirect github.com/google/uuid v1.6.0 // indirect + github.com/gorilla/websocket v1.4.2 // indirect github.com/holiman/uint256 v1.3.2 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/compress v1.16.0 // indirect github.com/klauspost/cpuid/v2 v2.3.0 // indirect github.com/leodido/go-urn v1.4.0 // indirect github.com/logrusorgru/aurora v2.0.3+incompatible // indirect - github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mitchellh/go-testing-interface v1.14.1 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect @@ -55,8 +60,11 @@ require ( github.com/pelletier/go-toml/v2 v2.2.4 // indirect github.com/quic-go/qpack v0.5.1 // indirect github.com/quic-go/quic-go v0.55.0 // indirect + github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible // indirect github.com/streamingfast/logging v0.0.0-20230608130331-f22c91403091 // indirect github.com/supranational/blst v0.3.16-0.20250831170142-f48500c1fdbe // indirect + github.com/tklauser/go-sysconf v0.3.12 // indirect + github.com/tklauser/numcpus v0.6.1 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/ugorji/go/codec v1.3.0 // indirect github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f // indirect @@ -68,15 +76,15 @@ require ( go.uber.org/ratelimit v0.2.0 // indirect go.uber.org/zap v1.21.0 // indirect golang.org/x/arch v0.20.0 // indirect - golang.org/x/crypto v0.41.0 // indirect - golang.org/x/mod v0.27.0 // indirect - golang.org/x/net v0.43.0 // indirect - golang.org/x/sync v0.16.0 // indirect - golang.org/x/sys v0.36.0 // indirect - golang.org/x/term v0.34.0 // indirect - golang.org/x/text v0.28.0 // indirect - golang.org/x/time v0.9.0 // indirect - golang.org/x/tools v0.36.0 // indirect + golang.org/x/crypto v0.46.0 // indirect + golang.org/x/mod v0.30.0 // indirect + golang.org/x/net v0.48.0 // indirect + golang.org/x/sync v0.19.0 // indirect + golang.org/x/sys v0.39.0 // indirect + golang.org/x/term v0.38.0 // indirect + golang.org/x/text v0.32.0 // indirect + golang.org/x/time v0.14.0 // indirect + golang.org/x/tools v0.39.0 // indirect google.golang.org/protobuf v1.36.9 // indirect ) diff --git a/e2e/servers/gin/go.sum b/e2e/servers/gin/go.sum index faaacc2034..071f4f88ab 100644 --- a/e2e/servers/gin/go.sum +++ b/e2e/servers/gin/go.sum @@ -2,14 +2,22 @@ filippo.io/edwards25519 v1.0.0-rc.1 h1:m0VOOB23frXZvAOK44usCgLWvtsxIoMCTBGJZlpmG filippo.io/edwards25519 v1.0.0-rc.1/go.mod h1:N1IkdkCkiLB6tki+MYJoSx2JTY9NUlxZE7eHn5EwJns= github.com/AlekSi/pointer v1.1.0 h1:SSDMPcXD9jSl8FPy9cRzoRaMJtm9g9ggGTxecRUbQoI= github.com/AlekSi/pointer v1.1.0/go.mod h1:y7BvfRI3wXPWKXEBhU71nbnIEEZX0QTSB2Bj48UJIZE= +github.com/DataDog/zstd v1.4.5 h1:EndNeuB0l9syBZhut0wns3gV1hL8zX8LIu6ZiVHWLIQ= +github.com/DataDog/zstd v1.4.5/go.mod h1:1jcaCB/ufaK+sKp1NBhlGmpz41jOoPQ35bpF36t7BBo= +github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= +github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20251001021608-1fe7b43fc4d6 h1:1zYrtlhrZ6/b6SAjLSfKzWtdgqK0U+HtH/VcBWh1BaU= github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20251001021608-1fe7b43fc4d6/go.mod h1:ioLG6R+5bUSO1oeGSDxOV3FADARuMoytZCSX6MEMQkI= github.com/StackExchange/wmi v1.2.1 h1:VIkavFPXSjcnS+O8yTq7NI32k0R5Aj+v39y29VYDOSA= github.com/StackExchange/wmi v1.2.1/go.mod h1:rcmrprowKIVzvc+NUiLncP2uuArMWLCbu9SBzvHz7e8= +github.com/VictoriaMetrics/fastcache v1.13.0 h1:AW4mheMR5Vd9FkAPUv+NH6Nhw+fmbTMGMsNAoA/+4G0= +github.com/VictoriaMetrics/fastcache v1.13.0/go.mod h1:hHXhl4DA2fTL2HTZDJFXWgW0LNjo6B+4aj2Wmng3TjU= github.com/andres-erbsen/clock v0.0.0-20160526145045-9e14626cd129 h1:MzBOUgng9orim59UnfUTLRjMpd09C5uEVQ6RPGeCaVI= github.com/andres-erbsen/clock v0.0.0-20160526145045-9e14626cd129/go.mod h1:rFgpPQZYZ8vdbc+48xibu8ALc3yeyd64IhHS+PU6Yyg= github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8= github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/bits-and-blooms/bitset v1.20.0 h1:2F+rfL86jE2d/bmw7OhqUg2Sj/1rURkBn3MdfoPyRVU= github.com/bits-and-blooms/bitset v1.20.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8= github.com/blendle/zapdriver v1.3.1 h1:C3dydBOWYRiOk+B8X9IVZ5IOe+7cl+tGOexN4QqHfpE= @@ -18,10 +26,26 @@ github.com/bytedance/sonic v1.14.0 h1:/OfKt8HFw0kh2rj8N0F6C/qPGRESq0BbaNZgcNXXzQ github.com/bytedance/sonic v1.14.0/go.mod h1:WoEbx8WTcFJfzCe0hbmyTGrfjt8PzNEBdxlNUO24NhA= github.com/bytedance/sonic/loader v0.3.0 h1:dskwH8edlzNMctoruo8FPTJDF3vLtDT0sXZwvZJyqeA= github.com/bytedance/sonic/loader v0.3.0/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M= github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU= +github.com/cockroachdb/errors v1.11.3 h1:5bA+k2Y6r+oz/6Z/RFlNeVCesGARKuC6YymtcDrbC/I= +github.com/cockroachdb/errors v1.11.3/go.mod h1:m4UIW4CDjx+R5cybPsNrRbreomiFqt8o1h1wUVazSd8= +github.com/cockroachdb/fifo v0.0.0-20240606204812-0bbfbd93a7ce h1:giXvy4KSc/6g/esnpM7Geqxka4WSqI1SZc7sMJFd3y4= +github.com/cockroachdb/fifo v0.0.0-20240606204812-0bbfbd93a7ce/go.mod h1:9/y3cnZ5GKakj/H4y9r9GTjCvAFta7KLgSHPJJYc52M= +github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b h1:r6VH0faHjZeQy818SGhaone5OnYfxFR/+AzdY3sf5aE= +github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b/go.mod h1:Vz9DsVWQQhf3vs21MhPMZpMGSht7O/2vFW2xusFUVOs= +github.com/cockroachdb/pebble v1.1.5 h1:5AAWCBWbat0uE0blr8qzufZP5tBjkRyy/jWe1QWLnvw= +github.com/cockroachdb/pebble v1.1.5/go.mod h1:17wO9el1YEigxkP/YtV8NtCivQDgoCyBg5c4VR/eOWo= +github.com/cockroachdb/redact v1.1.5 h1:u1PMllDkdFfPWaNGMyLD1+so+aq3uUItthCFqzwPJ30= +github.com/cockroachdb/redact v1.1.5/go.mod h1:BVNblN9mBWFyMyqK1k3AAiSxhvhfK2oOZZ2lK+dpvRg= +github.com/cockroachdb/tokenbucket v0.0.0-20230807174530-cc333fc44b06 h1:zuQyyAKVxetITBuuhv3BI9cMrmStnpT18zmgmTxunpo= +github.com/cockroachdb/tokenbucket v0.0.0-20230807174530-cc333fc44b06/go.mod h1:7nc4anLGjupUW/PeY5qiNYsdNXj7zopG+eqsS7To5IQ= github.com/consensys/gnark-crypto v0.18.0 h1:vIye/FqI50VeAr0B3dx+YjeIvmc3LWz4yEfbWBpTUf0= github.com/consensys/gnark-crypto v0.18.0/go.mod h1:L3mXGFTe1ZN+RSJ+CLjUt9x7PNdx8ubaYfDROyp2Z8c= +github.com/cpuguy83/go-md2man/v2 v2.0.5 h1:ZtcqGrnekaHpVLArFSe4HK5DoKx1T0rq2DwVB0alcyc= +github.com/cpuguy83/go-md2man/v2 v2.0.5/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/crate-crypto/go-eth-kzg v1.4.0 h1:WzDGjHk4gFg6YzV0rJOAsTK4z3Qkz5jd4RE3DAvPFkg= github.com/crate-crypto/go-eth-kzg v1.4.0/go.mod h1:J9/u5sWfznSObptgfa92Jq8rTswn6ahQWEuiLHOjCUI= github.com/crate-crypto/go-ipa v0.0.0-20240724233137-53bbb0ceb27a h1:W8mUrRp6NOVl3J+MYp5kPMoUZPp7aOYHtaua31lwRHg= @@ -29,6 +53,10 @@ github.com/crate-crypto/go-ipa v0.0.0-20240724233137-53bbb0ceb27a/go.mod h1:sTwz github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dchest/siphash v1.2.3 h1:QXwFc8cFOR2dSa/gE6o/HokBMWtLUaNDVd+22aKHeEA= +github.com/dchest/siphash v1.2.3/go.mod h1:0NvQU092bT0ipiFN++/rXm69QG9tVxLAlQHIXMPAkHc= +github.com/deckarep/golang-set/v2 v2.6.0 h1:XfcQbWM1LlMB8BsJ8N9vW5ehnnPVIw0je80NsVHagjM= +github.com/deckarep/golang-set/v2 v2.6.0/go.mod h1:VAky9rY/yGXJOLEDv3OMci+7wtDpOF4IN+y82NBOac4= github.com/decred/dcrd/crypto/blake256 v1.0.0 h1:/8DMNYp9SGi5f0w7uCm6d6M4OU2rGFK09Y2A4Xv7EE0= github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 h1:YLtO71vCjJRCBcrPMtQ9nqBsqpA1m5sE92cU+pd5Mcc= @@ -37,6 +65,8 @@ github.com/emicklei/dot v1.6.2 h1:08GN+DD79cy/tzN6uLCT84+2Wk9u+wvqP+Hkx/dIR8A= github.com/emicklei/dot v1.6.2/go.mod h1:DeV7GvQtIw4h2u73RKBkkFdvVAz0D9fzeJrgPW6gy/s= github.com/ethereum/c-kzg-4844/v2 v2.1.5 h1:aVtoLK5xwJ6c5RiqO8g8ptJ5KU+2Hdquf6G3aXiHh5s= github.com/ethereum/c-kzg-4844/v2 v2.1.5/go.mod h1:u59hRTTah4Co6i9fDWtiCjTrblJv0UwsqZKCc0GfgUs= +github.com/ethereum/go-bigmodexpfix v0.0.0-20250911101455-f9e208c548ab h1:rvv6MJhy07IMfEKuARQ9TKojGqLVNxQajaXEp/BoqSk= +github.com/ethereum/go-bigmodexpfix v0.0.0-20250911101455-f9e208c548ab/go.mod h1:IuLm4IsPipXKF7CW5Lzf68PIbZ5yl7FFd74l/E0o9A8= github.com/ethereum/go-ethereum v1.16.7 h1:qeM4TvbrWK0UC0tgkZ7NiRsmBGwsjqc64BHo20U59UQ= github.com/ethereum/go-ethereum v1.16.7/go.mod h1:Fs6QebQbavneQTYcA39PEKv2+zIjX7rPUZ14DER46wk= github.com/ethereum/go-verkle v0.2.2 h1:I2W0WjnrFUIzzVPwm8ykY+7pL2d4VhlsePn4j7cnFk8= @@ -55,10 +85,13 @@ github.com/gagliardetto/solana-go v1.14.0 h1:3WfAi70jOOjAJ0deFMjdhFYlLXATF4tOQXs github.com/gagliardetto/solana-go v1.14.0/go.mod h1:l/qqqIN6qJJPtxW/G1PF4JtcE3Zg2vD2EliZrr9Gn5k= github.com/gagliardetto/treeout v0.1.4 h1:ozeYerrLCmCubo1TcIjFiOWTTGteOOHND1twdFpgwaw= github.com/gagliardetto/treeout v0.1.4/go.mod h1:loUefvXTrlRG5rYmJmExNryyBRh8f89VZhmMOyCyqok= +github.com/getsentry/sentry-go v0.27.0 h1:Pv98CIbtB3LkMWmXi4Joa5OOcwbmnX88sF5qbK3r3Ps= +github.com/getsentry/sentry-go v0.27.0/go.mod h1:lc76E2QywIyW8WuBnwl8Lc4bkmQH4+w1gwTf25trprY= github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w= github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM= github.com/gin-gonic/gin v1.11.0 h1:OW/6PLjyusp2PPXtyxKHU0RbX6I/l28FTdDlae5ueWk= github.com/gin-gonic/gin v1.11.0/go.mod h1:+iq/FyxlGzII0KHiBGjuNn4UNENUlKbGlNmc+W50Dls= +github.com/go-ole/go-ole v1.2.5/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= @@ -75,6 +108,10 @@ github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw= github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= github.com/gofrs/flock v0.12.1 h1:MTLVXXHf8ekldpJk3AKicLij9MdwOWkZ+a/jHHZby9E= github.com/gofrs/flock v0.12.1/go.mod h1:9zxTsyu5xtJ9DK+1tFZyibEV7y3uwDxPPfbxeeHCoD0= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI= +github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs= github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= @@ -86,8 +123,20 @@ github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= +github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/hashicorp/go-bexpr v0.1.10 h1:9kuI5PFotCboP3dkDYFr/wi0gg0QVbSNz5oFRpxn4uE= +github.com/hashicorp/go-bexpr v0.1.10/go.mod h1:oxlubA2vC/gFVfX1A6JGp7ls7uCDlfJn732ehYYg+g0= +github.com/holiman/billy v0.0.0-20250707135307-f2f9b9aae7db h1:IZUYC/xb3giYwBLMnr8d0TGTzPKFGNTCGgGLoyeX330= +github.com/holiman/billy v0.0.0-20250707135307-f2f9b9aae7db/go.mod h1:xTEYN9KCHxuYHs+NmrmzFcnvHMzLLNiGFafCb1n3Mfg= +github.com/holiman/bloomfilter/v2 v2.0.3 h1:73e0e/V0tCydx14a0SCYS/EWCxgwLZ18CZcZKVu0fao= +github.com/holiman/bloomfilter/v2 v2.0.3/go.mod h1:zpoh+gs7qcpqrHr3dB55AMiJwo0iURXE7ZOP9L9hSkA= github.com/holiman/uint256 v1.3.2 h1:a9EgMPSC1AAaj1SZL5zIQD3WbwTuHrMGOerLjGmM/TA= github.com/holiman/uint256 v1.3.2/go.mod h1:EOMSn4q6Nyt9P6efbI3bueV4e1b3dGlUCXeiRV4ng7E= +github.com/huin/goupnp v1.3.0 h1:UvLUlWDNpoUdYzb2TCn+MuTWtcjXKSza2n6CBdQ0xXc= +github.com/huin/goupnp v1.3.0/go.mod h1:gnGPsThkYa7bFi/KWmEysQRf48l2dvR5bxr2OFckNX8= +github.com/jackpal/go-nat-pmp v1.0.2 h1:KzKSgb7qkJvOUTqYl9/Hg/me3pWgBmERKrTGD7BdWus= +github.com/jackpal/go-nat-pmp v1.0.2/go.mod h1:QPH045xvCAeXUZOxsnwmrtiCoxIr9eob+4orBN1SBKc= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= @@ -100,8 +149,12 @@ github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzh github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/leanovate/gopter v0.2.11 h1:vRjThO1EKPb/1NsDXuDrzldR28RLkBflWYcU9CvzWu4= @@ -110,9 +163,8 @@ github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= github.com/logrusorgru/aurora v2.0.3+incompatible h1:tOpm7WcpBTn4fjmVfgpQq0EfczGlG91VSDkswnjF5A8= github.com/logrusorgru/aurora v2.0.3+incompatible/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4= -github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= -github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= -github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= +github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU= @@ -123,6 +175,8 @@ github.com/mitchellh/go-testing-interface v1.14.1 h1:jrgshOhYAUVNMAJiKbEu7EqAwgJ github.com/mitchellh/go-testing-interface v1.14.1/go.mod h1:gfgS7OtZj6MA4U1UrDRp04twqAjfvlZyCfX3sDjEym8= github.com/mitchellh/mapstructure v1.4.1 h1:CpVNEelQCZBooIPDn+AR3NpivK/TIKU8bDxdASFVQag= github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/mitchellh/pointerstructure v1.2.0 h1:O+i9nHnXS3l/9Wu7r4NrEdwA2VFTicjUEN1uBnDo34A= +github.com/mitchellh/pointerstructure v1.2.0/go.mod h1:BRAsLI5zgXmw97Lf6s25bs8ohIXc3tViBH44KcwB2g4= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -139,17 +193,41 @@ github.com/onsi/gomega v1.10.1 h1:o0+MgICZLuZ7xjH7Vx6zS/zcu93/BEp1VwkIW1mEXCE= github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= +github.com/pion/dtls/v2 v2.2.7 h1:cSUBsETxepsCSFSxC3mc/aDo14qQLMSL+O6IjG28yV8= +github.com/pion/dtls/v2 v2.2.7/go.mod h1:8WiMkebSHFD0T+dIU+UeBaoV7kDhOW5oDCzZ7WZ/F9s= +github.com/pion/logging v0.2.2 h1:M9+AIj/+pxNsDfAT64+MAVgJO0rsyLnoJKCqf//DoeY= +github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms= +github.com/pion/stun/v2 v2.0.0 h1:A5+wXKLAypxQri59+tmQKVs7+l6mMM+3d+eER9ifRU0= +github.com/pion/stun/v2 v2.0.0/go.mod h1:22qRSh08fSEttYUmJZGlriq9+03jtVmXNODgLccj8GQ= +github.com/pion/transport/v2 v2.2.1 h1:7qYnCBlpgSJNYMbLCKuSY9KbQdBFoETvPNETv0y4N7c= +github.com/pion/transport/v2 v2.2.1/go.mod h1:cXXWavvCnFF6McHTft3DWS9iic2Mftcz1Aq29pGcU5g= +github.com/pion/transport/v3 v3.0.1 h1:gDTlPJwROfSfz6QfSi0ZmeCSkFcnWWiiR9ES0ouANiM= +github.com/pion/transport/v3 v3.0.1/go.mod h1:UY7kiITrlMv7/IKgd5eTUcaahZx5oUN3l9SzK5f5xE0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v1.19.1 h1:wZWJDwK+NameRJuPGDhlnFgx8e8HN3XHQeLaYJFJBOE= +github.com/prometheus/client_golang v1.19.1/go.mod h1:mP78NwGzrVks5S2H6ab8+ZZGJLZUq1hoULYBAYBw1Ho= +github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw= +github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI= +github.com/prometheus/common v0.48.0 h1:QO8U2CdOzSn1BBsmXJXduaaW+dY/5QLjfB8svtSzKKE= +github.com/prometheus/common v0.48.0/go.mod h1:0/KsvlIEfPQCQ5I2iNSAWKPZziNCvRs5EC6ILDTlAPc= +github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo= +github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo= github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI= github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg= github.com/quic-go/quic-go v0.55.0 h1:zccPQIqYCXDt5NmcEabyYvOnomjs8Tlwl7tISjJh9Mk= github.com/quic-go/quic-go v0.55.0/go.mod h1:DR51ilwU1uE164KuWXhinFcKWGlEjzys2l8zUl5Ss1U= github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= +github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= +github.com/rs/cors v1.7.0 h1:+88SsELBHx5r+hZ8TCkggzSstaWNbDvThkVK8H6f9ik= +github.com/rs/cors v1.7.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU= +github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible h1:Bn1aCHHRnjv4Bl16T8rcaFjYSrGrIZvpiGO6P3Q4GpU= github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA= github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8= @@ -169,6 +247,8 @@ github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/supranational/blst v0.3.16-0.20250831170142-f48500c1fdbe h1:nbdqkIGOGfUAD54q1s2YBcBz/WcsxCO9HUQ4aGV5hUw= github.com/supranational/blst v0.3.16-0.20250831170142-f48500c1fdbe/go.mod h1:jZJtfjgudtNl4en1tzwPIV3KjUnQUvG3/j+w+fVonLw= +github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 h1:epCh84lMvA70Z7CTTCmYQn2CKbY8j86K7/FAIr141uY= +github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45IWZaF22tdD+VEXcAWRA037jwmWEB5VWYORlTpc= github.com/test-go/testify v1.1.4 h1:Tf9lntrKUMHiXQ07qBScBTSA0dhYQlu83hswqelv1iE= github.com/test-go/testify v1.1.4/go.mod h1:rH7cfJo/47vWGdi4GPj16x3/t1xGOj2YxzmNQzk2ghU= github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU= @@ -179,6 +259,8 @@ github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA= github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4= +github.com/urfave/cli/v2 v2.27.5 h1:WoHEJLdsXr6dDWoJgMq/CboDmyY/8HMMH1fTECbih+w= +github.com/urfave/cli/v2 v2.27.5/go.mod h1:3Sevf16NykTbInEnD0yKkjDAeZDS0A6bzhBH5hrMvTQ= github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4= github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM= @@ -188,6 +270,8 @@ github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHo github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74= github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= +github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4= +github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM= github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= @@ -216,13 +300,15 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20220214200702-86341886e292/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4= -golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc= +golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU= +golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0= +golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df h1:UA2aFVmmsIlefxMk29Dp2juaUSth8Pyn3Tq5Y5mJGME= +golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc= golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ= -golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc= +golang.org/x/mod v0.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk= +golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= @@ -230,15 +316,16 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE= -golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg= +golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU= +golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= -golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= +golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -246,31 +333,33 @@ golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k= -golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= +golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.34.0 h1:O/2T7POpk0ZZ7MAzMeWFSg6S5IpWd/RXDlM9hgM3DR4= -golang.org/x/term v0.34.0/go.mod h1:5jC53AEywhIVebHgPVeg0mj8OD3VO9OzclacVrqpaAw= +golang.org/x/term v0.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q= +golang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng= -golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU= -golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY= -golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU= +golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY= +golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= +golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg= -golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s= +golang.org/x/tools v0.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ= +golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -280,6 +369,8 @@ google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7I google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc= +gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= diff --git a/e2e/servers/gin/main.go b/e2e/servers/gin/main.go index 65ad0adeba..a52a7bc234 100644 --- a/e2e/servers/gin/main.go +++ b/e2e/servers/gin/main.go @@ -15,7 +15,8 @@ import ( "github.com/coinbase/x402/go/extensions/types" x402http "github.com/coinbase/x402/go/http" ginmw "github.com/coinbase/x402/go/http/gin" - evm "github.com/coinbase/x402/go/mechanisms/evm/exact/server" + exactevm "github.com/coinbase/x402/go/mechanisms/evm/exact/server" + uptoevm "github.com/coinbase/x402/go/mechanisms/evm/upto/server" svm "github.com/coinbase/x402/go/mechanisms/svm/exact/server" ginfw "github.com/gin-gonic/gin" "github.com/joho/godotenv" @@ -23,12 +24,10 @@ import ( var shutdownRequested bool -/** - * Gin E2E Test Server with x402 v2 Payment Middleware - * - * This server demonstrates how to integrate x402 v2 payment middleware - * with a Gin application for end-to-end testing. - */ +// Gin E2E Test Server with x402 v2 Payment Middleware +// +// This server demonstrates how to integrate x402 v2 payment middleware +// with a Gin application for end-to-end testing. func main() { // Load .env file if it exists @@ -72,6 +71,11 @@ func main() { evmNetwork := x402.Network(evmNetworkStr) svmNetwork := x402.Network(svmNetworkStr) + evmPermit2Asset := os.Getenv("EVM_PERMIT2_ASSET") + if evmPermit2Asset == "" { + evmPermit2Asset = "0x036CbD53842c5426634e7929541eC2318f3dCF7e" + } + fmt.Printf("EVM Payee address: %s\n", evmPayeeAddress) fmt.Printf("SVM Payee address: %s\n", svmPayeeAddress) fmt.Printf("Using remote facilitator at: %s\n", facilitatorURL) @@ -86,12 +90,6 @@ func main() { URL: facilitatorURL, }) - /** - * Configure x402 payment middleware - * - * This middleware protects the /protected endpoint with a $0.001 USDC payment requirement - * on the Base Sepolia testnet with bazaar discovery extension. - */ // Declare bazaar discovery extension for GET endpoints discoveryExtension, err := bazaar.DeclareDiscoveryExtension( bazaar.MethodGET, @@ -117,7 +115,7 @@ func main() { } routes := x402http.RoutesConfig{ - "GET /protected": { + "GET /exact/evm/eip3009": { Accepts: x402http.PaymentOptions{ { Scheme: "exact", @@ -126,11 +124,11 @@ func main() { Network: evmNetwork, }, }, - Extensions: map[string]interface{}{ - types.BAZAAR.Key(): discoveryExtension, + Extensions: map[string]interface{}{ + types.BAZAAR.Key(): discoveryExtension, + }, }, - }, - "GET /protected-svm": { + "GET /exact/svm": { Accepts: x402http.PaymentOptions{ { Scheme: "exact", @@ -139,31 +137,58 @@ func main() { Network: svmNetwork, }, }, - Extensions: map[string]interface{}{ - types.BAZAAR.Key(): discoveryExtension, + Extensions: map[string]interface{}{ + types.BAZAAR.Key(): discoveryExtension, + }, }, - }, - // Permit2 endpoint - explicitly requires Permit2 flow instead of EIP-3009 - "GET /protected-permit2": { + // Permit2 direct endpoint - standard settle, no gas sponsoring (client must pre-approve Permit2) + "GET /exact/evm/permit2": { Accepts: x402http.PaymentOptions{ { Scheme: "exact", PayTo: evmPayeeAddress, Network: evmNetwork, - // Use pre-parsed price with assetTransferMethod to force Permit2 Price: map[string]interface{}{ - "amount": "1000", // 0.001 USDC (6 decimals) - "asset": "0x036CbD53842c5426634e7929541eC2318f3dCF7e", // Base Sepolia USDC + "amount": "1000", + "asset": "0x036CbD53842c5426634e7929541eC2318f3dCF7e", "extra": map[string]interface{}{ "assetTransferMethod": "permit2", }, }, }, }, - Extensions: func() map[string]interface{} { - ext := map[string]interface{}{ + Extensions: map[string]interface{}{ types.BAZAAR.Key(): discoveryExtension, - } + }, + }, + // Permit2 endpoint - explicitly requires Permit2 flow instead of EIP-3009 + "GET /exact/evm/permit2-eip2612GasSponsoring": { + Accepts: x402http.PaymentOptions{ + { + Scheme: "exact", + PayTo: evmPayeeAddress, + Network: evmNetwork, + Price: map[string]interface{}{ + "amount": "1000", + "asset": evmPermit2Asset, + "extra": func() map[string]interface{} { + name := "USD Coin" + if evmNetworkStr == "eip155:84532" { + name = "USDC" + } + return map[string]interface{}{ + "assetTransferMethod": "permit2", + "name": name, + "version": "2", + } + }(), + }, + }, + }, + Extensions: func() map[string]interface{} { + ext := map[string]interface{}{ + types.BAZAAR.Key(): discoveryExtension, + } // Add EIP-2612 gas sponsoring extension for k, v := range eip2612gassponsor.DeclareEip2612GasSponsoringExtension() { ext[k] = v @@ -171,46 +196,73 @@ func main() { return ext }(), }, - // Permit2 ERC-20 approval endpoint - requires Permit2 flow with a generic ERC-20 token (no EIP-2612) - "GET /protected-permit2-erc20": { - Accepts: x402http.PaymentOptions{ - { - Scheme: "exact", - PayTo: evmPayeeAddress, - Network: evmNetwork, - // Use MockGenericERC20 token that does NOT implement EIP-2612 - Price: map[string]interface{}{ - "amount": "1000", // smallest unit - "asset": "0xeED520980fC7C7B4eB379B96d61CEdea2423005a", // MockGenericERC20 on Base Sepolia - "extra": map[string]interface{}{ - "assetTransferMethod": "permit2", + "GET /upto/evm/permit2": { + Accepts: x402http.PaymentOptions{ + { + Scheme: "upto", + PayTo: evmPayeeAddress, + Network: evmNetwork, + Price: map[string]interface{}{ + "amount": "2000", + "asset": evmPermit2Asset, + "extra": map[string]interface{}{ + "assetTransferMethod": "permit2", + "name": "USDC", + "version": "2", + }, }, }, }, + Extensions: func() map[string]interface{} { + ext := map[string]interface{}{ + types.BAZAAR.Key(): discoveryExtension, + } + for k, v := range eip2612gassponsor.DeclareEip2612GasSponsoringExtension() { + ext[k] = v + } + return ext + }(), }, - Extensions: func() map[string]interface{} { - ext := map[string]interface{}{ - types.BAZAAR.Key(): discoveryExtension, - } - // Advertise ERC-20 approval gas sponsoring (for tokens without EIP-2612) - for k, v := range erc20approvalgassponsor.DeclareExtension() { - ext[k] = v - } - return ext - }(), - }, -} + // Permit2 ERC-20 approval endpoint - requires Permit2 flow with a generic ERC-20 token (no EIP-2612) + "GET /exact/evm/permit2-erc20ApprovalGasSponsoring": { + Accepts: x402http.PaymentOptions{ + { + Scheme: "exact", + PayTo: evmPayeeAddress, + Network: evmNetwork, + Price: map[string]interface{}{ + "amount": "1000", + "asset": evmPermit2Asset, + "extra": map[string]interface{}{ + "assetTransferMethod": "permit2", + }, + }, + }, + }, + Extensions: func() map[string]interface{} { + ext := map[string]interface{}{ + types.BAZAAR.Key(): discoveryExtension, + } + // Advertise ERC-20 approval gas sponsoring (for tokens without EIP-2612) + for k, v := range erc20approvalgassponsor.DeclareExtension() { + ext[k] = v + } + return ext + }(), + }, + } // Apply payment middleware with detailed error logging r.Use(ginmw.X402Payment(ginmw.Config{ Routes: routes, Facilitator: facilitatorClient, Schemes: []ginmw.SchemeConfig{ - {Network: evmNetwork, Server: evm.NewExactEvmScheme()}, + {Network: evmNetwork, Server: exactevm.NewExactEvmScheme()}, + {Network: evmNetwork, Server: uptoevm.NewUptoEvmScheme()}, {Network: svmNetwork, Server: svm.NewExactSvmScheme()}, }, SyncFacilitatorOnStart: true, - Timeout: 30 * time.Second, + Timeout: 30 * time.Second, ErrorHandler: func(c *ginfw.Context, err error) { // Log detailed error information for debugging fmt.Printf("❌ [E2E SERVER ERROR] Payment error occurred\n") @@ -234,13 +286,11 @@ func main() { }, })) - /** - * Protected endpoint - requires payment to access - * - * This endpoint demonstrates a resource protected by x402 payment middleware. - * Clients must provide a valid payment signature to access this endpoint. - */ - r.GET("/protected", func(c *ginfw.Context) { + // Protected endpoint - requires payment to access + // + // This endpoint demonstrates a resource protected by x402 payment middleware. + // Clients must provide a valid payment signature to access this endpoint. + r.GET("/exact/evm/eip3009", func(c *ginfw.Context) { if shutdownRequested { c.JSON(http.StatusServiceUnavailable, ginfw.H{ "error": "Server shutting down", @@ -255,13 +305,11 @@ func main() { }) }) - /** - * Protected SVM endpoint - requires payment to access - * - * This endpoint demonstrates a Solana payment protected resource. - * Clients must provide a valid payment signature to access this endpoint. - */ - r.GET("/protected-svm", func(c *ginfw.Context) { + // Protected SVM endpoint - requires payment to access + // + // This endpoint demonstrates a Solana payment protected resource. + // Clients must provide a valid payment signature to access this endpoint. + r.GET("/exact/svm", func(c *ginfw.Context) { if shutdownRequested { c.JSON(http.StatusServiceUnavailable, ginfw.H{ "error": "Server shutting down", @@ -276,13 +324,8 @@ func main() { }) }) - /** - * Protected Permit2 endpoint - requires payment via Permit2 flow - * - * This endpoint demonstrates the Permit2 payment flow. - * Clients must have approved Permit2 to spend their USDC before accessing. - */ - r.GET("/protected-permit2", func(c *ginfw.Context) { + // Protected Permit2 direct endpoint - standard settle (no gas sponsoring) + r.GET("/exact/evm/permit2", func(c *ginfw.Context) { if shutdownRequested { c.JSON(http.StatusServiceUnavailable, ginfw.H{ "error": "Server shutting down", @@ -297,12 +340,27 @@ func main() { }) }) - /** - * Protected Permit2 ERC-20 approval endpoint - requires payment via Permit2 flow - * using a generic ERC-20 token that does NOT support EIP-2612. - * The facilitator sponsors the approve(Permit2, MaxUint256) transaction. - */ - r.GET("/protected-permit2-erc20", func(c *ginfw.Context) { + // Protected Permit2 EIP-2612 endpoint - requires payment via Permit2 with gas sponsoring. + // Uses EIP-2612 permit atomically in settleWithPermit. No pre-approval needed. + r.GET("/exact/evm/permit2-eip2612GasSponsoring", func(c *ginfw.Context) { + if shutdownRequested { + c.JSON(http.StatusServiceUnavailable, ginfw.H{ + "error": "Server shutting down", + }) + return + } + + c.JSON(http.StatusOK, ginfw.H{ + "message": "Permit2 EIP-2612 endpoint accessed successfully", + "timestamp": time.Now().Format(time.RFC3339), + "method": "permit2-eip2612", + }) + }) + + // Protected Permit2 ERC-20 approval endpoint - requires payment via Permit2 flow + // using a generic ERC-20 token that does NOT support EIP-2612. + // The facilitator sponsors the approve(Permit2, MaxUint256) transaction. + r.GET("/exact/evm/permit2-erc20ApprovalGasSponsoring", func(c *ginfw.Context) { if shutdownRequested { c.JSON(http.StatusServiceUnavailable, ginfw.H{ "error": "Server shutting down", @@ -317,11 +375,30 @@ func main() { }) }) - /** - * Health check endpoint - no payment required - * - * Used to verify the server is running and responsive. - */ + // Upto Permit2 endpoint - settles with partial amount + r.GET("/upto/evm/permit2", func(c *ginfw.Context) { + if shutdownRequested { + c.JSON(http.StatusServiceUnavailable, ginfw.H{ + "error": "Server shutting down", + }) + return + } + + // Settle with partial amount (for e2e tests) + ginmw.SetSettlementOverrides(c, &x402.SettlementOverrides{ + Amount: "1000", + }) + + c.JSON(http.StatusOK, ginfw.H{ + "message": "Upto Permit2 endpoint accessed successfully", + "timestamp": time.Now().Format(time.RFC3339), + "method": "upto-permit2", + }) + }) + + // Health check endpoint - no payment required + // + // Used to verify the server is running and responsive. r.GET("/health", func(c *ginfw.Context) { c.JSON(http.StatusOK, ginfw.H{ "status": "ok", @@ -333,11 +410,9 @@ func main() { }) }) - /** - * Shutdown endpoint - used by e2e tests - * - * Allows graceful shutdown of the server during testing. - */ + // Shutdown endpoint - used by e2e tests + // + // Allows graceful shutdown of the server during testing. r.POST("/close", func(c *ginfw.Context) { shutdownRequested = true @@ -375,10 +450,12 @@ func main() { ║ SVM Payee: %-40s ║ ║ ║ ║ Endpoints: ║ -║ • GET /protected (EIP-3009 payment) ║ -║ • GET /protected-svm (SVM payment) ║ -║ • GET /protected-permit2 (Permit2 payment) ║ -║ • GET /protected-permit2-erc20 (Permit2 ERC-20) ║ +║ • GET /exact/evm/eip3009 (EVM EIP-3009) ║ +║ • GET /exact/evm/permit2 (Permit2) ║ +║ • GET /exact/evm/permit2-eip2612GasSponsoring ║ +║ • GET /exact/evm/permit2-erc20ApprovalGasSponsoring ║ +║ • GET /upto/evm/permit2 (Upto Permit2) ║ +║ • GET /exact/svm (SVM) ║ ║ • GET /health (no payment required) ║ ║ • POST /close (shutdown server) ║ ╚════════════════════════════════════════════════════════╝ diff --git a/e2e/servers/gin/test.config.json b/e2e/servers/gin/test.config.json index 055a3b04bc..5e434fbfc3 100644 --- a/e2e/servers/gin/test.config.json +++ b/e2e/servers/gin/test.config.json @@ -11,7 +11,7 @@ "description": "Go Gin server with x402 v2 payment middleware", "endpoints": [ { - "path": "/protected", + "path": "/exact/evm/eip3009", "method": "GET", "description": "Protected endpoint requiring EIP-3009 payment", "requiresPayment": true, @@ -19,24 +19,44 @@ "transferMethod": "eip3009" }, { - "path": "/protected-permit2", + "path": "/exact/evm/permit2", "method": "GET", - "description": "Protected endpoint requiring Permit2 payment", + "description": "Protected endpoint requiring Permit2 payment (standard settle, no gas sponsoring)", "requiresPayment": true, "protocolFamily": "evm", - "transferMethod": "permit2" + "transferMethod": "permit2", + "permit2Direct": true + }, + { + "path": "/exact/evm/permit2-eip2612GasSponsoring", + "method": "GET", + "description": "Protected endpoint requiring Permit2 payment with EIP-2612 gas sponsoring", + "requiresPayment": true, + "protocolFamily": "evm", + "transferMethod": "permit2", + "coldstart": true }, { - "path": "/protected-permit2-erc20", + "path": "/exact/evm/permit2-erc20ApprovalGasSponsoring", "method": "GET", "description": "Protected endpoint requiring Permit2 payment with ERC-20 approval gas sponsoring", "requiresPayment": true, "protocolFamily": "evm", "transferMethod": "permit2", - "extension": "erc20ApprovalGasSponsoring" + "extensions": ["erc20ApprovalGasSponsoring"], + "coldstart": true + }, + { + "path": "/upto/evm/permit2", + "method": "GET", + "description": "Protected endpoint requiring upto Permit2 payment (usage-based settlement)", + "requiresPayment": true, + "protocolFamily": "evm", + "transferMethod": "upto", + "permit2Direct": true }, { - "path": "/protected-svm", + "path": "/exact/svm", "method": "GET", "description": "Protected endpoint requiring payment (SVM)", "requiresPayment": true, @@ -64,4 +84,4 @@ ], "optional": [] } -} \ No newline at end of file +} diff --git a/e2e/servers/hono/README.md b/e2e/servers/hono/README.md index 708dd989df..2f0483f54b 100644 --- a/e2e/servers/hono/README.md +++ b/e2e/servers/hono/README.md @@ -1,13 +1,13 @@ -# E2E Test Server: Express (TypeScript) +# E2E Test Server: Hono (TypeScript) -This server demonstrates and tests the x402 Express.js middleware with both EVM and SVM payment protection. +This server demonstrates and tests the x402 Hono middleware with EVM, SVM, and optional Stellar payment protection. ## What It Tests ### Core Functionality - ✅ **V2 Protocol** - Modern x402 server middleware - ✅ **Payment Protection** - Middleware protecting specific routes -- ✅ **Multi-chain Support** - EVM and SVM payment acceptance +- ✅ **Multi-chain Support** - EVM, SVM, and optional Stellar payment acceptance - ✅ **Facilitator Integration** - HTTP communication with facilitator - ✅ **Extension Support** - Bazaar discovery metadata - ✅ **Settlement Handling** - Payment verification and confirmation @@ -15,6 +15,7 @@ This server demonstrates and tests the x402 Express.js middleware with both EVM ### Protected Endpoints - ✅ `GET /protected` - Requires EVM payment (USDC on Base Sepolia) - ✅ `GET /protected-svm` - Requires SVM payment (USDC on Solana Devnet) +- ✅ `GET /protected-stellar` - Requires Stellar payment (USDC on Stellar Testnet, optional) ## What It Demonstrates @@ -25,6 +26,7 @@ import express from "express"; import { x402Middleware } from "@x402/server/express"; import { ExactEvmServer } from "@x402/evm"; import { ExactEvmServer } from "@x402/svm"; +import { ExactStellarServer } from "@x402/stellar"; const app = express(); @@ -47,6 +49,15 @@ const routes = { extensions: { bazaar: discoveryMetadata } + }, + "GET /protected-stellar": { + scheme: "exact", + network: "stellar:testnet", + payTo: "YourStellarAddress", + price: "$0.001", + extensions: { + bazaar: discoveryMetadata + } } }; @@ -56,7 +67,8 @@ app.use(x402Middleware({ facilitatorUrl: "http://localhost:4023", servers: { "eip155:84532": new ExactEvmServer(), - "solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1": new ExactSvmServer() + "solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1": new ExactSvmServer(), + "stellar:testnet": new ExactStellarServer() } })); @@ -68,6 +80,10 @@ app.get("/protected", (req, res) => { app.get("/protected-svm", (req, res) => { res.json({ message: "SVM payment successful!" }); }); + +app.get("/protected-stellar", (req, res) => { + res.json({ message: "Stellar payment successful!" }); +}); ``` ### Key Concepts Shown @@ -84,7 +100,7 @@ app.get("/protected-svm", (req, res) => { This server is tested with: - **Clients:** TypeScript Fetch, Go HTTP - **Facilitators:** TypeScript, Go -- **Payment Types:** EVM (Base Sepolia), SVM (Solana Devnet) +- **Payment Types:** EVM (Base Sepolia), SVM (Solana Devnet), Stellar (Stellar Testnet) - **Protocols:** V2 (primary), V1 (via client negotiation) ### Request Flow @@ -100,25 +116,31 @@ This server is tested with: ```bash # Via e2e test suite cd e2e -pnpm test --server=express +pnpm test --server=hono # Direct execution -cd e2e/servers/express +cd e2e/servers/hono export FACILITATOR_URL="http://localhost:4023" export EVM_PAYEE_ADDRESS="0x..." export SVM_PAYEE_ADDRESS="..." +export STELLAR_PAYEE_ADDRESS="G..." # optional export PORT=4022 pnpm start ``` ## Environment Variables +### Required - `PORT` - HTTP server port (default: 4022) - `FACILITATOR_URL` - Facilitator endpoint URL - `EVM_PAYEE_ADDRESS` - Ethereum address to receive payments - `SVM_PAYEE_ADDRESS` - Solana address to receive payments + +### Optional +- `STELLAR_PAYEE_ADDRESS` - Stellar address to receive payments - enables Stellar endpoint - `EVM_NETWORK` - EVM network (default: eip155:84532) - `SVM_NETWORK` - SVM network (default: solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1) +- `STELLAR_NETWORK` - Stellar network (default: stellar:testnet) ## Response Examples @@ -151,8 +173,9 @@ PAYMENT-RESPONSE: - `@x402/server` - Express middleware - `@x402/evm` - EVM service - `@x402/svm` - SVM service +- `@x402/stellar` - Stellar server (optional) - `@x402/extensions/bazaar` - Discovery extension -- `express` - HTTP server framework +- `hono` - HTTP server framework ## Implementation Highlights @@ -166,5 +189,6 @@ PAYMENT-RESPONSE: ### Service Integration - **EVM Service** - Handles Base Sepolia USDC payments - **SVM Service** - Handles Solana Devnet USDC payments +- **Stellar Service** - Handles Stellar Testnet USDC contract payments - **Price Conversion** - "$0.001" → token amounts with decimals - **Asset Resolution** - Automatic USDC contract/mint lookup diff --git a/e2e/servers/hono/index.ts b/e2e/servers/hono/index.ts index 0eb94b4744..9536d6b30f 100644 --- a/e2e/servers/hono/index.ts +++ b/e2e/servers/hono/index.ts @@ -1,10 +1,12 @@ import { serve } from "@hono/node-server"; import { Hono } from "hono"; -import { paymentMiddleware } from "@x402/hono"; +import { paymentMiddleware, setSettlementOverrides } from "@x402/hono"; import { x402ResourceServer, HTTPFacilitatorClient } from "@x402/core/server"; import { ExactEvmScheme } from "@x402/evm/exact/server"; +import { UptoEvmScheme } from "@x402/evm/upto/server"; import { ExactSvmScheme } from "@x402/svm/exact/server"; import { ExactAptosScheme } from "@x402/aptos/exact/server"; +import { ExactStellarScheme } from "@x402/stellar/exact/server"; import { bazaarResourceServerExtension, declareDiscoveryExtension } from "@x402/extensions/bazaar"; import { declareEip2612GasSponsoringExtension, @@ -23,11 +25,15 @@ dotenv.config(); const PORT = process.env.PORT || "4023"; const EVM_NETWORK = (process.env.EVM_NETWORK || "eip155:84532") as `${string}:${string}`; -const SVM_NETWORK = (process.env.SVM_NETWORK || "solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1") as `${string}:${string}`; +const SVM_NETWORK = (process.env.SVM_NETWORK || + "solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1") as `${string}:${string}`; const APTOS_NETWORK = (process.env.APTOS_NETWORK || "aptos:2") as `${string}:${string}`; +const STELLAR_NETWORK = (process.env.STELLAR_NETWORK || "stellar:testnet") as `${string}:${string}`; const EVM_PAYEE_ADDRESS = process.env.EVM_PAYEE_ADDRESS as `0x${string}`; const SVM_PAYEE_ADDRESS = process.env.SVM_PAYEE_ADDRESS as string; const APTOS_PAYEE_ADDRESS = process.env.APTOS_PAYEE_ADDRESS as string; +const STELLAR_PAYEE_ADDRESS = process.env.STELLAR_PAYEE_ADDRESS as string | undefined; +const EVM_PERMIT2_ASSET = process.env.EVM_PERMIT2_ASSET as `0x${string}`; const facilitatorUrl = process.env.FACILITATOR_URL; if (!EVM_PAYEE_ADDRESS) { @@ -40,7 +46,6 @@ if (!SVM_PAYEE_ADDRESS) { process.exit(1); } - if (!facilitatorUrl) { console.error("❌ FACILITATOR_URL environment variable is required"); process.exit(1); @@ -49,18 +54,26 @@ if (!facilitatorUrl) { // Initialize Hono app const app = new Hono(); -// Create HTTP facilitator client -const facilitatorClient = new HTTPFacilitatorClient({ url: facilitatorUrl }); +// Create facilitator clients (mock facilitator as fallback for startup validation) +const facilitatorClients = [new HTTPFacilitatorClient({ url: facilitatorUrl })]; +const mockFacilitatorUrl = process.env.MOCK_FACILITATOR_URL; +if (mockFacilitatorUrl) { + facilitatorClients.push(new HTTPFacilitatorClient({ url: mockFacilitatorUrl })); +} // Create x402 resource server with builder pattern (cleaner!) -const x402Server = new x402ResourceServer(facilitatorClient); +const x402Server = new x402ResourceServer(facilitatorClients); // Register server schemes x402Server.register("eip155:*", new ExactEvmScheme()); +x402Server.register("eip155:*", new UptoEvmScheme()); x402Server.register("solana:*", new ExactSvmScheme()); if (APTOS_PAYEE_ADDRESS) { x402Server.register("aptos:*", new ExactAptosScheme()); } +if (STELLAR_PAYEE_ADDRESS) { + x402Server.register("stellar:*", new ExactStellarScheme()); +} // Register Bazaar discovery extension x402Server.registerExtension(bazaarResourceServerExtension); @@ -74,11 +87,28 @@ console.log(`Using remote facilitator at: ${facilitatorUrl}`); * Pre-middleware guard for optional Aptos endpoint * Returns 501 Not Implemented if Aptos is not configured */ -app.use("/protected-aptos", async (c, next) => { +app.use("/exact/aptos", async (c, next) => { if (!APTOS_PAYEE_ADDRESS) { + return c.json( + { + error: "Aptos payments not configured", + message: "APTOS_PAYEE_ADDRESS environment variable is not set", + }, + 501, + ); + } + await next(); +}); + +/** + * Pre-middleware guard for optional Stellar endpoint + * Returns 501 Not Implemented if Stellar is not configured + */ +app.use("/exact/stellar", async (c, next) => { + if (!STELLAR_PAYEE_ADDRESS) { return c.json({ - error: "Aptos payments not configured", - message: "APTOS_PAYEE_ADDRESS environment variable is not set", + error: "Stellar payments not configured", + message: "STELLAR_PAYEE_ADDRESS environment variable is not set", }, 501); } await next(); @@ -95,7 +125,7 @@ app.use( paymentMiddleware( { // Route-specific payment configuration - "GET /protected": { + "GET /exact/evm/eip3009": { accepts: { payTo: EVM_PAYEE_ADDRESS, scheme: "exact", @@ -120,7 +150,7 @@ app.use( }), }, }, - "GET /protected-svm": { + "GET /exact/svm": { accepts: { payTo: SVM_PAYEE_ADDRESS, scheme: "exact", @@ -147,44 +177,44 @@ app.use( }, ...(APTOS_PAYEE_ADDRESS ? { - "GET /protected-aptos": { - accepts: { - payTo: APTOS_PAYEE_ADDRESS, - scheme: "exact", - price: "$0.001", - network: APTOS_NETWORK, - }, - extensions: { - ...declareDiscoveryExtension({ - output: { - example: { - message: "Protected endpoint accessed successfully", - timestamp: "2024-01-01T00:00:00Z", - }, - schema: { - properties: { - message: { type: "string" }, - timestamp: { type: "string" }, - }, - required: ["message", "timestamp"], + "GET /exact/aptos": { + accepts: { + payTo: APTOS_PAYEE_ADDRESS, + scheme: "exact", + price: "$0.001", + network: APTOS_NETWORK, + }, + extensions: { + ...declareDiscoveryExtension({ + output: { + example: { + message: "Protected endpoint accessed successfully", + timestamp: "2024-01-01T00:00:00Z", + }, + schema: { + properties: { + message: { type: "string" }, + timestamp: { type: "string" }, }, + required: ["message", "timestamp"], }, - }), - }, + }, + }), }, - } + }, + } : {}), - "GET /protected-permit2": { + "GET /exact/evm/permit2": { accepts: { payTo: EVM_PAYEE_ADDRESS, scheme: "exact", network: EVM_NETWORK, price: { amount: "1000", - asset: "0x036CbD53842c5426634e7929541eC2318f3dCF7e", + asset: EVM_PERMIT2_ASSET, extra: { assetTransferMethod: "permit2", - name: "USDC", + name: EVM_NETWORK == "eip155:84532" ? "USDC" : "USD Coin", version: "2", }, }, @@ -207,17 +237,46 @@ app.use( }, }, }), + }, + }, + "GET /exact/evm/permit2-eip2612GasSponsoring": { + accepts: { + payTo: EVM_PAYEE_ADDRESS, + scheme: "exact", + network: EVM_NETWORK, + price: "$0.001", + // Use pre-parsed price with assetTransferMethod to force Permit2 + extra: { assetTransferMethod: "permit2" }, + }, + extensions: { + ...declareDiscoveryExtension({ + output: { + example: { + message: "Permit2 EIP-2612 endpoint accessed successfully", + timestamp: "2024-01-01T00:00:00Z", + method: "permit2-eip2612", + }, + schema: { + properties: { + message: { type: "string" }, + timestamp: { type: "string" }, + method: { type: "string" }, + }, + required: ["message", "timestamp", "method"], + }, + }, + }), ...declareEip2612GasSponsoringExtension(), }, }, - "GET /protected-permit2-erc20": { + "GET /exact/evm/permit2-erc20ApprovalGasSponsoring": { accepts: { payTo: EVM_PAYEE_ADDRESS, scheme: "exact", network: EVM_NETWORK, price: { amount: "1000", - asset: "0xeED520980fC7C7B4eB379B96d61CEdea2423005a", + asset: EVM_PERMIT2_ASSET, extra: { assetTransferMethod: "permit2", }, @@ -227,6 +286,93 @@ app.use( ...declareErc20ApprovalGasSponsoringExtension(), }, }, + // Upto Permit2 direct endpoint - client must have Permit2 pre-approved + // Authorizes up to 2000 atomic units, settles 1000 (partial settlement) + "GET /upto/evm/permit2": { + accepts: { + payTo: EVM_PAYEE_ADDRESS, + scheme: "upto", + network: EVM_NETWORK, + price: { + amount: "2000", + asset: EVM_PERMIT2_ASSET, + extra: { + assetTransferMethod: "permit2", + name: EVM_NETWORK == "eip155:84532" ? "USDC" : "USD Coin", + version: "2", + }, + }, + }, + }, + // Upto Permit2 endpoint with EIP-2612 gas sponsoring + // Authorizes up to 2000 atomic units, settles 1000 (partial settlement) + "GET /upto/evm/permit2-eip2612GasSponsoring": { + accepts: { + payTo: EVM_PAYEE_ADDRESS, + scheme: "upto", + network: EVM_NETWORK, + price: { + amount: "2000", + asset: EVM_PERMIT2_ASSET, + extra: { + assetTransferMethod: "permit2", + name: EVM_NETWORK == "eip155:84532" ? "USDC" : "USD Coin", + version: "2", + }, + }, + }, + extensions: { + ...declareEip2612GasSponsoringExtension(), + }, + }, + // Upto Permit2 endpoint for ERC-20 approval gas sponsoring (no EIP-2612) + // Authorizes up to 2000 atomic units, settles 1000 (partial settlement) + "GET /upto/evm/permit2-erc20ApprovalGasSponsoring": { + accepts: { + payTo: EVM_PAYEE_ADDRESS, + scheme: "upto", + network: EVM_NETWORK, + price: { + amount: "2000", + asset: EVM_PERMIT2_ASSET, + extra: { + assetTransferMethod: "permit2", + }, + }, + }, + extensions: { + ...declareErc20ApprovalGasSponsoringExtension(), + }, + }, + ...(STELLAR_PAYEE_ADDRESS + ? { + "GET /exact/stellar": { + accepts: { + payTo: STELLAR_PAYEE_ADDRESS!, + scheme: "exact", + price: "$0.001", + network: STELLAR_NETWORK, + }, + extensions: { + ...declareDiscoveryExtension({ + output: { + example: { + message: "Protected Stellar endpoint accessed successfully", + timestamp: "2024-01-01T00:00:00Z", + }, + schema: { + properties: { + message: { type: "string" }, + timestamp: { type: "string" }, + }, + required: ["message", "timestamp"], + }, + }, + }), + }, + }, + } + : {}), }, x402Server, // Pass pre-configured server instance ), @@ -238,7 +384,7 @@ app.use( * This endpoint demonstrates a resource protected by x402 payment middleware. * Clients must provide a valid payment signature to access this endpoint. */ -app.get("/protected", (c) => { +app.get("/exact/evm/eip3009", c => { return c.json({ message: "Protected endpoint accessed successfully", timestamp: new Date().toISOString(), @@ -251,7 +397,7 @@ app.get("/protected", (c) => { * This endpoint demonstrates a resource protected by x402 payment middleware for SVM. * Clients must provide a valid payment signature to access this endpoint. */ -app.get("/protected-svm", (c) => { +app.get("/exact/svm", c => { return c.json({ message: "Protected endpoint accessed successfully", timestamp: new Date().toISOString(), @@ -265,7 +411,7 @@ app.get("/protected-svm", (c) => { * Clients must provide a valid payment signature to access this endpoint. * Note: 501 check is handled by pre-middleware guard above. */ -app.get("/protected-aptos", (c) => { +app.get("/exact/aptos", c => { return c.json({ message: "Protected endpoint accessed successfully", timestamp: new Date().toISOString(), @@ -273,9 +419,9 @@ app.get("/protected-aptos", (c) => { }); /** - * Protected Permit2 endpoint - requires Permit2 payment with EIP-2612 gas sponsoring + * Protected Permit2 endpoint - standard settle (no gas sponsoring) */ -app.get("/protected-permit2", (c) => { +app.get("/exact/evm/permit2", c => { return c.json({ message: "Permit2 endpoint accessed successfully", timestamp: new Date().toISOString(), @@ -283,10 +429,21 @@ app.get("/protected-permit2", (c) => { }); }); +/** + * Protected Permit2 EIP-2612 endpoint - requires Permit2 with gas sponsoring + */ +app.get("/exact/evm/permit2-eip2612GasSponsoring", c => { + return c.json({ + message: "Permit2 EIP-2612 endpoint accessed successfully", + timestamp: new Date().toISOString(), + method: "permit2-eip2612", + }); +}); + /** * Protected Permit2 ERC-20 endpoint - requires Permit2 payment with ERC-20 approval gas sponsoring */ -app.get("/protected-permit2-erc20", (c) => { +app.get("/exact/evm/permit2-erc20ApprovalGasSponsoring", c => { return c.json({ message: "Permit2 ERC-20 approval endpoint accessed successfully", timestamp: new Date().toISOString(), @@ -294,12 +451,67 @@ app.get("/protected-permit2-erc20", (c) => { }); }); +/** + * Upto Permit2 direct endpoint - upto scheme, client must have Permit2 pre-approved + * Authorizes 2000, settles 1000 (partial settlement) + */ +app.get("/upto/evm/permit2", c => { + setSettlementOverrides(c, { amount: "1000" }); + return c.json({ + message: "Upto Permit2 endpoint accessed successfully", + timestamp: new Date().toISOString(), + method: "upto-permit2", + }); +}); + +/** + * Upto Permit2 EIP-2612 endpoint - upto scheme with gas sponsoring + * Authorizes 2000, settles 1000 (partial settlement) + */ +app.get("/upto/evm/permit2-eip2612GasSponsoring", c => { + setSettlementOverrides(c, { amount: "1000" }); + return c.json({ + message: "Upto Permit2 EIP-2612 endpoint accessed successfully", + timestamp: new Date().toISOString(), + method: "upto-permit2-eip2612", + }); +}); + +/** + * Upto Permit2 ERC-20 endpoint - upto scheme with ERC-20 approval gas sponsoring + * Authorizes 2000, settles 1000 (partial settlement) + */ +app.get("/upto/evm/permit2-erc20ApprovalGasSponsoring", c => { + setSettlementOverrides(c, { amount: "1000" }); + return c.json({ + message: "Upto Permit2 ERC-20 approval endpoint accessed successfully", + timestamp: new Date().toISOString(), + method: "upto-permit2-erc20-approval", + }); +}); + +/** + * Protected Stellar endpoint - requires payment to access + * + * This endpoint demonstrates a resource protected by x402 payment middleware for Stellar. + * Clients must provide a valid payment signature to access this endpoint. + * Note: 501 check is handled by pre-middleware guard above. + */ +if (STELLAR_PAYEE_ADDRESS) { + app.get("/exact/stellar", c => { + return c.json({ + message: "Protected Stellar endpoint accessed successfully", + timestamp: new Date().toISOString(), + }); + }); +} + /** * Health check endpoint - no payment required * * Used to verify the server is running and responsive. */ -app.get("/health", (c) => { +app.get("/health", c => { return c.json({ status: "ok", network: EVM_NETWORK, @@ -313,7 +525,7 @@ app.get("/health", (c) => { * * Allows graceful shutdown of the server during testing. */ -app.post("/close", (c) => { +app.post("/close", c => { console.log("Received shutdown request"); // Give time for response to be sent @@ -338,16 +550,20 @@ console.log(` ║ EVM Network: ${EVM_NETWORK} ║ ║ SVM Network: ${SVM_NETWORK} ║ ║ Aptos Network: ${APTOS_NETWORK} ║ +║ Stellar Network: ${STELLAR_NETWORK} ║ ║ EVM Payee: ${EVM_PAYEE_ADDRESS} ║ ║ SVM Payee: ${SVM_PAYEE_ADDRESS} ║ ║ Aptos Payee: ${APTOS_PAYEE_ADDRESS || "(not configured)"} +║ Stellar Payee: ${STELLAR_PAYEE_ADDRESS || "(not configured)"} ║ ║ ║ Endpoints: ║ -║ • GET /protected (EIP-3009 payment) ║ -║ • GET /protected-permit2 (Permit2 + EIP-2612) ║ -║ • GET /protected-permit2-erc20 (Permit2 + ERC-20 approval)║ -║ • GET /protected-svm (SVM payment) ║ -║ • GET /protected-aptos (Aptos payment) ║ +║ • GET /exact/evm/eip3009 (EVM EIP-3009) ║ +║ • GET /exact/evm/permit2 (Permit2) ║ +║ • GET /exact/evm/permit2-eip2612GasSponsoring ║ +║ • GET /exact/evm/permit2-erc20ApprovalGasSponsoring ║ +║ • GET /exact/svm (SVM) ║ +║ • GET /exact/aptos (Aptos) ║ +║ • GET /exact/stellar (Stellar) ║ ║ • GET /health (no payment required) ║ ║ • POST /close (shutdown server) ║ ╚════════════════════════════════════════════════════════╝ diff --git a/e2e/servers/hono/package.json b/e2e/servers/hono/package.json index c4924a9b02..ba14bf7e2d 100644 --- a/e2e/servers/hono/package.json +++ b/e2e/servers/hono/package.json @@ -16,6 +16,7 @@ "@x402/evm": "workspace:*", "@x402/extensions": "workspace:*", "@x402/hono": "workspace:*", + "@x402/stellar": "workspace:*", "@x402/svm": "workspace:*", "dotenv": "^16.6.1", "hono": "^4.7.1" @@ -34,4 +35,4 @@ "tsx": "^4.7.0", "typescript": "^5.3.0" } -} \ No newline at end of file +} diff --git a/e2e/servers/hono/test.config.json b/e2e/servers/hono/test.config.json index 2030b88cf0..c014288c77 100644 --- a/e2e/servers/hono/test.config.json +++ b/e2e/servers/hono/test.config.json @@ -3,14 +3,11 @@ "type": "server", "language": "typescript", "x402Version": 2, - "extensions": [ - "bazaar", - "eip2612GasSponsoring", - "erc20ApprovalGasSponsoring" - ], + "extensions": ["bazaar", "eip2612GasSponsoring", "erc20ApprovalGasSponsoring"], + "endpoints": [ { - "path": "/protected", + "path": "/exact/evm/eip3009", "method": "GET", "description": "Protected endpoint requiring EIP-3009 payment", "requiresPayment": true, @@ -18,36 +15,82 @@ "transferMethod": "eip3009" }, { - "path": "/protected-permit2", + "path": "/exact/evm/permit2", + "method": "GET", + "description": "Protected endpoint requiring Permit2 payment (standard settle, no gas sponsoring)", + "requiresPayment": true, + "protocolFamily": "evm", + "transferMethod": "permit2", + "permit2Direct": true + }, + { + "path": "/exact/evm/permit2-eip2612GasSponsoring", "method": "GET", - "description": "Protected endpoint requiring Permit2 payment", + "description": "Protected endpoint requiring Permit2 payment with EIP-2612 gas sponsoring", "requiresPayment": true, "protocolFamily": "evm", - "permit2": true + "transferMethod": "permit2", + "coldstart": true }, { - "path": "/protected-permit2-erc20", + "path": "/exact/evm/permit2-erc20ApprovalGasSponsoring", "method": "GET", "description": "Protected endpoint requiring Permit2 payment with ERC-20 approval gas sponsoring", "requiresPayment": true, "protocolFamily": "evm", "transferMethod": "permit2", - "extensions": ["erc20ApprovalGasSponsoring"] + "extensions": ["erc20ApprovalGasSponsoring"], + "coldstart": true }, { - "path": "/protected-svm", + "path": "/upto/evm/permit2", + "method": "GET", + "description": "Protected endpoint requiring upto Permit2 payment (direct, client must pre-approve)", + "requiresPayment": true, + "protocolFamily": "evm", + "transferMethod": "upto", + "permit2Direct": true + }, + { + "path": "/upto/evm/permit2-eip2612GasSponsoring", + "method": "GET", + "description": "Protected endpoint requiring Upto Permit2 payment with EIP-2612 gas sponsoring", + "requiresPayment": true, + "protocolFamily": "evm", + "transferMethod": "upto", + "coldstart": true + }, + { + "path": "/upto/evm/permit2-erc20ApprovalGasSponsoring", + "method": "GET", + "description": "Protected endpoint requiring Upto Permit2 payment with ERC-20 approval gas sponsoring", + "requiresPayment": true, + "protocolFamily": "evm", + "transferMethod": "upto", + "extensions": ["erc20ApprovalGasSponsoring"], + "coldstart": true + }, + { + "path": "/exact/svm", "method": "GET", "description": "Protected endpoint requiring payment on SVM network", "requiresPayment": true, "protocolFamily": "svm" }, { - "path": "/protected-aptos", + "path": "/exact/aptos", "method": "GET", "description": "Protected endpoint requiring payment on Aptos network", "requiresPayment": true, "protocolFamily": "aptos" }, + { + "path": "/exact/stellar", + "method": "GET", + "description": "Protected endpoint requiring payment on Stellar network", + "requiresPayment": true, + "protocolFamily": "stellar" + }, { "path": "/health", "method": "GET", @@ -62,14 +105,7 @@ } ], "environment": { - "required": [ - "PORT", - "EVM_PAYEE_ADDRESS", - "SVM_PAYEE_ADDRESS", - "FACILITATOR_URL" - ], - "optional": [ - "APTOS_PAYEE_ADDRESS" - ] + "required": ["PORT", "EVM_PAYEE_ADDRESS", "SVM_PAYEE_ADDRESS", "FACILITATOR_URL"], + "optional": ["APTOS_PAYEE_ADDRESS", "STELLAR_PAYEE_ADDRESS"] } -} \ No newline at end of file +} diff --git a/e2e/servers/mcp-go/go.mod b/e2e/servers/mcp-go/go.mod index ef853a52a3..9fc72ccfb4 100644 --- a/e2e/servers/mcp-go/go.mod +++ b/e2e/servers/mcp-go/go.mod @@ -23,10 +23,10 @@ require ( github.com/holiman/uint256 v1.3.2 // indirect github.com/supranational/blst v0.3.16-0.20250831170142-f48500c1fdbe // indirect github.com/yosida95/uritemplate/v3 v3.0.2 // indirect - golang.org/x/crypto v0.41.0 // indirect + golang.org/x/crypto v0.46.0 // indirect golang.org/x/oauth2 v0.30.0 // indirect - golang.org/x/sync v0.16.0 // indirect - golang.org/x/sys v0.36.0 // indirect + golang.org/x/sync v0.19.0 // indirect + golang.org/x/sys v0.39.0 // indirect ) replace github.com/coinbase/x402/go => ../../../go diff --git a/e2e/servers/mcp-go/go.sum b/e2e/servers/mcp-go/go.sum index f897f19fa1..44cd6d3944 100644 --- a/e2e/servers/mcp-go/go.sum +++ b/e2e/servers/mcp-go/go.sum @@ -40,6 +40,8 @@ github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/jsonschema-go v0.4.2 h1:tmrUohrwoLZZS/P3x7ex0WAVknEkBZM46iALbcqoRA8= github.com/google/jsonschema-go v0.4.2/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/holiman/uint256 v1.3.2 h1:a9EgMPSC1AAaj1SZL5zIQD3WbwTuHrMGOerLjGmM/TA= github.com/holiman/uint256 v1.3.2/go.mod h1:EOMSn4q6Nyt9P6efbI3bueV4e1b3dGlUCXeiRV4ng7E= github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= @@ -72,18 +74,24 @@ github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFA github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI= github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk= github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY= +github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f h1:J9EGpcZtP0E/raorCMxlFGSTBrsSlaDGf3jU/qvAE2c= +github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= +github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0= +github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= +github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74= +github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4= github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4= -golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4= -golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc= +golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU= +golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0= golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI= golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU= -golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= -golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= -golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k= -golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= -golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg= -golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s= +golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= +golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= +golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/tools v0.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ= +golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/e2e/servers/mcp-python/uv.lock b/e2e/servers/mcp-python/uv.lock index 31abc9061e..77e4c907b6 100644 --- a/e2e/servers/mcp-python/uv.lock +++ b/e2e/servers/mcp-python/uv.lock @@ -1,5 +1,5 @@ version = 1 -revision = 3 +revision = 2 requires-python = ">=3.10" [[package]] @@ -2137,7 +2137,7 @@ wheels = [ [[package]] name = "x402" -version = "2.1.0" +version = "2.5.0" source = { editable = "../../../python/x402" } dependencies = [ { name = "nest-asyncio" }, @@ -2160,7 +2160,7 @@ mcp = [ [package.metadata] requires-dist = [ { name = "eth-abi", marker = "extra == 'evm'", specifier = ">=5.0.0" }, - { name = "eth-account", marker = "extra == 'evm'", specifier = ">=0.12.0" }, + { name = "eth-account", marker = "extra == 'evm'", specifier = ">=0.13.0" }, { name = "eth-keys", marker = "extra == 'evm'", specifier = ">=0.5.0" }, { name = "eth-utils", marker = "extra == 'evm'", specifier = ">=4.0.0" }, { name = "fastapi", extras = ["standard"], marker = "extra == 'fastapi'", specifier = ">=0.115.0" }, @@ -2187,7 +2187,7 @@ provides-extras = ["httpx", "requests", "flask", "fastapi", "evm", "svm", "mcp", dev = [ { name = "black", specifier = ">=23.0.0" }, { name = "eth-abi", specifier = ">=5.0.0" }, - { name = "eth-account", specifier = ">=0.12.0" }, + { name = "eth-account", specifier = ">=0.13.0" }, { name = "eth-keys", specifier = ">=0.5.0" }, { name = "eth-utils", specifier = ">=4.0.0" }, { name = "fastapi", extras = ["standard"], specifier = ">=0.115.0" }, diff --git a/e2e/servers/nethttp/README.md b/e2e/servers/nethttp/README.md new file mode 100644 index 0000000000..a5b9e3667f --- /dev/null +++ b/e2e/servers/nethttp/README.md @@ -0,0 +1,204 @@ +# E2E Test Server: net/http (Go) + +This server demonstrates and tests the x402 net/http middleware with both EVM and SVM payment protection. + +## What It Tests + +### Core Functionality +- ✅ **V2 Protocol** - Modern x402 server middleware +- ✅ **Payment Protection** - Middleware protecting specific routes +- ✅ **Multi-chain Support** - EVM and SVM payment acceptance +- ✅ **Facilitator Integration** - HTTP communication with facilitator +- ✅ **Extension Support** - Bazaar discovery metadata +- ✅ **Settlement Handling** - Payment verification and confirmation + +### Protected Endpoints +- ✅ `GET /protected` - Requires EVM payment (USDC on Base Sepolia) +- ✅ `GET /protected-svm` - Requires SVM payment (USDC on Solana Devnet) + +## What It Demonstrates + +### Server Setup + +```go +import ( + "net/http" + x402 "github.com/coinbase/x402/go" + x402http "github.com/coinbase/x402/go/http" + nethttpmw "github.com/coinbase/x402/go/http/nethttp" + evm "github.com/coinbase/x402/go/mechanisms/evm/exact/server" + svm "github.com/coinbase/x402/go/mechanisms/svm/exact/server" + "github.com/coinbase/x402/go/extensions/bazaar" +) + +// Create ServeMux +mux := http.NewServeMux() + +// Define payment routes +routes := x402http.RoutesConfig{ + "GET /protected": { + Accepts: x402http.PaymentOptions{ + { + Scheme: "exact", + Network: "eip155:84532", + PayTo: evmPayeeAddress, + Price: "$0.001", + }, + }, + Extensions: map[string]interface{}{ + "bazaar": discoveryExtension, + }, + }, + "GET /protected-svm": { + Accepts: x402http.PaymentOptions{ + { + Scheme: "exact", + Network: "solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1", + PayTo: svmPayeeAddress, + Price: "$0.001", + }, + }, + Extensions: map[string]interface{}{ + "bazaar": discoveryExtension, + }, + }, +} + +// Define protected endpoints +mux.HandleFunc("GET /protected", func(w http.ResponseWriter, r *http.Request) { + json.NewEncoder(w).Encode(map[string]string{"message": "EVM payment successful!"}) +}) + +mux.HandleFunc("GET /protected-svm", func(w http.ResponseWriter, r *http.Request) { + json.NewEncoder(w).Encode(map[string]string{"message": "SVM payment successful!"}) +}) + +// Apply payment middleware +handler := nethttpmw.X402Payment(nethttpmw.Config{ + Routes: routes, + Facilitator: facilitatorClient, + Schemes: []nethttpmw.SchemeConfig{ + {Network: "eip155:84532", Server: evm.NewExactEvmScheme()}, + {Network: "solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1", Server: svm.NewExactSvmScheme()}, + }, + Timeout: 30 * time.Second, +})(mux) + +http.ListenAndServe(":4021", handler) +``` + +### Key Concepts Shown + +1. **Route Configuration** - Map of route → payment requirements +2. **Multi-Chain Services** - Different services for EVM vs SVM +3. **Facilitator Client** - HTTP client for verification/settlement +4. **Middleware Options** - Functional options pattern +5. **Extension Integration** - Bazaar discovery declarations +6. **Automatic Initialization** - Service initialization on startup + +## Test Scenarios + +This server is tested with: +- **Clients:** TypeScript Fetch, Go HTTP +- **Facilitators:** TypeScript, Go +- **Payment Types:** EVM (Base Sepolia), SVM (Solana Devnet) +- **Protocols:** V2 (primary), V1 (via client negotiation) + +### Request Flow +1. Client makes initial request (no payment) +2. Middleware returns 402 with `PAYMENT-REQUIRED` header +3. Client creates payment payload +4. Client retries with `PAYMENT-SIGNATURE` header +5. Middleware forwards to facilitator for verification +6. Middleware returns protected content + `PAYMENT-RESPONSE` header + +## Running + +```bash +# Via e2e test suite +cd e2e +pnpm test --server=nethttp + +# Direct execution +cd e2e/servers/nethttp +export FACILITATOR_URL="http://localhost:4024" +export EVM_PAYEE_ADDRESS="0x..." +export SVM_PAYEE_ADDRESS="..." +export PORT=4023 +./nethttp +``` + +## Environment Variables + +- `PORT` - HTTP server port (default: 4021) +- `FACILITATOR_URL` - Facilitator endpoint URL +- `EVM_PAYEE_ADDRESS` - Ethereum address to receive payments +- `SVM_PAYEE_ADDRESS` - Solana address to receive payments +- `EVM_NETWORK` - EVM network (default: eip155:84532) +- `SVM_NETWORK` - SVM network (default: solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1) + +## Response Examples + +### 402 Payment Required + +``` +HTTP/1.1 402 Payment Required +PAYMENT-REQUIRED: +Content-Type: application/json + +{ + "error": "Payment required", + "x402Version": 2, + "accepts": [...], + "resource": {...}, + "extensions": { + "bazaar": { + "method": "GET", + "outputExample": {...} + } + } +} +``` + +### 200 Success (After Payment) + +``` +HTTP/1.1 200 OK +PAYMENT-RESPONSE: +Content-Type: application/json + +{ + "message": "Protected endpoint accessed successfully", + "timestamp": "2024-01-01T00:00:00Z" +} +``` + +## Dependencies + +- `github.com/coinbase/x402/go` - Core x402 +- `github.com/coinbase/x402/go/http` - HTTP integration +- `github.com/coinbase/x402/go/http/nethttp` - net/http middleware +- `github.com/coinbase/x402/go/mechanisms/evm` - EVM server +- `github.com/coinbase/x402/go/mechanisms/svm` - SVM server +- `github.com/coinbase/x402/go/extensions/bazaar` - Discovery extension + +## Implementation Highlights + +### Middleware Features +- **Route Matching** - Pattern-based route configuration +- **Payment Requirement Building** - Automatic 402 response generation +- **Facilitator Communication** - HTTP client for verification +- **Settlement Callbacks** - Optional handlers for payment events +- **Extension Support** - Bazaar metadata in responses +- **Timeout Handling** - Configurable facilitator timeouts + +### Service Integration +- **EVM Server** - Base Sepolia USDC +- **SVM Server** - Solana Devnet USDC +- **Initialization** - Fetches supported kinds from facilitator +- **Price Parsing** - Dollar strings → token amounts + +### Bazaar Extension +- **Method Declaration** - GET with output schema +- **Example Output** - Response structure preview +- **Schema Definition** - JSON Schema for validation diff --git a/e2e/servers/nethttp/build.sh b/e2e/servers/nethttp/build.sh new file mode 100755 index 0000000000..6ce0831b72 --- /dev/null +++ b/e2e/servers/nethttp/build.sh @@ -0,0 +1,6 @@ +#!/bin/bash +set -e + +echo "Building net/http server..." +go build -o nethttp . +echo "✅ Build completed: nethttp" diff --git a/e2e/servers/nethttp/go.mod b/e2e/servers/nethttp/go.mod new file mode 100644 index 0000000000..acc801d29a --- /dev/null +++ b/e2e/servers/nethttp/go.mod @@ -0,0 +1,67 @@ +module github.com/coinbase/x402/e2e/servers/nethttp + +go 1.24.0 + +toolchain go1.24.1 + +require ( + github.com/coinbase/x402/go v0.0.0 + github.com/joho/godotenv v1.5.1 +) + +require ( + filippo.io/edwards25519 v1.0.0-rc.1 // indirect + github.com/Microsoft/go-winio v0.6.2 // indirect + github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20251001021608-1fe7b43fc4d6 // indirect + github.com/StackExchange/wmi v1.2.1 // indirect + github.com/andres-erbsen/clock v0.0.0-20160526145045-9e14626cd129 // indirect + github.com/bits-and-blooms/bitset v1.20.0 // indirect + github.com/blendle/zapdriver v1.3.1 // indirect + github.com/consensys/gnark-crypto v0.18.0 // indirect + github.com/crate-crypto/go-eth-kzg v1.4.0 // indirect + github.com/crate-crypto/go-ipa v0.0.0-20240724233137-53bbb0ceb27a // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/deckarep/golang-set/v2 v2.6.0 // indirect + github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 // indirect + github.com/ethereum/c-kzg-4844/v2 v2.1.5 // indirect + github.com/ethereum/go-ethereum v1.16.7 // indirect + github.com/ethereum/go-verkle v0.2.2 // indirect + github.com/fatih/color v1.16.0 // indirect + github.com/gagliardetto/binary v0.8.0 // indirect + github.com/gagliardetto/solana-go v1.14.0 // indirect + github.com/gagliardetto/treeout v0.1.4 // indirect + github.com/go-ole/go-ole v1.3.0 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/gorilla/websocket v1.4.2 // indirect + github.com/holiman/uint256 v1.3.2 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/compress v1.16.0 // indirect + github.com/logrusorgru/aurora v2.0.3+incompatible // indirect + github.com/mattn/go-colorable v0.1.14 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mitchellh/go-testing-interface v1.14.1 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/mostynb/zstdpool-freelist v0.0.0-20201229113212-927304c0c3b1 // indirect + github.com/mr-tron/base58 v1.2.0 // indirect + github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible // indirect + github.com/streamingfast/logging v0.0.0-20230608130331-f22c91403091 // indirect + github.com/supranational/blst v0.3.16-0.20250831170142-f48500c1fdbe // indirect + github.com/tklauser/go-sysconf v0.3.12 // indirect + github.com/tklauser/numcpus v0.6.1 // indirect + github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f // indirect + github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect + github.com/xeipuuv/gojsonschema v1.2.0 // indirect + go.mongodb.org/mongo-driver v1.12.2 // indirect + go.uber.org/atomic v1.7.0 // indirect + go.uber.org/multierr v1.6.0 // indirect + go.uber.org/ratelimit v0.2.0 // indirect + go.uber.org/zap v1.21.0 // indirect + golang.org/x/crypto v0.46.0 // indirect + golang.org/x/sync v0.19.0 // indirect + golang.org/x/sys v0.39.0 // indirect + golang.org/x/term v0.38.0 // indirect + golang.org/x/time v0.14.0 // indirect +) + +replace github.com/coinbase/x402/go => ../../../go diff --git a/e2e/servers/nethttp/go.sum b/e2e/servers/nethttp/go.sum new file mode 100644 index 0000000000..bac78a1035 --- /dev/null +++ b/e2e/servers/nethttp/go.sum @@ -0,0 +1,334 @@ +filippo.io/edwards25519 v1.0.0-rc.1 h1:m0VOOB23frXZvAOK44usCgLWvtsxIoMCTBGJZlpmGfU= +filippo.io/edwards25519 v1.0.0-rc.1/go.mod h1:N1IkdkCkiLB6tki+MYJoSx2JTY9NUlxZE7eHn5EwJns= +github.com/AlekSi/pointer v1.1.0 h1:SSDMPcXD9jSl8FPy9cRzoRaMJtm9g9ggGTxecRUbQoI= +github.com/AlekSi/pointer v1.1.0/go.mod h1:y7BvfRI3wXPWKXEBhU71nbnIEEZX0QTSB2Bj48UJIZE= +github.com/DataDog/zstd v1.4.5 h1:EndNeuB0l9syBZhut0wns3gV1hL8zX8LIu6ZiVHWLIQ= +github.com/DataDog/zstd v1.4.5/go.mod h1:1jcaCB/ufaK+sKp1NBhlGmpz41jOoPQ35bpF36t7BBo= +github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= +github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= +github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20251001021608-1fe7b43fc4d6 h1:1zYrtlhrZ6/b6SAjLSfKzWtdgqK0U+HtH/VcBWh1BaU= +github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20251001021608-1fe7b43fc4d6/go.mod h1:ioLG6R+5bUSO1oeGSDxOV3FADARuMoytZCSX6MEMQkI= +github.com/StackExchange/wmi v1.2.1 h1:VIkavFPXSjcnS+O8yTq7NI32k0R5Aj+v39y29VYDOSA= +github.com/StackExchange/wmi v1.2.1/go.mod h1:rcmrprowKIVzvc+NUiLncP2uuArMWLCbu9SBzvHz7e8= +github.com/VictoriaMetrics/fastcache v1.13.0 h1:AW4mheMR5Vd9FkAPUv+NH6Nhw+fmbTMGMsNAoA/+4G0= +github.com/VictoriaMetrics/fastcache v1.13.0/go.mod h1:hHXhl4DA2fTL2HTZDJFXWgW0LNjo6B+4aj2Wmng3TjU= +github.com/andres-erbsen/clock v0.0.0-20160526145045-9e14626cd129 h1:MzBOUgng9orim59UnfUTLRjMpd09C5uEVQ6RPGeCaVI= +github.com/andres-erbsen/clock v0.0.0-20160526145045-9e14626cd129/go.mod h1:rFgpPQZYZ8vdbc+48xibu8ALc3yeyd64IhHS+PU6Yyg= +github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8= +github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/bits-and-blooms/bitset v1.20.0 h1:2F+rfL86jE2d/bmw7OhqUg2Sj/1rURkBn3MdfoPyRVU= +github.com/bits-and-blooms/bitset v1.20.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8= +github.com/blendle/zapdriver v1.3.1 h1:C3dydBOWYRiOk+B8X9IVZ5IOe+7cl+tGOexN4QqHfpE= +github.com/blendle/zapdriver v1.3.1/go.mod h1:mdXfREi6u5MArG4j9fewC+FGnXaBR+T4Ox4J2u4eHCc= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cockroachdb/errors v1.11.3 h1:5bA+k2Y6r+oz/6Z/RFlNeVCesGARKuC6YymtcDrbC/I= +github.com/cockroachdb/errors v1.11.3/go.mod h1:m4UIW4CDjx+R5cybPsNrRbreomiFqt8o1h1wUVazSd8= +github.com/cockroachdb/fifo v0.0.0-20240606204812-0bbfbd93a7ce h1:giXvy4KSc/6g/esnpM7Geqxka4WSqI1SZc7sMJFd3y4= +github.com/cockroachdb/fifo v0.0.0-20240606204812-0bbfbd93a7ce/go.mod h1:9/y3cnZ5GKakj/H4y9r9GTjCvAFta7KLgSHPJJYc52M= +github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b h1:r6VH0faHjZeQy818SGhaone5OnYfxFR/+AzdY3sf5aE= +github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b/go.mod h1:Vz9DsVWQQhf3vs21MhPMZpMGSht7O/2vFW2xusFUVOs= +github.com/cockroachdb/pebble v1.1.5 h1:5AAWCBWbat0uE0blr8qzufZP5tBjkRyy/jWe1QWLnvw= +github.com/cockroachdb/pebble v1.1.5/go.mod h1:17wO9el1YEigxkP/YtV8NtCivQDgoCyBg5c4VR/eOWo= +github.com/cockroachdb/redact v1.1.5 h1:u1PMllDkdFfPWaNGMyLD1+so+aq3uUItthCFqzwPJ30= +github.com/cockroachdb/redact v1.1.5/go.mod h1:BVNblN9mBWFyMyqK1k3AAiSxhvhfK2oOZZ2lK+dpvRg= +github.com/cockroachdb/tokenbucket v0.0.0-20230807174530-cc333fc44b06 h1:zuQyyAKVxetITBuuhv3BI9cMrmStnpT18zmgmTxunpo= +github.com/cockroachdb/tokenbucket v0.0.0-20230807174530-cc333fc44b06/go.mod h1:7nc4anLGjupUW/PeY5qiNYsdNXj7zopG+eqsS7To5IQ= +github.com/consensys/gnark-crypto v0.18.0 h1:vIye/FqI50VeAr0B3dx+YjeIvmc3LWz4yEfbWBpTUf0= +github.com/consensys/gnark-crypto v0.18.0/go.mod h1:L3mXGFTe1ZN+RSJ+CLjUt9x7PNdx8ubaYfDROyp2Z8c= +github.com/cpuguy83/go-md2man/v2 v2.0.5 h1:ZtcqGrnekaHpVLArFSe4HK5DoKx1T0rq2DwVB0alcyc= +github.com/cpuguy83/go-md2man/v2 v2.0.5/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/crate-crypto/go-eth-kzg v1.4.0 h1:WzDGjHk4gFg6YzV0rJOAsTK4z3Qkz5jd4RE3DAvPFkg= +github.com/crate-crypto/go-eth-kzg v1.4.0/go.mod h1:J9/u5sWfznSObptgfa92Jq8rTswn6ahQWEuiLHOjCUI= +github.com/crate-crypto/go-ipa v0.0.0-20240724233137-53bbb0ceb27a h1:W8mUrRp6NOVl3J+MYp5kPMoUZPp7aOYHtaua31lwRHg= +github.com/crate-crypto/go-ipa v0.0.0-20240724233137-53bbb0ceb27a/go.mod h1:sTwzHBvIzm2RfVCGNEBZgRyjwK40bVoun3ZnGOCafNM= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dchest/siphash v1.2.3 h1:QXwFc8cFOR2dSa/gE6o/HokBMWtLUaNDVd+22aKHeEA= +github.com/dchest/siphash v1.2.3/go.mod h1:0NvQU092bT0ipiFN++/rXm69QG9tVxLAlQHIXMPAkHc= +github.com/deckarep/golang-set/v2 v2.6.0 h1:XfcQbWM1LlMB8BsJ8N9vW5ehnnPVIw0je80NsVHagjM= +github.com/deckarep/golang-set/v2 v2.6.0/go.mod h1:VAky9rY/yGXJOLEDv3OMci+7wtDpOF4IN+y82NBOac4= +github.com/decred/dcrd/crypto/blake256 v1.0.0 h1:/8DMNYp9SGi5f0w7uCm6d6M4OU2rGFK09Y2A4Xv7EE0= +github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 h1:YLtO71vCjJRCBcrPMtQ9nqBsqpA1m5sE92cU+pd5Mcc= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1/go.mod h1:hyedUtir6IdtD/7lIxGeCxkaw7y45JueMRL4DIyJDKs= +github.com/emicklei/dot v1.6.2 h1:08GN+DD79cy/tzN6uLCT84+2Wk9u+wvqP+Hkx/dIR8A= +github.com/emicklei/dot v1.6.2/go.mod h1:DeV7GvQtIw4h2u73RKBkkFdvVAz0D9fzeJrgPW6gy/s= +github.com/ethereum/c-kzg-4844/v2 v2.1.5 h1:aVtoLK5xwJ6c5RiqO8g8ptJ5KU+2Hdquf6G3aXiHh5s= +github.com/ethereum/c-kzg-4844/v2 v2.1.5/go.mod h1:u59hRTTah4Co6i9fDWtiCjTrblJv0UwsqZKCc0GfgUs= +github.com/ethereum/go-bigmodexpfix v0.0.0-20250911101455-f9e208c548ab h1:rvv6MJhy07IMfEKuARQ9TKojGqLVNxQajaXEp/BoqSk= +github.com/ethereum/go-bigmodexpfix v0.0.0-20250911101455-f9e208c548ab/go.mod h1:IuLm4IsPipXKF7CW5Lzf68PIbZ5yl7FFd74l/E0o9A8= +github.com/ethereum/go-ethereum v1.16.7 h1:qeM4TvbrWK0UC0tgkZ7NiRsmBGwsjqc64BHo20U59UQ= +github.com/ethereum/go-ethereum v1.16.7/go.mod h1:Fs6QebQbavneQTYcA39PEKv2+zIjX7rPUZ14DER46wk= +github.com/ethereum/go-verkle v0.2.2 h1:I2W0WjnrFUIzzVPwm8ykY+7pL2d4VhlsePn4j7cnFk8= +github.com/ethereum/go-verkle v0.2.2/go.mod h1:M3b90YRnzqKyyzBEWJGqj8Qff4IDeXnzFw0P9bFw3uk= +github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= +github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= +github.com/ferranbt/fastssz v0.1.4 h1:OCDB+dYDEQDvAgtAGnTSidK1Pe2tW3nFV40XyMkTeDY= +github.com/ferranbt/fastssz v0.1.4/go.mod h1:Ea3+oeoRGGLGm5shYAeDgu6PGUlcvQhE2fILyD9+tGg= +github.com/gagliardetto/binary v0.8.0 h1:U9ahc45v9HW0d15LoN++vIXSJyqR/pWw8DDlhd7zvxg= +github.com/gagliardetto/binary v0.8.0/go.mod h1:2tfj51g5o9dnvsc+fL3Jxr22MuWzYXwx9wEoN0XQ7/c= +github.com/gagliardetto/gofuzz v1.2.2 h1:XL/8qDMzcgvR4+CyRQW9UGdwPRPMHVJfqQ/uMvSUuQw= +github.com/gagliardetto/gofuzz v1.2.2/go.mod h1:bkH/3hYLZrMLbfYWA0pWzXmi5TTRZnu4pMGZBkqMKvY= +github.com/gagliardetto/solana-go v1.14.0 h1:3WfAi70jOOjAJ0deFMjdhFYlLXATF4tOQXsDNWJtOLw= +github.com/gagliardetto/solana-go v1.14.0/go.mod h1:l/qqqIN6qJJPtxW/G1PF4JtcE3Zg2vD2EliZrr9Gn5k= +github.com/gagliardetto/treeout v0.1.4 h1:ozeYerrLCmCubo1TcIjFiOWTTGteOOHND1twdFpgwaw= +github.com/gagliardetto/treeout v0.1.4/go.mod h1:loUefvXTrlRG5rYmJmExNryyBRh8f89VZhmMOyCyqok= +github.com/getsentry/sentry-go v0.27.0 h1:Pv98CIbtB3LkMWmXi4Joa5OOcwbmnX88sF5qbK3r3Ps= +github.com/getsentry/sentry-go v0.27.0/go.mod h1:lc76E2QywIyW8WuBnwl8Lc4bkmQH4+w1gwTf25trprY= +github.com/go-ole/go-ole v1.2.5/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= +github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= +github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= +github.com/gofrs/flock v0.12.1 h1:MTLVXXHf8ekldpJk3AKicLij9MdwOWkZ+a/jHHZby9E= +github.com/gofrs/flock v0.12.1/go.mod h1:9zxTsyu5xtJ9DK+1tFZyibEV7y3uwDxPPfbxeeHCoD0= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI= +github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs= +github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/go-cmp v0.5.2 h1:X2ev0eStA3AbceY54o37/0PQ/UWqKEiiO2dKL5OPaFM= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= +github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= +github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/hashicorp/go-bexpr v0.1.10 h1:9kuI5PFotCboP3dkDYFr/wi0gg0QVbSNz5oFRpxn4uE= +github.com/hashicorp/go-bexpr v0.1.10/go.mod h1:oxlubA2vC/gFVfX1A6JGp7ls7uCDlfJn732ehYYg+g0= +github.com/holiman/billy v0.0.0-20250707135307-f2f9b9aae7db h1:IZUYC/xb3giYwBLMnr8d0TGTzPKFGNTCGgGLoyeX330= +github.com/holiman/billy v0.0.0-20250707135307-f2f9b9aae7db/go.mod h1:xTEYN9KCHxuYHs+NmrmzFcnvHMzLLNiGFafCb1n3Mfg= +github.com/holiman/bloomfilter/v2 v2.0.3 h1:73e0e/V0tCydx14a0SCYS/EWCxgwLZ18CZcZKVu0fao= +github.com/holiman/bloomfilter/v2 v2.0.3/go.mod h1:zpoh+gs7qcpqrHr3dB55AMiJwo0iURXE7ZOP9L9hSkA= +github.com/holiman/uint256 v1.3.2 h1:a9EgMPSC1AAaj1SZL5zIQD3WbwTuHrMGOerLjGmM/TA= +github.com/holiman/uint256 v1.3.2/go.mod h1:EOMSn4q6Nyt9P6efbI3bueV4e1b3dGlUCXeiRV4ng7E= +github.com/huin/goupnp v1.3.0 h1:UvLUlWDNpoUdYzb2TCn+MuTWtcjXKSza2n6CBdQ0xXc= +github.com/huin/goupnp v1.3.0/go.mod h1:gnGPsThkYa7bFi/KWmEysQRf48l2dvR5bxr2OFckNX8= +github.com/jackpal/go-nat-pmp v1.0.2 h1:KzKSgb7qkJvOUTqYl9/Hg/me3pWgBmERKrTGD7BdWus= +github.com/jackpal/go-nat-pmp v1.0.2/go.mod h1:QPH045xvCAeXUZOxsnwmrtiCoxIr9eob+4orBN1SBKc= +github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= +github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/klauspost/compress v1.11.4/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= +github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= +github.com/klauspost/compress v1.16.0 h1:iULayQNOReoYUe+1qtKOqw9CwJv3aNQu8ivo7lw1HU4= +github.com/klauspost/compress v1.16.0/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= +github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= +github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/leanovate/gopter v0.2.11 h1:vRjThO1EKPb/1NsDXuDrzldR28RLkBflWYcU9CvzWu4= +github.com/leanovate/gopter v0.2.11/go.mod h1:aK3tzZP/C+p1m3SPRE4SYZFGP7jjkuSI4f7Xvpt0S9c= +github.com/logrusorgru/aurora v2.0.3+incompatible h1:tOpm7WcpBTn4fjmVfgpQq0EfczGlG91VSDkswnjF5A8= +github.com/logrusorgru/aurora v2.0.3+incompatible/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4= +github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= +github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU= +github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo= +github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= +github.com/minio/sha256-simd v1.0.0 h1:v1ta+49hkWZyvaKwrQB8elexRqm6Y0aMLjCNsrYxo6g= +github.com/minio/sha256-simd v1.0.0/go.mod h1:OuYzVNI5vcoYIAmbIvHPl3N3jUzVedXbKy5RFepssQM= +github.com/mitchellh/go-testing-interface v1.14.1 h1:jrgshOhYAUVNMAJiKbEu7EqAwgJJ2JqpQmpLJOu07cU= +github.com/mitchellh/go-testing-interface v1.14.1/go.mod h1:gfgS7OtZj6MA4U1UrDRp04twqAjfvlZyCfX3sDjEym8= +github.com/mitchellh/mapstructure v1.4.1 h1:CpVNEelQCZBooIPDn+AR3NpivK/TIKU8bDxdASFVQag= +github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/mitchellh/pointerstructure v1.2.0 h1:O+i9nHnXS3l/9Wu7r4NrEdwA2VFTicjUEN1uBnDo34A= +github.com/mitchellh/pointerstructure v1.2.0/go.mod h1:BRAsLI5zgXmw97Lf6s25bs8ohIXc3tViBH44KcwB2g4= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc= +github.com/mostynb/zstdpool-freelist v0.0.0-20201229113212-927304c0c3b1 h1:mPMvm6X6tf4w8y7j9YIt6V9jfWhL6QlbEc7CCmeQlWk= +github.com/mostynb/zstdpool-freelist v0.0.0-20201229113212-927304c0c3b1/go.mod h1:ye2e/VUEtE2BHE+G/QcKkcLQVAEJoYRFj5VUOQatCRE= +github.com/mr-tron/base58 v1.2.0 h1:T/HDJBh4ZCPbU39/+c3rRvE0uKBQlU27+QI8LJ4t64o= +github.com/mr-tron/base58 v1.2.0/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc= +github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= +github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= +github.com/onsi/gomega v1.10.1 h1:o0+MgICZLuZ7xjH7Vx6zS/zcu93/BEp1VwkIW1mEXCE= +github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= +github.com/pion/dtls/v2 v2.2.7 h1:cSUBsETxepsCSFSxC3mc/aDo14qQLMSL+O6IjG28yV8= +github.com/pion/dtls/v2 v2.2.7/go.mod h1:8WiMkebSHFD0T+dIU+UeBaoV7kDhOW5oDCzZ7WZ/F9s= +github.com/pion/logging v0.2.2 h1:M9+AIj/+pxNsDfAT64+MAVgJO0rsyLnoJKCqf//DoeY= +github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms= +github.com/pion/stun/v2 v2.0.0 h1:A5+wXKLAypxQri59+tmQKVs7+l6mMM+3d+eER9ifRU0= +github.com/pion/stun/v2 v2.0.0/go.mod h1:22qRSh08fSEttYUmJZGlriq9+03jtVmXNODgLccj8GQ= +github.com/pion/transport/v2 v2.2.1 h1:7qYnCBlpgSJNYMbLCKuSY9KbQdBFoETvPNETv0y4N7c= +github.com/pion/transport/v2 v2.2.1/go.mod h1:cXXWavvCnFF6McHTft3DWS9iic2Mftcz1Aq29pGcU5g= +github.com/pion/transport/v3 v3.0.1 h1:gDTlPJwROfSfz6QfSi0ZmeCSkFcnWWiiR9ES0ouANiM= +github.com/pion/transport/v3 v3.0.1/go.mod h1:UY7kiITrlMv7/IKgd5eTUcaahZx5oUN3l9SzK5f5xE0= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v1.15.0 h1:5fCgGYogn0hFdhyhLbw7hEsWxufKtY9klyvdNfFlFhM= +github.com/prometheus/client_golang v1.15.0/go.mod h1:e9yaBhRPU2pPNsZwE+JdQl0KEt1N9XgF6zxWmaC0xOk= +github.com/prometheus/client_model v0.3.0 h1:UBgGFHqYdG/TPFD1B1ogZywDqEkwp3fBMvqdiQ7Xew4= +github.com/prometheus/client_model v0.3.0/go.mod h1:LDGWKZIo7rky3hgvBe+caln+Dr3dPggB5dvjtD7w9+w= +github.com/prometheus/common v0.42.0 h1:EKsfXEYo4JpWMHH5cg+KOUWeuJSov1Id8zGR8eeI1YM= +github.com/prometheus/common v0.42.0/go.mod h1:xBwqVerjNdUDjgODMpudtOMwlOwf2SaTr1yjz4b7Zbc= +github.com/prometheus/procfs v0.9.0 h1:wzCHvIvM5SxWqYvwgVL7yJY8Lz3PKn49KQtpgMYJfhI= +github.com/prometheus/procfs v0.9.0/go.mod h1:+pB4zwohETzFnmlpe6yd2lSc+0/46IYZRB/chUwxUZY= +github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= +github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= +github.com/rs/cors v1.7.0 h1:+88SsELBHx5r+hZ8TCkggzSstaWNbDvThkVK8H6f9ik= +github.com/rs/cors v1.7.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU= +github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible h1:Bn1aCHHRnjv4Bl16T8rcaFjYSrGrIZvpiGO6P3Q4GpU= +github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA= +github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8= +github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= +github.com/streamingfast/logging v0.0.0-20230608130331-f22c91403091 h1:RN5mrigyirb8anBEtdjtHFIufXdacyTi6i4KBfeNXeo= +github.com/streamingfast/logging v0.0.0-20230608130331-f22c91403091/go.mod h1:VlduQ80JcGJSargkRU4Sg9Xo63wZD/l8A5NC/Uo1/uU= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/supranational/blst v0.3.16-0.20250831170142-f48500c1fdbe h1:nbdqkIGOGfUAD54q1s2YBcBz/WcsxCO9HUQ4aGV5hUw= +github.com/supranational/blst v0.3.16-0.20250831170142-f48500c1fdbe/go.mod h1:jZJtfjgudtNl4en1tzwPIV3KjUnQUvG3/j+w+fVonLw= +github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 h1:epCh84lMvA70Z7CTTCmYQn2CKbY8j86K7/FAIr141uY= +github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45IWZaF22tdD+VEXcAWRA037jwmWEB5VWYORlTpc= +github.com/test-go/testify v1.1.4 h1:Tf9lntrKUMHiXQ07qBScBTSA0dhYQlu83hswqelv1iE= +github.com/test-go/testify v1.1.4/go.mod h1:rH7cfJo/47vWGdi4GPj16x3/t1xGOj2YxzmNQzk2ghU= +github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU= +github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI= +github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk= +github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY= +github.com/urfave/cli/v2 v2.27.5 h1:WoHEJLdsXr6dDWoJgMq/CboDmyY/8HMMH1fTECbih+w= +github.com/urfave/cli/v2 v2.27.5/go.mod h1:3Sevf16NykTbInEnD0yKkjDAeZDS0A6bzhBH5hrMvTQ= +github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= +github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4= +github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM= +github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f h1:J9EGpcZtP0E/raorCMxlFGSTBrsSlaDGf3jU/qvAE2c= +github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= +github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0= +github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= +github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74= +github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= +github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4= +github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM= +github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA= +github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +go.mongodb.org/mongo-driver v1.12.2 h1:gbWY1bJkkmUB9jjZzcdhOL8O85N9H+Vvsf2yFN0RDws= +go.mongodb.org/mongo-driver v1.12.2/go.mod h1:/rGBTebI3XYboVmgz+Wv3Bcbl3aD0QF9zl6kDDw18rQ= +go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw= +go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/goleak v1.1.11/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= +go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4= +go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= +go.uber.org/ratelimit v0.2.0 h1:UQE2Bgi7p2B85uP5dC2bbRtig0C+OeNRnNEafLjsLPA= +go.uber.org/ratelimit v0.2.0/go.mod h1:YYBV4e4naJvhpitQrWJu1vCpgB7CboMe0qhltKt6mUg= +go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= +go.uber.org/zap v1.21.0 h1:WefMeulhovoZ2sYXz7st6K0sLj7bBhpiFaud4r4zST8= +go.uber.org/zap v1.21.0/go.mod h1:wjWOCqI0f2ZZrJF/UufIOkiC8ii6tm1iqIsLo76RfJw= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20220214200702-86341886e292/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU= +golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0= +golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df h1:UA2aFVmmsIlefxMk29Dp2juaUSth8Pyn3Tq5Y5mJGME= +golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU= +golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= +golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= +golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q= +golang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU= +golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY= +golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= +golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw= +google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc= +gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/e2e/servers/nethttp/install.sh b/e2e/servers/nethttp/install.sh new file mode 100755 index 0000000000..5043a56808 --- /dev/null +++ b/e2e/servers/nethttp/install.sh @@ -0,0 +1,6 @@ +#!/bin/bash +set -e + +echo "Installing Go dependencies for net/http server..." +go mod tidy +echo "✅ Dependencies installed" diff --git a/e2e/servers/nethttp/main.go b/e2e/servers/nethttp/main.go new file mode 100644 index 0000000000..f3d13206e4 --- /dev/null +++ b/e2e/servers/nethttp/main.go @@ -0,0 +1,460 @@ +package main + +import ( + "encoding/json" + "fmt" + "net/http" + "os" + "os/signal" + "syscall" + "time" + + x402 "github.com/coinbase/x402/go" + "github.com/coinbase/x402/go/extensions/bazaar" + "github.com/coinbase/x402/go/extensions/eip2612gassponsor" + "github.com/coinbase/x402/go/extensions/erc20approvalgassponsor" + "github.com/coinbase/x402/go/extensions/types" + x402http "github.com/coinbase/x402/go/http" + nethttpmw "github.com/coinbase/x402/go/http/nethttp" + exactevm "github.com/coinbase/x402/go/mechanisms/evm/exact/server" + uptoevm "github.com/coinbase/x402/go/mechanisms/evm/upto/server" + svm "github.com/coinbase/x402/go/mechanisms/svm/exact/server" + "github.com/joho/godotenv" +) + +var shutdownRequested bool + +// net/http E2E Test Server with x402 v2 Payment Middleware +// +// This server demonstrates how to integrate x402 v2 payment middleware +// with a standard net/http application for end-to-end testing. + +func main() { + // Load .env file if it exists + if err := godotenv.Load(); err != nil { + fmt.Println("Warning: .env file not found. Using environment variables.") + } + + // Get configuration from environment + port := os.Getenv("PORT") + if port == "" { + port = "4021" + } + + evmPayeeAddress := os.Getenv("EVM_PAYEE_ADDRESS") + if evmPayeeAddress == "" { + fmt.Println("❌ EVM_PAYEE_ADDRESS environment variable is required") + os.Exit(1) + } + + svmPayeeAddress := os.Getenv("SVM_PAYEE_ADDRESS") + if svmPayeeAddress == "" { + fmt.Println("❌ SVM_PAYEE_ADDRESS environment variable is required") + os.Exit(1) + } + + facilitatorURL := os.Getenv("FACILITATOR_URL") + if facilitatorURL == "" { + fmt.Println("❌ FACILITATOR_URL environment variable is required") + os.Exit(1) + } + + // Network configurations (from env or defaults) + evmNetworkStr := os.Getenv("EVM_NETWORK") + if evmNetworkStr == "" { + evmNetworkStr = "eip155:84532" // Default: Base Sepolia + } + svmNetworkStr := os.Getenv("SVM_NETWORK") + if svmNetworkStr == "" { + svmNetworkStr = "solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1" // Default: Solana Devnet + } + evmNetwork := x402.Network(evmNetworkStr) + svmNetwork := x402.Network(svmNetworkStr) + + evmPermit2Asset := os.Getenv("EVM_PERMIT2_ASSET") + if evmPermit2Asset == "" { + evmPermit2Asset = "0x036CbD53842c5426634e7929541eC2318f3dCF7e" + } + + fmt.Printf("EVM Payee address: %s\n", evmPayeeAddress) + fmt.Printf("SVM Payee address: %s\n", svmPayeeAddress) + fmt.Printf("Using remote facilitator at: %s\n", facilitatorURL) + + // Create HTTP facilitator client + facilitatorClient := x402http.NewHTTPFacilitatorClient(&x402http.FacilitatorConfig{ + URL: facilitatorURL, + }) + + // Configure x402 payment middleware + // + // This middleware protects /exact/* payment routes with USDC payment requirements + // on the Base Sepolia testnet with bazaar discovery extension. + + // Declare bazaar discovery extension for GET endpoints + discoveryExtension, err := bazaar.DeclareDiscoveryExtension( + bazaar.MethodGET, + nil, // No query params + nil, // No input schema + "", // No body type (GET method) + &types.OutputConfig{ + Example: map[string]interface{}{ + "message": "Protected endpoint accessed successfully", + "timestamp": "2024-01-01T00:00:00Z", + }, + Schema: types.JSONSchema{ + "properties": map[string]interface{}{ + "message": map[string]interface{}{"type": "string"}, + "timestamp": map[string]interface{}{"type": "string"}, + }, + "required": []string{"message", "timestamp"}, + }, + }, + ) + if err != nil { + fmt.Printf("Warning: Failed to create bazaar extension: %v\n", err) + } + + routes := x402http.RoutesConfig{ + "GET /exact/evm/eip3009": { + Accepts: x402http.PaymentOptions{ + { + Scheme: "exact", + PayTo: evmPayeeAddress, + Price: "$0.001", + Network: evmNetwork, + }, + }, + Extensions: map[string]interface{}{ + types.BAZAAR.Key(): discoveryExtension, + }, + }, + "GET /exact/svm": { + Accepts: x402http.PaymentOptions{ + { + Scheme: "exact", + PayTo: svmPayeeAddress, + Price: "$0.001", + Network: svmNetwork, + }, + }, + Extensions: map[string]interface{}{ + types.BAZAAR.Key(): discoveryExtension, + }, + }, + "GET /exact/evm/permit2": { + Accepts: x402http.PaymentOptions{ + { + Scheme: "exact", + PayTo: evmPayeeAddress, + Network: evmNetwork, + Price: map[string]interface{}{ + "amount": "1000", + "asset": "0x036CbD53842c5426634e7929541eC2318f3dCF7e", + "extra": map[string]interface{}{ + "assetTransferMethod": "permit2", + }, + }, + }, + }, + Extensions: map[string]interface{}{ + types.BAZAAR.Key(): discoveryExtension, + }, + }, + "GET /exact/evm/permit2-eip2612GasSponsoring": { + Accepts: x402http.PaymentOptions{ + { + Scheme: "exact", + PayTo: evmPayeeAddress, + Network: evmNetwork, + Price: map[string]interface{}{ + "amount": "1000", + "asset": evmPermit2Asset, + "extra": func() map[string]interface{} { + name := "USD Coin" + if evmNetworkStr == "eip155:84532" { + name = "USDC" + } + return map[string]interface{}{ + "assetTransferMethod": "permit2", + "name": name, + "version": "2", + } + }(), + }, + }, + }, + Extensions: func() map[string]interface{} { + ext := map[string]interface{}{ + types.BAZAAR.Key(): discoveryExtension, + } + for k, v := range eip2612gassponsor.DeclareEip2612GasSponsoringExtension() { + ext[k] = v + } + return ext + }(), + }, + "GET /upto/evm/permit2": { + Accepts: x402http.PaymentOptions{ + { + Scheme: "upto", + PayTo: evmPayeeAddress, + Network: evmNetwork, + Price: map[string]interface{}{ + "amount": "2000", + "asset": evmPermit2Asset, + "extra": map[string]interface{}{ + "assetTransferMethod": "permit2", + "name": "USDC", + "version": "2", + }, + }, + }, + }, + Extensions: func() map[string]interface{} { + ext := map[string]interface{}{ + types.BAZAAR.Key(): discoveryExtension, + } + for k, v := range eip2612gassponsor.DeclareEip2612GasSponsoringExtension() { + ext[k] = v + } + return ext + }(), + }, + "GET /exact/evm/permit2-erc20ApprovalGasSponsoring": { + Accepts: x402http.PaymentOptions{ + { + Scheme: "exact", + PayTo: evmPayeeAddress, + Network: evmNetwork, + Price: map[string]interface{}{ + "amount": "1000", + "asset": evmPermit2Asset, + "extra": map[string]interface{}{ + "assetTransferMethod": "permit2", + }, + }, + }, + }, + Extensions: func() map[string]interface{} { + ext := map[string]interface{}{ + types.BAZAAR.Key(): discoveryExtension, + } + for k, v := range erc20approvalgassponsor.DeclareExtension() { + ext[k] = v + } + return ext + }(), + }, + } + + // Create ServeMux and register handlers + mux := http.NewServeMux() + + // Protected endpoint - requires payment to access + mux.HandleFunc("GET /exact/evm/eip3009", func(w http.ResponseWriter, r *http.Request) { + if shutdownRequested { + writeJSON(w, http.StatusServiceUnavailable, map[string]interface{}{ + "error": "Server shutting down", + }) + return + } + + writeJSON(w, http.StatusOK, map[string]interface{}{ + "message": "Protected endpoint accessed successfully (EVM)", + "timestamp": time.Now().Format(time.RFC3339), + "network": "eip155:84532", + }) + }) + + // Protected SVM endpoint - requires payment to access + mux.HandleFunc("GET /exact/svm", func(w http.ResponseWriter, r *http.Request) { + if shutdownRequested { + writeJSON(w, http.StatusServiceUnavailable, map[string]interface{}{ + "error": "Server shutting down", + }) + return + } + + writeJSON(w, http.StatusOK, map[string]interface{}{ + "message": "Protected endpoint accessed successfully (SVM)", + "timestamp": time.Now().Format(time.RFC3339), + "network": "solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1", + }) + }) + + // Protected Permit2 direct endpoint - standard settle (no gas sponsoring) + mux.HandleFunc("GET /exact/evm/permit2", func(w http.ResponseWriter, r *http.Request) { + if shutdownRequested { + writeJSON(w, http.StatusServiceUnavailable, map[string]interface{}{ + "error": "Server shutting down", + }) + return + } + + writeJSON(w, http.StatusOK, map[string]interface{}{ + "message": "Permit2 endpoint accessed successfully", + "timestamp": time.Now().Format(time.RFC3339), + "method": "permit2", + }) + }) + + // Protected Permit2 EIP-2612 endpoint - Permit2 with gas sponsoring + mux.HandleFunc("GET /exact/evm/permit2-eip2612GasSponsoring", func(w http.ResponseWriter, r *http.Request) { + if shutdownRequested { + writeJSON(w, http.StatusServiceUnavailable, map[string]interface{}{ + "error": "Server shutting down", + }) + return + } + + writeJSON(w, http.StatusOK, map[string]interface{}{ + "message": "Permit2 EIP-2612 endpoint accessed successfully", + "timestamp": time.Now().Format(time.RFC3339), + "method": "permit2-eip2612", + }) + }) + + // Protected Permit2 ERC-20 approval endpoint + mux.HandleFunc("GET /exact/evm/permit2-erc20ApprovalGasSponsoring", func(w http.ResponseWriter, r *http.Request) { + if shutdownRequested { + writeJSON(w, http.StatusServiceUnavailable, map[string]interface{}{ + "error": "Server shutting down", + }) + return + } + + writeJSON(w, http.StatusOK, map[string]interface{}{ + "message": "Permit2 ERC-20 approval endpoint accessed successfully", + "timestamp": time.Now().Format(time.RFC3339), + "method": "permit2-erc20-approval", + }) + }) + + mux.HandleFunc("GET /upto/evm/permit2", func(w http.ResponseWriter, r *http.Request) { + if shutdownRequested { + writeJSON(w, http.StatusServiceUnavailable, map[string]interface{}{ + "error": "Server shutting down", + }) + return + } + + nethttpmw.SetSettlementOverrides(w, &x402.SettlementOverrides{Amount: "1000"}) + + writeJSON(w, http.StatusOK, map[string]interface{}{ + "message": "Upto Permit2 endpoint accessed successfully", + "timestamp": time.Now().Format(time.RFC3339), + "method": "upto-permit2", + }) + }) + + // Health check endpoint - no payment required + mux.HandleFunc("GET /health", func(w http.ResponseWriter, r *http.Request) { + writeJSON(w, http.StatusOK, map[string]interface{}{ + "status": "ok", + "version": "2.0.0", + "evm_network": string(evmNetwork), + "evm_payee": evmPayeeAddress, + "svm_network": string(svmNetwork), + "svm_payee": svmPayeeAddress, + }) + }) + + // Shutdown endpoint - used by e2e tests + mux.HandleFunc("POST /close", func(w http.ResponseWriter, r *http.Request) { + shutdownRequested = true + + writeJSON(w, http.StatusOK, map[string]interface{}{ + "message": "Server shutting down gracefully", + }) + fmt.Println("Received shutdown request") + + // Schedule server shutdown after response + go func() { + time.Sleep(100 * time.Millisecond) + os.Exit(0) + }() + }) + + // Apply payment middleware with detailed error logging + handler := nethttpmw.X402Payment(nethttpmw.Config{ + Routes: routes, + Facilitator: facilitatorClient, + Schemes: []nethttpmw.SchemeConfig{ + {Network: evmNetwork, Server: exactevm.NewExactEvmScheme()}, + {Network: evmNetwork, Server: uptoevm.NewUptoEvmScheme()}, + {Network: svmNetwork, Server: svm.NewExactSvmScheme()}, + }, + SyncFacilitatorOnStart: true, + Timeout: 30 * time.Second, + ErrorHandler: func(w http.ResponseWriter, r *http.Request, err error) { + // Log detailed error information for debugging + fmt.Printf("❌ [E2E SERVER ERROR] Payment error occurred\n") + fmt.Printf(" Path: %s\n", r.URL.Path) + fmt.Printf(" Method: %s\n", r.Method) + fmt.Printf(" Error: %v\n", err) + fmt.Printf(" Headers: %v\n", r.Header) + + // Default error response + writeJSON(w, http.StatusPaymentRequired, map[string]interface{}{ + "error": err.Error(), + }) + }, + SettlementHandler: func(w http.ResponseWriter, r *http.Request, settleResp *x402.SettleResponse) { + // Log successful settlement + fmt.Printf("✅ [E2E SERVER SUCCESS] Payment settled\n") + fmt.Printf(" Path: %s\n", r.URL.Path) + fmt.Printf(" Transaction: %s\n", settleResp.Transaction) + fmt.Printf(" Network: %s\n", settleResp.Network) + fmt.Printf(" Payer: %s\n", settleResp.Payer) + }, + })(mux) + + // Set up graceful shutdown + quit := make(chan os.Signal, 1) + signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) + + go func() { + <-quit + fmt.Println("Received shutdown signal, exiting...") + os.Exit(0) + }() + + // Print startup banner + fmt.Printf(` +╔════════════════════════════════════════════════════════╗ +║ x402 net/http E2E Test Server ║ +╠════════════════════════════════════════════════════════╣ +║ Server: http://localhost:%-29s ║ +║ EVM Network: %-40s ║ +║ EVM Payee: %-40s ║ +║ SVM Network: %-40s ║ +║ SVM Payee: %-40s ║ +║ ║ +║ Endpoints: ║ +║ • GET /exact/evm/eip3009 (EVM EIP-3009) ║ +║ • GET /exact/evm/permit2 (Permit2) ║ +║ • GET /exact/evm/permit2-eip2612GasSponsoring ║ +║ • GET /exact/evm/permit2-erc20ApprovalGasSponsoring ║ +║ • GET /exact/svm (SVM) ║ +║ • GET /health (no payment required) ║ +║ • POST /close (shutdown server) ║ +╚════════════════════════════════════════════════════════╝ +`, port, evmNetwork, evmPayeeAddress, svmNetwork, svmPayeeAddress) + + server := &http.Server{ + Addr: ":" + port, + Handler: handler, + } + + if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed { + fmt.Printf("Error starting server: %v\n", err) + os.Exit(1) + } +} + +// writeJSON is a helper to write JSON responses. +func writeJSON(w http.ResponseWriter, status int, data interface{}) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(status) + _ = json.NewEncoder(w).Encode(data) +} diff --git a/e2e/servers/nethttp/run.sh b/e2e/servers/nethttp/run.sh new file mode 100755 index 0000000000..5f2f5f8501 --- /dev/null +++ b/e2e/servers/nethttp/run.sh @@ -0,0 +1,2 @@ +#!/bin/bash +go run main.go diff --git a/e2e/servers/nethttp/test.config.json b/e2e/servers/nethttp/test.config.json new file mode 100644 index 0000000000..a77eb5bbb6 --- /dev/null +++ b/e2e/servers/nethttp/test.config.json @@ -0,0 +1,88 @@ +{ + "name": "nethttp", + "type": "server", + "language": "go", + "x402Version": 2, + "extensions": [ + "bazaar", + "eip2612GasSponsoring", + "erc20ApprovalGasSponsoring" + ], + "evm": { "transferMethods": ["eip3009", "permit2"] }, + "description": "Go net/http server with x402 v2 payment middleware", + "endpoints": [ + { + "path": "/exact/evm/eip3009", + "method": "GET", + "description": "Protected endpoint requiring EIP-3009 payment", + "requiresPayment": true, + "protocolFamily": "evm", + "transferMethod": "eip3009" + }, + { + "path": "/exact/evm/permit2", + "method": "GET", + "description": "Protected endpoint requiring Permit2 payment (standard settle, no gas sponsoring)", + "requiresPayment": true, + "protocolFamily": "evm", + "transferMethod": "permit2", + "permit2Direct": true + }, + { + "path": "/exact/evm/permit2-eip2612GasSponsoring", + "method": "GET", + "description": "Protected endpoint requiring Permit2 payment with EIP-2612 gas sponsoring", + "requiresPayment": true, + "protocolFamily": "evm", + "transferMethod": "permit2", + "coldstart": true + }, + { + "path": "/exact/evm/permit2-erc20ApprovalGasSponsoring", + "method": "GET", + "description": "Protected endpoint requiring Permit2 payment with ERC-20 approval gas sponsoring", + "requiresPayment": true, + "protocolFamily": "evm", + "transferMethod": "permit2", + "extensions": ["erc20ApprovalGasSponsoring"], + "coldstart": true + }, + { + "path": "/upto/evm/permit2", + "method": "GET", + "description": "Protected endpoint requiring upto Permit2 payment (usage-based settlement)", + "requiresPayment": true, + "protocolFamily": "evm", + "transferMethod": "upto", + "permit2Direct": true + }, + { + "path": "/exact/svm", + "method": "GET", + "description": "Protected endpoint requiring payment (SVM)", + "requiresPayment": true, + "protocolFamily": "svm" + }, + { + "path": "/health", + "method": "GET", + "description": "Health check endpoint", + "health": true + }, + { + "path": "/close", + "method": "POST", + "description": "Graceful shutdown endpoint", + "close": true + } + ], + "environment": { + "required": [ + "PORT", + "EVM_PAYEE_ADDRESS", + "SVM_PAYEE_ADDRESS", + "FACILITATOR_URL" + ], + "optional": [] + } +} diff --git a/e2e/servers/next/.env-local b/e2e/servers/next/.env-local index 922c516362..4fb43f9906 100644 --- a/e2e/servers/next/.env-local +++ b/e2e/servers/next/.env-local @@ -1,3 +1,5 @@ -NEXT_PUBLIC_FACILITATOR_URL=https://x402.org/facilitator -NETWORK=base-sepolia -RESOURCE_WALLET_ADDRESS= +PORT=3000 +EVM_PAYEE_ADDRESS= +SVM_PAYEE_ADDRESS= +STELLAR_PAYEE_ADDRESS= +FACILITATOR_URL=http://localhost:4000 diff --git a/e2e/servers/next/README.md b/e2e/servers/next/README.md index 87fb5e128a..5992b03e2d 100644 --- a/e2e/servers/next/README.md +++ b/e2e/servers/next/README.md @@ -1,16 +1,18 @@ # x402-next Example App -This is a Next.js application that demonstrates how to use the `x402-next` middleware to implement paywall functionality in your Next.js routes. +This is a Next.js application that demonstrates how to use the `x402-next` middleware to implement paywall functionality in your Next.js routes with EVM, SVM, and optional Stellar payment support. ## Prerequisites - Node.js v20+ (install via [nvm](https://github.com/nvm-sh/nvm)) - pnpm v10 (install via [pnpm.io/installation](https://pnpm.io/installation)) - A valid Ethereum address for receiving payments +- A valid Solana address for receiving payments +- (Optional) A valid Stellar address for receiving payments ## Setup -1. Copy `.env.local` to `.env` and add your Ethereum address to receive payments: +1. Copy `.env.local` to `.env` and add your addresses to receive payments: ```bash cp .env.local .env @@ -125,3 +127,17 @@ export const config = { matcher: ["/protected/:path*", "/api/premium/:path*"], }; ``` + +## Environment Variables + +### Required +- `PORT` - HTTP server port +- `EVM_PAYEE_ADDRESS` - Ethereum address to receive payments +- `SVM_PAYEE_ADDRESS` - Solana address to receive payments +- `FACILITATOR_URL` - Facilitator endpoint URL + +### Optional +- `STELLAR_PAYEE_ADDRESS` - Stellar address to receive payments - enables Stellar endpoints +- `EVM_NETWORK` - EVM network (default: eip155:84532) +- `SVM_NETWORK` - SVM network (default: solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1) +- `STELLAR_NETWORK` - Stellar network (default: stellar:testnet) diff --git a/e2e/servers/next/app/api/close/route.ts b/e2e/servers/next/app/api/close/route.ts index d2132c17c3..94835fd216 100644 --- a/e2e/servers/next/app/api/close/route.ts +++ b/e2e/servers/next/app/api/close/route.ts @@ -20,4 +20,4 @@ export async function POST() { return NextResponse.json({ message: "Shutting down gracefully", }); -} \ No newline at end of file +} diff --git a/e2e/servers/next/app/api/protected-proxy/route.ts b/e2e/servers/next/app/api/exact/aptos/route.ts similarity index 66% rename from e2e/servers/next/app/api/protected-proxy/route.ts rename to e2e/servers/next/app/api/exact/aptos/route.ts index 739c214d86..95caf09527 100644 --- a/e2e/servers/next/app/api/protected-proxy/route.ts +++ b/e2e/servers/next/app/api/exact/aptos/route.ts @@ -1,17 +1,13 @@ import { NextResponse } from "next/server"; /** - * Protected endpoint requiring payment (proxy middleware) + * Aptos endpoint requiring payment (proxy middleware) */ export const runtime = "nodejs"; -/** - * Protected endpoint requiring payment (proxy middleware) - */ export async function GET() { return NextResponse.json({ message: "Protected endpoint accessed successfully", timestamp: new Date().toISOString(), }); } - diff --git a/e2e/servers/next/app/api/protected-svm-proxy/route.ts b/e2e/servers/next/app/api/exact/evm/eip3009/proxy/route.ts similarity index 65% rename from e2e/servers/next/app/api/protected-svm-proxy/route.ts rename to e2e/servers/next/app/api/exact/evm/eip3009/proxy/route.ts index f908c8c635..7912dfc6b4 100644 --- a/e2e/servers/next/app/api/protected-svm-proxy/route.ts +++ b/e2e/servers/next/app/api/exact/evm/eip3009/proxy/route.ts @@ -1,17 +1,13 @@ import { NextResponse } from "next/server"; /** - * Protected SVM endpoint requiring payment (proxy middleware) + * EVM EIP-3009 endpoint requiring payment (proxy middleware) */ export const runtime = "nodejs"; -/** - * Protected SVM endpoint requiring payment (proxy middleware) - */ export async function GET() { return NextResponse.json({ message: "Protected endpoint accessed successfully", timestamp: new Date().toISOString(), }); } - diff --git a/e2e/servers/next/app/api/protected-withx402/route.ts b/e2e/servers/next/app/api/exact/evm/eip3009/withx402/route.ts similarity index 99% rename from e2e/servers/next/app/api/protected-withx402/route.ts rename to e2e/servers/next/app/api/exact/evm/eip3009/withx402/route.ts index a7bbbb2f1c..5edb0c1e1d 100644 --- a/e2e/servers/next/app/api/protected-withx402/route.ts +++ b/e2e/servers/next/app/api/exact/evm/eip3009/withx402/route.ts @@ -46,4 +46,3 @@ export const GET = withX402( }, server, ); - diff --git a/e2e/servers/next/app/api/protected-permit2-proxy/route.ts b/e2e/servers/next/app/api/exact/evm/permit2-eip2612GasSponsoring/proxy/route.ts similarity index 73% rename from e2e/servers/next/app/api/protected-permit2-proxy/route.ts rename to e2e/servers/next/app/api/exact/evm/permit2-eip2612GasSponsoring/proxy/route.ts index aec9f5a500..dc3e3b0e1a 100644 --- a/e2e/servers/next/app/api/protected-permit2-proxy/route.ts +++ b/e2e/servers/next/app/api/exact/evm/permit2-eip2612GasSponsoring/proxy/route.ts @@ -1,14 +1,13 @@ import { NextResponse } from "next/server"; /** - * Protected Permit2 endpoint requiring payment (proxy middleware) + * EVM Permit2 EIP-2612 gas sponsoring endpoint requiring payment (proxy middleware) */ export const runtime = "nodejs"; export async function GET() { return NextResponse.json({ - message: "Permit2 endpoint accessed successfully", + message: "Protected endpoint accessed successfully", timestamp: new Date().toISOString(), - method: "permit2", }); } diff --git a/e2e/servers/next/app/api/exact/evm/permit2-erc20ApprovalGasSponsoring/proxy/route.ts b/e2e/servers/next/app/api/exact/evm/permit2-erc20ApprovalGasSponsoring/proxy/route.ts new file mode 100644 index 0000000000..5867755d06 --- /dev/null +++ b/e2e/servers/next/app/api/exact/evm/permit2-erc20ApprovalGasSponsoring/proxy/route.ts @@ -0,0 +1,13 @@ +import { NextResponse } from "next/server"; + +/** + * EVM Permit2 ERC-20 approval gas sponsoring endpoint requiring payment (proxy middleware) + */ +export const runtime = "nodejs"; + +export async function GET() { + return NextResponse.json({ + message: "Protected endpoint accessed successfully", + timestamp: new Date().toISOString(), + }); +} diff --git a/e2e/servers/next/app/api/exact/evm/permit2/proxy/route.ts b/e2e/servers/next/app/api/exact/evm/permit2/proxy/route.ts new file mode 100644 index 0000000000..09b5d2e9d2 --- /dev/null +++ b/e2e/servers/next/app/api/exact/evm/permit2/proxy/route.ts @@ -0,0 +1,13 @@ +import { NextResponse } from "next/server"; + +/** + * EVM Permit2 direct endpoint requiring payment (proxy middleware) + */ +export const runtime = "nodejs"; + +export async function GET() { + return NextResponse.json({ + message: "Protected endpoint accessed successfully", + timestamp: new Date().toISOString(), + }); +} diff --git a/e2e/servers/next/app/api/exact/stellar/route.ts b/e2e/servers/next/app/api/exact/stellar/route.ts new file mode 100644 index 0000000000..38b773d52a --- /dev/null +++ b/e2e/servers/next/app/api/exact/stellar/route.ts @@ -0,0 +1,13 @@ +import { NextResponse } from "next/server"; + +/** + * Stellar endpoint requiring payment (proxy middleware) + */ +export const runtime = "nodejs"; + +export async function GET() { + return NextResponse.json({ + message: "Protected endpoint accessed successfully", + timestamp: new Date().toISOString(), + }); +} diff --git a/e2e/servers/next/app/api/exact/stellar/withx402/route.ts b/e2e/servers/next/app/api/exact/stellar/withx402/route.ts new file mode 100644 index 0000000000..7082891b44 --- /dev/null +++ b/e2e/servers/next/app/api/exact/stellar/withx402/route.ts @@ -0,0 +1,55 @@ +import { NextRequest, NextResponse } from "next/server"; +import { withX402 } from "@x402/next"; +import { declareDiscoveryExtension } from "@x402/extensions/bazaar"; +import { server, STELLAR_PAYEE_ADDRESS, STELLAR_NETWORK } from "../../../proxy"; + +/** + * Handler for the protected endpoint + */ +const handler = async (_: NextRequest) => { + return NextResponse.json({ + message: "Protected Stellar endpoint accessed successfully (withX402)", + timestamp: new Date().toISOString(), + }); +}; + +/** + * Protected Stellar endpoint using withX402 wrapper + * Only exported if STELLAR_PAYEE_ADDRESS is configured + */ +export const GET = STELLAR_PAYEE_ADDRESS + ? withX402( + handler, + { + accepts: { + payTo: STELLAR_PAYEE_ADDRESS, + scheme: "exact", + price: "$0.001", + network: STELLAR_NETWORK, + }, + extensions: { + ...declareDiscoveryExtension({ + output: { + example: { + message: "Protected Stellar endpoint accessed successfully (withX402)", + timestamp: "2024-01-01T00:00:00Z", + }, + schema: { + properties: { + message: { type: "string" }, + timestamp: { type: "string" }, + }, + required: ["message", "timestamp"], + }, + }, + }), + }, + }, + server, + ) + : async () => { + return NextResponse.json( + { error: "Stellar not configured" }, + { status: 503 }, + ); + }; diff --git a/e2e/servers/next/app/api/exact/svm/route.ts b/e2e/servers/next/app/api/exact/svm/route.ts new file mode 100644 index 0000000000..001d600906 --- /dev/null +++ b/e2e/servers/next/app/api/exact/svm/route.ts @@ -0,0 +1,13 @@ +import { NextResponse } from "next/server"; + +/** + * SVM endpoint requiring payment (proxy middleware) + */ +export const runtime = "nodejs"; + +export async function GET() { + return NextResponse.json({ + message: "Protected endpoint accessed successfully", + timestamp: new Date().toISOString(), + }); +} diff --git a/e2e/servers/next/app/api/protected-svm-withx402/route.ts b/e2e/servers/next/app/api/exact/svm/withx402/route.ts similarity index 99% rename from e2e/servers/next/app/api/protected-svm-withx402/route.ts rename to e2e/servers/next/app/api/exact/svm/withx402/route.ts index 57b3736191..ea63bc43c2 100644 --- a/e2e/servers/next/app/api/protected-svm-withx402/route.ts +++ b/e2e/servers/next/app/api/exact/svm/withx402/route.ts @@ -45,4 +45,3 @@ export const GET = withX402( }, server, ); - diff --git a/e2e/servers/next/app/api/health/route.ts b/e2e/servers/next/app/api/health/route.ts index 8a57257b35..1c3a28cfc3 100644 --- a/e2e/servers/next/app/api/health/route.ts +++ b/e2e/servers/next/app/api/health/route.ts @@ -12,4 +12,4 @@ export async function GET() { return NextResponse.json({ status: "healthy", }); -} \ No newline at end of file +} diff --git a/e2e/servers/next/app/api/protected-aptos-proxy/route.ts b/e2e/servers/next/app/api/protected-aptos-proxy/route.ts deleted file mode 100644 index 2b3a78839d..0000000000 --- a/e2e/servers/next/app/api/protected-aptos-proxy/route.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { NextResponse } from "next/server"; -import { APTOS_PAYEE_ADDRESS } from "../../../proxy"; - -/** - * Protected Aptos endpoint requiring payment (proxy middleware) - */ -export const runtime = "nodejs"; - -/** - * Protected Aptos endpoint requiring payment (proxy middleware) - */ -export async function GET() { - if (!APTOS_PAYEE_ADDRESS) { - return NextResponse.json({ - error: "Aptos payments not configured", - message: "APTOS_PAYEE_ADDRESS environment variable is not set", - }, { status: 501 }); - } - return NextResponse.json({ - message: "Protected endpoint accessed successfully", - timestamp: new Date().toISOString(), - }); -} diff --git a/e2e/servers/next/app/api/protected-permit2-erc20-proxy/route.ts b/e2e/servers/next/app/api/protected-permit2-erc20-proxy/route.ts deleted file mode 100644 index 66b4c383c4..0000000000 --- a/e2e/servers/next/app/api/protected-permit2-erc20-proxy/route.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { NextResponse } from "next/server"; - -/** - * Protected Permit2 ERC-20 endpoint requiring payment with ERC-20 approval gas sponsoring (proxy middleware) - */ -export const runtime = "nodejs"; - -export async function GET() { - return NextResponse.json({ - message: "Permit2 ERC-20 approval endpoint accessed successfully", - timestamp: new Date().toISOString(), - method: "permit2-erc20-approval", - }); -} diff --git a/e2e/servers/next/app/api/upto/evm/permit2-eip2612GasSponsoring/route.ts b/e2e/servers/next/app/api/upto/evm/permit2-eip2612GasSponsoring/route.ts new file mode 100644 index 0000000000..f7ff3bbe24 --- /dev/null +++ b/e2e/servers/next/app/api/upto/evm/permit2-eip2612GasSponsoring/route.ts @@ -0,0 +1,14 @@ +import { NextResponse } from "next/server"; + +/** + * Upto Permit2 EIP-2612 endpoint requiring payment (proxy middleware) + */ +export const runtime = "nodejs"; + +export async function GET() { + return NextResponse.json({ + message: "Upto Permit2 EIP-2612 endpoint accessed successfully", + timestamp: new Date().toISOString(), + method: "upto-permit2-eip2612", + }); +} diff --git a/e2e/servers/next/app/api/upto/evm/permit2-erc20ApprovalGasSponsoring/route.ts b/e2e/servers/next/app/api/upto/evm/permit2-erc20ApprovalGasSponsoring/route.ts new file mode 100644 index 0000000000..526b5f786b --- /dev/null +++ b/e2e/servers/next/app/api/upto/evm/permit2-erc20ApprovalGasSponsoring/route.ts @@ -0,0 +1,14 @@ +import { NextResponse } from "next/server"; + +/** + * Upto Permit2 ERC-20 approval endpoint requiring payment (proxy middleware) + */ +export const runtime = "nodejs"; + +export async function GET() { + return NextResponse.json({ + message: "Upto Permit2 ERC-20 approval endpoint accessed successfully", + timestamp: new Date().toISOString(), + method: "upto-permit2-erc20-approval", + }); +} diff --git a/e2e/servers/next/app/api/upto/evm/permit2/route.ts b/e2e/servers/next/app/api/upto/evm/permit2/route.ts new file mode 100644 index 0000000000..79743431b9 --- /dev/null +++ b/e2e/servers/next/app/api/upto/evm/permit2/route.ts @@ -0,0 +1,18 @@ +import { NextResponse } from "next/server"; +import { SETTLEMENT_OVERRIDES_HEADER } from "@x402/core/server"; + +/** + * Upto Permit2 direct endpoint requiring payment (proxy middleware) + * Client must have Permit2 pre-approved. Settles partial amount (1000 of 2000 authorized). + */ +export const runtime = "nodejs"; + +export async function GET() { + const response = NextResponse.json({ + message: "Upto Permit2 endpoint accessed successfully", + timestamp: new Date().toISOString(), + method: "upto-permit2", + }); + response.headers.set(SETTLEMENT_OVERRIDES_HEADER, JSON.stringify({ amount: "1000" })); + return response; +} diff --git a/e2e/servers/next/next.config.ts b/e2e/servers/next/next.config.ts index db0a372753..cb651cdc00 100644 --- a/e2e/servers/next/next.config.ts +++ b/e2e/servers/next/next.config.ts @@ -1,6 +1,5 @@ import type { NextConfig } from "next"; -const nextConfig: NextConfig = { -}; +const nextConfig: NextConfig = {}; export default nextConfig; diff --git a/e2e/servers/next/package.json b/e2e/servers/next/package.json index 917e7c47ad..e709f385b2 100644 --- a/e2e/servers/next/package.json +++ b/e2e/servers/next/package.json @@ -18,6 +18,7 @@ "@x402/evm": "workspace:*", "@x402/extensions": "workspace:*", "@x402/next": "workspace:*", + "@x402/stellar": "workspace:*", "@x402/svm": "workspace:*", "@heroicons/react": "^2.2.0", "next": "^16.0.10", @@ -45,4 +46,4 @@ "tailwindcss": "^3.4.1", "typescript": "^5" } -} \ No newline at end of file +} diff --git a/e2e/servers/next/proxy.ts b/e2e/servers/next/proxy.ts index 5225b8204c..8f5f877dd9 100644 --- a/e2e/servers/next/proxy.ts +++ b/e2e/servers/next/proxy.ts @@ -1,8 +1,10 @@ import { paymentProxy } from "@x402/next"; import { x402ResourceServer, HTTPFacilitatorClient } from "@x402/core/server"; import { ExactEvmScheme } from "@x402/evm/exact/server"; +import { UptoEvmScheme } from "@x402/evm/upto/server"; import { ExactSvmScheme } from "@x402/svm/exact/server"; import { ExactAptosScheme } from "@x402/aptos/exact/server"; +import { ExactStellarScheme } from "@x402/stellar/exact/server"; import { bazaarResourceServerExtension, declareDiscoveryExtension } from "@x402/extensions/bazaar"; import { declareEip2612GasSponsoringExtension, @@ -12,9 +14,14 @@ import { export const EVM_PAYEE_ADDRESS = process.env.EVM_PAYEE_ADDRESS as `0x${string}`; export const SVM_PAYEE_ADDRESS = process.env.SVM_PAYEE_ADDRESS as string; export const APTOS_PAYEE_ADDRESS = process.env.APTOS_PAYEE_ADDRESS as string; +export const STELLAR_PAYEE_ADDRESS = process.env.STELLAR_PAYEE_ADDRESS as string | undefined; export const EVM_NETWORK = (process.env.EVM_NETWORK || "eip155:84532") as `${string}:${string}`; -export const SVM_NETWORK = (process.env.SVM_NETWORK || "solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1") as `${string}:${string}`; +export const SVM_NETWORK = (process.env.SVM_NETWORK || + "solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1") as `${string}:${string}`; export const APTOS_NETWORK = (process.env.APTOS_NETWORK || "aptos:2") as `${string}:${string}`; +export const STELLAR_NETWORK = (process.env.STELLAR_NETWORK || + "stellar:testnet") as `${string}:${string}`; +const EVM_PERMIT2_ASSET = process.env.EVM_PERMIT2_ASSET as `0x${string}`; const facilitatorUrl = process.env.FACILITATOR_URL; if (!facilitatorUrl) { @@ -22,18 +29,26 @@ if (!facilitatorUrl) { process.exit(1); } -// Create HTTP facilitator client -const facilitatorClient = new HTTPFacilitatorClient({ url: facilitatorUrl }); +// Create facilitator clients (mock facilitator as fallback for startup validation) +const facilitatorClients = [new HTTPFacilitatorClient({ url: facilitatorUrl })]; +const mockFacilitatorUrl = process.env.MOCK_FACILITATOR_URL; +if (mockFacilitatorUrl) { + facilitatorClients.push(new HTTPFacilitatorClient({ url: mockFacilitatorUrl })); +} // Create x402 resource server with builder pattern (cleaner!) -export const server = new x402ResourceServer(facilitatorClient); +export const server = new x402ResourceServer(facilitatorClients); // Register server schemes server.register("eip155:*", new ExactEvmScheme()); +server.register("eip155:*", new UptoEvmScheme()); server.register("solana:*", new ExactSvmScheme()); if (APTOS_PAYEE_ADDRESS) { server.register("aptos:*", new ExactAptosScheme()); } +if (STELLAR_PAYEE_ADDRESS) { + server.register("stellar:*", new ExactStellarScheme()); +} // Register Bazaar discovery extension server.registerExtension(bazaarResourceServerExtension); @@ -42,7 +57,7 @@ console.log(`Using remote facilitator at: ${facilitatorUrl}`); export const proxy = paymentProxy( { - "/api/protected-proxy": { + "/api/exact/evm/eip3009/proxy": { accepts: { payTo: EVM_PAYEE_ADDRESS, scheme: "exact", @@ -67,7 +82,7 @@ export const proxy = paymentProxy( }), }, }, - "/api/protected-svm-proxy": { + "/api/exact/svm": { accepts: { payTo: SVM_PAYEE_ADDRESS, scheme: "exact", @@ -94,45 +109,72 @@ export const proxy = paymentProxy( }, ...(APTOS_PAYEE_ADDRESS ? { - "/api/protected-aptos-proxy": { - accepts: { - payTo: APTOS_PAYEE_ADDRESS, - scheme: "exact", - price: "$0.001", - network: APTOS_NETWORK, - }, - extensions: { - ...declareDiscoveryExtension({ - output: { - example: { - message: "Protected endpoint accessed successfully", - timestamp: "2024-01-01T00:00:00Z", + "/api/exact/aptos": { + accepts: { + payTo: APTOS_PAYEE_ADDRESS, + scheme: "exact", + price: "$0.001", + network: APTOS_NETWORK, + }, + extensions: { + ...declareDiscoveryExtension({ + output: { + example: { + message: "Protected endpoint accessed successfully", + timestamp: "2024-01-01T00:00:00Z", + }, + schema: { + properties: { + message: { type: "string" }, + timestamp: { type: "string" }, + }, + required: ["message", "timestamp"], + }, }, - schema: { - properties: { - message: { type: "string" }, - timestamp: { type: "string" }, + }), + }, + }, + } + : {}), + ...(STELLAR_PAYEE_ADDRESS + ? { + "/api/exact/stellar": { + accepts: { + payTo: STELLAR_PAYEE_ADDRESS, + scheme: "exact", + price: "$0.001", + network: STELLAR_NETWORK, + }, + extensions: { + ...declareDiscoveryExtension({ + output: { + example: { + message: "Protected endpoint accessed successfully", + timestamp: "2024-01-01T00:00:00Z", + }, + schema: { + properties: { + message: { type: "string" }, + timestamp: { type: "string" }, + }, + required: ["message", "timestamp"], }, - required: ["message", "timestamp"], }, - }, - }), + }), + }, }, - }, - } + } : {}), - "/api/protected-permit2-proxy": { + "/api/exact/evm/permit2/proxy": { accepts: { payTo: EVM_PAYEE_ADDRESS, scheme: "exact", network: EVM_NETWORK, price: { amount: "1000", - asset: "0x036CbD53842c5426634e7929541eC2318f3dCF7e", + asset: EVM_PERMIT2_ASSET, extra: { assetTransferMethod: "permit2", - name: "USDC", - version: "2", }, }, }, @@ -154,17 +196,97 @@ export const proxy = paymentProxy( }, }, }), + }, + }, + "/api/exact/evm/permit2-eip2612GasSponsoring/proxy": { + accepts: { + payTo: EVM_PAYEE_ADDRESS, + scheme: "exact", + network: EVM_NETWORK, + price: "$0.001", + extra: { assetTransferMethod: "permit2" }, + }, + extensions: { + ...declareDiscoveryExtension({ + output: { + example: { + message: "Permit2 EIP-2612 endpoint accessed successfully", + timestamp: "2024-01-01T00:00:00Z", + method: "permit2-eip2612", + }, + schema: { + properties: { + message: { type: "string" }, + timestamp: { type: "string" }, + method: { type: "string" }, + }, + required: ["message", "timestamp", "method"], + }, + }, + }), ...declareEip2612GasSponsoringExtension(), }, }, - "/api/protected-permit2-erc20-proxy": { + "/api/exact/evm/permit2-erc20ApprovalGasSponsoring/proxy": { accepts: { payTo: EVM_PAYEE_ADDRESS, scheme: "exact", network: EVM_NETWORK, price: { amount: "1000", - asset: "0xeED520980fC7C7B4eB379B96d61CEdea2423005a", + asset: EVM_PERMIT2_ASSET, + extra: { + assetTransferMethod: "permit2", + }, + }, + }, + extensions: { + ...declareErc20ApprovalGasSponsoringExtension(), + }, + }, + "/api/upto/evm/permit2": { + accepts: { + payTo: EVM_PAYEE_ADDRESS, + scheme: "upto", + network: EVM_NETWORK, + price: { + amount: "2000", + asset: EVM_PERMIT2_ASSET, + extra: { + assetTransferMethod: "permit2", + name: EVM_NETWORK == "eip155:84532" ? "USDC" : "USD Coin", + version: "2", + }, + }, + }, + }, + "/api/upto/evm/permit2-eip2612GasSponsoring": { + accepts: { + payTo: EVM_PAYEE_ADDRESS, + scheme: "upto", + network: EVM_NETWORK, + price: { + amount: "2000", + asset: EVM_PERMIT2_ASSET, + extra: { + assetTransferMethod: "permit2", + name: EVM_NETWORK == "eip155:84532" ? "USDC" : "USD Coin", + version: "2", + }, + }, + }, + extensions: { + ...declareEip2612GasSponsoringExtension(), + }, + }, + "/api/upto/evm/permit2-erc20ApprovalGasSponsoring": { + accepts: { + payTo: EVM_PAYEE_ADDRESS, + scheme: "upto", + network: EVM_NETWORK, + price: { + amount: "2000", + asset: EVM_PERMIT2_ASSET, extra: { assetTransferMethod: "permit2", }, @@ -179,5 +301,16 @@ export const proxy = paymentProxy( ); export const config = { - matcher: ["/api/protected-proxy", "/api/protected-svm-proxy", "/api/protected-aptos-proxy", "/api/protected-permit2-proxy", "/api/protected-permit2-erc20-proxy"], + matcher: [ + "/api/exact/evm/eip3009/proxy", + "/api/exact/svm", + "/api/exact/aptos", + "/api/exact/stellar", + "/api/exact/evm/permit2/proxy", + "/api/exact/evm/permit2-eip2612GasSponsoring/proxy", + "/api/exact/evm/permit2-erc20ApprovalGasSponsoring/proxy", + "/api/upto/evm/permit2", + "/api/upto/evm/permit2-eip2612GasSponsoring", + "/api/upto/evm/permit2-erc20ApprovalGasSponsoring", + ], }; diff --git a/e2e/servers/next/test.config.json b/e2e/servers/next/test.config.json index cc502099bb..be5db0dc67 100644 --- a/e2e/servers/next/test.config.json +++ b/e2e/servers/next/test.config.json @@ -3,66 +3,116 @@ "type": "server", "language": "typescript", "x402Version": 2, - "extensions": [ - "bazaar", - "eip2612GasSponsoring", - "erc20ApprovalGasSponsoring" - ], + "extensions": ["bazaar", "eip2612GasSponsoring", "erc20ApprovalGasSponsoring"], + "endpoints": [ { - "path": "/api/protected-proxy", + "path": "/api/exact/evm/eip3009/proxy", "method": "GET", - "description": "Protected endpoint using proxy middleware", + "description": "EVM EIP-3009 endpoint using proxy middleware", "requiresPayment": true, "protocolFamily": "evm", "transferMethod": "eip3009" }, { - "path": "/api/protected-permit2-proxy", + "path": "/api/exact/evm/permit2/proxy", + "method": "GET", + "description": "EVM Permit2 direct endpoint (standard settle, no gas sponsoring)", + "requiresPayment": true, + "protocolFamily": "evm", + "transferMethod": "permit2", + "permit2Direct": true + }, + { + "path": "/api/exact/evm/permit2-eip2612GasSponsoring/proxy", "method": "GET", - "description": "Protected Permit2 endpoint using proxy middleware", + "description": "EVM Permit2 endpoint with EIP-2612 gas sponsoring using proxy middleware", "requiresPayment": true, "protocolFamily": "evm", - "permit2": true + "transferMethod": "permit2", + "coldstart": true }, { - "path": "/api/protected-permit2-erc20-proxy", + "path": "/api/exact/evm/permit2-erc20ApprovalGasSponsoring/proxy", "method": "GET", - "description": "Protected Permit2 endpoint with ERC-20 approval gas sponsoring using proxy middleware", + "description": "EVM Permit2 endpoint with ERC-20 approval gas sponsoring using proxy middleware", "requiresPayment": true, "protocolFamily": "evm", "transferMethod": "permit2", - "extensions": ["erc20ApprovalGasSponsoring"] + "extensions": ["erc20ApprovalGasSponsoring"], + "coldstart": true }, { - "path": "/api/protected-svm-proxy", + "path": "/api/upto/evm/permit2", "method": "GET", - "description": "Protected endpoint using proxy middleware on SVM", + "description": "Protected Upto Permit2 endpoint (direct, client must pre-approve)", + "requiresPayment": true, + "protocolFamily": "evm", + "transferMethod": "upto", + "permit2Direct": true + }, + { + "path": "/api/upto/evm/permit2-eip2612GasSponsoring", + "method": "GET", + "description": "Protected Upto Permit2 endpoint with EIP-2612 gas sponsoring", + "requiresPayment": true, + "protocolFamily": "evm", + "transferMethod": "upto", + "coldstart": true + }, + { + "path": "/api/upto/evm/permit2-erc20ApprovalGasSponsoring", + "method": "GET", + "description": "Protected Upto Permit2 endpoint with ERC-20 approval gas sponsoring", + "requiresPayment": true, + "protocolFamily": "evm", + "transferMethod": "upto", + "extensions": ["erc20ApprovalGasSponsoring"], + "coldstart": true + }, + { + "path": "/api/exact/svm", + "method": "GET", + "description": "SVM endpoint using proxy middleware", "requiresPayment": true, "protocolFamily": "svm" }, { - "path": "/api/protected-aptos-proxy", + "path": "/api/exact/aptos", "method": "GET", - "description": "Protected endpoint using proxy middleware on Aptos", + "description": "Aptos endpoint using proxy middleware", "requiresPayment": true, "protocolFamily": "aptos" }, { - "path": "/api/protected-withx402", + "path": "/api/exact/stellar", "method": "GET", - "description": "Protected endpoint using withX402 wrapper", + "description": "Stellar endpoint using proxy middleware", + "requiresPayment": true, + "protocolFamily": "stellar" + }, + { + "path": "/api/exact/evm/eip3009/withx402", + "method": "GET", + "description": "EVM EIP-3009 endpoint using withX402 wrapper", "requiresPayment": true, "protocolFamily": "evm", "transferMethod": "eip3009" }, { - "path": "/api/protected-svm-withx402", + "path": "/api/exact/svm/withx402", "method": "GET", - "description": "Protected endpoint using withX402 wrapper on SVM", + "description": "SVM endpoint using withX402 wrapper", "requiresPayment": true, "protocolFamily": "svm" }, + { + "path": "/api/exact/stellar/withx402", + "method": "GET", + "description": "Stellar endpoint using withX402 wrapper", + "requiresPayment": true, + "protocolFamily": "stellar" + }, { "path": "/api/health", "method": "GET", @@ -77,14 +127,7 @@ } ], "environment": { - "required": [ - "PORT", - "EVM_PAYEE_ADDRESS", - "SVM_PAYEE_ADDRESS", - "FACILITATOR_URL" - ], - "optional": [ - "APTOS_PAYEE_ADDRESS" - ] + "required": ["PORT", "EVM_PAYEE_ADDRESS", "SVM_PAYEE_ADDRESS", "FACILITATOR_URL"], + "optional": ["APTOS_PAYEE_ADDRESS", "STELLAR_PAYEE_ADDRESS"] } -} \ No newline at end of file +} diff --git a/e2e/servers/next/tsconfig.json b/e2e/servers/next/tsconfig.json index aa175a3f81..6a3c51ac3f 100644 --- a/e2e/servers/next/tsconfig.json +++ b/e2e/servers/next/tsconfig.json @@ -1,11 +1,7 @@ { "compilerOptions": { "target": "ES2022", - "lib": [ - "dom", - "dom.iterable", - "esnext" - ], + "lib": ["dom", "dom.iterable", "esnext"], "allowJs": true, "skipLibCheck": true, "strict": false, @@ -23,9 +19,7 @@ } ], "paths": { - "@/*": [ - "./*" - ] + "@/*": ["./*"] } }, "include": [ @@ -36,7 +30,5 @@ ".next/types/**/*.ts", ".next/dev/types/**/*.ts" ], - "exclude": [ - "node_modules" - ] + "exclude": ["node_modules"] } diff --git a/e2e/servers/text-server-protocol.txt b/e2e/servers/text-server-protocol.txt index 10fbd43e80..956e573b0c 100644 --- a/e2e/servers/text-server-protocol.txt +++ b/e2e/servers/text-server-protocol.txt @@ -18,6 +18,13 @@ Servers must declare which x402 protocol version they implement using the `x402V Servers may declare which protocol extensions they support using the `extensions` field: - **extensions**: Array of supported extension names (e.g., ["bazaar"]) +## Coldstart Tests +Some endpoints require special pre-test state setup before they can be exercised (e.g., revoking a Permit2 approval so that the gas-sponsoring extension path is triggered). This setup is expensive: it involves funding the client wallet, submitting on-chain transactions, and draining ETH back. + +Endpoints that require this setup declare `"coldstart": true` in their test config. The test runner treats the **first** test for a given endpoint path within a combo as the "coldstart" test — it performs the full setup. Subsequent tests for the same endpoint path within the same combo skip the setup and run directly, exercising the "warm" (already-approved) code path. + +All explicit gas sponsorship extension endpoints (EIP-2612 and ERC-20 approval) should have `coldstart` enabled, as they must succeed without the user having gas or approval ahead of time. + ## EVM Transfer Method For EVM endpoints, servers must declare the `transferMethod` used for the payment transfer: - **eip3009**: EIP-3009 transferWithAuthorization (default if omitted) diff --git a/e2e/src/cli/args.ts b/e2e/src/cli/args.ts index bab1b6b019..ec6332ff71 100644 --- a/e2e/src/cli/args.ts +++ b/e2e/src/cli/args.ts @@ -16,6 +16,7 @@ export interface ParsedArgs { networkMode?: NetworkMode; // undefined = prompt user, set = skip prompt parallel: boolean; concurrency: number; + endpoints?: string[]; } export function parseArgs(): ParsedArgs { @@ -35,14 +36,15 @@ export function parseArgs(): ParsedArgs { } // Check if any filter args present -> programmatic mode - const hasFilterArgs = args.some(arg => + const hasFilterArgs = args.some(arg => arg.startsWith('--transport=') || arg.startsWith('--facilitators=') || arg.startsWith('--servers=') || arg.startsWith('--clients=') || arg.startsWith('--extensions=') || arg.startsWith('--versions=') || - arg.startsWith('--families=') + arg.startsWith('--families=') || + arg.startsWith('--endpoints=') ); const mode: 'interactive' | 'programmatic' = hasFilterArgs ? 'programmatic' : 'interactive'; @@ -50,8 +52,20 @@ export function parseArgs(): ParsedArgs { // Parse verbose const verbose = args.includes('-v') || args.includes('--verbose'); - // Parse log file - const logFile = args.find(arg => arg.startsWith('--log-file='))?.split('=')[1]; + // Parse log file — supports --log (timestamped default), --log=path, and legacy --log-file=path + let logFile: string | undefined; + const logArg = args.find(arg => arg === '--log' || arg.startsWith('--log=')); + const legacyLogArg = args.find(arg => arg.startsWith('--log-file=')); + if (logArg) { + if (logArg.includes('=')) { + logFile = logArg.split('=').slice(1).join('='); + } else { + const ts = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19); + logFile = `logs/e2e-run-${ts}.log`; + } + } else if (legacyLogArg) { + logFile = legacyLogArg.split('=')[1]; + } // Parse JSON output file const outputJson = args.find(arg => arg.startsWith('--output-json='))?.split('=')[1]; @@ -80,6 +94,7 @@ export function parseArgs(): ParsedArgs { const extensions = parseListArg(args, '--extensions'); const versions = parseListArg(args, '--versions')?.map(v => parseInt(v)); const families = parseListArg(args, '--families'); + const endpoints = parseListArg(args, '--endpoints'); return { mode, @@ -94,12 +109,14 @@ export function parseArgs(): ParsedArgs { extensions, versions, protocolFamilies: families, + endpoints, }, showHelp: false, minimize, networkMode, parallel, concurrency, + endpoints, }; } @@ -130,10 +147,12 @@ export function printHelp(): void { console.log(' --extensions= Comma-separated extensions (e.g., bazaar)'); console.log(' --versions= Comma-separated version numbers (e.g., 1,2)'); console.log(' --families= Comma-separated protocol families (e.g., evm,svm)'); + console.log(' --endpoints= Comma-separated endpoint paths or regex patterns (auto-anchored)'); console.log(''); console.log('Options:'); console.log(' -v, --verbose Enable verbose logging'); - console.log(' --log-file= Save verbose output to file'); + console.log(' --log[=] Write output to file (default: logs/e2e-run-.log)'); + console.log(' --log-file= Alias for --log= (legacy)'); console.log(' --output-json= Write structured JSON results to file'); console.log(' --min Minimize tests (coverage-based skipping)'); console.log(' --parallel Run server+facilitator combos concurrently'); @@ -147,6 +166,8 @@ export function printHelp(): void { console.log(' pnpm test --min -v # Minimize with verbose'); console.log(' pnpm test --transport=mcp # MCP transport only'); console.log(' pnpm test --mainnet --facilitators=go --servers=express # Mainnet programmatic'); + console.log(" pnpm test --testnet --endpoints='/protected' # Exact path match"); + console.log(" pnpm test --testnet --endpoints='/protected-permit2.*' # Regex: all permit2 routes"); console.log(' pnpm test --testnet --min --parallel -v # Parallel mode'); console.log(' pnpm test --testnet --min --parallel --concurrency=2 -v # Limited concurrency'); console.log(''); diff --git a/e2e/src/cli/filters.ts b/e2e/src/cli/filters.ts index 4ce7435c43..bf300f3cd9 100644 --- a/e2e/src/cli/filters.ts +++ b/e2e/src/cli/filters.ts @@ -8,6 +8,7 @@ export interface TestFilters { extensions?: string[]; // For test output control (doesn't filter scenarios) versions?: number[]; protocolFamilies?: string[]; + endpoints?: string[]; // Regex patterns to filter by endpoint path } /** @@ -64,6 +65,30 @@ export function filterScenarios( } } + // Endpoint filter — each entry is treated as a regex pattern. + // Patterns are auto-anchored (^...$) so that "/protected" matches only + // that exact path. To match a prefix, use "/protected.*"; for a substring + // anywhere, use ".*permit2.*" or omit anchors explicitly via ^ / $. + if (filters.endpoints && filters.endpoints.length > 0) { + const endpointPath = scenario.endpoint.path; + const matched = filters.endpoints.some(rawPattern => { + // Ensure patterns that look like paths start with / + const pattern = (!rawPattern.startsWith('/') && !rawPattern.startsWith('^')) + ? `/${rawPattern}` + : rawPattern; + try { + const anchored = (pattern.startsWith('^') || pattern.endsWith('$')) + ? pattern + : `^${pattern}$`; + return new RegExp(anchored).test(endpointPath); + } catch { + // Fall back to exact match if pattern is not valid regex + return endpointPath === pattern; + } + }); + if (!matched) return false; + } + // NOTE: Extensions filter NOT applied - it only controls test output visibility // Extensions are stored separately and passed to test execution logic diff --git a/e2e/src/cli/interactive.ts b/e2e/src/cli/interactive.ts index 655d606254..db4eab771e 100644 --- a/e2e/src/cli/interactive.ts +++ b/e2e/src/cli/interactive.ts @@ -27,16 +27,13 @@ export async function runInteractiveMode( preselectedNetworkMode?: NetworkMode ): Promise { - log('\n🎯 Interactive Mode'); - log('==================\n'); - // Question 1: Select facilitators (multi-select) // Sort facilitators: regular ones first, external ones at the bottom const regularFacilitators = allFacilitators.filter(f => !f.isExternal); const externalFacilitators = allFacilitators.filter(f => f.isExternal); - + const facilitatorChoices: any[] = []; - + // Add regular facilitators regularFacilitators.forEach(f => { facilitatorChoices.push({ @@ -45,7 +42,7 @@ export async function runInteractiveMode( selected: minimize // With --min: all selected. Without --min: none selected }); }); - + // Add external facilitators section if any exist if (externalFacilitators.length > 0) { // Add separator/header for external facilitators @@ -54,7 +51,7 @@ export async function runInteractiveMode( value: '__external_separator__', disabled: true }); - + externalFacilitators.forEach(f => { facilitatorChoices.push({ title: `${f.name} (${formatVersions(f.config.x402Versions)}) [${f.config.protocolFamilies?.join(', ') || ''}]${f.config.extensions ? ' {' + f.config.extensions.join(', ') + '}' : ''}`, @@ -283,7 +280,24 @@ export async function runInteractiveMode( selectedFamilies = availableFamilies; } - // Question 8: Select network mode (testnet/mainnet) - LAST question + // Question 8: Endpoint filter (optional free-text, comma-separated regex patterns) + const endpointsResponse = await prompts({ + type: 'text', + name: 'endpoints', + message: 'Filter endpoints (comma-separated patterns, blank = all)', + initial: '', + hint: 'e.g. /protected, permit2.* — supports regex', + }); + + // null means Ctrl-C; empty string means "all" + if (endpointsResponse.endpoints === undefined) { + return null; + } + const selectedEndpoints: string[] | undefined = endpointsResponse.endpoints + ? (endpointsResponse.endpoints as string).split(',').map((p: string) => p.trim()).filter((p: string) => p.length > 0) + : undefined; + + // Question 9: Select network mode (testnet/mainnet) - LAST question // Skip if preselected via CLI flag let networkMode: NetworkMode; @@ -332,6 +346,7 @@ export async function runInteractiveMode( extensions: selectedExtensions, versions: selectedVersions, protocolFamilies: selectedFamilies, + endpoints: selectedEndpoints, networkMode, }; } diff --git a/e2e/src/clients/generic-client.ts b/e2e/src/clients/generic-client.ts index c4d0ec8142..aff2ed44a0 100644 --- a/e2e/src/clients/generic-client.ts +++ b/e2e/src/clients/generic-client.ts @@ -23,8 +23,11 @@ export class GenericClientProxy extends BaseProxy implements ClientProxy { EVM_PRIVATE_KEY: config.evmPrivateKey, SVM_PRIVATE_KEY: config.svmPrivateKey, APTOS_PRIVATE_KEY: config.aptosPrivateKey, + STELLAR_PRIVATE_KEY: config.stellarPrivateKey, RESOURCE_SERVER_URL: config.serverUrl, ENDPOINT_PATH: config.endpointPath, + EVM_NETWORK: config.evmNetwork, + EVM_RPC_URL: config.evmRpcUrl, } }; diff --git a/e2e/src/discovery.ts b/e2e/src/discovery.ts index 7f9b062ffb..46ea72118b 100644 --- a/e2e/src/discovery.ts +++ b/e2e/src/discovery.ts @@ -257,7 +257,6 @@ export class TestDiscovery { }) || []; for (const endpoint of testableEndpoints) { - // Default to EVM if no protocol family specified for backward compatibility const endpointProtocolFamily = endpoint.protocolFamily || 'evm'; // Only create scenarios where client supports endpoint's protocol family @@ -313,12 +312,12 @@ export class TestDiscovery { const facilitators = this.discoverFacilitators(); const scenarios = this.generateTestScenarios(); - log('🔍 Test Discovery Summary'); - log('========================'); + verboseLog('🔍 Test Discovery Summary'); + verboseLog('========================'); if (this.includeLegacy) { - log('🔄 Legacy mode enabled - including legacy implementations'); + verboseLog('🔄 Legacy mode enabled - including legacy implementations'); } - log(`📡 Servers found: ${servers.length}`); + verboseLog(`📡 Servers found: ${servers.length}`); servers.forEach(server => { const paidEndpoints = server.config.endpoints?.filter(e => e.requiresPayment).length || 0; const protocolFamilies = new Set( @@ -326,22 +325,21 @@ export class TestDiscovery { ); const version = server.config.x402Version || 1; const transport = server.config.transport || 'http'; - log(` - ${server.name} (${server.config.language}) [${transport}] v${version} - ${paidEndpoints} x402 endpoints [${Array.from(protocolFamilies).join(', ')}]`); + verboseLog(` - ${server.name} (${server.config.language}) [${transport}] v${version} - ${paidEndpoints} x402 endpoints [${Array.from(protocolFamilies).join(', ')}]`); }); - log(`📱 Clients found: ${clients.length}`); + verboseLog(`📱 Clients found: ${clients.length}`); clients.forEach(client => { const protocolFamilies = client.config.protocolFamilies || ['evm']; const versions = client.config.x402Versions || [1]; const transport = client.config.transport || 'http'; const evmTransferMethods = client.config.evm?.transferMethods || ['eip3009']; const evmInfo = protocolFamilies.includes('evm') ? ` evm:${evmTransferMethods.join(',')}` : ''; - log(` - ${client.name} (${client.config.language}) [${transport}] v[${versions.join(', ')}] [${protocolFamilies.join(', ')}]${evmInfo}`); const extInfo = client.config.extensions ? ` {${client.config.extensions.join(', ')}}` : ''; - log(` - ${client.name} (${client.config.language}) [${transport}] v[${versions.join(', ')}] [${protocolFamilies.join(', ')}]${extInfo}`); + verboseLog(` - ${client.name} (${client.config.language}) [${transport}] v[${versions.join(', ')}] [${protocolFamilies.join(', ')}]${evmInfo}${extInfo}`); }); - log(`🏛️ Facilitators found: ${facilitators.length}`); + verboseLog(`🏛️ Facilitators found: ${facilitators.length}`); const regularFacilitators = facilitators.filter(f => !f.isExternal); const externalFacilitators = facilitators.filter(f => f.isExternal); @@ -351,17 +349,17 @@ export class TestDiscovery { const versions = facilitator.config.x402Versions || [2]; const evmTransferMethods = facilitator.config.evm?.transferMethods || ['eip3009']; const evmInfo = protocolFamilies.includes('evm') ? ` evm:${evmTransferMethods.join(',')}` : ''; - log(` - ${facilitator.name} (${facilitator.config.language}) v[${versions.join(', ')}] [${protocolFamilies.join(', ')}]${evmInfo}`); + verboseLog(` - ${facilitator.name} (${facilitator.config.language}) v[${versions.join(', ')}] [${protocolFamilies.join(', ')}]${evmInfo}`); }); if (externalFacilitators.length > 0) { - log(` External:`); + verboseLog(` External:`); externalFacilitators.forEach(facilitator => { const protocolFamilies = facilitator.config.protocolFamilies || ['evm']; const versions = facilitator.config.x402Versions || [2]; const evmTransferMethods = facilitator.config.evm?.transferMethods || ['eip3009']; const evmInfo = protocolFamilies.includes('evm') ? ` evm:${evmTransferMethods.join(',')}` : ''; - log(` - ${facilitator.name} (${facilitator.config.language}) v[${versions.join(', ')}] [${protocolFamilies.join(', ')}]${evmInfo}`); + verboseLog(` - ${facilitator.name} (${facilitator.config.language}) v[${versions.join(', ')}] [${protocolFamilies.join(', ')}]${evmInfo}`); }); } @@ -371,10 +369,10 @@ export class TestDiscovery { return acc; }, {} as Record); - log(`📊 Test scenarios: ${scenarios.length}`); + verboseLog(`📊 Test scenarios: ${scenarios.length}`); Object.entries(protocolBreakdown).forEach(([protocol, count]) => { - log(` - ${protocol.toUpperCase()}: ${count} scenarios`); + verboseLog(` - ${protocol.toUpperCase()}: ${count} scenarios`); }); - log(''); + verboseLog(''); } } diff --git a/e2e/src/facilitators/facilitator-manager.ts b/e2e/src/facilitators/facilitator-manager.ts index 9583c31228..e54be2da78 100644 --- a/e2e/src/facilitators/facilitator-manager.ts +++ b/e2e/src/facilitators/facilitator-manager.ts @@ -36,6 +36,7 @@ export class FacilitatorManager { evmPrivateKey: process.env.FACILITATOR_EVM_PRIVATE_KEY, svmPrivateKey: process.env.FACILITATOR_SVM_PRIVATE_KEY, aptosPrivateKey: process.env.FACILITATOR_APTOS_PRIVATE_KEY, + stellarPrivateKey: process.env.FACILITATOR_STELLAR_PRIVATE_KEY, networks, }); diff --git a/e2e/src/facilitators/generic-facilitator.ts b/e2e/src/facilitators/generic-facilitator.ts index 03f510bbae..672f758a8a 100644 --- a/e2e/src/facilitators/generic-facilitator.ts +++ b/e2e/src/facilitators/generic-facilitator.ts @@ -54,6 +54,7 @@ export interface FacilitatorConfig { evmPrivateKey?: string; svmPrivateKey?: string; aptosPrivateKey?: string; + stellarPrivateKey?: string; networks: NetworkSet; } @@ -114,6 +115,7 @@ export class GenericFacilitatorProxy extends BaseProxy implements FacilitatorPro EVM_PRIVATE_KEY: config.evmPrivateKey || '', SVM_PRIVATE_KEY: config.svmPrivateKey || '', APTOS_PRIVATE_KEY: config.aptosPrivateKey || '', + STELLAR_PRIVATE_KEY: config.stellarPrivateKey || '', // Network configs from NetworkSet EVM_NETWORK: config.networks.evm.caip2, @@ -122,6 +124,8 @@ export class GenericFacilitatorProxy extends BaseProxy implements FacilitatorPro SVM_RPC_URL: config.networks.svm.rpcUrl, APTOS_NETWORK: config.networks.aptos.caip2, APTOS_RPC_URL: config.networks.aptos.rpcUrl, + STELLAR_NETWORK: config.networks.stellar.caip2, + STELLAR_RPC_URL: config.networks.stellar.rpcUrl, }; // Pass through any additional environment variables required by the facilitator diff --git a/e2e/src/networks/networks.ts b/e2e/src/networks/networks.ts index c4d64d65c3..5d8441a1b6 100644 --- a/e2e/src/networks/networks.ts +++ b/e2e/src/networks/networks.ts @@ -6,18 +6,20 @@ */ export type NetworkMode = 'testnet' | 'mainnet'; -export type ProtocolFamily = 'evm' | 'svm' | 'aptos'; +export type ProtocolFamily = 'evm' | 'svm' | 'aptos' | 'stellar'; export type NetworkConfig = { name: string; caip2: `${string}:${string}`; rpcUrl: string; + permit2Asset?: string; }; export type NetworkSet = { evm: NetworkConfig; svm: NetworkConfig; aptos: NetworkConfig; + stellar: NetworkConfig; }; /** @@ -29,6 +31,7 @@ const NETWORK_SETS: Record = { name: 'Base Sepolia', caip2: 'eip155:84532', rpcUrl: process.env.BASE_SEPOLIA_RPC_URL || 'https://sepolia.base.org', + permit2Asset: '0x036CbD53842c5426634e7929541eC2318f3dCF7e', }, svm: { name: 'Solana Devnet', @@ -40,12 +43,18 @@ const NETWORK_SETS: Record = { caip2: 'aptos:2', rpcUrl: process.env.APTOS_TESTNET_RPC_URL || 'https://fullnode.testnet.aptoslabs.com/v1', }, + stellar: { + name: 'Stellar Testnet', + caip2: 'stellar:testnet', + rpcUrl: process.env.STELLAR_TESTNET_RPC_URL || 'https://soroban-testnet.stellar.org', + }, }, mainnet: { evm: { name: 'Base', caip2: 'eip155:8453', rpcUrl: process.env.BASE_RPC_URL || 'https://mainnet.base.org', + permit2Asset: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913', }, svm: { name: 'Solana', @@ -57,6 +66,11 @@ const NETWORK_SETS: Record = { caip2: 'aptos:1', rpcUrl: process.env.APTOS_RPC_URL || 'https://fullnode.mainnet.aptoslabs.com/v1', }, + stellar: { + name: 'Stellar Pubnet', + caip2: 'stellar:pubnet', + rpcUrl: process.env.STELLAR_RPC_URL || 'https://mainnet.sorobanrpc.com', + }, }, }; @@ -74,7 +88,7 @@ export function getNetworkSet(mode: NetworkMode): NetworkSet { * Get network config for a protocol family in a given mode * * @param mode - 'testnet' or 'mainnet' - * @param protocolFamily - 'evm', 'svm', or 'aptos' + * @param protocolFamily - 'evm', 'svm', 'aptos', or 'stellar' * @returns NetworkConfig for the specified protocol */ export function getNetworkForProtocol( @@ -92,5 +106,6 @@ export function getNetworkForProtocol( */ export function getNetworkModeDescription(mode: NetworkMode): string { const set = NETWORK_SETS[mode]; - return `${set.evm.name} + ${set.svm.name} + ${set.aptos.name}`; + const networks = [set.evm.name, set.svm.name, set.aptos.name, set.stellar.name]; + return networks.join(' + '); } diff --git a/e2e/src/servers/generic-server.ts b/e2e/src/servers/generic-server.ts index 83783aa513..f769dbed0d 100644 --- a/e2e/src/servers/generic-server.ts +++ b/e2e/src/servers/generic-server.ts @@ -92,6 +92,7 @@ export class GenericServerProxy extends BaseProxy implements ServerProxy { EVM_NETWORK: evmNetwork, EVM_RPC_URL: config.networks.evm.rpcUrl, EVM_PAYEE_ADDRESS: config.evmPayTo, + EVM_PERMIT2_ASSET: config.networks.evm.permit2Asset || '', // SVM network config SVM_NETWORK: svmNetwork, @@ -103,8 +104,14 @@ export class GenericServerProxy extends BaseProxy implements ServerProxy { APTOS_RPC_URL: config.networks.aptos.rpcUrl, APTOS_PAYEE_ADDRESS: config.aptosPayTo, + // Stellar network config + STELLAR_NETWORK: config.networks.stellar.caip2, + STELLAR_RPC_URL: config.networks.stellar.rpcUrl, + STELLAR_PAYEE_ADDRESS: config.stellarPayTo, + // Facilitator FACILITATOR_URL: config.facilitatorUrl || '', + MOCK_FACILITATOR_URL: config.mockFacilitatorUrl || '', } }; diff --git a/e2e/src/types.ts b/e2e/src/types.ts index 991e6050cb..841f200c39 100644 --- a/e2e/src/types.ts +++ b/e2e/src/types.ts @@ -1,8 +1,8 @@ import type { NetworkSet } from './networks/networks'; -export type ProtocolFamily = 'evm' | 'svm' | 'aptos'; +export type ProtocolFamily = 'evm' | 'svm' | 'aptos' | 'stellar'; export type Transport = 'http' | 'mcp'; -export type TransferMethod = 'eip3009' | 'permit2'; +export type TransferMethod = 'eip3009' | 'permit2' | 'upto'; export interface ClientResult { success: boolean; @@ -16,8 +16,11 @@ export interface ClientConfig { evmPrivateKey: string; svmPrivateKey: string; aptosPrivateKey: string; + stellarPrivateKey: string; serverUrl: string; endpointPath: string; + evmNetwork: string; + evmRpcUrl: string; } export interface ServerConfig { @@ -25,8 +28,10 @@ export interface ServerConfig { evmPayTo: string; svmPayTo: string; aptosPayTo: string; + stellarPayTo: string; networks: NetworkSet; facilitatorUrl?: string; + mockFacilitatorUrl?: string; } export interface ServerProxy { @@ -48,6 +53,11 @@ export interface TestEndpoint { requiresPayment?: boolean; protocolFamily?: ProtocolFamily; transferMethod?: TransferMethod; + extensions?: string[]; + /** True for Permit2 standard/direct settle - requires pre-approval (approve before test, not revoke) */ + permit2Direct?: boolean; + /** True for endpoints that require Permit2 revocation + fund/drain state setup before the first test (coldstart). */ + coldstart?: boolean; health?: boolean; close?: boolean; } diff --git a/e2e/test.ts b/e2e/test.ts index 8d0fcb17e1..4349597506 100644 --- a/e2e/test.ts +++ b/e2e/test.ts @@ -1,6 +1,10 @@ import { config } from 'dotenv'; -import { spawn, execSync } from 'child_process'; +import { spawn, execSync, ChildProcess } from 'child_process'; import { writeFileSync } from 'fs'; +import { join } from 'path'; +import { createWalletClient, createPublicClient, http, parseEther, formatEther } from 'viem'; +import { privateKeyToAccount } from 'viem/accounts'; +import { base, baseSepolia } from 'viem/chains'; import { TestDiscovery } from './src/discovery'; import { ClientConfig, ScenarioResult, ServerConfig, TestScenario } from './src/types'; import { config as loggerConfig, log, verboseLog, errorLog, close as closeLogger, createComboLogger } from './src/logger'; @@ -15,14 +19,24 @@ import { Semaphore, FacilitatorLock } from './src/concurrency'; import { FacilitatorManager } from './src/facilitators/facilitator-manager'; import { waitForHealth } from './src/health'; +// Base Sepolia token addresses used by permit2 E2E tests +const USDC_BASE_SEPOLIA = '0x036CbD53842c5426634e7929541eC2318f3dCF7e'; +const MOCK_ERC20_BASE_SEPOLIA = '0xeED520980fC7C7B4eB379B96d61CEdea2423005a'; + /** - * Run Permit2 setup script to ensure the client wallet has approved the Permit2 contract + * Approve Permit2 so that the standard/direct settle path can be exercised. + * Grants unlimited Permit2 allowance for the given token (or USDC by default). */ -async function setupPermit2Approval(): Promise { +async function approvePermit2Approval(tokenAddress?: string): Promise { return new Promise((resolve) => { - log('\n🔑 Setting up Permit2 approval for EVM client wallet...'); + const label = tokenAddress ? `token ${tokenAddress}` : 'USDC (default)'; + verboseLog(` 🔓 Approving Permit2 for ${label}...`); - const child = spawn('pnpm', ['permit2:approve'], { + const args = ['scripts/permit2-approval.ts', 'approve']; + if (tokenAddress) { + args.push(tokenAddress); + } + const child = spawn('tsx', args, { cwd: process.cwd(), stdio: 'pipe', shell: true, @@ -41,10 +55,10 @@ async function setupPermit2Approval(): Promise { child.on('close', (code) => { if (code === 0) { - log(' ✅ Permit2 approval setup complete'); + verboseLog(' ✅ Permit2 approval granted'); resolve(true); } else { - errorLog(` ❌ Permit2 setup failed (exit code ${code})`); + errorLog(` ❌ Permit2 approve failed (exit code ${code})`); if (stderr) { errorLog(` Error: ${stderr}`); } @@ -53,21 +67,27 @@ async function setupPermit2Approval(): Promise { }); child.on('error', (error) => { - errorLog(` ❌ Failed to run Permit2 setup: ${error.message}`); + errorLog(` ❌ Failed to run Permit2 approve: ${error.message}`); resolve(false); }); }); } /** - * Revoke Permit2 approval so that EIP-2612 gas sponsoring extension is exercised. - * Sets the Permit2 allowance to 0, forcing the client to use the EIP-2612 permit path. + * Revoke Permit2 approval so that gas sponsoring extensions are exercised. + * Sets the Permit2 allowance to 0 for the given token (or USDC by default), + * forcing the client into the EIP-2612 or ERC-20 approval extension path. */ -async function revokePermit2Approval(): Promise { +async function revokePermit2Approval(tokenAddress?: string): Promise { return new Promise((resolve) => { - verboseLog(' 🔓 Revoking Permit2 approval for EIP-2612 test...'); + const label = tokenAddress ? `token ${tokenAddress}` : 'USDC (default)'; + verboseLog(` 🔓 Revoking Permit2 approval for ${label}...`); - const child = spawn('pnpm', ['permit2:revoke'], { + const args = ['scripts/permit2-approval.ts', 'revoke']; + if (tokenAddress) { + args.push(tokenAddress); + } + const child = spawn('tsx', args, { cwd: process.cwd(), stdio: 'pipe', shell: true, @@ -104,6 +124,133 @@ async function revokePermit2Approval(): Promise { }); } +/** + * Shared EVM clients for the ETH sandwich helpers. + * Lazily initialised on first use so that missing env vars don't blow up + * non-EVM test runs. + */ +function getEvmClients() { + const evmNetwork = process.env.EVM_NETWORK || 'eip155:84532'; + const evmRpcUrl = process.env.EVM_RPC_URL; + const evmChain = evmNetwork === 'eip155:8453' ? base : baseSepolia; + + const facilitatorKey = process.env.FACILITATOR_EVM_PRIVATE_KEY; + const clientKey = process.env.CLIENT_EVM_PRIVATE_KEY; + if (!facilitatorKey || !clientKey) { + throw new Error('FACILITATOR_EVM_PRIVATE_KEY and CLIENT_EVM_PRIVATE_KEY must be set'); + } + + const facilitatorAccount = privateKeyToAccount(facilitatorKey as `0x${string}`); + const clientAccount = privateKeyToAccount(clientKey as `0x${string}`); + + const publicClient = createPublicClient({ + chain: evmChain, + transport: http(evmRpcUrl), + }); + const facilitatorWallet = createWalletClient({ + account: facilitatorAccount, + chain: evmChain, + transport: http(evmRpcUrl), + }); + const clientWallet = createWalletClient({ + account: clientAccount, + chain: evmChain, + transport: http(evmRpcUrl), + }); + + return { publicClient, facilitatorWallet, clientWallet, facilitatorAccount, clientAccount }; +} + +const REVOKE_FUND_AMOUNT = parseEther('0.001'); + +/** + * Send a small amount of ETH from the facilitator wallet to the client wallet + * so the client can pay gas for Permit2 revocation transactions. + */ +async function fundClientForRevoke(): Promise { + const { publicClient, facilitatorWallet, facilitatorAccount, clientAccount } = getEvmClients(); + + const clientBalance = await publicClient.getBalance({ address: clientAccount.address }); + if (clientBalance >= REVOKE_FUND_AMOUNT) { + verboseLog(` ℹ️ Client already has ${formatEther(clientBalance)} ETH, skipping fund`); + return true; + } + + const facilitatorBalance = await publicClient.getBalance({ address: facilitatorAccount.address }); + if (facilitatorBalance < REVOKE_FUND_AMOUNT) { + errorLog(` ❌ Facilitator wallet ${facilitatorAccount.address} has insufficient ETH (${formatEther(facilitatorBalance)}) to fund client for revoke.`); + errorLog(` Please fund the facilitator wallet with testnet ETH (need at least ${formatEther(REVOKE_FUND_AMOUNT)} ETH).`); + return false; + } + + verboseLog(` 💸 Funding client ${clientAccount.address} with ${formatEther(REVOKE_FUND_AMOUNT)} ETH for revoke...`); + // Retry on nonce errors: load-balanced RPCs can return stale pending nonces, + // especially when the facilitator SERVICE process (same private key) is settling + // payments concurrently. A fresh nonce fetch + small delay usually resolves it. + let lastErr: Error | null = null; + for (let attempt = 0; attempt < 3; attempt++) { + if (attempt > 0) await new Promise(r => setTimeout(r, 500)); + try { + const nonce = await publicClient.getTransactionCount({ + address: facilitatorAccount.address, + blockTag: 'pending', + }); + const hash = await facilitatorWallet.sendTransaction({ + to: clientAccount.address, + value: REVOKE_FUND_AMOUNT, + nonce, + }); + verboseLog(` ✅ Funded client (tx: ${hash})`); + return true; + } catch (err) { + lastErr = err instanceof Error ? err : new Error(String(err)); + const isNonceError = lastErr.message.toLowerCase().includes('nonce'); + if (!isNonceError) break; + } + } + const errLines = lastErr!.message.split('\n'); + errorLog(` ❌ Failed to fund client for revoke: ${errLines[0].trim()}`); + if (errLines.length > 1) verboseLog(errLines.slice(1).join('\n')); + return false; +} + +/** + * Drain all ETH from the client wallet back to the facilitator wallet, + * leaving the client with ~0 ETH so the gas sponsoring funding step is + * exercised during the test. + */ +async function drainClientETH(): Promise { + try { + const { publicClient, clientWallet, facilitatorAccount, clientAccount } = getEvmClients(); + + // Use pending balance so we see any in-flight fund transaction that hasn't confirmed yet. + const balance = await publicClient.getBalance({ address: clientAccount.address, blockTag: 'pending' }); + + // Reserve enough for gas. On L2s getGasPrice() returns a tiny value but + // viem's sendTransaction uses a higher maxFeePerGas with safety margin. + // Use a generous fixed buffer to avoid "insufficient funds" from the + // estimateGas pre-check. + const GAS_RESERVE = parseEther('0.0001'); + const sendAmount = balance - GAS_RESERVE; + + if (sendAmount <= 0n) { + verboseLog(` ℹ️ Client balance (${formatEther(balance)} ETH) too small to drain, leaving as dust`); + return true; + } + + verboseLog(` 💸 Draining ${formatEther(sendAmount)} ETH from client back to facilitator...`); + const hash = await clientWallet.sendTransaction({ + to: facilitatorAccount.address, + value: sendAmount, + }); + verboseLog(` ✅ Drained client ETH (tx: ${hash})`); + return true; + } catch (err) { + errorLog(` ❌ Failed to drain client ETH: ${err instanceof Error ? err.message : err}`); + return false; + } +} + // Load environment variables config(); @@ -237,13 +384,15 @@ async function runTest() { const serverEvmAddress = process.env.SERVER_EVM_ADDRESS; const serverSvmAddress = process.env.SERVER_SVM_ADDRESS; const serverAptosAddress = process.env.SERVER_APTOS_ADDRESS; + const serverStellarAddress = process.env.SERVER_STELLAR_ADDRESS; const clientEvmPrivateKey = process.env.CLIENT_EVM_PRIVATE_KEY; const clientSvmPrivateKey = process.env.CLIENT_SVM_PRIVATE_KEY; const clientAptosPrivateKey = process.env.CLIENT_APTOS_PRIVATE_KEY; + const clientStellarPrivateKey = process.env.CLIENT_STELLAR_PRIVATE_KEY; const facilitatorEvmPrivateKey = process.env.FACILITATOR_EVM_PRIVATE_KEY; const facilitatorSvmPrivateKey = process.env.FACILITATOR_SVM_PRIVATE_KEY; const facilitatorAptosPrivateKey = process.env.FACILITATOR_APTOS_PRIVATE_KEY; - + const facilitatorStellarPrivateKey = process.env.FACILITATOR_STELLAR_PRIVATE_KEY; if (!serverEvmAddress || !serverSvmAddress || !clientEvmPrivateKey || !clientSvmPrivateKey || !facilitatorEvmPrivateKey || !facilitatorSvmPrivateKey) { errorLog('❌ Missing required environment variables:'); errorLog(' SERVER_EVM_ADDRESS, SERVER_SVM_ADDRESS, CLIENT_EVM_PRIVATE_KEY, CLIENT_SVM_PRIVATE_KEY, FACILITATOR_EVM_PRIVATE_KEY, and FACILITATOR_SVM_PRIVATE_KEY must be set'); @@ -320,6 +469,7 @@ async function runTest() { log(` EVM: ${networks.evm.name} (${networks.evm.caip2})`); log(` SVM: ${networks.svm.name} (${networks.svm.caip2})`); log(` APTOS: ${networks.aptos.name} (${networks.aptos.caip2})`); + log(` STELLAR: ${networks.stellar.name} (${networks.stellar.caip2})`); if (networkMode === 'mainnet') { log('\n⚠️ WARNING: Running on MAINNET - real funds will be used!'); @@ -353,31 +503,40 @@ async function runTest() { } log(''); - // Auto-detect Permit2 scenarios + // Branch coverage assertions for EVM scenarios + const evmScenarios = filteredScenarios.filter(s => s.protocolFamily === 'evm'); + if (evmScenarios.length > 0) { + const hasEip3009 = evmScenarios.some(s => (s.endpoint.transferMethod || 'eip3009') === 'eip3009'); + const hasPermit2 = evmScenarios.some(s => s.endpoint.transferMethod === 'permit2'); + const hasPermit2Direct = evmScenarios.some(s => s.endpoint.transferMethod === 'permit2' && s.endpoint.permit2Direct === true); + const hasPermit2Eip2612 = evmScenarios.some(s => s.endpoint.transferMethod === 'permit2' && !s.endpoint.extensions?.includes('erc20ApprovalGasSponsoring') && !s.endpoint.permit2Direct); + const hasPermit2Erc20 = evmScenarios.some(s => s.endpoint.transferMethod === 'permit2' && s.endpoint.extensions?.includes('erc20ApprovalGasSponsoring')); + + const hasUpto = evmScenarios.some(s => s.endpoint.transferMethod === 'upto'); + const hasUptoDirect = evmScenarios.some(s => s.endpoint.transferMethod === 'upto' && s.endpoint.permit2Direct === true); + const hasUptoEip2612 = evmScenarios.some(s => s.endpoint.transferMethod === 'upto' && !s.endpoint.extensions?.includes('erc20ApprovalGasSponsoring') && !s.endpoint.permit2Direct); + const hasUptoErc20 = evmScenarios.some(s => s.endpoint.transferMethod === 'upto' && s.endpoint.extensions?.includes('erc20ApprovalGasSponsoring')); + + log('🔍 EVM Branch Coverage Check:'); + log(` EIP-3009 route: ${hasEip3009 ? '✅' : '❌ MISSING'}`); + log(` Permit2 route: ${hasPermit2 ? '✅' : '❌ MISSING'}`); + log(` Permit2+direct settle: ${hasPermit2Direct ? '✅' : '⚠️ not found'}`); + log(` Permit2+EIP2612 route: ${hasPermit2Eip2612 ? '✅' : '⚠️ not found (may be covered by permit2 route if eip2612 extension enabled)'}`); + log(` Permit2+ERC20 route: ${hasPermit2Erc20 ? '✅' : '⚠️ not found'}`); + log(` Upto route: ${hasUpto ? '✅' : '⚠️ not found'}`); + log(` Upto+direct settle: ${hasUptoDirect ? '✅' : '⚠️ not found'}`); + log(` Upto+EIP2612 route: ${hasUptoEip2612 ? '✅' : '⚠️ not found'}`); + log(` Upto+ERC20 route: ${hasUptoErc20 ? '✅' : '⚠️ not found'}`); + log(''); + } + + // Auto-detect Permit2 scenarios (upto uses Permit2 under the hood) const hasPermit2Scenarios = filteredScenarios.some( - (s) => s.endpoint.transferMethod === 'permit2' + (s) => s.endpoint.transferMethod === 'permit2' || s.endpoint.transferMethod === 'upto' ); - // Check if eip2612GasSponsoring extension should be tested - const hasEip2612Extension = selectedExtensions?.includes('eip2612GasSponsoring') ?? false; - if (hasPermit2Scenarios) { - if (hasEip2612Extension) { - log('🔐 Permit2 scenarios detected with eip2612GasSponsoring extension'); - } else { - // Standard permit2 flow: ensure approval exists - log('🔐 Permit2 scenarios detected - checking approval...'); - const setupSuccess = await setupPermit2Approval(); - if (!setupSuccess) { - errorLog( - '\n❌ Failed to setup Permit2 approval. Cannot continue with Permit2 tests.' - ); - errorLog( - '💡 Make sure CLIENT_EVM_PRIVATE_KEY is set and the wallet has USDC.' - ); - process.exit(1); - } - } + log('🔐 Permit2 scenarios detected — revoke before gas-sponsored tests, approve before permit2-direct tests'); } // Collect unique facilitators and servers @@ -396,7 +555,21 @@ async function runTest() { const missingEnvVars: { facilitatorName: string; missingVars: string[] }[] = []; // Environment variables managed by the test framework (don't require user to set) - const systemManagedVars = new Set(['PORT', 'EVM_PRIVATE_KEY', 'SVM_PRIVATE_KEY', 'APTOS_PRIVATE_KEY', 'EVM_NETWORK', 'SVM_NETWORK', 'APTOS_NETWORK', 'EVM_RPC_URL', 'SVM_RPC_URL', 'APTOS_RPC_URL']); + const systemManagedVars = new Set([ + 'PORT', + 'EVM_PRIVATE_KEY', + 'SVM_PRIVATE_KEY', + 'APTOS_PRIVATE_KEY', + 'STELLAR_PRIVATE_KEY', + 'EVM_NETWORK', + 'SVM_NETWORK', + 'APTOS_NETWORK', + 'STELLAR_NETWORK', + 'EVM_RPC_URL', + 'SVM_RPC_URL', + 'APTOS_RPC_URL', + 'STELLAR_RPC_URL', + ]); for (const [facilitatorName, facilitator] of uniqueFacilitators) { const requiredVars = facilitator.config.environment?.required || []; @@ -523,6 +696,51 @@ async function runTest() { log(` ✅ Facilitator ${facilitatorName} ready at ${url}`); } + // Start mock facilitator (claims to support everything, used as fallback so + // servers with routes unsupported by the real facilitator can still start) + const mockFacilitatorPort = currentPort++; + log(`\n🎭 Starting mock facilitator on port ${mockFacilitatorPort}...`); + const mockFacilitatorProcess: ChildProcess = spawn( + 'npx', ['tsx', 'index.ts'], + { + cwd: join(process.cwd(), 'mock-facilitator'), + env: { + ...process.env, + PORT: mockFacilitatorPort.toString(), + EVM_NETWORK: networks.evm.caip2, + SVM_NETWORK: networks.svm.caip2, + APTOS_NETWORK: networks.aptos.caip2, + STELLAR_NETWORK: networks.stellar.caip2, + }, + stdio: 'pipe', + }, + ); + mockFacilitatorProcess.stderr?.on('data', (data: Buffer) => { + verboseLog(`[mock-facilitator] stderr: ${data.toString().trim()}`); + }); + mockFacilitatorProcess.stdout?.on('data', (data: Buffer) => { + verboseLog(`[mock-facilitator] stdout: ${data.toString().trim()}`); + }); + + const mockFacilitatorUrl = `http://localhost:${mockFacilitatorPort}`; + const mockHealthy = await waitForHealth( + async () => { + try { + const res = await fetch(`${mockFacilitatorUrl}/health`); + return { success: res.ok }; + } catch { + return { success: false }; + } + }, + { label: 'Mock facilitator' }, + ); + if (!mockHealthy) { + log('❌ Failed to start mock facilitator'); + mockFacilitatorProcess.kill(); + process.exit(1); + } + log(` ✅ Mock facilitator ready at ${mockFacilitatorUrl}`); + log('\n✅ All facilitators are ready! Servers will be started/restarted as needed per test scenario.\n'); log(`🔧 Server/Facilitator combinations: ${serverFacilitatorCombos.length}`); @@ -551,8 +769,11 @@ async function runTest() { evmPrivateKey: clientEvmPrivateKey!, svmPrivateKey: clientSvmPrivateKey!, aptosPrivateKey: clientAptosPrivateKey || '', + stellarPrivateKey: clientStellarPrivateKey || '', serverUrl: `http://localhost:${port}`, endpointPath: scenario.endpoint.path, + evmNetwork: networks.evm.caip2, + evmRpcUrl: networks.evm.rpcUrl, }; try { @@ -630,14 +851,17 @@ async function runTest() { const facilitatorConfig = facilitatorName ? uniqueFacilitators.get(facilitatorName)?.config : undefined; const facilitatorSupportsAptos = facilitatorConfig?.protocolFamilies?.includes('aptos') ?? false; + const facilitatorSupportsStellar = facilitatorConfig?.protocolFamilies?.includes('stellar') ?? false; const serverConfig: ServerConfig = { port, evmPayTo: serverEvmAddress!, svmPayTo: serverSvmAddress!, aptosPayTo: facilitatorSupportsAptos ? (serverAptosAddress || '') : '', + stellarPayTo: facilitatorSupportsStellar ? (serverStellarAddress || '') : '', networks, facilitatorUrl, + mockFacilitatorUrl, }; const started = await startServer(serverProxy, serverConfig); @@ -657,20 +881,43 @@ async function runTest() { cLog.log(` ✅ Server ${serverName} ready`); const results: DetailedTestResult[] = []; + // Track which endpoint paths have already been "cold started" in this combo. + // The first test for each path runs the full state-setup (fund/revoke/drain); + // subsequent tests for the same path skip the setup and run warm. + const coldStartedEndpoints = new Set(); try { for (const scenario of scenarios) { const tn = nextTestNumber(); const isEvm = scenario.protocolFamily === 'evm'; - if (hasEip2612Extension && scenario.endpoint.transferMethod === 'permit2') { - await revokePermit2Approval(); + if (scenario.endpoint.permit2Direct) { + await approvePermit2Approval(USDC_BASE_SEPOLIA); + } else if (scenario.endpoint.coldstart) { + const endpointKey = scenario.endpoint.path; + if (!coldStartedEndpoints.has(endpointKey)) { + coldStartedEndpoints.add(endpointKey); + const token = + scenario.endpoint.extensions?.includes('erc20ApprovalGasSponsoring') + ? MOCK_ERC20_BASE_SEPOLIA + : USDC_BASE_SEPOLIA; + await fundClientForRevoke(); + // Give fund tx 1s to propagate before submitting revoke (from client wallet) + await new Promise(resolve => setTimeout(resolve, 1000)); + await revokePermit2Approval(token); + // Give revoke tx 1s to propagate before drain reads pending balance + await new Promise(resolve => setTimeout(resolve, 1000)); + await drainClientETH(); + // Wait for RPC nonce propagation across load-balanced nodes before the + // test client (which may use a separate RPC connection) queries the nonce. + await new Promise(resolve => setTimeout(resolve, 1500)); + } } if (isEvm && facilitatorName && evmLock) { const releaseLock = await evmLock.acquire(facilitatorName); try { results.push(await runSingleTest(scenario, port, tn, cLog)); - await new Promise(resolve => setTimeout(resolve, 2000)); + await new Promise(resolve => setTimeout(resolve, 1000)); } finally { releaseLock(); } @@ -679,230 +926,232 @@ async function runTest() { } } } finally { - cLog.verboseLog(` 🛑 Stopping ${serverName} (finished combo)`); - await serverProxy.stop(); -} + cLog.verboseLog(` 🛑 Stopping ${serverName} (finished combo)`); + await serverProxy.stop(); + } -return results; + return results; } -// ── Unified execution: concurrency=1 for sequential, N for parallel ── -const effectiveConcurrency = parsedArgs.parallel ? parsedArgs.concurrency : 1; -const evmLock = parsedArgs.parallel ? new FacilitatorLock() : null; -const semaphore = new Semaphore(effectiveConcurrency); + // ── Unified execution: concurrency=1 for sequential, N for parallel ── + const effectiveConcurrency = parsedArgs.parallel ? parsedArgs.concurrency : 1; + const evmLock = parsedArgs.parallel ? new FacilitatorLock() : null; + const semaphore = new Semaphore(effectiveConcurrency); -let globalTestNumber = 0; -const nextTestNumber = () => ++globalTestNumber; + let globalTestNumber = 0; + const nextTestNumber = () => ++globalTestNumber; -const comboPromises = serverFacilitatorCombos.map(async (combo) => { - const release = await semaphore.acquire(); - try { - return await executeCombo(combo, evmLock, nextTestNumber); - } finally { - release(); - } -}); + const comboPromises = serverFacilitatorCombos.map(async (combo) => { + const release = await semaphore.acquire(); + try { + return await executeCombo(combo, evmLock, nextTestNumber); + } finally { + release(); + } + }); -testResults = (await Promise.all(comboPromises)).flat(); + testResults = (await Promise.all(comboPromises)).flat(); -// Run discovery validation before cleanup (while facilitators are still running) -const facilitatorsWithConfig = Array.from(uniqueFacilitators.values()).map((f: any) => ({ - proxy: facilitatorManagers.get(f.name)!.getProxy(), - config: f.config, -})); + // Run discovery validation before cleanup (while facilitators are still running) + const facilitatorsWithConfig = Array.from(uniqueFacilitators.values()).map((f: any) => ({ + proxy: facilitatorManagers.get(f.name)!.getProxy(), + config: f.config, + })); -const serversArray = Array.from(uniqueServers.values()); + const serversArray = Array.from(uniqueServers.values()); -// Build a serverName→port map for discovery validation (first combo per server). -const discoveryServerPorts = new Map(); -for (const combo of serverFacilitatorCombos) { - if (!discoveryServerPorts.has(combo.serverName)) { - discoveryServerPorts.set(combo.serverName, combo.port); + // Build a serverName→port map for discovery validation (first combo per server). + const discoveryServerPorts = new Map(); + for (const combo of serverFacilitatorCombos) { + if (!discoveryServerPorts.has(combo.serverName)) { + discoveryServerPorts.set(combo.serverName, combo.port); + } } -} -// Run discovery validation if bazaar extension is enabled -const showBazaarOutput = shouldShowExtensionOutput('bazaar', selectedExtensions); -if (showBazaarOutput && shouldRunDiscoveryValidation(facilitatorsWithConfig, serversArray)) { - log('\n🔍 Running Bazaar Discovery Validation...\n'); - await handleDiscoveryValidation( - facilitatorsWithConfig, - serversArray, - discoveryServerPorts, - facilitatorServerMap - ); -} + // Run discovery validation if bazaar extension is enabled + const showBazaarOutput = shouldShowExtensionOutput('bazaar', selectedExtensions); + if (showBazaarOutput && shouldRunDiscoveryValidation(facilitatorsWithConfig, serversArray)) { + log('\n🔍 Running Bazaar Discovery Validation...\n'); + await handleDiscoveryValidation( + facilitatorsWithConfig, + serversArray, + discoveryServerPorts, + facilitatorServerMap + ); + } -// Clean up facilitators (servers already stopped in test loop for both modes) -log('\n🧹 Cleaning up...'); + // Clean up facilitators (servers already stopped in test loop for both modes) + log('\n🧹 Cleaning up...'); -// Stop all facilitators -const facilitatorStopPromises: Promise[] = []; -for (const [facilitatorName, manager] of facilitatorManagers) { - log(` 🛑 Stopping facilitator: ${facilitatorName}`); - facilitatorStopPromises.push(manager.stop()); -} -await Promise.all(facilitatorStopPromises); - -// Calculate totals -const passed = testResults.filter(r => r.passed).length; -const failed = testResults.filter(r => !r.passed).length; - -// Summary -log(''); -log('📊 Test Summary'); -log('=============='); -log(`🌐 Network: ${networkMode} (${getNetworkModeDescription(networkMode)})`); -log(`✅ Passed: ${passed}`); -log(`❌ Failed: ${failed}`); -log(`📈 Total: ${passed + failed}`); -log(''); - -// Detailed results table -log('📋 Detailed Test Results'); -log('========================'); -log(''); - -// Group by status -const passedTests = testResults.filter(r => r.passed); -const failedTests = testResults.filter(r => !r.passed); - -if (passedTests.length > 0) { - log('✅ PASSED TESTS:'); + // Stop all facilitators + const facilitatorStopPromises: Promise[] = []; + for (const [facilitatorName, manager] of facilitatorManagers) { + log(` 🛑 Stopping facilitator: ${facilitatorName}`); + facilitatorStopPromises.push(manager.stop()); + } + log(' 🛑 Stopping mock facilitator'); + mockFacilitatorProcess.kill(); + await Promise.all(facilitatorStopPromises); + + // Calculate totals + const passed = testResults.filter(r => r.passed).length; + const failed = testResults.filter(r => !r.passed).length; + + // Summary log(''); - passedTests.forEach(test => { - log(` #${test.testNumber.toString().padStart(2, ' ')}: ${test.client} → ${test.server} → ${test.endpoint}`); - log(` Facilitator: ${test.facilitator}`); - if (test.network) { - log(` Network: ${test.network}`); - } - if (test.transaction) { - log(` Tx: ${test.transaction}`); - } - }); + log('📊 Test Summary'); + log('=============='); + log(`🌐 Network: ${networkMode} (${getNetworkModeDescription(networkMode)})`); + log(`✅ Passed: ${passed}`); + log(`❌ Failed: ${failed}`); + log(`📈 Total: ${passed + failed}`); log(''); -} -if (failedTests.length > 0) { - log('❌ FAILED TESTS:'); + // Detailed results table + log('📋 Detailed Test Results'); + log('========================'); log(''); - failedTests.forEach(test => { - log(` #${test.testNumber.toString().padStart(2, ' ')}: ${test.client} → ${test.server} → ${test.endpoint}`); - log(` Facilitator: ${test.facilitator}`); - if (test.network) { - log(` Network: ${test.network}`); - } - log(` Error: ${test.error || 'Unknown error'}`); + + // Group by status + const passedTests = testResults.filter(r => r.passed); + const failedTests = testResults.filter(r => !r.passed); + + if (passedTests.length > 0) { + log('✅ PASSED TESTS:'); + log(''); + passedTests.forEach(test => { + log(` #${test.testNumber.toString().padStart(2, ' ')}: ${test.client} → ${test.server} → ${test.endpoint}`); + log(` Facilitator: ${test.facilitator}`); + if (test.network) { + log(` Network: ${test.network}`); + } + if (test.transaction) { + log(` Tx: ${test.transaction}`); + } + }); + log(''); + } + + if (failedTests.length > 0) { + log('❌ FAILED TESTS:'); + log(''); + failedTests.forEach(test => { + log(` #${test.testNumber.toString().padStart(2, ' ')}: ${test.client} → ${test.server} → ${test.endpoint}`); + log(` Facilitator: ${test.facilitator}`); + if (test.network) { + log(` Network: ${test.network}`); + } + log(` Error: ${test.error || 'Unknown error'}`); + }); + log(''); + } + + // Breakdown by facilitator + const facilitatorBreakdown = testResults.reduce((acc, test) => { + const key = test.facilitator; + if (!acc[key]) acc[key] = { passed: 0, failed: 0 }; + if (test.passed) acc[key].passed++; + else acc[key].failed++; + return acc; + }, {} as Record); + + log('📊 Breakdown by Facilitator:'); + Object.entries(facilitatorBreakdown).forEach(([facilitator, stats]) => { + const total = stats.passed + stats.failed; + const passRate = total > 0 ? Math.round((stats.passed / total) * 100) : 0; + log(` ${facilitator.padEnd(15)} ✅ ${stats.passed} / ❌ ${stats.failed} (${passRate}%)`); }); log(''); -} -// Breakdown by facilitator -const facilitatorBreakdown = testResults.reduce((acc, test) => { - const key = test.facilitator; - if (!acc[key]) acc[key] = { passed: 0, failed: 0 }; - if (test.passed) acc[key].passed++; - else acc[key].failed++; - return acc; -}, {} as Record); - -log('📊 Breakdown by Facilitator:'); -Object.entries(facilitatorBreakdown).forEach(([facilitator, stats]) => { - const total = stats.passed + stats.failed; - const passRate = total > 0 ? Math.round((stats.passed / total) * 100) : 0; - log(` ${facilitator.padEnd(15)} ✅ ${stats.passed} / ❌ ${stats.failed} (${passRate}%)`); -}); -log(''); - -// Breakdown by server -const serverBreakdown = testResults.reduce((acc, test) => { - const key = test.server; - if (!acc[key]) acc[key] = { passed: 0, failed: 0 }; - if (test.passed) acc[key].passed++; - else acc[key].failed++; - return acc; -}, {} as Record); - -log('📊 Breakdown by Server:'); -Object.entries(serverBreakdown).forEach(([server, stats]) => { - const total = stats.passed + stats.failed; - const passRate = total > 0 ? Math.round((stats.passed / total) * 100) : 0; - log(` ${server.padEnd(20)} ✅ ${stats.passed} / ❌ ${stats.failed} (${passRate}%)`); -}); -log(''); - -// Breakdown by client -const clientBreakdown = testResults.reduce((acc, test) => { - const key = test.client; - if (!acc[key]) acc[key] = { passed: 0, failed: 0 }; - if (test.passed) acc[key].passed++; - else acc[key].failed++; - return acc; -}, {} as Record); - -log('📊 Breakdown by Client:'); -Object.entries(clientBreakdown).forEach(([client, stats]) => { - const total = stats.passed + stats.failed; - const passRate = total > 0 ? Math.round((stats.passed / total) * 100) : 0; - log(` ${client.padEnd(20)} ✅ ${stats.passed} / ❌ ${stats.failed} (${passRate}%)`); -}); -log(''); - -// Protocol family breakdown -const protocolBreakdown = testResults.reduce((acc, test) => { - const key = test.protocolFamily; - if (!acc[key]) acc[key] = { passed: 0, failed: 0 }; - if (test.passed) acc[key].passed++; - else acc[key].failed++; - return acc; -}, {} as Record); - -if (Object.keys(protocolBreakdown).length > 1) { - log('📊 Protocol Family Breakdown:'); - Object.entries(protocolBreakdown).forEach(([protocol, stats]) => { + // Breakdown by server + const serverBreakdown = testResults.reduce((acc, test) => { + const key = test.server; + if (!acc[key]) acc[key] = { passed: 0, failed: 0 }; + if (test.passed) acc[key].passed++; + else acc[key].failed++; + return acc; + }, {} as Record); + + log('📊 Breakdown by Server:'); + Object.entries(serverBreakdown).forEach(([server, stats]) => { const total = stats.passed + stats.failed; - log(` ${protocol.toUpperCase()}: ✅ ${stats.passed} / ❌ ${stats.failed} / 📈 ${total} total`); + const passRate = total > 0 ? Math.round((stats.passed / total) * 100) : 0; + log(` ${server.padEnd(20)} ✅ ${stats.passed} / ❌ ${stats.failed} (${passRate}%)`); }); log(''); -} -// Write structured JSON output if requested -if (parsedArgs.outputJson) { - const breakdown = (results: DetailedTestResult[], key: keyof DetailedTestResult) => - results.reduce((acc, test) => { - const k = String(test[key]); - if (!acc[k]) acc[k] = { passed: 0, failed: 0 }; - if (test.passed) acc[k].passed++; - else acc[k].failed++; - return acc; - }, {} as Record); - - const jsonOutput = { - summary: { - total: passed + failed, - passed, - failed, - networkMode, - }, - results: testResults, - breakdowns: { - byFacilitator: breakdown(testResults, 'facilitator'), - byServer: breakdown(testResults, 'server'), - byClient: breakdown(testResults, 'client'), - byProtocolFamily: breakdown(testResults, 'protocolFamily'), - }, - }; + // Breakdown by client + const clientBreakdown = testResults.reduce((acc, test) => { + const key = test.client; + if (!acc[key]) acc[key] = { passed: 0, failed: 0 }; + if (test.passed) acc[key].passed++; + else acc[key].failed++; + return acc; + }, {} as Record); + + log('📊 Breakdown by Client:'); + Object.entries(clientBreakdown).forEach(([client, stats]) => { + const total = stats.passed + stats.failed; + const passRate = total > 0 ? Math.round((stats.passed / total) * 100) : 0; + log(` ${client.padEnd(20)} ✅ ${stats.passed} / ❌ ${stats.failed} (${passRate}%)`); + }); + log(''); - writeFileSync(parsedArgs.outputJson, JSON.stringify(jsonOutput, null, 2)); - log(`📄 JSON results written to ${parsedArgs.outputJson}`); -} + // Protocol family breakdown + const protocolBreakdown = testResults.reduce((acc, test) => { + const key = test.protocolFamily; + if (!acc[key]) acc[key] = { passed: 0, failed: 0 }; + if (test.passed) acc[key].passed++; + else acc[key].failed++; + return acc; + }, {} as Record); + + if (Object.keys(protocolBreakdown).length > 1) { + log('📊 Protocol Family Breakdown:'); + Object.entries(protocolBreakdown).forEach(([protocol, stats]) => { + const total = stats.passed + stats.failed; + log(` ${protocol.toUpperCase()}: ✅ ${stats.passed} / ❌ ${stats.failed} / 📈 ${total} total`); + }); + log(''); + } -// Close logger -closeLogger(); + // Write structured JSON output if requested + if (parsedArgs.outputJson) { + const breakdown = (results: DetailedTestResult[], key: keyof DetailedTestResult) => + results.reduce((acc, test) => { + const k = String(test[key]); + if (!acc[k]) acc[k] = { passed: 0, failed: 0 }; + if (test.passed) acc[k].passed++; + else acc[k].failed++; + return acc; + }, {} as Record); + + const jsonOutput = { + summary: { + total: passed + failed, + passed, + failed, + networkMode, + }, + results: testResults, + breakdowns: { + byFacilitator: breakdown(testResults, 'facilitator'), + byServer: breakdown(testResults, 'server'), + byClient: breakdown(testResults, 'client'), + byProtocolFamily: breakdown(testResults, 'protocolFamily'), + }, + }; -if (failed > 0) { - process.exit(1); -} + writeFileSync(parsedArgs.outputJson, JSON.stringify(jsonOutput, null, 2)); + log(`📄 JSON results written to ${parsedArgs.outputJson}`); + } + + // Close logger + closeLogger(); + + if (failed > 0) { + process.exit(1); + } } // Run the test diff --git a/examples/go/clients/advanced/all_networks.go b/examples/go/clients/advanced/all_networks.go index be399f1bd0..229b102536 100644 --- a/examples/go/clients/advanced/all_networks.go +++ b/examples/go/clients/advanced/all_networks.go @@ -38,7 +38,7 @@ func runAllNetworksExample(ctx context.Context, evmPrivateKey, svmPrivateKey, ur if err != nil { return fmt.Errorf("failed to create EVM signer: %w", err) } - client.Register("eip155:*", evm.NewExactEvmScheme(evmSigner)) + client.Register("eip155:*", evm.NewExactEvmScheme(evmSigner, nil)) fmt.Printf("✅ Registered EVM networks (eip155:*)\n") } @@ -106,7 +106,13 @@ func printPaymentDetails(headers http.Header) { } fmt.Println("💰 Payment Details:") - fmt.Printf(" Transaction: %s\n", settleResp.Transaction) + fmt.Printf(" Success: %v\n", settleResp.Success) + if settleResp.ErrorReason != "" { + fmt.Printf(" ErrorReason: %s\n", settleResp.ErrorReason) + } + if settleResp.Transaction != "" { + fmt.Printf(" Transaction: %s\n", settleResp.Transaction) + } fmt.Printf(" Network: %s\n", settleResp.Network) fmt.Printf(" Payer: %s\n", settleResp.Payer) } diff --git a/examples/go/clients/advanced/custom_transport.go b/examples/go/clients/advanced/custom_transport.go index 7b8ee4c2c9..2206722bdd 100644 --- a/examples/go/clients/advanced/custom_transport.go +++ b/examples/go/clients/advanced/custom_transport.go @@ -90,7 +90,7 @@ func runCustomTransportExample(ctx context.Context, evmPrivateKey, url string) e // Create x402 client client := x402.Newx402Client(). - Register("eip155:*", evm.NewExactEvmScheme(evmSigner)) + Register("eip155:*", evm.NewExactEvmScheme(evmSigner, nil)) httpClient := x402http.Newx402HTTPClient(client) @@ -135,6 +135,10 @@ func runCustomTransportExample(ctx context.Context, evmPrivateKey, url string) e } defer resp.Body.Close() - return printResponse(resp, "Response with custom transport") + if err := printResponse(resp, "Response with custom transport"); err != nil { + return err + } + printPaymentDetails(resp.Header) + return nil } diff --git a/examples/go/clients/advanced/error_recovery.go b/examples/go/clients/advanced/error_recovery.go index b4ad618f2e..8b65d2599e 100644 --- a/examples/go/clients/advanced/error_recovery.go +++ b/examples/go/clients/advanced/error_recovery.go @@ -38,7 +38,7 @@ func runErrorRecoveryExample(ctx context.Context, evmPrivateKey, url string) err // Create x402 client with comprehensive error handling client := x402.Newx402Client(). - Register("eip155:*", evm.NewExactEvmScheme(evmSigner)) + Register("eip155:*", evm.NewExactEvmScheme(evmSigner, nil)) // Recovery counter recoveryAttempts := 0 @@ -122,7 +122,11 @@ func runErrorRecoveryExample(ctx context.Context, evmPrivateKey, url string) err fmt.Printf(" Successful recoveries: %d\n", successfulRecoveries) fmt.Printf(" Final status: %d\n\n", resp.StatusCode) - return printResponse(resp, "Response after error recovery") + if err := printResponse(resp, "Response after error recovery"); err != nil { + return err + } + printPaymentDetails(resp.Header) + return nil } // classifyError categorizes errors for targeted recovery strategies diff --git a/examples/go/clients/advanced/hooks.go b/examples/go/clients/advanced/hooks.go index d4a0c7e8e0..0a3bad969e 100644 --- a/examples/go/clients/advanced/hooks.go +++ b/examples/go/clients/advanced/hooks.go @@ -38,7 +38,7 @@ func runHooksExample(ctx context.Context, evmPrivateKey, url string) error { // Create client with scheme registration client := x402.Newx402Client(). - Register("eip155:*", evm.NewExactEvmScheme(evmSigner)) + Register("eip155:*", evm.NewExactEvmScheme(evmSigner, nil)) // Register lifecycle hooks @@ -107,6 +107,10 @@ func runHooksExample(ctx context.Context, evmPrivateKey, url string) error { fmt.Println("✅ Request completed successfully with hooks\n") - return printResponse(resp, "Response with hooks") + if err := printResponse(resp, "Response with hooks"); err != nil { + return err + } + printPaymentDetails(resp.Header) + return nil } diff --git a/examples/go/clients/advanced/multi_network_priority.go b/examples/go/clients/advanced/multi_network_priority.go index b3cb6e1c3d..be2145585f 100644 --- a/examples/go/clients/advanced/multi_network_priority.go +++ b/examples/go/clients/advanced/multi_network_priority.go @@ -48,17 +48,17 @@ func runMultiNetworkPriorityExample(ctx context.Context, evmPrivateKey, url stri // Level 1: Specific networks (highest priority) fmt.Println("✅ Registering Ethereum Mainnet (eip155:1) with mainnet signer") - client.Register("eip155:1", evm.NewExactEvmScheme(mainnetSigner)) + client.Register("eip155:1", evm.NewExactEvmScheme(mainnetSigner, nil)) fmt.Println("✅ Registering Base Mainnet (eip155:8453) with base signer") - client.Register("eip155:8453", evm.NewExactEvmScheme(baseSigner)) + client.Register("eip155:8453", evm.NewExactEvmScheme(baseSigner, nil)) fmt.Println("✅ Registering Base Sepolia (eip155:84532) with testnet signer") - client.Register("eip155:84532", evm.NewExactEvmScheme(testnetSigner)) + client.Register("eip155:84532", evm.NewExactEvmScheme(testnetSigner, nil)) // Level 2: Wildcard for all other EVM networks (fallback) fmt.Println("✅ Registering all other EVM networks (eip155:*) with primary signer\n") - client.Register("eip155:*", evm.NewExactEvmScheme(primarySigner)) + client.Register("eip155:*", evm.NewExactEvmScheme(primarySigner, nil)) // Add logging to show which network is being used client.OnBeforePaymentCreation(func(ctx x402.PaymentCreationContext) (*x402.BeforePaymentCreationHookResult, error) { @@ -111,6 +111,10 @@ func runMultiNetworkPriorityExample(ctx context.Context, evmPrivateKey, url stri fmt.Println(" This allows fine-grained control per network while having") fmt.Println(" sensible defaults for unknown networks.\n") - return printResponse(resp, "Response with multi-network priority") + if err := printResponse(resp, "Response with multi-network priority"); err != nil { + return err + } + printPaymentDetails(resp.Header) + return nil } diff --git a/examples/go/clients/custom/main.go b/examples/go/clients/custom/main.go index ee28bf4510..fa1e0dd370 100644 --- a/examples/go/clients/custom/main.go +++ b/examples/go/clients/custom/main.go @@ -70,17 +70,22 @@ func main() { } x402Client := x402.Newx402Client(). - Register("eip155:*", evm.NewExactEvmScheme(evmSigner)) + Register("eip155:*", evm.NewExactEvmScheme(evmSigner, nil)) // Make the request with custom payment handling fmt.Println("🔧 Using custom payment implementation (no wrapper)\n") - + resp, err := makeRequestWithPayment(ctx, x402Client, url) + if resp != nil { + defer resp.Body.Close() + } if err != nil { fmt.Printf("❌ Request failed: %v\n", err) + if resp != nil { + displayPaymentDetails(resp) + } os.Exit(1) } - defer resp.Body.Close() // Read and display response var responseData interface{} @@ -93,16 +98,33 @@ func main() { prettyJSON, _ := json.MarshalIndent(responseData, " ", " ") fmt.Printf(" %s\n", string(prettyJSON)) - // Extract payment settlement details - if paymentHeader := resp.Header.Get("PAYMENT-RESPONSE"); paymentHeader != "" { - settleResp, err := extractSettlementResponse(paymentHeader) - if err == nil { - fmt.Println("\n💰 Payment Settlement Details:") - fmt.Printf(" Transaction: %s\n", settleResp.Transaction) - fmt.Printf(" Network: %s\n", settleResp.Network) - fmt.Printf(" Payer: %s\n", settleResp.Payer) - } + displayPaymentDetails(resp) +} + +// displayPaymentDetails extracts and prints payment settlement from response headers. +// Payment-response header is sent on both success and error. +func displayPaymentDetails(resp *http.Response) { + paymentHeader := resp.Header.Get("PAYMENT-RESPONSE") + if paymentHeader == "" { + paymentHeader = resp.Header.Get("X-PAYMENT-RESPONSE") + } + if paymentHeader == "" { + return + } + settleResp, err := extractSettlementResponse(paymentHeader) + if err != nil { + return + } + fmt.Println("\n💰 Payment Settlement Details:") + fmt.Printf(" Success: %v\n", settleResp.Success) + if settleResp.ErrorReason != "" { + fmt.Printf(" ErrorReason: %s\n", settleResp.ErrorReason) + } + if settleResp.Transaction != "" { + fmt.Printf(" Transaction: %s\n", settleResp.Transaction) } + fmt.Printf(" Network: %s\n", settleResp.Network) + fmt.Printf(" Payer: %s\n", settleResp.Payer) } // makeRequestWithPayment implements the complete payment flow manually @@ -247,9 +269,8 @@ func makeRequestWithPayment(ctx context.Context, x402Client *x402.X402Client, ur // Step 6: Verify success // ======================================================================== if retryResp.StatusCode >= 400 { - defer retryResp.Body.Close() errorBody, _ := io.ReadAll(retryResp.Body) - return nil, fmt.Errorf("payment failed: status %d, body: %s", retryResp.StatusCode, string(errorBody)) + return retryResp, fmt.Errorf("payment failed: status %d, body: %s", retryResp.StatusCode, string(errorBody)) } fmt.Println("✅ Step 6: Payment successful!\n") diff --git a/examples/go/clients/http/builder_pattern.go b/examples/go/clients/http/builder_pattern.go index ec9dd45dd7..1256006bdc 100644 --- a/examples/go/clients/http/builder_pattern.go +++ b/examples/go/clients/http/builder_pattern.go @@ -29,12 +29,12 @@ func createBuilderPatternClient(evmPrivateKey, svmPrivateKey string) (*x402.X402 client := x402.Newx402Client() // Register EVM scheme for all EVM networks - client.Register("eip155:*", evm.NewExactEvmScheme(evmSigner)) + client.Register("eip155:*", evm.NewExactEvmScheme(evmSigner, nil)) // You can also register specific networks for fine-grained control // For example, use a different signer for Ethereum mainnet: // ethereumSigner := evmsigners.NewClientSignerFromPrivateKey(ethereumKey) - // client.Register("eip155:1", evm.NewExactEvmScheme(ethereumSigner)) + // client.Register("eip155:1", evm.NewExactEvmScheme(ethereumSigner, nil)) // Register SVM scheme if key is provided if svmPrivateKey != "" { diff --git a/examples/go/clients/http/main.go b/examples/go/clients/http/main.go index 8eab6c3446..5adbbad5f1 100644 --- a/examples/go/clients/http/main.go +++ b/examples/go/clients/http/main.go @@ -121,7 +121,13 @@ func makeRequest(client *x402.X402Client, url string) error { fmt.Println("\n💰 Payment Details:") settleResp, err := extractPaymentResponse(resp.Header) if err == nil { - fmt.Printf(" Transaction: %s\n", settleResp.Transaction) + fmt.Printf(" Success: %v\n", settleResp.Success) + if settleResp.ErrorReason != "" { + fmt.Printf(" ErrorReason: %s\n", settleResp.ErrorReason) + } + if settleResp.Transaction != "" { + fmt.Printf(" Transaction: %s\n", settleResp.Transaction) + } fmt.Printf(" Network: %s\n", settleResp.Network) fmt.Printf(" Payer: %s\n", settleResp.Payer) } diff --git a/examples/go/clients/http/mechanism_helper_registration.go b/examples/go/clients/http/mechanism_helper_registration.go index cf4d79b403..d2fa8cee9c 100644 --- a/examples/go/clients/http/mechanism_helper_registration.go +++ b/examples/go/clients/http/mechanism_helper_registration.go @@ -31,7 +31,7 @@ func createMechanismHelperRegistrationClient(evmPrivateKey, svmPrivateKey string // Register EVM scheme for all EVM networks using wildcard // This registers: // - eip155:* (all EVM networks in v2) - client.Register("eip155:*", evm.NewExactEvmScheme(evmSigner)) + client.Register("eip155:*", evm.NewExactEvmScheme(evmSigner, nil)) // Register SVM scheme if key is provided if svmPrivateKey != "" { @@ -48,7 +48,7 @@ func createMechanismHelperRegistrationClient(evmPrivateKey, svmPrivateKey string // The fluent API allows chaining for clean code: // client := x402.Newx402Client(). - // Register("eip155:*", evm.NewExactEvmScheme(evmSigner)). + // Register("eip155:*", evm.NewExactEvmScheme(evmSigner, nil)). // Register("solana:*", svm.NewExactSvmScheme(svmSigner)) return client, nil diff --git a/examples/go/clients/mcp-chatbot/main.go b/examples/go/clients/mcp-chatbot/main.go index 34acea0b8b..4ecfaa5322 100644 --- a/examples/go/clients/mcp-chatbot/main.go +++ b/examples/go/clients/mcp-chatbot/main.go @@ -105,7 +105,7 @@ func run() error { // Create x402 payment client and wrap session paymentClient := x402.Newx402Client() - paymentClient.Register("eip155:84532", evm.NewExactEvmScheme(evmSigner)) + paymentClient.Register("eip155:84532", evm.NewExactEvmScheme(evmSigner, nil)) x402Mcp := mcp.NewX402MCPClient(clientSession, paymentClient, mcp.Options{ AutoPayment: mcp.BoolPtr(true), OnPaymentRequested: func(context mcp.PaymentRequiredContext) (bool, error) { diff --git a/examples/go/clients/mcp/advanced.go b/examples/go/clients/mcp/advanced.go index e08517c469..5855bc0583 100644 --- a/examples/go/clients/mcp/advanced.go +++ b/examples/go/clients/mcp/advanced.go @@ -81,7 +81,7 @@ func runAdvanced() error { // Step 2: Create x402 payment client manually paymentClient := x402.Newx402Client() - paymentClient.Register("eip155:84532", evm.NewExactEvmScheme(evmSigner)) + paymentClient.Register("eip155:84532", evm.NewExactEvmScheme(evmSigner, nil)) // Step 3: Compose into X402MCPClient with session x402Mcp := mcp.NewX402MCPClient(clientSession, paymentClient, mcp.Options{ diff --git a/examples/go/clients/mcp/simple.go b/examples/go/clients/mcp/simple.go index 9b324cbc61..1fc19d0530 100644 --- a/examples/go/clients/mcp/simple.go +++ b/examples/go/clients/mcp/simple.go @@ -73,7 +73,7 @@ func runSimple() error { // Create x402 payment client and wrap session paymentClient := x402.Newx402Client() - paymentClient.Register("eip155:84532", evm.NewExactEvmScheme(evmSigner)) + paymentClient.Register("eip155:84532", evm.NewExactEvmScheme(evmSigner, nil)) x402Mcp := mcp.NewX402MCPClient(clientSession, paymentClient, mcp.Options{ AutoPayment: mcp.BoolPtr(true), OnPaymentRequested: func(context mcp.PaymentRequiredContext) (bool, error) { diff --git a/examples/go/clients/payment-identifier/main.go b/examples/go/clients/payment-identifier/main.go index 53121fd870..5cc78af831 100644 --- a/examples/go/clients/payment-identifier/main.go +++ b/examples/go/clients/payment-identifier/main.go @@ -145,7 +145,7 @@ func main() { // Create client with scheme registration client := x402.Newx402Client(). - Register("eip155:*", evm.NewExactEvmScheme(evmSigner)) + Register("eip155:*", evm.NewExactEvmScheme(evmSigner, nil)) // Generate a unique payment ID for this session paymentID := paymentidentifier.GeneratePaymentID("") @@ -169,27 +169,29 @@ func main() { fmt.Printf("Making request to: %s\n\n", serverURL) startTime1 := time.Now() - resp1, err := makeRequest(ctx, wrappedClient, serverURL) + resp1, httpResp1, err := makeRequest(ctx, wrappedClient, serverURL) duration1 := time.Since(startTime1) if err != nil { fmt.Printf("Request failed: %v\n", err) os.Exit(1) } - fmt.Printf("Response (%v): %s\n\n", duration1, resp1) + fmt.Printf("Response (%v): %s\n", duration1, resp1) + printPaymentDetails(httpResp1) // Second request - replay the same signed payment (true retry) - fmt.Println("Second Request (retry with cached payment)") + fmt.Println("\nSecond Request (retry with cached payment)") fmt.Printf("Making request to: %s\n", serverURL) fmt.Println("Expected: Server returns cached response (same ID, same payload)") startTime2 := time.Now() - resp2, err := makeRequest(ctx, wrappedClient, serverURL) + resp2, httpResp2, err := makeRequest(ctx, wrappedClient, serverURL) duration2 := time.Since(startTime2) if err != nil { fmt.Printf("Request failed: %v\n", err) os.Exit(1) } - fmt.Printf("Response (%v): %s\n\n", duration2, resp2) + fmt.Printf("Response (%v): %s\n", duration2, resp2) + printPaymentDetails(httpResp2) // Summary fmt.Println("Summary") @@ -198,23 +200,52 @@ func main() { fmt.Printf(" Second request: %v\n", duration2) } -func makeRequest(ctx context.Context, client *http.Client, url string) (string, error) { +func makeRequest(ctx context.Context, client *http.Client, url string) (string, *http.Response, error) { req, err := http.NewRequestWithContext(ctx, "POST", url, strings.NewReader(`{"item": "widget"}`)) if err != nil { - return "", err + return "", nil, err } req.Header.Set("Content-Type", "application/json") resp, err := client.Do(req) if err != nil { - return "", err + return "", nil, err } defer resp.Body.Close() body, err := io.ReadAll(resp.Body) if err != nil { - return "", err + return "", resp, err } - return string(body), nil + return string(body), resp, nil +} + +// printPaymentDetails extracts and prints payment settlement from response headers. +func printPaymentDetails(resp *http.Response) { + paymentHeader := resp.Header.Get("PAYMENT-RESPONSE") + if paymentHeader == "" { + paymentHeader = resp.Header.Get("X-PAYMENT-RESPONSE") + } + if paymentHeader == "" { + return + } + decoded, err := base64.StdEncoding.DecodeString(paymentHeader) + if err != nil { + return + } + var settleResp x402.SettleResponse + if err := json.Unmarshal(decoded, &settleResp); err != nil { + return + } + fmt.Println("💰 Payment Details:") + fmt.Printf(" Success: %v\n", settleResp.Success) + if settleResp.ErrorReason != "" { + fmt.Printf(" ErrorReason: %s\n", settleResp.ErrorReason) + } + if settleResp.Transaction != "" { + fmt.Printf(" Transaction: %s\n", settleResp.Transaction) + } + fmt.Printf(" Network: %s\n", settleResp.Network) + fmt.Printf(" Payer: %s\n", settleResp.Payer) } diff --git a/examples/go/facilitator/advanced/signer.go b/examples/go/facilitator/advanced/signer.go index 19f57d0857..eac33b0a58 100644 --- a/examples/go/facilitator/advanced/signer.go +++ b/examples/go/facilitator/advanced/signer.go @@ -190,6 +190,11 @@ func (s *facilitatorEvmSigner) ReadContract( return nil, fmt.Errorf("failed to parse ABI: %w", err) } + methodObj, exists := contractABI.Methods[method] + if !exists { + return nil, fmt.Errorf("method %s not found in ABI", method) + } + // Pack the method call data, err := contractABI.Pack(method, args...) if err != nil { @@ -209,23 +214,11 @@ func (s *facilitatorEvmSigner) ReadContract( return nil, fmt.Errorf("failed to call contract: %w", err) } - // Handle empty result - if len(result) == 0 { - if method == "authorizationState" { - return false, nil - } - if method == "balanceOf" || method == "allowance" { - return big.NewInt(0), nil - } - return nil, fmt.Errorf("empty result from contract call") + if len(methodObj.Outputs) == 0 { + return nil, nil } // Unpack the result - methodObj, exists := contractABI.Methods[method] - if !exists { - return nil, fmt.Errorf("method %s not found in ABI", method) - } - output, err := methodObj.Outputs.Unpack(result) if err != nil { return nil, fmt.Errorf("failed to unpack result: %w", err) @@ -606,4 +599,3 @@ func getBigIntFromInterface(v interface{}) *big.Int { } return big.NewInt(0) } - diff --git a/examples/go/facilitator/basic/main.go b/examples/go/facilitator/basic/main.go index 589373b230..c9ead5daa9 100644 --- a/examples/go/facilitator/basic/main.go +++ b/examples/go/facilitator/basic/main.go @@ -11,6 +11,7 @@ import ( x402 "github.com/coinbase/x402/go" evm "github.com/coinbase/x402/go/mechanisms/evm/exact/facilitator" evmv1 "github.com/coinbase/x402/go/mechanisms/evm/exact/v1/facilitator" + svmmech "github.com/coinbase/x402/go/mechanisms/svm" svm "github.com/coinbase/x402/go/mechanisms/svm/exact/facilitator" svmv1 "github.com/coinbase/x402/go/mechanisms/svm/exact/v1/facilitator" "github.com/gin-gonic/gin" @@ -61,8 +62,9 @@ func main() { facilitator.RegisterV1([]x402.Network{"base-sepolia"}, evmv1.NewExactEvmSchemeV1(evmSigner, evmV1Config)) if svmSigner != nil { - facilitator.Register([]x402.Network{svmNetwork}, svm.NewExactSvmScheme(svmSigner)) - facilitator.RegisterV1([]x402.Network{"solana-devnet"}, svmv1.NewExactSvmSchemeV1(svmSigner)) + settlementCache := svmmech.NewSettlementCache() + facilitator.Register([]x402.Network{svmNetwork}, svm.NewExactSvmScheme(svmSigner, settlementCache)) + facilitator.RegisterV1([]x402.Network{"solana-devnet"}, svmv1.NewExactSvmSchemeV1(svmSigner, settlementCache)) } facilitator.OnAfterVerify(func(ctx x402.FacilitatorVerifyResultContext) error { diff --git a/examples/go/facilitator/basic/signer.go b/examples/go/facilitator/basic/signer.go index 19f57d0857..442da10515 100644 --- a/examples/go/facilitator/basic/signer.go +++ b/examples/go/facilitator/basic/signer.go @@ -184,12 +184,18 @@ func (s *facilitatorEvmSigner) ReadContract( method string, args ...interface{}, ) (interface{}, error) { + // Parse ABI contractABI, err := abi.JSON(strings.NewReader(string(abiJSON))) if err != nil { return nil, fmt.Errorf("failed to parse ABI: %w", err) } + methodObj, exists := contractABI.Methods[method] + if !exists { + return nil, fmt.Errorf("method %s not found in ABI", method) + } + // Pack the method call data, err := contractABI.Pack(method, args...) if err != nil { @@ -208,24 +214,12 @@ func (s *facilitatorEvmSigner) ReadContract( if err != nil { return nil, fmt.Errorf("failed to call contract: %w", err) } - - // Handle empty result - if len(result) == 0 { - if method == "authorizationState" { - return false, nil - } - if method == "balanceOf" || method == "allowance" { - return big.NewInt(0), nil - } - return nil, fmt.Errorf("empty result from contract call") + + if len(methodObj.Outputs) == 0 { + return nil, nil } // Unpack the result - methodObj, exists := contractABI.Methods[method] - if !exists { - return nil, fmt.Errorf("method %s not found in ABI", method) - } - output, err := methodObj.Outputs.Unpack(result) if err != nil { return nil, fmt.Errorf("failed to unpack result: %w", err) @@ -606,4 +600,3 @@ func getBigIntFromInterface(v interface{}) *big.Int { } return big.NewInt(0) } - diff --git a/examples/go/servers/bazaar/.env-example b/examples/go/servers/bazaar/.env-example new file mode 100644 index 0000000000..7e5cdfcaf6 --- /dev/null +++ b/examples/go/servers/bazaar/.env-example @@ -0,0 +1,3 @@ +EVM_PAYEE_ADDRESS= +SVM_PAYEE_ADDRESS= +FACILITATOR_URL= diff --git a/examples/go/servers/bazaar/.gitignore b/examples/go/servers/bazaar/.gitignore new file mode 100644 index 0000000000..8acdd4dc03 --- /dev/null +++ b/examples/go/servers/bazaar/.gitignore @@ -0,0 +1,24 @@ +# Environment variables +.env + +# Binaries +*.exe +*.exe~ +*.dll +*.so +*.dylib +gin +server + +# Go build artifacts +*.test +*.out +go.sum + +# IDE +.idea/ +.vscode/ +*.swp +*.swo +*~ + diff --git a/examples/go/servers/bazaar/README.md b/examples/go/servers/bazaar/README.md new file mode 100644 index 0000000000..056b77ee35 --- /dev/null +++ b/examples/go/servers/bazaar/README.md @@ -0,0 +1,140 @@ +# Bazaar Discovery Example Server (Go) + +Gin server demonstrating how to make a paid API **discoverable** using the Bazaar extension with dynamic route parameters. + +The key addition over a basic x402 server is `DeclareDiscoveryExtension` -- it describes your endpoint's inputs, outputs, and path parameters so that facilitators (and agents) can automatically catalog and invoke your API. + +## What This Example Shows + +**Dynamic route parameters** -- the route `GET /weather/:city` uses a `:city` slug. The x402 middleware automatically: + +1. Matches `/weather/san-francisco`, `/weather/tokyo`, etc. against the route pattern +2. Extracts `{ city: "san-francisco" }` as `pathParams` in the discovery extension +3. Produces `routeTemplate: "/weather/:city"` so all concrete URLs consolidate into **one** catalog entry + +```go +weatherExtension, _ := bazaar.DeclareDiscoveryExtension( + bazaar.MethodGET, nil, nil, "", + &types.OutputConfig{ + Example: map[string]interface{}{"city": "san-francisco", "weather": "foggy", "temperature": 60}, + }, + bazaar.DeclareDiscoveryExtensionOpts{ + PathParamsSchema: types.JSONSchema{ + "properties": map[string]interface{}{ + "city": map[string]interface{}{"type": "string", "description": "City name slug"}, + }, + "required": []string{"city"}, + }, + }, +) + +routes := x402http.RoutesConfig{ + "GET /weather/:city": { + Accepts: x402http.PaymentOptions{{Scheme: "exact", Price: "$0.001", ...}}, + Description: "Weather data for a city", + Extensions: map[string]interface{}{bazaar.BAZAAR.Key(): weatherExtension}, + }, +} +``` + +Note that the x402 route key uses `:city` which aligns with both Express convention and Gin's `c.Param("city")` extraction. + +## Prerequisites + +- Go 1.24 or higher +- Valid EVM address for receiving payments +- Valid SVM address for receiving payments +- URL of a facilitator supporting the desired payment network, see [facilitator list](https://www.x402.org/ecosystem?category=facilitators) + +## Setup + +1. Copy `.env-example` to `.env`: + +```bash +cp .env-example .env +``` + +and fill required environment variables: + +- `FACILITATOR_URL` - Facilitator endpoint URL +- `EVM_PAYEE_ADDRESS` - Ethereum address to receive payments +- `SVM_PAYEE_ADDRESS` - Solana address to receive payments + +2. Install dependencies: +```bash +go mod download +``` + +3. Run the server: +```bash +go run main.go +``` + +Server runs at http://localhost:4021 + +## How Discovery Works + +When a client hits `GET /weather/san-francisco` without a payment, the 402 response includes the enriched bazaar extension: + +```json +{ + "x402Version": 2, + "error": "Payment required", + "resource": { "url": "http://localhost:4021/weather/san-francisco" }, + "extensions": { + "bazaar": { + "routeTemplate": "/weather/:city", + "info": { + "input": { + "type": "http", + "method": "GET", + "pathParams": { "city": "san-francisco" } + }, + "output": { + "type": "json", + "example": { "city": "san-francisco", "weather": "foggy", "temperature": 60 } + } + } + } + }, + "accepts": [{ "..." : "..." }] +} +``` + +The facilitator uses `routeTemplate` as the canonical catalog key, so requests to `/weather/san-francisco`, `/weather/tokyo`, and `/weather/new-york` all map to a single discoverable endpoint: `/weather/:city`. + +## Example Endpoints + +| Endpoint | Payment | Price | +|----------|---------|-------| +| `GET /health` | No | - | +| `GET /weather/:city` | Yes | $0.001 USDC | +| `GET /weather/:country/:city` | Yes | $0.001 USDC | + +## Multiple Path Parameters + +Routes can have multiple `:param` segments. Param names are matched by **position in the URL**, not by the order they appear in `PathParamsSchema`: + +``` +GET /weather/:country/:city + ^ ^ + | └── second URL segment -> "city" + └──────────── first URL segment -> "country" +``` + +A request to `/weather/us/san-francisco` produces `pathParams: { country: "us", city: "san-francisco" }`. The property order in `PathParamsSchema` does not affect matching -- only the segment position in the URL matters. + +## `DeclareDiscoveryExtension` API + +```go +func DeclareDiscoveryExtension( + method interface{}, // bazaar.MethodGET, bazaar.MethodPOST, etc. + input interface{}, // Example input data (query params or body) + inputSchema types.JSONSchema, // JSON Schema for input + bodyType types.BodyType, // For POST/PUT/PATCH: "json", "form-data", "text" + output *types.OutputConfig, // Example response + opts ...DeclareDiscoveryExtensionOpts, // PathParamsSchema +) (types.DiscoveryExtension, error) +``` + +The optional `DeclareDiscoveryExtensionOpts` struct provides `PathParamsSchema` for describing URL path parameters. diff --git a/examples/go/servers/bazaar/go.mod b/examples/go/servers/bazaar/go.mod new file mode 100644 index 0000000000..5b5b438c56 --- /dev/null +++ b/examples/go/servers/bazaar/go.mod @@ -0,0 +1,83 @@ +module github.com/coinbase/x402/examples/go/servers/bazaar + +go 1.24.0 + +toolchain go1.24.1 + +replace github.com/coinbase/x402/go => ../../../../go + +require ( + github.com/coinbase/x402/go v0.0.0-00010101000000-000000000000 + github.com/gin-gonic/gin v1.11.0 + github.com/joho/godotenv v1.5.1 +) + +require ( + filippo.io/edwards25519 v1.0.0-rc.1 // indirect + github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20251001021608-1fe7b43fc4d6 // indirect + github.com/andres-erbsen/clock v0.0.0-20160526145045-9e14626cd129 // indirect + github.com/bits-and-blooms/bitset v1.20.0 // indirect + github.com/blendle/zapdriver v1.3.1 // indirect + github.com/bytedance/sonic v1.14.0 // indirect + github.com/bytedance/sonic/loader v0.3.0 // indirect + github.com/cloudwego/base64x v0.1.6 // indirect + github.com/consensys/gnark-crypto v0.18.0 // indirect + github.com/crate-crypto/go-eth-kzg v1.4.0 // indirect + github.com/crate-crypto/go-ipa v0.0.0-20240724233137-53bbb0ceb27a // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 // indirect + github.com/ethereum/c-kzg-4844/v2 v2.1.5 // indirect + github.com/ethereum/go-ethereum v1.16.7 // indirect + github.com/ethereum/go-verkle v0.2.2 // indirect + github.com/fatih/color v1.16.0 // indirect + github.com/gabriel-vasile/mimetype v1.4.8 // indirect + github.com/gagliardetto/binary v0.8.0 // indirect + github.com/gagliardetto/solana-go v1.14.0 // indirect + github.com/gagliardetto/treeout v0.1.4 // indirect + github.com/gin-contrib/sse v1.1.0 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/go-playground/validator/v10 v10.27.0 // indirect + github.com/goccy/go-json v0.10.4 // indirect + github.com/goccy/go-yaml v1.18.0 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/holiman/uint256 v1.3.2 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/compress v1.16.0 // indirect + github.com/klauspost/cpuid/v2 v2.3.0 // indirect + github.com/leodido/go-urn v1.4.0 // indirect + github.com/logrusorgru/aurora v2.0.3+incompatible // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mitchellh/go-testing-interface v1.14.1 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/mostynb/zstdpool-freelist v0.0.0-20201229113212-927304c0c3b1 // indirect + github.com/mr-tron/base58 v1.2.0 // indirect + github.com/pelletier/go-toml/v2 v2.2.4 // indirect + github.com/quic-go/qpack v0.5.1 // indirect + github.com/quic-go/quic-go v0.55.0 // indirect + github.com/streamingfast/logging v0.0.0-20230608130331-f22c91403091 // indirect + github.com/supranational/blst v0.3.16-0.20250831170142-f48500c1fdbe // indirect + github.com/twitchyliquid64/golang-asm v0.15.1 // indirect + github.com/ugorji/go/codec v1.3.0 // indirect + github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f // indirect + github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect + github.com/xeipuuv/gojsonschema v1.2.0 // indirect + go.mongodb.org/mongo-driver v1.12.2 // indirect + go.uber.org/atomic v1.7.0 // indirect + go.uber.org/multierr v1.6.0 // indirect + go.uber.org/ratelimit v0.2.0 // indirect + go.uber.org/zap v1.21.0 // indirect + golang.org/x/arch v0.20.0 // indirect + golang.org/x/crypto v0.41.0 // indirect + golang.org/x/mod v0.27.0 // indirect + golang.org/x/net v0.43.0 // indirect + golang.org/x/sync v0.16.0 // indirect + golang.org/x/sys v0.36.0 // indirect + golang.org/x/term v0.34.0 // indirect + golang.org/x/text v0.28.0 // indirect + golang.org/x/time v0.9.0 // indirect + golang.org/x/tools v0.36.0 // indirect + google.golang.org/protobuf v1.36.9 // indirect +) diff --git a/examples/go/servers/bazaar/main.go b/examples/go/servers/bazaar/main.go new file mode 100644 index 0000000000..c6e6c48e0c --- /dev/null +++ b/examples/go/servers/bazaar/main.go @@ -0,0 +1,172 @@ +package main + +import ( + "fmt" + "net/http" + "os" + "time" + + x402http "github.com/coinbase/x402/go/http" + "github.com/coinbase/x402/go/extensions/bazaar" + "github.com/coinbase/x402/go/extensions/types" + ginmw "github.com/coinbase/x402/go/http/gin" + evm "github.com/coinbase/x402/go/mechanisms/evm/exact/server" + svm "github.com/coinbase/x402/go/mechanisms/svm/exact/server" + ginfw "github.com/gin-gonic/gin" + "github.com/joho/godotenv" +) + +const ( + DefaultPort = "4021" +) + +func main() { + godotenv.Load() + + evmAddress := os.Getenv("EVM_PAYEE_ADDRESS") + if evmAddress == "" { + fmt.Println("EVM_PAYEE_ADDRESS environment variable is required") + os.Exit(1) + } + + svmAddress := os.Getenv("SVM_PAYEE_ADDRESS") + if svmAddress == "" { + fmt.Println("SVM_PAYEE_ADDRESS environment variable is required") + os.Exit(1) + } + + facilitatorURL := os.Getenv("FACILITATOR_URL") + if facilitatorURL == "" { + fmt.Println("FACILITATOR_URL environment variable is required") + os.Exit(1) + } + + fmt.Printf("Starting Bazaar discovery example server...\n") + fmt.Printf(" EVM Payee: %s\n", evmAddress) + fmt.Printf(" SVM Payee: %s\n", svmAddress) + fmt.Printf(" Facilitator: %s\n", facilitatorURL) + + r := ginfw.Default() + + facilitatorClient := x402http.NewHTTPFacilitatorClient(&x402http.FacilitatorConfig{ + URL: facilitatorURL, + }) + + paymentOptions := x402http.PaymentOptions{ + {Scheme: "exact", Price: "$0.001", Network: "eip155:84532", PayTo: evmAddress}, + {Scheme: "exact", Price: "$0.001", Network: "solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1", PayTo: svmAddress}, + } + + // Single path param: /weather/:city + weatherByCityExt, err := bazaar.DeclareDiscoveryExtension( + bazaar.MethodGET, nil, nil, "", + &types.OutputConfig{ + Example: map[string]interface{}{"city": "san-francisco", "weather": "foggy", "temperature": 60}, + }, + bazaar.DeclareDiscoveryExtensionOpts{ + PathParamsSchema: types.JSONSchema{ + "properties": map[string]interface{}{ + "city": map[string]interface{}{"type": "string", "description": "City name slug"}, + }, + "required": []string{"city"}, + }, + }, + ) + if err != nil { + fmt.Printf("Error declaring discovery extension: %v\n", err) + os.Exit(1) + } + + // Multiple path params: /weather/:country/:city + // Param names are matched by position in the URL, not by declaration order in the schema. + // /weather/us/san-francisco -> { country: "us", city: "san-francisco" } + weatherByCountryCityExt, err := bazaar.DeclareDiscoveryExtension( + bazaar.MethodGET, nil, nil, "", + &types.OutputConfig{ + Example: map[string]interface{}{"country": "us", "city": "san-francisco", "weather": "foggy", "temperature": 60}, + }, + bazaar.DeclareDiscoveryExtensionOpts{ + PathParamsSchema: types.JSONSchema{ + "properties": map[string]interface{}{ + "country": map[string]interface{}{"type": "string", "description": "Country code"}, + "city": map[string]interface{}{"type": "string", "description": "City name slug"}, + }, + "required": []string{"country", "city"}, + }, + }, + ) + if err != nil { + fmt.Printf("Error declaring discovery extension: %v\n", err) + os.Exit(1) + } + + routes := x402http.RoutesConfig{ + "GET /weather/:city": { + Accepts: paymentOptions, + Description: "Weather data for a city", + MimeType: "application/json", + Extensions: map[string]interface{}{bazaar.BAZAAR.Key(): weatherByCityExt}, + }, + "GET /weather/:country/:city": { + Accepts: paymentOptions, + Description: "Weather data for a city in a specific country", + MimeType: "application/json", + Extensions: map[string]interface{}{bazaar.BAZAAR.Key(): weatherByCountryCityExt}, + }, + } + + r.Use(ginmw.X402Payment(ginmw.Config{ + Routes: routes, + Facilitator: facilitatorClient, + Schemes: []ginmw.SchemeConfig{ + {Network: "eip155:84532", Server: evm.NewExactEvmScheme()}, + {Network: "solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1", Server: svm.NewExactSvmScheme()}, + }, + Timeout: 30 * time.Second, + })) + + r.GET("/weather/:city", func(c *ginfw.Context) { + city := c.Param("city") + weatherData := map[string]map[string]interface{}{ + "san-francisco": {"weather": "foggy", "temperature": 60}, + "new-york": {"weather": "cloudy", "temperature": 55}, + "tokyo": {"weather": "rainy", "temperature": 65}, + } + data, exists := weatherData[city] + if !exists { + data = map[string]interface{}{"weather": "sunny", "temperature": 70} + } + c.JSON(http.StatusOK, ginfw.H{"city": city, "weather": data["weather"], "temperature": data["temperature"]}) + }) + + r.GET("/weather/:country/:city", func(c *ginfw.Context) { + country := c.Param("country") + city := c.Param("city") + weatherData := map[string]map[string]map[string]interface{}{ + "us": { + "san-francisco": {"weather": "foggy", "temperature": 60}, + "new-york": {"weather": "cloudy", "temperature": 55}, + }, + "jp": { + "tokyo": {"weather": "rainy", "temperature": 65}, + "osaka": {"weather": "clear", "temperature": 72}, + }, + } + data, exists := weatherData[country][city] + if !exists { + data = map[string]interface{}{"weather": "sunny", "temperature": 70} + } + c.JSON(http.StatusOK, ginfw.H{"country": country, "city": city, "weather": data["weather"], "temperature": data["temperature"]}) + }) + + r.GET("/health", func(c *ginfw.Context) { + c.JSON(http.StatusOK, ginfw.H{"status": "ok"}) + }) + + fmt.Printf(" Listening on http://localhost:%s\n\n", DefaultPort) + + if err := r.Run(":" + DefaultPort); err != nil { + fmt.Printf("Error starting server: %v\n", err) + os.Exit(1) + } +} diff --git a/examples/go/servers/echo/README.md b/examples/go/servers/echo/README.md new file mode 100644 index 0000000000..08cebaf7dc --- /dev/null +++ b/examples/go/servers/echo/README.md @@ -0,0 +1,224 @@ +# x402-echo Example Server + +Echo framework server demonstrating how to protect API endpoints with a paywall using the +`x402/go/http/echo` middleware. + +## Prerequisites + +- Go 1.24 or higher +- Valid EVM address for receiving payments +- URL of a facilitator supporting the desired payment network, see [facilitator list](https://www.x402.org/ecosystem?category=facilitators) + +## Setup + +1. Copy `.env-example` to `.env`: + +```bash +cp .env-example .env +``` + +and fill required environment variables: + +- `FACILITATOR_URL` - Facilitator endpoint URL +- `EVM_PAYEE_ADDRESS` - Ethereum address to receive payments +- `SVM_PAYEE_ADDRESS` - Solana address to receive payments + +2. Install dependencies: +```bash +go mod download +``` + +3. Run the server: +```bash +go run main.go +``` + +## Testing the Server + +You can test the server using one of the example clients: + +### Using the Go HTTP Client +```bash +cd ../../clients/http +# Ensure .env is setup +go run main.go +``` + +These clients will demonstrate how to: +1. Make an initial request to get payment requirements +2. Process the payment requirements +3. Make a second request with the payment token + +## Example Endpoint + +The server includes a single example endpoint at `/weather` that accepts payment of 0.001 USDC on either Base Sepolia (EVM) or Solana Devnet (SVM). The endpoint returns a simple weather report. + +## Response Format + +### Payment Required (402) + +``` +HTTP/1.1 402 Payment Required +Content-Type: application/json +PAYMENT-REQUIRED: + +null +``` + +The `PAYMENT-REQUIRED` header contains base64-encoded JSON with the payment requirements. +Note: `amount` is in atomic units (e.g., 1000 = 0.001 USDC, since USDC has 6 decimals): + +```json +{ + "x402Version": 2, + "error": "Payment required", + "resource": { + "url": "http://localhost:4021/weather", + "description": "Get weather data for a city", + "mimeType": "application/json" + }, + "accepts": [ + { + "scheme": "exact", + "network": "eip155:84532", + "amount": "1000", + "asset": "0x036CbD53842c5426634e7929541eC2318f3dCF7e", + "payTo": "0x...", + "maxTimeoutSeconds": 300, + "extra": { + "name": "USDC", + "version": "2", + "resourceUrl": "http://localhost:4021/weather" + } + }, + { + "scheme": "exact", + "network": "solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1", + "amount": "1000", + "asset": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", + "payTo": "...", + "maxTimeoutSeconds": 300, + "extra": { + "feePayer": "...", + } + } + ] +} +``` + +### Successful Response + +``` +HTTP/1.1 200 OK +Content-Type: application/json +PAYMENT-RESPONSE: + +{"city":"San Francisco","weather":"foggy","temperature":60,"timestamp":"2025-01-01T12:00:00Z"} +``` + +The `PAYMENT-RESPONSE` header contains base64-encoded JSON with the settlement details: + +```json +{ + "success": true, + "transaction": "0x...", + "network": "eip155:84532", + "payer": "0x..." +} +``` + +## Extending the Example + +To add more paid endpoints, follow this pattern: + +```go +// First, configure the payment middleware with your routes +routes := x402http.RoutesConfig{ + "GET /your-endpoint": { + Accepts: x402http.PaymentOptions{ + { + Scheme: "exact", + PayTo: evmPayeeAddress, + Price: "$0.10", + Network: x402.Network("eip155:84532"), + }, + }, + Description: "Your endpoint description", + MimeType: "application/json", + }, +} + +e := echo.New() + +// Apply x402 payment middleware +e.Use(echomw.X402Payment(echomw.Config{ + Routes: routes, + Facilitator: facilitatorClient, + Schemes: []echomw.SchemeConfig{ + {Network: x402.Network("eip155:*"), Server: evm.NewExactEvmScheme()}, + }, + Timeout: 30 * time.Second, +})) + +// Then define your routes as normal +e.GET("/your-endpoint", func(c echo.Context) error { + return c.JSON(http.StatusOK, map[string]interface{}{ + // Your response data + }) +}) + +e.Start(":4021") +``` + +**Network identifiers** use [CAIP-2](https://github.com/ChainAgnostic/CAIPs/blob/main/CAIPs/caip-2.md) format, for example: +- `eip155:84532` — Base Sepolia +- `eip155:8453` — Base Mainnet +- `solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1` — Solana Devnet +- `solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp` — Solana Mainnet + +## x402ResourceServer Config + +The middleware uses scheme registrations to declare how payments for each network should be processed: + +```go +e.Use(echomw.X402Payment(echomw.Config{ + Routes: routes, + Facilitator: facilitatorClient, + Schemes: []echomw.SchemeConfig{ + {Network: x402.Network("eip155:*"), Server: evm.NewExactEvmScheme()}, // All EVM chains + // {Network: x402.Network("solana:*"), Server: svm.NewExactSvmScheme()}, // All SVM chains + }, + Timeout: 30 * time.Second, +})) +``` + +## Facilitator Config + +The `HTTPFacilitatorClient` connects to a facilitator service that verifies and settles payments on-chain: + +```go +facilitatorClient := x402http.NewHTTPFacilitatorClient(&x402http.FacilitatorConfig{ + URL: facilitatorURL, +}) + +// Or use multiple facilitators for redundancy +facilitatorClients := []x402.FacilitatorClient{ + x402http.NewHTTPFacilitatorClient(&x402http.FacilitatorConfig{URL: primaryFacilitatorURL}), + x402http.NewHTTPFacilitatorClient(&x402http.FacilitatorConfig{URL: backupFacilitatorURL}), +} +``` + +## Next Steps + +See [Advanced Examples](../advanced/) for: +- **Bazaar discovery** — make your API discoverable +- **Dynamic pricing** — price based on request context +- **Dynamic payTo** — route payments to different recipients +- **Lifecycle hooks** — custom logic on verify/settle +- **Custom tokens** — accept payments in custom tokens + +## Related Resources + +- [Echo Documentation](https://echo.labstack.com/docs) +- [x402 Go Package Documentation](../../../../go/) +- [Client Examples](../../clients/) — build clients that can make paid requests diff --git a/examples/go/servers/echo/go.mod b/examples/go/servers/echo/go.mod new file mode 100644 index 0000000000..4c09ee7ccd --- /dev/null +++ b/examples/go/servers/echo/go.mod @@ -0,0 +1,65 @@ +module github.com/coinbase/x402/examples/go/servers/echo + +go 1.24.0 + +toolchain go1.24.1 + +replace github.com/coinbase/x402/go => ../../../../go + +require ( + github.com/coinbase/x402/go v0.0.0 + github.com/joho/godotenv v1.5.1 + github.com/labstack/echo/v4 v4.15.1 +) + +require ( + filippo.io/edwards25519 v1.0.0-rc.1 // indirect + github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20251001021608-1fe7b43fc4d6 // indirect + github.com/andres-erbsen/clock v0.0.0-20160526145045-9e14626cd129 // indirect + github.com/bits-and-blooms/bitset v1.20.0 // indirect + github.com/blendle/zapdriver v1.3.1 // indirect + github.com/consensys/gnark-crypto v0.18.0 // indirect + github.com/crate-crypto/go-eth-kzg v1.4.0 // indirect + github.com/crate-crypto/go-ipa v0.0.0-20240724233137-53bbb0ceb27a // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 // indirect + github.com/ethereum/c-kzg-4844/v2 v2.1.5 // indirect + github.com/ethereum/go-ethereum v1.16.7 // indirect + github.com/ethereum/go-verkle v0.2.2 // indirect + github.com/fatih/color v1.16.0 // indirect + github.com/gagliardetto/binary v0.8.0 // indirect + github.com/gagliardetto/solana-go v1.14.0 // indirect + github.com/gagliardetto/treeout v0.1.4 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/holiman/uint256 v1.3.2 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/compress v1.16.0 // indirect + github.com/labstack/gommon v0.4.2 // indirect + github.com/logrusorgru/aurora v2.0.3+incompatible // indirect + github.com/mattn/go-colorable v0.1.14 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mitchellh/go-testing-interface v1.14.1 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/mostynb/zstdpool-freelist v0.0.0-20201229113212-927304c0c3b1 // indirect + github.com/mr-tron/base58 v1.2.0 // indirect + github.com/streamingfast/logging v0.0.0-20230608130331-f22c91403091 // indirect + github.com/supranational/blst v0.3.16-0.20250831170142-f48500c1fdbe // indirect + github.com/valyala/bytebufferpool v1.0.0 // indirect + github.com/valyala/fasttemplate v1.2.2 // indirect + github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f // indirect + github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect + github.com/xeipuuv/gojsonschema v1.2.0 // indirect + go.mongodb.org/mongo-driver v1.12.2 // indirect + go.uber.org/atomic v1.7.0 // indirect + go.uber.org/multierr v1.6.0 // indirect + go.uber.org/ratelimit v0.2.0 // indirect + go.uber.org/zap v1.21.0 // indirect + golang.org/x/crypto v0.46.0 // indirect + golang.org/x/net v0.48.0 // indirect + golang.org/x/sync v0.19.0 // indirect + golang.org/x/sys v0.39.0 // indirect + golang.org/x/term v0.38.0 // indirect + golang.org/x/text v0.32.0 // indirect + golang.org/x/time v0.14.0 // indirect +) diff --git a/examples/go/servers/echo/go.sum b/examples/go/servers/echo/go.sum new file mode 100644 index 0000000000..05bd271eff --- /dev/null +++ b/examples/go/servers/echo/go.sum @@ -0,0 +1,243 @@ +filippo.io/edwards25519 v1.0.0-rc.1 h1:m0VOOB23frXZvAOK44usCgLWvtsxIoMCTBGJZlpmGfU= +filippo.io/edwards25519 v1.0.0-rc.1/go.mod h1:N1IkdkCkiLB6tki+MYJoSx2JTY9NUlxZE7eHn5EwJns= +github.com/AlekSi/pointer v1.1.0 h1:SSDMPcXD9jSl8FPy9cRzoRaMJtm9g9ggGTxecRUbQoI= +github.com/AlekSi/pointer v1.1.0/go.mod h1:y7BvfRI3wXPWKXEBhU71nbnIEEZX0QTSB2Bj48UJIZE= +github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20251001021608-1fe7b43fc4d6 h1:1zYrtlhrZ6/b6SAjLSfKzWtdgqK0U+HtH/VcBWh1BaU= +github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20251001021608-1fe7b43fc4d6/go.mod h1:ioLG6R+5bUSO1oeGSDxOV3FADARuMoytZCSX6MEMQkI= +github.com/StackExchange/wmi v1.2.1 h1:VIkavFPXSjcnS+O8yTq7NI32k0R5Aj+v39y29VYDOSA= +github.com/StackExchange/wmi v1.2.1/go.mod h1:rcmrprowKIVzvc+NUiLncP2uuArMWLCbu9SBzvHz7e8= +github.com/andres-erbsen/clock v0.0.0-20160526145045-9e14626cd129 h1:MzBOUgng9orim59UnfUTLRjMpd09C5uEVQ6RPGeCaVI= +github.com/andres-erbsen/clock v0.0.0-20160526145045-9e14626cd129/go.mod h1:rFgpPQZYZ8vdbc+48xibu8ALc3yeyd64IhHS+PU6Yyg= +github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8= +github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= +github.com/bits-and-blooms/bitset v1.20.0 h1:2F+rfL86jE2d/bmw7OhqUg2Sj/1rURkBn3MdfoPyRVU= +github.com/bits-and-blooms/bitset v1.20.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8= +github.com/blendle/zapdriver v1.3.1 h1:C3dydBOWYRiOk+B8X9IVZ5IOe+7cl+tGOexN4QqHfpE= +github.com/blendle/zapdriver v1.3.1/go.mod h1:mdXfREi6u5MArG4j9fewC+FGnXaBR+T4Ox4J2u4eHCc= +github.com/consensys/gnark-crypto v0.18.0 h1:vIye/FqI50VeAr0B3dx+YjeIvmc3LWz4yEfbWBpTUf0= +github.com/consensys/gnark-crypto v0.18.0/go.mod h1:L3mXGFTe1ZN+RSJ+CLjUt9x7PNdx8ubaYfDROyp2Z8c= +github.com/crate-crypto/go-eth-kzg v1.4.0 h1:WzDGjHk4gFg6YzV0rJOAsTK4z3Qkz5jd4RE3DAvPFkg= +github.com/crate-crypto/go-eth-kzg v1.4.0/go.mod h1:J9/u5sWfznSObptgfa92Jq8rTswn6ahQWEuiLHOjCUI= +github.com/crate-crypto/go-ipa v0.0.0-20240724233137-53bbb0ceb27a h1:W8mUrRp6NOVl3J+MYp5kPMoUZPp7aOYHtaua31lwRHg= +github.com/crate-crypto/go-ipa v0.0.0-20240724233137-53bbb0ceb27a/go.mod h1:sTwzHBvIzm2RfVCGNEBZgRyjwK40bVoun3ZnGOCafNM= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/decred/dcrd/crypto/blake256 v1.0.0 h1:/8DMNYp9SGi5f0w7uCm6d6M4OU2rGFK09Y2A4Xv7EE0= +github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 h1:YLtO71vCjJRCBcrPMtQ9nqBsqpA1m5sE92cU+pd5Mcc= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1/go.mod h1:hyedUtir6IdtD/7lIxGeCxkaw7y45JueMRL4DIyJDKs= +github.com/emicklei/dot v1.6.2 h1:08GN+DD79cy/tzN6uLCT84+2Wk9u+wvqP+Hkx/dIR8A= +github.com/emicklei/dot v1.6.2/go.mod h1:DeV7GvQtIw4h2u73RKBkkFdvVAz0D9fzeJrgPW6gy/s= +github.com/ethereum/c-kzg-4844/v2 v2.1.5 h1:aVtoLK5xwJ6c5RiqO8g8ptJ5KU+2Hdquf6G3aXiHh5s= +github.com/ethereum/c-kzg-4844/v2 v2.1.5/go.mod h1:u59hRTTah4Co6i9fDWtiCjTrblJv0UwsqZKCc0GfgUs= +github.com/ethereum/go-ethereum v1.16.7 h1:qeM4TvbrWK0UC0tgkZ7NiRsmBGwsjqc64BHo20U59UQ= +github.com/ethereum/go-ethereum v1.16.7/go.mod h1:Fs6QebQbavneQTYcA39PEKv2+zIjX7rPUZ14DER46wk= +github.com/ethereum/go-verkle v0.2.2 h1:I2W0WjnrFUIzzVPwm8ykY+7pL2d4VhlsePn4j7cnFk8= +github.com/ethereum/go-verkle v0.2.2/go.mod h1:M3b90YRnzqKyyzBEWJGqj8Qff4IDeXnzFw0P9bFw3uk= +github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= +github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= +github.com/ferranbt/fastssz v0.1.4 h1:OCDB+dYDEQDvAgtAGnTSidK1Pe2tW3nFV40XyMkTeDY= +github.com/ferranbt/fastssz v0.1.4/go.mod h1:Ea3+oeoRGGLGm5shYAeDgu6PGUlcvQhE2fILyD9+tGg= +github.com/gagliardetto/binary v0.8.0 h1:U9ahc45v9HW0d15LoN++vIXSJyqR/pWw8DDlhd7zvxg= +github.com/gagliardetto/binary v0.8.0/go.mod h1:2tfj51g5o9dnvsc+fL3Jxr22MuWzYXwx9wEoN0XQ7/c= +github.com/gagliardetto/gofuzz v1.2.2 h1:XL/8qDMzcgvR4+CyRQW9UGdwPRPMHVJfqQ/uMvSUuQw= +github.com/gagliardetto/gofuzz v1.2.2/go.mod h1:bkH/3hYLZrMLbfYWA0pWzXmi5TTRZnu4pMGZBkqMKvY= +github.com/gagliardetto/solana-go v1.14.0 h1:3WfAi70jOOjAJ0deFMjdhFYlLXATF4tOQXsDNWJtOLw= +github.com/gagliardetto/solana-go v1.14.0/go.mod h1:l/qqqIN6qJJPtxW/G1PF4JtcE3Zg2vD2EliZrr9Gn5k= +github.com/gagliardetto/treeout v0.1.4 h1:ozeYerrLCmCubo1TcIjFiOWTTGteOOHND1twdFpgwaw= +github.com/gagliardetto/treeout v0.1.4/go.mod h1:loUefvXTrlRG5rYmJmExNryyBRh8f89VZhmMOyCyqok= +github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= +github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= +github.com/gofrs/flock v0.12.1 h1:MTLVXXHf8ekldpJk3AKicLij9MdwOWkZ+a/jHHZby9E= +github.com/gofrs/flock v0.12.1/go.mod h1:9zxTsyu5xtJ9DK+1tFZyibEV7y3uwDxPPfbxeeHCoD0= +github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs= +github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/go-cmp v0.5.2 h1:X2ev0eStA3AbceY54o37/0PQ/UWqKEiiO2dKL5OPaFM= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= +github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/holiman/uint256 v1.3.2 h1:a9EgMPSC1AAaj1SZL5zIQD3WbwTuHrMGOerLjGmM/TA= +github.com/holiman/uint256 v1.3.2/go.mod h1:EOMSn4q6Nyt9P6efbI3bueV4e1b3dGlUCXeiRV4ng7E= +github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= +github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/klauspost/compress v1.11.4/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= +github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= +github.com/klauspost/compress v1.16.0 h1:iULayQNOReoYUe+1qtKOqw9CwJv3aNQu8ivo7lw1HU4= +github.com/klauspost/compress v1.16.0/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= +github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= +github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/labstack/echo/v4 v4.15.1 h1:S9keusg26gZpjMmPqB5hOEvNKnmd1lNmcHrbbH2lnFs= +github.com/labstack/echo/v4 v4.15.1/go.mod h1:xmw1clThob0BSVRX1CRQkGQ/vjwcpOMjQZSZa9fKA/c= +github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0= +github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU= +github.com/leanovate/gopter v0.2.11 h1:vRjThO1EKPb/1NsDXuDrzldR28RLkBflWYcU9CvzWu4= +github.com/leanovate/gopter v0.2.11/go.mod h1:aK3tzZP/C+p1m3SPRE4SYZFGP7jjkuSI4f7Xvpt0S9c= +github.com/logrusorgru/aurora v2.0.3+incompatible h1:tOpm7WcpBTn4fjmVfgpQq0EfczGlG91VSDkswnjF5A8= +github.com/logrusorgru/aurora v2.0.3+incompatible/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4= +github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= +github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU= +github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/minio/sha256-simd v1.0.0 h1:v1ta+49hkWZyvaKwrQB8elexRqm6Y0aMLjCNsrYxo6g= +github.com/minio/sha256-simd v1.0.0/go.mod h1:OuYzVNI5vcoYIAmbIvHPl3N3jUzVedXbKy5RFepssQM= +github.com/mitchellh/go-testing-interface v1.14.1 h1:jrgshOhYAUVNMAJiKbEu7EqAwgJJ2JqpQmpLJOu07cU= +github.com/mitchellh/go-testing-interface v1.14.1/go.mod h1:gfgS7OtZj6MA4U1UrDRp04twqAjfvlZyCfX3sDjEym8= +github.com/mitchellh/mapstructure v1.4.1 h1:CpVNEelQCZBooIPDn+AR3NpivK/TIKU8bDxdASFVQag= +github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc= +github.com/mostynb/zstdpool-freelist v0.0.0-20201229113212-927304c0c3b1 h1:mPMvm6X6tf4w8y7j9YIt6V9jfWhL6QlbEc7CCmeQlWk= +github.com/mostynb/zstdpool-freelist v0.0.0-20201229113212-927304c0c3b1/go.mod h1:ye2e/VUEtE2BHE+G/QcKkcLQVAEJoYRFj5VUOQatCRE= +github.com/mr-tron/base58 v1.2.0 h1:T/HDJBh4ZCPbU39/+c3rRvE0uKBQlU27+QI8LJ4t64o= +github.com/mr-tron/base58 v1.2.0/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc= +github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= +github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= +github.com/onsi/gomega v1.10.1 h1:o0+MgICZLuZ7xjH7Vx6zS/zcu93/BEp1VwkIW1mEXCE= +github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible h1:Bn1aCHHRnjv4Bl16T8rcaFjYSrGrIZvpiGO6P3Q4GpU= +github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA= +github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8= +github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= +github.com/streamingfast/logging v0.0.0-20230608130331-f22c91403091 h1:RN5mrigyirb8anBEtdjtHFIufXdacyTi6i4KBfeNXeo= +github.com/streamingfast/logging v0.0.0-20230608130331-f22c91403091/go.mod h1:VlduQ80JcGJSargkRU4Sg9Xo63wZD/l8A5NC/Uo1/uU= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/supranational/blst v0.3.16-0.20250831170142-f48500c1fdbe h1:nbdqkIGOGfUAD54q1s2YBcBz/WcsxCO9HUQ4aGV5hUw= +github.com/supranational/blst v0.3.16-0.20250831170142-f48500c1fdbe/go.mod h1:jZJtfjgudtNl4en1tzwPIV3KjUnQUvG3/j+w+fVonLw= +github.com/test-go/testify v1.1.4 h1:Tf9lntrKUMHiXQ07qBScBTSA0dhYQlu83hswqelv1iE= +github.com/test-go/testify v1.1.4/go.mod h1:rH7cfJo/47vWGdi4GPj16x3/t1xGOj2YxzmNQzk2ghU= +github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU= +github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI= +github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk= +github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY= +github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= +github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= +github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo= +github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= +github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= +github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4= +github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM= +github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f h1:J9EGpcZtP0E/raorCMxlFGSTBrsSlaDGf3jU/qvAE2c= +github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= +github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0= +github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= +github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74= +github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= +github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA= +github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +go.mongodb.org/mongo-driver v1.12.2 h1:gbWY1bJkkmUB9jjZzcdhOL8O85N9H+Vvsf2yFN0RDws= +go.mongodb.org/mongo-driver v1.12.2/go.mod h1:/rGBTebI3XYboVmgz+Wv3Bcbl3aD0QF9zl6kDDw18rQ= +go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw= +go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/goleak v1.1.11/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= +go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4= +go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= +go.uber.org/ratelimit v0.2.0 h1:UQE2Bgi7p2B85uP5dC2bbRtig0C+OeNRnNEafLjsLPA= +go.uber.org/ratelimit v0.2.0/go.mod h1:YYBV4e4naJvhpitQrWJu1vCpgB7CboMe0qhltKt6mUg= +go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= +go.uber.org/zap v1.21.0 h1:WefMeulhovoZ2sYXz7st6K0sLj7bBhpiFaud4r4zST8= +go.uber.org/zap v1.21.0/go.mod h1:wjWOCqI0f2ZZrJF/UufIOkiC8ii6tm1iqIsLo76RfJw= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20220214200702-86341886e292/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU= +golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU= +golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= +golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= +golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q= +golang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU= +golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY= +golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= +golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/examples/go/servers/echo/main.go b/examples/go/servers/echo/main.go new file mode 100644 index 0000000000..9c34a3998e --- /dev/null +++ b/examples/go/servers/echo/main.go @@ -0,0 +1,153 @@ +package main + +import ( + "fmt" + "net/http" + "os" + "time" + + x402 "github.com/coinbase/x402/go" + x402http "github.com/coinbase/x402/go/http" + echomw "github.com/coinbase/x402/go/http/echo" + evm "github.com/coinbase/x402/go/mechanisms/evm/exact/server" + svm "github.com/coinbase/x402/go/mechanisms/svm/exact/server" + "github.com/joho/godotenv" + "github.com/labstack/echo/v4" +) + +const ( + DefaultPort = "4021" +) + +func main() { + godotenv.Load() + + evmAddress := os.Getenv("EVM_PAYEE_ADDRESS") + if evmAddress == "" { + fmt.Println("❌ EVM_PAYEE_ADDRESS environment variable is required") + os.Exit(1) + } + + svmAddress := os.Getenv("SVM_PAYEE_ADDRESS") + if svmAddress == "" { + fmt.Println("❌ SVM_PAYEE_ADDRESS environment variable is required") + os.Exit(1) + } + + facilitatorURL := os.Getenv("FACILITATOR_URL") + if facilitatorURL == "" { + fmt.Println("❌ FACILITATOR_URL environment variable is required") + fmt.Println(" Example: https://x402.org/facilitator") + os.Exit(1) + } + + // Network configuration - Base Sepolia testnet + evmNetwork := x402.Network("eip155:84532") + svmNetwork := x402.Network("solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1") + + fmt.Printf("🚀 Starting Echo x402 server...\n") + fmt.Printf(" EVM Payee address: %s\n", evmAddress) + fmt.Printf(" SVM Payee address: %s\n", svmAddress) + fmt.Printf(" EVM Network: %s\n", evmNetwork) + fmt.Printf(" SVM Network: %s\n", svmNetwork) + fmt.Printf(" Facilitator: %s\n", facilitatorURL) + + // Create Echo instance + e := echo.New() + e.HideBanner = true + + // Create HTTP facilitator client + facilitatorClient := x402http.NewHTTPFacilitatorClient(&x402http.FacilitatorConfig{ + URL: facilitatorURL, + }) + + /** + * Configure x402 payment middleware + * + * This middleware protects specific routes with payment requirements. + * When a client accesses a protected route without payment, they receive + * a 402 Payment Required response with payment details. + */ + routes := x402http.RoutesConfig{ + "GET /weather": { + Accepts: x402http.PaymentOptions{ + { + Scheme: "exact", + Price: "$0.001", + Network: "eip155:84532", + PayTo: evmAddress, + }, + { + Scheme: "exact", + Price: "$0.001", + Network: "solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1", + PayTo: svmAddress, + }, + }, + Description: "Get weather data for a city", + MimeType: "application/json", + }, + } + + // Apply x402 payment middleware + e.Use(echomw.X402Payment(echomw.Config{ + Routes: routes, + Facilitator: facilitatorClient, + Schemes: []echomw.SchemeConfig{ + {Network: evmNetwork, Server: evm.NewExactEvmScheme()}, + {Network: svmNetwork, Server: svm.NewExactSvmScheme()}, + }, + Timeout: 30 * time.Second, + })) + + /** + * Protected endpoint - requires $0.001 USDC payment + * + * Clients must provide a valid x402 payment to access this endpoint. + * The payment is verified and settled before the endpoint handler runs. + */ + e.GET("/weather", func(c echo.Context) error { + city := c.QueryParam("city") + if city == "" { + city = "San Francisco" + } + + weatherData := map[string]map[string]interface{}{ + "San Francisco": {"weather": "foggy", "temperature": 60}, + "New York": {"weather": "cloudy", "temperature": 55}, + "London": {"weather": "rainy", "temperature": 50}, + "Tokyo": {"weather": "clear", "temperature": 65}, + } + + data, exists := weatherData[city] + if !exists { + data = map[string]interface{}{"weather": "sunny", "temperature": 70} + } + + return c.JSON(http.StatusOK, map[string]interface{}{ + "city": city, + "weather": data["weather"], + "temperature": data["temperature"], + "timestamp": time.Now().Format(time.RFC3339), + }) + }) + + /** + * Health check endpoint - no payment required + * + * This endpoint is not protected by x402 middleware. + */ + e.GET("/health", func(c echo.Context) error { + return c.JSON(http.StatusOK, map[string]interface{}{ + "status": "ok", + "version": "2.0.0", + }) + }) + + fmt.Printf(" Server listening on http://localhost:%s\n\n", DefaultPort) + + if err := e.Start(":" + DefaultPort); err != nil { + fmt.Printf("Error starting server: %v\n", err) + os.Exit(1) + } +} diff --git a/examples/go/servers/nethttp/README.md b/examples/go/servers/nethttp/README.md new file mode 100644 index 0000000000..14d3d44a92 --- /dev/null +++ b/examples/go/servers/nethttp/README.md @@ -0,0 +1,226 @@ +# x402-nethttp Example Server + +Standard library `net/http` server demonstrating how to protect API endpoints with a paywall using the +`x402/go/http/nethttp` middleware. + +## Prerequisites + +- Go 1.24 or higher +- Valid EVM address for receiving payments +- URL of a facilitator supporting the desired payment network, see [facilitator list](https://www.x402.org/ecosystem?category=facilitators) + +## Setup + +1. Copy `.env-example` to `.env`: + +```bash +cp .env-example .env +``` + +and fill required environment variables: + +- `FACILITATOR_URL` - Facilitator endpoint URL +- `EVM_PAYEE_ADDRESS` - Ethereum address to receive payments +- `SVM_PAYEE_ADDRESS` - Solana address to receive payments + +2. Install dependencies: +```bash +go mod download +``` + +3. Run the server: +```bash +go run main.go +``` + +## Testing the Server + +You can test the server using one of the example clients: + +### Using the Go HTTP Client +```bash +cd ../../clients/http +# Ensure .env is setup +go run main.go +``` + +These clients will demonstrate how to: +1. Make an initial request to get payment requirements +2. Process the payment requirements +3. Make a second request with the payment token + +## Example Endpoint + +The server includes a single example endpoint at `/weather` that accepts payment of 0.001 USDC on either Base Sepolia (EVM) or Solana Devnet (SVM). The endpoint returns a simple weather report. + +## Response Format + +### Payment Required (402) + +``` +HTTP/1.1 402 Payment Required +Content-Type: application/json +PAYMENT-REQUIRED: + +null +``` + +The `PAYMENT-REQUIRED` header contains base64-encoded JSON with the payment requirements. +Note: `amount` is in atomic units (e.g., 1000 = 0.001 USDC, since USDC has 6 decimals): + +```json +{ + "x402Version": 2, + "error": "Payment required", + "resource": { + "url": "http://localhost:4021/weather", + "description": "Get weather data for a city", + "mimeType": "application/json" + }, + "accepts": [ + { + "scheme": "exact", + "network": "eip155:84532", + "amount": "1000", + "asset": "0x036CbD53842c5426634e7929541eC2318f3dCF7e", + "payTo": "0x...", + "maxTimeoutSeconds": 300, + "extra": { + "name": "USDC", + "version": "2", + "resourceUrl": "http://localhost:4021/weather" + } + }, + { + "scheme": "exact", + "network": "solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1", + "amount": "1000", + "asset": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", + "payTo": "...", + "maxTimeoutSeconds": 300, + "extra": { + "feePayer": "...", + } + } + ] +} +``` + +### Successful Response + +``` +HTTP/1.1 200 OK +Content-Type: application/json +PAYMENT-RESPONSE: + +{"city":"San Francisco","weather":"foggy","temperature":60,"timestamp":"2025-01-01T12:00:00Z"} +``` + +The `PAYMENT-RESPONSE` header contains base64-encoded JSON with the settlement details: + +```json +{ + "success": true, + "transaction": "0x...", + "network": "eip155:84532", + "payer": "0x..." +} +``` + +## Extending the Example + +To add more paid endpoints, follow this pattern: + +```go +// First, configure the payment middleware with your routes +routes := x402http.RoutesConfig{ + "GET /your-endpoint": { + Accepts: x402http.PaymentOptions{ + { + Scheme: "exact", + PayTo: evmPayeeAddress, + Price: "$0.10", + Network: x402.Network("eip155:84532"), + }, + }, + Description: "Your endpoint description", + MimeType: "application/json", + }, +} + +mux := http.NewServeMux() + +// Define your routes +mux.HandleFunc("GET /your-endpoint", func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + json.NewEncoder(w).Encode(map[string]interface{}{ + // Your response data + }) +}) + +// Apply x402 payment middleware +handler := nethttpmw.X402Payment(nethttpmw.Config{ + Routes: routes, + Facilitator: facilitatorClient, + Schemes: []nethttpmw.SchemeConfig{ + {Network: x402.Network("eip155:*"), Server: evm.NewExactEvmScheme()}, + }, + Timeout: 30 * time.Second, +})(mux) + +http.ListenAndServe(":4021", handler) +``` + +**Network identifiers** use [CAIP-2](https://github.com/ChainAgnostic/CAIPs/blob/main/CAIPs/caip-2.md) format, for example: +- `eip155:84532` — Base Sepolia +- `eip155:8453` — Base Mainnet +- `solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1` — Solana Devnet +- `solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp` — Solana Mainnet + +## x402ResourceServer Config + +The middleware uses scheme registrations to declare how payments for each network should be processed: + +```go +handler := nethttpmw.X402Payment(nethttpmw.Config{ + Routes: routes, + Facilitator: facilitatorClient, + Schemes: []nethttpmw.SchemeConfig{ + {Network: x402.Network("eip155:*"), Server: evm.NewExactEvmScheme()}, // All EVM chains + // {Network: x402.Network("solana:*"), Server: svm.NewExactSvmScheme()}, // All SVM chains + }, + Timeout: 30 * time.Second, +})(mux) +``` + +## Facilitator Config + +The `HTTPFacilitatorClient` connects to a facilitator service that verifies and settles payments on-chain: + +```go +facilitatorClient := x402http.NewHTTPFacilitatorClient(&x402http.FacilitatorConfig{ + URL: facilitatorURL, +}) + +// Or use multiple facilitators for redundancy +facilitatorClients := []x402.FacilitatorClient{ + x402http.NewHTTPFacilitatorClient(&x402http.FacilitatorConfig{URL: primaryFacilitatorURL}), + x402http.NewHTTPFacilitatorClient(&x402http.FacilitatorConfig{URL: backupFacilitatorURL}), +} +``` + +## Next Steps + +See [Advanced Examples](../advanced/) for: +- **Bazaar discovery** — make your API discoverable +- **Dynamic pricing** — price based on request context +- **Dynamic payTo** — route payments to different recipients +- **Lifecycle hooks** — custom logic on verify/settle +- **Custom tokens** — accept payments in custom tokens + +## Related Resources + +- [net/http Documentation](https://pkg.go.dev/net/http) +- [x402 Go Package Documentation](../../../../go/) +- [Client Examples](../../clients/) — build clients that can make paid requests diff --git a/examples/go/servers/nethttp/go.mod b/examples/go/servers/nethttp/go.mod new file mode 100644 index 0000000000..cf8d3a11de --- /dev/null +++ b/examples/go/servers/nethttp/go.mod @@ -0,0 +1,59 @@ +module github.com/coinbase/x402/examples/go/servers/nethttp + +go 1.24.0 + +toolchain go1.24.1 + +replace github.com/coinbase/x402/go => ../../../../go + +require ( + github.com/coinbase/x402/go v0.0.0 + github.com/joho/godotenv v1.5.1 +) + +require ( + filippo.io/edwards25519 v1.0.0-rc.1 // indirect + github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20251001021608-1fe7b43fc4d6 // indirect + github.com/andres-erbsen/clock v0.0.0-20160526145045-9e14626cd129 // indirect + github.com/bits-and-blooms/bitset v1.20.0 // indirect + github.com/blendle/zapdriver v1.3.1 // indirect + github.com/consensys/gnark-crypto v0.18.0 // indirect + github.com/crate-crypto/go-eth-kzg v1.4.0 // indirect + github.com/crate-crypto/go-ipa v0.0.0-20240724233137-53bbb0ceb27a // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 // indirect + github.com/ethereum/c-kzg-4844/v2 v2.1.5 // indirect + github.com/ethereum/go-ethereum v1.16.7 // indirect + github.com/ethereum/go-verkle v0.2.2 // indirect + github.com/fatih/color v1.16.0 // indirect + github.com/gagliardetto/binary v0.8.0 // indirect + github.com/gagliardetto/solana-go v1.14.0 // indirect + github.com/gagliardetto/treeout v0.1.4 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/holiman/uint256 v1.3.2 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/compress v1.16.0 // indirect + github.com/logrusorgru/aurora v2.0.3+incompatible // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mitchellh/go-testing-interface v1.14.1 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/mostynb/zstdpool-freelist v0.0.0-20201229113212-927304c0c3b1 // indirect + github.com/mr-tron/base58 v1.2.0 // indirect + github.com/streamingfast/logging v0.0.0-20230608130331-f22c91403091 // indirect + github.com/supranational/blst v0.3.16-0.20250831170142-f48500c1fdbe // indirect + github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f // indirect + github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect + github.com/xeipuuv/gojsonschema v1.2.0 // indirect + go.mongodb.org/mongo-driver v1.12.2 // indirect + go.uber.org/atomic v1.7.0 // indirect + go.uber.org/multierr v1.6.0 // indirect + go.uber.org/ratelimit v0.2.0 // indirect + go.uber.org/zap v1.21.0 // indirect + golang.org/x/crypto v0.41.0 // indirect + golang.org/x/sync v0.16.0 // indirect + golang.org/x/sys v0.36.0 // indirect + golang.org/x/term v0.34.0 // indirect + golang.org/x/time v0.9.0 // indirect +) diff --git a/examples/go/servers/nethttp/go.sum b/examples/go/servers/nethttp/go.sum new file mode 100644 index 0000000000..f5e8398346 --- /dev/null +++ b/examples/go/servers/nethttp/go.sum @@ -0,0 +1,237 @@ +filippo.io/edwards25519 v1.0.0-rc.1 h1:m0VOOB23frXZvAOK44usCgLWvtsxIoMCTBGJZlpmGfU= +filippo.io/edwards25519 v1.0.0-rc.1/go.mod h1:N1IkdkCkiLB6tki+MYJoSx2JTY9NUlxZE7eHn5EwJns= +github.com/AlekSi/pointer v1.1.0 h1:SSDMPcXD9jSl8FPy9cRzoRaMJtm9g9ggGTxecRUbQoI= +github.com/AlekSi/pointer v1.1.0/go.mod h1:y7BvfRI3wXPWKXEBhU71nbnIEEZX0QTSB2Bj48UJIZE= +github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20251001021608-1fe7b43fc4d6 h1:1zYrtlhrZ6/b6SAjLSfKzWtdgqK0U+HtH/VcBWh1BaU= +github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20251001021608-1fe7b43fc4d6/go.mod h1:ioLG6R+5bUSO1oeGSDxOV3FADARuMoytZCSX6MEMQkI= +github.com/StackExchange/wmi v1.2.1 h1:VIkavFPXSjcnS+O8yTq7NI32k0R5Aj+v39y29VYDOSA= +github.com/StackExchange/wmi v1.2.1/go.mod h1:rcmrprowKIVzvc+NUiLncP2uuArMWLCbu9SBzvHz7e8= +github.com/andres-erbsen/clock v0.0.0-20160526145045-9e14626cd129 h1:MzBOUgng9orim59UnfUTLRjMpd09C5uEVQ6RPGeCaVI= +github.com/andres-erbsen/clock v0.0.0-20160526145045-9e14626cd129/go.mod h1:rFgpPQZYZ8vdbc+48xibu8ALc3yeyd64IhHS+PU6Yyg= +github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8= +github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= +github.com/bits-and-blooms/bitset v1.20.0 h1:2F+rfL86jE2d/bmw7OhqUg2Sj/1rURkBn3MdfoPyRVU= +github.com/bits-and-blooms/bitset v1.20.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8= +github.com/blendle/zapdriver v1.3.1 h1:C3dydBOWYRiOk+B8X9IVZ5IOe+7cl+tGOexN4QqHfpE= +github.com/blendle/zapdriver v1.3.1/go.mod h1:mdXfREi6u5MArG4j9fewC+FGnXaBR+T4Ox4J2u4eHCc= +github.com/consensys/gnark-crypto v0.18.0 h1:vIye/FqI50VeAr0B3dx+YjeIvmc3LWz4yEfbWBpTUf0= +github.com/consensys/gnark-crypto v0.18.0/go.mod h1:L3mXGFTe1ZN+RSJ+CLjUt9x7PNdx8ubaYfDROyp2Z8c= +github.com/crate-crypto/go-eth-kzg v1.4.0 h1:WzDGjHk4gFg6YzV0rJOAsTK4z3Qkz5jd4RE3DAvPFkg= +github.com/crate-crypto/go-eth-kzg v1.4.0/go.mod h1:J9/u5sWfznSObptgfa92Jq8rTswn6ahQWEuiLHOjCUI= +github.com/crate-crypto/go-ipa v0.0.0-20240724233137-53bbb0ceb27a h1:W8mUrRp6NOVl3J+MYp5kPMoUZPp7aOYHtaua31lwRHg= +github.com/crate-crypto/go-ipa v0.0.0-20240724233137-53bbb0ceb27a/go.mod h1:sTwzHBvIzm2RfVCGNEBZgRyjwK40bVoun3ZnGOCafNM= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/decred/dcrd/crypto/blake256 v1.0.0 h1:/8DMNYp9SGi5f0w7uCm6d6M4OU2rGFK09Y2A4Xv7EE0= +github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 h1:YLtO71vCjJRCBcrPMtQ9nqBsqpA1m5sE92cU+pd5Mcc= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1/go.mod h1:hyedUtir6IdtD/7lIxGeCxkaw7y45JueMRL4DIyJDKs= +github.com/emicklei/dot v1.6.2 h1:08GN+DD79cy/tzN6uLCT84+2Wk9u+wvqP+Hkx/dIR8A= +github.com/emicklei/dot v1.6.2/go.mod h1:DeV7GvQtIw4h2u73RKBkkFdvVAz0D9fzeJrgPW6gy/s= +github.com/ethereum/c-kzg-4844/v2 v2.1.5 h1:aVtoLK5xwJ6c5RiqO8g8ptJ5KU+2Hdquf6G3aXiHh5s= +github.com/ethereum/c-kzg-4844/v2 v2.1.5/go.mod h1:u59hRTTah4Co6i9fDWtiCjTrblJv0UwsqZKCc0GfgUs= +github.com/ethereum/go-ethereum v1.16.7 h1:qeM4TvbrWK0UC0tgkZ7NiRsmBGwsjqc64BHo20U59UQ= +github.com/ethereum/go-ethereum v1.16.7/go.mod h1:Fs6QebQbavneQTYcA39PEKv2+zIjX7rPUZ14DER46wk= +github.com/ethereum/go-verkle v0.2.2 h1:I2W0WjnrFUIzzVPwm8ykY+7pL2d4VhlsePn4j7cnFk8= +github.com/ethereum/go-verkle v0.2.2/go.mod h1:M3b90YRnzqKyyzBEWJGqj8Qff4IDeXnzFw0P9bFw3uk= +github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= +github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= +github.com/ferranbt/fastssz v0.1.4 h1:OCDB+dYDEQDvAgtAGnTSidK1Pe2tW3nFV40XyMkTeDY= +github.com/ferranbt/fastssz v0.1.4/go.mod h1:Ea3+oeoRGGLGm5shYAeDgu6PGUlcvQhE2fILyD9+tGg= +github.com/gagliardetto/binary v0.8.0 h1:U9ahc45v9HW0d15LoN++vIXSJyqR/pWw8DDlhd7zvxg= +github.com/gagliardetto/binary v0.8.0/go.mod h1:2tfj51g5o9dnvsc+fL3Jxr22MuWzYXwx9wEoN0XQ7/c= +github.com/gagliardetto/gofuzz v1.2.2 h1:XL/8qDMzcgvR4+CyRQW9UGdwPRPMHVJfqQ/uMvSUuQw= +github.com/gagliardetto/gofuzz v1.2.2/go.mod h1:bkH/3hYLZrMLbfYWA0pWzXmi5TTRZnu4pMGZBkqMKvY= +github.com/gagliardetto/solana-go v1.14.0 h1:3WfAi70jOOjAJ0deFMjdhFYlLXATF4tOQXsDNWJtOLw= +github.com/gagliardetto/solana-go v1.14.0/go.mod h1:l/qqqIN6qJJPtxW/G1PF4JtcE3Zg2vD2EliZrr9Gn5k= +github.com/gagliardetto/treeout v0.1.4 h1:ozeYerrLCmCubo1TcIjFiOWTTGteOOHND1twdFpgwaw= +github.com/gagliardetto/treeout v0.1.4/go.mod h1:loUefvXTrlRG5rYmJmExNryyBRh8f89VZhmMOyCyqok= +github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= +github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= +github.com/gofrs/flock v0.12.1 h1:MTLVXXHf8ekldpJk3AKicLij9MdwOWkZ+a/jHHZby9E= +github.com/gofrs/flock v0.12.1/go.mod h1:9zxTsyu5xtJ9DK+1tFZyibEV7y3uwDxPPfbxeeHCoD0= +github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs= +github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/go-cmp v0.5.2 h1:X2ev0eStA3AbceY54o37/0PQ/UWqKEiiO2dKL5OPaFM= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= +github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/holiman/uint256 v1.3.2 h1:a9EgMPSC1AAaj1SZL5zIQD3WbwTuHrMGOerLjGmM/TA= +github.com/holiman/uint256 v1.3.2/go.mod h1:EOMSn4q6Nyt9P6efbI3bueV4e1b3dGlUCXeiRV4ng7E= +github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= +github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/klauspost/compress v1.11.4/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= +github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= +github.com/klauspost/compress v1.16.0 h1:iULayQNOReoYUe+1qtKOqw9CwJv3aNQu8ivo7lw1HU4= +github.com/klauspost/compress v1.16.0/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= +github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= +github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/leanovate/gopter v0.2.11 h1:vRjThO1EKPb/1NsDXuDrzldR28RLkBflWYcU9CvzWu4= +github.com/leanovate/gopter v0.2.11/go.mod h1:aK3tzZP/C+p1m3SPRE4SYZFGP7jjkuSI4f7Xvpt0S9c= +github.com/logrusorgru/aurora v2.0.3+incompatible h1:tOpm7WcpBTn4fjmVfgpQq0EfczGlG91VSDkswnjF5A8= +github.com/logrusorgru/aurora v2.0.3+incompatible/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU= +github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/minio/sha256-simd v1.0.0 h1:v1ta+49hkWZyvaKwrQB8elexRqm6Y0aMLjCNsrYxo6g= +github.com/minio/sha256-simd v1.0.0/go.mod h1:OuYzVNI5vcoYIAmbIvHPl3N3jUzVedXbKy5RFepssQM= +github.com/mitchellh/go-testing-interface v1.14.1 h1:jrgshOhYAUVNMAJiKbEu7EqAwgJJ2JqpQmpLJOu07cU= +github.com/mitchellh/go-testing-interface v1.14.1/go.mod h1:gfgS7OtZj6MA4U1UrDRp04twqAjfvlZyCfX3sDjEym8= +github.com/mitchellh/mapstructure v1.4.1 h1:CpVNEelQCZBooIPDn+AR3NpivK/TIKU8bDxdASFVQag= +github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc= +github.com/mostynb/zstdpool-freelist v0.0.0-20201229113212-927304c0c3b1 h1:mPMvm6X6tf4w8y7j9YIt6V9jfWhL6QlbEc7CCmeQlWk= +github.com/mostynb/zstdpool-freelist v0.0.0-20201229113212-927304c0c3b1/go.mod h1:ye2e/VUEtE2BHE+G/QcKkcLQVAEJoYRFj5VUOQatCRE= +github.com/mr-tron/base58 v1.2.0 h1:T/HDJBh4ZCPbU39/+c3rRvE0uKBQlU27+QI8LJ4t64o= +github.com/mr-tron/base58 v1.2.0/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc= +github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= +github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= +github.com/onsi/gomega v1.10.1 h1:o0+MgICZLuZ7xjH7Vx6zS/zcu93/BEp1VwkIW1mEXCE= +github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible h1:Bn1aCHHRnjv4Bl16T8rcaFjYSrGrIZvpiGO6P3Q4GpU= +github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA= +github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8= +github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= +github.com/streamingfast/logging v0.0.0-20230608130331-f22c91403091 h1:RN5mrigyirb8anBEtdjtHFIufXdacyTi6i4KBfeNXeo= +github.com/streamingfast/logging v0.0.0-20230608130331-f22c91403091/go.mod h1:VlduQ80JcGJSargkRU4Sg9Xo63wZD/l8A5NC/Uo1/uU= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/supranational/blst v0.3.16-0.20250831170142-f48500c1fdbe h1:nbdqkIGOGfUAD54q1s2YBcBz/WcsxCO9HUQ4aGV5hUw= +github.com/supranational/blst v0.3.16-0.20250831170142-f48500c1fdbe/go.mod h1:jZJtfjgudtNl4en1tzwPIV3KjUnQUvG3/j+w+fVonLw= +github.com/test-go/testify v1.1.4 h1:Tf9lntrKUMHiXQ07qBScBTSA0dhYQlu83hswqelv1iE= +github.com/test-go/testify v1.1.4/go.mod h1:rH7cfJo/47vWGdi4GPj16x3/t1xGOj2YxzmNQzk2ghU= +github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU= +github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI= +github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk= +github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY= +github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= +github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4= +github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM= +github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f h1:J9EGpcZtP0E/raorCMxlFGSTBrsSlaDGf3jU/qvAE2c= +github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= +github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0= +github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= +github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74= +github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= +github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA= +github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +go.mongodb.org/mongo-driver v1.12.2 h1:gbWY1bJkkmUB9jjZzcdhOL8O85N9H+Vvsf2yFN0RDws= +go.mongodb.org/mongo-driver v1.12.2/go.mod h1:/rGBTebI3XYboVmgz+Wv3Bcbl3aD0QF9zl6kDDw18rQ= +go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw= +go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/goleak v1.1.11/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= +go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4= +go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= +go.uber.org/ratelimit v0.2.0 h1:UQE2Bgi7p2B85uP5dC2bbRtig0C+OeNRnNEafLjsLPA= +go.uber.org/ratelimit v0.2.0/go.mod h1:YYBV4e4naJvhpitQrWJu1vCpgB7CboMe0qhltKt6mUg= +go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= +go.uber.org/zap v1.21.0 h1:WefMeulhovoZ2sYXz7st6K0sLj7bBhpiFaud4r4zST8= +go.uber.org/zap v1.21.0/go.mod h1:wjWOCqI0f2ZZrJF/UufIOkiC8ii6tm1iqIsLo76RfJw= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20220214200702-86341886e292/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4= +golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE= +golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= +golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k= +golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.34.0 h1:O/2T7POpk0ZZ7MAzMeWFSg6S5IpWd/RXDlM9hgM3DR4= +golang.org/x/term v0.34.0/go.mod h1:5jC53AEywhIVebHgPVeg0mj8OD3VO9OzclacVrqpaAw= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng= +golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU= +golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY= +golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/examples/go/servers/nethttp/main.go b/examples/go/servers/nethttp/main.go new file mode 100644 index 0000000000..71c683874a --- /dev/null +++ b/examples/go/servers/nethttp/main.go @@ -0,0 +1,156 @@ +package main + +import ( + "encoding/json" + "fmt" + "net/http" + "os" + "time" + + x402 "github.com/coinbase/x402/go" + x402http "github.com/coinbase/x402/go/http" + nethttpmw "github.com/coinbase/x402/go/http/nethttp" + evm "github.com/coinbase/x402/go/mechanisms/evm/exact/server" + svm "github.com/coinbase/x402/go/mechanisms/svm/exact/server" + "github.com/joho/godotenv" +) + +const ( + DefaultPort = "4021" +) + +func main() { + godotenv.Load() + + evmAddress := os.Getenv("EVM_PAYEE_ADDRESS") + if evmAddress == "" { + fmt.Println("❌ EVM_PAYEE_ADDRESS environment variable is required") + os.Exit(1) + } + + svmAddress := os.Getenv("SVM_PAYEE_ADDRESS") + if svmAddress == "" { + fmt.Println("❌ SVM_PAYEE_ADDRESS environment variable is required") + os.Exit(1) + } + + facilitatorURL := os.Getenv("FACILITATOR_URL") + if facilitatorURL == "" { + fmt.Println("❌ FACILITATOR_URL environment variable is required") + fmt.Println(" Example: https://x402.org/facilitator") + os.Exit(1) + } + + // Network configuration - Base Sepolia testnet + evmNetwork := x402.Network("eip155:84532") + svmNetwork := x402.Network("solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1") + + fmt.Printf("🚀 Starting net/http x402 server...\n") + fmt.Printf(" EVM Payee address: %s\n", evmAddress) + fmt.Printf(" SVM Payee address: %s\n", svmAddress) + fmt.Printf(" EVM Network: %s\n", evmNetwork) + fmt.Printf(" SVM Network: %s\n", svmNetwork) + fmt.Printf(" Facilitator: %s\n", facilitatorURL) + + // Create HTTP facilitator client + facilitatorClient := x402http.NewHTTPFacilitatorClient(&x402http.FacilitatorConfig{ + URL: facilitatorURL, + }) + + /** + * Configure x402 payment middleware + * + * This middleware protects specific routes with payment requirements. + * When a client accesses a protected route without payment, they receive + * a 402 Payment Required response with payment details. + */ + routes := x402http.RoutesConfig{ + "GET /weather": { + Accepts: x402http.PaymentOptions{ + { + Scheme: "exact", + Price: "$0.001", + Network: "eip155:84532", + PayTo: evmAddress, + }, + { + Scheme: "exact", + Price: "$0.001", + Network: "solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1", + PayTo: svmAddress, + }, + }, + Description: "Get weather data for a city", + MimeType: "application/json", + }, + } + + // Create ServeMux and register handlers + mux := http.NewServeMux() + + /** + * Protected endpoint - requires $0.001 USDC payment + * + * Clients must provide a valid x402 payment to access this endpoint. + * The payment is verified and settled before the endpoint handler runs. + */ + mux.HandleFunc("GET /weather", func(w http.ResponseWriter, r *http.Request) { + city := r.URL.Query().Get("city") + if city == "" { + city = "San Francisco" + } + + weatherData := map[string]map[string]interface{}{ + "San Francisco": {"weather": "foggy", "temperature": 60}, + "New York": {"weather": "cloudy", "temperature": 55}, + "London": {"weather": "rainy", "temperature": 50}, + "Tokyo": {"weather": "clear", "temperature": 65}, + } + + data, exists := weatherData[city] + if !exists { + data = map[string]interface{}{"weather": "sunny", "temperature": 70} + } + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _ = json.NewEncoder(w).Encode(map[string]interface{}{ + "city": city, + "weather": data["weather"], + "temperature": data["temperature"], + "timestamp": time.Now().Format(time.RFC3339), + }) + }) + + /** + * Health check endpoint - no payment required + * + * This endpoint is not protected by x402 middleware. + */ + mux.HandleFunc("GET /health", func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _ = json.NewEncoder(w).Encode(map[string]interface{}{ + "status": "ok", + "version": "2.0.0", + }) + }) + + // Apply x402 payment middleware + handler := nethttpmw.X402Payment(nethttpmw.Config{ + Routes: routes, + Facilitator: facilitatorClient, + Schemes: []nethttpmw.SchemeConfig{ + {Network: evmNetwork, Server: evm.NewExactEvmScheme()}, + {Network: svmNetwork, Server: svm.NewExactSvmScheme()}, + }, + Timeout: 30 * time.Second, + })(mux) + + fmt.Printf(" Server listening on http://localhost:%s\n\n", DefaultPort) + + if err := http.ListenAndServe(":"+DefaultPort, handler); err != nil { + fmt.Printf("Error starting server: %v\n", err) + os.Exit(1) + } +} diff --git a/examples/go/servers/upto/go.mod b/examples/go/servers/upto/go.mod new file mode 100644 index 0000000000..dceea77cf5 --- /dev/null +++ b/examples/go/servers/upto/go.mod @@ -0,0 +1,60 @@ +module github.com/coinbase/x402/examples/go/servers/upto + +go 1.24.0 + +toolchain go1.24.1 + +replace github.com/coinbase/x402/go => ../../../../go + +require ( + github.com/coinbase/x402/go v0.0.0-00010101000000-000000000000 + github.com/gin-gonic/gin v1.11.0 + github.com/joho/godotenv v1.5.1 +) + +require ( + github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20251001021608-1fe7b43fc4d6 // indirect + github.com/bits-and-blooms/bitset v1.20.0 // indirect + github.com/bytedance/sonic v1.14.0 // indirect + github.com/bytedance/sonic/loader v0.3.0 // indirect + github.com/cloudwego/base64x v0.1.6 // indirect + github.com/consensys/gnark-crypto v0.18.0 // indirect + github.com/crate-crypto/go-eth-kzg v1.4.0 // indirect + github.com/crate-crypto/go-ipa v0.0.0-20240724233137-53bbb0ceb27a // indirect + github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 // indirect + github.com/ethereum/c-kzg-4844/v2 v2.1.5 // indirect + github.com/ethereum/go-ethereum v1.16.7 // indirect + github.com/ethereum/go-verkle v0.2.2 // indirect + github.com/gabriel-vasile/mimetype v1.4.8 // indirect + github.com/gin-contrib/sse v1.1.0 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/go-playground/validator/v10 v10.27.0 // indirect + github.com/goccy/go-json v0.10.4 // indirect + github.com/goccy/go-yaml v1.18.0 // indirect + github.com/holiman/uint256 v1.3.2 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/cpuid/v2 v2.3.0 // indirect + github.com/leodido/go-urn v1.4.0 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/pelletier/go-toml/v2 v2.2.4 // indirect + github.com/quic-go/qpack v0.5.1 // indirect + github.com/quic-go/quic-go v0.55.0 // indirect + github.com/supranational/blst v0.3.16-0.20250831170142-f48500c1fdbe // indirect + github.com/twitchyliquid64/golang-asm v0.15.1 // indirect + github.com/ugorji/go/codec v1.3.0 // indirect + github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f // indirect + github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect + github.com/xeipuuv/gojsonschema v1.2.0 // indirect + golang.org/x/arch v0.20.0 // indirect + golang.org/x/crypto v0.46.0 // indirect + golang.org/x/mod v0.30.0 // indirect + golang.org/x/net v0.48.0 // indirect + golang.org/x/sync v0.19.0 // indirect + golang.org/x/sys v0.39.0 // indirect + golang.org/x/text v0.32.0 // indirect + golang.org/x/tools v0.39.0 // indirect + google.golang.org/protobuf v1.36.9 // indirect +) diff --git a/examples/go/servers/upto/go.sum b/examples/go/servers/upto/go.sum new file mode 100644 index 0000000000..289a1ffb33 --- /dev/null +++ b/examples/go/servers/upto/go.sum @@ -0,0 +1,159 @@ +github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20251001021608-1fe7b43fc4d6 h1:1zYrtlhrZ6/b6SAjLSfKzWtdgqK0U+HtH/VcBWh1BaU= +github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20251001021608-1fe7b43fc4d6/go.mod h1:ioLG6R+5bUSO1oeGSDxOV3FADARuMoytZCSX6MEMQkI= +github.com/StackExchange/wmi v1.2.1 h1:VIkavFPXSjcnS+O8yTq7NI32k0R5Aj+v39y29VYDOSA= +github.com/StackExchange/wmi v1.2.1/go.mod h1:rcmrprowKIVzvc+NUiLncP2uuArMWLCbu9SBzvHz7e8= +github.com/bits-and-blooms/bitset v1.20.0 h1:2F+rfL86jE2d/bmw7OhqUg2Sj/1rURkBn3MdfoPyRVU= +github.com/bits-and-blooms/bitset v1.20.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8= +github.com/bytedance/sonic v1.14.0 h1:/OfKt8HFw0kh2rj8N0F6C/qPGRESq0BbaNZgcNXXzQQ= +github.com/bytedance/sonic v1.14.0/go.mod h1:WoEbx8WTcFJfzCe0hbmyTGrfjt8PzNEBdxlNUO24NhA= +github.com/bytedance/sonic/loader v0.3.0 h1:dskwH8edlzNMctoruo8FPTJDF3vLtDT0sXZwvZJyqeA= +github.com/bytedance/sonic/loader v0.3.0/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI= +github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M= +github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU= +github.com/consensys/gnark-crypto v0.18.0 h1:vIye/FqI50VeAr0B3dx+YjeIvmc3LWz4yEfbWBpTUf0= +github.com/consensys/gnark-crypto v0.18.0/go.mod h1:L3mXGFTe1ZN+RSJ+CLjUt9x7PNdx8ubaYfDROyp2Z8c= +github.com/crate-crypto/go-eth-kzg v1.4.0 h1:WzDGjHk4gFg6YzV0rJOAsTK4z3Qkz5jd4RE3DAvPFkg= +github.com/crate-crypto/go-eth-kzg v1.4.0/go.mod h1:J9/u5sWfznSObptgfa92Jq8rTswn6ahQWEuiLHOjCUI= +github.com/crate-crypto/go-ipa v0.0.0-20240724233137-53bbb0ceb27a h1:W8mUrRp6NOVl3J+MYp5kPMoUZPp7aOYHtaua31lwRHg= +github.com/crate-crypto/go-ipa v0.0.0-20240724233137-53bbb0ceb27a/go.mod h1:sTwzHBvIzm2RfVCGNEBZgRyjwK40bVoun3ZnGOCafNM= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/decred/dcrd/crypto/blake256 v1.0.0 h1:/8DMNYp9SGi5f0w7uCm6d6M4OU2rGFK09Y2A4Xv7EE0= +github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 h1:YLtO71vCjJRCBcrPMtQ9nqBsqpA1m5sE92cU+pd5Mcc= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1/go.mod h1:hyedUtir6IdtD/7lIxGeCxkaw7y45JueMRL4DIyJDKs= +github.com/emicklei/dot v1.6.2 h1:08GN+DD79cy/tzN6uLCT84+2Wk9u+wvqP+Hkx/dIR8A= +github.com/emicklei/dot v1.6.2/go.mod h1:DeV7GvQtIw4h2u73RKBkkFdvVAz0D9fzeJrgPW6gy/s= +github.com/ethereum/c-kzg-4844/v2 v2.1.5 h1:aVtoLK5xwJ6c5RiqO8g8ptJ5KU+2Hdquf6G3aXiHh5s= +github.com/ethereum/c-kzg-4844/v2 v2.1.5/go.mod h1:u59hRTTah4Co6i9fDWtiCjTrblJv0UwsqZKCc0GfgUs= +github.com/ethereum/go-ethereum v1.16.7 h1:qeM4TvbrWK0UC0tgkZ7NiRsmBGwsjqc64BHo20U59UQ= +github.com/ethereum/go-ethereum v1.16.7/go.mod h1:Fs6QebQbavneQTYcA39PEKv2+zIjX7rPUZ14DER46wk= +github.com/ethereum/go-verkle v0.2.2 h1:I2W0WjnrFUIzzVPwm8ykY+7pL2d4VhlsePn4j7cnFk8= +github.com/ethereum/go-verkle v0.2.2/go.mod h1:M3b90YRnzqKyyzBEWJGqj8Qff4IDeXnzFw0P9bFw3uk= +github.com/ferranbt/fastssz v0.1.4 h1:OCDB+dYDEQDvAgtAGnTSidK1Pe2tW3nFV40XyMkTeDY= +github.com/ferranbt/fastssz v0.1.4/go.mod h1:Ea3+oeoRGGLGm5shYAeDgu6PGUlcvQhE2fILyD9+tGg= +github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM= +github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8= +github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w= +github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM= +github.com/gin-gonic/gin v1.11.0 h1:OW/6PLjyusp2PPXtyxKHU0RbX6I/l28FTdDlae5ueWk= +github.com/gin-gonic/gin v1.11.0/go.mod h1:+iq/FyxlGzII0KHiBGjuNn4UNENUlKbGlNmc+W50Dls= +github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= +github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= +github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.27.0 h1:w8+XrWVMhGkxOaaowyKH35gFydVHOvC0/uWoy2Fzwn4= +github.com/go-playground/validator/v10 v10.27.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo= +github.com/goccy/go-json v0.10.4 h1:JSwxQzIqKfmFX1swYPpUThQZp/Ka4wzJdK0LWVytLPM= +github.com/goccy/go-json v0.10.4/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= +github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw= +github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= +github.com/gofrs/flock v0.12.1 h1:MTLVXXHf8ekldpJk3AKicLij9MdwOWkZ+a/jHHZby9E= +github.com/gofrs/flock v0.12.1/go.mod h1:9zxTsyu5xtJ9DK+1tFZyibEV7y3uwDxPPfbxeeHCoD0= +github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs= +github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= +github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/holiman/uint256 v1.3.2 h1:a9EgMPSC1AAaj1SZL5zIQD3WbwTuHrMGOerLjGmM/TA= +github.com/holiman/uint256 v1.3.2/go.mod h1:EOMSn4q6Nyt9P6efbI3bueV4e1b3dGlUCXeiRV4ng7E= +github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= +github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= +github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/leanovate/gopter v0.2.11 h1:vRjThO1EKPb/1NsDXuDrzldR28RLkBflWYcU9CvzWu4= +github.com/leanovate/gopter v0.2.11/go.mod h1:aK3tzZP/C+p1m3SPRE4SYZFGP7jjkuSI4f7Xvpt0S9c= +github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= +github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU= +github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/minio/sha256-simd v1.0.0 h1:v1ta+49hkWZyvaKwrQB8elexRqm6Y0aMLjCNsrYxo6g= +github.com/minio/sha256-simd v1.0.0/go.mod h1:OuYzVNI5vcoYIAmbIvHPl3N3jUzVedXbKy5RFepssQM= +github.com/mitchellh/mapstructure v1.4.1 h1:CpVNEelQCZBooIPDn+AR3NpivK/TIKU8bDxdASFVQag= +github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= +github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= +github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= +github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI= +github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg= +github.com/quic-go/quic-go v0.55.0 h1:zccPQIqYCXDt5NmcEabyYvOnomjs8Tlwl7tISjJh9Mk= +github.com/quic-go/quic-go v0.55.0/go.mod h1:DR51ilwU1uE164KuWXhinFcKWGlEjzys2l8zUl5Ss1U= +github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible h1:Bn1aCHHRnjv4Bl16T8rcaFjYSrGrIZvpiGO6P3Q4GpU= +github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/supranational/blst v0.3.16-0.20250831170142-f48500c1fdbe h1:nbdqkIGOGfUAD54q1s2YBcBz/WcsxCO9HUQ4aGV5hUw= +github.com/supranational/blst v0.3.16-0.20250831170142-f48500c1fdbe/go.mod h1:jZJtfjgudtNl4en1tzwPIV3KjUnQUvG3/j+w+fVonLw= +github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU= +github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI= +github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk= +github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY= +github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= +github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= +github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA= +github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4= +github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f h1:J9EGpcZtP0E/raorCMxlFGSTBrsSlaDGf3jU/qvAE2c= +github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= +github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0= +github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= +github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74= +github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= +go.uber.org/mock v0.5.2 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko= +go.uber.org/mock v0.5.2/go.mod h1:wLlUxC2vVTPTaE3UD51E0BGOAElKrILxhVSDYQLld5o= +golang.org/x/arch v0.20.0 h1:dx1zTU0MAE98U+TQ8BLl7XsJbgze2WnNKF/8tGp/Q6c= +golang.org/x/arch v0.20.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk= +golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU= +golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0= +golang.org/x/mod v0.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk= +golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc= +golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU= +golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY= +golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= +golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= +golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU= +golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY= +golang.org/x/tools v0.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ= +golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ= +google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw= +google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/examples/go/servers/upto/main.go b/examples/go/servers/upto/main.go new file mode 100644 index 0000000000..3a2c41f77a --- /dev/null +++ b/examples/go/servers/upto/main.go @@ -0,0 +1,108 @@ +package main + +import ( + "fmt" + "math/rand" + "net/http" + "os" + "time" + + x402 "github.com/coinbase/x402/go" + x402http "github.com/coinbase/x402/go/http" + ginmw "github.com/coinbase/x402/go/http/gin" + uptoevm "github.com/coinbase/x402/go/mechanisms/evm/upto/server" + ginfw "github.com/gin-gonic/gin" + "github.com/joho/godotenv" +) + +const DefaultPort = "4021" + +func main() { + godotenv.Load() + + evmAddress := os.Getenv("EVM_PAYEE_ADDRESS") + if evmAddress == "" { + fmt.Println("EVM_PAYEE_ADDRESS environment variable is required") + os.Exit(1) + } + + facilitatorURL := os.Getenv("FACILITATOR_URL") + if facilitatorURL == "" { + fmt.Println("FACILITATOR_URL environment variable is required") + os.Exit(1) + } + + evmNetwork := x402.Network("eip155:84532") + + fmt.Printf("Starting Gin x402 upto server...\n") + fmt.Printf(" EVM Payee address: %s\n", evmAddress) + fmt.Printf(" EVM Network: %s\n", evmNetwork) + fmt.Printf(" Facilitator: %s\n", facilitatorURL) + + r := ginfw.Default() + + facilitatorClient := x402http.NewHTTPFacilitatorClient(&x402http.FacilitatorConfig{ + URL: facilitatorURL, + }) + + maxPrice := "$0.10" // Maximum the client authorizes + + routes := x402http.RoutesConfig{ + "GET /api/generate": { + Accepts: x402http.PaymentOptions{ + { + Scheme: "upto", + Price: maxPrice, + Network: evmNetwork, + PayTo: evmAddress, + }, + }, + Description: "AI text generation - billed by token usage", + MimeType: "application/json", + }, + } + + r.Use(ginmw.X402Payment(ginmw.Config{ + Routes: routes, + Facilitator: facilitatorClient, + Schemes: []ginmw.SchemeConfig{ + {Network: evmNetwork, Server: uptoevm.NewUptoEvmScheme()}, + }, + Timeout: 30 * time.Second, + })) + + r.GET("/api/generate", func(c *ginfw.Context) { + // Simulate work that produces a variable cost. + // In production this might be LLM token count, bytes served, compute time, etc. + maxAmountAtomic := 100000 // 10 cents in 6-decimal USDC atomic units + actualUsage := rand.Intn(maxAmountAtomic + 1) + + // Tell the middleware to settle only what was actually used. + ginmw.SetSettlementOverrides(c, &x402.SettlementOverrides{ + Amount: fmt.Sprintf("%d", actualUsage), + }) + + c.JSON(http.StatusOK, ginfw.H{ + "result": "Here is your generated text...", + "usage": ginfw.H{ + "authorizedMaxAtomic": fmt.Sprintf("%d", maxAmountAtomic), + "actualChargedAtomic": fmt.Sprintf("%d", actualUsage), + }, + }) + }) + + r.GET("/health", func(c *ginfw.Context) { + c.JSON(http.StatusOK, ginfw.H{ + "status": "ok", + "version": "2.0.0", + }) + }) + + fmt.Printf(" Server listening on http://localhost:%s\n\n", DefaultPort) + fmt.Println(" GET /api/generate - usage-based billing via upto scheme") + + if err := r.Run(":" + DefaultPort); err != nil { + fmt.Printf("Error starting server: %v\n", err) + os.Exit(1) + } +} diff --git a/examples/python/clients/advanced/all_networks.py b/examples/python/clients/advanced/all_networks.py index 0305161e72..fa4158d010 100644 --- a/examples/python/clients/advanced/all_networks.py +++ b/examples/python/clients/advanced/all_networks.py @@ -93,18 +93,13 @@ async def main() -> None: print(f"Response body: {response.text}") # Extract and print payment response if present - if response.is_success: - try: - settle_response = http_client.get_payment_settle_response( - lambda name: response.headers.get(name) - ) - print( - f"\nPayment response: {settle_response.model_dump_json(indent=2)}" - ) - except ValueError: - print("\nNo payment response header found") - else: - print(f"\nRequest failed (status: {response.status_code})") + try: + settle_response = http_client.get_payment_settle_response( + lambda name: response.headers.get(name) + ) + print(f"\nPayment response: {settle_response.model_dump_json(indent=2)}") + except ValueError: + print("\nNo payment response header found") if __name__ == "__main__": diff --git a/examples/python/clients/advanced/builder_pattern.py b/examples/python/clients/advanced/builder_pattern.py index 80290c7184..88de409ce9 100644 --- a/examples/python/clients/advanced/builder_pattern.py +++ b/examples/python/clients/advanced/builder_pattern.py @@ -92,16 +92,14 @@ async def run_builder_pattern_example( print(f"Response status: {response.status_code}") print(f"Response body: {response.text}") - if response.is_success: - try: - settle_response = http_client.get_payment_settle_response( - lambda name: response.headers.get(name) - ) - print( - f"\n💰 Payment Details: {settle_response.model_dump_json(indent=2)}" - ) - except ValueError: - print("\nNo payment response header found") + # Extract and print payment response if present + try: + settle_response = http_client.get_payment_settle_response( + lambda name: response.headers.get(name) + ) + print(f"\n💰 Payment Details: {settle_response.model_dump_json(indent=2)}") + except ValueError: + print("\nNo payment response header found") async def main() -> None: diff --git a/examples/python/clients/advanced/hooks.py b/examples/python/clients/advanced/hooks.py index 633c1c2658..1ce5a73bd4 100644 --- a/examples/python/clients/advanced/hooks.py +++ b/examples/python/clients/advanced/hooks.py @@ -122,16 +122,14 @@ async def run_hooks_example(private_key: str, url: str) -> None: print(f"Response status: {response.status_code}") print(f"Response body: {response.text}") - if response.is_success: - try: - settle_response = http_client.get_payment_settle_response( - lambda name: response.headers.get(name) - ) - print( - f"\n💰 Payment Details: {settle_response.model_dump_json(indent=2)}" - ) - except ValueError: - print("\nNo payment response header found") + # Extract and print payment response if present + try: + settle_response = http_client.get_payment_settle_response( + lambda name: response.headers.get(name) + ) + print(f"\n💰 Payment Details: {settle_response.model_dump_json(indent=2)}") + except ValueError: + print("\nNo payment response header found") async def main() -> None: diff --git a/examples/python/clients/advanced/preferred_network.py b/examples/python/clients/advanced/preferred_network.py index 3cb7a42471..498aef00ac 100644 --- a/examples/python/clients/advanced/preferred_network.py +++ b/examples/python/clients/advanced/preferred_network.py @@ -129,16 +129,14 @@ async def run_preferred_network_example( print(f"\nResponse status: {response.status_code}") print(f"Response body: {response.text}") - if response.is_success: - try: - settle_response = http_client.get_payment_settle_response( - lambda name: response.headers.get(name) - ) - print( - f"\n💰 Payment Details: {settle_response.model_dump_json(indent=2)}" - ) - except ValueError: - print("\nNo payment response header found") + # Extract and print payment response if present + try: + settle_response = http_client.get_payment_settle_response( + lambda name: response.headers.get(name) + ) + print(f"\n💰 Payment Details: {settle_response.model_dump_json(indent=2)}") + except ValueError: + print("\nNo payment response header found") async def main() -> None: diff --git a/examples/python/clients/httpx/main.py b/examples/python/clients/httpx/main.py index b8e8ad0aaf..d0a6c26099 100644 --- a/examples/python/clients/httpx/main.py +++ b/examples/python/clients/httpx/main.py @@ -85,18 +85,13 @@ async def main() -> None: print(f"Response body: {response.text}") # Extract and print payment response if present - if response.is_success: - try: - settle_response = http_client.get_payment_settle_response( - lambda name: response.headers.get(name) - ) - print( - f"\nPayment response: {settle_response.model_dump_json(indent=2)}" - ) - except ValueError: - print("\nNo payment response header found") - else: - print(f"\nRequest failed (status: {response.status_code})") + try: + settle_response = http_client.get_payment_settle_response( + lambda name: response.headers.get(name) + ) + print(f"\nPayment response: {settle_response.model_dump_json(indent=2)}") + except ValueError: + print("\nNo payment response header found") if __name__ == "__main__": diff --git a/examples/python/clients/httpx/uv.lock b/examples/python/clients/httpx/uv.lock index 8316ef3fec..b5b3fbfc6b 100644 --- a/examples/python/clients/httpx/uv.lock +++ b/examples/python/clients/httpx/uv.lock @@ -1,5 +1,5 @@ version = 1 -revision = 3 +revision = 2 requires-python = ">=3.10" [[package]] @@ -1744,7 +1744,7 @@ wheels = [ [[package]] name = "x402" -version = "2.1.0" +version = "2.2.0" source = { editable = "../../../../python/x402" } dependencies = [ { name = "nest-asyncio" }, diff --git a/examples/python/clients/payment-identifier/main.py b/examples/python/clients/payment-identifier/main.py index 8de03d540a..a7238d45fc 100644 --- a/examples/python/clients/payment-identifier/main.py +++ b/examples/python/clients/payment-identifier/main.py @@ -82,14 +82,14 @@ async def before_payment_creation(context: PaymentCreationContext) -> None: print(f"Response ({duration1}ms): {response1.text}") - if response1.is_success: - try: - settle_response = http_client.get_payment_settle_response( - lambda name: response1.headers.get(name) - ) - print(f"\nPayment settled on {settle_response.network}") - except ValueError: - pass + # Extract and print payment response if present + try: + settle_response = http_client.get_payment_settle_response( + lambda name: response1.headers.get(name) + ) + print(f"\nPayment response: {settle_response.model_dump_json(indent=2)}") + except ValueError: + pass # Second request - same payment ID, should return from cache print("\n" + "=" * 52) @@ -105,14 +105,13 @@ async def before_payment_creation(context: PaymentCreationContext) -> None: print(f"Response ({duration2}ms): {response2.text}") - if response2.is_success: - try: - settle_response = http_client.get_payment_settle_response( - lambda name: response2.headers.get(name) - ) - print("\nPayment settled (unexpected - should have been cached)") - except ValueError: - print("\nNo payment processed - response served from cache!") + try: + settle_response = http_client.get_payment_settle_response( + lambda name: response2.headers.get(name) + ) + print(f"\nPayment response: {settle_response.model_dump_json(indent=2)}") + except ValueError: + print("\nNo payment processed - response served from cache!") # Summary print("\n" + "=" * 52) diff --git a/examples/python/clients/requests/main.py b/examples/python/clients/requests/main.py index 9447f15d8a..f3e88758ce 100644 --- a/examples/python/clients/requests/main.py +++ b/examples/python/clients/requests/main.py @@ -83,18 +83,13 @@ def main() -> None: print(f"Response body: {response.text}") # Extract and print payment response if present - if response.ok: # requests uses .ok - try: - settle_response = http_client.get_payment_settle_response( - lambda name: response.headers.get(name) - ) - print( - f"\nPayment response: {settle_response.model_dump_json(indent=2)}" - ) - except ValueError: - print("\nNo payment response header found") - else: - print(f"\nRequest failed (status: {response.status_code})") + try: + settle_response = http_client.get_payment_settle_response( + lambda name: response.headers.get(name) + ) + print(f"\nPayment response: {settle_response.model_dump_json(indent=2)}") + except ValueError: + print("\nNo payment response header found") if __name__ == "__main__": diff --git a/examples/python/clients/requests/uv.lock b/examples/python/clients/requests/uv.lock index 674ea277a6..0a7f7621cc 100644 --- a/examples/python/clients/requests/uv.lock +++ b/examples/python/clients/requests/uv.lock @@ -1,5 +1,5 @@ version = 1 -revision = 3 +revision = 2 requires-python = ">=3.10" [[package]] @@ -1744,7 +1744,7 @@ wheels = [ [[package]] name = "x402" -version = "2.1.0" +version = "2.2.0" source = { editable = "../../../../python/x402" } dependencies = [ { name = "nest-asyncio" }, diff --git a/examples/python/facilitator/advanced/all_networks.py b/examples/python/facilitator/advanced/all_networks.py index e78078663b..2cc72f2656 100644 --- a/examples/python/facilitator/advanced/all_networks.py +++ b/examples/python/facilitator/advanced/all_networks.py @@ -145,11 +145,7 @@ async def verify(request: VerifyRequest): # Verify payment (await async method) response = await facilitator.verify(payload, requirements) - return { - "isValid": response.is_valid, - "payer": response.payer, - "invalidReason": response.invalid_reason, - } + return response.model_dump(by_alias=True, exclude_none=True) except Exception as e: print(f"Verify error: {e}") raise HTTPException(status_code=500, detail=str(e)) from e @@ -175,24 +171,21 @@ async def settle(request: SettleRequest): # Settle payment (await async method) response = await facilitator.settle(payload, requirements) - return { - "success": response.success, - "transaction": response.transaction, - "network": response.network, - "payer": response.payer, - "errorReason": response.error_reason, - } + return response.model_dump(by_alias=True, exclude_none=True) except Exception as e: print(f"Settle error: {e}") # Check if this was an abort from hook if "aborted" in str(e).lower(): - return { - "success": False, - "errorReason": str(e), - "network": request.paymentPayload.get("accepted", {}).get("network", "unknown"), - "transaction": "", - } + from x402.schemas import SettleResponse + + abort = SettleResponse( + success=False, + error_reason=str(e), + network=request.paymentPayload.get("accepted", {}).get("network", "unknown"), + transaction="", + ) + return abort.model_dump(by_alias=True, exclude_none=True) raise HTTPException(status_code=500, detail=str(e)) from e @@ -208,15 +201,7 @@ async def supported(): response = facilitator.get_supported() return { - "kinds": [ - { - "x402Version": k.x402_version, - "scheme": k.scheme, - "network": k.network, - "extra": k.extra, - } - for k in response.kinds - ], + "kinds": [k.model_dump(by_alias=True, exclude_none=True) for k in response.kinds], "extensions": response.extensions, "signers": response.signers, } diff --git a/examples/python/facilitator/advanced/bazaar.py b/examples/python/facilitator/advanced/bazaar.py index 3c18fadf30..807a82e7f5 100644 --- a/examples/python/facilitator/advanced/bazaar.py +++ b/examples/python/facilitator/advanced/bazaar.py @@ -205,11 +205,7 @@ async def verify(request: VerifyRequest): # - Extract and catalog discovery info (on_after_verify) response = await facilitator.verify(payload, requirements) - return { - "isValid": response.is_valid, - "payer": response.payer, - "invalidReason": response.invalid_reason, - } + return response.model_dump(by_alias=True, exclude_none=True) except Exception as e: print(f"Verify error: {e}") raise HTTPException(status_code=500, detail=str(e)) from e @@ -234,25 +230,21 @@ async def settle(request: SettleRequest): response = await facilitator.settle(payload, requirements) - return { - "success": response.success, - "transaction": response.transaction, - "network": response.network, - "payer": response.payer, - "errorReason": response.error_reason, - } + return response.model_dump(by_alias=True, exclude_none=True) except Exception as e: print(f"Settle error: {e}") # Check if this was an abort from hook if "aborted" in str(e).lower() or "Settlement aborted" in str(e): - return { - "success": False, - "errorReason": str(e).replace("Settlement aborted: ", ""), - "network": request.paymentPayload.get("accepted", {}).get("network", "unknown"), - "transaction": "", - "payer": None, - } + from x402.schemas import SettleResponse + + abort = SettleResponse( + success=False, + error_reason=str(e).replace("Settlement aborted: ", ""), + network=request.paymentPayload.get("accepted", {}).get("network", "unknown"), + transaction="", + ) + return abort.model_dump(by_alias=True, exclude_none=True) raise HTTPException(status_code=500, detail=str(e)) from e @@ -268,15 +260,7 @@ async def supported(): response = facilitator.get_supported() return { - "kinds": [ - { - "x402Version": k.x402_version, - "scheme": k.scheme, - "network": k.network, - "extra": k.extra, - } - for k in response.kinds - ], + "kinds": [k.model_dump(by_alias=True, exclude_none=True) for k in response.kinds], "extensions": response.extensions, "signers": response.signers, } diff --git a/examples/python/facilitator/basic/main.py b/examples/python/facilitator/basic/main.py index 66197be635..4f71e39a37 100644 --- a/examples/python/facilitator/basic/main.py +++ b/examples/python/facilitator/basic/main.py @@ -145,11 +145,7 @@ async def verify(request: VerifyRequest): # Verify payment (await async method) response = await facilitator.verify(payload, requirements) - return { - "isValid": response.is_valid, - "payer": response.payer, - "invalidReason": response.invalid_reason, - } + return response.model_dump(by_alias=True, exclude_none=True) except Exception as e: print(f"Verify error: {e}") raise HTTPException(status_code=500, detail=str(e)) from e @@ -175,24 +171,21 @@ async def settle(request: SettleRequest): # Settle payment (await async method) response = await facilitator.settle(payload, requirements) - return { - "success": response.success, - "transaction": response.transaction, - "network": response.network, - "payer": response.payer, - "errorReason": response.error_reason, - } + return response.model_dump(by_alias=True, exclude_none=True) except Exception as e: print(f"Settle error: {e}") # Check if this was an abort from hook if "aborted" in str(e).lower(): - return { - "success": False, - "errorReason": str(e), - "network": request.paymentPayload.get("accepted", {}).get("network", "unknown"), - "transaction": "", - } + from x402.schemas import SettleResponse + + abort = SettleResponse( + success=False, + error_reason=str(e), + network=request.paymentPayload.get("accepted", {}).get("network", "unknown"), + transaction="", + ) + return abort.model_dump(by_alias=True, exclude_none=True) raise HTTPException(status_code=500, detail=str(e)) from e @@ -208,15 +201,7 @@ async def supported(): response = facilitator.get_supported() return { - "kinds": [ - { - "x402Version": k.x402_version, - "scheme": k.scheme, - "network": k.network, - "extra": k.extra, - } - for k in response.kinds - ], + "kinds": [k.model_dump(by_alias=True, exclude_none=True) for k in response.kinds], "extensions": response.extensions, "signers": response.signers, } diff --git a/examples/python/facilitator/basic/uv.lock b/examples/python/facilitator/basic/uv.lock index c6a3f7d852..78300b2b72 100644 --- a/examples/python/facilitator/basic/uv.lock +++ b/examples/python/facilitator/basic/uv.lock @@ -1,5 +1,5 @@ version = 1 -revision = 3 +revision = 2 requires-python = ">=3.10" [[package]] @@ -2976,7 +2976,7 @@ wheels = [ [[package]] name = "x402" -version = "2.1.0" +version = "2.5.0" source = { editable = "../../../../python/x402" } dependencies = [ { name = "nest-asyncio" }, diff --git a/examples/python/servers/bazaar/.env-local b/examples/python/servers/bazaar/.env-local new file mode 100644 index 0000000000..aeea0693ab --- /dev/null +++ b/examples/python/servers/bazaar/.env-local @@ -0,0 +1,3 @@ +EVM_ADDRESS= +SVM_ADDRESS= +FACILITATOR_URL=https://x402.org/facilitator diff --git a/examples/python/servers/bazaar/README.md b/examples/python/servers/bazaar/README.md new file mode 100644 index 0000000000..2f0efac9f7 --- /dev/null +++ b/examples/python/servers/bazaar/README.md @@ -0,0 +1,139 @@ +# Bazaar Discovery Example Server (Python) + +FastAPI server demonstrating how to make a paid API **discoverable** using the Bazaar extension with dynamic route parameters. + +The key addition over a basic x402 server is `declare_discovery_extension` -- it describes your endpoint's inputs, outputs, and path parameters so that facilitators (and agents) can automatically catalog and invoke your API. + +## What This Example Shows + +**Dynamic route parameters** -- the route `GET /weather/:city` uses a `:city` slug. The x402 middleware automatically: + +1. Matches `/weather/san-francisco`, `/weather/tokyo`, etc. against the route pattern +2. Extracts `{ city: "san-francisco" }` as `pathParams` in the discovery extension +3. Produces `routeTemplate: "/weather/:city"` so all concrete URLs consolidate into **one** catalog entry + +```python +from x402.extensions.bazaar import declare_discovery_extension, OutputConfig + +routes = { + "GET /weather/:city": RouteConfig( + accepts=[ + PaymentOption(scheme="exact", price="$0.01", network=EVM_NETWORK, pay_to=EVM_ADDRESS), + ], + description="Weather data for a city", + extensions=declare_discovery_extension( + path_params_schema={ + "properties": {"city": {"type": "string", "description": "City name slug"}}, + "required": ["city"], + }, + output=OutputConfig( + example={"city": "san-francisco", "weather": "foggy", "temperature": 60} + ), + ), + ), +} + +@app.get("/weather/{city}") +async def get_weather(city: str) -> dict: + ... +``` + +Note that the x402 route key uses `:city` (Express convention) while the FastAPI handler uses `{city}` (FastAPI convention). The x402 middleware handles the `:city` matching; FastAPI handles the `{city}` extraction for your handler. + +## Prerequisites + +- Python 3.10+ +- uv (install via [docs.astral.sh/uv](https://docs.astral.sh/uv/getting-started/installation/)) +- Valid EVM address for receiving payments (Base Sepolia) +- Valid SVM address for receiving payments (Solana Devnet) +- URL of a facilitator supporting the desired payment network, see [facilitator list](https://www.x402.org/ecosystem?category=facilitators) + +## Setup + +1. Copy `.env-local` to `.env`: + +```bash +cp .env-local .env +``` + +2. Fill required environment variables: + +- `EVM_ADDRESS` - Ethereum address to receive payments (Base Sepolia) +- `SVM_ADDRESS` - Solana address to receive payments (Solana Devnet) +- `FACILITATOR_URL` - Facilitator endpoint URL (optional, defaults to production) + +3. Install dependencies: + +```bash +uv sync +``` + +4. Run the server: + +```bash +uv run python main.py +``` + +Server runs at http://localhost:4021 + +## How Discovery Works + +When a client hits `GET /weather/san-francisco` without a payment, the 402 response includes the enriched bazaar extension: + +```json +{ + "x402Version": 2, + "error": "Payment required", + "resource": { "url": "http://localhost:4021/weather/san-francisco" }, + "extensions": { + "bazaar": { + "routeTemplate": "/weather/:city", + "info": { + "input": { + "type": "http", + "method": "GET", + "pathParams": { "city": "san-francisco" } + }, + "output": { + "type": "json", + "example": { "city": "san-francisco", "weather": "foggy", "temperature": 60 } + } + } + } + }, + "accepts": [{ "..." : "..." }] +} +``` + +The facilitator uses `routeTemplate` as the canonical catalog key, so requests to `/weather/san-francisco`, `/weather/tokyo`, and `/weather/new-york` all map to a single discoverable endpoint: `/weather/:city`. + +## Example Endpoints + +| Endpoint | Payment | Price | +|----------|---------|-------| +| `GET /health` | No | - | +| `GET /weather/:city` | Yes | $0.01 USDC | +| `GET /weather/:country/:city` | Yes | $0.01 USDC | + +## Multiple Path Parameters + +Routes can have multiple `:param` segments. Param names are matched by **position in the URL**, not by the order they appear in `path_params_schema`: + +``` +GET /weather/:country/:city + ^ ^ + | └── second URL segment -> "city" + └──────────── first URL segment -> "country" +``` + +A request to `/weather/us/san-francisco` produces `pathParams: { country: "us", city: "san-francisco" }`. The property order in `path_params_schema` does not affect matching -- only the segment position in the URL matters. + +## `declare_discovery_extension` API + +| Parameter | Purpose | +|-----------|---------| +| `input` | Example query parameter values (for GET/HEAD/DELETE) | +| `input_schema` | JSON Schema for query parameters | +| `path_params_schema` | JSON Schema for URL path parameters (`:param` segments) | +| `output` | `OutputConfig(example=...)` -- example response body | +| `body_type` | For POST/PUT/PATCH: `"json"`, `"form-data"`, or `"text"` | diff --git a/examples/python/servers/bazaar/main.py b/examples/python/servers/bazaar/main.py new file mode 100644 index 0000000000..cde583132e --- /dev/null +++ b/examples/python/servers/bazaar/main.py @@ -0,0 +1,124 @@ +import os + +from dotenv import load_dotenv +from fastapi import FastAPI + +from x402.extensions.bazaar import OutputConfig, declare_discovery_extension +from x402.http import FacilitatorConfig, HTTPFacilitatorClient, PaymentOption +from x402.http.middleware.fastapi import PaymentMiddlewareASGI +from x402.http.types import RouteConfig +from x402.mechanisms.evm.exact import ExactEvmServerScheme +from x402.mechanisms.svm.exact import ExactSvmServerScheme +from x402.schemas import Network +from x402.server import x402ResourceServer + +load_dotenv() + +# Config +EVM_ADDRESS = os.getenv("EVM_ADDRESS") +SVM_ADDRESS = os.getenv("SVM_ADDRESS") +EVM_NETWORK: Network = "eip155:84532" # Base Sepolia +SVM_NETWORK: Network = "solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1" # Solana Devnet +FACILITATOR_URL = os.getenv("FACILITATOR_URL", "https://x402.org/facilitator") + +if not EVM_ADDRESS or not SVM_ADDRESS: + raise ValueError("Missing required environment variables") + + +# App +app = FastAPI() + + +# x402 Middleware +facilitator = HTTPFacilitatorClient(FacilitatorConfig(url=FACILITATOR_URL)) +server = x402ResourceServer(facilitator) +server.register(EVM_NETWORK, ExactEvmServerScheme()) +server.register(SVM_NETWORK, ExactSvmServerScheme()) + +payment_options = [ + PaymentOption(scheme="exact", pay_to=EVM_ADDRESS, price="$0.01", network=EVM_NETWORK), + PaymentOption(scheme="exact", pay_to=SVM_ADDRESS, price="$0.01", network=SVM_NETWORK), +] + +routes = { + # Single path param: /weather/:city + "GET /weather/:city": RouteConfig( + accepts=payment_options, + mime_type="application/json", + description="Weather data for a city", + extensions=declare_discovery_extension( + path_params_schema={ + "properties": {"city": {"type": "string", "description": "City name slug"}}, + "required": ["city"], + }, + output=OutputConfig( + example={"city": "san-francisco", "weather": "foggy", "temperature": 60} + ), + ), + ), + # Multiple path params: /weather/:country/:city + # Param names are matched by position in the URL, not by declaration order in the schema. + # /weather/us/san-francisco -> { country: "us", city: "san-francisco" } + "GET /weather/:country/:city": RouteConfig( + accepts=payment_options, + mime_type="application/json", + description="Weather data for a city in a specific country", + extensions=declare_discovery_extension( + path_params_schema={ + "properties": { + "country": {"type": "string", "description": "Country code"}, + "city": {"type": "string", "description": "City name slug"}, + }, + "required": ["country", "city"], + }, + output=OutputConfig( + example={ + "country": "us", + "city": "san-francisco", + "weather": "foggy", + "temperature": 60, + } + ), + ), + ), +} +app.add_middleware(PaymentMiddlewareASGI, routes=routes, server=server) + + +# Routes +@app.get("/health") +async def health_check() -> dict[str, str]: + return {"status": "ok"} + + +@app.get("/weather/{city}") +async def get_weather(city: str) -> dict: + weather_data = { + "san-francisco": {"weather": "foggy", "temperature": 60}, + "new-york": {"weather": "cloudy", "temperature": 55}, + "tokyo": {"weather": "rainy", "temperature": 65}, + } + data = weather_data.get(city, {"weather": "sunny", "temperature": 70}) + return {"city": city, "weather": data["weather"], "temperature": data["temperature"]} + + +@app.get("/weather/{country}/{city}") +async def get_weather_by_country(country: str, city: str) -> dict: + weather_data: dict[str, dict[str, dict]] = { + "us": { + "san-francisco": {"weather": "foggy", "temperature": 60}, + "new-york": {"weather": "cloudy", "temperature": 55}, + }, + "jp": { + "tokyo": {"weather": "rainy", "temperature": 65}, + "osaka": {"weather": "clear", "temperature": 72}, + }, + } + data = weather_data.get(country, {}).get(city, {"weather": "sunny", "temperature": 70}) + return {"country": country, "city": city, "weather": data["weather"], "temperature": data["temperature"]} + + +if __name__ == "__main__": + import uvicorn + + uvicorn.run(app, host="0.0.0.0", port=4021) diff --git a/examples/python/servers/bazaar/pyproject.toml b/examples/python/servers/bazaar/pyproject.toml new file mode 100644 index 0000000000..b76ba97a62 --- /dev/null +++ b/examples/python/servers/bazaar/pyproject.toml @@ -0,0 +1,60 @@ +[project] +name = "x402-v2-bazaar-example" +version = "0.1.0" +description = "Example of using Bazaar discovery with dynamic routes in x402" +readme = "README.md" +authors = [ + { name = "logan", email = "kcs93023@gmail.com" }, +] +requires-python = ">=3.10" +keywords = ["x402", "payment", "protocol", "http", "402"] +dependencies = [ + "x402[fastapi,evm,svm]", + "python-dotenv>=1.2.1", + "uvicorn[standard]>=0.40.0", +] + +[dependency-groups] +dev = [ + "pytest>=7.0.0", + "pytest-asyncio>=0.21.0", + "mypy>=1.0.0", + "ruff>=0.1.0", + "black>=23.0.0", +] + +[tool.uv] +package = false + +[tool.uv.sources] +x402 = { path = "../../../../python/x402", editable = true } + +[tool.ruff] +line-length = 100 +target-version = "py310" + +[tool.ruff.lint] +select = [ + "E", # pycodestyle errors + "W", # pycodestyle warnings + "F", # pyflakes + "I", # isort + "B", # flake8-bugbear + "C4", # flake8-comprehensions + "UP", # pyupgrade +] +ignore = [ + "E501", # line too long (handled by black) +] + +[tool.ruff.lint.isort] +known-first-party = ["x402"] + +[tool.mypy] +python_version = "3.10" +strict = true +warn_return_any = true +warn_unused_configs = true + +[tool.pytest.ini_options] +testpaths = ["tests"] diff --git a/examples/python/servers/bazaar/uv.lock b/examples/python/servers/bazaar/uv.lock new file mode 100644 index 0000000000..4ca7418ccd --- /dev/null +++ b/examples/python/servers/bazaar/uv.lock @@ -0,0 +1,3209 @@ +version = 1 +revision = 3 +requires-python = ">=3.10" + +[[package]] +name = "aiohappyeyeballs" +version = "2.6.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/26/30/f84a107a9c4331c14b2b586036f40965c128aa4fee4dda5d3d51cb14ad54/aiohappyeyeballs-2.6.1.tar.gz", hash = "sha256:c3f9d0113123803ccadfdf3f0faa505bc78e6a72d1cc4806cbd719826e943558", size = 22760, upload-time = "2025-03-12T01:42:48.764Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0f/15/5bf3b99495fb160b63f95972b81750f18f7f4e02ad051373b669d17d44f2/aiohappyeyeballs-2.6.1-py3-none-any.whl", hash = "sha256:f349ba8f4b75cb25c99c5c2d84e997e485204d2902a9597802b0371f09331fb8", size = 15265, upload-time = "2025-03-12T01:42:47.083Z" }, +] + +[[package]] +name = "aiohttp" +version = "3.13.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiohappyeyeballs" }, + { name = "aiosignal" }, + { name = "async-timeout", marker = "python_full_version < '3.11'" }, + { name = "attrs" }, + { name = "frozenlist" }, + { name = "multidict" }, + { name = "propcache" }, + { name = "yarl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1c/ce/3b83ebba6b3207a7135e5fcaba49706f8a4b6008153b4e30540c982fae26/aiohttp-3.13.2.tar.gz", hash = "sha256:40176a52c186aefef6eb3cad2cdd30cd06e3afbe88fe8ab2af9c0b90f228daca", size = 7837994, upload-time = "2025-10-28T20:59:39.937Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6d/34/939730e66b716b76046dedfe0842995842fa906ccc4964bba414ff69e429/aiohttp-3.13.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:2372b15a5f62ed37789a6b383ff7344fc5b9f243999b0cd9b629d8bc5f5b4155", size = 736471, upload-time = "2025-10-28T20:55:27.924Z" }, + { url = "https://files.pythonhosted.org/packages/fd/cf/dcbdf2df7f6ca72b0bb4c0b4509701f2d8942cf54e29ca197389c214c07f/aiohttp-3.13.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e7f8659a48995edee7229522984bd1009c1213929c769c2daa80b40fe49a180c", size = 493985, upload-time = "2025-10-28T20:55:29.456Z" }, + { url = "https://files.pythonhosted.org/packages/9d/87/71c8867e0a1d0882dcbc94af767784c3cb381c1c4db0943ab4aae4fed65e/aiohttp-3.13.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:939ced4a7add92296b0ad38892ce62b98c619288a081170695c6babe4f50e636", size = 489274, upload-time = "2025-10-28T20:55:31.134Z" }, + { url = "https://files.pythonhosted.org/packages/38/0f/46c24e8dae237295eaadd113edd56dee96ef6462adf19b88592d44891dc5/aiohttp-3.13.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6315fb6977f1d0dd41a107c527fee2ed5ab0550b7d885bc15fee20ccb17891da", size = 1668171, upload-time = "2025-10-28T20:55:36.065Z" }, + { url = "https://files.pythonhosted.org/packages/eb/c6/4cdfb4440d0e28483681a48f69841fa5e39366347d66ef808cbdadddb20e/aiohttp-3.13.2-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6e7352512f763f760baaed2637055c49134fd1d35b37c2dedfac35bfe5cf8725", size = 1636036, upload-time = "2025-10-28T20:55:37.576Z" }, + { url = "https://files.pythonhosted.org/packages/84/37/8708cf678628216fb678ab327a4e1711c576d6673998f4f43e86e9ae90dd/aiohttp-3.13.2-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e09a0a06348a2dd73e7213353c90d709502d9786219f69b731f6caa0efeb46f5", size = 1727975, upload-time = "2025-10-28T20:55:39.457Z" }, + { url = "https://files.pythonhosted.org/packages/e6/2e/3ebfe12fdcb9b5f66e8a0a42dffcd7636844c8a018f261efb2419f68220b/aiohttp-3.13.2-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a09a6d073fb5789456545bdee2474d14395792faa0527887f2f4ec1a486a59d3", size = 1815823, upload-time = "2025-10-28T20:55:40.958Z" }, + { url = "https://files.pythonhosted.org/packages/a1/4f/ca2ef819488cbb41844c6cf92ca6dd15b9441e6207c58e5ae0e0fc8d70ad/aiohttp-3.13.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b59d13c443f8e049d9e94099c7e412e34610f1f49be0f230ec656a10692a5802", size = 1669374, upload-time = "2025-10-28T20:55:42.745Z" }, + { url = "https://files.pythonhosted.org/packages/f8/fe/1fe2e1179a0d91ce09c99069684aab619bf2ccde9b20bd6ca44f8837203e/aiohttp-3.13.2-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:20db2d67985d71ca033443a1ba2001c4b5693fe09b0e29f6d9358a99d4d62a8a", size = 1555315, upload-time = "2025-10-28T20:55:44.264Z" }, + { url = "https://files.pythonhosted.org/packages/5a/2b/f3781899b81c45d7cbc7140cddb8a3481c195e7cbff8e36374759d2ab5a5/aiohttp-3.13.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:960c2fc686ba27b535f9fd2b52d87ecd7e4fd1cf877f6a5cba8afb5b4a8bd204", size = 1639140, upload-time = "2025-10-28T20:55:46.626Z" }, + { url = "https://files.pythonhosted.org/packages/72/27/c37e85cd3ece6f6c772e549bd5a253d0c122557b25855fb274224811e4f2/aiohttp-3.13.2-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:6c00dbcf5f0d88796151e264a8eab23de2997c9303dd7c0bf622e23b24d3ce22", size = 1645496, upload-time = "2025-10-28T20:55:48.933Z" }, + { url = "https://files.pythonhosted.org/packages/66/20/3af1ab663151bd3780b123e907761cdb86ec2c4e44b2d9b195ebc91fbe37/aiohttp-3.13.2-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:fed38a5edb7945f4d1bcabe2fcd05db4f6ec7e0e82560088b754f7e08d93772d", size = 1697625, upload-time = "2025-10-28T20:55:50.377Z" }, + { url = "https://files.pythonhosted.org/packages/95/eb/ae5cab15efa365e13d56b31b0d085a62600298bf398a7986f8388f73b598/aiohttp-3.13.2-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:b395bbca716c38bef3c764f187860e88c724b342c26275bc03e906142fc5964f", size = 1542025, upload-time = "2025-10-28T20:55:51.861Z" }, + { url = "https://files.pythonhosted.org/packages/e9/2d/1683e8d67ec72d911397fe4e575688d2a9b8f6a6e03c8fdc9f3fd3d4c03f/aiohttp-3.13.2-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:204ffff2426c25dfda401ba08da85f9c59525cdc42bda26660463dd1cbcfec6f", size = 1714918, upload-time = "2025-10-28T20:55:53.515Z" }, + { url = "https://files.pythonhosted.org/packages/99/a2/ffe8e0e1c57c5e542d47ffa1fcf95ef2b3ea573bf7c4d2ee877252431efc/aiohttp-3.13.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:05c4dd3c48fb5f15db31f57eb35374cb0c09afdde532e7fb70a75aede0ed30f6", size = 1656113, upload-time = "2025-10-28T20:55:55.438Z" }, + { url = "https://files.pythonhosted.org/packages/0d/42/d511aff5c3a2b06c09d7d214f508a4ad8ac7799817f7c3d23e7336b5e896/aiohttp-3.13.2-cp310-cp310-win32.whl", hash = "sha256:e574a7d61cf10351d734bcddabbe15ede0eaa8a02070d85446875dc11189a251", size = 432290, upload-time = "2025-10-28T20:55:56.96Z" }, + { url = "https://files.pythonhosted.org/packages/8b/ea/1c2eb7098b5bad4532994f2b7a8228d27674035c9b3234fe02c37469ef14/aiohttp-3.13.2-cp310-cp310-win_amd64.whl", hash = "sha256:364f55663085d658b8462a1c3f17b2b84a5c2e1ba858e1b79bff7b2e24ad1514", size = 455075, upload-time = "2025-10-28T20:55:58.373Z" }, + { url = "https://files.pythonhosted.org/packages/35/74/b321e7d7ca762638cdf8cdeceb39755d9c745aff7a64c8789be96ddf6e96/aiohttp-3.13.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:4647d02df098f6434bafd7f32ad14942f05a9caa06c7016fdcc816f343997dd0", size = 743409, upload-time = "2025-10-28T20:56:00.354Z" }, + { url = "https://files.pythonhosted.org/packages/99/3d/91524b905ec473beaf35158d17f82ef5a38033e5809fe8742e3657cdbb97/aiohttp-3.13.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e3403f24bcb9c3b29113611c3c16a2a447c3953ecf86b79775e7be06f7ae7ccb", size = 497006, upload-time = "2025-10-28T20:56:01.85Z" }, + { url = "https://files.pythonhosted.org/packages/eb/d3/7f68bc02a67716fe80f063e19adbd80a642e30682ce74071269e17d2dba1/aiohttp-3.13.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:43dff14e35aba17e3d6d5ba628858fb8cb51e30f44724a2d2f0c75be492c55e9", size = 493195, upload-time = "2025-10-28T20:56:03.314Z" }, + { url = "https://files.pythonhosted.org/packages/98/31/913f774a4708775433b7375c4f867d58ba58ead833af96c8af3621a0d243/aiohttp-3.13.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e2a9ea08e8c58bb17655630198833109227dea914cd20be660f52215f6de5613", size = 1747759, upload-time = "2025-10-28T20:56:04.904Z" }, + { url = "https://files.pythonhosted.org/packages/e8/63/04efe156f4326f31c7c4a97144f82132c3bb21859b7bb84748d452ccc17c/aiohttp-3.13.2-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:53b07472f235eb80e826ad038c9d106c2f653584753f3ddab907c83f49eedead", size = 1704456, upload-time = "2025-10-28T20:56:06.986Z" }, + { url = "https://files.pythonhosted.org/packages/8e/02/4e16154d8e0a9cf4ae76f692941fd52543bbb148f02f098ca73cab9b1c1b/aiohttp-3.13.2-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e736c93e9c274fce6419af4aac199984d866e55f8a4cec9114671d0ea9688780", size = 1807572, upload-time = "2025-10-28T20:56:08.558Z" }, + { url = "https://files.pythonhosted.org/packages/34/58/b0583defb38689e7f06798f0285b1ffb3a6fb371f38363ce5fd772112724/aiohttp-3.13.2-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ff5e771f5dcbc81c64898c597a434f7682f2259e0cd666932a913d53d1341d1a", size = 1895954, upload-time = "2025-10-28T20:56:10.545Z" }, + { url = "https://files.pythonhosted.org/packages/6b/f3/083907ee3437425b4e376aa58b2c915eb1a33703ec0dc30040f7ae3368c6/aiohttp-3.13.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a3b6fb0c207cc661fa0bf8c66d8d9b657331ccc814f4719468af61034b478592", size = 1747092, upload-time = "2025-10-28T20:56:12.118Z" }, + { url = "https://files.pythonhosted.org/packages/ac/61/98a47319b4e425cc134e05e5f3fc512bf9a04bf65aafd9fdcda5d57ec693/aiohttp-3.13.2-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:97a0895a8e840ab3520e2288db7cace3a1981300d48babeb50e7425609e2e0ab", size = 1606815, upload-time = "2025-10-28T20:56:14.191Z" }, + { url = "https://files.pythonhosted.org/packages/97/4b/e78b854d82f66bb974189135d31fce265dee0f5344f64dd0d345158a5973/aiohttp-3.13.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9e8f8afb552297aca127c90cb840e9a1d4bfd6a10d7d8f2d9176e1acc69bad30", size = 1723789, upload-time = "2025-10-28T20:56:16.101Z" }, + { url = "https://files.pythonhosted.org/packages/ed/fc/9d2ccc794fc9b9acd1379d625c3a8c64a45508b5091c546dea273a41929e/aiohttp-3.13.2-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:ed2f9c7216e53c3df02264f25d824b079cc5914f9e2deba94155190ef648ee40", size = 1718104, upload-time = "2025-10-28T20:56:17.655Z" }, + { url = "https://files.pythonhosted.org/packages/66/65/34564b8765ea5c7d79d23c9113135d1dd3609173da13084830f1507d56cf/aiohttp-3.13.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:99c5280a329d5fa18ef30fd10c793a190d996567667908bef8a7f81f8202b948", size = 1785584, upload-time = "2025-10-28T20:56:19.238Z" }, + { url = "https://files.pythonhosted.org/packages/30/be/f6a7a426e02fc82781afd62016417b3948e2207426d90a0e478790d1c8a4/aiohttp-3.13.2-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:2ca6ffef405fc9c09a746cb5d019c1672cd7f402542e379afc66b370833170cf", size = 1595126, upload-time = "2025-10-28T20:56:20.836Z" }, + { url = "https://files.pythonhosted.org/packages/e5/c7/8e22d5d28f94f67d2af496f14a83b3c155d915d1fe53d94b66d425ec5b42/aiohttp-3.13.2-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:47f438b1a28e926c37632bff3c44df7d27c9b57aaf4e34b1def3c07111fdb782", size = 1800665, upload-time = "2025-10-28T20:56:22.922Z" }, + { url = "https://files.pythonhosted.org/packages/d1/11/91133c8b68b1da9fc16555706aa7276fdf781ae2bb0876c838dd86b8116e/aiohttp-3.13.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9acda8604a57bb60544e4646a4615c1866ee6c04a8edef9b8ee6fd1d8fa2ddc8", size = 1739532, upload-time = "2025-10-28T20:56:25.924Z" }, + { url = "https://files.pythonhosted.org/packages/17/6b/3747644d26a998774b21a616016620293ddefa4d63af6286f389aedac844/aiohttp-3.13.2-cp311-cp311-win32.whl", hash = "sha256:868e195e39b24aaa930b063c08bb0c17924899c16c672a28a65afded9c46c6ec", size = 431876, upload-time = "2025-10-28T20:56:27.524Z" }, + { url = "https://files.pythonhosted.org/packages/c3/63/688462108c1a00eb9f05765331c107f95ae86f6b197b865d29e930b7e462/aiohttp-3.13.2-cp311-cp311-win_amd64.whl", hash = "sha256:7fd19df530c292542636c2a9a85854fab93474396a52f1695e799186bbd7f24c", size = 456205, upload-time = "2025-10-28T20:56:29.062Z" }, + { url = "https://files.pythonhosted.org/packages/29/9b/01f00e9856d0a73260e86dd8ed0c2234a466c5c1712ce1c281548df39777/aiohttp-3.13.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:b1e56bab2e12b2b9ed300218c351ee2a3d8c8fdab5b1ec6193e11a817767e47b", size = 737623, upload-time = "2025-10-28T20:56:30.797Z" }, + { url = "https://files.pythonhosted.org/packages/5a/1b/4be39c445e2b2bd0aab4ba736deb649fabf14f6757f405f0c9685019b9e9/aiohttp-3.13.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:364e25edaabd3d37b1db1f0cbcee8c73c9a3727bfa262b83e5e4cf3489a2a9dc", size = 492664, upload-time = "2025-10-28T20:56:32.708Z" }, + { url = "https://files.pythonhosted.org/packages/28/66/d35dcfea8050e131cdd731dff36434390479b4045a8d0b9d7111b0a968f1/aiohttp-3.13.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c5c94825f744694c4b8db20b71dba9a257cd2ba8e010a803042123f3a25d50d7", size = 491808, upload-time = "2025-10-28T20:56:34.57Z" }, + { url = "https://files.pythonhosted.org/packages/00/29/8e4609b93e10a853b65f8291e64985de66d4f5848c5637cddc70e98f01f8/aiohttp-3.13.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ba2715d842ffa787be87cbfce150d5e88c87a98e0b62e0f5aa489169a393dbbb", size = 1738863, upload-time = "2025-10-28T20:56:36.377Z" }, + { url = "https://files.pythonhosted.org/packages/9d/fa/4ebdf4adcc0def75ced1a0d2d227577cd7b1b85beb7edad85fcc87693c75/aiohttp-3.13.2-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:585542825c4bc662221fb257889e011a5aa00f1ae4d75d1d246a5225289183e3", size = 1700586, upload-time = "2025-10-28T20:56:38.034Z" }, + { url = "https://files.pythonhosted.org/packages/da/04/73f5f02ff348a3558763ff6abe99c223381b0bace05cd4530a0258e52597/aiohttp-3.13.2-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:39d02cb6025fe1aabca329c5632f48c9532a3dabccd859e7e2f110668972331f", size = 1768625, upload-time = "2025-10-28T20:56:39.75Z" }, + { url = "https://files.pythonhosted.org/packages/f8/49/a825b79ffec124317265ca7d2344a86bcffeb960743487cb11988ffb3494/aiohttp-3.13.2-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e67446b19e014d37342f7195f592a2a948141d15a312fe0e700c2fd2f03124f6", size = 1867281, upload-time = "2025-10-28T20:56:41.471Z" }, + { url = "https://files.pythonhosted.org/packages/b9/48/adf56e05f81eac31edcfae45c90928f4ad50ef2e3ea72cb8376162a368f8/aiohttp-3.13.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4356474ad6333e41ccefd39eae869ba15a6c5299c9c01dfdcfdd5c107be4363e", size = 1752431, upload-time = "2025-10-28T20:56:43.162Z" }, + { url = "https://files.pythonhosted.org/packages/30/ab/593855356eead019a74e862f21523db09c27f12fd24af72dbc3555b9bfd9/aiohttp-3.13.2-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:eeacf451c99b4525f700f078becff32c32ec327b10dcf31306a8a52d78166de7", size = 1562846, upload-time = "2025-10-28T20:56:44.85Z" }, + { url = "https://files.pythonhosted.org/packages/39/0f/9f3d32271aa8dc35036e9668e31870a9d3b9542dd6b3e2c8a30931cb27ae/aiohttp-3.13.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d8a9b889aeabd7a4e9af0b7f4ab5ad94d42e7ff679aaec6d0db21e3b639ad58d", size = 1699606, upload-time = "2025-10-28T20:56:46.519Z" }, + { url = "https://files.pythonhosted.org/packages/2c/3c/52d2658c5699b6ef7692a3f7128b2d2d4d9775f2a68093f74bca06cf01e1/aiohttp-3.13.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:fa89cb11bc71a63b69568d5b8a25c3ca25b6d54c15f907ca1c130d72f320b76b", size = 1720663, upload-time = "2025-10-28T20:56:48.528Z" }, + { url = "https://files.pythonhosted.org/packages/9b/d4/8f8f3ff1fb7fb9e3f04fcad4e89d8a1cd8fc7d05de67e3de5b15b33008ff/aiohttp-3.13.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:8aa7c807df234f693fed0ecd507192fc97692e61fee5702cdc11155d2e5cadc8", size = 1737939, upload-time = "2025-10-28T20:56:50.77Z" }, + { url = "https://files.pythonhosted.org/packages/03/d3/ddd348f8a27a634daae39a1b8e291ff19c77867af438af844bf8b7e3231b/aiohttp-3.13.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:9eb3e33fdbe43f88c3c75fa608c25e7c47bbd80f48d012763cb67c47f39a7e16", size = 1555132, upload-time = "2025-10-28T20:56:52.568Z" }, + { url = "https://files.pythonhosted.org/packages/39/b8/46790692dc46218406f94374903ba47552f2f9f90dad554eed61bfb7b64c/aiohttp-3.13.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:9434bc0d80076138ea986833156c5a48c9c7a8abb0c96039ddbb4afc93184169", size = 1764802, upload-time = "2025-10-28T20:56:54.292Z" }, + { url = "https://files.pythonhosted.org/packages/ba/e4/19ce547b58ab2a385e5f0b8aa3db38674785085abcf79b6e0edd1632b12f/aiohttp-3.13.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ff15c147b2ad66da1f2cbb0622313f2242d8e6e8f9b79b5206c84523a4473248", size = 1719512, upload-time = "2025-10-28T20:56:56.428Z" }, + { url = "https://files.pythonhosted.org/packages/70/30/6355a737fed29dcb6dfdd48682d5790cb5eab050f7b4e01f49b121d3acad/aiohttp-3.13.2-cp312-cp312-win32.whl", hash = "sha256:27e569eb9d9e95dbd55c0fc3ec3a9335defbf1d8bc1d20171a49f3c4c607b93e", size = 426690, upload-time = "2025-10-28T20:56:58.736Z" }, + { url = "https://files.pythonhosted.org/packages/0a/0d/b10ac09069973d112de6ef980c1f6bb31cb7dcd0bc363acbdad58f927873/aiohttp-3.13.2-cp312-cp312-win_amd64.whl", hash = "sha256:8709a0f05d59a71f33fd05c17fc11fcb8c30140506e13c2f5e8ee1b8964e1b45", size = 453465, upload-time = "2025-10-28T20:57:00.795Z" }, + { url = "https://files.pythonhosted.org/packages/bf/78/7e90ca79e5aa39f9694dcfd74f4720782d3c6828113bb1f3197f7e7c4a56/aiohttp-3.13.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:7519bdc7dfc1940d201651b52bf5e03f5503bda45ad6eacf64dda98be5b2b6be", size = 732139, upload-time = "2025-10-28T20:57:02.455Z" }, + { url = "https://files.pythonhosted.org/packages/db/ed/1f59215ab6853fbaa5c8495fa6cbc39edfc93553426152b75d82a5f32b76/aiohttp-3.13.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:088912a78b4d4f547a1f19c099d5a506df17eacec3c6f4375e2831ec1d995742", size = 490082, upload-time = "2025-10-28T20:57:04.784Z" }, + { url = "https://files.pythonhosted.org/packages/68/7b/fe0fe0f5e05e13629d893c760465173a15ad0039c0a5b0d0040995c8075e/aiohttp-3.13.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5276807b9de9092af38ed23ce120539ab0ac955547b38563a9ba4f5b07b95293", size = 489035, upload-time = "2025-10-28T20:57:06.894Z" }, + { url = "https://files.pythonhosted.org/packages/d2/04/db5279e38471b7ac801d7d36a57d1230feeee130bbe2a74f72731b23c2b1/aiohttp-3.13.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1237c1375eaef0db4dcd7c2559f42e8af7b87ea7d295b118c60c36a6e61cb811", size = 1720387, upload-time = "2025-10-28T20:57:08.685Z" }, + { url = "https://files.pythonhosted.org/packages/31/07/8ea4326bd7dae2bd59828f69d7fdc6e04523caa55e4a70f4a8725a7e4ed2/aiohttp-3.13.2-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:96581619c57419c3d7d78703d5b78c1e5e5fc0172d60f555bdebaced82ded19a", size = 1688314, upload-time = "2025-10-28T20:57:10.693Z" }, + { url = "https://files.pythonhosted.org/packages/48/ab/3d98007b5b87ffd519d065225438cc3b668b2f245572a8cb53da5dd2b1bc/aiohttp-3.13.2-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a2713a95b47374169409d18103366de1050fe0ea73db358fc7a7acb2880422d4", size = 1756317, upload-time = "2025-10-28T20:57:12.563Z" }, + { url = "https://files.pythonhosted.org/packages/97/3d/801ca172b3d857fafb7b50c7c03f91b72b867a13abca982ed6b3081774ef/aiohttp-3.13.2-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:228a1cd556b3caca590e9511a89444925da87d35219a49ab5da0c36d2d943a6a", size = 1858539, upload-time = "2025-10-28T20:57:14.623Z" }, + { url = "https://files.pythonhosted.org/packages/f7/0d/4764669bdf47bd472899b3d3db91fffbe925c8e3038ec591a2fd2ad6a14d/aiohttp-3.13.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ac6cde5fba8d7d8c6ac963dbb0256a9854e9fafff52fbcc58fdf819357892c3e", size = 1739597, upload-time = "2025-10-28T20:57:16.399Z" }, + { url = "https://files.pythonhosted.org/packages/c4/52/7bd3c6693da58ba16e657eb904a5b6decfc48ecd06e9ac098591653b1566/aiohttp-3.13.2-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f2bef8237544f4e42878c61cef4e2839fee6346dc60f5739f876a9c50be7fcdb", size = 1555006, upload-time = "2025-10-28T20:57:18.288Z" }, + { url = "https://files.pythonhosted.org/packages/48/30/9586667acec5993b6f41d2ebcf96e97a1255a85f62f3c653110a5de4d346/aiohttp-3.13.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:16f15a4eac3bc2d76c45f7ebdd48a65d41b242eb6c31c2245463b40b34584ded", size = 1683220, upload-time = "2025-10-28T20:57:20.241Z" }, + { url = "https://files.pythonhosted.org/packages/71/01/3afe4c96854cfd7b30d78333852e8e851dceaec1c40fd00fec90c6402dd2/aiohttp-3.13.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:bb7fb776645af5cc58ab804c58d7eba545a97e047254a52ce89c157b5af6cd0b", size = 1712570, upload-time = "2025-10-28T20:57:22.253Z" }, + { url = "https://files.pythonhosted.org/packages/11/2c/22799d8e720f4697a9e66fd9c02479e40a49de3de2f0bbe7f9f78a987808/aiohttp-3.13.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:e1b4951125ec10c70802f2cb09736c895861cd39fd9dcb35107b4dc8ae6220b8", size = 1733407, upload-time = "2025-10-28T20:57:24.37Z" }, + { url = "https://files.pythonhosted.org/packages/34/cb/90f15dd029f07cebbd91f8238a8b363978b530cd128488085b5703683594/aiohttp-3.13.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:550bf765101ae721ee1d37d8095f47b1f220650f85fe1af37a90ce75bab89d04", size = 1550093, upload-time = "2025-10-28T20:57:26.257Z" }, + { url = "https://files.pythonhosted.org/packages/69/46/12dce9be9d3303ecbf4d30ad45a7683dc63d90733c2d9fe512be6716cd40/aiohttp-3.13.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:fe91b87fc295973096251e2d25a811388e7d8adf3bd2b97ef6ae78bc4ac6c476", size = 1758084, upload-time = "2025-10-28T20:57:28.349Z" }, + { url = "https://files.pythonhosted.org/packages/f9/c8/0932b558da0c302ffd639fc6362a313b98fdf235dc417bc2493da8394df7/aiohttp-3.13.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e0c8e31cfcc4592cb200160344b2fb6ae0f9e4effe06c644b5a125d4ae5ebe23", size = 1716987, upload-time = "2025-10-28T20:57:30.233Z" }, + { url = "https://files.pythonhosted.org/packages/5d/8b/f5bd1a75003daed099baec373aed678f2e9b34f2ad40d85baa1368556396/aiohttp-3.13.2-cp313-cp313-win32.whl", hash = "sha256:0740f31a60848d6edb296a0df827473eede90c689b8f9f2a4cdde74889eb2254", size = 425859, upload-time = "2025-10-28T20:57:32.105Z" }, + { url = "https://files.pythonhosted.org/packages/5d/28/a8a9fc6957b2cee8902414e41816b5ab5536ecf43c3b1843c10e82c559b2/aiohttp-3.13.2-cp313-cp313-win_amd64.whl", hash = "sha256:a88d13e7ca367394908f8a276b89d04a3652044612b9a408a0bb22a5ed976a1a", size = 452192, upload-time = "2025-10-28T20:57:34.166Z" }, + { url = "https://files.pythonhosted.org/packages/9b/36/e2abae1bd815f01c957cbf7be817b3043304e1c87bad526292a0410fdcf9/aiohttp-3.13.2-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:2475391c29230e063ef53a66669b7b691c9bfc3f1426a0f7bcdf1216bdbac38b", size = 735234, upload-time = "2025-10-28T20:57:36.415Z" }, + { url = "https://files.pythonhosted.org/packages/ca/e3/1ee62dde9b335e4ed41db6bba02613295a0d5b41f74a783c142745a12763/aiohttp-3.13.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:f33c8748abef4d8717bb20e8fb1b3e07c6adacb7fd6beaae971a764cf5f30d61", size = 490733, upload-time = "2025-10-28T20:57:38.205Z" }, + { url = "https://files.pythonhosted.org/packages/1a/aa/7a451b1d6a04e8d15a362af3e9b897de71d86feac3babf8894545d08d537/aiohttp-3.13.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ae32f24bbfb7dbb485a24b30b1149e2f200be94777232aeadba3eecece4d0aa4", size = 491303, upload-time = "2025-10-28T20:57:40.122Z" }, + { url = "https://files.pythonhosted.org/packages/57/1e/209958dbb9b01174870f6a7538cd1f3f28274fdbc88a750c238e2c456295/aiohttp-3.13.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5d7f02042c1f009ffb70067326ef183a047425bb2ff3bc434ead4dd4a4a66a2b", size = 1717965, upload-time = "2025-10-28T20:57:42.28Z" }, + { url = "https://files.pythonhosted.org/packages/08/aa/6a01848d6432f241416bc4866cae8dc03f05a5a884d2311280f6a09c73d6/aiohttp-3.13.2-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:93655083005d71cd6c072cdab54c886e6570ad2c4592139c3fb967bfc19e4694", size = 1667221, upload-time = "2025-10-28T20:57:44.869Z" }, + { url = "https://files.pythonhosted.org/packages/87/4f/36c1992432d31bbc789fa0b93c768d2e9047ec8c7177e5cd84ea85155f36/aiohttp-3.13.2-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0db1e24b852f5f664cd728db140cf11ea0e82450471232a394b3d1a540b0f906", size = 1757178, upload-time = "2025-10-28T20:57:47.216Z" }, + { url = "https://files.pythonhosted.org/packages/ac/b4/8e940dfb03b7e0f68a82b88fd182b9be0a65cb3f35612fe38c038c3112cf/aiohttp-3.13.2-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b009194665bcd128e23eaddef362e745601afa4641930848af4c8559e88f18f9", size = 1838001, upload-time = "2025-10-28T20:57:49.337Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ef/39f3448795499c440ab66084a9db7d20ca7662e94305f175a80f5b7e0072/aiohttp-3.13.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c038a8fdc8103cd51dbd986ecdce141473ffd9775a7a8057a6ed9c3653478011", size = 1716325, upload-time = "2025-10-28T20:57:51.327Z" }, + { url = "https://files.pythonhosted.org/packages/d7/51/b311500ffc860b181c05d91c59a1313bdd05c82960fdd4035a15740d431e/aiohttp-3.13.2-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:66bac29b95a00db411cd758fea0e4b9bdba6d549dfe333f9a945430f5f2cc5a6", size = 1547978, upload-time = "2025-10-28T20:57:53.554Z" }, + { url = "https://files.pythonhosted.org/packages/31/64/b9d733296ef79815226dab8c586ff9e3df41c6aff2e16c06697b2d2e6775/aiohttp-3.13.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4ebf9cfc9ba24a74cf0718f04aac2a3bbe745902cc7c5ebc55c0f3b5777ef213", size = 1682042, upload-time = "2025-10-28T20:57:55.617Z" }, + { url = "https://files.pythonhosted.org/packages/3f/30/43d3e0f9d6473a6db7d472104c4eff4417b1e9df01774cb930338806d36b/aiohttp-3.13.2-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:a4b88ebe35ce54205c7074f7302bd08a4cb83256a3e0870c72d6f68a3aaf8e49", size = 1680085, upload-time = "2025-10-28T20:57:57.59Z" }, + { url = "https://files.pythonhosted.org/packages/16/51/c709f352c911b1864cfd1087577760ced64b3e5bee2aa88b8c0c8e2e4972/aiohttp-3.13.2-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:98c4fb90bb82b70a4ed79ca35f656f4281885be076f3f970ce315402b53099ae", size = 1728238, upload-time = "2025-10-28T20:57:59.525Z" }, + { url = "https://files.pythonhosted.org/packages/19/e2/19bd4c547092b773caeb48ff5ae4b1ae86756a0ee76c16727fcfd281404b/aiohttp-3.13.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:ec7534e63ae0f3759df3a1ed4fa6bc8f75082a924b590619c0dd2f76d7043caa", size = 1544395, upload-time = "2025-10-28T20:58:01.914Z" }, + { url = "https://files.pythonhosted.org/packages/cf/87/860f2803b27dfc5ed7be532832a3498e4919da61299b4a1f8eb89b8ff44d/aiohttp-3.13.2-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:5b927cf9b935a13e33644cbed6c8c4b2d0f25b713d838743f8fe7191b33829c4", size = 1742965, upload-time = "2025-10-28T20:58:03.972Z" }, + { url = "https://files.pythonhosted.org/packages/67/7f/db2fc7618925e8c7a601094d5cbe539f732df4fb570740be88ed9e40e99a/aiohttp-3.13.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:88d6c017966a78c5265d996c19cdb79235be5e6412268d7e2ce7dee339471b7a", size = 1697585, upload-time = "2025-10-28T20:58:06.189Z" }, + { url = "https://files.pythonhosted.org/packages/0c/07/9127916cb09bb38284db5036036042b7b2c514c8ebaeee79da550c43a6d6/aiohttp-3.13.2-cp314-cp314-win32.whl", hash = "sha256:f7c183e786e299b5d6c49fb43a769f8eb8e04a2726a2bd5887b98b5cc2d67940", size = 431621, upload-time = "2025-10-28T20:58:08.636Z" }, + { url = "https://files.pythonhosted.org/packages/fb/41/554a8a380df6d3a2bba8a7726429a23f4ac62aaf38de43bb6d6cde7b4d4d/aiohttp-3.13.2-cp314-cp314-win_amd64.whl", hash = "sha256:fe242cd381e0fb65758faf5ad96c2e460df6ee5b2de1072fe97e4127927e00b4", size = 457627, upload-time = "2025-10-28T20:58:11Z" }, + { url = "https://files.pythonhosted.org/packages/c7/8e/3824ef98c039d3951cb65b9205a96dd2b20f22241ee17d89c5701557c826/aiohttp-3.13.2-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:f10d9c0b0188fe85398c61147bbd2a657d616c876863bfeff43376e0e3134673", size = 767360, upload-time = "2025-10-28T20:58:13.358Z" }, + { url = "https://files.pythonhosted.org/packages/a4/0f/6a03e3fc7595421274fa34122c973bde2d89344f8a881b728fa8c774e4f1/aiohttp-3.13.2-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:e7c952aefdf2460f4ae55c5e9c3e80aa72f706a6317e06020f80e96253b1accd", size = 504616, upload-time = "2025-10-28T20:58:15.339Z" }, + { url = "https://files.pythonhosted.org/packages/c6/aa/ed341b670f1bc8a6f2c6a718353d13b9546e2cef3544f573c6a1ff0da711/aiohttp-3.13.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c20423ce14771d98353d2e25e83591fa75dfa90a3c1848f3d7c68243b4fbded3", size = 509131, upload-time = "2025-10-28T20:58:17.693Z" }, + { url = "https://files.pythonhosted.org/packages/7f/f0/c68dac234189dae5c4bbccc0f96ce0cc16b76632cfc3a08fff180045cfa4/aiohttp-3.13.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e96eb1a34396e9430c19d8338d2ec33015e4a87ef2b4449db94c22412e25ccdf", size = 1864168, upload-time = "2025-10-28T20:58:20.113Z" }, + { url = "https://files.pythonhosted.org/packages/8f/65/75a9a76db8364b5d0e52a0c20eabc5d52297385d9af9c35335b924fafdee/aiohttp-3.13.2-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:23fb0783bc1a33640036465019d3bba069942616a6a2353c6907d7fe1ccdaf4e", size = 1719200, upload-time = "2025-10-28T20:58:22.583Z" }, + { url = "https://files.pythonhosted.org/packages/f5/55/8df2ed78d7f41d232f6bd3ff866b6f617026551aa1d07e2f03458f964575/aiohttp-3.13.2-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2e1a9bea6244a1d05a4e57c295d69e159a5c50d8ef16aa390948ee873478d9a5", size = 1843497, upload-time = "2025-10-28T20:58:24.672Z" }, + { url = "https://files.pythonhosted.org/packages/e9/e0/94d7215e405c5a02ccb6a35c7a3a6cfff242f457a00196496935f700cde5/aiohttp-3.13.2-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0a3d54e822688b56e9f6b5816fb3de3a3a64660efac64e4c2dc435230ad23bad", size = 1935703, upload-time = "2025-10-28T20:58:26.758Z" }, + { url = "https://files.pythonhosted.org/packages/0b/78/1eeb63c3f9b2d1015a4c02788fb543141aad0a03ae3f7a7b669b2483f8d4/aiohttp-3.13.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7a653d872afe9f33497215745da7a943d1dc15b728a9c8da1c3ac423af35178e", size = 1792738, upload-time = "2025-10-28T20:58:29.787Z" }, + { url = "https://files.pythonhosted.org/packages/41/75/aaf1eea4c188e51538c04cc568040e3082db263a57086ea74a7d38c39e42/aiohttp-3.13.2-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:56d36e80d2003fa3fc0207fac644216d8532e9504a785ef9a8fd013f84a42c61", size = 1624061, upload-time = "2025-10-28T20:58:32.529Z" }, + { url = "https://files.pythonhosted.org/packages/9b/c2/3b6034de81fbcc43de8aeb209073a2286dfb50b86e927b4efd81cf848197/aiohttp-3.13.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:78cd586d8331fb8e241c2dd6b2f4061778cc69e150514b39a9e28dd050475661", size = 1789201, upload-time = "2025-10-28T20:58:34.618Z" }, + { url = "https://files.pythonhosted.org/packages/c9/38/c15dcf6d4d890217dae79d7213988f4e5fe6183d43893a9cf2fe9e84ca8d/aiohttp-3.13.2-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:20b10bbfbff766294fe99987f7bb3b74fdd2f1a2905f2562132641ad434dcf98", size = 1776868, upload-time = "2025-10-28T20:58:38.835Z" }, + { url = "https://files.pythonhosted.org/packages/04/75/f74fd178ac81adf4f283a74847807ade5150e48feda6aef024403716c30c/aiohttp-3.13.2-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:9ec49dff7e2b3c85cdeaa412e9d438f0ecd71676fde61ec57027dd392f00c693", size = 1790660, upload-time = "2025-10-28T20:58:41.507Z" }, + { url = "https://files.pythonhosted.org/packages/e7/80/7368bd0d06b16b3aba358c16b919e9c46cf11587dc572091031b0e9e3ef0/aiohttp-3.13.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:94f05348c4406450f9d73d38efb41d669ad6cd90c7ee194810d0eefbfa875a7a", size = 1617548, upload-time = "2025-10-28T20:58:43.674Z" }, + { url = "https://files.pythonhosted.org/packages/7d/4b/a6212790c50483cb3212e507378fbe26b5086d73941e1ec4b56a30439688/aiohttp-3.13.2-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:fa4dcb605c6f82a80c7f95713c2b11c3b8e9893b3ebd2bc9bde93165ed6107be", size = 1817240, upload-time = "2025-10-28T20:58:45.787Z" }, + { url = "https://files.pythonhosted.org/packages/ff/f7/ba5f0ba4ea8d8f3c32850912944532b933acbf0f3a75546b89269b9b7dde/aiohttp-3.13.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cf00e5db968c3f67eccd2778574cf64d8b27d95b237770aa32400bd7a1ca4f6c", size = 1762334, upload-time = "2025-10-28T20:58:47.936Z" }, + { url = "https://files.pythonhosted.org/packages/7e/83/1a5a1856574588b1cad63609ea9ad75b32a8353ac995d830bf5da9357364/aiohttp-3.13.2-cp314-cp314t-win32.whl", hash = "sha256:d23b5fe492b0805a50d3371e8a728a9134d8de5447dce4c885f5587294750734", size = 464685, upload-time = "2025-10-28T20:58:50.642Z" }, + { url = "https://files.pythonhosted.org/packages/9f/4d/d22668674122c08f4d56972297c51a624e64b3ed1efaa40187607a7cb66e/aiohttp-3.13.2-cp314-cp314t-win_amd64.whl", hash = "sha256:ff0a7b0a82a7ab905cbda74006318d1b12e37c797eb1b0d4eb3e316cf47f658f", size = 498093, upload-time = "2025-10-28T20:58:52.782Z" }, +] + +[[package]] +name = "aiosignal" +version = "1.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "frozenlist" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/61/62/06741b579156360248d1ec624842ad0edf697050bbaf7c3e46394e106ad1/aiosignal-1.4.0.tar.gz", hash = "sha256:f47eecd9468083c2029cc99945502cb7708b082c232f9aca65da147157b251c7", size = 25007, upload-time = "2025-07-03T22:54:43.528Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/76/641ae371508676492379f16e2fa48f4e2c11741bd63c48be4b12a6b09cba/aiosignal-1.4.0-py3-none-any.whl", hash = "sha256:053243f8b92b990551949e63930a839ff0cf0b0ebbe0597b0f3fb19e1a0fe82e", size = 7490, upload-time = "2025-07-03T22:54:42.156Z" }, +] + +[[package]] +name = "annotated-doc" +version = "0.0.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/57/ba/046ceea27344560984e26a590f90bc7f4a75b06701f653222458922b558c/annotated_doc-0.0.4.tar.gz", hash = "sha256:fbcda96e87e9c92ad167c2e53839e57503ecfda18804ea28102353485033faa4", size = 7288, upload-time = "2025-11-10T22:07:42.062Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl", hash = "sha256:571ac1dc6991c450b25a9c2d84a3705e2ae7a53467b5d111c24fa8baabbed320", size = 5303, upload-time = "2025-11-10T22:07:40.673Z" }, +] + +[[package]] +name = "annotated-types" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, +] + +[[package]] +name = "anyio" +version = "4.12.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "idna" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/16/ce/8a777047513153587e5434fd752e89334ac33e379aa3497db860eeb60377/anyio-4.12.0.tar.gz", hash = "sha256:73c693b567b0c55130c104d0b43a9baf3aa6a31fc6110116509f27bf75e21ec0", size = 228266, upload-time = "2025-11-28T23:37:38.911Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7f/9c/36c5c37947ebfb8c7f22e0eb6e4d188ee2d53aa3880f3f2744fb894f0cb1/anyio-4.12.0-py3-none-any.whl", hash = "sha256:dad2376a628f98eeca4881fc56cd06affd18f659b17a747d3ff0307ced94b1bb", size = 113362, upload-time = "2025-11-28T23:36:57.897Z" }, +] + +[[package]] +name = "async-timeout" +version = "5.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a5/ae/136395dfbfe00dfc94da3f3e136d0b13f394cba8f4841120e34226265780/async_timeout-5.0.1.tar.gz", hash = "sha256:d9321a7a3d5a6a5e187e824d2fa0793ce379a202935782d555d6e9d2735677d3", size = 9274, upload-time = "2024-11-06T16:41:39.6Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fe/ba/e2081de779ca30d473f21f5b30e0e737c438205440784c7dfc81efc2b029/async_timeout-5.0.1-py3-none-any.whl", hash = "sha256:39e3809566ff85354557ec2398b55e096c8364bacac9405a7a1fa429e77fe76c", size = 6233, upload-time = "2024-11-06T16:41:37.9Z" }, +] + +[[package]] +name = "attrs" +version = "25.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6b/5c/685e6633917e101e5dcb62b9dd76946cbb57c26e133bae9e0cd36033c0a9/attrs-25.4.0.tar.gz", hash = "sha256:16d5969b87f0859ef33a48b35d55ac1be6e42ae49d5e853b597db70c35c57e11", size = 934251, upload-time = "2025-10-06T13:54:44.725Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3a/2a/7cc015f5b9f5db42b7d48157e23356022889fc354a2813c15934b7cb5c0e/attrs-25.4.0-py3-none-any.whl", hash = "sha256:adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373", size = 67615, upload-time = "2025-10-06T13:54:43.17Z" }, +] + +[[package]] +name = "backports-asyncio-runner" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8e/ff/70dca7d7cb1cbc0edb2c6cc0c38b65cba36cccc491eca64cabd5fe7f8670/backports_asyncio_runner-1.2.0.tar.gz", hash = "sha256:a5aa7b2b7d8f8bfcaa2b57313f70792df84e32a2a746f585213373f900b42162", size = 69893, upload-time = "2025-07-02T02:27:15.685Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/59/76ab57e3fe74484f48a53f8e337171b4a2349e506eabe136d7e01d059086/backports_asyncio_runner-1.2.0-py3-none-any.whl", hash = "sha256:0da0a936a8aeb554eccb426dc55af3ba63bcdc69fa1a600b5bb305413a4477b5", size = 12313, upload-time = "2025-07-02T02:27:14.263Z" }, +] + +[[package]] +name = "bitarray" +version = "3.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/95/06/92fdc84448d324ab8434b78e65caf4fb4c6c90b4f8ad9bdd4c8021bfaf1e/bitarray-3.8.0.tar.gz", hash = "sha256:3eae38daffd77c9621ae80c16932eea3fb3a4af141fb7cc724d4ad93eff9210d", size = 151991, upload-time = "2025-11-02T21:41:15.117Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e8/b9/8a645fd36fc4c01ee223f97eccd4699c2f2e91681ccb33c0e963881c8e58/bitarray-3.8.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f08342dc8d19214faa7ef99574dea6c37a2790d6d04a9793ef8fa76c188dc08d", size = 148504, upload-time = "2025-11-02T21:38:54.596Z" }, + { url = "https://files.pythonhosted.org/packages/c0/f4/11b562e13ff732bd0674376f367f0a272034ebc28b8efbafbeb924552d21/bitarray-3.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:792462abfeeca6cc8c6c1e6d27e14319682f0182f6b0ba37befe911af794db70", size = 145481, upload-time = "2025-11-02T21:38:56.253Z" }, + { url = "https://files.pythonhosted.org/packages/d3/7c/5a2487da579491b38abab3b437e01d3b05be6e16e69cc5eb304040dcebd5/bitarray-3.8.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0df69d26f21a9d2f1b20266f6737fa43f08aa5015c99900fb69f255fbe4dabb4", size = 322760, upload-time = "2025-11-02T21:38:57.189Z" }, + { url = "https://files.pythonhosted.org/packages/8d/59/f0ef82d6a878d4af1b4961d208a716317929aa172fc0dfa5f4115319a873/bitarray-3.8.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b4f10d3f304be7183fac79bf2cd997f82e16aa9a9f37343d76c026c6e435a8a8", size = 350332, upload-time = "2025-11-02T21:38:58.238Z" }, + { url = "https://files.pythonhosted.org/packages/e5/ec/d444b22fce853327d4a8adec1de9987e11b28fcc2d7204dcbc544e196ed9/bitarray-3.8.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fc98ff43abad61f00515ad9a06213b7716699146e46eabd256cdfe7cb522bd97", size = 360787, upload-time = "2025-11-02T21:38:59.239Z" }, + { url = "https://files.pythonhosted.org/packages/9f/9e/60b205f52ea9ff155e9f12249090475159c909039daa29e47cd95e115dd5/bitarray-3.8.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:81c6b4a6c1af800d52a6fa32389ef8f4281583f4f99dc1a40f2bb47667281541", size = 329050, upload-time = "2025-11-02T21:39:00.455Z" }, + { url = "https://files.pythonhosted.org/packages/e3/da/2ce373b423bc85a0eb93ee1cba3977971259a92a116932632f417b1b04d2/bitarray-3.8.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f3fd8df63c41ff6a676d031956aebf68ebbc687b47c507da25501eb22eec341f", size = 320507, upload-time = "2025-11-02T21:39:01.714Z" }, + { url = "https://files.pythonhosted.org/packages/2a/88/437408a2674b8bdb02063dd1535969b9c73cb8fdd197485de431e506c50e/bitarray-3.8.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:f0ce9d9e07c75da8027c62b4c9f45771d1d8aae7dc9ad7fb606c6a5aedbe9741", size = 348449, upload-time = "2025-11-02T21:39:03.124Z" }, + { url = "https://files.pythonhosted.org/packages/97/46/d799e7e731c778b6dcb4627bafd395102065e5ab15a4a31f4222a3e20706/bitarray-3.8.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:8a9c962c64a4c08def58b9799333e33af94ec53038cf151d36edacdb41f81646", size = 344776, upload-time = "2025-11-02T21:39:04.147Z" }, + { url = "https://files.pythonhosted.org/packages/b3/9a/129fff56d22d316b1c848c6e13e64191485756b5cd6ceb08e640edb80020/bitarray-3.8.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:1a54d7e7999735faacdcbe8128e30207abc2caf9f9fd7102d180b32f1b78bfce", size = 325899, upload-time = "2025-11-02T21:39:05.118Z" }, + { url = "https://files.pythonhosted.org/packages/f8/ba/4b01e99452ecc39f4abccf9bf83fe0f01c390e9794dad2d04b2c8b893c5f/bitarray-3.8.0-cp310-cp310-win32.whl", hash = "sha256:3ea52df96566457735314794422274bd1962066bfb609e7eea9113d70cf04ffe", size = 142756, upload-time = "2025-11-02T21:39:06.402Z" }, + { url = "https://files.pythonhosted.org/packages/18/3f/c83635a67d90f45f88012468566c233eed1e9e9a9184fa882ba4039fadb3/bitarray-3.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:82a07de83dce09b4fa1bccbdc8bde8f188b131666af0dc9048ba0a0e448d8a3b", size = 149527, upload-time = "2025-11-02T21:39:07.377Z" }, + { url = "https://files.pythonhosted.org/packages/33/46/391b3902a523d4555313640746460b19d317c6233d9379e150af97fa1554/bitarray-3.8.0-cp310-cp310-win_arm64.whl", hash = "sha256:c5ba07e58fd98c9782201e79eb8dd4225733d212a5a3700f9a84d329bd0463a6", size = 146453, upload-time = "2025-11-02T21:39:08.624Z" }, + { url = "https://files.pythonhosted.org/packages/bc/7d/63558f1d0eb09217a3d30c1c847890879973e224a728fcff9391fab999b8/bitarray-3.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:25b9cff6c9856bc396232e2f609ea0c5ec1a8a24c500cee4cca96ba8a3cd50b6", size = 148502, upload-time = "2025-11-02T21:39:09.993Z" }, + { url = "https://files.pythonhosted.org/packages/5e/7b/f957ad211cb0172965b5f0881b67b99e2b6d41512af0a1001f44a44ddf4a/bitarray-3.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4d9984017314da772f5f7460add7a0301a4ffc06c72c2998bb16c300a6253607", size = 145484, upload-time = "2025-11-02T21:39:10.904Z" }, + { url = "https://files.pythonhosted.org/packages/9f/dc/897973734f14f91467a3a795a4624752238053ecffaec7c8bbda1e363fda/bitarray-3.8.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bbbbfbb7d039b20d289ce56b1beb46138d65769d04af50c199c6ac4cb6054d52", size = 330909, upload-time = "2025-11-02T21:39:12.276Z" }, + { url = "https://files.pythonhosted.org/packages/67/be/24b4b792426d92de289e73e09682915d567c2e69d47e8857586cbdc865d0/bitarray-3.8.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f1f723e260c35e1c7c57a09d3a6ebe681bd56c83e1208ae3ce1869b7c0d10d4f", size = 358469, upload-time = "2025-11-02T21:39:13.766Z" }, + { url = "https://files.pythonhosted.org/packages/3e/0e/2eda69a7a59a6998df8fb57cc9d1e0e62888c599fb5237b0a8b479a01afb/bitarray-3.8.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:cbd1660fb48827381ce3a621a4fdc237959e1cd4e98b098952a8f624a0726425", size = 369131, upload-time = "2025-11-02T21:39:15.041Z" }, + { url = "https://files.pythonhosted.org/packages/f7/7b/8a372d6635a6b2622477b2f96a569b2cd0318a62bc95a4a2144c7942c987/bitarray-3.8.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:df6d7bf3e15b7e6e202a16ff4948a51759354016026deb04ab9b5acbbe35e096", size = 337089, upload-time = "2025-11-02T21:39:16.124Z" }, + { url = "https://files.pythonhosted.org/packages/93/f0/8eca934dbe5dee47a0e5ef44eeb72e85acacc8097c27cd164337bc4ec5d3/bitarray-3.8.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d5c931ec1c03111718cabf85f6012bb2815fa0ce578175567fa8d6f2cc15d3b4", size = 328504, upload-time = "2025-11-02T21:39:17.321Z" }, + { url = "https://files.pythonhosted.org/packages/88/dd/928b8e23a9950f8a8bfc42bc1e7de41f4e27f57de01a716308be5f683c2b/bitarray-3.8.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:41b53711f89008ba2de62e4c2d2260a8b357072fd4f18e1351b28955db2719dc", size = 356461, upload-time = "2025-11-02T21:39:18.396Z" }, + { url = "https://files.pythonhosted.org/packages/a9/93/4fb58417aff47fa2fe1874a39c9346b589a1d78c93a9cb24cccede5dc737/bitarray-3.8.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:4f298daaaea58d45e245a132d6d2bdfb6f856da50dc03d75ebb761439fb626cf", size = 353008, upload-time = "2025-11-02T21:39:19.828Z" }, + { url = "https://files.pythonhosted.org/packages/da/54/aa04e4a7b45aa5913f08ee377d43319b0979925e3c0407882eb29df3be66/bitarray-3.8.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:30989a2451b693c3f9359d91098a744992b5431a0be4858f1fdf0ec76b457125", size = 334048, upload-time = "2025-11-02T21:39:20.924Z" }, + { url = "https://files.pythonhosted.org/packages/da/52/e851f41076df014c05d6ac1ce34fbf7db5fa31241da3e2f09bb2be9e283d/bitarray-3.8.0-cp311-cp311-win32.whl", hash = "sha256:e5aed4754895942ae15ffa48c52d181e1c1463236fda68d2dba29c03aa61786b", size = 142907, upload-time = "2025-11-02T21:39:22.312Z" }, + { url = "https://files.pythonhosted.org/packages/28/01/db0006148b1dd13b4ac2686df8fa57d12f5887df313a506e939af0cb0997/bitarray-3.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:22c540ed20167d3dbb1e2d868ca935180247d620c40eace90efa774504a40e3b", size = 149670, upload-time = "2025-11-02T21:39:23.341Z" }, + { url = "https://files.pythonhosted.org/packages/7b/ea/b7d55ee269b1426f758a535c9ec2a07c056f20f403fa981685c3c8b4798c/bitarray-3.8.0-cp311-cp311-win_arm64.whl", hash = "sha256:84b52b2cf77bb7f703d16c4007b021078dbbe6cf8ffb57abe81a7bacfc175ef2", size = 146709, upload-time = "2025-11-02T21:39:24.343Z" }, + { url = "https://files.pythonhosted.org/packages/82/a0/0c41d893eda756315491adfdbf9bc928aee3d377a7f97a8834d453aa5de1/bitarray-3.8.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f2fcbe9b3a5996b417e030aa33a562e7e20dfc86271e53d7e841fc5df16268b8", size = 148575, upload-time = "2025-11-02T21:39:25.718Z" }, + { url = "https://files.pythonhosted.org/packages/0e/30/12ab2f4a4429bd844b419c37877caba93d676d18be71354fbbeb21d9f4cc/bitarray-3.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:cd761d158f67e288fd0ebe00c3b158095ce80a4bc7c32b60c7121224003ba70d", size = 145454, upload-time = "2025-11-02T21:39:26.695Z" }, + { url = "https://files.pythonhosted.org/packages/26/58/314b3e3f219533464e120f0c51ac5123e7b1c1b91f725a4073fb70c5a858/bitarray-3.8.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c394a3f055b49f92626f83c1a0b6d6cd2c628f1ccd72481c3e3c6aa4695f3b20", size = 332949, upload-time = "2025-11-02T21:39:27.801Z" }, + { url = "https://files.pythonhosted.org/packages/ea/ce/ca8c706bd8341c7a22dd92d2a528af71f7e5f4726085d93f81fd768cb03b/bitarray-3.8.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:969fd67de8c42affdb47b38b80f1eaa79ac0ef17d65407cdd931db1675315af1", size = 360599, upload-time = "2025-11-02T21:39:28.964Z" }, + { url = "https://files.pythonhosted.org/packages/ef/dc/aa181df85f933052d962804906b282acb433cb9318b08ec2aceb4ee34faf/bitarray-3.8.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:99d25aff3745c54e61ab340b98400c52ebec04290a62078155e0d7eb30380220", size = 371972, upload-time = "2025-11-02T21:39:30.228Z" }, + { url = "https://files.pythonhosted.org/packages/ff/d9/b805bfa158c7bcf4df0ac19b1be581b47e1ddb792c11023aed80a7058e78/bitarray-3.8.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e645b4c365d6f1f9e0799380ad6395268f3c3b898244a650aaeb8d9d27b74c35", size = 340303, upload-time = "2025-11-02T21:39:31.342Z" }, + { url = "https://files.pythonhosted.org/packages/1f/42/5308cc97ea929e30727292617a3a88293470166851e13c9e3f16f395da55/bitarray-3.8.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2fa23fdb3beab313950bbb49674e8a161e61449332d3997089fe3944953f1b77", size = 330494, upload-time = "2025-11-02T21:39:32.769Z" }, + { url = "https://files.pythonhosted.org/packages/4c/89/64f1596cb80433323efdbc8dcd0d6e57c40dfbe6ea3341623f34ec397edd/bitarray-3.8.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:165052a0e61c880f7093808a0c524ce1b3555bfa114c0dfb5c809cd07918a60d", size = 358123, upload-time = "2025-11-02T21:39:34.331Z" }, + { url = "https://files.pythonhosted.org/packages/27/fd/f3d49c5443b57087f888b5e118c8dd78bb7c8e8cfeeed250f8e92128a05f/bitarray-3.8.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:337c8cd46a4c6568d367ed676cbf2d7de16f890bb31dbb54c44c1d6bb6d4a1de", size = 356046, upload-time = "2025-11-02T21:39:35.449Z" }, + { url = "https://files.pythonhosted.org/packages/aa/db/1fd0b402bd2b47142e958b6930dbb9445235d03fa703c9a24caa6e576ae2/bitarray-3.8.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:21ca6a47bf20db9e7ad74ca04b3d479e4d76109b68333eb23535553d2705339e", size = 336872, upload-time = "2025-11-02T21:39:36.891Z" }, + { url = "https://files.pythonhosted.org/packages/58/73/680b47718f1313b4538af479c4732eaca0aeda34d93fc5b869f87932d57d/bitarray-3.8.0-cp312-cp312-win32.whl", hash = "sha256:178c5a4c7fdfb5cd79e372ae7f675390e670f3732e5bc68d327e01a5b3ff8d55", size = 143025, upload-time = "2025-11-02T21:39:38.303Z" }, + { url = "https://files.pythonhosted.org/packages/f8/11/7792587c19c79a8283e8838f44709fa4338a8f7d2a3091dfd81c07ae89c7/bitarray-3.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:75a3b6e9c695a6570ea488db75b84bb592ff70a944957efa1c655867c575018b", size = 149969, upload-time = "2025-11-02T21:39:39.715Z" }, + { url = "https://files.pythonhosted.org/packages/9a/00/9df64b5d8a84e8e9ec392f6f9ce93f50626a5b301cb6c6b3fe3406454d66/bitarray-3.8.0-cp312-cp312-win_arm64.whl", hash = "sha256:5591daf81313096909d973fb2612fccd87528fdfdd39f6478bdce54543178954", size = 146907, upload-time = "2025-11-02T21:39:40.815Z" }, + { url = "https://files.pythonhosted.org/packages/3e/35/480364d4baf1e34c79076750914664373f561c58abb5c31c35b3fae613ff/bitarray-3.8.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:18214bac86341f1cc413772e66447d6cca10981e2880b70ecaf4e826c04f95e9", size = 148582, upload-time = "2025-11-02T21:39:42.268Z" }, + { url = "https://files.pythonhosted.org/packages/5e/a8/718b95524c803937f4edbaaf6480f39c80f6ed189d61357b345e8361ffb6/bitarray-3.8.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:01c5f0dc080b0ebb432f7a68ee1e88a76bd34f6d89c9568fcec65fb16ed71f0e", size = 145433, upload-time = "2025-11-02T21:39:43.552Z" }, + { url = "https://files.pythonhosted.org/packages/03/66/4a10f30dc9e2e01e3b4ecd44a511219f98e63c86b0e0f704c90fac24059b/bitarray-3.8.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:86685fa04067f7175f9718489ae755f6acde03593a1a9ca89305554af40e14fd", size = 332986, upload-time = "2025-11-02T21:39:44.656Z" }, + { url = "https://files.pythonhosted.org/packages/53/25/4c08774d847f80a1166e4c704b4e0f1c417c0afe6306eae0bc5e70d35faa/bitarray-3.8.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:56896ceeffe25946c4010320629e2d858ca763cd8ded273c81672a5edbcb1e0a", size = 360634, upload-time = "2025-11-02T21:39:45.798Z" }, + { url = "https://files.pythonhosted.org/packages/a5/8f/bf8ad26169ebd0b2746d5c7564db734453ca467f8aab87e9d43b0a794383/bitarray-3.8.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:9858dcbc23ba7eaadcd319786b982278a1a2b2020720b19db43e309579ff76fb", size = 371992, upload-time = "2025-11-02T21:39:46.968Z" }, + { url = "https://files.pythonhosted.org/packages/a9/16/ce166754e7c9d10650e02914552fa637cf3b2591f7ed16632bbf6b783312/bitarray-3.8.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:aa7dec53c25f1949513457ef8b0ea1fb40e76c672cc4d2daa8ad3c8d6b73491a", size = 340315, upload-time = "2025-11-02T21:39:48.182Z" }, + { url = "https://files.pythonhosted.org/packages/de/2a/fbba3a106ddd260e84b9a624f730257c32ba51a8a029565248dfedfdf6f2/bitarray-3.8.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:15a2eff91f54d2b1f573cca8ca6fb58763ce8fea80e7899ab028f3987ef71cd5", size = 330473, upload-time = "2025-11-02T21:39:49.705Z" }, + { url = "https://files.pythonhosted.org/packages/68/97/56cf3c70196e7307ad32318a9d6ed969dbdc6a4534bbe429112fa7dfe42e/bitarray-3.8.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:b1572ee0eb1967e71787af636bb7d1eb9c6735d5337762c450650e7f51844594", size = 358129, upload-time = "2025-11-02T21:39:51.189Z" }, + { url = "https://files.pythonhosted.org/packages/fd/be/afd391a5c0896d3339613321b2f94af853f29afc8bd3fbc327431244c642/bitarray-3.8.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:5bfac7f236ba1a4d402644bdce47fb9db02a7cf3214a1f637d3a88390f9e5428", size = 356005, upload-time = "2025-11-02T21:39:52.355Z" }, + { url = "https://files.pythonhosted.org/packages/ae/08/a8e1a371babba29bad3378bb3a2cdca2b012170711e7fe1f22031a6b7b95/bitarray-3.8.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f0a55cf02d2cdd739b40ce10c09bbdd520e141217696add7a48b56e67bdfdfe6", size = 336862, upload-time = "2025-11-02T21:39:54.345Z" }, + { url = "https://files.pythonhosted.org/packages/ee/8a/6dc1d0fdc06991c8dc3b1fcfe1ae49fbaced42064cd1b5f24278e73fe05f/bitarray-3.8.0-cp313-cp313-win32.whl", hash = "sha256:a2ba92f59e30ce915e9e79af37649432e3a212ddddf416d4d686b1b4825bcdb2", size = 143018, upload-time = "2025-11-02T21:39:56.361Z" }, + { url = "https://files.pythonhosted.org/packages/2e/72/76e13f5cd23b8b9071747909663ce3b02da24a5e7e22c35146338625db35/bitarray-3.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:1c8f2a5d8006db5a555e06f9437e76bf52537d3dfd130cb8ae2b30866aca32c9", size = 149977, upload-time = "2025-11-02T21:39:57.718Z" }, + { url = "https://files.pythonhosted.org/packages/01/37/60f336c32336cc3ec03b0c61076f16ea2f05d5371c8a56e802161d218b77/bitarray-3.8.0-cp313-cp313-win_arm64.whl", hash = "sha256:50ddbe3a7b4b6ab96812f5a4d570f401a2cdb95642fd04c062f98939610bbeee", size = 146930, upload-time = "2025-11-02T21:39:59.308Z" }, + { url = "https://files.pythonhosted.org/packages/1b/b0/411327a6c7f6b2bead64bb06fe60b92e0344957ec1ab0645d5ccc25fdafe/bitarray-3.8.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8cbd4bfc933b33b85c43ef4c1f4d5e3e9d91975ea6368acf5fbac02bac06ea89", size = 148563, upload-time = "2025-11-02T21:40:01.006Z" }, + { url = "https://files.pythonhosted.org/packages/2a/bc/ff80d97c627d774f879da0ea93223adb1267feab7e07d5c17580ffe6d632/bitarray-3.8.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9d35d8f8a1c9ed4e2b08187b513f8a3c71958600129db3aa26d85ea3abfd1310", size = 145422, upload-time = "2025-11-02T21:40:02.535Z" }, + { url = "https://files.pythonhosted.org/packages/66/e7/b4cb6c5689aacd0a32f3aa8a507155eaa33528c63de2f182b60843fbf700/bitarray-3.8.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:99f55e14e7c56f4fafe1343480c32b110ef03836c21ff7c48bae7add6818f77c", size = 332852, upload-time = "2025-11-02T21:40:03.645Z" }, + { url = "https://files.pythonhosted.org/packages/e7/91/fbd1b047e3e2f4b65590f289c8151df1d203d75b005f5aae4e072fe77d76/bitarray-3.8.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:dfbe2aa45b273f49e715c5345d94874cb65a28482bf231af408891c260601b8d", size = 360801, upload-time = "2025-11-02T21:40:04.827Z" }, + { url = "https://files.pythonhosted.org/packages/ef/4a/63064c593627bac8754fdafcb5343999c93ab2aeb27bcd9d270a010abea5/bitarray-3.8.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:64af877116edf051375b45f0bda648143176a017b13803ec7b3a3111dc05f4c5", size = 371408, upload-time = "2025-11-02T21:40:05.985Z" }, + { url = "https://files.pythonhosted.org/packages/46/97/ddc07723767bdafd170f2ff6e173c940fa874192783ee464aa3c1dedf07d/bitarray-3.8.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cdfbb27f2c46bb5bbdcee147530cbc5ca8ab858d7693924e88e30ada21b2c5e2", size = 340033, upload-time = "2025-11-02T21:40:07.189Z" }, + { url = "https://files.pythonhosted.org/packages/ad/1e/e1ea9f1146fd4af032817069ff118918d73e5de519854ce3860e2ed560ff/bitarray-3.8.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4d73d4948dcc5591d880db8933004e01f1dd2296df9de815354d53469beb26fe", size = 330774, upload-time = "2025-11-02T21:40:08.496Z" }, + { url = "https://files.pythonhosted.org/packages/cf/9f/8242296c124a48d1eab471fd0838aeb7ea9c6fd720302d99ab7855d3e6d3/bitarray-3.8.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:28a85b056c0eb7f5d864c0ceef07034117e8ebfca756f50648c71950a568ba11", size = 358337, upload-time = "2025-11-02T21:40:10.035Z" }, + { url = "https://files.pythonhosted.org/packages/b5/6b/9095d75264c67d479f298c80802422464ce18c3cdd893252eeccf4997611/bitarray-3.8.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:79ec4498a545733ecace48d780d22407411b07403a2e08b9a4d7596c0b97ebd7", size = 355639, upload-time = "2025-11-02T21:40:11.485Z" }, + { url = "https://files.pythonhosted.org/packages/a0/af/c93c0ae5ef824136e90ac7ddf6cceccb1232f34240b2f55a922f874da9b4/bitarray-3.8.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:33af25c4ff7723363cb8404dfc2eefeab4110b654f6c98d26aba8a08c745d860", size = 336999, upload-time = "2025-11-02T21:40:12.709Z" }, + { url = "https://files.pythonhosted.org/packages/81/0f/72c951f5997b2876355d5e671f78dd2362493254876675cf22dbd24389ae/bitarray-3.8.0-cp314-cp314-win32.whl", hash = "sha256:2c3bb96b6026643ce24677650889b09073f60b9860a71765f843c99f9ab38b25", size = 142169, upload-time = "2025-11-02T21:40:14.031Z" }, + { url = "https://files.pythonhosted.org/packages/8a/55/ef1b4de8107bf13823da8756c20e1fbc9452228b4e837f46f6d9ddba3eb3/bitarray-3.8.0-cp314-cp314-win_amd64.whl", hash = "sha256:847c7f61964225fc489fe1d49eda7e0e0d253e98862c012cecf845f9ad45cdf4", size = 148737, upload-time = "2025-11-02T21:40:15.436Z" }, + { url = "https://files.pythonhosted.org/packages/5f/26/bc0784136775024ac56cc67c0d6f9aa77a7770de7f82c3a7c9be11c217cd/bitarray-3.8.0-cp314-cp314-win_arm64.whl", hash = "sha256:a2cb35a6efaa0e3623d8272471371a12c7e07b51a33e5efce9b58f655d864b4e", size = 146083, upload-time = "2025-11-02T21:40:17.135Z" }, + { url = "https://files.pythonhosted.org/packages/6e/64/57984e64264bf43d93a1809e645972771566a2d0345f4896b041ce20b000/bitarray-3.8.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:15e8d0597cc6e8496de6f4dea2a6880c57e1251502a7072f5631108a1aa28521", size = 149455, upload-time = "2025-11-02T21:40:18.558Z" }, + { url = "https://files.pythonhosted.org/packages/81/c0/0d5f2eaef1867f462f764bdb07d1e116c33a1bf052ea21889aefe4282f5b/bitarray-3.8.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:8ffe660e963ae711cb9e2b8d8461c9b1ad6167823837fc17d59d5e539fb898fa", size = 146491, upload-time = "2025-11-02T21:40:19.665Z" }, + { url = "https://files.pythonhosted.org/packages/65/c6/bc1261f7a8862c0c59220a484464739e52235fd1e2afcb24d7f7d3fb5702/bitarray-3.8.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4779f356083c62e29b4198d290b7b17a39a69702d150678b7efff0fdddf494a8", size = 339721, upload-time = "2025-11-02T21:40:21.277Z" }, + { url = "https://files.pythonhosted.org/packages/81/d8/289ca55dd2939ea17b1108dc53bffc0fdc5160ba44f77502dfaae35d08c6/bitarray-3.8.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:025d133bf4ca8cf75f904eeb8ea946228d7c043231866143f31946a6f4dd0bf3", size = 367823, upload-time = "2025-11-02T21:40:22.463Z" }, + { url = "https://files.pythonhosted.org/packages/91/a2/61e7461ca9ac0fcb70f327a2e84b006996d2a840898e69037a39c87c6d06/bitarray-3.8.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:451f9958850ea98440d542278368c8d1e1ea821e2494b204570ba34a340759df", size = 377341, upload-time = "2025-11-02T21:40:23.789Z" }, + { url = "https://files.pythonhosted.org/packages/6c/87/4a0c9c8bdb13916d443e04d8f8542eef9190f31425da3c17c3478c40173f/bitarray-3.8.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6d79f659965290af60d6acc8e2716341865fe74609a7ede2a33c2f86ad893b8f", size = 344985, upload-time = "2025-11-02T21:40:25.261Z" }, + { url = "https://files.pythonhosted.org/packages/17/4c/ff9259b916efe53695b631772e5213699c738efc2471b5ffe273f4000994/bitarray-3.8.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:fbf05678c2ae0064fb1b8de7e9e8f0fc30621b73c8477786dd0fb3868044a8c8", size = 336796, upload-time = "2025-11-02T21:40:26.942Z" }, + { url = "https://files.pythonhosted.org/packages/0f/4b/51b2468bbddbade5e2f3b8d5db08282c5b309e8687b0f02f75a8b5ff559c/bitarray-3.8.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:c396358023b876cff547ce87f4e8ff8a2280598873a137e8cc69e115262260b8", size = 365085, upload-time = "2025-11-02T21:40:28.224Z" }, + { url = "https://files.pythonhosted.org/packages/bf/79/53473bfc2e052c6dbb628cdc1b156be621c77aaeb715918358b01574be55/bitarray-3.8.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:ed3493a369fe849cce98542d7405c88030b355e4d2e113887cb7ecc86c205773", size = 361012, upload-time = "2025-11-02T21:40:29.635Z" }, + { url = "https://files.pythonhosted.org/packages/c4/b1/242bf2e44bfc69e73fa2b954b425d761a8e632f78ea31008f1c3cfad0854/bitarray-3.8.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:c764fb167411d5afaef88138542a4bfa28bd5e5ded5e8e42df87cef965efd6e9", size = 340644, upload-time = "2025-11-02T21:40:31.089Z" }, + { url = "https://files.pythonhosted.org/packages/cf/01/12e5ecf30a5de28a32485f226cad4b8a546845f65f755ce0365057ab1e92/bitarray-3.8.0-cp314-cp314t-win32.whl", hash = "sha256:e12769d3adcc419e65860de946df8d2ed274932177ac1cdb05186e498aaa9149", size = 143630, upload-time = "2025-11-02T21:40:32.351Z" }, + { url = "https://files.pythonhosted.org/packages/b6/92/6b6ade587b08024a8a890b07724775d29da9cf7497be5c3cbe226185e463/bitarray-3.8.0-cp314-cp314t-win_amd64.whl", hash = "sha256:0ca70ccf789446a6dfde40b482ec21d28067172cd1f8efd50d5548159fccad9e", size = 150250, upload-time = "2025-11-02T21:40:33.596Z" }, + { url = "https://files.pythonhosted.org/packages/ed/40/be3858ffed004e47e48a2cefecdbf9b950d41098b780f9dc3aa609a88351/bitarray-3.8.0-cp314-cp314t-win_arm64.whl", hash = "sha256:2a3d1b05ffdd3e95687942ae7b13c63689f85d3f15c39b33329e3cb9ce6c015f", size = 147015, upload-time = "2025-11-02T21:40:35.064Z" }, +] + +[[package]] +name = "black" +version = "25.12.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "mypy-extensions" }, + { name = "packaging" }, + { name = "pathspec" }, + { name = "platformdirs" }, + { name = "pytokens" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c4/d9/07b458a3f1c525ac392b5edc6b191ff140b596f9d77092429417a54e249d/black-25.12.0.tar.gz", hash = "sha256:8d3dd9cea14bff7ddc0eb243c811cdb1a011ebb4800a5f0335a01a68654796a7", size = 659264, upload-time = "2025-12-08T01:40:52.501Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/37/d5/8d3145999d380e5d09bb00b0f7024bf0a8ccb5c07b5648e9295f02ec1d98/black-25.12.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f85ba1ad15d446756b4ab5f3044731bf68b777f8f9ac9cdabd2425b97cd9c4e8", size = 1895720, upload-time = "2025-12-08T01:46:58.197Z" }, + { url = "https://files.pythonhosted.org/packages/06/97/7acc85c4add41098f4f076b21e3e4e383ad6ed0a3da26b2c89627241fc11/black-25.12.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:546eecfe9a3a6b46f9d69d8a642585a6eaf348bcbbc4d87a19635570e02d9f4a", size = 1727193, upload-time = "2025-12-08T01:52:26.674Z" }, + { url = "https://files.pythonhosted.org/packages/24/f0/fdf0eb8ba907ddeb62255227d29d349e8256ef03558fbcadfbc26ecfe3b2/black-25.12.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:17dcc893da8d73d8f74a596f64b7c98ef5239c2cd2b053c0f25912c4494bf9ea", size = 1774506, upload-time = "2025-12-08T01:46:25.721Z" }, + { url = "https://files.pythonhosted.org/packages/e4/f5/9203a78efe00d13336786b133c6180a9303d46908a9aa72d1104ca214222/black-25.12.0-cp310-cp310-win_amd64.whl", hash = "sha256:09524b0e6af8ba7a3ffabdfc7a9922fb9adef60fed008c7cd2fc01f3048e6e6f", size = 1416085, upload-time = "2025-12-08T01:46:06.073Z" }, + { url = "https://files.pythonhosted.org/packages/ba/cc/7a6090e6b081c3316282c05c546e76affdce7bf7a3b7d2c3a2a69438bd01/black-25.12.0-cp310-cp310-win_arm64.whl", hash = "sha256:b162653ed89eb942758efeb29d5e333ca5bb90e5130216f8369857db5955a7da", size = 1226038, upload-time = "2025-12-08T01:45:29.388Z" }, + { url = "https://files.pythonhosted.org/packages/60/ad/7ac0d0e1e0612788dbc48e62aef8a8e8feffac7eb3d787db4e43b8462fa8/black-25.12.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d0cfa263e85caea2cff57d8f917f9f51adae8e20b610e2b23de35b5b11ce691a", size = 1877003, upload-time = "2025-12-08T01:43:29.967Z" }, + { url = "https://files.pythonhosted.org/packages/e8/dd/a237e9f565f3617a88b49284b59cbca2a4f56ebe68676c1aad0ce36a54a7/black-25.12.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1a2f578ae20c19c50a382286ba78bfbeafdf788579b053d8e4980afb079ab9be", size = 1712639, upload-time = "2025-12-08T01:52:46.756Z" }, + { url = "https://files.pythonhosted.org/packages/12/80/e187079df1ea4c12a0c63282ddd8b81d5107db6d642f7d7b75a6bcd6fc21/black-25.12.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d3e1b65634b0e471d07ff86ec338819e2ef860689859ef4501ab7ac290431f9b", size = 1758143, upload-time = "2025-12-08T01:45:29.137Z" }, + { url = "https://files.pythonhosted.org/packages/93/b5/3096ccee4f29dc2c3aac57274326c4d2d929a77e629f695f544e159bfae4/black-25.12.0-cp311-cp311-win_amd64.whl", hash = "sha256:a3fa71e3b8dd9f7c6ac4d818345237dfb4175ed3bf37cd5a581dbc4c034f1ec5", size = 1420698, upload-time = "2025-12-08T01:45:53.379Z" }, + { url = "https://files.pythonhosted.org/packages/7e/39/f81c0ffbc25ffbe61c7d0385bf277e62ffc3e52f5ee668d7369d9854fadf/black-25.12.0-cp311-cp311-win_arm64.whl", hash = "sha256:51e267458f7e650afed8445dc7edb3187143003d52a1b710c7321aef22aa9655", size = 1229317, upload-time = "2025-12-08T01:46:35.606Z" }, + { url = "https://files.pythonhosted.org/packages/d1/bd/26083f805115db17fda9877b3c7321d08c647df39d0df4c4ca8f8450593e/black-25.12.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:31f96b7c98c1ddaeb07dc0f56c652e25bdedaac76d5b68a059d998b57c55594a", size = 1924178, upload-time = "2025-12-08T01:49:51.048Z" }, + { url = "https://files.pythonhosted.org/packages/89/6b/ea00d6651561e2bdd9231c4177f4f2ae19cc13a0b0574f47602a7519b6ca/black-25.12.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:05dd459a19e218078a1f98178c13f861fe6a9a5f88fc969ca4d9b49eb1809783", size = 1742643, upload-time = "2025-12-08T01:49:59.09Z" }, + { url = "https://files.pythonhosted.org/packages/6d/f3/360fa4182e36e9875fabcf3a9717db9d27a8d11870f21cff97725c54f35b/black-25.12.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c1f68c5eff61f226934be6b5b80296cf6939e5d2f0c2f7d543ea08b204bfaf59", size = 1800158, upload-time = "2025-12-08T01:44:27.301Z" }, + { url = "https://files.pythonhosted.org/packages/f8/08/2c64830cb6616278067e040acca21d4f79727b23077633953081c9445d61/black-25.12.0-cp312-cp312-win_amd64.whl", hash = "sha256:274f940c147ddab4442d316b27f9e332ca586d39c85ecf59ebdea82cc9ee8892", size = 1426197, upload-time = "2025-12-08T01:45:51.198Z" }, + { url = "https://files.pythonhosted.org/packages/d4/60/a93f55fd9b9816b7432cf6842f0e3000fdd5b7869492a04b9011a133ee37/black-25.12.0-cp312-cp312-win_arm64.whl", hash = "sha256:169506ba91ef21e2e0591563deda7f00030cb466e747c4b09cb0a9dae5db2f43", size = 1237266, upload-time = "2025-12-08T01:45:10.556Z" }, + { url = "https://files.pythonhosted.org/packages/c8/52/c551e36bc95495d2aa1a37d50566267aa47608c81a53f91daa809e03293f/black-25.12.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a05ddeb656534c3e27a05a29196c962877c83fa5503db89e68857d1161ad08a5", size = 1923809, upload-time = "2025-12-08T01:46:55.126Z" }, + { url = "https://files.pythonhosted.org/packages/a0/f7/aac9b014140ee56d247e707af8db0aae2e9efc28d4a8aba92d0abd7ae9d1/black-25.12.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9ec77439ef3e34896995503865a85732c94396edcc739f302c5673a2315e1e7f", size = 1742384, upload-time = "2025-12-08T01:49:37.022Z" }, + { url = "https://files.pythonhosted.org/packages/74/98/38aaa018b2ab06a863974c12b14a6266badc192b20603a81b738c47e902e/black-25.12.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e509c858adf63aa61d908061b52e580c40eae0dfa72415fa47ac01b12e29baf", size = 1798761, upload-time = "2025-12-08T01:46:05.386Z" }, + { url = "https://files.pythonhosted.org/packages/16/3a/a8ac542125f61574a3f015b521ca83b47321ed19bb63fe6d7560f348bfe1/black-25.12.0-cp313-cp313-win_amd64.whl", hash = "sha256:252678f07f5bac4ff0d0e9b261fbb029fa530cfa206d0a636a34ab445ef8ca9d", size = 1429180, upload-time = "2025-12-08T01:45:34.903Z" }, + { url = "https://files.pythonhosted.org/packages/e6/2d/bdc466a3db9145e946762d52cd55b1385509d9f9004fec1c97bdc8debbfb/black-25.12.0-cp313-cp313-win_arm64.whl", hash = "sha256:bc5b1c09fe3c931ddd20ee548511c64ebf964ada7e6f0763d443947fd1c603ce", size = 1239350, upload-time = "2025-12-08T01:46:09.458Z" }, + { url = "https://files.pythonhosted.org/packages/35/46/1d8f2542210c502e2ae1060b2e09e47af6a5e5963cb78e22ec1a11170b28/black-25.12.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:0a0953b134f9335c2434864a643c842c44fba562155c738a2a37a4d61f00cad5", size = 1917015, upload-time = "2025-12-08T01:53:27.987Z" }, + { url = "https://files.pythonhosted.org/packages/41/37/68accadf977672beb8e2c64e080f568c74159c1aaa6414b4cd2aef2d7906/black-25.12.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:2355bbb6c3b76062870942d8cc450d4f8ac71f9c93c40122762c8784df49543f", size = 1741830, upload-time = "2025-12-08T01:54:36.861Z" }, + { url = "https://files.pythonhosted.org/packages/ac/76/03608a9d8f0faad47a3af3a3c8c53af3367f6c0dd2d23a84710456c7ac56/black-25.12.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9678bd991cc793e81d19aeeae57966ee02909877cb65838ccffef24c3ebac08f", size = 1791450, upload-time = "2025-12-08T01:44:52.581Z" }, + { url = "https://files.pythonhosted.org/packages/06/99/b2a4bd7dfaea7964974f947e1c76d6886d65fe5d24f687df2d85406b2609/black-25.12.0-cp314-cp314-win_amd64.whl", hash = "sha256:97596189949a8aad13ad12fcbb4ae89330039b96ad6742e6f6b45e75ad5cfd83", size = 1452042, upload-time = "2025-12-08T01:46:13.188Z" }, + { url = "https://files.pythonhosted.org/packages/b2/7c/d9825de75ae5dd7795d007681b752275ea85a1c5d83269b4b9c754c2aaab/black-25.12.0-cp314-cp314-win_arm64.whl", hash = "sha256:778285d9ea197f34704e3791ea9404cd6d07595745907dd2ce3da7a13627b29b", size = 1267446, upload-time = "2025-12-08T01:46:14.497Z" }, + { url = "https://files.pythonhosted.org/packages/68/11/21331aed19145a952ad28fca2756a1433ee9308079bd03bd898e903a2e53/black-25.12.0-py3-none-any.whl", hash = "sha256:48ceb36c16dbc84062740049eef990bb2ce07598272e673c17d1a7720c71c828", size = 206191, upload-time = "2025-12-08T01:40:50.963Z" }, +] + +[[package]] +name = "certifi" +version = "2025.11.12" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/8c/58f469717fa48465e4a50c014a0400602d3c437d7c0c468e17ada824da3a/certifi-2025.11.12.tar.gz", hash = "sha256:d8ab5478f2ecd78af242878415affce761ca6bc54a22a27e026d7c25357c3316", size = 160538, upload-time = "2025-11-12T02:54:51.517Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/70/7d/9bc192684cea499815ff478dfcdc13835ddf401365057044fb721ec6bddb/certifi-2025.11.12-py3-none-any.whl", hash = "sha256:97de8790030bbd5c2d96b7ec782fc2f7820ef8dba6db909ccf95449f2d062d4b", size = 159438, upload-time = "2025-11-12T02:54:49.735Z" }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/13/69/33ddede1939fdd074bce5434295f38fae7136463422fe4fd3e0e89b98062/charset_normalizer-3.4.4.tar.gz", hash = "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a", size = 129418, upload-time = "2025-10-14T04:42:32.879Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1f/b8/6d51fc1d52cbd52cd4ccedd5b5b2f0f6a11bbf6765c782298b0f3e808541/charset_normalizer-3.4.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e824f1492727fa856dd6eda4f7cee25f8518a12f3c4a56a74e8095695089cf6d", size = 209709, upload-time = "2025-10-14T04:40:11.385Z" }, + { url = "https://files.pythonhosted.org/packages/5c/af/1f9d7f7faafe2ddfb6f72a2e07a548a629c61ad510fe60f9630309908fef/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4bd5d4137d500351a30687c2d3971758aac9a19208fc110ccb9d7188fbe709e8", size = 148814, upload-time = "2025-10-14T04:40:13.135Z" }, + { url = "https://files.pythonhosted.org/packages/79/3d/f2e3ac2bbc056ca0c204298ea4e3d9db9b4afe437812638759db2c976b5f/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:027f6de494925c0ab2a55eab46ae5129951638a49a34d87f4c3eda90f696b4ad", size = 144467, upload-time = "2025-10-14T04:40:14.728Z" }, + { url = "https://files.pythonhosted.org/packages/ec/85/1bf997003815e60d57de7bd972c57dc6950446a3e4ccac43bc3070721856/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f820802628d2694cb7e56db99213f930856014862f3fd943d290ea8438d07ca8", size = 162280, upload-time = "2025-10-14T04:40:16.14Z" }, + { url = "https://files.pythonhosted.org/packages/3e/8e/6aa1952f56b192f54921c436b87f2aaf7c7a7c3d0d1a765547d64fd83c13/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:798d75d81754988d2565bff1b97ba5a44411867c0cf32b77a7e8f8d84796b10d", size = 159454, upload-time = "2025-10-14T04:40:17.567Z" }, + { url = "https://files.pythonhosted.org/packages/36/3b/60cbd1f8e93aa25d1c669c649b7a655b0b5fb4c571858910ea9332678558/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d1bb833febdff5c8927f922386db610b49db6e0d4f4ee29601d71e7c2694313", size = 153609, upload-time = "2025-10-14T04:40:19.08Z" }, + { url = "https://files.pythonhosted.org/packages/64/91/6a13396948b8fd3c4b4fd5bc74d045f5637d78c9675585e8e9fbe5636554/charset_normalizer-3.4.4-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9cd98cdc06614a2f768d2b7286d66805f94c48cde050acdbbb7db2600ab3197e", size = 151849, upload-time = "2025-10-14T04:40:20.607Z" }, + { url = "https://files.pythonhosted.org/packages/b7/7a/59482e28b9981d105691e968c544cc0df3b7d6133152fb3dcdc8f135da7a/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:077fbb858e903c73f6c9db43374fd213b0b6a778106bc7032446a8e8b5b38b93", size = 151586, upload-time = "2025-10-14T04:40:21.719Z" }, + { url = "https://files.pythonhosted.org/packages/92/59/f64ef6a1c4bdd2baf892b04cd78792ed8684fbc48d4c2afe467d96b4df57/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:244bfb999c71b35de57821b8ea746b24e863398194a4014e4c76adc2bbdfeff0", size = 145290, upload-time = "2025-10-14T04:40:23.069Z" }, + { url = "https://files.pythonhosted.org/packages/6b/63/3bf9f279ddfa641ffa1962b0db6a57a9c294361cc2f5fcac997049a00e9c/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:64b55f9dce520635f018f907ff1b0df1fdc31f2795a922fb49dd14fbcdf48c84", size = 163663, upload-time = "2025-10-14T04:40:24.17Z" }, + { url = "https://files.pythonhosted.org/packages/ed/09/c9e38fc8fa9e0849b172b581fd9803bdf6e694041127933934184e19f8c3/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:faa3a41b2b66b6e50f84ae4a68c64fcd0c44355741c6374813a800cd6695db9e", size = 151964, upload-time = "2025-10-14T04:40:25.368Z" }, + { url = "https://files.pythonhosted.org/packages/d2/d1/d28b747e512d0da79d8b6a1ac18b7ab2ecfd81b2944c4c710e166d8dd09c/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:6515f3182dbe4ea06ced2d9e8666d97b46ef4c75e326b79bb624110f122551db", size = 161064, upload-time = "2025-10-14T04:40:26.806Z" }, + { url = "https://files.pythonhosted.org/packages/bb/9a/31d62b611d901c3b9e5500c36aab0ff5eb442043fb3a1c254200d3d397d9/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:cc00f04ed596e9dc0da42ed17ac5e596c6ccba999ba6bd92b0e0aef2f170f2d6", size = 155015, upload-time = "2025-10-14T04:40:28.284Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f3/107e008fa2bff0c8b9319584174418e5e5285fef32f79d8ee6a430d0039c/charset_normalizer-3.4.4-cp310-cp310-win32.whl", hash = "sha256:f34be2938726fc13801220747472850852fe6b1ea75869a048d6f896838c896f", size = 99792, upload-time = "2025-10-14T04:40:29.613Z" }, + { url = "https://files.pythonhosted.org/packages/eb/66/e396e8a408843337d7315bab30dbf106c38966f1819f123257f5520f8a96/charset_normalizer-3.4.4-cp310-cp310-win_amd64.whl", hash = "sha256:a61900df84c667873b292c3de315a786dd8dac506704dea57bc957bd31e22c7d", size = 107198, upload-time = "2025-10-14T04:40:30.644Z" }, + { url = "https://files.pythonhosted.org/packages/b5/58/01b4f815bf0312704c267f2ccb6e5d42bcc7752340cd487bc9f8c3710597/charset_normalizer-3.4.4-cp310-cp310-win_arm64.whl", hash = "sha256:cead0978fc57397645f12578bfd2d5ea9138ea0fac82b2f63f7f7c6877986a69", size = 100262, upload-time = "2025-10-14T04:40:32.108Z" }, + { url = "https://files.pythonhosted.org/packages/ed/27/c6491ff4954e58a10f69ad90aca8a1b6fe9c5d3c6f380907af3c37435b59/charset_normalizer-3.4.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6e1fcf0720908f200cd21aa4e6750a48ff6ce4afe7ff5a79a90d5ed8a08296f8", size = 206988, upload-time = "2025-10-14T04:40:33.79Z" }, + { url = "https://files.pythonhosted.org/packages/94/59/2e87300fe67ab820b5428580a53cad894272dbb97f38a7a814a2a1ac1011/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f819d5fe9234f9f82d75bdfa9aef3a3d72c4d24a6e57aeaebba32a704553aa0", size = 147324, upload-time = "2025-10-14T04:40:34.961Z" }, + { url = "https://files.pythonhosted.org/packages/07/fb/0cf61dc84b2b088391830f6274cb57c82e4da8bbc2efeac8c025edb88772/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a59cb51917aa591b1c4e6a43c132f0cdc3c76dbad6155df4e28ee626cc77a0a3", size = 142742, upload-time = "2025-10-14T04:40:36.105Z" }, + { url = "https://files.pythonhosted.org/packages/62/8b/171935adf2312cd745d290ed93cf16cf0dfe320863ab7cbeeae1dcd6535f/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8ef3c867360f88ac904fd3f5e1f902f13307af9052646963ee08ff4f131adafc", size = 160863, upload-time = "2025-10-14T04:40:37.188Z" }, + { url = "https://files.pythonhosted.org/packages/09/73/ad875b192bda14f2173bfc1bc9a55e009808484a4b256748d931b6948442/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d9e45d7faa48ee908174d8fe84854479ef838fc6a705c9315372eacbc2f02897", size = 157837, upload-time = "2025-10-14T04:40:38.435Z" }, + { url = "https://files.pythonhosted.org/packages/6d/fc/de9cce525b2c5b94b47c70a4b4fb19f871b24995c728e957ee68ab1671ea/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:840c25fb618a231545cbab0564a799f101b63b9901f2569faecd6b222ac72381", size = 151550, upload-time = "2025-10-14T04:40:40.053Z" }, + { url = "https://files.pythonhosted.org/packages/55/c2/43edd615fdfba8c6f2dfbd459b25a6b3b551f24ea21981e23fb768503ce1/charset_normalizer-3.4.4-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ca5862d5b3928c4940729dacc329aa9102900382fea192fc5e52eb69d6093815", size = 149162, upload-time = "2025-10-14T04:40:41.163Z" }, + { url = "https://files.pythonhosted.org/packages/03/86/bde4ad8b4d0e9429a4e82c1e8f5c659993a9a863ad62c7df05cf7b678d75/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d9c7f57c3d666a53421049053eaacdd14bbd0a528e2186fcb2e672effd053bb0", size = 150019, upload-time = "2025-10-14T04:40:42.276Z" }, + { url = "https://files.pythonhosted.org/packages/1f/86/a151eb2af293a7e7bac3a739b81072585ce36ccfb4493039f49f1d3cae8c/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:277e970e750505ed74c832b4bf75dac7476262ee2a013f5574dd49075879e161", size = 143310, upload-time = "2025-10-14T04:40:43.439Z" }, + { url = "https://files.pythonhosted.org/packages/b5/fe/43dae6144a7e07b87478fdfc4dbe9efd5defb0e7ec29f5f58a55aeef7bf7/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:31fd66405eaf47bb62e8cd575dc621c56c668f27d46a61d975a249930dd5e2a4", size = 162022, upload-time = "2025-10-14T04:40:44.547Z" }, + { url = "https://files.pythonhosted.org/packages/80/e6/7aab83774f5d2bca81f42ac58d04caf44f0cc2b65fc6db2b3b2e8a05f3b3/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:0d3d8f15c07f86e9ff82319b3d9ef6f4bf907608f53fe9d92b28ea9ae3d1fd89", size = 149383, upload-time = "2025-10-14T04:40:46.018Z" }, + { url = "https://files.pythonhosted.org/packages/4f/e8/b289173b4edae05c0dde07f69f8db476a0b511eac556dfe0d6bda3c43384/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:9f7fcd74d410a36883701fafa2482a6af2ff5ba96b9a620e9e0721e28ead5569", size = 159098, upload-time = "2025-10-14T04:40:47.081Z" }, + { url = "https://files.pythonhosted.org/packages/d8/df/fe699727754cae3f8478493c7f45f777b17c3ef0600e28abfec8619eb49c/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ebf3e58c7ec8a8bed6d66a75d7fb37b55e5015b03ceae72a8e7c74495551e224", size = 152991, upload-time = "2025-10-14T04:40:48.246Z" }, + { url = "https://files.pythonhosted.org/packages/1a/86/584869fe4ddb6ffa3bd9f491b87a01568797fb9bd8933f557dba9771beaf/charset_normalizer-3.4.4-cp311-cp311-win32.whl", hash = "sha256:eecbc200c7fd5ddb9a7f16c7decb07b566c29fa2161a16cf67b8d068bd21690a", size = 99456, upload-time = "2025-10-14T04:40:49.376Z" }, + { url = "https://files.pythonhosted.org/packages/65/f6/62fdd5feb60530f50f7e38b4f6a1d5203f4d16ff4f9f0952962c044e919a/charset_normalizer-3.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:5ae497466c7901d54b639cf42d5b8c1b6a4fead55215500d2f486d34db48d016", size = 106978, upload-time = "2025-10-14T04:40:50.844Z" }, + { url = "https://files.pythonhosted.org/packages/7a/9d/0710916e6c82948b3be62d9d398cb4fcf4e97b56d6a6aeccd66c4b2f2bd5/charset_normalizer-3.4.4-cp311-cp311-win_arm64.whl", hash = "sha256:65e2befcd84bc6f37095f5961e68a6f077bf44946771354a28ad434c2cce0ae1", size = 99969, upload-time = "2025-10-14T04:40:52.272Z" }, + { url = "https://files.pythonhosted.org/packages/f3/85/1637cd4af66fa687396e757dec650f28025f2a2f5a5531a3208dc0ec43f2/charset_normalizer-3.4.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0a98e6759f854bd25a58a73fa88833fba3b7c491169f86ce1180c948ab3fd394", size = 208425, upload-time = "2025-10-14T04:40:53.353Z" }, + { url = "https://files.pythonhosted.org/packages/9d/6a/04130023fef2a0d9c62d0bae2649b69f7b7d8d24ea5536feef50551029df/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b5b290ccc2a263e8d185130284f8501e3e36c5e02750fc6b6bdeb2e9e96f1e25", size = 148162, upload-time = "2025-10-14T04:40:54.558Z" }, + { url = "https://files.pythonhosted.org/packages/78/29/62328d79aa60da22c9e0b9a66539feae06ca0f5a4171ac4f7dc285b83688/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74bb723680f9f7a6234dcf67aea57e708ec1fbdf5699fb91dfd6f511b0a320ef", size = 144558, upload-time = "2025-10-14T04:40:55.677Z" }, + { url = "https://files.pythonhosted.org/packages/86/bb/b32194a4bf15b88403537c2e120b817c61cd4ecffa9b6876e941c3ee38fe/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f1e34719c6ed0b92f418c7c780480b26b5d9c50349e9a9af7d76bf757530350d", size = 161497, upload-time = "2025-10-14T04:40:57.217Z" }, + { url = "https://files.pythonhosted.org/packages/19/89/a54c82b253d5b9b111dc74aca196ba5ccfcca8242d0fb64146d4d3183ff1/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2437418e20515acec67d86e12bf70056a33abdacb5cb1655042f6538d6b085a8", size = 159240, upload-time = "2025-10-14T04:40:58.358Z" }, + { url = "https://files.pythonhosted.org/packages/c0/10/d20b513afe03acc89ec33948320a5544d31f21b05368436d580dec4e234d/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11d694519d7f29d6cd09f6ac70028dba10f92f6cdd059096db198c283794ac86", size = 153471, upload-time = "2025-10-14T04:40:59.468Z" }, + { url = "https://files.pythonhosted.org/packages/61/fa/fbf177b55bdd727010f9c0a3c49eefa1d10f960e5f09d1d887bf93c2e698/charset_normalizer-3.4.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ac1c4a689edcc530fc9d9aa11f5774b9e2f33f9a0c6a57864e90908f5208d30a", size = 150864, upload-time = "2025-10-14T04:41:00.623Z" }, + { url = "https://files.pythonhosted.org/packages/05/12/9fbc6a4d39c0198adeebbde20b619790e9236557ca59fc40e0e3cebe6f40/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:21d142cc6c0ec30d2efee5068ca36c128a30b0f2c53c1c07bd78cb6bc1d3be5f", size = 150647, upload-time = "2025-10-14T04:41:01.754Z" }, + { url = "https://files.pythonhosted.org/packages/ad/1f/6a9a593d52e3e8c5d2b167daf8c6b968808efb57ef4c210acb907c365bc4/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:5dbe56a36425d26d6cfb40ce79c314a2e4dd6211d51d6d2191c00bed34f354cc", size = 145110, upload-time = "2025-10-14T04:41:03.231Z" }, + { url = "https://files.pythonhosted.org/packages/30/42/9a52c609e72471b0fc54386dc63c3781a387bb4fe61c20231a4ebcd58bdd/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5bfbb1b9acf3334612667b61bd3002196fe2a1eb4dd74d247e0f2a4d50ec9bbf", size = 162839, upload-time = "2025-10-14T04:41:04.715Z" }, + { url = "https://files.pythonhosted.org/packages/c4/5b/c0682bbf9f11597073052628ddd38344a3d673fda35a36773f7d19344b23/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:d055ec1e26e441f6187acf818b73564e6e6282709e9bcb5b63f5b23068356a15", size = 150667, upload-time = "2025-10-14T04:41:05.827Z" }, + { url = "https://files.pythonhosted.org/packages/e4/24/a41afeab6f990cf2daf6cb8c67419b63b48cf518e4f56022230840c9bfb2/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:af2d8c67d8e573d6de5bc30cdb27e9b95e49115cd9baad5ddbd1a6207aaa82a9", size = 160535, upload-time = "2025-10-14T04:41:06.938Z" }, + { url = "https://files.pythonhosted.org/packages/2a/e5/6a4ce77ed243c4a50a1fecca6aaaab419628c818a49434be428fe24c9957/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:780236ac706e66881f3b7f2f32dfe90507a09e67d1d454c762cf642e6e1586e0", size = 154816, upload-time = "2025-10-14T04:41:08.101Z" }, + { url = "https://files.pythonhosted.org/packages/a8/ef/89297262b8092b312d29cdb2517cb1237e51db8ecef2e9af5edbe7b683b1/charset_normalizer-3.4.4-cp312-cp312-win32.whl", hash = "sha256:5833d2c39d8896e4e19b689ffc198f08ea58116bee26dea51e362ecc7cd3ed26", size = 99694, upload-time = "2025-10-14T04:41:09.23Z" }, + { url = "https://files.pythonhosted.org/packages/3d/2d/1e5ed9dd3b3803994c155cd9aacb60c82c331bad84daf75bcb9c91b3295e/charset_normalizer-3.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:a79cfe37875f822425b89a82333404539ae63dbdddf97f84dcbc3d339aae9525", size = 107131, upload-time = "2025-10-14T04:41:10.467Z" }, + { url = "https://files.pythonhosted.org/packages/d0/d9/0ed4c7098a861482a7b6a95603edce4c0d9db2311af23da1fb2b75ec26fc/charset_normalizer-3.4.4-cp312-cp312-win_arm64.whl", hash = "sha256:376bec83a63b8021bb5c8ea75e21c4ccb86e7e45ca4eb81146091b56599b80c3", size = 100390, upload-time = "2025-10-14T04:41:11.915Z" }, + { url = "https://files.pythonhosted.org/packages/97/45/4b3a1239bbacd321068ea6e7ac28875b03ab8bc0aa0966452db17cd36714/charset_normalizer-3.4.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794", size = 208091, upload-time = "2025-10-14T04:41:13.346Z" }, + { url = "https://files.pythonhosted.org/packages/7d/62/73a6d7450829655a35bb88a88fca7d736f9882a27eacdca2c6d505b57e2e/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed", size = 147936, upload-time = "2025-10-14T04:41:14.461Z" }, + { url = "https://files.pythonhosted.org/packages/89/c5/adb8c8b3d6625bef6d88b251bbb0d95f8205831b987631ab0c8bb5d937c2/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72", size = 144180, upload-time = "2025-10-14T04:41:15.588Z" }, + { url = "https://files.pythonhosted.org/packages/91/ed/9706e4070682d1cc219050b6048bfd293ccf67b3d4f5a4f39207453d4b99/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:81d5eb2a312700f4ecaa977a8235b634ce853200e828fbadf3a9c50bab278328", size = 161346, upload-time = "2025-10-14T04:41:16.738Z" }, + { url = "https://files.pythonhosted.org/packages/d5/0d/031f0d95e4972901a2f6f09ef055751805ff541511dc1252ba3ca1f80cf5/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5bd2293095d766545ec1a8f612559f6b40abc0eb18bb2f5d1171872d34036ede", size = 158874, upload-time = "2025-10-14T04:41:17.923Z" }, + { url = "https://files.pythonhosted.org/packages/f5/83/6ab5883f57c9c801ce5e5677242328aa45592be8a00644310a008d04f922/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8a8b89589086a25749f471e6a900d3f662d1d3b6e2e59dcecf787b1cc3a1894", size = 153076, upload-time = "2025-10-14T04:41:19.106Z" }, + { url = "https://files.pythonhosted.org/packages/75/1e/5ff781ddf5260e387d6419959ee89ef13878229732732ee73cdae01800f2/charset_normalizer-3.4.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc7637e2f80d8530ee4a78e878bce464f70087ce73cf7c1caf142416923b98f1", size = 150601, upload-time = "2025-10-14T04:41:20.245Z" }, + { url = "https://files.pythonhosted.org/packages/d7/57/71be810965493d3510a6ca79b90c19e48696fb1ff964da319334b12677f0/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f8bf04158c6b607d747e93949aa60618b61312fe647a6369f88ce2ff16043490", size = 150376, upload-time = "2025-10-14T04:41:21.398Z" }, + { url = "https://files.pythonhosted.org/packages/e5/d5/c3d057a78c181d007014feb7e9f2e65905a6c4ef182c0ddf0de2924edd65/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:554af85e960429cf30784dd47447d5125aaa3b99a6f0683589dbd27e2f45da44", size = 144825, upload-time = "2025-10-14T04:41:22.583Z" }, + { url = "https://files.pythonhosted.org/packages/e6/8c/d0406294828d4976f275ffbe66f00266c4b3136b7506941d87c00cab5272/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:74018750915ee7ad843a774364e13a3db91682f26142baddf775342c3f5b1133", size = 162583, upload-time = "2025-10-14T04:41:23.754Z" }, + { url = "https://files.pythonhosted.org/packages/d7/24/e2aa1f18c8f15c4c0e932d9287b8609dd30ad56dbe41d926bd846e22fb8d/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c0463276121fdee9c49b98908b3a89c39be45d86d1dbaa22957e38f6321d4ce3", size = 150366, upload-time = "2025-10-14T04:41:25.27Z" }, + { url = "https://files.pythonhosted.org/packages/e4/5b/1e6160c7739aad1e2df054300cc618b06bf784a7a164b0f238360721ab86/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:362d61fd13843997c1c446760ef36f240cf81d3ebf74ac62652aebaf7838561e", size = 160300, upload-time = "2025-10-14T04:41:26.725Z" }, + { url = "https://files.pythonhosted.org/packages/7a/10/f882167cd207fbdd743e55534d5d9620e095089d176d55cb22d5322f2afd/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a26f18905b8dd5d685d6d07b0cdf98a79f3c7a918906af7cc143ea2e164c8bc", size = 154465, upload-time = "2025-10-14T04:41:28.322Z" }, + { url = "https://files.pythonhosted.org/packages/89/66/c7a9e1b7429be72123441bfdbaf2bc13faab3f90b933f664db506dea5915/charset_normalizer-3.4.4-cp313-cp313-win32.whl", hash = "sha256:9b35f4c90079ff2e2edc5b26c0c77925e5d2d255c42c74fdb70fb49b172726ac", size = 99404, upload-time = "2025-10-14T04:41:29.95Z" }, + { url = "https://files.pythonhosted.org/packages/c4/26/b9924fa27db384bdcd97ab83b4f0a8058d96ad9626ead570674d5e737d90/charset_normalizer-3.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14", size = 107092, upload-time = "2025-10-14T04:41:31.188Z" }, + { url = "https://files.pythonhosted.org/packages/af/8f/3ed4bfa0c0c72a7ca17f0380cd9e4dd842b09f664e780c13cff1dcf2ef1b/charset_normalizer-3.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:542d2cee80be6f80247095cc36c418f7bddd14f4a6de45af91dfad36d817bba2", size = 100408, upload-time = "2025-10-14T04:41:32.624Z" }, + { url = "https://files.pythonhosted.org/packages/2a/35/7051599bd493e62411d6ede36fd5af83a38f37c4767b92884df7301db25d/charset_normalizer-3.4.4-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd", size = 207746, upload-time = "2025-10-14T04:41:33.773Z" }, + { url = "https://files.pythonhosted.org/packages/10/9a/97c8d48ef10d6cd4fcead2415523221624bf58bcf68a802721a6bc807c8f/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb", size = 147889, upload-time = "2025-10-14T04:41:34.897Z" }, + { url = "https://files.pythonhosted.org/packages/10/bf/979224a919a1b606c82bd2c5fa49b5c6d5727aa47b4312bb27b1734f53cd/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e", size = 143641, upload-time = "2025-10-14T04:41:36.116Z" }, + { url = "https://files.pythonhosted.org/packages/ba/33/0ad65587441fc730dc7bd90e9716b30b4702dc7b617e6ba4997dc8651495/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:752944c7ffbfdd10c074dc58ec2d5a8a4cd9493b314d367c14d24c17684ddd14", size = 160779, upload-time = "2025-10-14T04:41:37.229Z" }, + { url = "https://files.pythonhosted.org/packages/67/ed/331d6b249259ee71ddea93f6f2f0a56cfebd46938bde6fcc6f7b9a3d0e09/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1f13550535ad8cff21b8d757a3257963e951d96e20ec82ab44bc64aeb62a191", size = 159035, upload-time = "2025-10-14T04:41:38.368Z" }, + { url = "https://files.pythonhosted.org/packages/67/ff/f6b948ca32e4f2a4576aa129d8bed61f2e0543bf9f5f2b7fc3758ed005c9/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ecaae4149d99b1c9e7b88bb03e3221956f68fd6d50be2ef061b2381b61d20838", size = 152542, upload-time = "2025-10-14T04:41:39.862Z" }, + { url = "https://files.pythonhosted.org/packages/16/85/276033dcbcc369eb176594de22728541a925b2632f9716428c851b149e83/charset_normalizer-3.4.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cb6254dc36b47a990e59e1068afacdcd02958bdcce30bb50cc1700a8b9d624a6", size = 149524, upload-time = "2025-10-14T04:41:41.319Z" }, + { url = "https://files.pythonhosted.org/packages/9e/f2/6a2a1f722b6aba37050e626530a46a68f74e63683947a8acff92569f979a/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c8ae8a0f02f57a6e61203a31428fa1d677cbe50c93622b4149d5c0f319c1d19e", size = 150395, upload-time = "2025-10-14T04:41:42.539Z" }, + { url = "https://files.pythonhosted.org/packages/60/bb/2186cb2f2bbaea6338cad15ce23a67f9b0672929744381e28b0592676824/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:47cc91b2f4dd2833fddaedd2893006b0106129d4b94fdb6af1f4ce5a9965577c", size = 143680, upload-time = "2025-10-14T04:41:43.661Z" }, + { url = "https://files.pythonhosted.org/packages/7d/a5/bf6f13b772fbb2a90360eb620d52ed8f796f3c5caee8398c3b2eb7b1c60d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:82004af6c302b5d3ab2cfc4cc5f29db16123b1a8417f2e25f9066f91d4411090", size = 162045, upload-time = "2025-10-14T04:41:44.821Z" }, + { url = "https://files.pythonhosted.org/packages/df/c5/d1be898bf0dc3ef9030c3825e5d3b83f2c528d207d246cbabe245966808d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b7d8f6c26245217bd2ad053761201e9f9680f8ce52f0fcd8d0755aeae5b2152", size = 149687, upload-time = "2025-10-14T04:41:46.442Z" }, + { url = "https://files.pythonhosted.org/packages/a5/42/90c1f7b9341eef50c8a1cb3f098ac43b0508413f33affd762855f67a410e/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:799a7a5e4fb2d5898c60b640fd4981d6a25f1c11790935a44ce38c54e985f828", size = 160014, upload-time = "2025-10-14T04:41:47.631Z" }, + { url = "https://files.pythonhosted.org/packages/76/be/4d3ee471e8145d12795ab655ece37baed0929462a86e72372fd25859047c/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:99ae2cffebb06e6c22bdc25801d7b30f503cc87dbd283479e7b606f70aff57ec", size = 154044, upload-time = "2025-10-14T04:41:48.81Z" }, + { url = "https://files.pythonhosted.org/packages/b0/6f/8f7af07237c34a1defe7defc565a9bc1807762f672c0fde711a4b22bf9c0/charset_normalizer-3.4.4-cp314-cp314-win32.whl", hash = "sha256:f9d332f8c2a2fcbffe1378594431458ddbef721c1769d78e2cbc06280d8155f9", size = 99940, upload-time = "2025-10-14T04:41:49.946Z" }, + { url = "https://files.pythonhosted.org/packages/4b/51/8ade005e5ca5b0d80fb4aff72a3775b325bdc3d27408c8113811a7cbe640/charset_normalizer-3.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:8a6562c3700cce886c5be75ade4a5db4214fda19fede41d9792d100288d8f94c", size = 107104, upload-time = "2025-10-14T04:41:51.051Z" }, + { url = "https://files.pythonhosted.org/packages/da/5f/6b8f83a55bb8278772c5ae54a577f3099025f9ade59d0136ac24a0df4bde/charset_normalizer-3.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:de00632ca48df9daf77a2c65a484531649261ec9f25489917f09e455cb09ddb2", size = 100743, upload-time = "2025-10-14T04:41:52.122Z" }, + { url = "https://files.pythonhosted.org/packages/0a/4c/925909008ed5a988ccbb72dcc897407e5d6d3bd72410d69e051fc0c14647/charset_normalizer-3.4.4-py3-none-any.whl", hash = "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f", size = 53402, upload-time = "2025-10-14T04:42:31.76Z" }, +] + +[[package]] +name = "ckzg" +version = "2.1.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b1/e8/b262fff67d6bcaecd19c71d19ebea9184a1204e00368664e1544a2511bd8/ckzg-2.1.5.tar.gz", hash = "sha256:e48e092f9b89ebb6aaa195de2e2bb72ad2d4b35c87d3a15e4545f13c51fbbe30", size = 1123745, upload-time = "2025-09-30T19:09:13.391Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9b/47/c52e96b5c3476524c24a8ac99002590b0dc700618e8c9ed52bfcba1acda7/ckzg-2.1.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:49ee4c830de89764bfd9e8188446f3020f14d32bd4486fcbc5a4a5afad775ac0", size = 116307, upload-time = "2025-09-30T19:07:35.486Z" }, + { url = "https://files.pythonhosted.org/packages/b5/71/a136af12f4354cd7533f52f0b5df75431824926b5cbeb5160684a1390ae9/ckzg-2.1.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:3b4f0c6c2f1a629d4d64e900c65633595c63d208001d588c61b6c8bc1b189dec", size = 99814, upload-time = "2025-09-30T19:07:36.882Z" }, + { url = "https://files.pythonhosted.org/packages/91/55/92ccf278a120de2f8433044bc591615165336ff297ae2c3ba723572c23be/ckzg-2.1.5-cp310-cp310-manylinux2010_i686.manylinux_2_12_i686.manylinux_2_28_i686.whl", hash = "sha256:10c8bc524267a40fe7c4fabd4c23f131ea18fcabd6016cdc4ddcb95cc757faf5", size = 179522, upload-time = "2025-11-06T21:05:26.357Z" }, + { url = "https://files.pythonhosted.org/packages/e5/bd/381970acf6b2425923b1f5ce01194330f88997584dedfd7fdba699fd1109/ckzg-2.1.5-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8ea589e60db460ee9ebb678f20e74cc9289e912ccad66693b3263459933aaffc", size = 165174, upload-time = "2025-11-06T21:05:28.723Z" }, + { url = "https://files.pythonhosted.org/packages/fe/9b/d5cb656245b239fe7d326670094ecbe16b123f15cbc095fcc9f795523505/ckzg-2.1.5-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:97769b53f7d8c46e794d5c8aa609a4c00ec1fb050e69b6833b45dbb23a7b6501", size = 174752, upload-time = "2025-11-06T21:05:29.846Z" }, + { url = "https://files.pythonhosted.org/packages/4a/9d/97f403cb2c93abcb9f522a3b575cfd0ee9961398e23967e4ff5570844186/ckzg-2.1.5-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a45aaea4a42babea48bb27e387fb209f2aaaaaa16abea25a4a92a056b616f9af", size = 175697, upload-time = "2025-09-30T19:07:37.688Z" }, + { url = "https://files.pythonhosted.org/packages/79/45/a5a414672a1daba079e43e4fb2b11d53a681b0934475f4eb32cb7babf633/ckzg-2.1.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:060562273057911c39a1491e9b76055c095c10cfff1704ed70011e38b53f83d8", size = 160993, upload-time = "2025-09-30T19:07:38.561Z" }, + { url = "https://files.pythonhosted.org/packages/6f/4b/54eb12939012294b47e5b112c1298f4fdbdbfded213926e5d404c4eae8dc/ckzg-2.1.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f12a90277b17e1cb5c326c5c261dad2ebb14a7136e754593e3a0a92c94799fc1", size = 170357, upload-time = "2025-09-30T19:07:39.77Z" }, + { url = "https://files.pythonhosted.org/packages/fc/24/6b202537c55b713a794b27f23749e569c7e4f4b9e204226edb49d5e622e1/ckzg-2.1.5-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:084f284d842b0a51befb2b595bf45c9c623ee3713c12500ceee9dcd05b24d14d", size = 172736, upload-time = "2025-09-30T19:07:40.635Z" }, + { url = "https://files.pythonhosted.org/packages/53/53/df9bd835bab4edc7c030f5fc97285109d917753146d06da0a49d4982764e/ckzg-2.1.5-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:7d42760b353c5d4a0f0d70a3161c1db75e22f4529fad4cef2228be1b8cd2d579", size = 187984, upload-time = "2025-09-30T19:07:41.752Z" }, + { url = "https://files.pythonhosted.org/packages/f9/b6/2a319f43ceae927217949a266a9794a427aeaebcdd463fadcf3f7297ed6c/ckzg-2.1.5-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:0c547c0c61d2087f70170898948cad4d0e4583a7e25b24fdf247a426066b47bc", size = 182390, upload-time = "2025-09-30T19:07:42.579Z" }, + { url = "https://files.pythonhosted.org/packages/c8/a5/48aaa7d84f061ec1ed0c6a20306304cc4186d3191b388c33e47548eda309/ckzg-2.1.5-cp310-cp310-win_amd64.whl", hash = "sha256:2b7ef12896e2afff613f058e3bc8e3478ff626ae8a6f2d3200950304a536935f", size = 100964, upload-time = "2025-09-30T19:07:43.42Z" }, + { url = "https://files.pythonhosted.org/packages/1e/32/d82185ecd05a91d1a35229c587eac9c518b30693c4c983ffde37e1f5a1a2/ckzg-2.1.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cead4ba760a49eaa4d7a50a0483aad9727d6103fc00c408aef15f2cd8f8dec7b", size = 116304, upload-time = "2025-09-30T19:07:44.531Z" }, + { url = "https://files.pythonhosted.org/packages/94/a7/d22f813e032a7380f758d437348d791e8609a5c8ef2bd3654201f85a0047/ckzg-2.1.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3156983ba598fa05f0136325125e75197e4cf24ded255aaa6ace068cede92932", size = 99811, upload-time = "2025-09-30T19:07:45.311Z" }, + { url = "https://files.pythonhosted.org/packages/9e/2d/96259b64512ab9876ec3d32527148179ce02d2b052974c80435316444f21/ckzg-2.1.5-cp311-cp311-manylinux2010_i686.manylinux_2_12_i686.manylinux_2_28_i686.whl", hash = "sha256:cac601a9690f133dd9d8e85f7a96578496427d42cdea771e0e07785b1cbbe9dc", size = 180251, upload-time = "2025-11-06T21:05:31.241Z" }, + { url = "https://files.pythonhosted.org/packages/e0/b0/020bf03e7b276775aa5d8a90605dfa6b4f23f2ae515142aa788389e6b68f/ckzg-2.1.5-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:05860f1477880376106a6934becdcb3a2c6330fc2386fed0d7e8f3b0ce5df81c", size = 165942, upload-time = "2025-11-06T21:05:32.634Z" }, + { url = "https://files.pythonhosted.org/packages/8b/56/01c8633366e61ab10c7fbc15b918a0ebec8108c711a250a13c6950e5e892/ckzg-2.1.5-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:92b18b0ec177b9e2b4238936a8bffcfdaee7626a58f8d0c7c2ac554b8a05c9b6", size = 175509, upload-time = "2025-11-06T21:05:33.847Z" }, + { url = "https://files.pythonhosted.org/packages/41/62/82ee6852a629bd9a783fb7787bcc2ee6e8c00c26b4ceb821a17484081760/ckzg-2.1.5-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d05e2c9466b2a4214dc19da35ea4cae636e033f3434768b982d37317a0f9c520", size = 176458, upload-time = "2025-09-30T19:07:46.433Z" }, + { url = "https://files.pythonhosted.org/packages/6d/00/5c68bb77f12eab6ba464f3955263e515ae988ddfc4bc57023b54b60feda0/ckzg-2.1.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c754bbc253cfce8814d633f135be4891e6f83a50125f418fee01323ba306f59a", size = 161841, upload-time = "2025-09-30T19:07:47.261Z" }, + { url = "https://files.pythonhosted.org/packages/42/b3/c75079d270a7895ba6b5a95f86674d5c95f2f7e0db03328f1b21dc9a90eb/ckzg-2.1.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc2b766d4aed52c8c717322f2af935da0b916bf59fbba771adb822499b45e491", size = 171101, upload-time = "2025-09-30T19:07:48.471Z" }, + { url = "https://files.pythonhosted.org/packages/63/bf/81c533231f3bdc0029a0ec8abef9c24b2eaf929a044a77c15b6da54b91dc/ckzg-2.1.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:dd7a296475baa5f20b5e7972448d4cb2f44d00b920d680de756c90c512af1c3b", size = 173403, upload-time = "2025-09-30T19:07:49.288Z" }, + { url = "https://files.pythonhosted.org/packages/58/6e/bbfd04b25185c6371020dd1f1c3eb0557e36878ad6cdd78a1ea2bb47d8df/ckzg-2.1.5-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:97d3e93b3d94031fbd376005d86bf9b2c230ecfb4a4f4ced3b06b4aeefae6c9f", size = 188738, upload-time = "2025-09-30T19:07:50.64Z" }, + { url = "https://files.pythonhosted.org/packages/3c/83/ab3cd495a2b37b72b4d886276b3669f101c4927b1d54b7e528aa537362ab/ckzg-2.1.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3d2d35ba937f002b72a9a168696d0073a8e5912fa7058e77a06c370b86586401", size = 183202, upload-time = "2025-09-30T19:07:51.544Z" }, + { url = "https://files.pythonhosted.org/packages/19/37/4ad60c2879b5e6988337b776f580b7aacb40ae6c05ef264e9835bed35e80/ckzg-2.1.5-cp311-cp311-win_amd64.whl", hash = "sha256:ce2047071353ee099d44aa6575974648663204eb9b42354bfa5ac6f9b8fb63e9", size = 100965, upload-time = "2025-09-30T19:07:52.437Z" }, + { url = "https://files.pythonhosted.org/packages/dd/9f/3ef8acd201e4d098af6bc368991ac1469a5390399abd1e78307fffb65218/ckzg-2.1.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:edead535bd9afef27b8650bba09659debd4f52638aee5ec1ab7d2c9d7e86953c", size = 116333, upload-time = "2025-09-30T19:07:53.223Z" }, + { url = "https://files.pythonhosted.org/packages/25/c2/202947c143336185180216a4939296d824cbffca4e1438d0fe696daf1904/ckzg-2.1.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:dc78622855de3d47767cdeecfdf58fd58911f43a0fa783524e414b7e75149020", size = 99822, upload-time = "2025-09-30T19:07:54.06Z" }, + { url = "https://files.pythonhosted.org/packages/0e/45/d720181bc2445340b9108a55c9e91a23a10e4eeb6c091588e550b0a28a54/ckzg-2.1.5-cp312-cp312-manylinux2010_i686.manylinux_2_12_i686.manylinux_2_28_i686.whl", hash = "sha256:e5639064b0dd147b73f2ce2c2506844b0c625b232396ac852dc52eced04bd529", size = 180441, upload-time = "2025-11-06T21:05:34.937Z" }, + { url = "https://files.pythonhosted.org/packages/ad/91/467ff00f3ec3d97d14b9e31789904107a907dca7526eb003e218be8038d1/ckzg-2.1.5-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eb0864813902b96cde171e65334ce8d13c5ff5b6855f2e71a2272ae268fa07e8", size = 166199, upload-time = "2025-11-06T21:05:36.497Z" }, + { url = "https://files.pythonhosted.org/packages/c4/8b/1148f4edbd252386e59d8c73670caa3138991292656cf84bb584ebb0e113/ckzg-2.1.5-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3e6f13f673a24c01e681eb66aed8f8e4ce191f009dd2149f3e1b9ad0dd59b4cd", size = 175829, upload-time = "2025-11-06T21:05:37.971Z" }, + { url = "https://files.pythonhosted.org/packages/ac/20/ace67811fbabcfece937f8286cdd96f5668757b8944a74630b6454131545/ckzg-2.1.5-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:094add5f197a3d278924ec1480d258f3b8b0e9f8851ae409eec83a21a738bffe", size = 176595, upload-time = "2025-09-30T19:07:54.792Z" }, + { url = "https://files.pythonhosted.org/packages/f1/65/127fa59aae21688887249ec1caa92dabaced331de5cb4e0224216270c3d0/ckzg-2.1.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2b4b05f798784400e8c4dedaf1a1d57bbbc54de790855855add876fff3c9f629", size = 162014, upload-time = "2025-09-30T19:07:55.776Z" }, + { url = "https://files.pythonhosted.org/packages/35/de/dcaa260f6f5aca83eb9017ea0c691d3d37458e08e24dcad5efcd348d807e/ckzg-2.1.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64aef50a1cf599041b9af018bc885a3fad6a20bbaf443fc45f0457cb47914610", size = 171396, upload-time = "2025-09-30T19:07:56.583Z" }, + { url = "https://files.pythonhosted.org/packages/c4/72/f87db164d687759ae0666a2188c5f5d11a62cac9093464efbedc1f69f4e1/ckzg-2.1.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0171484eedc42b9417a79e33aff3f35d48915b01c54f42c829b891947ac06551", size = 173548, upload-time = "2025-09-30T19:07:58.555Z" }, + { url = "https://files.pythonhosted.org/packages/03/ad/b5a88a445f27dbd39eece56edffbe986bf356003bded75f79ef59e2b37c9/ckzg-2.1.5-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:2342b98acd7b6e6e33fbbc48ccec9093e1652461daf4353115adcd708498efcd", size = 188988, upload-time = "2025-09-30T19:07:59.496Z" }, + { url = "https://files.pythonhosted.org/packages/6e/57/42fbf29d39bd3f11a673a4e61af41b5485aa0ecf99473a0d4afc2528d24b/ckzg-2.1.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:cbce75c1e17fa60b5c33bae5069b8533cf5a4d028ef7d1f755b14a16f72307cf", size = 183513, upload-time = "2025-09-30T19:08:00.341Z" }, + { url = "https://files.pythonhosted.org/packages/27/c0/ef4c9e9256088e5a425cedb80f26e2a0c853128571b027d8174caf97b2f6/ckzg-2.1.5-cp312-cp312-win_amd64.whl", hash = "sha256:827be2aeffc8a10bfb39b8dad45def82164dfcde735818c4053f5064474ae1b4", size = 100992, upload-time = "2025-09-30T19:08:01.633Z" }, + { url = "https://files.pythonhosted.org/packages/ba/4b/089392b6f0015bb368b453f26330c643bf0087f77835df2328a1da2af401/ckzg-2.1.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0d955f4e18bb9a9b3a6f55114052edd41650c29edd5f81e417c8f01abace8207", size = 116340, upload-time = "2025-09-30T19:08:02.478Z" }, + { url = "https://files.pythonhosted.org/packages/bb/45/4d8b70f69f0bc67e9262ec68200707d2d92a27e712cda2c163ebd4b4dcfa/ckzg-2.1.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0c0961a685761196264aa49b1cf06e8a2b2add4d57987853d7dd7a7240dc5de7", size = 99822, upload-time = "2025-09-30T19:08:03.65Z" }, + { url = "https://files.pythonhosted.org/packages/49/f0/1e03c6a491899264117a5a80670a26a569f9eeb67c723157891141d1646f/ckzg-2.1.5-cp313-cp313-manylinux2010_i686.manylinux_2_12_i686.manylinux_2_28_i686.whl", hash = "sha256:026ef3bba0637032c21f6bdb8e92aefeae7c67003bf631a4ee80c515a36a9dbd", size = 180443, upload-time = "2025-11-06T21:05:39.2Z" }, + { url = "https://files.pythonhosted.org/packages/60/f2/b85b5e5fee12d4ea13060066e9b50260f747a0a5db23634dc199e742894f/ckzg-2.1.5-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bf031139a86e4ff00a717f9539331ef148ae9013b58848f2a7ac14596d812915", size = 166248, upload-time = "2025-11-06T21:05:40.384Z" }, + { url = "https://files.pythonhosted.org/packages/1c/41/07c5c7471d70d9cc49f2ce5013bb174529f2184611478d176c88c2fa048f/ckzg-2.1.5-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f51339d58541ae450c78a509b32822eec643595d8b96949fb1963fba802dc78b", size = 175870, upload-time = "2025-11-06T21:05:41.495Z" }, + { url = "https://files.pythonhosted.org/packages/c4/95/4193e4af65dc4839fa9fe07efad689fe726303b3ba62ee2f46c403458bec/ckzg-2.1.5-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:badb1c7dc6b932bed2c3f7695e1ce3e4bcc9601706136957408ac2bde5dd0892", size = 176586, upload-time = "2025-09-30T19:08:04.818Z" }, + { url = "https://files.pythonhosted.org/packages/7d/9e/850f48cb41685f5016028dbde8f7846ce9c56bfdc2e9e0f3df1a975263fe/ckzg-2.1.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:58d92816b9babaee87bd9f23be10c07d5d07c709be184aa7ea08ddb2bcf2541c", size = 161970, upload-time = "2025-09-30T19:08:05.734Z" }, + { url = "https://files.pythonhosted.org/packages/ca/df/a9993dc124e95eb30059c108efd83a1504709cf069d3bee0745d450262a0/ckzg-2.1.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8cf39f9abe8b3f1a71188fb601a8589672ee40eb0671fc36d8cdf4e78f00f43f", size = 171364, upload-time = "2025-09-30T19:08:06.979Z" }, + { url = "https://files.pythonhosted.org/packages/f9/03/78e8a723c1b832766e5698f7b39cc8dc27da95b62bc5c738a59564cb5f2c/ckzg-2.1.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:999df675674d8d31528fd9b9afd548e86decc86447f5555b451237e7953fd63f", size = 173571, upload-time = "2025-09-30T19:08:08.173Z" }, + { url = "https://files.pythonhosted.org/packages/e3/64/27f96201c6d78fbdb9a0812cf45dded974c4d03d876dac11d9c764ef858f/ckzg-2.1.5-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:c39a1c7b32ac345cc44046076fd069ad6b7e6f7bef230ef9be414c712c4453b8", size = 189014, upload-time = "2025-09-30T19:08:09.045Z" }, + { url = "https://files.pythonhosted.org/packages/d2/6e/82177c4530265694f7ec151821c79351a07706dda4d8b23e8b37d0c122f0/ckzg-2.1.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e4564765b0cc65929eca057241b9c030afac1dbae015f129cb60ca6abd6ff620", size = 183530, upload-time = "2025-09-30T19:08:09.867Z" }, + { url = "https://files.pythonhosted.org/packages/4d/41/1edfbd007b0398321defeedf6ad2d9f86a73f6a99d5ca4b4944bf6f2d757/ckzg-2.1.5-cp313-cp313-win_amd64.whl", hash = "sha256:55013b36514b8176197655b929bc53f020aa51a144331720dead2efc3793ed85", size = 100992, upload-time = "2025-09-30T19:08:10.719Z" }, + { url = "https://files.pythonhosted.org/packages/8f/07/6ac017fc1593ea8059de1271825eab1f55d0a2f2127e811d5597cc0f328e/ckzg-2.1.5-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:a0cab7deaed093898a92d3644d4ca8621b63cb49296833e2d8b3edac456656d5", size = 116524, upload-time = "2025-11-06T21:05:42.614Z" }, + { url = "https://files.pythonhosted.org/packages/cc/57/c08133d854dad59d1052ad11796a1c6326c87363049feb8848ee291e68ba/ckzg-2.1.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:caedc9eba3d28584be9b6051585f20745f6abfec0d0657cce3dd45edb7f28586", size = 99833, upload-time = "2025-11-06T21:05:43.647Z" }, + { url = "https://files.pythonhosted.org/packages/df/80/b07dc3a7581e202dd871a53d8ff65eb70beace3cd81f17e587c3bac64c42/ckzg-2.1.5-cp314-cp314-manylinux2010_i686.manylinux_2_12_i686.manylinux_2_28_i686.whl", hash = "sha256:2f67e545d41ba960189b1011d078953311259674620c485e619c933494b88fd9", size = 180474, upload-time = "2025-11-06T21:05:44.734Z" }, + { url = "https://files.pythonhosted.org/packages/e2/38/eaa3d40cf5c886966cb32b987f45d6fe07fded3ec2a731b71ca320574849/ckzg-2.1.5-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d6f65ff296033c259d0829093d2c55bb45651e001e0269b8b88d072fdc86ecc6", size = 166274, upload-time = "2025-11-06T21:05:45.882Z" }, + { url = "https://files.pythonhosted.org/packages/7f/74/a878da70ea299f75c0f279b01bfc46101893a1cc827ead5d5df661ff209a/ckzg-2.1.5-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0d66d34ff33be94c8a1f0da86483cd5bfdc15842998f3654ed91b8fdbffa2a81", size = 175904, upload-time = "2025-11-06T21:05:47.039Z" }, + { url = "https://files.pythonhosted.org/packages/bb/6f/72029116643f22b70adeb622ead6137af5d504f74f064d08397e972648dc/ckzg-2.1.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:25cf954bae3e2b2db6fa5e811d9800f89199d3eb4fa906c96a1c03434d4893c9", size = 173641, upload-time = "2025-11-06T21:05:48.147Z" }, + { url = "https://files.pythonhosted.org/packages/3c/67/a618cb1a7b48a810d7dbeeec282ec4337d872111fbdaded2630c224e6566/ckzg-2.1.5-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:34d7128735e0bcfcac876bff47d0f85e674f1e24f99014e326ec266abed7a82c", size = 189020, upload-time = "2025-11-06T21:05:49.215Z" }, + { url = "https://files.pythonhosted.org/packages/19/3b/417f0c9a8b40a2876c70384f19fe63289214a6f1480bc86e3a3beaf21b6b/ckzg-2.1.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:1dec3efae8679f7b8e26263b8bb0d3061ef4c9c6fe395e55b71f8f0df90ca8a0", size = 183519, upload-time = "2025-11-06T21:05:50.542Z" }, + { url = "https://files.pythonhosted.org/packages/81/77/5b1c3d31adf65040e52e77f13e38e89707a2ac46e0ca0ecf881a68833944/ckzg-2.1.5-cp314-cp314-win_amd64.whl", hash = "sha256:ce37c0ee0effe55d4ceed1735a2d85a3556a86238f3c89b7b7d1ca4ce4e92358", size = 104038, upload-time = "2025-11-06T21:05:51.677Z" }, + { url = "https://files.pythonhosted.org/packages/d9/fc/5ebcd1d75513e270440f4517a7423c496c0d025bf730da12c7c8693932c9/ckzg-2.1.5-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:db804d27f4b08e3aea440cdc6558af4ceb8256b18ea2b83681d80cc654a4085b", size = 116740, upload-time = "2025-11-06T21:05:52.767Z" }, + { url = "https://files.pythonhosted.org/packages/ad/2e/b661f589b8cdc586304c7a88cc58d48ca34a28200659e1222ffec8a58994/ckzg-2.1.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:d472e3beeb95a110275b4d27e51d1c2b26ab99ddb91ac1c5587d710080c39c5e", size = 100101, upload-time = "2025-11-06T21:05:54.007Z" }, + { url = "https://files.pythonhosted.org/packages/34/3f/88544854ca9623433aba919d85db5f2a3c190922eb7e96bf151b35273c79/ckzg-2.1.5-cp314-cp314t-manylinux2010_i686.manylinux_2_12_i686.manylinux_2_28_i686.whl", hash = "sha256:4b44a018124a79138fab8fde25221083574c181c324519be51eab09b1e43ae27", size = 183321, upload-time = "2025-11-06T21:05:55.085Z" }, + { url = "https://files.pythonhosted.org/packages/0a/11/b9dd3ea012bd215d2aff8e49953e8fe57e62c962eb1e2717663fab5bdc6a/ckzg-2.1.5-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6a91d7b444300cf8ecae4f55983726630530cdde15cab92023026230a30d094e", size = 169404, upload-time = "2025-11-06T21:05:56.212Z" }, + { url = "https://files.pythonhosted.org/packages/cf/cf/d695acc82fc7386b65833b2bcfe5b312070f9eb58ae7c5bdfcad7f8e460d/ckzg-2.1.5-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8674c64efbf2a12edf6d776061847bbe182997737e7690a69af932ce61a9c2a", size = 178676, upload-time = "2025-11-06T21:05:57.528Z" }, + { url = "https://files.pythonhosted.org/packages/82/35/9319f1d8a8aa2ae9a7779bf6d49a46e6e2af481178eaabbca1ea9d8f9072/ckzg-2.1.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:4290aa17c6402c98f16017fd6ee0bff8aeb5c97be5c3cee7c72aea1b7d176f3a", size = 176309, upload-time = "2025-11-06T21:05:59.047Z" }, + { url = "https://files.pythonhosted.org/packages/b9/24/e28206e43160f411d3ae53f2e557c1905af2928854f7ce4a1be1af893915/ckzg-2.1.5-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:a0f82b8958ea97df12e29094f0a672cbe7532399724ea61b2399545991ed6017", size = 191777, upload-time = "2025-11-06T21:06:00.456Z" }, + { url = "https://files.pythonhosted.org/packages/aa/ae/51b4e2575d1b4ab76433c6ef56d4dfc1bad38c2f7ffb33353e271c4e4d05/ckzg-2.1.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:22300bf0d717a083c388de5cfafec08443c9938b3abde2e89f9d5d1fffde1c51", size = 186138, upload-time = "2025-11-06T21:06:01.684Z" }, + { url = "https://files.pythonhosted.org/packages/fe/6e/8ea848be3043b6bf9a7761492719a8c2d2c17a3da7b9551be7ec88a52c01/ckzg-2.1.5-cp314-cp314t-win_amd64.whl", hash = "sha256:aa8228206c3e3729fc117ca38e27588c079b0928a5ab628ee4d9fccaa2b8467d", size = 104191, upload-time = "2025-11-06T21:06:03.188Z" }, + { url = "https://files.pythonhosted.org/packages/a4/7a/9f7534c99f79465ac751b6f9b9445f90f6a0d5ab990c630d888cc9792386/ckzg-2.1.5-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:65960cf6feaf1b281af76efdfedecc536f52e47ec3982c1a2c58c0d1b36a391b", size = 113098, upload-time = "2025-09-30T19:08:43.849Z" }, + { url = "https://files.pythonhosted.org/packages/8a/d6/8beb84f852cd8a30441d9dfce66679463cfbcb59386b474f6406f12ff979/ckzg-2.1.5-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:f97d29d2ef0bfd4ad4ded126a514ced89c30ee1a30dc5d20d68918263b883c23", size = 95813, upload-time = "2025-09-30T19:08:44.767Z" }, + { url = "https://files.pythonhosted.org/packages/40/30/4179c4933607e4b233e50676c3d8228234fa543467f801daa04dd8f08dca/ckzg-2.1.5-pp310-pypy310_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3560a4dcd50f3b3a289c8b73657b239bbc6461eb9aa6ef5fe81a242f70591ff4", size = 126632, upload-time = "2025-09-30T19:08:45.55Z" }, + { url = "https://files.pythonhosted.org/packages/c9/4e/c535ed740c6c949c9382d3c22253d83adf8600612358434f365bc5178c77/ckzg-2.1.5-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:13150a17d6aa76eb39d301440c02e5395540796d30e4d9ae30724ce191c50a28", size = 102843, upload-time = "2025-09-30T19:08:46.915Z" }, + { url = "https://files.pythonhosted.org/packages/46/ff/efd63e7d9bf6651cbdd68cb28cae40bedeae3bdb8e32d4b7f73fe4ca48a2/ckzg-2.1.5-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:242e77af051028e4388df7c1ebc68897cf630cf80745f7b26ff0eb6e3ec7a78d", size = 111740, upload-time = "2025-09-30T19:08:47.916Z" }, + { url = "https://files.pythonhosted.org/packages/22/71/18e05c16a2b7cb74c3ef4a96bee7a1d58b8b3be59db3f1b49a54f292b0b7/ckzg-2.1.5-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:a77975d10c17f617d3a43d664d0f74eb342b2cb3deb9f20860e2e2aaa24643c1", size = 101017, upload-time = "2025-09-30T19:08:49.099Z" }, + { url = "https://files.pythonhosted.org/packages/03/bd/4cc75266991d2420c3212614dc6e659490bcb8c1a03a638ff688cc775ff8/ckzg-2.1.5-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0ae260c705a82d9cf4b88eaa2e8f86263c23d99d4ec282f22838f27d24f9306c", size = 113098, upload-time = "2025-09-30T19:08:49.956Z" }, + { url = "https://files.pythonhosted.org/packages/d6/4c/fc5e6a3463b8d8bb36d21230f41fee13d5ba3fe3f594f1c20bb9bab1fab3/ckzg-2.1.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:7c21b0a4ad05a9e32e715118695d7a0912b4ee73198d63cc98de4d585597627e", size = 95811, upload-time = "2025-09-30T19:08:50.883Z" }, + { url = "https://files.pythonhosted.org/packages/6d/2e/3be3977c57a51f516f2897d543ddca7901659ae1705b5dc3dbf54e0b66f2/ckzg-2.1.5-pp311-pypy311_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c0577aee9848d7a9cef750ff6f303f586caf33da986a762ca57ac0c57e59fb6d", size = 126633, upload-time = "2025-09-30T19:08:51.734Z" }, + { url = "https://files.pythonhosted.org/packages/72/3b/8314ca493654bd035cc0a66308cec9a1089cf3bc33c2837e3e265345f3cc/ckzg-2.1.5-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e15faa1145b2408e17e3b2f0b159de325b0198615aa30268bb6cd8f4385ed745", size = 102844, upload-time = "2025-09-30T19:08:52.621Z" }, + { url = "https://files.pythonhosted.org/packages/f4/9b/1729ee420865a71e68f18880341b37ef980c2881674dc0b06460253ef25e/ckzg-2.1.5-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:46d009dba9838630183610008a81bd80aafb389d45d8293d7a2fff7a5ea82266", size = 111739, upload-time = "2025-09-30T19:08:53.54Z" }, + { url = "https://files.pythonhosted.org/packages/40/40/f259e2bf986d39717427bc12baa8189cd43f9675e81cd3bcab639e593614/ckzg-2.1.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:df66d2be54d91f74aded4ceb71e7b1f789e2636a3015f438904a22ec9de750f1", size = 101018, upload-time = "2025-09-30T19:08:54.391Z" }, +] + +[[package]] +name = "click" +version = "8.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3d/fa/656b739db8587d7b5dfa22e22ed02566950fbfbcdc20311993483657a5c0/click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a", size = 295065, upload-time = "2025-11-15T20:45:42.706Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6", size = 108274, upload-time = "2025-11-15T20:45:41.139Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "construct" +version = "2.10.70" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/02/77/8c84b98eca70d245a2a956452f21d57930d22ab88cbeed9290ca630cf03f/construct-2.10.70.tar.gz", hash = "sha256:4d2472f9684731e58cc9c56c463be63baa1447d674e0d66aeb5627b22f512c29", size = 86337, upload-time = "2023-11-29T08:44:49.545Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b2/fb/08b3f4bf05da99aba8ffea52a558758def16e8516bc75ca94ff73587e7d3/construct-2.10.70-py3-none-any.whl", hash = "sha256:c80be81ef595a1a821ec69dc16099550ed22197615f4320b57cc9ce2a672cb30", size = 63020, upload-time = "2023-11-29T08:44:46.876Z" }, +] + +[[package]] +name = "construct-typing" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "construct" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f6/ae/659fe4866d89ef5a3a65cddbdd7b35882f4feb72db383821965f2fcea934/construct_typing-0.7.0.tar.gz", hash = "sha256:71d110dedff39bd3b603c734077032a7065bc597a49db1f5b03a211d05dbac23", size = 45104, upload-time = "2025-10-27T19:30:29.614Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8c/0c/2db6f7e1ae9795e436c6a0dc0bc38b12b8c8a228cb63203e24190b755b3b/construct_typing-0.7.0-py3-none-any.whl", hash = "sha256:c92383c6e8e5d07ba25811c8d5163820458d821e73bb1006541f43f89788646c", size = 24350, upload-time = "2025-10-27T19:30:27.505Z" }, +] + +[[package]] +name = "cytoolz" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "toolz" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bd/d4/16916f3dc20a3f5455b63c35dcb260b3716f59ce27a93586804e70e431d5/cytoolz-1.1.0.tar.gz", hash = "sha256:13a7bf254c3c0d28b12e2290b82aed0f0977a4c2a2bf84854fcdc7796a29f3b0", size = 642510, upload-time = "2025-10-19T00:44:56.174Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/7a/3244e6e3587be9abfee3b1c320e43a279831b3c3a31fe5d08c1ee6193e6b/cytoolz-1.1.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:72d7043a88ea5e61ba9d17ea0d1c1eff10f645d7edfcc4e56a31ef78be287644", size = 1307813, upload-time = "2025-10-19T00:39:34.198Z" }, + { url = "https://files.pythonhosted.org/packages/32/7e/eaf504ca59addce323ef4d4ffedc2913d83c121ec19f6419bc402f7702dc/cytoolz-1.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d759e9ed421bacfeb456d47af8d734c057b9912b5f2441f95b27ca35e5efab07", size = 985777, upload-time = "2025-10-19T00:39:36.545Z" }, + { url = "https://files.pythonhosted.org/packages/d4/a1/ec95443f0cf4cd0dbc574fa26ac85a0442d35f3b601a90a0e3dda077f614/cytoolz-1.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fdb5be8fbcc0396141189022724155a4c1c93712ac4aef8c03829af0c2a816d7", size = 982865, upload-time = "2025-10-19T00:39:38.19Z" }, + { url = "https://files.pythonhosted.org/packages/a7/1b/8503604b0c0534977363fb77d371019395dfa031a216f9b1d8729d1280e4/cytoolz-1.1.0-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:c8c0a513dc89bc05cc72893609118815bced5ef201f1a317b4cc3423b3a0e750", size = 2597969, upload-time = "2025-10-19T00:39:40.26Z" }, + { url = "https://files.pythonhosted.org/packages/4e/e5/30748da06417cb2d4bc58e380b0c11d8c6539f4e289dc1e4f4b4fc248d0e/cytoolz-1.1.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ce94db4f8ebe842c30c0ece42ff5de977c47859088c2c363dede5a68f6906484", size = 2692230, upload-time = "2025-10-19T00:39:42.327Z" }, + { url = "https://files.pythonhosted.org/packages/d6/84/e06580b74deb97dfd3513e4e6b660c2dedc220c7653f5bd3e4f772f4d885/cytoolz-1.1.0-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b622d4f54e370c853ded94a668f94fe72c6d70e06ac102f17a2746661c27ab52", size = 2565243, upload-time = "2025-10-19T00:39:44.403Z" }, + { url = "https://files.pythonhosted.org/packages/91/5e/79c0122a34c33afcb5aaee1fec35be24fe16cecefb9bb8890f2908feae56/cytoolz-1.1.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:375a65baa5a5b4ff6a0c5ff17e170cf23312e4c710755771ca966144c24216b5", size = 2868602, upload-time = "2025-10-19T00:39:46.051Z" }, + { url = "https://files.pythonhosted.org/packages/3f/84/404698ff02b32292db1e39cc4a2fbdabe15164b092cc364902984c3ce0f4/cytoolz-1.1.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c0d51bcdb3203a062a78f66bbe33db5e3123048e24a5f0e1402422d79df8ee2d", size = 2905121, upload-time = "2025-10-19T00:39:48.078Z" }, + { url = "https://files.pythonhosted.org/packages/9f/33/afad6593829ba73fc87b5ae64441e380fc937f79f24a1cda60d23cb99b8c/cytoolz-1.1.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1010869529bb05dc9802b6d776a34ca1b6d48b9deec70ad5e2918ae175be5c2f", size = 2684382, upload-time = "2025-10-19T00:39:49.766Z" }, + { url = "https://files.pythonhosted.org/packages/ce/86/7900013a82ca9c6cadbfb22bf50d0fbfc3b192915d2bdd9fab3f69a9afba/cytoolz-1.1.0-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:11a8f2e83295bdb33f35454d6bafcb7845b03b5881dcaed66ecbd726c7f16772", size = 2518183, upload-time = "2025-10-19T00:39:51.433Z" }, + { url = "https://files.pythonhosted.org/packages/c3/4b/acf9be2953fed6a6d795fb66de37c367915037a998a5b3d3b69476cf91fe/cytoolz-1.1.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0499c5e0a8e688ed367a2e51cc13792ae8f08226c15f7d168589fc44b9b9cada", size = 2609368, upload-time = "2025-10-19T00:39:53.458Z" }, + { url = "https://files.pythonhosted.org/packages/fd/ec/3e30455fd526f5cc37bd3dd2a0e2aafb803ae4d271e50ce53bfc30810053/cytoolz-1.1.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:87d44e6033d4c5e95a7d39ba59b8e105ba1c29b1ccd1d215f26477cc1d64be39", size = 2561458, upload-time = "2025-10-19T00:39:55.493Z" }, + { url = "https://files.pythonhosted.org/packages/49/27/e5815c85bb18cdf95780f9596dcfd76dee910a4d635a1924648cb8a636c6/cytoolz-1.1.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:a68cef396a7de237f7b97422a6a450dfb111722296ba217ba5b34551832f1f6e", size = 2578236, upload-time = "2025-10-19T00:39:57.512Z" }, + { url = "https://files.pythonhosted.org/packages/17/db/588e266eff397670398ea335a809152e77b02ee92e0ec42091115b42f09b/cytoolz-1.1.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:06ad4c95b258141f138a93ebfdc1d76ac087afc1a82f1401100a1f44b44ba656", size = 2770523, upload-time = "2025-10-19T00:39:59.194Z" }, + { url = "https://files.pythonhosted.org/packages/ab/ad/82be0b999c7a0a0b362cedfc183eb090b872fd42937af2d6e97d58bc70f8/cytoolz-1.1.0-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:ada59a4b3c59d4ac7162e0ed08667ffa78abf48e975c8a9f9d5b9bc50720f4fd", size = 2512909, upload-time = "2025-10-19T00:40:01.199Z" }, + { url = "https://files.pythonhosted.org/packages/25/21/45f07ab0339a20c518bc9006100922babc397ab7ea5ef40a395db83b9cdd/cytoolz-1.1.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:a8957bcaea1ba01327a9b219d2adb84144377684f51444253890dab500ca171f", size = 2755345, upload-time = "2025-10-19T00:40:03.322Z" }, + { url = "https://files.pythonhosted.org/packages/8b/a7/e530bf2b304206f79b36d793caba1ff9448348713a41bb1ad0197714a0f2/cytoolz-1.1.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:6d8cdc299d67eb0f3b9ecdafeeb55eb3b7b7470e2d950ac34b05ed4c7a5572b8", size = 2617790, upload-time = "2025-10-19T00:40:05.03Z" }, + { url = "https://files.pythonhosted.org/packages/9f/77/7f53092121d7431589344c7d65c3d43c4111547aafabb21d3ca9032d126c/cytoolz-1.1.0-cp310-cp310-win32.whl", hash = "sha256:d8e08464c5cdea4f6df31e84b11ed6bfd79cedb99fbcbfdc15eb9361a6053c5a", size = 900209, upload-time = "2025-10-19T00:40:06.647Z" }, + { url = "https://files.pythonhosted.org/packages/84/e4/902578658303b9bc76b1704d3ed85e6d307d311bd9fa0b919581bea56e62/cytoolz-1.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:7e49922a7ed54262d41960bf3b835a7700327bf79cff1e9bfc73d79021132ff8", size = 944802, upload-time = "2025-10-19T00:40:08.983Z" }, + { url = "https://files.pythonhosted.org/packages/71/9f/56a7003617b4eabd8ddfb470aacc240425cbe6ddeb756adfbbaadaa175f1/cytoolz-1.1.0-cp310-cp310-win_arm64.whl", hash = "sha256:943a662d2e72ffc4438d43ab5a1de8d852237775a423236594a3b3e381b8032c", size = 904835, upload-time = "2025-10-19T00:40:11.024Z" }, + { url = "https://files.pythonhosted.org/packages/69/82/edf1d0c32b6222f2c22e5618d6db855d44eb59f9b6f22436ff963c5d0a5c/cytoolz-1.1.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:dba8e5a8c6e3c789d27b0eb5e7ce5ed7d032a7a9aae17ca4ba5147b871f6e327", size = 1314345, upload-time = "2025-10-19T00:40:13.273Z" }, + { url = "https://files.pythonhosted.org/packages/2d/b5/0e3c1edaa26c2bd9db90cba0ac62c85bbca84224c7ae1c2e0072c4ea64c5/cytoolz-1.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:44b31c05addb0889167a720123b3b497b28dd86f8a0aeaf3ae4ffa11e2c85d55", size = 989259, upload-time = "2025-10-19T00:40:15.196Z" }, + { url = "https://files.pythonhosted.org/packages/09/aa/e2b2ee9fc684867e817640764ea5807f9d25aa1e7bdba02dd4b249aab0f7/cytoolz-1.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:653cb18c4fc5d8a8cfce2bce650aabcbe82957cd0536827367d10810566d5294", size = 986551, upload-time = "2025-10-19T00:40:16.831Z" }, + { url = "https://files.pythonhosted.org/packages/39/9f/4e8ee41acf6674f10a9c2c9117b2f219429a5a0f09bba6135f34ca4f08a6/cytoolz-1.1.0-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:853a5b4806915020c890e1ce70cc056bbc1dd8bc44f2d74d555cccfd7aefba7d", size = 2688378, upload-time = "2025-10-19T00:40:18.552Z" }, + { url = "https://files.pythonhosted.org/packages/78/94/ef006f3412bc22444d855a0fc9ecb81424237fb4e5c1a1f8f5fb79ac978f/cytoolz-1.1.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c7b44e9de86bea013fe84fd8c399d6016bbb96c37c5290769e5c99460b9c53e5", size = 2798299, upload-time = "2025-10-19T00:40:20.191Z" }, + { url = "https://files.pythonhosted.org/packages/df/aa/365953926ee8b4f2e07df7200c0d73632155908c8867af14b2d19cc9f1f7/cytoolz-1.1.0-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:098d628a801dc142e9740126be5624eb7aef1d732bc7a5719f60a2095547b485", size = 2639311, upload-time = "2025-10-19T00:40:22.289Z" }, + { url = "https://files.pythonhosted.org/packages/7c/ee/62beaaee7df208f22590ad07ef8875519af49c52ca39d99460b14a00f15a/cytoolz-1.1.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:779ee4096ed7a82cffab89372ffc339631c285079dbf33dbe7aff1f6174985df", size = 2979532, upload-time = "2025-10-19T00:40:24.006Z" }, + { url = "https://files.pythonhosted.org/packages/c5/04/2211251e450bed111ada1194dc42c461da9aea441de62a01e4085ea6de9f/cytoolz-1.1.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f2ce18dd99533d077e9712f9faa852f389f560351b1efd2f2bdb193a95eddde2", size = 3018632, upload-time = "2025-10-19T00:40:26.175Z" }, + { url = "https://files.pythonhosted.org/packages/ed/a2/4a3400e4d07d3916172bf74fede08020d7b4df01595d8a97f1e9507af5ae/cytoolz-1.1.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ac266a34437812cf841cecbfe19f355ab9c3dd1ef231afc60415d40ff12a76e4", size = 2788579, upload-time = "2025-10-19T00:40:27.878Z" }, + { url = "https://files.pythonhosted.org/packages/fe/82/bb88caa53a41f600e7763c517d50e2efbbe6427ea395716a92b83f44882a/cytoolz-1.1.0-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1920b9b9c13d60d0bb6cd14594b3bce0870022eccb430618c37156da5f2b7a55", size = 2593024, upload-time = "2025-10-19T00:40:29.601Z" }, + { url = "https://files.pythonhosted.org/packages/09/a8/8b25e59570da16c7a0f173b8c6ec0aa6f3abd47fd385c007485acb459896/cytoolz-1.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47caa376dafd2bdc29f8a250acf59c810ec9105cd6f7680b9a9d070aae8490ec", size = 2715304, upload-time = "2025-10-19T00:40:31.151Z" }, + { url = "https://files.pythonhosted.org/packages/d4/56/faec7696f235521b926ffdf92c102f5b029f072d28e1020364e55b084820/cytoolz-1.1.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:5ab2c97d8aaa522b038cca9187b1153347af22309e7c998b14750c6fdec7b1cb", size = 2654461, upload-time = "2025-10-19T00:40:32.884Z" }, + { url = "https://files.pythonhosted.org/packages/aa/82/f790ed167c04b8d2a33bed30770a9b7066fc4f573321d797190e5f05685f/cytoolz-1.1.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:4bce006121b120e8b359244ee140bb0b1093908efc8b739db8dbaa3f8fb42139", size = 2672077, upload-time = "2025-10-19T00:40:34.543Z" }, + { url = "https://files.pythonhosted.org/packages/d9/b3/80b8183e7eee44f45bfa3cdd3ebdadf3dd43ffc686f96d442a6c4dded45d/cytoolz-1.1.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:7fc0f1e4e9bb384d26e73c6657bbc26abdae4ff66a95933c00f3d578be89181b", size = 2881589, upload-time = "2025-10-19T00:40:36.315Z" }, + { url = "https://files.pythonhosted.org/packages/8f/05/ac5ba5ddb88a3ba7ecea4bf192194a838af564d22ea7a4812cbb6bd106ce/cytoolz-1.1.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:dd3f894ff972da1994d06ac6157d74e40dda19eb31fe5e9b7863ca4278c3a167", size = 2589924, upload-time = "2025-10-19T00:40:38.317Z" }, + { url = "https://files.pythonhosted.org/packages/8e/cd/100483cae3849d24351c8333a815dc6adaf3f04912486e59386d86d9db9a/cytoolz-1.1.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:0846f49cf8a4496bd42659040e68bd0484ce6af819709cae234938e039203ba0", size = 2868059, upload-time = "2025-10-19T00:40:40.025Z" }, + { url = "https://files.pythonhosted.org/packages/34/6e/3a7c56b325772d39397fc3aafb4dc054273982097178b6c3917c6dad48de/cytoolz-1.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:16a3af394ade1973226d64bb2f9eb3336adbdea03ed5b134c1bbec5a3b20028e", size = 2721692, upload-time = "2025-10-19T00:40:41.621Z" }, + { url = "https://files.pythonhosted.org/packages/fa/ca/9fdaee32c3bc769dfb7e7991d9499136afccea67e423d097b8fb3c5acbc1/cytoolz-1.1.0-cp311-cp311-win32.whl", hash = "sha256:b786c9c8aeab76cc2f76011e986f7321a23a56d985b77d14f155d5e5514ea781", size = 899349, upload-time = "2025-10-19T00:40:43.183Z" }, + { url = "https://files.pythonhosted.org/packages/fd/04/2ab98edeea90311e4029e1643e43d2027b54da61453292d9ea51a103ee87/cytoolz-1.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:ebf06d1c5344fb22fee71bf664234733e55db72d74988f2ecb7294b05e4db30c", size = 945831, upload-time = "2025-10-19T00:40:44.693Z" }, + { url = "https://files.pythonhosted.org/packages/b4/8d/777d86ea6bcc68b0fc926b0ef8ab51819e2176b37aadea072aac949d5231/cytoolz-1.1.0-cp311-cp311-win_arm64.whl", hash = "sha256:b63f5f025fac893393b186e132e3e242de8ee7265d0cd3f5bdd4dda93f6616c9", size = 904076, upload-time = "2025-10-19T00:40:46.678Z" }, + { url = "https://files.pythonhosted.org/packages/c6/ec/01426224f7acf60183d3921b25e1a8e71713d3d39cb464d64ac7aace6ea6/cytoolz-1.1.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:99f8e134c9be11649342853ec8c90837af4089fc8ff1e8f9a024a57d1fa08514", size = 1327800, upload-time = "2025-10-19T00:40:48.674Z" }, + { url = "https://files.pythonhosted.org/packages/b4/07/e07e8fedd332ac9626ad58bea31416dda19bfd14310731fa38b16a97e15f/cytoolz-1.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:0a6f44cf9319c30feb9a50aa513d777ef51efec16f31c404409e7deb8063df64", size = 997118, upload-time = "2025-10-19T00:40:50.919Z" }, + { url = "https://files.pythonhosted.org/packages/ab/72/c0f766d63ed2f9ea8dc8e1628d385d99b41fb834ce17ac3669e3f91e115d/cytoolz-1.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:945580dc158c557172fca899a35a99a16fbcebf6db0c77cb6621084bc82189f9", size = 991169, upload-time = "2025-10-19T00:40:52.887Z" }, + { url = "https://files.pythonhosted.org/packages/df/4b/1f757353d1bf33e56a7391ecc9bc49c1e529803b93a9d2f67fe5f92906fe/cytoolz-1.1.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:257905ec050d04f2f856854620d1e25556fd735064cebd81b460f54939b9f9d5", size = 2700680, upload-time = "2025-10-19T00:40:54.597Z" }, + { url = "https://files.pythonhosted.org/packages/25/73/9b25bb7ed8d419b9d6ff2ae0b3d06694de79a3f98f5169a1293ff7ad3a3f/cytoolz-1.1.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:82779049f352fb3ab5e8c993ab45edbb6e02efb1f17f0b50f4972c706cc51d76", size = 2824951, upload-time = "2025-10-19T00:40:56.137Z" }, + { url = "https://files.pythonhosted.org/packages/0c/93/9c787f7c909e75670fff467f2504725d06d8c3f51d6dfe22c55a08c8ccd4/cytoolz-1.1.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7d3e405e435320e08c5a1633afaf285a392e2d9cef35c925d91e2a31dfd7a688", size = 2679635, upload-time = "2025-10-19T00:40:57.799Z" }, + { url = "https://files.pythonhosted.org/packages/50/aa/9ee92c302cccf7a41a7311b325b51ebeff25d36c1f82bdc1bbe3f58dc947/cytoolz-1.1.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:923df8f5591e0d20543060c29909c149ab1963a7267037b39eee03a83dbc50a8", size = 2938352, upload-time = "2025-10-19T00:40:59.49Z" }, + { url = "https://files.pythonhosted.org/packages/6a/a3/3b58c5c1692c3bacd65640d0d5c7267a7ebb76204f7507aec29de7063d2f/cytoolz-1.1.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:25db9e4862f22ea0ae2e56c8bec9fc9fd756b655ae13e8c7b5625d7ed1c582d4", size = 3022121, upload-time = "2025-10-19T00:41:01.209Z" }, + { url = "https://files.pythonhosted.org/packages/e1/93/c647bc3334355088c57351a536c2d4a83dd45f7de591fab383975e45bff9/cytoolz-1.1.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c7a98deb11ccd8e5d9f9441ef2ff3352aab52226a2b7d04756caaa53cd612363", size = 2857656, upload-time = "2025-10-19T00:41:03.456Z" }, + { url = "https://files.pythonhosted.org/packages/b2/c2/43fea146bf4141deea959e19dcddf268c5ed759dec5c2ed4a6941d711933/cytoolz-1.1.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:dce4ee9fc99104bc77efdea80f32ca5a650cd653bcc8a1d984a931153d3d9b58", size = 2551284, upload-time = "2025-10-19T00:41:05.347Z" }, + { url = "https://files.pythonhosted.org/packages/6f/df/cdc7a81ce5cfcde7ef523143d545635fc37e80ccacce140ae58483a21da3/cytoolz-1.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:80d6da158f7d20c15819701bbda1c041f0944ede2f564f5c739b1bc80a9ffb8b", size = 2721673, upload-time = "2025-10-19T00:41:07.528Z" }, + { url = "https://files.pythonhosted.org/packages/45/be/f8524bb9ad8812ad375e61238dcaa3177628234d1b908ad0b74e3657cafd/cytoolz-1.1.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:3b5c5a192abda123ad45ef716ec9082b4cf7d95e9ada8291c5c2cc5558be858b", size = 2722884, upload-time = "2025-10-19T00:41:09.698Z" }, + { url = "https://files.pythonhosted.org/packages/23/e6/6bb8e4f9c267ad42d1ff77b6d2e4984665505afae50a216290e1d7311431/cytoolz-1.1.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5b399ce7d967b1cb6280250818b786be652aa8ddffd3c0bb5c48c6220d945ab5", size = 2685486, upload-time = "2025-10-19T00:41:11.349Z" }, + { url = "https://files.pythonhosted.org/packages/d7/dd/88619f9c8d2b682562c0c886bbb7c35720cb83fda2ac9a41bdd14073d9bd/cytoolz-1.1.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:e7e29a1a03f00b4322196cfe8e2c38da9a6c8d573566052c586df83aacc5663c", size = 2839661, upload-time = "2025-10-19T00:41:13.053Z" }, + { url = "https://files.pythonhosted.org/packages/b8/8d/4478ebf471ee78dd496d254dc0f4ad729cd8e6ba8257de4f0a98a2838ef2/cytoolz-1.1.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:5291b117d71652a817ec164e7011f18e6a51f8a352cc9a70ed5b976c51102fda", size = 2547095, upload-time = "2025-10-19T00:41:16.054Z" }, + { url = "https://files.pythonhosted.org/packages/e6/68/f1dea33367b0b3f64e199c230a14a6b6f243c189020effafd31e970ca527/cytoolz-1.1.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:8caef62f846a9011676c51bda9189ae394cdd6bb17f2946ecaedc23243268320", size = 2870901, upload-time = "2025-10-19T00:41:17.727Z" }, + { url = "https://files.pythonhosted.org/packages/4a/9a/33591c09dfe799b8fb692cf2ad383e2c41ab6593cc960b00d1fc8a145655/cytoolz-1.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:de425c5a8e3be7bb3a195e19191d28d9eb3c2038046064a92edc4505033ec9cb", size = 2765422, upload-time = "2025-10-19T00:41:20.075Z" }, + { url = "https://files.pythonhosted.org/packages/60/2b/a8aa233c9416df87f004e57ae4280bd5e1f389b4943d179f01020c6ec629/cytoolz-1.1.0-cp312-cp312-win32.whl", hash = "sha256:296440a870e8d1f2e1d1edf98f60f1532b9d3ab8dfbd4b25ec08cd76311e79e5", size = 901933, upload-time = "2025-10-19T00:41:21.646Z" }, + { url = "https://files.pythonhosted.org/packages/ad/33/4c9bdf8390dc01d2617c7f11930697157164a52259b6818ddfa2f94f89f4/cytoolz-1.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:07156987f224c6dac59aa18fb8bf91e1412f5463961862716a3381bf429c8699", size = 947989, upload-time = "2025-10-19T00:41:23.288Z" }, + { url = "https://files.pythonhosted.org/packages/35/ac/6e2708835875f5acb52318462ed296bf94ed0cb8c7cb70e62fbd03f709e3/cytoolz-1.1.0-cp312-cp312-win_arm64.whl", hash = "sha256:23e616b38f5b3160c7bb45b0f84a8f3deb4bd26b29fb2dfc716f241c738e27b8", size = 903913, upload-time = "2025-10-19T00:41:24.992Z" }, + { url = "https://files.pythonhosted.org/packages/71/4a/b3ddb3ee44fe0045e95dd973746f93f033b6f92cce1fc3cbbe24b329943c/cytoolz-1.1.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:76c9b58555300be6dde87a41faf1f97966d79b9a678b7a526fcff75d28ef4945", size = 976728, upload-time = "2025-10-19T00:41:26.5Z" }, + { url = "https://files.pythonhosted.org/packages/42/21/a3681434aa425875dd828bb515924b0f12c37a55c7d2bc5c0c5de3aeb0b4/cytoolz-1.1.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:d1d638b10d3144795655e9395566ce35807df09219fd7cacd9e6acbdef67946a", size = 986057, upload-time = "2025-10-19T00:41:28.911Z" }, + { url = "https://files.pythonhosted.org/packages/d9/cb/efc1b29e211e0670a6953222afaac84dcbba5cb940b130c0e49858978040/cytoolz-1.1.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:26801c1a165e84786a99e03c9c9973356caaca002d66727b761fb1042878ef06", size = 992632, upload-time = "2025-10-19T00:41:30.612Z" }, + { url = "https://files.pythonhosted.org/packages/be/b0/e50621d21e939338c97faab651f58ea7fa32101226a91de79ecfb89d71e1/cytoolz-1.1.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:2a9a464542912d3272f6dccc5142df057c71c6a5cbd30439389a732df401afb7", size = 1317534, upload-time = "2025-10-19T00:41:32.625Z" }, + { url = "https://files.pythonhosted.org/packages/0d/6b/25aa9739b0235a5bc4c1ea293186bc6822a4c6607acfe1422423287e7400/cytoolz-1.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ed6104fa942aa5784bf54f339563de637557e3443b105760bc4de8f16a7fc79b", size = 992336, upload-time = "2025-10-19T00:41:34.073Z" }, + { url = "https://files.pythonhosted.org/packages/e1/53/5f4deb0ff958805309d135d899c764364c1e8a632ce4994bd7c45fb98df2/cytoolz-1.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:56161f0ab60dc4159ec343509abaf809dc88e85c7e420e354442c62e3e7cbb77", size = 986118, upload-time = "2025-10-19T00:41:35.7Z" }, + { url = "https://files.pythonhosted.org/packages/1c/e3/f6255b76c8cc0debbe1c0779130777dc0434da6d9b28a90d9f76f8cb67cd/cytoolz-1.1.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:832bd36cc9123535f1945acf6921f8a2a15acc19cfe4065b1c9b985a28671886", size = 2679563, upload-time = "2025-10-19T00:41:37.926Z" }, + { url = "https://files.pythonhosted.org/packages/59/8a/acc6e39a84e930522b965586ad3a36694f9bf247b23188ee0eb47b1c9ed1/cytoolz-1.1.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1842636b6e034f229bf084c2bcdcfd36c8437e752eefd2c74ce9e2f10415cb6e", size = 2813020, upload-time = "2025-10-19T00:41:39.935Z" }, + { url = "https://files.pythonhosted.org/packages/db/f5/0083608286ad1716eda7c41f868e85ac549f6fd6b7646993109fa0bdfd98/cytoolz-1.1.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:823df012ab90d2f2a0f92fea453528539bf71ac1879e518524cd0c86aa6df7b9", size = 2669312, upload-time = "2025-10-19T00:41:41.55Z" }, + { url = "https://files.pythonhosted.org/packages/47/a8/d16080b575520fe5da00cede1ece4e0a4180ec23f88dcdc6a2f5a90a7f7f/cytoolz-1.1.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2f1fcf9e7e7b3487883ff3f815abc35b89dcc45c4cf81c72b7ee457aa72d197b", size = 2922147, upload-time = "2025-10-19T00:41:43.252Z" }, + { url = "https://files.pythonhosted.org/packages/7e/bc/716c9c1243701e58cad511eb3937fd550e645293c5ed1907639c5d66f194/cytoolz-1.1.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4cdb3fa1772116827f263f25b0cdd44c663b6701346a56411960534a06c082de", size = 2981602, upload-time = "2025-10-19T00:41:45.354Z" }, + { url = "https://files.pythonhosted.org/packages/14/bc/571b232996846b27f4ac0c957dc8bf60261e9b4d0d01c8d955e82329544e/cytoolz-1.1.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d1b5c95041741b81430454db65183e133976f45ac3c03454cfa8147952568529", size = 2830103, upload-time = "2025-10-19T00:41:47.959Z" }, + { url = "https://files.pythonhosted.org/packages/5b/55/c594afb46ecd78e4b7e1fb92c947ed041807875661ceda73baaf61baba4f/cytoolz-1.1.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b2079fd9f1a65f4c61e6278c8a6d4f85edf30c606df8d5b32f1add88cbbe2286", size = 2533802, upload-time = "2025-10-19T00:41:49.683Z" }, + { url = "https://files.pythonhosted.org/packages/93/83/1edcf95832555a78fc43b975f3ebe8ceadcc9664dd47fd33747a14df5069/cytoolz-1.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a92a320d72bef1c7e2d4c6d875125cf57fc38be45feb3fac1bfa64ea401f54a4", size = 2706071, upload-time = "2025-10-19T00:41:51.386Z" }, + { url = "https://files.pythonhosted.org/packages/e2/df/035a408df87f25cfe3611557818b250126cd2281b2104cd88395de205583/cytoolz-1.1.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:06d1c79aa51e6a92a90b0e456ebce2288f03dd6a76c7f582bfaa3eda7692e8a5", size = 2707575, upload-time = "2025-10-19T00:41:53.305Z" }, + { url = "https://files.pythonhosted.org/packages/7a/a4/ef78e13e16e93bf695a9331321d75fbc834a088d941f1c19e6b63314e257/cytoolz-1.1.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:e1d7be25f6971e986a52b6d3a0da28e1941850985417c35528f6823aef2cfec5", size = 2660486, upload-time = "2025-10-19T00:41:55.542Z" }, + { url = "https://files.pythonhosted.org/packages/30/7a/2c3d60682b26058d435416c4e90d4a94db854de5be944dfd069ed1be648a/cytoolz-1.1.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:964b248edc31efc50a65e9eaa0c845718503823439d2fa5f8d2c7e974c2b5409", size = 2819605, upload-time = "2025-10-19T00:41:58.257Z" }, + { url = "https://files.pythonhosted.org/packages/45/92/19b722a1d83cc443fbc0c16e0dc376f8a451437890d3d9ee370358cf0709/cytoolz-1.1.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c9ff2b3c57c79b65cb5be14a18c6fd4a06d5036fb3f33e973a9f70e9ac13ca28", size = 2533559, upload-time = "2025-10-19T00:42:00.324Z" }, + { url = "https://files.pythonhosted.org/packages/1d/15/fa3b7891da51115204416f14192081d3dea0eaee091f123fdc1347de8dd1/cytoolz-1.1.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:22290b73086af600042d99f5ce52a43d4ad9872c382610413176e19fc1d4fd2d", size = 2839171, upload-time = "2025-10-19T00:42:01.881Z" }, + { url = "https://files.pythonhosted.org/packages/46/40/d3519d5cd86eebebf1e8b7174ec32dfb6ecec67b48b0cfb92bf226659b5a/cytoolz-1.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a2ade74fccd080ea793382968913ee38d7a35c921df435bbf0a6aeecf0d17574", size = 2743379, upload-time = "2025-10-19T00:42:03.809Z" }, + { url = "https://files.pythonhosted.org/packages/93/e2/a9e7511f0a13fdbefa5bf73cf8e4763878140de9453fd3e50d6ac57b6be7/cytoolz-1.1.0-cp313-cp313-win32.whl", hash = "sha256:db5dbcfda1c00e937426cbf9bdc63c24ebbc358c3263bfcbc1ab4a88dc52aa8e", size = 900844, upload-time = "2025-10-19T00:42:05.967Z" }, + { url = "https://files.pythonhosted.org/packages/d6/a4/fb7eb403c6a4c81e5a30363f34a71adcc8bf5292dc8ea32e2440aa5668f2/cytoolz-1.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:9e2d3fe3b45c3eb7233746f7aca37789be3dceec3e07dcc406d3e045ea0f7bdc", size = 946461, upload-time = "2025-10-19T00:42:07.983Z" }, + { url = "https://files.pythonhosted.org/packages/93/bb/1c8c33d353548d240bc6e8677ee8c3560ce5fa2f084e928facf7c35a6dcf/cytoolz-1.1.0-cp313-cp313-win_arm64.whl", hash = "sha256:32c559f95ff44a9ebcbd934acaa1e6dc8f3e6ffce4762a79a88528064873d6d5", size = 902673, upload-time = "2025-10-19T00:42:09.982Z" }, + { url = "https://files.pythonhosted.org/packages/c4/ba/4a53acc60f59030fcaf48c7766e3c4c81bd997379425aa45b129396557b5/cytoolz-1.1.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:9e2cd93b28f667c5870a070ab2b8bb4397470a85c4b204f2454b0ad001cd1ca3", size = 1372336, upload-time = "2025-10-19T00:42:12.104Z" }, + { url = "https://files.pythonhosted.org/packages/ac/90/f28fd8ad8319d8f5c8da69a2c29b8cf52a6d2c0161602d92b366d58926ab/cytoolz-1.1.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:f494124e141a9361f31d79875fe7ea459a3be2b9dadd90480427c0c52a0943d4", size = 1011930, upload-time = "2025-10-19T00:42:14.231Z" }, + { url = "https://files.pythonhosted.org/packages/c9/95/4561c4e0ad1c944f7673d6d916405d68080f10552cfc5d69a1cf2475a9a1/cytoolz-1.1.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:53a3262bf221f19437ed544bf8c0e1980c81ac8e2a53d87a9bc075dba943d36f", size = 1020610, upload-time = "2025-10-19T00:42:15.877Z" }, + { url = "https://files.pythonhosted.org/packages/c3/14/b2e1ffa4995ec36e1372e243411ff36325e4e6d7ffa34eb4098f5357d176/cytoolz-1.1.0-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:47663e57d3f3f124921f38055e86a1022d0844c444ede2e8f090d3bbf80deb65", size = 2917327, upload-time = "2025-10-19T00:42:17.706Z" }, + { url = "https://files.pythonhosted.org/packages/4a/29/7cab6c609b4514ac84cca2f7dca6c509977a8fc16d27c3a50e97f105fa6a/cytoolz-1.1.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a5a8755c4104ee4e3d5ba434c543b5f85fdee6a1f1df33d93f518294da793a60", size = 3108951, upload-time = "2025-10-19T00:42:19.363Z" }, + { url = "https://files.pythonhosted.org/packages/9a/71/1d1103b819458679277206ad07d78ca6b31c4bb88d6463fd193e19bfb270/cytoolz-1.1.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:4d96ff3d381423af1b105295f97de86d1db51732c9566eb37378bab6670c5010", size = 2807149, upload-time = "2025-10-19T00:42:20.964Z" }, + { url = "https://files.pythonhosted.org/packages/1a/d4/3d83a05a21e7d2ed2b9e6daf489999c29934b005de9190272b8a2e3735d0/cytoolz-1.1.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0ec96b3d537cdf47d4e76ded199f7440715f4c71029b45445cff92c1248808c2", size = 3111608, upload-time = "2025-10-19T00:42:22.684Z" }, + { url = "https://files.pythonhosted.org/packages/51/88/96f68354c3d4af68de41f0db4fe41a23b96a50a4a416636cea325490cfeb/cytoolz-1.1.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:208e2f2ef90a32b0acbff3303d90d89b13570a228d491d2e622a7883a3c68148", size = 3179373, upload-time = "2025-10-19T00:42:24.395Z" }, + { url = "https://files.pythonhosted.org/packages/ce/50/ed87a5cd8e6f27ffbb64c39e9730e18ec66c37631db2888ae711909f10c9/cytoolz-1.1.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0d416a81bb0bd517558668e49d30a7475b5445f9bbafaab7dcf066f1e9adba36", size = 3003120, upload-time = "2025-10-19T00:42:26.18Z" }, + { url = "https://files.pythonhosted.org/packages/d3/a7/acde155b050d6eaa8e9c7845c98fc5fb28501568e78e83ebbf44f8855274/cytoolz-1.1.0-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f32e94c91ffe49af04835ee713ebd8e005c85ebe83e7e1fdcc00f27164c2d636", size = 2703225, upload-time = "2025-10-19T00:42:27.93Z" }, + { url = "https://files.pythonhosted.org/packages/1b/b6/9d518597c5bdea626b61101e8d2ff94124787a42259dafd9f5fc396f346a/cytoolz-1.1.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:15d0c6405efc040499c46df44056a5c382f551a7624a41cf3e4c84a96b988a15", size = 2956033, upload-time = "2025-10-19T00:42:29.993Z" }, + { url = "https://files.pythonhosted.org/packages/89/7a/93e5f860926165538c85e1c5e1670ad3424f158df810f8ccd269da652138/cytoolz-1.1.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:bf069c5381d757debae891401b88b3a346ba3a28ca45ba9251103b282463fad8", size = 2862950, upload-time = "2025-10-19T00:42:31.803Z" }, + { url = "https://files.pythonhosted.org/packages/76/e6/99d6af00487bedc27597b54c9fcbfd5c833a69c6b7a9b9f0fff777bfc7aa/cytoolz-1.1.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:7d5cf15892e63411ec1bd67deff0e84317d974e6ab2cdfefdd4a7cea2989df66", size = 2861757, upload-time = "2025-10-19T00:42:33.625Z" }, + { url = "https://files.pythonhosted.org/packages/71/ca/adfa1fb7949478135a37755cb8e88c20cd6b75c22a05f1128f05f3ab2c60/cytoolz-1.1.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:3e3872c21170f8341656f8692f8939e8800dcee6549ad2474d4c817bdefd62cd", size = 2979049, upload-time = "2025-10-19T00:42:35.377Z" }, + { url = "https://files.pythonhosted.org/packages/70/4c/7bf47a03a4497d500bc73d4204e2d907771a017fa4457741b2a1d7c09319/cytoolz-1.1.0-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:b9ddeff8e8fd65eb1fcefa61018100b2b627e759ea6ad275d2e2a93ffac147bf", size = 2699492, upload-time = "2025-10-19T00:42:37.133Z" }, + { url = "https://files.pythonhosted.org/packages/7e/e7/3d034b0e4817314f07aa465d5864e9b8df9d25cb260a53dd84583e491558/cytoolz-1.1.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:02feeeda93e1fa3b33414eb57c2b0aefd1db8f558dd33fdfcce664a0f86056e4", size = 2995646, upload-time = "2025-10-19T00:42:38.912Z" }, + { url = "https://files.pythonhosted.org/packages/c1/62/be357181c71648d9fe1d1ce91cd42c63457dcf3c158e144416fd51dced83/cytoolz-1.1.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:d08154ad45349162b6c37f12d5d1b2e6eef338e657b85e1621e4e6a4a69d64cb", size = 2919481, upload-time = "2025-10-19T00:42:40.85Z" }, + { url = "https://files.pythonhosted.org/packages/62/d5/bf5434fde726c4f80cb99912b2d8e0afa1587557e2a2d7e0315eb942f2de/cytoolz-1.1.0-cp313-cp313t-win32.whl", hash = "sha256:10ae4718a056948d73ca3e1bb9ab1f95f897ec1e362f829b9d37cc29ab566c60", size = 951595, upload-time = "2025-10-19T00:42:42.877Z" }, + { url = "https://files.pythonhosted.org/packages/64/29/39c161e9204a9715321ddea698cbd0abc317e78522c7c642363c20589e71/cytoolz-1.1.0-cp313-cp313t-win_amd64.whl", hash = "sha256:1bb77bc6197e5cb19784b6a42bb0f8427e81737a630d9d7dda62ed31733f9e6c", size = 1004445, upload-time = "2025-10-19T00:42:44.855Z" }, + { url = "https://files.pythonhosted.org/packages/e2/5a/7cbff5e9a689f558cb0bdf277f9562b2ac51acf7cd15e055b8c3efb0e1ef/cytoolz-1.1.0-cp313-cp313t-win_arm64.whl", hash = "sha256:563dda652c6ff52d215704fbe6b491879b78d7bbbb3a9524ec8e763483cb459f", size = 926207, upload-time = "2025-10-19T00:42:46.456Z" }, + { url = "https://files.pythonhosted.org/packages/b7/e8/297a85ba700f437c01eba962428e6ab4572f6c3e68e8ff442ce5c9d3a496/cytoolz-1.1.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:d542cee7c7882d2a914a33dec4d3600416fb336734df979473249d4c53d207a1", size = 980613, upload-time = "2025-10-19T00:42:47.988Z" }, + { url = "https://files.pythonhosted.org/packages/e8/d7/2b02c9d18e9cc263a0e22690f78080809f1eafe72f26b29ccc115d3bf5c8/cytoolz-1.1.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:31922849b701b0f24bb62e56eb2488dcd3aa6ae3057694bd6b3b7c4c2bc27c2f", size = 990476, upload-time = "2025-10-19T00:42:49.653Z" }, + { url = "https://files.pythonhosted.org/packages/89/26/b6b159d2929310fca0eff8a4989cd4b1ecbdf7c46fdff46c7a20fcae55c8/cytoolz-1.1.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:e68308d32afd31943314735c1335e4ab5696110e96b405f6bdb8f2a8dc771a16", size = 992712, upload-time = "2025-10-19T00:42:51.306Z" }, + { url = "https://files.pythonhosted.org/packages/42/a0/f7c572aa151ed466b0fce4a327c3cc916d3ef3c82e341be59ea4b9bee9e4/cytoolz-1.1.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:fc4bb48b3b866e1867f7c6411a4229e5b44be3989060663713e10efc24c9bd5f", size = 1322596, upload-time = "2025-10-19T00:42:52.978Z" }, + { url = "https://files.pythonhosted.org/packages/72/7c/a55d035e20b77b6725e85c8f1a418b3a4c23967288b8b0c2d1a40f158cbe/cytoolz-1.1.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:456f77207d1445025d7ef262b8370a05492dcb1490cb428b0f3bf1bd744a89b0", size = 992825, upload-time = "2025-10-19T00:42:55.026Z" }, + { url = "https://files.pythonhosted.org/packages/03/af/39d2d3db322136e12e9336a1f13bab51eab88b386bfb11f91d3faff8ba34/cytoolz-1.1.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:174ebc71ebb20a9baeffce6ee07ee2cd913754325c93f99d767380d8317930f7", size = 990525, upload-time = "2025-10-19T00:42:56.666Z" }, + { url = "https://files.pythonhosted.org/packages/a6/bd/65d7a869d307f9b10ad45c2c1cbb40b81a8d0ed1138fa17fd904f5c83298/cytoolz-1.1.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:8b3604fef602bcd53415055a4f68468339192fd17be39e687ae24f476d23d56e", size = 2672409, upload-time = "2025-10-19T00:42:58.81Z" }, + { url = "https://files.pythonhosted.org/packages/2d/fb/74dfd844bfd67e810bd36e8e3903a143035447245828e7fcd7c81351d775/cytoolz-1.1.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3604b959a01f64c366e7d10ec7634d5f5cfe10301e27a8f090f6eb3b2a628a18", size = 2808477, upload-time = "2025-10-19T00:43:00.577Z" }, + { url = "https://files.pythonhosted.org/packages/d6/1f/587686c43e31c19241ec317da66438d093523921ea7749bbc65558a30df9/cytoolz-1.1.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6db2127a3c1bc2f59f08010d2ae53a760771a9de2f67423ad8d400e9ba4276e8", size = 2636881, upload-time = "2025-10-19T00:43:02.24Z" }, + { url = "https://files.pythonhosted.org/packages/bc/6d/90468cd34f77cb38a11af52c4dc6199efcc97a486395a21bef72e9b7602e/cytoolz-1.1.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:56584745ac647993a016a21bc76399113b7595e312f8d0a1b140c9fcf9b58a27", size = 2937315, upload-time = "2025-10-19T00:43:03.954Z" }, + { url = "https://files.pythonhosted.org/packages/d9/50/7b92cd78c613b92e3509e6291d3fb7e0d72ebda999a8df806a96c40ca9ab/cytoolz-1.1.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:db2c4c3a7f7bd7e03bb1a236a125c8feb86c75802f4ecda6ecfaf946610b2930", size = 2959988, upload-time = "2025-10-19T00:43:05.758Z" }, + { url = "https://files.pythonhosted.org/packages/44/d5/34b5a28a8d9bb329f984b4c2259407ca3f501d1abeb01bacea07937d85d1/cytoolz-1.1.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:48cb8a692111a285d2b9acd16d185428176bfbffa8a7c274308525fccd01dd42", size = 2795116, upload-time = "2025-10-19T00:43:07.411Z" }, + { url = "https://files.pythonhosted.org/packages/f5/d9/5dd829e33273ec03bdc3c812e6c3281987ae2c5c91645582f6c331544a64/cytoolz-1.1.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d2f344ba5eb17dcf38ee37fdde726f69053f54927db8f8a1bed6ac61e5b1890d", size = 2535390, upload-time = "2025-10-19T00:43:09.104Z" }, + { url = "https://files.pythonhosted.org/packages/87/1f/7f9c58068a8eec2183110df051bc6b69dd621143f84473eeb6dc1b32905a/cytoolz-1.1.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:abf76b1c1abd031f098f293b6d90ee08bdaa45f8b5678430e331d991b82684b1", size = 2704834, upload-time = "2025-10-19T00:43:10.942Z" }, + { url = "https://files.pythonhosted.org/packages/d2/90/667def5665333575d01a65fe3ec0ca31b897895f6e3bc1a42d6ea3659369/cytoolz-1.1.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:ddf9a38a5b686091265ff45b53d142e44a538cd6c2e70610d3bc6be094219032", size = 2658441, upload-time = "2025-10-19T00:43:12.655Z" }, + { url = "https://files.pythonhosted.org/packages/23/79/6615f9a14960bd29ac98b823777b6589357833f65cf1a11b5abc1587c120/cytoolz-1.1.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:946786755274f07bb2be0400f28adb31d7d85a7c7001873c0a8e24a503428fb3", size = 2654766, upload-time = "2025-10-19T00:43:14.325Z" }, + { url = "https://files.pythonhosted.org/packages/b0/99/be59c6e0ae02153ef10ae1ff0f380fb19d973c651b50cf829a731f6c9e79/cytoolz-1.1.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:d5b8f78b9fed79cf185ad4ddec099abeef45951bdcb416c5835ba05f0a1242c7", size = 2827649, upload-time = "2025-10-19T00:43:16.132Z" }, + { url = "https://files.pythonhosted.org/packages/19/b7/854ddcf9f9618844108677c20d48f4611b5c636956adea0f0e85e027608f/cytoolz-1.1.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:fccde6efefdbc02e676ccb352a2ccc8a8e929f59a1c6d3d60bb78e923a49ca44", size = 2533456, upload-time = "2025-10-19T00:43:17.764Z" }, + { url = "https://files.pythonhosted.org/packages/45/66/bfe6fbb2bdcf03c8377c8c2f542576e15f3340c905a09d78a6cb3badd39a/cytoolz-1.1.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:717b7775313da5f51b0fbf50d865aa9c39cb241bd4cb605df3cf2246d6567397", size = 2826455, upload-time = "2025-10-19T00:43:19.561Z" }, + { url = "https://files.pythonhosted.org/packages/c3/0c/cce4047bd927e95f59e73319c02c9bc86bd3d76392e0eb9e41a1147a479c/cytoolz-1.1.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5158744a09d0e0e4a4f82225e3a3c4ebf38f9ae74467aaa905467270e52f2794", size = 2714897, upload-time = "2025-10-19T00:43:21.291Z" }, + { url = "https://files.pythonhosted.org/packages/ac/9a/061323bb289b565802bad14fb7ab59fcd8713105df142bcf4dd9ff64f8ac/cytoolz-1.1.0-cp314-cp314-win32.whl", hash = "sha256:1ed534bdbbf063b2bb28fca7d0f6723a3e5a72b086e7c7fe6d74ae8c3e4d00e2", size = 901490, upload-time = "2025-10-19T00:43:22.895Z" }, + { url = "https://files.pythonhosted.org/packages/a3/20/1f3a733d710d2a25d6f10b463bef55ada52fe6392a5d233c8d770191f48a/cytoolz-1.1.0-cp314-cp314-win_amd64.whl", hash = "sha256:472c1c9a085f5ad973ec0ad7f0b9ba0969faea6f96c9e397f6293d386f3a25ec", size = 946730, upload-time = "2025-10-19T00:43:24.838Z" }, + { url = "https://files.pythonhosted.org/packages/f2/22/2d657db4a5d1c10a152061800f812caba9ef20d7bd2406f51a5fd800c180/cytoolz-1.1.0-cp314-cp314-win_arm64.whl", hash = "sha256:a7ad7ca3386fa86bd301be3fa36e7f0acb024f412f665937955acfc8eb42deff", size = 905722, upload-time = "2025-10-19T00:43:26.439Z" }, + { url = "https://files.pythonhosted.org/packages/19/97/b4a8c76796a9a8b9bc90c7992840fa1589a1af8e0426562dea4ce9b384a7/cytoolz-1.1.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:64b63ed4b71b1ba813300ad0f06b8aff19a12cf51116e0e4f1ed837cea4debcf", size = 1372606, upload-time = "2025-10-19T00:43:28.491Z" }, + { url = "https://files.pythonhosted.org/packages/08/d4/a1bb1a32b454a2d650db8374ff3bf875ba0fc1c36e6446ec02a83b9140a1/cytoolz-1.1.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:a60ba6f2ed9eb0003a737e1ee1e9fa2258e749da6477946008d4324efa25149f", size = 1012189, upload-time = "2025-10-19T00:43:30.177Z" }, + { url = "https://files.pythonhosted.org/packages/21/4b/2f5cbbd81588918ee7dd70cffb66731608f578a9b72166aafa991071af7d/cytoolz-1.1.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1aa58e2434d732241f7f051e6f17657e969a89971025e24578b5cbc6f1346485", size = 1020624, upload-time = "2025-10-19T00:43:31.712Z" }, + { url = "https://files.pythonhosted.org/packages/f5/99/c4954dd86cd593cd776a038b36795a259b8b5c12cbab6363edf5f6d9c909/cytoolz-1.1.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:6965af3fc7214645970e312deb9bd35a213a1eaabcfef4f39115e60bf2f76867", size = 2917016, upload-time = "2025-10-19T00:43:33.531Z" }, + { url = "https://files.pythonhosted.org/packages/b2/7c/f1f70a17e272b433232bc8a27df97e46b202d6cc07e3b0d63f7f41ba0f2d/cytoolz-1.1.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ddd2863f321d67527d3b67a93000a378ad6f967056f68c06467fe011278a6d0e", size = 3107634, upload-time = "2025-10-19T00:43:35.57Z" }, + { url = "https://files.pythonhosted.org/packages/8f/bd/c3226a57474b4aef1f90040510cba30d0decd3515fed48dc229b37c2f898/cytoolz-1.1.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:4e6b428e9eb5126053c2ae0efa62512ff4b38ed3951f4d0888ca7005d63e56f5", size = 2806221, upload-time = "2025-10-19T00:43:37.707Z" }, + { url = "https://files.pythonhosted.org/packages/c3/47/2f7bfe4aaa1e07dc9828bea228ed744faf73b26aee0c1bdf3b5520bf1909/cytoolz-1.1.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d758e5ef311d2671e0ae8c214c52e44617cf1e58bef8f022b547b9802a5a7f30", size = 3107671, upload-time = "2025-10-19T00:43:39.401Z" }, + { url = "https://files.pythonhosted.org/packages/4d/12/6ff3b04fbd1369d0fcd5f8b5910ba6e427e33bf113754c4c35ec3f747924/cytoolz-1.1.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a95416eca473e6c1179b48d86adcf528b59c63ce78f4cb9934f2e413afa9b56b", size = 3176350, upload-time = "2025-10-19T00:43:41.148Z" }, + { url = "https://files.pythonhosted.org/packages/e6/8c/6691d986b728e77b5d2872743ebcd962d37a2d0f7e9ad95a81b284fbf905/cytoolz-1.1.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:36c8ede93525cf11e2cc787b7156e5cecd7340193ef800b816a16f1404a8dc6d", size = 3001173, upload-time = "2025-10-19T00:43:42.923Z" }, + { url = "https://files.pythonhosted.org/packages/7a/cb/f59d83a5058e1198db5a1f04e4a124c94d60390e4fa89b6d2e38ee8288a0/cytoolz-1.1.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c0c949755b6d8a649c5fbc888bc30915926f1b09fe42fea9f289e297c2f6ddd3", size = 2701374, upload-time = "2025-10-19T00:43:44.716Z" }, + { url = "https://files.pythonhosted.org/packages/b7/f0/1ae6d28df503b0bdae094879da2072b8ba13db5919cd3798918761578411/cytoolz-1.1.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e1b6d37545816905a76d9ed59fa4e332f929e879f062a39ea0f6f620405cdc27", size = 2953081, upload-time = "2025-10-19T00:43:47.103Z" }, + { url = "https://files.pythonhosted.org/packages/f4/06/d86fe811c6222dc32d3e08f5d88d2be598a6055b4d0590e7c1428d55c386/cytoolz-1.1.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:05332112d4087904842b36954cd1d3fc0e463a2f4a7ef9477bd241427c593c3b", size = 2862228, upload-time = "2025-10-19T00:43:49.353Z" }, + { url = "https://files.pythonhosted.org/packages/ae/32/978ef6f42623be44a0a03ae9de875ab54aa26c7e38c5c4cd505460b0927d/cytoolz-1.1.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:31538ca2fad2d688cbd962ccc3f1da847329e2258a52940f10a2ac0719e526be", size = 2861971, upload-time = "2025-10-19T00:43:51.028Z" }, + { url = "https://files.pythonhosted.org/packages/ee/f7/74c69497e756b752b359925d1feef68b91df024a4124a823740f675dacd3/cytoolz-1.1.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:747562aa70abf219ea16f07d50ac0157db856d447f7f498f592e097cbc77df0b", size = 2975304, upload-time = "2025-10-19T00:43:52.99Z" }, + { url = "https://files.pythonhosted.org/packages/5b/2b/3ce0e6889a6491f3418ad4d84ae407b8456b02169a5a1f87990dbba7433b/cytoolz-1.1.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:3dc15c48b20c0f467e15e341e102896c8422dccf8efc6322def5c1b02f074629", size = 2697371, upload-time = "2025-10-19T00:43:55.312Z" }, + { url = "https://files.pythonhosted.org/packages/15/87/c616577f0891d97860643c845f7221e95240aa589586de727e28a5eb6e52/cytoolz-1.1.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:3c03137ee6103ba92d5d6ad6a510e86fded69cd67050bd8a1843f15283be17ac", size = 2992436, upload-time = "2025-10-19T00:43:57.253Z" }, + { url = "https://files.pythonhosted.org/packages/e7/9f/490c81bffb3428ab1fa114051fbb5ba18aaa2e2fe4da5bf4170ca524e6b3/cytoolz-1.1.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:be8e298d88f88bd172b59912240558be3b7a04959375646e7fd4996401452941", size = 2917612, upload-time = "2025-10-19T00:43:59.423Z" }, + { url = "https://files.pythonhosted.org/packages/66/35/0fec2769660ca6472bbf3317ab634675827bb706d193e3240aaf20eab961/cytoolz-1.1.0-cp314-cp314t-win32.whl", hash = "sha256:3d407140f5604a89578285d4aac7b18b8eafa055cf776e781aabb89c48738fad", size = 960842, upload-time = "2025-10-19T00:44:01.143Z" }, + { url = "https://files.pythonhosted.org/packages/46/b4/b7ce3d3cd20337becfec978ecfa6d0ef64884d0cf32d44edfed8700914b9/cytoolz-1.1.0-cp314-cp314t-win_amd64.whl", hash = "sha256:56e5afb69eb6e1b3ffc34716ee5f92ffbdb5cb003b3a5ca4d4b0fe700e217162", size = 1020835, upload-time = "2025-10-19T00:44:03.246Z" }, + { url = "https://files.pythonhosted.org/packages/2c/1f/0498009aa563a9c5d04f520aadc6e1c0942434d089d0b2f51ea986470f55/cytoolz-1.1.0-cp314-cp314t-win_arm64.whl", hash = "sha256:27b19b4a286b3ff52040efa42dbe403730aebe5fdfd2def704eb285e2125c63e", size = 927963, upload-time = "2025-10-19T00:44:04.85Z" }, + { url = "https://files.pythonhosted.org/packages/84/32/0522207170294cf691112a93c70a8ef942f60fa9ff8e793b63b1f09cedc0/cytoolz-1.1.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:f32e93a55681d782fc6af939f6df36509d65122423cbc930be39b141064adff8", size = 922014, upload-time = "2025-10-19T00:44:44.911Z" }, + { url = "https://files.pythonhosted.org/packages/4c/49/9be2d24adaa18fa307ff14e3e43f02b2ae4b69c4ce51cee6889eb2114990/cytoolz-1.1.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:5d9bc596751cbda8073e65be02ca11706f00029768fbbbc81e11a8c290bb41aa", size = 918134, upload-time = "2025-10-19T00:44:47.122Z" }, + { url = "https://files.pythonhosted.org/packages/5c/b3/6a76c3b94c6c87c72ea822e7e67405be6b649c2e37778eeac7c0c0c69de8/cytoolz-1.1.0-pp311-pypy311_pp73-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:9b16660d01c3931951fab49db422c627897c38c1a1f0393a97582004019a4887", size = 981970, upload-time = "2025-10-19T00:44:48.906Z" }, + { url = "https://files.pythonhosted.org/packages/f6/8a/606e4c7ed14aa6a86aee6ca84a2cb804754dc6c4905b8f94e09e49f1ce60/cytoolz-1.1.0-pp311-pypy311_pp73-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b7de5718e2113d4efccea3f06055758cdbc17388ecc3341ba4d1d812837d7c1a", size = 978877, upload-time = "2025-10-19T00:44:50.819Z" }, + { url = "https://files.pythonhosted.org/packages/97/ec/ad474dcb1f6c1ebfdda3c2ad2edbb1af122a0e79c9ff2cb901ffb5f59662/cytoolz-1.1.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a12a2a1a6bc44099491c05a12039efa08cc33a3d0f8c7b0566185e085e139283", size = 964279, upload-time = "2025-10-19T00:44:52.476Z" }, + { url = "https://files.pythonhosted.org/packages/68/8c/d245fd416c69d27d51f14d5ad62acc4ee5971088ee31c40ffe1cc109af68/cytoolz-1.1.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:047defa7f5f9a32f82373dbc3957289562e8a3fa58ae02ec8e4dca4f43a33a21", size = 916630, upload-time = "2025-10-19T00:44:54.059Z" }, +] + +[[package]] +name = "dnspython" +version = "2.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8c/8b/57666417c0f90f08bcafa776861060426765fdb422eb10212086fb811d26/dnspython-2.8.0.tar.gz", hash = "sha256:181d3c6996452cb1189c4046c61599b84a5a86e099562ffde77d26984ff26d0f", size = 368251, upload-time = "2025-09-07T18:58:00.022Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ba/5a/18ad964b0086c6e62e2e7500f7edc89e3faa45033c71c1893d34eed2b2de/dnspython-2.8.0-py3-none-any.whl", hash = "sha256:01d9bbc4a2d76bf0db7c1f729812ded6d912bd318d3b1cf81d30c0f845dbf3af", size = 331094, upload-time = "2025-09-07T18:57:58.071Z" }, +] + +[[package]] +name = "email-validator" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "dnspython" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f5/22/900cb125c76b7aaa450ce02fd727f452243f2e91a61af068b40adba60ea9/email_validator-2.3.0.tar.gz", hash = "sha256:9fc05c37f2f6cf439ff414f8fc46d917929974a82244c20eb10231ba60c54426", size = 51238, upload-time = "2025-08-26T13:09:06.831Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/de/15/545e2b6cf2e3be84bc1ed85613edd75b8aea69807a71c26f4ca6a9258e82/email_validator-2.3.0-py3-none-any.whl", hash = "sha256:80f13f623413e6b197ae73bb10bf4eb0908faf509ad8362c5edeb0be7fd450b4", size = 35604, upload-time = "2025-08-26T13:09:05.858Z" }, +] + +[[package]] +name = "eth-abi" +version = "5.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "eth-typing" }, + { name = "eth-utils" }, + { name = "parsimonious" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/00/71/d9e1380bd77fd22f98b534699af564f189b56d539cc2b9dab908d4e4c242/eth_abi-5.2.0.tar.gz", hash = "sha256:178703fa98c07d8eecd5ae569e7e8d159e493ebb6eeb534a8fe973fbc4e40ef0", size = 49797, upload-time = "2025-01-14T16:29:34.629Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7a/b4/2f3982c4cbcbf5eeb6aec62df1533c0e63c653b3021ff338d44944405676/eth_abi-5.2.0-py3-none-any.whl", hash = "sha256:17abe47560ad753f18054f5b3089fcb588f3e3a092136a416b6c1502cb7e8877", size = 28511, upload-time = "2025-01-14T16:29:31.862Z" }, +] + +[[package]] +name = "eth-account" +version = "0.13.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "bitarray" }, + { name = "ckzg" }, + { name = "eth-abi" }, + { name = "eth-keyfile" }, + { name = "eth-keys" }, + { name = "eth-rlp" }, + { name = "eth-utils" }, + { name = "hexbytes" }, + { name = "pydantic" }, + { name = "rlp" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/74/cf/20f76a29be97339c969fd765f1237154286a565a1d61be98e76bb7af946a/eth_account-0.13.7.tar.gz", hash = "sha256:5853ecbcbb22e65411176f121f5f24b8afeeaf13492359d254b16d8b18c77a46", size = 935998, upload-time = "2025-04-21T21:11:21.204Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/46/18/088fb250018cbe665bc2111974301b2d59f294a565aff7564c4df6878da2/eth_account-0.13.7-py3-none-any.whl", hash = "sha256:39727de8c94d004ff61d10da7587509c04d2dc7eac71e04830135300bdfc6d24", size = 587452, upload-time = "2025-04-21T21:11:18.346Z" }, +] + +[[package]] +name = "eth-hash" +version = "0.7.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/38/577b7bc9380ef9dff0f1dffefe0c9a1ded2385e7a06c306fd95afb6f9451/eth_hash-0.7.1.tar.gz", hash = "sha256:d2411a403a0b0a62e8247b4117932d900ffb4c8c64b15f92620547ca5ce46be5", size = 12227, upload-time = "2025-01-13T21:29:21.765Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/eb/db/f8775490669d28aca24871c67dd56b3e72105cb3bcae9a4ec65dd70859b3/eth_hash-0.7.1-py3-none-any.whl", hash = "sha256:0fb1add2adf99ef28883fd6228eb447ef519ea72933535ad1a0b28c6f65f868a", size = 8028, upload-time = "2025-01-13T21:29:19.365Z" }, +] + +[package.optional-dependencies] +pycryptodome = [ + { name = "pycryptodome" }, +] + +[[package]] +name = "eth-keyfile" +version = "0.8.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "eth-keys" }, + { name = "eth-utils" }, + { name = "pycryptodome" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/35/66/dd823b1537befefbbff602e2ada88f1477c5b40ec3731e3d9bc676c5f716/eth_keyfile-0.8.1.tar.gz", hash = "sha256:9708bc31f386b52cca0969238ff35b1ac72bd7a7186f2a84b86110d3c973bec1", size = 12267, upload-time = "2024-04-23T20:28:53.862Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/fc/48a586175f847dd9e05e5b8994d2fe8336098781ec2e9836a2ad94280281/eth_keyfile-0.8.1-py3-none-any.whl", hash = "sha256:65387378b82fe7e86d7cb9f8d98e6d639142661b2f6f490629da09fddbef6d64", size = 7510, upload-time = "2024-04-23T20:28:51.063Z" }, +] + +[[package]] +name = "eth-keys" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "eth-typing" }, + { name = "eth-utils" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/58/11/1ed831c50bd74f57829aa06e58bd82a809c37e070ee501c953b9ac1f1552/eth_keys-0.7.0.tar.gz", hash = "sha256:79d24fd876201df67741de3e3fefb3f4dbcbb6ace66e47e6fe662851a4547814", size = 30166, upload-time = "2025-04-07T17:40:21.697Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4d/25/0ae00f2b0095e559d61ad3dc32171bd5a29dfd95ab04b4edd641f7c75f72/eth_keys-0.7.0-py3-none-any.whl", hash = "sha256:b0cdda8ffe8e5ba69c7c5ca33f153828edcace844f67aabd4542d7de38b159cf", size = 20656, upload-time = "2025-04-07T17:40:20.441Z" }, +] + +[[package]] +name = "eth-rlp" +version = "2.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "eth-utils" }, + { name = "hexbytes" }, + { name = "rlp" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7f/ea/ad39d001fa9fed07fad66edb00af701e29b48be0ed44a3bcf58cb3adf130/eth_rlp-2.2.0.tar.gz", hash = "sha256:5e4b2eb1b8213e303d6a232dfe35ab8c29e2d3051b86e8d359def80cd21db83d", size = 7720, upload-time = "2025-02-04T21:51:08.134Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/99/3b/57efe2bc2df0980680d57c01a36516cd3171d2319ceb30e675de19fc2cc5/eth_rlp-2.2.0-py3-none-any.whl", hash = "sha256:5692d595a741fbaef1203db6a2fedffbd2506d31455a6ad378c8449ee5985c47", size = 4446, upload-time = "2025-02-04T21:51:05.823Z" }, +] + +[[package]] +name = "eth-typing" +version = "5.2.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/60/54/62aa24b9cc708f06316167ee71c362779c8ed21fc8234a5cd94a8f53b623/eth_typing-5.2.1.tar.gz", hash = "sha256:7557300dbf02a93c70fa44af352b5c4a58f94e997a0fd6797fb7d1c29d9538ee", size = 21806, upload-time = "2025-04-14T20:39:28.217Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/30/72/c370bbe4c53da7bf998d3523f5a0f38867654923a82192df88d0705013d3/eth_typing-5.2.1-py3-none-any.whl", hash = "sha256:b0c2812ff978267563b80e9d701f487dd926f1d376d674f3b535cfe28b665d3d", size = 19163, upload-time = "2025-04-14T20:39:26.571Z" }, +] + +[[package]] +name = "eth-utils" +version = "5.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cytoolz", marker = "implementation_name == 'cpython'" }, + { name = "eth-hash" }, + { name = "eth-typing" }, + { name = "pydantic" }, + { name = "toolz", marker = "implementation_name == 'pypy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e6/e1/ee3a8728227c3558853e63ff35bd4c449abdf5022a19601369400deacd39/eth_utils-5.3.1.tar.gz", hash = "sha256:c94e2d2abd024a9a42023b4ddc1c645814ff3d6a737b33d5cfd890ebf159c2d1", size = 123506, upload-time = "2025-08-27T16:37:17.378Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bf/4d/257cdc01ada430b8e84b9f2385c2553f33218f5b47da9adf0a616308d4b7/eth_utils-5.3.1-py3-none-any.whl", hash = "sha256:1f5476d8f29588d25b8ae4987e1ffdfae6d4c09026e476c4aad13b32dda3ead0", size = 102529, upload-time = "2025-08-27T16:37:15.449Z" }, +] + +[[package]] +name = "exceptiongroup" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" }, +] + +[[package]] +name = "fastapi" +version = "0.128.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-doc" }, + { name = "pydantic" }, + { name = "starlette" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/52/08/8c8508db6c7b9aae8f7175046af41baad690771c9bcde676419965e338c7/fastapi-0.128.0.tar.gz", hash = "sha256:1cc179e1cef10a6be60ffe429f79b829dce99d8de32d7acb7e6c8dfdf7f2645a", size = 365682, upload-time = "2025-12-27T15:21:13.714Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5c/05/5cbb59154b093548acd0f4c7c474a118eda06da25aa75c616b72d8fcd92a/fastapi-0.128.0-py3-none-any.whl", hash = "sha256:aebd93f9716ee3b4f4fcfe13ffb7cf308d99c9f3ab5622d8877441072561582d", size = 103094, upload-time = "2025-12-27T15:21:12.154Z" }, +] + +[package.optional-dependencies] +standard = [ + { name = "email-validator" }, + { name = "fastapi-cli", extra = ["standard"] }, + { name = "httpx" }, + { name = "jinja2" }, + { name = "pydantic-extra-types" }, + { name = "pydantic-settings" }, + { name = "python-multipart" }, + { name = "uvicorn", extra = ["standard"] }, +] + +[[package]] +name = "fastapi-cli" +version = "0.0.20" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "rich-toolkit" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, + { name = "typer" }, + { name = "uvicorn", extra = ["standard"] }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d3/ca/d90fb3bfbcbd6e56c77afd9d114dd6ce8955d8bb90094399d1c70e659e40/fastapi_cli-0.0.20.tar.gz", hash = "sha256:d17c2634f7b96b6b560bc16b0035ed047d523c912011395f49f00a421692bc3a", size = 19786, upload-time = "2025-12-22T17:13:33.794Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/08/89/5c4eef60524d0fd704eb0706885b82cd5623a43396b94e4a5b17d3a3f516/fastapi_cli-0.0.20-py3-none-any.whl", hash = "sha256:e58b6a0038c0b1532b7a0af690656093dee666201b6b19d3c87175b358e9f783", size = 12390, upload-time = "2025-12-22T17:13:31.708Z" }, +] + +[package.optional-dependencies] +standard = [ + { name = "fastapi-cloud-cli" }, + { name = "uvicorn", extra = ["standard"] }, +] + +[[package]] +name = "fastapi-cloud-cli" +version = "0.8.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "fastar" }, + { name = "httpx" }, + { name = "pydantic", extra = ["email"] }, + { name = "rich-toolkit" }, + { name = "rignore" }, + { name = "sentry-sdk" }, + { name = "typer" }, + { name = "uvicorn", extra = ["standard"] }, +] +sdist = { url = "https://files.pythonhosted.org/packages/51/5d/3b33438de35521fab4968b232caa9a4bd568a5078f2b2dfb7bb8a4528603/fastapi_cloud_cli-0.8.0.tar.gz", hash = "sha256:cf07c502528bfd9e6b184776659f05d9212811d76bbec9fbb6bf34bed4c7456f", size = 30257, upload-time = "2025-12-23T12:08:33.904Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dd/8e/abb95ef59e91bb5adaa2d18fbf9ea70fd524010bb03f406a2dd2a4775ef9/fastapi_cloud_cli-0.8.0-py3-none-any.whl", hash = "sha256:e9f40bee671d985fd25d7a5409b56d4f103777bf8a0c6d746ea5fbf97a8186d9", size = 22306, upload-time = "2025-12-23T12:08:32.68Z" }, +] + +[[package]] +name = "fastar" +version = "0.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/69/e7/f89d54fb04104114dd0552836dc2b47914f416cc0e200b409dd04a33de5e/fastar-0.8.0.tar.gz", hash = "sha256:f4d4d68dbf1c4c2808f0e730fac5843493fc849f70fe3ad3af60dfbaf68b9a12", size = 68524, upload-time = "2025-11-26T02:36:00.72Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c3/e2/51d9ee443aabcd5aa581d45b18b6198ced364b5cd97e5504c5d782ceb82c/fastar-0.8.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:c9f930cff014cf79d396d0541bd9f3a3f170c9b5e45d10d634d98f9ed08788c3", size = 708536, upload-time = "2025-11-26T02:34:35.236Z" }, + { url = "https://files.pythonhosted.org/packages/07/2a/edfc6274768b8a3859a5ca4f8c29cb7f614d7f27d2378e2c88aa91cda54e/fastar-0.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:07b70f712d20622346531a4b46bb332569bea621f61314c0b7e80903a16d14cf", size = 632235, upload-time = "2025-11-26T02:34:19.367Z" }, + { url = "https://files.pythonhosted.org/packages/ef/1e/3cfbaaec464caef196700ee2ffae1c03f94f7c5e2a85d0ec0ea9cdd1da81/fastar-0.8.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:330639db3bfba4c6d132421a2a4aeb81e7bea8ce9159cdb6e247fbc5fae97686", size = 871386, upload-time = "2025-11-26T02:33:47.613Z" }, + { url = "https://files.pythonhosted.org/packages/82/50/224a674ad541054179e4e6e0b54bb6e162f04f698a2512b42a8085fc6b6f/fastar-0.8.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:98ea7ceb6231e48d7bb0d7dc13e946baa29c7f6873eaf4afb69725d6da349033", size = 764955, upload-time = "2025-11-26T02:32:44.279Z" }, + { url = "https://files.pythonhosted.org/packages/4d/5e/4608184aa57cb6a54f62c1eb3e5133ba8d461fc7f13193c0255effbec12a/fastar-0.8.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a90695a601a78bbca910fdf2efcdf3103c55d0de5a5c6e93556d707bf886250b", size = 765987, upload-time = "2025-11-26T02:32:59.701Z" }, + { url = "https://files.pythonhosted.org/packages/e0/53/6afd2b680dddfa10df9a16bbcf6cabfee0d92435d5c7e3f4cfe3b1712662/fastar-0.8.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9d0bf655ff4c9320b0ca8a5b128063d5093c0c8c1645a2b5f7167143fd8531aa", size = 930900, upload-time = "2025-11-26T02:33:16.059Z" }, + { url = "https://files.pythonhosted.org/packages/ef/1e/b7a304bfcc1d06845cbfa4b464516f6fff9c8c6692f6ef80a3a86b04e199/fastar-0.8.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d8df22cdd8d58e7689aa89b2e4a07e8e5fa4f88d2d9c2621f0e88a49be97ccea", size = 821523, upload-time = "2025-11-26T02:33:30.897Z" }, + { url = "https://files.pythonhosted.org/packages/1d/da/9ef8605c6d233cd6ca3a95f7f518ac22aa064903afe6afa57733bfb7c31b/fastar-0.8.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e8a5e6ad722685128521c8fb44cf25bd38669650ba3a4b466b8903e5aa28e1a0", size = 821268, upload-time = "2025-11-26T02:34:04.003Z" }, + { url = "https://files.pythonhosted.org/packages/7e/22/ed37c78a6b4420de1677d82e79742787975c34847229c33dc376334c7283/fastar-0.8.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:31cd541231a2456e32104da891cf9962c3b40234d0465cbf9322a6bc8a1b05d5", size = 986286, upload-time = "2025-11-26T02:34:50.279Z" }, + { url = "https://files.pythonhosted.org/packages/ca/a6/366b15f432d85d4089e6e4b52a09cc2a2bcf4d7a1f0771e3d3194deccb1e/fastar-0.8.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:175db2a98d67ced106468e8987975484f8bbbd5ad99201da823b38bafb565ed5", size = 1041921, upload-time = "2025-11-26T02:35:07.292Z" }, + { url = "https://files.pythonhosted.org/packages/f4/45/45f8e6991e3ce9f8aeefdc8d4c200daada41097a36808643d1703464c3e2/fastar-0.8.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:ada877ab1c65197d772ce1b1c2e244d4799680d8b3f136a4308360f3d8661b23", size = 1047302, upload-time = "2025-11-26T02:35:24.995Z" }, + { url = "https://files.pythonhosted.org/packages/c2/e2/a587796111a3cd4b78cd61ec3fc1252d8517d81f763f4164ed5680f84810/fastar-0.8.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:01084cb75f13ca6a8e80bd41584322523189f8e81b472053743d6e6c3062b5a6", size = 995141, upload-time = "2025-11-26T02:35:42.449Z" }, + { url = "https://files.pythonhosted.org/packages/89/c0/7a8ec86695b0b77168e220cf2af1aa30592f5ecdbd0ce6d641d29c4a8bae/fastar-0.8.0-cp310-cp310-win32.whl", hash = "sha256:ca639b9909805e44364ea13cca2682b487e74826e4ad75957115ec693228d6b6", size = 456544, upload-time = "2025-11-26T02:36:23.801Z" }, + { url = "https://files.pythonhosted.org/packages/be/a9/8da4deb840121c59deabd939ce2dca3d6beec85576f3743d1144441938b5/fastar-0.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:fbc0f2ed0f4add7fb58034c576584d44d7eaaf93dee721dfb26dbed6e222dbac", size = 490701, upload-time = "2025-11-26T02:36:09.625Z" }, + { url = "https://files.pythonhosted.org/packages/cd/15/1c764530b81b266f6d27d78d49b6bef22a73b3300cd83a280bfd244908c5/fastar-0.8.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:cd9c0d3ebf7a0a6f642f771cf41b79f7c98d40a3072a8abe1174fbd9bd615bd3", size = 708427, upload-time = "2025-11-26T02:34:36.502Z" }, + { url = "https://files.pythonhosted.org/packages/41/fc/75d42c008516543219e4293e4d8ac55da57a5c63147484f10468bd1bc24e/fastar-0.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2875a077340fe4f8099bd3ed8fa90d9595e1ac3cd62ae19ab690d5bf550eeb35", size = 631740, upload-time = "2025-11-26T02:34:20.718Z" }, + { url = "https://files.pythonhosted.org/packages/50/8d/9632984f7824ed2210157dcebd8e9821ef6d4f2b28510d0516db6625ff9b/fastar-0.8.0-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:a999263d9f87184bf2801833b2ecf105e03c0dd91cac78685673b70da564fd64", size = 871628, upload-time = "2025-11-26T02:33:49.279Z" }, + { url = "https://files.pythonhosted.org/packages/05/97/3eb6ea71b7544d45cd29cacb764ca23cde8ce0aed1a6a02251caa4c0a818/fastar-0.8.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c41111da56430f638cbfc498ebdcc7d30f63416e904b27b7695c29bd4889cb8", size = 765005, upload-time = "2025-11-26T02:32:45.833Z" }, + { url = "https://files.pythonhosted.org/packages/d6/45/3eb0ee945a0b5d5f9df7e7c25c037ce7fa441cd0b4d44f76d286e2f4396a/fastar-0.8.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3719541a12bb09ab1eae91d2c987a9b2b7d7149c52e7109ba6e15b74aabc49b1", size = 765587, upload-time = "2025-11-26T02:33:01.174Z" }, + { url = "https://files.pythonhosted.org/packages/51/bb/7defd6ec0d9570b1987d8ebde52d07d97f3f26e10b592fb3e12738eba39a/fastar-0.8.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7a9b0fff8079b18acdface7ef1b7f522fd9a589f65ca4a1a0dd7c92a0886c2a2", size = 931150, upload-time = "2025-11-26T02:33:17.374Z" }, + { url = "https://files.pythonhosted.org/packages/28/54/62e51e684dab347c61878afbf09e177029c1a91eb1e39ef244e6b3ef9efa/fastar-0.8.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ac073576c1931959191cb20df38bab21dd152f66c940aa3ca8b22e39f753b2f3", size = 821354, upload-time = "2025-11-26T02:33:32.083Z" }, + { url = "https://files.pythonhosted.org/packages/53/a8/12708ea4d21e3cf9f485b2a67d44ce84d949a6eddcc9aa5b3d324585ab43/fastar-0.8.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:003b59a7c3e405b6a7bff8fab17d31e0ccbc7f06730a8f8ca1694eeea75f3c76", size = 821626, upload-time = "2025-11-26T02:34:05.685Z" }, + { url = "https://files.pythonhosted.org/packages/e7/c4/1b4d3347c7a759853f963410bf6baf42fe014d587c50c39c8e145f4bf1a0/fastar-0.8.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a7b96748425efd9fc155cd920d65088a1b0d754421962418ea73413d02ff515a", size = 986187, upload-time = "2025-11-26T02:34:52.047Z" }, + { url = "https://files.pythonhosted.org/packages/dc/59/2dbe0dc2570764475e60030403738faa261a9d3bff16b08629c378ab939a/fastar-0.8.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:90957a30e64418b02df5b4d525bea50403d98a4b1f29143ce5914ddfa7e54ee4", size = 1041536, upload-time = "2025-11-26T02:35:08.926Z" }, + { url = "https://files.pythonhosted.org/packages/d9/0f/639b295669c7ca6fbc2b4be2a7832aaeac1a5e06923f15a8a6d6daecbc7d/fastar-0.8.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:f6e784a8015623fbb7ccca1af372fd82cb511b408ddd2348dc929fc6e415df73", size = 1047149, upload-time = "2025-11-26T02:35:26.597Z" }, + { url = "https://files.pythonhosted.org/packages/cb/e7/23e3a19e06d261d1894f98eca9458f98c090c505a0c712dafc0ff1fc2965/fastar-0.8.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a03eaf287bbc93064688a1220580ce261e7557c8898f687f4d0b281c85b28d3c", size = 994992, upload-time = "2025-11-26T02:35:44.009Z" }, + { url = "https://files.pythonhosted.org/packages/f2/7a/3ea4726bae3ac9358d02107ae48f3e10ee186dbed554af79e00b7b498c44/fastar-0.8.0-cp311-cp311-win32.whl", hash = "sha256:661a47ed90762f419406c47e802f46af63a08254ba96abd1c8191e4ce967b665", size = 456449, upload-time = "2025-11-26T02:36:25.291Z" }, + { url = "https://files.pythonhosted.org/packages/cb/3c/0142bee993c431ee91cf5535e6e4b079ad491f620c215fcd79b7e5ffeb2b/fastar-0.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:b48abd6056fef7bc3d414aafb453c5b07fdf06d2df5a2841d650288a3aa1e9d3", size = 490863, upload-time = "2025-11-26T02:36:11.114Z" }, + { url = "https://files.pythonhosted.org/packages/3b/18/d119944f6bdbf6e722e204e36db86390ea45684a1bf6be6e3aa42abd471f/fastar-0.8.0-cp311-cp311-win_arm64.whl", hash = "sha256:50c18788b3c6ffb85e176dcb8548bb8e54616a0519dcdbbfba66f6bbc4316933", size = 462230, upload-time = "2025-11-26T02:36:01.917Z" }, + { url = "https://files.pythonhosted.org/packages/58/f1/5b2ff898abac7f1a418284aad285e3a4f68d189c572ab2db0f6c9079dd16/fastar-0.8.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:0f10d2adfe40f47ff228f4efaa32d409d732ded98580e03ed37c9535b5fc923d", size = 706369, upload-time = "2025-11-26T02:34:37.783Z" }, + { url = "https://files.pythonhosted.org/packages/23/60/8046a386dca39154f80c927cbbeeb4b1c1267a3271bffe61552eb9995757/fastar-0.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b930da9d598e3bc69513d131f397e6d6be4643926ef3de5d33d1e826631eb036", size = 629097, upload-time = "2025-11-26T02:34:21.888Z" }, + { url = "https://files.pythonhosted.org/packages/22/7e/1ae005addc789924a9268da2394d3bb5c6f96836f7e37b7e3d23c2362675/fastar-0.8.0-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:9d210da2de733ca801de83e931012349d209f38b92d9630ccaa94bd445bdc9b8", size = 868938, upload-time = "2025-11-26T02:33:51.119Z" }, + { url = "https://files.pythonhosted.org/packages/a6/77/290a892b073b84bf82e6b2259708dfe79c54f356e252c2dd40180b16fe07/fastar-0.8.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa02270721517078a5bd61a38719070ac2537a4aa6b6c48cf369cf2abc59174a", size = 765204, upload-time = "2025-11-26T02:32:47.02Z" }, + { url = "https://files.pythonhosted.org/packages/d0/00/c3155171b976003af3281f5258189f1935b15d1221bfc7467b478c631216/fastar-0.8.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:83c391e5b789a720e4d0029b9559f5d6dee3226693c5b39c0eab8eaece997e0f", size = 764717, upload-time = "2025-11-26T02:33:02.453Z" }, + { url = "https://files.pythonhosted.org/packages/b7/43/405b7ad76207b2c11b7b59335b70eac19e4a2653977f5588a1ac8fed54f4/fastar-0.8.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3258d7a78a72793cdd081545da61cabe85b1f37634a1d0b97ffee0ff11d105ef", size = 931502, upload-time = "2025-11-26T02:33:18.619Z" }, + { url = "https://files.pythonhosted.org/packages/da/8a/a3dde6d37cc3da4453f2845cdf16675b5686b73b164f37e2cc579b057c2c/fastar-0.8.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e6eab95dd985cdb6a50666cbeb9e4814676e59cfe52039c880b69d67cfd44767", size = 821454, upload-time = "2025-11-26T02:33:33.427Z" }, + { url = "https://files.pythonhosted.org/packages/da/c1/904fe2468609c8990dce9fe654df3fbc7324a8d8e80d8240ae2c89757064/fastar-0.8.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:829b1854166141860887273c116c94e31357213fa8e9fe8baeb18bd6c38aa8d9", size = 821647, upload-time = "2025-11-26T02:34:07Z" }, + { url = "https://files.pythonhosted.org/packages/c8/73/a0642ab7a400bc07528091785e868ace598fde06fcd139b8f865ec1b6f3c/fastar-0.8.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b1667eae13f9457a3c737f4376d68e8c3e548353538b28f7e4273a30cb3965cd", size = 986342, upload-time = "2025-11-26T02:34:53.371Z" }, + { url = "https://files.pythonhosted.org/packages/af/af/60c1bfa6edab72366461a95f053d0f5f7ab1825fe65ca2ca367432cd8629/fastar-0.8.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:b864a95229a7db0814cd9ef7987cb713fd43dce1b0d809dd17d9cd6f02fdde3e", size = 1040207, upload-time = "2025-11-26T02:35:10.65Z" }, + { url = "https://files.pythonhosted.org/packages/f6/a0/0d624290dec622e7fa084b6881f456809f68777d54a314f5dde932714506/fastar-0.8.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:c05fbc5618ce17675a42576fa49858d79734627f0a0c74c0875ab45ee8de340c", size = 1045031, upload-time = "2025-11-26T02:35:28.108Z" }, + { url = "https://files.pythonhosted.org/packages/a7/74/cf663af53c4706ba88e6b4af44a6b0c3bd7d7ca09f079dc40647a8f06585/fastar-0.8.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7f41c51ee96f338662ee3c3df4840511ba3f9969606840f1b10b7cb633a3c716", size = 994877, upload-time = "2025-11-26T02:35:45.797Z" }, + { url = "https://files.pythonhosted.org/packages/52/17/444c8be6e77206050e350da7c338102b6cab384be937fa0b1d6d1f9ede73/fastar-0.8.0-cp312-cp312-win32.whl", hash = "sha256:d949a1a2ea7968b734632c009df0571c94636a5e1622c87a6e2bf712a7334f47", size = 455996, upload-time = "2025-11-26T02:36:26.938Z" }, + { url = "https://files.pythonhosted.org/packages/dc/34/fc3b5e56d71a17b1904800003d9251716e8fd65f662e1b10a26881698a74/fastar-0.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:fc645994d5b927d769121094e8a649b09923b3c13a8b0b98696d8f853f23c532", size = 490429, upload-time = "2025-11-26T02:36:12.707Z" }, + { url = "https://files.pythonhosted.org/packages/35/a8/5608cc837417107c594e2e7be850b9365bcb05e99645966a5d6a156285fe/fastar-0.8.0-cp312-cp312-win_arm64.whl", hash = "sha256:d81ee82e8dc78a0adb81728383bd39611177d642a8fa2d601d4ad5ad59e5f3bd", size = 461297, upload-time = "2025-11-26T02:36:03.546Z" }, + { url = "https://files.pythonhosted.org/packages/d1/a5/79ecba3646e22d03eef1a66fb7fc156567213e2e4ab9faab3bbd4489e483/fastar-0.8.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:a3253a06845462ca2196024c7a18f5c0ba4de1532ab1c4bad23a40b332a06a6a", size = 706112, upload-time = "2025-11-26T02:34:39.237Z" }, + { url = "https://files.pythonhosted.org/packages/0a/03/4f883bce878218a8676c2d7ca09b50c856a5470bb3b7f63baf9521ea6995/fastar-0.8.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5cbeb3ebfa0980c68ff8b126295cc6b208ccd81b638aebc5a723d810a7a0e5d2", size = 628954, upload-time = "2025-11-26T02:34:23.705Z" }, + { url = "https://files.pythonhosted.org/packages/4f/f1/892e471f156b03d10ba48ace9384f5a896702a54506137462545f38e40b8/fastar-0.8.0-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:1c0d5956b917daac77d333d48b3f0f3ff927b8039d5b32d8125462782369f761", size = 868685, upload-time = "2025-11-26T02:33:53.077Z" }, + { url = "https://files.pythonhosted.org/packages/39/ba/e24915045852e30014ec6840446975c03f4234d1c9270394b51d3ad18394/fastar-0.8.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:27b404db2b786b65912927ce7f3790964a4bcbde42cdd13091b82a89cd655e1c", size = 765044, upload-time = "2025-11-26T02:32:48.187Z" }, + { url = "https://files.pythonhosted.org/packages/14/2c/1aa11ac21a99984864c2fca4994e094319ff3a2046e7a0343c39317bd5b9/fastar-0.8.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0902fc89dcf1e7f07b8563032a4159fe2b835e4c16942c76fd63451d0e5f76a3", size = 764322, upload-time = "2025-11-26T02:33:03.859Z" }, + { url = "https://files.pythonhosted.org/packages/ba/f0/4b91902af39fe2d3bae7c85c6d789586b9fbcf618d7fdb3d37323915906d/fastar-0.8.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:069347e2f0f7a8b99bbac8cd1bc0e06c7b4a31dc964fc60d84b95eab3d869dc1", size = 931016, upload-time = "2025-11-26T02:33:19.902Z" }, + { url = "https://files.pythonhosted.org/packages/c9/97/8fc43a5a9c0a2dc195730f6f7a0f367d171282cd8be2511d0e87c6d2dad0/fastar-0.8.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7fd135306f6bfe9a835918280e0eb440b70ab303e0187d90ab51ca86e143f70d", size = 821308, upload-time = "2025-11-26T02:33:34.664Z" }, + { url = "https://files.pythonhosted.org/packages/0c/e9/058615b63a7fd27965e8c5966f393ed0c169f7ff5012e1674f21684de3ba/fastar-0.8.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:78d06d6897f43c27154b5f2d0eb930a43a81b7eec73f6f0b0114814d4a10ab38", size = 821171, upload-time = "2025-11-26T02:34:08.498Z" }, + { url = "https://files.pythonhosted.org/packages/ca/cf/69e16a17961570a755c37ffb5b5aa7610d2e77807625f537989da66f2a9d/fastar-0.8.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a922f8439231fa0c32b15e8d70ff6d415619b9d40492029dabbc14a0c53b5f18", size = 986227, upload-time = "2025-11-26T02:34:55.06Z" }, + { url = "https://files.pythonhosted.org/packages/fb/83/2100192372e59b56f4ace37d7d9cabda511afd71b5febad1643d1c334271/fastar-0.8.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:a739abd51eb766384b4caff83050888e80cd75bbcfec61e6d1e64875f94e4a40", size = 1039395, upload-time = "2025-11-26T02:35:12.166Z" }, + { url = "https://files.pythonhosted.org/packages/75/15/cdd03aca972f55872efbb7cf7540c3fa7b97a75d626303a3ea46932163dc/fastar-0.8.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:5a65f419d808b23ac89d5cd1b13a2f340f15bc5d1d9af79f39fdb77bba48ff1b", size = 1044766, upload-time = "2025-11-26T02:35:29.62Z" }, + { url = "https://files.pythonhosted.org/packages/3d/29/945e69e4e2652329ace545999334ec31f1431fbae3abb0105587e11af2ae/fastar-0.8.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7bb2ae6c0cce58f0db1c9f20495e7557cca2c1ee9c69bbd90eafd54f139171c5", size = 994740, upload-time = "2025-11-26T02:35:47.887Z" }, + { url = "https://files.pythonhosted.org/packages/4b/5d/dbfe28f8cd1eb484bba0c62e5259b2cf6fea229d6ef43e05c06b5a78c034/fastar-0.8.0-cp313-cp313-win32.whl", hash = "sha256:b28753e0d18a643272597cb16d39f1053842aa43131ad3e260c03a2417d38401", size = 455990, upload-time = "2025-11-26T02:36:28.502Z" }, + { url = "https://files.pythonhosted.org/packages/e1/01/e965740bd36e60ef4c5aa2cbe42b6c4eb1dc3551009238a97c2e5e96bd23/fastar-0.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:620e5d737dce8321d49a5ebb7997f1fd0047cde3512082c27dc66d6ac8c1927a", size = 490227, upload-time = "2025-11-26T02:36:14.363Z" }, + { url = "https://files.pythonhosted.org/packages/dd/10/c99202719b83e5249f26902ae53a05aea67d840eeb242019322f20fc171c/fastar-0.8.0-cp313-cp313-win_arm64.whl", hash = "sha256:c4c4bd08df563120cd33e854fe0a93b81579e8571b11f9b7da9e84c37da2d6b6", size = 461078, upload-time = "2025-11-26T02:36:04.94Z" }, + { url = "https://files.pythonhosted.org/packages/96/4a/9573b87a0ef07580ed111e7230259aec31bb33ca3667963ebee77022ec61/fastar-0.8.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:50b36ce654ba44b0e13fae607ae17ee6e1597b69f71df1bee64bb8328d881dfc", size = 706041, upload-time = "2025-11-26T02:34:40.638Z" }, + { url = "https://files.pythonhosted.org/packages/4a/19/f95444a1d4f375333af49300aa75ee93afa3335c0e40fda528e460ed859c/fastar-0.8.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:63a892762683d7ab00df0227d5ea9677c62ff2cde9b875e666c0be569ed940f3", size = 628617, upload-time = "2025-11-26T02:34:24.893Z" }, + { url = "https://files.pythonhosted.org/packages/b3/c9/b51481b38b7e3f16ef2b9e233b1a3623386c939d745d6e41bbd389eaae30/fastar-0.8.0-cp314-cp314-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:4ae6a145c1bff592644bde13f2115e0239f4b7babaf506d14e7d208483cf01a5", size = 869299, upload-time = "2025-11-26T02:33:54.274Z" }, + { url = "https://files.pythonhosted.org/packages/bf/02/3ba1267ee5ba7314e29c431cf82eaa68586f2c40cdfa08be3632b7d07619/fastar-0.8.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ae0ff7c0a1c7e1428404b81faee8aebef466bfd0be25bfe4dabf5d535c68741", size = 764667, upload-time = "2025-11-26T02:32:49.606Z" }, + { url = "https://files.pythonhosted.org/packages/1b/84/bf33530fd015b5d7c2cc69e0bce4a38d736754a6955487005aab1af6adcd/fastar-0.8.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:dbfd87dbd217b45c898b2dbcd0169aae534b2c1c5cbe3119510881f6a5ac8ef5", size = 763993, upload-time = "2025-11-26T02:33:05.782Z" }, + { url = "https://files.pythonhosted.org/packages/da/e0/9564d24e7cea6321a8d921c6d2a457044a476ef197aa4708e179d3d97f0d/fastar-0.8.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9a5abd99fcba83ef28c8fe6ae2927edc79053db43a0457a962ed85c9bf150d37", size = 930153, upload-time = "2025-11-26T02:33:21.53Z" }, + { url = "https://files.pythonhosted.org/packages/35/b1/6f57fcd8d6e192cfebf97e58eb27751640ad93784c857b79039e84387b51/fastar-0.8.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:91d4c685620c3a9d6b5ae091dbabab4f98b20049b7ecc7976e19cc9016c0d5d6", size = 821177, upload-time = "2025-11-26T02:33:35.839Z" }, + { url = "https://files.pythonhosted.org/packages/b3/78/9e004ea9f3aa7466f5ddb6f9518780e1d2f0ed3ca55f093632982598bace/fastar-0.8.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f77c2f2cad76e9dc7b6701297adb1eba87d0485944b416fc2ccf5516c01219a3", size = 820652, upload-time = "2025-11-26T02:34:09.776Z" }, + { url = "https://files.pythonhosted.org/packages/42/95/b604ed536544005c9f1aee7c4c74b00150db3d8d535cd8232dc20f947063/fastar-0.8.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e7f07c4a3dada7757a8fc430a5b4a29e6ef696d2212747213f57086ffd970316", size = 985961, upload-time = "2025-11-26T02:34:56.401Z" }, + { url = "https://files.pythonhosted.org/packages/f2/7b/fa9d4d96a5d494bdb8699363bb9de8178c0c21a02e1d89cd6f913d127018/fastar-0.8.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:90c0c3fe55105c0aed8a83135dbdeb31e683455dbd326a1c48fa44c378b85616", size = 1039316, upload-time = "2025-11-26T02:35:13.807Z" }, + { url = "https://files.pythonhosted.org/packages/4e/f9/8462789243bc3f33e8401378ec6d54de4e20cfa60c96a0e15e3e9d1389bb/fastar-0.8.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:fb9ee51e5bffe0dab3d3126d3a4fac8d8f7235cedcb4b8e74936087ce1c157f3", size = 1045028, upload-time = "2025-11-26T02:35:31.079Z" }, + { url = "https://files.pythonhosted.org/packages/a5/71/9abb128777e616127194b509e98fcda3db797d76288c1a8c23dd22afc14f/fastar-0.8.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e380b1e8d30317f52406c43b11e98d11e1d68723bbd031e18049ea3497b59a6d", size = 994677, upload-time = "2025-11-26T02:35:49.391Z" }, + { url = "https://files.pythonhosted.org/packages/de/c1/b81b3f194853d7ad232a67a1d768f5f51a016f165cfb56cb31b31bbc6177/fastar-0.8.0-cp314-cp314-win32.whl", hash = "sha256:1c4ffc06e9c4a8ca498c07e094670d8d8c0d25b17ca6465b9774da44ea997ab1", size = 456687, upload-time = "2025-11-26T02:36:30.205Z" }, + { url = "https://files.pythonhosted.org/packages/cb/87/9e0cd4768a98181d56f0cdbab2363404cc15deb93f4aad3b99cd2761bbaa/fastar-0.8.0-cp314-cp314-win_amd64.whl", hash = "sha256:5517a8ad4726267c57a3e0e2a44430b782e00b230bf51c55b5728e758bb3a692", size = 490578, upload-time = "2025-11-26T02:36:16.218Z" }, + { url = "https://files.pythonhosted.org/packages/aa/1e/580a76cf91847654f2ad6520e956e93218f778540975bc4190d363f709e2/fastar-0.8.0-cp314-cp314-win_arm64.whl", hash = "sha256:58030551046ff4a8616931e52a36c83545ff05996db5beb6e0cd2b7e748aa309", size = 461473, upload-time = "2025-11-26T02:36:06.373Z" }, + { url = "https://files.pythonhosted.org/packages/58/4c/bdb5c6efe934f68708529c8c9d4055ebef5c4be370621966438f658b29bd/fastar-0.8.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:1e7d29b6bfecb29db126a08baf3c04a5ab667f6cea2b7067d3e623a67729c4a6", size = 705570, upload-time = "2025-11-26T02:34:42.01Z" }, + { url = "https://files.pythonhosted.org/packages/6d/78/f01ac7e71d5a37621bd13598a26e948a12b85ca8042f7ee1a0a8c9f59cda/fastar-0.8.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:05eb7b96940f9526b485f1d0b02393839f0f61cac4b1f60024984f8b326d2640", size = 627761, upload-time = "2025-11-26T02:34:26.152Z" }, + { url = "https://files.pythonhosted.org/packages/06/45/6df0ecda86ea9d2e95053c1a655d153dee55fc121b6e13ea6d1e246a50b6/fastar-0.8.0-cp314-cp314t-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:619352d8ac011794e2345c462189dc02ba634750d23cd9d86a9267dd71b1f278", size = 869414, upload-time = "2025-11-26T02:33:55.618Z" }, + { url = "https://files.pythonhosted.org/packages/b2/72/486421f5a8c0c377cc82e7a50c8a8ea899a6ec2aa72bde8f09fb667a2dc8/fastar-0.8.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:74ebfecef3fe6d7a90355fac1402fd30636988332a1d33f3e80019a10782bb24", size = 763863, upload-time = "2025-11-26T02:32:51.051Z" }, + { url = "https://files.pythonhosted.org/packages/d4/64/39f654dbb41a3867fb1f2c8081c014d8f1d32ea10585d84cacbef0b32995/fastar-0.8.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2975aca5a639e26a3ab0d23b4b0628d6dd6d521146c3c11486d782be621a35aa", size = 763065, upload-time = "2025-11-26T02:33:07.274Z" }, + { url = "https://files.pythonhosted.org/packages/4e/bd/c011a34fb3534c4c3301f7c87c4ffd7e47f6113c904c092ddc8a59a303ea/fastar-0.8.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:afc438eaed8ff0dcdd9308268be5cb38c1db7e94c3ccca7c498ca13a4a4535a3", size = 930530, upload-time = "2025-11-26T02:33:23.117Z" }, + { url = "https://files.pythonhosted.org/packages/55/9d/aa6e887a7033c571b1064429222bbe09adc9a3c1e04f3d1788ba5838ebd5/fastar-0.8.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6ced0a5399cc0a84a858ef0a31ca2d0c24d3bbec4bcda506a9192d8119f3590a", size = 820572, upload-time = "2025-11-26T02:33:37.542Z" }, + { url = "https://files.pythonhosted.org/packages/ad/9c/7a3a2278a1052e1a5d98646de7c095a00cffd2492b3b84ce730e2f1cd93a/fastar-0.8.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec9b23da8c4c039da3fe2e358973c66976a0c8508aa06d6626b4403cb5666c19", size = 820649, upload-time = "2025-11-26T02:34:11.108Z" }, + { url = "https://files.pythonhosted.org/packages/02/9e/d38edc1f4438cd047e56137c26d94783ffade42e1b3bde620ccf17b771ef/fastar-0.8.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:dfba078fcd53478032fd0ceed56960ec6b7ff0511cfc013a8a3a4307e3a7bac4", size = 985653, upload-time = "2025-11-26T02:34:57.884Z" }, + { url = "https://files.pythonhosted.org/packages/69/d9/2147d0c19757e165cd62d41cec3f7b38fad2ad68ab784978b5f81716c7ea/fastar-0.8.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:ade56c94c14be356d295fecb47a3fcd473dd43a8803ead2e2b5b9e58feb6dcfa", size = 1038140, upload-time = "2025-11-26T02:35:15.778Z" }, + { url = "https://files.pythonhosted.org/packages/7f/1d/ec4c717ffb8a308871e9602ec3197d957e238dc0227127ac573ec9bca952/fastar-0.8.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:e48d938f9366db5e59441728f70b7f6c1ccfab7eff84f96f9b7e689b07786c52", size = 1045195, upload-time = "2025-11-26T02:35:32.865Z" }, + { url = "https://files.pythonhosted.org/packages/6a/9f/637334dc8c8f3bb391388b064ae13f0ad9402bc5a6c3e77b8887d0c31921/fastar-0.8.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:79c441dc1482ff51a54fb3f57ae6f7bb3d2cff88fa2cc5d196c519f8aab64a56", size = 994686, upload-time = "2025-11-26T02:35:51.392Z" }, + { url = "https://files.pythonhosted.org/packages/c9/e2/dfa19a4b260b8ab3581b7484dcb80c09b25324f4daa6b6ae1c7640d1607a/fastar-0.8.0-cp314-cp314t-win32.whl", hash = "sha256:187f61dc739afe45ac8e47ed7fd1adc45d52eac110cf27d579155720507d6fbe", size = 455767, upload-time = "2025-11-26T02:36:34.758Z" }, + { url = "https://files.pythonhosted.org/packages/51/47/df65c72afc1297797b255f90c4778b5d6f1f0f80282a134d5ab610310ed9/fastar-0.8.0-cp314-cp314t-win_amd64.whl", hash = "sha256:40e9d763cf8bf85ce2fa256e010aa795c0fe3d3bd1326d5c3084e6ce7857127e", size = 489971, upload-time = "2025-11-26T02:36:22.081Z" }, + { url = "https://files.pythonhosted.org/packages/85/11/0aa8455af26f0ae89e42be67f3a874255ee5d7f0f026fc86e8d56f76b428/fastar-0.8.0-cp314-cp314t-win_arm64.whl", hash = "sha256:e59673307b6a08210987059a2bdea2614fe26e3335d0e5d1a3d95f49a05b1418", size = 460467, upload-time = "2025-11-26T02:36:07.978Z" }, + { url = "https://files.pythonhosted.org/packages/25/9f/6eaa810c240236eff2edf736cd50a17c97dbab1693cda4f7bcea09d13418/fastar-0.8.0-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:2127cf2e80ffd49744a160201e0e2f55198af6c028a7b3f750026e0b1f1caa4e", size = 710544, upload-time = "2025-11-26T02:34:46.195Z" }, + { url = "https://files.pythonhosted.org/packages/1d/a5/58ff9e49a1cd5fbfc8f1238226cbf83b905376a391a6622cdd396b2cfa29/fastar-0.8.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:ff85094f10003801339ac4fa9b20a3410c2d8f284d4cba2dc99de6e98c877812", size = 634020, upload-time = "2025-11-26T02:34:31.085Z" }, + { url = "https://files.pythonhosted.org/packages/80/94/f839257c6600a83fbdb5a7fcc06319599086137b25ba38ca3d2c0fe14562/fastar-0.8.0-pp310-pypy310_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:3dbca235f0bd804cca6602fe055d3892bebf95fb802e6c6c7d872fb10f7abc6c", size = 871735, upload-time = "2025-11-26T02:34:00.088Z" }, + { url = "https://files.pythonhosted.org/packages/eb/79/4124c54260f7ee5cb7034bfe499eff2f8512b052d54be4671e59d4f25a4f/fastar-0.8.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:722e54bfdee6c81a0005e147319e93d8797f442308032c92fa28d03ef8fda076", size = 766779, upload-time = "2025-11-26T02:32:55.109Z" }, + { url = "https://files.pythonhosted.org/packages/36/b6/043b263c4126bf6557c942d099503989af9c5c7ee5cca9a04e00f754816f/fastar-0.8.0-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0a78e5221b94a80800930b7fd0d0e797ae73aadf7044c05ed46cb9bdf870f022", size = 766755, upload-time = "2025-11-26T02:33:11.595Z" }, + { url = "https://files.pythonhosted.org/packages/57/ff/29a5dc06f2940439ebf98661ecc98d48d3f22fed8d6a2d5dc985d1e8da24/fastar-0.8.0-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:997092d31ff451de8d0568f6773f3517cb87dcd0bc76184edb65d7154390a6f8", size = 932732, upload-time = "2025-11-26T02:33:27.122Z" }, + { url = "https://files.pythonhosted.org/packages/eb/e8/2218830f422b37aad52c24b53cb84b5d88bd6fd6ad411bd6689b1a32500d/fastar-0.8.0-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:558e8fcf8fe574541df5db14a46cd98bfbed14a811b7014a54f2b714c0cfac42", size = 822571, upload-time = "2025-11-26T02:33:42.986Z" }, + { url = "https://files.pythonhosted.org/packages/6e/fd/ba6dfeff77cddfe58d85c490b1735c002b81c0d6f826916a8b6c4f8818bc/fastar-0.8.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f1d2a54f87e2908cc19e1a6ee249620174fbefc54a219aba1eaa6f31657683c3", size = 822440, upload-time = "2025-11-26T02:34:15.439Z" }, + { url = "https://files.pythonhosted.org/packages/a7/57/54d5740c84b35de0eb12975397ecc16785b5ad8bed2dbac38b8c8a7c1edd/fastar-0.8.0-pp310-pypy310_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:ef94901537be277f9ec59db939eb817960496c6351afede5b102699b5098604d", size = 987424, upload-time = "2025-11-26T02:35:02.742Z" }, + { url = "https://files.pythonhosted.org/packages/ee/c7/18115927f16deb1ddffdbd4ae992e7e33064bc6defa2b92a147948f8bc0c/fastar-0.8.0-pp310-pypy310_pp73-musllinux_1_2_armv7l.whl", hash = "sha256:0afbb92f78bf29d5e9db76fb46cbabc429e49015cddf72ab9e761afbe88ac100", size = 1042675, upload-time = "2025-11-26T02:35:20.252Z" }, + { url = "https://files.pythonhosted.org/packages/d7/1a/ca884fc7973ec6d765e87af23a4dd25784fb0a36ac2df825f18c3630bbab/fastar-0.8.0-pp310-pypy310_pp73-musllinux_1_2_i686.whl", hash = "sha256:fb59c7925e7710ad178d9e1a3e65edf295d9a042a0cdcb673b4040949eb8ad0a", size = 1047098, upload-time = "2025-11-26T02:35:37.643Z" }, + { url = "https://files.pythonhosted.org/packages/44/ee/25cd645db749b206bb95e1512e57e75d56ccbbb8ec3536f52a7979deab6b/fastar-0.8.0-pp310-pypy310_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:e6c4d6329da568ec36b1347b0c09c4d27f9dfdeddf9f438ddb16799ecf170098", size = 997397, upload-time = "2025-11-26T02:35:56.215Z" }, + { url = "https://files.pythonhosted.org/packages/98/6e/6c46aa7f8c8734e7f96ee5141acd3877667ce66f34eea10703aa7571d191/fastar-0.8.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:998e3fa4b555b63eb134e6758437ed739ad1652fdd2a61dfe1dacbfddc35fe66", size = 710662, upload-time = "2025-11-26T02:34:47.593Z" }, + { url = "https://files.pythonhosted.org/packages/70/27/fd622442f2fbd4ff5459677987481ef1c60e077cb4e63a2ed4d8dce6f869/fastar-0.8.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:5f83e60d845091f3a12bc37f412774264d161576eaf810ed8b43567eb934b7e5", size = 634049, upload-time = "2025-11-26T02:34:32.365Z" }, + { url = "https://files.pythonhosted.org/packages/8f/ee/aa4d08aea25b5419a7277132e738ab1cd775f26aebddce11413b07e2fdff/fastar-0.8.0-pp311-pypy311_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:299672e1c74d8b73c61684fac9159cfc063d35f4b165996a88facb0e26862cb5", size = 872055, upload-time = "2025-11-26T02:34:01.377Z" }, + { url = "https://files.pythonhosted.org/packages/92/9a/2bf2f77aade575e67997e0c759fd55cb1c66b7a5b437b1cd0e97d8b241bc/fastar-0.8.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a3d3a27066b84d015deab5faee78565509bb33b137896443e4144cb1be1a5f90", size = 766787, upload-time = "2025-11-26T02:32:57.161Z" }, + { url = "https://files.pythonhosted.org/packages/0b/90/23a3f6c252f11b10c70f854bce09abc61f71b5a0e6a4b0eac2bcb9a2c583/fastar-0.8.0-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ef0bcf4385bbdd3c1acecce2d9ea7dab7cc9b8ee0581bbccb7ab11908a7ce288", size = 766861, upload-time = "2025-11-26T02:33:12.824Z" }, + { url = "https://files.pythonhosted.org/packages/76/bb/beeb9078380acd4484db5c957d066171695d9340e3526398eb230127b0c2/fastar-0.8.0-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f10ef62b6eda6cb6fd9ba8e1fe08a07d7b2bdcc8eaa00eb91566143b92ed7eee", size = 932667, upload-time = "2025-11-26T02:33:28.405Z" }, + { url = "https://files.pythonhosted.org/packages/f4/6d/b034cc637bd0ee638d5a85d08e941b0b8ffd44cf391fb751ba98233734f7/fastar-0.8.0-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c4f6c82a8ee98c17aa48585ee73b51c89c1b010e5c951af83e07c3436180e3fc", size = 822712, upload-time = "2025-11-26T02:33:44.27Z" }, + { url = "https://files.pythonhosted.org/packages/e2/2b/7d183c63f59227c4689792042d6647f2586a5e7273b55e81745063088d81/fastar-0.8.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c6129067fcb86276635b5857010f4e9b9c7d5d15dd571bb03c6c1ed73c40fd92", size = 822659, upload-time = "2025-11-26T02:34:16.815Z" }, + { url = "https://files.pythonhosted.org/packages/3e/f9/716e0cd9de2427fdf766bc68176f76226cd01fffef3a56c5046fa863f5f0/fastar-0.8.0-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:4cc9e77019e489f1ddac446b6a5b9dfb5c3d9abd142652c22a1d9415dbcc0e47", size = 987412, upload-time = "2025-11-26T02:35:04.259Z" }, + { url = "https://files.pythonhosted.org/packages/a4/b9/9a8c3fd59958c1c8027bc075af11722cdc62c4968bb277e841d131232289/fastar-0.8.0-pp311-pypy311_pp73-musllinux_1_2_armv7l.whl", hash = "sha256:382bfe82c026086487cb17fee12f4c1e2b4e67ce230f2e04487d3e7ddfd69031", size = 1042911, upload-time = "2025-11-26T02:35:21.857Z" }, + { url = "https://files.pythonhosted.org/packages/e2/2f/c3f30963b47022134b8a231c12845f4d7cfba520f59bbc1a82468aea77c7/fastar-0.8.0-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:908d2b9a1ff3d549cc304b32f95706a536da8f0bcb0bc0f9e4c1cce39b80e218", size = 1047464, upload-time = "2025-11-26T02:35:39.376Z" }, + { url = "https://files.pythonhosted.org/packages/9e/8a/218ab6d9a2bab3b07718e6cd8405529600edc1e9c266320e8524c8f63251/fastar-0.8.0-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:1aa7dbde2d2d73eb5b6203d0f74875cb66350f0f1b4325b4839fc8fbbf5d074e", size = 997309, upload-time = "2025-11-26T02:35:57.722Z" }, +] + +[[package]] +name = "frozenlist" +version = "1.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2d/f5/c831fac6cc817d26fd54c7eaccd04ef7e0288806943f7cc5bbf69f3ac1f0/frozenlist-1.8.0.tar.gz", hash = "sha256:3ede829ed8d842f6cd48fc7081d7a41001a56f1f38603f9d49bf3020d59a31ad", size = 45875, upload-time = "2025-10-06T05:38:17.865Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/83/4a/557715d5047da48d54e659203b9335be7bfaafda2c3f627b7c47e0b3aaf3/frozenlist-1.8.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:b37f6d31b3dcea7deb5e9696e529a6aa4a898adc33db82da12e4c60a7c4d2011", size = 86230, upload-time = "2025-10-06T05:35:23.699Z" }, + { url = "https://files.pythonhosted.org/packages/a2/fb/c85f9fed3ea8fe8740e5b46a59cc141c23b842eca617da8876cfce5f760e/frozenlist-1.8.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ef2b7b394f208233e471abc541cc6991f907ffd47dc72584acee3147899d6565", size = 49621, upload-time = "2025-10-06T05:35:25.341Z" }, + { url = "https://files.pythonhosted.org/packages/63/70/26ca3f06aace16f2352796b08704338d74b6d1a24ca38f2771afbb7ed915/frozenlist-1.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a88f062f072d1589b7b46e951698950e7da00442fc1cacbe17e19e025dc327ad", size = 49889, upload-time = "2025-10-06T05:35:26.797Z" }, + { url = "https://files.pythonhosted.org/packages/5d/ed/c7895fd2fde7f3ee70d248175f9b6cdf792fb741ab92dc59cd9ef3bd241b/frozenlist-1.8.0-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f57fb59d9f385710aa7060e89410aeb5058b99e62f4d16b08b91986b9a2140c2", size = 219464, upload-time = "2025-10-06T05:35:28.254Z" }, + { url = "https://files.pythonhosted.org/packages/6b/83/4d587dccbfca74cb8b810472392ad62bfa100bf8108c7223eb4c4fa2f7b3/frozenlist-1.8.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:799345ab092bee59f01a915620b5d014698547afd011e691a208637312db9186", size = 221649, upload-time = "2025-10-06T05:35:29.454Z" }, + { url = "https://files.pythonhosted.org/packages/6a/c6/fd3b9cd046ec5fff9dab66831083bc2077006a874a2d3d9247dea93ddf7e/frozenlist-1.8.0-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c23c3ff005322a6e16f71bf8692fcf4d5a304aaafe1e262c98c6d4adc7be863e", size = 219188, upload-time = "2025-10-06T05:35:30.951Z" }, + { url = "https://files.pythonhosted.org/packages/ce/80/6693f55eb2e085fc8afb28cf611448fb5b90e98e068fa1d1b8d8e66e5c7d/frozenlist-1.8.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8a76ea0f0b9dfa06f254ee06053d93a600865b3274358ca48a352ce4f0798450", size = 231748, upload-time = "2025-10-06T05:35:32.101Z" }, + { url = "https://files.pythonhosted.org/packages/97/d6/e9459f7c5183854abd989ba384fe0cc1a0fb795a83c033f0571ec5933ca4/frozenlist-1.8.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c7366fe1418a6133d5aa824ee53d406550110984de7637d65a178010f759c6ef", size = 236351, upload-time = "2025-10-06T05:35:33.834Z" }, + { url = "https://files.pythonhosted.org/packages/97/92/24e97474b65c0262e9ecd076e826bfd1d3074adcc165a256e42e7b8a7249/frozenlist-1.8.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:13d23a45c4cebade99340c4165bd90eeb4a56c6d8a9d8aa49568cac19a6d0dc4", size = 218767, upload-time = "2025-10-06T05:35:35.205Z" }, + { url = "https://files.pythonhosted.org/packages/ee/bf/dc394a097508f15abff383c5108cb8ad880d1f64a725ed3b90d5c2fbf0bb/frozenlist-1.8.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:e4a3408834f65da56c83528fb52ce7911484f0d1eaf7b761fc66001db1646eff", size = 235887, upload-time = "2025-10-06T05:35:36.354Z" }, + { url = "https://files.pythonhosted.org/packages/40/90/25b201b9c015dbc999a5baf475a257010471a1fa8c200c843fd4abbee725/frozenlist-1.8.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:42145cd2748ca39f32801dad54aeea10039da6f86e303659db90db1c4b614c8c", size = 228785, upload-time = "2025-10-06T05:35:37.949Z" }, + { url = "https://files.pythonhosted.org/packages/84/f4/b5bc148df03082f05d2dd30c089e269acdbe251ac9a9cf4e727b2dbb8a3d/frozenlist-1.8.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:e2de870d16a7a53901e41b64ffdf26f2fbb8917b3e6ebf398098d72c5b20bd7f", size = 230312, upload-time = "2025-10-06T05:35:39.178Z" }, + { url = "https://files.pythonhosted.org/packages/db/4b/87e95b5d15097c302430e647136b7d7ab2398a702390cf4c8601975709e7/frozenlist-1.8.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:20e63c9493d33ee48536600d1a5c95eefc870cd71e7ab037763d1fbb89cc51e7", size = 217650, upload-time = "2025-10-06T05:35:40.377Z" }, + { url = "https://files.pythonhosted.org/packages/e5/70/78a0315d1fea97120591a83e0acd644da638c872f142fd72a6cebee825f3/frozenlist-1.8.0-cp310-cp310-win32.whl", hash = "sha256:adbeebaebae3526afc3c96fad434367cafbfd1b25d72369a9e5858453b1bb71a", size = 39659, upload-time = "2025-10-06T05:35:41.863Z" }, + { url = "https://files.pythonhosted.org/packages/66/aa/3f04523fb189a00e147e60c5b2205126118f216b0aa908035c45336e27e4/frozenlist-1.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:667c3777ca571e5dbeb76f331562ff98b957431df140b54c85fd4d52eea8d8f6", size = 43837, upload-time = "2025-10-06T05:35:43.205Z" }, + { url = "https://files.pythonhosted.org/packages/39/75/1135feecdd7c336938bd55b4dc3b0dfc46d85b9be12ef2628574b28de776/frozenlist-1.8.0-cp310-cp310-win_arm64.whl", hash = "sha256:80f85f0a7cc86e7a54c46d99c9e1318ff01f4687c172ede30fd52d19d1da1c8e", size = 39989, upload-time = "2025-10-06T05:35:44.596Z" }, + { url = "https://files.pythonhosted.org/packages/bc/03/077f869d540370db12165c0aa51640a873fb661d8b315d1d4d67b284d7ac/frozenlist-1.8.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:09474e9831bc2b2199fad6da3c14c7b0fbdd377cce9d3d77131be28906cb7d84", size = 86912, upload-time = "2025-10-06T05:35:45.98Z" }, + { url = "https://files.pythonhosted.org/packages/df/b5/7610b6bd13e4ae77b96ba85abea1c8cb249683217ef09ac9e0ae93f25a91/frozenlist-1.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:17c883ab0ab67200b5f964d2b9ed6b00971917d5d8a92df149dc2c9779208ee9", size = 50046, upload-time = "2025-10-06T05:35:47.009Z" }, + { url = "https://files.pythonhosted.org/packages/6e/ef/0e8f1fe32f8a53dd26bdd1f9347efe0778b0fddf62789ea683f4cc7d787d/frozenlist-1.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:fa47e444b8ba08fffd1c18e8cdb9a75db1b6a27f17507522834ad13ed5922b93", size = 50119, upload-time = "2025-10-06T05:35:48.38Z" }, + { url = "https://files.pythonhosted.org/packages/11/b1/71a477adc7c36e5fb628245dfbdea2166feae310757dea848d02bd0689fd/frozenlist-1.8.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2552f44204b744fba866e573be4c1f9048d6a324dfe14475103fd51613eb1d1f", size = 231067, upload-time = "2025-10-06T05:35:49.97Z" }, + { url = "https://files.pythonhosted.org/packages/45/7e/afe40eca3a2dc19b9904c0f5d7edfe82b5304cb831391edec0ac04af94c2/frozenlist-1.8.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:957e7c38f250991e48a9a73e6423db1bb9dd14e722a10f6b8bb8e16a0f55f695", size = 233160, upload-time = "2025-10-06T05:35:51.729Z" }, + { url = "https://files.pythonhosted.org/packages/a6/aa/7416eac95603ce428679d273255ffc7c998d4132cfae200103f164b108aa/frozenlist-1.8.0-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:8585e3bb2cdea02fc88ffa245069c36555557ad3609e83be0ec71f54fd4abb52", size = 228544, upload-time = "2025-10-06T05:35:53.246Z" }, + { url = "https://files.pythonhosted.org/packages/8b/3d/2a2d1f683d55ac7e3875e4263d28410063e738384d3adc294f5ff3d7105e/frozenlist-1.8.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:edee74874ce20a373d62dc28b0b18b93f645633c2943fd90ee9d898550770581", size = 243797, upload-time = "2025-10-06T05:35:54.497Z" }, + { url = "https://files.pythonhosted.org/packages/78/1e/2d5565b589e580c296d3bb54da08d206e797d941a83a6fdea42af23be79c/frozenlist-1.8.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c9a63152fe95756b85f31186bddf42e4c02c6321207fd6601a1c89ebac4fe567", size = 247923, upload-time = "2025-10-06T05:35:55.861Z" }, + { url = "https://files.pythonhosted.org/packages/aa/c3/65872fcf1d326a7f101ad4d86285c403c87be7d832b7470b77f6d2ed5ddc/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b6db2185db9be0a04fecf2f241c70b63b1a242e2805be291855078f2b404dd6b", size = 230886, upload-time = "2025-10-06T05:35:57.399Z" }, + { url = "https://files.pythonhosted.org/packages/a0/76/ac9ced601d62f6956f03cc794f9e04c81719509f85255abf96e2510f4265/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:f4be2e3d8bc8aabd566f8d5b8ba7ecc09249d74ba3c9ed52e54dc23a293f0b92", size = 245731, upload-time = "2025-10-06T05:35:58.563Z" }, + { url = "https://files.pythonhosted.org/packages/b9/49/ecccb5f2598daf0b4a1415497eba4c33c1e8ce07495eb07d2860c731b8d5/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:c8d1634419f39ea6f5c427ea2f90ca85126b54b50837f31497f3bf38266e853d", size = 241544, upload-time = "2025-10-06T05:35:59.719Z" }, + { url = "https://files.pythonhosted.org/packages/53/4b/ddf24113323c0bbcc54cb38c8b8916f1da7165e07b8e24a717b4a12cbf10/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:1a7fa382a4a223773ed64242dbe1c9c326ec09457e6b8428efb4118c685c3dfd", size = 241806, upload-time = "2025-10-06T05:36:00.959Z" }, + { url = "https://files.pythonhosted.org/packages/a7/fb/9b9a084d73c67175484ba2789a59f8eebebd0827d186a8102005ce41e1ba/frozenlist-1.8.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:11847b53d722050808926e785df837353bd4d75f1d494377e59b23594d834967", size = 229382, upload-time = "2025-10-06T05:36:02.22Z" }, + { url = "https://files.pythonhosted.org/packages/95/a3/c8fb25aac55bf5e12dae5c5aa6a98f85d436c1dc658f21c3ac73f9fa95e5/frozenlist-1.8.0-cp311-cp311-win32.whl", hash = "sha256:27c6e8077956cf73eadd514be8fb04d77fc946a7fe9f7fe167648b0b9085cc25", size = 39647, upload-time = "2025-10-06T05:36:03.409Z" }, + { url = "https://files.pythonhosted.org/packages/0a/f5/603d0d6a02cfd4c8f2a095a54672b3cf967ad688a60fb9faf04fc4887f65/frozenlist-1.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:ac913f8403b36a2c8610bbfd25b8013488533e71e62b4b4adce9c86c8cea905b", size = 44064, upload-time = "2025-10-06T05:36:04.368Z" }, + { url = "https://files.pythonhosted.org/packages/5d/16/c2c9ab44e181f043a86f9a8f84d5124b62dbcb3a02c0977ec72b9ac1d3e0/frozenlist-1.8.0-cp311-cp311-win_arm64.whl", hash = "sha256:d4d3214a0f8394edfa3e303136d0575eece0745ff2b47bd2cb2e66dd92d4351a", size = 39937, upload-time = "2025-10-06T05:36:05.669Z" }, + { url = "https://files.pythonhosted.org/packages/69/29/948b9aa87e75820a38650af445d2ef2b6b8a6fab1a23b6bb9e4ef0be2d59/frozenlist-1.8.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:78f7b9e5d6f2fdb88cdde9440dc147259b62b9d3b019924def9f6478be254ac1", size = 87782, upload-time = "2025-10-06T05:36:06.649Z" }, + { url = "https://files.pythonhosted.org/packages/64/80/4f6e318ee2a7c0750ed724fa33a4bdf1eacdc5a39a7a24e818a773cd91af/frozenlist-1.8.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:229bf37d2e4acdaf808fd3f06e854a4a7a3661e871b10dc1f8f1896a3b05f18b", size = 50594, upload-time = "2025-10-06T05:36:07.69Z" }, + { url = "https://files.pythonhosted.org/packages/2b/94/5c8a2b50a496b11dd519f4a24cb5496cf125681dd99e94c604ccdea9419a/frozenlist-1.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f833670942247a14eafbb675458b4e61c82e002a148f49e68257b79296e865c4", size = 50448, upload-time = "2025-10-06T05:36:08.78Z" }, + { url = "https://files.pythonhosted.org/packages/6a/bd/d91c5e39f490a49df14320f4e8c80161cfcce09f1e2cde1edd16a551abb3/frozenlist-1.8.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:494a5952b1c597ba44e0e78113a7266e656b9794eec897b19ead706bd7074383", size = 242411, upload-time = "2025-10-06T05:36:09.801Z" }, + { url = "https://files.pythonhosted.org/packages/8f/83/f61505a05109ef3293dfb1ff594d13d64a2324ac3482be2cedc2be818256/frozenlist-1.8.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96f423a119f4777a4a056b66ce11527366a8bb92f54e541ade21f2374433f6d4", size = 243014, upload-time = "2025-10-06T05:36:11.394Z" }, + { url = "https://files.pythonhosted.org/packages/d8/cb/cb6c7b0f7d4023ddda30cf56b8b17494eb3a79e3fda666bf735f63118b35/frozenlist-1.8.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3462dd9475af2025c31cc61be6652dfa25cbfb56cbbf52f4ccfe029f38decaf8", size = 234909, upload-time = "2025-10-06T05:36:12.598Z" }, + { url = "https://files.pythonhosted.org/packages/31/c5/cd7a1f3b8b34af009fb17d4123c5a778b44ae2804e3ad6b86204255f9ec5/frozenlist-1.8.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c4c800524c9cd9bac5166cd6f55285957fcfc907db323e193f2afcd4d9abd69b", size = 250049, upload-time = "2025-10-06T05:36:14.065Z" }, + { url = "https://files.pythonhosted.org/packages/c0/01/2f95d3b416c584a1e7f0e1d6d31998c4a795f7544069ee2e0962a4b60740/frozenlist-1.8.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d6a5df73acd3399d893dafc71663ad22534b5aa4f94e8a2fabfe856c3c1b6a52", size = 256485, upload-time = "2025-10-06T05:36:15.39Z" }, + { url = "https://files.pythonhosted.org/packages/ce/03/024bf7720b3abaebcff6d0793d73c154237b85bdf67b7ed55e5e9596dc9a/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:405e8fe955c2280ce66428b3ca55e12b3c4e9c336fb2103a4937e891c69a4a29", size = 237619, upload-time = "2025-10-06T05:36:16.558Z" }, + { url = "https://files.pythonhosted.org/packages/69/fa/f8abdfe7d76b731f5d8bd217827cf6764d4f1d9763407e42717b4bed50a0/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:908bd3f6439f2fef9e85031b59fd4f1297af54415fb60e4254a95f75b3cab3f3", size = 250320, upload-time = "2025-10-06T05:36:17.821Z" }, + { url = "https://files.pythonhosted.org/packages/f5/3c/b051329f718b463b22613e269ad72138cc256c540f78a6de89452803a47d/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:294e487f9ec720bd8ffcebc99d575f7eff3568a08a253d1ee1a0378754b74143", size = 246820, upload-time = "2025-10-06T05:36:19.046Z" }, + { url = "https://files.pythonhosted.org/packages/0f/ae/58282e8f98e444b3f4dd42448ff36fa38bef29e40d40f330b22e7108f565/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:74c51543498289c0c43656701be6b077f4b265868fa7f8a8859c197006efb608", size = 250518, upload-time = "2025-10-06T05:36:20.763Z" }, + { url = "https://files.pythonhosted.org/packages/8f/96/007e5944694d66123183845a106547a15944fbbb7154788cbf7272789536/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:776f352e8329135506a1d6bf16ac3f87bc25b28e765949282dcc627af36123aa", size = 239096, upload-time = "2025-10-06T05:36:22.129Z" }, + { url = "https://files.pythonhosted.org/packages/66/bb/852b9d6db2fa40be96f29c0d1205c306288f0684df8fd26ca1951d461a56/frozenlist-1.8.0-cp312-cp312-win32.whl", hash = "sha256:433403ae80709741ce34038da08511d4a77062aa924baf411ef73d1146e74faf", size = 39985, upload-time = "2025-10-06T05:36:23.661Z" }, + { url = "https://files.pythonhosted.org/packages/b8/af/38e51a553dd66eb064cdf193841f16f077585d4d28394c2fa6235cb41765/frozenlist-1.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:34187385b08f866104f0c0617404c8eb08165ab1272e884abc89c112e9c00746", size = 44591, upload-time = "2025-10-06T05:36:24.958Z" }, + { url = "https://files.pythonhosted.org/packages/a7/06/1dc65480ab147339fecc70797e9c2f69d9cea9cf38934ce08df070fdb9cb/frozenlist-1.8.0-cp312-cp312-win_arm64.whl", hash = "sha256:fe3c58d2f5db5fbd18c2987cba06d51b0529f52bc3a6cdc33d3f4eab725104bd", size = 40102, upload-time = "2025-10-06T05:36:26.333Z" }, + { url = "https://files.pythonhosted.org/packages/2d/40/0832c31a37d60f60ed79e9dfb5a92e1e2af4f40a16a29abcc7992af9edff/frozenlist-1.8.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8d92f1a84bb12d9e56f818b3a746f3efba93c1b63c8387a73dde655e1e42282a", size = 85717, upload-time = "2025-10-06T05:36:27.341Z" }, + { url = "https://files.pythonhosted.org/packages/30/ba/b0b3de23f40bc55a7057bd38434e25c34fa48e17f20ee273bbde5e0650f3/frozenlist-1.8.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:96153e77a591c8adc2ee805756c61f59fef4cf4073a9275ee86fe8cba41241f7", size = 49651, upload-time = "2025-10-06T05:36:28.855Z" }, + { url = "https://files.pythonhosted.org/packages/0c/ab/6e5080ee374f875296c4243c381bbdef97a9ac39c6e3ce1d5f7d42cb78d6/frozenlist-1.8.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f21f00a91358803399890ab167098c131ec2ddd5f8f5fd5fe9c9f2c6fcd91e40", size = 49417, upload-time = "2025-10-06T05:36:29.877Z" }, + { url = "https://files.pythonhosted.org/packages/d5/4e/e4691508f9477ce67da2015d8c00acd751e6287739123113a9fca6f1604e/frozenlist-1.8.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fb30f9626572a76dfe4293c7194a09fb1fe93ba94c7d4f720dfae3b646b45027", size = 234391, upload-time = "2025-10-06T05:36:31.301Z" }, + { url = "https://files.pythonhosted.org/packages/40/76/c202df58e3acdf12969a7895fd6f3bc016c642e6726aa63bd3025e0fc71c/frozenlist-1.8.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eaa352d7047a31d87dafcacbabe89df0aa506abb5b1b85a2fb91bc3faa02d822", size = 233048, upload-time = "2025-10-06T05:36:32.531Z" }, + { url = "https://files.pythonhosted.org/packages/f9/c0/8746afb90f17b73ca5979c7a3958116e105ff796e718575175319b5bb4ce/frozenlist-1.8.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:03ae967b4e297f58f8c774c7eabcce57fe3c2434817d4385c50661845a058121", size = 226549, upload-time = "2025-10-06T05:36:33.706Z" }, + { url = "https://files.pythonhosted.org/packages/7e/eb/4c7eefc718ff72f9b6c4893291abaae5fbc0c82226a32dcd8ef4f7a5dbef/frozenlist-1.8.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f6292f1de555ffcc675941d65fffffb0a5bcd992905015f85d0592201793e0e5", size = 239833, upload-time = "2025-10-06T05:36:34.947Z" }, + { url = "https://files.pythonhosted.org/packages/c2/4e/e5c02187cf704224f8b21bee886f3d713ca379535f16893233b9d672ea71/frozenlist-1.8.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:29548f9b5b5e3460ce7378144c3010363d8035cea44bc0bf02d57f5a685e084e", size = 245363, upload-time = "2025-10-06T05:36:36.534Z" }, + { url = "https://files.pythonhosted.org/packages/1f/96/cb85ec608464472e82ad37a17f844889c36100eed57bea094518bf270692/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ec3cc8c5d4084591b4237c0a272cc4f50a5b03396a47d9caaf76f5d7b38a4f11", size = 229314, upload-time = "2025-10-06T05:36:38.582Z" }, + { url = "https://files.pythonhosted.org/packages/5d/6f/4ae69c550e4cee66b57887daeebe006fe985917c01d0fff9caab9883f6d0/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:517279f58009d0b1f2e7c1b130b377a349405da3f7621ed6bfae50b10adf20c1", size = 243365, upload-time = "2025-10-06T05:36:40.152Z" }, + { url = "https://files.pythonhosted.org/packages/7a/58/afd56de246cf11780a40a2c28dc7cbabbf06337cc8ddb1c780a2d97e88d8/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:db1e72ede2d0d7ccb213f218df6a078a9c09a7de257c2fe8fcef16d5925230b1", size = 237763, upload-time = "2025-10-06T05:36:41.355Z" }, + { url = "https://files.pythonhosted.org/packages/cb/36/cdfaf6ed42e2644740d4a10452d8e97fa1c062e2a8006e4b09f1b5fd7d63/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:b4dec9482a65c54a5044486847b8a66bf10c9cb4926d42927ec4e8fd5db7fed8", size = 240110, upload-time = "2025-10-06T05:36:42.716Z" }, + { url = "https://files.pythonhosted.org/packages/03/a8/9ea226fbefad669f11b52e864c55f0bd57d3c8d7eb07e9f2e9a0b39502e1/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:21900c48ae04d13d416f0e1e0c4d81f7931f73a9dfa0b7a8746fb2fe7dd970ed", size = 233717, upload-time = "2025-10-06T05:36:44.251Z" }, + { url = "https://files.pythonhosted.org/packages/1e/0b/1b5531611e83ba7d13ccc9988967ea1b51186af64c42b7a7af465dcc9568/frozenlist-1.8.0-cp313-cp313-win32.whl", hash = "sha256:8b7b94a067d1c504ee0b16def57ad5738701e4ba10cec90529f13fa03c833496", size = 39628, upload-time = "2025-10-06T05:36:45.423Z" }, + { url = "https://files.pythonhosted.org/packages/d8/cf/174c91dbc9cc49bc7b7aab74d8b734e974d1faa8f191c74af9b7e80848e6/frozenlist-1.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:878be833caa6a3821caf85eb39c5ba92d28e85df26d57afb06b35b2efd937231", size = 43882, upload-time = "2025-10-06T05:36:46.796Z" }, + { url = "https://files.pythonhosted.org/packages/c1/17/502cd212cbfa96eb1388614fe39a3fc9ab87dbbe042b66f97acb57474834/frozenlist-1.8.0-cp313-cp313-win_arm64.whl", hash = "sha256:44389d135b3ff43ba8cc89ff7f51f5a0bb6b63d829c8300f79a2fe4fe61bcc62", size = 39676, upload-time = "2025-10-06T05:36:47.8Z" }, + { url = "https://files.pythonhosted.org/packages/d2/5c/3bbfaa920dfab09e76946a5d2833a7cbdf7b9b4a91c714666ac4855b88b4/frozenlist-1.8.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:e25ac20a2ef37e91c1b39938b591457666a0fa835c7783c3a8f33ea42870db94", size = 89235, upload-time = "2025-10-06T05:36:48.78Z" }, + { url = "https://files.pythonhosted.org/packages/d2/d6/f03961ef72166cec1687e84e8925838442b615bd0b8854b54923ce5b7b8a/frozenlist-1.8.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:07cdca25a91a4386d2e76ad992916a85038a9b97561bf7a3fd12d5d9ce31870c", size = 50742, upload-time = "2025-10-06T05:36:49.837Z" }, + { url = "https://files.pythonhosted.org/packages/1e/bb/a6d12b7ba4c3337667d0e421f7181c82dda448ce4e7ad7ecd249a16fa806/frozenlist-1.8.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4e0c11f2cc6717e0a741f84a527c52616140741cd812a50422f83dc31749fb52", size = 51725, upload-time = "2025-10-06T05:36:50.851Z" }, + { url = "https://files.pythonhosted.org/packages/bc/71/d1fed0ffe2c2ccd70b43714c6cab0f4188f09f8a67a7914a6b46ee30f274/frozenlist-1.8.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b3210649ee28062ea6099cfda39e147fa1bc039583c8ee4481cb7811e2448c51", size = 284533, upload-time = "2025-10-06T05:36:51.898Z" }, + { url = "https://files.pythonhosted.org/packages/c9/1f/fb1685a7b009d89f9bf78a42d94461bc06581f6e718c39344754a5d9bada/frozenlist-1.8.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:581ef5194c48035a7de2aefc72ac6539823bb71508189e5de01d60c9dcd5fa65", size = 292506, upload-time = "2025-10-06T05:36:53.101Z" }, + { url = "https://files.pythonhosted.org/packages/e6/3b/b991fe1612703f7e0d05c0cf734c1b77aaf7c7d321df4572e8d36e7048c8/frozenlist-1.8.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3ef2d026f16a2b1866e1d86fc4e1291e1ed8a387b2c333809419a2f8b3a77b82", size = 274161, upload-time = "2025-10-06T05:36:54.309Z" }, + { url = "https://files.pythonhosted.org/packages/ca/ec/c5c618767bcdf66e88945ec0157d7f6c4a1322f1473392319b7a2501ded7/frozenlist-1.8.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5500ef82073f599ac84d888e3a8c1f77ac831183244bfd7f11eaa0289fb30714", size = 294676, upload-time = "2025-10-06T05:36:55.566Z" }, + { url = "https://files.pythonhosted.org/packages/7c/ce/3934758637d8f8a88d11f0585d6495ef54b2044ed6ec84492a91fa3b27aa/frozenlist-1.8.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:50066c3997d0091c411a66e710f4e11752251e6d2d73d70d8d5d4c76442a199d", size = 300638, upload-time = "2025-10-06T05:36:56.758Z" }, + { url = "https://files.pythonhosted.org/packages/fc/4f/a7e4d0d467298f42de4b41cbc7ddaf19d3cfeabaf9ff97c20c6c7ee409f9/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:5c1c8e78426e59b3f8005e9b19f6ff46e5845895adbde20ece9218319eca6506", size = 283067, upload-time = "2025-10-06T05:36:57.965Z" }, + { url = "https://files.pythonhosted.org/packages/dc/48/c7b163063d55a83772b268e6d1affb960771b0e203b632cfe09522d67ea5/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:eefdba20de0d938cec6a89bd4d70f346a03108a19b9df4248d3cf0d88f1b0f51", size = 292101, upload-time = "2025-10-06T05:36:59.237Z" }, + { url = "https://files.pythonhosted.org/packages/9f/d0/2366d3c4ecdc2fd391e0afa6e11500bfba0ea772764d631bbf82f0136c9d/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:cf253e0e1c3ceb4aaff6df637ce033ff6535fb8c70a764a8f46aafd3d6ab798e", size = 289901, upload-time = "2025-10-06T05:37:00.811Z" }, + { url = "https://files.pythonhosted.org/packages/b8/94/daff920e82c1b70e3618a2ac39fbc01ae3e2ff6124e80739ce5d71c9b920/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:032efa2674356903cd0261c4317a561a6850f3ac864a63fc1583147fb05a79b0", size = 289395, upload-time = "2025-10-06T05:37:02.115Z" }, + { url = "https://files.pythonhosted.org/packages/e3/20/bba307ab4235a09fdcd3cc5508dbabd17c4634a1af4b96e0f69bfe551ebd/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6da155091429aeba16851ecb10a9104a108bcd32f6c1642867eadaee401c1c41", size = 283659, upload-time = "2025-10-06T05:37:03.711Z" }, + { url = "https://files.pythonhosted.org/packages/fd/00/04ca1c3a7a124b6de4f8a9a17cc2fcad138b4608e7a3fc5877804b8715d7/frozenlist-1.8.0-cp313-cp313t-win32.whl", hash = "sha256:0f96534f8bfebc1a394209427d0f8a63d343c9779cda6fc25e8e121b5fd8555b", size = 43492, upload-time = "2025-10-06T05:37:04.915Z" }, + { url = "https://files.pythonhosted.org/packages/59/5e/c69f733a86a94ab10f68e496dc6b7e8bc078ebb415281d5698313e3af3a1/frozenlist-1.8.0-cp313-cp313t-win_amd64.whl", hash = "sha256:5d63a068f978fc69421fb0e6eb91a9603187527c86b7cd3f534a5b77a592b888", size = 48034, upload-time = "2025-10-06T05:37:06.343Z" }, + { url = "https://files.pythonhosted.org/packages/16/6c/be9d79775d8abe79b05fa6d23da99ad6e7763a1d080fbae7290b286093fd/frozenlist-1.8.0-cp313-cp313t-win_arm64.whl", hash = "sha256:bf0a7e10b077bf5fb9380ad3ae8ce20ef919a6ad93b4552896419ac7e1d8e042", size = 41749, upload-time = "2025-10-06T05:37:07.431Z" }, + { url = "https://files.pythonhosted.org/packages/f1/c8/85da824b7e7b9b6e7f7705b2ecaf9591ba6f79c1177f324c2735e41d36a2/frozenlist-1.8.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:cee686f1f4cadeb2136007ddedd0aaf928ab95216e7691c63e50a8ec066336d0", size = 86127, upload-time = "2025-10-06T05:37:08.438Z" }, + { url = "https://files.pythonhosted.org/packages/8e/e8/a1185e236ec66c20afd72399522f142c3724c785789255202d27ae992818/frozenlist-1.8.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:119fb2a1bd47307e899c2fac7f28e85b9a543864df47aa7ec9d3c1b4545f096f", size = 49698, upload-time = "2025-10-06T05:37:09.48Z" }, + { url = "https://files.pythonhosted.org/packages/a1/93/72b1736d68f03fda5fdf0f2180fb6caaae3894f1b854d006ac61ecc727ee/frozenlist-1.8.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4970ece02dbc8c3a92fcc5228e36a3e933a01a999f7094ff7c23fbd2beeaa67c", size = 49749, upload-time = "2025-10-06T05:37:10.569Z" }, + { url = "https://files.pythonhosted.org/packages/a7/b2/fabede9fafd976b991e9f1b9c8c873ed86f202889b864756f240ce6dd855/frozenlist-1.8.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:cba69cb73723c3f329622e34bdbf5ce1f80c21c290ff04256cff1cd3c2036ed2", size = 231298, upload-time = "2025-10-06T05:37:11.993Z" }, + { url = "https://files.pythonhosted.org/packages/3a/3b/d9b1e0b0eed36e70477ffb8360c49c85c8ca8ef9700a4e6711f39a6e8b45/frozenlist-1.8.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:778a11b15673f6f1df23d9586f83c4846c471a8af693a22e066508b77d201ec8", size = 232015, upload-time = "2025-10-06T05:37:13.194Z" }, + { url = "https://files.pythonhosted.org/packages/dc/94/be719d2766c1138148564a3960fc2c06eb688da592bdc25adcf856101be7/frozenlist-1.8.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0325024fe97f94c41c08872db482cf8ac4800d80e79222c6b0b7b162d5b13686", size = 225038, upload-time = "2025-10-06T05:37:14.577Z" }, + { url = "https://files.pythonhosted.org/packages/e4/09/6712b6c5465f083f52f50cf74167b92d4ea2f50e46a9eea0523d658454ae/frozenlist-1.8.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:97260ff46b207a82a7567b581ab4190bd4dfa09f4db8a8b49d1a958f6aa4940e", size = 240130, upload-time = "2025-10-06T05:37:15.781Z" }, + { url = "https://files.pythonhosted.org/packages/f8/d4/cd065cdcf21550b54f3ce6a22e143ac9e4836ca42a0de1022da8498eac89/frozenlist-1.8.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:54b2077180eb7f83dd52c40b2750d0a9f175e06a42e3213ce047219de902717a", size = 242845, upload-time = "2025-10-06T05:37:17.037Z" }, + { url = "https://files.pythonhosted.org/packages/62/c3/f57a5c8c70cd1ead3d5d5f776f89d33110b1addae0ab010ad774d9a44fb9/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:2f05983daecab868a31e1da44462873306d3cbfd76d1f0b5b69c473d21dbb128", size = 229131, upload-time = "2025-10-06T05:37:18.221Z" }, + { url = "https://files.pythonhosted.org/packages/6c/52/232476fe9cb64f0742f3fde2b7d26c1dac18b6d62071c74d4ded55e0ef94/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:33f48f51a446114bc5d251fb2954ab0164d5be02ad3382abcbfe07e2531d650f", size = 240542, upload-time = "2025-10-06T05:37:19.771Z" }, + { url = "https://files.pythonhosted.org/packages/5f/85/07bf3f5d0fb5414aee5f47d33c6f5c77bfe49aac680bfece33d4fdf6a246/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:154e55ec0655291b5dd1b8731c637ecdb50975a2ae70c606d100750a540082f7", size = 237308, upload-time = "2025-10-06T05:37:20.969Z" }, + { url = "https://files.pythonhosted.org/packages/11/99/ae3a33d5befd41ac0ca2cc7fd3aa707c9c324de2e89db0e0f45db9a64c26/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:4314debad13beb564b708b4a496020e5306c7333fa9a3ab90374169a20ffab30", size = 238210, upload-time = "2025-10-06T05:37:22.252Z" }, + { url = "https://files.pythonhosted.org/packages/b2/60/b1d2da22f4970e7a155f0adde9b1435712ece01b3cd45ba63702aea33938/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:073f8bf8becba60aa931eb3bc420b217bb7d5b8f4750e6f8b3be7f3da85d38b7", size = 231972, upload-time = "2025-10-06T05:37:23.5Z" }, + { url = "https://files.pythonhosted.org/packages/3f/ab/945b2f32de889993b9c9133216c068b7fcf257d8595a0ac420ac8677cab0/frozenlist-1.8.0-cp314-cp314-win32.whl", hash = "sha256:bac9c42ba2ac65ddc115d930c78d24ab8d4f465fd3fc473cdedfccadb9429806", size = 40536, upload-time = "2025-10-06T05:37:25.581Z" }, + { url = "https://files.pythonhosted.org/packages/59/ad/9caa9b9c836d9ad6f067157a531ac48b7d36499f5036d4141ce78c230b1b/frozenlist-1.8.0-cp314-cp314-win_amd64.whl", hash = "sha256:3e0761f4d1a44f1d1a47996511752cf3dcec5bbdd9cc2b4fe595caf97754b7a0", size = 44330, upload-time = "2025-10-06T05:37:26.928Z" }, + { url = "https://files.pythonhosted.org/packages/82/13/e6950121764f2676f43534c555249f57030150260aee9dcf7d64efda11dd/frozenlist-1.8.0-cp314-cp314-win_arm64.whl", hash = "sha256:d1eaff1d00c7751b7c6662e9c5ba6eb2c17a2306ba5e2a37f24ddf3cc953402b", size = 40627, upload-time = "2025-10-06T05:37:28.075Z" }, + { url = "https://files.pythonhosted.org/packages/c0/c7/43200656ecc4e02d3f8bc248df68256cd9572b3f0017f0a0c4e93440ae23/frozenlist-1.8.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:d3bb933317c52d7ea5004a1c442eef86f426886fba134ef8cf4226ea6ee1821d", size = 89238, upload-time = "2025-10-06T05:37:29.373Z" }, + { url = "https://files.pythonhosted.org/packages/d1/29/55c5f0689b9c0fb765055629f472c0de484dcaf0acee2f7707266ae3583c/frozenlist-1.8.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:8009897cdef112072f93a0efdce29cd819e717fd2f649ee3016efd3cd885a7ed", size = 50738, upload-time = "2025-10-06T05:37:30.792Z" }, + { url = "https://files.pythonhosted.org/packages/ba/7d/b7282a445956506fa11da8c2db7d276adcbf2b17d8bb8407a47685263f90/frozenlist-1.8.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2c5dcbbc55383e5883246d11fd179782a9d07a986c40f49abe89ddf865913930", size = 51739, upload-time = "2025-10-06T05:37:32.127Z" }, + { url = "https://files.pythonhosted.org/packages/62/1c/3d8622e60d0b767a5510d1d3cf21065b9db874696a51ea6d7a43180a259c/frozenlist-1.8.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:39ecbc32f1390387d2aa4f5a995e465e9e2f79ba3adcac92d68e3e0afae6657c", size = 284186, upload-time = "2025-10-06T05:37:33.21Z" }, + { url = "https://files.pythonhosted.org/packages/2d/14/aa36d5f85a89679a85a1d44cd7a6657e0b1c75f61e7cad987b203d2daca8/frozenlist-1.8.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92db2bf818d5cc8d9c1f1fc56b897662e24ea5adb36ad1f1d82875bd64e03c24", size = 292196, upload-time = "2025-10-06T05:37:36.107Z" }, + { url = "https://files.pythonhosted.org/packages/05/23/6bde59eb55abd407d34f77d39a5126fb7b4f109a3f611d3929f14b700c66/frozenlist-1.8.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2dc43a022e555de94c3b68a4ef0b11c4f747d12c024a520c7101709a2144fb37", size = 273830, upload-time = "2025-10-06T05:37:37.663Z" }, + { url = "https://files.pythonhosted.org/packages/d2/3f/22cff331bfad7a8afa616289000ba793347fcd7bc275f3b28ecea2a27909/frozenlist-1.8.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cb89a7f2de3602cfed448095bab3f178399646ab7c61454315089787df07733a", size = 294289, upload-time = "2025-10-06T05:37:39.261Z" }, + { url = "https://files.pythonhosted.org/packages/a4/89/5b057c799de4838b6c69aa82b79705f2027615e01be996d2486a69ca99c4/frozenlist-1.8.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:33139dc858c580ea50e7e60a1b0ea003efa1fd42e6ec7fdbad78fff65fad2fd2", size = 300318, upload-time = "2025-10-06T05:37:43.213Z" }, + { url = "https://files.pythonhosted.org/packages/30/de/2c22ab3eb2a8af6d69dc799e48455813bab3690c760de58e1bf43b36da3e/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:168c0969a329b416119507ba30b9ea13688fafffac1b7822802537569a1cb0ef", size = 282814, upload-time = "2025-10-06T05:37:45.337Z" }, + { url = "https://files.pythonhosted.org/packages/59/f7/970141a6a8dbd7f556d94977858cfb36fa9b66e0892c6dd780d2219d8cd8/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:28bd570e8e189d7f7b001966435f9dac6718324b5be2990ac496cf1ea9ddb7fe", size = 291762, upload-time = "2025-10-06T05:37:46.657Z" }, + { url = "https://files.pythonhosted.org/packages/c1/15/ca1adae83a719f82df9116d66f5bb28bb95557b3951903d39135620ef157/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:b2a095d45c5d46e5e79ba1e5b9cb787f541a8dee0433836cea4b96a2c439dcd8", size = 289470, upload-time = "2025-10-06T05:37:47.946Z" }, + { url = "https://files.pythonhosted.org/packages/ac/83/dca6dc53bf657d371fbc88ddeb21b79891e747189c5de990b9dfff2ccba1/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:eab8145831a0d56ec9c4139b6c3e594c7a83c2c8be25d5bcf2d86136a532287a", size = 289042, upload-time = "2025-10-06T05:37:49.499Z" }, + { url = "https://files.pythonhosted.org/packages/96/52/abddd34ca99be142f354398700536c5bd315880ed0a213812bc491cff5e4/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:974b28cf63cc99dfb2188d8d222bc6843656188164848c4f679e63dae4b0708e", size = 283148, upload-time = "2025-10-06T05:37:50.745Z" }, + { url = "https://files.pythonhosted.org/packages/af/d3/76bd4ed4317e7119c2b7f57c3f6934aba26d277acc6309f873341640e21f/frozenlist-1.8.0-cp314-cp314t-win32.whl", hash = "sha256:342c97bf697ac5480c0a7ec73cd700ecfa5a8a40ac923bd035484616efecc2df", size = 44676, upload-time = "2025-10-06T05:37:52.222Z" }, + { url = "https://files.pythonhosted.org/packages/89/76/c615883b7b521ead2944bb3480398cbb07e12b7b4e4d073d3752eb721558/frozenlist-1.8.0-cp314-cp314t-win_amd64.whl", hash = "sha256:06be8f67f39c8b1dc671f5d83aaefd3358ae5cdcf8314552c57e7ed3e6475bdd", size = 49451, upload-time = "2025-10-06T05:37:53.425Z" }, + { url = "https://files.pythonhosted.org/packages/e0/a3/5982da14e113d07b325230f95060e2169f5311b1017ea8af2a29b374c289/frozenlist-1.8.0-cp314-cp314t-win_arm64.whl", hash = "sha256:102e6314ca4da683dca92e3b1355490fed5f313b768500084fbe6371fddfdb79", size = 42507, upload-time = "2025-10-06T05:37:54.513Z" }, + { url = "https://files.pythonhosted.org/packages/9a/9a/e35b4a917281c0b8419d4207f4334c8e8c5dbf4f3f5f9ada73958d937dcc/frozenlist-1.8.0-py3-none-any.whl", hash = "sha256:0c18a16eab41e82c295618a77502e17b195883241c563b00f0aa5106fc4eaa0d", size = 13409, upload-time = "2025-10-06T05:38:16.721Z" }, +] + +[[package]] +name = "h11" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, +] + +[[package]] +name = "hexbytes" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7f/87/adf4635b4b8c050283d74e6db9a81496063229c9263e6acc1903ab79fbec/hexbytes-1.3.1.tar.gz", hash = "sha256:a657eebebdfe27254336f98d8af6e2236f3f83aed164b87466b6cf6c5f5a4765", size = 8633, upload-time = "2025-05-14T16:45:17.5Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8d/e0/3b31492b1c89da3c5a846680517871455b30c54738486fc57ac79a5761bd/hexbytes-1.3.1-py3-none-any.whl", hash = "sha256:da01ff24a1a9a2b1881c4b85f0e9f9b0f51b526b379ffa23832ae7899d29c2c7", size = 5074, upload-time = "2025-05-14T16:45:16.179Z" }, +] + +[[package]] +name = "httpcore" +version = "1.0.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, +] + +[[package]] +name = "httptools" +version = "0.7.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b5/46/120a669232c7bdedb9d52d4aeae7e6c7dfe151e99dc70802e2fc7a5e1993/httptools-0.7.1.tar.gz", hash = "sha256:abd72556974f8e7c74a259655924a717a2365b236c882c3f6f8a45fe94703ac9", size = 258961, upload-time = "2025-10-10T03:55:08.559Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/e5/c07e0bcf4ec8db8164e9f6738c048b2e66aabf30e7506f440c4cc6953f60/httptools-0.7.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:11d01b0ff1fe02c4c32d60af61a4d613b74fad069e47e06e9067758c01e9ac78", size = 204531, upload-time = "2025-10-10T03:54:20.887Z" }, + { url = "https://files.pythonhosted.org/packages/7e/4f/35e3a63f863a659f92ffd92bef131f3e81cf849af26e6435b49bd9f6f751/httptools-0.7.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:84d86c1e5afdc479a6fdabf570be0d3eb791df0ae727e8dbc0259ed1249998d4", size = 109408, upload-time = "2025-10-10T03:54:22.455Z" }, + { url = "https://files.pythonhosted.org/packages/f5/71/b0a9193641d9e2471ac541d3b1b869538a5fb6419d52fd2669fa9c79e4b8/httptools-0.7.1-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c8c751014e13d88d2be5f5f14fc8b89612fcfa92a9cc480f2bc1598357a23a05", size = 440889, upload-time = "2025-10-10T03:54:23.753Z" }, + { url = "https://files.pythonhosted.org/packages/eb/d9/2e34811397b76718750fea44658cb0205b84566e895192115252e008b152/httptools-0.7.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:654968cb6b6c77e37b832a9be3d3ecabb243bbe7a0b8f65fbc5b6b04c8fcabed", size = 440460, upload-time = "2025-10-10T03:54:25.313Z" }, + { url = "https://files.pythonhosted.org/packages/01/3f/a04626ebeacc489866bb4d82362c0657b2262bef381d68310134be7f40bb/httptools-0.7.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:b580968316348b474b020edf3988eecd5d6eec4634ee6561e72ae3a2a0e00a8a", size = 425267, upload-time = "2025-10-10T03:54:26.81Z" }, + { url = "https://files.pythonhosted.org/packages/a5/99/adcd4f66614db627b587627c8ad6f4c55f18881549bab10ecf180562e7b9/httptools-0.7.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:d496e2f5245319da9d764296e86c5bb6fcf0cf7a8806d3d000717a889c8c0b7b", size = 424429, upload-time = "2025-10-10T03:54:28.174Z" }, + { url = "https://files.pythonhosted.org/packages/d5/72/ec8fc904a8fd30ba022dfa85f3bbc64c3c7cd75b669e24242c0658e22f3c/httptools-0.7.1-cp310-cp310-win_amd64.whl", hash = "sha256:cbf8317bfccf0fed3b5680c559d3459cccf1abe9039bfa159e62e391c7270568", size = 86173, upload-time = "2025-10-10T03:54:29.5Z" }, + { url = "https://files.pythonhosted.org/packages/9c/08/17e07e8d89ab8f343c134616d72eebfe03798835058e2ab579dcc8353c06/httptools-0.7.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:474d3b7ab469fefcca3697a10d11a32ee2b9573250206ba1e50d5980910da657", size = 206521, upload-time = "2025-10-10T03:54:31.002Z" }, + { url = "https://files.pythonhosted.org/packages/aa/06/c9c1b41ff52f16aee526fd10fbda99fa4787938aa776858ddc4a1ea825ec/httptools-0.7.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a3c3b7366bb6c7b96bd72d0dbe7f7d5eead261361f013be5f6d9590465ea1c70", size = 110375, upload-time = "2025-10-10T03:54:31.941Z" }, + { url = "https://files.pythonhosted.org/packages/cc/cc/10935db22fda0ee34c76f047590ca0a8bd9de531406a3ccb10a90e12ea21/httptools-0.7.1-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:379b479408b8747f47f3b253326183d7c009a3936518cdb70db58cffd369d9df", size = 456621, upload-time = "2025-10-10T03:54:33.176Z" }, + { url = "https://files.pythonhosted.org/packages/0e/84/875382b10d271b0c11aa5d414b44f92f8dd53e9b658aec338a79164fa548/httptools-0.7.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cad6b591a682dcc6cf1397c3900527f9affef1e55a06c4547264796bbd17cf5e", size = 454954, upload-time = "2025-10-10T03:54:34.226Z" }, + { url = "https://files.pythonhosted.org/packages/30/e1/44f89b280f7e46c0b1b2ccee5737d46b3bb13136383958f20b580a821ca0/httptools-0.7.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:eb844698d11433d2139bbeeb56499102143beb582bd6c194e3ba69c22f25c274", size = 440175, upload-time = "2025-10-10T03:54:35.942Z" }, + { url = "https://files.pythonhosted.org/packages/6f/7e/b9287763159e700e335028bc1824359dc736fa9b829dacedace91a39b37e/httptools-0.7.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f65744d7a8bdb4bda5e1fa23e4ba16832860606fcc09d674d56e425e991539ec", size = 440310, upload-time = "2025-10-10T03:54:37.1Z" }, + { url = "https://files.pythonhosted.org/packages/b3/07/5b614f592868e07f5c94b1f301b5e14a21df4e8076215a3bccb830a687d8/httptools-0.7.1-cp311-cp311-win_amd64.whl", hash = "sha256:135fbe974b3718eada677229312e97f3b31f8a9c8ffa3ae6f565bf808d5b6bcb", size = 86875, upload-time = "2025-10-10T03:54:38.421Z" }, + { url = "https://files.pythonhosted.org/packages/53/7f/403e5d787dc4942316e515e949b0c8a013d84078a915910e9f391ba9b3ed/httptools-0.7.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:38e0c83a2ea9746ebbd643bdfb521b9aa4a91703e2cd705c20443405d2fd16a5", size = 206280, upload-time = "2025-10-10T03:54:39.274Z" }, + { url = "https://files.pythonhosted.org/packages/2a/0d/7f3fd28e2ce311ccc998c388dd1c53b18120fda3b70ebb022b135dc9839b/httptools-0.7.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f25bbaf1235e27704f1a7b86cd3304eabc04f569c828101d94a0e605ef7205a5", size = 110004, upload-time = "2025-10-10T03:54:40.403Z" }, + { url = "https://files.pythonhosted.org/packages/84/a6/b3965e1e146ef5762870bbe76117876ceba51a201e18cc31f5703e454596/httptools-0.7.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2c15f37ef679ab9ecc06bfc4e6e8628c32a8e4b305459de7cf6785acd57e4d03", size = 517655, upload-time = "2025-10-10T03:54:41.347Z" }, + { url = "https://files.pythonhosted.org/packages/11/7d/71fee6f1844e6fa378f2eddde6c3e41ce3a1fb4b2d81118dd544e3441ec0/httptools-0.7.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7fe6e96090df46b36ccfaf746f03034e5ab723162bc51b0a4cf58305324036f2", size = 511440, upload-time = "2025-10-10T03:54:42.452Z" }, + { url = "https://files.pythonhosted.org/packages/22/a5/079d216712a4f3ffa24af4a0381b108aa9c45b7a5cc6eb141f81726b1823/httptools-0.7.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f72fdbae2dbc6e68b8239defb48e6a5937b12218e6ffc2c7846cc37befa84362", size = 495186, upload-time = "2025-10-10T03:54:43.937Z" }, + { url = "https://files.pythonhosted.org/packages/e9/9e/025ad7b65278745dee3bd0ebf9314934c4592560878308a6121f7f812084/httptools-0.7.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e99c7b90a29fd82fea9ef57943d501a16f3404d7b9ee81799d41639bdaae412c", size = 499192, upload-time = "2025-10-10T03:54:45.003Z" }, + { url = "https://files.pythonhosted.org/packages/6d/de/40a8f202b987d43afc4d54689600ff03ce65680ede2f31df348d7f368b8f/httptools-0.7.1-cp312-cp312-win_amd64.whl", hash = "sha256:3e14f530fefa7499334a79b0cf7e7cd2992870eb893526fb097d51b4f2d0f321", size = 86694, upload-time = "2025-10-10T03:54:45.923Z" }, + { url = "https://files.pythonhosted.org/packages/09/8f/c77b1fcbfd262d422f12da02feb0d218fa228d52485b77b953832105bb90/httptools-0.7.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:6babce6cfa2a99545c60bfef8bee0cc0545413cb0018f617c8059a30ad985de3", size = 202889, upload-time = "2025-10-10T03:54:47.089Z" }, + { url = "https://files.pythonhosted.org/packages/0a/1a/22887f53602feaa066354867bc49a68fc295c2293433177ee90870a7d517/httptools-0.7.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:601b7628de7504077dd3dcb3791c6b8694bbd967148a6d1f01806509254fb1ca", size = 108180, upload-time = "2025-10-10T03:54:48.052Z" }, + { url = "https://files.pythonhosted.org/packages/32/6a/6aaa91937f0010d288d3d124ca2946d48d60c3a5ee7ca62afe870e3ea011/httptools-0.7.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:04c6c0e6c5fb0739c5b8a9eb046d298650a0ff38cf42537fc372b28dc7e4472c", size = 478596, upload-time = "2025-10-10T03:54:48.919Z" }, + { url = "https://files.pythonhosted.org/packages/6d/70/023d7ce117993107be88d2cbca566a7c1323ccbaf0af7eabf2064fe356f6/httptools-0.7.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:69d4f9705c405ae3ee83d6a12283dc9feba8cc6aaec671b412917e644ab4fa66", size = 473268, upload-time = "2025-10-10T03:54:49.993Z" }, + { url = "https://files.pythonhosted.org/packages/32/4d/9dd616c38da088e3f436e9a616e1d0cc66544b8cdac405cc4e81c8679fc7/httptools-0.7.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:44c8f4347d4b31269c8a9205d8a5ee2df5322b09bbbd30f8f862185bb6b05346", size = 455517, upload-time = "2025-10-10T03:54:51.066Z" }, + { url = "https://files.pythonhosted.org/packages/1d/3a/a6c595c310b7df958e739aae88724e24f9246a514d909547778d776799be/httptools-0.7.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:465275d76db4d554918aba40bf1cbebe324670f3dfc979eaffaa5d108e2ed650", size = 458337, upload-time = "2025-10-10T03:54:52.196Z" }, + { url = "https://files.pythonhosted.org/packages/fd/82/88e8d6d2c51edc1cc391b6e044c6c435b6aebe97b1abc33db1b0b24cd582/httptools-0.7.1-cp313-cp313-win_amd64.whl", hash = "sha256:322d00c2068d125bd570f7bf78b2d367dad02b919d8581d7476d8b75b294e3e6", size = 85743, upload-time = "2025-10-10T03:54:53.448Z" }, + { url = "https://files.pythonhosted.org/packages/34/50/9d095fcbb6de2d523e027a2f304d4551855c2f46e0b82befd718b8b20056/httptools-0.7.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:c08fe65728b8d70b6923ce31e3956f859d5e1e8548e6f22ec520a962c6757270", size = 203619, upload-time = "2025-10-10T03:54:54.321Z" }, + { url = "https://files.pythonhosted.org/packages/07/f0/89720dc5139ae54b03f861b5e2c55a37dba9a5da7d51e1e824a1f343627f/httptools-0.7.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:7aea2e3c3953521c3c51106ee11487a910d45586e351202474d45472db7d72d3", size = 108714, upload-time = "2025-10-10T03:54:55.163Z" }, + { url = "https://files.pythonhosted.org/packages/b3/cb/eea88506f191fb552c11787c23f9a405f4c7b0c5799bf73f2249cd4f5228/httptools-0.7.1-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0e68b8582f4ea9166be62926077a3334064d422cf08ab87d8b74664f8e9058e1", size = 472909, upload-time = "2025-10-10T03:54:56.056Z" }, + { url = "https://files.pythonhosted.org/packages/e0/4a/a548bdfae6369c0d078bab5769f7b66f17f1bfaa6fa28f81d6be6959066b/httptools-0.7.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:df091cf961a3be783d6aebae963cc9b71e00d57fa6f149025075217bc6a55a7b", size = 470831, upload-time = "2025-10-10T03:54:57.219Z" }, + { url = "https://files.pythonhosted.org/packages/4d/31/14df99e1c43bd132eec921c2e7e11cda7852f65619bc0fc5bdc2d0cb126c/httptools-0.7.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f084813239e1eb403ddacd06a30de3d3e09a9b76e7894dcda2b22f8a726e9c60", size = 452631, upload-time = "2025-10-10T03:54:58.219Z" }, + { url = "https://files.pythonhosted.org/packages/22/d2/b7e131f7be8d854d48cb6d048113c30f9a46dca0c9a8b08fcb3fcd588cdc/httptools-0.7.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7347714368fb2b335e9063bc2b96f2f87a9ceffcd9758ac295f8bbcd3ffbc0ca", size = 452910, upload-time = "2025-10-10T03:54:59.366Z" }, + { url = "https://files.pythonhosted.org/packages/53/cf/878f3b91e4e6e011eff6d1fa9ca39f7eb17d19c9d7971b04873734112f30/httptools-0.7.1-cp314-cp314-win_amd64.whl", hash = "sha256:cfabda2a5bb85aa2a904ce06d974a3f30fb36cc63d7feaddec05d2050acede96", size = 88205, upload-time = "2025-10-10T03:55:00.389Z" }, +] + +[[package]] +name = "httpx" +version = "0.28.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "certifi" }, + { name = "httpcore" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, +] + +[[package]] +name = "idna" +version = "3.11" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, +] + +[[package]] +name = "jinja2" +version = "3.1.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, +] + +[[package]] +name = "jsonalias" +version = "0.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ec/45/ee7e17002cb7f3264f755ff6a1a72c55d1830e07808d643167d2a2277c4f/jsonalias-0.1.1.tar.gz", hash = "sha256:64f04d935397d579fc94509e1fcb6212f2d081235d9d6395bd10baedf760a769", size = 1095, upload-time = "2022-10-28T22:57:56.224Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/41/ed/05aebce69f78c104feff2ffcdd5a6f9d668a208aba3a8bf56e3750809fd8/jsonalias-0.1.1-py3-none-any.whl", hash = "sha256:a56d2888e6397812c606156504e861e8ec00e188005af149f003c787db3d3f18", size = 1312, upload-time = "2022-10-28T22:57:54.763Z" }, +] + +[[package]] +name = "librt" +version = "0.7.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b5/8a/071f6628363d83e803d4783e0cd24fb9c5b798164300fcfaaa47c30659c0/librt-0.7.5.tar.gz", hash = "sha256:de4221a1181fa9c8c4b5f35506ed6f298948f44003d84d2a8b9885d7e01e6cfa", size = 145868, upload-time = "2025-12-25T03:53:16.039Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/f2/3248d8419db99ab80bb36266735d1241f766ad5fd993071211f789b618a5/librt-0.7.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:81056e01bba1394f1d92904ec61a4078f66df785316275edbaf51d90da8c6e26", size = 54703, upload-time = "2025-12-25T03:51:48.394Z" }, + { url = "https://files.pythonhosted.org/packages/7b/30/7e179543dbcb1311f84b7e797658ad85cf2d4474c468f5dbafa13f2a98a5/librt-0.7.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d7c72c8756eeb3aefb1b9e3dac7c37a4a25db63640cac0ab6fc18e91a0edf05a", size = 56660, upload-time = "2025-12-25T03:51:49.791Z" }, + { url = "https://files.pythonhosted.org/packages/15/91/3ba03ac1ac1abd66757a134b3bd56d9674928b163d0e686ea065a2bbb92d/librt-0.7.5-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:ddc4a16207f88f9597b397fc1f60781266d13b13de922ff61c206547a29e4bbd", size = 161026, upload-time = "2025-12-25T03:51:51.021Z" }, + { url = "https://files.pythonhosted.org/packages/0d/6e/b8365f547817d37b44c4be2ffa02630be995ef18be52d72698cecc3640c5/librt-0.7.5-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:63055d3dda433ebb314c9f1819942f16a19203c454508fdb2d167613f7017169", size = 169530, upload-time = "2025-12-25T03:51:52.417Z" }, + { url = "https://files.pythonhosted.org/packages/63/6a/8442eb0b6933c651a06e1888f863971f3391cc11338fdaa6ab969f7d1eac/librt-0.7.5-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9f85f9b5db87b0f52e53c68ad2a0c5a53e00afa439bd54a1723742a2b1021276", size = 183272, upload-time = "2025-12-25T03:51:53.713Z" }, + { url = "https://files.pythonhosted.org/packages/90/c4/b1166df6ef8e1f68d309f50bf69e8e750a5ea12fe7e2cf202c771ff359fc/librt-0.7.5-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:c566a4672564c5d54d8ab65cdaae5a87ee14c1564c1a2ddc7a9f5811c750f023", size = 179040, upload-time = "2025-12-25T03:51:55.048Z" }, + { url = "https://files.pythonhosted.org/packages/fc/30/8f3fd9fd975b16c37832d6c248b976d2a0e33f155063781e064f249b37f1/librt-0.7.5-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:fee15c2a190ef389f14928135c6fb2d25cd3fdb7887bfd9a7b444bbdc8c06b96", size = 173506, upload-time = "2025-12-25T03:51:56.407Z" }, + { url = "https://files.pythonhosted.org/packages/75/71/c3d4d5658f9849bf8e07ffba99f892d49a0c9a4001323ed610db72aedc82/librt-0.7.5-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:584cb3e605ec45ba350962cec853e17be0a25a772f21f09f1e422f7044ae2a7d", size = 193573, upload-time = "2025-12-25T03:51:57.949Z" }, + { url = "https://files.pythonhosted.org/packages/86/7c/c1c8a0116a2eed3d58c8946c589a8f9e1354b9b825cc92eba58bb15f6fb1/librt-0.7.5-cp310-cp310-win32.whl", hash = "sha256:9c08527055fbb03c641c15bbc5b79dd2942fb6a3bd8dabf141dd7e97eeea4904", size = 42603, upload-time = "2025-12-25T03:51:59.215Z" }, + { url = "https://files.pythonhosted.org/packages/1d/00/b52c77ca294247420020b829b70465c6e6f2b9d59ab21d8051aac20432da/librt-0.7.5-cp310-cp310-win_amd64.whl", hash = "sha256:dd810f2d39c526c42ea205e0addad5dc08ef853c625387806a29d07f9d150d9b", size = 48977, upload-time = "2025-12-25T03:52:00.519Z" }, + { url = "https://files.pythonhosted.org/packages/11/89/42b3ccb702a7e5f7a4cf2afc8a0a8f8c5e7d4b4d3a7c3de6357673dddddb/librt-0.7.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f952e1a78c480edee8fb43aa2bf2e84dcd46c917d44f8065b883079d3893e8fc", size = 54705, upload-time = "2025-12-25T03:52:01.433Z" }, + { url = "https://files.pythonhosted.org/packages/bb/90/c16970b509c3c448c365041d326eeef5aeb2abaed81eb3187b26a3cd13f8/librt-0.7.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:75965c1f4efb7234ff52a58b729d245a21e87e4b6a26a0ec08052f02b16274e4", size = 56667, upload-time = "2025-12-25T03:52:02.391Z" }, + { url = "https://files.pythonhosted.org/packages/ac/2f/da4bdf6c190503f4663fbb781dfae5564a2b1c3f39a2da8e1ac7536ac7bd/librt-0.7.5-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:732e0aa0385b59a1b2545159e781c792cc58ce9c134249233a7c7250a44684c4", size = 161705, upload-time = "2025-12-25T03:52:03.395Z" }, + { url = "https://files.pythonhosted.org/packages/fb/88/c5da8e1f5f22b23d56e1fbd87266799dcf32828d47bf69fabc6f9673c6eb/librt-0.7.5-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cdde31759bd8888f3ef0eebda80394a48961328a17c264dce8cc35f4b9cde35d", size = 171029, upload-time = "2025-12-25T03:52:04.798Z" }, + { url = "https://files.pythonhosted.org/packages/38/8a/8dfc00a6f1febc094ed9a55a448fc0b3a591b5dfd83be6cfd76d0910b1f0/librt-0.7.5-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:df3146d52465b3b6397d25d513f428cb421c18df65b7378667bb5f1e3cc45805", size = 184704, upload-time = "2025-12-25T03:52:05.887Z" }, + { url = "https://files.pythonhosted.org/packages/ad/57/65dec835ff235f431801064a3b41268f2f5ee0d224dc3bbf46d911af5c1a/librt-0.7.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:29c8d2fae11d4379ea207ba7fc69d43237e42cf8a9f90ec6e05993687e6d648b", size = 180720, upload-time = "2025-12-25T03:52:06.925Z" }, + { url = "https://files.pythonhosted.org/packages/1e/27/92033d169bbcaa0d9a2dd476c179e5171ec22ed574b1b135a3c6104fb7d4/librt-0.7.5-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:bb41f04046b4f22b1e7ba5ef513402cd2e3477ec610e5f92d38fe2bba383d419", size = 174538, upload-time = "2025-12-25T03:52:08.075Z" }, + { url = "https://files.pythonhosted.org/packages/44/5c/0127098743575d5340624d8d4ec508d4d5ff0877dcee6f55f54bf03e5ed0/librt-0.7.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:8bb7883c1e94ceb87c2bf81385266f032da09cd040e804cc002f2c9d6b842e2f", size = 195240, upload-time = "2025-12-25T03:52:09.427Z" }, + { url = "https://files.pythonhosted.org/packages/47/0f/be028c3e906a8ee6d29a42fd362e6d57d4143057f2bc0c454d489a0f898b/librt-0.7.5-cp311-cp311-win32.whl", hash = "sha256:84d4a6b9efd6124f728558a18e79e7cc5c5d4efc09b2b846c910de7e564f5bad", size = 42941, upload-time = "2025-12-25T03:52:10.527Z" }, + { url = "https://files.pythonhosted.org/packages/ac/3a/2f0ed57f4c3ae3c841780a95dfbea4cd811c6842d9ee66171ce1af606d25/librt-0.7.5-cp311-cp311-win_amd64.whl", hash = "sha256:ab4b0d3bee6f6ff7017e18e576ac7e41a06697d8dea4b8f3ab9e0c8e1300c409", size = 49244, upload-time = "2025-12-25T03:52:11.832Z" }, + { url = "https://files.pythonhosted.org/packages/ee/7c/d7932aedfa5a87771f9e2799e7185ec3a322f4a1f4aa87c234159b75c8c8/librt-0.7.5-cp311-cp311-win_arm64.whl", hash = "sha256:730be847daad773a3c898943cf67fb9845a3961d06fb79672ceb0a8cd8624cfa", size = 42614, upload-time = "2025-12-25T03:52:12.745Z" }, + { url = "https://files.pythonhosted.org/packages/33/9d/cb0a296cee177c0fee7999ada1c1af7eee0e2191372058814a4ca6d2baf0/librt-0.7.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ba1077c562a046208a2dc6366227b3eeae8f2c2ab4b41eaf4fd2fa28cece4203", size = 55689, upload-time = "2025-12-25T03:52:14.041Z" }, + { url = "https://files.pythonhosted.org/packages/79/5c/d7de4d4228b74c5b81a3fbada157754bb29f0e1f8c38229c669a7f90422a/librt-0.7.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:654fdc971c76348a73af5240d8e2529265b9a7ba6321e38dd5bae7b0d4ab3abe", size = 57142, upload-time = "2025-12-25T03:52:15.336Z" }, + { url = "https://files.pythonhosted.org/packages/e5/b2/5da779184aae369b69f4ae84225f63741662a0fe422e91616c533895d7a4/librt-0.7.5-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:6b7b58913d475911f6f33e8082f19dd9b120c4f4a5c911d07e395d67b81c6982", size = 165323, upload-time = "2025-12-25T03:52:16.384Z" }, + { url = "https://files.pythonhosted.org/packages/5a/40/6d5abc15ab6cc70e04c4d201bb28baffff4cfb46ab950b8e90935b162d58/librt-0.7.5-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b8e0fd344bad57026a8f4ccfaf406486c2fc991838050c2fef156170edc3b775", size = 174218, upload-time = "2025-12-25T03:52:17.518Z" }, + { url = "https://files.pythonhosted.org/packages/0d/d0/5239a8507e6117a3cb59ce0095bdd258bd2a93d8d4b819a506da06d8d645/librt-0.7.5-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:46aa91813c267c3f60db75d56419b42c0c0b9748ec2c568a0e3588e543fb4233", size = 189007, upload-time = "2025-12-25T03:52:18.585Z" }, + { url = "https://files.pythonhosted.org/packages/1f/a4/8eed1166ffddbb01c25363e4c4e655f4bac298debe9e5a2dcfaf942438a1/librt-0.7.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ddc0ab9dbc5f9ceaf2bf7a367bf01f2697660e908f6534800e88f43590b271db", size = 183962, upload-time = "2025-12-25T03:52:19.723Z" }, + { url = "https://files.pythonhosted.org/packages/a1/83/260e60aab2f5ccba04579c5c46eb3b855e51196fde6e2bcf6742d89140a8/librt-0.7.5-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:7a488908a470451338607650f1c064175094aedebf4a4fa37890682e30ce0b57", size = 177611, upload-time = "2025-12-25T03:52:21.18Z" }, + { url = "https://files.pythonhosted.org/packages/c4/36/6dcfed0df41e9695665462bab59af15b7ed2b9c668d85c7ebadd022cbb76/librt-0.7.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e47fc52602ffc374e69bf1b76536dc99f7f6dd876bd786c8213eaa3598be030a", size = 199273, upload-time = "2025-12-25T03:52:22.25Z" }, + { url = "https://files.pythonhosted.org/packages/a6/b7/157149c8cffae6bc4293a52e0267860cee2398cb270798d94f1c8a69b9ae/librt-0.7.5-cp312-cp312-win32.whl", hash = "sha256:cda8b025875946ffff5a9a7590bf9acde3eb02cb6200f06a2d3e691ef3d9955b", size = 43191, upload-time = "2025-12-25T03:52:23.643Z" }, + { url = "https://files.pythonhosted.org/packages/f8/91/197dfeb8d3bdeb0a5344d0d8b3077f183ba5e76c03f158126f6072730998/librt-0.7.5-cp312-cp312-win_amd64.whl", hash = "sha256:b591c094afd0ffda820e931148c9e48dc31a556dc5b2b9b3cc552fa710d858e4", size = 49462, upload-time = "2025-12-25T03:52:24.637Z" }, + { url = "https://files.pythonhosted.org/packages/03/ea/052a79454cc52081dfaa9a1c4c10a529f7a6a6805b2fac5805fea5b25975/librt-0.7.5-cp312-cp312-win_arm64.whl", hash = "sha256:532ddc6a8a6ca341b1cd7f4d999043e4c71a212b26fe9fd2e7f1e8bb4e873544", size = 42830, upload-time = "2025-12-25T03:52:25.944Z" }, + { url = "https://files.pythonhosted.org/packages/9f/9a/8f61e16de0ff76590af893cfb5b1aa5fa8b13e5e54433d0809c7033f59ed/librt-0.7.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b1795c4b2789b458fa290059062c2f5a297ddb28c31e704d27e161386469691a", size = 55750, upload-time = "2025-12-25T03:52:26.975Z" }, + { url = "https://files.pythonhosted.org/packages/05/7c/a8a883804851a066f301e0bad22b462260b965d5c9e7fe3c5de04e6f91f8/librt-0.7.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2fcbf2e135c11f721193aa5f42ba112bb1046afafbffd407cbc81d8d735c74d0", size = 57170, upload-time = "2025-12-25T03:52:27.948Z" }, + { url = "https://files.pythonhosted.org/packages/d6/5d/b3b47facf5945be294cf8a835b03589f70ee0e791522f99ec6782ed738b3/librt-0.7.5-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:c039bbf79a9a2498404d1ae7e29a6c175e63678d7a54013a97397c40aee026c5", size = 165834, upload-time = "2025-12-25T03:52:29.09Z" }, + { url = "https://files.pythonhosted.org/packages/b4/b6/b26910cd0a4e43e5d02aacaaea0db0d2a52e87660dca08293067ee05601a/librt-0.7.5-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3919c9407faeeee35430ae135e3a78acd4ecaaaa73767529e2c15ca1d73ba325", size = 174820, upload-time = "2025-12-25T03:52:30.463Z" }, + { url = "https://files.pythonhosted.org/packages/a5/a3/81feddd345d4c869b7a693135a462ae275f964fcbbe793d01ea56a84c2ee/librt-0.7.5-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:26b46620e1e0e45af510d9848ea0915e7040605dd2ae94ebefb6c962cbb6f7ec", size = 189609, upload-time = "2025-12-25T03:52:31.492Z" }, + { url = "https://files.pythonhosted.org/packages/ce/a9/31310796ef4157d1d37648bf4a3b84555319f14cee3e9bad7bdd7bfd9a35/librt-0.7.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:9bbb8facc5375476d392990dd6a71f97e4cb42e2ac66f32e860f6e47299d5e89", size = 184589, upload-time = "2025-12-25T03:52:32.59Z" }, + { url = "https://files.pythonhosted.org/packages/32/22/da3900544cb0ac6ab7a2857850158a0a093b86f92b264aa6c4a4f2355ff3/librt-0.7.5-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:e9e9c988b5ffde7be02180f864cbd17c0b0c1231c235748912ab2afa05789c25", size = 178251, upload-time = "2025-12-25T03:52:33.745Z" }, + { url = "https://files.pythonhosted.org/packages/db/77/78e02609846e78b9b8c8e361753b3dbac9a07e6d5b567fe518de9e074ab0/librt-0.7.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:edf6b465306215b19dbe6c3fb63cf374a8f3e1ad77f3b4c16544b83033bbb67b", size = 199852, upload-time = "2025-12-25T03:52:34.826Z" }, + { url = "https://files.pythonhosted.org/packages/2a/25/05706f6b346429c951582f1b3561f4d5e1418d0d7ba1a0c181237cd77b3b/librt-0.7.5-cp313-cp313-win32.whl", hash = "sha256:060bde69c3604f694bd8ae21a780fe8be46bb3dbb863642e8dfc75c931ca8eee", size = 43250, upload-time = "2025-12-25T03:52:35.905Z" }, + { url = "https://files.pythonhosted.org/packages/d9/59/c38677278ac0b9ae1afc611382ef6c9ea87f52ad257bd3d8d65f0eacdc6a/librt-0.7.5-cp313-cp313-win_amd64.whl", hash = "sha256:a82d5a0ee43aeae2116d7292c77cc8038f4841830ade8aa922e098933b468b9e", size = 49421, upload-time = "2025-12-25T03:52:36.895Z" }, + { url = "https://files.pythonhosted.org/packages/c0/47/1d71113df4a81de5fdfbd3d7244e05d3d67e89f25455c3380ca50b92741e/librt-0.7.5-cp313-cp313-win_arm64.whl", hash = "sha256:3c98a8d0ac9e2a7cb8ff8c53e5d6e8d82bfb2839abf144fdeaaa832f2a12aa45", size = 42827, upload-time = "2025-12-25T03:52:37.856Z" }, + { url = "https://files.pythonhosted.org/packages/97/ae/8635b4efdc784220f1378be640d8b1a794332f7f6ea81bb4859bf9d18aa7/librt-0.7.5-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:9937574e6d842f359b8585903d04f5b4ab62277a091a93e02058158074dc52f2", size = 55191, upload-time = "2025-12-25T03:52:38.839Z" }, + { url = "https://files.pythonhosted.org/packages/52/11/ed7ef6955dc2032af37db9b0b31cd5486a138aa792e1bb9e64f0f4950e27/librt-0.7.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:5cd3afd71e9bc146203b6c8141921e738364158d4aa7cdb9a874e2505163770f", size = 56894, upload-time = "2025-12-25T03:52:39.805Z" }, + { url = "https://files.pythonhosted.org/packages/24/f1/02921d4a66a1b5dcd0493b89ce76e2762b98c459fe2ad04b67b2ea6fdd39/librt-0.7.5-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:9cffa3ef0af29687455161cb446eff059bf27607f95163d6a37e27bcb37180f6", size = 163726, upload-time = "2025-12-25T03:52:40.79Z" }, + { url = "https://files.pythonhosted.org/packages/65/87/27df46d2756fcb7a82fa7f6ca038a0c6064c3e93ba65b0b86fbf6a4f76a2/librt-0.7.5-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:82f3f088482e2229387eadf8215c03f7726d56f69cce8c0c40f0795aebc9b361", size = 172470, upload-time = "2025-12-25T03:52:42.226Z" }, + { url = "https://files.pythonhosted.org/packages/9f/a9/e65a35e5d423639f4f3d8e17301ff13cc41c2ff97677fe9c361c26dbfbb7/librt-0.7.5-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d7aa33153a5bb0bac783d2c57885889b1162823384e8313d47800a0e10d0070e", size = 186807, upload-time = "2025-12-25T03:52:43.688Z" }, + { url = "https://files.pythonhosted.org/packages/d7/b0/ac68aa582a996b1241773bd419823290c42a13dc9f494704a12a17ddd7b6/librt-0.7.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:265729b551a2dd329cc47b323a182fb7961af42abf21e913c9dd7d3331b2f3c2", size = 181810, upload-time = "2025-12-25T03:52:45.095Z" }, + { url = "https://files.pythonhosted.org/packages/e1/c1/03f6717677f20acd2d690813ec2bbe12a2de305f32c61479c53f7b9413bc/librt-0.7.5-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:168e04663e126416ba712114050f413ac306759a1791d87b7c11d4428ba75760", size = 175599, upload-time = "2025-12-25T03:52:46.177Z" }, + { url = "https://files.pythonhosted.org/packages/01/d7/f976ff4c07c59b69bb5eec7e5886d43243075bbef834428124b073471c86/librt-0.7.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:553dc58987d1d853adda8aeadf4db8e29749f0b11877afcc429a9ad892818ae2", size = 196506, upload-time = "2025-12-25T03:52:47.327Z" }, + { url = "https://files.pythonhosted.org/packages/b7/74/004f068b8888e61b454568b5479f88018fceb14e511ac0609cccee7dd227/librt-0.7.5-cp314-cp314-win32.whl", hash = "sha256:263f4fae9eba277513357c871275b18d14de93fd49bf5e43dc60a97b81ad5eb8", size = 39747, upload-time = "2025-12-25T03:52:48.437Z" }, + { url = "https://files.pythonhosted.org/packages/37/b1/ea3ec8fcf5f0a00df21f08972af77ad799604a306db58587308067d27af8/librt-0.7.5-cp314-cp314-win_amd64.whl", hash = "sha256:85f485b7471571e99fab4f44eeb327dc0e1f814ada575f3fa85e698417d8a54e", size = 45970, upload-time = "2025-12-25T03:52:49.389Z" }, + { url = "https://files.pythonhosted.org/packages/5d/30/5e3fb7ac4614a50fc67e6954926137d50ebc27f36419c9963a94f931f649/librt-0.7.5-cp314-cp314-win_arm64.whl", hash = "sha256:49c596cd18e90e58b7caa4d7ca7606049c1802125fcff96b8af73fa5c3870e4d", size = 39075, upload-time = "2025-12-25T03:52:50.395Z" }, + { url = "https://files.pythonhosted.org/packages/a4/7f/0af0a9306a06c2aabee3a790f5aa560c50ec0a486ab818a572dd3db6c851/librt-0.7.5-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:54d2aef0b0f5056f130981ad45081b278602ff3657fe16c88529f5058038e802", size = 57375, upload-time = "2025-12-25T03:52:51.439Z" }, + { url = "https://files.pythonhosted.org/packages/57/1f/c85e510baf6572a3d6ef40c742eacedc02973ed2acdb5dba2658751d9af8/librt-0.7.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0b4791202296ad51ac09a3ff58eb49d9da8e3a4009167a6d76ac418a974e5fd4", size = 59234, upload-time = "2025-12-25T03:52:52.687Z" }, + { url = "https://files.pythonhosted.org/packages/49/b1/bb6535e4250cd18b88d6b18257575a0239fa1609ebba925f55f51ae08e8e/librt-0.7.5-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:6e860909fea75baef941ee6436e0453612505883b9d0d87924d4fda27865b9a2", size = 183873, upload-time = "2025-12-25T03:52:53.705Z" }, + { url = "https://files.pythonhosted.org/packages/8e/49/ad4a138cca46cdaa7f0e15fa912ce3ccb4cc0d4090bfeb8ccc35766fa6d5/librt-0.7.5-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f02c4337bf271c4f06637f5ff254fad2238c0b8e32a3a480ebb2fc5e26f754a5", size = 194609, upload-time = "2025-12-25T03:52:54.884Z" }, + { url = "https://files.pythonhosted.org/packages/9c/2d/3b3cb933092d94bb2c1d3c9b503d8775f08d806588c19a91ee4d1495c2a8/librt-0.7.5-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f7f51ffe59f4556243d3cc82d827bde74765f594fa3ceb80ec4de0c13ccd3416", size = 206777, upload-time = "2025-12-25T03:52:55.969Z" }, + { url = "https://files.pythonhosted.org/packages/3a/52/6e7611d3d1347812233dabc44abca4c8065ee97b83c9790d7ecc3f782bc8/librt-0.7.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:0b7f080ba30601dfa3e3deed3160352273e1b9bc92e652f51103c3e9298f7899", size = 203208, upload-time = "2025-12-25T03:52:57.036Z" }, + { url = "https://files.pythonhosted.org/packages/27/aa/466ae4654bd2d45903fbf180815d41e3ae8903e5a1861f319f73c960a843/librt-0.7.5-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:fb565b4219abc8ea2402e61c7ba648a62903831059ed3564fa1245cc245d58d7", size = 196698, upload-time = "2025-12-25T03:52:58.481Z" }, + { url = "https://files.pythonhosted.org/packages/97/8f/424f7e4525bb26fe0d3e984d1c0810ced95e53be4fd867ad5916776e18a3/librt-0.7.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:8a3cfb15961e7333ea6ef033dc574af75153b5c230d5ad25fbcd55198f21e0cf", size = 217194, upload-time = "2025-12-25T03:52:59.575Z" }, + { url = "https://files.pythonhosted.org/packages/9e/33/13a4cb798a171b173f3c94db23adaf13a417130e1493933dc0df0d7fb439/librt-0.7.5-cp314-cp314t-win32.whl", hash = "sha256:118716de5ad6726332db1801bc90fa6d94194cd2e07c1a7822cebf12c496714d", size = 40282, upload-time = "2025-12-25T03:53:01.091Z" }, + { url = "https://files.pythonhosted.org/packages/5f/f1/62b136301796399d65dad73b580f4509bcbd347dff885a450bff08e80cb6/librt-0.7.5-cp314-cp314t-win_amd64.whl", hash = "sha256:3dd58f7ce20360c6ce0c04f7bd9081c7f9c19fc6129a3c705d0c5a35439f201d", size = 46764, upload-time = "2025-12-25T03:53:02.381Z" }, + { url = "https://files.pythonhosted.org/packages/49/cb/940431d9410fda74f941f5cd7f0e5a22c63be7b0c10fa98b2b7022b48cb1/librt-0.7.5-cp314-cp314t-win_arm64.whl", hash = "sha256:08153ea537609d11f774d2bfe84af39d50d5c9ca3a4d061d946e0c9d8bce04a1", size = 39728, upload-time = "2025-12-25T03:53:03.306Z" }, +] + +[[package]] +name = "markdown-it-py" +version = "4.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mdurl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5b/f5/4ec618ed16cc4f8fb3b701563655a69816155e79e24a17b651541804721d/markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3", size = 73070, upload-time = "2025-08-11T12:57:52.854Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321, upload-time = "2025-08-11T12:57:51.923Z" }, +] + +[[package]] +name = "markupsafe" +version = "3.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e8/4b/3541d44f3937ba468b75da9eebcae497dcf67adb65caa16760b0a6807ebb/markupsafe-3.0.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2f981d352f04553a7171b8e44369f2af4055f888dfb147d55e42d29e29e74559", size = 11631, upload-time = "2025-09-27T18:36:05.558Z" }, + { url = "https://files.pythonhosted.org/packages/98/1b/fbd8eed11021cabd9226c37342fa6ca4e8a98d8188a8d9b66740494960e4/markupsafe-3.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e1c1493fb6e50ab01d20a22826e57520f1284df32f2d8601fdd90b6304601419", size = 12057, upload-time = "2025-09-27T18:36:07.165Z" }, + { url = "https://files.pythonhosted.org/packages/40/01/e560d658dc0bb8ab762670ece35281dec7b6c1b33f5fbc09ebb57a185519/markupsafe-3.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ba88449deb3de88bd40044603fafffb7bc2b055d626a330323a9ed736661695", size = 22050, upload-time = "2025-09-27T18:36:08.005Z" }, + { url = "https://files.pythonhosted.org/packages/af/cd/ce6e848bbf2c32314c9b237839119c5a564a59725b53157c856e90937b7a/markupsafe-3.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f42d0984e947b8adf7dd6dde396e720934d12c506ce84eea8476409563607591", size = 20681, upload-time = "2025-09-27T18:36:08.881Z" }, + { url = "https://files.pythonhosted.org/packages/c9/2a/b5c12c809f1c3045c4d580b035a743d12fcde53cf685dbc44660826308da/markupsafe-3.0.3-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c0c0b3ade1c0b13b936d7970b1d37a57acde9199dc2aecc4c336773e1d86049c", size = 20705, upload-time = "2025-09-27T18:36:10.131Z" }, + { url = "https://files.pythonhosted.org/packages/cf/e3/9427a68c82728d0a88c50f890d0fc072a1484de2f3ac1ad0bfc1a7214fd5/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0303439a41979d9e74d18ff5e2dd8c43ed6c6001fd40e5bf2e43f7bd9bbc523f", size = 21524, upload-time = "2025-09-27T18:36:11.324Z" }, + { url = "https://files.pythonhosted.org/packages/bc/36/23578f29e9e582a4d0278e009b38081dbe363c5e7165113fad546918a232/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:d2ee202e79d8ed691ceebae8e0486bd9a2cd4794cec4824e1c99b6f5009502f6", size = 20282, upload-time = "2025-09-27T18:36:12.573Z" }, + { url = "https://files.pythonhosted.org/packages/56/21/dca11354e756ebd03e036bd8ad58d6d7168c80ce1fe5e75218e4945cbab7/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:177b5253b2834fe3678cb4a5f0059808258584c559193998be2601324fdeafb1", size = 20745, upload-time = "2025-09-27T18:36:13.504Z" }, + { url = "https://files.pythonhosted.org/packages/87/99/faba9369a7ad6e4d10b6a5fbf71fa2a188fe4a593b15f0963b73859a1bbd/markupsafe-3.0.3-cp310-cp310-win32.whl", hash = "sha256:2a15a08b17dd94c53a1da0438822d70ebcd13f8c3a95abe3a9ef9f11a94830aa", size = 14571, upload-time = "2025-09-27T18:36:14.779Z" }, + { url = "https://files.pythonhosted.org/packages/d6/25/55dc3ab959917602c96985cb1253efaa4ff42f71194bddeb61eb7278b8be/markupsafe-3.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:c4ffb7ebf07cfe8931028e3e4c85f0357459a3f9f9490886198848f4fa002ec8", size = 15056, upload-time = "2025-09-27T18:36:16.125Z" }, + { url = "https://files.pythonhosted.org/packages/d0/9e/0a02226640c255d1da0b8d12e24ac2aa6734da68bff14c05dd53b94a0fc3/markupsafe-3.0.3-cp310-cp310-win_arm64.whl", hash = "sha256:e2103a929dfa2fcaf9bb4e7c091983a49c9ac3b19c9061b6d5427dd7d14d81a1", size = 13932, upload-time = "2025-09-27T18:36:17.311Z" }, + { url = "https://files.pythonhosted.org/packages/08/db/fefacb2136439fc8dd20e797950e749aa1f4997ed584c62cfb8ef7c2be0e/markupsafe-3.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1cc7ea17a6824959616c525620e387f6dd30fec8cb44f649e31712db02123dad", size = 11631, upload-time = "2025-09-27T18:36:18.185Z" }, + { url = "https://files.pythonhosted.org/packages/e1/2e/5898933336b61975ce9dc04decbc0a7f2fee78c30353c5efba7f2d6ff27a/markupsafe-3.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4bd4cd07944443f5a265608cc6aab442e4f74dff8088b0dfc8238647b8f6ae9a", size = 12058, upload-time = "2025-09-27T18:36:19.444Z" }, + { url = "https://files.pythonhosted.org/packages/1d/09/adf2df3699d87d1d8184038df46a9c80d78c0148492323f4693df54e17bb/markupsafe-3.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b5420a1d9450023228968e7e6a9ce57f65d148ab56d2313fcd589eee96a7a50", size = 24287, upload-time = "2025-09-27T18:36:20.768Z" }, + { url = "https://files.pythonhosted.org/packages/30/ac/0273f6fcb5f42e314c6d8cd99effae6a5354604d461b8d392b5ec9530a54/markupsafe-3.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0bf2a864d67e76e5c9a34dc26ec616a66b9888e25e7b9460e1c76d3293bd9dbf", size = 22940, upload-time = "2025-09-27T18:36:22.249Z" }, + { url = "https://files.pythonhosted.org/packages/19/ae/31c1be199ef767124c042c6c3e904da327a2f7f0cd63a0337e1eca2967a8/markupsafe-3.0.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc51efed119bc9cfdf792cdeaa4d67e8f6fcccab66ed4bfdd6bde3e59bfcbb2f", size = 21887, upload-time = "2025-09-27T18:36:23.535Z" }, + { url = "https://files.pythonhosted.org/packages/b2/76/7edcab99d5349a4532a459e1fe64f0b0467a3365056ae550d3bcf3f79e1e/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:068f375c472b3e7acbe2d5318dea141359e6900156b5b2ba06a30b169086b91a", size = 23692, upload-time = "2025-09-27T18:36:24.823Z" }, + { url = "https://files.pythonhosted.org/packages/a4/28/6e74cdd26d7514849143d69f0bf2399f929c37dc2b31e6829fd2045b2765/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7be7b61bb172e1ed687f1754f8e7484f1c8019780f6f6b0786e76bb01c2ae115", size = 21471, upload-time = "2025-09-27T18:36:25.95Z" }, + { url = "https://files.pythonhosted.org/packages/62/7e/a145f36a5c2945673e590850a6f8014318d5577ed7e5920a4b3448e0865d/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a", size = 22923, upload-time = "2025-09-27T18:36:27.109Z" }, + { url = "https://files.pythonhosted.org/packages/0f/62/d9c46a7f5c9adbeeeda52f5b8d802e1094e9717705a645efc71b0913a0a8/markupsafe-3.0.3-cp311-cp311-win32.whl", hash = "sha256:0db14f5dafddbb6d9208827849fad01f1a2609380add406671a26386cdf15a19", size = 14572, upload-time = "2025-09-27T18:36:28.045Z" }, + { url = "https://files.pythonhosted.org/packages/83/8a/4414c03d3f891739326e1783338e48fb49781cc915b2e0ee052aa490d586/markupsafe-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:de8a88e63464af587c950061a5e6a67d3632e36df62b986892331d4620a35c01", size = 15077, upload-time = "2025-09-27T18:36:29.025Z" }, + { url = "https://files.pythonhosted.org/packages/35/73/893072b42e6862f319b5207adc9ae06070f095b358655f077f69a35601f0/markupsafe-3.0.3-cp311-cp311-win_arm64.whl", hash = "sha256:3b562dd9e9ea93f13d53989d23a7e775fdfd1066c33494ff43f5418bc8c58a5c", size = 13876, upload-time = "2025-09-27T18:36:29.954Z" }, + { url = "https://files.pythonhosted.org/packages/5a/72/147da192e38635ada20e0a2e1a51cf8823d2119ce8883f7053879c2199b5/markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e", size = 11615, upload-time = "2025-09-27T18:36:30.854Z" }, + { url = "https://files.pythonhosted.org/packages/9a/81/7e4e08678a1f98521201c3079f77db69fb552acd56067661f8c2f534a718/markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce", size = 12020, upload-time = "2025-09-27T18:36:31.971Z" }, + { url = "https://files.pythonhosted.org/packages/1e/2c/799f4742efc39633a1b54a92eec4082e4f815314869865d876824c257c1e/markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d", size = 24332, upload-time = "2025-09-27T18:36:32.813Z" }, + { url = "https://files.pythonhosted.org/packages/3c/2e/8d0c2ab90a8c1d9a24f0399058ab8519a3279d1bd4289511d74e909f060e/markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d", size = 22947, upload-time = "2025-09-27T18:36:33.86Z" }, + { url = "https://files.pythonhosted.org/packages/2c/54/887f3092a85238093a0b2154bd629c89444f395618842e8b0c41783898ea/markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a", size = 21962, upload-time = "2025-09-27T18:36:35.099Z" }, + { url = "https://files.pythonhosted.org/packages/c9/2f/336b8c7b6f4a4d95e91119dc8521402461b74a485558d8f238a68312f11c/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b", size = 23760, upload-time = "2025-09-27T18:36:36.001Z" }, + { url = "https://files.pythonhosted.org/packages/32/43/67935f2b7e4982ffb50a4d169b724d74b62a3964bc1a9a527f5ac4f1ee2b/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f", size = 21529, upload-time = "2025-09-27T18:36:36.906Z" }, + { url = "https://files.pythonhosted.org/packages/89/e0/4486f11e51bbba8b0c041098859e869e304d1c261e59244baa3d295d47b7/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b", size = 23015, upload-time = "2025-09-27T18:36:37.868Z" }, + { url = "https://files.pythonhosted.org/packages/2f/e1/78ee7a023dac597a5825441ebd17170785a9dab23de95d2c7508ade94e0e/markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d", size = 14540, upload-time = "2025-09-27T18:36:38.761Z" }, + { url = "https://files.pythonhosted.org/packages/aa/5b/bec5aa9bbbb2c946ca2733ef9c4ca91c91b6a24580193e891b5f7dbe8e1e/markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c", size = 15105, upload-time = "2025-09-27T18:36:39.701Z" }, + { url = "https://files.pythonhosted.org/packages/e5/f1/216fc1bbfd74011693a4fd837e7026152e89c4bcf3e77b6692fba9923123/markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f", size = 13906, upload-time = "2025-09-27T18:36:40.689Z" }, + { url = "https://files.pythonhosted.org/packages/38/2f/907b9c7bbba283e68f20259574b13d005c121a0fa4c175f9bed27c4597ff/markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795", size = 11622, upload-time = "2025-09-27T18:36:41.777Z" }, + { url = "https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", size = 12029, upload-time = "2025-09-27T18:36:43.257Z" }, + { url = "https://files.pythonhosted.org/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", size = 24374, upload-time = "2025-09-27T18:36:44.508Z" }, + { url = "https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676", size = 22980, upload-time = "2025-09-27T18:36:45.385Z" }, + { url = "https://files.pythonhosted.org/packages/7f/71/544260864f893f18b6827315b988c146b559391e6e7e8f7252839b1b846a/markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9", size = 21990, upload-time = "2025-09-27T18:36:46.916Z" }, + { url = "https://files.pythonhosted.org/packages/c2/28/b50fc2f74d1ad761af2f5dcce7492648b983d00a65b8c0e0cb457c82ebbe/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1", size = 23784, upload-time = "2025-09-27T18:36:47.884Z" }, + { url = "https://files.pythonhosted.org/packages/ed/76/104b2aa106a208da8b17a2fb72e033a5a9d7073c68f7e508b94916ed47a9/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc", size = 21588, upload-time = "2025-09-27T18:36:48.82Z" }, + { url = "https://files.pythonhosted.org/packages/b5/99/16a5eb2d140087ebd97180d95249b00a03aa87e29cc224056274f2e45fd6/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12", size = 23041, upload-time = "2025-09-27T18:36:49.797Z" }, + { url = "https://files.pythonhosted.org/packages/19/bc/e7140ed90c5d61d77cea142eed9f9c303f4c4806f60a1044c13e3f1471d0/markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed", size = 14543, upload-time = "2025-09-27T18:36:51.584Z" }, + { url = "https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5", size = 15113, upload-time = "2025-09-27T18:36:52.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/3a/fa34a0f7cfef23cf9500d68cb7c32dd64ffd58a12b09225fb03dd37d5b80/markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485", size = 13911, upload-time = "2025-09-27T18:36:53.513Z" }, + { url = "https://files.pythonhosted.org/packages/e4/d7/e05cd7efe43a88a17a37b3ae96e79a19e846f3f456fe79c57ca61356ef01/markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73", size = 11658, upload-time = "2025-09-27T18:36:54.819Z" }, + { url = "https://files.pythonhosted.org/packages/99/9e/e412117548182ce2148bdeacdda3bb494260c0b0184360fe0d56389b523b/markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37", size = 12066, upload-time = "2025-09-27T18:36:55.714Z" }, + { url = "https://files.pythonhosted.org/packages/bc/e6/fa0ffcda717ef64a5108eaa7b4f5ed28d56122c9a6d70ab8b72f9f715c80/markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19", size = 25639, upload-time = "2025-09-27T18:36:56.908Z" }, + { url = "https://files.pythonhosted.org/packages/96/ec/2102e881fe9d25fc16cb4b25d5f5cde50970967ffa5dddafdb771237062d/markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025", size = 23569, upload-time = "2025-09-27T18:36:57.913Z" }, + { url = "https://files.pythonhosted.org/packages/4b/30/6f2fce1f1f205fc9323255b216ca8a235b15860c34b6798f810f05828e32/markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6", size = 23284, upload-time = "2025-09-27T18:36:58.833Z" }, + { url = "https://files.pythonhosted.org/packages/58/47/4a0ccea4ab9f5dcb6f79c0236d954acb382202721e704223a8aafa38b5c8/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f", size = 24801, upload-time = "2025-09-27T18:36:59.739Z" }, + { url = "https://files.pythonhosted.org/packages/6a/70/3780e9b72180b6fecb83a4814d84c3bf4b4ae4bf0b19c27196104149734c/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb", size = 22769, upload-time = "2025-09-27T18:37:00.719Z" }, + { url = "https://files.pythonhosted.org/packages/98/c5/c03c7f4125180fc215220c035beac6b9cb684bc7a067c84fc69414d315f5/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009", size = 23642, upload-time = "2025-09-27T18:37:01.673Z" }, + { url = "https://files.pythonhosted.org/packages/80/d6/2d1b89f6ca4bff1036499b1e29a1d02d282259f3681540e16563f27ebc23/markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354", size = 14612, upload-time = "2025-09-27T18:37:02.639Z" }, + { url = "https://files.pythonhosted.org/packages/2b/98/e48a4bfba0a0ffcf9925fe2d69240bfaa19c6f7507b8cd09c70684a53c1e/markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218", size = 15200, upload-time = "2025-09-27T18:37:03.582Z" }, + { url = "https://files.pythonhosted.org/packages/0e/72/e3cc540f351f316e9ed0f092757459afbc595824ca724cbc5a5d4263713f/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", size = 13973, upload-time = "2025-09-27T18:37:04.929Z" }, + { url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619, upload-time = "2025-09-27T18:37:06.342Z" }, + { url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029, upload-time = "2025-09-27T18:37:07.213Z" }, + { url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408, upload-time = "2025-09-27T18:37:09.572Z" }, + { url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005, upload-time = "2025-09-27T18:37:10.58Z" }, + { url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048, upload-time = "2025-09-27T18:37:11.547Z" }, + { url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821, upload-time = "2025-09-27T18:37:12.48Z" }, + { url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606, upload-time = "2025-09-27T18:37:13.485Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043, upload-time = "2025-09-27T18:37:14.408Z" }, + { url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747, upload-time = "2025-09-27T18:37:15.36Z" }, + { url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341, upload-time = "2025-09-27T18:37:16.496Z" }, + { url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073, upload-time = "2025-09-27T18:37:17.476Z" }, + { url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661, upload-time = "2025-09-27T18:37:18.453Z" }, + { url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069, upload-time = "2025-09-27T18:37:19.332Z" }, + { url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670, upload-time = "2025-09-27T18:37:20.245Z" }, + { url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598, upload-time = "2025-09-27T18:37:21.177Z" }, + { url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261, upload-time = "2025-09-27T18:37:22.167Z" }, + { url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835, upload-time = "2025-09-27T18:37:23.296Z" }, + { url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733, upload-time = "2025-09-27T18:37:24.237Z" }, + { url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload-time = "2025-09-27T18:37:25.271Z" }, + { url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819, upload-time = "2025-09-27T18:37:26.285Z" }, + { url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426, upload-time = "2025-09-27T18:37:27.316Z" }, + { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" }, +] + +[[package]] +name = "mdurl" +version = "0.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, +] + +[[package]] +name = "multidict" +version = "6.7.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/80/1e/5492c365f222f907de1039b91f922b93fa4f764c713ee858d235495d8f50/multidict-6.7.0.tar.gz", hash = "sha256:c6e99d9a65ca282e578dfea819cfa9c0a62b2499d8677392e09feaf305e9e6f5", size = 101834, upload-time = "2025-10-06T14:52:30.657Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a9/63/7bdd4adc330abcca54c85728db2327130e49e52e8c3ce685cec44e0f2e9f/multidict-6.7.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:9f474ad5acda359c8758c8accc22032c6abe6dc87a8be2440d097785e27a9349", size = 77153, upload-time = "2025-10-06T14:48:26.409Z" }, + { url = "https://files.pythonhosted.org/packages/3f/bb/b6c35ff175ed1a3142222b78455ee31be71a8396ed3ab5280fbe3ebe4e85/multidict-6.7.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:4b7a9db5a870f780220e931d0002bbfd88fb53aceb6293251e2c839415c1b20e", size = 44993, upload-time = "2025-10-06T14:48:28.4Z" }, + { url = "https://files.pythonhosted.org/packages/e0/1f/064c77877c5fa6df6d346e68075c0f6998547afe952d6471b4c5f6a7345d/multidict-6.7.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:03ca744319864e92721195fa28c7a3b2bc7b686246b35e4078c1e4d0eb5466d3", size = 44607, upload-time = "2025-10-06T14:48:29.581Z" }, + { url = "https://files.pythonhosted.org/packages/04/7a/bf6aa92065dd47f287690000b3d7d332edfccb2277634cadf6a810463c6a/multidict-6.7.0-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:f0e77e3c0008bc9316e662624535b88d360c3a5d3f81e15cf12c139a75250046", size = 241847, upload-time = "2025-10-06T14:48:32.107Z" }, + { url = "https://files.pythonhosted.org/packages/94/39/297a8de920f76eda343e4ce05f3b489f0ab3f9504f2576dfb37b7c08ca08/multidict-6.7.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:08325c9e5367aa379a3496aa9a022fe8837ff22e00b94db256d3a1378c76ab32", size = 242616, upload-time = "2025-10-06T14:48:34.054Z" }, + { url = "https://files.pythonhosted.org/packages/39/3a/d0eee2898cfd9d654aea6cb8c4addc2f9756e9a7e09391cfe55541f917f7/multidict-6.7.0-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e2862408c99f84aa571ab462d25236ef9cb12a602ea959ba9c9009a54902fc73", size = 222333, upload-time = "2025-10-06T14:48:35.9Z" }, + { url = "https://files.pythonhosted.org/packages/05/48/3b328851193c7a4240815b71eea165b49248867bbb6153a0aee227a0bb47/multidict-6.7.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4d72a9a2d885f5c208b0cb91ff2ed43636bb7e345ec839ff64708e04f69a13cc", size = 253239, upload-time = "2025-10-06T14:48:37.302Z" }, + { url = "https://files.pythonhosted.org/packages/b1/ca/0706a98c8d126a89245413225ca4a3fefc8435014de309cf8b30acb68841/multidict-6.7.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:478cc36476687bac1514d651cbbaa94b86b0732fb6855c60c673794c7dd2da62", size = 251618, upload-time = "2025-10-06T14:48:38.963Z" }, + { url = "https://files.pythonhosted.org/packages/5e/4f/9c7992f245554d8b173f6f0a048ad24b3e645d883f096857ec2c0822b8bd/multidict-6.7.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6843b28b0364dc605f21481c90fadb5f60d9123b442eb8a726bb74feef588a84", size = 241655, upload-time = "2025-10-06T14:48:40.312Z" }, + { url = "https://files.pythonhosted.org/packages/31/79/26a85991ae67efd1c0b1fc2e0c275b8a6aceeb155a68861f63f87a798f16/multidict-6.7.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:23bfeee5316266e5ee2d625df2d2c602b829435fc3a235c2ba2131495706e4a0", size = 239245, upload-time = "2025-10-06T14:48:41.848Z" }, + { url = "https://files.pythonhosted.org/packages/14/1e/75fa96394478930b79d0302eaf9a6c69f34005a1a5251ac8b9c336486ec9/multidict-6.7.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:680878b9f3d45c31e1f730eef731f9b0bc1da456155688c6745ee84eb818e90e", size = 233523, upload-time = "2025-10-06T14:48:43.749Z" }, + { url = "https://files.pythonhosted.org/packages/b2/5e/085544cb9f9c4ad2b5d97467c15f856df8d9bac410cffd5c43991a5d878b/multidict-6.7.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:eb866162ef2f45063acc7a53a88ef6fe8bf121d45c30ea3c9cd87ce7e191a8d4", size = 243129, upload-time = "2025-10-06T14:48:45.225Z" }, + { url = "https://files.pythonhosted.org/packages/b9/c3/e9d9e2f20c9474e7a8fcef28f863c5cbd29bb5adce6b70cebe8bdad0039d/multidict-6.7.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:df0e3bf7993bdbeca5ac25aa859cf40d39019e015c9c91809ba7093967f7a648", size = 248999, upload-time = "2025-10-06T14:48:46.703Z" }, + { url = "https://files.pythonhosted.org/packages/b5/3f/df171b6efa3239ae33b97b887e42671cd1d94d460614bfb2c30ffdab3b95/multidict-6.7.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:661709cdcd919a2ece2234f9bae7174e5220c80b034585d7d8a755632d3e2111", size = 243711, upload-time = "2025-10-06T14:48:48.146Z" }, + { url = "https://files.pythonhosted.org/packages/3c/2f/9b5564888c4e14b9af64c54acf149263721a283aaf4aa0ae89b091d5d8c1/multidict-6.7.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:096f52730c3fb8ed419db2d44391932b63891b2c5ed14850a7e215c0ba9ade36", size = 237504, upload-time = "2025-10-06T14:48:49.447Z" }, + { url = "https://files.pythonhosted.org/packages/6c/3a/0bd6ca0f7d96d790542d591c8c3354c1e1b6bfd2024d4d92dc3d87485ec7/multidict-6.7.0-cp310-cp310-win32.whl", hash = "sha256:afa8a2978ec65d2336305550535c9c4ff50ee527914328c8677b3973ade52b85", size = 41422, upload-time = "2025-10-06T14:48:50.789Z" }, + { url = "https://files.pythonhosted.org/packages/00/35/f6a637ea2c75f0d3b7c7d41b1189189acff0d9deeb8b8f35536bb30f5e33/multidict-6.7.0-cp310-cp310-win_amd64.whl", hash = "sha256:b15b3afff74f707b9275d5ba6a91ae8f6429c3ffb29bbfd216b0b375a56f13d7", size = 46050, upload-time = "2025-10-06T14:48:51.938Z" }, + { url = "https://files.pythonhosted.org/packages/e7/b8/f7bf8329b39893d02d9d95cf610c75885d12fc0f402b1c894e1c8e01c916/multidict-6.7.0-cp310-cp310-win_arm64.whl", hash = "sha256:4b73189894398d59131a66ff157837b1fafea9974be486d036bb3d32331fdbf0", size = 43153, upload-time = "2025-10-06T14:48:53.146Z" }, + { url = "https://files.pythonhosted.org/packages/34/9e/5c727587644d67b2ed479041e4b1c58e30afc011e3d45d25bbe35781217c/multidict-6.7.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:4d409aa42a94c0b3fa617708ef5276dfe81012ba6753a0370fcc9d0195d0a1fc", size = 76604, upload-time = "2025-10-06T14:48:54.277Z" }, + { url = "https://files.pythonhosted.org/packages/17/e4/67b5c27bd17c085a5ea8f1ec05b8a3e5cba0ca734bfcad5560fb129e70ca/multidict-6.7.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:14c9e076eede3b54c636f8ce1c9c252b5f057c62131211f0ceeec273810c9721", size = 44715, upload-time = "2025-10-06T14:48:55.445Z" }, + { url = "https://files.pythonhosted.org/packages/4d/e1/866a5d77be6ea435711bef2a4291eed11032679b6b28b56b4776ab06ba3e/multidict-6.7.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4c09703000a9d0fa3c3404b27041e574cc7f4df4c6563873246d0e11812a94b6", size = 44332, upload-time = "2025-10-06T14:48:56.706Z" }, + { url = "https://files.pythonhosted.org/packages/31/61/0c2d50241ada71ff61a79518db85ada85fdabfcf395d5968dae1cbda04e5/multidict-6.7.0-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:a265acbb7bb33a3a2d626afbe756371dce0279e7b17f4f4eda406459c2b5ff1c", size = 245212, upload-time = "2025-10-06T14:48:58.042Z" }, + { url = "https://files.pythonhosted.org/packages/ac/e0/919666a4e4b57fff1b57f279be1c9316e6cdc5de8a8b525d76f6598fefc7/multidict-6.7.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:51cb455de290ae462593e5b1cb1118c5c22ea7f0d3620d9940bf695cea5a4bd7", size = 246671, upload-time = "2025-10-06T14:49:00.004Z" }, + { url = "https://files.pythonhosted.org/packages/a1/cc/d027d9c5a520f3321b65adea289b965e7bcbd2c34402663f482648c716ce/multidict-6.7.0-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:db99677b4457c7a5c5a949353e125ba72d62b35f74e26da141530fbb012218a7", size = 225491, upload-time = "2025-10-06T14:49:01.393Z" }, + { url = "https://files.pythonhosted.org/packages/75/c4/bbd633980ce6155a28ff04e6a6492dd3335858394d7bb752d8b108708558/multidict-6.7.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f470f68adc395e0183b92a2f4689264d1ea4b40504a24d9882c27375e6662bb9", size = 257322, upload-time = "2025-10-06T14:49:02.745Z" }, + { url = "https://files.pythonhosted.org/packages/4c/6d/d622322d344f1f053eae47e033b0b3f965af01212de21b10bcf91be991fb/multidict-6.7.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0db4956f82723cc1c270de9c6e799b4c341d327762ec78ef82bb962f79cc07d8", size = 254694, upload-time = "2025-10-06T14:49:04.15Z" }, + { url = "https://files.pythonhosted.org/packages/a8/9f/78f8761c2705d4c6d7516faed63c0ebdac569f6db1bef95e0d5218fdc146/multidict-6.7.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3e56d780c238f9e1ae66a22d2adf8d16f485381878250db8d496623cd38b22bd", size = 246715, upload-time = "2025-10-06T14:49:05.967Z" }, + { url = "https://files.pythonhosted.org/packages/78/59/950818e04f91b9c2b95aab3d923d9eabd01689d0dcd889563988e9ea0fd8/multidict-6.7.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9d14baca2ee12c1a64740d4531356ba50b82543017f3ad6de0deb943c5979abb", size = 243189, upload-time = "2025-10-06T14:49:07.37Z" }, + { url = "https://files.pythonhosted.org/packages/7a/3d/77c79e1934cad2ee74991840f8a0110966d9599b3af95964c0cd79bb905b/multidict-6.7.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:295a92a76188917c7f99cda95858c822f9e4aae5824246bba9b6b44004ddd0a6", size = 237845, upload-time = "2025-10-06T14:49:08.759Z" }, + { url = "https://files.pythonhosted.org/packages/63/1b/834ce32a0a97a3b70f86437f685f880136677ac00d8bce0027e9fd9c2db7/multidict-6.7.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:39f1719f57adbb767ef592a50ae5ebb794220d1188f9ca93de471336401c34d2", size = 246374, upload-time = "2025-10-06T14:49:10.574Z" }, + { url = "https://files.pythonhosted.org/packages/23/ef/43d1c3ba205b5dec93dc97f3fba179dfa47910fc73aaaea4f7ceb41cec2a/multidict-6.7.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:0a13fb8e748dfc94749f622de065dd5c1def7e0d2216dba72b1d8069a389c6ff", size = 253345, upload-time = "2025-10-06T14:49:12.331Z" }, + { url = "https://files.pythonhosted.org/packages/6b/03/eaf95bcc2d19ead522001f6a650ef32811aa9e3624ff0ad37c445c7a588c/multidict-6.7.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:e3aa16de190d29a0ea1b48253c57d99a68492c8dd8948638073ab9e74dc9410b", size = 246940, upload-time = "2025-10-06T14:49:13.821Z" }, + { url = "https://files.pythonhosted.org/packages/e8/df/ec8a5fd66ea6cd6f525b1fcbb23511b033c3e9bc42b81384834ffa484a62/multidict-6.7.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a048ce45dcdaaf1defb76b2e684f997fb5abf74437b6cb7b22ddad934a964e34", size = 242229, upload-time = "2025-10-06T14:49:15.603Z" }, + { url = "https://files.pythonhosted.org/packages/8a/a2/59b405d59fd39ec86d1142630e9049243015a5f5291ba49cadf3c090c541/multidict-6.7.0-cp311-cp311-win32.whl", hash = "sha256:a90af66facec4cebe4181b9e62a68be65e45ac9b52b67de9eec118701856e7ff", size = 41308, upload-time = "2025-10-06T14:49:16.871Z" }, + { url = "https://files.pythonhosted.org/packages/32/0f/13228f26f8b882c34da36efa776c3b7348455ec383bab4a66390e42963ae/multidict-6.7.0-cp311-cp311-win_amd64.whl", hash = "sha256:95b5ffa4349df2887518bb839409bcf22caa72d82beec453216802f475b23c81", size = 46037, upload-time = "2025-10-06T14:49:18.457Z" }, + { url = "https://files.pythonhosted.org/packages/84/1f/68588e31b000535a3207fd3c909ebeec4fb36b52c442107499c18a896a2a/multidict-6.7.0-cp311-cp311-win_arm64.whl", hash = "sha256:329aa225b085b6f004a4955271a7ba9f1087e39dcb7e65f6284a988264a63912", size = 43023, upload-time = "2025-10-06T14:49:19.648Z" }, + { url = "https://files.pythonhosted.org/packages/c2/9e/9f61ac18d9c8b475889f32ccfa91c9f59363480613fc807b6e3023d6f60b/multidict-6.7.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:8a3862568a36d26e650a19bb5cbbba14b71789032aebc0423f8cc5f150730184", size = 76877, upload-time = "2025-10-06T14:49:20.884Z" }, + { url = "https://files.pythonhosted.org/packages/38/6f/614f09a04e6184f8824268fce4bc925e9849edfa654ddd59f0b64508c595/multidict-6.7.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:960c60b5849b9b4f9dcc9bea6e3626143c252c74113df2c1540aebce70209b45", size = 45467, upload-time = "2025-10-06T14:49:22.054Z" }, + { url = "https://files.pythonhosted.org/packages/b3/93/c4f67a436dd026f2e780c433277fff72be79152894d9fc36f44569cab1a6/multidict-6.7.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2049be98fb57a31b4ccf870bf377af2504d4ae35646a19037ec271e4c07998aa", size = 43834, upload-time = "2025-10-06T14:49:23.566Z" }, + { url = "https://files.pythonhosted.org/packages/7f/f5/013798161ca665e4a422afbc5e2d9e4070142a9ff8905e482139cd09e4d0/multidict-6.7.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:0934f3843a1860dd465d38895c17fce1f1cb37295149ab05cd1b9a03afacb2a7", size = 250545, upload-time = "2025-10-06T14:49:24.882Z" }, + { url = "https://files.pythonhosted.org/packages/71/2f/91dbac13e0ba94669ea5119ba267c9a832f0cb65419aca75549fcf09a3dc/multidict-6.7.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b3e34f3a1b8131ba06f1a73adab24f30934d148afcd5f5de9a73565a4404384e", size = 258305, upload-time = "2025-10-06T14:49:26.778Z" }, + { url = "https://files.pythonhosted.org/packages/ef/b0/754038b26f6e04488b48ac621f779c341338d78503fb45403755af2df477/multidict-6.7.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:efbb54e98446892590dc2458c19c10344ee9a883a79b5cec4bc34d6656e8d546", size = 242363, upload-time = "2025-10-06T14:49:28.562Z" }, + { url = "https://files.pythonhosted.org/packages/87/15/9da40b9336a7c9fa606c4cf2ed80a649dffeb42b905d4f63a1d7eb17d746/multidict-6.7.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a35c5fc61d4f51eb045061e7967cfe3123d622cd500e8868e7c0c592a09fedc4", size = 268375, upload-time = "2025-10-06T14:49:29.96Z" }, + { url = "https://files.pythonhosted.org/packages/82/72/c53fcade0cc94dfaad583105fd92b3a783af2091eddcb41a6d5a52474000/multidict-6.7.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:29fe6740ebccba4175af1b9b87bf553e9c15cd5868ee967e010efcf94e4fd0f1", size = 269346, upload-time = "2025-10-06T14:49:31.404Z" }, + { url = "https://files.pythonhosted.org/packages/0d/e2/9baffdae21a76f77ef8447f1a05a96ec4bc0a24dae08767abc0a2fe680b8/multidict-6.7.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:123e2a72e20537add2f33a79e605f6191fba2afda4cbb876e35c1a7074298a7d", size = 256107, upload-time = "2025-10-06T14:49:32.974Z" }, + { url = "https://files.pythonhosted.org/packages/3c/06/3f06f611087dc60d65ef775f1fb5aca7c6d61c6db4990e7cda0cef9b1651/multidict-6.7.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b284e319754366c1aee2267a2036248b24eeb17ecd5dc16022095e747f2f4304", size = 253592, upload-time = "2025-10-06T14:49:34.52Z" }, + { url = "https://files.pythonhosted.org/packages/20/24/54e804ec7945b6023b340c412ce9c3f81e91b3bf5fa5ce65558740141bee/multidict-6.7.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:803d685de7be4303b5a657b76e2f6d1240e7e0a8aa2968ad5811fa2285553a12", size = 251024, upload-time = "2025-10-06T14:49:35.956Z" }, + { url = "https://files.pythonhosted.org/packages/14/48/011cba467ea0b17ceb938315d219391d3e421dfd35928e5dbdc3f4ae76ef/multidict-6.7.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:c04a328260dfd5db8c39538f999f02779012268f54614902d0afc775d44e0a62", size = 251484, upload-time = "2025-10-06T14:49:37.631Z" }, + { url = "https://files.pythonhosted.org/packages/0d/2f/919258b43bb35b99fa127435cfb2d91798eb3a943396631ef43e3720dcf4/multidict-6.7.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:8a19cdb57cd3df4cd865849d93ee14920fb97224300c88501f16ecfa2604b4e0", size = 263579, upload-time = "2025-10-06T14:49:39.502Z" }, + { url = "https://files.pythonhosted.org/packages/31/22/a0e884d86b5242b5a74cf08e876bdf299e413016b66e55511f7a804a366e/multidict-6.7.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:9b2fd74c52accced7e75de26023b7dccee62511a600e62311b918ec5c168fc2a", size = 259654, upload-time = "2025-10-06T14:49:41.32Z" }, + { url = "https://files.pythonhosted.org/packages/b2/e5/17e10e1b5c5f5a40f2fcbb45953c9b215f8a4098003915e46a93f5fcaa8f/multidict-6.7.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3e8bfdd0e487acf992407a140d2589fe598238eaeffa3da8448d63a63cd363f8", size = 251511, upload-time = "2025-10-06T14:49:46.021Z" }, + { url = "https://files.pythonhosted.org/packages/e3/9a/201bb1e17e7af53139597069c375e7b0dcbd47594604f65c2d5359508566/multidict-6.7.0-cp312-cp312-win32.whl", hash = "sha256:dd32a49400a2c3d52088e120ee00c1e3576cbff7e10b98467962c74fdb762ed4", size = 41895, upload-time = "2025-10-06T14:49:48.718Z" }, + { url = "https://files.pythonhosted.org/packages/46/e2/348cd32faad84eaf1d20cce80e2bb0ef8d312c55bca1f7fa9865e7770aaf/multidict-6.7.0-cp312-cp312-win_amd64.whl", hash = "sha256:92abb658ef2d7ef22ac9f8bb88e8b6c3e571671534e029359b6d9e845923eb1b", size = 46073, upload-time = "2025-10-06T14:49:50.28Z" }, + { url = "https://files.pythonhosted.org/packages/25/ec/aad2613c1910dce907480e0c3aa306905830f25df2e54ccc9dea450cb5aa/multidict-6.7.0-cp312-cp312-win_arm64.whl", hash = "sha256:490dab541a6a642ce1a9d61a4781656b346a55c13038f0b1244653828e3a83ec", size = 43226, upload-time = "2025-10-06T14:49:52.304Z" }, + { url = "https://files.pythonhosted.org/packages/d2/86/33272a544eeb36d66e4d9a920602d1a2f57d4ebea4ef3cdfe5a912574c95/multidict-6.7.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:bee7c0588aa0076ce77c0ea5d19a68d76ad81fcd9fe8501003b9a24f9d4000f6", size = 76135, upload-time = "2025-10-06T14:49:54.26Z" }, + { url = "https://files.pythonhosted.org/packages/91/1c/eb97db117a1ebe46d457a3d235a7b9d2e6dcab174f42d1b67663dd9e5371/multidict-6.7.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7ef6b61cad77091056ce0e7ce69814ef72afacb150b7ac6a3e9470def2198159", size = 45117, upload-time = "2025-10-06T14:49:55.82Z" }, + { url = "https://files.pythonhosted.org/packages/f1/d8/6c3442322e41fb1dd4de8bd67bfd11cd72352ac131f6368315617de752f1/multidict-6.7.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9c0359b1ec12b1d6849c59f9d319610b7f20ef990a6d454ab151aa0e3b9f78ca", size = 43472, upload-time = "2025-10-06T14:49:57.048Z" }, + { url = "https://files.pythonhosted.org/packages/75/3f/e2639e80325af0b6c6febdf8e57cc07043ff15f57fa1ef808f4ccb5ac4cd/multidict-6.7.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:cd240939f71c64bd658f186330603aac1a9a81bf6273f523fca63673cb7378a8", size = 249342, upload-time = "2025-10-06T14:49:58.368Z" }, + { url = "https://files.pythonhosted.org/packages/5d/cc/84e0585f805cbeaa9cbdaa95f9a3d6aed745b9d25700623ac89a6ecff400/multidict-6.7.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a60a4d75718a5efa473ebd5ab685786ba0c67b8381f781d1be14da49f1a2dc60", size = 257082, upload-time = "2025-10-06T14:49:59.89Z" }, + { url = "https://files.pythonhosted.org/packages/b0/9c/ac851c107c92289acbbf5cfb485694084690c1b17e555f44952c26ddc5bd/multidict-6.7.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:53a42d364f323275126aff81fb67c5ca1b7a04fda0546245730a55c8c5f24bc4", size = 240704, upload-time = "2025-10-06T14:50:01.485Z" }, + { url = "https://files.pythonhosted.org/packages/50/cc/5f93e99427248c09da95b62d64b25748a5f5c98c7c2ab09825a1d6af0e15/multidict-6.7.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3b29b980d0ddbecb736735ee5bef69bb2ddca56eff603c86f3f29a1128299b4f", size = 266355, upload-time = "2025-10-06T14:50:02.955Z" }, + { url = "https://files.pythonhosted.org/packages/ec/0c/2ec1d883ceb79c6f7f6d7ad90c919c898f5d1c6ea96d322751420211e072/multidict-6.7.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f8a93b1c0ed2d04b97a5e9336fd2d33371b9a6e29ab7dd6503d63407c20ffbaf", size = 267259, upload-time = "2025-10-06T14:50:04.446Z" }, + { url = "https://files.pythonhosted.org/packages/c6/2d/f0b184fa88d6630aa267680bdb8623fb69cb0d024b8c6f0d23f9a0f406d3/multidict-6.7.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9ff96e8815eecacc6645da76c413eb3b3d34cfca256c70b16b286a687d013c32", size = 254903, upload-time = "2025-10-06T14:50:05.98Z" }, + { url = "https://files.pythonhosted.org/packages/06/c9/11ea263ad0df7dfabcad404feb3c0dd40b131bc7f232d5537f2fb1356951/multidict-6.7.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7516c579652f6a6be0e266aec0acd0db80829ca305c3d771ed898538804c2036", size = 252365, upload-time = "2025-10-06T14:50:07.511Z" }, + { url = "https://files.pythonhosted.org/packages/41/88/d714b86ee2c17d6e09850c70c9d310abac3d808ab49dfa16b43aba9d53fd/multidict-6.7.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:040f393368e63fb0f3330e70c26bfd336656bed925e5cbe17c9da839a6ab13ec", size = 250062, upload-time = "2025-10-06T14:50:09.074Z" }, + { url = "https://files.pythonhosted.org/packages/15/fe/ad407bb9e818c2b31383f6131ca19ea7e35ce93cf1310fce69f12e89de75/multidict-6.7.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b3bc26a951007b1057a1c543af845f1c7e3e71cc240ed1ace7bf4484aa99196e", size = 249683, upload-time = "2025-10-06T14:50:10.714Z" }, + { url = "https://files.pythonhosted.org/packages/8c/a4/a89abdb0229e533fb925e7c6e5c40201c2873efebc9abaf14046a4536ee6/multidict-6.7.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:7b022717c748dd1992a83e219587aabe45980d88969f01b316e78683e6285f64", size = 261254, upload-time = "2025-10-06T14:50:12.28Z" }, + { url = "https://files.pythonhosted.org/packages/8d/aa/0e2b27bd88b40a4fb8dc53dd74eecac70edaa4c1dd0707eb2164da3675b3/multidict-6.7.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:9600082733859f00d79dee64effc7aef1beb26adb297416a4ad2116fd61374bd", size = 257967, upload-time = "2025-10-06T14:50:14.16Z" }, + { url = "https://files.pythonhosted.org/packages/d0/8e/0c67b7120d5d5f6d874ed85a085f9dc770a7f9d8813e80f44a9fec820bb7/multidict-6.7.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:94218fcec4d72bc61df51c198d098ce2b378e0ccbac41ddbed5ef44092913288", size = 250085, upload-time = "2025-10-06T14:50:15.639Z" }, + { url = "https://files.pythonhosted.org/packages/ba/55/b73e1d624ea4b8fd4dd07a3bb70f6e4c7c6c5d9d640a41c6ffe5cdbd2a55/multidict-6.7.0-cp313-cp313-win32.whl", hash = "sha256:a37bd74c3fa9d00be2d7b8eca074dc56bd8077ddd2917a839bd989612671ed17", size = 41713, upload-time = "2025-10-06T14:50:17.066Z" }, + { url = "https://files.pythonhosted.org/packages/32/31/75c59e7d3b4205075b4c183fa4ca398a2daf2303ddf616b04ae6ef55cffe/multidict-6.7.0-cp313-cp313-win_amd64.whl", hash = "sha256:30d193c6cc6d559db42b6bcec8a5d395d34d60c9877a0b71ecd7c204fcf15390", size = 45915, upload-time = "2025-10-06T14:50:18.264Z" }, + { url = "https://files.pythonhosted.org/packages/31/2a/8987831e811f1184c22bc2e45844934385363ee61c0a2dcfa8f71b87e608/multidict-6.7.0-cp313-cp313-win_arm64.whl", hash = "sha256:ea3334cabe4d41b7ccd01e4d349828678794edbc2d3ae97fc162a3312095092e", size = 43077, upload-time = "2025-10-06T14:50:19.853Z" }, + { url = "https://files.pythonhosted.org/packages/e8/68/7b3a5170a382a340147337b300b9eb25a9ddb573bcdfff19c0fa3f31ffba/multidict-6.7.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:ad9ce259f50abd98a1ca0aa6e490b58c316a0fce0617f609723e40804add2c00", size = 83114, upload-time = "2025-10-06T14:50:21.223Z" }, + { url = "https://files.pythonhosted.org/packages/55/5c/3fa2d07c84df4e302060f555bbf539310980362236ad49f50eeb0a1c1eb9/multidict-6.7.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:07f5594ac6d084cbb5de2df218d78baf55ef150b91f0ff8a21cc7a2e3a5a58eb", size = 48442, upload-time = "2025-10-06T14:50:22.871Z" }, + { url = "https://files.pythonhosted.org/packages/fc/56/67212d33239797f9bd91962bb899d72bb0f4c35a8652dcdb8ed049bef878/multidict-6.7.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:0591b48acf279821a579282444814a2d8d0af624ae0bc600aa4d1b920b6e924b", size = 46885, upload-time = "2025-10-06T14:50:24.258Z" }, + { url = "https://files.pythonhosted.org/packages/46/d1/908f896224290350721597a61a69cd19b89ad8ee0ae1f38b3f5cd12ea2ac/multidict-6.7.0-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:749a72584761531d2b9467cfbdfd29487ee21124c304c4b6cb760d8777b27f9c", size = 242588, upload-time = "2025-10-06T14:50:25.716Z" }, + { url = "https://files.pythonhosted.org/packages/ab/67/8604288bbd68680eee0ab568fdcb56171d8b23a01bcd5cb0c8fedf6e5d99/multidict-6.7.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b4c3d199f953acd5b446bf7c0de1fe25d94e09e79086f8dc2f48a11a129cdf1", size = 249966, upload-time = "2025-10-06T14:50:28.192Z" }, + { url = "https://files.pythonhosted.org/packages/20/33/9228d76339f1ba51e3efef7da3ebd91964d3006217aae13211653193c3ff/multidict-6.7.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:9fb0211dfc3b51efea2f349ec92c114d7754dd62c01f81c3e32b765b70c45c9b", size = 228618, upload-time = "2025-10-06T14:50:29.82Z" }, + { url = "https://files.pythonhosted.org/packages/f8/2d/25d9b566d10cab1c42b3b9e5b11ef79c9111eaf4463b8c257a3bd89e0ead/multidict-6.7.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a027ec240fe73a8d6281872690b988eed307cd7d91b23998ff35ff577ca688b5", size = 257539, upload-time = "2025-10-06T14:50:31.731Z" }, + { url = "https://files.pythonhosted.org/packages/b6/b1/8d1a965e6637fc33de3c0d8f414485c2b7e4af00f42cab3d84e7b955c222/multidict-6.7.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1d964afecdf3a8288789df2f5751dc0a8261138c3768d9af117ed384e538fad", size = 256345, upload-time = "2025-10-06T14:50:33.26Z" }, + { url = "https://files.pythonhosted.org/packages/ba/0c/06b5a8adbdeedada6f4fb8d8f193d44a347223b11939b42953eeb6530b6b/multidict-6.7.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:caf53b15b1b7df9fbd0709aa01409000a2b4dd03a5f6f5cc548183c7c8f8b63c", size = 247934, upload-time = "2025-10-06T14:50:34.808Z" }, + { url = "https://files.pythonhosted.org/packages/8f/31/b2491b5fe167ca044c6eb4b8f2c9f3b8a00b24c432c365358eadac5d7625/multidict-6.7.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:654030da3197d927f05a536a66186070e98765aa5142794c9904555d3a9d8fb5", size = 245243, upload-time = "2025-10-06T14:50:36.436Z" }, + { url = "https://files.pythonhosted.org/packages/61/1a/982913957cb90406c8c94f53001abd9eafc271cb3e70ff6371590bec478e/multidict-6.7.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:2090d3718829d1e484706a2f525e50c892237b2bf9b17a79b059cb98cddc2f10", size = 235878, upload-time = "2025-10-06T14:50:37.953Z" }, + { url = "https://files.pythonhosted.org/packages/be/c0/21435d804c1a1cf7a2608593f4d19bca5bcbd7a81a70b253fdd1c12af9c0/multidict-6.7.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:2d2cfeec3f6f45651b3d408c4acec0ebf3daa9bc8a112a084206f5db5d05b754", size = 243452, upload-time = "2025-10-06T14:50:39.574Z" }, + { url = "https://files.pythonhosted.org/packages/54/0a/4349d540d4a883863191be6eb9a928846d4ec0ea007d3dcd36323bb058ac/multidict-6.7.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:4ef089f985b8c194d341eb2c24ae6e7408c9a0e2e5658699c92f497437d88c3c", size = 252312, upload-time = "2025-10-06T14:50:41.612Z" }, + { url = "https://files.pythonhosted.org/packages/26/64/d5416038dbda1488daf16b676e4dbfd9674dde10a0cc8f4fc2b502d8125d/multidict-6.7.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:e93a0617cd16998784bf4414c7e40f17a35d2350e5c6f0bd900d3a8e02bd3762", size = 246935, upload-time = "2025-10-06T14:50:43.972Z" }, + { url = "https://files.pythonhosted.org/packages/9f/8c/8290c50d14e49f35e0bd4abc25e1bc7711149ca9588ab7d04f886cdf03d9/multidict-6.7.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f0feece2ef8ebc42ed9e2e8c78fc4aa3cf455733b507c09ef7406364c94376c6", size = 243385, upload-time = "2025-10-06T14:50:45.648Z" }, + { url = "https://files.pythonhosted.org/packages/ef/a0/f83ae75e42d694b3fbad3e047670e511c138be747bc713cf1b10d5096416/multidict-6.7.0-cp313-cp313t-win32.whl", hash = "sha256:19a1d55338ec1be74ef62440ca9e04a2f001a04d0cc49a4983dc320ff0f3212d", size = 47777, upload-time = "2025-10-06T14:50:47.154Z" }, + { url = "https://files.pythonhosted.org/packages/dc/80/9b174a92814a3830b7357307a792300f42c9e94664b01dee8e457551fa66/multidict-6.7.0-cp313-cp313t-win_amd64.whl", hash = "sha256:3da4fb467498df97e986af166b12d01f05d2e04f978a9c1c680ea1988e0bc4b6", size = 53104, upload-time = "2025-10-06T14:50:48.851Z" }, + { url = "https://files.pythonhosted.org/packages/cc/28/04baeaf0428d95bb7a7bea0e691ba2f31394338ba424fb0679a9ed0f4c09/multidict-6.7.0-cp313-cp313t-win_arm64.whl", hash = "sha256:b4121773c49a0776461f4a904cdf6264c88e42218aaa8407e803ca8025872792", size = 45503, upload-time = "2025-10-06T14:50:50.16Z" }, + { url = "https://files.pythonhosted.org/packages/e2/b1/3da6934455dd4b261d4c72f897e3a5728eba81db59959f3a639245891baa/multidict-6.7.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3bab1e4aff7adaa34410f93b1f8e57c4b36b9af0426a76003f441ee1d3c7e842", size = 75128, upload-time = "2025-10-06T14:50:51.92Z" }, + { url = "https://files.pythonhosted.org/packages/14/2c/f069cab5b51d175a1a2cb4ccdf7a2c2dabd58aa5bd933fa036a8d15e2404/multidict-6.7.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:b8512bac933afc3e45fb2b18da8e59b78d4f408399a960339598374d4ae3b56b", size = 44410, upload-time = "2025-10-06T14:50:53.275Z" }, + { url = "https://files.pythonhosted.org/packages/42/e2/64bb41266427af6642b6b128e8774ed84c11b80a90702c13ac0a86bb10cc/multidict-6.7.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:79dcf9e477bc65414ebfea98ffd013cb39552b5ecd62908752e0e413d6d06e38", size = 43205, upload-time = "2025-10-06T14:50:54.911Z" }, + { url = "https://files.pythonhosted.org/packages/02/68/6b086fef8a3f1a8541b9236c594f0c9245617c29841f2e0395d979485cde/multidict-6.7.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:31bae522710064b5cbeddaf2e9f32b1abab70ac6ac91d42572502299e9953128", size = 245084, upload-time = "2025-10-06T14:50:56.369Z" }, + { url = "https://files.pythonhosted.org/packages/15/ee/f524093232007cd7a75c1d132df70f235cfd590a7c9eaccd7ff422ef4ae8/multidict-6.7.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4a0df7ff02397bb63e2fd22af2c87dfa39e8c7f12947bc524dbdc528282c7e34", size = 252667, upload-time = "2025-10-06T14:50:57.991Z" }, + { url = "https://files.pythonhosted.org/packages/02/a5/eeb3f43ab45878f1895118c3ef157a480db58ede3f248e29b5354139c2c9/multidict-6.7.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7a0222514e8e4c514660e182d5156a415c13ef0aabbd71682fc714e327b95e99", size = 233590, upload-time = "2025-10-06T14:50:59.589Z" }, + { url = "https://files.pythonhosted.org/packages/6a/1e/76d02f8270b97269d7e3dbd45644b1785bda457b474315f8cf999525a193/multidict-6.7.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2397ab4daaf2698eb51a76721e98db21ce4f52339e535725de03ea962b5a3202", size = 264112, upload-time = "2025-10-06T14:51:01.183Z" }, + { url = "https://files.pythonhosted.org/packages/76/0b/c28a70ecb58963847c2a8efe334904cd254812b10e535aefb3bcce513918/multidict-6.7.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8891681594162635948a636c9fe0ff21746aeb3dd5463f6e25d9bea3a8a39ca1", size = 261194, upload-time = "2025-10-06T14:51:02.794Z" }, + { url = "https://files.pythonhosted.org/packages/b4/63/2ab26e4209773223159b83aa32721b4021ffb08102f8ac7d689c943fded1/multidict-6.7.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:18706cc31dbf402a7945916dd5cddf160251b6dab8a2c5f3d6d5a55949f676b3", size = 248510, upload-time = "2025-10-06T14:51:04.724Z" }, + { url = "https://files.pythonhosted.org/packages/93/cd/06c1fa8282af1d1c46fd55c10a7930af652afdce43999501d4d68664170c/multidict-6.7.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f844a1bbf1d207dd311a56f383f7eda2d0e134921d45751842d8235e7778965d", size = 248395, upload-time = "2025-10-06T14:51:06.306Z" }, + { url = "https://files.pythonhosted.org/packages/99/ac/82cb419dd6b04ccf9e7e61befc00c77614fc8134362488b553402ecd55ce/multidict-6.7.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:d4393e3581e84e5645506923816b9cc81f5609a778c7e7534054091acc64d1c6", size = 239520, upload-time = "2025-10-06T14:51:08.091Z" }, + { url = "https://files.pythonhosted.org/packages/fa/f3/a0f9bf09493421bd8716a362e0cd1d244f5a6550f5beffdd6b47e885b331/multidict-6.7.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:fbd18dc82d7bf274b37aa48d664534330af744e03bccf696d6f4c6042e7d19e7", size = 245479, upload-time = "2025-10-06T14:51:10.365Z" }, + { url = "https://files.pythonhosted.org/packages/8d/01/476d38fc73a212843f43c852b0eee266b6971f0e28329c2184a8df90c376/multidict-6.7.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:b6234e14f9314731ec45c42fc4554b88133ad53a09092cc48a88e771c125dadb", size = 258903, upload-time = "2025-10-06T14:51:12.466Z" }, + { url = "https://files.pythonhosted.org/packages/49/6d/23faeb0868adba613b817d0e69c5f15531b24d462af8012c4f6de4fa8dc3/multidict-6.7.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:08d4379f9744d8f78d98c8673c06e202ffa88296f009c71bbafe8a6bf847d01f", size = 252333, upload-time = "2025-10-06T14:51:14.48Z" }, + { url = "https://files.pythonhosted.org/packages/1e/cc/48d02ac22b30fa247f7dad82866e4b1015431092f4ba6ebc7e77596e0b18/multidict-6.7.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:9fe04da3f79387f450fd0061d4dd2e45a72749d31bf634aecc9e27f24fdc4b3f", size = 243411, upload-time = "2025-10-06T14:51:16.072Z" }, + { url = "https://files.pythonhosted.org/packages/4a/03/29a8bf5a18abf1fe34535c88adbdfa88c9fb869b5a3b120692c64abe8284/multidict-6.7.0-cp314-cp314-win32.whl", hash = "sha256:fbafe31d191dfa7c4c51f7a6149c9fb7e914dcf9ffead27dcfd9f1ae382b3885", size = 40940, upload-time = "2025-10-06T14:51:17.544Z" }, + { url = "https://files.pythonhosted.org/packages/82/16/7ed27b680791b939de138f906d5cf2b4657b0d45ca6f5dd6236fdddafb1a/multidict-6.7.0-cp314-cp314-win_amd64.whl", hash = "sha256:2f67396ec0310764b9222a1728ced1ab638f61aadc6226f17a71dd9324f9a99c", size = 45087, upload-time = "2025-10-06T14:51:18.875Z" }, + { url = "https://files.pythonhosted.org/packages/cd/3c/e3e62eb35a1950292fe39315d3c89941e30a9d07d5d2df42965ab041da43/multidict-6.7.0-cp314-cp314-win_arm64.whl", hash = "sha256:ba672b26069957ee369cfa7fc180dde1fc6f176eaf1e6beaf61fbebbd3d9c000", size = 42368, upload-time = "2025-10-06T14:51:20.225Z" }, + { url = "https://files.pythonhosted.org/packages/8b/40/cd499bd0dbc5f1136726db3153042a735fffd0d77268e2ee20d5f33c010f/multidict-6.7.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:c1dcc7524066fa918c6a27d61444d4ee7900ec635779058571f70d042d86ed63", size = 82326, upload-time = "2025-10-06T14:51:21.588Z" }, + { url = "https://files.pythonhosted.org/packages/13/8a/18e031eca251c8df76daf0288e6790561806e439f5ce99a170b4af30676b/multidict-6.7.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:27e0b36c2d388dc7b6ced3406671b401e84ad7eb0656b8f3a2f46ed0ce483718", size = 48065, upload-time = "2025-10-06T14:51:22.93Z" }, + { url = "https://files.pythonhosted.org/packages/40/71/5e6701277470a87d234e433fb0a3a7deaf3bcd92566e421e7ae9776319de/multidict-6.7.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2a7baa46a22e77f0988e3b23d4ede5513ebec1929e34ee9495be535662c0dfe2", size = 46475, upload-time = "2025-10-06T14:51:24.352Z" }, + { url = "https://files.pythonhosted.org/packages/fe/6a/bab00cbab6d9cfb57afe1663318f72ec28289ea03fd4e8236bb78429893a/multidict-6.7.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:7bf77f54997a9166a2f5675d1201520586439424c2511723a7312bdb4bcc034e", size = 239324, upload-time = "2025-10-06T14:51:25.822Z" }, + { url = "https://files.pythonhosted.org/packages/2a/5f/8de95f629fc22a7769ade8b41028e3e5a822c1f8904f618d175945a81ad3/multidict-6.7.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e011555abada53f1578d63389610ac8a5400fc70ce71156b0aa30d326f1a5064", size = 246877, upload-time = "2025-10-06T14:51:27.604Z" }, + { url = "https://files.pythonhosted.org/packages/23/b4/38881a960458f25b89e9f4a4fdcb02ac101cfa710190db6e5528841e67de/multidict-6.7.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:28b37063541b897fd6a318007373930a75ca6d6ac7c940dbe14731ffdd8d498e", size = 225824, upload-time = "2025-10-06T14:51:29.664Z" }, + { url = "https://files.pythonhosted.org/packages/1e/39/6566210c83f8a261575f18e7144736059f0c460b362e96e9cf797a24b8e7/multidict-6.7.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:05047ada7a2fde2631a0ed706f1fd68b169a681dfe5e4cf0f8e4cb6618bbc2cd", size = 253558, upload-time = "2025-10-06T14:51:31.684Z" }, + { url = "https://files.pythonhosted.org/packages/00/a3/67f18315100f64c269f46e6c0319fa87ba68f0f64f2b8e7fd7c72b913a0b/multidict-6.7.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:716133f7d1d946a4e1b91b1756b23c088881e70ff180c24e864c26192ad7534a", size = 252339, upload-time = "2025-10-06T14:51:33.699Z" }, + { url = "https://files.pythonhosted.org/packages/c8/2a/1cb77266afee2458d82f50da41beba02159b1d6b1f7973afc9a1cad1499b/multidict-6.7.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d1bed1b467ef657f2a0ae62844a607909ef1c6889562de5e1d505f74457d0b96", size = 244895, upload-time = "2025-10-06T14:51:36.189Z" }, + { url = "https://files.pythonhosted.org/packages/dd/72/09fa7dd487f119b2eb9524946ddd36e2067c08510576d43ff68469563b3b/multidict-6.7.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ca43bdfa5d37bd6aee89d85e1d0831fb86e25541be7e9d376ead1b28974f8e5e", size = 241862, upload-time = "2025-10-06T14:51:41.291Z" }, + { url = "https://files.pythonhosted.org/packages/65/92/bc1f8bd0853d8669300f732c801974dfc3702c3eeadae2f60cef54dc69d7/multidict-6.7.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:44b546bd3eb645fd26fb949e43c02a25a2e632e2ca21a35e2e132c8105dc8599", size = 232376, upload-time = "2025-10-06T14:51:43.55Z" }, + { url = "https://files.pythonhosted.org/packages/09/86/ac39399e5cb9d0c2ac8ef6e10a768e4d3bc933ac808d49c41f9dc23337eb/multidict-6.7.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:a6ef16328011d3f468e7ebc326f24c1445f001ca1dec335b2f8e66bed3006394", size = 240272, upload-time = "2025-10-06T14:51:45.265Z" }, + { url = "https://files.pythonhosted.org/packages/3d/b6/fed5ac6b8563ec72df6cb1ea8dac6d17f0a4a1f65045f66b6d3bf1497c02/multidict-6.7.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:5aa873cbc8e593d361ae65c68f85faadd755c3295ea2c12040ee146802f23b38", size = 248774, upload-time = "2025-10-06T14:51:46.836Z" }, + { url = "https://files.pythonhosted.org/packages/6b/8d/b954d8c0dc132b68f760aefd45870978deec6818897389dace00fcde32ff/multidict-6.7.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:3d7b6ccce016e29df4b7ca819659f516f0bc7a4b3efa3bb2012ba06431b044f9", size = 242731, upload-time = "2025-10-06T14:51:48.541Z" }, + { url = "https://files.pythonhosted.org/packages/16/9d/a2dac7009125d3540c2f54e194829ea18ac53716c61b655d8ed300120b0f/multidict-6.7.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:171b73bd4ee683d307599b66793ac80981b06f069b62eea1c9e29c9241aa66b0", size = 240193, upload-time = "2025-10-06T14:51:50.355Z" }, + { url = "https://files.pythonhosted.org/packages/39/ca/c05f144128ea232ae2178b008d5011d4e2cea86e4ee8c85c2631b1b94802/multidict-6.7.0-cp314-cp314t-win32.whl", hash = "sha256:b2d7f80c4e1fd010b07cb26820aae86b7e73b681ee4889684fb8d2d4537aab13", size = 48023, upload-time = "2025-10-06T14:51:51.883Z" }, + { url = "https://files.pythonhosted.org/packages/ba/8f/0a60e501584145588be1af5cc829265701ba3c35a64aec8e07cbb71d39bb/multidict-6.7.0-cp314-cp314t-win_amd64.whl", hash = "sha256:09929cab6fcb68122776d575e03c6cc64ee0b8fca48d17e135474b042ce515cd", size = 53507, upload-time = "2025-10-06T14:51:53.672Z" }, + { url = "https://files.pythonhosted.org/packages/7f/ae/3148b988a9c6239903e786eac19c889fab607c31d6efa7fb2147e5680f23/multidict-6.7.0-cp314-cp314t-win_arm64.whl", hash = "sha256:cc41db090ed742f32bd2d2c721861725e6109681eddf835d0a82bd3a5c382827", size = 44804, upload-time = "2025-10-06T14:51:55.415Z" }, + { url = "https://files.pythonhosted.org/packages/b7/da/7d22601b625e241d4f23ef1ebff8acfc60da633c9e7e7922e24d10f592b3/multidict-6.7.0-py3-none-any.whl", hash = "sha256:394fc5c42a333c9ffc3e421a4c85e08580d990e08b99f6bf35b4132114c5dcb3", size = 12317, upload-time = "2025-10-06T14:52:29.272Z" }, +] + +[[package]] +name = "mypy" +version = "1.19.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "librt", marker = "platform_python_implementation != 'PyPy'" }, + { name = "mypy-extensions" }, + { name = "pathspec" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f5/db/4efed9504bc01309ab9c2da7e352cc223569f05478012b5d9ece38fd44d2/mypy-1.19.1.tar.gz", hash = "sha256:19d88bb05303fe63f71dd2c6270daca27cb9401c4ca8255fe50d1d920e0eb9ba", size = 3582404, upload-time = "2025-12-15T05:03:48.42Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2f/63/e499890d8e39b1ff2df4c0c6ce5d371b6844ee22b8250687a99fd2f657a8/mypy-1.19.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5f05aa3d375b385734388e844bc01733bd33c644ab48e9684faa54e5389775ec", size = 13101333, upload-time = "2025-12-15T05:03:03.28Z" }, + { url = "https://files.pythonhosted.org/packages/72/4b/095626fc136fba96effc4fd4a82b41d688ab92124f8c4f7564bffe5cf1b0/mypy-1.19.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:022ea7279374af1a5d78dfcab853fe6a536eebfda4b59deab53cd21f6cd9f00b", size = 12164102, upload-time = "2025-12-15T05:02:33.611Z" }, + { url = "https://files.pythonhosted.org/packages/0c/5b/952928dd081bf88a83a5ccd49aaecfcd18fd0d2710c7ff07b8fb6f7032b9/mypy-1.19.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee4c11e460685c3e0c64a4c5de82ae143622410950d6be863303a1c4ba0e36d6", size = 12765799, upload-time = "2025-12-15T05:03:28.44Z" }, + { url = "https://files.pythonhosted.org/packages/2a/0d/93c2e4a287f74ef11a66fb6d49c7a9f05e47b0a4399040e6719b57f500d2/mypy-1.19.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:de759aafbae8763283b2ee5869c7255391fbc4de3ff171f8f030b5ec48381b74", size = 13522149, upload-time = "2025-12-15T05:02:36.011Z" }, + { url = "https://files.pythonhosted.org/packages/7b/0e/33a294b56aaad2b338d203e3a1d8b453637ac36cb278b45005e0901cf148/mypy-1.19.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ab43590f9cd5108f41aacf9fca31841142c786827a74ab7cc8a2eacb634e09a1", size = 13810105, upload-time = "2025-12-15T05:02:40.327Z" }, + { url = "https://files.pythonhosted.org/packages/0e/fd/3e82603a0cb66b67c5e7abababce6bf1a929ddf67bf445e652684af5c5a0/mypy-1.19.1-cp310-cp310-win_amd64.whl", hash = "sha256:2899753e2f61e571b3971747e302d5f420c3fd09650e1951e99f823bc3089dac", size = 10057200, upload-time = "2025-12-15T05:02:51.012Z" }, + { url = "https://files.pythonhosted.org/packages/ef/47/6b3ebabd5474d9cdc170d1342fbf9dddc1b0ec13ec90bf9004ee6f391c31/mypy-1.19.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d8dfc6ab58ca7dda47d9237349157500468e404b17213d44fc1cb77bce532288", size = 13028539, upload-time = "2025-12-15T05:03:44.129Z" }, + { url = "https://files.pythonhosted.org/packages/5c/a6/ac7c7a88a3c9c54334f53a941b765e6ec6c4ebd65d3fe8cdcfbe0d0fd7db/mypy-1.19.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e3f276d8493c3c97930e354b2595a44a21348b320d859fb4a2b9f66da9ed27ab", size = 12083163, upload-time = "2025-12-15T05:03:37.679Z" }, + { url = "https://files.pythonhosted.org/packages/67/af/3afa9cf880aa4a2c803798ac24f1d11ef72a0c8079689fac5cfd815e2830/mypy-1.19.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2abb24cf3f17864770d18d673c85235ba52456b36a06b6afc1e07c1fdcd3d0e6", size = 12687629, upload-time = "2025-12-15T05:02:31.526Z" }, + { url = "https://files.pythonhosted.org/packages/2d/46/20f8a7114a56484ab268b0ab372461cb3a8f7deed31ea96b83a4e4cfcfca/mypy-1.19.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a009ffa5a621762d0c926a078c2d639104becab69e79538a494bcccb62cc0331", size = 13436933, upload-time = "2025-12-15T05:03:15.606Z" }, + { url = "https://files.pythonhosted.org/packages/5b/f8/33b291ea85050a21f15da910002460f1f445f8007adb29230f0adea279cb/mypy-1.19.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f7cee03c9a2e2ee26ec07479f38ea9c884e301d42c6d43a19d20fb014e3ba925", size = 13661754, upload-time = "2025-12-15T05:02:26.731Z" }, + { url = "https://files.pythonhosted.org/packages/fd/a3/47cbd4e85bec4335a9cd80cf67dbc02be21b5d4c9c23ad6b95d6c5196bac/mypy-1.19.1-cp311-cp311-win_amd64.whl", hash = "sha256:4b84a7a18f41e167f7995200a1d07a4a6810e89d29859df936f1c3923d263042", size = 10055772, upload-time = "2025-12-15T05:03:26.179Z" }, + { url = "https://files.pythonhosted.org/packages/06/8a/19bfae96f6615aa8a0604915512e0289b1fad33d5909bf7244f02935d33a/mypy-1.19.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a8174a03289288c1f6c46d55cef02379b478bfbc8e358e02047487cad44c6ca1", size = 13206053, upload-time = "2025-12-15T05:03:46.622Z" }, + { url = "https://files.pythonhosted.org/packages/a5/34/3e63879ab041602154ba2a9f99817bb0c85c4df19a23a1443c8986e4d565/mypy-1.19.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ffcebe56eb09ff0c0885e750036a095e23793ba6c2e894e7e63f6d89ad51f22e", size = 12219134, upload-time = "2025-12-15T05:03:24.367Z" }, + { url = "https://files.pythonhosted.org/packages/89/cc/2db6f0e95366b630364e09845672dbee0cbf0bbe753a204b29a944967cd9/mypy-1.19.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b64d987153888790bcdb03a6473d321820597ab8dd9243b27a92153c4fa50fd2", size = 12731616, upload-time = "2025-12-15T05:02:44.725Z" }, + { url = "https://files.pythonhosted.org/packages/00/be/dd56c1fd4807bc1eba1cf18b2a850d0de7bacb55e158755eb79f77c41f8e/mypy-1.19.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c35d298c2c4bba75feb2195655dfea8124d855dfd7343bf8b8c055421eaf0cf8", size = 13620847, upload-time = "2025-12-15T05:03:39.633Z" }, + { url = "https://files.pythonhosted.org/packages/6d/42/332951aae42b79329f743bf1da088cd75d8d4d9acc18fbcbd84f26c1af4e/mypy-1.19.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:34c81968774648ab5ac09c29a375fdede03ba253f8f8287847bd480782f73a6a", size = 13834976, upload-time = "2025-12-15T05:03:08.786Z" }, + { url = "https://files.pythonhosted.org/packages/6f/63/e7493e5f90e1e085c562bb06e2eb32cae27c5057b9653348d38b47daaecc/mypy-1.19.1-cp312-cp312-win_amd64.whl", hash = "sha256:b10e7c2cd7870ba4ad9b2d8a6102eb5ffc1f16ca35e3de6bfa390c1113029d13", size = 10118104, upload-time = "2025-12-15T05:03:10.834Z" }, + { url = "https://files.pythonhosted.org/packages/de/9f/a6abae693f7a0c697dbb435aac52e958dc8da44e92e08ba88d2e42326176/mypy-1.19.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e3157c7594ff2ef1634ee058aafc56a82db665c9438fd41b390f3bde1ab12250", size = 13201927, upload-time = "2025-12-15T05:02:29.138Z" }, + { url = "https://files.pythonhosted.org/packages/9a/a4/45c35ccf6e1c65afc23a069f50e2c66f46bd3798cbe0d680c12d12935caa/mypy-1.19.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bdb12f69bcc02700c2b47e070238f42cb87f18c0bc1fc4cdb4fb2bc5fd7a3b8b", size = 12206730, upload-time = "2025-12-15T05:03:01.325Z" }, + { url = "https://files.pythonhosted.org/packages/05/bb/cdcf89678e26b187650512620eec8368fded4cfd99cfcb431e4cdfd19dec/mypy-1.19.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f859fb09d9583a985be9a493d5cfc5515b56b08f7447759a0c5deaf68d80506e", size = 12724581, upload-time = "2025-12-15T05:03:20.087Z" }, + { url = "https://files.pythonhosted.org/packages/d1/32/dd260d52babf67bad8e6770f8e1102021877ce0edea106e72df5626bb0ec/mypy-1.19.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c9a6538e0415310aad77cb94004ca6482330fece18036b5f360b62c45814c4ef", size = 13616252, upload-time = "2025-12-15T05:02:49.036Z" }, + { url = "https://files.pythonhosted.org/packages/71/d0/5e60a9d2e3bd48432ae2b454b7ef2b62a960ab51292b1eda2a95edd78198/mypy-1.19.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:da4869fc5e7f62a88f3fe0b5c919d1d9f7ea3cef92d3689de2823fd27e40aa75", size = 13840848, upload-time = "2025-12-15T05:02:55.95Z" }, + { url = "https://files.pythonhosted.org/packages/98/76/d32051fa65ecf6cc8c6610956473abdc9b4c43301107476ac03559507843/mypy-1.19.1-cp313-cp313-win_amd64.whl", hash = "sha256:016f2246209095e8eda7538944daa1d60e1e8134d98983b9fc1e92c1fc0cb8dd", size = 10135510, upload-time = "2025-12-15T05:02:58.438Z" }, + { url = "https://files.pythonhosted.org/packages/de/eb/b83e75f4c820c4247a58580ef86fcd35165028f191e7e1ba57128c52782d/mypy-1.19.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:06e6170bd5836770e8104c8fdd58e5e725cfeb309f0a6c681a811f557e97eac1", size = 13199744, upload-time = "2025-12-15T05:03:30.823Z" }, + { url = "https://files.pythonhosted.org/packages/94/28/52785ab7bfa165f87fcbb61547a93f98bb20e7f82f90f165a1f69bce7b3d/mypy-1.19.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:804bd67b8054a85447c8954215a906d6eff9cabeabe493fb6334b24f4bfff718", size = 12215815, upload-time = "2025-12-15T05:02:42.323Z" }, + { url = "https://files.pythonhosted.org/packages/0a/c6/bdd60774a0dbfb05122e3e925f2e9e846c009e479dcec4821dad881f5b52/mypy-1.19.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:21761006a7f497cb0d4de3d8ef4ca70532256688b0523eee02baf9eec895e27b", size = 12740047, upload-time = "2025-12-15T05:03:33.168Z" }, + { url = "https://files.pythonhosted.org/packages/32/2a/66ba933fe6c76bd40d1fe916a83f04fed253152f451a877520b3c4a5e41e/mypy-1.19.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:28902ee51f12e0f19e1e16fbe2f8f06b6637f482c459dd393efddd0ec7f82045", size = 13601998, upload-time = "2025-12-15T05:03:13.056Z" }, + { url = "https://files.pythonhosted.org/packages/e3/da/5055c63e377c5c2418760411fd6a63ee2b96cf95397259038756c042574f/mypy-1.19.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:481daf36a4c443332e2ae9c137dfee878fcea781a2e3f895d54bd3002a900957", size = 13807476, upload-time = "2025-12-15T05:03:17.977Z" }, + { url = "https://files.pythonhosted.org/packages/cd/09/4ebd873390a063176f06b0dbf1f7783dd87bd120eae7727fa4ae4179b685/mypy-1.19.1-cp314-cp314-win_amd64.whl", hash = "sha256:8bb5c6f6d043655e055be9b542aa5f3bdd30e4f3589163e85f93f3640060509f", size = 10281872, upload-time = "2025-12-15T05:03:05.549Z" }, + { url = "https://files.pythonhosted.org/packages/8d/f4/4ce9a05ce5ded1de3ec1c1d96cf9f9504a04e54ce0ed55cfa38619a32b8d/mypy-1.19.1-py3-none-any.whl", hash = "sha256:f1235f5ea01b7db5468d53ece6aaddf1ad0b88d9e7462b86ef96fe04995d7247", size = 2471239, upload-time = "2025-12-15T05:03:07.248Z" }, +] + +[[package]] +name = "mypy-extensions" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343, upload-time = "2025-04-22T14:54:24.164Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" }, +] + +[[package]] +name = "nest-asyncio" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/83/f8/51569ac65d696c8ecbee95938f89d4abf00f47d58d48f6fbabfe8f0baefe/nest_asyncio-1.6.0.tar.gz", hash = "sha256:6f172d5449aca15afd6c646851f4e31e02c598d553a667e38cafa997cfec55fe", size = 7418, upload-time = "2024-01-21T14:25:19.227Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/c4/c2971a3ba4c6103a3d10c4b0f24f461ddc027f0f09763220cf35ca1401b3/nest_asyncio-1.6.0-py3-none-any.whl", hash = "sha256:87af6efd6b5e897c81050477ef65c62e2b2f35d51703cae01aff2905b1852e1c", size = 5195, upload-time = "2024-01-21T14:25:17.223Z" }, +] + +[[package]] +name = "packaging" +version = "25.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, +] + +[[package]] +name = "parsimonious" +version = "0.10.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "regex" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7b/91/abdc50c4ef06fdf8d047f60ee777ca9b2a7885e1a9cea81343fbecda52d7/parsimonious-0.10.0.tar.gz", hash = "sha256:8281600da180ec8ae35427a4ab4f7b82bfec1e3d1e52f80cb60ea82b9512501c", size = 52172, upload-time = "2022-09-03T17:01:17.004Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/aa/0f/c8b64d9b54ea631fcad4e9e3c8dbe8c11bb32a623be94f22974c88e71eaf/parsimonious-0.10.0-py3-none-any.whl", hash = "sha256:982ab435fabe86519b57f6b35610aa4e4e977e9f02a14353edf4bbc75369fc0f", size = 48427, upload-time = "2022-09-03T17:01:13.814Z" }, +] + +[[package]] +name = "pathspec" +version = "0.12.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ca/bc/f35b8446f4531a7cb215605d100cd88b7ac6f44ab3fc94870c120ab3adbf/pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712", size = 51043, upload-time = "2023-12-10T22:30:45Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cc/20/ff623b09d963f88bfde16306a54e12ee5ea43e9b597108672ff3a408aad6/pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", size = 31191, upload-time = "2023-12-10T22:30:43.14Z" }, +] + +[[package]] +name = "platformdirs" +version = "4.5.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cf/86/0248f086a84f01b37aaec0fa567b397df1a119f73c16f6c7a9aac73ea309/platformdirs-4.5.1.tar.gz", hash = "sha256:61d5cdcc6065745cdd94f0f878977f8de9437be93de97c1c12f853c9c0cdcbda", size = 21715, upload-time = "2025-12-05T13:52:58.638Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/28/3bfe2fa5a7b9c46fe7e13c97bda14c895fb10fa2ebf1d0abb90e0cea7ee1/platformdirs-4.5.1-py3-none-any.whl", hash = "sha256:d03afa3963c806a9bed9d5125c8f4cb2fdaf74a55ab60e5d59b3fde758104d31", size = 18731, upload-time = "2025-12-05T13:52:56.823Z" }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + +[[package]] +name = "propcache" +version = "0.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9e/da/e9fc233cf63743258bff22b3dfa7ea5baef7b5bc324af47a0ad89b8ffc6f/propcache-0.4.1.tar.gz", hash = "sha256:f48107a8c637e80362555f37ecf49abe20370e557cc4ab374f04ec4423c97c3d", size = 46442, upload-time = "2025-10-08T19:49:02.291Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3c/0e/934b541323035566a9af292dba85a195f7b78179114f2c6ebb24551118a9/propcache-0.4.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7c2d1fa3201efaf55d730400d945b5b3ab6e672e100ba0f9a409d950ab25d7db", size = 79534, upload-time = "2025-10-08T19:46:02.083Z" }, + { url = "https://files.pythonhosted.org/packages/a1/6b/db0d03d96726d995dc7171286c6ba9d8d14251f37433890f88368951a44e/propcache-0.4.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:1eb2994229cc8ce7fe9b3db88f5465f5fd8651672840b2e426b88cdb1a30aac8", size = 45526, upload-time = "2025-10-08T19:46:03.884Z" }, + { url = "https://files.pythonhosted.org/packages/e4/c3/82728404aea669e1600f304f2609cde9e665c18df5a11cdd57ed73c1dceb/propcache-0.4.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:66c1f011f45a3b33d7bcb22daed4b29c0c9e2224758b6be00686731e1b46f925", size = 47263, upload-time = "2025-10-08T19:46:05.405Z" }, + { url = "https://files.pythonhosted.org/packages/df/1b/39313ddad2bf9187a1432654c38249bab4562ef535ef07f5eb6eb04d0b1b/propcache-0.4.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9a52009f2adffe195d0b605c25ec929d26b36ef986ba85244891dee3b294df21", size = 201012, upload-time = "2025-10-08T19:46:07.165Z" }, + { url = "https://files.pythonhosted.org/packages/5b/01/f1d0b57d136f294a142acf97f4ed58c8e5b974c21e543000968357115011/propcache-0.4.1-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5d4e2366a9c7b837555cf02fb9be2e3167d333aff716332ef1b7c3a142ec40c5", size = 209491, upload-time = "2025-10-08T19:46:08.909Z" }, + { url = "https://files.pythonhosted.org/packages/a1/c8/038d909c61c5bb039070b3fb02ad5cccdb1dde0d714792e251cdb17c9c05/propcache-0.4.1-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:9d2b6caef873b4f09e26ea7e33d65f42b944837563a47a94719cc3544319a0db", size = 215319, upload-time = "2025-10-08T19:46:10.7Z" }, + { url = "https://files.pythonhosted.org/packages/08/57/8c87e93142b2c1fa2408e45695205a7ba05fb5db458c0bf5c06ba0e09ea6/propcache-0.4.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2b16ec437a8c8a965ecf95739448dd938b5c7f56e67ea009f4300d8df05f32b7", size = 196856, upload-time = "2025-10-08T19:46:12.003Z" }, + { url = "https://files.pythonhosted.org/packages/42/df/5615fec76aa561987a534759b3686008a288e73107faa49a8ae5795a9f7a/propcache-0.4.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:296f4c8ed03ca7476813fe666c9ea97869a8d7aec972618671b33a38a5182ef4", size = 193241, upload-time = "2025-10-08T19:46:13.495Z" }, + { url = "https://files.pythonhosted.org/packages/d5/21/62949eb3a7a54afe8327011c90aca7e03547787a88fb8bd9726806482fea/propcache-0.4.1-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:1f0978529a418ebd1f49dad413a2b68af33f85d5c5ca5c6ca2a3bed375a7ac60", size = 190552, upload-time = "2025-10-08T19:46:14.938Z" }, + { url = "https://files.pythonhosted.org/packages/30/ee/ab4d727dd70806e5b4de96a798ae7ac6e4d42516f030ee60522474b6b332/propcache-0.4.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:fd138803047fb4c062b1c1dd95462f5209456bfab55c734458f15d11da288f8f", size = 200113, upload-time = "2025-10-08T19:46:16.695Z" }, + { url = "https://files.pythonhosted.org/packages/8a/0b/38b46208e6711b016aa8966a3ac793eee0d05c7159d8342aa27fc0bc365e/propcache-0.4.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:8c9b3cbe4584636d72ff556d9036e0c9317fa27b3ac1f0f558e7e84d1c9c5900", size = 200778, upload-time = "2025-10-08T19:46:18.023Z" }, + { url = "https://files.pythonhosted.org/packages/cf/81/5abec54355ed344476bee711e9f04815d4b00a311ab0535599204eecc257/propcache-0.4.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f93243fdc5657247533273ac4f86ae106cc6445a0efacb9a1bfe982fcfefd90c", size = 193047, upload-time = "2025-10-08T19:46:19.449Z" }, + { url = "https://files.pythonhosted.org/packages/ec/b6/1f237c04e32063cb034acd5f6ef34ef3a394f75502e72703545631ab1ef6/propcache-0.4.1-cp310-cp310-win32.whl", hash = "sha256:a0ee98db9c5f80785b266eb805016e36058ac72c51a064040f2bc43b61101cdb", size = 38093, upload-time = "2025-10-08T19:46:20.643Z" }, + { url = "https://files.pythonhosted.org/packages/a6/67/354aac4e0603a15f76439caf0427781bcd6797f370377f75a642133bc954/propcache-0.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:1cdb7988c4e5ac7f6d175a28a9aa0c94cb6f2ebe52756a3c0cda98d2809a9e37", size = 41638, upload-time = "2025-10-08T19:46:21.935Z" }, + { url = "https://files.pythonhosted.org/packages/e0/e1/74e55b9fd1a4c209ff1a9a824bf6c8b3d1fc5a1ac3eabe23462637466785/propcache-0.4.1-cp310-cp310-win_arm64.whl", hash = "sha256:d82ad62b19645419fe79dd63b3f9253e15b30e955c0170e5cebc350c1844e581", size = 38229, upload-time = "2025-10-08T19:46:23.368Z" }, + { url = "https://files.pythonhosted.org/packages/8c/d4/4e2c9aaf7ac2242b9358f98dccd8f90f2605402f5afeff6c578682c2c491/propcache-0.4.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:60a8fda9644b7dfd5dece8c61d8a85e271cb958075bfc4e01083c148b61a7caf", size = 80208, upload-time = "2025-10-08T19:46:24.597Z" }, + { url = "https://files.pythonhosted.org/packages/c2/21/d7b68e911f9c8e18e4ae43bdbc1e1e9bbd971f8866eb81608947b6f585ff/propcache-0.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c30b53e7e6bda1d547cabb47c825f3843a0a1a42b0496087bb58d8fedf9f41b5", size = 45777, upload-time = "2025-10-08T19:46:25.733Z" }, + { url = "https://files.pythonhosted.org/packages/d3/1d/11605e99ac8ea9435651ee71ab4cb4bf03f0949586246476a25aadfec54a/propcache-0.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6918ecbd897443087a3b7cd978d56546a812517dcaaca51b49526720571fa93e", size = 47647, upload-time = "2025-10-08T19:46:27.304Z" }, + { url = "https://files.pythonhosted.org/packages/58/1a/3c62c127a8466c9c843bccb503d40a273e5cc69838805f322e2826509e0d/propcache-0.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3d902a36df4e5989763425a8ab9e98cd8ad5c52c823b34ee7ef307fd50582566", size = 214929, upload-time = "2025-10-08T19:46:28.62Z" }, + { url = "https://files.pythonhosted.org/packages/56/b9/8fa98f850960b367c4b8fe0592e7fc341daa7a9462e925228f10a60cf74f/propcache-0.4.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a9695397f85973bb40427dedddf70d8dc4a44b22f1650dd4af9eedf443d45165", size = 221778, upload-time = "2025-10-08T19:46:30.358Z" }, + { url = "https://files.pythonhosted.org/packages/46/a6/0ab4f660eb59649d14b3d3d65c439421cf2f87fe5dd68591cbe3c1e78a89/propcache-0.4.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2bb07ffd7eaad486576430c89f9b215f9e4be68c4866a96e97db9e97fead85dc", size = 228144, upload-time = "2025-10-08T19:46:32.607Z" }, + { url = "https://files.pythonhosted.org/packages/52/6a/57f43e054fb3d3a56ac9fc532bc684fc6169a26c75c353e65425b3e56eef/propcache-0.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fd6f30fdcf9ae2a70abd34da54f18da086160e4d7d9251f81f3da0ff84fc5a48", size = 210030, upload-time = "2025-10-08T19:46:33.969Z" }, + { url = "https://files.pythonhosted.org/packages/40/e2/27e6feebb5f6b8408fa29f5efbb765cd54c153ac77314d27e457a3e993b7/propcache-0.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:fc38cba02d1acba4e2869eef1a57a43dfbd3d49a59bf90dda7444ec2be6a5570", size = 208252, upload-time = "2025-10-08T19:46:35.309Z" }, + { url = "https://files.pythonhosted.org/packages/9e/f8/91c27b22ccda1dbc7967f921c42825564fa5336a01ecd72eb78a9f4f53c2/propcache-0.4.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:67fad6162281e80e882fb3ec355398cf72864a54069d060321f6cd0ade95fe85", size = 202064, upload-time = "2025-10-08T19:46:36.993Z" }, + { url = "https://files.pythonhosted.org/packages/f2/26/7f00bd6bd1adba5aafe5f4a66390f243acab58eab24ff1a08bebb2ef9d40/propcache-0.4.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f10207adf04d08bec185bae14d9606a1444715bc99180f9331c9c02093e1959e", size = 212429, upload-time = "2025-10-08T19:46:38.398Z" }, + { url = "https://files.pythonhosted.org/packages/84/89/fd108ba7815c1117ddca79c228f3f8a15fc82a73bca8b142eb5de13b2785/propcache-0.4.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:e9b0d8d0845bbc4cfcdcbcdbf5086886bc8157aa963c31c777ceff7846c77757", size = 216727, upload-time = "2025-10-08T19:46:39.732Z" }, + { url = "https://files.pythonhosted.org/packages/79/37/3ec3f7e3173e73f1d600495d8b545b53802cbf35506e5732dd8578db3724/propcache-0.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:981333cb2f4c1896a12f4ab92a9cc8f09ea664e9b7dbdc4eff74627af3a11c0f", size = 205097, upload-time = "2025-10-08T19:46:41.025Z" }, + { url = "https://files.pythonhosted.org/packages/61/b0/b2631c19793f869d35f47d5a3a56fb19e9160d3c119f15ac7344fc3ccae7/propcache-0.4.1-cp311-cp311-win32.whl", hash = "sha256:f1d2f90aeec838a52f1c1a32fe9a619fefd5e411721a9117fbf82aea638fe8a1", size = 38084, upload-time = "2025-10-08T19:46:42.693Z" }, + { url = "https://files.pythonhosted.org/packages/f4/78/6cce448e2098e9f3bfc91bb877f06aa24b6ccace872e39c53b2f707c4648/propcache-0.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:364426a62660f3f699949ac8c621aad6977be7126c5807ce48c0aeb8e7333ea6", size = 41637, upload-time = "2025-10-08T19:46:43.778Z" }, + { url = "https://files.pythonhosted.org/packages/9c/e9/754f180cccd7f51a39913782c74717c581b9cc8177ad0e949f4d51812383/propcache-0.4.1-cp311-cp311-win_arm64.whl", hash = "sha256:e53f3a38d3510c11953f3e6a33f205c6d1b001129f972805ca9b42fc308bc239", size = 38064, upload-time = "2025-10-08T19:46:44.872Z" }, + { url = "https://files.pythonhosted.org/packages/a2/0f/f17b1b2b221d5ca28b4b876e8bb046ac40466513960646bda8e1853cdfa2/propcache-0.4.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e153e9cd40cc8945138822807139367f256f89c6810c2634a4f6902b52d3b4e2", size = 80061, upload-time = "2025-10-08T19:46:46.075Z" }, + { url = "https://files.pythonhosted.org/packages/76/47/8ccf75935f51448ba9a16a71b783eb7ef6b9ee60f5d14c7f8a8a79fbeed7/propcache-0.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:cd547953428f7abb73c5ad82cbb32109566204260d98e41e5dfdc682eb7f8403", size = 46037, upload-time = "2025-10-08T19:46:47.23Z" }, + { url = "https://files.pythonhosted.org/packages/0a/b6/5c9a0e42df4d00bfb4a3cbbe5cf9f54260300c88a0e9af1f47ca5ce17ac0/propcache-0.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f048da1b4f243fc44f205dfd320933a951b8d89e0afd4c7cacc762a8b9165207", size = 47324, upload-time = "2025-10-08T19:46:48.384Z" }, + { url = "https://files.pythonhosted.org/packages/9e/d3/6c7ee328b39a81ee877c962469f1e795f9db87f925251efeb0545e0020d0/propcache-0.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ec17c65562a827bba85e3872ead335f95405ea1674860d96483a02f5c698fa72", size = 225505, upload-time = "2025-10-08T19:46:50.055Z" }, + { url = "https://files.pythonhosted.org/packages/01/5d/1c53f4563490b1d06a684742cc6076ef944bc6457df6051b7d1a877c057b/propcache-0.4.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:405aac25c6394ef275dee4c709be43745d36674b223ba4eb7144bf4d691b7367", size = 230242, upload-time = "2025-10-08T19:46:51.815Z" }, + { url = "https://files.pythonhosted.org/packages/20/e1/ce4620633b0e2422207c3cb774a0ee61cac13abc6217763a7b9e2e3f4a12/propcache-0.4.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0013cb6f8dde4b2a2f66903b8ba740bdfe378c943c4377a200551ceb27f379e4", size = 238474, upload-time = "2025-10-08T19:46:53.208Z" }, + { url = "https://files.pythonhosted.org/packages/46/4b/3aae6835b8e5f44ea6a68348ad90f78134047b503765087be2f9912140ea/propcache-0.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:15932ab57837c3368b024473a525e25d316d8353016e7cc0e5ba9eb343fbb1cf", size = 221575, upload-time = "2025-10-08T19:46:54.511Z" }, + { url = "https://files.pythonhosted.org/packages/6e/a5/8a5e8678bcc9d3a1a15b9a29165640d64762d424a16af543f00629c87338/propcache-0.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:031dce78b9dc099f4c29785d9cf5577a3faf9ebf74ecbd3c856a7b92768c3df3", size = 216736, upload-time = "2025-10-08T19:46:56.212Z" }, + { url = "https://files.pythonhosted.org/packages/f1/63/b7b215eddeac83ca1c6b934f89d09a625aa9ee4ba158338854c87210cc36/propcache-0.4.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:ab08df6c9a035bee56e31af99be621526bd237bea9f32def431c656b29e41778", size = 213019, upload-time = "2025-10-08T19:46:57.595Z" }, + { url = "https://files.pythonhosted.org/packages/57/74/f580099a58c8af587cac7ba19ee7cb418506342fbbe2d4a4401661cca886/propcache-0.4.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4d7af63f9f93fe593afbf104c21b3b15868efb2c21d07d8732c0c4287e66b6a6", size = 220376, upload-time = "2025-10-08T19:46:59.067Z" }, + { url = "https://files.pythonhosted.org/packages/c4/ee/542f1313aff7eaf19c2bb758c5d0560d2683dac001a1c96d0774af799843/propcache-0.4.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:cfc27c945f422e8b5071b6e93169679e4eb5bf73bbcbf1ba3ae3a83d2f78ebd9", size = 226988, upload-time = "2025-10-08T19:47:00.544Z" }, + { url = "https://files.pythonhosted.org/packages/8f/18/9c6b015dd9c6930f6ce2229e1f02fb35298b847f2087ea2b436a5bfa7287/propcache-0.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:35c3277624a080cc6ec6f847cbbbb5b49affa3598c4535a0a4682a697aaa5c75", size = 215615, upload-time = "2025-10-08T19:47:01.968Z" }, + { url = "https://files.pythonhosted.org/packages/80/9e/e7b85720b98c45a45e1fca6a177024934dc9bc5f4d5dd04207f216fc33ed/propcache-0.4.1-cp312-cp312-win32.whl", hash = "sha256:671538c2262dadb5ba6395e26c1731e1d52534bfe9ae56d0b5573ce539266aa8", size = 38066, upload-time = "2025-10-08T19:47:03.503Z" }, + { url = "https://files.pythonhosted.org/packages/54/09/d19cff2a5aaac632ec8fc03737b223597b1e347416934c1b3a7df079784c/propcache-0.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:cb2d222e72399fcf5890d1d5cc1060857b9b236adff2792ff48ca2dfd46c81db", size = 41655, upload-time = "2025-10-08T19:47:04.973Z" }, + { url = "https://files.pythonhosted.org/packages/68/ab/6b5c191bb5de08036a8c697b265d4ca76148efb10fa162f14af14fb5f076/propcache-0.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:204483131fb222bdaaeeea9f9e6c6ed0cac32731f75dfc1d4a567fc1926477c1", size = 37789, upload-time = "2025-10-08T19:47:06.077Z" }, + { url = "https://files.pythonhosted.org/packages/bf/df/6d9c1b6ac12b003837dde8a10231a7344512186e87b36e855bef32241942/propcache-0.4.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:43eedf29202c08550aac1d14e0ee619b0430aaef78f85864c1a892294fbc28cf", size = 77750, upload-time = "2025-10-08T19:47:07.648Z" }, + { url = "https://files.pythonhosted.org/packages/8b/e8/677a0025e8a2acf07d3418a2e7ba529c9c33caf09d3c1f25513023c1db56/propcache-0.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d62cdfcfd89ccb8de04e0eda998535c406bf5e060ffd56be6c586cbcc05b3311", size = 44780, upload-time = "2025-10-08T19:47:08.851Z" }, + { url = "https://files.pythonhosted.org/packages/89/a4/92380f7ca60f99ebae761936bc48a72a639e8a47b29050615eef757cb2a7/propcache-0.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cae65ad55793da34db5f54e4029b89d3b9b9490d8abe1b4c7ab5d4b8ec7ebf74", size = 46308, upload-time = "2025-10-08T19:47:09.982Z" }, + { url = "https://files.pythonhosted.org/packages/2d/48/c5ac64dee5262044348d1d78a5f85dd1a57464a60d30daee946699963eb3/propcache-0.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:333ddb9031d2704a301ee3e506dc46b1fe5f294ec198ed6435ad5b6a085facfe", size = 208182, upload-time = "2025-10-08T19:47:11.319Z" }, + { url = "https://files.pythonhosted.org/packages/c6/0c/cd762dd011a9287389a6a3eb43aa30207bde253610cca06824aeabfe9653/propcache-0.4.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:fd0858c20f078a32cf55f7e81473d96dcf3b93fd2ccdb3d40fdf54b8573df3af", size = 211215, upload-time = "2025-10-08T19:47:13.146Z" }, + { url = "https://files.pythonhosted.org/packages/30/3e/49861e90233ba36890ae0ca4c660e95df565b2cd15d4a68556ab5865974e/propcache-0.4.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:678ae89ebc632c5c204c794f8dab2837c5f159aeb59e6ed0539500400577298c", size = 218112, upload-time = "2025-10-08T19:47:14.913Z" }, + { url = "https://files.pythonhosted.org/packages/f1/8b/544bc867e24e1bd48f3118cecd3b05c694e160a168478fa28770f22fd094/propcache-0.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d472aeb4fbf9865e0c6d622d7f4d54a4e101a89715d8904282bb5f9a2f476c3f", size = 204442, upload-time = "2025-10-08T19:47:16.277Z" }, + { url = "https://files.pythonhosted.org/packages/50/a6/4282772fd016a76d3e5c0df58380a5ea64900afd836cec2c2f662d1b9bb3/propcache-0.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4d3df5fa7e36b3225954fba85589da77a0fe6a53e3976de39caf04a0db4c36f1", size = 199398, upload-time = "2025-10-08T19:47:17.962Z" }, + { url = "https://files.pythonhosted.org/packages/3e/ec/d8a7cd406ee1ddb705db2139f8a10a8a427100347bd698e7014351c7af09/propcache-0.4.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:ee17f18d2498f2673e432faaa71698032b0127ebf23ae5974eeaf806c279df24", size = 196920, upload-time = "2025-10-08T19:47:19.355Z" }, + { url = "https://files.pythonhosted.org/packages/f6/6c/f38ab64af3764f431e359f8baf9e0a21013e24329e8b85d2da32e8ed07ca/propcache-0.4.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:580e97762b950f993ae618e167e7be9256b8353c2dcd8b99ec100eb50f5286aa", size = 203748, upload-time = "2025-10-08T19:47:21.338Z" }, + { url = "https://files.pythonhosted.org/packages/d6/e3/fa846bd70f6534d647886621388f0a265254d30e3ce47e5c8e6e27dbf153/propcache-0.4.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:501d20b891688eb8e7aa903021f0b72d5a55db40ffaab27edefd1027caaafa61", size = 205877, upload-time = "2025-10-08T19:47:23.059Z" }, + { url = "https://files.pythonhosted.org/packages/e2/39/8163fc6f3133fea7b5f2827e8eba2029a0277ab2c5beee6c1db7b10fc23d/propcache-0.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a0bd56e5b100aef69bd8562b74b46254e7c8812918d3baa700c8a8009b0af66", size = 199437, upload-time = "2025-10-08T19:47:24.445Z" }, + { url = "https://files.pythonhosted.org/packages/93/89/caa9089970ca49c7c01662bd0eeedfe85494e863e8043565aeb6472ce8fe/propcache-0.4.1-cp313-cp313-win32.whl", hash = "sha256:bcc9aaa5d80322bc2fb24bb7accb4a30f81e90ab8d6ba187aec0744bc302ad81", size = 37586, upload-time = "2025-10-08T19:47:25.736Z" }, + { url = "https://files.pythonhosted.org/packages/f5/ab/f76ec3c3627c883215b5c8080debb4394ef5a7a29be811f786415fc1e6fd/propcache-0.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:381914df18634f5494334d201e98245c0596067504b9372d8cf93f4bb23e025e", size = 40790, upload-time = "2025-10-08T19:47:26.847Z" }, + { url = "https://files.pythonhosted.org/packages/59/1b/e71ae98235f8e2ba5004d8cb19765a74877abf189bc53fc0c80d799e56c3/propcache-0.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:8873eb4460fd55333ea49b7d189749ecf6e55bf85080f11b1c4530ed3034cba1", size = 37158, upload-time = "2025-10-08T19:47:27.961Z" }, + { url = "https://files.pythonhosted.org/packages/83/ce/a31bbdfc24ee0dcbba458c8175ed26089cf109a55bbe7b7640ed2470cfe9/propcache-0.4.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:92d1935ee1f8d7442da9c0c4fa7ac20d07e94064184811b685f5c4fada64553b", size = 81451, upload-time = "2025-10-08T19:47:29.445Z" }, + { url = "https://files.pythonhosted.org/packages/25/9c/442a45a470a68456e710d96cacd3573ef26a1d0a60067e6a7d5e655621ed/propcache-0.4.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:473c61b39e1460d386479b9b2f337da492042447c9b685f28be4f74d3529e566", size = 46374, upload-time = "2025-10-08T19:47:30.579Z" }, + { url = "https://files.pythonhosted.org/packages/f4/bf/b1d5e21dbc3b2e889ea4327044fb16312a736d97640fb8b6aa3f9c7b3b65/propcache-0.4.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:c0ef0aaafc66fbd87842a3fe3902fd889825646bc21149eafe47be6072725835", size = 48396, upload-time = "2025-10-08T19:47:31.79Z" }, + { url = "https://files.pythonhosted.org/packages/f4/04/5b4c54a103d480e978d3c8a76073502b18db0c4bc17ab91b3cb5092ad949/propcache-0.4.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f95393b4d66bfae908c3ca8d169d5f79cd65636ae15b5e7a4f6e67af675adb0e", size = 275950, upload-time = "2025-10-08T19:47:33.481Z" }, + { url = "https://files.pythonhosted.org/packages/b4/c1/86f846827fb969c4b78b0af79bba1d1ea2156492e1b83dea8b8a6ae27395/propcache-0.4.1-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c07fda85708bc48578467e85099645167a955ba093be0a2dcba962195676e859", size = 273856, upload-time = "2025-10-08T19:47:34.906Z" }, + { url = "https://files.pythonhosted.org/packages/36/1d/fc272a63c8d3bbad6878c336c7a7dea15e8f2d23a544bda43205dfa83ada/propcache-0.4.1-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:af223b406d6d000830c6f65f1e6431783fc3f713ba3e6cc8c024d5ee96170a4b", size = 280420, upload-time = "2025-10-08T19:47:36.338Z" }, + { url = "https://files.pythonhosted.org/packages/07/0c/01f2219d39f7e53d52e5173bcb09c976609ba30209912a0680adfb8c593a/propcache-0.4.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a78372c932c90ee474559c5ddfffd718238e8673c340dc21fe45c5b8b54559a0", size = 263254, upload-time = "2025-10-08T19:47:37.692Z" }, + { url = "https://files.pythonhosted.org/packages/2d/18/cd28081658ce597898f0c4d174d4d0f3c5b6d4dc27ffafeef835c95eb359/propcache-0.4.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:564d9f0d4d9509e1a870c920a89b2fec951b44bf5ba7d537a9e7c1ccec2c18af", size = 261205, upload-time = "2025-10-08T19:47:39.659Z" }, + { url = "https://files.pythonhosted.org/packages/7a/71/1f9e22eb8b8316701c2a19fa1f388c8a3185082607da8e406a803c9b954e/propcache-0.4.1-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:17612831fda0138059cc5546f4d12a2aacfb9e47068c06af35c400ba58ba7393", size = 247873, upload-time = "2025-10-08T19:47:41.084Z" }, + { url = "https://files.pythonhosted.org/packages/4a/65/3d4b61f36af2b4eddba9def857959f1016a51066b4f1ce348e0cf7881f58/propcache-0.4.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:41a89040cb10bd345b3c1a873b2bf36413d48da1def52f268a055f7398514874", size = 262739, upload-time = "2025-10-08T19:47:42.51Z" }, + { url = "https://files.pythonhosted.org/packages/2a/42/26746ab087faa77c1c68079b228810436ccd9a5ce9ac85e2b7307195fd06/propcache-0.4.1-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:e35b88984e7fa64aacecea39236cee32dd9bd8c55f57ba8a75cf2399553f9bd7", size = 263514, upload-time = "2025-10-08T19:47:43.927Z" }, + { url = "https://files.pythonhosted.org/packages/94/13/630690fe201f5502d2403dd3cfd451ed8858fe3c738ee88d095ad2ff407b/propcache-0.4.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6f8b465489f927b0df505cbe26ffbeed4d6d8a2bbc61ce90eb074ff129ef0ab1", size = 257781, upload-time = "2025-10-08T19:47:45.448Z" }, + { url = "https://files.pythonhosted.org/packages/92/f7/1d4ec5841505f423469efbfc381d64b7b467438cd5a4bbcbb063f3b73d27/propcache-0.4.1-cp313-cp313t-win32.whl", hash = "sha256:2ad890caa1d928c7c2965b48f3a3815c853180831d0e5503d35cf00c472f4717", size = 41396, upload-time = "2025-10-08T19:47:47.202Z" }, + { url = "https://files.pythonhosted.org/packages/48/f0/615c30622316496d2cbbc29f5985f7777d3ada70f23370608c1d3e081c1f/propcache-0.4.1-cp313-cp313t-win_amd64.whl", hash = "sha256:f7ee0e597f495cf415bcbd3da3caa3bd7e816b74d0d52b8145954c5e6fd3ff37", size = 44897, upload-time = "2025-10-08T19:47:48.336Z" }, + { url = "https://files.pythonhosted.org/packages/fd/ca/6002e46eccbe0e33dcd4069ef32f7f1c9e243736e07adca37ae8c4830ec3/propcache-0.4.1-cp313-cp313t-win_arm64.whl", hash = "sha256:929d7cbe1f01bb7baffb33dc14eb5691c95831450a26354cd210a8155170c93a", size = 39789, upload-time = "2025-10-08T19:47:49.876Z" }, + { url = "https://files.pythonhosted.org/packages/8e/5c/bca52d654a896f831b8256683457ceddd490ec18d9ec50e97dfd8fc726a8/propcache-0.4.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3f7124c9d820ba5548d431afb4632301acf965db49e666aa21c305cbe8c6de12", size = 78152, upload-time = "2025-10-08T19:47:51.051Z" }, + { url = "https://files.pythonhosted.org/packages/65/9b/03b04e7d82a5f54fb16113d839f5ea1ede58a61e90edf515f6577c66fa8f/propcache-0.4.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:c0d4b719b7da33599dfe3b22d3db1ef789210a0597bc650b7cee9c77c2be8c5c", size = 44869, upload-time = "2025-10-08T19:47:52.594Z" }, + { url = "https://files.pythonhosted.org/packages/b2/fa/89a8ef0468d5833a23fff277b143d0573897cf75bd56670a6d28126c7d68/propcache-0.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9f302f4783709a78240ebc311b793f123328716a60911d667e0c036bc5dcbded", size = 46596, upload-time = "2025-10-08T19:47:54.073Z" }, + { url = "https://files.pythonhosted.org/packages/86/bd/47816020d337f4a746edc42fe8d53669965138f39ee117414c7d7a340cfe/propcache-0.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c80ee5802e3fb9ea37938e7eecc307fb984837091d5fd262bb37238b1ae97641", size = 206981, upload-time = "2025-10-08T19:47:55.715Z" }, + { url = "https://files.pythonhosted.org/packages/df/f6/c5fa1357cc9748510ee55f37173eb31bfde6d94e98ccd9e6f033f2fc06e1/propcache-0.4.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ed5a841e8bb29a55fb8159ed526b26adc5bdd7e8bd7bf793ce647cb08656cdf4", size = 211490, upload-time = "2025-10-08T19:47:57.499Z" }, + { url = "https://files.pythonhosted.org/packages/80/1e/e5889652a7c4a3846683401a48f0f2e5083ce0ec1a8a5221d8058fbd1adf/propcache-0.4.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:55c72fd6ea2da4c318e74ffdf93c4fe4e926051133657459131a95c846d16d44", size = 215371, upload-time = "2025-10-08T19:47:59.317Z" }, + { url = "https://files.pythonhosted.org/packages/b2/f2/889ad4b2408f72fe1a4f6a19491177b30ea7bf1a0fd5f17050ca08cfc882/propcache-0.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8326e144341460402713f91df60ade3c999d601e7eb5ff8f6f7862d54de0610d", size = 201424, upload-time = "2025-10-08T19:48:00.67Z" }, + { url = "https://files.pythonhosted.org/packages/27/73/033d63069b57b0812c8bd19f311faebeceb6ba31b8f32b73432d12a0b826/propcache-0.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:060b16ae65bc098da7f6d25bf359f1f31f688384858204fe5d652979e0015e5b", size = 197566, upload-time = "2025-10-08T19:48:02.604Z" }, + { url = "https://files.pythonhosted.org/packages/dc/89/ce24f3dc182630b4e07aa6d15f0ff4b14ed4b9955fae95a0b54c58d66c05/propcache-0.4.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:89eb3fa9524f7bec9de6e83cf3faed9d79bffa560672c118a96a171a6f55831e", size = 193130, upload-time = "2025-10-08T19:48:04.499Z" }, + { url = "https://files.pythonhosted.org/packages/a9/24/ef0d5fd1a811fb5c609278d0209c9f10c35f20581fcc16f818da959fc5b4/propcache-0.4.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:dee69d7015dc235f526fe80a9c90d65eb0039103fe565776250881731f06349f", size = 202625, upload-time = "2025-10-08T19:48:06.213Z" }, + { url = "https://files.pythonhosted.org/packages/f5/02/98ec20ff5546f68d673df2f7a69e8c0d076b5abd05ca882dc7ee3a83653d/propcache-0.4.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:5558992a00dfd54ccbc64a32726a3357ec93825a418a401f5cc67df0ac5d9e49", size = 204209, upload-time = "2025-10-08T19:48:08.432Z" }, + { url = "https://files.pythonhosted.org/packages/a0/87/492694f76759b15f0467a2a93ab68d32859672b646aa8a04ce4864e7932d/propcache-0.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c9b822a577f560fbd9554812526831712c1436d2c046cedee4c3796d3543b144", size = 197797, upload-time = "2025-10-08T19:48:09.968Z" }, + { url = "https://files.pythonhosted.org/packages/ee/36/66367de3575db1d2d3f3d177432bd14ee577a39d3f5d1b3d5df8afe3b6e2/propcache-0.4.1-cp314-cp314-win32.whl", hash = "sha256:ab4c29b49d560fe48b696cdcb127dd36e0bc2472548f3bf56cc5cb3da2b2984f", size = 38140, upload-time = "2025-10-08T19:48:11.232Z" }, + { url = "https://files.pythonhosted.org/packages/0c/2a/a758b47de253636e1b8aef181c0b4f4f204bf0dd964914fb2af90a95b49b/propcache-0.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:5a103c3eb905fcea0ab98be99c3a9a5ab2de60228aa5aceedc614c0281cf6153", size = 41257, upload-time = "2025-10-08T19:48:12.707Z" }, + { url = "https://files.pythonhosted.org/packages/34/5e/63bd5896c3fec12edcbd6f12508d4890d23c265df28c74b175e1ef9f4f3b/propcache-0.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:74c1fb26515153e482e00177a1ad654721bf9207da8a494a0c05e797ad27b992", size = 38097, upload-time = "2025-10-08T19:48:13.923Z" }, + { url = "https://files.pythonhosted.org/packages/99/85/9ff785d787ccf9bbb3f3106f79884a130951436f58392000231b4c737c80/propcache-0.4.1-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:824e908bce90fb2743bd6b59db36eb4f45cd350a39637c9f73b1c1ea66f5b75f", size = 81455, upload-time = "2025-10-08T19:48:15.16Z" }, + { url = "https://files.pythonhosted.org/packages/90/85/2431c10c8e7ddb1445c1f7c4b54d886e8ad20e3c6307e7218f05922cad67/propcache-0.4.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:c2b5e7db5328427c57c8e8831abda175421b709672f6cfc3d630c3b7e2146393", size = 46372, upload-time = "2025-10-08T19:48:16.424Z" }, + { url = "https://files.pythonhosted.org/packages/01/20/b0972d902472da9bcb683fa595099911f4d2e86e5683bcc45de60dd05dc3/propcache-0.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6f6ff873ed40292cd4969ef5310179afd5db59fdf055897e282485043fc80ad0", size = 48411, upload-time = "2025-10-08T19:48:17.577Z" }, + { url = "https://files.pythonhosted.org/packages/e2/e3/7dc89f4f21e8f99bad3d5ddb3a3389afcf9da4ac69e3deb2dcdc96e74169/propcache-0.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:49a2dc67c154db2c1463013594c458881a069fcf98940e61a0569016a583020a", size = 275712, upload-time = "2025-10-08T19:48:18.901Z" }, + { url = "https://files.pythonhosted.org/packages/20/67/89800c8352489b21a8047c773067644e3897f02ecbbd610f4d46b7f08612/propcache-0.4.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:005f08e6a0529984491e37d8dbc3dd86f84bd78a8ceb5fa9a021f4c48d4984be", size = 273557, upload-time = "2025-10-08T19:48:20.762Z" }, + { url = "https://files.pythonhosted.org/packages/e2/a1/b52b055c766a54ce6d9c16d9aca0cad8059acd9637cdf8aa0222f4a026ef/propcache-0.4.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5c3310452e0d31390da9035c348633b43d7e7feb2e37be252be6da45abd1abcc", size = 280015, upload-time = "2025-10-08T19:48:22.592Z" }, + { url = "https://files.pythonhosted.org/packages/48/c8/33cee30bd890672c63743049f3c9e4be087e6780906bfc3ec58528be59c1/propcache-0.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4c3c70630930447f9ef1caac7728c8ad1c56bc5015338b20fed0d08ea2480b3a", size = 262880, upload-time = "2025-10-08T19:48:23.947Z" }, + { url = "https://files.pythonhosted.org/packages/0c/b1/8f08a143b204b418285c88b83d00edbd61afbc2c6415ffafc8905da7038b/propcache-0.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8e57061305815dfc910a3634dcf584f08168a8836e6999983569f51a8544cd89", size = 260938, upload-time = "2025-10-08T19:48:25.656Z" }, + { url = "https://files.pythonhosted.org/packages/cf/12/96e4664c82ca2f31e1c8dff86afb867348979eb78d3cb8546a680287a1e9/propcache-0.4.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:521a463429ef54143092c11a77e04056dd00636f72e8c45b70aaa3140d639726", size = 247641, upload-time = "2025-10-08T19:48:27.207Z" }, + { url = "https://files.pythonhosted.org/packages/18/ed/e7a9cfca28133386ba52278136d42209d3125db08d0a6395f0cba0c0285c/propcache-0.4.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:120c964da3fdc75e3731aa392527136d4ad35868cc556fd09bb6d09172d9a367", size = 262510, upload-time = "2025-10-08T19:48:28.65Z" }, + { url = "https://files.pythonhosted.org/packages/f5/76/16d8bf65e8845dd62b4e2b57444ab81f07f40caa5652b8969b87ddcf2ef6/propcache-0.4.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:d8f353eb14ee3441ee844ade4277d560cdd68288838673273b978e3d6d2c8f36", size = 263161, upload-time = "2025-10-08T19:48:30.133Z" }, + { url = "https://files.pythonhosted.org/packages/e7/70/c99e9edb5d91d5ad8a49fa3c1e8285ba64f1476782fed10ab251ff413ba1/propcache-0.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ab2943be7c652f09638800905ee1bab2c544e537edb57d527997a24c13dc1455", size = 257393, upload-time = "2025-10-08T19:48:31.567Z" }, + { url = "https://files.pythonhosted.org/packages/08/02/87b25304249a35c0915d236575bc3574a323f60b47939a2262b77632a3ee/propcache-0.4.1-cp314-cp314t-win32.whl", hash = "sha256:05674a162469f31358c30bcaa8883cb7829fa3110bf9c0991fe27d7896c42d85", size = 42546, upload-time = "2025-10-08T19:48:32.872Z" }, + { url = "https://files.pythonhosted.org/packages/cb/ef/3c6ecf8b317aa982f309835e8f96987466123c6e596646d4e6a1dfcd080f/propcache-0.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:990f6b3e2a27d683cb7602ed6c86f15ee6b43b1194736f9baaeb93d0016633b1", size = 46259, upload-time = "2025-10-08T19:48:34.226Z" }, + { url = "https://files.pythonhosted.org/packages/c4/2d/346e946d4951f37eca1e4f55be0f0174c52cd70720f84029b02f296f4a38/propcache-0.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:ecef2343af4cc68e05131e45024ba34f6095821988a9d0a02aa7c73fcc448aa9", size = 40428, upload-time = "2025-10-08T19:48:35.441Z" }, + { url = "https://files.pythonhosted.org/packages/5b/5a/bc7b4a4ef808fa59a816c17b20c4bef6884daebbdf627ff2a161da67da19/propcache-0.4.1-py3-none-any.whl", hash = "sha256:af2a6052aeb6cf17d3e46ee169099044fd8224cbaf75c76a2ef596e8163e2237", size = 13305, upload-time = "2025-10-08T19:49:00.792Z" }, +] + +[[package]] +name = "pycryptodome" +version = "3.23.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8e/a6/8452177684d5e906854776276ddd34eca30d1b1e15aa1ee9cefc289a33f5/pycryptodome-3.23.0.tar.gz", hash = "sha256:447700a657182d60338bab09fdb27518f8856aecd80ae4c6bdddb67ff5da44ef", size = 4921276, upload-time = "2025-05-17T17:21:45.242Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/5d/bdb09489b63cd34a976cc9e2a8d938114f7a53a74d3dd4f125ffa49dce82/pycryptodome-3.23.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:0011f7f00cdb74879142011f95133274741778abba114ceca229adbf8e62c3e4", size = 2495152, upload-time = "2025-05-17T17:20:20.833Z" }, + { url = "https://files.pythonhosted.org/packages/a7/ce/7840250ed4cc0039c433cd41715536f926d6e86ce84e904068eb3244b6a6/pycryptodome-3.23.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:90460fc9e088ce095f9ee8356722d4f10f86e5be06e2354230a9880b9c549aae", size = 1639348, upload-time = "2025-05-17T17:20:23.171Z" }, + { url = "https://files.pythonhosted.org/packages/ee/f0/991da24c55c1f688d6a3b5a11940567353f74590734ee4a64294834ae472/pycryptodome-3.23.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4764e64b269fc83b00f682c47443c2e6e85b18273712b98aa43bcb77f8570477", size = 2184033, upload-time = "2025-05-17T17:20:25.424Z" }, + { url = "https://files.pythonhosted.org/packages/54/16/0e11882deddf00f68b68dd4e8e442ddc30641f31afeb2bc25588124ac8de/pycryptodome-3.23.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eb8f24adb74984aa0e5d07a2368ad95276cf38051fe2dc6605cbcf482e04f2a7", size = 2270142, upload-time = "2025-05-17T17:20:27.808Z" }, + { url = "https://files.pythonhosted.org/packages/d5/fc/4347fea23a3f95ffb931f383ff28b3f7b1fe868739182cb76718c0da86a1/pycryptodome-3.23.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d97618c9c6684a97ef7637ba43bdf6663a2e2e77efe0f863cce97a76af396446", size = 2309384, upload-time = "2025-05-17T17:20:30.765Z" }, + { url = "https://files.pythonhosted.org/packages/6e/d9/c5261780b69ce66d8cfab25d2797bd6e82ba0241804694cd48be41add5eb/pycryptodome-3.23.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9a53a4fe5cb075075d515797d6ce2f56772ea7e6a1e5e4b96cf78a14bac3d265", size = 2183237, upload-time = "2025-05-17T17:20:33.736Z" }, + { url = "https://files.pythonhosted.org/packages/5a/6f/3af2ffedd5cfa08c631f89452c6648c4d779e7772dfc388c77c920ca6bbf/pycryptodome-3.23.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:763d1d74f56f031788e5d307029caef067febf890cd1f8bf61183ae142f1a77b", size = 2343898, upload-time = "2025-05-17T17:20:36.086Z" }, + { url = "https://files.pythonhosted.org/packages/9a/dc/9060d807039ee5de6e2f260f72f3d70ac213993a804f5e67e0a73a56dd2f/pycryptodome-3.23.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:954af0e2bd7cea83ce72243b14e4fb518b18f0c1649b576d114973e2073b273d", size = 2269197, upload-time = "2025-05-17T17:20:38.414Z" }, + { url = "https://files.pythonhosted.org/packages/f9/34/e6c8ca177cb29dcc4967fef73f5de445912f93bd0343c9c33c8e5bf8cde8/pycryptodome-3.23.0-cp313-cp313t-win32.whl", hash = "sha256:257bb3572c63ad8ba40b89f6fc9d63a2a628e9f9708d31ee26560925ebe0210a", size = 1768600, upload-time = "2025-05-17T17:20:40.688Z" }, + { url = "https://files.pythonhosted.org/packages/e4/1d/89756b8d7ff623ad0160f4539da571d1f594d21ee6d68be130a6eccb39a4/pycryptodome-3.23.0-cp313-cp313t-win_amd64.whl", hash = "sha256:6501790c5b62a29fcb227bd6b62012181d886a767ce9ed03b303d1f22eb5c625", size = 1799740, upload-time = "2025-05-17T17:20:42.413Z" }, + { url = "https://files.pythonhosted.org/packages/5d/61/35a64f0feaea9fd07f0d91209e7be91726eb48c0f1bfc6720647194071e4/pycryptodome-3.23.0-cp313-cp313t-win_arm64.whl", hash = "sha256:9a77627a330ab23ca43b48b130e202582e91cc69619947840ea4d2d1be21eb39", size = 1703685, upload-time = "2025-05-17T17:20:44.388Z" }, + { url = "https://files.pythonhosted.org/packages/db/6c/a1f71542c969912bb0e106f64f60a56cc1f0fabecf9396f45accbe63fa68/pycryptodome-3.23.0-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:187058ab80b3281b1de11c2e6842a357a1f71b42cb1e15bce373f3d238135c27", size = 2495627, upload-time = "2025-05-17T17:20:47.139Z" }, + { url = "https://files.pythonhosted.org/packages/6e/4e/a066527e079fc5002390c8acdd3aca431e6ea0a50ffd7201551175b47323/pycryptodome-3.23.0-cp37-abi3-macosx_10_9_x86_64.whl", hash = "sha256:cfb5cd445280c5b0a4e6187a7ce8de5a07b5f3f897f235caa11f1f435f182843", size = 1640362, upload-time = "2025-05-17T17:20:50.392Z" }, + { url = "https://files.pythonhosted.org/packages/50/52/adaf4c8c100a8c49d2bd058e5b551f73dfd8cb89eb4911e25a0c469b6b4e/pycryptodome-3.23.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:67bd81fcbe34f43ad9422ee8fd4843c8e7198dd88dd3d40e6de42ee65fbe1490", size = 2182625, upload-time = "2025-05-17T17:20:52.866Z" }, + { url = "https://files.pythonhosted.org/packages/5f/e9/a09476d436d0ff1402ac3867d933c61805ec2326c6ea557aeeac3825604e/pycryptodome-3.23.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c8987bd3307a39bc03df5c8e0e3d8be0c4c3518b7f044b0f4c15d1aa78f52575", size = 2268954, upload-time = "2025-05-17T17:20:55.027Z" }, + { url = "https://files.pythonhosted.org/packages/f9/c5/ffe6474e0c551d54cab931918127c46d70cab8f114e0c2b5a3c071c2f484/pycryptodome-3.23.0-cp37-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aa0698f65e5b570426fc31b8162ed4603b0c2841cbb9088e2b01641e3065915b", size = 2308534, upload-time = "2025-05-17T17:20:57.279Z" }, + { url = "https://files.pythonhosted.org/packages/18/28/e199677fc15ecf43010f2463fde4c1a53015d1fe95fb03bca2890836603a/pycryptodome-3.23.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:53ecbafc2b55353edcebd64bf5da94a2a2cdf5090a6915bcca6eca6cc452585a", size = 2181853, upload-time = "2025-05-17T17:20:59.322Z" }, + { url = "https://files.pythonhosted.org/packages/ce/ea/4fdb09f2165ce1365c9eaefef36625583371ee514db58dc9b65d3a255c4c/pycryptodome-3.23.0-cp37-abi3-musllinux_1_2_i686.whl", hash = "sha256:156df9667ad9f2ad26255926524e1c136d6664b741547deb0a86a9acf5ea631f", size = 2342465, upload-time = "2025-05-17T17:21:03.83Z" }, + { url = "https://files.pythonhosted.org/packages/22/82/6edc3fc42fe9284aead511394bac167693fb2b0e0395b28b8bedaa07ef04/pycryptodome-3.23.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:dea827b4d55ee390dc89b2afe5927d4308a8b538ae91d9c6f7a5090f397af1aa", size = 2267414, upload-time = "2025-05-17T17:21:06.72Z" }, + { url = "https://files.pythonhosted.org/packages/59/fe/aae679b64363eb78326c7fdc9d06ec3de18bac68be4b612fc1fe8902693c/pycryptodome-3.23.0-cp37-abi3-win32.whl", hash = "sha256:507dbead45474b62b2bbe318eb1c4c8ee641077532067fec9c1aa82c31f84886", size = 1768484, upload-time = "2025-05-17T17:21:08.535Z" }, + { url = "https://files.pythonhosted.org/packages/54/2f/e97a1b8294db0daaa87012c24a7bb714147c7ade7656973fd6c736b484ff/pycryptodome-3.23.0-cp37-abi3-win_amd64.whl", hash = "sha256:c75b52aacc6c0c260f204cbdd834f76edc9fb0d8e0da9fbf8352ef58202564e2", size = 1799636, upload-time = "2025-05-17T17:21:10.393Z" }, + { url = "https://files.pythonhosted.org/packages/18/3d/f9441a0d798bf2b1e645adc3265e55706aead1255ccdad3856dbdcffec14/pycryptodome-3.23.0-cp37-abi3-win_arm64.whl", hash = "sha256:11eeeb6917903876f134b56ba11abe95c0b0fd5e3330def218083c7d98bbcb3c", size = 1703675, upload-time = "2025-05-17T17:21:13.146Z" }, + { url = "https://files.pythonhosted.org/packages/d9/12/e33935a0709c07de084d7d58d330ec3f4daf7910a18e77937affdb728452/pycryptodome-3.23.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:ddb95b49df036ddd264a0ad246d1be5b672000f12d6961ea2c267083a5e19379", size = 1623886, upload-time = "2025-05-17T17:21:20.614Z" }, + { url = "https://files.pythonhosted.org/packages/22/0b/aa8f9419f25870889bebf0b26b223c6986652bdf071f000623df11212c90/pycryptodome-3.23.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8e95564beb8782abfd9e431c974e14563a794a4944c29d6d3b7b5ea042110b4", size = 1672151, upload-time = "2025-05-17T17:21:22.666Z" }, + { url = "https://files.pythonhosted.org/packages/d4/5e/63f5cbde2342b7f70a39e591dbe75d9809d6338ce0b07c10406f1a140cdc/pycryptodome-3.23.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:14e15c081e912c4b0d75632acd8382dfce45b258667aa3c67caf7a4d4c13f630", size = 1664461, upload-time = "2025-05-17T17:21:25.225Z" }, + { url = "https://files.pythonhosted.org/packages/d6/92/608fbdad566ebe499297a86aae5f2a5263818ceeecd16733006f1600403c/pycryptodome-3.23.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a7fc76bf273353dc7e5207d172b83f569540fc9a28d63171061c42e361d22353", size = 1702440, upload-time = "2025-05-17T17:21:27.991Z" }, + { url = "https://files.pythonhosted.org/packages/d1/92/2eadd1341abd2989cce2e2740b4423608ee2014acb8110438244ee97d7ff/pycryptodome-3.23.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:45c69ad715ca1a94f778215a11e66b7ff989d792a4d63b68dc586a1da1392ff5", size = 1803005, upload-time = "2025-05-17T17:21:31.37Z" }, +] + +[[package]] +name = "pydantic" +version = "2.12.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/69/44/36f1a6e523abc58ae5f928898e4aca2e0ea509b5aa6f6f392a5d882be928/pydantic-2.12.5.tar.gz", hash = "sha256:4d351024c75c0f085a9febbb665ce8c0c6ec5d30e903bdb6394b7ede26aebb49", size = 821591, upload-time = "2025-11-26T15:11:46.471Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl", hash = "sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d", size = 463580, upload-time = "2025-11-26T15:11:44.605Z" }, +] + +[package.optional-dependencies] +email = [ + { name = "email-validator" }, +] + +[[package]] +name = "pydantic-core" +version = "2.41.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/71/70/23b021c950c2addd24ec408e9ab05d59b035b39d97cdc1130e1bce647bb6/pydantic_core-2.41.5.tar.gz", hash = "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e", size = 460952, upload-time = "2025-11-04T13:43:49.098Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c6/90/32c9941e728d564b411d574d8ee0cf09b12ec978cb22b294995bae5549a5/pydantic_core-2.41.5-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:77b63866ca88d804225eaa4af3e664c5faf3568cea95360d21f4725ab6e07146", size = 2107298, upload-time = "2025-11-04T13:39:04.116Z" }, + { url = "https://files.pythonhosted.org/packages/fb/a8/61c96a77fe28993d9a6fb0f4127e05430a267b235a124545d79fea46dd65/pydantic_core-2.41.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:dfa8a0c812ac681395907e71e1274819dec685fec28273a28905df579ef137e2", size = 1901475, upload-time = "2025-11-04T13:39:06.055Z" }, + { url = "https://files.pythonhosted.org/packages/5d/b6/338abf60225acc18cdc08b4faef592d0310923d19a87fba1faf05af5346e/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5921a4d3ca3aee735d9fd163808f5e8dd6c6972101e4adbda9a4667908849b97", size = 1918815, upload-time = "2025-11-04T13:39:10.41Z" }, + { url = "https://files.pythonhosted.org/packages/d1/1c/2ed0433e682983d8e8cba9c8d8ef274d4791ec6a6f24c58935b90e780e0a/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e25c479382d26a2a41b7ebea1043564a937db462816ea07afa8a44c0866d52f9", size = 2065567, upload-time = "2025-11-04T13:39:12.244Z" }, + { url = "https://files.pythonhosted.org/packages/b3/24/cf84974ee7d6eae06b9e63289b7b8f6549d416b5c199ca2d7ce13bbcf619/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f547144f2966e1e16ae626d8ce72b4cfa0caedc7fa28052001c94fb2fcaa1c52", size = 2230442, upload-time = "2025-11-04T13:39:13.962Z" }, + { url = "https://files.pythonhosted.org/packages/fd/21/4e287865504b3edc0136c89c9c09431be326168b1eb7841911cbc877a995/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6f52298fbd394f9ed112d56f3d11aabd0d5bd27beb3084cc3d8ad069483b8941", size = 2350956, upload-time = "2025-11-04T13:39:15.889Z" }, + { url = "https://files.pythonhosted.org/packages/a8/76/7727ef2ffa4b62fcab916686a68a0426b9b790139720e1934e8ba797e238/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:100baa204bb412b74fe285fb0f3a385256dad1d1879f0a5cb1499ed2e83d132a", size = 2068253, upload-time = "2025-11-04T13:39:17.403Z" }, + { url = "https://files.pythonhosted.org/packages/d5/8c/a4abfc79604bcb4c748e18975c44f94f756f08fb04218d5cb87eb0d3a63e/pydantic_core-2.41.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:05a2c8852530ad2812cb7914dc61a1125dc4e06252ee98e5638a12da6cc6fb6c", size = 2177050, upload-time = "2025-11-04T13:39:19.351Z" }, + { url = "https://files.pythonhosted.org/packages/67/b1/de2e9a9a79b480f9cb0b6e8b6ba4c50b18d4e89852426364c66aa82bb7b3/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:29452c56df2ed968d18d7e21f4ab0ac55e71dc59524872f6fc57dcf4a3249ed2", size = 2147178, upload-time = "2025-11-04T13:39:21Z" }, + { url = "https://files.pythonhosted.org/packages/16/c1/dfb33f837a47b20417500efaa0378adc6635b3c79e8369ff7a03c494b4ac/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:d5160812ea7a8a2ffbe233d8da666880cad0cbaf5d4de74ae15c313213d62556", size = 2341833, upload-time = "2025-11-04T13:39:22.606Z" }, + { url = "https://files.pythonhosted.org/packages/47/36/00f398642a0f4b815a9a558c4f1dca1b4020a7d49562807d7bc9ff279a6c/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:df3959765b553b9440adfd3c795617c352154e497a4eaf3752555cfb5da8fc49", size = 2321156, upload-time = "2025-11-04T13:39:25.843Z" }, + { url = "https://files.pythonhosted.org/packages/7e/70/cad3acd89fde2010807354d978725ae111ddf6d0ea46d1ea1775b5c1bd0c/pydantic_core-2.41.5-cp310-cp310-win32.whl", hash = "sha256:1f8d33a7f4d5a7889e60dc39856d76d09333d8a6ed0f5f1190635cbec70ec4ba", size = 1989378, upload-time = "2025-11-04T13:39:27.92Z" }, + { url = "https://files.pythonhosted.org/packages/76/92/d338652464c6c367e5608e4488201702cd1cbb0f33f7b6a85a60fe5f3720/pydantic_core-2.41.5-cp310-cp310-win_amd64.whl", hash = "sha256:62de39db01b8d593e45871af2af9e497295db8d73b085f6bfd0b18c83c70a8f9", size = 2013622, upload-time = "2025-11-04T13:39:29.848Z" }, + { url = "https://files.pythonhosted.org/packages/e8/72/74a989dd9f2084b3d9530b0915fdda64ac48831c30dbf7c72a41a5232db8/pydantic_core-2.41.5-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a3a52f6156e73e7ccb0f8cced536adccb7042be67cb45f9562e12b319c119da6", size = 2105873, upload-time = "2025-11-04T13:39:31.373Z" }, + { url = "https://files.pythonhosted.org/packages/12/44/37e403fd9455708b3b942949e1d7febc02167662bf1a7da5b78ee1ea2842/pydantic_core-2.41.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7f3bf998340c6d4b0c9a2f02d6a400e51f123b59565d74dc60d252ce888c260b", size = 1899826, upload-time = "2025-11-04T13:39:32.897Z" }, + { url = "https://files.pythonhosted.org/packages/33/7f/1d5cab3ccf44c1935a359d51a8a2a9e1a654b744b5e7f80d41b88d501eec/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:378bec5c66998815d224c9ca994f1e14c0c21cb95d2f52b6021cc0b2a58f2a5a", size = 1917869, upload-time = "2025-11-04T13:39:34.469Z" }, + { url = "https://files.pythonhosted.org/packages/6e/6a/30d94a9674a7fe4f4744052ed6c5e083424510be1e93da5bc47569d11810/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e7b576130c69225432866fe2f4a469a85a54ade141d96fd396dffcf607b558f8", size = 2063890, upload-time = "2025-11-04T13:39:36.053Z" }, + { url = "https://files.pythonhosted.org/packages/50/be/76e5d46203fcb2750e542f32e6c371ffa9b8ad17364cf94bb0818dbfb50c/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6cb58b9c66f7e4179a2d5e0f849c48eff5c1fca560994d6eb6543abf955a149e", size = 2229740, upload-time = "2025-11-04T13:39:37.753Z" }, + { url = "https://files.pythonhosted.org/packages/d3/ee/fed784df0144793489f87db310a6bbf8118d7b630ed07aa180d6067e653a/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:88942d3a3dff3afc8288c21e565e476fc278902ae4d6d134f1eeda118cc830b1", size = 2350021, upload-time = "2025-11-04T13:39:40.94Z" }, + { url = "https://files.pythonhosted.org/packages/c8/be/8fed28dd0a180dca19e72c233cbf58efa36df055e5b9d90d64fd1740b828/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f31d95a179f8d64d90f6831d71fa93290893a33148d890ba15de25642c5d075b", size = 2066378, upload-time = "2025-11-04T13:39:42.523Z" }, + { url = "https://files.pythonhosted.org/packages/b0/3b/698cf8ae1d536a010e05121b4958b1257f0b5522085e335360e53a6b1c8b/pydantic_core-2.41.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c1df3d34aced70add6f867a8cf413e299177e0c22660cc767218373d0779487b", size = 2175761, upload-time = "2025-11-04T13:39:44.553Z" }, + { url = "https://files.pythonhosted.org/packages/b8/ba/15d537423939553116dea94ce02f9c31be0fa9d0b806d427e0308ec17145/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:4009935984bd36bd2c774e13f9a09563ce8de4abaa7226f5108262fa3e637284", size = 2146303, upload-time = "2025-11-04T13:39:46.238Z" }, + { url = "https://files.pythonhosted.org/packages/58/7f/0de669bf37d206723795f9c90c82966726a2ab06c336deba4735b55af431/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:34a64bc3441dc1213096a20fe27e8e128bd3ff89921706e83c0b1ac971276594", size = 2340355, upload-time = "2025-11-04T13:39:48.002Z" }, + { url = "https://files.pythonhosted.org/packages/e5/de/e7482c435b83d7e3c3ee5ee4451f6e8973cff0eb6007d2872ce6383f6398/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c9e19dd6e28fdcaa5a1de679aec4141f691023916427ef9bae8584f9c2fb3b0e", size = 2319875, upload-time = "2025-11-04T13:39:49.705Z" }, + { url = "https://files.pythonhosted.org/packages/fe/e6/8c9e81bb6dd7560e33b9053351c29f30c8194b72f2d6932888581f503482/pydantic_core-2.41.5-cp311-cp311-win32.whl", hash = "sha256:2c010c6ded393148374c0f6f0bf89d206bf3217f201faa0635dcd56bd1520f6b", size = 1987549, upload-time = "2025-11-04T13:39:51.842Z" }, + { url = "https://files.pythonhosted.org/packages/11/66/f14d1d978ea94d1bc21fc98fcf570f9542fe55bfcc40269d4e1a21c19bf7/pydantic_core-2.41.5-cp311-cp311-win_amd64.whl", hash = "sha256:76ee27c6e9c7f16f47db7a94157112a2f3a00e958bc626e2f4ee8bec5c328fbe", size = 2011305, upload-time = "2025-11-04T13:39:53.485Z" }, + { url = "https://files.pythonhosted.org/packages/56/d8/0e271434e8efd03186c5386671328154ee349ff0354d83c74f5caaf096ed/pydantic_core-2.41.5-cp311-cp311-win_arm64.whl", hash = "sha256:4bc36bbc0b7584de96561184ad7f012478987882ebf9f9c389b23f432ea3d90f", size = 1972902, upload-time = "2025-11-04T13:39:56.488Z" }, + { url = "https://files.pythonhosted.org/packages/5f/5d/5f6c63eebb5afee93bcaae4ce9a898f3373ca23df3ccaef086d0233a35a7/pydantic_core-2.41.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f41a7489d32336dbf2199c8c0a215390a751c5b014c2c1c5366e817202e9cdf7", size = 2110990, upload-time = "2025-11-04T13:39:58.079Z" }, + { url = "https://files.pythonhosted.org/packages/aa/32/9c2e8ccb57c01111e0fd091f236c7b371c1bccea0fa85247ac55b1e2b6b6/pydantic_core-2.41.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:070259a8818988b9a84a449a2a7337c7f430a22acc0859c6b110aa7212a6d9c0", size = 1896003, upload-time = "2025-11-04T13:39:59.956Z" }, + { url = "https://files.pythonhosted.org/packages/68/b8/a01b53cb0e59139fbc9e4fda3e9724ede8de279097179be4ff31f1abb65a/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e96cea19e34778f8d59fe40775a7a574d95816eb150850a85a7a4c8f4b94ac69", size = 1919200, upload-time = "2025-11-04T13:40:02.241Z" }, + { url = "https://files.pythonhosted.org/packages/38/de/8c36b5198a29bdaade07b5985e80a233a5ac27137846f3bc2d3b40a47360/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed2e99c456e3fadd05c991f8f437ef902e00eedf34320ba2b0842bd1c3ca3a75", size = 2052578, upload-time = "2025-11-04T13:40:04.401Z" }, + { url = "https://files.pythonhosted.org/packages/00/b5/0e8e4b5b081eac6cb3dbb7e60a65907549a1ce035a724368c330112adfdd/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:65840751b72fbfd82c3c640cff9284545342a4f1eb1586ad0636955b261b0b05", size = 2208504, upload-time = "2025-11-04T13:40:06.072Z" }, + { url = "https://files.pythonhosted.org/packages/77/56/87a61aad59c7c5b9dc8caad5a41a5545cba3810c3e828708b3d7404f6cef/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e536c98a7626a98feb2d3eaf75944ef6f3dbee447e1f841eae16f2f0a72d8ddc", size = 2335816, upload-time = "2025-11-04T13:40:07.835Z" }, + { url = "https://files.pythonhosted.org/packages/0d/76/941cc9f73529988688a665a5c0ecff1112b3d95ab48f81db5f7606f522d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eceb81a8d74f9267ef4081e246ffd6d129da5d87e37a77c9bde550cb04870c1c", size = 2075366, upload-time = "2025-11-04T13:40:09.804Z" }, + { url = "https://files.pythonhosted.org/packages/d3/43/ebef01f69baa07a482844faaa0a591bad1ef129253ffd0cdaa9d8a7f72d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d38548150c39b74aeeb0ce8ee1d8e82696f4a4e16ddc6de7b1d8823f7de4b9b5", size = 2171698, upload-time = "2025-11-04T13:40:12.004Z" }, + { url = "https://files.pythonhosted.org/packages/b1/87/41f3202e4193e3bacfc2c065fab7706ebe81af46a83d3e27605029c1f5a6/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c23e27686783f60290e36827f9c626e63154b82b116d7fe9adba1fda36da706c", size = 2132603, upload-time = "2025-11-04T13:40:13.868Z" }, + { url = "https://files.pythonhosted.org/packages/49/7d/4c00df99cb12070b6bccdef4a195255e6020a550d572768d92cc54dba91a/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:482c982f814460eabe1d3bb0adfdc583387bd4691ef00b90575ca0d2b6fe2294", size = 2329591, upload-time = "2025-11-04T13:40:15.672Z" }, + { url = "https://files.pythonhosted.org/packages/cc/6a/ebf4b1d65d458f3cda6a7335d141305dfa19bdc61140a884d165a8a1bbc7/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:bfea2a5f0b4d8d43adf9d7b8bf019fb46fdd10a2e5cde477fbcb9d1fa08c68e1", size = 2319068, upload-time = "2025-11-04T13:40:17.532Z" }, + { url = "https://files.pythonhosted.org/packages/49/3b/774f2b5cd4192d5ab75870ce4381fd89cf218af999515baf07e7206753f0/pydantic_core-2.41.5-cp312-cp312-win32.whl", hash = "sha256:b74557b16e390ec12dca509bce9264c3bbd128f8a2c376eaa68003d7f327276d", size = 1985908, upload-time = "2025-11-04T13:40:19.309Z" }, + { url = "https://files.pythonhosted.org/packages/86/45/00173a033c801cacf67c190fef088789394feaf88a98a7035b0e40d53dc9/pydantic_core-2.41.5-cp312-cp312-win_amd64.whl", hash = "sha256:1962293292865bca8e54702b08a4f26da73adc83dd1fcf26fbc875b35d81c815", size = 2020145, upload-time = "2025-11-04T13:40:21.548Z" }, + { url = "https://files.pythonhosted.org/packages/f9/22/91fbc821fa6d261b376a3f73809f907cec5ca6025642c463d3488aad22fb/pydantic_core-2.41.5-cp312-cp312-win_arm64.whl", hash = "sha256:1746d4a3d9a794cacae06a5eaaccb4b8643a131d45fbc9af23e353dc0a5ba5c3", size = 1976179, upload-time = "2025-11-04T13:40:23.393Z" }, + { url = "https://files.pythonhosted.org/packages/87/06/8806241ff1f70d9939f9af039c6c35f2360cf16e93c2ca76f184e76b1564/pydantic_core-2.41.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:941103c9be18ac8daf7b7adca8228f8ed6bb7a1849020f643b3a14d15b1924d9", size = 2120403, upload-time = "2025-11-04T13:40:25.248Z" }, + { url = "https://files.pythonhosted.org/packages/94/02/abfa0e0bda67faa65fef1c84971c7e45928e108fe24333c81f3bfe35d5f5/pydantic_core-2.41.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:112e305c3314f40c93998e567879e887a3160bb8689ef3d2c04b6cc62c33ac34", size = 1896206, upload-time = "2025-11-04T13:40:27.099Z" }, + { url = "https://files.pythonhosted.org/packages/15/df/a4c740c0943e93e6500f9eb23f4ca7ec9bf71b19e608ae5b579678c8d02f/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cbaad15cb0c90aa221d43c00e77bb33c93e8d36e0bf74760cd00e732d10a6a0", size = 1919307, upload-time = "2025-11-04T13:40:29.806Z" }, + { url = "https://files.pythonhosted.org/packages/9a/e3/6324802931ae1d123528988e0e86587c2072ac2e5394b4bc2bc34b61ff6e/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:03ca43e12fab6023fc79d28ca6b39b05f794ad08ec2feccc59a339b02f2b3d33", size = 2063258, upload-time = "2025-11-04T13:40:33.544Z" }, + { url = "https://files.pythonhosted.org/packages/c9/d4/2230d7151d4957dd79c3044ea26346c148c98fbf0ee6ebd41056f2d62ab5/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc799088c08fa04e43144b164feb0c13f9a0bc40503f8df3e9fde58a3c0c101e", size = 2214917, upload-time = "2025-11-04T13:40:35.479Z" }, + { url = "https://files.pythonhosted.org/packages/e6/9f/eaac5df17a3672fef0081b6c1bb0b82b33ee89aa5cec0d7b05f52fd4a1fa/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:97aeba56665b4c3235a0e52b2c2f5ae9cd071b8a8310ad27bddb3f7fb30e9aa2", size = 2332186, upload-time = "2025-11-04T13:40:37.436Z" }, + { url = "https://files.pythonhosted.org/packages/cf/4e/35a80cae583a37cf15604b44240e45c05e04e86f9cfd766623149297e971/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:406bf18d345822d6c21366031003612b9c77b3e29ffdb0f612367352aab7d586", size = 2073164, upload-time = "2025-11-04T13:40:40.289Z" }, + { url = "https://files.pythonhosted.org/packages/bf/e3/f6e262673c6140dd3305d144d032f7bd5f7497d3871c1428521f19f9efa2/pydantic_core-2.41.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b93590ae81f7010dbe380cdeab6f515902ebcbefe0b9327cc4804d74e93ae69d", size = 2179146, upload-time = "2025-11-04T13:40:42.809Z" }, + { url = "https://files.pythonhosted.org/packages/75/c7/20bd7fc05f0c6ea2056a4565c6f36f8968c0924f19b7d97bbfea55780e73/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:01a3d0ab748ee531f4ea6c3e48ad9dac84ddba4b0d82291f87248f2f9de8d740", size = 2137788, upload-time = "2025-11-04T13:40:44.752Z" }, + { url = "https://files.pythonhosted.org/packages/3a/8d/34318ef985c45196e004bc46c6eab2eda437e744c124ef0dbe1ff2c9d06b/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:6561e94ba9dacc9c61bce40e2d6bdc3bfaa0259d3ff36ace3b1e6901936d2e3e", size = 2340133, upload-time = "2025-11-04T13:40:46.66Z" }, + { url = "https://files.pythonhosted.org/packages/9c/59/013626bf8c78a5a5d9350d12e7697d3d4de951a75565496abd40ccd46bee/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:915c3d10f81bec3a74fbd4faebe8391013ba61e5a1a8d48c4455b923bdda7858", size = 2324852, upload-time = "2025-11-04T13:40:48.575Z" }, + { url = "https://files.pythonhosted.org/packages/1a/d9/c248c103856f807ef70c18a4f986693a46a8ffe1602e5d361485da502d20/pydantic_core-2.41.5-cp313-cp313-win32.whl", hash = "sha256:650ae77860b45cfa6e2cdafc42618ceafab3a2d9a3811fcfbd3bbf8ac3c40d36", size = 1994679, upload-time = "2025-11-04T13:40:50.619Z" }, + { url = "https://files.pythonhosted.org/packages/9e/8b/341991b158ddab181cff136acd2552c9f35bd30380422a639c0671e99a91/pydantic_core-2.41.5-cp313-cp313-win_amd64.whl", hash = "sha256:79ec52ec461e99e13791ec6508c722742ad745571f234ea6255bed38c6480f11", size = 2019766, upload-time = "2025-11-04T13:40:52.631Z" }, + { url = "https://files.pythonhosted.org/packages/73/7d/f2f9db34af103bea3e09735bb40b021788a5e834c81eedb541991badf8f5/pydantic_core-2.41.5-cp313-cp313-win_arm64.whl", hash = "sha256:3f84d5c1b4ab906093bdc1ff10484838aca54ef08de4afa9de0f5f14d69639cd", size = 1981005, upload-time = "2025-11-04T13:40:54.734Z" }, + { url = "https://files.pythonhosted.org/packages/ea/28/46b7c5c9635ae96ea0fbb779e271a38129df2550f763937659ee6c5dbc65/pydantic_core-2.41.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:3f37a19d7ebcdd20b96485056ba9e8b304e27d9904d233d7b1015db320e51f0a", size = 2119622, upload-time = "2025-11-04T13:40:56.68Z" }, + { url = "https://files.pythonhosted.org/packages/74/1a/145646e5687e8d9a1e8d09acb278c8535ebe9e972e1f162ed338a622f193/pydantic_core-2.41.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1d1d9764366c73f996edd17abb6d9d7649a7eb690006ab6adbda117717099b14", size = 1891725, upload-time = "2025-11-04T13:40:58.807Z" }, + { url = "https://files.pythonhosted.org/packages/23/04/e89c29e267b8060b40dca97bfc64a19b2a3cf99018167ea1677d96368273/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25e1c2af0fce638d5f1988b686f3b3ea8cd7de5f244ca147c777769e798a9cd1", size = 1915040, upload-time = "2025-11-04T13:41:00.853Z" }, + { url = "https://files.pythonhosted.org/packages/84/a3/15a82ac7bd97992a82257f777b3583d3e84bdb06ba6858f745daa2ec8a85/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:506d766a8727beef16b7adaeb8ee6217c64fc813646b424d0804d67c16eddb66", size = 2063691, upload-time = "2025-11-04T13:41:03.504Z" }, + { url = "https://files.pythonhosted.org/packages/74/9b/0046701313c6ef08c0c1cf0e028c67c770a4e1275ca73131563c5f2a310a/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4819fa52133c9aa3c387b3328f25c1facc356491e6135b459f1de698ff64d869", size = 2213897, upload-time = "2025-11-04T13:41:05.804Z" }, + { url = "https://files.pythonhosted.org/packages/8a/cd/6bac76ecd1b27e75a95ca3a9a559c643b3afcd2dd62086d4b7a32a18b169/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b761d210c9ea91feda40d25b4efe82a1707da2ef62901466a42492c028553a2", size = 2333302, upload-time = "2025-11-04T13:41:07.809Z" }, + { url = "https://files.pythonhosted.org/packages/4c/d2/ef2074dc020dd6e109611a8be4449b98cd25e1b9b8a303c2f0fca2f2bcf7/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22f0fb8c1c583a3b6f24df2470833b40207e907b90c928cc8d3594b76f874375", size = 2064877, upload-time = "2025-11-04T13:41:09.827Z" }, + { url = "https://files.pythonhosted.org/packages/18/66/e9db17a9a763d72f03de903883c057b2592c09509ccfe468187f2a2eef29/pydantic_core-2.41.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2782c870e99878c634505236d81e5443092fba820f0373997ff75f90f68cd553", size = 2180680, upload-time = "2025-11-04T13:41:12.379Z" }, + { url = "https://files.pythonhosted.org/packages/d3/9e/3ce66cebb929f3ced22be85d4c2399b8e85b622db77dad36b73c5387f8f8/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:0177272f88ab8312479336e1d777f6b124537d47f2123f89cb37e0accea97f90", size = 2138960, upload-time = "2025-11-04T13:41:14.627Z" }, + { url = "https://files.pythonhosted.org/packages/a6/62/205a998f4327d2079326b01abee48e502ea739d174f0a89295c481a2272e/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:63510af5e38f8955b8ee5687740d6ebf7c2a0886d15a6d65c32814613681bc07", size = 2339102, upload-time = "2025-11-04T13:41:16.868Z" }, + { url = "https://files.pythonhosted.org/packages/3c/0d/f05e79471e889d74d3d88f5bd20d0ed189ad94c2423d81ff8d0000aab4ff/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:e56ba91f47764cc14f1daacd723e3e82d1a89d783f0f5afe9c364b8bb491ccdb", size = 2326039, upload-time = "2025-11-04T13:41:18.934Z" }, + { url = "https://files.pythonhosted.org/packages/ec/e1/e08a6208bb100da7e0c4b288eed624a703f4d129bde2da475721a80cab32/pydantic_core-2.41.5-cp314-cp314-win32.whl", hash = "sha256:aec5cf2fd867b4ff45b9959f8b20ea3993fc93e63c7363fe6851424c8a7e7c23", size = 1995126, upload-time = "2025-11-04T13:41:21.418Z" }, + { url = "https://files.pythonhosted.org/packages/48/5d/56ba7b24e9557f99c9237e29f5c09913c81eeb2f3217e40e922353668092/pydantic_core-2.41.5-cp314-cp314-win_amd64.whl", hash = "sha256:8e7c86f27c585ef37c35e56a96363ab8de4e549a95512445b85c96d3e2f7c1bf", size = 2015489, upload-time = "2025-11-04T13:41:24.076Z" }, + { url = "https://files.pythonhosted.org/packages/4e/bb/f7a190991ec9e3e0ba22e4993d8755bbc4a32925c0b5b42775c03e8148f9/pydantic_core-2.41.5-cp314-cp314-win_arm64.whl", hash = "sha256:e672ba74fbc2dc8eea59fb6d4aed6845e6905fc2a8afe93175d94a83ba2a01a0", size = 1977288, upload-time = "2025-11-04T13:41:26.33Z" }, + { url = "https://files.pythonhosted.org/packages/92/ed/77542d0c51538e32e15afe7899d79efce4b81eee631d99850edc2f5e9349/pydantic_core-2.41.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8566def80554c3faa0e65ac30ab0932b9e3a5cd7f8323764303d468e5c37595a", size = 2120255, upload-time = "2025-11-04T13:41:28.569Z" }, + { url = "https://files.pythonhosted.org/packages/bb/3d/6913dde84d5be21e284439676168b28d8bbba5600d838b9dca99de0fad71/pydantic_core-2.41.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b80aa5095cd3109962a298ce14110ae16b8c1aece8b72f9dafe81cf597ad80b3", size = 1863760, upload-time = "2025-11-04T13:41:31.055Z" }, + { url = "https://files.pythonhosted.org/packages/5a/f0/e5e6b99d4191da102f2b0eb9687aaa7f5bea5d9964071a84effc3e40f997/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3006c3dd9ba34b0c094c544c6006cc79e87d8612999f1a5d43b769b89181f23c", size = 1878092, upload-time = "2025-11-04T13:41:33.21Z" }, + { url = "https://files.pythonhosted.org/packages/71/48/36fb760642d568925953bcc8116455513d6e34c4beaa37544118c36aba6d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72f6c8b11857a856bcfa48c86f5368439f74453563f951e473514579d44aa612", size = 2053385, upload-time = "2025-11-04T13:41:35.508Z" }, + { url = "https://files.pythonhosted.org/packages/20/25/92dc684dd8eb75a234bc1c764b4210cf2646479d54b47bf46061657292a8/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cb1b2f9742240e4bb26b652a5aeb840aa4b417c7748b6f8387927bc6e45e40d", size = 2218832, upload-time = "2025-11-04T13:41:37.732Z" }, + { url = "https://files.pythonhosted.org/packages/e2/09/f53e0b05023d3e30357d82eb35835d0f6340ca344720a4599cd663dca599/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd3d54f38609ff308209bd43acea66061494157703364ae40c951f83ba99a1a9", size = 2327585, upload-time = "2025-11-04T13:41:40Z" }, + { url = "https://files.pythonhosted.org/packages/aa/4e/2ae1aa85d6af35a39b236b1b1641de73f5a6ac4d5a7509f77b814885760c/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ff4321e56e879ee8d2a879501c8e469414d948f4aba74a2d4593184eb326660", size = 2041078, upload-time = "2025-11-04T13:41:42.323Z" }, + { url = "https://files.pythonhosted.org/packages/cd/13/2e215f17f0ef326fc72afe94776edb77525142c693767fc347ed6288728d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0d2568a8c11bf8225044aa94409e21da0cb09dcdafe9ecd10250b2baad531a9", size = 2173914, upload-time = "2025-11-04T13:41:45.221Z" }, + { url = "https://files.pythonhosted.org/packages/02/7a/f999a6dcbcd0e5660bc348a3991c8915ce6599f4f2c6ac22f01d7a10816c/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:a39455728aabd58ceabb03c90e12f71fd30fa69615760a075b9fec596456ccc3", size = 2129560, upload-time = "2025-11-04T13:41:47.474Z" }, + { url = "https://files.pythonhosted.org/packages/3a/b1/6c990ac65e3b4c079a4fb9f5b05f5b013afa0f4ed6780a3dd236d2cbdc64/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:239edca560d05757817c13dc17c50766136d21f7cd0fac50295499ae24f90fdf", size = 2329244, upload-time = "2025-11-04T13:41:49.992Z" }, + { url = "https://files.pythonhosted.org/packages/d9/02/3c562f3a51afd4d88fff8dffb1771b30cfdfd79befd9883ee094f5b6c0d8/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:2a5e06546e19f24c6a96a129142a75cee553cc018ffee48a460059b1185f4470", size = 2331955, upload-time = "2025-11-04T13:41:54.079Z" }, + { url = "https://files.pythonhosted.org/packages/5c/96/5fb7d8c3c17bc8c62fdb031c47d77a1af698f1d7a406b0f79aaa1338f9ad/pydantic_core-2.41.5-cp314-cp314t-win32.whl", hash = "sha256:b4ececa40ac28afa90871c2cc2b9ffd2ff0bf749380fbdf57d165fd23da353aa", size = 1988906, upload-time = "2025-11-04T13:41:56.606Z" }, + { url = "https://files.pythonhosted.org/packages/22/ed/182129d83032702912c2e2d8bbe33c036f342cc735737064668585dac28f/pydantic_core-2.41.5-cp314-cp314t-win_amd64.whl", hash = "sha256:80aa89cad80b32a912a65332f64a4450ed00966111b6615ca6816153d3585a8c", size = 1981607, upload-time = "2025-11-04T13:41:58.889Z" }, + { url = "https://files.pythonhosted.org/packages/9f/ed/068e41660b832bb0b1aa5b58011dea2a3fe0ba7861ff38c4d4904c1c1a99/pydantic_core-2.41.5-cp314-cp314t-win_arm64.whl", hash = "sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008", size = 1974769, upload-time = "2025-11-04T13:42:01.186Z" }, + { url = "https://files.pythonhosted.org/packages/11/72/90fda5ee3b97e51c494938a4a44c3a35a9c96c19bba12372fb9c634d6f57/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:b96d5f26b05d03cc60f11a7761a5ded1741da411e7fe0909e27a5e6a0cb7b034", size = 2115441, upload-time = "2025-11-04T13:42:39.557Z" }, + { url = "https://files.pythonhosted.org/packages/1f/53/8942f884fa33f50794f119012dc6a1a02ac43a56407adaac20463df8e98f/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:634e8609e89ceecea15e2d61bc9ac3718caaaa71963717bf3c8f38bfde64242c", size = 1930291, upload-time = "2025-11-04T13:42:42.169Z" }, + { url = "https://files.pythonhosted.org/packages/79/c8/ecb9ed9cd942bce09fc888ee960b52654fbdbede4ba6c2d6e0d3b1d8b49c/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:93e8740d7503eb008aa2df04d3b9735f845d43ae845e6dcd2be0b55a2da43cd2", size = 1948632, upload-time = "2025-11-04T13:42:44.564Z" }, + { url = "https://files.pythonhosted.org/packages/2e/1b/687711069de7efa6af934e74f601e2a4307365e8fdc404703afc453eab26/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f15489ba13d61f670dcc96772e733aad1a6f9c429cc27574c6cdaed82d0146ad", size = 2138905, upload-time = "2025-11-04T13:42:47.156Z" }, + { url = "https://files.pythonhosted.org/packages/09/32/59b0c7e63e277fa7911c2fc70ccfb45ce4b98991e7ef37110663437005af/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:7da7087d756b19037bc2c06edc6c170eeef3c3bafcb8f532ff17d64dc427adfd", size = 2110495, upload-time = "2025-11-04T13:42:49.689Z" }, + { url = "https://files.pythonhosted.org/packages/aa/81/05e400037eaf55ad400bcd318c05bb345b57e708887f07ddb2d20e3f0e98/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:aabf5777b5c8ca26f7824cb4a120a740c9588ed58df9b2d196ce92fba42ff8dc", size = 1915388, upload-time = "2025-11-04T13:42:52.215Z" }, + { url = "https://files.pythonhosted.org/packages/6e/0d/e3549b2399f71d56476b77dbf3cf8937cec5cd70536bdc0e374a421d0599/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c007fe8a43d43b3969e8469004e9845944f1a80e6acd47c150856bb87f230c56", size = 1942879, upload-time = "2025-11-04T13:42:56.483Z" }, + { url = "https://files.pythonhosted.org/packages/f7/07/34573da085946b6a313d7c42f82f16e8920bfd730665de2d11c0c37a74b5/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76d0819de158cd855d1cbb8fcafdf6f5cf1eb8e470abe056d5d161106e38062b", size = 2139017, upload-time = "2025-11-04T13:42:59.471Z" }, + { url = "https://files.pythonhosted.org/packages/e6/b0/1a2aa41e3b5a4ba11420aba2d091b2d17959c8d1519ece3627c371951e73/pydantic_core-2.41.5-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b5819cd790dbf0c5eb9f82c73c16b39a65dd6dd4d1439dcdea7816ec9adddab8", size = 2103351, upload-time = "2025-11-04T13:43:02.058Z" }, + { url = "https://files.pythonhosted.org/packages/a4/ee/31b1f0020baaf6d091c87900ae05c6aeae101fa4e188e1613c80e4f1ea31/pydantic_core-2.41.5-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:5a4e67afbc95fa5c34cf27d9089bca7fcab4e51e57278d710320a70b956d1b9a", size = 1925363, upload-time = "2025-11-04T13:43:05.159Z" }, + { url = "https://files.pythonhosted.org/packages/e1/89/ab8e86208467e467a80deaca4e434adac37b10a9d134cd2f99b28a01e483/pydantic_core-2.41.5-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ece5c59f0ce7d001e017643d8d24da587ea1f74f6993467d85ae8a5ef9d4f42b", size = 2135615, upload-time = "2025-11-04T13:43:08.116Z" }, + { url = "https://files.pythonhosted.org/packages/99/0a/99a53d06dd0348b2008f2f30884b34719c323f16c3be4e6cc1203b74a91d/pydantic_core-2.41.5-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:16f80f7abe3351f8ea6858914ddc8c77e02578544a0ebc15b4c2e1a0e813b0b2", size = 2175369, upload-time = "2025-11-04T13:43:12.49Z" }, + { url = "https://files.pythonhosted.org/packages/6d/94/30ca3b73c6d485b9bb0bc66e611cff4a7138ff9736b7e66bcf0852151636/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:33cb885e759a705b426baada1fe68cbb0a2e68e34c5d0d0289a364cf01709093", size = 2144218, upload-time = "2025-11-04T13:43:15.431Z" }, + { url = "https://files.pythonhosted.org/packages/87/57/31b4f8e12680b739a91f472b5671294236b82586889ef764b5fbc6669238/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:c8d8b4eb992936023be7dee581270af5c6e0697a8559895f527f5b7105ecd36a", size = 2329951, upload-time = "2025-11-04T13:43:18.062Z" }, + { url = "https://files.pythonhosted.org/packages/7d/73/3c2c8edef77b8f7310e6fb012dbc4b8551386ed575b9eb6fb2506e28a7eb/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:242a206cd0318f95cd21bdacff3fcc3aab23e79bba5cac3db5a841c9ef9c6963", size = 2318428, upload-time = "2025-11-04T13:43:20.679Z" }, + { url = "https://files.pythonhosted.org/packages/2f/02/8559b1f26ee0d502c74f9cca5c0d2fd97e967e083e006bbbb4e97f3a043a/pydantic_core-2.41.5-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d3a978c4f57a597908b7e697229d996d77a6d3c94901e9edee593adada95ce1a", size = 2147009, upload-time = "2025-11-04T13:43:23.286Z" }, + { url = "https://files.pythonhosted.org/packages/5f/9b/1b3f0e9f9305839d7e84912f9e8bfbd191ed1b1ef48083609f0dabde978c/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b2379fa7ed44ddecb5bfe4e48577d752db9fc10be00a6b7446e9663ba143de26", size = 2101980, upload-time = "2025-11-04T13:43:25.97Z" }, + { url = "https://files.pythonhosted.org/packages/a4/ed/d71fefcb4263df0da6a85b5d8a7508360f2f2e9b3bf5814be9c8bccdccc1/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:266fb4cbf5e3cbd0b53669a6d1b039c45e3ce651fd5442eff4d07c2cc8d66808", size = 1923865, upload-time = "2025-11-04T13:43:28.763Z" }, + { url = "https://files.pythonhosted.org/packages/ce/3a/626b38db460d675f873e4444b4bb030453bbe7b4ba55df821d026a0493c4/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58133647260ea01e4d0500089a8c4f07bd7aa6ce109682b1426394988d8aaacc", size = 2134256, upload-time = "2025-11-04T13:43:31.71Z" }, + { url = "https://files.pythonhosted.org/packages/83/d9/8412d7f06f616bbc053d30cb4e5f76786af3221462ad5eee1f202021eb4e/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:287dad91cfb551c363dc62899a80e9e14da1f0e2b6ebde82c806612ca2a13ef1", size = 2174762, upload-time = "2025-11-04T13:43:34.744Z" }, + { url = "https://files.pythonhosted.org/packages/55/4c/162d906b8e3ba3a99354e20faa1b49a85206c47de97a639510a0e673f5da/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:03b77d184b9eb40240ae9fd676ca364ce1085f203e1b1256f8ab9984dca80a84", size = 2143141, upload-time = "2025-11-04T13:43:37.701Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f2/f11dd73284122713f5f89fc940f370d035fa8e1e078d446b3313955157fe/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:a668ce24de96165bb239160b3d854943128f4334822900534f2fe947930e5770", size = 2330317, upload-time = "2025-11-04T13:43:40.406Z" }, + { url = "https://files.pythonhosted.org/packages/88/9d/b06ca6acfe4abb296110fb1273a4d848a0bfb2ff65f3ee92127b3244e16b/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f14f8f046c14563f8eb3f45f499cc658ab8d10072961e07225e507adb700e93f", size = 2316992, upload-time = "2025-11-04T13:43:43.602Z" }, + { url = "https://files.pythonhosted.org/packages/36/c7/cfc8e811f061c841d7990b0201912c3556bfeb99cdcb7ed24adc8d6f8704/pydantic_core-2.41.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:56121965f7a4dc965bff783d70b907ddf3d57f6eba29b6d2e5dabfaf07799c51", size = 2145302, upload-time = "2025-11-04T13:43:46.64Z" }, +] + +[[package]] +name = "pydantic-extra-types" +version = "2.11.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fd/35/2fee58b1316a73e025728583d3b1447218a97e621933fc776fb8c0f2ebdd/pydantic_extra_types-2.11.0.tar.gz", hash = "sha256:4e9991959d045b75feb775683437a97991d02c138e00b59176571db9ce634f0e", size = 157226, upload-time = "2025-12-31T16:18:27.944Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fe/17/fabd56da47096d240dd45ba627bead0333b0cf0ee8ada9bec579287dadf3/pydantic_extra_types-2.11.0-py3-none-any.whl", hash = "sha256:84b864d250a0fc62535b7ec591e36f2c5b4d1325fa0017eb8cda9aeb63b374a6", size = 74296, upload-time = "2025-12-31T16:18:26.38Z" }, +] + +[[package]] +name = "pydantic-settings" +version = "2.12.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, + { name = "python-dotenv" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/43/4b/ac7e0aae12027748076d72a8764ff1c9d82ca75a7a52622e67ed3f765c54/pydantic_settings-2.12.0.tar.gz", hash = "sha256:005538ef951e3c2a68e1c08b292b5f2e71490def8589d4221b95dab00dafcfd0", size = 194184, upload-time = "2025-11-10T14:25:47.013Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c1/60/5d4751ba3f4a40a6891f24eec885f51afd78d208498268c734e256fb13c4/pydantic_settings-2.12.0-py3-none-any.whl", hash = "sha256:fddb9fd99a5b18da837b29710391e945b1e30c135477f484084ee513adb93809", size = 51880, upload-time = "2025-11-10T14:25:45.546Z" }, +] + +[[package]] +name = "pygments" +version = "2.19.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, +] + +[[package]] +name = "pytest" +version = "9.0.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901, upload-time = "2025-12-06T21:30:51.014Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801, upload-time = "2025-12-06T21:30:49.154Z" }, +] + +[[package]] +name = "pytest-asyncio" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "backports-asyncio-runner", marker = "python_full_version < '3.11'" }, + { name = "pytest" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/90/2c/8af215c0f776415f3590cac4f9086ccefd6fd463befeae41cd4d3f193e5a/pytest_asyncio-1.3.0.tar.gz", hash = "sha256:d7f52f36d231b80ee124cd216ffb19369aa168fc10095013c6b014a34d3ee9e5", size = 50087, upload-time = "2025-11-10T16:07:47.256Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/35/f8b19922b6a25bc0880171a2f1a003eaeb93657475193ab516fd87cac9da/pytest_asyncio-1.3.0-py3-none-any.whl", hash = "sha256:611e26147c7f77640e6d0a92a38ed17c3e9848063698d5c93d5aa7aa11cebff5", size = 15075, upload-time = "2025-11-10T16:07:45.537Z" }, +] + +[[package]] +name = "python-dotenv" +version = "1.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f0/26/19cadc79a718c5edbec86fd4919a6b6d3f681039a2f6d66d14be94e75fb9/python_dotenv-1.2.1.tar.gz", hash = "sha256:42667e897e16ab0d66954af0e60a9caa94f0fd4ecf3aaf6d2d260eec1aa36ad6", size = 44221, upload-time = "2025-10-26T15:12:10.434Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/14/1b/a298b06749107c305e1fe0f814c6c74aea7b2f1e10989cb30f544a1b3253/python_dotenv-1.2.1-py3-none-any.whl", hash = "sha256:b81ee9561e9ca4004139c6cbba3a238c32b03e4894671e181b671e8cb8425d61", size = 21230, upload-time = "2025-10-26T15:12:09.109Z" }, +] + +[[package]] +name = "python-multipart" +version = "0.0.21" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/78/96/804520d0850c7db98e5ccb70282e29208723f0964e88ffd9d0da2f52ea09/python_multipart-0.0.21.tar.gz", hash = "sha256:7137ebd4d3bbf70ea1622998f902b97a29434a9e8dc40eb203bbcf7c2a2cba92", size = 37196, upload-time = "2025-12-17T09:24:22.446Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/aa/76/03af049af4dcee5d27442f71b6924f01f3efb5d2bd34f23fcd563f2cc5f5/python_multipart-0.0.21-py3-none-any.whl", hash = "sha256:cf7a6713e01c87aa35387f4774e812c4361150938d20d232800f75ffcf266090", size = 24541, upload-time = "2025-12-17T09:24:21.153Z" }, +] + +[[package]] +name = "pytokens" +version = "0.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4e/8d/a762be14dae1c3bf280202ba3172020b2b0b4c537f94427435f19c413b72/pytokens-0.3.0.tar.gz", hash = "sha256:2f932b14ed08de5fcf0b391ace2642f858f1394c0857202959000b68ed7a458a", size = 17644, upload-time = "2025-11-05T13:36:35.34Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/84/25/d9db8be44e205a124f6c98bc0324b2bb149b7431c53877fc6d1038dddaf5/pytokens-0.3.0-py3-none-any.whl", hash = "sha256:95b2b5eaf832e469d141a378872480ede3f251a5a5041b8ec6e581d3ac71bbf3", size = 12195, upload-time = "2025-11-05T13:36:33.183Z" }, +] + +[[package]] +name = "pyunormalize" +version = "17.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/25/ab/b912c484cfb96ba4834efe050bbf10c9e157bd8189eb859aefba8712b136/pyunormalize-17.0.0.tar.gz", hash = "sha256:0949a3e56817e287febcaf1b0cc4b5adf0bb107628d379335938040947eec792", size = 53121, upload-time = "2025-09-28T20:53:06.141Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/92/80/61512483dc509e3ae8a42fb143479d1e406ce1d91f8f08d538a3dde39c6d/pyunormalize-17.0.0-py3-none-any.whl", hash = "sha256:f0d93b076f938db2b26d319d04f2b58505d1cd7a80b5b72badbe7d1aa4d2a31c", size = 51358, upload-time = "2025-09-28T20:53:04.876Z" }, +] + +[[package]] +name = "pywin32" +version = "311" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7b/40/44efbb0dfbd33aca6a6483191dae0716070ed99e2ecb0c53683f400a0b4f/pywin32-311-cp310-cp310-win32.whl", hash = "sha256:d03ff496d2a0cd4a5893504789d4a15399133fe82517455e78bad62efbb7f0a3", size = 8760432, upload-time = "2025-07-14T20:13:05.9Z" }, + { url = "https://files.pythonhosted.org/packages/5e/bf/360243b1e953bd254a82f12653974be395ba880e7ec23e3731d9f73921cc/pywin32-311-cp310-cp310-win_amd64.whl", hash = "sha256:797c2772017851984b97180b0bebe4b620bb86328e8a884bb626156295a63b3b", size = 9590103, upload-time = "2025-07-14T20:13:07.698Z" }, + { url = "https://files.pythonhosted.org/packages/57/38/d290720e6f138086fb3d5ffe0b6caa019a791dd57866940c82e4eeaf2012/pywin32-311-cp310-cp310-win_arm64.whl", hash = "sha256:0502d1facf1fed4839a9a51ccbcc63d952cf318f78ffc00a7e78528ac27d7a2b", size = 8778557, upload-time = "2025-07-14T20:13:11.11Z" }, + { url = "https://files.pythonhosted.org/packages/7c/af/449a6a91e5d6db51420875c54f6aff7c97a86a3b13a0b4f1a5c13b988de3/pywin32-311-cp311-cp311-win32.whl", hash = "sha256:184eb5e436dea364dcd3d2316d577d625c0351bf237c4e9a5fabbcfa5a58b151", size = 8697031, upload-time = "2025-07-14T20:13:13.266Z" }, + { url = "https://files.pythonhosted.org/packages/51/8f/9bb81dd5bb77d22243d33c8397f09377056d5c687aa6d4042bea7fbf8364/pywin32-311-cp311-cp311-win_amd64.whl", hash = "sha256:3ce80b34b22b17ccbd937a6e78e7225d80c52f5ab9940fe0506a1a16f3dab503", size = 9508308, upload-time = "2025-07-14T20:13:15.147Z" }, + { url = "https://files.pythonhosted.org/packages/44/7b/9c2ab54f74a138c491aba1b1cd0795ba61f144c711daea84a88b63dc0f6c/pywin32-311-cp311-cp311-win_arm64.whl", hash = "sha256:a733f1388e1a842abb67ffa8e7aad0e70ac519e09b0f6a784e65a136ec7cefd2", size = 8703930, upload-time = "2025-07-14T20:13:16.945Z" }, + { url = "https://files.pythonhosted.org/packages/e7/ab/01ea1943d4eba0f850c3c61e78e8dd59757ff815ff3ccd0a84de5f541f42/pywin32-311-cp312-cp312-win32.whl", hash = "sha256:750ec6e621af2b948540032557b10a2d43b0cee2ae9758c54154d711cc852d31", size = 8706543, upload-time = "2025-07-14T20:13:20.765Z" }, + { url = "https://files.pythonhosted.org/packages/d1/a8/a0e8d07d4d051ec7502cd58b291ec98dcc0c3fff027caad0470b72cfcc2f/pywin32-311-cp312-cp312-win_amd64.whl", hash = "sha256:b8c095edad5c211ff31c05223658e71bf7116daa0ecf3ad85f3201ea3190d067", size = 9495040, upload-time = "2025-07-14T20:13:22.543Z" }, + { url = "https://files.pythonhosted.org/packages/ba/3a/2ae996277b4b50f17d61f0603efd8253cb2d79cc7ae159468007b586396d/pywin32-311-cp312-cp312-win_arm64.whl", hash = "sha256:e286f46a9a39c4a18b319c28f59b61de793654af2f395c102b4f819e584b5852", size = 8710102, upload-time = "2025-07-14T20:13:24.682Z" }, + { url = "https://files.pythonhosted.org/packages/a5/be/3fd5de0979fcb3994bfee0d65ed8ca9506a8a1260651b86174f6a86f52b3/pywin32-311-cp313-cp313-win32.whl", hash = "sha256:f95ba5a847cba10dd8c4d8fefa9f2a6cf283b8b88ed6178fa8a6c1ab16054d0d", size = 8705700, upload-time = "2025-07-14T20:13:26.471Z" }, + { url = "https://files.pythonhosted.org/packages/e3/28/e0a1909523c6890208295a29e05c2adb2126364e289826c0a8bc7297bd5c/pywin32-311-cp313-cp313-win_amd64.whl", hash = "sha256:718a38f7e5b058e76aee1c56ddd06908116d35147e133427e59a3983f703a20d", size = 9494700, upload-time = "2025-07-14T20:13:28.243Z" }, + { url = "https://files.pythonhosted.org/packages/04/bf/90339ac0f55726dce7d794e6d79a18a91265bdf3aa70b6b9ca52f35e022a/pywin32-311-cp313-cp313-win_arm64.whl", hash = "sha256:7b4075d959648406202d92a2310cb990fea19b535c7f4a78d3f5e10b926eeb8a", size = 8709318, upload-time = "2025-07-14T20:13:30.348Z" }, + { url = "https://files.pythonhosted.org/packages/c9/31/097f2e132c4f16d99a22bfb777e0fd88bd8e1c634304e102f313af69ace5/pywin32-311-cp314-cp314-win32.whl", hash = "sha256:b7a2c10b93f8986666d0c803ee19b5990885872a7de910fc460f9b0c2fbf92ee", size = 8840714, upload-time = "2025-07-14T20:13:32.449Z" }, + { url = "https://files.pythonhosted.org/packages/90/4b/07c77d8ba0e01349358082713400435347df8426208171ce297da32c313d/pywin32-311-cp314-cp314-win_amd64.whl", hash = "sha256:3aca44c046bd2ed8c90de9cb8427f581c479e594e99b5c0bb19b29c10fd6cb87", size = 9656800, upload-time = "2025-07-14T20:13:34.312Z" }, + { url = "https://files.pythonhosted.org/packages/c0/d2/21af5c535501a7233e734b8af901574572da66fcc254cb35d0609c9080dd/pywin32-311-cp314-cp314-win_arm64.whl", hash = "sha256:a508e2d9025764a8270f93111a970e1d0fbfc33f4153b388bb649b7eec4f9b42", size = 8932540, upload-time = "2025-07-14T20:13:36.379Z" }, +] + +[[package]] +name = "pyyaml" +version = "6.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/a0/39350dd17dd6d6c6507025c0e53aef67a9293a6d37d3511f23ea510d5800/pyyaml-6.0.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b", size = 184227, upload-time = "2025-09-25T21:31:46.04Z" }, + { url = "https://files.pythonhosted.org/packages/05/14/52d505b5c59ce73244f59c7a50ecf47093ce4765f116cdb98286a71eeca2/pyyaml-6.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956", size = 174019, upload-time = "2025-09-25T21:31:47.706Z" }, + { url = "https://files.pythonhosted.org/packages/43/f7/0e6a5ae5599c838c696adb4e6330a59f463265bfa1e116cfd1fbb0abaaae/pyyaml-6.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8", size = 740646, upload-time = "2025-09-25T21:31:49.21Z" }, + { url = "https://files.pythonhosted.org/packages/2f/3a/61b9db1d28f00f8fd0ae760459a5c4bf1b941baf714e207b6eb0657d2578/pyyaml-6.0.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:66291b10affd76d76f54fad28e22e51719ef9ba22b29e1d7d03d6777a9174198", size = 840793, upload-time = "2025-09-25T21:31:50.735Z" }, + { url = "https://files.pythonhosted.org/packages/7a/1e/7acc4f0e74c4b3d9531e24739e0ab832a5edf40e64fbae1a9c01941cabd7/pyyaml-6.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b", size = 770293, upload-time = "2025-09-25T21:31:51.828Z" }, + { url = "https://files.pythonhosted.org/packages/8b/ef/abd085f06853af0cd59fa5f913d61a8eab65d7639ff2a658d18a25d6a89d/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:418cf3f2111bc80e0933b2cd8cd04f286338bb88bdc7bc8e6dd775ebde60b5e0", size = 732872, upload-time = "2025-09-25T21:31:53.282Z" }, + { url = "https://files.pythonhosted.org/packages/1f/15/2bc9c8faf6450a8b3c9fc5448ed869c599c0a74ba2669772b1f3a0040180/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5e0b74767e5f8c593e8c9b5912019159ed0533c70051e9cce3e8b6aa699fcd69", size = 758828, upload-time = "2025-09-25T21:31:54.807Z" }, + { url = "https://files.pythonhosted.org/packages/a3/00/531e92e88c00f4333ce359e50c19b8d1de9fe8d581b1534e35ccfbc5f393/pyyaml-6.0.3-cp310-cp310-win32.whl", hash = "sha256:28c8d926f98f432f88adc23edf2e6d4921ac26fb084b028c733d01868d19007e", size = 142415, upload-time = "2025-09-25T21:31:55.885Z" }, + { url = "https://files.pythonhosted.org/packages/2a/fa/926c003379b19fca39dd4634818b00dec6c62d87faf628d1394e137354d4/pyyaml-6.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:bdb2c67c6c1390b63c6ff89f210c8fd09d9a1217a465701eac7316313c915e4c", size = 158561, upload-time = "2025-09-25T21:31:57.406Z" }, + { url = "https://files.pythonhosted.org/packages/6d/16/a95b6757765b7b031c9374925bb718d55e0a9ba8a1b6a12d25962ea44347/pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e", size = 185826, upload-time = "2025-09-25T21:31:58.655Z" }, + { url = "https://files.pythonhosted.org/packages/16/19/13de8e4377ed53079ee996e1ab0a9c33ec2faf808a4647b7b4c0d46dd239/pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824", size = 175577, upload-time = "2025-09-25T21:32:00.088Z" }, + { url = "https://files.pythonhosted.org/packages/0c/62/d2eb46264d4b157dae1275b573017abec435397aa59cbcdab6fc978a8af4/pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c", size = 775556, upload-time = "2025-09-25T21:32:01.31Z" }, + { url = "https://files.pythonhosted.org/packages/10/cb/16c3f2cf3266edd25aaa00d6c4350381c8b012ed6f5276675b9eba8d9ff4/pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00", size = 882114, upload-time = "2025-09-25T21:32:03.376Z" }, + { url = "https://files.pythonhosted.org/packages/71/60/917329f640924b18ff085ab889a11c763e0b573da888e8404ff486657602/pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d", size = 806638, upload-time = "2025-09-25T21:32:04.553Z" }, + { url = "https://files.pythonhosted.org/packages/dd/6f/529b0f316a9fd167281a6c3826b5583e6192dba792dd55e3203d3f8e655a/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a", size = 767463, upload-time = "2025-09-25T21:32:06.152Z" }, + { url = "https://files.pythonhosted.org/packages/f2/6a/b627b4e0c1dd03718543519ffb2f1deea4a1e6d42fbab8021936a4d22589/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4", size = 794986, upload-time = "2025-09-25T21:32:07.367Z" }, + { url = "https://files.pythonhosted.org/packages/45/91/47a6e1c42d9ee337c4839208f30d9f09caa9f720ec7582917b264defc875/pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b", size = 142543, upload-time = "2025-09-25T21:32:08.95Z" }, + { url = "https://files.pythonhosted.org/packages/da/e3/ea007450a105ae919a72393cb06f122f288ef60bba2dc64b26e2646fa315/pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf", size = 158763, upload-time = "2025-09-25T21:32:09.96Z" }, + { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" }, + { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" }, + { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" }, + { url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" }, + { url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" }, + { url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" }, + { url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" }, + { url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" }, + { url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" }, + { url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" }, + { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" }, + { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" }, + { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" }, + { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" }, + { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" }, + { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" }, + { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" }, + { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" }, + { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" }, + { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" }, + { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" }, + { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" }, + { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" }, + { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" }, + { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" }, + { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" }, + { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" }, + { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" }, + { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" }, + { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" }, + { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" }, + { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" }, + { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" }, + { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" }, + { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, +] + +[[package]] +name = "regex" +version = "2025.11.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cc/a9/546676f25e573a4cf00fe8e119b78a37b6a8fe2dc95cda877b30889c9c45/regex-2025.11.3.tar.gz", hash = "sha256:1fedc720f9bb2494ce31a58a1631f9c82df6a09b49c19517ea5cc280b4541e01", size = 414669, upload-time = "2025-11-03T21:34:22.089Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/d6/d788d52da01280a30a3f6268aef2aa71043bff359c618fea4c5b536654d5/regex-2025.11.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:2b441a4ae2c8049106e8b39973bfbddfb25a179dda2bdb99b0eeb60c40a6a3af", size = 488087, upload-time = "2025-11-03T21:30:47.317Z" }, + { url = "https://files.pythonhosted.org/packages/69/39/abec3bd688ec9bbea3562de0fd764ff802976185f5ff22807bf0a2697992/regex-2025.11.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2fa2eed3f76677777345d2f81ee89f5de2f5745910e805f7af7386a920fa7313", size = 290544, upload-time = "2025-11-03T21:30:49.912Z" }, + { url = "https://files.pythonhosted.org/packages/39/b3/9a231475d5653e60002508f41205c61684bb2ffbf2401351ae2186897fc4/regex-2025.11.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d8b4a27eebd684319bdf473d39f1d79eed36bf2cd34bd4465cdb4618d82b3d56", size = 288408, upload-time = "2025-11-03T21:30:51.344Z" }, + { url = "https://files.pythonhosted.org/packages/c3/c5/1929a0491bd5ac2d1539a866768b88965fa8c405f3e16a8cef84313098d6/regex-2025.11.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5cf77eac15bd264986c4a2c63353212c095b40f3affb2bc6b4ef80c4776c1a28", size = 781584, upload-time = "2025-11-03T21:30:52.596Z" }, + { url = "https://files.pythonhosted.org/packages/ce/fd/16aa16cf5d497ef727ec966f74164fbe75d6516d3d58ac9aa989bc9cdaad/regex-2025.11.3-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b7f9ee819f94c6abfa56ec7b1dbab586f41ebbdc0a57e6524bd5e7f487a878c7", size = 850733, upload-time = "2025-11-03T21:30:53.825Z" }, + { url = "https://files.pythonhosted.org/packages/e6/49/3294b988855a221cb6565189edf5dc43239957427df2d81d4a6b15244f64/regex-2025.11.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:838441333bc90b829406d4a03cb4b8bf7656231b84358628b0406d803931ef32", size = 898691, upload-time = "2025-11-03T21:30:55.575Z" }, + { url = "https://files.pythonhosted.org/packages/14/62/b56d29e70b03666193369bdbdedfdc23946dbe9f81dd78ce262c74d988ab/regex-2025.11.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cfe6d3f0c9e3b7e8c0c694b24d25e677776f5ca26dce46fd6b0489f9c8339391", size = 791662, upload-time = "2025-11-03T21:30:57.262Z" }, + { url = "https://files.pythonhosted.org/packages/15/fc/e4c31d061eced63fbf1ce9d853975f912c61a7d406ea14eda2dd355f48e7/regex-2025.11.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2ab815eb8a96379a27c3b6157fcb127c8f59c36f043c1678110cea492868f1d5", size = 782587, upload-time = "2025-11-03T21:30:58.788Z" }, + { url = "https://files.pythonhosted.org/packages/b2/bb/5e30c7394bcf63f0537121c23e796be67b55a8847c3956ae6068f4c70702/regex-2025.11.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:728a9d2d173a65b62bdc380b7932dd8e74ed4295279a8fe1021204ce210803e7", size = 774709, upload-time = "2025-11-03T21:31:00.081Z" }, + { url = "https://files.pythonhosted.org/packages/c5/c4/fce773710af81b0cb37cb4ff0947e75d5d17dee304b93d940b87a67fc2f4/regex-2025.11.3-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:509dc827f89c15c66a0c216331260d777dd6c81e9a4e4f830e662b0bb296c313", size = 845773, upload-time = "2025-11-03T21:31:01.583Z" }, + { url = "https://files.pythonhosted.org/packages/7b/5e/9466a7ec4b8ec282077095c6eb50a12a389d2e036581134d4919e8ca518c/regex-2025.11.3-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:849202cd789e5f3cf5dcc7822c34b502181b4824a65ff20ce82da5524e45e8e9", size = 836164, upload-time = "2025-11-03T21:31:03.244Z" }, + { url = "https://files.pythonhosted.org/packages/95/18/82980a60e8ed1594eb3c89eb814fb276ef51b9af7caeab1340bfd8564af6/regex-2025.11.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b6f78f98741dcc89607c16b1e9426ee46ce4bf31ac5e6b0d40e81c89f3481ea5", size = 779832, upload-time = "2025-11-03T21:31:04.876Z" }, + { url = "https://files.pythonhosted.org/packages/03/cc/90ab0fdbe6dce064a42015433f9152710139fb04a8b81b4fb57a1cb63ffa/regex-2025.11.3-cp310-cp310-win32.whl", hash = "sha256:149eb0bba95231fb4f6d37c8f760ec9fa6fabf65bab555e128dde5f2475193ec", size = 265802, upload-time = "2025-11-03T21:31:06.581Z" }, + { url = "https://files.pythonhosted.org/packages/34/9d/e9e8493a85f3b1ddc4a5014465f5c2b78c3ea1cbf238dcfde78956378041/regex-2025.11.3-cp310-cp310-win_amd64.whl", hash = "sha256:ee3a83ce492074c35a74cc76cf8235d49e77b757193a5365ff86e3f2f93db9fd", size = 277722, upload-time = "2025-11-03T21:31:08.144Z" }, + { url = "https://files.pythonhosted.org/packages/15/c4/b54b24f553966564506dbf873a3e080aef47b356a3b39b5d5aba992b50db/regex-2025.11.3-cp310-cp310-win_arm64.whl", hash = "sha256:38af559ad934a7b35147716655d4a2f79fcef2d695ddfe06a06ba40ae631fa7e", size = 270289, upload-time = "2025-11-03T21:31:10.267Z" }, + { url = "https://files.pythonhosted.org/packages/f7/90/4fb5056e5f03a7048abd2b11f598d464f0c167de4f2a51aa868c376b8c70/regex-2025.11.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:eadade04221641516fa25139273505a1c19f9bf97589a05bc4cfcd8b4a618031", size = 488081, upload-time = "2025-11-03T21:31:11.946Z" }, + { url = "https://files.pythonhosted.org/packages/85/23/63e481293fac8b069d84fba0299b6666df720d875110efd0338406b5d360/regex-2025.11.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:feff9e54ec0dd3833d659257f5c3f5322a12eee58ffa360984b716f8b92983f4", size = 290554, upload-time = "2025-11-03T21:31:13.387Z" }, + { url = "https://files.pythonhosted.org/packages/2b/9d/b101d0262ea293a0066b4522dfb722eb6a8785a8c3e084396a5f2c431a46/regex-2025.11.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3b30bc921d50365775c09a7ed446359e5c0179e9e2512beec4a60cbcef6ddd50", size = 288407, upload-time = "2025-11-03T21:31:14.809Z" }, + { url = "https://files.pythonhosted.org/packages/0c/64/79241c8209d5b7e00577ec9dca35cd493cc6be35b7d147eda367d6179f6d/regex-2025.11.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f99be08cfead2020c7ca6e396c13543baea32343b7a9a5780c462e323bd8872f", size = 793418, upload-time = "2025-11-03T21:31:16.556Z" }, + { url = "https://files.pythonhosted.org/packages/3d/e2/23cd5d3573901ce8f9757c92ca4db4d09600b865919b6d3e7f69f03b1afd/regex-2025.11.3-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6dd329a1b61c0ee95ba95385fb0c07ea0d3fe1a21e1349fa2bec272636217118", size = 860448, upload-time = "2025-11-03T21:31:18.12Z" }, + { url = "https://files.pythonhosted.org/packages/2a/4c/aecf31beeaa416d0ae4ecb852148d38db35391aac19c687b5d56aedf3a8b/regex-2025.11.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4c5238d32f3c5269d9e87be0cf096437b7622b6920f5eac4fd202468aaeb34d2", size = 907139, upload-time = "2025-11-03T21:31:20.753Z" }, + { url = "https://files.pythonhosted.org/packages/61/22/b8cb00df7d2b5e0875f60628594d44dba283e951b1ae17c12f99e332cc0a/regex-2025.11.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:10483eefbfb0adb18ee9474498c9a32fcf4e594fbca0543bb94c48bac6183e2e", size = 800439, upload-time = "2025-11-03T21:31:22.069Z" }, + { url = "https://files.pythonhosted.org/packages/02/a8/c4b20330a5cdc7a8eb265f9ce593f389a6a88a0c5f280cf4d978f33966bc/regex-2025.11.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:78c2d02bb6e1da0720eedc0bad578049cad3f71050ef8cd065ecc87691bed2b0", size = 782965, upload-time = "2025-11-03T21:31:23.598Z" }, + { url = "https://files.pythonhosted.org/packages/b4/4c/ae3e52988ae74af4b04d2af32fee4e8077f26e51b62ec2d12d246876bea2/regex-2025.11.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e6b49cd2aad93a1790ce9cffb18964f6d3a4b0b3dbdbd5de094b65296fce6e58", size = 854398, upload-time = "2025-11-03T21:31:25.008Z" }, + { url = "https://files.pythonhosted.org/packages/06/d1/a8b9cf45874eda14b2e275157ce3b304c87e10fb38d9fc26a6e14eb18227/regex-2025.11.3-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:885b26aa3ee56433b630502dc3d36ba78d186a00cc535d3806e6bfd9ed3c70ab", size = 845897, upload-time = "2025-11-03T21:31:26.427Z" }, + { url = "https://files.pythonhosted.org/packages/ea/fe/1830eb0236be93d9b145e0bd8ab499f31602fe0999b1f19e99955aa8fe20/regex-2025.11.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ddd76a9f58e6a00f8772e72cff8ebcff78e022be95edf018766707c730593e1e", size = 788906, upload-time = "2025-11-03T21:31:28.078Z" }, + { url = "https://files.pythonhosted.org/packages/66/47/dc2577c1f95f188c1e13e2e69d8825a5ac582ac709942f8a03af42ed6e93/regex-2025.11.3-cp311-cp311-win32.whl", hash = "sha256:3e816cc9aac1cd3cc9a4ec4d860f06d40f994b5c7b4d03b93345f44e08cc68bf", size = 265812, upload-time = "2025-11-03T21:31:29.72Z" }, + { url = "https://files.pythonhosted.org/packages/50/1e/15f08b2f82a9bbb510621ec9042547b54d11e83cb620643ebb54e4eb7d71/regex-2025.11.3-cp311-cp311-win_amd64.whl", hash = "sha256:087511f5c8b7dfbe3a03f5d5ad0c2a33861b1fc387f21f6f60825a44865a385a", size = 277737, upload-time = "2025-11-03T21:31:31.422Z" }, + { url = "https://files.pythonhosted.org/packages/f4/fc/6500eb39f5f76c5e47a398df82e6b535a5e345f839581012a418b16f9cc3/regex-2025.11.3-cp311-cp311-win_arm64.whl", hash = "sha256:1ff0d190c7f68ae7769cd0313fe45820ba07ffebfddfaa89cc1eb70827ba0ddc", size = 270290, upload-time = "2025-11-03T21:31:33.041Z" }, + { url = "https://files.pythonhosted.org/packages/e8/74/18f04cb53e58e3fb107439699bd8375cf5a835eec81084e0bddbd122e4c2/regex-2025.11.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:bc8ab71e2e31b16e40868a40a69007bc305e1109bd4658eb6cad007e0bf67c41", size = 489312, upload-time = "2025-11-03T21:31:34.343Z" }, + { url = "https://files.pythonhosted.org/packages/78/3f/37fcdd0d2b1e78909108a876580485ea37c91e1acf66d3bb8e736348f441/regex-2025.11.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:22b29dda7e1f7062a52359fca6e58e548e28c6686f205e780b02ad8ef710de36", size = 291256, upload-time = "2025-11-03T21:31:35.675Z" }, + { url = "https://files.pythonhosted.org/packages/bf/26/0a575f58eb23b7ebd67a45fccbc02ac030b737b896b7e7a909ffe43ffd6a/regex-2025.11.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3a91e4a29938bc1a082cc28fdea44be420bf2bebe2665343029723892eb073e1", size = 288921, upload-time = "2025-11-03T21:31:37.07Z" }, + { url = "https://files.pythonhosted.org/packages/ea/98/6a8dff667d1af907150432cf5abc05a17ccd32c72a3615410d5365ac167a/regex-2025.11.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:08b884f4226602ad40c5d55f52bf91a9df30f513864e0054bad40c0e9cf1afb7", size = 798568, upload-time = "2025-11-03T21:31:38.784Z" }, + { url = "https://files.pythonhosted.org/packages/64/15/92c1db4fa4e12733dd5a526c2dd2b6edcbfe13257e135fc0f6c57f34c173/regex-2025.11.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3e0b11b2b2433d1c39c7c7a30e3f3d0aeeea44c2a8d0bae28f6b95f639927a69", size = 864165, upload-time = "2025-11-03T21:31:40.559Z" }, + { url = "https://files.pythonhosted.org/packages/f9/e7/3ad7da8cdee1ce66c7cd37ab5ab05c463a86ffeb52b1a25fe7bd9293b36c/regex-2025.11.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:87eb52a81ef58c7ba4d45c3ca74e12aa4b4e77816f72ca25258a85b3ea96cb48", size = 912182, upload-time = "2025-11-03T21:31:42.002Z" }, + { url = "https://files.pythonhosted.org/packages/84/bd/9ce9f629fcb714ffc2c3faf62b6766ecb7a585e1e885eb699bcf130a5209/regex-2025.11.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a12ab1f5c29b4e93db518f5e3872116b7e9b1646c9f9f426f777b50d44a09e8c", size = 803501, upload-time = "2025-11-03T21:31:43.815Z" }, + { url = "https://files.pythonhosted.org/packages/7c/0f/8dc2e4349d8e877283e6edd6c12bdcebc20f03744e86f197ab6e4492bf08/regex-2025.11.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7521684c8c7c4f6e88e35ec89680ee1aa8358d3f09d27dfbdf62c446f5d4c695", size = 787842, upload-time = "2025-11-03T21:31:45.353Z" }, + { url = "https://files.pythonhosted.org/packages/f9/73/cff02702960bc185164d5619c0c62a2f598a6abff6695d391b096237d4ab/regex-2025.11.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:7fe6e5440584e94cc4b3f5f4d98a25e29ca12dccf8873679a635638349831b98", size = 858519, upload-time = "2025-11-03T21:31:46.814Z" }, + { url = "https://files.pythonhosted.org/packages/61/83/0e8d1ae71e15bc1dc36231c90b46ee35f9d52fab2e226b0e039e7ea9c10a/regex-2025.11.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:8e026094aa12b43f4fd74576714e987803a315c76edb6b098b9809db5de58f74", size = 850611, upload-time = "2025-11-03T21:31:48.289Z" }, + { url = "https://files.pythonhosted.org/packages/c8/f5/70a5cdd781dcfaa12556f2955bf170cd603cb1c96a1827479f8faea2df97/regex-2025.11.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:435bbad13e57eb5606a68443af62bed3556de2f46deb9f7d4237bc2f1c9fb3a0", size = 789759, upload-time = "2025-11-03T21:31:49.759Z" }, + { url = "https://files.pythonhosted.org/packages/59/9b/7c29be7903c318488983e7d97abcf8ebd3830e4c956c4c540005fcfb0462/regex-2025.11.3-cp312-cp312-win32.whl", hash = "sha256:3839967cf4dc4b985e1570fd8d91078f0c519f30491c60f9ac42a8db039be204", size = 266194, upload-time = "2025-11-03T21:31:51.53Z" }, + { url = "https://files.pythonhosted.org/packages/1a/67/3b92df89f179d7c367be654ab5626ae311cb28f7d5c237b6bb976cd5fbbb/regex-2025.11.3-cp312-cp312-win_amd64.whl", hash = "sha256:e721d1b46e25c481dc5ded6f4b3f66c897c58d2e8cfdf77bbced84339108b0b9", size = 277069, upload-time = "2025-11-03T21:31:53.151Z" }, + { url = "https://files.pythonhosted.org/packages/d7/55/85ba4c066fe5094d35b249c3ce8df0ba623cfd35afb22d6764f23a52a1c5/regex-2025.11.3-cp312-cp312-win_arm64.whl", hash = "sha256:64350685ff08b1d3a6fff33f45a9ca183dc1d58bbfe4981604e70ec9801bbc26", size = 270330, upload-time = "2025-11-03T21:31:54.514Z" }, + { url = "https://files.pythonhosted.org/packages/e1/a7/dda24ebd49da46a197436ad96378f17df30ceb40e52e859fc42cac45b850/regex-2025.11.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:c1e448051717a334891f2b9a620fe36776ebf3dd8ec46a0b877c8ae69575feb4", size = 489081, upload-time = "2025-11-03T21:31:55.9Z" }, + { url = "https://files.pythonhosted.org/packages/19/22/af2dc751aacf88089836aa088a1a11c4f21a04707eb1b0478e8e8fb32847/regex-2025.11.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:9b5aca4d5dfd7fbfbfbdaf44850fcc7709a01146a797536a8f84952e940cca76", size = 291123, upload-time = "2025-11-03T21:31:57.758Z" }, + { url = "https://files.pythonhosted.org/packages/a3/88/1a3ea5672f4b0a84802ee9891b86743438e7c04eb0b8f8c4e16a42375327/regex-2025.11.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:04d2765516395cf7dda331a244a3282c0f5ae96075f728629287dfa6f76ba70a", size = 288814, upload-time = "2025-11-03T21:32:01.12Z" }, + { url = "https://files.pythonhosted.org/packages/fb/8c/f5987895bf42b8ddeea1b315c9fedcfe07cadee28b9c98cf50d00adcb14d/regex-2025.11.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5d9903ca42bfeec4cebedba8022a7c97ad2aab22e09573ce9976ba01b65e4361", size = 798592, upload-time = "2025-11-03T21:32:03.006Z" }, + { url = "https://files.pythonhosted.org/packages/99/2a/6591ebeede78203fa77ee46a1c36649e02df9eaa77a033d1ccdf2fcd5d4e/regex-2025.11.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:639431bdc89d6429f6721625e8129413980ccd62e9d3f496be618a41d205f160", size = 864122, upload-time = "2025-11-03T21:32:04.553Z" }, + { url = "https://files.pythonhosted.org/packages/94/d6/be32a87cf28cf8ed064ff281cfbd49aefd90242a83e4b08b5a86b38e8eb4/regex-2025.11.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f117efad42068f9715677c8523ed2be1518116d1c49b1dd17987716695181efe", size = 912272, upload-time = "2025-11-03T21:32:06.148Z" }, + { url = "https://files.pythonhosted.org/packages/62/11/9bcef2d1445665b180ac7f230406ad80671f0fc2a6ffb93493b5dd8cd64c/regex-2025.11.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4aecb6f461316adf9f1f0f6a4a1a3d79e045f9b71ec76055a791affa3b285850", size = 803497, upload-time = "2025-11-03T21:32:08.162Z" }, + { url = "https://files.pythonhosted.org/packages/e5/a7/da0dc273d57f560399aa16d8a68ae7f9b57679476fc7ace46501d455fe84/regex-2025.11.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:3b3a5f320136873cc5561098dfab677eea139521cb9a9e8db98b7e64aef44cbc", size = 787892, upload-time = "2025-11-03T21:32:09.769Z" }, + { url = "https://files.pythonhosted.org/packages/da/4b/732a0c5a9736a0b8d6d720d4945a2f1e6f38f87f48f3173559f53e8d5d82/regex-2025.11.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:75fa6f0056e7efb1f42a1c34e58be24072cb9e61a601340cc1196ae92326a4f9", size = 858462, upload-time = "2025-11-03T21:32:11.769Z" }, + { url = "https://files.pythonhosted.org/packages/0c/f5/a2a03df27dc4c2d0c769220f5110ba8c4084b0bfa9ab0f9b4fcfa3d2b0fc/regex-2025.11.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:dbe6095001465294f13f1adcd3311e50dd84e5a71525f20a10bd16689c61ce0b", size = 850528, upload-time = "2025-11-03T21:32:13.906Z" }, + { url = "https://files.pythonhosted.org/packages/d6/09/e1cd5bee3841c7f6eb37d95ca91cdee7100b8f88b81e41c2ef426910891a/regex-2025.11.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:454d9b4ae7881afbc25015b8627c16d88a597479b9dea82b8c6e7e2e07240dc7", size = 789866, upload-time = "2025-11-03T21:32:15.748Z" }, + { url = "https://files.pythonhosted.org/packages/eb/51/702f5ea74e2a9c13d855a6a85b7f80c30f9e72a95493260193c07f3f8d74/regex-2025.11.3-cp313-cp313-win32.whl", hash = "sha256:28ba4d69171fc6e9896337d4fc63a43660002b7da53fc15ac992abcf3410917c", size = 266189, upload-time = "2025-11-03T21:32:17.493Z" }, + { url = "https://files.pythonhosted.org/packages/8b/00/6e29bb314e271a743170e53649db0fdb8e8ff0b64b4f425f5602f4eb9014/regex-2025.11.3-cp313-cp313-win_amd64.whl", hash = "sha256:bac4200befe50c670c405dc33af26dad5a3b6b255dd6c000d92fe4629f9ed6a5", size = 277054, upload-time = "2025-11-03T21:32:19.042Z" }, + { url = "https://files.pythonhosted.org/packages/25/f1/b156ff9f2ec9ac441710764dda95e4edaf5f36aca48246d1eea3f1fd96ec/regex-2025.11.3-cp313-cp313-win_arm64.whl", hash = "sha256:2292cd5a90dab247f9abe892ac584cb24f0f54680c73fcb4a7493c66c2bf2467", size = 270325, upload-time = "2025-11-03T21:32:21.338Z" }, + { url = "https://files.pythonhosted.org/packages/20/28/fd0c63357caefe5680b8ea052131acbd7f456893b69cc2a90cc3e0dc90d4/regex-2025.11.3-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:1eb1ebf6822b756c723e09f5186473d93236c06c579d2cc0671a722d2ab14281", size = 491984, upload-time = "2025-11-03T21:32:23.466Z" }, + { url = "https://files.pythonhosted.org/packages/df/ec/7014c15626ab46b902b3bcc4b28a7bae46d8f281fc7ea9c95e22fcaaa917/regex-2025.11.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:1e00ec2970aab10dc5db34af535f21fcf32b4a31d99e34963419636e2f85ae39", size = 292673, upload-time = "2025-11-03T21:32:25.034Z" }, + { url = "https://files.pythonhosted.org/packages/23/ab/3b952ff7239f20d05f1f99e9e20188513905f218c81d52fb5e78d2bf7634/regex-2025.11.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a4cb042b615245d5ff9b3794f56be4138b5adc35a4166014d31d1814744148c7", size = 291029, upload-time = "2025-11-03T21:32:26.528Z" }, + { url = "https://files.pythonhosted.org/packages/21/7e/3dc2749fc684f455f162dcafb8a187b559e2614f3826877d3844a131f37b/regex-2025.11.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:44f264d4bf02f3176467d90b294d59bf1db9fe53c141ff772f27a8b456b2a9ed", size = 807437, upload-time = "2025-11-03T21:32:28.363Z" }, + { url = "https://files.pythonhosted.org/packages/1b/0b/d529a85ab349c6a25d1ca783235b6e3eedf187247eab536797021f7126c6/regex-2025.11.3-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7be0277469bf3bd7a34a9c57c1b6a724532a0d235cd0dc4e7f4316f982c28b19", size = 873368, upload-time = "2025-11-03T21:32:30.4Z" }, + { url = "https://files.pythonhosted.org/packages/7d/18/2d868155f8c9e3e9d8f9e10c64e9a9f496bb8f7e037a88a8bed26b435af6/regex-2025.11.3-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0d31e08426ff4b5b650f68839f5af51a92a5b51abd8554a60c2fbc7c71f25d0b", size = 914921, upload-time = "2025-11-03T21:32:32.123Z" }, + { url = "https://files.pythonhosted.org/packages/2d/71/9d72ff0f354fa783fe2ba913c8734c3b433b86406117a8db4ea2bf1c7a2f/regex-2025.11.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e43586ce5bd28f9f285a6e729466841368c4a0353f6fd08d4ce4630843d3648a", size = 812708, upload-time = "2025-11-03T21:32:34.305Z" }, + { url = "https://files.pythonhosted.org/packages/e7/19/ce4bf7f5575c97f82b6e804ffb5c4e940c62609ab2a0d9538d47a7fdf7d4/regex-2025.11.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:0f9397d561a4c16829d4e6ff75202c1c08b68a3bdbfe29dbfcdb31c9830907c6", size = 795472, upload-time = "2025-11-03T21:32:36.364Z" }, + { url = "https://files.pythonhosted.org/packages/03/86/fd1063a176ffb7b2315f9a1b08d17b18118b28d9df163132615b835a26ee/regex-2025.11.3-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:dd16e78eb18ffdb25ee33a0682d17912e8cc8a770e885aeee95020046128f1ce", size = 868341, upload-time = "2025-11-03T21:32:38.042Z" }, + { url = "https://files.pythonhosted.org/packages/12/43/103fb2e9811205e7386366501bc866a164a0430c79dd59eac886a2822950/regex-2025.11.3-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:ffcca5b9efe948ba0661e9df0fa50d2bc4b097c70b9810212d6b62f05d83b2dd", size = 854666, upload-time = "2025-11-03T21:32:40.079Z" }, + { url = "https://files.pythonhosted.org/packages/7d/22/e392e53f3869b75804762c7c848bd2dd2abf2b70fb0e526f58724638bd35/regex-2025.11.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c56b4d162ca2b43318ac671c65bd4d563e841a694ac70e1a976ac38fcf4ca1d2", size = 799473, upload-time = "2025-11-03T21:32:42.148Z" }, + { url = "https://files.pythonhosted.org/packages/4f/f9/8bd6b656592f925b6845fcbb4d57603a3ac2fb2373344ffa1ed70aa6820a/regex-2025.11.3-cp313-cp313t-win32.whl", hash = "sha256:9ddc42e68114e161e51e272f667d640f97e84a2b9ef14b7477c53aac20c2d59a", size = 268792, upload-time = "2025-11-03T21:32:44.13Z" }, + { url = "https://files.pythonhosted.org/packages/e5/87/0e7d603467775ff65cd2aeabf1b5b50cc1c3708556a8b849a2fa4dd1542b/regex-2025.11.3-cp313-cp313t-win_amd64.whl", hash = "sha256:7a7c7fdf755032ffdd72c77e3d8096bdcb0eb92e89e17571a196f03d88b11b3c", size = 280214, upload-time = "2025-11-03T21:32:45.853Z" }, + { url = "https://files.pythonhosted.org/packages/8d/d0/2afc6f8e94e2b64bfb738a7c2b6387ac1699f09f032d363ed9447fd2bb57/regex-2025.11.3-cp313-cp313t-win_arm64.whl", hash = "sha256:df9eb838c44f570283712e7cff14c16329a9f0fb19ca492d21d4b7528ee6821e", size = 271469, upload-time = "2025-11-03T21:32:48.026Z" }, + { url = "https://files.pythonhosted.org/packages/31/e9/f6e13de7e0983837f7b6d238ad9458800a874bf37c264f7923e63409944c/regex-2025.11.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:9697a52e57576c83139d7c6f213d64485d3df5bf84807c35fa409e6c970801c6", size = 489089, upload-time = "2025-11-03T21:32:50.027Z" }, + { url = "https://files.pythonhosted.org/packages/a3/5c/261f4a262f1fa65141c1b74b255988bd2fa020cc599e53b080667d591cfc/regex-2025.11.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:e18bc3f73bd41243c9b38a6d9f2366cd0e0137a9aebe2d8ff76c5b67d4c0a3f4", size = 291059, upload-time = "2025-11-03T21:32:51.682Z" }, + { url = "https://files.pythonhosted.org/packages/8e/57/f14eeb7f072b0e9a5a090d1712741fd8f214ec193dba773cf5410108bb7d/regex-2025.11.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:61a08bcb0ec14ff4e0ed2044aad948d0659604f824cbd50b55e30b0ec6f09c73", size = 288900, upload-time = "2025-11-03T21:32:53.569Z" }, + { url = "https://files.pythonhosted.org/packages/3c/6b/1d650c45e99a9b327586739d926a1cd4e94666b1bd4af90428b36af66dc7/regex-2025.11.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c9c30003b9347c24bcc210958c5d167b9e4f9be786cb380a7d32f14f9b84674f", size = 799010, upload-time = "2025-11-03T21:32:55.222Z" }, + { url = "https://files.pythonhosted.org/packages/99/ee/d66dcbc6b628ce4e3f7f0cbbb84603aa2fc0ffc878babc857726b8aab2e9/regex-2025.11.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4e1e592789704459900728d88d41a46fe3969b82ab62945560a31732ffc19a6d", size = 864893, upload-time = "2025-11-03T21:32:57.239Z" }, + { url = "https://files.pythonhosted.org/packages/bf/2d/f238229f1caba7ac87a6c4153d79947fb0261415827ae0f77c304260c7d3/regex-2025.11.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6538241f45eb5a25aa575dbba1069ad786f68a4f2773a29a2bd3dd1f9de787be", size = 911522, upload-time = "2025-11-03T21:32:59.274Z" }, + { url = "https://files.pythonhosted.org/packages/bd/3d/22a4eaba214a917c80e04f6025d26143690f0419511e0116508e24b11c9b/regex-2025.11.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bce22519c989bb72a7e6b36a199384c53db7722fe669ba891da75907fe3587db", size = 803272, upload-time = "2025-11-03T21:33:01.393Z" }, + { url = "https://files.pythonhosted.org/packages/84/b1/03188f634a409353a84b5ef49754b97dbcc0c0f6fd6c8ede505a8960a0a4/regex-2025.11.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:66d559b21d3640203ab9075797a55165d79017520685fb407b9234d72ab63c62", size = 787958, upload-time = "2025-11-03T21:33:03.379Z" }, + { url = "https://files.pythonhosted.org/packages/99/6a/27d072f7fbf6fadd59c64d210305e1ff865cc3b78b526fd147db768c553b/regex-2025.11.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:669dcfb2e38f9e8c69507bace46f4889e3abbfd9b0c29719202883c0a603598f", size = 859289, upload-time = "2025-11-03T21:33:05.374Z" }, + { url = "https://files.pythonhosted.org/packages/9a/70/1b3878f648e0b6abe023172dacb02157e685564853cc363d9961bcccde4e/regex-2025.11.3-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:32f74f35ff0f25a5021373ac61442edcb150731fbaa28286bbc8bb1582c89d02", size = 850026, upload-time = "2025-11-03T21:33:07.131Z" }, + { url = "https://files.pythonhosted.org/packages/dd/d5/68e25559b526b8baab8e66839304ede68ff6727237a47727d240006bd0ff/regex-2025.11.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e6c7a21dffba883234baefe91bc3388e629779582038f75d2a5be918e250f0ed", size = 789499, upload-time = "2025-11-03T21:33:09.141Z" }, + { url = "https://files.pythonhosted.org/packages/fc/df/43971264857140a350910d4e33df725e8c94dd9dee8d2e4729fa0d63d49e/regex-2025.11.3-cp314-cp314-win32.whl", hash = "sha256:795ea137b1d809eb6836b43748b12634291c0ed55ad50a7d72d21edf1cd565c4", size = 271604, upload-time = "2025-11-03T21:33:10.9Z" }, + { url = "https://files.pythonhosted.org/packages/01/6f/9711b57dc6894a55faf80a4c1b5aa4f8649805cb9c7aef46f7d27e2b9206/regex-2025.11.3-cp314-cp314-win_amd64.whl", hash = "sha256:9f95fbaa0ee1610ec0fc6b26668e9917a582ba80c52cc6d9ada15e30aa9ab9ad", size = 280320, upload-time = "2025-11-03T21:33:12.572Z" }, + { url = "https://files.pythonhosted.org/packages/f1/7e/f6eaa207d4377481f5e1775cdeb5a443b5a59b392d0065f3417d31d80f87/regex-2025.11.3-cp314-cp314-win_arm64.whl", hash = "sha256:dfec44d532be4c07088c3de2876130ff0fbeeacaa89a137decbbb5f665855a0f", size = 273372, upload-time = "2025-11-03T21:33:14.219Z" }, + { url = "https://files.pythonhosted.org/packages/c3/06/49b198550ee0f5e4184271cee87ba4dfd9692c91ec55289e6282f0f86ccf/regex-2025.11.3-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:ba0d8a5d7f04f73ee7d01d974d47c5834f8a1b0224390e4fe7c12a3a92a78ecc", size = 491985, upload-time = "2025-11-03T21:33:16.555Z" }, + { url = "https://files.pythonhosted.org/packages/ce/bf/abdafade008f0b1c9da10d934034cb670432d6cf6cbe38bbb53a1cfd6cf8/regex-2025.11.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:442d86cf1cfe4faabf97db7d901ef58347efd004934da045c745e7b5bd57ac49", size = 292669, upload-time = "2025-11-03T21:33:18.32Z" }, + { url = "https://files.pythonhosted.org/packages/f9/ef/0c357bb8edbd2ad8e273fcb9e1761bc37b8acbc6e1be050bebd6475f19c1/regex-2025.11.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:fd0a5e563c756de210bb964789b5abe4f114dacae9104a47e1a649b910361536", size = 291030, upload-time = "2025-11-03T21:33:20.048Z" }, + { url = "https://files.pythonhosted.org/packages/79/06/edbb67257596649b8fb088d6aeacbcb248ac195714b18a65e018bf4c0b50/regex-2025.11.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bf3490bcbb985a1ae97b2ce9ad1c0f06a852d5b19dde9b07bdf25bf224248c95", size = 807674, upload-time = "2025-11-03T21:33:21.797Z" }, + { url = "https://files.pythonhosted.org/packages/f4/d9/ad4deccfce0ea336296bd087f1a191543bb99ee1c53093dcd4c64d951d00/regex-2025.11.3-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3809988f0a8b8c9dcc0f92478d6501fac7200b9ec56aecf0ec21f4a2ec4b6009", size = 873451, upload-time = "2025-11-03T21:33:23.741Z" }, + { url = "https://files.pythonhosted.org/packages/13/75/a55a4724c56ef13e3e04acaab29df26582f6978c000ac9cd6810ad1f341f/regex-2025.11.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f4ff94e58e84aedb9c9fce66d4ef9f27a190285b451420f297c9a09f2b9abee9", size = 914980, upload-time = "2025-11-03T21:33:25.999Z" }, + { url = "https://files.pythonhosted.org/packages/67/1e/a1657ee15bd9116f70d4a530c736983eed997b361e20ecd8f5ca3759d5c5/regex-2025.11.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7eb542fd347ce61e1321b0a6b945d5701528dca0cd9759c2e3bb8bd57e47964d", size = 812852, upload-time = "2025-11-03T21:33:27.852Z" }, + { url = "https://files.pythonhosted.org/packages/b8/6f/f7516dde5506a588a561d296b2d0044839de06035bb486b326065b4c101e/regex-2025.11.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:d6c2d5919075a1f2e413c00b056ea0c2f065b3f5fe83c3d07d325ab92dce51d6", size = 795566, upload-time = "2025-11-03T21:33:32.364Z" }, + { url = "https://files.pythonhosted.org/packages/d9/dd/3d10b9e170cc16fb34cb2cef91513cf3df65f440b3366030631b2984a264/regex-2025.11.3-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:3f8bf11a4827cc7ce5a53d4ef6cddd5ad25595d3c1435ef08f76825851343154", size = 868463, upload-time = "2025-11-03T21:33:34.459Z" }, + { url = "https://files.pythonhosted.org/packages/f5/8e/935e6beff1695aa9085ff83195daccd72acc82c81793df480f34569330de/regex-2025.11.3-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:22c12d837298651e5550ac1d964e4ff57c3f56965fc1812c90c9fb2028eaf267", size = 854694, upload-time = "2025-11-03T21:33:36.793Z" }, + { url = "https://files.pythonhosted.org/packages/92/12/10650181a040978b2f5720a6a74d44f841371a3d984c2083fc1752e4acf6/regex-2025.11.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:62ba394a3dda9ad41c7c780f60f6e4a70988741415ae96f6d1bf6c239cf01379", size = 799691, upload-time = "2025-11-03T21:33:39.079Z" }, + { url = "https://files.pythonhosted.org/packages/67/90/8f37138181c9a7690e7e4cb388debbd389342db3c7381d636d2875940752/regex-2025.11.3-cp314-cp314t-win32.whl", hash = "sha256:4bf146dca15cdd53224a1bf46d628bd7590e4a07fbb69e720d561aea43a32b38", size = 274583, upload-time = "2025-11-03T21:33:41.302Z" }, + { url = "https://files.pythonhosted.org/packages/8f/cd/867f5ec442d56beb56f5f854f40abcfc75e11d10b11fdb1869dd39c63aaf/regex-2025.11.3-cp314-cp314t-win_amd64.whl", hash = "sha256:adad1a1bcf1c9e76346e091d22d23ac54ef28e1365117d99521631078dfec9de", size = 284286, upload-time = "2025-11-03T21:33:43.324Z" }, + { url = "https://files.pythonhosted.org/packages/20/31/32c0c4610cbc070362bf1d2e4ea86d1ea29014d400a6d6c2486fcfd57766/regex-2025.11.3-cp314-cp314t-win_arm64.whl", hash = "sha256:c54f768482cef41e219720013cd05933b6f971d9562544d691c68699bf2b6801", size = 274741, upload-time = "2025-11-03T21:33:45.557Z" }, +] + +[[package]] +name = "requests" +version = "2.32.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517, upload-time = "2025-08-18T20:46:02.573Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" }, +] + +[[package]] +name = "rich" +version = "14.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fb/d2/8920e102050a0de7bfabeb4c4614a49248cf8d5d7a8d01885fbb24dc767a/rich-14.2.0.tar.gz", hash = "sha256:73ff50c7c0c1c77c8243079283f4edb376f0f6442433aecb8ce7e6d0b92d1fe4", size = 219990, upload-time = "2025-10-09T14:16:53.064Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/25/7a/b0178788f8dc6cafce37a212c99565fa1fe7872c70c6c9c1e1a372d9d88f/rich-14.2.0-py3-none-any.whl", hash = "sha256:76bc51fe2e57d2b1be1f96c524b890b816e334ab4c1e45888799bfaab0021edd", size = 243393, upload-time = "2025-10-09T14:16:51.245Z" }, +] + +[[package]] +name = "rich-toolkit" +version = "0.17.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "rich" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/97/09/3f9b8d9daaf235195c626f21e03604c05b987404ee3bcacee0c1f67f2a8e/rich_toolkit-0.17.1.tar.gz", hash = "sha256:5af54df8d1dd9c8530e462e1bdcaed625c9b49f5a55b035aa0ba1c17bdb87c9a", size = 187925, upload-time = "2025-12-17T10:49:22.583Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7f/7b/15e55fa8a76d0d41bf34d965af78acdaf80a315907adb30de8b63c272694/rich_toolkit-0.17.1-py3-none-any.whl", hash = "sha256:96d24bb921ecd225ffce7c526a9149e74006410c05e6d405bd74ffd54d5631ed", size = 31412, upload-time = "2025-12-17T10:49:21.793Z" }, +] + +[[package]] +name = "rignore" +version = "0.7.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e5/f5/8bed2310abe4ae04b67a38374a4d311dd85220f5d8da56f47ae9361be0b0/rignore-0.7.6.tar.gz", hash = "sha256:00d3546cd793c30cb17921ce674d2c8f3a4b00501cb0e3dd0e82217dbeba2671", size = 57140, upload-time = "2025-11-05T21:41:21.968Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/86/7a/b970cd0138b0ece72eb28f086e933f9ed75b795716ad3de5ab22994b3b54/rignore-0.7.6-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:f3c74a7e5ee77aea669c95fdb3933f2a6c7549893700082e759128a29cf67e45", size = 884999, upload-time = "2025-11-05T20:42:38.373Z" }, + { url = "https://files.pythonhosted.org/packages/ca/05/23faca29616d8966ada63fb0e13c214107811fa9a0aba2275e4c7ca63bd5/rignore-0.7.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b7202404958f5fe3474bac91f65350f0b1dde1a5e05089f2946549b7e91e79ec", size = 824824, upload-time = "2025-11-05T20:42:22.1Z" }, + { url = "https://files.pythonhosted.org/packages/fa/2e/05a1e61f04cf2548524224f0b5f21ca19ea58f7273a863bac10846b8ff69/rignore-0.7.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6bde7c5835fa3905bfb7e329a4f1d7eccb676de63da7a3f934ddd5c06df20597", size = 899121, upload-time = "2025-11-05T20:40:48.94Z" }, + { url = "https://files.pythonhosted.org/packages/ff/35/71518847e10bdbf359badad8800e4681757a01f4777b3c5e03dbde8a42d8/rignore-0.7.6-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:626c3d4ba03af266694d25101bc1d8d16eda49c5feb86cedfec31c614fceca7d", size = 873813, upload-time = "2025-11-05T20:41:04.71Z" }, + { url = "https://files.pythonhosted.org/packages/f6/c8/32ae405d3e7fd4d9f9b7838f2fcca0a5005bb87fa514b83f83fd81c0df22/rignore-0.7.6-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0a43841e651e7a05a4274b9026cc408d1912e64016ede8cd4c145dae5d0635be", size = 1168019, upload-time = "2025-11-05T20:41:20.723Z" }, + { url = "https://files.pythonhosted.org/packages/25/98/013c955982bc5b4719bf9a5bea58be317eea28aa12bfd004025e3cd7c000/rignore-0.7.6-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7978c498dbf7f74d30cdb8859fe612167d8247f0acd377ae85180e34490725da", size = 942822, upload-time = "2025-11-05T20:41:36.99Z" }, + { url = "https://files.pythonhosted.org/packages/90/fb/9a3f3156c6ed30bcd597e63690353edac1fcffe9d382ad517722b56ac195/rignore-0.7.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2d22f72ab695c07d2d96d2a645208daff17084441b5d58c07378c9dd6f9c4c87", size = 959820, upload-time = "2025-11-05T20:42:06.364Z" }, + { url = "https://files.pythonhosted.org/packages/5e/b2/93bf609633021e9658acaff24cfb055d8cdaf7f5855d10ebb35307900dda/rignore-0.7.6-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d5bd8e1a91ed1a789b2cbe39eeea9204a6719d4f2cf443a9544b521a285a295f", size = 985050, upload-time = "2025-11-05T20:41:51.124Z" }, + { url = "https://files.pythonhosted.org/packages/69/bc/ec2d040469bdfd7b743df10f2201c5d285009a4263d506edbf7a06a090bb/rignore-0.7.6-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:bc1fc03efad5789365018e94ac4079f851a999bc154d1551c45179f7fcf45322", size = 1079164, upload-time = "2025-11-05T21:40:10.368Z" }, + { url = "https://files.pythonhosted.org/packages/df/26/4b635f4ea5baf4baa8ba8eee06163f6af6e76dfbe72deb57da34bb24b19d/rignore-0.7.6-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:ce2617fe28c51367fd8abfd4eeea9e61664af63c17d4ea00353d8ef56dfb95fa", size = 1139028, upload-time = "2025-11-05T21:40:27.977Z" }, + { url = "https://files.pythonhosted.org/packages/6a/54/a3147ebd1e477b06eb24e2c2c56d951ae5faa9045b7b36d7892fec5080d9/rignore-0.7.6-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:7c4ad2cee85068408e7819a38243043214e2c3047e9bd4c506f8de01c302709e", size = 1119024, upload-time = "2025-11-05T21:40:45.148Z" }, + { url = "https://files.pythonhosted.org/packages/fb/f4/27475db769a57cff18fe7e7267b36e6cdb5b1281caa185ba544171106cba/rignore-0.7.6-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:02cd240bfd59ecc3907766f4839cbba20530a2e470abca09eaa82225e4d946fb", size = 1128531, upload-time = "2025-11-05T21:41:02.734Z" }, + { url = "https://files.pythonhosted.org/packages/97/32/6e782d3b352e4349fa0e90bf75b13cb7f11d8908b36d9e2b262224b65d9a/rignore-0.7.6-cp310-cp310-win32.whl", hash = "sha256:fe2bd8fa1ff555259df54c376abc73855cb02628a474a40d51b358c3a1ddc55b", size = 646817, upload-time = "2025-11-05T21:41:47.51Z" }, + { url = "https://files.pythonhosted.org/packages/c0/8a/53185c69abb3bb362e8a46b8089999f820bf15655629ff8395107633c8ab/rignore-0.7.6-cp310-cp310-win_amd64.whl", hash = "sha256:d80afd6071c78baf3765ec698841071b19e41c326f994cfa69b5a1df676f5d39", size = 727001, upload-time = "2025-11-05T21:41:32.778Z" }, + { url = "https://files.pythonhosted.org/packages/25/41/b6e2be3069ef3b7f24e35d2911bd6deb83d20ed5642ad81d5a6d1c015473/rignore-0.7.6-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:40be8226e12d6653abbebaffaea2885f80374c1c8f76fe5ca9e0cadd120a272c", size = 885285, upload-time = "2025-11-05T20:42:39.763Z" }, + { url = "https://files.pythonhosted.org/packages/52/66/ba7f561b6062402022887706a7f2b2c2e2e2a28f1e3839202b0a2f77e36d/rignore-0.7.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:182f4e5e4064d947c756819446a7d4cdede8e756b8c81cf9e509683fe38778d7", size = 823882, upload-time = "2025-11-05T20:42:23.488Z" }, + { url = "https://files.pythonhosted.org/packages/f5/81/4087453df35a90b07370647b19017029324950c1b9137d54bf1f33843f17/rignore-0.7.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:16b63047648a916a87be1e51bb5c009063f1b8b6f5afe4f04f875525507e63dc", size = 899362, upload-time = "2025-11-05T20:40:51.111Z" }, + { url = "https://files.pythonhosted.org/packages/fb/c9/390a8fdfabb76d71416be773bd9f162977bd483084f68daf19da1dec88a6/rignore-0.7.6-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ba5524f5178deca4d7695e936604ebc742acb8958f9395776e1fcb8133f8257a", size = 873633, upload-time = "2025-11-05T20:41:06.193Z" }, + { url = "https://files.pythonhosted.org/packages/df/c9/79404fcb0faa76edfbc9df0901f8ef18568d1104919ebbbad6d608c888d1/rignore-0.7.6-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:62020dbb89a1dd4b84ab3d60547b3b2eb2723641d5fb198463643f71eaaed57d", size = 1167633, upload-time = "2025-11-05T20:41:22.491Z" }, + { url = "https://files.pythonhosted.org/packages/6e/8d/b3466d32d445d158a0aceb80919085baaae495b1f540fb942f91d93b5e5b/rignore-0.7.6-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b34acd532769d5a6f153a52a98dcb81615c949ab11697ce26b2eb776af2e174d", size = 941434, upload-time = "2025-11-05T20:41:38.151Z" }, + { url = "https://files.pythonhosted.org/packages/e8/40/9cd949761a7af5bc27022a939c91ff622d29c7a0b66d0c13a863097dde2d/rignore-0.7.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1c5e53b752f9de44dff7b3be3c98455ce3bf88e69d6dc0cf4f213346c5e3416c", size = 959461, upload-time = "2025-11-05T20:42:08.476Z" }, + { url = "https://files.pythonhosted.org/packages/b5/87/1e1a145731f73bdb7835e11f80da06f79a00d68b370d9a847de979575e6d/rignore-0.7.6-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:25b3536d13a5d6409ce85f23936f044576eeebf7b6db1d078051b288410fc049", size = 985323, upload-time = "2025-11-05T20:41:52.735Z" }, + { url = "https://files.pythonhosted.org/packages/6c/31/1ecff992fc3f59c4fcdcb6c07d5f6c1e6dfb55ccda19c083aca9d86fa1c6/rignore-0.7.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6e01cad2b0b92f6b1993f29fc01f23f2d78caf4bf93b11096d28e9d578eb08ce", size = 1079173, upload-time = "2025-11-05T21:40:12.007Z" }, + { url = "https://files.pythonhosted.org/packages/17/18/162eedadb4c2282fa4c521700dbf93c9b14b8842e8354f7d72b445b8d593/rignore-0.7.6-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:5991e46ab9b4868334c9e372ab0892b0150f3f586ff2b1e314272caeb38aaedb", size = 1139012, upload-time = "2025-11-05T21:40:29.399Z" }, + { url = "https://files.pythonhosted.org/packages/78/96/a9ca398a8af74bb143ad66c2a31303c894111977e28b0d0eab03867f1b43/rignore-0.7.6-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:6c8ae562e5d1246cba5eaeb92a47b2a279e7637102828dde41dcbe291f529a3e", size = 1118827, upload-time = "2025-11-05T21:40:46.6Z" }, + { url = "https://files.pythonhosted.org/packages/9f/22/1c1a65047df864def9a047dbb40bc0b580b8289a4280e62779cd61ae21f2/rignore-0.7.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:aaf938530dcc0b47c4cfa52807aa2e5bfd5ca6d57a621125fe293098692f6345", size = 1128182, upload-time = "2025-11-05T21:41:04.239Z" }, + { url = "https://files.pythonhosted.org/packages/bd/f4/1526eb01fdc2235aca1fd9d0189bee4021d009a8dcb0161540238c24166e/rignore-0.7.6-cp311-cp311-win32.whl", hash = "sha256:166ebce373105dd485ec213a6a2695986346e60c94ff3d84eb532a237b24a4d5", size = 646547, upload-time = "2025-11-05T21:41:49.439Z" }, + { url = "https://files.pythonhosted.org/packages/7c/c8/dda0983e1845706beb5826459781549a840fe5a7eb934abc523e8cd17814/rignore-0.7.6-cp311-cp311-win_amd64.whl", hash = "sha256:44f35ee844b1a8cea50d056e6a595190ce9d42d3cccf9f19d280ae5f3058973a", size = 727139, upload-time = "2025-11-05T21:41:34.367Z" }, + { url = "https://files.pythonhosted.org/packages/e3/47/eb1206b7bf65970d41190b879e1723fc6bbdb2d45e53565f28991a8d9d96/rignore-0.7.6-cp311-cp311-win_arm64.whl", hash = "sha256:14b58f3da4fa3d5c3fa865cab49821675371f5e979281c683e131ae29159a581", size = 657598, upload-time = "2025-11-05T21:41:23.758Z" }, + { url = "https://files.pythonhosted.org/packages/0b/0e/012556ef3047a2628842b44e753bb15f4dc46806780ff090f1e8fe4bf1eb/rignore-0.7.6-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:03e82348cb7234f8d9b2834f854400ddbbd04c0f8f35495119e66adbd37827a8", size = 883488, upload-time = "2025-11-05T20:42:41.359Z" }, + { url = "https://files.pythonhosted.org/packages/93/b0/d4f1f3fe9eb3f8e382d45ce5b0547ea01c4b7e0b4b4eb87bcd66a1d2b888/rignore-0.7.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b9e624f6be6116ea682e76c5feb71ea91255c67c86cb75befe774365b2931961", size = 820411, upload-time = "2025-11-05T20:42:24.782Z" }, + { url = "https://files.pythonhosted.org/packages/4a/c8/dea564b36dedac8de21c18e1851789545bc52a0c22ece9843444d5608a6a/rignore-0.7.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bda49950d405aa8d0ebe26af807c4e662dd281d926530f03f29690a2e07d649a", size = 897821, upload-time = "2025-11-05T20:40:52.613Z" }, + { url = "https://files.pythonhosted.org/packages/b3/2b/ee96db17ac1835e024c5d0742eefb7e46de60020385ac883dd3d1cde2c1f/rignore-0.7.6-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b5fd5ab3840b8c16851d327ed06e9b8be6459702a53e5ab1fc4073b684b3789e", size = 873963, upload-time = "2025-11-05T20:41:07.49Z" }, + { url = "https://files.pythonhosted.org/packages/a5/8c/ad5a57bbb9d14d5c7e5960f712a8a0b902472ea3f4a2138cbf70d1777b75/rignore-0.7.6-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ced2a248352636a5c77504cb755dc02c2eef9a820a44d3f33061ce1bb8a7f2d2", size = 1169216, upload-time = "2025-11-05T20:41:23.73Z" }, + { url = "https://files.pythonhosted.org/packages/80/e6/5b00bc2a6bc1701e6878fca798cf5d9125eb3113193e33078b6fc0d99123/rignore-0.7.6-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a04a3b73b75ddc12c9c9b21efcdaab33ca3832941d6f1d67bffd860941cd448a", size = 942942, upload-time = "2025-11-05T20:41:39.393Z" }, + { url = "https://files.pythonhosted.org/packages/85/e5/7f99bd0cc9818a91d0e8b9acc65b792e35750e3bdccd15a7ee75e64efca4/rignore-0.7.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d24321efac92140b7ec910ac7c53ab0f0c86a41133d2bb4b0e6a7c94967f44dd", size = 959787, upload-time = "2025-11-05T20:42:09.765Z" }, + { url = "https://files.pythonhosted.org/packages/55/54/2ffea79a7c1eabcede1926347ebc2a81bc6b81f447d05b52af9af14948b9/rignore-0.7.6-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:73c7aa109d41e593785c55fdaa89ad80b10330affa9f9d3e3a51fa695f739b20", size = 984245, upload-time = "2025-11-05T20:41:54.062Z" }, + { url = "https://files.pythonhosted.org/packages/41/f7/e80f55dfe0f35787fa482aa18689b9c8251e045076c35477deb0007b3277/rignore-0.7.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1734dc49d1e9501b07852ef44421f84d9f378da9fbeda729e77db71f49cac28b", size = 1078647, upload-time = "2025-11-05T21:40:13.463Z" }, + { url = "https://files.pythonhosted.org/packages/d4/cf/2c64f0b6725149f7c6e7e5a909d14354889b4beaadddaa5fff023ec71084/rignore-0.7.6-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:5719ea14ea2b652c0c0894be5dfde954e1853a80dea27dd2fbaa749618d837f5", size = 1139186, upload-time = "2025-11-05T21:40:31.27Z" }, + { url = "https://files.pythonhosted.org/packages/75/95/a86c84909ccc24af0d094b50d54697951e576c252a4d9f21b47b52af9598/rignore-0.7.6-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:8e23424fc7ce35726854f639cb7968151a792c0c3d9d082f7f67e0c362cfecca", size = 1117604, upload-time = "2025-11-05T21:40:48.07Z" }, + { url = "https://files.pythonhosted.org/packages/7f/5e/13b249613fd5d18d58662490ab910a9f0be758981d1797789913adb4e918/rignore-0.7.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3efdcf1dd84d45f3e2bd2f93303d9be103888f56dfa7c3349b5bf4f0657ec696", size = 1127725, upload-time = "2025-11-05T21:41:05.804Z" }, + { url = "https://files.pythonhosted.org/packages/c7/28/fa5dcd1e2e16982c359128664e3785f202d3eca9b22dd0b2f91c4b3d242f/rignore-0.7.6-cp312-cp312-win32.whl", hash = "sha256:ccca9d1a8b5234c76b71546fc3c134533b013f40495f394a65614a81f7387046", size = 646145, upload-time = "2025-11-05T21:41:51.096Z" }, + { url = "https://files.pythonhosted.org/packages/26/87/69387fb5dd81a0f771936381431780b8cf66fcd2cfe9495e1aaf41548931/rignore-0.7.6-cp312-cp312-win_amd64.whl", hash = "sha256:c96a285e4a8bfec0652e0bfcf42b1aabcdda1e7625f5006d188e3b1c87fdb543", size = 726090, upload-time = "2025-11-05T21:41:36.485Z" }, + { url = "https://files.pythonhosted.org/packages/24/5f/e8418108dcda8087fb198a6f81caadbcda9fd115d61154bf0df4d6d3619b/rignore-0.7.6-cp312-cp312-win_arm64.whl", hash = "sha256:a64a750e7a8277a323f01ca50b7784a764845f6cce2fe38831cb93f0508d0051", size = 656317, upload-time = "2025-11-05T21:41:25.305Z" }, + { url = "https://files.pythonhosted.org/packages/b7/8a/a4078f6e14932ac7edb171149c481de29969d96ddee3ece5dc4c26f9e0c3/rignore-0.7.6-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:2bdab1d31ec9b4fb1331980ee49ea051c0d7f7bb6baa28b3125ef03cdc48fdaf", size = 883057, upload-time = "2025-11-05T20:42:42.741Z" }, + { url = "https://files.pythonhosted.org/packages/f9/8f/f8daacd177db4bf7c2223bab41e630c52711f8af9ed279be2058d2fe4982/rignore-0.7.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:90f0a00ce0c866c275bf888271f1dc0d2140f29b82fcf33cdbda1e1a6af01010", size = 820150, upload-time = "2025-11-05T20:42:26.545Z" }, + { url = "https://files.pythonhosted.org/packages/36/31/b65b837e39c3f7064c426754714ac633b66b8c2290978af9d7f513e14aa9/rignore-0.7.6-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c1ad295537041dc2ed4b540fb1a3906bd9ede6ccdad3fe79770cd89e04e3c73c", size = 897406, upload-time = "2025-11-05T20:40:53.854Z" }, + { url = "https://files.pythonhosted.org/packages/ca/58/1970ce006c427e202ac7c081435719a076c478f07b3a23f469227788dc23/rignore-0.7.6-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f782dbd3a65a5ac85adfff69e5c6b101285ef3f845c3a3cae56a54bebf9fe116", size = 874050, upload-time = "2025-11-05T20:41:08.922Z" }, + { url = "https://files.pythonhosted.org/packages/d4/00/eb45db9f90137329072a732273be0d383cb7d7f50ddc8e0bceea34c1dfdf/rignore-0.7.6-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:65cece3b36e5b0826d946494734c0e6aaf5a0337e18ff55b071438efe13d559e", size = 1167835, upload-time = "2025-11-05T20:41:24.997Z" }, + { url = "https://files.pythonhosted.org/packages/f3/f1/6f1d72ddca41a64eed569680587a1236633587cc9f78136477ae69e2c88a/rignore-0.7.6-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d7e4bb66c13cd7602dc8931822c02dfbbd5252015c750ac5d6152b186f0a8be0", size = 941945, upload-time = "2025-11-05T20:41:40.628Z" }, + { url = "https://files.pythonhosted.org/packages/48/6f/2f178af1c1a276a065f563ec1e11e7a9e23d4996fd0465516afce4b5c636/rignore-0.7.6-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:297e500c15766e196f68aaaa70e8b6db85fa23fdc075b880d8231fdfba738cd7", size = 959067, upload-time = "2025-11-05T20:42:11.09Z" }, + { url = "https://files.pythonhosted.org/packages/5b/db/423a81c4c1e173877c7f9b5767dcaf1ab50484a94f60a0b2ed78be3fa765/rignore-0.7.6-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a07084211a8d35e1a5b1d32b9661a5ed20669970b369df0cf77da3adea3405de", size = 984438, upload-time = "2025-11-05T20:41:55.443Z" }, + { url = "https://files.pythonhosted.org/packages/31/eb/c4f92cc3f2825d501d3c46a244a671eb737fc1bcf7b05a3ecd34abb3e0d7/rignore-0.7.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:181eb2a975a22256a1441a9d2f15eb1292839ea3f05606620bd9e1938302cf79", size = 1078365, upload-time = "2025-11-05T21:40:15.148Z" }, + { url = "https://files.pythonhosted.org/packages/26/09/99442f02794bd7441bfc8ed1c7319e890449b816a7493b2db0e30af39095/rignore-0.7.6-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:7bbcdc52b5bf9f054b34ce4af5269df5d863d9c2456243338bc193c28022bd7b", size = 1139066, upload-time = "2025-11-05T21:40:32.771Z" }, + { url = "https://files.pythonhosted.org/packages/2c/88/bcfc21e520bba975410e9419450f4b90a2ac8236b9a80fd8130e87d098af/rignore-0.7.6-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:f2e027a6da21a7c8c0d87553c24ca5cc4364def18d146057862c23a96546238e", size = 1118036, upload-time = "2025-11-05T21:40:49.646Z" }, + { url = "https://files.pythonhosted.org/packages/e2/25/d37215e4562cda5c13312636393aea0bafe38d54d4e0517520a4cc0753ec/rignore-0.7.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ee4a18b82cbbc648e4aac1510066682fe62beb5dc88e2c67c53a83954e541360", size = 1127550, upload-time = "2025-11-05T21:41:07.648Z" }, + { url = "https://files.pythonhosted.org/packages/dc/76/a264ab38bfa1620ec12a8ff1c07778da89e16d8c0f3450b0333020d3d6dc/rignore-0.7.6-cp313-cp313-win32.whl", hash = "sha256:a7d7148b6e5e95035d4390396895adc384d37ff4e06781a36fe573bba7c283e5", size = 646097, upload-time = "2025-11-05T21:41:53.201Z" }, + { url = "https://files.pythonhosted.org/packages/62/44/3c31b8983c29ea8832b6082ddb1d07b90379c2d993bd20fce4487b71b4f4/rignore-0.7.6-cp313-cp313-win_amd64.whl", hash = "sha256:b037c4b15a64dced08fc12310ee844ec2284c4c5c1ca77bc37d0a04f7bff386e", size = 726170, upload-time = "2025-11-05T21:41:38.131Z" }, + { url = "https://files.pythonhosted.org/packages/aa/41/e26a075cab83debe41a42661262f606166157df84e0e02e2d904d134c0d8/rignore-0.7.6-cp313-cp313-win_arm64.whl", hash = "sha256:e47443de9b12fe569889bdbe020abe0e0b667516ee2ab435443f6d0869bd2804", size = 656184, upload-time = "2025-11-05T21:41:27.396Z" }, + { url = "https://files.pythonhosted.org/packages/9a/b9/1f5bd82b87e5550cd843ceb3768b4a8ef274eb63f29333cf2f29644b3d75/rignore-0.7.6-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:8e41be9fa8f2f47239ded8920cc283699a052ac4c371f77f5ac017ebeed75732", size = 882632, upload-time = "2025-11-05T20:42:44.063Z" }, + { url = "https://files.pythonhosted.org/packages/e9/6b/07714a3efe4a8048864e8a5b7db311ba51b921e15268b17defaebf56d3db/rignore-0.7.6-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:6dc1e171e52cefa6c20e60c05394a71165663b48bca6c7666dee4f778f2a7d90", size = 820760, upload-time = "2025-11-05T20:42:27.885Z" }, + { url = "https://files.pythonhosted.org/packages/ac/0f/348c829ea2d8d596e856371b14b9092f8a5dfbb62674ec9b3f67e4939a9d/rignore-0.7.6-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ce2268837c3600f82ab8db58f5834009dc638ee17103582960da668963bebc5", size = 899044, upload-time = "2025-11-05T20:40:55.336Z" }, + { url = "https://files.pythonhosted.org/packages/f0/30/2e1841a19b4dd23878d73edd5d82e998a83d5ed9570a89675f140ca8b2ad/rignore-0.7.6-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:690a3e1b54bfe77e89c4bacb13f046e642f8baadafc61d68f5a726f324a76ab6", size = 874144, upload-time = "2025-11-05T20:41:10.195Z" }, + { url = "https://files.pythonhosted.org/packages/c2/bf/0ce9beb2e5f64c30e3580bef09f5829236889f01511a125f98b83169b993/rignore-0.7.6-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:09d12ac7a0b6210c07bcd145007117ebd8abe99c8eeb383e9e4673910c2754b2", size = 1168062, upload-time = "2025-11-05T20:41:26.511Z" }, + { url = "https://files.pythonhosted.org/packages/b9/8b/571c178414eb4014969865317da8a02ce4cf5241a41676ef91a59aab24de/rignore-0.7.6-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2a2b2b74a8c60203b08452479b90e5ce3dbe96a916214bc9eb2e5af0b6a9beb0", size = 942542, upload-time = "2025-11-05T20:41:41.838Z" }, + { url = "https://files.pythonhosted.org/packages/19/62/7a3cf601d5a45137a7e2b89d10c05b5b86499190c4b7ca5c3c47d79ee519/rignore-0.7.6-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8fc5a531ef02131e44359419a366bfac57f773ea58f5278c2cdd915f7d10ea94", size = 958739, upload-time = "2025-11-05T20:42:12.463Z" }, + { url = "https://files.pythonhosted.org/packages/5f/1f/4261f6a0d7caf2058a5cde2f5045f565ab91aa7badc972b57d19ce58b14e/rignore-0.7.6-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b7a1f77d9c4cd7e76229e252614d963442686bfe12c787a49f4fe481df49e7a9", size = 984138, upload-time = "2025-11-05T20:41:56.775Z" }, + { url = "https://files.pythonhosted.org/packages/2b/bf/628dfe19c75e8ce1f45f7c248f5148b17dfa89a817f8e3552ab74c3ae812/rignore-0.7.6-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ead81f728682ba72b5b1c3d5846b011d3e0174da978de87c61645f2ed36659a7", size = 1079299, upload-time = "2025-11-05T21:40:16.639Z" }, + { url = "https://files.pythonhosted.org/packages/af/a5/be29c50f5c0c25c637ed32db8758fdf5b901a99e08b608971cda8afb293b/rignore-0.7.6-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:12ffd50f520c22ffdabed8cd8bfb567d9ac165b2b854d3e679f4bcaef11a9441", size = 1139618, upload-time = "2025-11-05T21:40:34.507Z" }, + { url = "https://files.pythonhosted.org/packages/2a/40/3c46cd7ce4fa05c20b525fd60f599165e820af66e66f2c371cd50644558f/rignore-0.7.6-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:e5a16890fbe3c894f8ca34b0fcacc2c200398d4d46ae654e03bc9b3dbf2a0a72", size = 1117626, upload-time = "2025-11-05T21:40:51.494Z" }, + { url = "https://files.pythonhosted.org/packages/8c/b9/aea926f263b8a29a23c75c2e0d8447965eb1879d3feb53cfcf84db67ed58/rignore-0.7.6-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:3abab3bf99e8a77488ef6c7c9a799fac22224c28fe9f25cc21aa7cc2b72bfc0b", size = 1128144, upload-time = "2025-11-05T21:41:09.169Z" }, + { url = "https://files.pythonhosted.org/packages/a4/f6/0d6242f8d0df7f2ecbe91679fefc1f75e7cd2072cb4f497abaab3f0f8523/rignore-0.7.6-cp314-cp314-win32.whl", hash = "sha256:eeef421c1782953c4375aa32f06ecae470c1285c6381eee2a30d2e02a5633001", size = 646385, upload-time = "2025-11-05T21:41:55.105Z" }, + { url = "https://files.pythonhosted.org/packages/d5/38/c0dcd7b10064f084343d6af26fe9414e46e9619c5f3224b5272e8e5d9956/rignore-0.7.6-cp314-cp314-win_amd64.whl", hash = "sha256:6aeed503b3b3d5af939b21d72a82521701a4bd3b89cd761da1e7dc78621af304", size = 725738, upload-time = "2025-11-05T21:41:39.736Z" }, + { url = "https://files.pythonhosted.org/packages/d9/7a/290f868296c1ece914d565757ab363b04730a728b544beb567ceb3b2d96f/rignore-0.7.6-cp314-cp314-win_arm64.whl", hash = "sha256:104f215b60b3c984c386c3e747d6ab4376d5656478694e22c7bd2f788ddd8304", size = 656008, upload-time = "2025-11-05T21:41:29.028Z" }, + { url = "https://files.pythonhosted.org/packages/ca/d2/3c74e3cd81fe8ea08a8dcd2d755c09ac2e8ad8fe409508904557b58383d3/rignore-0.7.6-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:bb24a5b947656dd94cb9e41c4bc8b23cec0c435b58be0d74a874f63c259549e8", size = 882835, upload-time = "2025-11-05T20:42:45.443Z" }, + { url = "https://files.pythonhosted.org/packages/77/61/a772a34b6b63154877433ac2d048364815b24c2dd308f76b212c408101a2/rignore-0.7.6-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5b1e33c9501cefe24b70a1eafd9821acfd0ebf0b35c3a379430a14df089993e3", size = 820301, upload-time = "2025-11-05T20:42:29.226Z" }, + { url = "https://files.pythonhosted.org/packages/71/30/054880b09c0b1b61d17eeb15279d8bf729c0ba52b36c3ada52fb827cbb3c/rignore-0.7.6-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bec3994665a44454df86deb762061e05cd4b61e3772f5b07d1882a8a0d2748d5", size = 897611, upload-time = "2025-11-05T20:40:56.475Z" }, + { url = "https://files.pythonhosted.org/packages/1e/40/b2d1c169f833d69931bf232600eaa3c7998ba4f9a402e43a822dad2ea9f2/rignore-0.7.6-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:26cba2edfe3cff1dfa72bddf65d316ddebf182f011f2f61538705d6dbaf54986", size = 873875, upload-time = "2025-11-05T20:41:11.561Z" }, + { url = "https://files.pythonhosted.org/packages/55/59/ca5ae93d83a1a60e44b21d87deb48b177a8db1b85e82fc8a9abb24a8986d/rignore-0.7.6-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ffa86694fec604c613696cb91e43892aa22e1fec5f9870e48f111c603e5ec4e9", size = 1167245, upload-time = "2025-11-05T20:41:28.29Z" }, + { url = "https://files.pythonhosted.org/packages/a5/52/cf3dce392ba2af806cba265aad6bcd9c48bb2a6cb5eee448d3319f6e505b/rignore-0.7.6-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:48efe2ed95aa8104145004afb15cdfa02bea5cdde8b0344afeb0434f0d989aa2", size = 941750, upload-time = "2025-11-05T20:41:43.111Z" }, + { url = "https://files.pythonhosted.org/packages/ec/be/3f344c6218d779395e785091d05396dfd8b625f6aafbe502746fcd880af2/rignore-0.7.6-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8dcae43eb44b7f2457fef7cc87f103f9a0013017a6f4e62182c565e924948f21", size = 958896, upload-time = "2025-11-05T20:42:13.784Z" }, + { url = "https://files.pythonhosted.org/packages/c9/34/d3fa71938aed7d00dcad87f0f9bcb02ad66c85d6ffc83ba31078ce53646a/rignore-0.7.6-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2cd649a7091c0dad2f11ef65630d30c698d505cbe8660dd395268e7c099cc99f", size = 983992, upload-time = "2025-11-05T20:41:58.022Z" }, + { url = "https://files.pythonhosted.org/packages/24/a4/52a697158e9920705bdbd0748d59fa63e0f3233fb92e9df9a71afbead6ca/rignore-0.7.6-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:42de84b0289d478d30ceb7ae59023f7b0527786a9a5b490830e080f0e4ea5aeb", size = 1078181, upload-time = "2025-11-05T21:40:18.151Z" }, + { url = "https://files.pythonhosted.org/packages/ac/65/aa76dbcdabf3787a6f0fd61b5cc8ed1e88580590556d6c0207960d2384bb/rignore-0.7.6-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:875a617e57b53b4acbc5a91de418233849711c02e29cc1f4f9febb2f928af013", size = 1139232, upload-time = "2025-11-05T21:40:35.966Z" }, + { url = "https://files.pythonhosted.org/packages/08/44/31b31a49b3233c6842acc1c0731aa1e7fb322a7170612acf30327f700b44/rignore-0.7.6-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:8703998902771e96e49968105207719f22926e4431b108450f3f430b4e268b7c", size = 1117349, upload-time = "2025-11-05T21:40:53.013Z" }, + { url = "https://files.pythonhosted.org/packages/e9/ae/1b199a2302c19c658cf74e5ee1427605234e8c91787cfba0015f2ace145b/rignore-0.7.6-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:602ef33f3e1b04c1e9a10a3c03f8bc3cef2d2383dcc250d309be42b49923cabc", size = 1127702, upload-time = "2025-11-05T21:41:10.881Z" }, + { url = "https://files.pythonhosted.org/packages/fc/d3/18210222b37e87e36357f7b300b7d98c6dd62b133771e71ae27acba83a4f/rignore-0.7.6-cp314-cp314t-win32.whl", hash = "sha256:c1d8f117f7da0a4a96a8daef3da75bc090e3792d30b8b12cfadc240c631353f9", size = 647033, upload-time = "2025-11-05T21:42:00.095Z" }, + { url = "https://files.pythonhosted.org/packages/3e/87/033eebfbee3ec7d92b3bb1717d8f68c88e6fc7de54537040f3b3a405726f/rignore-0.7.6-cp314-cp314t-win_amd64.whl", hash = "sha256:ca36e59408bec81de75d307c568c2d0d410fb880b1769be43611472c61e85c96", size = 725647, upload-time = "2025-11-05T21:41:44.449Z" }, + { url = "https://files.pythonhosted.org/packages/79/62/b88e5879512c55b8ee979c666ee6902adc4ed05007226de266410ae27965/rignore-0.7.6-cp314-cp314t-win_arm64.whl", hash = "sha256:b83adabeb3e8cf662cabe1931b83e165b88c526fa6af6b3aa90429686e474896", size = 656035, upload-time = "2025-11-05T21:41:31.13Z" }, + { url = "https://files.pythonhosted.org/packages/85/12/62d690b4644c330d7ac0f739b7f078190ab4308faa909a60842d0e4af5b2/rignore-0.7.6-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:c3d3a523af1cd4ed2c0cba8d277a32d329b0c96ef9901fb7ca45c8cfaccf31a5", size = 887462, upload-time = "2025-11-05T20:42:50.804Z" }, + { url = "https://files.pythonhosted.org/packages/05/bc/6528a0e97ed2bd7a7c329183367d1ffbc5b9762ae8348d88dae72cc9d1f5/rignore-0.7.6-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:990853566e65184a506e1e2af2d15045afad3ebaebb8859cb85b882081915110", size = 826918, upload-time = "2025-11-05T20:42:33.689Z" }, + { url = "https://files.pythonhosted.org/packages/3e/2c/7d7bad116e09a04e9e1688c6f891fa2d4fd33f11b69ac0bd92419ddebeae/rignore-0.7.6-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1cab9ff2e436ce7240d7ee301c8ef806ed77c1fd6b8a8239ff65f9bbbcb5b8a3", size = 900922, upload-time = "2025-11-05T20:41:00.361Z" }, + { url = "https://files.pythonhosted.org/packages/09/ba/e5ea89fbde8e37a90ce456e31c5e9d85512cef5ae38e0f4d2426eb776a19/rignore-0.7.6-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d1a6671b2082c13bfd9a5cf4ce64670f832a6d41470556112c4ab0b6519b2fc4", size = 876987, upload-time = "2025-11-05T20:41:16.219Z" }, + { url = "https://files.pythonhosted.org/packages/d0/fb/93d14193f0ec0c3d35b763f0a000e9780f63b2031f3d3756442c2152622d/rignore-0.7.6-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2468729b4c5295c199d084ab88a40afcb7c8b974276805105239c07855bbacee", size = 1171110, upload-time = "2025-11-05T20:41:32.631Z" }, + { url = "https://files.pythonhosted.org/packages/9e/46/08436312ff96ffa29cfa4e1a987efc37e094531db46ba5e9fda9bb792afd/rignore-0.7.6-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:775710777fd71e5fdf54df69cdc249996a1d6f447a2b5bfb86dbf033fddd9cf9", size = 943339, upload-time = "2025-11-05T20:41:47.128Z" }, + { url = "https://files.pythonhosted.org/packages/34/28/3b3c51328f505cfaf7e53f408f78a1e955d561135d02f9cb0341ea99f69a/rignore-0.7.6-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4565407f4a77f72cf9d91469e75d15d375f755f0a01236bb8aaa176278cc7085", size = 961680, upload-time = "2025-11-05T20:42:18.061Z" }, + { url = "https://files.pythonhosted.org/packages/5c/9e/cbff75c8676d4f4a90bd58a1581249d255c7305141b0868f0abc0324836b/rignore-0.7.6-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:dc44c33f8fb2d5c9da748de7a6e6653a78aa740655e7409895e94a247ffa97c8", size = 987045, upload-time = "2025-11-05T20:42:02.315Z" }, + { url = "https://files.pythonhosted.org/packages/8c/25/d802d1d369502a7ddb8816059e7c79d2d913e17df975b863418e0aca4d8a/rignore-0.7.6-pp310-pypy310_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:8f32478f05540513c11923e8838afab9efef0131d66dca7f67f0e1bbd118af6a", size = 1080310, upload-time = "2025-11-05T21:40:23.184Z" }, + { url = "https://files.pythonhosted.org/packages/43/f0/250b785c2e473b1ab763eaf2be820934c2a5409a722e94b279dddac21c7d/rignore-0.7.6-pp310-pypy310_pp73-musllinux_1_2_armv7l.whl", hash = "sha256:1b63a3dd76225ea35b01dd6596aa90b275b5d0f71d6dc28fce6dd295d98614aa", size = 1140998, upload-time = "2025-11-05T21:40:40.603Z" }, + { url = "https://files.pythonhosted.org/packages/f5/d6/bb42fd2a8bba6aea327962656e20621fd495523259db40cfb4c5f760f05c/rignore-0.7.6-pp310-pypy310_pp73-musllinux_1_2_i686.whl", hash = "sha256:fe6c41175c36554a4ef0994cd1b4dbd6d73156fca779066456b781707402048e", size = 1121178, upload-time = "2025-11-05T21:40:57.585Z" }, + { url = "https://files.pythonhosted.org/packages/97/f4/aeb548374129dce3dc191a4bb598c944d9ed663f467b9af830315d86059c/rignore-0.7.6-pp310-pypy310_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:9a0c6792406ae36f4e7664dc772da909451d46432ff8485774526232d4885063", size = 1130190, upload-time = "2025-11-05T21:41:16.403Z" }, + { url = "https://files.pythonhosted.org/packages/82/78/a6250ff0c49a3cdb943910ada4116e708118e9b901c878cfae616c80a904/rignore-0.7.6-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:a20b6fb61bcced9a83dfcca6599ad45182b06ba720cff7c8d891e5b78db5b65f", size = 886470, upload-time = "2025-11-05T20:42:52.314Z" }, + { url = "https://files.pythonhosted.org/packages/35/af/c69c0c51b8f9f7914d95c4ea91c29a2ac067572048cae95dd6d2efdbe05d/rignore-0.7.6-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:392dcabfecbe176c9ebbcb40d85a5e86a5989559c4f988c2741da7daf1b5be25", size = 825976, upload-time = "2025-11-05T20:42:35.118Z" }, + { url = "https://files.pythonhosted.org/packages/f1/d2/1b264f56132264ea609d3213ab603d6a27016b19559a1a1ede1a66a03dcd/rignore-0.7.6-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22baa462abdc36fdd5a5e2dae423107723351b85ff093762f9261148b9d0a04a", size = 899739, upload-time = "2025-11-05T20:41:01.518Z" }, + { url = "https://files.pythonhosted.org/packages/55/e4/b3c5dfdd8d8a10741dfe7199ef45d19a0e42d0c13aa377c83bd6caf65d90/rignore-0.7.6-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:53fb28882d2538cb2d231972146c4927a9d9455e62b209f85d634408c4103538", size = 874843, upload-time = "2025-11-05T20:41:17.687Z" }, + { url = "https://files.pythonhosted.org/packages/cc/10/d6f3750233881a2a154cefc9a6a0a9b19da526b19f7f08221b552c6f827d/rignore-0.7.6-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:87409f7eeb1103d6b77f3472a3a0d9a5953e3ae804a55080bdcb0120ee43995b", size = 1170348, upload-time = "2025-11-05T20:41:34.21Z" }, + { url = "https://files.pythonhosted.org/packages/6e/10/ad98ca05c9771c15af734cee18114a3c280914b6e34fde9ffea2e61e88aa/rignore-0.7.6-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:684014e42e4341ab3ea23a203551857fcc03a7f8ae96ca3aefb824663f55db32", size = 942315, upload-time = "2025-11-05T20:41:48.508Z" }, + { url = "https://files.pythonhosted.org/packages/de/00/ab5c0f872acb60d534e687e629c17e0896c62da9b389c66d3aa16b817aa8/rignore-0.7.6-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:77356ebb01ba13f8a425c3d30fcad40e57719c0e37670d022d560884a30e4767", size = 961047, upload-time = "2025-11-05T20:42:19.403Z" }, + { url = "https://files.pythonhosted.org/packages/b8/86/3030fdc363a8f0d1cd155b4c453d6db9bab47a24fcc64d03f61d9d78fe6a/rignore-0.7.6-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6cbd8a48abbd3747a6c830393cd578782fab5d43f4deea48c5f5e344b8fed2b0", size = 986090, upload-time = "2025-11-05T20:42:03.581Z" }, + { url = "https://files.pythonhosted.org/packages/33/b8/133aa4002cee0ebbb39362f94e4898eec7fbd09cec9fcbce1cd65b355b7f/rignore-0.7.6-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:2673225dcec7f90497e79438c35e34638d0d0391ccea3cbb79bfb9adc0dc5bd7", size = 1079656, upload-time = "2025-11-05T21:40:24.89Z" }, + { url = "https://files.pythonhosted.org/packages/67/56/36d5d34210e5e7dfcd134eed8335b19e80ae940ee758f493e4f2b344dd70/rignore-0.7.6-pp311-pypy311_pp73-musllinux_1_2_armv7l.whl", hash = "sha256:c081f17290d8a2b96052b79207622aa635686ea39d502b976836384ede3d303c", size = 1139789, upload-time = "2025-11-05T21:40:42.119Z" }, + { url = "https://files.pythonhosted.org/packages/6b/5b/bb4f9420802bf73678033a4a55ab1bede36ce2e9b41fec5f966d83d932b3/rignore-0.7.6-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:57e8327aacc27f921968cb2a174f9e47b084ce9a7dd0122c8132d22358f6bd79", size = 1120308, upload-time = "2025-11-05T21:40:59.402Z" }, + { url = "https://files.pythonhosted.org/packages/ce/8b/a1299085b28a2f6135e30370b126e3c5055b61908622f2488ade67641479/rignore-0.7.6-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:d8955b57e42f2a5434670d5aa7b75eaf6e74602ccd8955dddf7045379cd762fb", size = 1129444, upload-time = "2025-11-05T21:41:17.906Z" }, +] + +[[package]] +name = "rlp" +version = "4.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "eth-utils" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1b/2d/439b0728a92964a04d9c88ea1ca9ebb128893fbbd5834faa31f987f2fd4c/rlp-4.1.0.tar.gz", hash = "sha256:be07564270a96f3e225e2c107db263de96b5bc1f27722d2855bd3459a08e95a9", size = 33429, upload-time = "2025-02-04T22:05:59.089Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/99/fb/e4c0ced9893b84ac95b7181d69a9786ce5879aeb3bbbcbba80a164f85d6a/rlp-4.1.0-py3-none-any.whl", hash = "sha256:8eca394c579bad34ee0b937aecb96a57052ff3716e19c7a578883e767bc5da6f", size = 19973, upload-time = "2025-02-04T22:05:57.05Z" }, +] + +[[package]] +name = "ruff" +version = "0.14.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/57/08/52232a877978dd8f9cf2aeddce3e611b40a63287dfca29b6b8da791f5e8d/ruff-0.14.10.tar.gz", hash = "sha256:9a2e830f075d1a42cd28420d7809ace390832a490ed0966fe373ba288e77aaf4", size = 5859763, upload-time = "2025-12-18T19:28:57.98Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/60/01/933704d69f3f05ee16ef11406b78881733c186fe14b6a46b05cfcaf6d3b2/ruff-0.14.10-py3-none-linux_armv6l.whl", hash = "sha256:7a3ce585f2ade3e1f29ec1b92df13e3da262178df8c8bdf876f48fa0e8316c49", size = 13527080, upload-time = "2025-12-18T19:29:25.642Z" }, + { url = "https://files.pythonhosted.org/packages/df/58/a0349197a7dfa603ffb7f5b0470391efa79ddc327c1e29c4851e85b09cc5/ruff-0.14.10-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:674f9be9372907f7257c51f1d4fc902cb7cf014b9980152b802794317941f08f", size = 13797320, upload-time = "2025-12-18T19:29:02.571Z" }, + { url = "https://files.pythonhosted.org/packages/7b/82/36be59f00a6082e38c23536df4e71cdbc6af8d7c707eade97fcad5c98235/ruff-0.14.10-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d85713d522348837ef9df8efca33ccb8bd6fcfc86a2cde3ccb4bc9d28a18003d", size = 12918434, upload-time = "2025-12-18T19:28:51.202Z" }, + { url = "https://files.pythonhosted.org/packages/a6/00/45c62a7f7e34da92a25804f813ebe05c88aa9e0c25e5cb5a7d23dd7450e3/ruff-0.14.10-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6987ebe0501ae4f4308d7d24e2d0fe3d7a98430f5adfd0f1fead050a740a3a77", size = 13371961, upload-time = "2025-12-18T19:29:04.991Z" }, + { url = "https://files.pythonhosted.org/packages/40/31/a5906d60f0405f7e57045a70f2d57084a93ca7425f22e1d66904769d1628/ruff-0.14.10-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:16a01dfb7b9e4eee556fbfd5392806b1b8550c9b4a9f6acd3dbe6812b193c70a", size = 13275629, upload-time = "2025-12-18T19:29:21.381Z" }, + { url = "https://files.pythonhosted.org/packages/3e/60/61c0087df21894cf9d928dc04bcd4fb10e8b2e8dca7b1a276ba2155b2002/ruff-0.14.10-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7165d31a925b7a294465fa81be8c12a0e9b60fb02bf177e79067c867e71f8b1f", size = 14029234, upload-time = "2025-12-18T19:29:00.132Z" }, + { url = "https://files.pythonhosted.org/packages/44/84/77d911bee3b92348b6e5dab5a0c898d87084ea03ac5dc708f46d88407def/ruff-0.14.10-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:c561695675b972effb0c0a45db233f2c816ff3da8dcfbe7dfc7eed625f218935", size = 15449890, upload-time = "2025-12-18T19:28:53.573Z" }, + { url = "https://files.pythonhosted.org/packages/e9/36/480206eaefa24a7ec321582dda580443a8f0671fdbf6b1c80e9c3e93a16a/ruff-0.14.10-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4bb98fcbbc61725968893682fd4df8966a34611239c9fd07a1f6a07e7103d08e", size = 15123172, upload-time = "2025-12-18T19:29:23.453Z" }, + { url = "https://files.pythonhosted.org/packages/5c/38/68e414156015ba80cef5473d57919d27dfb62ec804b96180bafdeaf0e090/ruff-0.14.10-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f24b47993a9d8cb858429e97bdf8544c78029f09b520af615c1d261bf827001d", size = 14460260, upload-time = "2025-12-18T19:29:27.808Z" }, + { url = "https://files.pythonhosted.org/packages/b3/19/9e050c0dca8aba824d67cc0db69fb459c28d8cd3f6855b1405b3f29cc91d/ruff-0.14.10-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:59aabd2e2c4fd614d2862e7939c34a532c04f1084476d6833dddef4afab87e9f", size = 14229978, upload-time = "2025-12-18T19:29:11.32Z" }, + { url = "https://files.pythonhosted.org/packages/51/eb/e8dd1dd6e05b9e695aa9dd420f4577debdd0f87a5ff2fedda33c09e9be8c/ruff-0.14.10-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:213db2b2e44be8625002dbea33bb9c60c66ea2c07c084a00d55732689d697a7f", size = 14338036, upload-time = "2025-12-18T19:29:09.184Z" }, + { url = "https://files.pythonhosted.org/packages/6a/12/f3e3a505db7c19303b70af370d137795fcfec136d670d5de5391e295c134/ruff-0.14.10-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:b914c40ab64865a17a9a5b67911d14df72346a634527240039eb3bd650e5979d", size = 13264051, upload-time = "2025-12-18T19:29:13.431Z" }, + { url = "https://files.pythonhosted.org/packages/08/64/8c3a47eaccfef8ac20e0484e68e0772013eb85802f8a9f7603ca751eb166/ruff-0.14.10-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:1484983559f026788e3a5c07c81ef7d1e97c1c78ed03041a18f75df104c45405", size = 13283998, upload-time = "2025-12-18T19:29:06.994Z" }, + { url = "https://files.pythonhosted.org/packages/12/84/534a5506f4074e5cc0529e5cd96cfc01bb480e460c7edf5af70d2bcae55e/ruff-0.14.10-py3-none-musllinux_1_2_i686.whl", hash = "sha256:c70427132db492d25f982fffc8d6c7535cc2fd2c83fc8888f05caaa248521e60", size = 13601891, upload-time = "2025-12-18T19:28:55.811Z" }, + { url = "https://files.pythonhosted.org/packages/0d/1e/14c916087d8598917dbad9b2921d340f7884824ad6e9c55de948a93b106d/ruff-0.14.10-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:5bcf45b681e9f1ee6445d317ce1fa9d6cba9a6049542d1c3d5b5958986be8830", size = 14336660, upload-time = "2025-12-18T19:29:16.531Z" }, + { url = "https://files.pythonhosted.org/packages/f2/1c/d7b67ab43f30013b47c12b42d1acd354c195351a3f7a1d67f59e54227ede/ruff-0.14.10-py3-none-win32.whl", hash = "sha256:104c49fc7ab73f3f3a758039adea978869a918f31b73280db175b43a2d9b51d6", size = 13196187, upload-time = "2025-12-18T19:29:19.006Z" }, + { url = "https://files.pythonhosted.org/packages/fb/9c/896c862e13886fae2af961bef3e6312db9ebc6adc2b156fe95e615dee8c1/ruff-0.14.10-py3-none-win_amd64.whl", hash = "sha256:466297bd73638c6bdf06485683e812db1c00c7ac96d4ddd0294a338c62fdc154", size = 14661283, upload-time = "2025-12-18T19:29:30.16Z" }, + { url = "https://files.pythonhosted.org/packages/74/31/b0e29d572670dca3674eeee78e418f20bdf97fa8aa9ea71380885e175ca0/ruff-0.14.10-py3-none-win_arm64.whl", hash = "sha256:e51d046cf6dda98a4633b8a8a771451107413b0f07183b2bef03f075599e44e6", size = 13729839, upload-time = "2025-12-18T19:28:48.636Z" }, +] + +[[package]] +name = "sentry-sdk" +version = "2.48.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/40/f0/0e9dc590513d5e742d7799e2038df3a05167cba084c6ca4f3cdd75b55164/sentry_sdk-2.48.0.tar.gz", hash = "sha256:5213190977ff7fdff8a58b722fb807f8d5524a80488626ebeda1b5676c0c1473", size = 384828, upload-time = "2025-12-16T14:55:41.722Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4d/19/8d77f9992e5cbfcaa9133c3bf63b4fbbb051248802e1e803fed5c552fbb2/sentry_sdk-2.48.0-py2.py3-none-any.whl", hash = "sha256:6b12ac256769d41825d9b7518444e57fa35b5642df4c7c5e322af4d2c8721172", size = 414555, upload-time = "2025-12-16T14:55:40.152Z" }, +] + +[[package]] +name = "shellingham" +version = "1.5.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310, upload-time = "2023-10-24T04:13:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755, upload-time = "2023-10-24T04:13:38.866Z" }, +] + +[[package]] +name = "solana" +version = "0.36.11" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "construct-typing" }, + { name = "httpx" }, + { name = "solders" }, + { name = "typing-extensions" }, + { name = "websockets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5e/66/b8cd6e4d95bfe46798942ace31935e7799005a4e2180869dc7bac6b75be9/solana-0.36.11.tar.gz", hash = "sha256:2fdcf483674f4b88fe6510524bf3234a5837d19fe1815aa5a285f2739d28b3a3", size = 54516, upload-time = "2026-01-03T02:11:52.243Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8b/8d/807eebf0560759ad90464060e0d1d87ff5409beb6ed56104c553a83a976a/solana-0.36.11-py3-none-any.whl", hash = "sha256:1d659decc67a40ee1e9b5ded373a076b87cf3b4bd0645e120d16d9348c2025ba", size = 64786, upload-time = "2026-01-03T02:11:50.811Z" }, +] + +[[package]] +name = "solders" +version = "0.27.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jsonalias" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/25/80a81bb3dc4c70329dd0016edbdfbf2e8d8300a98ab9cd1a6ea0266bda7c/solders-0.27.1.tar.gz", hash = "sha256:7d8a24ad2f193afcdc02d6f3975917a7358b0f0ab7f4b3695b135ff2008222c8", size = 180923, upload-time = "2025-11-15T07:50:52.32Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4b/6b/0c0ee4766705824261779d00229fb95308d6b28422613e0e2af577f60ee3/solders-0.27.1-cp38-abi3-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:4dcd8e766bab24afbe9e0ae363d86f9810457e04b00c8a9149f69ca939ed587c", size = 24883435, upload-time = "2025-11-15T07:50:34.42Z" }, + { url = "https://files.pythonhosted.org/packages/33/1c/be04a1b26e18c409dd006d214198dc03f0b657c1cb34f4c83b763f8348f0/solders-0.27.1-cp38-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5d87b145cc0129095f9cff8c7f28d2e910bc5b5a4cf257c263b08a4b95f111dd", size = 6480729, upload-time = "2025-11-15T07:50:37.323Z" }, + { url = "https://files.pythonhosted.org/packages/48/03/98dc73c266b11ed5c13b3933510a1aa115becf97f45bec1a22da9d03ffa9/solders-0.27.1-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6082bbe46b7b1b2b005d046011f89fcae75fc5ea4f1a0ef5c2e9dfb5fe7930ce", size = 12744782, upload-time = "2025-11-15T07:50:39.283Z" }, + { url = "https://files.pythonhosted.org/packages/a0/39/35384d8fb80d05937bd9e8af7237cfe3f0d017c8aba357209d90d428f3a0/solders-0.27.1-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:ccb821c2e4af43d976f312086f248a67352b3986e5f4c87af41cfeac6d8b5683", size = 6601257, upload-time = "2025-11-15T07:50:41.738Z" }, + { url = "https://files.pythonhosted.org/packages/8c/65/8989e521142473bf1130613476a4449e106bb97ed6cc86097f6f519b1234/solders-0.27.1-cp38-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:663a10566ae81f67c4515d4db5fbf51b735204741728c1a5cde11c4e019a51df", size = 7277802, upload-time = "2025-11-15T07:50:43.789Z" }, + { url = "https://files.pythonhosted.org/packages/f2/41/87ecf12cec0e7aa9c67b0cf1b8079fb28aa0af91e97328a3bd0c5e3001ba/solders-0.27.1-cp38-abi3-musllinux_1_2_i686.whl", hash = "sha256:d14f05a77dbbf7966fb26f255c81302e6127550bdb66c2fdc99f522043fdf376", size = 7082541, upload-time = "2025-11-15T07:50:45.847Z" }, + { url = "https://files.pythonhosted.org/packages/33/b9/35e6f59b41bb205b26c7318fcdca43f3d59464fd3ddc13d36f36427f64d4/solders-0.27.1-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:f778eeab411acec0a765a01c7b772f8eca8a8543d98276bd83cb826960da211b", size = 6845568, upload-time = "2025-11-15T07:50:47.698Z" }, + { url = "https://files.pythonhosted.org/packages/9b/f3/14ed12d8d5047ababaca3271f82ebbf500ff74b6358f283962232103a12d/solders-0.27.1-cp38-abi3-win_amd64.whl", hash = "sha256:f3b787c29570a46d219c7a67543d8b0fadc73abda346653aa20e8eccd839e78b", size = 5295092, upload-time = "2025-11-15T07:50:50.517Z" }, +] + +[[package]] +name = "starlette" +version = "0.50.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ba/b8/73a0e6a6e079a9d9cfa64113d771e421640b6f679a52eeb9b32f72d871a1/starlette-0.50.0.tar.gz", hash = "sha256:a2a17b22203254bcbc2e1f926d2d55f3f9497f769416b3190768befe598fa3ca", size = 2646985, upload-time = "2025-11-01T15:25:27.516Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d9/52/1064f510b141bd54025f9b55105e26d1fa970b9be67ad766380a3c9b74b0/starlette-0.50.0-py3-none-any.whl", hash = "sha256:9e5391843ec9b6e472eed1365a78c8098cfceb7a74bfd4d6b1c0c0095efb3bca", size = 74033, upload-time = "2025-11-01T15:25:25.461Z" }, +] + +[[package]] +name = "tomli" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/52/ed/3f73f72945444548f33eba9a87fc7a6e969915e7b1acc8260b30e1f76a2f/tomli-2.3.0.tar.gz", hash = "sha256:64be704a875d2a59753d80ee8a533c3fe183e3f06807ff7dc2232938ccb01549", size = 17392, upload-time = "2025-10-08T22:01:47.119Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/2e/299f62b401438d5fe1624119c723f5d877acc86a4c2492da405626665f12/tomli-2.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:88bd15eb972f3664f5ed4b57c1634a97153b4bac4479dcb6a495f41921eb7f45", size = 153236, upload-time = "2025-10-08T22:01:00.137Z" }, + { url = "https://files.pythonhosted.org/packages/86/7f/d8fffe6a7aefdb61bced88fcb5e280cfd71e08939da5894161bd71bea022/tomli-2.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:883b1c0d6398a6a9d29b508c331fa56adbcdff647f6ace4dfca0f50e90dfd0ba", size = 148084, upload-time = "2025-10-08T22:01:01.63Z" }, + { url = "https://files.pythonhosted.org/packages/47/5c/24935fb6a2ee63e86d80e4d3b58b222dafaf438c416752c8b58537c8b89a/tomli-2.3.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d1381caf13ab9f300e30dd8feadb3de072aeb86f1d34a8569453ff32a7dea4bf", size = 234832, upload-time = "2025-10-08T22:01:02.543Z" }, + { url = "https://files.pythonhosted.org/packages/89/da/75dfd804fc11e6612846758a23f13271b76d577e299592b4371a4ca4cd09/tomli-2.3.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a0e285d2649b78c0d9027570d4da3425bdb49830a6156121360b3f8511ea3441", size = 242052, upload-time = "2025-10-08T22:01:03.836Z" }, + { url = "https://files.pythonhosted.org/packages/70/8c/f48ac899f7b3ca7eb13af73bacbc93aec37f9c954df3c08ad96991c8c373/tomli-2.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0a154a9ae14bfcf5d8917a59b51ffd5a3ac1fd149b71b47a3a104ca4edcfa845", size = 239555, upload-time = "2025-10-08T22:01:04.834Z" }, + { url = "https://files.pythonhosted.org/packages/ba/28/72f8afd73f1d0e7829bfc093f4cb98ce0a40ffc0cc997009ee1ed94ba705/tomli-2.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:74bf8464ff93e413514fefd2be591c3b0b23231a77f901db1eb30d6f712fc42c", size = 245128, upload-time = "2025-10-08T22:01:05.84Z" }, + { url = "https://files.pythonhosted.org/packages/b6/eb/a7679c8ac85208706d27436e8d421dfa39d4c914dcf5fa8083a9305f58d9/tomli-2.3.0-cp311-cp311-win32.whl", hash = "sha256:00b5f5d95bbfc7d12f91ad8c593a1659b6387b43f054104cda404be6bda62456", size = 96445, upload-time = "2025-10-08T22:01:06.896Z" }, + { url = "https://files.pythonhosted.org/packages/0a/fe/3d3420c4cb1ad9cb462fb52967080575f15898da97e21cb6f1361d505383/tomli-2.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:4dc4ce8483a5d429ab602f111a93a6ab1ed425eae3122032db7e9acf449451be", size = 107165, upload-time = "2025-10-08T22:01:08.107Z" }, + { url = "https://files.pythonhosted.org/packages/ff/b7/40f36368fcabc518bb11c8f06379a0fd631985046c038aca08c6d6a43c6e/tomli-2.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d7d86942e56ded512a594786a5ba0a5e521d02529b3826e7761a05138341a2ac", size = 154891, upload-time = "2025-10-08T22:01:09.082Z" }, + { url = "https://files.pythonhosted.org/packages/f9/3f/d9dd692199e3b3aab2e4e4dd948abd0f790d9ded8cd10cbaae276a898434/tomli-2.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:73ee0b47d4dad1c5e996e3cd33b8a76a50167ae5f96a2607cbe8cc773506ab22", size = 148796, upload-time = "2025-10-08T22:01:10.266Z" }, + { url = "https://files.pythonhosted.org/packages/60/83/59bff4996c2cf9f9387a0f5a3394629c7efa5ef16142076a23a90f1955fa/tomli-2.3.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:792262b94d5d0a466afb5bc63c7daa9d75520110971ee269152083270998316f", size = 242121, upload-time = "2025-10-08T22:01:11.332Z" }, + { url = "https://files.pythonhosted.org/packages/45/e5/7c5119ff39de8693d6baab6c0b6dcb556d192c165596e9fc231ea1052041/tomli-2.3.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4f195fe57ecceac95a66a75ac24d9d5fbc98ef0962e09b2eddec5d39375aae52", size = 250070, upload-time = "2025-10-08T22:01:12.498Z" }, + { url = "https://files.pythonhosted.org/packages/45/12/ad5126d3a278f27e6701abde51d342aa78d06e27ce2bb596a01f7709a5a2/tomli-2.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e31d432427dcbf4d86958c184b9bfd1e96b5b71f8eb17e6d02531f434fd335b8", size = 245859, upload-time = "2025-10-08T22:01:13.551Z" }, + { url = "https://files.pythonhosted.org/packages/fb/a1/4d6865da6a71c603cfe6ad0e6556c73c76548557a8d658f9e3b142df245f/tomli-2.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7b0882799624980785240ab732537fcfc372601015c00f7fc367c55308c186f6", size = 250296, upload-time = "2025-10-08T22:01:14.614Z" }, + { url = "https://files.pythonhosted.org/packages/a0/b7/a7a7042715d55c9ba6e8b196d65d2cb662578b4d8cd17d882d45322b0d78/tomli-2.3.0-cp312-cp312-win32.whl", hash = "sha256:ff72b71b5d10d22ecb084d345fc26f42b5143c5533db5e2eaba7d2d335358876", size = 97124, upload-time = "2025-10-08T22:01:15.629Z" }, + { url = "https://files.pythonhosted.org/packages/06/1e/f22f100db15a68b520664eb3328fb0ae4e90530887928558112c8d1f4515/tomli-2.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:1cb4ed918939151a03f33d4242ccd0aa5f11b3547d0cf30f7c74a408a5b99878", size = 107698, upload-time = "2025-10-08T22:01:16.51Z" }, + { url = "https://files.pythonhosted.org/packages/89/48/06ee6eabe4fdd9ecd48bf488f4ac783844fd777f547b8d1b61c11939974e/tomli-2.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5192f562738228945d7b13d4930baffda67b69425a7f0da96d360b0a3888136b", size = 154819, upload-time = "2025-10-08T22:01:17.964Z" }, + { url = "https://files.pythonhosted.org/packages/f1/01/88793757d54d8937015c75dcdfb673c65471945f6be98e6a0410fba167ed/tomli-2.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:be71c93a63d738597996be9528f4abe628d1adf5e6eb11607bc8fe1a510b5dae", size = 148766, upload-time = "2025-10-08T22:01:18.959Z" }, + { url = "https://files.pythonhosted.org/packages/42/17/5e2c956f0144b812e7e107f94f1cc54af734eb17b5191c0bbfb72de5e93e/tomli-2.3.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c4665508bcbac83a31ff8ab08f424b665200c0e1e645d2bd9ab3d3e557b6185b", size = 240771, upload-time = "2025-10-08T22:01:20.106Z" }, + { url = "https://files.pythonhosted.org/packages/d5/f4/0fbd014909748706c01d16824eadb0307115f9562a15cbb012cd9b3512c5/tomli-2.3.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4021923f97266babc6ccab9f5068642a0095faa0a51a246a6a02fccbb3514eaf", size = 248586, upload-time = "2025-10-08T22:01:21.164Z" }, + { url = "https://files.pythonhosted.org/packages/30/77/fed85e114bde5e81ecf9bc5da0cc69f2914b38f4708c80ae67d0c10180c5/tomli-2.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4ea38c40145a357d513bffad0ed869f13c1773716cf71ccaa83b0fa0cc4e42f", size = 244792, upload-time = "2025-10-08T22:01:22.417Z" }, + { url = "https://files.pythonhosted.org/packages/55/92/afed3d497f7c186dc71e6ee6d4fcb0acfa5f7d0a1a2878f8beae379ae0cc/tomli-2.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ad805ea85eda330dbad64c7ea7a4556259665bdf9d2672f5dccc740eb9d3ca05", size = 248909, upload-time = "2025-10-08T22:01:23.859Z" }, + { url = "https://files.pythonhosted.org/packages/f8/84/ef50c51b5a9472e7265ce1ffc7f24cd4023d289e109f669bdb1553f6a7c2/tomli-2.3.0-cp313-cp313-win32.whl", hash = "sha256:97d5eec30149fd3294270e889b4234023f2c69747e555a27bd708828353ab606", size = 96946, upload-time = "2025-10-08T22:01:24.893Z" }, + { url = "https://files.pythonhosted.org/packages/b2/b7/718cd1da0884f281f95ccfa3a6cc572d30053cba64603f79d431d3c9b61b/tomli-2.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:0c95ca56fbe89e065c6ead5b593ee64b84a26fca063b5d71a1122bf26e533999", size = 107705, upload-time = "2025-10-08T22:01:26.153Z" }, + { url = "https://files.pythonhosted.org/packages/19/94/aeafa14a52e16163008060506fcb6aa1949d13548d13752171a755c65611/tomli-2.3.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:cebc6fe843e0733ee827a282aca4999b596241195f43b4cc371d64fc6639da9e", size = 154244, upload-time = "2025-10-08T22:01:27.06Z" }, + { url = "https://files.pythonhosted.org/packages/db/e4/1e58409aa78eefa47ccd19779fc6f36787edbe7d4cd330eeeedb33a4515b/tomli-2.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4c2ef0244c75aba9355561272009d934953817c49f47d768070c3c94355c2aa3", size = 148637, upload-time = "2025-10-08T22:01:28.059Z" }, + { url = "https://files.pythonhosted.org/packages/26/b6/d1eccb62f665e44359226811064596dd6a366ea1f985839c566cd61525ae/tomli-2.3.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c22a8bf253bacc0cf11f35ad9808b6cb75ada2631c2d97c971122583b129afbc", size = 241925, upload-time = "2025-10-08T22:01:29.066Z" }, + { url = "https://files.pythonhosted.org/packages/70/91/7cdab9a03e6d3d2bb11beae108da5bdc1c34bdeb06e21163482544ddcc90/tomli-2.3.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0eea8cc5c5e9f89c9b90c4896a8deefc74f518db5927d0e0e8d4a80953d774d0", size = 249045, upload-time = "2025-10-08T22:01:31.98Z" }, + { url = "https://files.pythonhosted.org/packages/15/1b/8c26874ed1f6e4f1fcfeb868db8a794cbe9f227299402db58cfcc858766c/tomli-2.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b74a0e59ec5d15127acdabd75ea17726ac4c5178ae51b85bfe39c4f8a278e879", size = 245835, upload-time = "2025-10-08T22:01:32.989Z" }, + { url = "https://files.pythonhosted.org/packages/fd/42/8e3c6a9a4b1a1360c1a2a39f0b972cef2cc9ebd56025168c4137192a9321/tomli-2.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b5870b50c9db823c595983571d1296a6ff3e1b88f734a4c8f6fc6188397de005", size = 253109, upload-time = "2025-10-08T22:01:34.052Z" }, + { url = "https://files.pythonhosted.org/packages/22/0c/b4da635000a71b5f80130937eeac12e686eefb376b8dee113b4a582bba42/tomli-2.3.0-cp314-cp314-win32.whl", hash = "sha256:feb0dacc61170ed7ab602d3d972a58f14ee3ee60494292d384649a3dc38ef463", size = 97930, upload-time = "2025-10-08T22:01:35.082Z" }, + { url = "https://files.pythonhosted.org/packages/b9/74/cb1abc870a418ae99cd5c9547d6bce30701a954e0e721821df483ef7223c/tomli-2.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:b273fcbd7fc64dc3600c098e39136522650c49bca95df2d11cf3b626422392c8", size = 107964, upload-time = "2025-10-08T22:01:36.057Z" }, + { url = "https://files.pythonhosted.org/packages/54/78/5c46fff6432a712af9f792944f4fcd7067d8823157949f4e40c56b8b3c83/tomli-2.3.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:940d56ee0410fa17ee1f12b817b37a4d4e4dc4d27340863cc67236c74f582e77", size = 163065, upload-time = "2025-10-08T22:01:37.27Z" }, + { url = "https://files.pythonhosted.org/packages/39/67/f85d9bd23182f45eca8939cd2bc7050e1f90c41f4a2ecbbd5963a1d1c486/tomli-2.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f85209946d1fe94416debbb88d00eb92ce9cd5266775424ff81bc959e001acaf", size = 159088, upload-time = "2025-10-08T22:01:38.235Z" }, + { url = "https://files.pythonhosted.org/packages/26/5a/4b546a0405b9cc0659b399f12b6adb750757baf04250b148d3c5059fc4eb/tomli-2.3.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a56212bdcce682e56b0aaf79e869ba5d15a6163f88d5451cbde388d48b13f530", size = 268193, upload-time = "2025-10-08T22:01:39.712Z" }, + { url = "https://files.pythonhosted.org/packages/42/4f/2c12a72ae22cf7b59a7fe75b3465b7aba40ea9145d026ba41cb382075b0e/tomli-2.3.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c5f3ffd1e098dfc032d4d3af5c0ac64f6d286d98bc148698356847b80fa4de1b", size = 275488, upload-time = "2025-10-08T22:01:40.773Z" }, + { url = "https://files.pythonhosted.org/packages/92/04/a038d65dbe160c3aa5a624e93ad98111090f6804027d474ba9c37c8ae186/tomli-2.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5e01decd096b1530d97d5d85cb4dff4af2d8347bd35686654a004f8dea20fc67", size = 272669, upload-time = "2025-10-08T22:01:41.824Z" }, + { url = "https://files.pythonhosted.org/packages/be/2f/8b7c60a9d1612a7cbc39ffcca4f21a73bf368a80fc25bccf8253e2563267/tomli-2.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:8a35dd0e643bb2610f156cca8db95d213a90015c11fee76c946aa62b7ae7e02f", size = 279709, upload-time = "2025-10-08T22:01:43.177Z" }, + { url = "https://files.pythonhosted.org/packages/7e/46/cc36c679f09f27ded940281c38607716c86cf8ba4a518d524e349c8b4874/tomli-2.3.0-cp314-cp314t-win32.whl", hash = "sha256:a1f7f282fe248311650081faafa5f4732bdbfef5d45fe3f2e702fbc6f2d496e0", size = 107563, upload-time = "2025-10-08T22:01:44.233Z" }, + { url = "https://files.pythonhosted.org/packages/84/ff/426ca8683cf7b753614480484f6437f568fd2fda2edbdf57a2d3d8b27a0b/tomli-2.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:70a251f8d4ba2d9ac2542eecf008b3c8a9fc5c3f9f02c56a9d7952612be2fdba", size = 119756, upload-time = "2025-10-08T22:01:45.234Z" }, + { url = "https://files.pythonhosted.org/packages/77/b8/0135fadc89e73be292b473cb820b4f5a08197779206b33191e801feeae40/tomli-2.3.0-py3-none-any.whl", hash = "sha256:e95b1af3c5b07d9e643909b5abbec77cd9f1217e6d0bca72b0234736b9fb1f1b", size = 14408, upload-time = "2025-10-08T22:01:46.04Z" }, +] + +[[package]] +name = "toolz" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/11/d6/114b492226588d6ff54579d95847662fc69196bdeec318eb45393b24c192/toolz-1.1.0.tar.gz", hash = "sha256:27a5c770d068c110d9ed9323f24f1543e83b2f300a687b7891c1a6d56b697b5b", size = 52613, upload-time = "2025-10-17T04:03:21.661Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/12/5911ae3eeec47800503a238d971e51722ccea5feb8569b735184d5fcdbc0/toolz-1.1.0-py3-none-any.whl", hash = "sha256:15ccc861ac51c53696de0a5d6d4607f99c210739caf987b5d2054f3efed429d8", size = 58093, upload-time = "2025-10-17T04:03:20.435Z" }, +] + +[[package]] +name = "typer" +version = "0.21.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "rich" }, + { name = "shellingham" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/36/bf/8825b5929afd84d0dabd606c67cd57b8388cb3ec385f7ef19c5cc2202069/typer-0.21.1.tar.gz", hash = "sha256:ea835607cd752343b6b2b7ce676893e5a0324082268b48f27aa058bdb7d2145d", size = 110371, upload-time = "2026-01-06T11:21:10.989Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/1d/d9257dd49ff2ca23ea5f132edf1281a0c4f9de8a762b9ae399b670a59235/typer-0.21.1-py3-none-any.whl", hash = "sha256:7985e89081c636b88d172c2ee0cfe33c253160994d47bdfdc302defd7d1f1d01", size = 47381, upload-time = "2026-01-06T11:21:09.824Z" }, +] + +[[package]] +name = "types-requests" +version = "2.32.4.20250913" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/36/27/489922f4505975b11de2b5ad07b4fe1dca0bca9be81a703f26c5f3acfce5/types_requests-2.32.4.20250913.tar.gz", hash = "sha256:abd6d4f9ce3a9383f269775a9835a4c24e5cd6b9f647d64f88aa4613c33def5d", size = 23113, upload-time = "2025-09-13T02:40:02.309Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/20/9a227ea57c1285986c4cf78400d0a91615d25b24e257fd9e2969606bdfae/types_requests-2.32.4.20250913-py3-none-any.whl", hash = "sha256:78c9c1fffebbe0fa487a418e0fa5252017e9c60d1a2da394077f1780f655d7e1", size = 20658, upload-time = "2025-09-13T02:40:01.115Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, +] + +[[package]] +name = "typing-inspection" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, +] + +[[package]] +name = "urllib3" +version = "2.6.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1e/24/a2a2ed9addd907787d7aa0355ba36a6cadf1768b934c652ea78acbd59dcd/urllib3-2.6.2.tar.gz", hash = "sha256:016f9c98bb7e98085cb2b4b17b87d2c702975664e4f060c6532e64d1c1a5e797", size = 432930, upload-time = "2025-12-11T15:56:40.252Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6d/b9/4095b668ea3678bf6a0af005527f39de12fb026516fb3df17495a733b7f8/urllib3-2.6.2-py3-none-any.whl", hash = "sha256:ec21cddfe7724fc7cb4ba4bea7aa8e2ef36f607a4bab81aa6ce42a13dc3f03dd", size = 131182, upload-time = "2025-12-11T15:56:38.584Z" }, +] + +[[package]] +name = "uvicorn" +version = "0.40.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "h11" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c3/d1/8f3c683c9561a4e6689dd3b1d345c815f10f86acd044ee1fb9a4dcd0b8c5/uvicorn-0.40.0.tar.gz", hash = "sha256:839676675e87e73694518b5574fd0f24c9d97b46bea16df7b8c05ea1a51071ea", size = 81761, upload-time = "2025-12-21T14:16:22.45Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3d/d8/2083a1daa7439a66f3a48589a57d576aa117726762618f6bb09fe3798796/uvicorn-0.40.0-py3-none-any.whl", hash = "sha256:c6c8f55bc8bf13eb6fa9ff87ad62308bbbc33d0b67f84293151efe87e0d5f2ee", size = 68502, upload-time = "2025-12-21T14:16:21.041Z" }, +] + +[package.optional-dependencies] +standard = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "httptools" }, + { name = "python-dotenv" }, + { name = "pyyaml" }, + { name = "uvloop", marker = "platform_python_implementation != 'PyPy' and sys_platform != 'cygwin' and sys_platform != 'win32'" }, + { name = "watchfiles" }, + { name = "websockets" }, +] + +[[package]] +name = "uvloop" +version = "0.22.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/06/f0/18d39dbd1971d6d62c4629cc7fa67f74821b0dc1f5a77af43719de7936a7/uvloop-0.22.1.tar.gz", hash = "sha256:6c84bae345b9147082b17371e3dd5d42775bddce91f885499017f4607fdaf39f", size = 2443250, upload-time = "2025-10-16T22:17:19.342Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/eb/14/ecceb239b65adaaf7fde510aa8bd534075695d1e5f8dadfa32b5723d9cfb/uvloop-0.22.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ef6f0d4cc8a9fa1f6a910230cd53545d9a14479311e87e3cb225495952eb672c", size = 1343335, upload-time = "2025-10-16T22:16:11.43Z" }, + { url = "https://files.pythonhosted.org/packages/ba/ae/6f6f9af7f590b319c94532b9567409ba11f4fa71af1148cab1bf48a07048/uvloop-0.22.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7cd375a12b71d33d46af85a3343b35d98e8116134ba404bd657b3b1d15988792", size = 742903, upload-time = "2025-10-16T22:16:12.979Z" }, + { url = "https://files.pythonhosted.org/packages/09/bd/3667151ad0702282a1f4d5d29288fce8a13c8b6858bf0978c219cd52b231/uvloop-0.22.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ac33ed96229b7790eb729702751c0e93ac5bc3bcf52ae9eccbff30da09194b86", size = 3648499, upload-time = "2025-10-16T22:16:14.451Z" }, + { url = "https://files.pythonhosted.org/packages/b3/f6/21657bb3beb5f8c57ce8be3b83f653dd7933c2fd00545ed1b092d464799a/uvloop-0.22.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:481c990a7abe2c6f4fc3d98781cc9426ebd7f03a9aaa7eb03d3bfc68ac2a46bd", size = 3700133, upload-time = "2025-10-16T22:16:16.272Z" }, + { url = "https://files.pythonhosted.org/packages/09/e0/604f61d004ded805f24974c87ddd8374ef675644f476f01f1df90e4cdf72/uvloop-0.22.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a592b043a47ad17911add5fbd087c76716d7c9ccc1d64ec9249ceafd735f03c2", size = 3512681, upload-time = "2025-10-16T22:16:18.07Z" }, + { url = "https://files.pythonhosted.org/packages/bb/ce/8491fd370b0230deb5eac69c7aae35b3be527e25a911c0acdffb922dc1cd/uvloop-0.22.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:1489cf791aa7b6e8c8be1c5a080bae3a672791fcb4e9e12249b05862a2ca9cec", size = 3615261, upload-time = "2025-10-16T22:16:19.596Z" }, + { url = "https://files.pythonhosted.org/packages/c7/d5/69900f7883235562f1f50d8184bb7dd84a2fb61e9ec63f3782546fdbd057/uvloop-0.22.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:c60ebcd36f7b240b30788554b6f0782454826a0ed765d8430652621b5de674b9", size = 1352420, upload-time = "2025-10-16T22:16:21.187Z" }, + { url = "https://files.pythonhosted.org/packages/a8/73/c4e271b3bce59724e291465cc936c37758886a4868787da0278b3b56b905/uvloop-0.22.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3b7f102bf3cb1995cfeaee9321105e8f5da76fdb104cdad8986f85461a1b7b77", size = 748677, upload-time = "2025-10-16T22:16:22.558Z" }, + { url = "https://files.pythonhosted.org/packages/86/94/9fb7fad2f824d25f8ecac0d70b94d0d48107ad5ece03769a9c543444f78a/uvloop-0.22.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:53c85520781d84a4b8b230e24a5af5b0778efdb39142b424990ff1ef7c48ba21", size = 3753819, upload-time = "2025-10-16T22:16:23.903Z" }, + { url = "https://files.pythonhosted.org/packages/74/4f/256aca690709e9b008b7108bc85fba619a2bc37c6d80743d18abad16ee09/uvloop-0.22.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:56a2d1fae65fd82197cb8c53c367310b3eabe1bbb9fb5a04d28e3e3520e4f702", size = 3804529, upload-time = "2025-10-16T22:16:25.246Z" }, + { url = "https://files.pythonhosted.org/packages/7f/74/03c05ae4737e871923d21a76fe28b6aad57f5c03b6e6bfcfa5ad616013e4/uvloop-0.22.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:40631b049d5972c6755b06d0bfe8233b1bd9a8a6392d9d1c45c10b6f9e9b2733", size = 3621267, upload-time = "2025-10-16T22:16:26.819Z" }, + { url = "https://files.pythonhosted.org/packages/75/be/f8e590fe61d18b4a92070905497aec4c0e64ae1761498cad09023f3f4b3e/uvloop-0.22.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:535cc37b3a04f6cd2c1ef65fa1d370c9a35b6695df735fcff5427323f2cd5473", size = 3723105, upload-time = "2025-10-16T22:16:28.252Z" }, + { url = "https://files.pythonhosted.org/packages/3d/ff/7f72e8170be527b4977b033239a83a68d5c881cc4775fca255c677f7ac5d/uvloop-0.22.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:fe94b4564e865d968414598eea1a6de60adba0c040ba4ed05ac1300de402cd42", size = 1359936, upload-time = "2025-10-16T22:16:29.436Z" }, + { url = "https://files.pythonhosted.org/packages/c3/c6/e5d433f88fd54d81ef4be58b2b7b0cea13c442454a1db703a1eea0db1a59/uvloop-0.22.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:51eb9bd88391483410daad430813d982010f9c9c89512321f5b60e2cddbdddd6", size = 752769, upload-time = "2025-10-16T22:16:30.493Z" }, + { url = "https://files.pythonhosted.org/packages/24/68/a6ac446820273e71aa762fa21cdcc09861edd3536ff47c5cd3b7afb10eeb/uvloop-0.22.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:700e674a166ca5778255e0e1dc4e9d79ab2acc57b9171b79e65feba7184b3370", size = 4317413, upload-time = "2025-10-16T22:16:31.644Z" }, + { url = "https://files.pythonhosted.org/packages/5f/6f/e62b4dfc7ad6518e7eff2516f680d02a0f6eb62c0c212e152ca708a0085e/uvloop-0.22.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7b5b1ac819a3f946d3b2ee07f09149578ae76066d70b44df3fa990add49a82e4", size = 4426307, upload-time = "2025-10-16T22:16:32.917Z" }, + { url = "https://files.pythonhosted.org/packages/90/60/97362554ac21e20e81bcef1150cb2a7e4ffdaf8ea1e5b2e8bf7a053caa18/uvloop-0.22.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e047cc068570bac9866237739607d1313b9253c3051ad84738cbb095be0537b2", size = 4131970, upload-time = "2025-10-16T22:16:34.015Z" }, + { url = "https://files.pythonhosted.org/packages/99/39/6b3f7d234ba3964c428a6e40006340f53ba37993f46ed6e111c6e9141d18/uvloop-0.22.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:512fec6815e2dd45161054592441ef76c830eddaad55c8aa30952e6fe1ed07c0", size = 4296343, upload-time = "2025-10-16T22:16:35.149Z" }, + { url = "https://files.pythonhosted.org/packages/89/8c/182a2a593195bfd39842ea68ebc084e20c850806117213f5a299dfc513d9/uvloop-0.22.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:561577354eb94200d75aca23fbde86ee11be36b00e52a4eaf8f50fb0c86b7705", size = 1358611, upload-time = "2025-10-16T22:16:36.833Z" }, + { url = "https://files.pythonhosted.org/packages/d2/14/e301ee96a6dc95224b6f1162cd3312f6d1217be3907b79173b06785f2fe7/uvloop-0.22.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1cdf5192ab3e674ca26da2eada35b288d2fa49fdd0f357a19f0e7c4e7d5077c8", size = 751811, upload-time = "2025-10-16T22:16:38.275Z" }, + { url = "https://files.pythonhosted.org/packages/b7/02/654426ce265ac19e2980bfd9ea6590ca96a56f10c76e63801a2df01c0486/uvloop-0.22.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e2ea3d6190a2968f4a14a23019d3b16870dd2190cd69c8180f7c632d21de68d", size = 4288562, upload-time = "2025-10-16T22:16:39.375Z" }, + { url = "https://files.pythonhosted.org/packages/15/c0/0be24758891ef825f2065cd5db8741aaddabe3e248ee6acc5e8a80f04005/uvloop-0.22.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0530a5fbad9c9e4ee3f2b33b148c6a64d47bbad8000ea63704fa8260f4cf728e", size = 4366890, upload-time = "2025-10-16T22:16:40.547Z" }, + { url = "https://files.pythonhosted.org/packages/d2/53/8369e5219a5855869bcee5f4d317f6da0e2c669aecf0ef7d371e3d084449/uvloop-0.22.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bc5ef13bbc10b5335792360623cc378d52d7e62c2de64660616478c32cd0598e", size = 4119472, upload-time = "2025-10-16T22:16:41.694Z" }, + { url = "https://files.pythonhosted.org/packages/f8/ba/d69adbe699b768f6b29a5eec7b47dd610bd17a69de51b251126a801369ea/uvloop-0.22.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1f38ec5e3f18c8a10ded09742f7fb8de0108796eb673f30ce7762ce1b8550cad", size = 4239051, upload-time = "2025-10-16T22:16:43.224Z" }, + { url = "https://files.pythonhosted.org/packages/90/cd/b62bdeaa429758aee8de8b00ac0dd26593a9de93d302bff3d21439e9791d/uvloop-0.22.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3879b88423ec7e97cd4eba2a443aa26ed4e59b45e6b76aabf13fe2f27023a142", size = 1362067, upload-time = "2025-10-16T22:16:44.503Z" }, + { url = "https://files.pythonhosted.org/packages/0d/f8/a132124dfda0777e489ca86732e85e69afcd1ff7686647000050ba670689/uvloop-0.22.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:4baa86acedf1d62115c1dc6ad1e17134476688f08c6efd8a2ab076e815665c74", size = 752423, upload-time = "2025-10-16T22:16:45.968Z" }, + { url = "https://files.pythonhosted.org/packages/a3/94/94af78c156f88da4b3a733773ad5ba0b164393e357cc4bd0ab2e2677a7d6/uvloop-0.22.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:297c27d8003520596236bdb2335e6b3f649480bd09e00d1e3a99144b691d2a35", size = 4272437, upload-time = "2025-10-16T22:16:47.451Z" }, + { url = "https://files.pythonhosted.org/packages/b5/35/60249e9fd07b32c665192cec7af29e06c7cd96fa1d08b84f012a56a0b38e/uvloop-0.22.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c1955d5a1dd43198244d47664a5858082a3239766a839b2102a269aaff7a4e25", size = 4292101, upload-time = "2025-10-16T22:16:49.318Z" }, + { url = "https://files.pythonhosted.org/packages/02/62/67d382dfcb25d0a98ce73c11ed1a6fba5037a1a1d533dcbb7cab033a2636/uvloop-0.22.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b31dc2fccbd42adc73bc4e7cdbae4fc5086cf378979e53ca5d0301838c5682c6", size = 4114158, upload-time = "2025-10-16T22:16:50.517Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7a/f1171b4a882a5d13c8b7576f348acfe6074d72eaf52cccef752f748d4a9f/uvloop-0.22.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:93f617675b2d03af4e72a5333ef89450dfaa5321303ede6e67ba9c9d26878079", size = 4177360, upload-time = "2025-10-16T22:16:52.646Z" }, + { url = "https://files.pythonhosted.org/packages/79/7b/b01414f31546caf0919da80ad57cbfe24c56b151d12af68cee1b04922ca8/uvloop-0.22.1-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:37554f70528f60cad66945b885eb01f1bb514f132d92b6eeed1c90fd54ed6289", size = 1454790, upload-time = "2025-10-16T22:16:54.355Z" }, + { url = "https://files.pythonhosted.org/packages/d4/31/0bb232318dd838cad3fa8fb0c68c8b40e1145b32025581975e18b11fab40/uvloop-0.22.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:b76324e2dc033a0b2f435f33eb88ff9913c156ef78e153fb210e03c13da746b3", size = 796783, upload-time = "2025-10-16T22:16:55.906Z" }, + { url = "https://files.pythonhosted.org/packages/42/38/c9b09f3271a7a723a5de69f8e237ab8e7803183131bc57c890db0b6bb872/uvloop-0.22.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:badb4d8e58ee08dad957002027830d5c3b06aea446a6a3744483c2b3b745345c", size = 4647548, upload-time = "2025-10-16T22:16:57.008Z" }, + { url = "https://files.pythonhosted.org/packages/c1/37/945b4ca0ac27e3dc4952642d4c900edd030b3da6c9634875af6e13ae80e5/uvloop-0.22.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b91328c72635f6f9e0282e4a57da7470c7350ab1c9f48546c0f2866205349d21", size = 4467065, upload-time = "2025-10-16T22:16:58.206Z" }, + { url = "https://files.pythonhosted.org/packages/97/cc/48d232f33d60e2e2e0b42f4e73455b146b76ebe216487e862700457fbf3c/uvloop-0.22.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:daf620c2995d193449393d6c62131b3fbd40a63bf7b307a1527856ace637fe88", size = 4328384, upload-time = "2025-10-16T22:16:59.36Z" }, + { url = "https://files.pythonhosted.org/packages/e4/16/c1fd27e9549f3c4baf1dc9c20c456cd2f822dbf8de9f463824b0c0357e06/uvloop-0.22.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6cde23eeda1a25c75b2e07d39970f3374105d5eafbaab2a4482be82f272d5a5e", size = 4296730, upload-time = "2025-10-16T22:17:00.744Z" }, +] + +[[package]] +name = "watchfiles" +version = "1.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c2/c9/8869df9b2a2d6c59d79220a4db37679e74f807c559ffe5265e08b227a210/watchfiles-1.1.1.tar.gz", hash = "sha256:a173cb5c16c4f40ab19cecf48a534c409f7ea983ab8fed0741304a1c0a31b3f2", size = 94440, upload-time = "2025-10-14T15:06:21.08Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/1a/206e8cf2dd86fddf939165a57b4df61607a1e0add2785f170a3f616b7d9f/watchfiles-1.1.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:eef58232d32daf2ac67f42dea51a2c80f0d03379075d44a587051e63cc2e368c", size = 407318, upload-time = "2025-10-14T15:04:18.753Z" }, + { url = "https://files.pythonhosted.org/packages/b3/0f/abaf5262b9c496b5dad4ed3c0e799cbecb1f8ea512ecb6ddd46646a9fca3/watchfiles-1.1.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:03fa0f5237118a0c5e496185cafa92878568b652a2e9a9382a5151b1a0380a43", size = 394478, upload-time = "2025-10-14T15:04:20.297Z" }, + { url = "https://files.pythonhosted.org/packages/b1/04/9cc0ba88697b34b755371f5ace8d3a4d9a15719c07bdc7bd13d7d8c6a341/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8ca65483439f9c791897f7db49202301deb6e15fe9f8fe2fed555bf986d10c31", size = 449894, upload-time = "2025-10-14T15:04:21.527Z" }, + { url = "https://files.pythonhosted.org/packages/d2/9c/eda4615863cd8621e89aed4df680d8c3ec3da6a4cf1da113c17decd87c7f/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f0ab1c1af0cb38e3f598244c17919fb1a84d1629cc08355b0074b6d7f53138ac", size = 459065, upload-time = "2025-10-14T15:04:22.795Z" }, + { url = "https://files.pythonhosted.org/packages/84/13/f28b3f340157d03cbc8197629bc109d1098764abe1e60874622a0be5c112/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3bc570d6c01c206c46deb6e935a260be44f186a2f05179f52f7fcd2be086a94d", size = 488377, upload-time = "2025-10-14T15:04:24.138Z" }, + { url = "https://files.pythonhosted.org/packages/86/93/cfa597fa9389e122488f7ffdbd6db505b3b915ca7435ecd7542e855898c2/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e84087b432b6ac94778de547e08611266f1f8ffad28c0ee4c82e028b0fc5966d", size = 595837, upload-time = "2025-10-14T15:04:25.057Z" }, + { url = "https://files.pythonhosted.org/packages/57/1e/68c1ed5652b48d89fc24d6af905d88ee4f82fa8bc491e2666004e307ded1/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:620bae625f4cb18427b1bb1a2d9426dc0dd5a5ba74c7c2cdb9de405f7b129863", size = 473456, upload-time = "2025-10-14T15:04:26.497Z" }, + { url = "https://files.pythonhosted.org/packages/d5/dc/1a680b7458ffa3b14bb64878112aefc8f2e4f73c5af763cbf0bd43100658/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:544364b2b51a9b0c7000a4b4b02f90e9423d97fbbf7e06689236443ebcad81ab", size = 455614, upload-time = "2025-10-14T15:04:27.539Z" }, + { url = "https://files.pythonhosted.org/packages/61/a5/3d782a666512e01eaa6541a72ebac1d3aae191ff4a31274a66b8dd85760c/watchfiles-1.1.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:bbe1ef33d45bc71cf21364df962af171f96ecaeca06bd9e3d0b583efb12aec82", size = 630690, upload-time = "2025-10-14T15:04:28.495Z" }, + { url = "https://files.pythonhosted.org/packages/9b/73/bb5f38590e34687b2a9c47a244aa4dd50c56a825969c92c9c5fc7387cea1/watchfiles-1.1.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:1a0bb430adb19ef49389e1ad368450193a90038b5b752f4ac089ec6942c4dff4", size = 622459, upload-time = "2025-10-14T15:04:29.491Z" }, + { url = "https://files.pythonhosted.org/packages/f1/ac/c9bb0ec696e07a20bd58af5399aeadaef195fb2c73d26baf55180fe4a942/watchfiles-1.1.1-cp310-cp310-win32.whl", hash = "sha256:3f6d37644155fb5beca5378feb8c1708d5783145f2a0f1c4d5a061a210254844", size = 272663, upload-time = "2025-10-14T15:04:30.435Z" }, + { url = "https://files.pythonhosted.org/packages/11/a0/a60c5a7c2ec59fa062d9a9c61d02e3b6abd94d32aac2d8344c4bdd033326/watchfiles-1.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:a36d8efe0f290835fd0f33da35042a1bb5dc0e83cbc092dcf69bce442579e88e", size = 287453, upload-time = "2025-10-14T15:04:31.53Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f8/2c5f479fb531ce2f0564eda479faecf253d886b1ab3630a39b7bf7362d46/watchfiles-1.1.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:f57b396167a2565a4e8b5e56a5a1c537571733992b226f4f1197d79e94cf0ae5", size = 406529, upload-time = "2025-10-14T15:04:32.899Z" }, + { url = "https://files.pythonhosted.org/packages/fe/cd/f515660b1f32f65df671ddf6f85bfaca621aee177712874dc30a97397977/watchfiles-1.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:421e29339983e1bebc281fab40d812742268ad057db4aee8c4d2bce0af43b741", size = 394384, upload-time = "2025-10-14T15:04:33.761Z" }, + { url = "https://files.pythonhosted.org/packages/7b/c3/28b7dc99733eab43fca2d10f55c86e03bd6ab11ca31b802abac26b23d161/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6e43d39a741e972bab5d8100b5cdacf69db64e34eb19b6e9af162bccf63c5cc6", size = 448789, upload-time = "2025-10-14T15:04:34.679Z" }, + { url = "https://files.pythonhosted.org/packages/4a/24/33e71113b320030011c8e4316ccca04194bf0cbbaeee207f00cbc7d6b9f5/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f537afb3276d12814082a2e9b242bdcf416c2e8fd9f799a737990a1dbe906e5b", size = 460521, upload-time = "2025-10-14T15:04:35.963Z" }, + { url = "https://files.pythonhosted.org/packages/f4/c3/3c9a55f255aa57b91579ae9e98c88704955fa9dac3e5614fb378291155df/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b2cd9e04277e756a2e2d2543d65d1e2166d6fd4c9b183f8808634fda23f17b14", size = 488722, upload-time = "2025-10-14T15:04:37.091Z" }, + { url = "https://files.pythonhosted.org/packages/49/36/506447b73eb46c120169dc1717fe2eff07c234bb3232a7200b5f5bd816e9/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5f3f58818dc0b07f7d9aa7fe9eb1037aecb9700e63e1f6acfed13e9fef648f5d", size = 596088, upload-time = "2025-10-14T15:04:38.39Z" }, + { url = "https://files.pythonhosted.org/packages/82/ab/5f39e752a9838ec4d52e9b87c1e80f1ee3ccdbe92e183c15b6577ab9de16/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9bb9f66367023ae783551042d31b1d7fd422e8289eedd91f26754a66f44d5cff", size = 472923, upload-time = "2025-10-14T15:04:39.666Z" }, + { url = "https://files.pythonhosted.org/packages/af/b9/a419292f05e302dea372fa7e6fda5178a92998411f8581b9830d28fb9edb/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aebfd0861a83e6c3d1110b78ad54704486555246e542be3e2bb94195eabb2606", size = 456080, upload-time = "2025-10-14T15:04:40.643Z" }, + { url = "https://files.pythonhosted.org/packages/b0/c3/d5932fd62bde1a30c36e10c409dc5d54506726f08cb3e1d8d0ba5e2bc8db/watchfiles-1.1.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:5fac835b4ab3c6487b5dbad78c4b3724e26bcc468e886f8ba8cc4306f68f6701", size = 629432, upload-time = "2025-10-14T15:04:41.789Z" }, + { url = "https://files.pythonhosted.org/packages/f7/77/16bddd9779fafb795f1a94319dc965209c5641db5bf1edbbccace6d1b3c0/watchfiles-1.1.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:399600947b170270e80134ac854e21b3ccdefa11a9529a3decc1327088180f10", size = 623046, upload-time = "2025-10-14T15:04:42.718Z" }, + { url = "https://files.pythonhosted.org/packages/46/ef/f2ecb9a0f342b4bfad13a2787155c6ee7ce792140eac63a34676a2feeef2/watchfiles-1.1.1-cp311-cp311-win32.whl", hash = "sha256:de6da501c883f58ad50db3a32ad397b09ad29865b5f26f64c24d3e3281685849", size = 271473, upload-time = "2025-10-14T15:04:43.624Z" }, + { url = "https://files.pythonhosted.org/packages/94/bc/f42d71125f19731ea435c3948cad148d31a64fccde3867e5ba4edee901f9/watchfiles-1.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:35c53bd62a0b885bf653ebf6b700d1bf05debb78ad9292cf2a942b23513dc4c4", size = 287598, upload-time = "2025-10-14T15:04:44.516Z" }, + { url = "https://files.pythonhosted.org/packages/57/c9/a30f897351f95bbbfb6abcadafbaca711ce1162f4db95fc908c98a9165f3/watchfiles-1.1.1-cp311-cp311-win_arm64.whl", hash = "sha256:57ca5281a8b5e27593cb7d82c2ac927ad88a96ed406aa446f6344e4328208e9e", size = 277210, upload-time = "2025-10-14T15:04:45.883Z" }, + { url = "https://files.pythonhosted.org/packages/74/d5/f039e7e3c639d9b1d09b07ea412a6806d38123f0508e5f9b48a87b0a76cc/watchfiles-1.1.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:8c89f9f2f740a6b7dcc753140dd5e1ab9215966f7a3530d0c0705c83b401bd7d", size = 404745, upload-time = "2025-10-14T15:04:46.731Z" }, + { url = "https://files.pythonhosted.org/packages/a5/96/a881a13aa1349827490dab2d363c8039527060cfcc2c92cc6d13d1b1049e/watchfiles-1.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:bd404be08018c37350f0d6e34676bd1e2889990117a2b90070b3007f172d0610", size = 391769, upload-time = "2025-10-14T15:04:48.003Z" }, + { url = "https://files.pythonhosted.org/packages/4b/5b/d3b460364aeb8da471c1989238ea0e56bec24b6042a68046adf3d9ddb01c/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8526e8f916bb5b9a0a777c8317c23ce65de259422bba5b31325a6fa6029d33af", size = 449374, upload-time = "2025-10-14T15:04:49.179Z" }, + { url = "https://files.pythonhosted.org/packages/b9/44/5769cb62d4ed055cb17417c0a109a92f007114a4e07f30812a73a4efdb11/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2edc3553362b1c38d9f06242416a5d8e9fe235c204a4072e988ce2e5bb1f69f6", size = 459485, upload-time = "2025-10-14T15:04:50.155Z" }, + { url = "https://files.pythonhosted.org/packages/19/0c/286b6301ded2eccd4ffd0041a1b726afda999926cf720aab63adb68a1e36/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:30f7da3fb3f2844259cba4720c3fc7138eb0f7b659c38f3bfa65084c7fc7abce", size = 488813, upload-time = "2025-10-14T15:04:51.059Z" }, + { url = "https://files.pythonhosted.org/packages/c7/2b/8530ed41112dd4a22f4dcfdb5ccf6a1baad1ff6eed8dc5a5f09e7e8c41c7/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f8979280bdafff686ba5e4d8f97840f929a87ed9cdf133cbbd42f7766774d2aa", size = 594816, upload-time = "2025-10-14T15:04:52.031Z" }, + { url = "https://files.pythonhosted.org/packages/ce/d2/f5f9fb49489f184f18470d4f99f4e862a4b3e9ac2865688eb2099e3d837a/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dcc5c24523771db3a294c77d94771abcfcb82a0e0ee8efd910c37c59ec1b31bb", size = 475186, upload-time = "2025-10-14T15:04:53.064Z" }, + { url = "https://files.pythonhosted.org/packages/cf/68/5707da262a119fb06fbe214d82dd1fe4a6f4af32d2d14de368d0349eb52a/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1db5d7ae38ff20153d542460752ff397fcf5c96090c1230803713cf3147a6803", size = 456812, upload-time = "2025-10-14T15:04:55.174Z" }, + { url = "https://files.pythonhosted.org/packages/66/ab/3cbb8756323e8f9b6f9acb9ef4ec26d42b2109bce830cc1f3468df20511d/watchfiles-1.1.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:28475ddbde92df1874b6c5c8aaeb24ad5be47a11f87cde5a28ef3835932e3e94", size = 630196, upload-time = "2025-10-14T15:04:56.22Z" }, + { url = "https://files.pythonhosted.org/packages/78/46/7152ec29b8335f80167928944a94955015a345440f524d2dfe63fc2f437b/watchfiles-1.1.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:36193ed342f5b9842edd3532729a2ad55c4160ffcfa3700e0d54be496b70dd43", size = 622657, upload-time = "2025-10-14T15:04:57.521Z" }, + { url = "https://files.pythonhosted.org/packages/0a/bf/95895e78dd75efe9a7f31733607f384b42eb5feb54bd2eb6ed57cc2e94f4/watchfiles-1.1.1-cp312-cp312-win32.whl", hash = "sha256:859e43a1951717cc8de7f4c77674a6d389b106361585951d9e69572823f311d9", size = 272042, upload-time = "2025-10-14T15:04:59.046Z" }, + { url = "https://files.pythonhosted.org/packages/87/0a/90eb755f568de2688cb220171c4191df932232c20946966c27a59c400850/watchfiles-1.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:91d4c9a823a8c987cce8fa2690923b069966dabb196dd8d137ea2cede885fde9", size = 288410, upload-time = "2025-10-14T15:05:00.081Z" }, + { url = "https://files.pythonhosted.org/packages/36/76/f322701530586922fbd6723c4f91ace21364924822a8772c549483abed13/watchfiles-1.1.1-cp312-cp312-win_arm64.whl", hash = "sha256:a625815d4a2bdca61953dbba5a39d60164451ef34c88d751f6c368c3ea73d404", size = 278209, upload-time = "2025-10-14T15:05:01.168Z" }, + { url = "https://files.pythonhosted.org/packages/bb/f4/f750b29225fe77139f7ae5de89d4949f5a99f934c65a1f1c0b248f26f747/watchfiles-1.1.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:130e4876309e8686a5e37dba7d5e9bc77e6ed908266996ca26572437a5271e18", size = 404321, upload-time = "2025-10-14T15:05:02.063Z" }, + { url = "https://files.pythonhosted.org/packages/2b/f9/f07a295cde762644aa4c4bb0f88921d2d141af45e735b965fb2e87858328/watchfiles-1.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5f3bde70f157f84ece3765b42b4a52c6ac1a50334903c6eaf765362f6ccca88a", size = 391783, upload-time = "2025-10-14T15:05:03.052Z" }, + { url = "https://files.pythonhosted.org/packages/bc/11/fc2502457e0bea39a5c958d86d2cb69e407a4d00b85735ca724bfa6e0d1a/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:14e0b1fe858430fc0251737ef3824c54027bedb8c37c38114488b8e131cf8219", size = 449279, upload-time = "2025-10-14T15:05:04.004Z" }, + { url = "https://files.pythonhosted.org/packages/e3/1f/d66bc15ea0b728df3ed96a539c777acfcad0eb78555ad9efcaa1274688f0/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f27db948078f3823a6bb3b465180db8ebecf26dd5dae6f6180bd87383b6b4428", size = 459405, upload-time = "2025-10-14T15:05:04.942Z" }, + { url = "https://files.pythonhosted.org/packages/be/90/9f4a65c0aec3ccf032703e6db02d89a157462fbb2cf20dd415128251cac0/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:059098c3a429f62fc98e8ec62b982230ef2c8df68c79e826e37b895bc359a9c0", size = 488976, upload-time = "2025-10-14T15:05:05.905Z" }, + { url = "https://files.pythonhosted.org/packages/37/57/ee347af605d867f712be7029bb94c8c071732a4b44792e3176fa3c612d39/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bfb5862016acc9b869bb57284e6cb35fdf8e22fe59f7548858e2f971d045f150", size = 595506, upload-time = "2025-10-14T15:05:06.906Z" }, + { url = "https://files.pythonhosted.org/packages/a8/78/cc5ab0b86c122047f75e8fc471c67a04dee395daf847d3e59381996c8707/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:319b27255aacd9923b8a276bb14d21a5f7ff82564c744235fc5eae58d95422ae", size = 474936, upload-time = "2025-10-14T15:05:07.906Z" }, + { url = "https://files.pythonhosted.org/packages/62/da/def65b170a3815af7bd40a3e7010bf6ab53089ef1b75d05dd5385b87cf08/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c755367e51db90e75b19454b680903631d41f9e3607fbd941d296a020c2d752d", size = 456147, upload-time = "2025-10-14T15:05:09.138Z" }, + { url = "https://files.pythonhosted.org/packages/57/99/da6573ba71166e82d288d4df0839128004c67d2778d3b566c138695f5c0b/watchfiles-1.1.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c22c776292a23bfc7237a98f791b9ad3144b02116ff10d820829ce62dff46d0b", size = 630007, upload-time = "2025-10-14T15:05:10.117Z" }, + { url = "https://files.pythonhosted.org/packages/a8/51/7439c4dd39511368849eb1e53279cd3454b4a4dbace80bab88feeb83c6b5/watchfiles-1.1.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:3a476189be23c3686bc2f4321dd501cb329c0a0469e77b7b534ee10129ae6374", size = 622280, upload-time = "2025-10-14T15:05:11.146Z" }, + { url = "https://files.pythonhosted.org/packages/95/9c/8ed97d4bba5db6fdcdb2b298d3898f2dd5c20f6b73aee04eabe56c59677e/watchfiles-1.1.1-cp313-cp313-win32.whl", hash = "sha256:bf0a91bfb5574a2f7fc223cf95eeea79abfefa404bf1ea5e339c0c1560ae99a0", size = 272056, upload-time = "2025-10-14T15:05:12.156Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f3/c14e28429f744a260d8ceae18bf58c1d5fa56b50d006a7a9f80e1882cb0d/watchfiles-1.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:52e06553899e11e8074503c8e716d574adeeb7e68913115c4b3653c53f9bae42", size = 288162, upload-time = "2025-10-14T15:05:13.208Z" }, + { url = "https://files.pythonhosted.org/packages/dc/61/fe0e56c40d5cd29523e398d31153218718c5786b5e636d9ae8ae79453d27/watchfiles-1.1.1-cp313-cp313-win_arm64.whl", hash = "sha256:ac3cc5759570cd02662b15fbcd9d917f7ecd47efe0d6b40474eafd246f91ea18", size = 277909, upload-time = "2025-10-14T15:05:14.49Z" }, + { url = "https://files.pythonhosted.org/packages/79/42/e0a7d749626f1e28c7108a99fb9bf524b501bbbeb9b261ceecde644d5a07/watchfiles-1.1.1-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:563b116874a9a7ce6f96f87cd0b94f7faf92d08d0021e837796f0a14318ef8da", size = 403389, upload-time = "2025-10-14T15:05:15.777Z" }, + { url = "https://files.pythonhosted.org/packages/15/49/08732f90ce0fbbc13913f9f215c689cfc9ced345fb1bcd8829a50007cc8d/watchfiles-1.1.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3ad9fe1dae4ab4212d8c91e80b832425e24f421703b5a42ef2e4a1e215aff051", size = 389964, upload-time = "2025-10-14T15:05:16.85Z" }, + { url = "https://files.pythonhosted.org/packages/27/0d/7c315d4bd5f2538910491a0393c56bf70d333d51bc5b34bee8e68e8cea19/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce70f96a46b894b36eba678f153f052967a0d06d5b5a19b336ab0dbbd029f73e", size = 448114, upload-time = "2025-10-14T15:05:17.876Z" }, + { url = "https://files.pythonhosted.org/packages/c3/24/9e096de47a4d11bc4df41e9d1e61776393eac4cb6eb11b3e23315b78b2cc/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:cb467c999c2eff23a6417e58d75e5828716f42ed8289fe6b77a7e5a91036ca70", size = 460264, upload-time = "2025-10-14T15:05:18.962Z" }, + { url = "https://files.pythonhosted.org/packages/cc/0f/e8dea6375f1d3ba5fcb0b3583e2b493e77379834c74fd5a22d66d85d6540/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:836398932192dae4146c8f6f737d74baeac8b70ce14831a239bdb1ca882fc261", size = 487877, upload-time = "2025-10-14T15:05:20.094Z" }, + { url = "https://files.pythonhosted.org/packages/ac/5b/df24cfc6424a12deb41503b64d42fbea6b8cb357ec62ca84a5a3476f654a/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:743185e7372b7bc7c389e1badcc606931a827112fbbd37f14c537320fca08620", size = 595176, upload-time = "2025-10-14T15:05:21.134Z" }, + { url = "https://files.pythonhosted.org/packages/8f/b5/853b6757f7347de4e9b37e8cc3289283fb983cba1ab4d2d7144694871d9c/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:afaeff7696e0ad9f02cbb8f56365ff4686ab205fcf9c4c5b6fdfaaa16549dd04", size = 473577, upload-time = "2025-10-14T15:05:22.306Z" }, + { url = "https://files.pythonhosted.org/packages/e1/f7/0a4467be0a56e80447c8529c9fce5b38eab4f513cb3d9bf82e7392a5696b/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3f7eb7da0eb23aa2ba036d4f616d46906013a68caf61b7fdbe42fc8b25132e77", size = 455425, upload-time = "2025-10-14T15:05:23.348Z" }, + { url = "https://files.pythonhosted.org/packages/8e/e0/82583485ea00137ddf69bc84a2db88bd92ab4a6e3c405e5fb878ead8d0e7/watchfiles-1.1.1-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:831a62658609f0e5c64178211c942ace999517f5770fe9436be4c2faeba0c0ef", size = 628826, upload-time = "2025-10-14T15:05:24.398Z" }, + { url = "https://files.pythonhosted.org/packages/28/9a/a785356fccf9fae84c0cc90570f11702ae9571036fb25932f1242c82191c/watchfiles-1.1.1-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:f9a2ae5c91cecc9edd47e041a930490c31c3afb1f5e6d71de3dc671bfaca02bf", size = 622208, upload-time = "2025-10-14T15:05:25.45Z" }, + { url = "https://files.pythonhosted.org/packages/c3/f4/0872229324ef69b2c3edec35e84bd57a1289e7d3fe74588048ed8947a323/watchfiles-1.1.1-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:d1715143123baeeaeadec0528bb7441103979a1d5f6fd0e1f915383fea7ea6d5", size = 404315, upload-time = "2025-10-14T15:05:26.501Z" }, + { url = "https://files.pythonhosted.org/packages/7b/22/16d5331eaed1cb107b873f6ae1b69e9ced582fcf0c59a50cd84f403b1c32/watchfiles-1.1.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:39574d6370c4579d7f5d0ad940ce5b20db0e4117444e39b6d8f99db5676c52fd", size = 390869, upload-time = "2025-10-14T15:05:27.649Z" }, + { url = "https://files.pythonhosted.org/packages/b2/7e/5643bfff5acb6539b18483128fdc0ef2cccc94a5b8fbda130c823e8ed636/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7365b92c2e69ee952902e8f70f3ba6360d0d596d9299d55d7d386df84b6941fb", size = 449919, upload-time = "2025-10-14T15:05:28.701Z" }, + { url = "https://files.pythonhosted.org/packages/51/2e/c410993ba5025a9f9357c376f48976ef0e1b1aefb73b97a5ae01a5972755/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bfff9740c69c0e4ed32416f013f3c45e2ae42ccedd1167ef2d805c000b6c71a5", size = 460845, upload-time = "2025-10-14T15:05:30.064Z" }, + { url = "https://files.pythonhosted.org/packages/8e/a4/2df3b404469122e8680f0fcd06079317e48db58a2da2950fb45020947734/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b27cf2eb1dda37b2089e3907d8ea92922b673c0c427886d4edc6b94d8dfe5db3", size = 489027, upload-time = "2025-10-14T15:05:31.064Z" }, + { url = "https://files.pythonhosted.org/packages/ea/84/4587ba5b1f267167ee715b7f66e6382cca6938e0a4b870adad93e44747e6/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:526e86aced14a65a5b0ec50827c745597c782ff46b571dbfe46192ab9e0b3c33", size = 595615, upload-time = "2025-10-14T15:05:32.074Z" }, + { url = "https://files.pythonhosted.org/packages/6a/0f/c6988c91d06e93cd0bb3d4a808bcf32375ca1904609835c3031799e3ecae/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:04e78dd0b6352db95507fd8cb46f39d185cf8c74e4cf1e4fbad1d3df96faf510", size = 474836, upload-time = "2025-10-14T15:05:33.209Z" }, + { url = "https://files.pythonhosted.org/packages/b4/36/ded8aebea91919485b7bbabbd14f5f359326cb5ec218cd67074d1e426d74/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5c85794a4cfa094714fb9c08d4a218375b2b95b8ed1666e8677c349906246c05", size = 455099, upload-time = "2025-10-14T15:05:34.189Z" }, + { url = "https://files.pythonhosted.org/packages/98/e0/8c9bdba88af756a2fce230dd365fab2baf927ba42cd47521ee7498fd5211/watchfiles-1.1.1-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:74d5012b7630714b66be7b7b7a78855ef7ad58e8650c73afc4c076a1f480a8d6", size = 630626, upload-time = "2025-10-14T15:05:35.216Z" }, + { url = "https://files.pythonhosted.org/packages/2a/84/a95db05354bf2d19e438520d92a8ca475e578c647f78f53197f5a2f17aaf/watchfiles-1.1.1-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:8fbe85cb3201c7d380d3d0b90e63d520f15d6afe217165d7f98c9c649654db81", size = 622519, upload-time = "2025-10-14T15:05:36.259Z" }, + { url = "https://files.pythonhosted.org/packages/1d/ce/d8acdc8de545de995c339be67711e474c77d643555a9bb74a9334252bd55/watchfiles-1.1.1-cp314-cp314-win32.whl", hash = "sha256:3fa0b59c92278b5a7800d3ee7733da9d096d4aabcfabb9a928918bd276ef9b9b", size = 272078, upload-time = "2025-10-14T15:05:37.63Z" }, + { url = "https://files.pythonhosted.org/packages/c4/c9/a74487f72d0451524be827e8edec251da0cc1fcf111646a511ae752e1a3d/watchfiles-1.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:c2047d0b6cea13b3316bdbafbfa0c4228ae593d995030fda39089d36e64fc03a", size = 287664, upload-time = "2025-10-14T15:05:38.95Z" }, + { url = "https://files.pythonhosted.org/packages/df/b8/8ac000702cdd496cdce998c6f4ee0ca1f15977bba51bdf07d872ebdfc34c/watchfiles-1.1.1-cp314-cp314-win_arm64.whl", hash = "sha256:842178b126593addc05acf6fce960d28bc5fae7afbaa2c6c1b3a7b9460e5be02", size = 277154, upload-time = "2025-10-14T15:05:39.954Z" }, + { url = "https://files.pythonhosted.org/packages/47/a8/e3af2184707c29f0f14b1963c0aace6529f9d1b8582d5b99f31bbf42f59e/watchfiles-1.1.1-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:88863fbbc1a7312972f1c511f202eb30866370ebb8493aef2812b9ff28156a21", size = 403820, upload-time = "2025-10-14T15:05:40.932Z" }, + { url = "https://files.pythonhosted.org/packages/c0/ec/e47e307c2f4bd75f9f9e8afbe3876679b18e1bcec449beca132a1c5ffb2d/watchfiles-1.1.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:55c7475190662e202c08c6c0f4d9e345a29367438cf8e8037f3155e10a88d5a5", size = 390510, upload-time = "2025-10-14T15:05:41.945Z" }, + { url = "https://files.pythonhosted.org/packages/d5/a0/ad235642118090f66e7b2f18fd5c42082418404a79205cdfca50b6309c13/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3f53fa183d53a1d7a8852277c92b967ae99c2d4dcee2bfacff8868e6e30b15f7", size = 448408, upload-time = "2025-10-14T15:05:43.385Z" }, + { url = "https://files.pythonhosted.org/packages/df/85/97fa10fd5ff3332ae17e7e40e20784e419e28521549780869f1413742e9d/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6aae418a8b323732fa89721d86f39ec8f092fc2af67f4217a2b07fd3e93c6101", size = 458968, upload-time = "2025-10-14T15:05:44.404Z" }, + { url = "https://files.pythonhosted.org/packages/47/c2/9059c2e8966ea5ce678166617a7f75ecba6164375f3b288e50a40dc6d489/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f096076119da54a6080e8920cbdaac3dbee667eb91dcc5e5b78840b87415bd44", size = 488096, upload-time = "2025-10-14T15:05:45.398Z" }, + { url = "https://files.pythonhosted.org/packages/94/44/d90a9ec8ac309bc26db808a13e7bfc0e4e78b6fc051078a554e132e80160/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:00485f441d183717038ed2e887a7c868154f216877653121068107b227a2f64c", size = 596040, upload-time = "2025-10-14T15:05:46.502Z" }, + { url = "https://files.pythonhosted.org/packages/95/68/4e3479b20ca305cfc561db3ed207a8a1c745ee32bf24f2026a129d0ddb6e/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a55f3e9e493158d7bfdb60a1165035f1cf7d320914e7b7ea83fe22c6023b58fc", size = 473847, upload-time = "2025-10-14T15:05:47.484Z" }, + { url = "https://files.pythonhosted.org/packages/4f/55/2af26693fd15165c4ff7857e38330e1b61ab8c37d15dc79118cdba115b7a/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8c91ed27800188c2ae96d16e3149f199d62f86c7af5f5f4d2c61a3ed8cd3666c", size = 455072, upload-time = "2025-10-14T15:05:48.928Z" }, + { url = "https://files.pythonhosted.org/packages/66/1d/d0d200b10c9311ec25d2273f8aad8c3ef7cc7ea11808022501811208a750/watchfiles-1.1.1-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:311ff15a0bae3714ffb603e6ba6dbfba4065ab60865d15a6ec544133bdb21099", size = 629104, upload-time = "2025-10-14T15:05:49.908Z" }, + { url = "https://files.pythonhosted.org/packages/e3/bd/fa9bb053192491b3867ba07d2343d9f2252e00811567d30ae8d0f78136fe/watchfiles-1.1.1-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:a916a2932da8f8ab582f242c065f5c81bed3462849ca79ee357dd9551b0e9b01", size = 622112, upload-time = "2025-10-14T15:05:50.941Z" }, + { url = "https://files.pythonhosted.org/packages/ba/4c/a888c91e2e326872fa4705095d64acd8aa2fb9c1f7b9bd0588f33850516c/watchfiles-1.1.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:17ef139237dfced9da49fb7f2232c86ca9421f666d78c264c7ffca6601d154c3", size = 409611, upload-time = "2025-10-14T15:06:05.809Z" }, + { url = "https://files.pythonhosted.org/packages/1e/c7/5420d1943c8e3ce1a21c0a9330bcf7edafb6aa65d26b21dbb3267c9e8112/watchfiles-1.1.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:672b8adf25b1a0d35c96b5888b7b18699d27d4194bac8beeae75be4b7a3fc9b2", size = 396889, upload-time = "2025-10-14T15:06:07.035Z" }, + { url = "https://files.pythonhosted.org/packages/0c/e5/0072cef3804ce8d3aaddbfe7788aadff6b3d3f98a286fdbee9fd74ca59a7/watchfiles-1.1.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77a13aea58bc2b90173bc69f2a90de8e282648939a00a602e1dc4ee23e26b66d", size = 451616, upload-time = "2025-10-14T15:06:08.072Z" }, + { url = "https://files.pythonhosted.org/packages/83/4e/b87b71cbdfad81ad7e83358b3e447fedd281b880a03d64a760fe0a11fc2e/watchfiles-1.1.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b495de0bb386df6a12b18335a0285dda90260f51bdb505503c02bcd1ce27a8b", size = 458413, upload-time = "2025-10-14T15:06:09.209Z" }, + { url = "https://files.pythonhosted.org/packages/d3/8e/e500f8b0b77be4ff753ac94dc06b33d8f0d839377fee1b78e8c8d8f031bf/watchfiles-1.1.1-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:db476ab59b6765134de1d4fe96a1a9c96ddf091683599be0f26147ea1b2e4b88", size = 408250, upload-time = "2025-10-14T15:06:10.264Z" }, + { url = "https://files.pythonhosted.org/packages/bd/95/615e72cd27b85b61eec764a5ca51bd94d40b5adea5ff47567d9ebc4d275a/watchfiles-1.1.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:89eef07eee5e9d1fda06e38822ad167a044153457e6fd997f8a858ab7564a336", size = 396117, upload-time = "2025-10-14T15:06:11.28Z" }, + { url = "https://files.pythonhosted.org/packages/c9/81/e7fe958ce8a7fb5c73cc9fb07f5aeaf755e6aa72498c57d760af760c91f8/watchfiles-1.1.1-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce19e06cbda693e9e7686358af9cd6f5d61312ab8b00488bc36f5aabbaf77e24", size = 450493, upload-time = "2025-10-14T15:06:12.321Z" }, + { url = "https://files.pythonhosted.org/packages/6e/d4/ed38dd3b1767193de971e694aa544356e63353c33a85d948166b5ff58b9e/watchfiles-1.1.1-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3e6f39af2eab0118338902798b5aa6664f46ff66bc0280de76fca67a7f262a49", size = 457546, upload-time = "2025-10-14T15:06:13.372Z" }, +] + +[[package]] +name = "web3" +version = "7.14.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiohttp" }, + { name = "eth-abi" }, + { name = "eth-account" }, + { name = "eth-hash", extra = ["pycryptodome"] }, + { name = "eth-typing" }, + { name = "eth-utils" }, + { name = "hexbytes" }, + { name = "pydantic" }, + { name = "pyunormalize" }, + { name = "pywin32", marker = "sys_platform == 'win32'" }, + { name = "requests" }, + { name = "types-requests" }, + { name = "typing-extensions" }, + { name = "websockets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4c/85/1515644dc1b0170e43e8c26531a3cec8ebc916185fbe2db0020e450b7114/web3-7.14.0.tar.gz", hash = "sha256:d82c78007c280e478b3920cd56658df17f2f76af584ee3318df6b60d4944b8a2", size = 2194249, upload-time = "2025-10-16T19:25:07.484Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/66/94/68ce430b5e19803d2b34736dd653e8627bde68d93cd8b0bec44384487a59/web3-7.14.0-py3-none-any.whl", hash = "sha256:a78c0a979bf11c47795f564512131c01b7598a276976f7031c55140f733e210a", size = 1370801, upload-time = "2025-10-16T19:25:04.052Z" }, +] + +[[package]] +name = "websockets" +version = "15.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/21/e6/26d09fab466b7ca9c7737474c52be4f76a40301b08362eb2dbc19dcc16c1/websockets-15.0.1.tar.gz", hash = "sha256:82544de02076bafba038ce055ee6412d68da13ab47f0c60cab827346de828dee", size = 177016, upload-time = "2025-03-05T20:03:41.606Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/da/6462a9f510c0c49837bbc9345aca92d767a56c1fb2939e1579df1e1cdcf7/websockets-15.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d63efaa0cd96cf0c5fe4d581521d9fa87744540d4bc999ae6e08595a1014b45b", size = 175423, upload-time = "2025-03-05T20:01:35.363Z" }, + { url = "https://files.pythonhosted.org/packages/1c/9f/9d11c1a4eb046a9e106483b9ff69bce7ac880443f00e5ce64261b47b07e7/websockets-15.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ac60e3b188ec7574cb761b08d50fcedf9d77f1530352db4eef1707fe9dee7205", size = 173080, upload-time = "2025-03-05T20:01:37.304Z" }, + { url = "https://files.pythonhosted.org/packages/d5/4f/b462242432d93ea45f297b6179c7333dd0402b855a912a04e7fc61c0d71f/websockets-15.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5756779642579d902eed757b21b0164cd6fe338506a8083eb58af5c372e39d9a", size = 173329, upload-time = "2025-03-05T20:01:39.668Z" }, + { url = "https://files.pythonhosted.org/packages/6e/0c/6afa1f4644d7ed50284ac59cc70ef8abd44ccf7d45850d989ea7310538d0/websockets-15.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0fdfe3e2a29e4db3659dbd5bbf04560cea53dd9610273917799f1cde46aa725e", size = 182312, upload-time = "2025-03-05T20:01:41.815Z" }, + { url = "https://files.pythonhosted.org/packages/dd/d4/ffc8bd1350b229ca7a4db2a3e1c482cf87cea1baccd0ef3e72bc720caeec/websockets-15.0.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4c2529b320eb9e35af0fa3016c187dffb84a3ecc572bcee7c3ce302bfeba52bf", size = 181319, upload-time = "2025-03-05T20:01:43.967Z" }, + { url = "https://files.pythonhosted.org/packages/97/3a/5323a6bb94917af13bbb34009fac01e55c51dfde354f63692bf2533ffbc2/websockets-15.0.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ac1e5c9054fe23226fb11e05a6e630837f074174c4c2f0fe442996112a6de4fb", size = 181631, upload-time = "2025-03-05T20:01:46.104Z" }, + { url = "https://files.pythonhosted.org/packages/a6/cc/1aeb0f7cee59ef065724041bb7ed667b6ab1eeffe5141696cccec2687b66/websockets-15.0.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:5df592cd503496351d6dc14f7cdad49f268d8e618f80dce0cd5a36b93c3fc08d", size = 182016, upload-time = "2025-03-05T20:01:47.603Z" }, + { url = "https://files.pythonhosted.org/packages/79/f9/c86f8f7af208e4161a7f7e02774e9d0a81c632ae76db2ff22549e1718a51/websockets-15.0.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:0a34631031a8f05657e8e90903e656959234f3a04552259458aac0b0f9ae6fd9", size = 181426, upload-time = "2025-03-05T20:01:48.949Z" }, + { url = "https://files.pythonhosted.org/packages/c7/b9/828b0bc6753db905b91df6ae477c0b14a141090df64fb17f8a9d7e3516cf/websockets-15.0.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3d00075aa65772e7ce9e990cab3ff1de702aa09be3940d1dc88d5abf1ab8a09c", size = 181360, upload-time = "2025-03-05T20:01:50.938Z" }, + { url = "https://files.pythonhosted.org/packages/89/fb/250f5533ec468ba6327055b7d98b9df056fb1ce623b8b6aaafb30b55d02e/websockets-15.0.1-cp310-cp310-win32.whl", hash = "sha256:1234d4ef35db82f5446dca8e35a7da7964d02c127b095e172e54397fb6a6c256", size = 176388, upload-time = "2025-03-05T20:01:52.213Z" }, + { url = "https://files.pythonhosted.org/packages/1c/46/aca7082012768bb98e5608f01658ff3ac8437e563eca41cf068bd5849a5e/websockets-15.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:39c1fec2c11dc8d89bba6b2bf1556af381611a173ac2b511cf7231622058af41", size = 176830, upload-time = "2025-03-05T20:01:53.922Z" }, + { url = "https://files.pythonhosted.org/packages/9f/32/18fcd5919c293a398db67443acd33fde142f283853076049824fc58e6f75/websockets-15.0.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:823c248b690b2fd9303ba00c4f66cd5e2d8c3ba4aa968b2779be9532a4dad431", size = 175423, upload-time = "2025-03-05T20:01:56.276Z" }, + { url = "https://files.pythonhosted.org/packages/76/70/ba1ad96b07869275ef42e2ce21f07a5b0148936688c2baf7e4a1f60d5058/websockets-15.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678999709e68425ae2593acf2e3ebcbcf2e69885a5ee78f9eb80e6e371f1bf57", size = 173082, upload-time = "2025-03-05T20:01:57.563Z" }, + { url = "https://files.pythonhosted.org/packages/86/f2/10b55821dd40eb696ce4704a87d57774696f9451108cff0d2824c97e0f97/websockets-15.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d50fd1ee42388dcfb2b3676132c78116490976f1300da28eb629272d5d93e905", size = 173330, upload-time = "2025-03-05T20:01:59.063Z" }, + { url = "https://files.pythonhosted.org/packages/a5/90/1c37ae8b8a113d3daf1065222b6af61cc44102da95388ac0018fcb7d93d9/websockets-15.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d99e5546bf73dbad5bf3547174cd6cb8ba7273062a23808ffea025ecb1cf8562", size = 182878, upload-time = "2025-03-05T20:02:00.305Z" }, + { url = "https://files.pythonhosted.org/packages/8e/8d/96e8e288b2a41dffafb78e8904ea7367ee4f891dafc2ab8d87e2124cb3d3/websockets-15.0.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:66dd88c918e3287efc22409d426c8f729688d89a0c587c88971a0faa2c2f3792", size = 181883, upload-time = "2025-03-05T20:02:03.148Z" }, + { url = "https://files.pythonhosted.org/packages/93/1f/5d6dbf551766308f6f50f8baf8e9860be6182911e8106da7a7f73785f4c4/websockets-15.0.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8dd8327c795b3e3f219760fa603dcae1dcc148172290a8ab15158cf85a953413", size = 182252, upload-time = "2025-03-05T20:02:05.29Z" }, + { url = "https://files.pythonhosted.org/packages/d4/78/2d4fed9123e6620cbf1706c0de8a1632e1a28e7774d94346d7de1bba2ca3/websockets-15.0.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8fdc51055e6ff4adeb88d58a11042ec9a5eae317a0a53d12c062c8a8865909e8", size = 182521, upload-time = "2025-03-05T20:02:07.458Z" }, + { url = "https://files.pythonhosted.org/packages/e7/3b/66d4c1b444dd1a9823c4a81f50231b921bab54eee2f69e70319b4e21f1ca/websockets-15.0.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:693f0192126df6c2327cce3baa7c06f2a117575e32ab2308f7f8216c29d9e2e3", size = 181958, upload-time = "2025-03-05T20:02:09.842Z" }, + { url = "https://files.pythonhosted.org/packages/08/ff/e9eed2ee5fed6f76fdd6032ca5cd38c57ca9661430bb3d5fb2872dc8703c/websockets-15.0.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:54479983bd5fb469c38f2f5c7e3a24f9a4e70594cd68cd1fa6b9340dadaff7cf", size = 181918, upload-time = "2025-03-05T20:02:11.968Z" }, + { url = "https://files.pythonhosted.org/packages/d8/75/994634a49b7e12532be6a42103597b71098fd25900f7437d6055ed39930a/websockets-15.0.1-cp311-cp311-win32.whl", hash = "sha256:16b6c1b3e57799b9d38427dda63edcbe4926352c47cf88588c0be4ace18dac85", size = 176388, upload-time = "2025-03-05T20:02:13.32Z" }, + { url = "https://files.pythonhosted.org/packages/98/93/e36c73f78400a65f5e236cd376713c34182e6663f6889cd45a4a04d8f203/websockets-15.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:27ccee0071a0e75d22cb35849b1db43f2ecd3e161041ac1ee9d2352ddf72f065", size = 176828, upload-time = "2025-03-05T20:02:14.585Z" }, + { url = "https://files.pythonhosted.org/packages/51/6b/4545a0d843594f5d0771e86463606a3988b5a09ca5123136f8a76580dd63/websockets-15.0.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:3e90baa811a5d73f3ca0bcbf32064d663ed81318ab225ee4f427ad4e26e5aff3", size = 175437, upload-time = "2025-03-05T20:02:16.706Z" }, + { url = "https://files.pythonhosted.org/packages/f4/71/809a0f5f6a06522af902e0f2ea2757f71ead94610010cf570ab5c98e99ed/websockets-15.0.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:592f1a9fe869c778694f0aa806ba0374e97648ab57936f092fd9d87f8bc03665", size = 173096, upload-time = "2025-03-05T20:02:18.832Z" }, + { url = "https://files.pythonhosted.org/packages/3d/69/1a681dd6f02180916f116894181eab8b2e25b31e484c5d0eae637ec01f7c/websockets-15.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0701bc3cfcb9164d04a14b149fd74be7347a530ad3bbf15ab2c678a2cd3dd9a2", size = 173332, upload-time = "2025-03-05T20:02:20.187Z" }, + { url = "https://files.pythonhosted.org/packages/a6/02/0073b3952f5bce97eafbb35757f8d0d54812b6174ed8dd952aa08429bcc3/websockets-15.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8b56bdcdb4505c8078cb6c7157d9811a85790f2f2b3632c7d1462ab5783d215", size = 183152, upload-time = "2025-03-05T20:02:22.286Z" }, + { url = "https://files.pythonhosted.org/packages/74/45/c205c8480eafd114b428284840da0b1be9ffd0e4f87338dc95dc6ff961a1/websockets-15.0.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0af68c55afbd5f07986df82831c7bff04846928ea8d1fd7f30052638788bc9b5", size = 182096, upload-time = "2025-03-05T20:02:24.368Z" }, + { url = "https://files.pythonhosted.org/packages/14/8f/aa61f528fba38578ec553c145857a181384c72b98156f858ca5c8e82d9d3/websockets-15.0.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64dee438fed052b52e4f98f76c5790513235efaa1ef7f3f2192c392cd7c91b65", size = 182523, upload-time = "2025-03-05T20:02:25.669Z" }, + { url = "https://files.pythonhosted.org/packages/ec/6d/0267396610add5bc0d0d3e77f546d4cd287200804fe02323797de77dbce9/websockets-15.0.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d5f6b181bb38171a8ad1d6aa58a67a6aa9d4b38d0f8c5f496b9e42561dfc62fe", size = 182790, upload-time = "2025-03-05T20:02:26.99Z" }, + { url = "https://files.pythonhosted.org/packages/02/05/c68c5adbf679cf610ae2f74a9b871ae84564462955d991178f95a1ddb7dd/websockets-15.0.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5d54b09eba2bada6011aea5375542a157637b91029687eb4fdb2dab11059c1b4", size = 182165, upload-time = "2025-03-05T20:02:30.291Z" }, + { url = "https://files.pythonhosted.org/packages/29/93/bb672df7b2f5faac89761cb5fa34f5cec45a4026c383a4b5761c6cea5c16/websockets-15.0.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3be571a8b5afed347da347bfcf27ba12b069d9d7f42cb8c7028b5e98bbb12597", size = 182160, upload-time = "2025-03-05T20:02:31.634Z" }, + { url = "https://files.pythonhosted.org/packages/ff/83/de1f7709376dc3ca9b7eeb4b9a07b4526b14876b6d372a4dc62312bebee0/websockets-15.0.1-cp312-cp312-win32.whl", hash = "sha256:c338ffa0520bdb12fbc527265235639fb76e7bc7faafbb93f6ba80d9c06578a9", size = 176395, upload-time = "2025-03-05T20:02:33.017Z" }, + { url = "https://files.pythonhosted.org/packages/7d/71/abf2ebc3bbfa40f391ce1428c7168fb20582d0ff57019b69ea20fa698043/websockets-15.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:fcd5cf9e305d7b8338754470cf69cf81f420459dbae8a3b40cee57417f4614a7", size = 176841, upload-time = "2025-03-05T20:02:34.498Z" }, + { url = "https://files.pythonhosted.org/packages/cb/9f/51f0cf64471a9d2b4d0fc6c534f323b664e7095640c34562f5182e5a7195/websockets-15.0.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ee443ef070bb3b6ed74514f5efaa37a252af57c90eb33b956d35c8e9c10a1931", size = 175440, upload-time = "2025-03-05T20:02:36.695Z" }, + { url = "https://files.pythonhosted.org/packages/8a/05/aa116ec9943c718905997412c5989f7ed671bc0188ee2ba89520e8765d7b/websockets-15.0.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5a939de6b7b4e18ca683218320fc67ea886038265fd1ed30173f5ce3f8e85675", size = 173098, upload-time = "2025-03-05T20:02:37.985Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0b/33cef55ff24f2d92924923c99926dcce78e7bd922d649467f0eda8368923/websockets-15.0.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:746ee8dba912cd6fc889a8147168991d50ed70447bf18bcda7039f7d2e3d9151", size = 173329, upload-time = "2025-03-05T20:02:39.298Z" }, + { url = "https://files.pythonhosted.org/packages/31/1d/063b25dcc01faa8fada1469bdf769de3768b7044eac9d41f734fd7b6ad6d/websockets-15.0.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:595b6c3969023ecf9041b2936ac3827e4623bfa3ccf007575f04c5a6aa318c22", size = 183111, upload-time = "2025-03-05T20:02:40.595Z" }, + { url = "https://files.pythonhosted.org/packages/93/53/9a87ee494a51bf63e4ec9241c1ccc4f7c2f45fff85d5bde2ff74fcb68b9e/websockets-15.0.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c714d2fc58b5ca3e285461a4cc0c9a66bd0e24c5da9911e30158286c9b5be7f", size = 182054, upload-time = "2025-03-05T20:02:41.926Z" }, + { url = "https://files.pythonhosted.org/packages/ff/b2/83a6ddf56cdcbad4e3d841fcc55d6ba7d19aeb89c50f24dd7e859ec0805f/websockets-15.0.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f3c1e2ab208db911594ae5b4f79addeb3501604a165019dd221c0bdcabe4db8", size = 182496, upload-time = "2025-03-05T20:02:43.304Z" }, + { url = "https://files.pythonhosted.org/packages/98/41/e7038944ed0abf34c45aa4635ba28136f06052e08fc2168520bb8b25149f/websockets-15.0.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:229cf1d3ca6c1804400b0a9790dc66528e08a6a1feec0d5040e8b9eb14422375", size = 182829, upload-time = "2025-03-05T20:02:48.812Z" }, + { url = "https://files.pythonhosted.org/packages/e0/17/de15b6158680c7623c6ef0db361da965ab25d813ae54fcfeae2e5b9ef910/websockets-15.0.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:756c56e867a90fb00177d530dca4b097dd753cde348448a1012ed6c5131f8b7d", size = 182217, upload-time = "2025-03-05T20:02:50.14Z" }, + { url = "https://files.pythonhosted.org/packages/33/2b/1f168cb6041853eef0362fb9554c3824367c5560cbdaad89ac40f8c2edfc/websockets-15.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:558d023b3df0bffe50a04e710bc87742de35060580a293c2a984299ed83bc4e4", size = 182195, upload-time = "2025-03-05T20:02:51.561Z" }, + { url = "https://files.pythonhosted.org/packages/86/eb/20b6cdf273913d0ad05a6a14aed4b9a85591c18a987a3d47f20fa13dcc47/websockets-15.0.1-cp313-cp313-win32.whl", hash = "sha256:ba9e56e8ceeeedb2e080147ba85ffcd5cd0711b89576b83784d8605a7df455fa", size = 176393, upload-time = "2025-03-05T20:02:53.814Z" }, + { url = "https://files.pythonhosted.org/packages/1b/6c/c65773d6cab416a64d191d6ee8a8b1c68a09970ea6909d16965d26bfed1e/websockets-15.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:e09473f095a819042ecb2ab9465aee615bd9c2028e4ef7d933600a8401c79561", size = 176837, upload-time = "2025-03-05T20:02:55.237Z" }, + { url = "https://files.pythonhosted.org/packages/02/9e/d40f779fa16f74d3468357197af8d6ad07e7c5a27ea1ca74ceb38986f77a/websockets-15.0.1-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0c9e74d766f2818bb95f84c25be4dea09841ac0f734d1966f415e4edfc4ef1c3", size = 173109, upload-time = "2025-03-05T20:03:17.769Z" }, + { url = "https://files.pythonhosted.org/packages/bc/cd/5b887b8585a593073fd92f7c23ecd3985cd2c3175025a91b0d69b0551372/websockets-15.0.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:1009ee0c7739c08a0cd59de430d6de452a55e42d6b522de7aa15e6f67db0b8e1", size = 173343, upload-time = "2025-03-05T20:03:19.094Z" }, + { url = "https://files.pythonhosted.org/packages/fe/ae/d34f7556890341e900a95acf4886833646306269f899d58ad62f588bf410/websockets-15.0.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:76d1f20b1c7a2fa82367e04982e708723ba0e7b8d43aa643d3dcd404d74f1475", size = 174599, upload-time = "2025-03-05T20:03:21.1Z" }, + { url = "https://files.pythonhosted.org/packages/71/e6/5fd43993a87db364ec60fc1d608273a1a465c0caba69176dd160e197ce42/websockets-15.0.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f29d80eb9a9263b8d109135351caf568cc3f80b9928bccde535c235de55c22d9", size = 174207, upload-time = "2025-03-05T20:03:23.221Z" }, + { url = "https://files.pythonhosted.org/packages/2b/fb/c492d6daa5ec067c2988ac80c61359ace5c4c674c532985ac5a123436cec/websockets-15.0.1-pp310-pypy310_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b359ed09954d7c18bbc1680f380c7301f92c60bf924171629c5db97febb12f04", size = 174155, upload-time = "2025-03-05T20:03:25.321Z" }, + { url = "https://files.pythonhosted.org/packages/68/a1/dcb68430b1d00b698ae7a7e0194433bce4f07ded185f0ee5fb21e2a2e91e/websockets-15.0.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:cad21560da69f4ce7658ca2cb83138fb4cf695a2ba3e475e0559e05991aa8122", size = 176884, upload-time = "2025-03-05T20:03:27.934Z" }, + { url = "https://files.pythonhosted.org/packages/fa/a8/5b41e0da817d64113292ab1f8247140aac61cbf6cfd085d6a0fa77f4984f/websockets-15.0.1-py3-none-any.whl", hash = "sha256:f7a866fbc1e97b5c617ee4116daaa09b722101d4a3c170c787450ba409f9736f", size = 169743, upload-time = "2025-03-05T20:03:39.41Z" }, +] + +[[package]] +name = "x402" +version = "2.1.0" +source = { editable = "../../../../python/x402" } +dependencies = [ + { name = "nest-asyncio" }, + { name = "pydantic" }, + { name = "typing-extensions" }, +] + +[package.optional-dependencies] +evm = [ + { name = "eth-abi" }, + { name = "eth-account" }, + { name = "eth-keys" }, + { name = "eth-utils" }, + { name = "web3" }, +] +fastapi = [ + { name = "fastapi", extra = ["standard"] }, + { name = "starlette" }, +] +svm = [ + { name = "solana" }, + { name = "solders" }, +] + +[package.metadata] +requires-dist = [ + { name = "eth-abi", marker = "extra == 'evm'", specifier = ">=5.0.0" }, + { name = "eth-account", marker = "extra == 'evm'", specifier = ">=0.12.0" }, + { name = "eth-keys", marker = "extra == 'evm'", specifier = ">=0.5.0" }, + { name = "eth-utils", marker = "extra == 'evm'", specifier = ">=4.0.0" }, + { name = "fastapi", extras = ["standard"], marker = "extra == 'fastapi'", specifier = ">=0.115.0" }, + { name = "flask", marker = "extra == 'flask'", specifier = ">=3.0.0" }, + { name = "httpx", marker = "extra == 'httpx'", specifier = ">=0.28.1" }, + { name = "jsonschema", marker = "extra == 'extensions'", specifier = ">=4.0.0" }, + { name = "mcp", marker = "extra == 'mcp'", specifier = ">=1.0.0" }, + { name = "nest-asyncio", specifier = ">=1.6.0" }, + { name = "pydantic", specifier = ">=2.0.0" }, + { name = "requests", marker = "extra == 'requests'", specifier = ">=2.31.0" }, + { name = "solana", marker = "extra == 'svm'", specifier = ">=0.36.0" }, + { name = "solders", marker = "extra == 'svm'", specifier = ">=0.27.0" }, + { name = "starlette", marker = "extra == 'fastapi'", specifier = ">=0.27.0" }, + { name = "typing-extensions", specifier = ">=4.0.0" }, + { name = "web3", marker = "extra == 'evm'", specifier = ">=7.0.0" }, + { name = "x402", extras = ["evm", "svm"], marker = "extra == 'mechanisms'" }, + { name = "x402", extras = ["flask", "fastapi"], marker = "extra == 'servers'" }, + { name = "x402", extras = ["httpx", "requests"], marker = "extra == 'clients'" }, + { name = "x402", extras = ["httpx", "requests", "flask", "fastapi", "evm", "svm", "mcp", "extensions"], marker = "extra == 'all'" }, +] +provides-extras = ["httpx", "requests", "flask", "fastapi", "evm", "svm", "mcp", "extensions", "clients", "servers", "mechanisms", "all"] + +[package.metadata.requires-dev] +dev = [ + { name = "black", specifier = ">=23.0.0" }, + { name = "eth-abi", specifier = ">=5.0.0" }, + { name = "eth-account", specifier = ">=0.12.0" }, + { name = "eth-keys", specifier = ">=0.5.0" }, + { name = "eth-utils", specifier = ">=4.0.0" }, + { name = "fastapi", extras = ["standard"], specifier = ">=0.115.0" }, + { name = "flask", specifier = ">=3.0.0" }, + { name = "httpx", specifier = ">=0.28.1" }, + { name = "jsonschema", specifier = ">=4.0.0" }, + { name = "mcp", specifier = ">=1.26.0" }, + { name = "mypy", specifier = ">=1.0.0" }, + { name = "nest-asyncio", specifier = ">=1.6.0" }, + { name = "pytest", specifier = ">=7.0.0" }, + { name = "pytest-asyncio", specifier = ">=0.21.0" }, + { name = "requests", specifier = ">=2.31.0" }, + { name = "ruff", specifier = ">=0.1.0" }, + { name = "solana", specifier = ">=0.36.0" }, + { name = "solders", specifier = ">=0.27.0" }, + { name = "starlette", specifier = ">=0.27.0" }, + { name = "towncrier", specifier = ">=24.8.0,<25" }, + { name = "web3", specifier = ">=7.0.0" }, +] + +[[package]] +name = "x402-v2-fastapi-example" +version = "0.1.0" +source = { virtual = "." } +dependencies = [ + { name = "python-dotenv" }, + { name = "uvicorn", extra = ["standard"] }, + { name = "x402", extra = ["evm", "fastapi", "svm"] }, +] + +[package.dev-dependencies] +dev = [ + { name = "black" }, + { name = "mypy" }, + { name = "pytest" }, + { name = "pytest-asyncio" }, + { name = "ruff" }, +] + +[package.metadata] +requires-dist = [ + { name = "python-dotenv", specifier = ">=1.2.1" }, + { name = "uvicorn", extras = ["standard"], specifier = ">=0.40.0" }, + { name = "x402", extras = ["fastapi", "evm", "svm"], editable = "../../../../python/x402" }, +] + +[package.metadata.requires-dev] +dev = [ + { name = "black", specifier = ">=23.0.0" }, + { name = "mypy", specifier = ">=1.0.0" }, + { name = "pytest", specifier = ">=7.0.0" }, + { name = "pytest-asyncio", specifier = ">=0.21.0" }, + { name = "ruff", specifier = ">=0.1.0" }, +] + +[[package]] +name = "yarl" +version = "1.22.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, + { name = "multidict" }, + { name = "propcache" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/57/63/0c6ebca57330cd313f6102b16dd57ffaf3ec4c83403dcb45dbd15c6f3ea1/yarl-1.22.0.tar.gz", hash = "sha256:bebf8557577d4401ba8bd9ff33906f1376c877aa78d1fe216ad01b4d6745af71", size = 187169, upload-time = "2025-10-06T14:12:55.963Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/43/a2204825342f37c337f5edb6637040fa14e365b2fcc2346960201d457579/yarl-1.22.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:c7bd6683587567e5a49ee6e336e0612bec8329be1b7d4c8af5687dcdeb67ee1e", size = 140517, upload-time = "2025-10-06T14:08:42.494Z" }, + { url = "https://files.pythonhosted.org/packages/44/6f/674f3e6f02266428c56f704cd2501c22f78e8b2eeb23f153117cc86fb28a/yarl-1.22.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5cdac20da754f3a723cceea5b3448e1a2074866406adeb4ef35b469d089adb8f", size = 93495, upload-time = "2025-10-06T14:08:46.2Z" }, + { url = "https://files.pythonhosted.org/packages/b8/12/5b274d8a0f30c07b91b2f02cba69152600b47830fcfb465c108880fcee9c/yarl-1.22.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:07a524d84df0c10f41e3ee918846e1974aba4ec017f990dc735aad487a0bdfdf", size = 94400, upload-time = "2025-10-06T14:08:47.855Z" }, + { url = "https://files.pythonhosted.org/packages/e2/7f/df1b6949b1fa1aa9ff6de6e2631876ad4b73c4437822026e85d8acb56bb1/yarl-1.22.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e1b329cb8146d7b736677a2440e422eadd775d1806a81db2d4cded80a48efc1a", size = 347545, upload-time = "2025-10-06T14:08:49.683Z" }, + { url = "https://files.pythonhosted.org/packages/84/09/f92ed93bd6cd77872ab6c3462df45ca45cd058d8f1d0c9b4f54c1704429f/yarl-1.22.0-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:75976c6945d85dbb9ee6308cd7ff7b1fb9409380c82d6119bd778d8fcfe2931c", size = 319598, upload-time = "2025-10-06T14:08:51.215Z" }, + { url = "https://files.pythonhosted.org/packages/c3/97/ac3f3feae7d522cf7ccec3d340bb0b2b61c56cb9767923df62a135092c6b/yarl-1.22.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:80ddf7a5f8c86cb3eb4bc9028b07bbbf1f08a96c5c0bc1244be5e8fefcb94147", size = 363893, upload-time = "2025-10-06T14:08:53.144Z" }, + { url = "https://files.pythonhosted.org/packages/06/49/f3219097403b9c84a4d079b1d7bda62dd9b86d0d6e4428c02d46ab2c77fc/yarl-1.22.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d332fc2e3c94dad927f2112395772a4e4fedbcf8f80efc21ed7cdfae4d574fdb", size = 371240, upload-time = "2025-10-06T14:08:55.036Z" }, + { url = "https://files.pythonhosted.org/packages/35/9f/06b765d45c0e44e8ecf0fe15c9eacbbde342bb5b7561c46944f107bfb6c3/yarl-1.22.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0cf71bf877efeac18b38d3930594c0948c82b64547c1cf420ba48722fe5509f6", size = 346965, upload-time = "2025-10-06T14:08:56.722Z" }, + { url = "https://files.pythonhosted.org/packages/c5/69/599e7cea8d0fcb1694323b0db0dda317fa3162f7b90166faddecf532166f/yarl-1.22.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:663e1cadaddae26be034a6ab6072449a8426ddb03d500f43daf952b74553bba0", size = 342026, upload-time = "2025-10-06T14:08:58.563Z" }, + { url = "https://files.pythonhosted.org/packages/95/6f/9dfd12c8bc90fea9eab39832ee32ea48f8e53d1256252a77b710c065c89f/yarl-1.22.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:6dcbb0829c671f305be48a7227918cfcd11276c2d637a8033a99a02b67bf9eda", size = 335637, upload-time = "2025-10-06T14:09:00.506Z" }, + { url = "https://files.pythonhosted.org/packages/57/2e/34c5b4eb9b07e16e873db5b182c71e5f06f9b5af388cdaa97736d79dd9a6/yarl-1.22.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:f0d97c18dfd9a9af4490631905a3f131a8e4c9e80a39353919e2cfed8f00aedc", size = 359082, upload-time = "2025-10-06T14:09:01.936Z" }, + { url = "https://files.pythonhosted.org/packages/31/71/fa7e10fb772d273aa1f096ecb8ab8594117822f683bab7d2c5a89914c92a/yarl-1.22.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:437840083abe022c978470b942ff832c3940b2ad3734d424b7eaffcd07f76737", size = 357811, upload-time = "2025-10-06T14:09:03.445Z" }, + { url = "https://files.pythonhosted.org/packages/26/da/11374c04e8e1184a6a03cf9c8f5688d3e5cec83ed6f31ad3481b3207f709/yarl-1.22.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:a899cbd98dce6f5d8de1aad31cb712ec0a530abc0a86bd6edaa47c1090138467", size = 351223, upload-time = "2025-10-06T14:09:05.401Z" }, + { url = "https://files.pythonhosted.org/packages/82/8f/e2d01f161b0c034a30410e375e191a5d27608c1f8693bab1a08b089ca096/yarl-1.22.0-cp310-cp310-win32.whl", hash = "sha256:595697f68bd1f0c1c159fcb97b661fc9c3f5db46498043555d04805430e79bea", size = 82118, upload-time = "2025-10-06T14:09:11.148Z" }, + { url = "https://files.pythonhosted.org/packages/62/46/94c76196642dbeae634c7a61ba3da88cd77bed875bf6e4a8bed037505aa6/yarl-1.22.0-cp310-cp310-win_amd64.whl", hash = "sha256:cb95a9b1adaa48e41815a55ae740cfda005758104049a640a398120bf02515ca", size = 86852, upload-time = "2025-10-06T14:09:12.958Z" }, + { url = "https://files.pythonhosted.org/packages/af/af/7df4f179d3b1a6dcb9a4bd2ffbc67642746fcafdb62580e66876ce83fff4/yarl-1.22.0-cp310-cp310-win_arm64.whl", hash = "sha256:b85b982afde6df99ecc996990d4ad7ccbdbb70e2a4ba4de0aecde5922ba98a0b", size = 82012, upload-time = "2025-10-06T14:09:14.664Z" }, + { url = "https://files.pythonhosted.org/packages/4d/27/5ab13fc84c76a0250afd3d26d5936349a35be56ce5785447d6c423b26d92/yarl-1.22.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:1ab72135b1f2db3fed3997d7e7dc1b80573c67138023852b6efb336a5eae6511", size = 141607, upload-time = "2025-10-06T14:09:16.298Z" }, + { url = "https://files.pythonhosted.org/packages/6a/a1/d065d51d02dc02ce81501d476b9ed2229d9a990818332242a882d5d60340/yarl-1.22.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:669930400e375570189492dc8d8341301578e8493aec04aebc20d4717f899dd6", size = 94027, upload-time = "2025-10-06T14:09:17.786Z" }, + { url = "https://files.pythonhosted.org/packages/c1/da/8da9f6a53f67b5106ffe902c6fa0164e10398d4e150d85838b82f424072a/yarl-1.22.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:792a2af6d58177ef7c19cbf0097aba92ca1b9cb3ffdd9c7470e156c8f9b5e028", size = 94963, upload-time = "2025-10-06T14:09:19.662Z" }, + { url = "https://files.pythonhosted.org/packages/68/fe/2c1f674960c376e29cb0bec1249b117d11738db92a6ccc4a530b972648db/yarl-1.22.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3ea66b1c11c9150f1372f69afb6b8116f2dd7286f38e14ea71a44eee9ec51b9d", size = 368406, upload-time = "2025-10-06T14:09:21.402Z" }, + { url = "https://files.pythonhosted.org/packages/95/26/812a540e1c3c6418fec60e9bbd38e871eaba9545e94fa5eff8f4a8e28e1e/yarl-1.22.0-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3e2daa88dc91870215961e96a039ec73e4937da13cf77ce17f9cad0c18df3503", size = 336581, upload-time = "2025-10-06T14:09:22.98Z" }, + { url = "https://files.pythonhosted.org/packages/0b/f5/5777b19e26fdf98563985e481f8be3d8a39f8734147a6ebf459d0dab5a6b/yarl-1.22.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ba440ae430c00eee41509353628600212112cd5018d5def7e9b05ea7ac34eb65", size = 388924, upload-time = "2025-10-06T14:09:24.655Z" }, + { url = "https://files.pythonhosted.org/packages/86/08/24bd2477bd59c0bbd994fe1d93b126e0472e4e3df5a96a277b0a55309e89/yarl-1.22.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e6438cc8f23a9c1478633d216b16104a586b9761db62bfacb6425bac0a36679e", size = 392890, upload-time = "2025-10-06T14:09:26.617Z" }, + { url = "https://files.pythonhosted.org/packages/46/00/71b90ed48e895667ecfb1eaab27c1523ee2fa217433ed77a73b13205ca4b/yarl-1.22.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4c52a6e78aef5cf47a98ef8e934755abf53953379b7d53e68b15ff4420e6683d", size = 365819, upload-time = "2025-10-06T14:09:28.544Z" }, + { url = "https://files.pythonhosted.org/packages/30/2d/f715501cae832651d3282387c6a9236cd26bd00d0ff1e404b3dc52447884/yarl-1.22.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:3b06bcadaac49c70f4c88af4ffcfbe3dc155aab3163e75777818092478bcbbe7", size = 363601, upload-time = "2025-10-06T14:09:30.568Z" }, + { url = "https://files.pythonhosted.org/packages/f8/f9/a678c992d78e394e7126ee0b0e4e71bd2775e4334d00a9278c06a6cce96a/yarl-1.22.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:6944b2dc72c4d7f7052683487e3677456050ff77fcf5e6204e98caf785ad1967", size = 358072, upload-time = "2025-10-06T14:09:32.528Z" }, + { url = "https://files.pythonhosted.org/packages/2c/d1/b49454411a60edb6fefdcad4f8e6dbba7d8019e3a508a1c5836cba6d0781/yarl-1.22.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:d5372ca1df0f91a86b047d1277c2aaf1edb32d78bbcefffc81b40ffd18f027ed", size = 385311, upload-time = "2025-10-06T14:09:34.634Z" }, + { url = "https://files.pythonhosted.org/packages/87/e5/40d7a94debb8448c7771a916d1861d6609dddf7958dc381117e7ba36d9e8/yarl-1.22.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:51af598701f5299012b8416486b40fceef8c26fc87dc6d7d1f6fc30609ea0aa6", size = 381094, upload-time = "2025-10-06T14:09:36.268Z" }, + { url = "https://files.pythonhosted.org/packages/35/d8/611cc282502381ad855448643e1ad0538957fc82ae83dfe7762c14069e14/yarl-1.22.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b266bd01fedeffeeac01a79ae181719ff848a5a13ce10075adbefc8f1daee70e", size = 370944, upload-time = "2025-10-06T14:09:37.872Z" }, + { url = "https://files.pythonhosted.org/packages/2d/df/fadd00fb1c90e1a5a8bd731fa3d3de2e165e5a3666a095b04e31b04d9cb6/yarl-1.22.0-cp311-cp311-win32.whl", hash = "sha256:a9b1ba5610a4e20f655258d5a1fdc7ebe3d837bb0e45b581398b99eb98b1f5ca", size = 81804, upload-time = "2025-10-06T14:09:39.359Z" }, + { url = "https://files.pythonhosted.org/packages/b5/f7/149bb6f45f267cb5c074ac40c01c6b3ea6d8a620d34b337f6321928a1b4d/yarl-1.22.0-cp311-cp311-win_amd64.whl", hash = "sha256:078278b9b0b11568937d9509b589ee83ef98ed6d561dfe2020e24a9fd08eaa2b", size = 86858, upload-time = "2025-10-06T14:09:41.068Z" }, + { url = "https://files.pythonhosted.org/packages/2b/13/88b78b93ad3f2f0b78e13bfaaa24d11cbc746e93fe76d8c06bf139615646/yarl-1.22.0-cp311-cp311-win_arm64.whl", hash = "sha256:b6a6f620cfe13ccec221fa312139135166e47ae169f8253f72a0abc0dae94376", size = 81637, upload-time = "2025-10-06T14:09:42.712Z" }, + { url = "https://files.pythonhosted.org/packages/75/ff/46736024fee3429b80a165a732e38e5d5a238721e634ab41b040d49f8738/yarl-1.22.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e340382d1afa5d32b892b3ff062436d592ec3d692aeea3bef3a5cfe11bbf8c6f", size = 142000, upload-time = "2025-10-06T14:09:44.631Z" }, + { url = "https://files.pythonhosted.org/packages/5a/9a/b312ed670df903145598914770eb12de1bac44599549b3360acc96878df8/yarl-1.22.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f1e09112a2c31ffe8d80be1b0988fa6a18c5d5cad92a9ffbb1c04c91bfe52ad2", size = 94338, upload-time = "2025-10-06T14:09:46.372Z" }, + { url = "https://files.pythonhosted.org/packages/ba/f5/0601483296f09c3c65e303d60c070a5c19fcdbc72daa061e96170785bc7d/yarl-1.22.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:939fe60db294c786f6b7c2d2e121576628468f65453d86b0fe36cb52f987bd74", size = 94909, upload-time = "2025-10-06T14:09:48.648Z" }, + { url = "https://files.pythonhosted.org/packages/60/41/9a1fe0b73dbcefce72e46cf149b0e0a67612d60bfc90fb59c2b2efdfbd86/yarl-1.22.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e1651bf8e0398574646744c1885a41198eba53dc8a9312b954073f845c90a8df", size = 372940, upload-time = "2025-10-06T14:09:50.089Z" }, + { url = "https://files.pythonhosted.org/packages/17/7a/795cb6dfee561961c30b800f0ed616b923a2ec6258b5def2a00bf8231334/yarl-1.22.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b8a0588521a26bf92a57a1705b77b8b59044cdceccac7151bd8d229e66b8dedb", size = 345825, upload-time = "2025-10-06T14:09:52.142Z" }, + { url = "https://files.pythonhosted.org/packages/d7/93/a58f4d596d2be2ae7bab1a5846c4d270b894958845753b2c606d666744d3/yarl-1.22.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:42188e6a615c1a75bcaa6e150c3fe8f3e8680471a6b10150c5f7e83f47cc34d2", size = 386705, upload-time = "2025-10-06T14:09:54.128Z" }, + { url = "https://files.pythonhosted.org/packages/61/92/682279d0e099d0e14d7fd2e176bd04f48de1484f56546a3e1313cd6c8e7c/yarl-1.22.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f6d2cb59377d99718913ad9a151030d6f83ef420a2b8f521d94609ecc106ee82", size = 396518, upload-time = "2025-10-06T14:09:55.762Z" }, + { url = "https://files.pythonhosted.org/packages/db/0f/0d52c98b8a885aeda831224b78f3be7ec2e1aa4a62091f9f9188c3c65b56/yarl-1.22.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:50678a3b71c751d58d7908edc96d332af328839eea883bb554a43f539101277a", size = 377267, upload-time = "2025-10-06T14:09:57.958Z" }, + { url = "https://files.pythonhosted.org/packages/22/42/d2685e35908cbeaa6532c1fc73e89e7f2efb5d8a7df3959ea8e37177c5a3/yarl-1.22.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1e8fbaa7cec507aa24ea27a01456e8dd4b6fab829059b69844bd348f2d467124", size = 365797, upload-time = "2025-10-06T14:09:59.527Z" }, + { url = "https://files.pythonhosted.org/packages/a2/83/cf8c7bcc6355631762f7d8bdab920ad09b82efa6b722999dfb05afa6cfac/yarl-1.22.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:433885ab5431bc3d3d4f2f9bd15bfa1614c522b0f1405d62c4f926ccd69d04fa", size = 365535, upload-time = "2025-10-06T14:10:01.139Z" }, + { url = "https://files.pythonhosted.org/packages/25/e1/5302ff9b28f0c59cac913b91fe3f16c59a033887e57ce9ca5d41a3a94737/yarl-1.22.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:b790b39c7e9a4192dc2e201a282109ed2985a1ddbd5ac08dc56d0e121400a8f7", size = 382324, upload-time = "2025-10-06T14:10:02.756Z" }, + { url = "https://files.pythonhosted.org/packages/bf/cd/4617eb60f032f19ae3a688dc990d8f0d89ee0ea378b61cac81ede3e52fae/yarl-1.22.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:31f0b53913220599446872d757257be5898019c85e7971599065bc55065dc99d", size = 383803, upload-time = "2025-10-06T14:10:04.552Z" }, + { url = "https://files.pythonhosted.org/packages/59/65/afc6e62bb506a319ea67b694551dab4a7e6fb7bf604e9bd9f3e11d575fec/yarl-1.22.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a49370e8f711daec68d09b821a34e1167792ee2d24d405cbc2387be4f158b520", size = 374220, upload-time = "2025-10-06T14:10:06.489Z" }, + { url = "https://files.pythonhosted.org/packages/e7/3d/68bf18d50dc674b942daec86a9ba922d3113d8399b0e52b9897530442da2/yarl-1.22.0-cp312-cp312-win32.whl", hash = "sha256:70dfd4f241c04bd9239d53b17f11e6ab672b9f1420364af63e8531198e3f5fe8", size = 81589, upload-time = "2025-10-06T14:10:09.254Z" }, + { url = "https://files.pythonhosted.org/packages/c8/9a/6ad1a9b37c2f72874f93e691b2e7ecb6137fb2b899983125db4204e47575/yarl-1.22.0-cp312-cp312-win_amd64.whl", hash = "sha256:8884d8b332a5e9b88e23f60bb166890009429391864c685e17bd73a9eda9105c", size = 87213, upload-time = "2025-10-06T14:10:11.369Z" }, + { url = "https://files.pythonhosted.org/packages/44/c5/c21b562d1680a77634d748e30c653c3ca918beb35555cff24986fff54598/yarl-1.22.0-cp312-cp312-win_arm64.whl", hash = "sha256:ea70f61a47f3cc93bdf8b2f368ed359ef02a01ca6393916bc8ff877427181e74", size = 81330, upload-time = "2025-10-06T14:10:13.112Z" }, + { url = "https://files.pythonhosted.org/packages/ea/f3/d67de7260456ee105dc1d162d43a019ecad6b91e2f51809d6cddaa56690e/yarl-1.22.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8dee9c25c74997f6a750cd317b8ca63545169c098faee42c84aa5e506c819b53", size = 139980, upload-time = "2025-10-06T14:10:14.601Z" }, + { url = "https://files.pythonhosted.org/packages/01/88/04d98af0b47e0ef42597b9b28863b9060bb515524da0a65d5f4db160b2d5/yarl-1.22.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:01e73b85a5434f89fc4fe27dcda2aff08ddf35e4d47bbbea3bdcd25321af538a", size = 93424, upload-time = "2025-10-06T14:10:16.115Z" }, + { url = "https://files.pythonhosted.org/packages/18/91/3274b215fd8442a03975ce6bee5fe6aa57a8326b29b9d3d56234a1dca244/yarl-1.22.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:22965c2af250d20c873cdbee8ff958fb809940aeb2e74ba5f20aaf6b7ac8c70c", size = 93821, upload-time = "2025-10-06T14:10:17.993Z" }, + { url = "https://files.pythonhosted.org/packages/61/3a/caf4e25036db0f2da4ca22a353dfeb3c9d3c95d2761ebe9b14df8fc16eb0/yarl-1.22.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b4f15793aa49793ec8d1c708ab7f9eded1aa72edc5174cae703651555ed1b601", size = 373243, upload-time = "2025-10-06T14:10:19.44Z" }, + { url = "https://files.pythonhosted.org/packages/6e/9e/51a77ac7516e8e7803b06e01f74e78649c24ee1021eca3d6a739cb6ea49c/yarl-1.22.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e5542339dcf2747135c5c85f68680353d5cb9ffd741c0f2e8d832d054d41f35a", size = 342361, upload-time = "2025-10-06T14:10:21.124Z" }, + { url = "https://files.pythonhosted.org/packages/d4/f8/33b92454789dde8407f156c00303e9a891f1f51a0330b0fad7c909f87692/yarl-1.22.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5c401e05ad47a75869c3ab3e35137f8468b846770587e70d71e11de797d113df", size = 387036, upload-time = "2025-10-06T14:10:22.902Z" }, + { url = "https://files.pythonhosted.org/packages/d9/9a/c5db84ea024f76838220280f732970aa4ee154015d7f5c1bfb60a267af6f/yarl-1.22.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:243dda95d901c733f5b59214d28b0120893d91777cb8aa043e6ef059d3cddfe2", size = 397671, upload-time = "2025-10-06T14:10:24.523Z" }, + { url = "https://files.pythonhosted.org/packages/11/c9/cd8538dc2e7727095e0c1d867bad1e40c98f37763e6d995c1939f5fdc7b1/yarl-1.22.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bec03d0d388060058f5d291a813f21c011041938a441c593374da6077fe21b1b", size = 377059, upload-time = "2025-10-06T14:10:26.406Z" }, + { url = "https://files.pythonhosted.org/packages/a1/b9/ab437b261702ced75122ed78a876a6dec0a1b0f5e17a4ac7a9a2482d8abe/yarl-1.22.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b0748275abb8c1e1e09301ee3cf90c8a99678a4e92e4373705f2a2570d581273", size = 365356, upload-time = "2025-10-06T14:10:28.461Z" }, + { url = "https://files.pythonhosted.org/packages/b2/9d/8e1ae6d1d008a9567877b08f0ce4077a29974c04c062dabdb923ed98e6fe/yarl-1.22.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:47fdb18187e2a4e18fda2c25c05d8251a9e4a521edaed757fef033e7d8498d9a", size = 361331, upload-time = "2025-10-06T14:10:30.541Z" }, + { url = "https://files.pythonhosted.org/packages/ca/5a/09b7be3905962f145b73beb468cdd53db8aa171cf18c80400a54c5b82846/yarl-1.22.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c7044802eec4524fde550afc28edda0dd5784c4c45f0be151a2d3ba017daca7d", size = 382590, upload-time = "2025-10-06T14:10:33.352Z" }, + { url = "https://files.pythonhosted.org/packages/aa/7f/59ec509abf90eda5048b0bc3e2d7b5099dffdb3e6b127019895ab9d5ef44/yarl-1.22.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:139718f35149ff544caba20fce6e8a2f71f1e39b92c700d8438a0b1d2a631a02", size = 385316, upload-time = "2025-10-06T14:10:35.034Z" }, + { url = "https://files.pythonhosted.org/packages/e5/84/891158426bc8036bfdfd862fabd0e0fa25df4176ec793e447f4b85cf1be4/yarl-1.22.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e1b51bebd221006d3d2f95fbe124b22b247136647ae5dcc8c7acafba66e5ee67", size = 374431, upload-time = "2025-10-06T14:10:37.76Z" }, + { url = "https://files.pythonhosted.org/packages/bb/49/03da1580665baa8bef5e8ed34c6df2c2aca0a2f28bf397ed238cc1bbc6f2/yarl-1.22.0-cp313-cp313-win32.whl", hash = "sha256:d3e32536234a95f513bd374e93d717cf6b2231a791758de6c509e3653f234c95", size = 81555, upload-time = "2025-10-06T14:10:39.649Z" }, + { url = "https://files.pythonhosted.org/packages/9a/ee/450914ae11b419eadd067c6183ae08381cfdfcb9798b90b2b713bbebddda/yarl-1.22.0-cp313-cp313-win_amd64.whl", hash = "sha256:47743b82b76d89a1d20b83e60d5c20314cbd5ba2befc9cda8f28300c4a08ed4d", size = 86965, upload-time = "2025-10-06T14:10:41.313Z" }, + { url = "https://files.pythonhosted.org/packages/98/4d/264a01eae03b6cf629ad69bae94e3b0e5344741e929073678e84bf7a3e3b/yarl-1.22.0-cp313-cp313-win_arm64.whl", hash = "sha256:5d0fcda9608875f7d052eff120c7a5da474a6796fe4d83e152e0e4d42f6d1a9b", size = 81205, upload-time = "2025-10-06T14:10:43.167Z" }, + { url = "https://files.pythonhosted.org/packages/88/fc/6908f062a2f77b5f9f6d69cecb1747260831ff206adcbc5b510aff88df91/yarl-1.22.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:719ae08b6972befcba4310e49edb1161a88cdd331e3a694b84466bd938a6ab10", size = 146209, upload-time = "2025-10-06T14:10:44.643Z" }, + { url = "https://files.pythonhosted.org/packages/65/47/76594ae8eab26210b4867be6f49129861ad33da1f1ebdf7051e98492bf62/yarl-1.22.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:47d8a5c446df1c4db9d21b49619ffdba90e77c89ec6e283f453856c74b50b9e3", size = 95966, upload-time = "2025-10-06T14:10:46.554Z" }, + { url = "https://files.pythonhosted.org/packages/ab/ce/05e9828a49271ba6b5b038b15b3934e996980dd78abdfeb52a04cfb9467e/yarl-1.22.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:cfebc0ac8333520d2d0423cbbe43ae43c8838862ddb898f5ca68565e395516e9", size = 97312, upload-time = "2025-10-06T14:10:48.007Z" }, + { url = "https://files.pythonhosted.org/packages/d1/c5/7dffad5e4f2265b29c9d7ec869c369e4223166e4f9206fc2243ee9eea727/yarl-1.22.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4398557cbf484207df000309235979c79c4356518fd5c99158c7d38203c4da4f", size = 361967, upload-time = "2025-10-06T14:10:49.997Z" }, + { url = "https://files.pythonhosted.org/packages/50/b2/375b933c93a54bff7fc041e1a6ad2c0f6f733ffb0c6e642ce56ee3b39970/yarl-1.22.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2ca6fd72a8cd803be290d42f2dec5cdcd5299eeb93c2d929bf060ad9efaf5de0", size = 323949, upload-time = "2025-10-06T14:10:52.004Z" }, + { url = "https://files.pythonhosted.org/packages/66/50/bfc2a29a1d78644c5a7220ce2f304f38248dc94124a326794e677634b6cf/yarl-1.22.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ca1f59c4e1ab6e72f0a23c13fca5430f889634166be85dbf1013683e49e3278e", size = 361818, upload-time = "2025-10-06T14:10:54.078Z" }, + { url = "https://files.pythonhosted.org/packages/46/96/f3941a46af7d5d0f0498f86d71275696800ddcdd20426298e572b19b91ff/yarl-1.22.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6c5010a52015e7c70f86eb967db0f37f3c8bd503a695a49f8d45700144667708", size = 372626, upload-time = "2025-10-06T14:10:55.767Z" }, + { url = "https://files.pythonhosted.org/packages/c1/42/8b27c83bb875cd89448e42cd627e0fb971fa1675c9ec546393d18826cb50/yarl-1.22.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d7672ecf7557476642c88497c2f8d8542f8e36596e928e9bcba0e42e1e7d71f", size = 341129, upload-time = "2025-10-06T14:10:57.985Z" }, + { url = "https://files.pythonhosted.org/packages/49/36/99ca3122201b382a3cf7cc937b95235b0ac944f7e9f2d5331d50821ed352/yarl-1.22.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:3b7c88eeef021579d600e50363e0b6ee4f7f6f728cd3486b9d0f3ee7b946398d", size = 346776, upload-time = "2025-10-06T14:10:59.633Z" }, + { url = "https://files.pythonhosted.org/packages/85/b4/47328bf996acd01a4c16ef9dcd2f59c969f495073616586f78cd5f2efb99/yarl-1.22.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:f4afb5c34f2c6fecdcc182dfcfc6af6cccf1aa923eed4d6a12e9d96904e1a0d8", size = 334879, upload-time = "2025-10-06T14:11:01.454Z" }, + { url = "https://files.pythonhosted.org/packages/c2/ad/b77d7b3f14a4283bffb8e92c6026496f6de49751c2f97d4352242bba3990/yarl-1.22.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:59c189e3e99a59cf8d83cbb31d4db02d66cda5a1a4374e8a012b51255341abf5", size = 350996, upload-time = "2025-10-06T14:11:03.452Z" }, + { url = "https://files.pythonhosted.org/packages/81/c8/06e1d69295792ba54d556f06686cbd6a7ce39c22307100e3fb4a2c0b0a1d/yarl-1.22.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:5a3bf7f62a289fa90f1990422dc8dff5a458469ea71d1624585ec3a4c8d6960f", size = 356047, upload-time = "2025-10-06T14:11:05.115Z" }, + { url = "https://files.pythonhosted.org/packages/4b/b8/4c0e9e9f597074b208d18cef227d83aac36184bfbc6eab204ea55783dbc5/yarl-1.22.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:de6b9a04c606978fdfe72666fa216ffcf2d1a9f6a381058d4378f8d7b1e5de62", size = 342947, upload-time = "2025-10-06T14:11:08.137Z" }, + { url = "https://files.pythonhosted.org/packages/e0/e5/11f140a58bf4c6ad7aca69a892bff0ee638c31bea4206748fc0df4ebcb3a/yarl-1.22.0-cp313-cp313t-win32.whl", hash = "sha256:1834bb90991cc2999f10f97f5f01317f99b143284766d197e43cd5b45eb18d03", size = 86943, upload-time = "2025-10-06T14:11:10.284Z" }, + { url = "https://files.pythonhosted.org/packages/31/74/8b74bae38ed7fe6793d0c15a0c8207bbb819cf287788459e5ed230996cdd/yarl-1.22.0-cp313-cp313t-win_amd64.whl", hash = "sha256:ff86011bd159a9d2dfc89c34cfd8aff12875980e3bd6a39ff097887520e60249", size = 93715, upload-time = "2025-10-06T14:11:11.739Z" }, + { url = "https://files.pythonhosted.org/packages/69/66/991858aa4b5892d57aef7ee1ba6b4d01ec3b7eb3060795d34090a3ca3278/yarl-1.22.0-cp313-cp313t-win_arm64.whl", hash = "sha256:7861058d0582b847bc4e3a4a4c46828a410bca738673f35a29ba3ca5db0b473b", size = 83857, upload-time = "2025-10-06T14:11:13.586Z" }, + { url = "https://files.pythonhosted.org/packages/46/b3/e20ef504049f1a1c54a814b4b9bed96d1ac0e0610c3b4da178f87209db05/yarl-1.22.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:34b36c2c57124530884d89d50ed2c1478697ad7473efd59cfd479945c95650e4", size = 140520, upload-time = "2025-10-06T14:11:15.465Z" }, + { url = "https://files.pythonhosted.org/packages/e4/04/3532d990fdbab02e5ede063676b5c4260e7f3abea2151099c2aa745acc4c/yarl-1.22.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:0dd9a702591ca2e543631c2a017e4a547e38a5c0f29eece37d9097e04a7ac683", size = 93504, upload-time = "2025-10-06T14:11:17.106Z" }, + { url = "https://files.pythonhosted.org/packages/11/63/ff458113c5c2dac9a9719ac68ee7c947cb621432bcf28c9972b1c0e83938/yarl-1.22.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:594fcab1032e2d2cc3321bb2e51271e7cd2b516c7d9aee780ece81b07ff8244b", size = 94282, upload-time = "2025-10-06T14:11:19.064Z" }, + { url = "https://files.pythonhosted.org/packages/a7/bc/315a56aca762d44a6aaaf7ad253f04d996cb6b27bad34410f82d76ea8038/yarl-1.22.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f3d7a87a78d46a2e3d5b72587ac14b4c16952dd0887dbb051451eceac774411e", size = 372080, upload-time = "2025-10-06T14:11:20.996Z" }, + { url = "https://files.pythonhosted.org/packages/3f/3f/08e9b826ec2e099ea6e7c69a61272f4f6da62cb5b1b63590bb80ca2e4a40/yarl-1.22.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:852863707010316c973162e703bddabec35e8757e67fcb8ad58829de1ebc8590", size = 338696, upload-time = "2025-10-06T14:11:22.847Z" }, + { url = "https://files.pythonhosted.org/packages/e3/9f/90360108e3b32bd76789088e99538febfea24a102380ae73827f62073543/yarl-1.22.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:131a085a53bfe839a477c0845acf21efc77457ba2bcf5899618136d64f3303a2", size = 387121, upload-time = "2025-10-06T14:11:24.889Z" }, + { url = "https://files.pythonhosted.org/packages/98/92/ab8d4657bd5b46a38094cfaea498f18bb70ce6b63508fd7e909bd1f93066/yarl-1.22.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:078a8aefd263f4d4f923a9677b942b445a2be970ca24548a8102689a3a8ab8da", size = 394080, upload-time = "2025-10-06T14:11:27.307Z" }, + { url = "https://files.pythonhosted.org/packages/f5/e7/d8c5a7752fef68205296201f8ec2bf718f5c805a7a7e9880576c67600658/yarl-1.22.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bca03b91c323036913993ff5c738d0842fc9c60c4648e5c8d98331526df89784", size = 372661, upload-time = "2025-10-06T14:11:29.387Z" }, + { url = "https://files.pythonhosted.org/packages/b6/2e/f4d26183c8db0bb82d491b072f3127fb8c381a6206a3a56332714b79b751/yarl-1.22.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:68986a61557d37bb90d3051a45b91fa3d5c516d177dfc6dd6f2f436a07ff2b6b", size = 364645, upload-time = "2025-10-06T14:11:31.423Z" }, + { url = "https://files.pythonhosted.org/packages/80/7c/428e5812e6b87cd00ee8e898328a62c95825bf37c7fa87f0b6bb2ad31304/yarl-1.22.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:4792b262d585ff0dff6bcb787f8492e40698443ec982a3568c2096433660c694", size = 355361, upload-time = "2025-10-06T14:11:33.055Z" }, + { url = "https://files.pythonhosted.org/packages/ec/2a/249405fd26776f8b13c067378ef4d7dd49c9098d1b6457cdd152a99e96a9/yarl-1.22.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:ebd4549b108d732dba1d4ace67614b9545b21ece30937a63a65dd34efa19732d", size = 381451, upload-time = "2025-10-06T14:11:35.136Z" }, + { url = "https://files.pythonhosted.org/packages/67/a8/fb6b1adbe98cf1e2dd9fad71003d3a63a1bc22459c6e15f5714eb9323b93/yarl-1.22.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:f87ac53513d22240c7d59203f25cc3beac1e574c6cd681bbfd321987b69f95fd", size = 383814, upload-time = "2025-10-06T14:11:37.094Z" }, + { url = "https://files.pythonhosted.org/packages/d9/f9/3aa2c0e480fb73e872ae2814c43bc1e734740bb0d54e8cb2a95925f98131/yarl-1.22.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:22b029f2881599e2f1b06f8f1db2ee63bd309e2293ba2d566e008ba12778b8da", size = 370799, upload-time = "2025-10-06T14:11:38.83Z" }, + { url = "https://files.pythonhosted.org/packages/50/3c/af9dba3b8b5eeb302f36f16f92791f3ea62e3f47763406abf6d5a4a3333b/yarl-1.22.0-cp314-cp314-win32.whl", hash = "sha256:6a635ea45ba4ea8238463b4f7d0e721bad669f80878b7bfd1f89266e2ae63da2", size = 82990, upload-time = "2025-10-06T14:11:40.624Z" }, + { url = "https://files.pythonhosted.org/packages/ac/30/ac3a0c5bdc1d6efd1b41fa24d4897a4329b3b1e98de9449679dd327af4f0/yarl-1.22.0-cp314-cp314-win_amd64.whl", hash = "sha256:0d6e6885777af0f110b0e5d7e5dda8b704efed3894da26220b7f3d887b839a79", size = 88292, upload-time = "2025-10-06T14:11:42.578Z" }, + { url = "https://files.pythonhosted.org/packages/df/0a/227ab4ff5b998a1b7410abc7b46c9b7a26b0ca9e86c34ba4b8d8bc7c63d5/yarl-1.22.0-cp314-cp314-win_arm64.whl", hash = "sha256:8218f4e98d3c10d683584cb40f0424f4b9fd6e95610232dd75e13743b070ee33", size = 82888, upload-time = "2025-10-06T14:11:44.863Z" }, + { url = "https://files.pythonhosted.org/packages/06/5e/a15eb13db90abd87dfbefb9760c0f3f257ac42a5cac7e75dbc23bed97a9f/yarl-1.22.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:45c2842ff0e0d1b35a6bf1cd6c690939dacb617a70827f715232b2e0494d55d1", size = 146223, upload-time = "2025-10-06T14:11:46.796Z" }, + { url = "https://files.pythonhosted.org/packages/18/82/9665c61910d4d84f41a5bf6837597c89e665fa88aa4941080704645932a9/yarl-1.22.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:d947071e6ebcf2e2bee8fce76e10faca8f7a14808ca36a910263acaacef08eca", size = 95981, upload-time = "2025-10-06T14:11:48.845Z" }, + { url = "https://files.pythonhosted.org/packages/5d/9a/2f65743589809af4d0a6d3aa749343c4b5f4c380cc24a8e94a3c6625a808/yarl-1.22.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:334b8721303e61b00019474cc103bdac3d7b1f65e91f0bfedeec2d56dfe74b53", size = 97303, upload-time = "2025-10-06T14:11:50.897Z" }, + { url = "https://files.pythonhosted.org/packages/b0/ab/5b13d3e157505c43c3b43b5a776cbf7b24a02bc4cccc40314771197e3508/yarl-1.22.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1e7ce67c34138a058fd092f67d07a72b8e31ff0c9236e751957465a24b28910c", size = 361820, upload-time = "2025-10-06T14:11:52.549Z" }, + { url = "https://files.pythonhosted.org/packages/fb/76/242a5ef4677615cf95330cfc1b4610e78184400699bdda0acb897ef5e49a/yarl-1.22.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d77e1b2c6d04711478cb1c4ab90db07f1609ccf06a287d5607fcd90dc9863acf", size = 323203, upload-time = "2025-10-06T14:11:54.225Z" }, + { url = "https://files.pythonhosted.org/packages/8c/96/475509110d3f0153b43d06164cf4195c64d16999e0c7e2d8a099adcd6907/yarl-1.22.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c4647674b6150d2cae088fc07de2738a84b8bcedebef29802cf0b0a82ab6face", size = 363173, upload-time = "2025-10-06T14:11:56.069Z" }, + { url = "https://files.pythonhosted.org/packages/c9/66/59db471aecfbd559a1fd48aedd954435558cd98c7d0da8b03cc6c140a32c/yarl-1.22.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:efb07073be061c8f79d03d04139a80ba33cbd390ca8f0297aae9cce6411e4c6b", size = 373562, upload-time = "2025-10-06T14:11:58.783Z" }, + { url = "https://files.pythonhosted.org/packages/03/1f/c5d94abc91557384719da10ff166b916107c1b45e4d0423a88457071dd88/yarl-1.22.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e51ac5435758ba97ad69617e13233da53908beccc6cfcd6c34bbed8dcbede486", size = 339828, upload-time = "2025-10-06T14:12:00.686Z" }, + { url = "https://files.pythonhosted.org/packages/5f/97/aa6a143d3afba17b6465733681c70cf175af89f76ec8d9286e08437a7454/yarl-1.22.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:33e32a0dd0c8205efa8e83d04fc9f19313772b78522d1bdc7d9aed706bfd6138", size = 347551, upload-time = "2025-10-06T14:12:02.628Z" }, + { url = "https://files.pythonhosted.org/packages/43/3c/45a2b6d80195959239a7b2a8810506d4eea5487dce61c2a3393e7fc3c52e/yarl-1.22.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:bf4a21e58b9cde0e401e683ebd00f6ed30a06d14e93f7c8fd059f8b6e8f87b6a", size = 334512, upload-time = "2025-10-06T14:12:04.871Z" }, + { url = "https://files.pythonhosted.org/packages/86/a0/c2ab48d74599c7c84cb104ebd799c5813de252bea0f360ffc29d270c2caa/yarl-1.22.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:e4b582bab49ac33c8deb97e058cd67c2c50dac0dd134874106d9c774fd272529", size = 352400, upload-time = "2025-10-06T14:12:06.624Z" }, + { url = "https://files.pythonhosted.org/packages/32/75/f8919b2eafc929567d3d8411f72bdb1a2109c01caaab4ebfa5f8ffadc15b/yarl-1.22.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:0b5bcc1a9c4839e7e30b7b30dd47fe5e7e44fb7054ec29b5bb8d526aa1041093", size = 357140, upload-time = "2025-10-06T14:12:08.362Z" }, + { url = "https://files.pythonhosted.org/packages/cf/72/6a85bba382f22cf78add705d8c3731748397d986e197e53ecc7835e76de7/yarl-1.22.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:c0232bce2170103ec23c454e54a57008a9a72b5d1c3105dc2496750da8cfa47c", size = 341473, upload-time = "2025-10-06T14:12:10.994Z" }, + { url = "https://files.pythonhosted.org/packages/35/18/55e6011f7c044dc80b98893060773cefcfdbf60dfefb8cb2f58b9bacbd83/yarl-1.22.0-cp314-cp314t-win32.whl", hash = "sha256:8009b3173bcd637be650922ac455946197d858b3630b6d8787aa9e5c4564533e", size = 89056, upload-time = "2025-10-06T14:12:13.317Z" }, + { url = "https://files.pythonhosted.org/packages/f9/86/0f0dccb6e59a9e7f122c5afd43568b1d31b8ab7dda5f1b01fb5c7025c9a9/yarl-1.22.0-cp314-cp314t-win_amd64.whl", hash = "sha256:9fb17ea16e972c63d25d4a97f016d235c78dd2344820eb35bc034bc32012ee27", size = 96292, upload-time = "2025-10-06T14:12:15.398Z" }, + { url = "https://files.pythonhosted.org/packages/48/b7/503c98092fb3b344a179579f55814b613c1fbb1c23b3ec14a7b008a66a6e/yarl-1.22.0-cp314-cp314t-win_arm64.whl", hash = "sha256:9f6d73c1436b934e3f01df1e1b21ff765cd1d28c77dfb9ace207f746d4610ee1", size = 85171, upload-time = "2025-10-06T14:12:16.935Z" }, + { url = "https://files.pythonhosted.org/packages/73/ae/b48f95715333080afb75a4504487cbe142cae1268afc482d06692d605ae6/yarl-1.22.0-py3-none-any.whl", hash = "sha256:1380560bdba02b6b6c90de54133c81c9f2a453dee9912fe58c1dcced1edb7cff", size = 46814, upload-time = "2025-10-06T14:12:53.872Z" }, +] diff --git a/examples/typescript/clients/advanced/.env-local b/examples/typescript/clients/advanced/.env-local index 8339ac0ab9..57f92e9636 100644 --- a/examples/typescript/clients/advanced/.env-local +++ b/examples/typescript/clients/advanced/.env-local @@ -1,4 +1,5 @@ EVM_PRIVATE_KEY= SVM_PRIVATE_KEY= +STELLAR_PRIVATE_KEY= RESOURCE_SERVER_URL=http://localhost:4021 ENDPOINT_PATH=/weather \ No newline at end of file diff --git a/examples/typescript/clients/advanced/README.md b/examples/typescript/clients/advanced/README.md index 743e968637..4c3470db26 100644 --- a/examples/typescript/clients/advanced/README.md +++ b/examples/typescript/clients/advanced/README.md @@ -24,7 +24,7 @@ const response = await fetchWithPayment("http://localhost:4021/weather"); - Node.js v20+ (install via [nvm](https://github.com/nvm-sh/nvm)) - pnpm v10 (install via [pnpm.io/installation](https://pnpm.io/installation)) -- Valid EVM and/or SVM private keys for making payments +- Valid EVM, SVM, and/or Stellar private keys for making payments - A running x402 server (see [server examples](../../servers/)) - Familiarity with the [basic fetch client](../fetch/) @@ -40,6 +40,7 @@ and fill required environment variables: - `EVM_PRIVATE_KEY` - Ethereum private key for EVM payments - `SVM_PRIVATE_KEY` - Solana private key for SVM payments +- `STELLAR_PRIVATE_KEY` - Stellar secret key (starts with `S`) for signing Stellar payments 2. Install and build all packages from the typescript examples root: @@ -55,6 +56,16 @@ cd clients/advanced pnpm dev ``` +### Account Setup Instructions + +#### Stellar Testnet + +Stellar accounts need to be created and funded with both XLM and USDC. Instructions: + +1. Go to [Stellar Laboratory](https://lab.stellar.org/account/create) ➡️ Generate keypair ➡️ Fund account with Friendbot, then copy the `Secret` and `Public` keys so you can use them. +2. Add USDC trustline (required to transact USDC): go to [Fund Account](https://lab.stellar.org/account/fund) ➡️ Paste your `Public Key` ➡️ Add USDC Trustline ➡️ paste your `Secret key` ➡️ Sign transaction ➡️ Add Trustline. +3. Get testnet USDC from [Circle Faucet](https://faucet.circle.com/) (select Stellar network). + ## Available Examples Each example demonstrates a specific advanced pattern: @@ -90,6 +101,7 @@ Use the builder pattern for fine-grained control over which networks are support import { x402Client, wrapFetchWithPayment } from "@x402/fetch"; import { ExactEvmScheme } from "@x402/evm/exact/client"; import { ExactSvmScheme } from "@x402/svm/exact/client"; +import { ExactStellarScheme } from "@x402/stellar/exact/client"; import { privateKeyToAccount } from "viem/accounts"; const evmSigner = privateKeyToAccount(evmPrivateKey); @@ -100,6 +112,7 @@ const client = new x402Client() .register("eip155:*", new ExactEvmScheme(evmSigner)) // All EVM networks .register("eip155:1", new ExactEvmScheme(mainnetSigner)) // Ethereum mainnet override .register("solana:*", new ExactSvmScheme(svmSigner)); // All Solana networks + .register("stellar:*", new ExactStellarScheme(stellarSigner)); // All Stellar networks const fetchWithPayment = wrapFetchWithPayment(fetch, client); const response = await fetchWithPayment("http://localhost:4021/weather"); @@ -162,9 +175,10 @@ Configure client-side network preferences with automatic fallback: import { x402Client, wrapFetchWithPayment, type PaymentRequirements } from "@x402/fetch"; import { ExactEvmScheme } from "@x402/evm/exact/client"; import { ExactSvmScheme } from "@x402/svm/exact/client"; +import { ExactStellarScheme } from "@x402/stellar/exact/client"; // Define network preference order (most preferred first) -const networkPreferences = ["solana:", "eip155:"]; +const networkPreferences = ["eip155:", "solana:", "stellar:"]; const preferredNetworkSelector = ( _x402Version: number, @@ -181,7 +195,8 @@ const preferredNetworkSelector = ( const client = new x402Client(preferredNetworkSelector) .register("eip155:*", new ExactEvmScheme(evmSigner)) - .register("solana:*", new ExactSvmScheme(svmSigner)); + .register("solana:*", new ExactSvmScheme(svmSigner)) + .register("stellar:*", new ExactStellarScheme(stellarSigner)); const fetchWithPayment = wrapFetchWithPayment(fetch, client); const response = await fetchWithPayment("http://localhost:4021/weather"); diff --git a/examples/typescript/clients/advanced/all_networks.ts b/examples/typescript/clients/advanced/all_networks.ts index 620819f476..01c8b78fad 100644 --- a/examples/typescript/clients/advanced/all_networks.ts +++ b/examples/typescript/clients/advanced/all_networks.ts @@ -5,13 +5,15 @@ * optional chain configuration via environment variables. * * New chain support should be added here in alphabetic order by network prefix - * (e.g., "eip155" before "solana"). + * (e.g., "eip155" before "solana" before "stellar"). */ import { config } from "dotenv"; import { x402Client, wrapFetchWithPayment, x402HTTPClient } from "@x402/fetch"; import { ExactEvmScheme } from "@x402/evm/exact/client"; import { ExactSvmScheme } from "@x402/svm/exact/client"; +import { ExactStellarScheme } from "@x402/stellar/exact/client"; +import { createEd25519Signer } from "@x402/stellar"; import { privateKeyToAccount } from "viem/accounts"; import { createKeyPairSignerFromBytes } from "@solana/kit"; import { base58 } from "@scure/base"; @@ -21,6 +23,7 @@ config(); // Configuration - optional per network const evmPrivateKey = process.env.EVM_PRIVATE_KEY as `0x${string}` | undefined; const svmPrivateKey = process.env.SVM_PRIVATE_KEY as string | undefined; +const stellarPrivateKey = process.env.STELLAR_PRIVATE_KEY as string | undefined; const baseURL = process.env.RESOURCE_SERVER_URL || "http://localhost:4021"; const endpointPath = process.env.ENDPOINT_PATH || "/weather"; const url = `${baseURL}${endpointPath}`; @@ -31,8 +34,10 @@ const url = `${baseURL}${endpointPath}`; */ async function main(): Promise { // Validate at least one private key is provided - if (!evmPrivateKey && !svmPrivateKey) { - console.error("❌ At least one of EVM_PRIVATE_KEY or SVM_PRIVATE_KEY is required"); + if (!evmPrivateKey && !svmPrivateKey && !stellarPrivateKey) { + console.error( + "❌ At least one of EVM_PRIVATE_KEY, SVM_PRIVATE_KEY, or STELLAR_PRIVATE_KEY is required", + ); process.exit(1); } @@ -53,6 +58,13 @@ async function main(): Promise { console.log(`Initialized SVM account: ${svmSigner.address}`); } + // Register Stellar scheme if private key is provided + if (stellarPrivateKey) { + const stellarSigner = createEd25519Signer(stellarPrivateKey); + client.register("stellar:*", new ExactStellarScheme(stellarSigner)); + console.log(`Initialized Stellar account: ${stellarSigner.address}`); + } + // Wrap fetch with payment handling const fetchWithPayment = wrapFetchWithPayment(fetch, client); @@ -63,15 +75,10 @@ async function main(): Promise { const body = await response.json(); console.log("Response body:", body); - // Extract payment response if present - if (response.ok) { - const paymentResponse = new x402HTTPClient(client).getPaymentSettleResponse(name => - response.headers.get(name), - ); - console.log("\nPayment response:", JSON.stringify(paymentResponse, null, 2)); - } else { - console.log(`\nNo payment settled (response status: ${response.status})`); - } + const paymentResponse = new x402HTTPClient(client).getPaymentSettleResponse(name => + response.headers.get(name), + ); + console.log("\nPayment response:", JSON.stringify(paymentResponse, null, 2)); } main().catch(error => { diff --git a/examples/typescript/clients/advanced/builder-pattern.ts b/examples/typescript/clients/advanced/builder-pattern.ts index 67c098145b..0a91bd122e 100644 --- a/examples/typescript/clients/advanced/builder-pattern.ts +++ b/examples/typescript/clients/advanced/builder-pattern.ts @@ -56,12 +56,10 @@ export async function runBuilderPatternExample( console.log("✅ Request completed\n"); console.log("Response body:", body); - if (response.ok) { - const paymentResponse = new x402HTTPClient(client).getPaymentSettleResponse(name => - response.headers.get(name), - ); - if (paymentResponse) { - console.log("\n💰 Payment Details:", paymentResponse); - } + const paymentResponse = new x402HTTPClient(client).getPaymentSettleResponse(name => + response.headers.get(name), + ); + if (paymentResponse) { + console.log("\n💰 Payment Details:", paymentResponse); } } diff --git a/examples/typescript/clients/advanced/package.json b/examples/typescript/clients/advanced/package.json index fa8b4a4b4d..66b2fc1a26 100644 --- a/examples/typescript/clients/advanced/package.json +++ b/examples/typescript/clients/advanced/package.json @@ -18,6 +18,7 @@ "@scure/base": "^1.2.6", "@x402/evm": "workspace:*", "@x402/svm": "workspace:*", + "@x402/stellar": "workspace:*", "@x402/fetch": "workspace:*", "dotenv": "^16.4.7", "viem": "^2.39.0", diff --git a/examples/typescript/clients/axios/index.ts b/examples/typescript/clients/axios/index.ts index b3cfed0f18..b0d96c249d 100644 --- a/examples/typescript/clients/axios/index.ts +++ b/examples/typescript/clients/axios/index.ts @@ -39,14 +39,10 @@ async function main(): Promise { const body = response.data; console.log("Response body:", body); - if (response.status < 400) { - const paymentResponse = new x402HTTPClient(client).getPaymentSettleResponse( - name => response.headers[name.toLowerCase()], - ); - console.log("\nPayment response:", paymentResponse); - } else { - console.log(`\nNo payment settled (response status: ${response.status})`); - } + const paymentResponse = new x402HTTPClient(client).getPaymentSettleResponse( + name => response.headers[name.toLowerCase()], + ); + console.log("\nPayment response:", paymentResponse); } main().catch(error => { diff --git a/examples/typescript/clients/fetch/index.ts b/examples/typescript/clients/fetch/index.ts index ef5670feb8..d51b341025 100644 --- a/examples/typescript/clients/fetch/index.ts +++ b/examples/typescript/clients/fetch/index.ts @@ -35,17 +35,16 @@ async function main(): Promise { console.log(`Making request to: ${url}\n`); const response = await fetchWithPayment(url, { method: "GET" }); - const body = await response.json(); + const contentType = response.headers.get("content-type") ?? ""; + const body = contentType.includes("application/json") + ? await response.json() + : await response.text(); console.log("Response body:", body); - if (response.ok) { - const paymentResponse = new x402HTTPClient(client).getPaymentSettleResponse(name => - response.headers.get(name), - ); - console.log("\nPayment response:", JSON.stringify(paymentResponse, null, 2)); - } else { - console.log(`\nNo payment settled (response status: ${response.status})`); - } + const paymentResponse = new x402HTTPClient(client).getPaymentSettleResponse(name => + response.headers.get(name), + ); + console.log("\nPayment response:", JSON.stringify(paymentResponse, null, 2)); } main().catch(error => { diff --git a/examples/typescript/clients/fetch/package.json b/examples/typescript/clients/fetch/package.json index dd487128e8..b7dc80299d 100644 --- a/examples/typescript/clients/fetch/package.json +++ b/examples/typescript/clients/fetch/package.json @@ -23,6 +23,7 @@ }, "devDependencies": { "@eslint/js": "^9.24.0", + "@types/node": "^20.0.0", "@typescript-eslint/eslint-plugin": "^8.29.1", "@typescript-eslint/parser": "^8.29.1", "eslint": "^9.24.0", diff --git a/examples/typescript/clients/offer-receipt/.env-local b/examples/typescript/clients/offer-receipt/.env-local new file mode 100644 index 0000000000..181329cc56 --- /dev/null +++ b/examples/typescript/clients/offer-receipt/.env-local @@ -0,0 +1,13 @@ +# x402 Receipt Attestation Example Configuration + +# EVM private key for making payments (required) +EVM_PRIVATE_KEY=0x_your_evm_private_key_here + +# SVM private key for Solana payments (required) +SVM_PRIVATE_KEY=your_solana_private_key_here + +# Resource server URL (optional, defaults to localhost:4021) +RESOURCE_SERVER_URL=http://localhost:4021 + +# Endpoint path (optional, defaults to /weather) +ENDPOINT_PATH=/weather diff --git a/examples/typescript/clients/offer-receipt/.prettierignore b/examples/typescript/clients/offer-receipt/.prettierignore new file mode 100644 index 0000000000..3049672b5c --- /dev/null +++ b/examples/typescript/clients/offer-receipt/.prettierignore @@ -0,0 +1,8 @@ +docs/ +dist/ +node_modules/ +coverage/ +.github/ +src/client +**/**/*.json +*.md diff --git a/examples/typescript/clients/offer-receipt/.prettierrc b/examples/typescript/clients/offer-receipt/.prettierrc new file mode 100644 index 0000000000..ffb416b74b --- /dev/null +++ b/examples/typescript/clients/offer-receipt/.prettierrc @@ -0,0 +1,11 @@ +{ + "tabWidth": 2, + "useTabs": false, + "semi": true, + "singleQuote": false, + "trailingComma": "all", + "bracketSpacing": true, + "arrowParens": "avoid", + "printWidth": 100, + "proseWrap": "never" +} diff --git a/examples/typescript/clients/offer-receipt/README.md b/examples/typescript/clients/offer-receipt/README.md new file mode 100644 index 0000000000..05eecfe6c3 --- /dev/null +++ b/examples/typescript/clients/offer-receipt/README.md @@ -0,0 +1,109 @@ +# Offer/Receipt Client Example + +Demonstrates how clients extract and verify signed offers and receipts from x402 payment flows. + +For background on why receipts matter, payload structure, and security considerations, see the [Offer/Receipt Extension README](../../../../typescript/packages/extensions/src/offer-receipt/README.md). + +## Use Cases for Signed Receipts/Offers + +- Verified user reviews ("Verified Purchase" badges) +- Audit trails and compliance records +- Dispute resolution evidence +- Agent memory (AI agents proving past interactions) + +## Quick Start + +1. Install dependencies from the typescript examples root: + +```bash +cd ../../ +pnpm install && pnpm build +cd clients/offer-receipt +``` + +2. Copy `.env-local` to `.env` and configure: + +```bash +cp .env-local .env +``` + +Required environment variables: +- `EVM_PRIVATE_KEY` - Private key for EVM payments +- `SVM_PRIVATE_KEY` - Private key for Solana payments (base58) +- `RESOURCE_SERVER_URL` - Server URL (default: `http://localhost:4021`) +- `ENDPOINT_PATH` - Endpoint path (default: `/weather`) + +3. Run the example: + +```bash +pnpm start +``` + +## What This Example Shows + +The example uses the raw flow (not the wrapper) for visibility into each step: + +1. Make initial request → receive 402 with signed offers +2. Extract and decode offers to inspect payment options +3. Verify offer signatures and select a verified offer +4. Find matching `accepts[]` entry for selected offer +5. Create payment and retry the request +6. Extract signed receipt from success response +7. Verify receipt signature +8. Verify receipt payload matches the offer + +See [index.ts](./index.ts) for the full implementation with detailed comments. + +## Signature Verification + +The extraction functions (`extractReceiptPayload`, `extractOfferPayload`) decode payloads without verifying signatures. To verify that offers and receipts are authentic, use the verification functions from `@x402/extensions/offer-receipt`: + +- `verifyOfferSignatureJWS` / `verifyReceiptSignatureJWS` - For JWS signatures +- `verifyOfferSignatureEIP712` / `verifyReceiptSignatureEIP712` - For EIP-712 signatures + +See [index.ts](./index.ts) for usage examples. + +### Supported Key Types (JWS) + +- **Ed25519** - EdDSA signatures +- **secp256k1** - ES256K signatures (Ethereum-compatible) +- **secp256r1** - ES256 signatures (NIST P-256) + +### Supported DID Methods (JWS) + +The `kid` header identifies the signing key. These DID methods support automatic key extraction: + +| Method | Description | Example | +|------------|------------------------------------------|----------------------------------| +| `did:key` | Self-contained key in the DID | `did:key:z6Mk...` | +| `did:jwk` | Base64url-encoded JWK | `did:jwk:eyJrdH...` | +| `did:web` | Fetches key from `.well-known/did.json` | `did:web:api.example.com#key-1` | + +For other DID methods, provide the public key explicitly to the verification function. + +### EIP-712 Verification + +EIP-712 signatures don't use DIDs - the signer address is recovered directly from the signature. + +## Key-to-Domain Binding + +Signature verification proves the offer/receipt was signed by a specific key. To fully trust it, verify the key is authorized to sign for the resource's domain via: + +- `did:web` document at `https:///.well-known/did.json` +- DNS TXT record binding the DID to the domain +- On-chain attestation (e.g., OMATrust key binding attestation) + +See: [Extension Specification §4.5.1](../../../../specs/extensions/extension-offer-and-receipt.md) + +## Security Considerations + +1. **Private Key Management**: Loading private keys from environment variables is for demonstration only. In production, use secure key management (HSM, KMS, hardware wallets). + +2. **Key Separation**: The payment signing key SHOULD be different from keys controlling wallets with significant funds. + +3. **Key-to-Domain Binding** (for servers): See [Extension Specification §4.5.1](../../../../specs/extensions/extension-offer-and-receipt.md) + +## Related + +- [Offer/Receipt Extension](../../../../typescript/packages/extensions/src/offer-receipt/) - Types, signing utilities, client functions +- [Extension Specification](../../../../specs/extensions/extension-offer-and-receipt.md) - Full protocol spec diff --git a/examples/typescript/clients/offer-receipt/eslint.config.js b/examples/typescript/clients/offer-receipt/eslint.config.js new file mode 100644 index 0000000000..ca28b5c47f --- /dev/null +++ b/examples/typescript/clients/offer-receipt/eslint.config.js @@ -0,0 +1,72 @@ +import js from "@eslint/js"; +import ts from "@typescript-eslint/eslint-plugin"; +import tsParser from "@typescript-eslint/parser"; +import prettier from "eslint-plugin-prettier"; +import jsdoc from "eslint-plugin-jsdoc"; +import importPlugin from "eslint-plugin-import"; + +export default [ + { + ignores: ["dist/**", "node_modules/**"], + }, + { + files: ["**/*.ts"], + languageOptions: { + parser: tsParser, + sourceType: "module", + ecmaVersion: 2020, + globals: { + process: "readonly", + __dirname: "readonly", + module: "readonly", + require: "readonly", + Buffer: "readonly", + exports: "readonly", + setTimeout: "readonly", + clearTimeout: "readonly", + setInterval: "readonly", + clearInterval: "readonly", + }, + }, + plugins: { + "@typescript-eslint": ts, + prettier: prettier, + jsdoc: jsdoc, + import: importPlugin, + }, + rules: { + ...ts.configs.recommended.rules, + "import/first": "error", + "prettier/prettier": "error", + "@typescript-eslint/member-ordering": "error", + "@typescript-eslint/no-unused-vars": ["error", { argsIgnorePattern: "^_$" }], + "jsdoc/tag-lines": ["error", "any", { startLines: 1 }], + "jsdoc/check-alignment": "error", + "jsdoc/no-undefined-types": "off", + "jsdoc/check-param-names": "error", + "jsdoc/check-tag-names": "error", + "jsdoc/check-types": "error", + "jsdoc/implements-on-classes": "error", + "jsdoc/require-description": "error", + "jsdoc/require-jsdoc": [ + "error", + { + require: { + FunctionDeclaration: true, + MethodDefinition: true, + ClassDeclaration: true, + ArrowFunctionExpression: false, + FunctionExpression: false, + }, + }, + ], + "jsdoc/require-param": "error", + "jsdoc/require-param-description": "error", + "jsdoc/require-param-type": "off", + "jsdoc/require-returns": "error", + "jsdoc/require-returns-description": "error", + "jsdoc/require-returns-type": "off", + "jsdoc/require-hyphen-before-param-description": ["error", "always"], + }, + }, +]; diff --git a/examples/typescript/clients/offer-receipt/index.ts b/examples/typescript/clients/offer-receipt/index.ts new file mode 100644 index 0000000000..be46cd1ce5 --- /dev/null +++ b/examples/typescript/clients/offer-receipt/index.ts @@ -0,0 +1,250 @@ +/** + * x402 Receipt Attestation Client Example + * + * Demonstrates extracting signed offers and receipts from x402 payment flows. + * Uses the raw flow for visibility into what's happening at each step. + */ + +import { config } from "dotenv"; +import { x402Client, x402HTTPClient, type PaymentRequired } from "@x402/fetch"; +import { registerExactEvmScheme } from "@x402/evm/exact/client"; +import { registerExactSvmScheme } from "@x402/svm/exact/client"; +import { privateKeyToAccount } from "viem/accounts"; +import { createKeyPairSignerFromBytes } from "@solana/kit"; +import { base58 } from "@scure/base"; +import { + extractOffersFromPaymentRequired, + decodeSignedOffers, + findAcceptsObjectFromSignedOffer, + extractReceiptFromResponse, + extractReceiptPayload, + verifyReceiptMatchesOffer, + verifyOfferSignatureJWS, + verifyOfferSignatureEIP712, + verifyReceiptSignatureJWS, + verifyReceiptSignatureEIP712, + isJWSSignedOffer, + isJWSSignedReceipt, +} from "@x402/extensions/offer-receipt"; + +config(); + +const evmPrivateKey = process.env.EVM_PRIVATE_KEY as `0x${string}`; +const svmPrivateKey = process.env.SVM_PRIVATE_KEY as string; +const baseURL = process.env.RESOURCE_SERVER_URL || "http://localhost:4021"; +const endpointPath = process.env.ENDPOINT_PATH || "/weather"; +const url = `${baseURL}${endpointPath}`; + +/** + * Main entry point demonstrating x402 payment flow with offer-receipt extension + * + * @returns - Promise that resolves when the example completes + */ +async function main(): Promise { + // Set up payment client + const evmSigner = privateKeyToAccount(evmPrivateKey); + const svmSigner = await createKeyPairSignerFromBytes(base58.decode(svmPrivateKey)); + + const client = new x402Client(); + registerExactEvmScheme(client, { signer: evmSigner }); + registerExactSvmScheme(client, { signer: svmSigner }); + + const httpClient = new x402HTTPClient(client); + + // ========================================================================= + // Step 1: Initial request (expect 402) + // ========================================================================= + console.log(`Requesting: ${url}`); + const initialResponse = await fetch(url, { method: "GET" }); + + if (initialResponse.status !== 402) { + const body = await initialResponse.json(); + console.log("Response:", body); + return; + } + + // ========================================================================= + // Step 2: Extract and decode signed offers from 402 response + // ========================================================================= + const paymentRequiredBody = (await initialResponse.json()) as PaymentRequired; + const getHeader = (name: string) => initialResponse.headers.get(name); + const paymentRequired = httpClient.getPaymentRequiredResponse(getHeader, paymentRequiredBody); + + const signedOffers = extractOffersFromPaymentRequired(paymentRequired); + + if (signedOffers.length === 0) { + console.log("No signed offers (server may not have offer signing enabled)"); + return; + } + + // Decode all offers to inspect their payloads + const decodedOffers = decodeSignedOffers(signedOffers); + + console.log(`\nSigned Offers (${decodedOffers.length}):`); + decodedOffers.forEach((d, i) => { + console.log(` [${i}] ${d.scheme} on ${d.network}: ${d.amount} to ${d.payTo}`); + }); + + // ========================================================================= + // Step 3: Verify offer signatures and select a verified offer + // ========================================================================= + // Only consider offers that pass signature verification + console.log("\nVerifying offer signatures..."); + + let selected = null; + for (const decoded of decodedOffers) { + try { + if (isJWSSignedOffer(decoded.signedOffer)) { + await verifyOfferSignatureJWS(decoded.signedOffer); + console.log(` [${decoded.acceptIndex}] JWS: ✓ Valid`); + } else { + const { signer } = await verifyOfferSignatureEIP712(decoded.signedOffer); + console.log(` [${decoded.acceptIndex}] EIP-712: ✓ Valid (signer: ${signer})`); + } + selected = decoded; + break; + } catch (err) { + console.log( + ` [${decoded.acceptIndex}] ✗ FAILED - ${err instanceof Error ? err.message : err}`, + ); + } + } + + if (!selected) { + console.log("\nNo offers passed signature verification"); + return; + } + + // ========================================================================= + // Step 4: Find matching accepts entry for selected offer + // ========================================================================= + const matchingAccept = findAcceptsObjectFromSignedOffer(selected, paymentRequired.accepts); + + if (!matchingAccept) { + console.log("\nNo matching accepts[] entry for signed offer"); + return; + } + + console.log(`\nSelected: ${selected.scheme} on ${selected.network}`); + + // ========================================================================= + // Step 5: Create payment and retry + // ========================================================================= + console.log("Making payment..."); + + const paymentPayload = await client.createPaymentPayload(paymentRequired); + const paymentHeaders = httpClient.encodePaymentSignatureHeader(paymentPayload); + + const paidResponse = await fetch(url, { + method: "GET", + headers: paymentHeaders, + }); + + if (!paidResponse.ok) { + console.error(`Payment failed: ${paidResponse.status}`); + return; + } + + const responseBody = await paidResponse.json(); + console.log("Response:", responseBody); + + const paymentResponse = httpClient.getPaymentSettleResponse(name => + paidResponse.headers.get(name), + ); + console.log("\nPayment response:", JSON.stringify(paymentResponse, null, 2)); + + // ========================================================================= + // Step 6: Extract signed receipt from success response + // ========================================================================= + const signedReceipt = extractReceiptFromResponse(paidResponse); + + if (signedReceipt) { + const receiptPayload = extractReceiptPayload(signedReceipt); + console.log(`\nSigned Receipt:`); + console.log(` format: ${signedReceipt.format}`); + console.log(` resourceUrl: ${receiptPayload.resourceUrl}`); + console.log(` payer: ${receiptPayload.payer}`); + console.log(` network: ${receiptPayload.network}`); + console.log(` issuedAt: ${new Date(receiptPayload.issuedAt * 1000).toISOString()}`); + if (receiptPayload.transaction) { + console.log(` transaction: ${receiptPayload.transaction}`); + } + } else { + console.log("\nNo signed receipt (server may not have receipt signing enabled)"); + } + + // ========================================================================= + // Step 7: Verify receipt signature + // ========================================================================= + if (signedReceipt) { + console.log("\nReceipt Signature Verification:"); + try { + if (isJWSSignedReceipt(signedReceipt)) { + const verifiedPayload = await verifyReceiptSignatureJWS(signedReceipt); + console.log(` JWS: ✓ Valid - payer: ${verifiedPayload.payer}`); + } else { + const { signer } = await verifyReceiptSignatureEIP712(signedReceipt); + console.log(` EIP-712: ✓ Valid - signer: ${signer}`); + } + } catch (err) { + console.log(` ✗ FAILED - ${err instanceof Error ? err.message : err}`); + } + } + + // ========================================================================= + // Step 8: Verify receipt matches offer (payload field verification) + // ========================================================================= + + if (signedReceipt) { + const payerAddresses = [evmSigner.address, svmSigner.address]; + const verified = verifyReceiptMatchesOffer(signedReceipt, selected, payerAddresses); + + console.log(`\nPayload Verification: ${verified ? "✓ PASSED" : "✗ FAILED"}`); + + if (!verified) { + const receiptPayload = extractReceiptPayload(signedReceipt); + console.log( + ` resourceUrl: ${receiptPayload.resourceUrl === selected.resourceUrl ? "✓" : "✗"}`, + ); + console.log(` network: ${receiptPayload.network === selected.network ? "✓" : "✗"}`); + const payerMatch = payerAddresses.some( + addr => receiptPayload.payer.toLowerCase() === addr.toLowerCase(), + ); + console.log(` payer: ${payerMatch ? "✓" : "✗"}`); + const issuedRecently = Math.floor(Date.now() / 1000) - receiptPayload.issuedAt < 3600; + console.log(` recent: ${issuedRecently ? "✓" : "✗"}`); + } + } + + // ========================================================================= + // Step 9: Summary - Proofs available for downstream use + // ========================================================================= + console.log("\n--- Proofs Available ---"); + if (signedReceipt) { + console.log("✓ x402-receipt (proves payment received AND service delivered)"); + } + if (selected) { + console.log("✓ x402-offer (proves server committed to payment terms)"); + } + + // ------------------------------------------------------------------------- + // Integration Point: Trust Systems (OMATrust, PEAC, etc.) + // ------------------------------------------------------------------------- + // + // This is where integration with downstream systems like OMATrust and PEAC + // can reside. These systems are planning to support x402 signed receipts + // and offers for use cases like: + // + // - Verified user reviews ("Verified Purchase" badges) + // - Audit trails and compliance records + // - Dispute resolution evidence + // - Agent memory proofs + // + // Integration examples will be added in a future update. + // ------------------------------------------------------------------------- +} + +main().catch(error => { + console.error(error?.response?.data?.error ?? error); + process.exit(1); +}); diff --git a/examples/typescript/clients/offer-receipt/package.json b/examples/typescript/clients/offer-receipt/package.json new file mode 100644 index 0000000000..f523c4e2b5 --- /dev/null +++ b/examples/typescript/clients/offer-receipt/package.json @@ -0,0 +1,35 @@ +{ + "name": "@x402/offer-receipt-example", + "private": true, + "type": "module", + "scripts": { + "start": "tsx index.ts", + "format": "prettier -c .prettierrc --write \"**/*.{ts,js,cjs,json,md}\"", + "format:check": "prettier -c .prettierrc --check \"**/*.{ts,js,cjs,json,md}\"", + "lint": "eslint . --ext .ts --fix", + "lint:check": "eslint . --ext .ts" + }, + "dependencies": { + "@scure/base": "^1.2.6", + "@solana/kit": "^2.1.1", + "@x402/evm": "workspace:*", + "@x402/extensions": "workspace:*", + "@x402/fetch": "workspace:*", + "@x402/svm": "workspace:*", + "dotenv": "^16.4.7", + "viem": "^2.39.0" + }, + "devDependencies": { + "@eslint/js": "^9.24.0", + "@types/node": "^20.0.0", + "@typescript-eslint/eslint-plugin": "^8.29.1", + "@typescript-eslint/parser": "^8.29.1", + "eslint": "^9.24.0", + "eslint-plugin-import": "^2.31.0", + "eslint-plugin-jsdoc": "^50.6.9", + "eslint-plugin-prettier": "^5.2.6", + "prettier": "3.5.2", + "tsx": "^4.7.0", + "typescript": "^5.3.0" + } +} diff --git a/examples/typescript/clients/offer-receipt/tsconfig.json b/examples/typescript/clients/offer-receipt/tsconfig.json new file mode 100644 index 0000000000..78f9479b1b --- /dev/null +++ b/examples/typescript/clients/offer-receipt/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "ES2020", + "moduleResolution": "bundler", + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "skipLibCheck": true, + "strict": true, + "resolveJsonModule": true, + "baseUrl": ".", + "types": ["node"] + }, + "include": ["index.ts"] +} diff --git a/examples/typescript/clients/payment-identifier/index.ts b/examples/typescript/clients/payment-identifier/index.ts index c11c80201a..51196ffbaf 100644 --- a/examples/typescript/clients/payment-identifier/index.ts +++ b/examples/typescript/clients/payment-identifier/index.ts @@ -65,13 +65,11 @@ async function main(): Promise { console.log(`Response (${duration1}ms):`, JSON.stringify(body1, null, 2)); - if (response1.ok) { - const paymentResponse = new x402HTTPClient(client).getPaymentSettleResponse(name => - response1.headers.get(name), - ); - if (paymentResponse) { - console.log(`\n💰 Payment settled on ${paymentResponse.network}`); - } + const paymentResponse1 = new x402HTTPClient(client).getPaymentSettleResponse(name => + response1.headers.get(name), + ); + if (paymentResponse1) { + console.log(`\n💰 Payment settled on ${paymentResponse1.network}`); } // Second request - same payment ID, should return from cache @@ -88,15 +86,13 @@ async function main(): Promise { console.log(`Response (${duration2}ms):`, JSON.stringify(body2, null, 2)); - if (response2.ok) { - const paymentResponse = new x402HTTPClient(client).getPaymentSettleResponse(name => - response2.headers.get(name), - ); - if (paymentResponse) { - console.log(`\n💰 Payment settled (unexpected - should have been cached)`); - } else { - console.log(`\n✅ No payment processed - response served from cache!`); - } + const paymentResponse2 = new x402HTTPClient(client).getPaymentSettleResponse(name => + response2.headers.get(name), + ); + if (paymentResponse2) { + console.log(`\n💰 Payment settled (unexpected - should have been cached)`); + } else { + console.log(`\n✅ No payment processed - response served from cache!`); } // Summary diff --git a/examples/typescript/clients/sign-in-with-x/README.md b/examples/typescript/clients/sign-in-with-x/README.md index e5dc2f0db5..08db11a13a 100644 --- a/examples/typescript/clients/sign-in-with-x/README.md +++ b/examples/typescript/clients/sign-in-with-x/README.md @@ -1,6 +1,8 @@ # Sign-In-With-X (SIWX) Client Example -Client demonstrating how to use Sign-In-With-X authentication with x402, allowing wallet signatures to prove prior payment and skip re-payment on subsequent requests. +Client demonstrating both SIWX flows supported by x402: +- Auth-only access for routes that require a wallet signature but no payment +- Paid-once access where SIWX proves a wallet has already paid ```typescript import { x402Client, x402HTTPClient, wrapFetchWithPayment } from "@x402/fetch"; @@ -24,19 +26,22 @@ const httpClient = new x402HTTPClient(client).onPaymentRequired( const fetchWithPayment = wrapFetchWithPayment(fetch, httpClient); -// First request: pays for access -const response1 = await fetchWithPayment("http://localhost:4021/weather"); +// Auth-only route: 402 challenge -> sign -> retry, no payment +const profile = await fetchWithPayment("http://localhost:4021/profile"); -// Second request: SIWX proves we already paid, no payment needed -const response2 = await fetchWithPayment("http://localhost:4021/weather"); +// Paid route: first request pays for access +const weather1 = await fetchWithPayment("http://localhost:4021/weather"); + +// Paid route: second request uses SIWX to prove prior payment +const weather2 = await fetchWithPayment("http://localhost:4021/weather"); ``` ## How It Works -1. **First request** — Client pays for resource access -2. **Server remembers** — Payment is recorded against wallet address -3. **Second request** — Client signs SIWX message proving wallet ownership -4. **Server grants access** — No payment required, authenticated via signature +1. **Auth-only route** — Client receives a SIWX challenge, signs it, and retries without payment +2. **Paid route, first request** — Client pays for resource access +3. **Server remembers** — Payment is recorded against wallet address +4. **Paid route, later request** — Client signs SIWX message proving wallet ownership instead of paying again ## Prerequisites @@ -59,7 +64,7 @@ and provide at least one private key: - `SVM_PRIVATE_KEY` - (Optional) Solana private key for SVM payments and SIWX authentication - `RESOURCE_SERVER_URL` - (Optional) Server URL (defaults to `http://localhost:4021`) -**Note:** At least one private key (EVM or SVM) is required. SIWX supports both EVM signatures (EIP-191) and Solana signatures (Ed25519), so authentication works with either key type. +**Note:** At least one private key (EVM or SVM) is required. The `/profile` auth-only example and the paid `/weather` and `/joke` routes all work with either signer type. 2. Install and build from typescript examples root: @@ -87,8 +92,13 @@ pnpm start ``` Client EVM address: 0x... +Client SVM address: ... Server: http://localhost:4021 +--- /profile (auth-only, no payment) --- + ✓ Authenticated via SIWX (no payment required) + Response: { address: '0x...', data: 'Your profile data' } + --- /weather --- 1. First request... ✓ Paid via payment settlement @@ -110,4 +120,6 @@ Server: http://localhost:4021 2. Second request... ✓ Authenticated via SIWX (previously paid) ... -``` \ No newline at end of file + +Done. /profile used auth-only SIWX. /weather and /joke used payment + SIWX. +``` diff --git a/examples/typescript/clients/sign-in-with-x/index.ts b/examples/typescript/clients/sign-in-with-x/index.ts index 4659510f82..a8b1b31cfe 100644 --- a/examples/typescript/clients/sign-in-with-x/index.ts +++ b/examples/typescript/clients/sign-in-with-x/index.ts @@ -79,8 +79,8 @@ async function demonstrateResource(path: string): Promise { const response1 = await fetchWithPayment(url); const body1 = await response1.json(); + logPaymentResponse(response1); if (response1.ok) { - logPaymentResponse(response1); console.log(" Response:", body1); } else if (body1.error) { console.log(" ✗ Payment failed:", body1.details || body1.error); @@ -91,8 +91,8 @@ async function demonstrateResource(path: string): Promise { const response2 = await fetchWithPayment(url); const body2 = await response2.json(); + const hasPayment = logPaymentResponse(response2); if (response2.ok) { - const hasPayment = logPaymentResponse(response2); if (!hasPayment) { console.log(" ✓ Authenticated via SIWX (previously paid)"); } @@ -102,6 +102,27 @@ async function demonstrateResource(path: string): Promise { } } +/** + * Demonstrates auth-only SIWX flow (no payment required). + * The client hook handles the 402 → sign → retry cycle automatically. + */ +async function demonstrateAuthOnly(): Promise { + const url = `${baseURL}/profile`; + console.log("\n--- /profile (auth-only, no payment) ---"); + + // fetchWithPayment handles auth-only routes the same way as paid routes: + // 402 → SIWX client hook signs the challenge → retry with signature + const response = await fetchWithPayment(url); + const body = await response.json(); + + if (response.ok) { + console.log(" ✓ Authenticated via SIWX (no payment required)"); + console.log(" Response:", body); + } else { + console.log(" ✗ Auth failed:", body); + } +} + /** * Main entry point - demonstrates SIWX authentication flow. */ @@ -114,6 +135,9 @@ async function main(): Promise { } console.log(`Server: ${baseURL}`); + // Auth-only: SIWX signature without payment + await demonstrateAuthOnly(); + await demonstrateResource("/weather"); // Small delay to avoid facilitator race condition with rapid payments @@ -121,7 +145,7 @@ async function main(): Promise { await demonstrateResource("/joke"); - console.log("\nDone. Each resource required payment once, then SIWX auth worked."); + console.log("\nDone. /profile used auth-only SIWX. /weather and /joke used payment + SIWX."); } main().catch(err => { diff --git a/examples/typescript/facilitator/.env-local b/examples/typescript/facilitator/.env-local index 7534548c3f..b9b7a2492b 100644 --- a/examples/typescript/facilitator/.env-local +++ b/examples/typescript/facilitator/.env-local @@ -1,3 +1,4 @@ PORT= EVM_PRIVATE_KEY= +STELLAR_PRIVATE_KEY= SVM_PRIVATE_KEY= \ No newline at end of file diff --git a/examples/typescript/facilitator/advanced/README.md b/examples/typescript/facilitator/advanced/README.md index d357b0d478..a0ab84758e 100644 --- a/examples/typescript/facilitator/advanced/README.md +++ b/examples/typescript/facilitator/advanced/README.md @@ -8,6 +8,7 @@ Express.js facilitator service demonstrating advanced x402 patterns including al - pnpm v10 (install via [pnpm.io/installation](https://pnpm.io/installation)) - EVM private key with Base Sepolia ETH for transaction fees - SVM private key with Solana Devnet SOL for transaction fees +- Stellar private key with testnet XLM for transaction fees (fund via [Stellar Laboratory](https://lab.stellar.org/account/create) ➡️ Generate keypair ➡️ Fund account with Friendbot) ## Setup @@ -21,6 +22,7 @@ and fill required environment variables: - `EVM_PRIVATE_KEY` - Ethereum private key - `SVM_PRIVATE_KEY` - Solana private key +- `STELLAR_PRIVATE_KEY` - Stellar secret key (starts with `S`) - `PORT` - Server port (optional, defaults to 4022) 2. Install and build all packages from the typescript examples root: @@ -42,10 +44,10 @@ pnpm dev:bazaar # Bazaar discovery extension Each example demonstrates a specific advanced pattern: -| Example | Command | Description | -| --- | --- | --- | +| Example | Command | Description | +| -------------- | ----------------------- | -------------------------------------------------------- | | `all-networks` | `pnpm dev:all-networks` | All supported networks with optional chain configuration | -| `bazaar` | `pnpm dev:bazaar` | Bazaar discovery extension for cataloging x402 resources | +| `bazaar` | `pnpm dev:bazaar` | Bazaar discovery extension for cataloging x402 resources | ## API Endpoints @@ -68,12 +70,21 @@ Returns payment schemes and networks this facilitator supports. "extra": { "feePayer": "..." } + }, + { + "x402Version": 2, + "scheme": "exact", + "network": "stellar:testnet", + "extra": { + "areFeesSponsored": true + } } ], "extensions": [], "signers": { "eip155": ["0x..."], - "solana": ["..."] + "solana": ["..."], + "stellar": ["G..."] } } ``` @@ -229,3 +240,5 @@ Networks use [CAIP-2](https://github.com/ChainAgnostic/CAIPs/blob/main/CAIPs/cai - `eip155:8453` — Base Mainnet - `solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1` — Solana Devnet - `solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp` — Solana Mainnet +- `stellar:testnet` — Stellar Testnet +- `stellar:pubnet` — Stellar Mainnet diff --git a/examples/typescript/facilitator/advanced/all_networks.ts b/examples/typescript/facilitator/advanced/all_networks.ts index 3692fd9b17..1513417b41 100644 --- a/examples/typescript/facilitator/advanced/all_networks.ts +++ b/examples/typescript/facilitator/advanced/all_networks.ts @@ -5,7 +5,7 @@ * optional chain configuration via environment variables. * * New chain support should be added here in alphabetic order by network prefix - * (e.g., "eip155" before "solana"). + * (e.g., "eip155" before "solana" before "stellar"). */ import { base58 } from "@scure/base"; @@ -21,6 +21,8 @@ import { toFacilitatorEvmSigner } from "@x402/evm"; import { ExactEvmScheme } from "@x402/evm/exact/facilitator"; import { toFacilitatorSvmSigner } from "@x402/svm"; import { ExactSvmScheme } from "@x402/svm/exact/facilitator"; +import { createEd25519Signer } from "@x402/stellar"; +import { ExactStellarScheme } from "@x402/stellar/exact/facilitator"; import dotenv from "dotenv"; import express from "express"; import { createWalletClient, http, publicActions } from "viem"; @@ -35,11 +37,12 @@ const PORT = process.env.PORT || "4022"; // Configuration - optional per network const evmPrivateKey = process.env.EVM_PRIVATE_KEY as `0x${string}` | undefined; const svmPrivateKey = process.env.SVM_PRIVATE_KEY as string | undefined; +const stellarPrivateKey = process.env.STELLAR_PRIVATE_KEY as string | undefined; // Validate at least one private key is provided -if (!evmPrivateKey && !svmPrivateKey) { +if (!evmPrivateKey && !svmPrivateKey && !stellarPrivateKey) { console.error( - "❌ At least one of EVM_PRIVATE_KEY or SVM_PRIVATE_KEY is required", + "❌ At least one of EVM_PRIVATE_KEY, SVM_PRIVATE_KEY, or STELLAR_PRIVATE_KEY is required", ); process.exit(1); } @@ -47,25 +50,26 @@ if (!evmPrivateKey && !svmPrivateKey) { // Network configuration const EVM_NETWORK = "eip155:84532"; // Base Sepolia const SVM_NETWORK = "solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1"; // Solana Devnet +const STELLAR_NETWORK = "stellar:testnet"; // Stellar Testnet // Initialize the x402 Facilitator const facilitator = new x402Facilitator() - .onBeforeVerify(async context => { + .onBeforeVerify(async (context) => { console.log("Before verify", context); }) - .onAfterVerify(async context => { + .onAfterVerify(async (context) => { console.log("After verify", context); }) - .onVerifyFailure(async context => { + .onVerifyFailure(async (context) => { console.log("Verify failure", context); }) - .onBeforeSettle(async context => { + .onBeforeSettle(async (context) => { console.log("Before settle", context); }) - .onAfterSettle(async context => { + .onAfterSettle(async (context) => { console.log("After settle", context); }) - .onSettleFailure(async context => { + .onSettleFailure(async (context) => { console.log("Settle failure", context); }); @@ -137,6 +141,17 @@ if (svmPrivateKey) { facilitator.register(SVM_NETWORK, new ExactSvmScheme(svmSigner)); } +// Register Stellar scheme if private key is provided +if (stellarPrivateKey) { + const stellarSigner = createEd25519Signer(stellarPrivateKey); + console.info(`Stellar Facilitator account: ${stellarSigner.address}`); + + facilitator.register( + STELLAR_NETWORK, + new ExactStellarScheme([stellarSigner]), + ); +} + // Initialize Express app const app = express(); app.use(express.json()); @@ -239,7 +254,14 @@ app.get("/health", (req, res) => { // Start the server app.listen(parseInt(PORT), () => { - console.log(`🚀 All Networks Facilitator listening on http://localhost:${PORT}`); - console.log(` Supported networks: ${facilitator.getSupported().kinds.map(k => k.network).join(", ")}`); + console.log( + `🚀 All Networks Facilitator listening on http://localhost:${PORT}`, + ); + console.log( + ` Supported networks: ${facilitator + .getSupported() + .kinds.map((k) => k.network) + .join(", ")}`, + ); console.log(); }); diff --git a/examples/typescript/facilitator/advanced/bazaar.ts b/examples/typescript/facilitator/advanced/bazaar.ts index d0637f4148..d1a701b560 100644 --- a/examples/typescript/facilitator/advanced/bazaar.ts +++ b/examples/typescript/facilitator/advanced/bazaar.ts @@ -59,13 +59,26 @@ interface DiscoveredResource { } // BazaarCatalog stores discovered resources +/** + * Catalog of discovered resources from bazaar discovery extension. + */ class BazaarCatalog { private resources: Map = new Map(); + /** + * Adds a discovered resource to the catalog. + * + * @param res - The discovered resource to add + */ add(res: DiscoveredResource): void { this.resources.set(res.resource, res); } + /** + * Returns all discovered resources in the catalog. + * + * @returns Array of all discovered resources + */ getAll(): DiscoveredResource[] { return Array.from(this.resources.values()); } @@ -75,10 +88,10 @@ const bazaarCatalog = new BazaarCatalog(); // Initialize the x402 Facilitator with discovery hooks const facilitator = new x402Facilitator() - .onBeforeVerify(async context => { + .onBeforeVerify(async (context) => { console.log("Before verify", context); }) - .onAfterVerify(async context => { + .onAfterVerify(async (context) => { console.log("✅ Payment verified"); // Extract discovered resource from payment for bazaar catalog @@ -93,7 +106,11 @@ const facilitator = new x402Facilitator() console.log(` 📝 Discovered resource: ${discovered.resourceUrl}`); console.log(` 📝 Description: ${discovered.description}`); console.log(` 📝 MimeType: ${discovered.mimeType}`); - console.log(` 📝 Method: ${discovered.method}`); + if ("method" in discovered && discovered.method !== undefined) { + console.log(` 📝 Method: ${discovered.method}`); + } else if ("toolName" in discovered) { + console.log(` 📝 Tool: ${discovered.toolName}`); + } console.log(` 📝 X402Version: ${discovered.x402Version}`); bazaarCatalog.add({ @@ -112,16 +129,16 @@ const facilitator = new x402Facilitator() console.log(` ⚠️ Failed to extract discovery info: ${err}`); } }) - .onVerifyFailure(async context => { + .onVerifyFailure(async (context) => { console.log("Verify failure", context); }) - .onBeforeSettle(async context => { + .onBeforeSettle(async (context) => { console.log("Before settle", context); }) - .onAfterSettle(async context => { + .onAfterSettle(async (context) => { console.log(`🎉 Payment settled: ${context.result.transaction}`); }) - .onSettleFailure(async context => { + .onSettleFailure(async (context) => { console.log("Settle failure", context); }); @@ -325,7 +342,12 @@ app.get("/health", (req, res) => { // Start the server app.listen(parseInt(PORT), () => { console.log(`🚀 Discovery Facilitator listening on http://localhost:${PORT}`); - console.log(` Supported networks: ${facilitator.getSupported().kinds.map(k => k.network).join(", ")}`); + console.log( + ` Supported networks: ${facilitator + .getSupported() + .kinds.map((k) => k.network) + .join(", ")}`, + ); console.log(` Discovery endpoint: GET /discovery/resources`); console.log(); }); diff --git a/examples/typescript/facilitator/advanced/package.json b/examples/typescript/facilitator/advanced/package.json index 4f8c88d05a..34253157d8 100644 --- a/examples/typescript/facilitator/advanced/package.json +++ b/examples/typescript/facilitator/advanced/package.json @@ -20,6 +20,7 @@ "@x402/evm": "workspace:*", "@x402/extensions": "workspace:*", "@x402/svm": "workspace:*", + "@x402/stellar": "workspace:*", "dotenv": "^16.4.5", "express": "^4.19.2", "viem": "^2.21.54" @@ -38,4 +39,4 @@ "tsx": "^4.19.2", "typescript": "^5.7.2" } -} \ No newline at end of file +} diff --git a/examples/typescript/facilitator/basic/index.ts b/examples/typescript/facilitator/basic/index.ts index b9cff1eec3..baf957bea8 100644 --- a/examples/typescript/facilitator/basic/index.ts +++ b/examples/typescript/facilitator/basic/index.ts @@ -116,8 +116,14 @@ const facilitator = new x402Facilitator() }); // Register EVM and SVM schemes -facilitator.register("eip155:84532", new ExactEvmScheme(evmSigner, { deployERC4337WithEIP6492: true })); // Base Sepolia -facilitator.register("solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1", new ExactSvmScheme(svmSigner)); // Devnet +facilitator.register( + "eip155:84532", + new ExactEvmScheme(evmSigner, { deployERC4337WithEIP6492: true }), +); // Base Sepolia +facilitator.register( + "solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1", + new ExactSvmScheme(svmSigner), +); // Devnet // Initialize Express app const app = express(); diff --git a/examples/typescript/fullstack/miniapp/app/page.tsx b/examples/typescript/fullstack/miniapp/app/page.tsx index 75ef8a3a7c..5aca262219 100644 --- a/examples/typescript/fullstack/miniapp/app/page.tsx +++ b/examples/typescript/fullstack/miniapp/app/page.tsx @@ -15,35 +15,53 @@ import { WalletDropdownDisconnect, } from "@coinbase/onchainkit/wallet"; import { useEffect, useMemo, useState, useCallback } from "react"; -import { useAccount, useWalletClient, useSwitchChain } from "wagmi"; +import { useAccount, useWalletClient, useSwitchChain, usePublicClient } from "wagmi"; import { baseSepolia } from "wagmi/chains"; import { sdk } from "@farcaster/miniapp-sdk"; import { x402Client, wrapFetchWithPayment } from "@x402/fetch"; import { ExactEvmScheme } from "@x402/evm/exact/client"; +import { toClientEvmSigner } from "@x402/evm"; import type { ClientEvmSigner } from "@x402/evm"; import type { WalletClient, Account } from "viem"; /** * Converts a wagmi/viem WalletClient to a ClientEvmSigner for x402Client */ -function wagmiToClientSigner(walletClient: WalletClient): ClientEvmSigner { +function wagmiToClientSigner( + walletClient: WalletClient, + publicClient: { readContract: (args: unknown) => Promise } +): ClientEvmSigner { if (!walletClient.account) { throw new Error("Wallet client must have an account"); } - return { - address: walletClient.account.address, - signTypedData: async (message) => { - const signature = await walletClient.signTypedData({ - account: walletClient.account as Account, - domain: message.domain, - types: message.types, - primaryType: message.primaryType, - message: message.message, - }); - return signature; + const readContractAdapter = { + readContract(args: { + address: `0x${string}`; + abi: readonly unknown[]; + functionName: string; + args?: readonly unknown[]; + }): Promise { + return publicClient.readContract(args); }, }; + + return toClientEvmSigner( + { + address: walletClient.account.address, + signTypedData: async (message) => { + const signature = await walletClient.signTypedData({ + account: walletClient.account as Account, + domain: message.domain, + types: message.types, + primaryType: message.primaryType, + message: message.message, + }); + return signature; + }, + }, + readContractAdapter + ); } export default function App() { @@ -54,6 +72,7 @@ export default function App() { const [message, setMessage] = useState(""); const { address, isConnected, chainId } = useAccount(); const { data: walletClient } = useWalletClient(); + const publicClient = usePublicClient(); const { switchChainAsync } = useSwitchChain(); sdk.actions.ready(); @@ -96,7 +115,7 @@ export default function App() { }, [addFrame]); const handleProtectedAction = useCallback(async () => { - if (!isConnected || !walletClient) { + if (!isConnected || !walletClient || !publicClient) { setMessage("Please connect your wallet first"); return; } @@ -112,7 +131,7 @@ export default function App() { // Create x402 client and register EVM scheme with wagmi signer const client = new x402Client(); - const signer = wagmiToClientSigner(walletClient); + const signer = wagmiToClientSigner(walletClient, publicClient); client.register("eip155:*", new ExactEvmScheme(signer)); // Wrap fetch with payment handling @@ -137,7 +156,7 @@ export default function App() { } finally { setIsLoading(false); } - }, [isConnected, walletClient, chainId, switchChainAsync]); + }, [isConnected, walletClient, publicClient, chainId, switchChainAsync]); const saveFrameButton = useMemo(() => { if (context && !context.client.added) { diff --git a/examples/typescript/legacy/mcp-embedded-wallet/README.md b/examples/typescript/legacy/mcp-embedded-wallet/README.md index 0c65830ea8..1c669ec86c 100644 --- a/examples/typescript/legacy/mcp-embedded-wallet/README.md +++ b/examples/typescript/legacy/mcp-embedded-wallet/README.md @@ -110,7 +110,7 @@ We now have everything we need on the renderer side. 3. Set up call from electron -Call from electron live in `operations` in `electron.ts` just for convinience. +Call from electron live in `operations` in `electron.ts` just for convenience. You might add something like this ```typescript diff --git a/examples/typescript/pnpm-lock.yaml b/examples/typescript/pnpm-lock.yaml index c3c6d73c07..0897f2bcf7 100644 --- a/examples/typescript/pnpm-lock.yaml +++ b/examples/typescript/pnpm-lock.yaml @@ -72,6 +72,9 @@ importers: ../../typescript/packages/extensions: dependencies: + '@noble/curves': + specifier: ^1.9.0 + version: 1.9.7 '@scure/base': specifier: ^1.2.6 version: 1.2.6 @@ -81,6 +84,9 @@ importers: ajv: specifier: ^8.17.1 version: 8.17.1 + jose: + specifier: ^5.9.6 + version: 5.10.0 siwe: specifier: ^2.3.2 version: 2.3.2(ethers@6.15.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)) @@ -200,12 +206,6 @@ importers: ../../typescript/packages/http/express: dependencies: - '@coinbase/cdp-sdk': - specifier: ^1.22.0 - version: 1.36.0(bufferutil@4.0.9)(encoding@0.1.13)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(utf-8-validate@5.0.10) - '@solana/kit': - specifier: ^6.1.0 - version: 6.1.0(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(utf-8-validate@5.0.10) '@x402/core': specifier: workspace:~ version: link:../../core @@ -213,7 +213,7 @@ importers: specifier: workspace:~ version: link:../../extensions '@x402/paywall': - specifier: workspace:* + specifier: workspace:^ version: link:../paywall viem: specifier: ^2.39.3 @@ -274,6 +274,67 @@ importers: specifier: ^3.0.5 version: 3.2.4(@types/debug@4.1.12)(@types/node@22.17.2)(jiti@2.6.1)(jsdom@26.1.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(lightningcss@1.30.2)(tsx@4.20.4)(yaml@2.8.1) + ../../typescript/packages/http/fastify: + dependencies: + '@x402/core': + specifier: workspace:~ + version: link:../../core + '@x402/extensions': + specifier: workspace:~ + version: link:../../extensions + '@x402/paywall': + specifier: workspace:* + version: link:../paywall + devDependencies: + '@eslint/js': + specifier: ^9.24.0 + version: 9.33.0 + '@types/node': + specifier: ^22.13.4 + version: 22.17.2 + '@typescript-eslint/eslint-plugin': + specifier: ^8.29.1 + version: 8.48.0(@typescript-eslint/parser@8.48.0(eslint@9.33.0(jiti@2.6.1))(typescript@5.9.2))(eslint@9.33.0(jiti@2.6.1))(typescript@5.9.2) + '@typescript-eslint/parser': + specifier: ^8.29.1 + version: 8.48.0(eslint@9.33.0(jiti@2.6.1))(typescript@5.9.2) + eslint: + specifier: ^9.24.0 + version: 9.33.0(jiti@2.6.1) + eslint-plugin-import: + specifier: ^2.31.0 + version: 2.32.0(@typescript-eslint/parser@8.48.0(eslint@9.33.0(jiti@2.6.1))(typescript@5.9.2))(eslint-import-resolver-typescript@3.10.1)(eslint@9.33.0(jiti@2.6.1)) + eslint-plugin-jsdoc: + specifier: ^50.6.9 + version: 50.8.0(eslint@9.33.0(jiti@2.6.1)) + eslint-plugin-prettier: + specifier: ^5.2.6 + version: 5.5.4(eslint-config-prettier@10.1.8(eslint@9.33.0(jiti@2.6.1)))(eslint@9.33.0(jiti@2.6.1))(prettier@3.5.2) + fastify: + specifier: ^5.0.0 + version: 5.8.2 + prettier: + specifier: 3.5.2 + version: 3.5.2 + tsup: + specifier: ^8.4.0 + version: 8.5.0(jiti@2.6.1)(postcss@8.5.6)(tsx@4.20.4)(typescript@5.9.2)(yaml@2.8.1) + tsx: + specifier: ^4.19.2 + version: 4.20.4 + typescript: + specifier: ^5.7.3 + version: 5.9.2 + vite: + specifier: ^6.2.6 + version: 6.3.5(@types/node@22.17.2)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.4)(yaml@2.8.1) + vite-tsconfig-paths: + specifier: ^5.1.4 + version: 5.1.4(typescript@5.9.2)(vite@6.3.5(@types/node@22.17.2)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.4)(yaml@2.8.1)) + vitest: + specifier: ^3.0.5 + version: 3.2.4(@types/debug@4.1.12)(@types/node@22.17.2)(jiti@2.6.1)(jsdom@26.1.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(lightningcss@1.30.2)(tsx@4.20.4)(yaml@2.8.1) + ../../typescript/packages/http/fetch: dependencies: '@x402/core': @@ -341,7 +402,7 @@ importers: specifier: workspace:~ version: link:../../extensions '@x402/paywall': - specifier: workspace:* + specifier: workspace:^ version: link:../paywall zod: specifier: ^3.24.2 @@ -373,7 +434,7 @@ importers: version: 5.5.4(eslint-config-prettier@10.1.8(eslint@9.33.0(jiti@2.6.1)))(eslint@9.33.0(jiti@2.6.1))(prettier@3.5.2) hono: specifier: ^4.7.1 - version: 4.9.2 + version: 4.10.7 prettier: specifier: 3.5.2 version: 3.5.2 @@ -398,9 +459,6 @@ importers: ../../typescript/packages/http/next: dependencies: - '@coinbase/cdp-sdk': - specifier: ^1.22.0 - version: 1.36.0(bufferutil@4.0.9)(encoding@0.1.13)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(utf-8-validate@5.0.10) '@x402/core': specifier: workspace:~ version: link:../../core @@ -408,7 +466,7 @@ importers: specifier: workspace:~ version: link:../../extensions '@x402/paywall': - specifier: workspace:* + specifier: workspace:^ version: link:../paywall next: specifier: ^16.0.10 @@ -907,7 +965,7 @@ importers: version: 5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.19.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)) hono: specifier: ^4.7.1 - version: 4.9.2 + version: 4.10.7 viem: specifier: ^2.21.26 version: 2.40.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) @@ -1158,9 +1216,6 @@ importers: '@x402/core': specifier: workspace:~ version: link:../../core - '@x402/extensions': - specifier: workspace:~ - version: link:../../extensions viem: specifier: ^2.39.3 version: 2.40.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) @@ -1214,6 +1269,61 @@ importers: specifier: ^3.0.5 version: 3.2.4(@types/debug@4.1.12)(@types/node@22.17.2)(jiti@2.6.1)(jsdom@26.1.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(lightningcss@1.30.2)(tsx@4.20.4)(yaml@2.8.1) + ../../typescript/packages/mechanisms/stellar: + dependencies: + '@stellar/stellar-sdk': + specifier: ^14.6.1 + version: 14.6.1 + '@x402/core': + specifier: workspace:* + version: link:../../core + devDependencies: + '@eslint/js': + specifier: ^9.24.0 + version: 9.33.0 + '@types/node': + specifier: ^22.13.4 + version: 22.17.2 + '@typescript-eslint/eslint-plugin': + specifier: ^8.29.1 + version: 8.48.0(@typescript-eslint/parser@8.48.0(eslint@9.33.0(jiti@2.6.1))(typescript@5.9.2))(eslint@9.33.0(jiti@2.6.1))(typescript@5.9.2) + '@typescript-eslint/parser': + specifier: ^8.29.1 + version: 8.48.0(eslint@9.33.0(jiti@2.6.1))(typescript@5.9.2) + eslint: + specifier: ^9.24.0 + version: 9.33.0(jiti@2.6.1) + eslint-plugin-import: + specifier: ^2.31.0 + version: 2.32.0(@typescript-eslint/parser@8.48.0(eslint@9.33.0(jiti@2.6.1))(typescript@5.9.2))(eslint-import-resolver-typescript@3.10.1)(eslint@9.33.0(jiti@2.6.1)) + eslint-plugin-jsdoc: + specifier: ^50.6.9 + version: 50.8.0(eslint@9.33.0(jiti@2.6.1)) + eslint-plugin-prettier: + specifier: ^5.2.6 + version: 5.5.4(eslint-config-prettier@10.1.8(eslint@9.33.0(jiti@2.6.1)))(eslint@9.33.0(jiti@2.6.1))(prettier@3.5.2) + prettier: + specifier: 3.5.2 + version: 3.5.2 + tsup: + specifier: ^8.4.0 + version: 8.5.0(jiti@2.6.1)(postcss@8.5.6)(tsx@4.20.4)(typescript@5.9.2)(yaml@2.8.1) + tsx: + specifier: ^4.19.2 + version: 4.20.4 + typescript: + specifier: ^5.7.3 + version: 5.9.2 + vite: + specifier: ^6.2.6 + version: 6.3.5(@types/node@22.17.2)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.4)(yaml@2.8.1) + vite-tsconfig-paths: + specifier: ^5.1.4 + version: 5.1.4(typescript@5.9.2)(vite@6.3.5(@types/node@22.17.2)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.4)(yaml@2.8.1)) + vitest: + specifier: ^3.0.5 + version: 3.2.4(@types/debug@4.1.12)(@types/node@22.17.2)(jiti@2.6.1)(jsdom@26.1.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(lightningcss@1.30.2)(tsx@4.20.4)(yaml@2.8.1) + ../../typescript/packages/mechanisms/svm: dependencies: '@solana-program/compute-budget': @@ -1295,6 +1405,9 @@ importers: '@x402/fetch': specifier: workspace:* version: link:../../../../typescript/packages/http/fetch + '@x402/stellar': + specifier: workspace:* + version: link:../../../../typescript/packages/mechanisms/stellar '@x402/svm': specifier: workspace:* version: link:../../../../typescript/packages/mechanisms/svm @@ -1476,6 +1589,9 @@ importers: '@eslint/js': specifier: ^9.24.0 version: 9.33.0 + '@types/node': + specifier: ^20.0.0 + version: 20.19.11 '@typescript-eslint/eslint-plugin': specifier: ^8.29.1 version: 8.40.0(@typescript-eslint/parser@8.40.0(eslint@9.33.0(jiti@2.6.1))(typescript@5.9.2))(eslint@9.33.0(jiti@2.6.1))(typescript@5.9.2) @@ -1614,6 +1730,67 @@ importers: specifier: ^5.9.2 version: 5.9.2 + clients/offer-receipt: + dependencies: + '@scure/base': + specifier: ^1.2.6 + version: 1.2.6 + '@solana/kit': + specifier: ^2.1.1 + version: 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.19.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + '@x402/evm': + specifier: workspace:* + version: link:../../../../typescript/packages/mechanisms/evm + '@x402/extensions': + specifier: workspace:* + version: link:../../../../typescript/packages/extensions + '@x402/fetch': + specifier: workspace:* + version: link:../../../../typescript/packages/http/fetch + '@x402/svm': + specifier: workspace:* + version: link:../../../../typescript/packages/mechanisms/svm + dotenv: + specifier: ^16.4.7 + version: 16.6.1 + viem: + specifier: ^2.39.0 + version: 2.43.5(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13) + devDependencies: + '@eslint/js': + specifier: ^9.24.0 + version: 9.33.0 + '@types/node': + specifier: ^20.0.0 + version: 20.19.11 + '@typescript-eslint/eslint-plugin': + specifier: ^8.29.1 + version: 8.48.0(@typescript-eslint/parser@8.48.0(eslint@9.33.0(jiti@2.6.1))(typescript@5.9.2))(eslint@9.33.0(jiti@2.6.1))(typescript@5.9.2) + '@typescript-eslint/parser': + specifier: ^8.29.1 + version: 8.48.0(eslint@9.33.0(jiti@2.6.1))(typescript@5.9.2) + eslint: + specifier: ^9.24.0 + version: 9.33.0(jiti@2.6.1) + eslint-plugin-import: + specifier: ^2.31.0 + version: 2.32.0(@typescript-eslint/parser@8.48.0(eslint@9.33.0(jiti@2.6.1))(typescript@5.9.2))(eslint-import-resolver-typescript@3.10.1)(eslint@9.33.0(jiti@2.6.1)) + eslint-plugin-jsdoc: + specifier: ^50.6.9 + version: 50.8.0(eslint@9.33.0(jiti@2.6.1)) + eslint-plugin-prettier: + specifier: ^5.2.6 + version: 5.5.4(eslint-config-prettier@10.1.8(eslint@9.33.0(jiti@2.6.1)))(eslint@9.33.0(jiti@2.6.1))(prettier@3.5.2) + prettier: + specifier: 3.5.2 + version: 3.5.2 + tsx: + specifier: ^4.7.0 + version: 4.20.4 + typescript: + specifier: ^5.3.0 + version: 5.9.2 + clients/payment-identifier: dependencies: '@x402/evm': @@ -1727,11 +1904,148 @@ importers: specifier: ^5.3.0 version: 5.9.2 + facilitator/advanced: + dependencies: + '@scure/base': + specifier: ^1.2.6 + version: 1.2.6 + '@solana/kit': + specifier: ^6.1.0 + version: 6.1.0(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(utf-8-validate@5.0.10) + '@x402/core': + specifier: workspace:* + version: link:../../../../typescript/packages/core + '@x402/evm': + specifier: workspace:* + version: link:../../../../typescript/packages/mechanisms/evm + '@x402/extensions': + specifier: workspace:* + version: link:../../../../typescript/packages/extensions + '@x402/stellar': + specifier: workspace:* + version: link:../../../../typescript/packages/mechanisms/stellar + '@x402/svm': + specifier: workspace:* + version: link:../../../../typescript/packages/mechanisms/svm + dotenv: + specifier: ^16.4.5 + version: 16.6.1 + express: + specifier: ^4.19.2 + version: 4.21.2 + viem: + specifier: ^2.21.54 + version: 2.43.5(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13) + devDependencies: + '@eslint/js': + specifier: ^9.24.0 + version: 9.33.0 + '@types/express': + specifier: ^4.17.21 + version: 4.17.25 + '@types/node': + specifier: ^22.10.1 + version: 22.17.2 + '@typescript-eslint/eslint-plugin': + specifier: ^8.29.1 + version: 8.48.0(@typescript-eslint/parser@8.48.0(eslint@9.33.0(jiti@2.6.1))(typescript@5.9.2))(eslint@9.33.0(jiti@2.6.1))(typescript@5.9.2) + '@typescript-eslint/parser': + specifier: ^8.29.1 + version: 8.48.0(eslint@9.33.0(jiti@2.6.1))(typescript@5.9.2) + eslint: + specifier: ^9.15.0 + version: 9.33.0(jiti@2.6.1) + eslint-plugin-import: + specifier: ^2.31.0 + version: 2.32.0(@typescript-eslint/parser@8.48.0(eslint@9.33.0(jiti@2.6.1))(typescript@5.9.2))(eslint-import-resolver-typescript@3.10.1)(eslint@9.33.0(jiti@2.6.1)) + eslint-plugin-jsdoc: + specifier: ^50.6.9 + version: 50.8.0(eslint@9.33.0(jiti@2.6.1)) + eslint-plugin-prettier: + specifier: ^5.2.6 + version: 5.5.4(eslint-config-prettier@10.1.8(eslint@9.33.0(jiti@2.6.1)))(eslint@9.33.0(jiti@2.6.1))(prettier@3.6.2) + prettier: + specifier: ^3.3.3 + version: 3.6.2 + tsx: + specifier: ^4.19.2 + version: 4.20.4 + typescript: + specifier: ^5.7.2 + version: 5.9.2 + + facilitator/basic: + dependencies: + '@scure/base': + specifier: ^1.2.6 + version: 1.2.6 + '@solana/kit': + specifier: ^6.1.0 + version: 6.1.0(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(utf-8-validate@5.0.10) + '@x402/core': + specifier: workspace:* + version: link:../../../../typescript/packages/core + '@x402/evm': + specifier: workspace:* + version: link:../../../../typescript/packages/mechanisms/evm + '@x402/extensions': + specifier: workspace:* + version: link:../../../../typescript/packages/extensions + '@x402/svm': + specifier: workspace:* + version: link:../../../../typescript/packages/mechanisms/svm + dotenv: + specifier: ^16.4.5 + version: 16.6.1 + express: + specifier: ^4.19.2 + version: 4.21.2 + viem: + specifier: ^2.21.54 + version: 2.43.5(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13) + devDependencies: + '@eslint/js': + specifier: ^9.24.0 + version: 9.33.0 + '@types/express': + specifier: ^4.17.21 + version: 4.17.25 + '@types/node': + specifier: ^22.10.1 + version: 22.17.2 + '@typescript-eslint/eslint-plugin': + specifier: ^8.29.1 + version: 8.48.0(@typescript-eslint/parser@8.48.0(eslint@9.33.0(jiti@2.6.1))(typescript@5.9.2))(eslint@9.33.0(jiti@2.6.1))(typescript@5.9.2) + '@typescript-eslint/parser': + specifier: ^8.29.1 + version: 8.48.0(eslint@9.33.0(jiti@2.6.1))(typescript@5.9.2) + eslint: + specifier: ^9.15.0 + version: 9.33.0(jiti@2.6.1) + eslint-plugin-import: + specifier: ^2.31.0 + version: 2.32.0(@typescript-eslint/parser@8.48.0(eslint@9.33.0(jiti@2.6.1))(typescript@5.9.2))(eslint-import-resolver-typescript@3.10.1)(eslint@9.33.0(jiti@2.6.1)) + eslint-plugin-jsdoc: + specifier: ^50.6.9 + version: 50.8.0(eslint@9.33.0(jiti@2.6.1)) + eslint-plugin-prettier: + specifier: ^5.2.6 + version: 5.5.4(eslint-config-prettier@10.1.8(eslint@9.33.0(jiti@2.6.1)))(eslint@9.33.0(jiti@2.6.1))(prettier@3.6.2) + prettier: + specifier: ^3.3.3 + version: 3.6.2 + tsx: + specifier: ^4.19.2 + version: 4.20.4 + typescript: + specifier: ^5.7.2 + version: 5.9.2 + fullstack/miniapp: dependencies: '@coinbase/onchainkit': specifier: 1.1.2 - version: 1.1.2(@tanstack/query-core@5.90.11)(@types/react-dom@19.2.1(@types/react@19.2.1))(@types/react@19.2.1)(bufferutil@4.0.9)(encoding@0.1.13)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.2)(use-sync-external-store@1.4.0(react@19.2.3))(utf-8-validate@5.0.10)(viem@2.40.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13))(wagmi@2.19.5(@tanstack/query-core@5.90.11)(@tanstack/react-query@5.90.11(react@19.2.3))(@types/react@19.2.1)(bufferutil@4.0.9)(encoding@0.1.13)(react@19.2.3)(typescript@5.9.2)(utf-8-validate@5.0.10)(viem@2.40.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13))(zod@4.1.13))(zod@4.1.13) + version: 1.1.2(@tanstack/query-core@5.90.11)(@types/react-dom@19.2.1(@types/react@19.2.1))(@types/react@19.2.1)(bufferutil@4.0.9)(encoding@0.1.13)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.2)(use-sync-external-store@1.4.0(react@19.2.3))(utf-8-validate@5.0.10)(viem@2.40.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13))(wagmi@2.19.5(@tanstack/query-core@5.90.11)(@tanstack/react-query@5.90.11(react@19.2.3))(@types/react@19.2.1)(bufferutil@4.0.9)(encoding@0.1.13)(fastestsmallesttextencoderdecoder@1.0.22)(react@19.2.3)(typescript@5.9.2)(utf-8-validate@5.0.10)(viem@2.40.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13))(ws@8.19.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@4.1.13))(zod@4.1.13) '@farcaster/miniapp-sdk': specifier: 0.2.1 version: 0.2.1(bufferutil@4.0.9)(encoding@0.1.13)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13) @@ -1764,7 +2078,7 @@ importers: version: 2.40.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13) wagmi: specifier: ^2.14.11 - version: 2.19.5(@tanstack/query-core@5.90.11)(@tanstack/react-query@5.90.11(react@19.2.3))(@types/react@19.2.1)(bufferutil@4.0.9)(encoding@0.1.13)(react@19.2.3)(typescript@5.9.2)(utf-8-validate@5.0.10)(viem@2.40.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13))(zod@4.1.13) + version: 2.19.5(@tanstack/query-core@5.90.11)(@tanstack/react-query@5.90.11(react@19.2.3))(@types/react@19.2.1)(bufferutil@4.0.9)(encoding@0.1.13)(fastestsmallesttextencoderdecoder@1.0.22)(react@19.2.3)(typescript@5.9.2)(utf-8-validate@5.0.10)(viem@2.40.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13))(ws@8.19.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@4.1.13) devDependencies: '@eslint/js': specifier: ^9.24.0 @@ -2054,7 +2368,7 @@ importers: dependencies: '@hono/node-server': specifier: ^1.13.8 - version: 1.19.0(hono@4.9.2) + version: 1.19.0(hono@4.10.7) axios: specifier: ^1.7.9 version: 1.13.2 @@ -2063,7 +2377,7 @@ importers: version: 16.6.1 hono: specifier: ^4.7.2 - version: 4.9.2 + version: 4.10.7 viem: specifier: ^2.23.5 version: 2.40.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13) @@ -2128,7 +2442,7 @@ importers: version: 0.39.0(encoding@0.1.13) '@hono/node-server': specifier: ^1.14.1 - version: 1.19.0(hono@4.9.2) + version: 1.19.0(hono@4.10.7) '@llamaindex/anthropic': specifier: ^0.3.3 version: 0.3.23(@llamaindex/core@0.6.5)(@llamaindex/env@0.1.30) @@ -2140,7 +2454,7 @@ importers: version: 1.13.2 hono: specifier: ^4.7.7 - version: 4.9.2 + version: 4.10.7 llamaindex: specifier: ^0.10.2 version: 0.10.6(encoding@0.1.13)(tree-sitter@0.22.4)(web-tree-sitter@0.24.7)(ws@8.19.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@3.25.76) @@ -2241,13 +2555,13 @@ importers: dependencies: '@hono/node-server': specifier: ^1.11.2 - version: 1.19.0(hono@4.9.2) + version: 1.19.0(hono@4.10.7) dotenv: specifier: ^16.4.5 version: 16.6.1 hono: specifier: ^4.4.0 - version: 4.9.2 + version: 4.10.7 node-fetch: specifier: ^3.3.2 version: 3.3.2 @@ -2284,7 +2598,7 @@ importers: dependencies: '@hono/node-server': specifier: ^1.14.0 - version: 1.19.0(hono@4.9.2) + version: 1.19.0(hono@4.10.7) axios: specifier: ^1.10.0 version: 1.13.2 @@ -2293,7 +2607,7 @@ importers: version: 16.6.1 hono: specifier: ^4.6.17 - version: 4.9.2 + version: 4.10.7 react: specifier: ^18.3.1 version: 18.3.1 @@ -2345,7 +2659,7 @@ importers: dependencies: '@coinbase/onchainkit': specifier: ^0.38.14 - version: 0.38.19(@farcaster/miniapp-sdk@0.2.1(bufferutil@4.0.9)(encoding@0.1.13)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13))(@tanstack/query-core@5.90.11)(@types/react@19.1.10)(@upstash/redis@1.35.3)(bufferutil@4.0.9)(encoding@0.1.13)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.2)(use-sync-external-store@1.5.0(react@19.2.3))(utf-8-validate@5.0.10)(zod@4.1.13) + version: 0.38.19(@farcaster/miniapp-sdk@0.2.1(bufferutil@4.0.9)(encoding@0.1.13)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13))(@tanstack/query-core@5.90.11)(@types/react@19.1.10)(@upstash/redis@1.35.3)(bufferutil@4.0.9)(encoding@0.1.13)(fastestsmallesttextencoderdecoder@1.0.22)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.2)(use-sync-external-store@1.5.0(react@19.2.3))(utf-8-validate@5.0.10)(ws@8.19.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@4.1.13) '@farcaster/frame-sdk': specifier: ^0.0.60 version: 0.0.60(bufferutil@4.0.9)(encoding@0.1.13)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13) @@ -2375,7 +2689,7 @@ importers: version: 2.40.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13) wagmi: specifier: ^2.14.11 - version: 2.19.5(@tanstack/query-core@5.90.11)(@tanstack/react-query@5.90.11(react@19.2.3))(@types/react@19.1.10)(@upstash/redis@1.35.3)(bufferutil@4.0.9)(encoding@0.1.13)(react@19.2.3)(typescript@5.9.2)(utf-8-validate@5.0.10)(viem@2.40.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13))(zod@4.1.13) + version: 2.19.5(@tanstack/query-core@5.90.11)(@tanstack/react-query@5.90.11(react@19.2.3))(@types/react@19.1.10)(@upstash/redis@1.35.3)(bufferutil@4.0.9)(encoding@0.1.13)(fastestsmallesttextencoderdecoder@1.0.22)(react@19.2.3)(typescript@5.9.2)(utf-8-validate@5.0.10)(viem@2.40.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13))(ws@8.19.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@4.1.13) x402-fetch: specifier: workspace:* version: link:../../../../../typescript/packages/legacy/x402-fetch @@ -2497,7 +2811,7 @@ importers: dependencies: '@coinbase/onchainkit': specifier: 0.38.14 - version: 0.38.14(@tanstack/query-core@5.90.11)(@types/react@19.1.10)(bufferutil@4.0.9)(encoding@0.1.13)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13) + version: 0.38.14(@tanstack/query-core@5.90.11)(@types/react@19.1.10)(bufferutil@4.0.9)(encoding@0.1.13)(fastestsmallesttextencoderdecoder@1.0.22)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.2)(utf-8-validate@5.0.10)(ws@8.19.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@4.1.13) '@tanstack/react-query': specifier: ^5 version: 5.90.11(react@19.2.3) @@ -2515,7 +2829,7 @@ importers: version: 2.40.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13) wagmi: specifier: ^2.15.6 - version: 2.19.5(@tanstack/query-core@5.90.11)(@tanstack/react-query@5.90.11(react@19.2.3))(@types/react@19.1.10)(bufferutil@4.0.9)(encoding@0.1.13)(react@19.2.3)(typescript@5.9.2)(utf-8-validate@5.0.10)(viem@2.40.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13))(zod@4.1.13) + version: 2.19.5(@tanstack/query-core@5.90.11)(@tanstack/react-query@5.90.11(react@19.2.3))(@types/react@19.1.10)(bufferutil@4.0.9)(encoding@0.1.13)(fastestsmallesttextencoderdecoder@1.0.22)(react@19.2.3)(typescript@5.9.2)(utf-8-validate@5.0.10)(viem@2.40.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13))(ws@8.19.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@4.1.13) x402: specifier: workspace:* version: link:../../../../../typescript/packages/legacy/x402 @@ -2762,13 +3076,13 @@ importers: dependencies: '@hono/node-server': specifier: ^1.13.8 - version: 1.19.0(hono@4.9.2) + version: 1.19.0(hono@4.10.7) dotenv: specifier: ^16.4.7 version: 16.6.1 hono: specifier: ^4.7.1 - version: 4.9.2 + version: 4.10.7 x402-hono: specifier: workspace:* version: link:../../../../../typescript/packages/legacy/x402-hono @@ -2807,7 +3121,71 @@ importers: specifier: ^5.3.0 version: 5.9.2 - servers/advanced: + servers/advanced: + dependencies: + '@x402/core': + specifier: workspace:* + version: link:../../../../typescript/packages/core + '@x402/evm': + specifier: workspace:* + version: link:../../../../typescript/packages/mechanisms/evm + '@x402/express': + specifier: workspace:* + version: link:../../../../typescript/packages/http/express + '@x402/extensions': + specifier: workspace:* + version: link:../../../../typescript/packages/extensions + '@x402/stellar': + specifier: workspace:* + version: link:../../../../typescript/packages/mechanisms/stellar + '@x402/svm': + specifier: workspace:* + version: link:../../../../typescript/packages/mechanisms/svm + dotenv: + specifier: ^16.4.7 + version: 16.6.1 + express: + specifier: ^4.18.2 + version: 4.21.2 + devDependencies: + '@eslint/js': + specifier: ^9.24.0 + version: 9.33.0 + '@types/express': + specifier: ^5.0.1 + version: 5.0.3 + '@typescript-eslint/eslint-plugin': + specifier: ^8.29.1 + version: 8.40.0(@typescript-eslint/parser@8.40.0(eslint@9.33.0(jiti@2.6.1))(typescript@5.9.2))(eslint@9.33.0(jiti@2.6.1))(typescript@5.9.2) + '@typescript-eslint/parser': + specifier: ^8.29.1 + version: 8.40.0(eslint@9.33.0(jiti@2.6.1))(typescript@5.9.2) + eslint: + specifier: ^9.24.0 + version: 9.33.0(jiti@2.6.1) + eslint-plugin-import: + specifier: ^2.31.0 + version: 2.32.0(@typescript-eslint/parser@8.40.0(eslint@9.33.0(jiti@2.6.1))(typescript@5.9.2))(eslint-import-resolver-typescript@3.10.1)(eslint@9.33.0(jiti@2.6.1)) + eslint-plugin-jsdoc: + specifier: ^50.6.9 + version: 50.8.0(eslint@9.33.0(jiti@2.6.1)) + eslint-plugin-prettier: + specifier: ^5.2.6 + version: 5.5.4(eslint-config-prettier@10.1.8(eslint@9.33.0(jiti@2.6.1)))(eslint@9.33.0(jiti@2.6.1))(prettier@3.5.2) + prettier: + specifier: 3.5.2 + version: 3.5.2 + tsup: + specifier: ^7.2.0 + version: 7.3.0(postcss@8.5.6)(typescript@5.9.2) + tsx: + specifier: ^4.7.0 + version: 4.20.4 + typescript: + specifier: ^5.3.0 + version: 5.9.2 + + servers/bazaar: dependencies: '@x402/core': specifier: workspace:* @@ -2839,16 +3217,16 @@ importers: version: 5.0.3 '@typescript-eslint/eslint-plugin': specifier: ^8.29.1 - version: 8.40.0(@typescript-eslint/parser@8.40.0(eslint@9.33.0(jiti@2.6.1))(typescript@5.9.2))(eslint@9.33.0(jiti@2.6.1))(typescript@5.9.2) + version: 8.48.0(@typescript-eslint/parser@8.48.0(eslint@9.33.0(jiti@2.6.1))(typescript@5.9.2))(eslint@9.33.0(jiti@2.6.1))(typescript@5.9.2) '@typescript-eslint/parser': specifier: ^8.29.1 - version: 8.40.0(eslint@9.33.0(jiti@2.6.1))(typescript@5.9.2) + version: 8.48.0(eslint@9.33.0(jiti@2.6.1))(typescript@5.9.2) eslint: specifier: ^9.24.0 version: 9.33.0(jiti@2.6.1) eslint-plugin-import: specifier: ^2.31.0 - version: 2.32.0(@typescript-eslint/parser@8.40.0(eslint@9.33.0(jiti@2.6.1))(typescript@5.9.2))(eslint-import-resolver-typescript@3.10.1)(eslint@9.33.0(jiti@2.6.1)) + version: 2.32.0(@typescript-eslint/parser@8.48.0(eslint@9.33.0(jiti@2.6.1))(typescript@5.9.2))(eslint-import-resolver-typescript@3.10.1)(eslint@9.33.0(jiti@2.6.1)) eslint-plugin-jsdoc: specifier: ^50.6.9 version: 50.8.0(eslint@9.33.0(jiti@2.6.1)) @@ -2921,9 +3299,6 @@ importers: servers/express: dependencies: - '@coinbase/x402': - specifier: ^2.1.0 - version: 2.1.0(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(utf-8-validate@5.0.10)(ws@8.19.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)) '@x402/core': specifier: workspace:* version: link:../../../../typescript/packages/core @@ -2983,11 +3358,69 @@ importers: specifier: ^5.3.0 version: 5.9.2 + servers/fastify: + dependencies: + '@x402/core': + specifier: workspace:* + version: link:../../../../typescript/packages/core + '@x402/evm': + specifier: workspace:* + version: link:../../../../typescript/packages/mechanisms/evm + '@x402/extensions': + specifier: workspace:* + version: link:../../../../typescript/packages/extensions + '@x402/fastify': + specifier: workspace:* + version: link:../../../../typescript/packages/http/fastify + '@x402/svm': + specifier: workspace:* + version: link:../../../../typescript/packages/mechanisms/svm + dotenv: + specifier: ^16.4.7 + version: 16.6.1 + fastify: + specifier: ^5.0.0 + version: 5.8.2 + devDependencies: + '@eslint/js': + specifier: ^9.24.0 + version: 9.33.0 + '@typescript-eslint/eslint-plugin': + specifier: ^8.29.1 + version: 8.48.0(@typescript-eslint/parser@8.48.0(eslint@9.33.0(jiti@2.6.1))(typescript@5.9.2))(eslint@9.33.0(jiti@2.6.1))(typescript@5.9.2) + '@typescript-eslint/parser': + specifier: ^8.29.1 + version: 8.48.0(eslint@9.33.0(jiti@2.6.1))(typescript@5.9.2) + eslint: + specifier: ^9.24.0 + version: 9.33.0(jiti@2.6.1) + eslint-plugin-import: + specifier: ^2.31.0 + version: 2.32.0(@typescript-eslint/parser@8.48.0(eslint@9.33.0(jiti@2.6.1))(typescript@5.9.2))(eslint-import-resolver-typescript@3.10.1)(eslint@9.33.0(jiti@2.6.1)) + eslint-plugin-jsdoc: + specifier: ^50.6.9 + version: 50.8.0(eslint@9.33.0(jiti@2.6.1)) + eslint-plugin-prettier: + specifier: ^5.2.6 + version: 5.5.4(eslint-config-prettier@10.1.8(eslint@9.33.0(jiti@2.6.1)))(eslint@9.33.0(jiti@2.6.1))(prettier@3.5.2) + prettier: + specifier: 3.5.2 + version: 3.5.2 + tsup: + specifier: ^7.2.0 + version: 7.3.0(postcss@8.5.6)(typescript@5.9.2) + tsx: + specifier: ^4.7.0 + version: 4.20.4 + typescript: + specifier: ^5.3.0 + version: 5.9.2 + servers/hono: dependencies: '@hono/node-server': specifier: ^1.14.0 - version: 1.19.0(hono@4.9.2) + version: 1.19.0(hono@4.10.7) '@x402/core': specifier: workspace:* version: link:../../../../typescript/packages/core @@ -3008,7 +3441,7 @@ importers: version: 16.6.1 hono: specifier: ^4.7.1 - version: 4.9.2 + version: 4.10.7 devDependencies: '@eslint/js': specifier: ^9.24.0 @@ -3108,6 +3541,70 @@ importers: specifier: ^5.9.2 version: 5.9.2 + servers/offer-receipt: + dependencies: + '@x402/core': + specifier: workspace:* + version: link:../../../../typescript/packages/core + '@x402/evm': + specifier: workspace:* + version: link:../../../../typescript/packages/mechanisms/evm + '@x402/express': + specifier: workspace:* + version: link:../../../../typescript/packages/http/express + '@x402/extensions': + specifier: workspace:* + version: link:../../../../typescript/packages/extensions + '@x402/svm': + specifier: workspace:* + version: link:../../../../typescript/packages/mechanisms/svm + dotenv: + specifier: ^16.4.7 + version: 16.6.1 + express: + specifier: ^4.18.2 + version: 4.21.2 + viem: + specifier: ^2.21.54 + version: 2.43.5(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13) + devDependencies: + '@eslint/js': + specifier: ^9.24.0 + version: 9.33.0 + '@types/express': + specifier: ^5.0.1 + version: 5.0.3 + '@types/node': + specifier: ^22.0.0 + version: 22.17.2 + '@typescript-eslint/eslint-plugin': + specifier: ^8.29.1 + version: 8.48.0(@typescript-eslint/parser@8.48.0(eslint@9.33.0(jiti@2.6.1))(typescript@5.9.2))(eslint@9.33.0(jiti@2.6.1))(typescript@5.9.2) + '@typescript-eslint/parser': + specifier: ^8.29.1 + version: 8.48.0(eslint@9.33.0(jiti@2.6.1))(typescript@5.9.2) + eslint: + specifier: ^9.24.0 + version: 9.33.0(jiti@2.6.1) + eslint-plugin-import: + specifier: ^2.31.0 + version: 2.32.0(@typescript-eslint/parser@8.48.0(eslint@9.33.0(jiti@2.6.1))(typescript@5.9.2))(eslint-import-resolver-typescript@3.10.1)(eslint@9.33.0(jiti@2.6.1)) + eslint-plugin-jsdoc: + specifier: ^50.6.9 + version: 50.8.0(eslint@9.33.0(jiti@2.6.1)) + eslint-plugin-prettier: + specifier: ^5.2.6 + version: 5.5.4(eslint-config-prettier@10.1.8(eslint@9.33.0(jiti@2.6.1)))(eslint@9.33.0(jiti@2.6.1))(prettier@3.5.2) + prettier: + specifier: 3.5.2 + version: 3.5.2 + tsx: + specifier: ^4.7.0 + version: 4.20.4 + typescript: + specifier: ^5.3.0 + version: 5.9.2 + servers/payment-identifier: dependencies: '@x402/core': @@ -3227,6 +3724,64 @@ importers: specifier: ^5.3.0 version: 5.9.2 + servers/upto: + dependencies: + '@x402/core': + specifier: workspace:* + version: link:../../../../typescript/packages/core + '@x402/evm': + specifier: workspace:* + version: link:../../../../typescript/packages/mechanisms/evm + '@x402/express': + specifier: workspace:* + version: link:../../../../typescript/packages/http/express + '@x402/extensions': + specifier: workspace:* + version: link:../../../../typescript/packages/extensions + dotenv: + specifier: ^16.4.7 + version: 16.6.1 + express: + specifier: ^4.18.2 + version: 4.21.2 + devDependencies: + '@eslint/js': + specifier: ^9.24.0 + version: 9.33.0 + '@types/express': + specifier: ^5.0.1 + version: 5.0.3 + '@typescript-eslint/eslint-plugin': + specifier: ^8.29.1 + version: 8.48.0(@typescript-eslint/parser@8.48.0(eslint@9.33.0(jiti@2.6.1))(typescript@5.9.2))(eslint@9.33.0(jiti@2.6.1))(typescript@5.9.2) + '@typescript-eslint/parser': + specifier: ^8.29.1 + version: 8.48.0(eslint@9.33.0(jiti@2.6.1))(typescript@5.9.2) + eslint: + specifier: ^9.24.0 + version: 9.33.0(jiti@2.6.1) + eslint-plugin-import: + specifier: ^2.31.0 + version: 2.32.0(@typescript-eslint/parser@8.48.0(eslint@9.33.0(jiti@2.6.1))(typescript@5.9.2))(eslint-import-resolver-typescript@3.10.1)(eslint@9.33.0(jiti@2.6.1)) + eslint-plugin-jsdoc: + specifier: ^50.6.9 + version: 50.8.0(eslint@9.33.0(jiti@2.6.1)) + eslint-plugin-prettier: + specifier: ^5.2.6 + version: 5.5.4(eslint-config-prettier@10.1.8(eslint@9.33.0(jiti@2.6.1)))(eslint@9.33.0(jiti@2.6.1))(prettier@3.5.2) + prettier: + specifier: 3.5.2 + version: 3.5.2 + tsup: + specifier: ^7.2.0 + version: 7.3.0(postcss@8.5.6)(typescript@5.9.2) + tsx: + specifier: ^4.7.0 + version: 4.20.4 + typescript: + specifier: ^5.3.0 + version: 5.9.2 + packages: 7zip-bin@5.2.0: @@ -3929,9 +4484,6 @@ packages: '@coinbase/wallet-sdk@4.3.6': resolution: {integrity: sha512-4q8BNG1ViL4mSAAvPAtpwlOs1gpC+67eQtgIwNvT3xyeyFFd+guwkc8bcX5rTmQhXpqnhzC4f0obACbP9CqMSA==} - '@coinbase/x402@2.1.0': - resolution: {integrity: sha512-aKeM+cz//+FjzPVu/zgz7830x0KLtKarwCyxoeC71QgCn+Xcf0NhFpn3Qyw0H496y5YOuR/IQ67gP8DZ/hXFqQ==} - '@colors/colors@1.6.0': resolution: {integrity: sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==} engines: {node: '>=0.1.90'} @@ -4445,6 +4997,24 @@ packages: peerDependencies: typescript: 5.8.3 + '@fastify/ajv-compiler@4.0.5': + resolution: {integrity: sha512-KoWKW+MhvfTRWL4qrhUwAAZoaChluo0m0vbiJlGMt2GXvL4LVPQEjt8kSpHI3IBq5Rez8fg+XeH3cneztq+C7A==} + + '@fastify/error@4.2.0': + resolution: {integrity: sha512-RSo3sVDXfHskiBZKBPRgnQTtIqpi/7zhJOEmAxCiBcM7d0uwdGdxLlsCaLzGs8v8NnxIRlfG0N51p5yFaOentQ==} + + '@fastify/fast-json-stringify-compiler@5.0.3': + resolution: {integrity: sha512-uik7yYHkLr6fxd8hJSZ8c+xF4WafPK+XzneQDPU+D10r5X19GW8lJcom2YijX2+qtFF1ENJlHXKFM9ouXNJYgQ==} + + '@fastify/forwarded@3.0.1': + resolution: {integrity: sha512-JqDochHFqXs3C3Ml3gOY58zM7OqO9ENqPo0UqAjAjH8L01fRZqwX9iLeX34//kiJubF7r2ZQHtBRU36vONbLlw==} + + '@fastify/merge-json-schemas@0.2.1': + resolution: {integrity: sha512-OA3KGBCy6KtIvLf8DINC5880o5iBlDX4SxzLQS8HorJAbqluzLRn80UXU0bxZn7UOFhFgpRJDasfwn9nG4FG4A==} + + '@fastify/proxy-addr@5.1.0': + resolution: {integrity: sha512-INS+6gh91cLUjB+PVHfu1UqcB76Sqtpyp7bnL+FYojhjygvOPA9ctiD/JDKsyD9Xgu4hUhCSJBPig/w7duNajw==} + '@floating-ui/core@1.7.3': resolution: {integrity: sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==} @@ -5110,6 +5680,9 @@ packages: resolution: {integrity: sha512-IHnV6A+zxU7XwmKFinmYjUcwlyK9+xkG3/s9KcQhI9BjQKycrJ1JRO+FbNYPwZiPKW3je/DR0k7w8/gLa5eaxQ==} deprecated: 'The package is now available as "qr": npm install qr' + '@pinojs/redact@0.4.0': + resolution: {integrity: sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==} + '@pkgjs/parseargs@0.11.0': resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} engines: {node: '>=14'} @@ -6086,6 +6659,12 @@ packages: peerDependencies: '@solana/kit': ^5.0 + '@solana/accounts@2.3.0': + resolution: {integrity: sha512-QgQTj404Z6PXNOyzaOpSzjgMOuGwG8vC66jSDB+3zHaRcEPRVRd2sVSrd1U6sHtnV3aiaS6YyDuPQMheg4K2jw==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5.3.3' + '@solana/accounts@3.0.3': resolution: {integrity: sha512-KqlePrlZaHXfu8YQTCxN204ZuVm9o68CCcUr6l27MG2cuRUtEM1Ta0iR8JFkRUAEfZJC4Cu0ZDjK/v49loXjZQ==} engines: {node: '>=20.18.0'} @@ -6355,6 +6934,12 @@ packages: peerDependencies: typescript: '>=5' + '@solana/codecs@2.3.0': + resolution: {integrity: sha512-JVqGPkzoeyU262hJGdH64kNLH0M+Oew2CIPOa/9tR3++q2pEd4jU2Rxdfye9sd0Ce3XJrR5AIa8ZfbyQXzjh+g==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5.3.3' + '@solana/codecs@3.0.3': resolution: {integrity: sha512-GOHwTlIQsCoJx9Ryr6cEf0FHKAQ7pY4aO4xgncAftrv0lveTQ1rPP2inQ1QT0gJllsIa8nwbfXAADs9nNJxQDA==} engines: {node: '>=20.18.0'} @@ -6585,6 +7170,12 @@ packages: typescript: optional: true + '@solana/kit@2.3.0': + resolution: {integrity: sha512-sb6PgwoW2LjE5oTFu4lhlS/cGt/NB3YrShEyx7JgWFWysfgLdJnhwWThgwy/4HjNsmtMrQGWVls0yVBHcMvlMQ==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5.3.3' + '@solana/kit@3.0.3': resolution: {integrity: sha512-CEEhCDmkvztd1zbgADsEQhmj9GyWOOGeW1hZD+gtwbBSF5YN1uofS/pex5MIh/VIqKRj+A2UnYWI1V+9+q/lyQ==} engines: {node: '>=20.18.0'} @@ -6665,6 +7256,12 @@ packages: peerDependencies: typescript: '>=5' + '@solana/options@2.3.0': + resolution: {integrity: sha512-PPnnZBRCWWoZQ11exPxf//DRzN2C6AoFsDI/u2AsQfYih434/7Kp4XLpfOMT/XESi+gdBMFNNfbES5zg3wAIkw==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5.3.3' + '@solana/options@3.0.3': resolution: {integrity: sha512-jarsmnQ63RN0JPC5j9sgUat07NrL9PC71XU7pUItd6LOHtu4+wJMio3l5mT0DHVfkfbFLL6iI6+QmXSVhTNF3g==} engines: {node: '>=20.18.0'} @@ -6719,6 +7316,12 @@ packages: typescript: optional: true + '@solana/programs@2.3.0': + resolution: {integrity: sha512-UXKujV71VCI5uPs+cFdwxybtHZAIZyQkqDiDnmK+DawtOO9mBn4Nimdb/6RjR2CXT78mzO9ZCZ3qfyX+ydcB7w==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5.3.3' + '@solana/programs@3.0.3': resolution: {integrity: sha512-JZlVE3/AeSNDuH3aEzCZoDu8GTXkMpGXxf93zXLzbxfxhiQ/kHrReN4XE/JWZ/uGWbaFZGR5B3UtdN2QsoZL7w==} engines: {node: '>=20.18.0'} @@ -7182,6 +7785,12 @@ packages: typescript: optional: true + '@solana/signers@2.3.0': + resolution: {integrity: sha512-OSv6fGr/MFRx6J+ZChQMRqKNPGGmdjkqarKkRzkwmv7v8quWsIRnJT5EV8tBy3LI4DLO/A8vKiNSPzvm1TdaiQ==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5.3.3' + '@solana/signers@3.0.3': resolution: {integrity: sha512-UwCd/uPYTZiwd283JKVyOWLLN5sIgMBqGDyUmNU3vo9hcmXKv5ZGm/9TvwMY2z35sXWuIOcj7etxJ8OoWc/ObQ==} engines: {node: '>=20.18.0'} @@ -7260,6 +7869,12 @@ packages: typescript: optional: true + '@solana/sysvars@2.3.0': + resolution: {integrity: sha512-LvjADZrpZ+CnhlHqfI5cmsRzX9Rpyb1Ox2dMHnbsRNzeKAMhu9w4ZBIaeTdO322zsTr509G1B+k2ABD3whvUBA==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5.3.3' + '@solana/sysvars@3.0.3': resolution: {integrity: sha512-GnHew+QeKCs2f9ow+20swEJMH4mDfJA/QhtPgOPTYQx/z69J4IieYJ7fZenSHnA//lJ45fVdNdmy1trypvPLBQ==} engines: {node: '>=20.18.0'} @@ -7411,6 +8026,18 @@ packages: '@stablelib/wipe@1.0.1': resolution: {integrity: sha512-WfqfX/eXGiAd3RJe4VU2snh/ZPwtSjLG4ynQ/vYzvghTh7dHFcI1wl+nrkWG6lGhukOxOsUHfv8dUXr58D0ayg==} + '@stellar/js-xdr@3.1.2': + resolution: {integrity: sha512-VVolPL5goVEIsvuGqDc5uiKxV03lzfWdvYg1KikvwheDmTBO68CKDji3bAZ/kppZrx5iTA8z3Ld5yuytcvhvOQ==} + + '@stellar/stellar-base@14.1.0': + resolution: {integrity: sha512-A8kFli6QGy22SRF45IjgPAJfUNGjnI+R7g4DF5NZYVsD1kGf7B4ITyc4OPclLV9tqNI4/lXxafGEw0JEUbHixw==} + engines: {node: '>=20.0.0'} + + '@stellar/stellar-sdk@14.6.1': + resolution: {integrity: sha512-A1rQWDLdUasXkMXnYSuhgep+3ZZzyuXJKdt5/KAIc0gkmSp906HTvUpbT4pu+bVr41tu0+J4Ugz9J4BQAGGytg==} + engines: {node: '>=20.0.0'} + hasBin: true + '@svgr/babel-plugin-add-jsx-attribute@8.0.0': resolution: {integrity: sha512-b9MIk7yhdS1pMCZM8VeNfUlSKVRhsHZNMl5O9SfaX0l0t5wjdgu4IDzGB8bpnGBBOjGST3rRFVsaaEtI4W6f7g==} engines: {node: '>=14'} @@ -7639,9 +8266,15 @@ packages: '@types/estree@1.0.8': resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + '@types/express-serve-static-core@4.19.8': + resolution: {integrity: sha512-02S5fmqeoKzVZCHPZid4b8JH2eM5HzQLZWN2FohQEy/0eXTq8VXZfSN6Pcr3F6N9R/vNrj7cpgbhjie6m/1tCA==} + '@types/express-serve-static-core@5.0.7': resolution: {integrity: sha512-R+33OsgWw7rOhD1emjU7dzCDHucJrgJXMA5PYCzJxVil0dsyx5iBEPHqpPfiKNJQb7lZ1vxwoLR4Z87bBUpeGQ==} + '@types/express@4.17.25': + resolution: {integrity: sha512-dVd04UKsfpINUnK0yBoYHDF3xu7xVH4BuDotC/xGuycx4CgbP48X/KF/586bcObxT0HENHXEU8Nqtu6NR+eKhw==} + '@types/express@5.0.3': resolution: {integrity: sha512-wGA0NX93b19/dZC1J18tKWVIYWyyF2ZjT9vin/NRu0qzzvfVzWjs04iq2rQ3H65vCTQYlRqs3YHfY7zjdV+9Kw==} @@ -8166,9 +8799,6 @@ packages: '@walletconnect/window-metadata@1.0.1': resolution: {integrity: sha512-9koTqyGrM2cqFRW517BPY/iEtUDx2r1+Pwwu5m7sJ7ka79wi3EyqhqcICk/yDmv6jAS1rjKgTKXlEhanYjijcA==} - '@x402/core@2.3.1': - resolution: {integrity: sha512-CWvsf09tslISoVOzQ2TIoBLBP+bUycPsYmmRVe3EV5X2FtD7eXXpiPsiXLEVtWP7zhqLNP/5OIATsA2hSVLSfw==} - '@xmldom/xmldom@0.8.11': resolution: {integrity: sha512-cQzWCtO6C8TQiYl1ruKNn2U6Ao4o4WBBcbL61yJl84x+j5sOWWFU9X7DpND8XZG3daDppSsigMdfAIl2upQBRw==} engines: {node: '>=10.0.0'} @@ -8224,6 +8854,9 @@ packages: resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==} engines: {node: '>=6.5'} + abstract-logging@2.0.1: + resolution: {integrity: sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA==} + accepts@1.3.8: resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==} engines: {node: '>= 0.6'} @@ -8261,6 +8894,14 @@ packages: resolution: {integrity: sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==} engines: {node: '>=8'} + ajv-formats@3.0.1: + resolution: {integrity: sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==} + peerDependencies: + ajv: ^8.0.0 + peerDependenciesMeta: + ajv: + optional: true + ajv-keywords@3.5.2: resolution: {integrity: sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==} peerDependencies: @@ -8413,6 +9054,9 @@ packages: resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} engines: {node: '>= 0.4'} + avvio@9.2.0: + resolution: {integrity: sha512-2t/sy01ArdHHE0vRH5Hsay+RtCZt3dLPji7W7/MMOCEgze5b7SNDC4j5H6FnVgPkI1MTNFGzHdHrVXDDl7QSSQ==} + axe-core@4.10.3: resolution: {integrity: sha512-Xm7bpRXnDSX2YE2YFfBk2FnF0ep6tmG7xPh8iHee8MIcrgq762Nkce856dYtJYLkuIoYZvGfTs/PbZhideTcEg==} engines: {node: '>=4'} @@ -8430,6 +9074,9 @@ packages: axios@1.13.2: resolution: {integrity: sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==} + axios@1.13.4: + resolution: {integrity: sha512-1wVkUaAO6WyaYtCkcYCOx12ZgpGf9Zif+qXa4n+oYzK558YryKqiL6UWwd5DqiH3VRW0GYhTZQ/vlgJrCoNQlg==} + axobject-query@4.1.0: resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==} engines: {node: '>= 0.4'} @@ -8461,6 +9108,10 @@ packages: base-x@5.0.1: resolution: {integrity: sha512-M7uio8Zt++eg3jPj+rHMfCC+IuygQHHCOU+IYsVtik6FWjuYpVt/+MRKcgsAMHh8mMFAwnB+Bs+mTrFiXjMzKg==} + base32.js@0.1.0: + resolution: {integrity: sha512-n3TkB02ixgBOhTvANakDb4xaMXnYUVkNoRFJjQflcqMQhyEKxEHdj3E6N8t8sUQ0mjH/3/JxzlXuz3ul/J90pQ==} + engines: {node: '>=0.12.0'} + base64-js@1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} @@ -8862,6 +9513,10 @@ packages: resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} engines: {node: '>= 0.6'} + cookie@1.1.1: + resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==} + engines: {node: '>=18'} + core-js-compat@3.45.1: resolution: {integrity: sha512-tqTt5T4PzsMIZ430XGviK4vzYSoeNJ6CXODi6c/voxOT6IZqBht5/EKaSNnYiEjjRYxjVz7DQIsOsY0XNi8PIA==} @@ -9077,6 +9732,10 @@ packages: resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} engines: {node: '>= 0.8'} + dequal@2.0.3: + resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} + engines: {node: '>=6'} + derive-valtio@0.1.0: resolution: {integrity: sha512-OCg2UsLbXK7GmmpzMXhYkdO64vhJ1ROUUGaTFyHjVwEdMEcTTRj7W1TxLbSBxdY8QLBPCcp66MTyaSy0RpO17A==} peerDependencies: @@ -9566,6 +10225,10 @@ packages: resolution: {integrity: sha512-bSRG85ZrMdmWtm7qkF9He9TNRzc/Bm99gEJMaQoHJ9E6Kv9QBbsldh2oMj7iXmYNEAVvNgvv5vPorG6W+XtBhQ==} engines: {node: '>=20.0.0'} + eventsource@2.0.2: + resolution: {integrity: sha512-IzUmBGPR3+oUG9dUeXynyNmf91/3zUSJg1lCktzKw47OXuhco54U3r9B7O4XX+Rb1Itm9OZ2b0RkTs10bICOxA==} + engines: {node: '>=12.0.0'} + eventsource@3.0.7: resolution: {integrity: sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==} engines: {node: '>=18.0.0'} @@ -9612,6 +10275,9 @@ packages: resolution: {integrity: sha512-GipyPsXO1anza0AOZdy69Im7hGFCNB7Y/NGjDlZGJ3GJJLtwNSb2vrzYrTYJRrRloVx7pl+bhUaTB8yiccPvFQ==} engines: {node: '> 0.1.90'} + fast-decode-uri-component@1.0.1: + resolution: {integrity: sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg==} + fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} @@ -9629,9 +10295,15 @@ packages: fast-json-stable-stringify@2.1.0: resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + fast-json-stringify@6.3.0: + resolution: {integrity: sha512-oRCntNDY/329HJPlmdNLIdogNtt6Vyjb1WuT01Soss3slIdyUp8kAcDU3saQTOquEK8KFVfwIIF7FebxUAu+yA==} + fast-levenshtein@2.0.6: resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + fast-querystring@1.1.2: + resolution: {integrity: sha512-g6KuKWmFXc0fID8WWH0jit4g0AGBoJhCkJMb1RmbsSEUNvQ+ZC8D6CUZ+GtF8nMzSPXnhiePyyqqipzNNEnHjg==} + fast-redact@3.5.0: resolution: {integrity: sha512-dwsoQlS7h9hMeYUq1W++23NDcBLV4KqONnITDV9DjfS3q1SgDGVrBdvvTLUotWtPSD7asWDV9/CmsZPy8Hf70A==} engines: {node: '>=6'} @@ -9651,6 +10323,9 @@ packages: fastestsmallesttextencoderdecoder@1.0.22: resolution: {integrity: sha512-Pb8d48e+oIuY4MaM64Cd7OW1gt4nxCHs7/ddPPZ/Ic3sg8yVGM7O9wDvZ7us6ScaUupzM+pfBolwtYhN1IxBIw==} + fastify@5.8.2: + resolution: {integrity: sha512-lZmt3navvZG915IE+f7/TIVamxIwmBd+OMB+O9WBzcpIwOo6F0LTh0sluoMFk5VkrKTvvrwIaoJPkir4Z+jtAg==} + fastq@1.19.1: resolution: {integrity: sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==} @@ -9666,6 +10341,9 @@ packages: picomatch: optional: true + feaxios@0.0.23: + resolution: {integrity: sha512-eghR0A21fvbkcQBgZuMfQhrXxJzC0GNUGC9fXhBge33D+mFDTwl0aJ35zoQQn575BhyjQitRc5N4f+L4cP708g==} + fecha@4.2.3: resolution: {integrity: sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==} @@ -9703,6 +10381,10 @@ packages: resolution: {integrity: sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==} engines: {node: '>= 0.8'} + find-my-way@9.5.0: + resolution: {integrity: sha512-VW2RfnmscZO5KgBY5XVyKREMW5nMZcxDy+buTOsL+zIPnBlbKm+00sgzoQzq1EVh4aALZLfKdwv6atBGcjvjrQ==} + engines: {node: '>=20'} + find-up@4.1.0: resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} engines: {node: '>=8'} @@ -9976,10 +10658,6 @@ packages: resolution: {integrity: sha512-icXIITfw/07Q88nLSkB9aiUrd8rYzSweK681Kjo/TSggaGbOX4RRyxxm71v+3PC8C/j+4rlxGeoTRxQDkaJkUw==} engines: {node: '>=16.9.0'} - hono@4.9.2: - resolution: {integrity: sha512-UG2jXGS/gkLH42l/1uROnwXpkjvvxkl3kpopL3LBo27NuaDPI6xHNfuUSilIHcrBkPfl4y0z6y2ByI455TjNRw==} - engines: {node: '>=16.9.0'} - hosted-git-info@4.1.0: resolution: {integrity: sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==} engines: {node: '>=10'} @@ -10093,6 +10771,10 @@ packages: resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} engines: {node: '>= 0.10'} + ipaddr.js@2.3.0: + resolution: {integrity: sha512-Zv/pA+ciVFbCSBBjGfaKUya/CcGmUHzTydLMaTwrUUEM2DIEO3iZvueGxmacvmN50fGpGVKeTXpb2LcYQxeVdg==} + engines: {node: '>= 10'} + iron-webcrypto@1.2.1: resolution: {integrity: sha512-feOM6FaSr6rEABp/eDfVseKyTMDt+KGpeB35SkVn9Tyn0CqvVsY3EwI0v5i8nMHyJnzCIQf7nsy3p41TPkJZhg==} @@ -10221,6 +10903,10 @@ packages: resolution: {integrity: sha512-XVm7LOeLpTW4jV19QSH38vkswxoLud8sQ57YwJVTPWdiaI9I8keEhGFpBlslyVsgdQy4Opg8QOLb8YRgsyZiQg==} engines: {node: '>=10'} + is-retry-allowed@3.0.0: + resolution: {integrity: sha512-9xH0xvoggby+u0uGF7cZXdrutWiBiaFG8ZT4YFPXL8NzkyAwX3AKGLeFQLvzDpM430+nDFBZ1LHkie/8ocL06A==} + engines: {node: '>=12'} + is-set@2.0.3: resolution: {integrity: sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==} engines: {node: '>= 0.4'} @@ -10383,6 +11069,9 @@ packages: json-rpc-random-id@1.0.1: resolution: {integrity: sha512-RJ9YYNCkhVDBuP4zN5BBtYAzEl03yq/jIIsyif0JY9qyJuQQZNeDK7anAPKKlyEtLSj2s8h6hNh2F8zO5q7ScA==} + json-schema-ref-resolver@3.0.0: + resolution: {integrity: sha512-hOrZIVL5jyYFjzk7+y7n5JDzGlU8rfWDuYyHwGa2WA8/pcmMHezp2xsVwxrebD/Q9t8Nc5DboieySDpCp4WG4A==} + json-schema-traverse@0.4.1: resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} @@ -10468,6 +11157,9 @@ packages: libphonenumber-js@1.12.13: resolution: {integrity: sha512-QZXnR/OGiDcBjF4hGk0wwVrPcZvbSSyzlvkjXv5LFfktj7O2VZDrt4Xs8SgR/vOFco+qk1i8J43ikMXZoTrtPw==} + light-my-request@6.6.0: + resolution: {integrity: sha512-CHYbu8RtboSIoVsHZ6Ye4cj4Aw/yg2oAFimlF7mNvfDV192LR7nDiKtSIfCuLT7KokPSTn/9kfVLm5OGN0A28A==} + lightningcss-android-arm64@1.30.2: resolution: {integrity: sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A==} engines: {node: '>= 12.0.0'} @@ -11017,6 +11709,10 @@ packages: on-exit-leak-free@0.2.0: resolution: {integrity: sha512-dqaz3u44QbRXQooZLTUKU41ZrzYrcvLISVgbrzbyCMxpmSLJvZ3ZamIJIZ29P6OhZIkNIQKosdeM6t1LYbA9hg==} + on-exit-leak-free@2.1.2: + resolution: {integrity: sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==} + engines: {node: '>=14.0.0'} + on-finished@2.4.1: resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} engines: {node: '>= 0.8'} @@ -11284,9 +11980,19 @@ packages: pino-abstract-transport@0.5.0: resolution: {integrity: sha512-+KAgmVeqXYbTtU2FScx1XS3kNyfZ5TrXY07V96QnUSFqo2gAqlvmaxH67Lj7SWazqsMabf+58ctdTcBgnOLUOQ==} + pino-abstract-transport@3.0.0: + resolution: {integrity: sha512-wlfUczU+n7Hy/Ha5j9a/gZNy7We5+cXp8YL+X+PG8S0KXxw7n/JXA3c46Y0zQznIJ83URJiwy7Lh56WLokNuxg==} + pino-std-serializers@4.0.0: resolution: {integrity: sha512-cK0pekc1Kjy5w9V2/n+8MkZwusa6EyyxfeQCB799CQRhRt/CqYKiWs5adeu8Shve2ZNffvfC/7J64A2PJo1W/Q==} + pino-std-serializers@7.1.0: + resolution: {integrity: sha512-BndPH67/JxGExRgiX1dX0w1FvZck5Wa4aal9198SrRhZjH3GxKQUKIBnYJTdj2HDN3UQAS06HlfcSbQj2OHmaw==} + + pino@10.3.1: + resolution: {integrity: sha512-r34yH/GlQpKZbU1BvFFqOjhISRo1MNx1tWYsYvmj6KIRHSPMT2+yHOEb1SG6NMvRoHRF0a07kCOox/9yakl1vg==} + hasBin: true + pino@7.11.0: resolution: {integrity: sha512-dMACeu63HtRLmCG8VKdy4cShCPKaYDR4youZqoSWLxl5Gu99HUw8bw75thbPv9Nip+H+QYX8o3ZJbTdVZZ2TVg==} hasBin: true @@ -11455,6 +12161,12 @@ packages: process-warning@1.0.0: resolution: {integrity: sha512-du4wfLyj4yCZq1VupnVSZmRsPJsNuxoDQFdCFHLaYiEbFBD7QE0a+I4D7hOxrVnh78QE/YipFAj9lXHiXocV+Q==} + process-warning@4.0.1: + resolution: {integrity: sha512-3c2LzQ3rY9d0hc1emcsHhfT9Jwz0cChib/QN89oME2R451w5fy3f0afAhERFZAwrbDU43wk12d0ORBpDVME50Q==} + + process-warning@5.0.0: + resolution: {integrity: sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA==} + progress@2.0.3: resolution: {integrity: sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==} engines: {node: '>=0.4.0'} @@ -11545,6 +12257,9 @@ packages: radix3@1.1.2: resolution: {integrity: sha512-b484I/7b8rDEdSDKckSSBA8knMpcdsXudlE/LNL639wFoHKwLbEkQFZHWEYwDC0wa0FKUcCY+GAF73Z7wxNVFA==} + randombytes@2.1.0: + resolution: {integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==} + range-parser@1.2.1: resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} engines: {node: '>= 0.6'} @@ -11668,6 +12383,10 @@ packages: resolution: {integrity: sha512-r/H9MzAWtrv8aSVjPCMFpDMl5q66GqtmmRkRjpHTsp4zBAa+snZyiQNlMONiUmEJcsnaw0wCauJ2GWODr/aFkg==} engines: {node: '>= 12.13.0'} + real-require@0.2.0: + resolution: {integrity: sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==} + engines: {node: '>= 12.13.0'} + redaxios@0.5.1: resolution: {integrity: sha512-FSD2AmfdbkYwl7KDExYQlVvIrFz6Yd83pGfaGjBzM9F6rpq8g652Q4Yq5QD4c+nf4g2AgeElv1y+8ajUPiOYMg==} @@ -11751,6 +12470,10 @@ packages: resolution: {integrity: sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==} engines: {node: '>=8'} + ret@0.5.0: + resolution: {integrity: sha512-I1XxrZSQ+oErkRR4jYbAyEEu2I0avBvvMM5JN+6EBprOGRCs63ENqZ3vjavq8fBw2+62G5LF5XelKwuJpcvcxw==} + engines: {node: '>=10'} + retry@0.12.0: resolution: {integrity: sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==} engines: {node: '>= 4'} @@ -11763,6 +12486,9 @@ packages: resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + rfdc@1.4.1: + resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==} + rimraf@2.6.3: resolution: {integrity: sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==} deprecated: Rimraf versions prior to v4 are no longer supported @@ -11819,6 +12545,10 @@ packages: resolution: {integrity: sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==} engines: {node: '>= 0.4'} + safe-regex2@5.1.0: + resolution: {integrity: sha512-pNHAuBW7TrcleFHsxBr5QMi/Iyp0ENjUKz7GCcX1UO7cMh+NmVK6HxQckNL1tJp1XAJVjG6B8OKIPqodqj9rtw==} + hasBin: true + safe-stable-stringify@2.5.0: resolution: {integrity: sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==} engines: {node: '>=10'} @@ -11849,6 +12579,9 @@ packages: resolution: {integrity: sha512-lDFs9AAIaWP9UCdtWrotXWWF9t8PWgQDcxqgAnpM9rMqxb3Oaq2J0thzPVSxBwdJgyQtkU/sYtFtbM1RSt/iYA==} engines: {node: '>=18.0.0'} + secure-json-parse@4.1.0: + resolution: {integrity: sha512-l4KnYfEyqYJxDwlNVyRfO2E4NTHfMKAWdUuA8J0yve2Dz/E/PdBepY03RvyJpssIpRFwJoCD55wA+mEDs6ByWA==} + selderee@0.11.0: resolution: {integrity: sha512-5TF+l7p4+OsnP8BCCvSyZiSPc4x4//p5uPwK8TCnVPJYRmU2aYKMpOXvw8zM5a5JvuuCGN1jmsMwuU2W02ukfA==} @@ -11896,6 +12629,9 @@ packages: set-blocking@2.0.0: resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==} + set-cookie-parser@2.7.2: + resolution: {integrity: sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==} + set-function-length@1.2.2: resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} engines: {node: '>= 0.4'} @@ -12007,6 +12743,9 @@ packages: sonic-boom@2.8.0: resolution: {integrity: sha512-kuonw1YOYYNOve5iHdSahXPOK49GqwA+LZhI6Wz/l0rP57iKyXXIHaRagOBHAPmGwJC6od2Z9zgvZ5loSgMlVg==} + sonic-boom@4.2.1: + resolution: {integrity: sha512-w6AxtubXa2wTXAUsZMMWERrsIRAdrK0Sc+FUytWvYAhBJLyuI4llrMIC1DtlNSdI99EI86KZum2MMq3EAZlF9Q==} + source-map-js@1.2.1: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} @@ -12263,6 +13002,10 @@ packages: thread-stream@0.15.2: resolution: {integrity: sha512-UkEhKIg2pD+fjkHQKyJO3yoIvAP3N6RlNFt2dUhcS1FGvCD1cQa1M/PGknCLFIyZdtJOWQjejp7bdNqmN7zwdA==} + thread-stream@4.0.0: + resolution: {integrity: sha512-4iMVL6HAINXWf1ZKZjIPcz5wYaOdPhtO8ATvZ+Xqp3BTdaqtAwQkNmKORqcIo5YkQqGXq5cwfswDwMqqQNrpJA==} + engines: {node: '>=20'} + tiny-async-pool@1.3.0: resolution: {integrity: sha512-01EAw5EDrcVrdgyCLgoSPvqznC0sVxDSVeiOz09FUpjh71G79VCqneOr+xvt7T1r76CF6ZZfPjHorN2+d+3mqA==} @@ -12314,10 +13057,17 @@ packages: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} + toad-cache@3.7.0: + resolution: {integrity: sha512-/m8M+2BJUpoJdgAHoG+baCwBT+tf2VraSfkBgl0Y00qIWt41DJ8R5B8nsEw0I58YwF5IZH6z24/2TobDKnqSWw==} + engines: {node: '>=12'} + toidentifier@1.0.1: resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} engines: {node: '>=0.6'} + toml@3.0.0: + resolution: {integrity: sha512-y/mWCZinnvxjTKYhJ+pYxwD0mRLVvOtdS2Awbgxln6iEnt4rk0yBxeSBHkGJcPucRiG0e55mwWp+g/05rsrd6w==} + tough-cookie@5.1.2: resolution: {integrity: sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==} engines: {node: '>=16'} @@ -12662,6 +13412,9 @@ packages: uri-js@4.4.1: resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + urijs@1.19.11: + resolution: {integrity: sha512-HXgFDgDommxn5/bIv0cnQZsPhHDA90NPHD6+c/v21U5+Sx5hoP8+dP9IZXBU1gIfvdRfhG8cel9QNPeionfcCQ==} + url-parse@1.5.10: resolution: {integrity: sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==} @@ -14138,9 +14891,9 @@ snapshots: - ws - zod - '@base-org/account@2.4.0(@types/react@19.1.10)(bufferutil@4.0.9)(encoding@0.1.13)(react@19.2.3)(typescript@5.9.2)(use-sync-external-store@1.4.0(react@19.2.3))(utf-8-validate@5.0.10)(zod@4.1.13)': + '@base-org/account@2.4.0(@types/react@19.1.10)(bufferutil@4.0.9)(encoding@0.1.13)(fastestsmallesttextencoderdecoder@1.0.22)(react@19.2.3)(typescript@5.9.2)(use-sync-external-store@1.4.0(react@19.2.3))(utf-8-validate@5.0.10)(ws@8.19.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@4.1.13)': dependencies: - '@coinbase/cdp-sdk': 1.39.0(bufferutil@4.0.9)(encoding@0.1.13)(typescript@5.9.2)(utf-8-validate@5.0.10) + '@coinbase/cdp-sdk': 1.39.0(bufferutil@4.0.9)(encoding@0.1.13)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(utf-8-validate@5.0.10)(ws@8.19.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)) '@noble/hashes': 1.4.0 clsx: 1.2.1 eventemitter3: 5.0.1 @@ -14163,9 +14916,9 @@ snapshots: - ws - zod - '@base-org/account@2.4.0(@types/react@19.2.1)(bufferutil@4.0.9)(encoding@0.1.13)(react@19.2.3)(typescript@5.9.2)(use-sync-external-store@1.4.0(react@19.2.3))(utf-8-validate@5.0.10)(zod@4.1.13)': + '@base-org/account@2.4.0(@types/react@19.2.1)(bufferutil@4.0.9)(encoding@0.1.13)(fastestsmallesttextencoderdecoder@1.0.22)(react@19.2.3)(typescript@5.9.2)(use-sync-external-store@1.4.0(react@19.2.3))(utf-8-validate@5.0.10)(ws@8.19.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@4.1.13)': dependencies: - '@coinbase/cdp-sdk': 1.39.0(bufferutil@4.0.9)(encoding@0.1.13)(typescript@5.9.2)(utf-8-validate@5.0.10) + '@coinbase/cdp-sdk': 1.39.0(bufferutil@4.0.9)(encoding@0.1.13)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(utf-8-validate@5.0.10)(ws@8.19.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)) '@noble/hashes': 1.4.0 clsx: 1.2.1 eventemitter3: 5.0.1 @@ -14297,8 +15050,8 @@ snapshots: '@solana/kit': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)) '@solana/web3.js': 1.98.4(bufferutil@4.0.9)(encoding@0.1.13)(typescript@5.9.2)(utf-8-validate@5.0.10) abitype: 1.0.6(typescript@5.9.2)(zod@3.25.76) - axios: 1.13.2 - axios-retry: 4.5.0(axios@1.13.2) + axios: 1.13.4 + axios-retry: 4.5.0(axios@1.13.4) jose: 6.0.12 md5: 2.3.0 uncrypto: 0.1.3 @@ -14320,31 +15073,8 @@ snapshots: '@solana/kit': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.19.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)) '@solana/web3.js': 1.98.4(bufferutil@4.0.9)(encoding@0.1.13)(typescript@5.9.2)(utf-8-validate@5.0.10) abitype: 1.0.6(typescript@5.9.2)(zod@3.25.76) - axios: 1.13.2 - axios-retry: 4.5.0(axios@1.13.2) - jose: 6.0.12 - md5: 2.3.0 - uncrypto: 0.1.3 - viem: 2.43.5(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) - zod: 3.25.76 - transitivePeerDependencies: - - bufferutil - - debug - - encoding - - fastestsmallesttextencoderdecoder - - typescript - - utf-8-validate - - ws - - '@coinbase/cdp-sdk@1.39.0(bufferutil@4.0.9)(encoding@0.1.13)(typescript@5.9.2)(utf-8-validate@5.0.10)': - dependencies: - '@solana-program/system': 0.8.1(@solana/kit@3.0.3(typescript@5.9.2)) - '@solana-program/token': 0.6.0(@solana/kit@3.0.3(typescript@5.9.2)) - '@solana/kit': 3.0.3(typescript@5.9.2) - '@solana/web3.js': 1.98.4(bufferutil@4.0.9)(encoding@0.1.13)(typescript@5.9.2)(utf-8-validate@5.0.10) - abitype: 1.0.6(typescript@5.9.2)(zod@3.25.76) - axios: 1.13.2 - axios-retry: 4.5.0(axios@1.13.2) + axios: 1.13.4 + axios-retry: 4.5.0(axios@1.13.4) jose: 6.0.12 md5: 2.3.0 uncrypto: 0.1.3 @@ -14382,7 +15112,7 @@ snapshots: - utf-8-validate - zod - '@coinbase/onchainkit@0.38.14(@tanstack/query-core@5.90.11)(@types/react@19.1.10)(bufferutil@4.0.9)(encoding@0.1.13)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13)': + '@coinbase/onchainkit@0.38.14(@tanstack/query-core@5.90.11)(@types/react@19.1.10)(bufferutil@4.0.9)(encoding@0.1.13)(fastestsmallesttextencoderdecoder@1.0.22)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.2)(utf-8-validate@5.0.10)(ws@8.19.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@4.1.13)': dependencies: '@farcaster/frame-sdk': 0.0.60(bufferutil@4.0.9)(encoding@0.1.13)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13) '@farcaster/frame-wagmi-connector': 0.0.42(@farcaster/frame-sdk@0.0.60(bufferutil@4.0.9)(encoding@0.1.13)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13))(@wagmi/core@2.22.1(@tanstack/query-core@5.90.11)(@types/react@19.1.10)(react@19.2.3)(typescript@5.9.2)(viem@2.40.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13)))(viem@2.43.5(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13)) @@ -14396,7 +15126,7 @@ snapshots: react-dom: 19.2.3(react@19.2.3) tailwind-merge: 2.6.0 viem: 2.43.5(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13) - wagmi: 2.19.5(@tanstack/query-core@5.90.11)(@tanstack/react-query@5.90.11(react@19.2.3))(@types/react@19.1.10)(bufferutil@4.0.9)(encoding@0.1.13)(react@19.2.3)(typescript@5.9.2)(utf-8-validate@5.0.10)(viem@2.43.5(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13))(zod@4.1.13) + wagmi: 2.19.5(@tanstack/query-core@5.90.11)(@tanstack/react-query@5.90.11(react@19.2.3))(@types/react@19.1.10)(bufferutil@4.0.9)(encoding@0.1.13)(fastestsmallesttextencoderdecoder@1.0.22)(react@19.2.3)(typescript@5.9.2)(utf-8-validate@5.0.10)(viem@2.43.5(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13))(ws@8.19.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@4.1.13) transitivePeerDependencies: - '@azure/app-configuration' - '@azure/cosmos' @@ -14487,7 +15217,7 @@ snapshots: - ws - zod - '@coinbase/onchainkit@0.38.19(@farcaster/miniapp-sdk@0.2.1(bufferutil@4.0.9)(encoding@0.1.13)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13))(@tanstack/query-core@5.90.11)(@types/react@19.1.10)(@upstash/redis@1.35.3)(bufferutil@4.0.9)(encoding@0.1.13)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.2)(use-sync-external-store@1.5.0(react@19.2.3))(utf-8-validate@5.0.10)(zod@4.1.13)': + '@coinbase/onchainkit@0.38.19(@farcaster/miniapp-sdk@0.2.1(bufferutil@4.0.9)(encoding@0.1.13)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13))(@tanstack/query-core@5.90.11)(@types/react@19.1.10)(@upstash/redis@1.35.3)(bufferutil@4.0.9)(encoding@0.1.13)(fastestsmallesttextencoderdecoder@1.0.22)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.2)(use-sync-external-store@1.5.0(react@19.2.3))(utf-8-validate@5.0.10)(ws@8.19.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@4.1.13)': dependencies: '@farcaster/frame-sdk': 0.1.9(bufferutil@4.0.9)(encoding@0.1.13)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13) '@farcaster/miniapp-wagmi-connector': 1.0.0(@farcaster/miniapp-sdk@0.2.1(bufferutil@4.0.9)(encoding@0.1.13)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13))(@wagmi/core@2.22.1(@tanstack/query-core@5.90.11)(@types/react@19.1.10)(react@19.2.3)(typescript@5.9.2)(use-sync-external-store@1.5.0(react@19.2.3))(viem@2.40.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13)))(viem@2.43.5(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13)) @@ -14501,7 +15231,7 @@ snapshots: react-dom: 19.2.3(react@19.2.3) tailwind-merge: 2.6.0 viem: 2.43.5(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13) - wagmi: 2.19.5(@tanstack/query-core@5.90.11)(@tanstack/react-query@5.90.11(react@19.2.3))(@types/react@19.1.10)(@upstash/redis@1.35.3)(bufferutil@4.0.9)(encoding@0.1.13)(react@19.2.3)(typescript@5.9.2)(utf-8-validate@5.0.10)(viem@2.43.5(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13))(zod@4.1.13) + wagmi: 2.19.5(@tanstack/query-core@5.90.11)(@tanstack/react-query@5.90.11(react@19.2.3))(@types/react@19.1.10)(@upstash/redis@1.35.3)(bufferutil@4.0.9)(encoding@0.1.13)(fastestsmallesttextencoderdecoder@1.0.22)(react@19.2.3)(typescript@5.9.2)(utf-8-validate@5.0.10)(viem@2.43.5(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13))(ws@8.19.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@4.1.13) transitivePeerDependencies: - '@azure/app-configuration' - '@azure/cosmos' @@ -14540,7 +15270,7 @@ snapshots: - ws - zod - '@coinbase/onchainkit@1.1.2(@tanstack/query-core@5.90.11)(@types/react-dom@19.2.1(@types/react@19.2.1))(@types/react@19.2.1)(bufferutil@4.0.9)(encoding@0.1.13)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.2)(use-sync-external-store@1.4.0(react@19.2.3))(utf-8-validate@5.0.10)(viem@2.40.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13))(wagmi@2.19.5(@tanstack/query-core@5.90.11)(@tanstack/react-query@5.90.11(react@19.2.3))(@types/react@19.2.1)(bufferutil@4.0.9)(encoding@0.1.13)(react@19.2.3)(typescript@5.9.2)(utf-8-validate@5.0.10)(viem@2.40.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13))(zod@4.1.13))(zod@4.1.13)': + '@coinbase/onchainkit@1.1.2(@tanstack/query-core@5.90.11)(@types/react-dom@19.2.1(@types/react@19.2.1))(@types/react@19.2.1)(bufferutil@4.0.9)(encoding@0.1.13)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.2)(use-sync-external-store@1.4.0(react@19.2.3))(utf-8-validate@5.0.10)(viem@2.40.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13))(wagmi@2.19.5(@tanstack/query-core@5.90.11)(@tanstack/react-query@5.90.11(react@19.2.3))(@types/react@19.2.1)(bufferutil@4.0.9)(encoding@0.1.13)(fastestsmallesttextencoderdecoder@1.0.22)(react@19.2.3)(typescript@5.9.2)(utf-8-validate@5.0.10)(viem@2.40.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13))(ws@8.19.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@4.1.13))(zod@4.1.13)': dependencies: '@farcaster/miniapp-sdk': 0.1.9(bufferutil@4.0.9)(encoding@0.1.13)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13) '@farcaster/miniapp-wagmi-connector': 1.0.0(@farcaster/miniapp-sdk@0.1.9(bufferutil@4.0.9)(encoding@0.1.13)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13))(@wagmi/core@2.22.1(@tanstack/query-core@5.90.11)(@types/react@19.2.1)(react@19.2.3)(typescript@5.9.2)(use-sync-external-store@1.4.0(react@19.2.3))(viem@2.40.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13)))(viem@2.40.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13)) @@ -14561,7 +15291,7 @@ snapshots: tailwind-merge: 3.4.0 usehooks-ts: 3.1.1(react@19.2.3) viem: 2.40.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13) - wagmi: 2.19.5(@tanstack/query-core@5.90.11)(@tanstack/react-query@5.90.11(react@19.2.3))(@types/react@19.2.1)(bufferutil@4.0.9)(encoding@0.1.13)(react@19.2.3)(typescript@5.9.2)(utf-8-validate@5.0.10)(viem@2.40.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13))(zod@4.1.13) + wagmi: 2.19.5(@tanstack/query-core@5.90.11)(@tanstack/react-query@5.90.11(react@19.2.3))(@types/react@19.2.1)(bufferutil@4.0.9)(encoding@0.1.13)(fastestsmallesttextencoderdecoder@1.0.22)(react@19.2.3)(typescript@5.9.2)(utf-8-validate@5.0.10)(viem@2.40.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13))(ws@8.19.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@4.1.13) transitivePeerDependencies: - '@tanstack/query-core' - '@types/react' @@ -14708,21 +15438,6 @@ snapshots: - utf-8-validate - zod - '@coinbase/x402@2.1.0(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(utf-8-validate@5.0.10)(ws@8.19.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))': - dependencies: - '@coinbase/cdp-sdk': 1.39.0(bufferutil@4.0.9)(encoding@0.1.13)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(utf-8-validate@5.0.10)(ws@8.19.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)) - '@x402/core': 2.3.1 - viem: 2.43.5(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) - zod: 3.25.76 - transitivePeerDependencies: - - bufferutil - - debug - - encoding - - fastestsmallesttextencoderdecoder - - typescript - - utf-8-validate - - ws - '@colors/colors@1.6.0': {} '@craftamap/esbuild-plugin-html@0.9.0(bufferutil@4.0.9)(esbuild@0.25.9)(utf-8-validate@5.0.10)': @@ -14900,7 +15615,7 @@ snapshots: '@es-joy/jsdoccomment@0.50.2': dependencies: '@types/estree': 1.0.8 - '@typescript-eslint/types': 8.40.0 + '@typescript-eslint/types': 8.48.0 comment-parser: 1.4.1 esquery: 1.6.0 jsdoc-type-pratt-parser: 4.1.0 @@ -15304,6 +16019,29 @@ snapshots: typescript: 5.9.2 zod: 3.25.76 + '@fastify/ajv-compiler@4.0.5': + dependencies: + ajv: 8.17.1 + ajv-formats: 3.0.1(ajv@8.17.1) + fast-uri: 3.0.6 + + '@fastify/error@4.2.0': {} + + '@fastify/fast-json-stringify-compiler@5.0.3': + dependencies: + fast-json-stringify: 6.3.0 + + '@fastify/forwarded@3.0.1': {} + + '@fastify/merge-json-schemas@0.2.1': + dependencies: + dequal: 2.0.3 + + '@fastify/proxy-addr@5.1.0': + dependencies: + '@fastify/forwarded': 3.0.1 + ipaddr.js: 2.3.0 + '@floating-ui/core@1.7.3': dependencies: '@floating-ui/utils': 0.2.10 @@ -15396,9 +16134,9 @@ snapshots: dependencies: react: 19.2.3 - '@hono/node-server@1.19.0(hono@4.9.2)': + '@hono/node-server@1.19.0(hono@4.10.7)': dependencies: - hono: 4.9.2 + hono: 4.10.7 '@hpke/chacha20poly1305@1.7.1': dependencies: @@ -16087,6 +16825,8 @@ snapshots: '@paulmillr/qr@0.2.1': {} + '@pinojs/redact@0.4.0': {} + '@pkgjs/parseargs@0.11.0': optional: true @@ -17364,7 +18104,7 @@ snapshots: dependencies: '@reown/appkit-common': 1.7.8(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13) '@reown/appkit-wallet': 1.7.8(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10) - '@walletconnect/universal-provider': 2.21.0(@upstash/redis@1.35.3)(bufferutil@4.0.9)(encoding@0.1.13)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13) + '@walletconnect/universal-provider': 2.21.0(bufferutil@4.0.9)(encoding@0.1.13)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13) valtio: 1.13.2(@types/react@19.1.10)(react@19.2.3) viem: 2.43.5(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13) transitivePeerDependencies: @@ -17398,7 +18138,7 @@ snapshots: dependencies: '@reown/appkit-common': 1.7.8(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13) '@reown/appkit-wallet': 1.7.8(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10) - '@walletconnect/universal-provider': 2.21.0(@upstash/redis@1.35.3)(bufferutil@4.0.9)(encoding@0.1.13)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13) + '@walletconnect/universal-provider': 2.21.0(bufferutil@4.0.9)(encoding@0.1.13)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13) valtio: 1.13.2(@types/react@19.2.1)(react@19.2.3) viem: 2.43.5(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13) transitivePeerDependencies: @@ -18075,7 +18815,7 @@ snapshots: '@reown/appkit-polyfills': 1.7.8 '@reown/appkit-wallet': 1.7.8(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10) '@walletconnect/logger': 2.1.2 - '@walletconnect/universal-provider': 2.21.0(@upstash/redis@1.35.3)(bufferutil@4.0.9)(encoding@0.1.13)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13) + '@walletconnect/universal-provider': 2.21.0(bufferutil@4.0.9)(encoding@0.1.13)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13) valtio: 1.13.2(@types/react@19.1.10)(react@19.2.3) viem: 2.43.5(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13) transitivePeerDependencies: @@ -18112,7 +18852,7 @@ snapshots: '@reown/appkit-polyfills': 1.7.8 '@reown/appkit-wallet': 1.7.8(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10) '@walletconnect/logger': 2.1.2 - '@walletconnect/universal-provider': 2.21.0(@upstash/redis@1.35.3)(bufferutil@4.0.9)(encoding@0.1.13)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13) + '@walletconnect/universal-provider': 2.21.0(bufferutil@4.0.9)(encoding@0.1.13)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13) valtio: 1.13.2(@types/react@19.2.1)(react@19.2.3) viem: 2.43.5(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13) transitivePeerDependencies: @@ -18290,7 +19030,7 @@ snapshots: '@reown/appkit-utils': 1.7.8(@types/react@19.1.10)(bufferutil@4.0.9)(encoding@0.1.13)(react@19.2.3)(typescript@5.9.2)(utf-8-validate@5.0.10)(valtio@1.13.2(@types/react@19.1.10)(react@19.2.3))(zod@4.1.13) '@reown/appkit-wallet': 1.7.8(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10) '@walletconnect/types': 2.21.0(@upstash/redis@1.35.3) - '@walletconnect/universal-provider': 2.21.0(@upstash/redis@1.35.3)(bufferutil@4.0.9)(encoding@0.1.13)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13) + '@walletconnect/universal-provider': 2.21.0(bufferutil@4.0.9)(encoding@0.1.13)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13) bs58: 6.0.0 valtio: 1.13.2(@types/react@19.1.10)(react@19.2.3) viem: 2.43.5(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13) @@ -18332,7 +19072,7 @@ snapshots: '@reown/appkit-utils': 1.7.8(@types/react@19.2.1)(bufferutil@4.0.9)(encoding@0.1.13)(react@19.2.3)(typescript@5.9.2)(utf-8-validate@5.0.10)(valtio@1.13.2(@types/react@19.2.1)(react@19.2.3))(zod@4.1.13) '@reown/appkit-wallet': 1.7.8(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10) '@walletconnect/types': 2.21.0(@upstash/redis@1.35.3) - '@walletconnect/universal-provider': 2.21.0(@upstash/redis@1.35.3)(bufferutil@4.0.9)(encoding@0.1.13)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13) + '@walletconnect/universal-provider': 2.21.0(bufferutil@4.0.9)(encoding@0.1.13)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13) bs58: 6.0.0 valtio: 1.13.2(@types/react@19.2.1)(react@19.2.3) viem: 2.43.5(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13) @@ -18555,10 +19295,6 @@ snapshots: dependencies: '@solana/kit': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.19.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)) - '@solana-program/system@0.8.1(@solana/kit@3.0.3(typescript@5.9.2))': - dependencies: - '@solana/kit': 3.0.3(typescript@5.9.2) - '@solana-program/token-2022@0.4.2(@solana/kit@6.1.0(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(utf-8-validate@5.0.10))(@solana/sysvars@6.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2))': dependencies: '@solana/kit': 6.1.0(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(utf-8-validate@5.0.10) @@ -18586,10 +19322,6 @@ snapshots: dependencies: '@solana/kit': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.19.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)) - '@solana-program/token@0.6.0(@solana/kit@3.0.3(typescript@5.9.2))': - dependencies: - '@solana/kit': 3.0.3(typescript@5.9.2) - '@solana-program/token@0.9.0(@solana/kit@5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.19.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)))': dependencies: '@solana/kit': 5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.19.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)) @@ -18598,6 +19330,18 @@ snapshots: dependencies: '@solana/kit': 5.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.19.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + '@solana/accounts@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)': + dependencies: + '@solana/addresses': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/codecs-core': 2.3.0(typescript@5.9.2) + '@solana/codecs-strings': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/errors': 2.3.0(typescript@5.9.2) + '@solana/rpc-spec': 2.3.0(typescript@5.9.2) + '@solana/rpc-types': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + typescript: 5.9.2 + transitivePeerDependencies: + - fastestsmallesttextencoderdecoder + '@solana/accounts@3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)': dependencies: '@solana/addresses': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) @@ -18917,6 +19661,17 @@ snapshots: transitivePeerDependencies: - fastestsmallesttextencoderdecoder + '@solana/codecs@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)': + dependencies: + '@solana/codecs-core': 2.3.0(typescript@5.9.2) + '@solana/codecs-data-structures': 2.3.0(typescript@5.9.2) + '@solana/codecs-numbers': 2.3.0(typescript@5.9.2) + '@solana/codecs-strings': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/options': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + typescript: 5.9.2 + transitivePeerDependencies: + - fastestsmallesttextencoderdecoder + '@solana/codecs@3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)': dependencies: '@solana/codecs-core': 3.0.3(typescript@5.9.2) @@ -18971,7 +19726,7 @@ snapshots: '@solana/errors@2.3.0(typescript@5.9.2)': dependencies: chalk: 5.6.2 - commander: 14.0.2 + commander: 14.0.3 typescript: 5.9.2 '@solana/errors@3.0.3(typescript@5.9.2)': @@ -19173,33 +19928,32 @@ snapshots: transitivePeerDependencies: - fastestsmallesttextencoderdecoder - '@solana/kit@3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))': + '@solana/kit@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.19.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))': dependencies: - '@solana/accounts': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) - '@solana/addresses': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) - '@solana/codecs': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) - '@solana/errors': 3.0.3(typescript@5.9.2) - '@solana/functional': 3.0.3(typescript@5.9.2) - '@solana/instruction-plans': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) - '@solana/instructions': 3.0.3(typescript@5.9.2) - '@solana/keys': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) - '@solana/programs': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) - '@solana/rpc': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) - '@solana/rpc-parsed-types': 3.0.3(typescript@5.9.2) - '@solana/rpc-spec-types': 3.0.3(typescript@5.9.2) - '@solana/rpc-subscriptions': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)) - '@solana/rpc-types': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) - '@solana/signers': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) - '@solana/sysvars': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) - '@solana/transaction-confirmation': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)) - '@solana/transaction-messages': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) - '@solana/transactions': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/accounts': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/addresses': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/codecs': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/errors': 2.3.0(typescript@5.9.2) + '@solana/functional': 2.3.0(typescript@5.9.2) + '@solana/instructions': 2.3.0(typescript@5.9.2) + '@solana/keys': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/programs': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/rpc': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/rpc-parsed-types': 2.3.0(typescript@5.9.2) + '@solana/rpc-spec-types': 2.3.0(typescript@5.9.2) + '@solana/rpc-subscriptions': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.19.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + '@solana/rpc-types': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/signers': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/sysvars': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/transaction-confirmation': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.19.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + '@solana/transaction-messages': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/transactions': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) typescript: 5.9.2 transitivePeerDependencies: - fastestsmallesttextencoderdecoder - ws - '@solana/kit@3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.19.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))': + '@solana/kit@3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))': dependencies: '@solana/accounts': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) '@solana/addresses': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) @@ -19213,11 +19967,11 @@ snapshots: '@solana/rpc': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) '@solana/rpc-parsed-types': 3.0.3(typescript@5.9.2) '@solana/rpc-spec-types': 3.0.3(typescript@5.9.2) - '@solana/rpc-subscriptions': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.19.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + '@solana/rpc-subscriptions': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)) '@solana/rpc-types': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) '@solana/signers': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) '@solana/sysvars': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) - '@solana/transaction-confirmation': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.19.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + '@solana/transaction-confirmation': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)) '@solana/transaction-messages': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) '@solana/transactions': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) typescript: 5.9.2 @@ -19225,7 +19979,7 @@ snapshots: - fastestsmallesttextencoderdecoder - ws - '@solana/kit@3.0.3(typescript@5.9.2)': + '@solana/kit@3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.19.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))': dependencies: '@solana/accounts': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) '@solana/addresses': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) @@ -19239,11 +19993,11 @@ snapshots: '@solana/rpc': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) '@solana/rpc-parsed-types': 3.0.3(typescript@5.9.2) '@solana/rpc-spec-types': 3.0.3(typescript@5.9.2) - '@solana/rpc-subscriptions': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + '@solana/rpc-subscriptions': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.19.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)) '@solana/rpc-types': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) '@solana/signers': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) '@solana/sysvars': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) - '@solana/transaction-confirmation': 3.0.3(typescript@5.9.2) + '@solana/transaction-confirmation': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.19.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)) '@solana/transaction-messages': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) '@solana/transactions': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) typescript: 5.9.2 @@ -19397,6 +20151,17 @@ snapshots: transitivePeerDependencies: - fastestsmallesttextencoderdecoder + '@solana/options@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)': + dependencies: + '@solana/codecs-core': 2.3.0(typescript@5.9.2) + '@solana/codecs-data-structures': 2.3.0(typescript@5.9.2) + '@solana/codecs-numbers': 2.3.0(typescript@5.9.2) + '@solana/codecs-strings': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/errors': 2.3.0(typescript@5.9.2) + typescript: 5.9.2 + transitivePeerDependencies: + - fastestsmallesttextencoderdecoder + '@solana/options@3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)': dependencies: '@solana/codecs-core': 3.0.3(typescript@5.9.2) @@ -19476,6 +20241,14 @@ snapshots: transitivePeerDependencies: - fastestsmallesttextencoderdecoder + '@solana/programs@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)': + dependencies: + '@solana/addresses': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/errors': 2.3.0(typescript@5.9.2) + typescript: 5.9.2 + transitivePeerDependencies: + - fastestsmallesttextencoderdecoder + '@solana/programs@3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)': dependencies: '@solana/addresses': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) @@ -19761,6 +20534,15 @@ snapshots: typescript: 5.9.2 ws: 8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10) + '@solana/rpc-subscriptions-channel-websocket@2.3.0(typescript@5.9.2)(ws@8.19.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))': + dependencies: + '@solana/errors': 2.3.0(typescript@5.9.2) + '@solana/functional': 2.3.0(typescript@5.9.2) + '@solana/rpc-subscriptions-spec': 2.3.0(typescript@5.9.2) + '@solana/subscribable': 2.3.0(typescript@5.9.2) + typescript: 5.9.2 + ws: 8.19.0(bufferutil@4.0.9)(utf-8-validate@5.0.10) + '@solana/rpc-subscriptions-channel-websocket@3.0.3(typescript@5.9.2)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))': dependencies: '@solana/errors': 3.0.3(typescript@5.9.2) @@ -19870,6 +20652,24 @@ snapshots: - fastestsmallesttextencoderdecoder - ws + '@solana/rpc-subscriptions@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.19.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))': + dependencies: + '@solana/errors': 2.3.0(typescript@5.9.2) + '@solana/fast-stable-stringify': 2.3.0(typescript@5.9.2) + '@solana/functional': 2.3.0(typescript@5.9.2) + '@solana/promises': 2.3.0(typescript@5.9.2) + '@solana/rpc-spec-types': 2.3.0(typescript@5.9.2) + '@solana/rpc-subscriptions-api': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/rpc-subscriptions-channel-websocket': 2.3.0(typescript@5.9.2)(ws@8.19.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + '@solana/rpc-subscriptions-spec': 2.3.0(typescript@5.9.2) + '@solana/rpc-transformers': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/rpc-types': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/subscribable': 2.3.0(typescript@5.9.2) + typescript: 5.9.2 + transitivePeerDependencies: + - fastestsmallesttextencoderdecoder + - ws + '@solana/rpc-subscriptions@3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))': dependencies: '@solana/errors': 3.0.3(typescript@5.9.2) @@ -20024,7 +20824,7 @@ snapshots: '@solana/rpc-spec': 2.3.0(typescript@5.9.2) '@solana/rpc-spec-types': 2.3.0(typescript@5.9.2) typescript: 5.9.2 - undici-types: 7.16.0 + undici-types: 7.22.0 '@solana/rpc-transport-http@3.0.3(typescript@5.9.2)': dependencies: @@ -20032,7 +20832,7 @@ snapshots: '@solana/rpc-spec': 3.0.3(typescript@5.9.2) '@solana/rpc-spec-types': 3.0.3(typescript@5.9.2) typescript: 5.9.2 - undici-types: 7.16.0 + undici-types: 7.22.0 '@solana/rpc-transport-http@5.0.0(typescript@5.9.2)': dependencies: @@ -20196,6 +20996,20 @@ snapshots: transitivePeerDependencies: - fastestsmallesttextencoderdecoder + '@solana/signers@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)': + dependencies: + '@solana/addresses': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/codecs-core': 2.3.0(typescript@5.9.2) + '@solana/errors': 2.3.0(typescript@5.9.2) + '@solana/instructions': 2.3.0(typescript@5.9.2) + '@solana/keys': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/nominal-types': 2.3.0(typescript@5.9.2) + '@solana/transaction-messages': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/transactions': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + typescript: 5.9.2 + transitivePeerDependencies: + - fastestsmallesttextencoderdecoder + '@solana/signers@3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)': dependencies: '@solana/addresses': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) @@ -20312,6 +21126,16 @@ snapshots: optionalDependencies: typescript: 5.9.2 + '@solana/sysvars@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)': + dependencies: + '@solana/accounts': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/codecs': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/errors': 2.3.0(typescript@5.9.2) + '@solana/rpc-types': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + typescript: 5.9.2 + transitivePeerDependencies: + - fastestsmallesttextencoderdecoder + '@solana/sysvars@3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)': dependencies: '@solana/accounts': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) @@ -20370,24 +21194,24 @@ snapshots: - fastestsmallesttextencoderdecoder - ws - '@solana/transaction-confirmation@3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))': + '@solana/transaction-confirmation@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.19.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))': dependencies: - '@solana/addresses': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) - '@solana/codecs-strings': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) - '@solana/errors': 3.0.3(typescript@5.9.2) - '@solana/keys': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) - '@solana/promises': 3.0.3(typescript@5.9.2) - '@solana/rpc': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) - '@solana/rpc-subscriptions': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)) - '@solana/rpc-types': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) - '@solana/transaction-messages': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) - '@solana/transactions': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/addresses': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/codecs-strings': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/errors': 2.3.0(typescript@5.9.2) + '@solana/keys': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/promises': 2.3.0(typescript@5.9.2) + '@solana/rpc': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/rpc-subscriptions': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.19.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + '@solana/rpc-types': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/transaction-messages': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/transactions': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) typescript: 5.9.2 transitivePeerDependencies: - fastestsmallesttextencoderdecoder - ws - '@solana/transaction-confirmation@3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.19.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))': + '@solana/transaction-confirmation@3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))': dependencies: '@solana/addresses': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) '@solana/codecs-strings': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) @@ -20395,7 +21219,7 @@ snapshots: '@solana/keys': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) '@solana/promises': 3.0.3(typescript@5.9.2) '@solana/rpc': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) - '@solana/rpc-subscriptions': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.19.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + '@solana/rpc-subscriptions': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)) '@solana/rpc-types': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) '@solana/transaction-messages': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) '@solana/transactions': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) @@ -20404,7 +21228,7 @@ snapshots: - fastestsmallesttextencoderdecoder - ws - '@solana/transaction-confirmation@3.0.3(typescript@5.9.2)': + '@solana/transaction-confirmation@3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.19.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))': dependencies: '@solana/addresses': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) '@solana/codecs-strings': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) @@ -20412,7 +21236,7 @@ snapshots: '@solana/keys': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) '@solana/promises': 3.0.3(typescript@5.9.2) '@solana/rpc': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) - '@solana/rpc-subscriptions': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + '@solana/rpc-subscriptions': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.19.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)) '@solana/rpc-types': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) '@solana/transaction-messages': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) '@solana/transactions': 3.0.3(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) @@ -20691,6 +21515,31 @@ snapshots: '@stablelib/wipe@1.0.1': {} + '@stellar/js-xdr@3.1.2': {} + + '@stellar/stellar-base@14.1.0': + dependencies: + '@noble/curves': 1.9.7 + '@stellar/js-xdr': 3.1.2 + base32.js: 0.1.0 + bignumber.js: 9.3.1 + buffer: 6.0.3 + sha.js: 2.4.12 + + '@stellar/stellar-sdk@14.6.1': + dependencies: + '@stellar/stellar-base': 14.1.0 + axios: 1.13.4 + bignumber.js: 9.3.1 + commander: 14.0.3 + eventsource: 2.0.2 + feaxios: 0.0.23 + randombytes: 2.1.0 + toml: 3.0.0 + urijs: 1.19.11 + transitivePeerDependencies: + - debug + '@svgr/babel-plugin-add-jsx-attribute@8.0.0(@babel/core@7.28.3)': dependencies: '@babel/core': 7.28.3 @@ -20940,6 +21789,13 @@ snapshots: '@types/estree@1.0.8': {} + '@types/express-serve-static-core@4.19.8': + dependencies: + '@types/node': 22.17.2 + '@types/qs': 6.14.0 + '@types/range-parser': 1.2.7 + '@types/send': 0.17.5 + '@types/express-serve-static-core@5.0.7': dependencies: '@types/node': 22.17.2 @@ -20947,6 +21803,13 @@ snapshots: '@types/range-parser': 1.2.7 '@types/send': 0.17.5 + '@types/express@4.17.25': + dependencies: + '@types/body-parser': 1.19.6 + '@types/express-serve-static-core': 4.19.8 + '@types/qs': 6.14.0 + '@types/serve-static': 1.15.8 + '@types/express@5.0.3': dependencies: '@types/body-parser': 1.19.6 @@ -21536,19 +22399,19 @@ snapshots: - utf-8-validate - zod - '@wagmi/connectors@6.2.0(7c0fb5ad6e91c192e500875cd884eb4c)': + '@wagmi/connectors@6.2.0(1aee367860625464d156864b651beb8c)': dependencies: - '@base-org/account': 2.4.0(@types/react@19.1.10)(bufferutil@4.0.9)(encoding@0.1.13)(fastestsmallesttextencoderdecoder@1.0.22)(react@19.1.1)(typescript@5.9.2)(use-sync-external-store@1.4.0(react@19.1.1))(utf-8-validate@5.0.10)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@3.25.76) - '@coinbase/wallet-sdk': 4.3.6(@types/react@19.1.10)(bufferutil@4.0.9)(react@19.1.1)(typescript@5.9.2)(use-sync-external-store@1.4.0(react@19.1.1))(utf-8-validate@5.0.10)(zod@3.25.76) - '@gemini-wallet/core': 0.3.2(viem@2.40.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76)) + '@base-org/account': 2.4.0(@types/react@19.2.1)(bufferutil@4.0.9)(encoding@0.1.13)(fastestsmallesttextencoderdecoder@1.0.22)(react@19.2.3)(typescript@5.9.2)(use-sync-external-store@1.4.0(react@19.2.3))(utf-8-validate@5.0.10)(ws@8.19.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@4.1.13) + '@coinbase/wallet-sdk': 4.3.6(@types/react@19.2.1)(bufferutil@4.0.9)(react@19.2.3)(typescript@5.9.2)(use-sync-external-store@1.4.0(react@19.2.3))(utf-8-validate@5.0.10)(zod@4.1.13) + '@gemini-wallet/core': 0.3.2(viem@2.40.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13)) '@metamask/sdk': 0.33.1(bufferutil@4.0.9)(encoding@0.1.13)(utf-8-validate@5.0.10) - '@safe-global/safe-apps-provider': 0.18.6(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) - '@safe-global/safe-apps-sdk': 9.1.0(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) - '@wagmi/core': 2.22.1(@tanstack/query-core@5.90.11)(@types/react@19.1.10)(react@19.1.1)(typescript@5.9.2)(use-sync-external-store@1.4.0(react@19.1.1))(viem@2.40.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76)) - '@walletconnect/ethereum-provider': 2.21.1(@types/react@19.1.10)(@upstash/redis@1.35.3)(bufferutil@4.0.9)(encoding@0.1.13)(react@19.1.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + '@safe-global/safe-apps-provider': 0.18.6(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13) + '@safe-global/safe-apps-sdk': 9.1.0(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13) + '@wagmi/core': 2.22.1(@tanstack/query-core@5.90.11)(@types/react@19.2.1)(react@19.2.3)(typescript@5.9.2)(use-sync-external-store@1.4.0(react@19.2.3))(viem@2.40.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13)) + '@walletconnect/ethereum-provider': 2.21.1(@types/react@19.2.1)(bufferutil@4.0.9)(encoding@0.1.13)(react@19.2.3)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13) cbw-sdk: '@coinbase/wallet-sdk@3.9.3' - porto: 0.2.35(@tanstack/react-query@5.90.11(react@19.1.1))(@types/react@19.1.10)(@wagmi/core@2.22.1(@tanstack/query-core@5.90.11)(@types/react@19.1.10)(react@19.1.1)(typescript@5.9.2)(use-sync-external-store@1.4.0(react@19.1.1))(viem@2.40.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76)))(react@19.1.1)(typescript@5.9.2)(use-sync-external-store@1.4.0(react@19.1.1))(viem@2.40.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76))(wagmi@2.19.5(@tanstack/query-core@5.90.11)(@tanstack/react-query@5.90.11(react@19.1.1))(@types/react@19.1.10)(@upstash/redis@1.35.3)(bufferutil@4.0.9)(encoding@0.1.13)(fastestsmallesttextencoderdecoder@1.0.22)(react@19.1.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(viem@2.40.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76))(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@3.25.76)) - viem: 2.40.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + porto: 0.2.35(@tanstack/react-query@5.90.11(react@19.2.3))(@types/react@19.2.1)(@wagmi/core@2.22.1(@tanstack/query-core@5.90.11)(@types/react@19.2.1)(react@19.2.3)(typescript@5.9.2)(use-sync-external-store@1.4.0(react@19.2.3))(viem@2.40.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13)))(react@19.2.3)(typescript@5.9.2)(use-sync-external-store@1.4.0(react@19.2.3))(viem@2.40.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13))(wagmi@2.19.5(@tanstack/query-core@5.90.11)(@tanstack/react-query@5.90.11(react@19.2.3))(@types/react@19.2.1)(bufferutil@4.0.9)(encoding@0.1.13)(fastestsmallesttextencoderdecoder@1.0.22)(react@19.2.3)(typescript@5.9.2)(utf-8-validate@5.0.10)(viem@2.40.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13))(ws@8.19.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@4.1.13)) + viem: 2.40.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13) optionalDependencies: typescript: 5.9.2 transitivePeerDependencies: @@ -21589,19 +22452,19 @@ snapshots: - ws - zod - '@wagmi/connectors@6.2.0(826cb7847d3729c23c5bbd1840c9486a)': + '@wagmi/connectors@6.2.0(26b6de6e2b894d3d8ec3de3fd9d7a34e)': dependencies: - '@base-org/account': 2.4.0(@types/react@19.1.10)(bufferutil@4.0.9)(encoding@0.1.13)(fastestsmallesttextencoderdecoder@1.0.22)(react@19.2.1)(typescript@5.9.2)(use-sync-external-store@1.4.0(react@19.2.1))(utf-8-validate@5.0.10)(ws@8.19.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@3.25.76) - '@coinbase/wallet-sdk': 4.3.6(@types/react@19.1.10)(bufferutil@4.0.9)(react@19.2.1)(typescript@5.9.2)(use-sync-external-store@1.4.0(react@19.2.1))(utf-8-validate@5.0.10)(zod@3.25.76) - '@gemini-wallet/core': 0.3.2(viem@2.40.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76)) + '@base-org/account': 2.4.0(@types/react@19.1.10)(bufferutil@4.0.9)(encoding@0.1.13)(fastestsmallesttextencoderdecoder@1.0.22)(react@19.2.3)(typescript@5.9.2)(use-sync-external-store@1.4.0(react@19.2.3))(utf-8-validate@5.0.10)(ws@8.19.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@4.1.13) + '@coinbase/wallet-sdk': 4.3.6(@types/react@19.1.10)(bufferutil@4.0.9)(react@19.2.3)(typescript@5.9.2)(use-sync-external-store@1.4.0(react@19.2.3))(utf-8-validate@5.0.10)(zod@4.1.13) + '@gemini-wallet/core': 0.3.2(viem@2.40.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13)) '@metamask/sdk': 0.33.1(bufferutil@4.0.9)(encoding@0.1.13)(utf-8-validate@5.0.10) - '@safe-global/safe-apps-provider': 0.18.6(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) - '@safe-global/safe-apps-sdk': 9.1.0(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) - '@wagmi/core': 2.22.1(@tanstack/query-core@5.90.11)(@types/react@19.1.10)(react@19.2.1)(typescript@5.9.2)(use-sync-external-store@1.4.0(react@19.2.1))(viem@2.40.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76)) - '@walletconnect/ethereum-provider': 2.21.1(@types/react@19.1.10)(@upstash/redis@1.35.3)(bufferutil@4.0.9)(encoding@0.1.13)(react@19.2.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + '@safe-global/safe-apps-provider': 0.18.6(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13) + '@safe-global/safe-apps-sdk': 9.1.0(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13) + '@wagmi/core': 2.22.1(@tanstack/query-core@5.90.11)(@types/react@19.1.10)(react@19.2.3)(typescript@5.9.2)(use-sync-external-store@1.5.0(react@19.2.3))(viem@2.40.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13)) + '@walletconnect/ethereum-provider': 2.21.1(@types/react@19.1.10)(@upstash/redis@1.35.3)(bufferutil@4.0.9)(encoding@0.1.13)(react@19.2.3)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13) cbw-sdk: '@coinbase/wallet-sdk@3.9.3' - porto: 0.2.35(@tanstack/react-query@5.90.11(react@19.2.1))(@types/react@19.1.10)(@wagmi/core@2.22.1(@tanstack/query-core@5.90.11)(@types/react@19.1.10)(react@19.2.1)(typescript@5.9.2)(use-sync-external-store@1.4.0(react@19.2.1))(viem@2.40.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76)))(react@19.2.1)(typescript@5.9.2)(use-sync-external-store@1.4.0(react@19.2.1))(viem@2.40.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76))(wagmi@2.19.5(@tanstack/query-core@5.90.11)(@tanstack/react-query@5.90.11(react@19.2.1))(@types/react@19.1.10)(@upstash/redis@1.35.3)(bufferutil@4.0.9)(encoding@0.1.13)(fastestsmallesttextencoderdecoder@1.0.22)(react@19.2.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(viem@2.40.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76))(ws@8.19.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@3.25.76)) - viem: 2.40.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + porto: 0.2.35(@tanstack/react-query@5.90.11(react@19.2.3))(@types/react@19.1.10)(@wagmi/core@2.22.1(@tanstack/query-core@5.90.11)(@types/react@19.1.10)(react@19.2.3)(typescript@5.9.2)(use-sync-external-store@1.5.0(react@19.2.3))(viem@2.40.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13)))(react@19.2.3)(typescript@5.9.2)(use-sync-external-store@1.4.0(react@19.2.3))(viem@2.40.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13))(wagmi@2.19.5(@tanstack/query-core@5.90.11)(@tanstack/react-query@5.90.11(react@19.2.3))(@types/react@19.1.10)(@upstash/redis@1.35.3)(bufferutil@4.0.9)(encoding@0.1.13)(fastestsmallesttextencoderdecoder@1.0.22)(react@19.2.3)(typescript@5.9.2)(utf-8-validate@5.0.10)(viem@2.40.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13))(ws@8.19.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@4.1.13)) + viem: 2.40.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13) optionalDependencies: typescript: 5.9.2 transitivePeerDependencies: @@ -21642,19 +22505,19 @@ snapshots: - ws - zod - '@wagmi/connectors@6.2.0(@tanstack/react-query@5.90.11(react@19.2.3))(@types/react@19.1.10)(@upstash/redis@1.35.3)(@wagmi/core@2.22.1(@tanstack/query-core@5.90.11)(@types/react@19.1.10)(react@19.2.3)(typescript@5.9.2)(use-sync-external-store@1.5.0(react@19.2.3))(viem@2.40.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13)))(bufferutil@4.0.9)(encoding@0.1.13)(react@19.2.3)(typescript@5.9.2)(use-sync-external-store@1.4.0(react@19.2.3))(utf-8-validate@5.0.10)(viem@2.40.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13))(wagmi@2.19.5(@tanstack/query-core@5.90.11)(@tanstack/react-query@5.90.11(react@19.2.3))(@types/react@19.1.10)(@upstash/redis@1.35.3)(bufferutil@4.0.9)(encoding@0.1.13)(react@19.2.3)(typescript@5.9.2)(utf-8-validate@5.0.10)(viem@2.40.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13))(zod@4.1.13))(zod@4.1.13)': + '@wagmi/connectors@6.2.0(7c0fb5ad6e91c192e500875cd884eb4c)': dependencies: - '@base-org/account': 2.4.0(@types/react@19.1.10)(bufferutil@4.0.9)(encoding@0.1.13)(react@19.2.3)(typescript@5.9.2)(use-sync-external-store@1.4.0(react@19.2.3))(utf-8-validate@5.0.10)(zod@4.1.13) - '@coinbase/wallet-sdk': 4.3.6(@types/react@19.1.10)(bufferutil@4.0.9)(react@19.2.3)(typescript@5.9.2)(use-sync-external-store@1.4.0(react@19.2.3))(utf-8-validate@5.0.10)(zod@4.1.13) - '@gemini-wallet/core': 0.3.2(viem@2.40.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13)) + '@base-org/account': 2.4.0(@types/react@19.1.10)(bufferutil@4.0.9)(encoding@0.1.13)(fastestsmallesttextencoderdecoder@1.0.22)(react@19.1.1)(typescript@5.9.2)(use-sync-external-store@1.4.0(react@19.1.1))(utf-8-validate@5.0.10)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@3.25.76) + '@coinbase/wallet-sdk': 4.3.6(@types/react@19.1.10)(bufferutil@4.0.9)(react@19.1.1)(typescript@5.9.2)(use-sync-external-store@1.4.0(react@19.1.1))(utf-8-validate@5.0.10)(zod@3.25.76) + '@gemini-wallet/core': 0.3.2(viem@2.40.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76)) '@metamask/sdk': 0.33.1(bufferutil@4.0.9)(encoding@0.1.13)(utf-8-validate@5.0.10) - '@safe-global/safe-apps-provider': 0.18.6(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13) - '@safe-global/safe-apps-sdk': 9.1.0(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13) - '@wagmi/core': 2.22.1(@tanstack/query-core@5.90.11)(@types/react@19.1.10)(react@19.2.3)(typescript@5.9.2)(use-sync-external-store@1.5.0(react@19.2.3))(viem@2.40.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13)) - '@walletconnect/ethereum-provider': 2.21.1(@types/react@19.1.10)(@upstash/redis@1.35.3)(bufferutil@4.0.9)(encoding@0.1.13)(react@19.2.3)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13) + '@safe-global/safe-apps-provider': 0.18.6(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + '@safe-global/safe-apps-sdk': 9.1.0(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + '@wagmi/core': 2.22.1(@tanstack/query-core@5.90.11)(@types/react@19.1.10)(react@19.1.1)(typescript@5.9.2)(use-sync-external-store@1.4.0(react@19.1.1))(viem@2.40.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76)) + '@walletconnect/ethereum-provider': 2.21.1(@types/react@19.1.10)(@upstash/redis@1.35.3)(bufferutil@4.0.9)(encoding@0.1.13)(react@19.1.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) cbw-sdk: '@coinbase/wallet-sdk@3.9.3' - porto: 0.2.35(@tanstack/react-query@5.90.11(react@19.2.3))(@types/react@19.1.10)(@wagmi/core@2.22.1(@tanstack/query-core@5.90.11)(@types/react@19.1.10)(react@19.2.3)(typescript@5.9.2)(use-sync-external-store@1.5.0(react@19.2.3))(viem@2.40.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13)))(react@19.2.3)(typescript@5.9.2)(use-sync-external-store@1.4.0(react@19.2.3))(viem@2.40.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13))(wagmi@2.19.5(@tanstack/query-core@5.90.11)(@tanstack/react-query@5.90.11(react@19.2.3))(@types/react@19.1.10)(@upstash/redis@1.35.3)(bufferutil@4.0.9)(encoding@0.1.13)(react@19.2.3)(typescript@5.9.2)(utf-8-validate@5.0.10)(viem@2.40.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13))(zod@4.1.13)) - viem: 2.40.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13) + porto: 0.2.35(@tanstack/react-query@5.90.11(react@19.1.1))(@types/react@19.1.10)(@wagmi/core@2.22.1(@tanstack/query-core@5.90.11)(@types/react@19.1.10)(react@19.1.1)(typescript@5.9.2)(use-sync-external-store@1.4.0(react@19.1.1))(viem@2.40.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76)))(react@19.1.1)(typescript@5.9.2)(use-sync-external-store@1.4.0(react@19.1.1))(viem@2.40.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76))(wagmi@2.19.5(@tanstack/query-core@5.90.11)(@tanstack/react-query@5.90.11(react@19.1.1))(@types/react@19.1.10)(@upstash/redis@1.35.3)(bufferutil@4.0.9)(encoding@0.1.13)(fastestsmallesttextencoderdecoder@1.0.22)(react@19.1.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(viem@2.40.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76))(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@3.25.76)) + viem: 2.40.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) optionalDependencies: typescript: 5.9.2 transitivePeerDependencies: @@ -21695,19 +22558,19 @@ snapshots: - ws - zod - '@wagmi/connectors@6.2.0(@tanstack/react-query@5.90.11(react@19.2.3))(@types/react@19.1.10)(@upstash/redis@1.35.3)(@wagmi/core@2.22.1(@tanstack/query-core@5.90.11)(@types/react@19.1.10)(react@19.2.3)(typescript@5.9.2)(use-sync-external-store@1.5.0(react@19.2.3))(viem@2.40.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13)))(bufferutil@4.0.9)(encoding@0.1.13)(react@19.2.3)(typescript@5.9.2)(use-sync-external-store@1.4.0(react@19.2.3))(utf-8-validate@5.0.10)(viem@2.43.5(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13))(wagmi@2.19.5(@tanstack/query-core@5.90.11)(@tanstack/react-query@5.90.11(react@19.2.3))(@types/react@19.1.10)(@upstash/redis@1.35.3)(bufferutil@4.0.9)(encoding@0.1.13)(react@19.2.3)(typescript@5.9.2)(utf-8-validate@5.0.10)(viem@2.40.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13))(zod@4.1.13))(zod@4.1.13)': + '@wagmi/connectors@6.2.0(826cb7847d3729c23c5bbd1840c9486a)': dependencies: - '@base-org/account': 2.4.0(@types/react@19.1.10)(bufferutil@4.0.9)(encoding@0.1.13)(react@19.2.3)(typescript@5.9.2)(use-sync-external-store@1.4.0(react@19.2.3))(utf-8-validate@5.0.10)(zod@4.1.13) - '@coinbase/wallet-sdk': 4.3.6(@types/react@19.1.10)(bufferutil@4.0.9)(react@19.2.3)(typescript@5.9.2)(use-sync-external-store@1.4.0(react@19.2.3))(utf-8-validate@5.0.10)(zod@4.1.13) - '@gemini-wallet/core': 0.3.2(viem@2.43.5(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13)) + '@base-org/account': 2.4.0(@types/react@19.1.10)(bufferutil@4.0.9)(encoding@0.1.13)(fastestsmallesttextencoderdecoder@1.0.22)(react@19.2.1)(typescript@5.9.2)(use-sync-external-store@1.4.0(react@19.2.1))(utf-8-validate@5.0.10)(ws@8.19.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@3.25.76) + '@coinbase/wallet-sdk': 4.3.6(@types/react@19.1.10)(bufferutil@4.0.9)(react@19.2.1)(typescript@5.9.2)(use-sync-external-store@1.4.0(react@19.2.1))(utf-8-validate@5.0.10)(zod@3.25.76) + '@gemini-wallet/core': 0.3.2(viem@2.40.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76)) '@metamask/sdk': 0.33.1(bufferutil@4.0.9)(encoding@0.1.13)(utf-8-validate@5.0.10) - '@safe-global/safe-apps-provider': 0.18.6(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13) - '@safe-global/safe-apps-sdk': 9.1.0(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13) - '@wagmi/core': 2.22.1(@tanstack/query-core@5.90.11)(@types/react@19.1.10)(react@19.2.3)(typescript@5.9.2)(use-sync-external-store@1.5.0(react@19.2.3))(viem@2.40.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13)) - '@walletconnect/ethereum-provider': 2.21.1(@types/react@19.1.10)(@upstash/redis@1.35.3)(bufferutil@4.0.9)(encoding@0.1.13)(react@19.2.3)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13) + '@safe-global/safe-apps-provider': 0.18.6(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + '@safe-global/safe-apps-sdk': 9.1.0(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + '@wagmi/core': 2.22.1(@tanstack/query-core@5.90.11)(@types/react@19.1.10)(react@19.2.1)(typescript@5.9.2)(use-sync-external-store@1.4.0(react@19.2.1))(viem@2.40.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76)) + '@walletconnect/ethereum-provider': 2.21.1(@types/react@19.1.10)(@upstash/redis@1.35.3)(bufferutil@4.0.9)(encoding@0.1.13)(react@19.2.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) cbw-sdk: '@coinbase/wallet-sdk@3.9.3' - porto: 0.2.35(@tanstack/react-query@5.90.11(react@19.2.3))(@types/react@19.1.10)(@wagmi/core@2.22.1(@tanstack/query-core@5.90.11)(@types/react@19.1.10)(react@19.2.3)(typescript@5.9.2)(use-sync-external-store@1.5.0(react@19.2.3))(viem@2.40.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13)))(react@19.2.3)(typescript@5.9.2)(use-sync-external-store@1.4.0(react@19.2.3))(viem@2.43.5(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13))(wagmi@2.19.5(@tanstack/query-core@5.90.11)(@tanstack/react-query@5.90.11(react@19.2.3))(@types/react@19.1.10)(@upstash/redis@1.35.3)(bufferutil@4.0.9)(encoding@0.1.13)(react@19.2.3)(typescript@5.9.2)(utf-8-validate@5.0.10)(viem@2.40.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13))(zod@4.1.13)) - viem: 2.43.5(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13) + porto: 0.2.35(@tanstack/react-query@5.90.11(react@19.2.1))(@types/react@19.1.10)(@wagmi/core@2.22.1(@tanstack/query-core@5.90.11)(@types/react@19.1.10)(react@19.2.1)(typescript@5.9.2)(use-sync-external-store@1.4.0(react@19.2.1))(viem@2.40.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76)))(react@19.2.1)(typescript@5.9.2)(use-sync-external-store@1.4.0(react@19.2.1))(viem@2.40.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76))(wagmi@2.19.5(@tanstack/query-core@5.90.11)(@tanstack/react-query@5.90.11(react@19.2.1))(@types/react@19.1.10)(@upstash/redis@1.35.3)(bufferutil@4.0.9)(encoding@0.1.13)(fastestsmallesttextencoderdecoder@1.0.22)(react@19.2.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(viem@2.40.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76))(ws@8.19.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@3.25.76)) + viem: 2.40.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) optionalDependencies: typescript: 5.9.2 transitivePeerDependencies: @@ -21748,19 +22611,19 @@ snapshots: - ws - zod - '@wagmi/connectors@6.2.0(@tanstack/react-query@5.90.11(react@19.2.3))(@types/react@19.1.10)(@wagmi/core@2.22.1(@tanstack/query-core@5.90.11)(@types/react@19.1.10)(react@19.2.3)(typescript@5.9.2)(viem@2.40.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13)))(bufferutil@4.0.9)(encoding@0.1.13)(react@19.2.3)(typescript@5.9.2)(use-sync-external-store@1.4.0(react@19.2.3))(utf-8-validate@5.0.10)(viem@2.40.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13))(wagmi@2.19.5(@tanstack/query-core@5.90.11)(@tanstack/react-query@5.90.11(react@19.2.3))(@types/react@19.1.10)(bufferutil@4.0.9)(encoding@0.1.13)(react@19.2.3)(typescript@5.9.2)(utf-8-validate@5.0.10)(viem@2.40.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13))(zod@4.1.13))(zod@4.1.13)': + '@wagmi/connectors@6.2.0(87f53a8d2b85f9075dbb2195dfb00723)': dependencies: - '@base-org/account': 2.4.0(@types/react@19.1.10)(bufferutil@4.0.9)(encoding@0.1.13)(react@19.2.3)(typescript@5.9.2)(use-sync-external-store@1.4.0(react@19.2.3))(utf-8-validate@5.0.10)(zod@4.1.13) + '@base-org/account': 2.4.0(@types/react@19.1.10)(bufferutil@4.0.9)(encoding@0.1.13)(fastestsmallesttextencoderdecoder@1.0.22)(react@19.2.3)(typescript@5.9.2)(use-sync-external-store@1.4.0(react@19.2.3))(utf-8-validate@5.0.10)(ws@8.19.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@4.1.13) '@coinbase/wallet-sdk': 4.3.6(@types/react@19.1.10)(bufferutil@4.0.9)(react@19.2.3)(typescript@5.9.2)(use-sync-external-store@1.4.0(react@19.2.3))(utf-8-validate@5.0.10)(zod@4.1.13) - '@gemini-wallet/core': 0.3.2(viem@2.40.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13)) + '@gemini-wallet/core': 0.3.2(viem@2.43.5(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13)) '@metamask/sdk': 0.33.1(bufferutil@4.0.9)(encoding@0.1.13)(utf-8-validate@5.0.10) '@safe-global/safe-apps-provider': 0.18.6(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13) '@safe-global/safe-apps-sdk': 9.1.0(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13) - '@wagmi/core': 2.22.1(@tanstack/query-core@5.90.11)(@types/react@19.1.10)(react@19.2.3)(typescript@5.9.2)(use-sync-external-store@1.4.0(react@19.2.3))(viem@2.43.5(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13)) - '@walletconnect/ethereum-provider': 2.21.1(@types/react@19.1.10)(bufferutil@4.0.9)(encoding@0.1.13)(react@19.2.3)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13) + '@wagmi/core': 2.22.1(@tanstack/query-core@5.90.11)(@types/react@19.1.10)(react@19.2.3)(typescript@5.9.2)(use-sync-external-store@1.5.0(react@19.2.3))(viem@2.40.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13)) + '@walletconnect/ethereum-provider': 2.21.1(@types/react@19.1.10)(@upstash/redis@1.35.3)(bufferutil@4.0.9)(encoding@0.1.13)(react@19.2.3)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13) cbw-sdk: '@coinbase/wallet-sdk@3.9.3' - porto: 0.2.35(@tanstack/react-query@5.90.11(react@19.2.3))(@types/react@19.1.10)(@wagmi/core@2.22.1(@tanstack/query-core@5.90.11)(@types/react@19.1.10)(react@19.2.3)(typescript@5.9.2)(viem@2.40.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13)))(react@19.2.3)(typescript@5.9.2)(use-sync-external-store@1.4.0(react@19.2.3))(viem@2.40.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13))(wagmi@2.19.5(@tanstack/query-core@5.90.11)(@tanstack/react-query@5.90.11(react@19.2.3))(@types/react@19.1.10)(bufferutil@4.0.9)(encoding@0.1.13)(react@19.2.3)(typescript@5.9.2)(utf-8-validate@5.0.10)(viem@2.40.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13))(zod@4.1.13)) - viem: 2.40.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13) + porto: 0.2.35(@tanstack/react-query@5.90.11(react@19.2.3))(@types/react@19.1.10)(@wagmi/core@2.22.1(@tanstack/query-core@5.90.11)(@types/react@19.1.10)(react@19.2.3)(typescript@5.9.2)(use-sync-external-store@1.5.0(react@19.2.3))(viem@2.40.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13)))(react@19.2.3)(typescript@5.9.2)(use-sync-external-store@1.4.0(react@19.2.3))(viem@2.43.5(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13))(wagmi@2.19.5(@tanstack/query-core@5.90.11)(@tanstack/react-query@5.90.11(react@19.2.3))(@types/react@19.1.10)(@upstash/redis@1.35.3)(bufferutil@4.0.9)(encoding@0.1.13)(fastestsmallesttextencoderdecoder@1.0.22)(react@19.2.3)(typescript@5.9.2)(utf-8-validate@5.0.10)(viem@2.40.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13))(ws@8.19.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@4.1.13)) + viem: 2.43.5(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13) optionalDependencies: typescript: 5.9.2 transitivePeerDependencies: @@ -21801,19 +22664,19 @@ snapshots: - ws - zod - '@wagmi/connectors@6.2.0(@tanstack/react-query@5.90.11(react@19.2.3))(@types/react@19.1.10)(@wagmi/core@2.22.1(@tanstack/query-core@5.90.11)(@types/react@19.1.10)(react@19.2.3)(typescript@5.9.2)(viem@2.40.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13)))(bufferutil@4.0.9)(encoding@0.1.13)(react@19.2.3)(typescript@5.9.2)(use-sync-external-store@1.4.0(react@19.2.3))(utf-8-validate@5.0.10)(viem@2.43.5(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13))(wagmi@2.19.5(@tanstack/query-core@5.90.11)(@tanstack/react-query@5.90.11(react@19.2.3))(@types/react@19.1.10)(bufferutil@4.0.9)(encoding@0.1.13)(react@19.2.3)(typescript@5.9.2)(utf-8-validate@5.0.10)(viem@2.40.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13))(zod@4.1.13))(zod@4.1.13)': + '@wagmi/connectors@6.2.0(@tanstack/react-query@5.90.11(react@19.2.3))(@types/react@19.1.10)(@wagmi/core@2.22.1(@tanstack/query-core@5.90.11)(@types/react@19.1.10)(react@19.2.3)(typescript@5.9.2)(viem@2.40.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13)))(bufferutil@4.0.9)(encoding@0.1.13)(fastestsmallesttextencoderdecoder@1.0.22)(react@19.2.3)(typescript@5.9.2)(use-sync-external-store@1.4.0(react@19.2.3))(utf-8-validate@5.0.10)(viem@2.40.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13))(wagmi@2.19.5(@tanstack/query-core@5.90.11)(@tanstack/react-query@5.90.11(react@19.2.3))(@types/react@19.1.10)(bufferutil@4.0.9)(encoding@0.1.13)(fastestsmallesttextencoderdecoder@1.0.22)(react@19.2.3)(typescript@5.9.2)(utf-8-validate@5.0.10)(viem@2.40.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13))(ws@8.19.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@4.1.13))(ws@8.19.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@4.1.13)': dependencies: - '@base-org/account': 2.4.0(@types/react@19.1.10)(bufferutil@4.0.9)(encoding@0.1.13)(react@19.2.3)(typescript@5.9.2)(use-sync-external-store@1.4.0(react@19.2.3))(utf-8-validate@5.0.10)(zod@4.1.13) + '@base-org/account': 2.4.0(@types/react@19.1.10)(bufferutil@4.0.9)(encoding@0.1.13)(fastestsmallesttextencoderdecoder@1.0.22)(react@19.2.3)(typescript@5.9.2)(use-sync-external-store@1.4.0(react@19.2.3))(utf-8-validate@5.0.10)(ws@8.19.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@4.1.13) '@coinbase/wallet-sdk': 4.3.6(@types/react@19.1.10)(bufferutil@4.0.9)(react@19.2.3)(typescript@5.9.2)(use-sync-external-store@1.4.0(react@19.2.3))(utf-8-validate@5.0.10)(zod@4.1.13) - '@gemini-wallet/core': 0.3.2(viem@2.43.5(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13)) + '@gemini-wallet/core': 0.3.2(viem@2.40.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13)) '@metamask/sdk': 0.33.1(bufferutil@4.0.9)(encoding@0.1.13)(utf-8-validate@5.0.10) '@safe-global/safe-apps-provider': 0.18.6(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13) '@safe-global/safe-apps-sdk': 9.1.0(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13) '@wagmi/core': 2.22.1(@tanstack/query-core@5.90.11)(@types/react@19.1.10)(react@19.2.3)(typescript@5.9.2)(use-sync-external-store@1.4.0(react@19.2.3))(viem@2.43.5(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13)) '@walletconnect/ethereum-provider': 2.21.1(@types/react@19.1.10)(bufferutil@4.0.9)(encoding@0.1.13)(react@19.2.3)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13) cbw-sdk: '@coinbase/wallet-sdk@3.9.3' - porto: 0.2.35(@tanstack/react-query@5.90.11(react@19.2.3))(@types/react@19.1.10)(@wagmi/core@2.22.1(@tanstack/query-core@5.90.11)(@types/react@19.1.10)(react@19.2.3)(typescript@5.9.2)(viem@2.40.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13)))(react@19.2.3)(typescript@5.9.2)(use-sync-external-store@1.4.0(react@19.2.3))(viem@2.43.5(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13))(wagmi@2.19.5(@tanstack/query-core@5.90.11)(@tanstack/react-query@5.90.11(react@19.2.3))(@types/react@19.1.10)(bufferutil@4.0.9)(encoding@0.1.13)(react@19.2.3)(typescript@5.9.2)(utf-8-validate@5.0.10)(viem@2.40.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13))(zod@4.1.13)) - viem: 2.43.5(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13) + porto: 0.2.35(@tanstack/react-query@5.90.11(react@19.2.3))(@types/react@19.1.10)(@wagmi/core@2.22.1(@tanstack/query-core@5.90.11)(@types/react@19.1.10)(react@19.2.3)(typescript@5.9.2)(viem@2.40.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13)))(react@19.2.3)(typescript@5.9.2)(use-sync-external-store@1.4.0(react@19.2.3))(viem@2.40.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13))(wagmi@2.19.5(@tanstack/query-core@5.90.11)(@tanstack/react-query@5.90.11(react@19.2.3))(@types/react@19.1.10)(bufferutil@4.0.9)(encoding@0.1.13)(fastestsmallesttextencoderdecoder@1.0.22)(react@19.2.3)(typescript@5.9.2)(utf-8-validate@5.0.10)(viem@2.40.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13))(ws@8.19.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@4.1.13)) + viem: 2.40.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13) optionalDependencies: typescript: 5.9.2 transitivePeerDependencies: @@ -21854,19 +22717,19 @@ snapshots: - ws - zod - '@wagmi/connectors@6.2.0(@tanstack/react-query@5.90.11(react@19.2.3))(@types/react@19.2.1)(@wagmi/core@2.22.1(@tanstack/query-core@5.90.11)(@types/react@19.2.1)(react@19.2.3)(typescript@5.9.2)(use-sync-external-store@1.4.0(react@19.2.3))(viem@2.40.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13)))(bufferutil@4.0.9)(encoding@0.1.13)(react@19.2.3)(typescript@5.9.2)(use-sync-external-store@1.4.0(react@19.2.3))(utf-8-validate@5.0.10)(viem@2.40.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13))(wagmi@2.19.5(@tanstack/query-core@5.90.11)(@tanstack/react-query@5.90.11(react@19.2.3))(@types/react@19.2.1)(bufferutil@4.0.9)(encoding@0.1.13)(react@19.2.3)(typescript@5.9.2)(utf-8-validate@5.0.10)(viem@2.40.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13))(zod@4.1.13))(zod@4.1.13)': + '@wagmi/connectors@6.2.0(@tanstack/react-query@5.90.11(react@19.2.3))(@types/react@19.1.10)(@wagmi/core@2.22.1(@tanstack/query-core@5.90.11)(@types/react@19.1.10)(react@19.2.3)(typescript@5.9.2)(viem@2.40.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13)))(bufferutil@4.0.9)(encoding@0.1.13)(fastestsmallesttextencoderdecoder@1.0.22)(react@19.2.3)(typescript@5.9.2)(use-sync-external-store@1.4.0(react@19.2.3))(utf-8-validate@5.0.10)(viem@2.43.5(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13))(wagmi@2.19.5(@tanstack/query-core@5.90.11)(@tanstack/react-query@5.90.11(react@19.2.3))(@types/react@19.1.10)(bufferutil@4.0.9)(encoding@0.1.13)(fastestsmallesttextencoderdecoder@1.0.22)(react@19.2.3)(typescript@5.9.2)(utf-8-validate@5.0.10)(viem@2.40.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13))(ws@8.19.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@4.1.13))(ws@8.19.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@4.1.13)': dependencies: - '@base-org/account': 2.4.0(@types/react@19.2.1)(bufferutil@4.0.9)(encoding@0.1.13)(react@19.2.3)(typescript@5.9.2)(use-sync-external-store@1.4.0(react@19.2.3))(utf-8-validate@5.0.10)(zod@4.1.13) - '@coinbase/wallet-sdk': 4.3.6(@types/react@19.2.1)(bufferutil@4.0.9)(react@19.2.3)(typescript@5.9.2)(use-sync-external-store@1.4.0(react@19.2.3))(utf-8-validate@5.0.10)(zod@4.1.13) - '@gemini-wallet/core': 0.3.2(viem@2.40.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13)) + '@base-org/account': 2.4.0(@types/react@19.1.10)(bufferutil@4.0.9)(encoding@0.1.13)(fastestsmallesttextencoderdecoder@1.0.22)(react@19.2.3)(typescript@5.9.2)(use-sync-external-store@1.4.0(react@19.2.3))(utf-8-validate@5.0.10)(ws@8.19.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@4.1.13) + '@coinbase/wallet-sdk': 4.3.6(@types/react@19.1.10)(bufferutil@4.0.9)(react@19.2.3)(typescript@5.9.2)(use-sync-external-store@1.4.0(react@19.2.3))(utf-8-validate@5.0.10)(zod@4.1.13) + '@gemini-wallet/core': 0.3.2(viem@2.43.5(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13)) '@metamask/sdk': 0.33.1(bufferutil@4.0.9)(encoding@0.1.13)(utf-8-validate@5.0.10) '@safe-global/safe-apps-provider': 0.18.6(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13) '@safe-global/safe-apps-sdk': 9.1.0(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13) - '@wagmi/core': 2.22.1(@tanstack/query-core@5.90.11)(@types/react@19.2.1)(react@19.2.3)(typescript@5.9.2)(use-sync-external-store@1.4.0(react@19.2.3))(viem@2.40.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13)) - '@walletconnect/ethereum-provider': 2.21.1(@types/react@19.2.1)(bufferutil@4.0.9)(encoding@0.1.13)(react@19.2.3)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13) + '@wagmi/core': 2.22.1(@tanstack/query-core@5.90.11)(@types/react@19.1.10)(react@19.2.3)(typescript@5.9.2)(use-sync-external-store@1.4.0(react@19.2.3))(viem@2.43.5(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13)) + '@walletconnect/ethereum-provider': 2.21.1(@types/react@19.1.10)(bufferutil@4.0.9)(encoding@0.1.13)(react@19.2.3)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13) cbw-sdk: '@coinbase/wallet-sdk@3.9.3' - porto: 0.2.35(@tanstack/react-query@5.90.11(react@19.2.3))(@types/react@19.2.1)(@wagmi/core@2.22.1(@tanstack/query-core@5.90.11)(@types/react@19.2.1)(react@19.2.3)(typescript@5.9.2)(use-sync-external-store@1.4.0(react@19.2.3))(viem@2.40.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13)))(react@19.2.3)(typescript@5.9.2)(use-sync-external-store@1.4.0(react@19.2.3))(viem@2.40.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13))(wagmi@2.19.5(@tanstack/query-core@5.90.11)(@tanstack/react-query@5.90.11(react@19.2.3))(@types/react@19.2.1)(bufferutil@4.0.9)(encoding@0.1.13)(react@19.2.3)(typescript@5.9.2)(utf-8-validate@5.0.10)(viem@2.40.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13))(zod@4.1.13)) - viem: 2.40.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13) + porto: 0.2.35(@tanstack/react-query@5.90.11(react@19.2.3))(@types/react@19.1.10)(@wagmi/core@2.22.1(@tanstack/query-core@5.90.11)(@types/react@19.1.10)(react@19.2.3)(typescript@5.9.2)(viem@2.40.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13)))(react@19.2.3)(typescript@5.9.2)(use-sync-external-store@1.4.0(react@19.2.3))(viem@2.43.5(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13))(wagmi@2.19.5(@tanstack/query-core@5.90.11)(@tanstack/react-query@5.90.11(react@19.2.3))(@types/react@19.1.10)(bufferutil@4.0.9)(encoding@0.1.13)(fastestsmallesttextencoderdecoder@1.0.22)(react@19.2.3)(typescript@5.9.2)(utf-8-validate@5.0.10)(viem@2.40.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13))(ws@8.19.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@4.1.13)) + viem: 2.43.5(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13) optionalDependencies: typescript: 5.9.2 transitivePeerDependencies: @@ -22221,6 +23084,49 @@ snapshots: - utf-8-validate - zod + '@walletconnect/core@2.21.0(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13)': + dependencies: + '@walletconnect/heartbeat': 1.2.2 + '@walletconnect/jsonrpc-provider': 1.0.14 + '@walletconnect/jsonrpc-types': 1.0.4 + '@walletconnect/jsonrpc-utils': 1.0.8 + '@walletconnect/jsonrpc-ws-connection': 1.0.16(bufferutil@4.0.9)(utf-8-validate@5.0.10) + '@walletconnect/keyvaluestorage': 1.1.1(@upstash/redis@1.35.3) + '@walletconnect/logger': 2.1.2 + '@walletconnect/relay-api': 1.0.11 + '@walletconnect/relay-auth': 1.1.0 + '@walletconnect/safe-json': 1.0.2 + '@walletconnect/time': 1.0.2 + '@walletconnect/types': 2.21.0(@upstash/redis@1.35.3) + '@walletconnect/utils': 2.21.0(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13) + '@walletconnect/window-getters': 1.0.1 + es-toolkit: 1.33.0 + events: 3.3.0 + uint8arrays: 3.1.0 + transitivePeerDependencies: + - '@azure/app-configuration' + - '@azure/cosmos' + - '@azure/data-tables' + - '@azure/identity' + - '@azure/keyvault-secrets' + - '@azure/storage-blob' + - '@capacitor/preferences' + - '@deno/kv' + - '@netlify/blobs' + - '@planetscale/database' + - '@react-native-async-storage/async-storage' + - '@upstash/redis' + - '@vercel/blob' + - '@vercel/kv' + - aws4fetch + - bufferutil + - db0 + - ioredis + - typescript + - uploadthing + - utf-8-validate + - zod + '@walletconnect/core@2.21.1(@upstash/redis@1.35.3)(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76)': dependencies: '@walletconnect/heartbeat': 1.2.2 @@ -22307,6 +23213,49 @@ snapshots: - utf-8-validate - zod + '@walletconnect/core@2.21.1(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13)': + dependencies: + '@walletconnect/heartbeat': 1.2.2 + '@walletconnect/jsonrpc-provider': 1.0.14 + '@walletconnect/jsonrpc-types': 1.0.4 + '@walletconnect/jsonrpc-utils': 1.0.8 + '@walletconnect/jsonrpc-ws-connection': 1.0.16(bufferutil@4.0.9)(utf-8-validate@5.0.10) + '@walletconnect/keyvaluestorage': 1.1.1(@upstash/redis@1.35.3) + '@walletconnect/logger': 2.1.2 + '@walletconnect/relay-api': 1.0.11 + '@walletconnect/relay-auth': 1.1.0 + '@walletconnect/safe-json': 1.0.2 + '@walletconnect/time': 1.0.2 + '@walletconnect/types': 2.21.1(@upstash/redis@1.35.3) + '@walletconnect/utils': 2.21.1(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13) + '@walletconnect/window-getters': 1.0.1 + es-toolkit: 1.33.0 + events: 3.3.0 + uint8arrays: 3.1.0 + transitivePeerDependencies: + - '@azure/app-configuration' + - '@azure/cosmos' + - '@azure/data-tables' + - '@azure/identity' + - '@azure/keyvault-secrets' + - '@azure/storage-blob' + - '@capacitor/preferences' + - '@deno/kv' + - '@netlify/blobs' + - '@planetscale/database' + - '@react-native-async-storage/async-storage' + - '@upstash/redis' + - '@vercel/blob' + - '@vercel/kv' + - aws4fetch + - bufferutil + - db0 + - ioredis + - typescript + - uploadthing + - utf-8-validate + - zod + '@walletconnect/environment@1.0.1': dependencies: tslib: 1.14.1 @@ -22439,10 +23388,10 @@ snapshots: '@walletconnect/jsonrpc-types': 1.0.4 '@walletconnect/jsonrpc-utils': 1.0.8 '@walletconnect/keyvaluestorage': 1.1.1(@upstash/redis@1.35.3) - '@walletconnect/sign-client': 2.21.1(@upstash/redis@1.35.3)(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13) + '@walletconnect/sign-client': 2.21.1(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13) '@walletconnect/types': 2.21.1(@upstash/redis@1.35.3) - '@walletconnect/universal-provider': 2.21.1(@upstash/redis@1.35.3)(bufferutil@4.0.9)(encoding@0.1.13)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13) - '@walletconnect/utils': 2.21.1(@upstash/redis@1.35.3)(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13) + '@walletconnect/universal-provider': 2.21.1(bufferutil@4.0.9)(encoding@0.1.13)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13) + '@walletconnect/utils': 2.21.1(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13) events: 3.3.0 transitivePeerDependencies: - '@azure/app-configuration' @@ -22479,10 +23428,10 @@ snapshots: '@walletconnect/jsonrpc-types': 1.0.4 '@walletconnect/jsonrpc-utils': 1.0.8 '@walletconnect/keyvaluestorage': 1.1.1(@upstash/redis@1.35.3) - '@walletconnect/sign-client': 2.21.1(@upstash/redis@1.35.3)(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13) + '@walletconnect/sign-client': 2.21.1(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13) '@walletconnect/types': 2.21.1(@upstash/redis@1.35.3) - '@walletconnect/universal-provider': 2.21.1(@upstash/redis@1.35.3)(bufferutil@4.0.9)(encoding@0.1.13)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13) - '@walletconnect/utils': 2.21.1(@upstash/redis@1.35.3)(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13) + '@walletconnect/universal-provider': 2.21.1(bufferutil@4.0.9)(encoding@0.1.13)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13) + '@walletconnect/utils': 2.21.1(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13) events: 3.3.0 transitivePeerDependencies: - '@azure/app-configuration' @@ -22673,16 +23622,86 @@ snapshots: - utf-8-validate - zod + '@walletconnect/sign-client@2.21.0(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13)': + dependencies: + '@walletconnect/core': 2.21.0(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13) + '@walletconnect/events': 1.0.1 + '@walletconnect/heartbeat': 1.2.2 + '@walletconnect/jsonrpc-utils': 1.0.8 + '@walletconnect/logger': 2.1.2 + '@walletconnect/time': 1.0.2 + '@walletconnect/types': 2.21.0(@upstash/redis@1.35.3) + '@walletconnect/utils': 2.21.0(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13) + events: 3.3.0 + transitivePeerDependencies: + - '@azure/app-configuration' + - '@azure/cosmos' + - '@azure/data-tables' + - '@azure/identity' + - '@azure/keyvault-secrets' + - '@azure/storage-blob' + - '@capacitor/preferences' + - '@deno/kv' + - '@netlify/blobs' + - '@planetscale/database' + - '@react-native-async-storage/async-storage' + - '@upstash/redis' + - '@vercel/blob' + - '@vercel/kv' + - aws4fetch + - bufferutil + - db0 + - ioredis + - typescript + - uploadthing + - utf-8-validate + - zod + '@walletconnect/sign-client@2.21.1(@upstash/redis@1.35.3)(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76)': dependencies: - '@walletconnect/core': 2.21.1(@upstash/redis@1.35.3)(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + '@walletconnect/core': 2.21.1(@upstash/redis@1.35.3)(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + '@walletconnect/events': 1.0.1 + '@walletconnect/heartbeat': 1.2.2 + '@walletconnect/jsonrpc-utils': 1.0.8 + '@walletconnect/logger': 2.1.2 + '@walletconnect/time': 1.0.2 + '@walletconnect/types': 2.21.1(@upstash/redis@1.35.3) + '@walletconnect/utils': 2.21.1(@upstash/redis@1.35.3)(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + events: 3.3.0 + transitivePeerDependencies: + - '@azure/app-configuration' + - '@azure/cosmos' + - '@azure/data-tables' + - '@azure/identity' + - '@azure/keyvault-secrets' + - '@azure/storage-blob' + - '@capacitor/preferences' + - '@deno/kv' + - '@netlify/blobs' + - '@planetscale/database' + - '@react-native-async-storage/async-storage' + - '@upstash/redis' + - '@vercel/blob' + - '@vercel/kv' + - aws4fetch + - bufferutil + - db0 + - ioredis + - typescript + - uploadthing + - utf-8-validate + - zod + + '@walletconnect/sign-client@2.21.1(@upstash/redis@1.35.3)(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13)': + dependencies: + '@walletconnect/core': 2.21.1(@upstash/redis@1.35.3)(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13) '@walletconnect/events': 1.0.1 '@walletconnect/heartbeat': 1.2.2 '@walletconnect/jsonrpc-utils': 1.0.8 '@walletconnect/logger': 2.1.2 '@walletconnect/time': 1.0.2 '@walletconnect/types': 2.21.1(@upstash/redis@1.35.3) - '@walletconnect/utils': 2.21.1(@upstash/redis@1.35.3)(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + '@walletconnect/utils': 2.21.1(@upstash/redis@1.35.3)(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13) events: 3.3.0 transitivePeerDependencies: - '@azure/app-configuration' @@ -22708,16 +23727,16 @@ snapshots: - utf-8-validate - zod - '@walletconnect/sign-client@2.21.1(@upstash/redis@1.35.3)(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13)': + '@walletconnect/sign-client@2.21.1(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13)': dependencies: - '@walletconnect/core': 2.21.1(@upstash/redis@1.35.3)(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13) + '@walletconnect/core': 2.21.1(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13) '@walletconnect/events': 1.0.1 '@walletconnect/heartbeat': 1.2.2 '@walletconnect/jsonrpc-utils': 1.0.8 '@walletconnect/logger': 2.1.2 '@walletconnect/time': 1.0.2 '@walletconnect/types': 2.21.1(@upstash/redis@1.35.3) - '@walletconnect/utils': 2.21.1(@upstash/redis@1.35.3)(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13) + '@walletconnect/utils': 2.21.1(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13) events: 3.3.0 transitivePeerDependencies: - '@azure/app-configuration' @@ -22881,6 +23900,45 @@ snapshots: - utf-8-validate - zod + '@walletconnect/universal-provider@2.21.0(bufferutil@4.0.9)(encoding@0.1.13)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13)': + dependencies: + '@walletconnect/events': 1.0.1 + '@walletconnect/jsonrpc-http-connection': 1.0.8(encoding@0.1.13) + '@walletconnect/jsonrpc-provider': 1.0.14 + '@walletconnect/jsonrpc-types': 1.0.4 + '@walletconnect/jsonrpc-utils': 1.0.8 + '@walletconnect/keyvaluestorage': 1.1.1(@upstash/redis@1.35.3) + '@walletconnect/logger': 2.1.2 + '@walletconnect/sign-client': 2.21.0(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13) + '@walletconnect/types': 2.21.0(@upstash/redis@1.35.3) + '@walletconnect/utils': 2.21.0(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13) + es-toolkit: 1.33.0 + events: 3.3.0 + transitivePeerDependencies: + - '@azure/app-configuration' + - '@azure/cosmos' + - '@azure/data-tables' + - '@azure/identity' + - '@azure/keyvault-secrets' + - '@azure/storage-blob' + - '@capacitor/preferences' + - '@deno/kv' + - '@netlify/blobs' + - '@planetscale/database' + - '@react-native-async-storage/async-storage' + - '@upstash/redis' + - '@vercel/blob' + - '@vercel/kv' + - aws4fetch + - bufferutil + - db0 + - encoding + - ioredis + - typescript + - uploadthing + - utf-8-validate + - zod + '@walletconnect/universal-provider@2.21.1(@upstash/redis@1.35.3)(bufferutil@4.0.9)(encoding@0.1.13)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76)': dependencies: '@walletconnect/events': 1.0.1 @@ -22959,6 +24017,45 @@ snapshots: - utf-8-validate - zod + '@walletconnect/universal-provider@2.21.1(bufferutil@4.0.9)(encoding@0.1.13)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13)': + dependencies: + '@walletconnect/events': 1.0.1 + '@walletconnect/jsonrpc-http-connection': 1.0.8(encoding@0.1.13) + '@walletconnect/jsonrpc-provider': 1.0.14 + '@walletconnect/jsonrpc-types': 1.0.4 + '@walletconnect/jsonrpc-utils': 1.0.8 + '@walletconnect/keyvaluestorage': 1.1.1(@upstash/redis@1.35.3) + '@walletconnect/logger': 2.1.2 + '@walletconnect/sign-client': 2.21.1(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13) + '@walletconnect/types': 2.21.1(@upstash/redis@1.35.3) + '@walletconnect/utils': 2.21.1(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13) + es-toolkit: 1.33.0 + events: 3.3.0 + transitivePeerDependencies: + - '@azure/app-configuration' + - '@azure/cosmos' + - '@azure/data-tables' + - '@azure/identity' + - '@azure/keyvault-secrets' + - '@azure/storage-blob' + - '@capacitor/preferences' + - '@deno/kv' + - '@netlify/blobs' + - '@planetscale/database' + - '@react-native-async-storage/async-storage' + - '@upstash/redis' + - '@vercel/blob' + - '@vercel/kv' + - aws4fetch + - bufferutil + - db0 + - encoding + - ioredis + - typescript + - uploadthing + - utf-8-validate + - zod + '@walletconnect/utils@2.21.0(@upstash/redis@1.35.3)(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76)': dependencies: '@noble/ciphers': 1.2.1 @@ -23045,6 +24142,49 @@ snapshots: - utf-8-validate - zod + '@walletconnect/utils@2.21.0(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13)': + dependencies: + '@noble/ciphers': 1.2.1 + '@noble/curves': 1.8.1 + '@noble/hashes': 1.7.1 + '@walletconnect/jsonrpc-utils': 1.0.8 + '@walletconnect/keyvaluestorage': 1.1.1(@upstash/redis@1.35.3) + '@walletconnect/relay-api': 1.0.11 + '@walletconnect/relay-auth': 1.1.0 + '@walletconnect/safe-json': 1.0.2 + '@walletconnect/time': 1.0.2 + '@walletconnect/types': 2.21.0(@upstash/redis@1.35.3) + '@walletconnect/window-getters': 1.0.1 + '@walletconnect/window-metadata': 1.0.1 + bs58: 6.0.0 + detect-browser: 5.3.0 + query-string: 7.1.3 + uint8arrays: 3.1.0 + viem: 2.23.2(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13) + transitivePeerDependencies: + - '@azure/app-configuration' + - '@azure/cosmos' + - '@azure/data-tables' + - '@azure/identity' + - '@azure/keyvault-secrets' + - '@azure/storage-blob' + - '@capacitor/preferences' + - '@deno/kv' + - '@netlify/blobs' + - '@planetscale/database' + - '@react-native-async-storage/async-storage' + - '@upstash/redis' + - '@vercel/blob' + - '@vercel/kv' + - aws4fetch + - bufferutil + - db0 + - ioredis + - typescript + - uploadthing + - utf-8-validate + - zod + '@walletconnect/utils@2.21.1(@upstash/redis@1.35.3)(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76)': dependencies: '@noble/ciphers': 1.2.1 @@ -23131,6 +24271,49 @@ snapshots: - utf-8-validate - zod + '@walletconnect/utils@2.21.1(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13)': + dependencies: + '@noble/ciphers': 1.2.1 + '@noble/curves': 1.8.1 + '@noble/hashes': 1.7.1 + '@walletconnect/jsonrpc-utils': 1.0.8 + '@walletconnect/keyvaluestorage': 1.1.1(@upstash/redis@1.35.3) + '@walletconnect/relay-api': 1.0.11 + '@walletconnect/relay-auth': 1.1.0 + '@walletconnect/safe-json': 1.0.2 + '@walletconnect/time': 1.0.2 + '@walletconnect/types': 2.21.1(@upstash/redis@1.35.3) + '@walletconnect/window-getters': 1.0.1 + '@walletconnect/window-metadata': 1.0.1 + bs58: 6.0.0 + detect-browser: 5.3.0 + query-string: 7.1.3 + uint8arrays: 3.1.0 + viem: 2.23.2(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13) + transitivePeerDependencies: + - '@azure/app-configuration' + - '@azure/cosmos' + - '@azure/data-tables' + - '@azure/identity' + - '@azure/keyvault-secrets' + - '@azure/storage-blob' + - '@capacitor/preferences' + - '@deno/kv' + - '@netlify/blobs' + - '@planetscale/database' + - '@react-native-async-storage/async-storage' + - '@upstash/redis' + - '@vercel/blob' + - '@vercel/kv' + - aws4fetch + - bufferutil + - db0 + - ioredis + - typescript + - uploadthing + - utf-8-validate + - zod + '@walletconnect/window-getters@1.0.1': dependencies: tslib: 1.14.1 @@ -23140,10 +24323,6 @@ snapshots: '@walletconnect/window-getters': 1.0.1 tslib: 1.14.1 - '@x402/core@2.3.1': - dependencies: - zod: 3.25.76 - '@xmldom/xmldom@0.8.11': {} abbrev@1.1.1: {} @@ -23192,6 +24371,8 @@ snapshots: dependencies: event-target-shim: 5.0.1 + abstract-logging@2.0.1: {} + accepts@1.3.8: dependencies: mime-types: 2.1.35 @@ -23227,6 +24408,10 @@ snapshots: clean-stack: 2.2.0 indent-string: 4.0.0 + ajv-formats@3.0.1(ajv@8.17.1): + optionalDependencies: + ajv: 8.17.1 + ajv-keywords@3.5.2(ajv@6.12.6): dependencies: ajv: 6.12.6 @@ -23301,7 +24486,7 @@ snapshots: minimatch: 10.0.3 plist: 3.1.0 resedit: 1.7.2 - semver: 7.7.2 + semver: 7.7.3 tar: 6.2.1 temp-file: 3.4.0 tiny-async-pool: 1.3.0 @@ -23422,6 +24607,11 @@ snapshots: dependencies: possible-typed-array-names: 1.1.0 + avvio@9.2.0: + dependencies: + '@fastify/error': 4.2.0 + fastq: 1.19.1 + axe-core@4.10.3: {} axios-mock-adapter@1.22.0(axios@1.13.2): @@ -23435,6 +24625,11 @@ snapshots: axios: 1.13.2 is-retry-allowed: 2.2.0 + axios-retry@4.5.0(axios@1.13.4): + dependencies: + axios: 1.13.4 + is-retry-allowed: 2.2.0 + axios@1.13.2: dependencies: follow-redirects: 1.15.11 @@ -23443,6 +24638,14 @@ snapshots: transitivePeerDependencies: - debug + axios@1.13.4: + dependencies: + follow-redirects: 1.15.11 + form-data: 4.0.4 + proxy-from-env: 1.1.0 + transitivePeerDependencies: + - debug + axobject-query@4.1.0: {} babel-plugin-polyfill-corejs2@0.4.14(@babel/core@7.28.3): @@ -23479,6 +24682,8 @@ snapshots: base-x@5.0.1: {} + base32.js@0.1.0: {} + base64-js@1.5.1: {} big.js@6.2.2: {} @@ -23930,6 +25135,8 @@ snapshots: cookie@0.7.2: {} + cookie@1.1.1: {} + core-js-compat@3.45.1: dependencies: browserslist: 4.25.3 @@ -24125,6 +25332,8 @@ snapshots: depd@2.0.0: {} + dequal@2.0.3: {} + derive-valtio@0.1.0(valtio@1.13.2(@types/react@19.1.10)(react@19.1.1)): dependencies: valtio: 1.13.2(@types/react@19.1.10)(react@19.1.1) @@ -24824,7 +26033,7 @@ snapshots: espree: 10.4.0 esquery: 1.6.0 parse-imports-exports: 0.2.4 - semver: 7.7.2 + semver: 7.7.3 spdx-expression-parse: 4.0.0 transitivePeerDependencies: - supports-color @@ -24885,6 +26094,15 @@ snapshots: optionalDependencies: eslint-config-prettier: 10.1.8(eslint@9.33.0(jiti@2.6.1)) + eslint-plugin-prettier@5.5.4(eslint-config-prettier@10.1.8(eslint@9.33.0(jiti@2.6.1)))(eslint@9.33.0(jiti@2.6.1))(prettier@3.6.2): + dependencies: + eslint: 9.33.0(jiti@2.6.1) + prettier: 3.6.2 + prettier-linter-helpers: 1.0.0 + synckit: 0.11.11 + optionalDependencies: + eslint-config-prettier: 10.1.8(eslint@9.33.0(jiti@2.6.1)) + eslint-plugin-react-hooks@5.2.0(eslint@8.57.1): dependencies: eslint: 8.57.1 @@ -25132,6 +26350,8 @@ snapshots: eventsource-parser@3.0.5: {} + eventsource@2.0.2: {} + eventsource@3.0.7: dependencies: eventsource-parser: 3.0.5 @@ -25244,6 +26464,8 @@ snapshots: eyes@0.1.8: {} + fast-decode-uri-component@1.0.1: {} + fast-deep-equal@3.1.3: {} fast-diff@1.3.0: {} @@ -25266,8 +26488,21 @@ snapshots: fast-json-stable-stringify@2.1.0: {} + fast-json-stringify@6.3.0: + dependencies: + '@fastify/merge-json-schemas': 0.2.1 + ajv: 8.17.1 + ajv-formats: 3.0.1(ajv@8.17.1) + fast-uri: 3.0.6 + json-schema-ref-resolver: 3.0.0 + rfdc: 1.4.1 + fast-levenshtein@2.0.6: {} + fast-querystring@1.1.2: + dependencies: + fast-decode-uri-component: 1.0.1 + fast-redact@3.5.0: {} fast-safe-stringify@2.1.1: {} @@ -25280,6 +26515,24 @@ snapshots: fastestsmallesttextencoderdecoder@1.0.22: {} + fastify@5.8.2: + dependencies: + '@fastify/ajv-compiler': 4.0.5 + '@fastify/error': 4.2.0 + '@fastify/fast-json-stringify-compiler': 5.0.3 + '@fastify/proxy-addr': 5.1.0 + abstract-logging: 2.0.1 + avvio: 9.2.0 + fast-json-stringify: 6.3.0 + find-my-way: 9.5.0 + light-my-request: 6.6.0 + pino: 10.3.1 + process-warning: 5.0.0 + rfdc: 1.4.1 + secure-json-parse: 4.1.0 + semver: 7.7.3 + toad-cache: 3.7.0 + fastq@1.19.1: dependencies: reusify: 1.1.0 @@ -25292,6 +26545,10 @@ snapshots: optionalDependencies: picomatch: 4.0.3 + feaxios@0.0.23: + dependencies: + is-retry-allowed: 3.0.0 + fecha@4.2.3: {} fetch-blob@3.2.0: @@ -25342,6 +26599,12 @@ snapshots: transitivePeerDependencies: - supports-color + find-my-way@9.5.0: + dependencies: + fast-deep-equal: 3.1.3 + fast-querystring: 1.1.2 + safe-regex2: 5.1.0 + find-up@4.1.0: dependencies: locate-path: 5.0.0 @@ -25663,8 +26926,6 @@ snapshots: hono@4.10.7: {} - hono@4.9.2: {} - hosted-git-info@4.1.0: dependencies: lru-cache: 6.0.0 @@ -25790,6 +27051,8 @@ snapshots: ipaddr.js@1.9.1: {} + ipaddr.js@2.3.0: {} + iron-webcrypto@1.2.1: {} is-arguments@1.2.0: @@ -25908,6 +27171,8 @@ snapshots: is-retry-allowed@2.2.0: {} + is-retry-allowed@3.0.0: {} + is-set@2.0.3: {} is-shared-array-buffer@1.0.4: @@ -26053,7 +27318,7 @@ snapshots: whatwg-encoding: 3.1.1 whatwg-mimetype: 4.0.0 whatwg-url: 14.2.0 - ws: 8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10) + ws: 8.19.0(bufferutil@4.0.9)(utf-8-validate@5.0.10) xml-name-validator: 5.0.0 transitivePeerDependencies: - bufferutil @@ -26075,6 +27340,10 @@ snapshots: json-rpc-random-id@1.0.1: {} + json-schema-ref-resolver@3.0.0: + dependencies: + dequal: 2.0.3 + json-schema-traverse@0.4.1: {} json-schema-traverse@1.0.0: {} @@ -26151,6 +27420,12 @@ snapshots: libphonenumber-js@1.12.13: {} + light-my-request@6.6.0: + dependencies: + cookie: 1.1.1 + process-warning: 4.0.1 + set-cookie-parser: 2.7.2 + lightningcss-android-arm64@1.30.2: optional: true @@ -26674,6 +27949,8 @@ snapshots: on-exit-leak-free@0.2.0: {} + on-exit-leak-free@2.1.2: {} + on-finished@2.4.1: dependencies: ee-first: 1.1.1 @@ -27079,8 +28356,28 @@ snapshots: duplexify: 4.1.3 split2: 4.2.0 + pino-abstract-transport@3.0.0: + dependencies: + split2: 4.2.0 + pino-std-serializers@4.0.0: {} + pino-std-serializers@7.1.0: {} + + pino@10.3.1: + dependencies: + '@pinojs/redact': 0.4.0 + atomic-sleep: 1.0.0 + on-exit-leak-free: 2.1.2 + pino-abstract-transport: 3.0.0 + pino-std-serializers: 7.1.0 + process-warning: 5.0.0 + quick-format-unescaped: 4.0.4 + real-require: 0.2.0 + safe-stable-stringify: 2.5.0 + sonic-boom: 4.2.1 + thread-stream: 4.0.0 + pino@7.11.0: dependencies: atomic-sleep: 1.0.0 @@ -27175,7 +28472,7 @@ snapshots: - immer - use-sync-external-store - porto@0.2.35(@tanstack/react-query@5.90.11(react@19.2.3))(@types/react@19.1.10)(@wagmi/core@2.22.1(@tanstack/query-core@5.90.11)(@types/react@19.1.10)(react@19.2.3)(typescript@5.9.2)(use-sync-external-store@1.5.0(react@19.2.3))(viem@2.40.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13)))(react@19.2.3)(typescript@5.9.2)(use-sync-external-store@1.4.0(react@19.2.3))(viem@2.40.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13))(wagmi@2.19.5(@tanstack/query-core@5.90.11)(@tanstack/react-query@5.90.11(react@19.2.3))(@types/react@19.1.10)(@upstash/redis@1.35.3)(bufferutil@4.0.9)(encoding@0.1.13)(react@19.2.3)(typescript@5.9.2)(utf-8-validate@5.0.10)(viem@2.40.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13))(zod@4.1.13)): + porto@0.2.35(@tanstack/react-query@5.90.11(react@19.2.3))(@types/react@19.1.10)(@wagmi/core@2.22.1(@tanstack/query-core@5.90.11)(@types/react@19.1.10)(react@19.2.3)(typescript@5.9.2)(use-sync-external-store@1.5.0(react@19.2.3))(viem@2.40.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13)))(react@19.2.3)(typescript@5.9.2)(use-sync-external-store@1.4.0(react@19.2.3))(viem@2.40.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13))(wagmi@2.19.5(@tanstack/query-core@5.90.11)(@tanstack/react-query@5.90.11(react@19.2.3))(@types/react@19.1.10)(@upstash/redis@1.35.3)(bufferutil@4.0.9)(encoding@0.1.13)(fastestsmallesttextencoderdecoder@1.0.22)(react@19.2.3)(typescript@5.9.2)(utf-8-validate@5.0.10)(viem@2.40.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13))(ws@8.19.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@4.1.13)): dependencies: '@wagmi/core': 2.22.1(@tanstack/query-core@5.90.11)(@types/react@19.1.10)(react@19.2.3)(typescript@5.9.2)(use-sync-external-store@1.5.0(react@19.2.3))(viem@2.40.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13)) hono: 4.10.7 @@ -27189,13 +28486,13 @@ snapshots: '@tanstack/react-query': 5.90.11(react@19.2.3) react: 19.2.3 typescript: 5.9.2 - wagmi: 2.19.5(@tanstack/query-core@5.90.11)(@tanstack/react-query@5.90.11(react@19.2.3))(@types/react@19.1.10)(@upstash/redis@1.35.3)(bufferutil@4.0.9)(encoding@0.1.13)(react@19.2.3)(typescript@5.9.2)(utf-8-validate@5.0.10)(viem@2.40.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13))(zod@4.1.13) + wagmi: 2.19.5(@tanstack/query-core@5.90.11)(@tanstack/react-query@5.90.11(react@19.2.3))(@types/react@19.1.10)(@upstash/redis@1.35.3)(bufferutil@4.0.9)(encoding@0.1.13)(fastestsmallesttextencoderdecoder@1.0.22)(react@19.2.3)(typescript@5.9.2)(utf-8-validate@5.0.10)(viem@2.40.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13))(ws@8.19.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@4.1.13) transitivePeerDependencies: - '@types/react' - immer - use-sync-external-store - porto@0.2.35(@tanstack/react-query@5.90.11(react@19.2.3))(@types/react@19.1.10)(@wagmi/core@2.22.1(@tanstack/query-core@5.90.11)(@types/react@19.1.10)(react@19.2.3)(typescript@5.9.2)(use-sync-external-store@1.5.0(react@19.2.3))(viem@2.40.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13)))(react@19.2.3)(typescript@5.9.2)(use-sync-external-store@1.4.0(react@19.2.3))(viem@2.43.5(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13))(wagmi@2.19.5(@tanstack/query-core@5.90.11)(@tanstack/react-query@5.90.11(react@19.2.3))(@types/react@19.1.10)(@upstash/redis@1.35.3)(bufferutil@4.0.9)(encoding@0.1.13)(react@19.2.3)(typescript@5.9.2)(utf-8-validate@5.0.10)(viem@2.40.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13))(zod@4.1.13)): + porto@0.2.35(@tanstack/react-query@5.90.11(react@19.2.3))(@types/react@19.1.10)(@wagmi/core@2.22.1(@tanstack/query-core@5.90.11)(@types/react@19.1.10)(react@19.2.3)(typescript@5.9.2)(use-sync-external-store@1.5.0(react@19.2.3))(viem@2.40.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13)))(react@19.2.3)(typescript@5.9.2)(use-sync-external-store@1.4.0(react@19.2.3))(viem@2.43.5(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13))(wagmi@2.19.5(@tanstack/query-core@5.90.11)(@tanstack/react-query@5.90.11(react@19.2.3))(@types/react@19.1.10)(@upstash/redis@1.35.3)(bufferutil@4.0.9)(encoding@0.1.13)(fastestsmallesttextencoderdecoder@1.0.22)(react@19.2.3)(typescript@5.9.2)(utf-8-validate@5.0.10)(viem@2.40.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13))(ws@8.19.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@4.1.13)): dependencies: '@wagmi/core': 2.22.1(@tanstack/query-core@5.90.11)(@types/react@19.1.10)(react@19.2.3)(typescript@5.9.2)(use-sync-external-store@1.5.0(react@19.2.3))(viem@2.40.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13)) hono: 4.10.7 @@ -27209,13 +28506,13 @@ snapshots: '@tanstack/react-query': 5.90.11(react@19.2.3) react: 19.2.3 typescript: 5.9.2 - wagmi: 2.19.5(@tanstack/query-core@5.90.11)(@tanstack/react-query@5.90.11(react@19.2.3))(@types/react@19.1.10)(@upstash/redis@1.35.3)(bufferutil@4.0.9)(encoding@0.1.13)(react@19.2.3)(typescript@5.9.2)(utf-8-validate@5.0.10)(viem@2.40.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13))(zod@4.1.13) + wagmi: 2.19.5(@tanstack/query-core@5.90.11)(@tanstack/react-query@5.90.11(react@19.2.3))(@types/react@19.1.10)(@upstash/redis@1.35.3)(bufferutil@4.0.9)(encoding@0.1.13)(fastestsmallesttextencoderdecoder@1.0.22)(react@19.2.3)(typescript@5.9.2)(utf-8-validate@5.0.10)(viem@2.40.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13))(ws@8.19.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@4.1.13) transitivePeerDependencies: - '@types/react' - immer - use-sync-external-store - porto@0.2.35(@tanstack/react-query@5.90.11(react@19.2.3))(@types/react@19.1.10)(@wagmi/core@2.22.1(@tanstack/query-core@5.90.11)(@types/react@19.1.10)(react@19.2.3)(typescript@5.9.2)(viem@2.40.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13)))(react@19.2.3)(typescript@5.9.2)(use-sync-external-store@1.4.0(react@19.2.3))(viem@2.40.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13))(wagmi@2.19.5(@tanstack/query-core@5.90.11)(@tanstack/react-query@5.90.11(react@19.2.3))(@types/react@19.1.10)(bufferutil@4.0.9)(encoding@0.1.13)(react@19.2.3)(typescript@5.9.2)(utf-8-validate@5.0.10)(viem@2.40.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13))(zod@4.1.13)): + porto@0.2.35(@tanstack/react-query@5.90.11(react@19.2.3))(@types/react@19.1.10)(@wagmi/core@2.22.1(@tanstack/query-core@5.90.11)(@types/react@19.1.10)(react@19.2.3)(typescript@5.9.2)(viem@2.40.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13)))(react@19.2.3)(typescript@5.9.2)(use-sync-external-store@1.4.0(react@19.2.3))(viem@2.40.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13))(wagmi@2.19.5(@tanstack/query-core@5.90.11)(@tanstack/react-query@5.90.11(react@19.2.3))(@types/react@19.1.10)(bufferutil@4.0.9)(encoding@0.1.13)(fastestsmallesttextencoderdecoder@1.0.22)(react@19.2.3)(typescript@5.9.2)(utf-8-validate@5.0.10)(viem@2.40.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13))(ws@8.19.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@4.1.13)): dependencies: '@wagmi/core': 2.22.1(@tanstack/query-core@5.90.11)(@types/react@19.1.10)(react@19.2.3)(typescript@5.9.2)(use-sync-external-store@1.4.0(react@19.2.3))(viem@2.43.5(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13)) hono: 4.10.7 @@ -27229,13 +28526,13 @@ snapshots: '@tanstack/react-query': 5.90.11(react@19.2.3) react: 19.2.3 typescript: 5.9.2 - wagmi: 2.19.5(@tanstack/query-core@5.90.11)(@tanstack/react-query@5.90.11(react@19.2.3))(@types/react@19.1.10)(bufferutil@4.0.9)(encoding@0.1.13)(react@19.2.3)(typescript@5.9.2)(utf-8-validate@5.0.10)(viem@2.40.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13))(zod@4.1.13) + wagmi: 2.19.5(@tanstack/query-core@5.90.11)(@tanstack/react-query@5.90.11(react@19.2.3))(@types/react@19.1.10)(bufferutil@4.0.9)(encoding@0.1.13)(fastestsmallesttextencoderdecoder@1.0.22)(react@19.2.3)(typescript@5.9.2)(utf-8-validate@5.0.10)(viem@2.40.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13))(ws@8.19.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@4.1.13) transitivePeerDependencies: - '@types/react' - immer - use-sync-external-store - porto@0.2.35(@tanstack/react-query@5.90.11(react@19.2.3))(@types/react@19.1.10)(@wagmi/core@2.22.1(@tanstack/query-core@5.90.11)(@types/react@19.1.10)(react@19.2.3)(typescript@5.9.2)(viem@2.40.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13)))(react@19.2.3)(typescript@5.9.2)(use-sync-external-store@1.4.0(react@19.2.3))(viem@2.43.5(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13))(wagmi@2.19.5(@tanstack/query-core@5.90.11)(@tanstack/react-query@5.90.11(react@19.2.3))(@types/react@19.1.10)(bufferutil@4.0.9)(encoding@0.1.13)(react@19.2.3)(typescript@5.9.2)(utf-8-validate@5.0.10)(viem@2.40.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13))(zod@4.1.13)): + porto@0.2.35(@tanstack/react-query@5.90.11(react@19.2.3))(@types/react@19.1.10)(@wagmi/core@2.22.1(@tanstack/query-core@5.90.11)(@types/react@19.1.10)(react@19.2.3)(typescript@5.9.2)(viem@2.40.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13)))(react@19.2.3)(typescript@5.9.2)(use-sync-external-store@1.4.0(react@19.2.3))(viem@2.43.5(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13))(wagmi@2.19.5(@tanstack/query-core@5.90.11)(@tanstack/react-query@5.90.11(react@19.2.3))(@types/react@19.1.10)(bufferutil@4.0.9)(encoding@0.1.13)(fastestsmallesttextencoderdecoder@1.0.22)(react@19.2.3)(typescript@5.9.2)(utf-8-validate@5.0.10)(viem@2.40.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13))(ws@8.19.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@4.1.13)): dependencies: '@wagmi/core': 2.22.1(@tanstack/query-core@5.90.11)(@types/react@19.1.10)(react@19.2.3)(typescript@5.9.2)(use-sync-external-store@1.4.0(react@19.2.3))(viem@2.43.5(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13)) hono: 4.10.7 @@ -27249,13 +28546,13 @@ snapshots: '@tanstack/react-query': 5.90.11(react@19.2.3) react: 19.2.3 typescript: 5.9.2 - wagmi: 2.19.5(@tanstack/query-core@5.90.11)(@tanstack/react-query@5.90.11(react@19.2.3))(@types/react@19.1.10)(bufferutil@4.0.9)(encoding@0.1.13)(react@19.2.3)(typescript@5.9.2)(utf-8-validate@5.0.10)(viem@2.40.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13))(zod@4.1.13) + wagmi: 2.19.5(@tanstack/query-core@5.90.11)(@tanstack/react-query@5.90.11(react@19.2.3))(@types/react@19.1.10)(bufferutil@4.0.9)(encoding@0.1.13)(fastestsmallesttextencoderdecoder@1.0.22)(react@19.2.3)(typescript@5.9.2)(utf-8-validate@5.0.10)(viem@2.40.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13))(ws@8.19.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@4.1.13) transitivePeerDependencies: - '@types/react' - immer - use-sync-external-store - porto@0.2.35(@tanstack/react-query@5.90.11(react@19.2.3))(@types/react@19.2.1)(@wagmi/core@2.22.1(@tanstack/query-core@5.90.11)(@types/react@19.2.1)(react@19.2.3)(typescript@5.9.2)(use-sync-external-store@1.4.0(react@19.2.3))(viem@2.40.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13)))(react@19.2.3)(typescript@5.9.2)(use-sync-external-store@1.4.0(react@19.2.3))(viem@2.40.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13))(wagmi@2.19.5(@tanstack/query-core@5.90.11)(@tanstack/react-query@5.90.11(react@19.2.3))(@types/react@19.2.1)(bufferutil@4.0.9)(encoding@0.1.13)(react@19.2.3)(typescript@5.9.2)(utf-8-validate@5.0.10)(viem@2.40.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13))(zod@4.1.13)): + porto@0.2.35(@tanstack/react-query@5.90.11(react@19.2.3))(@types/react@19.2.1)(@wagmi/core@2.22.1(@tanstack/query-core@5.90.11)(@types/react@19.2.1)(react@19.2.3)(typescript@5.9.2)(use-sync-external-store@1.4.0(react@19.2.3))(viem@2.40.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13)))(react@19.2.3)(typescript@5.9.2)(use-sync-external-store@1.4.0(react@19.2.3))(viem@2.40.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13))(wagmi@2.19.5(@tanstack/query-core@5.90.11)(@tanstack/react-query@5.90.11(react@19.2.3))(@types/react@19.2.1)(bufferutil@4.0.9)(encoding@0.1.13)(fastestsmallesttextencoderdecoder@1.0.22)(react@19.2.3)(typescript@5.9.2)(utf-8-validate@5.0.10)(viem@2.40.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13))(ws@8.19.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@4.1.13)): dependencies: '@wagmi/core': 2.22.1(@tanstack/query-core@5.90.11)(@types/react@19.2.1)(react@19.2.3)(typescript@5.9.2)(use-sync-external-store@1.4.0(react@19.2.3))(viem@2.40.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13)) hono: 4.10.7 @@ -27269,7 +28566,7 @@ snapshots: '@tanstack/react-query': 5.90.11(react@19.2.3) react: 19.2.3 typescript: 5.9.2 - wagmi: 2.19.5(@tanstack/query-core@5.90.11)(@tanstack/react-query@5.90.11(react@19.2.3))(@types/react@19.2.1)(bufferutil@4.0.9)(encoding@0.1.13)(react@19.2.3)(typescript@5.9.2)(utf-8-validate@5.0.10)(viem@2.40.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13))(zod@4.1.13) + wagmi: 2.19.5(@tanstack/query-core@5.90.11)(@tanstack/react-query@5.90.11(react@19.2.3))(@types/react@19.2.1)(bufferutil@4.0.9)(encoding@0.1.13)(fastestsmallesttextencoderdecoder@1.0.22)(react@19.2.3)(typescript@5.9.2)(utf-8-validate@5.0.10)(viem@2.40.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13))(ws@8.19.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@4.1.13) transitivePeerDependencies: - '@types/react' - immer @@ -27356,6 +28653,10 @@ snapshots: process-warning@1.0.0: {} + process-warning@4.0.1: {} + + process-warning@5.0.0: {} + progress@2.0.3: {} promise-inflight@1.0.1: {} @@ -27490,6 +28791,10 @@ snapshots: radix3@1.1.2: {} + randombytes@2.1.0: + dependencies: + safe-buffer: 5.2.1 + range-parser@1.2.1: {} raw-body@2.5.2: @@ -27642,6 +28947,8 @@ snapshots: real-require@0.1.0: {} + real-require@0.2.0: {} + redaxios@0.5.1: {} reflect-metadata@0.2.2: {} @@ -27732,12 +29039,16 @@ snapshots: onetime: 5.1.2 signal-exit: 3.0.7 + ret@0.5.0: {} + retry@0.12.0: {} retry@0.13.1: {} reusify@1.1.0: {} + rfdc@1.4.1: {} + rimraf@2.6.3: dependencies: glob: 7.2.3 @@ -27805,7 +29116,7 @@ snapshots: buffer: 6.0.3 eventemitter3: 5.0.1 uuid: 8.3.2 - ws: 8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10) + ws: 8.19.0(bufferutil@4.0.9)(utf-8-validate@5.0.10) optionalDependencies: bufferutil: 4.0.9 utf-8-validate: 5.0.10 @@ -27843,6 +29154,10 @@ snapshots: es-errors: 1.3.0 is-regex: 1.2.1 + safe-regex2@5.1.0: + dependencies: + ret: 0.5.0 + safe-stable-stringify@2.5.0: {} safer-buffer@2.1.2: {} @@ -27871,6 +29186,8 @@ snapshots: node-addon-api: 5.1.0 node-gyp-build: 4.8.4 + secure-json-parse@4.1.0: {} + selderee@0.11.0: dependencies: parseley: 0.12.1 @@ -27945,6 +29262,8 @@ snapshots: set-blocking@2.0.0: {} + set-cookie-parser@2.7.2: {} + set-function-length@1.2.2: dependencies: define-data-property: 1.1.4 @@ -28055,7 +29374,7 @@ snapshots: simple-update-notifier@2.0.0: dependencies: - semver: 7.7.2 + semver: 7.7.3 simple-wcswidth@1.1.2: {} @@ -28118,6 +29437,10 @@ snapshots: dependencies: atomic-sleep: 1.0.0 + sonic-boom@4.2.1: + dependencies: + atomic-sleep: 1.0.0 + source-map-js@1.2.1: {} source-map-support@0.5.21: @@ -28408,6 +29731,10 @@ snapshots: dependencies: real-require: 0.1.0 + thread-stream@4.0.0: + dependencies: + real-require: 0.2.0 + tiny-async-pool@1.3.0: dependencies: semver: 5.7.2 @@ -28454,8 +29781,12 @@ snapshots: dependencies: is-number: 7.0.0 + toad-cache@3.7.0: {} + toidentifier@1.0.1: {} + toml@3.0.0: {} + tough-cookie@5.1.2: dependencies: tldts: 6.1.86 @@ -28771,6 +30102,8 @@ snapshots: dependencies: punycode: 2.3.1 + urijs@1.19.11: {} + url-parse@1.5.10: dependencies: querystringify: 2.2.0 @@ -29292,10 +30625,10 @@ snapshots: - ws - zod - wagmi@2.19.5(@tanstack/query-core@5.90.11)(@tanstack/react-query@5.90.11(react@19.2.3))(@types/react@19.1.10)(@upstash/redis@1.35.3)(bufferutil@4.0.9)(encoding@0.1.13)(react@19.2.3)(typescript@5.9.2)(utf-8-validate@5.0.10)(viem@2.40.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13))(zod@4.1.13): + wagmi@2.19.5(@tanstack/query-core@5.90.11)(@tanstack/react-query@5.90.11(react@19.2.3))(@types/react@19.1.10)(@upstash/redis@1.35.3)(bufferutil@4.0.9)(encoding@0.1.13)(fastestsmallesttextencoderdecoder@1.0.22)(react@19.2.3)(typescript@5.9.2)(utf-8-validate@5.0.10)(viem@2.40.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13))(ws@8.19.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@4.1.13): dependencies: '@tanstack/react-query': 5.90.11(react@19.2.3) - '@wagmi/connectors': 6.2.0(@tanstack/react-query@5.90.11(react@19.2.3))(@types/react@19.1.10)(@upstash/redis@1.35.3)(@wagmi/core@2.22.1(@tanstack/query-core@5.90.11)(@types/react@19.1.10)(react@19.2.3)(typescript@5.9.2)(use-sync-external-store@1.5.0(react@19.2.3))(viem@2.40.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13)))(bufferutil@4.0.9)(encoding@0.1.13)(react@19.2.3)(typescript@5.9.2)(use-sync-external-store@1.4.0(react@19.2.3))(utf-8-validate@5.0.10)(viem@2.40.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13))(wagmi@2.19.5(@tanstack/query-core@5.90.11)(@tanstack/react-query@5.90.11(react@19.2.3))(@types/react@19.1.10)(@upstash/redis@1.35.3)(bufferutil@4.0.9)(encoding@0.1.13)(react@19.2.3)(typescript@5.9.2)(utf-8-validate@5.0.10)(viem@2.40.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13))(zod@4.1.13))(zod@4.1.13) + '@wagmi/connectors': 6.2.0(26b6de6e2b894d3d8ec3de3fd9d7a34e) '@wagmi/core': 2.22.1(@tanstack/query-core@5.90.11)(@types/react@19.1.10)(react@19.2.3)(typescript@5.9.2)(use-sync-external-store@1.4.0(react@19.2.3))(viem@2.40.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13)) react: 19.2.3 use-sync-external-store: 1.4.0(react@19.2.3) @@ -29337,10 +30670,10 @@ snapshots: - ws - zod - wagmi@2.19.5(@tanstack/query-core@5.90.11)(@tanstack/react-query@5.90.11(react@19.2.3))(@types/react@19.1.10)(@upstash/redis@1.35.3)(bufferutil@4.0.9)(encoding@0.1.13)(react@19.2.3)(typescript@5.9.2)(utf-8-validate@5.0.10)(viem@2.43.5(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13))(zod@4.1.13): + wagmi@2.19.5(@tanstack/query-core@5.90.11)(@tanstack/react-query@5.90.11(react@19.2.3))(@types/react@19.1.10)(@upstash/redis@1.35.3)(bufferutil@4.0.9)(encoding@0.1.13)(fastestsmallesttextencoderdecoder@1.0.22)(react@19.2.3)(typescript@5.9.2)(utf-8-validate@5.0.10)(viem@2.43.5(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13))(ws@8.19.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@4.1.13): dependencies: '@tanstack/react-query': 5.90.11(react@19.2.3) - '@wagmi/connectors': 6.2.0(@tanstack/react-query@5.90.11(react@19.2.3))(@types/react@19.1.10)(@upstash/redis@1.35.3)(@wagmi/core@2.22.1(@tanstack/query-core@5.90.11)(@types/react@19.1.10)(react@19.2.3)(typescript@5.9.2)(use-sync-external-store@1.5.0(react@19.2.3))(viem@2.40.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13)))(bufferutil@4.0.9)(encoding@0.1.13)(react@19.2.3)(typescript@5.9.2)(use-sync-external-store@1.4.0(react@19.2.3))(utf-8-validate@5.0.10)(viem@2.43.5(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13))(wagmi@2.19.5(@tanstack/query-core@5.90.11)(@tanstack/react-query@5.90.11(react@19.2.3))(@types/react@19.1.10)(@upstash/redis@1.35.3)(bufferutil@4.0.9)(encoding@0.1.13)(react@19.2.3)(typescript@5.9.2)(utf-8-validate@5.0.10)(viem@2.40.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13))(zod@4.1.13))(zod@4.1.13) + '@wagmi/connectors': 6.2.0(87f53a8d2b85f9075dbb2195dfb00723) '@wagmi/core': 2.22.1(@tanstack/query-core@5.90.11)(@types/react@19.1.10)(react@19.2.3)(typescript@5.9.2)(use-sync-external-store@1.4.0(react@19.2.3))(viem@2.43.5(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13)) react: 19.2.3 use-sync-external-store: 1.4.0(react@19.2.3) @@ -29382,10 +30715,10 @@ snapshots: - ws - zod - wagmi@2.19.5(@tanstack/query-core@5.90.11)(@tanstack/react-query@5.90.11(react@19.2.3))(@types/react@19.1.10)(bufferutil@4.0.9)(encoding@0.1.13)(react@19.2.3)(typescript@5.9.2)(utf-8-validate@5.0.10)(viem@2.40.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13))(zod@4.1.13): + wagmi@2.19.5(@tanstack/query-core@5.90.11)(@tanstack/react-query@5.90.11(react@19.2.3))(@types/react@19.1.10)(bufferutil@4.0.9)(encoding@0.1.13)(fastestsmallesttextencoderdecoder@1.0.22)(react@19.2.3)(typescript@5.9.2)(utf-8-validate@5.0.10)(viem@2.40.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13))(ws@8.19.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@4.1.13): dependencies: '@tanstack/react-query': 5.90.11(react@19.2.3) - '@wagmi/connectors': 6.2.0(@tanstack/react-query@5.90.11(react@19.2.3))(@types/react@19.1.10)(@wagmi/core@2.22.1(@tanstack/query-core@5.90.11)(@types/react@19.1.10)(react@19.2.3)(typescript@5.9.2)(viem@2.40.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13)))(bufferutil@4.0.9)(encoding@0.1.13)(react@19.2.3)(typescript@5.9.2)(use-sync-external-store@1.4.0(react@19.2.3))(utf-8-validate@5.0.10)(viem@2.40.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13))(wagmi@2.19.5(@tanstack/query-core@5.90.11)(@tanstack/react-query@5.90.11(react@19.2.3))(@types/react@19.1.10)(bufferutil@4.0.9)(encoding@0.1.13)(react@19.2.3)(typescript@5.9.2)(utf-8-validate@5.0.10)(viem@2.40.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13))(zod@4.1.13))(zod@4.1.13) + '@wagmi/connectors': 6.2.0(@tanstack/react-query@5.90.11(react@19.2.3))(@types/react@19.1.10)(@wagmi/core@2.22.1(@tanstack/query-core@5.90.11)(@types/react@19.1.10)(react@19.2.3)(typescript@5.9.2)(viem@2.40.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13)))(bufferutil@4.0.9)(encoding@0.1.13)(fastestsmallesttextencoderdecoder@1.0.22)(react@19.2.3)(typescript@5.9.2)(use-sync-external-store@1.4.0(react@19.2.3))(utf-8-validate@5.0.10)(viem@2.40.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13))(wagmi@2.19.5(@tanstack/query-core@5.90.11)(@tanstack/react-query@5.90.11(react@19.2.3))(@types/react@19.1.10)(bufferutil@4.0.9)(encoding@0.1.13)(fastestsmallesttextencoderdecoder@1.0.22)(react@19.2.3)(typescript@5.9.2)(utf-8-validate@5.0.10)(viem@2.40.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13))(ws@8.19.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@4.1.13))(ws@8.19.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@4.1.13) '@wagmi/core': 2.22.1(@tanstack/query-core@5.90.11)(@types/react@19.1.10)(react@19.2.3)(typescript@5.9.2)(use-sync-external-store@1.4.0(react@19.2.3))(viem@2.40.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13)) react: 19.2.3 use-sync-external-store: 1.4.0(react@19.2.3) @@ -29427,10 +30760,10 @@ snapshots: - ws - zod - wagmi@2.19.5(@tanstack/query-core@5.90.11)(@tanstack/react-query@5.90.11(react@19.2.3))(@types/react@19.1.10)(bufferutil@4.0.9)(encoding@0.1.13)(react@19.2.3)(typescript@5.9.2)(utf-8-validate@5.0.10)(viem@2.43.5(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13))(zod@4.1.13): + wagmi@2.19.5(@tanstack/query-core@5.90.11)(@tanstack/react-query@5.90.11(react@19.2.3))(@types/react@19.1.10)(bufferutil@4.0.9)(encoding@0.1.13)(fastestsmallesttextencoderdecoder@1.0.22)(react@19.2.3)(typescript@5.9.2)(utf-8-validate@5.0.10)(viem@2.43.5(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13))(ws@8.19.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@4.1.13): dependencies: '@tanstack/react-query': 5.90.11(react@19.2.3) - '@wagmi/connectors': 6.2.0(@tanstack/react-query@5.90.11(react@19.2.3))(@types/react@19.1.10)(@wagmi/core@2.22.1(@tanstack/query-core@5.90.11)(@types/react@19.1.10)(react@19.2.3)(typescript@5.9.2)(viem@2.40.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13)))(bufferutil@4.0.9)(encoding@0.1.13)(react@19.2.3)(typescript@5.9.2)(use-sync-external-store@1.4.0(react@19.2.3))(utf-8-validate@5.0.10)(viem@2.43.5(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13))(wagmi@2.19.5(@tanstack/query-core@5.90.11)(@tanstack/react-query@5.90.11(react@19.2.3))(@types/react@19.1.10)(bufferutil@4.0.9)(encoding@0.1.13)(react@19.2.3)(typescript@5.9.2)(utf-8-validate@5.0.10)(viem@2.40.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13))(zod@4.1.13))(zod@4.1.13) + '@wagmi/connectors': 6.2.0(@tanstack/react-query@5.90.11(react@19.2.3))(@types/react@19.1.10)(@wagmi/core@2.22.1(@tanstack/query-core@5.90.11)(@types/react@19.1.10)(react@19.2.3)(typescript@5.9.2)(viem@2.40.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13)))(bufferutil@4.0.9)(encoding@0.1.13)(fastestsmallesttextencoderdecoder@1.0.22)(react@19.2.3)(typescript@5.9.2)(use-sync-external-store@1.4.0(react@19.2.3))(utf-8-validate@5.0.10)(viem@2.43.5(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13))(wagmi@2.19.5(@tanstack/query-core@5.90.11)(@tanstack/react-query@5.90.11(react@19.2.3))(@types/react@19.1.10)(bufferutil@4.0.9)(encoding@0.1.13)(fastestsmallesttextencoderdecoder@1.0.22)(react@19.2.3)(typescript@5.9.2)(utf-8-validate@5.0.10)(viem@2.40.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13))(ws@8.19.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@4.1.13))(ws@8.19.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@4.1.13) '@wagmi/core': 2.22.1(@tanstack/query-core@5.90.11)(@types/react@19.1.10)(react@19.2.3)(typescript@5.9.2)(use-sync-external-store@1.4.0(react@19.2.3))(viem@2.43.5(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13)) react: 19.2.3 use-sync-external-store: 1.4.0(react@19.2.3) @@ -29472,10 +30805,10 @@ snapshots: - ws - zod - wagmi@2.19.5(@tanstack/query-core@5.90.11)(@tanstack/react-query@5.90.11(react@19.2.3))(@types/react@19.2.1)(bufferutil@4.0.9)(encoding@0.1.13)(react@19.2.3)(typescript@5.9.2)(utf-8-validate@5.0.10)(viem@2.40.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13))(zod@4.1.13): + wagmi@2.19.5(@tanstack/query-core@5.90.11)(@tanstack/react-query@5.90.11(react@19.2.3))(@types/react@19.2.1)(bufferutil@4.0.9)(encoding@0.1.13)(fastestsmallesttextencoderdecoder@1.0.22)(react@19.2.3)(typescript@5.9.2)(utf-8-validate@5.0.10)(viem@2.40.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13))(ws@8.19.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@4.1.13): dependencies: '@tanstack/react-query': 5.90.11(react@19.2.3) - '@wagmi/connectors': 6.2.0(@tanstack/react-query@5.90.11(react@19.2.3))(@types/react@19.2.1)(@wagmi/core@2.22.1(@tanstack/query-core@5.90.11)(@types/react@19.2.1)(react@19.2.3)(typescript@5.9.2)(use-sync-external-store@1.4.0(react@19.2.3))(viem@2.40.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13)))(bufferutil@4.0.9)(encoding@0.1.13)(react@19.2.3)(typescript@5.9.2)(use-sync-external-store@1.4.0(react@19.2.3))(utf-8-validate@5.0.10)(viem@2.40.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13))(wagmi@2.19.5(@tanstack/query-core@5.90.11)(@tanstack/react-query@5.90.11(react@19.2.3))(@types/react@19.2.1)(bufferutil@4.0.9)(encoding@0.1.13)(react@19.2.3)(typescript@5.9.2)(utf-8-validate@5.0.10)(viem@2.40.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13))(zod@4.1.13))(zod@4.1.13) + '@wagmi/connectors': 6.2.0(1aee367860625464d156864b651beb8c) '@wagmi/core': 2.22.1(@tanstack/query-core@5.90.11)(@types/react@19.2.1)(react@19.2.3)(typescript@5.9.2)(use-sync-external-store@1.4.0(react@19.2.3))(viem@2.40.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.13)) react: 19.2.3 use-sync-external-store: 1.4.0(react@19.2.3) diff --git a/examples/typescript/pnpm-workspace.yaml b/examples/typescript/pnpm-workspace.yaml index ef34f7b6d5..c98a039f40 100644 --- a/examples/typescript/pnpm-workspace.yaml +++ b/examples/typescript/pnpm-workspace.yaml @@ -2,7 +2,7 @@ packages: - "clients/*" - "servers/*" - "fullstack/*" - - "facilitator" + - "facilitator/*" - "legacy/facilitator" - "legacy/servers/*" - "legacy/clients/*" diff --git a/examples/typescript/servers/advanced/.env-local b/examples/typescript/servers/advanced/.env-local index 9ec8cdc305..e16c5a2761 100644 --- a/examples/typescript/servers/advanced/.env-local +++ b/examples/typescript/servers/advanced/.env-local @@ -1,2 +1,4 @@ EVM_ADDRESS= +SVM_ADDRESS= +STELLAR_ADDRESS= FACILITATOR_URL= \ No newline at end of file diff --git a/examples/typescript/servers/advanced/README.md b/examples/typescript/servers/advanced/README.md index 8372d128a8..17d98b2314 100644 --- a/examples/typescript/servers/advanced/README.md +++ b/examples/typescript/servers/advanced/README.md @@ -5,10 +5,12 @@ Express.js server demonstrating advanced x402 patterns including dynamic pricing ```typescript import { paymentMiddleware, x402ResourceServer } from "@x402/express"; import { ExactEvmScheme } from "@x402/evm/exact/server"; +import { ExactStellarScheme } from "@x402/stellar/exact/server"; import { HTTPFacilitatorClient } from "@x402/core/server"; const resourceServer = new x402ResourceServer(new HTTPFacilitatorClient({ url: facilitatorUrl })) .register("eip155:84532", new ExactEvmScheme()) + .register("stellar:*", new ExactStellarScheme()) .onBeforeVerify(async ctx => console.log("Verifying payment...")) .onAfterSettle(async ctx => console.log("Settled:", ctx.result.transaction)); @@ -23,6 +25,14 @@ app.use( payTo: evmAddress, }, }, + "GET /weather-stellar": { + accepts: { + scheme: "exact", + price: ctx => (ctx.adapter.getQueryParam?.("tier") === "premium" ? "$0.01" : "$0.001"), + network: "stellar:*", + payTo: stellarAddress, + }, + }, }, resourceServer, ), @@ -48,6 +58,7 @@ and fill required environment variables: - `FACILITATOR_URL` - Facilitator endpoint URL - `EVM_ADDRESS` - Ethereum address to receive payments +- `STELLAR_ADDRESS` - Stellar public address (starts with `G`) to receive payments 2. Install and build all packages from the typescript examples root: @@ -63,6 +74,16 @@ cd servers/advanced pnpm dev ``` +### Account Setup Instructions + +#### Stellar Testnet + +Stellar accounts need to be created and funded with both XLM and USDC. Instructions: + +1. Go to [Stellar Laboratory](https://lab.stellar.org/account/create) ➡️ Generate keypair ➡️ Fund account with Friendbot, then copy the `Secret` and `Public` keys so you can use them. +2. Add USDC trustline (required to transact USDC): go to [Fund Account](https://lab.stellar.org/account/fund) ➡️ Paste your `Public Key` ➡️ Add USDC Trustline ➡️ paste your `Secret key` ➡️ Sign transaction ➡️ Add Trustline. +3. Get testnet USDC from [Circle Faucet](https://faucet.circle.com/) (select Stellar network). + ## Available Examples Each example demonstrates a specific advanced pattern: diff --git a/examples/typescript/servers/advanced/all_networks.ts b/examples/typescript/servers/advanced/all_networks.ts index e3b474e236..8d7f639642 100644 --- a/examples/typescript/servers/advanced/all_networks.ts +++ b/examples/typescript/servers/advanced/all_networks.ts @@ -5,7 +5,7 @@ * optional chain configuration via environment variables. * * New chain support should be added here in alphabetic order by network prefix - * (e.g., "eip155" before "solana"). + * (e.g., "eip155" before "solana" before "stellar"). */ import { config } from "dotenv"; @@ -13,6 +13,7 @@ import express from "express"; import { paymentMiddleware, x402ResourceServer } from "@x402/express"; import { ExactEvmScheme } from "@x402/evm/exact/server"; import { ExactSvmScheme } from "@x402/svm/exact/server"; +import { ExactStellarScheme } from "@x402/stellar/exact/server"; import { HTTPFacilitatorClient } from "@x402/core/server"; config(); @@ -20,10 +21,11 @@ config(); // Configuration - optional per network const evmAddress = process.env.EVM_ADDRESS as `0x${string}` | undefined; const svmAddress = process.env.SVM_ADDRESS as string | undefined; +const stellarAddress = process.env.STELLAR_ADDRESS as string | undefined; // Validate at least one address is provided -if (!evmAddress && !svmAddress) { - console.error("❌ At least one of EVM_ADDRESS or SVM_ADDRESS is required"); +if (!evmAddress && !svmAddress && !stellarAddress) { + console.error("❌ At least one of EVM_ADDRESS, SVM_ADDRESS, or STELLAR_ADDRESS is required"); process.exit(1); } @@ -36,6 +38,7 @@ if (!facilitatorUrl) { // Network configuration const EVM_NETWORK = "eip155:84532" as const; // Base Sepolia const SVM_NETWORK = "solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1" as const; // Solana Devnet +const STELLAR_NETWORK = "stellar:testnet" as const; // Stellar Testnet // Build accepts array dynamically based on configured addresses const accepts: Array<{ @@ -60,6 +63,14 @@ if (svmAddress) { payTo: svmAddress, }); } +if (stellarAddress) { + accepts.push({ + scheme: "exact", + price: "$0.001", + network: STELLAR_NETWORK, + payTo: stellarAddress, + }); +} // Create facilitator client const facilitatorClient = new HTTPFacilitatorClient({ url: facilitatorUrl }); @@ -72,6 +83,9 @@ if (evmAddress) { if (svmAddress) { server.register(SVM_NETWORK, new ExactSvmScheme()); } +if (stellarAddress) { + server.register(STELLAR_NETWORK, new ExactStellarScheme()); +} // Create Express app const app = express(); @@ -115,6 +129,9 @@ app.listen(port, () => { if (svmAddress) { console.log(` SVM: ${svmAddress} on ${SVM_NETWORK}`); } + if (stellarAddress) { + console.log(` Stellar: ${stellarAddress} on ${STELLAR_NETWORK}`); + } console.log(` Facilitator: ${facilitatorUrl}`); console.log(); }); diff --git a/examples/typescript/servers/advanced/package.json b/examples/typescript/servers/advanced/package.json index 140935e5c6..a7fa9475ae 100644 --- a/examples/typescript/servers/advanced/package.json +++ b/examples/typescript/servers/advanced/package.json @@ -23,6 +23,7 @@ "@x402/express": "workspace:*", "@x402/evm": "workspace:*", "@x402/svm": "workspace:*", + "@x402/stellar": "workspace:*", "@x402/extensions": "workspace:*" }, "devDependencies": { diff --git a/examples/typescript/servers/bazaar/.env-local b/examples/typescript/servers/bazaar/.env-local new file mode 100644 index 0000000000..4ddfc506b9 --- /dev/null +++ b/examples/typescript/servers/bazaar/.env-local @@ -0,0 +1,3 @@ +EVM_ADDRESS= +SVM_ADDRESS= +FACILITATOR_URL=https://x402.org/facilitator \ No newline at end of file diff --git a/examples/typescript/servers/bazaar/.prettierignore b/examples/typescript/servers/bazaar/.prettierignore new file mode 100644 index 0000000000..5bd240ba90 --- /dev/null +++ b/examples/typescript/servers/bazaar/.prettierignore @@ -0,0 +1,8 @@ +docs/ +dist/ +node_modules/ +coverage/ +.github/ +src/client +**/**/*.json +*.md \ No newline at end of file diff --git a/examples/typescript/servers/bazaar/.prettierrc b/examples/typescript/servers/bazaar/.prettierrc new file mode 100644 index 0000000000..ffb416b74b --- /dev/null +++ b/examples/typescript/servers/bazaar/.prettierrc @@ -0,0 +1,11 @@ +{ + "tabWidth": 2, + "useTabs": false, + "semi": true, + "singleQuote": false, + "trailingComma": "all", + "bracketSpacing": true, + "arrowParens": "avoid", + "printWidth": 100, + "proseWrap": "never" +} diff --git a/examples/typescript/servers/bazaar/README.md b/examples/typescript/servers/bazaar/README.md new file mode 100644 index 0000000000..f413de661b --- /dev/null +++ b/examples/typescript/servers/bazaar/README.md @@ -0,0 +1,137 @@ +# Bazaar Discovery Example Server + +Express.js server demonstrating how to make a paid API **discoverable** using the Bazaar extension with dynamic route parameters. + +The key addition over a basic x402 server is `declareDiscoveryExtension` — it describes your endpoint's inputs, outputs, and path parameters so that facilitators (and agents) can automatically catalog and invoke your API. + +## What This Example Shows + +**Dynamic route parameters** — the route `GET /weather/:city` uses a `:city` slug. The x402 middleware automatically: + +1. Matches `/weather/san-francisco`, `/weather/tokyo`, etc. against the route pattern +2. Extracts `{ city: "san-francisco" }` as `pathParams` in the discovery extension +3. Produces `routeTemplate: "/weather/:city"` so all concrete URLs consolidate into **one** catalog entry + +```typescript +import { declareDiscoveryExtension } from "@x402/extensions/bazaar"; + +app.use( + paymentMiddleware( + { + "GET /weather/:city": { + accepts: { scheme: "exact", price: "$0.001", network: "eip155:84532", payTo: evmAddress }, + description: "Weather data for a city", + mimeType: "application/json", + extensions: { + ...declareDiscoveryExtension({ + pathParamsSchema: { + properties: { city: { type: "string", description: "City name slug" } }, + required: ["city"], + }, + output: { + example: { city: "san-francisco", weather: "foggy", temperature: 60 }, + }, + }), + }, + }, + }, + resourceServer, + ), +); + +app.get("/weather/:city", (req, res) => { + const city = req.params.city; + // ... return weather for city +}); +``` + +## Prerequisites + +- Node.js v20+ (install via [nvm](https://github.com/nvm-sh/nvm)) +- pnpm v10 (install via [pnpm.io/installation](https://pnpm.io/installation)) +- Valid EVM and SVM addresses for receiving payments +- URL of a facilitator supporting the desired payment network, see [facilitator list](https://www.x402.org/ecosystem?category=facilitators) + +## Setup + +1. Copy `.env-local` to `.env`: + +```bash +cp .env-local .env +``` + +and fill required environment variables: + +- `FACILITATOR_URL` - Facilitator endpoint URL +- `EVM_ADDRESS` - Ethereum address to receive payments +- `SVM_ADDRESS` - Solana address to receive payments + +2. Install and build all packages from the typescript examples root: +```bash +cd ../../ +pnpm install && pnpm build +cd servers/bazaar +``` + +3. Run the server +```bash +pnpm dev +``` + +## How Discovery Works + +When a client hits `GET /weather/san-francisco` without a payment, the 402 response includes the enriched bazaar extension: + +```json +{ + "x402Version": 2, + "error": "Payment required", + "resource": { "url": "http://localhost:4021/weather/san-francisco" }, + "extensions": { + "bazaar": { + "routeTemplate": "/weather/:city", + "info": { + "input": { + "type": "http", + "method": "GET", + "pathParams": { "city": "san-francisco" } + }, + "output": { + "type": "json", + "example": { "city": "san-francisco", "weather": "foggy", "temperature": 60 } + } + }, + "schema": { "..." : "..." } + } + }, + "accepts": [{ "..." : "..." }] +} +``` + +The facilitator uses `routeTemplate` as the canonical catalog key, so requests to `/weather/san-francisco`, `/weather/tokyo`, and `/weather/new-york` all map to a single discoverable endpoint: `/weather/:city`. + +## Multiple Path Parameters + +Routes can have multiple `:param` segments. Param names are matched by **position in the URL**, not by the order they appear in `pathParamsSchema`: + +``` +GET /weather/:country/:city + ^ ^ + | └── second URL segment -> "city" + └──────────── first URL segment -> "country" +``` + +A request to `/weather/us/san-francisco` produces `pathParams: { country: "us", city: "san-francisco" }`. The property order in `pathParamsSchema` does not affect matching -- only the segment position in the URL matters. + +## `declareDiscoveryExtension` API + +The function accepts a config object describing your endpoint: + +| Field | Purpose | +|-------|---------| +| `input` | Example query parameter values (for GET/HEAD/DELETE) | +| `inputSchema` | JSON Schema for query parameters | +| `pathParamsSchema` | JSON Schema for URL path parameters (`:param` segments) | +| `output.example` | Example response body (helps agents understand what they'll get) | +| `output.schema` | JSON Schema for the response body | +| `bodyType` | For POST/PUT/PATCH: `"json"`, `"form-data"`, or `"text"` | diff --git a/examples/typescript/servers/bazaar/eslint.config.js b/examples/typescript/servers/bazaar/eslint.config.js new file mode 100644 index 0000000000..e2fde7b3b8 --- /dev/null +++ b/examples/typescript/servers/bazaar/eslint.config.js @@ -0,0 +1,73 @@ +import js from "@eslint/js"; +import ts from "@typescript-eslint/eslint-plugin"; +import tsParser from "@typescript-eslint/parser"; +import prettier from "eslint-plugin-prettier"; +import jsdoc from "eslint-plugin-jsdoc"; +import importPlugin from "eslint-plugin-import"; + +export default [ + { + ignores: ["dist/**", "node_modules/**"], + }, + { + files: ["**/*.ts"], + languageOptions: { + parser: tsParser, + sourceType: "module", + ecmaVersion: 2020, + globals: { + process: "readonly", + __dirname: "readonly", + module: "readonly", + require: "readonly", + Buffer: "readonly", + console: "readonly", + exports: "readonly", + setTimeout: "readonly", + clearTimeout: "readonly", + setInterval: "readonly", + clearInterval: "readonly", + }, + }, + plugins: { + "@typescript-eslint": ts, + prettier: prettier, + jsdoc: jsdoc, + import: importPlugin, + }, + rules: { + ...ts.configs.recommended.rules, + "import/first": "error", + "prettier/prettier": "error", + "@typescript-eslint/member-ordering": "error", + "@typescript-eslint/no-unused-vars": ["error", { argsIgnorePattern: "^_$" }], + "jsdoc/tag-lines": ["error", "any", { startLines: 1 }], + "jsdoc/check-alignment": "error", + "jsdoc/no-undefined-types": "off", + "jsdoc/check-param-names": "error", + "jsdoc/check-tag-names": "error", + "jsdoc/check-types": "error", + "jsdoc/implements-on-classes": "error", + "jsdoc/require-description": "error", + "jsdoc/require-jsdoc": [ + "error", + { + require: { + FunctionDeclaration: true, + MethodDefinition: true, + ClassDeclaration: true, + ArrowFunctionExpression: false, + FunctionExpression: false, + }, + }, + ], + "jsdoc/require-param": "error", + "jsdoc/require-param-description": "error", + "jsdoc/require-param-type": "off", + "jsdoc/require-returns": "error", + "jsdoc/require-returns-description": "error", + "jsdoc/require-returns-type": "off", + "jsdoc/require-hyphen-before-param-description": ["error", "always"], + }, + }, +]; diff --git a/examples/typescript/servers/bazaar/index.ts b/examples/typescript/servers/bazaar/index.ts new file mode 100644 index 0000000000..35c208b870 --- /dev/null +++ b/examples/typescript/servers/bazaar/index.ts @@ -0,0 +1,124 @@ +import { config } from "dotenv"; +import express from "express"; +import { paymentMiddleware, x402ResourceServer } from "@x402/express"; +import { ExactEvmScheme } from "@x402/evm/exact/server"; +import { ExactSvmScheme } from "@x402/svm/exact/server"; +import { HTTPFacilitatorClient } from "@x402/core/server"; +import { declareDiscoveryExtension } from "@x402/extensions/bazaar"; +config(); + +const evmAddress = process.env.EVM_ADDRESS as `0x${string}`; +const svmAddress = process.env.SVM_ADDRESS; +if (!evmAddress || !svmAddress) { + console.error("Missing required environment variables"); + process.exit(1); +} + +const facilitatorUrl = process.env.FACILITATOR_URL; +if (!facilitatorUrl) { + console.error("FACILITATOR_URL environment variable is required"); + process.exit(1); +} +const facilitatorClient = new HTTPFacilitatorClient({ url: facilitatorUrl }); + +const app = express(); + +const paymentOptions = [ + { + scheme: "exact" as const, + price: "$0.001", + network: "eip155:84532", + payTo: evmAddress, + }, + { + scheme: "exact" as const, + price: "$0.001", + network: "solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1", + payTo: svmAddress, + }, +]; + +app.use( + paymentMiddleware( + { + // Single path param: /weather/:city + "GET /weather/:city": { + accepts: paymentOptions, + description: "Weather data for a city", + mimeType: "application/json", + extensions: { + ...declareDiscoveryExtension({ + pathParamsSchema: { + properties: { city: { type: "string", description: "City name slug" } }, + required: ["city"], + }, + output: { + example: { city: "san-francisco", weather: "foggy", temperature: 60 }, + }, + }), + }, + }, + + // Multiple path params: /weather/:country/:city + // Param names are matched by position in the URL, not by declaration order in the schema. + // /weather/us/san-francisco -> { country: "us", city: "san-francisco" } + "GET /weather/:country/:city": { + accepts: paymentOptions, + description: "Weather data for a city in a specific country", + mimeType: "application/json", + extensions: { + ...declareDiscoveryExtension({ + pathParamsSchema: { + properties: { + country: { type: "string", description: "Country code" }, + city: { type: "string", description: "City name slug" }, + }, + required: ["country", "city"], + }, + output: { + example: { country: "us", city: "san-francisco", weather: "foggy", temperature: 60 }, + }, + }), + }, + }, + }, + new x402ResourceServer(facilitatorClient) + .register("eip155:84532", new ExactEvmScheme()) + .register("solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1", new ExactSvmScheme()), + ), +); + +app.get("/weather/:city", (req, res) => { + const { city } = req.params; + + const weatherData: Record = { + "san-francisco": { weather: "foggy", temperature: 60 }, + "new-york": { weather: "cloudy", temperature: 55 }, + tokyo: { weather: "rainy", temperature: 65 }, + }; + + const data = weatherData[city] || { weather: "sunny", temperature: 70 }; + res.send({ city, weather: data.weather, temperature: data.temperature }); +}); + +app.get("/weather/:country/:city", (req, res) => { + const { country, city } = req.params; + + const weatherData: Record> = { + us: { + "san-francisco": { weather: "foggy", temperature: 60 }, + "new-york": { weather: "cloudy", temperature: 55 }, + }, + jp: { + tokyo: { weather: "rainy", temperature: 65 }, + osaka: { weather: "clear", temperature: 72 }, + }, + }; + + const data = weatherData[country]?.[city] || { weather: "sunny", temperature: 70 }; + res.send({ country, city, weather: data.weather, temperature: data.temperature }); +}); + +app.listen(4021, () => { + console.log(`Server listening at http://localhost:${4021}`); +}); diff --git a/examples/typescript/servers/bazaar/package.json b/examples/typescript/servers/bazaar/package.json new file mode 100644 index 0000000000..7857c989ab --- /dev/null +++ b/examples/typescript/servers/bazaar/package.json @@ -0,0 +1,35 @@ +{ + "name": "@x402/bazaar-server-example", + "private": true, + "type": "module", + "scripts": { + "dev": "tsx index.ts", + "format": "prettier -c .prettierrc --write \"**/*.{ts,js,cjs,json,md}\"", + "format:check": "prettier -c .prettierrc --check \"**/*.{ts,js,cjs,json,md}\"", + "lint": "eslint . --ext .ts --fix", + "lint:check": "eslint . --ext .ts" + }, + "dependencies": { + "@x402/core": "workspace:*", + "@x402/evm": "workspace:*", + "@x402/express": "workspace:*", + "@x402/extensions": "workspace:*", + "@x402/svm": "workspace:*", + "dotenv": "^16.4.7", + "express": "^4.18.2" + }, + "devDependencies": { + "@eslint/js": "^9.24.0", + "@types/express": "^5.0.1", + "@typescript-eslint/eslint-plugin": "^8.29.1", + "@typescript-eslint/parser": "^8.29.1", + "eslint": "^9.24.0", + "eslint-plugin-import": "^2.31.0", + "eslint-plugin-jsdoc": "^50.6.9", + "eslint-plugin-prettier": "^5.2.6", + "prettier": "3.5.2", + "tsup": "^7.2.0", + "tsx": "^4.7.0", + "typescript": "^5.3.0" + } +} diff --git a/examples/typescript/servers/bazaar/tsconfig.json b/examples/typescript/servers/bazaar/tsconfig.json new file mode 100644 index 0000000000..99fe25e8e7 --- /dev/null +++ b/examples/typescript/servers/bazaar/tsconfig.json @@ -0,0 +1,22 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "ES2020", + "moduleResolution": "bundler", + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "skipLibCheck": true, + "strict": true, + "resolveJsonModule": true, + "baseUrl": ".", + "types": ["node"] + }, + "include": [ + "index.ts", + "bazaar.ts", + "custom-money-definition.ts", + "dynamic-pay-to.ts", + "dynamic-price.ts", + "hooks.ts" + ] +} diff --git a/examples/typescript/servers/express/package.json b/examples/typescript/servers/express/package.json index 690dd8ae8f..adcbe1f6a8 100644 --- a/examples/typescript/servers/express/package.json +++ b/examples/typescript/servers/express/package.json @@ -10,7 +10,6 @@ "lint:check": "eslint . --ext .ts" }, "dependencies": { - "@coinbase/x402": "^2.1.0", "@x402/core": "workspace:*", "@x402/evm": "workspace:*", "@x402/express": "workspace:*", diff --git a/examples/typescript/servers/fastify/.env-local b/examples/typescript/servers/fastify/.env-local new file mode 100644 index 0000000000..4ddfc506b9 --- /dev/null +++ b/examples/typescript/servers/fastify/.env-local @@ -0,0 +1,3 @@ +EVM_ADDRESS= +SVM_ADDRESS= +FACILITATOR_URL=https://x402.org/facilitator \ No newline at end of file diff --git a/examples/typescript/servers/fastify/.prettierignore b/examples/typescript/servers/fastify/.prettierignore new file mode 100644 index 0000000000..3049672b5c --- /dev/null +++ b/examples/typescript/servers/fastify/.prettierignore @@ -0,0 +1,8 @@ +docs/ +dist/ +node_modules/ +coverage/ +.github/ +src/client +**/**/*.json +*.md diff --git a/examples/typescript/servers/fastify/.prettierrc b/examples/typescript/servers/fastify/.prettierrc new file mode 100644 index 0000000000..ffb416b74b --- /dev/null +++ b/examples/typescript/servers/fastify/.prettierrc @@ -0,0 +1,11 @@ +{ + "tabWidth": 2, + "useTabs": false, + "semi": true, + "singleQuote": false, + "trailingComma": "all", + "bracketSpacing": true, + "arrowParens": "avoid", + "printWidth": 100, + "proseWrap": "never" +} diff --git a/examples/typescript/servers/fastify/README.md b/examples/typescript/servers/fastify/README.md new file mode 100644 index 0000000000..dcb087d304 --- /dev/null +++ b/examples/typescript/servers/fastify/README.md @@ -0,0 +1,252 @@ +# @x402/fastify Example Server + +Fastify server demonstrating how to protect API endpoints with a paywall using the `@x402/fastify` middleware. + +```typescript +import Fastify from "fastify"; +import { paymentMiddleware, x402ResourceServer } from "@x402/fastify"; +import { ExactEvmScheme } from "@x402/evm/exact/server"; +import { HTTPFacilitatorClient } from "@x402/core/server"; + +const app = Fastify(); + +paymentMiddleware( + app, + { + "GET /weather": { + accepts: { scheme: "exact", price: "$0.001", network: "eip155:84532", payTo: evmAddress }, + description: "Weather data", + mimeType: "application/json", + }, + }, + new x402ResourceServer(new HTTPFacilitatorClient({ url: facilitatorUrl })) + .register("eip155:84532", new ExactEvmScheme()), +); + +app.get("/weather", async () => ({ weather: "sunny", temperature: 70 })); +``` + +## Prerequisites + +- Node.js v20+ (install via [nvm](https://github.com/nvm-sh/nvm)) +- pnpm v10 (install via [pnpm.io/installation](https://pnpm.io/installation)) +- Valid EVM and SVM addresses for receiving payments +- URL of a facilitator supporting the desired payment network, see [facilitator list](https://www.x402.org/ecosystem?category=facilitators) + +## Setup + +1. Copy `.env-local` to `.env`: + +```bash +cp .env-local .env +``` + +and fill required environment variables: + +- `FACILITATOR_URL` - Facilitator endpoint URL +- `EVM_ADDRESS` - Ethereum address to receive payments +- `SVM_ADDRESS` - Solana address to receive payments + +2. Install and build all packages from the typescript examples root: + +```bash +cd ../../ +pnpm install && pnpm build +cd servers/fastify +``` + +3. Run the server + +```bash +pnpm dev +``` + +## Testing the Server + +You can test the server using one of the example clients: + +### Using the Fetch Client + +```bash +cd ../clients/fetch +# Ensure .env is setup +pnpm dev +``` + +### Using the Axios Client + +```bash +cd ../clients/axios +# Ensure .env is setup +pnpm dev +``` + +These clients will demonstrate how to: + +1. Make an initial request to get payment requirements +2. Process the payment requirements +3. Make a second request with the payment token + +## Example Endpoint + +The server includes a single example endpoint at `/weather` that requires a payment of 0.001 USDC on Base Sepolia or Solana Devnet to access. The endpoint returns a simple weather report. + +## Response Format + +### Payment Required (402) + +``` +HTTP/1.1 402 Payment Required +Content-Type: application/json; charset=utf-8 +PAYMENT-REQUIRED: + +{} +``` + +The `PAYMENT-REQUIRED` header contains base64-encoded JSON with the payment requirements. +Note: `amount` is in atomic units (e.g., 1000 = 0.001 USDC, since USDC has 6 decimals): + +```json +{ + "x402Version": 2, + "error": "Payment required", + "resource": { + "url": "http://localhost:4021/weather", + "description": "Weather data", + "mimeType": "application/json" + }, + "accepts": [ + { + "scheme": "exact", + "network": "eip155:84532", + "amount": "1000", + "asset": "0x036CbD53842c5426634e7929541eC2318f3dCF7e", + "payTo": "0x1c47E9C085c2B7458F5b6C16cCBD65A65255a9f6", + "maxTimeoutSeconds": 300, + "extra": { + "name": "USDC", + "version": "2", + "resourceUrl": "http://localhost:4021/weather" + } + }, + { + "scheme": "exact", + "network": "solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1", + "amount": "1000", + "asset": "4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU", + "payTo": "FV6JPj6Fy12HG8SYStyHdcecXYmV1oeWERAokrh4GQ1n", + "maxTimeoutSeconds": 300, + "extra": { + "feePayer": "...", + "resourceUrl": "http://localhost:4021/weather" + } + } + ] +} +``` + +### Successful Response + +``` +HTTP/1.1 200 OK +Content-Type: application/json; charset=utf-8 +PAYMENT-RESPONSE: + +{"report":{"weather":"sunny","temperature":70}} +``` + +The `PAYMENT-RESPONSE` header contains base64-encoded JSON with the settlement details: + +```json +{ + "success": true, + "transaction": "0x...", + "network": "eip155:84532", + "payer": "0x...", + "requirements": { + "scheme": "exact", + "network": "eip155:84532", + "amount": "1000", + "asset": "0x036CbD53842c5426634e7929541eC2318f3dCF7e", + "payTo": "0x...", + "maxTimeoutSeconds": 300, + "extra": { + "name": "USDC", + "version": "2", + "resourceUrl": "http://localhost:4021/weather" + } + } +} +``` + +## Extending the Example + +To add more paid endpoints, follow this pattern: + +```typescript +// First, configure the payment middleware with your routes +paymentMiddleware( + app, + { + "GET /your-endpoint": { + accepts: { + scheme: "exact", + price: "$0.10", + network: "eip155:84532", + payTo: evmAddress, + }, + description: "Your endpoint description", + mimeType: "application/json", + }, + }, + resourceServer, +); + +// Then define your routes as normal +app.get("/your-endpoint", async () => { + return { + // Your response data + }; +}); +``` + +**Network identifiers** use [CAIP-2](https://github.com/ChainAgnostic/CAIPs/blob/main/CAIPs/caip-2.md) format, for example: + +- `eip155:84532` — Base Sepolia +- `eip155:8453` — Base Mainnet +- `solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1` — Solana Devnet +- `solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp` — Solana Mainnet + +## x402ResourceServer Config + +The `x402ResourceServer` uses a builder pattern to register payment schemes that declare how payments for each network should be processed: + +```typescript +const resourceServer = new x402ResourceServer(facilitatorClient) + .register("eip155:*", new ExactEvmScheme()) // All EVM chains + .register("solana:*", new ExactSvmScheme()); // All SVM chains +``` + +## Facilitator Config + +The `HTTPFacilitatorClient` connects to a facilitator service that verifies and settles payments on-chain: + +```typescript +const facilitatorClient = new HTTPFacilitatorClient({ url: facilitatorUrl }); + +// Or use multiple facilitators for redundancy +const facilitatorClient = [ + new HTTPFacilitatorClient({ url: primaryFacilitatorUrl }), + new HTTPFacilitatorClient({ url: backupFacilitatorUrl }), +]; +``` + +## Next Steps + +See [Advanced Examples](../advanced/) for: + +- **Bazaar discovery** — make your API discoverable +- **Dynamic pricing** — price based on request context +- **Dynamic payTo** — route payments to different recipients +- **Lifecycle hooks** — custom logic on verify/settle +- **Custom tokens** — accept payments in custom tokens diff --git a/examples/typescript/servers/fastify/eslint.config.js b/examples/typescript/servers/fastify/eslint.config.js new file mode 100644 index 0000000000..e2fde7b3b8 --- /dev/null +++ b/examples/typescript/servers/fastify/eslint.config.js @@ -0,0 +1,73 @@ +import js from "@eslint/js"; +import ts from "@typescript-eslint/eslint-plugin"; +import tsParser from "@typescript-eslint/parser"; +import prettier from "eslint-plugin-prettier"; +import jsdoc from "eslint-plugin-jsdoc"; +import importPlugin from "eslint-plugin-import"; + +export default [ + { + ignores: ["dist/**", "node_modules/**"], + }, + { + files: ["**/*.ts"], + languageOptions: { + parser: tsParser, + sourceType: "module", + ecmaVersion: 2020, + globals: { + process: "readonly", + __dirname: "readonly", + module: "readonly", + require: "readonly", + Buffer: "readonly", + console: "readonly", + exports: "readonly", + setTimeout: "readonly", + clearTimeout: "readonly", + setInterval: "readonly", + clearInterval: "readonly", + }, + }, + plugins: { + "@typescript-eslint": ts, + prettier: prettier, + jsdoc: jsdoc, + import: importPlugin, + }, + rules: { + ...ts.configs.recommended.rules, + "import/first": "error", + "prettier/prettier": "error", + "@typescript-eslint/member-ordering": "error", + "@typescript-eslint/no-unused-vars": ["error", { argsIgnorePattern: "^_$" }], + "jsdoc/tag-lines": ["error", "any", { startLines: 1 }], + "jsdoc/check-alignment": "error", + "jsdoc/no-undefined-types": "off", + "jsdoc/check-param-names": "error", + "jsdoc/check-tag-names": "error", + "jsdoc/check-types": "error", + "jsdoc/implements-on-classes": "error", + "jsdoc/require-description": "error", + "jsdoc/require-jsdoc": [ + "error", + { + require: { + FunctionDeclaration: true, + MethodDefinition: true, + ClassDeclaration: true, + ArrowFunctionExpression: false, + FunctionExpression: false, + }, + }, + ], + "jsdoc/require-param": "error", + "jsdoc/require-param-description": "error", + "jsdoc/require-param-type": "off", + "jsdoc/require-returns": "error", + "jsdoc/require-returns-description": "error", + "jsdoc/require-returns-type": "off", + "jsdoc/require-hyphen-before-param-description": ["error", "always"], + }, + }, +]; diff --git a/examples/typescript/servers/fastify/index.ts b/examples/typescript/servers/fastify/index.ts new file mode 100644 index 0000000000..8f1bc079e4 --- /dev/null +++ b/examples/typescript/servers/fastify/index.ts @@ -0,0 +1,67 @@ +import { config } from "dotenv"; +import Fastify from "fastify"; +import { paymentMiddleware, x402ResourceServer } from "@x402/fastify"; +import { ExactEvmScheme } from "@x402/evm/exact/server"; +import { ExactSvmScheme } from "@x402/svm/exact/server"; +import { HTTPFacilitatorClient } from "@x402/core/server"; +config(); + +const evmAddress = process.env.EVM_ADDRESS as `0x${string}`; +const svmAddress = process.env.SVM_ADDRESS; +if (!evmAddress || !svmAddress) { + console.error("Missing required environment variables"); + process.exit(1); +} + +const facilitatorUrl = process.env.FACILITATOR_URL; +if (!facilitatorUrl) { + console.error("❌ FACILITATOR_URL environment variable is required"); + process.exit(1); +} +const facilitatorClient = new HTTPFacilitatorClient({ url: facilitatorUrl }); + +const app = Fastify(); + +paymentMiddleware( + app, + { + "GET /weather": { + accepts: [ + { + scheme: "exact", + price: "$0.001", + network: "eip155:84532", + payTo: evmAddress, + }, + { + scheme: "exact", + price: "$0.001", + network: "solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1", + payTo: svmAddress, + }, + ], + description: "Weather data", + mimeType: "application/json", + }, + }, + new x402ResourceServer(facilitatorClient) + .register("eip155:84532", new ExactEvmScheme()) + .register("solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1", new ExactSvmScheme()), +); + +app.get("/weather", async () => { + return { + report: { + weather: "sunny", + temperature: 70, + }, + }; +}); + +app.listen({ port: 4021 }, (err, address) => { + if (err) { + console.error(err); + process.exit(1); + } + console.log(`Server listening at ${address}`); +}); diff --git a/examples/typescript/servers/fastify/package.json b/examples/typescript/servers/fastify/package.json new file mode 100644 index 0000000000..e6474abe6a --- /dev/null +++ b/examples/typescript/servers/fastify/package.json @@ -0,0 +1,34 @@ +{ + "name": "@x402/fastify-server-example", + "private": true, + "type": "module", + "scripts": { + "dev": "tsx index.ts", + "format": "prettier -c .prettierrc --write \"**/*.{ts,js,cjs,json,md}\"", + "format:check": "prettier -c .prettierrc --check \"**/*.{ts,js,cjs,json,md}\"", + "lint": "eslint . --ext .ts --fix", + "lint:check": "eslint . --ext .ts" + }, + "dependencies": { + "@x402/core": "workspace:*", + "@x402/evm": "workspace:*", + "@x402/fastify": "workspace:*", + "@x402/extensions": "workspace:*", + "@x402/svm": "workspace:*", + "dotenv": "^16.4.7", + "fastify": "^5.0.0" + }, + "devDependencies": { + "@eslint/js": "^9.24.0", + "@typescript-eslint/eslint-plugin": "^8.29.1", + "@typescript-eslint/parser": "^8.29.1", + "eslint": "^9.24.0", + "eslint-plugin-import": "^2.31.0", + "eslint-plugin-jsdoc": "^50.6.9", + "eslint-plugin-prettier": "^5.2.6", + "prettier": "3.5.2", + "tsup": "^7.2.0", + "tsx": "^4.7.0", + "typescript": "^5.3.0" + } +} diff --git a/examples/typescript/servers/fastify/tsconfig.json b/examples/typescript/servers/fastify/tsconfig.json new file mode 100644 index 0000000000..78f9479b1b --- /dev/null +++ b/examples/typescript/servers/fastify/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "ES2020", + "moduleResolution": "bundler", + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "skipLibCheck": true, + "strict": true, + "resolveJsonModule": true, + "baseUrl": ".", + "types": ["node"] + }, + "include": ["index.ts"] +} diff --git a/examples/typescript/servers/offer-receipt/.env-local b/examples/typescript/servers/offer-receipt/.env-local new file mode 100644 index 0000000000..c4f6b7e1c6 --- /dev/null +++ b/examples/typescript/servers/offer-receipt/.env-local @@ -0,0 +1,27 @@ +# Facilitator URL for payment verification and settlement +FACILITATOR_URL=https://x402.org/facilitator + +# EVM address to receive payments +EVM_ADDRESS=0x1234567890123456789012345678901234567890 + +# Solana address to receive payments +SVM_ADDRESS=FV6JPj6Fy12HG8SYStyHdcecXYmV1oeWERAokrh4GQ1n + +# Signing format: "jws" (default) or "eip712" +# - JWS: Base64-encoded PKCS#8 private key (P-256, secp256k1, or Ed25519) +# - EIP-712: Hex-encoded secp256k1 private key (Ethereum's curve) +SIGNING_FORMAT=jws + +# Private key for signing offers/receipts (format depends on SIGNING_FORMAT above) +# +# For JWS with ES256 (P-256): +# openssl ecparam -genkey -name prime256v1 -noout | openssl pkcs8 -topk8 -nocrypt | base64 | tr -d '\n' +# +# For EIP-712 (secp256k1 hex): +# openssl ecparam -genkey -name secp256k1 -noout | openssl ec -text -noout 2>/dev/null | grep -A3 'priv:' | tail -3 | tr -d ' :\n' +SIGNING_PRIVATE_KEY= + +# Server domain for DID (only required for JWS format) +# Used to construct the key identifier: did:web:#key-1 +# For localhost with port, use percent-encoding: localhost%3A4021 +SERVER_DOMAIN=localhost%3A4021 diff --git a/examples/typescript/servers/offer-receipt/.prettierignore b/examples/typescript/servers/offer-receipt/.prettierignore new file mode 100644 index 0000000000..3049672b5c --- /dev/null +++ b/examples/typescript/servers/offer-receipt/.prettierignore @@ -0,0 +1,8 @@ +docs/ +dist/ +node_modules/ +coverage/ +.github/ +src/client +**/**/*.json +*.md diff --git a/examples/typescript/servers/offer-receipt/.prettierrc b/examples/typescript/servers/offer-receipt/.prettierrc new file mode 100644 index 0000000000..ffb416b74b --- /dev/null +++ b/examples/typescript/servers/offer-receipt/.prettierrc @@ -0,0 +1,11 @@ +{ + "tabWidth": 2, + "useTabs": false, + "semi": true, + "singleQuote": false, + "trailingComma": "all", + "bracketSpacing": true, + "arrowParens": "avoid", + "printWidth": 100, + "proseWrap": "never" +} diff --git a/examples/typescript/servers/offer-receipt/README.md b/examples/typescript/servers/offer-receipt/README.md new file mode 100644 index 0000000000..07d8b7036f --- /dev/null +++ b/examples/typescript/servers/offer-receipt/README.md @@ -0,0 +1,72 @@ +# Offer-Receipt Extension Server Example + +Express.js server demonstrating the offer-receipt extension for x402. This extension adds signed offers and receipts to payment flows, enabling: + +- **Signed offers** — cryptographic proof of payment terms from the server +- **Signed receipts** — proof of service delivery after payment + +## Signing Formats + +The server supports two signing formats: + +| Format | Key Type | Verification | +| ----------------- | -------------------------- | ----------------------------------- | +| **JWS** (default) | P-256, secp256k1, Ed25519 | Resolve `did:web` to get public key | +| **EIP-712** | secp256k1 only | Recover address from signature | + +## Setup + +1. Copy `.env-local` to `.env`: + +```bash +cp .env-local .env +``` + +2. Generate a signing key: + +**For JWS (ES256/P-256):** +```bash +openssl ecparam -genkey -name prime256v1 -noout | openssl pkcs8 -topk8 -nocrypt | base64 | tr -d '\n' +``` + +**For EIP-712 (secp256k1 hex):** +```bash +openssl ecparam -genkey -name secp256k1 -noout | openssl ec -text -noout 2>/dev/null | grep -A3 'priv:' | tail -3 | tr -d ' :\n' +``` + +3. Configure `.env`: + - `SIGNING_FORMAT` — `jws` or `eip712` + - `SIGNING_PRIVATE_KEY` — Key in the appropriate format + - `SERVER_DOMAIN` — Required for JWS (e.g., `localhost%3A4021`) + - `FACILITATOR_URL`, `EVM_ADDRESS`, `SVM_ADDRESS` + +4. Install and run: + +```bash +cd ../../ +pnpm install && pnpm build +cd servers/offer-receipt +pnpm dev +``` + +## DID Document (JWS only) + +For JWS signing, the server exposes `/.well-known/did.json` for signature verification: + +```bash +curl http://localhost:4021/.well-known/did.json +``` + +The library's `resolveDidWeb` automatically uses HTTP for `localhost` and `127.0.0.1`. + +## Configuration Options + +`declareOfferReceiptExtension()` accepts: + +- `includeTxHash` — Include transaction hash in receipt (default: `false` for privacy) +- `offerValiditySeconds` — How long offers remain valid (default: 300) + +## Related + +- [Extension Specification](../../../../typescript/packages/extensions/src/offer-receipt/README.md) +- [Offer/Receipt Client Example](../../clients/offer-receipt/) diff --git a/examples/typescript/servers/offer-receipt/eip712-signer.ts b/examples/typescript/servers/offer-receipt/eip712-signer.ts new file mode 100644 index 0000000000..f757ce9a4f --- /dev/null +++ b/examples/typescript/servers/offer-receipt/eip712-signer.ts @@ -0,0 +1,31 @@ +import { privateKeyToAccount } from "viem/accounts"; +import type { SignTypedDataFn } from "@x402/extensions/offer-receipt"; + +export interface EIP712SignerResult { + signTypedData: SignTypedDataFn; + address: `0x${string}`; +} + +/** + * Create an EIP-712 signer from a hex private key + * + * For production, use a proper key management solution (HSM, KMS, etc.) + * This is a simple implementation for demonstration purposes. + * + * @param privateKeyHex - Hex-encoded secp256k1 private key (with or without 0x prefix). + * EIP-712 only supports secp256k1 keys (Ethereum's curve). + * @returns EIP712SignerResult containing the signTypedData function and address + */ +export function createEIP712SignerFromPrivateKey(privateKeyHex: string): EIP712SignerResult { + // Ensure 0x prefix + const normalizedKey = privateKeyHex.startsWith("0x") + ? (privateKeyHex as `0x${string}`) + : (`0x${privateKeyHex}` as `0x${string}`); + + const account = privateKeyToAccount(normalizedKey); + + return { + signTypedData: account.signTypedData.bind(account) as SignTypedDataFn, + address: account.address, + }; +} diff --git a/examples/typescript/servers/offer-receipt/eslint.config.js b/examples/typescript/servers/offer-receipt/eslint.config.js new file mode 100644 index 0000000000..e2fde7b3b8 --- /dev/null +++ b/examples/typescript/servers/offer-receipt/eslint.config.js @@ -0,0 +1,73 @@ +import js from "@eslint/js"; +import ts from "@typescript-eslint/eslint-plugin"; +import tsParser from "@typescript-eslint/parser"; +import prettier from "eslint-plugin-prettier"; +import jsdoc from "eslint-plugin-jsdoc"; +import importPlugin from "eslint-plugin-import"; + +export default [ + { + ignores: ["dist/**", "node_modules/**"], + }, + { + files: ["**/*.ts"], + languageOptions: { + parser: tsParser, + sourceType: "module", + ecmaVersion: 2020, + globals: { + process: "readonly", + __dirname: "readonly", + module: "readonly", + require: "readonly", + Buffer: "readonly", + console: "readonly", + exports: "readonly", + setTimeout: "readonly", + clearTimeout: "readonly", + setInterval: "readonly", + clearInterval: "readonly", + }, + }, + plugins: { + "@typescript-eslint": ts, + prettier: prettier, + jsdoc: jsdoc, + import: importPlugin, + }, + rules: { + ...ts.configs.recommended.rules, + "import/first": "error", + "prettier/prettier": "error", + "@typescript-eslint/member-ordering": "error", + "@typescript-eslint/no-unused-vars": ["error", { argsIgnorePattern: "^_$" }], + "jsdoc/tag-lines": ["error", "any", { startLines: 1 }], + "jsdoc/check-alignment": "error", + "jsdoc/no-undefined-types": "off", + "jsdoc/check-param-names": "error", + "jsdoc/check-tag-names": "error", + "jsdoc/check-types": "error", + "jsdoc/implements-on-classes": "error", + "jsdoc/require-description": "error", + "jsdoc/require-jsdoc": [ + "error", + { + require: { + FunctionDeclaration: true, + MethodDefinition: true, + ClassDeclaration: true, + ArrowFunctionExpression: false, + FunctionExpression: false, + }, + }, + ], + "jsdoc/require-param": "error", + "jsdoc/require-param-description": "error", + "jsdoc/require-param-type": "off", + "jsdoc/require-returns": "error", + "jsdoc/require-returns-description": "error", + "jsdoc/require-returns-type": "off", + "jsdoc/require-hyphen-before-param-description": ["error", "always"], + }, + }, +]; diff --git a/examples/typescript/servers/offer-receipt/index.ts b/examples/typescript/servers/offer-receipt/index.ts new file mode 100644 index 0000000000..71f18b1532 --- /dev/null +++ b/examples/typescript/servers/offer-receipt/index.ts @@ -0,0 +1,163 @@ +import { config } from "dotenv"; +import express from "express"; +import { paymentMiddleware, x402ResourceServer } from "@x402/express"; +import { ExactEvmScheme } from "@x402/evm/exact/server"; +import { ExactSvmScheme } from "@x402/svm/exact/server"; +import { HTTPFacilitatorClient } from "@x402/core/server"; +import { + createOfferReceiptExtension, + createJWSOfferReceiptIssuer, + createEIP712OfferReceiptIssuer, + declareOfferReceiptExtension, +} from "@x402/extensions/offer-receipt"; +import { createJWSSignerFromPrivateKey } from "./jws-signer"; +import { createEIP712SignerFromPrivateKey } from "./eip712-signer"; +config(); + +const evmAddress = process.env.EVM_ADDRESS as `0x${string}`; +const svmAddress = process.env.SVM_ADDRESS; +if (!evmAddress || !svmAddress) { + console.error("Missing EVM_ADDRESS or SVM_ADDRESS environment variable"); + process.exit(1); +} + +const facilitatorUrl = process.env.FACILITATOR_URL; +if (!facilitatorUrl) { + console.error("❌ FACILITATOR_URL environment variable is required"); + process.exit(1); +} + +// For production, use a proper key management solution (HSM, KMS, etc.) +// This example uses a simple private key for demonstration +const signingPrivateKey = process.env.SIGNING_PRIVATE_KEY; +if (!signingPrivateKey) { + console.error("❌ SIGNING_PRIVATE_KEY environment variable is required"); + process.exit(1); +} + +// Signing format: "jws" (default) or "eip712" +const signingFormat = (process.env.SIGNING_FORMAT || "jws").toLowerCase(); +if (signingFormat !== "jws" && signingFormat !== "eip712") { + console.error('❌ SIGNING_FORMAT must be "jws" or "eip712"'); + process.exit(1); +} + +const facilitatorClient = new HTTPFacilitatorClient({ url: facilitatorUrl }); + +// Create the appropriate issuer based on signing format +let offerReceiptIssuer; +let kid: string; +let didDocument: object | null = null; + +if (signingFormat === "eip712") { + // EIP-712 signing using Ethereum private key + const { signTypedData, address } = createEIP712SignerFromPrivateKey(signingPrivateKey); + + // Use did:pkh for EIP-712 (identifies the Ethereum address) + // Format: did:pkh:eip155::
+ // Using chainId 1 (mainnet) as the canonical identifier + kid = `did:pkh:eip155:1:${address}#key-1`; + offerReceiptIssuer = createEIP712OfferReceiptIssuer(kid, signTypedData); + + console.log(`Using EIP-712 signing with address: ${address}`); +} else { + // JWS signing using PKCS#8 private key + const serverDomain = process.env.SERVER_DOMAIN; + if (!serverDomain) { + console.error( + "❌ SERVER_DOMAIN environment variable is required for JWS signing (e.g., localhost%3A4021)", + ); + process.exit(1); + } + + const did = `did:web:${serverDomain}`; + kid = `${did}#key-1`; + const { signer: jwsSigner, publicKeyJwk } = createJWSSignerFromPrivateKey(signingPrivateKey, kid); + offerReceiptIssuer = createJWSOfferReceiptIssuer(kid, jwsSigner); + + // Build DID document for /.well-known/did.json (only needed for JWS) + didDocument = { + "@context": ["https://www.w3.org/ns/did/v1", "https://w3id.org/security/suites/jws-2020/v1"], + id: did, + verificationMethod: [ + { + id: kid, + type: "JsonWebKey2020", + controller: did, + publicKeyJwk, + }, + ], + assertionMethod: [kid], + }; + + console.log(`Using JWS signing with did:web: ${did}`); +} + +const app = express(); + +// Create the resource server with the offer-receipt extension registered +const resourceServer = new x402ResourceServer(facilitatorClient) + .register("eip155:84532", new ExactEvmScheme()) + .register("solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1", new ExactSvmScheme()) + .registerExtension(createOfferReceiptExtension(offerReceiptIssuer)); + +app.use( + paymentMiddleware( + { + "GET /weather": { + accepts: [ + { + scheme: "exact", + // Note: "price" is SDK syntactic sugar that converts to "amount" in atomic units + // The wire protocol uses "amount" per the x402 spec + price: "$0.001", + network: "eip155:84532", + payTo: evmAddress, + }, + { + scheme: "exact", + price: "$0.001", + network: "solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1", + payTo: svmAddress, + }, + ], + description: "Weather data", + mimeType: "application/json", + extensions: { + // Declare the offer-receipt extension for this route + // includeTxHash: false (default) for privacy, true for verifiability + ...declareOfferReceiptExtension({ includeTxHash: false }), + }, + }, + }, + resourceServer, + ), +); + +app.get("/weather", (req, res) => { + res.send({ + report: { + weather: "sunny", + temperature: 70, + }, + }); +}); + +// Serve DID document for JWS verification (only needed for JWS format) +// did:web resolves to /.well-known/did.json +if (didDocument) { + app.get("/.well-known/did.json", (req, res) => { + res.setHeader("Content-Type", "application/did+json"); + res.json(didDocument); + }); +} + +app.listen(4021, () => { + console.log(`Server listening at http://localhost:${4021}`); + console.log("Offer-receipt extension enabled - responses will include signed offers/receipts"); + console.log(`Signing format: ${signingFormat.toUpperCase()}`); + console.log(`Key ID: ${kid}`); + if (didDocument) { + console.log(`DID document available at http://localhost:4021/.well-known/did.json`); + } +}); diff --git a/examples/typescript/servers/offer-receipt/jws-signer.ts b/examples/typescript/servers/offer-receipt/jws-signer.ts new file mode 100644 index 0000000000..49c6fb481f --- /dev/null +++ b/examples/typescript/servers/offer-receipt/jws-signer.ts @@ -0,0 +1,92 @@ +import * as crypto from "crypto"; +import type { JWSSigner } from "@x402/extensions/offer-receipt"; + +export interface SignerWithPublicKey { + signer: JWSSigner; + publicKeyJwk: JsonWebKey; +} + +/** + * Create a JWS signer from a base64-encoded PKCS#8 private key + * + * For production, use a proper key management solution (HSM, KMS, etc.) + * This is a simple implementation for demonstration purposes. + * + * @param privateKeyBase64 - Base64-encoded PKCS#8 private key + * @param kid - Key identifier DID URL + * @returns SignerWithPublicKey containing the signer and public key JWK + */ +export function createJWSSignerFromPrivateKey( + privateKeyBase64: string, + kid: string, +): SignerWithPublicKey { + // Decode base64 and check if it's PEM or DER format + const decoded = Buffer.from(privateKeyBase64, "base64").toString("utf8"); + const isPem = decoded.includes("-----BEGIN"); + + let privateKeyPem: string; + if (isPem) { + // Already PEM format (base64-encoded PEM) + privateKeyPem = decoded; + } else { + // Raw DER format, wrap in PEM headers + privateKeyPem = `-----BEGIN PRIVATE KEY-----\n${privateKeyBase64}\n-----END PRIVATE KEY-----`; + } + + const keyObject = crypto.createPrivateKey(privateKeyPem); + const publicKeyJwk = keyObject.export({ format: "jwk" }) as JsonWebKey; + // Remove private key component + delete (publicKeyJwk as Record).d; + + const signer: JWSSigner = { + kid, + format: "jws", + algorithm: "ES256", + async sign(payload: Uint8Array): Promise { + const sign = crypto.createSign("SHA256"); + sign.update(payload); + const signature = sign.sign(privateKeyPem); + // Convert DER signature to raw r||s format for JWS + const rawSignature = derToRaw(signature); + return Buffer.from(rawSignature).toString("base64url"); + }, + }; + + return { signer, publicKeyJwk }; +} + +/** + * Convert DER-encoded ECDSA signature to raw r||s format + * + * @param derSignature - DER-encoded signature buffer + * @returns Raw signature as Uint8Array (64 bytes for P-256) + */ +function derToRaw(derSignature: Buffer): Uint8Array { + // DER format: 0x30 [total-length] 0x02 [r-length] [r] 0x02 [s-length] [s] + let offset = 2; // Skip 0x30 and total length + + // Read r + if (derSignature[offset] !== 0x02) throw new Error("Invalid DER signature"); + offset++; + const rLength = derSignature[offset]; + offset++; + let r = derSignature.subarray(offset, offset + rLength); + offset += rLength; + + // Read s + if (derSignature[offset] !== 0x02) throw new Error("Invalid DER signature"); + offset++; + const sLength = derSignature[offset]; + offset++; + let s = derSignature.subarray(offset, offset + sLength); + + // Remove leading zeros and pad to 32 bytes + if (r.length > 32) r = r.subarray(r.length - 32); + if (s.length > 32) s = s.subarray(s.length - 32); + + const raw = new Uint8Array(64); + raw.set(r, 32 - r.length); + raw.set(s, 64 - s.length); + + return raw; +} diff --git a/examples/typescript/servers/offer-receipt/package.json b/examples/typescript/servers/offer-receipt/package.json new file mode 100644 index 0000000000..2ecb0909e6 --- /dev/null +++ b/examples/typescript/servers/offer-receipt/package.json @@ -0,0 +1,36 @@ +{ + "name": "@x402/offer-receipt-server-example", + "private": true, + "type": "module", + "scripts": { + "dev": "tsx index.ts", + "format": "prettier -c .prettierrc --write \"**/*.{ts,js,cjs,json,md}\"", + "format:check": "prettier -c .prettierrc --check \"**/*.{ts,js,cjs,json,md}\"", + "lint": "eslint . --ext .ts --fix", + "lint:check": "eslint . --ext .ts" + }, + "dependencies": { + "dotenv": "^16.4.7", + "express": "^4.18.2", + "viem": "^2.21.54", + "@x402/core": "workspace:*", + "@x402/express": "workspace:*", + "@x402/evm": "workspace:*", + "@x402/svm": "workspace:*", + "@x402/extensions": "workspace:*" + }, + "devDependencies": { + "@eslint/js": "^9.24.0", + "@types/express": "^5.0.1", + "@types/node": "^22.0.0", + "@typescript-eslint/eslint-plugin": "^8.29.1", + "@typescript-eslint/parser": "^8.29.1", + "eslint": "^9.24.0", + "eslint-plugin-import": "^2.31.0", + "eslint-plugin-jsdoc": "^50.6.9", + "eslint-plugin-prettier": "^5.2.6", + "prettier": "3.5.2", + "tsx": "^4.7.0", + "typescript": "^5.3.0" + } +} diff --git a/examples/typescript/servers/offer-receipt/tsconfig.json b/examples/typescript/servers/offer-receipt/tsconfig.json new file mode 100644 index 0000000000..828d934ed6 --- /dev/null +++ b/examples/typescript/servers/offer-receipt/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "ES2020", + "moduleResolution": "bundler", + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "skipLibCheck": true, + "strict": true, + "resolveJsonModule": true, + "baseUrl": ".", + "types": ["node"] + }, + "include": ["index.ts", "jws-signer.ts"] +} diff --git a/examples/typescript/servers/sign-in-with-x/README.md b/examples/typescript/servers/sign-in-with-x/README.md index 61cf87879f..dc37bc16af 100644 --- a/examples/typescript/servers/sign-in-with-x/README.md +++ b/examples/typescript/servers/sign-in-with-x/README.md @@ -1,6 +1,8 @@ # Sign-In-With-X (SIWX) Server Example -Express.js server demonstrating how to implement Sign-In-With-X authentication, allowing clients to prove prior payment via wallet signatures instead of paying again on subsequent requests. +Express.js server demonstrating both SIWX patterns supported by x402: +- Auth-only routes that require a wallet signature but no payment +- Paid routes where a wallet can pay once, then authenticate with SIWX on later requests ```typescript import express from "express"; @@ -31,16 +33,16 @@ app.use(paymentMiddlewareFromHTTPServer(httpServer)); ## How It Works -1. **Client pays** — First request requires payment -2. **Server records** — Payment recorded against wallet address in storage -3. **Client signs** — Subsequent requests include SIWX signature -4. **Server verifies** — Signature proves wallet ownership, grants access without payment +1. **Auth-only route** — Server returns a SIWX challenge and grants access on a valid signature alone +2. **Paid route** — First request requires payment +3. **Server records** — Payment is recorded against the wallet address in storage +4. **Later paid-route request** — Signature proves wallet ownership and grants access without re-payment ## Prerequisites - Node.js v20+ (install via [nvm](https://github.com/nvm-sh/nvm)) - pnpm v10 (install via [pnpm.io/installation](https://pnpm.io/installation)) -- Valid EVM address (SVM optional) +- At least one payout address: EVM, SVM, or both - Facilitator URL (see [facilitator list](https://www.x402.org/ecosystem?category=facilitators)) ## Setup @@ -54,9 +56,11 @@ cp .env-local .env and fill required environment variables: - `FACILITATOR_URL` - Facilitator endpoint URL -- `EVM_ADDRESS` - Ethereum address to receive payments +- `EVM_ADDRESS` - (Optional) Ethereum address to receive payments - `SVM_ADDRESS` - (Optional) Solana address for SVM payments +At least one of `EVM_ADDRESS` or `SVM_ADDRESS` is required. + 2. Install and build from typescript examples root: ```bash @@ -77,22 +81,24 @@ Start the SIWX client to test: ```bash cd ../../clients/sign-in-with-x -# Ensure .env is setup with EVM_PRIVATE_KEY +# Ensure .env is setup with EVM_PRIVATE_KEY or SVM_PRIVATE_KEY pnpm start ``` The client will: -1. Make first request and pay for `/weather` -2. Make second request with SIWX signature (no payment) -3. Make first request and pay for `/joke` -4. Make second request with SIWX signature (no payment) +1. Access `/profile` with SIWX and no payment +2. Make first request and pay for `/weather` +3. Make second request to `/weather` with SIWX instead of payment +4. Make first request and pay for `/joke` +5. Make second request to `/joke` with SIWX instead of payment ## Example Endpoints +- `GET /profile` — Auth-only wallet-gated profile data (no payment) - `GET /weather` — Weather data ($0.001 USDC) - `GET /joke` — Joke content ($0.001 USDC) -Each endpoint requires payment once per wallet address. Subsequent requests from the same wallet authenticate via SIWX signature. +`/profile` requires only a valid SIWX signature. `/weather` and `/joke` require payment once per wallet address, then accept SIWX on later requests. ## SIWX Extension Configuration @@ -108,6 +114,15 @@ const routes = { mimeType: "application/json", extensions: declareSIWxExtension(), // Announces SIWX support }, + "GET /profile": { + accepts: [], + description: "Auth-only: wallet signature required", + extensions: declareSIWxExtension({ + network: ["eip155:84532", "solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1"], + statement: "Sign in to view your profile", + expirationSeconds: 300, + }), + }, }; ``` @@ -127,6 +142,8 @@ const httpServer = new x402HTTPResourceServer(resourceServer, routes) .onProtectedRequest(createSIWxRequestHook({ storage })); // Checks SIWX auth ``` +For routes declared with `accepts: []`, the request hook grants access on valid SIWX alone. For paid routes, it also checks whether that wallet has already paid. + ## Storage Backend This example uses in-memory storage (`InMemorySIWxStorage`). For production, implement persistent storage: @@ -135,11 +152,11 @@ This example uses in-memory storage (`InMemorySIWxStorage`). For production, imp import { SIWxStorage } from "@x402/extensions/sign-in-with-x"; class RedisSIWxStorage implements SIWxStorage { - async recordPayment(address: string, resource: string): Promise { + async recordPayment(resource: string, address: string): Promise { // Store in Redis/database } - async hasAccess(address: string, resource: string): Promise { + async hasPaid(resource: string, address: string): Promise { // Check Redis/database } } @@ -171,5 +188,6 @@ createSIWxRequestHook({ storage, onEvent }); Event types: - `payment_recorded` — Wallet paid for resource -- `access_granted` — SIWX signature verified -- `access_denied` — Invalid or missing signature \ No newline at end of file +- `access_granted` — SIWX signature verified and access granted +- `validation_failed` — Header parsing, message validation, or signature verification failed +- `nonce_reused` — A previously used SIWX nonce was replayed diff --git a/examples/typescript/servers/sign-in-with-x/index.ts b/examples/typescript/servers/sign-in-with-x/index.ts index 2801fa8728..0bfdaa6d91 100644 --- a/examples/typescript/servers/sign-in-with-x/index.ts +++ b/examples/typescript/servers/sign-in-with-x/index.ts @@ -14,6 +14,7 @@ import { createSIWxSettleHook, createSIWxRequestHook, InMemorySIWxStorage, + parseSIWxHeader, } from "@x402/extensions/sign-in-with-x"; config(); @@ -95,6 +96,15 @@ function routeConfig(path: string) { const routes = { "GET /weather": routeConfig("/weather"), "GET /joke": routeConfig("/joke"), + "GET /profile": { + accepts: [] as [], + description: "Auth-only: wallet signature required", + extensions: declareSIWxExtension({ + network: [EVM_NETWORK, SVM_NETWORK], // Required for auth-only routes (no payment to infer from) + statement: "Sign in to view your profile", + expirationSeconds: 300, + }), + }, }; // Configure resource server with SIWX extension and settle hook @@ -114,14 +124,20 @@ const httpServer = new x402HTTPResourceServer(resourceServer, routes).onProtecte ); const app = express(); + app.use(paymentMiddlewareFromHTTPServer(httpServer)); app.get("/weather", (_req, res) => res.json({ weather: "sunny", temperature: 72 })); app.get("/joke", (_req, res) => res.json({ joke: "Why do programmers prefer dark mode? Because light attracts bugs." }), ); +app.get("/profile", (req, res) => { + // SIWX hook already verified the signature — just parse to extract the address + const { address } = parseSIWxHeader(req.headers["sign-in-with-x"] as string); + res.json({ address, data: "Your profile data" }); +}); app.listen(PORT, () => { console.log(`Server running at http://localhost:${PORT}`); - console.log(`Routes: GET /weather, GET /joke`); + console.log(`Routes: GET /weather, GET /joke, GET /profile (auth-only)`); }); diff --git a/examples/typescript/servers/upto/.env-local b/examples/typescript/servers/upto/.env-local new file mode 100644 index 0000000000..7c000b6d83 --- /dev/null +++ b/examples/typescript/servers/upto/.env-local @@ -0,0 +1,2 @@ +EVM_ADDRESS= +FACILITATOR_URL=https://x402.org/facilitator diff --git a/examples/typescript/servers/upto/.prettierignore b/examples/typescript/servers/upto/.prettierignore new file mode 100644 index 0000000000..5bd240ba90 --- /dev/null +++ b/examples/typescript/servers/upto/.prettierignore @@ -0,0 +1,8 @@ +docs/ +dist/ +node_modules/ +coverage/ +.github/ +src/client +**/**/*.json +*.md \ No newline at end of file diff --git a/examples/typescript/servers/upto/.prettierrc b/examples/typescript/servers/upto/.prettierrc new file mode 100644 index 0000000000..ffb416b74b --- /dev/null +++ b/examples/typescript/servers/upto/.prettierrc @@ -0,0 +1,11 @@ +{ + "tabWidth": 2, + "useTabs": false, + "semi": true, + "singleQuote": false, + "trailingComma": "all", + "bracketSpacing": true, + "arrowParens": "avoid", + "printWidth": 100, + "proseWrap": "never" +} diff --git a/examples/typescript/servers/upto/eslint.config.js b/examples/typescript/servers/upto/eslint.config.js new file mode 100644 index 0000000000..e2fde7b3b8 --- /dev/null +++ b/examples/typescript/servers/upto/eslint.config.js @@ -0,0 +1,73 @@ +import js from "@eslint/js"; +import ts from "@typescript-eslint/eslint-plugin"; +import tsParser from "@typescript-eslint/parser"; +import prettier from "eslint-plugin-prettier"; +import jsdoc from "eslint-plugin-jsdoc"; +import importPlugin from "eslint-plugin-import"; + +export default [ + { + ignores: ["dist/**", "node_modules/**"], + }, + { + files: ["**/*.ts"], + languageOptions: { + parser: tsParser, + sourceType: "module", + ecmaVersion: 2020, + globals: { + process: "readonly", + __dirname: "readonly", + module: "readonly", + require: "readonly", + Buffer: "readonly", + console: "readonly", + exports: "readonly", + setTimeout: "readonly", + clearTimeout: "readonly", + setInterval: "readonly", + clearInterval: "readonly", + }, + }, + plugins: { + "@typescript-eslint": ts, + prettier: prettier, + jsdoc: jsdoc, + import: importPlugin, + }, + rules: { + ...ts.configs.recommended.rules, + "import/first": "error", + "prettier/prettier": "error", + "@typescript-eslint/member-ordering": "error", + "@typescript-eslint/no-unused-vars": ["error", { argsIgnorePattern: "^_$" }], + "jsdoc/tag-lines": ["error", "any", { startLines: 1 }], + "jsdoc/check-alignment": "error", + "jsdoc/no-undefined-types": "off", + "jsdoc/check-param-names": "error", + "jsdoc/check-tag-names": "error", + "jsdoc/check-types": "error", + "jsdoc/implements-on-classes": "error", + "jsdoc/require-description": "error", + "jsdoc/require-jsdoc": [ + "error", + { + require: { + FunctionDeclaration: true, + MethodDefinition: true, + ClassDeclaration: true, + ArrowFunctionExpression: false, + FunctionExpression: false, + }, + }, + ], + "jsdoc/require-param": "error", + "jsdoc/require-param-description": "error", + "jsdoc/require-param-type": "off", + "jsdoc/require-returns": "error", + "jsdoc/require-returns-description": "error", + "jsdoc/require-returns-type": "off", + "jsdoc/require-hyphen-before-param-description": ["error", "always"], + }, + }, +]; diff --git a/examples/typescript/servers/upto/index.ts b/examples/typescript/servers/upto/index.ts new file mode 100644 index 0000000000..f75071bc9c --- /dev/null +++ b/examples/typescript/servers/upto/index.ts @@ -0,0 +1,70 @@ +import { config } from "dotenv"; +import express from "express"; +import { paymentMiddleware, setSettlementOverrides, x402ResourceServer } from "@x402/express"; +import { UptoEvmScheme } from "@x402/evm/upto/server"; +import { HTTPFacilitatorClient } from "@x402/core/server"; +import { declareEip2612GasSponsoringExtension } from "@x402/extensions"; +config(); + +const evmAddress = process.env.EVM_ADDRESS as `0x${string}`; +if (!evmAddress) { + console.error("Missing required EVM_ADDRESS environment variable"); + process.exit(1); +} + +const facilitatorUrl = process.env.FACILITATOR_URL; +if (!facilitatorUrl) { + console.error("Missing required FACILITATOR_URL environment variable"); + process.exit(1); +} +const facilitatorClient = new HTTPFacilitatorClient({ url: facilitatorUrl }); + +const app = express(); + +// The "upto" scheme authorizes up to a maximum amount but settles only what you specify. +// This enables usage-based billing: authorize a ceiling, then charge actual usage. +const maxPrice = "$0.10"; // Maximum the client authorizes (10 cents) + +app.use( + paymentMiddleware( + { + "GET /api/generate": { + accepts: { + scheme: "upto", + price: maxPrice, + network: "eip155:84532", + payTo: evmAddress, + }, + description: "AI text generation — billed by token usage", + mimeType: "application/json", + extensions: { + ...declareEip2612GasSponsoringExtension(), + }, + }, + }, + new x402ResourceServer(facilitatorClient).register("eip155:84532", new UptoEvmScheme()), + ), +); + +app.get("/api/generate", (req, res) => { + // Simulate work that produces a variable cost. + // In production this might be LLM token count, bytes served, compute time, etc. + const maxAmountAtomic = 100000; // 10 cents in 6-decimal USDC atomic units + const actualUsage = Math.floor(Math.random() * (maxAmountAtomic + 1)); + + // Tell the middleware to settle only what was actually used. + setSettlementOverrides(res, { amount: String(actualUsage) }); + + res.json({ + result: "Here is your generated text...", + usage: { + authorizedMaxAtomic: String(maxAmountAtomic), + actualChargedAtomic: String(actualUsage), + }, + }); +}); + +app.listen(4021, () => { + console.log("Upto server listening at http://localhost:4021"); + console.log(" GET /api/generate — usage-based billing via upto scheme"); +}); diff --git a/examples/typescript/servers/upto/package.json b/examples/typescript/servers/upto/package.json new file mode 100644 index 0000000000..8caa658be5 --- /dev/null +++ b/examples/typescript/servers/upto/package.json @@ -0,0 +1,34 @@ +{ + "name": "@x402/upto-server-example", + "private": true, + "type": "module", + "scripts": { + "dev": "tsx index.ts", + "format": "prettier -c .prettierrc --write \"**/*.{ts,js,cjs,json,md}\"", + "format:check": "prettier -c .prettierrc --check \"**/*.{ts,js,cjs,json,md}\"", + "lint": "eslint . --ext .ts --fix", + "lint:check": "eslint . --ext .ts" + }, + "dependencies": { + "@x402/core": "workspace:*", + "@x402/evm": "workspace:*", + "@x402/express": "workspace:*", + "@x402/extensions": "workspace:*", + "dotenv": "^16.4.7", + "express": "^4.18.2" + }, + "devDependencies": { + "@eslint/js": "^9.24.0", + "@types/express": "^5.0.1", + "@typescript-eslint/eslint-plugin": "^8.29.1", + "@typescript-eslint/parser": "^8.29.1", + "eslint": "^9.24.0", + "eslint-plugin-import": "^2.31.0", + "eslint-plugin-jsdoc": "^50.6.9", + "eslint-plugin-prettier": "^5.2.6", + "prettier": "3.5.2", + "tsup": "^7.2.0", + "tsx": "^4.7.0", + "typescript": "^5.3.0" + } +} diff --git a/examples/typescript/servers/upto/tsconfig.json b/examples/typescript/servers/upto/tsconfig.json new file mode 100644 index 0000000000..78f9479b1b --- /dev/null +++ b/examples/typescript/servers/upto/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "ES2020", + "moduleResolution": "bundler", + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "skipLibCheck": true, + "strict": true, + "resolveJsonModule": true, + "baseUrl": ".", + "types": ["node"] + }, + "include": ["index.ts"] +} diff --git a/go/.changes/unreleased/add-arbitrum-one-and-arbitrum-sepolia-default-stablecoin.yaml b/go/.changes/unreleased/add-arbitrum-one-and-arbitrum-sepolia-default-stablecoin.yaml new file mode 100644 index 0000000000..a4abbc4adc --- /dev/null +++ b/go/.changes/unreleased/add-arbitrum-one-and-arbitrum-sepolia-default-stablecoin.yaml @@ -0,0 +1,2 @@ +kind: added +body: Add Arbitrum One (chain ID 42161) and Arbitrum Sepolid (chain ID 421614) support with USDC as the default stablecoin \ No newline at end of file diff --git a/go/.changes/unreleased/added-20260227-085721.yaml b/go/.changes/unreleased/added-20260227-085721.yaml deleted file mode 100644 index 838dd8748f..0000000000 --- a/go/.changes/unreleased/added-20260227-085721.yaml +++ /dev/null @@ -1,3 +0,0 @@ -kind: added -body: Add route configuration validation during Initialize() to catch scheme/facilitator mismatches at startup -time: 2026-02-27T08:57:21.873969+09:00 diff --git a/go/.changes/unreleased/added-20260303-121913.yaml b/go/.changes/unreleased/added-20260303-121913.yaml new file mode 100644 index 0000000000..783a23343b --- /dev/null +++ b/go/.changes/unreleased/added-20260303-121913.yaml @@ -0,0 +1,3 @@ +kind: added +body: Add net/http standard library adapter for x402 payment middleware (http/nethttp package) +time: 2026-03-03T12:19:13.785928+09:00 diff --git a/go/.changes/unreleased/added-20260303-144745.yaml b/go/.changes/unreleased/added-20260303-144745.yaml new file mode 100644 index 0000000000..58b54c2218 --- /dev/null +++ b/go/.changes/unreleased/added-20260303-144745.yaml @@ -0,0 +1,3 @@ +kind: added +body: Add Echo framework middleware adapter for x402 payment handling in go/http/echo package +time: 2026-03-03T14:47:45.605889+09:00 diff --git a/go/.changes/unreleased/changed-20260225-174750.yaml b/go/.changes/unreleased/changed-20260225-174750.yaml deleted file mode 100644 index 37b6385c4b..0000000000 --- a/go/.changes/unreleased/changed-20260225-174750.yaml +++ /dev/null @@ -1,3 +0,0 @@ -kind: changed -body: Separated v1 legacy network name resolution from v2 CAIP-2 resolution; v1 code now uses evm/v1 package, shared utils only accept eip155:CHAIN_ID format -time: 2026-02-25T17:47:50.706805-08:00 diff --git a/go/.changes/unreleased/changed-20260226-175344.yaml b/go/.changes/unreleased/changed-20260226-175344.yaml deleted file mode 100644 index 921f0987cb..0000000000 --- a/go/.changes/unreleased/changed-20260226-175344.yaml +++ /dev/null @@ -1,3 +0,0 @@ -kind: changed -body: GetSupported retries up to 3 times with exponential backoff on 429 rate limit responses -time: 2026-02-26T17:53:44.305222+09:00 diff --git a/go/.changes/unreleased/changed-20260330-155601.yaml b/go/.changes/unreleased/changed-20260330-155601.yaml new file mode 100644 index 0000000000..f565b5064e --- /dev/null +++ b/go/.changes/unreleased/changed-20260330-155601.yaml @@ -0,0 +1,3 @@ +kind: changed +body: Updated x402UptoPermit2Proxy canonical address to 0x4020A4f3b7b90ccA423B9fabCc0CE57C6C240002, deployed with deterministic bytecode for reproducible cross-chain CREATE2 addresses +time: 2026-03-30T15:56:01.761933-07:00 diff --git a/go/.changes/unreleased/fixed-20260324-164854.yaml b/go/.changes/unreleased/fixed-20260324-164854.yaml new file mode 100644 index 0000000000..5f8d2a2509 --- /dev/null +++ b/go/.changes/unreleased/fixed-20260324-164854.yaml @@ -0,0 +1,3 @@ +kind: fixed +body: 'Fix gin streaming content leak and echo panic on flush ' +time: 2026-03-24T16:48:54.420564+09:00 diff --git a/go/.changes/unreleased/mezo-testnet-default-asset.yaml b/go/.changes/unreleased/mezo-testnet-default-asset.yaml new file mode 100644 index 0000000000..78afc2f626 --- /dev/null +++ b/go/.changes/unreleased/mezo-testnet-default-asset.yaml @@ -0,0 +1,2 @@ +kind: added +body: Add Mezo Testnet (chain ID 31611) support with mUSD as the default stablecoin diff --git a/go/.changes/unreleased/polygon-support.yaml b/go/.changes/unreleased/polygon-support.yaml new file mode 100644 index 0000000000..6aaaeb62be --- /dev/null +++ b/go/.changes/unreleased/polygon-support.yaml @@ -0,0 +1,2 @@ +kind: added +body: Add Polygon mainnet (chain ID 137) support with USDC as the default stablecoin diff --git a/go/.changes/unreleased/stable-support.yaml b/go/.changes/unreleased/stable-support.yaml new file mode 100644 index 0000000000..ea6a24c24c --- /dev/null +++ b/go/.changes/unreleased/stable-support.yaml @@ -0,0 +1,2 @@ +kind: added +body: Add Stable mainnet (chain ID 988) support with USDT0 as the default stablecoin diff --git a/go/.changes/unreleased/stable-testnet-support.yaml b/go/.changes/unreleased/stable-testnet-support.yaml new file mode 100644 index 0000000000..b924cd3c12 --- /dev/null +++ b/go/.changes/unreleased/stable-testnet-support.yaml @@ -0,0 +1,2 @@ +kind: added +body: Add Stable testnet (chain ID 2201) support with USDT0 as the default stablecoin diff --git a/go/.changes/unreleased/upto-evm-scheme.yaml b/go/.changes/unreleased/upto-evm-scheme.yaml new file mode 100644 index 0000000000..fec62091f4 --- /dev/null +++ b/go/.changes/unreleased/upto-evm-scheme.yaml @@ -0,0 +1,3 @@ +kind: added +body: Add upto EVM payment scheme with client, facilitator, and server support for permit2-based partial settlement on EVM chains +time: 2026-03-26T00:00:00.000000+00:00 diff --git a/go/.changeset/facilitator-response-errors.md b/go/.changeset/facilitator-response-errors.md new file mode 100644 index 0000000000..d6a01891c5 --- /dev/null +++ b/go/.changeset/facilitator-response-errors.md @@ -0,0 +1,5 @@ +--- +'github.com/coinbase/x402/go': patch +--- + +Treat malformed facilitator success payloads as facilitator boundary errors in the Go HTTP client instead of surfacing them as verification or settlement failures. diff --git a/go/CHANGELOG.md b/go/CHANGELOG.md index 402d4af069..6e218768ad 100644 --- a/go/CHANGELOG.md +++ b/go/CHANGELOG.md @@ -1,3 +1,34 @@ +## v2.7.0 - 2026-03-23 +### Changed +- Changed Bazaar discovery extension to support dynamic route patterns. EnrichDeclaration now +translates [param] route segments to :param-style routeTemplate and populates pathParams with +concrete values from each request. The EnrichExtensions call in go/http/server.go, previously +disabled (commented out) in all prior Go releases, is now active: ALL existing Go routes that +declare extensions will have their extensions enriched at request time. Added RouteTemplate field +to DiscoveryExtension so callers can read it without a type assertion. + +## v2.6.0 - 2026-03-17 +### Added +- Added simulation to permit2 verify and (optional) settle +### Changed +- Replaced SendRawApprovalAndSettle with a generic SendTransactions signer method that accepts an array of transaction requests (pre-signed or unsigned intents). Closed fail-open verification paths, aligned Permit2 amount check to exact match, and improved client extension fallback error handling +- Simulate transaction in verify and (optional) settle; Added multicall utility for efficient rpc calls; Fixed undeployed smart wallet handling +### Fixed +- Fixed paywall config injection targeting `` causing SVG parse errors in the browser + +## v2.5.0 - 2026-03-06 +### Added +- Add route configuration validation during Initialize() to catch scheme/facilitator mismatches at startup +- Added assetTransferMethod and supportsEip2612 flag to defaultAssets +- Added `onProtectedRequest` hook to HTTP resource server +- Add WithBazaar facilitator client decorator for querying /discovery/resources endpoint from bazaar in go +- Added dynamic function for servers to generate custom response for settlement failures defaulting to empty +- Add in-memory SettlementCache to prevent duplicate SVM transaction settlement during on-chain confirmation window +### Changed +- Separated v1 legacy network name resolution from v2 CAIP-2 resolution; v1 code now uses evm/v1 package, shared utils only accept eip155:CHAIN_ID format +- GetSupported retries up to 3 times with exponential backoff on 429 rate limit responses +- Add pluggable PaywallProvider interface for custom paywall HTML generation with PaywallBuilder pattern + ## 2.4.1 - 2026-02-25 ### Fixed - Fixed changelog generation to include version extension and eliminate trailing dots which prevent go from importing diff --git a/go/CLIENT.md b/go/CLIENT.md index c78fa1d96a..90bbfbee00 100644 --- a/go/CLIENT.md +++ b/go/CLIENT.md @@ -39,7 +39,7 @@ func main() { // 2. Create x402 client and register schemes client := x402.Newx402Client(). - Register("eip155:*", evm.NewExactEvmScheme(signer)) + Register("eip155:*", evm.NewExactEvmScheme(signer, nil)) // 3. Wrap HTTP client httpClient := x402http.WrapHTTPClientWithPayment( @@ -109,7 +109,7 @@ Register mechanisms to enable payment creation for different networks. ```go // All EVM networks -client.Register("eip155:*", evm.NewExactEvmScheme(evmSigner)) +client.Register("eip155:*", evm.NewExactEvmScheme(evmSigner, nil)) // All Solana networks client.Register("solana:*", svm.NewExactSvmScheme(svmSigner)) @@ -119,13 +119,13 @@ client.Register("solana:*", svm.NewExactSvmScheme(svmSigner)) ```go // Ethereum Mainnet -client.Register("eip155:1", evm.NewExactEvmScheme(mainnetSigner)) +client.Register("eip155:1", evm.NewExactEvmScheme(mainnetSigner, nil)) // Base Mainnet -client.Register("eip155:8453", evm.NewExactEvmScheme(baseSigner)) +client.Register("eip155:8453", evm.NewExactEvmScheme(baseSigner, nil)) // Base Sepolia -client.Register("eip155:84532", evm.NewExactEvmScheme(testnetSigner)) +client.Register("eip155:84532", evm.NewExactEvmScheme(testnetSigner, nil)) ``` #### Registration Precedence @@ -134,8 +134,34 @@ More specific registrations override wildcards: ```go client. - Register("eip155:*", evm.NewExactEvmScheme(defaultSigner)). // Fallback - Register("eip155:1", evm.NewExactEvmScheme(mainnetSigner)) // Override for mainnet + Register("eip155:*", evm.NewExactEvmScheme(defaultSigner, nil)). // Fallback + Register("eip155:1", evm.NewExactEvmScheme(mainnetSigner, nil)) // Override for mainnet +``` + +#### Optional Extension RPC Config (Explicit-Only) + +`NewExactEvmScheme` supports optional RPC config used only for extension +enrichment when signer read/fee capabilities are unavailable. + +No chain-default RPC fallback is applied by the SDK. + +```go +// Per-network explicit registration +client. + Register("eip155:137", evm.NewExactEvmScheme(polygonSigner, &evm.ExactEvmSchemeConfig{ + RPCURL: "https://polygon.example", + })). + Register("eip155:8453", evm.NewExactEvmScheme(baseSigner, &evm.ExactEvmSchemeConfig{ + RPCURL: "https://base.example", + })) + +// Wildcard registration with chain-id map +client.Register("eip155:*", evm.NewExactEvmScheme(defaultSigner, &evm.ExactEvmSchemeConfig{ + RPCByChainID: map[int64]evm.ExactEvmChainConfig{ + 137: {RPCURL: "https://polygon.example"}, + 8453: {RPCURL: "https://base.example"}, + }, +})) ``` ### 4. HTTP Integration diff --git a/go/FACILITATOR.md b/go/FACILITATOR.md index a45222c8d4..c45409332d 100644 --- a/go/FACILITATOR.md +++ b/go/FACILITATOR.md @@ -382,6 +382,24 @@ Facilitator signers need to: - Monitor for unusual patterns - Set transaction value limits +#### Duplicate Settlement (Solana / SVM) + +A race condition exists on Solana where the same payment transaction can be submitted to the `/settle` endpoint multiple times before the first submission is confirmed on-chain. Because Solana's RPC returns "success" for duplicate transaction submissions (the network deduplicates at the consensus level), the facilitator could return `success` to each caller. A malicious client can exploit this to obtain access to multiple resources while only paying once. + +The SVM mechanism packages include a built-in `SettlementCache` that mitigates this. When registering SVM facilitator schemes, pass a shared cache instance to both V1 and V2 schemes: + +```go +import svm "github.com/coinbase/x402/go/mechanisms/svm" + +cache := svm.NewSettlementCache() +v2Scheme := facilitator.NewExactSvmScheme(signer, cache) +v1Scheme := v1facilitator.NewExactSvmSchemeV1(signer, cache) +``` + +The cache rejects concurrent settlement attempts for the same transaction payload with a `duplicate_settlement` error. Entries are evicted after 120 seconds (approximately twice the Solana blockhash lifetime). + +See the [Exact SVM Scheme Specification](../specs/schemes/exact/scheme_exact_svm.md#duplicate-settlement-mitigation-recommended) for full details. + ### High Availability - Run multiple facilitator instances diff --git a/go/SERVER.md b/go/SERVER.md index f6a2495f6d..f34d3eec01 100644 --- a/go/SERVER.md +++ b/go/SERVER.md @@ -147,8 +147,12 @@ httpServer := x402http.Newx402HTTPResourceServer( // Process HTTP requests result := httpServer.ProcessHTTPRequest(ctx, reqCtx, nil) -// Handle settlement -headers, _ := httpServer.ProcessSettlement(ctx, payload, requirements, statusCode) +// Handle settlement with transport context +settleResult := httpServer.ProcessSettlement(ctx, payload, requirements, nil, &x402http.HTTPTransportContext{ + Request: &reqCtx, + ResponseBody: responseBody, + ResponseHeaders: responseHeaders, +}) ``` ### 4. Facilitator Client diff --git a/go/constants.go b/go/constants.go index 27cda41c8f..3640ab3085 100644 --- a/go/constants.go +++ b/go/constants.go @@ -3,7 +3,7 @@ package x402 // Version constants const ( // Version is the SDK version - Version = "2.4.0" + Version = "2.7.0" // ProtocolVersion is the current x402 protocol version ProtocolVersion = 2 diff --git a/go/extensions/bazaar/bazaar_test.go b/go/extensions/bazaar/bazaar_test.go index fac691b810..8eb7591269 100644 --- a/go/extensions/bazaar/bazaar_test.go +++ b/go/extensions/bazaar/bazaar_test.go @@ -1615,6 +1615,47 @@ func TestExtractDiscoveredResourceFromPaymentRequired(t *testing.T) { require.NotNil(t, info) assert.Equal(t, "GET", info.Method) }) + + t.Run("v2: should use routeTemplate as canonical URL for dynamic routes", func(t *testing.T) { + // Mirrors the TestBazaarDynamicRoutes/should use routeTemplate for canonical URL test, + // but exercises the ExtractDiscoveredResourceFromPaymentRequired code path so that + // facilitators consuming payment-required responses also produce stable catalog keys. + enrichedExt := map[string]interface{}{ + "info": map[string]interface{}{ + "input": map[string]interface{}{ + "type": "http", + "method": "GET", + }, + }, + "schema": map[string]interface{}{}, + "routeTemplate": "/products/:productId", + } + + paymentRequired := x402.PaymentRequired{ + X402Version: 2, + Resource: &x402.ResourceInfo{ + URL: "https://api.example.com/products/42", + }, + Accepts: []x402.PaymentRequirements{ + { + Scheme: "exact", + Network: "eip155:8453", + }, + }, + Extensions: map[string]interface{}{ + bazaar.BAZAAR.Key(): enrichedExt, + }, + } + + paymentRequiredBytes, _ := json.Marshal(paymentRequired) + + info, err := bazaar.ExtractDiscoveredResourceFromPaymentRequired(paymentRequiredBytes, false) + require.NoError(t, err) + require.NotNil(t, info) + // The routeTemplate replaces the concrete URL path as the canonical catalog key + assert.Equal(t, "https://api.example.com/products/:productId", info.ResourceURL) + assert.Equal(t, "/products/:productId", info.RouteTemplate) + }) } // extractMethodEnum is a test helper that extracts the method enum from a discovery extension schema. @@ -1775,6 +1816,58 @@ func TestBazaarResourceServerExtension(t *testing.T) { assert.True(t, hasMethod, "method should be in required array") }) + t.Run("should produce a valid extension after enrichment (GET)", func(t *testing.T) { + extension, err := bazaar.DeclareDiscoveryExtension( + bazaar.MethodGET, + map[string]interface{}{"query": "test"}, + bazaar.JSONSchema{ + "properties": map[string]interface{}{ + "query": map[string]interface{}{"type": "string"}, + }, + }, + "", + nil, + ) + require.NoError(t, err) + + httpContext := x402http.HTTPRequestContext{ + Method: "GET", + } + + enriched := bazaar.BazaarResourceServerExtension.EnrichDeclaration(extension, httpContext) + enrichedExt, ok := enriched.(bazaar.DiscoveryExtension) + require.True(t, ok) + + result := bazaar.ValidateDiscoveryExtension(enrichedExt) + assert.True(t, result.Valid, "enriched GET extension should pass validation: %v", result.Errors) + }) + + t.Run("should produce a valid extension after enrichment (POST)", func(t *testing.T) { + extension, err := bazaar.DeclareDiscoveryExtension( + bazaar.MethodPOST, + map[string]interface{}{"data": "test"}, + bazaar.JSONSchema{ + "properties": map[string]interface{}{ + "data": map[string]interface{}{"type": "string"}, + }, + }, + bazaar.BodyTypeJSON, + nil, + ) + require.NoError(t, err) + + httpContext := x402http.HTTPRequestContext{ + Method: "POST", + } + + enriched := bazaar.BazaarResourceServerExtension.EnrichDeclaration(extension, httpContext) + enrichedExt, ok := enriched.(bazaar.DiscoveryExtension) + require.True(t, ok) + + result := bazaar.ValidateDiscoveryExtension(enrichedExt) + assert.True(t, result.Valid, "enriched POST extension should pass validation: %v", result.Errors) + }) + t.Run("should return unchanged declaration for non-HTTP context", func(t *testing.T) { extension, err := bazaar.DeclareDiscoveryExtension( bazaar.MethodPOST, @@ -1800,3 +1893,303 @@ func TestBazaarResourceServerExtension(t *testing.T) { assert.Equal(t, extension.Info, resultExt.Info) }) } + +func declareEmptyGETExtension(t *testing.T) bazaar.DiscoveryExtension { + t.Helper() + ext, err := bazaar.DeclareDiscoveryExtension( + bazaar.MethodGET, + map[string]interface{}{}, + bazaar.JSONSchema{"properties": map[string]interface{}{}}, + "", + nil, + ) + require.NoError(t, err) + return ext +} + +type mockHTTPAdapterForBazaar struct { + headers map[string]string + method string + path string + url string + accept string + agent string +} + +func (m *mockHTTPAdapterForBazaar) GetHeader(name string) string { + if m.headers == nil { + return "" + } + return m.headers[name] +} +func (m *mockHTTPAdapterForBazaar) GetMethod() string { return m.method } +func (m *mockHTTPAdapterForBazaar) GetPath() string { return m.path } +func (m *mockHTTPAdapterForBazaar) GetURL() string { return m.url } +func (m *mockHTTPAdapterForBazaar) GetAcceptHeader() string { return m.accept } +func (m *mockHTTPAdapterForBazaar) GetUserAgent() string { return m.agent } + +func TestBazaarDynamicRoutes(t *testing.T) { + t.Run("should leave static routes unchanged", func(t *testing.T) { + extension, err := bazaar.DeclareDiscoveryExtension( + bazaar.MethodGET, + map[string]interface{}{"query": "test"}, + bazaar.JSONSchema{"properties": map[string]interface{}{"query": map[string]interface{}{"type": "string"}}}, + "", + nil, + ) + require.NoError(t, err) + + httpContext := x402http.HTTPRequestContext{ + Method: "GET", + Path: "/users", + RoutePattern: "/users", + Adapter: &mockHTTPAdapterForBazaar{path: "/users"}, + } + + enriched := bazaar.BazaarResourceServerExtension.EnrichDeclaration(extension, httpContext) + + // Static route: enriched should be DiscoveryExtension with empty RouteTemplate + enrichedExt, ok := enriched.(bazaar.DiscoveryExtension) + require.True(t, ok, "should always produce DiscoveryExtension") + assert.Empty(t, enrichedExt.RouteTemplate, "static route should have empty RouteTemplate") + }) + + t.Run("should produce routeTemplate for dynamic routes", func(t *testing.T) { + extension := declareEmptyGETExtension(t) + + httpContext := x402http.HTTPRequestContext{ + Method: "GET", + Path: "/users/123", + RoutePattern: "/users/[userId]", + Adapter: &mockHTTPAdapterForBazaar{path: "/users/123"}, + } + + enriched := bazaar.BazaarResourceServerExtension.EnrichDeclaration(extension, httpContext) + + enrichedExt, ok := enriched.(bazaar.DiscoveryExtension) + require.True(t, ok, "dynamic route should produce a DiscoveryExtension") + assert.Equal(t, "/users/:userId", enrichedExt.RouteTemplate) + }) + + t.Run("should extract path params from concrete URL", func(t *testing.T) { + extension := declareEmptyGETExtension(t) + + httpContext := x402http.HTTPRequestContext{ + Method: "GET", + Path: "/users/123", + RoutePattern: "/users/[userId]", + Adapter: &mockHTTPAdapterForBazaar{path: "/users/123"}, + } + + enriched := bazaar.BazaarResourceServerExtension.EnrichDeclaration(extension, httpContext) + + enrichedExt, ok := enriched.(bazaar.DiscoveryExtension) + require.True(t, ok) + + queryInput, ok := enrichedExt.Info.Input.(bazaar.QueryInput) + require.True(t, ok) + require.NotNil(t, queryInput.PathParams) + assert.Equal(t, "123", queryInput.PathParams["userId"]) + }) + + t.Run("should extract multiple path params", func(t *testing.T) { + extension := declareEmptyGETExtension(t) + + httpContext := x402http.HTTPRequestContext{ + Method: "GET", + Path: "/users/42/posts/7", + RoutePattern: "/users/[userId]/posts/[postId]", + Adapter: &mockHTTPAdapterForBazaar{path: "/users/42/posts/7"}, + } + + enriched := bazaar.BazaarResourceServerExtension.EnrichDeclaration(extension, httpContext) + + enrichedExt, ok := enriched.(bazaar.DiscoveryExtension) + require.True(t, ok) + assert.Equal(t, "/users/:userId/posts/:postId", enrichedExt.RouteTemplate) + + queryInput, ok := enrichedExt.Info.Input.(bazaar.QueryInput) + require.True(t, ok) + assert.Equal(t, "42", queryInput.PathParams["userId"]) + assert.Equal(t, "7", queryInput.PathParams["postId"]) + }) + + t.Run("should produce routeTemplate for colon-style dynamic routes", func(t *testing.T) { + extension := declareEmptyGETExtension(t) + + httpContext := x402http.HTTPRequestContext{ + Method: "GET", + Path: "/users/123", + RoutePattern: "/users/:userId", + Adapter: &mockHTTPAdapterForBazaar{path: "/users/123"}, + } + + enriched := bazaar.BazaarResourceServerExtension.EnrichDeclaration(extension, httpContext) + + enrichedExt, ok := enriched.(bazaar.DiscoveryExtension) + require.True(t, ok, "colon-param dynamic route should produce a DiscoveryExtension") + assert.Equal(t, "/users/:userId", enrichedExt.RouteTemplate) + }) + + t.Run("should extract path params from colon-style pattern", func(t *testing.T) { + extension := declareEmptyGETExtension(t) + + httpContext := x402http.HTTPRequestContext{ + Method: "GET", + Path: "/users/42/posts/7", + RoutePattern: "/users/:userId/posts/:postId", + Adapter: &mockHTTPAdapterForBazaar{path: "/users/42/posts/7"}, + } + + enriched := bazaar.BazaarResourceServerExtension.EnrichDeclaration(extension, httpContext) + + enrichedExt, ok := enriched.(bazaar.DiscoveryExtension) + require.True(t, ok) + assert.Equal(t, "/users/:userId/posts/:postId", enrichedExt.RouteTemplate) + + queryInput, ok := enrichedExt.Info.Input.(bazaar.QueryInput) + require.True(t, ok) + assert.Equal(t, "42", queryInput.PathParams["userId"]) + assert.Equal(t, "7", queryInput.PathParams["postId"]) + }) + + t.Run("should handle mixed [param] and :param patterns", func(t *testing.T) { + extension := declareEmptyGETExtension(t) + + httpContext := x402http.HTTPRequestContext{ + Method: "GET", + Path: "/users/42/posts/7", + RoutePattern: "/users/[userId]/posts/:postId", + Adapter: &mockHTTPAdapterForBazaar{path: "/users/42/posts/7"}, + } + + enriched := bazaar.BazaarResourceServerExtension.EnrichDeclaration(extension, httpContext) + + enrichedExt, ok := enriched.(bazaar.DiscoveryExtension) + require.True(t, ok) + assert.Equal(t, "/users/:userId/posts/:postId", enrichedExt.RouteTemplate) + + queryInput, ok := enrichedExt.Info.Input.(bazaar.QueryInput) + require.True(t, ok) + assert.Equal(t, "42", queryInput.PathParams["userId"]) + assert.Equal(t, "7", queryInput.PathParams["postId"]) + }) + + t.Run("should auto-convert wildcard * to :varN for discovery", func(t *testing.T) { + extension := declareEmptyGETExtension(t) + + httpContext := x402http.HTTPRequestContext{ + Method: "GET", + Path: "/weather/san-francisco", + RoutePattern: "/weather/*", + Adapter: &mockHTTPAdapterForBazaar{path: "/weather/san-francisco"}, + } + + enriched := bazaar.BazaarResourceServerExtension.EnrichDeclaration(extension, httpContext) + + enrichedExt, ok := enriched.(bazaar.DiscoveryExtension) + require.True(t, ok) + assert.Equal(t, "/weather/:var1", enrichedExt.RouteTemplate) + + queryInput, ok := enrichedExt.Info.Input.(bazaar.QueryInput) + require.True(t, ok) + assert.Equal(t, "san-francisco", queryInput.PathParams["var1"]) + }) + + t.Run("should auto-convert multiple wildcards to :var1 :var2 etc", func(t *testing.T) { + extension := declareEmptyGETExtension(t) + + httpContext := x402http.HTTPRequestContext{ + Method: "GET", + Path: "/api/users/42/posts/7", + RoutePattern: "/api/*/*/posts/*", + Adapter: &mockHTTPAdapterForBazaar{path: "/api/users/42/posts/7"}, + } + + enriched := bazaar.BazaarResourceServerExtension.EnrichDeclaration(extension, httpContext) + + enrichedExt, ok := enriched.(bazaar.DiscoveryExtension) + require.True(t, ok) + assert.Equal(t, "/api/:var1/:var2/posts/:var3", enrichedExt.RouteTemplate) + }) + + t.Run("should use concrete URL for static routes", func(t *testing.T) { + extension, err := bazaar.DeclareDiscoveryExtension( + bazaar.MethodGET, + map[string]interface{}{"query": "test"}, + bazaar.JSONSchema{"properties": map[string]interface{}{"query": map[string]interface{}{"type": "string"}}}, + "", + nil, + ) + require.NoError(t, err) + + extensionJSON, err := json.Marshal(map[string]interface{}{ + "x402Version": 2, + "scheme": "exact", + "network": "eip155:8453", + "payload": map[string]interface{}{}, + "accepted": map[string]interface{}{}, + "resource": map[string]interface{}{"url": "http://example.com/search?q=test"}, + "extensions": map[string]interface{}{ + bazaar.BAZAAR.Key(): extension, + }, + }) + require.NoError(t, err) + + var payload x402.PaymentPayload + require.NoError(t, json.Unmarshal(extensionJSON, &payload)) + + payloadJSON, err := json.Marshal(payload) + require.NoError(t, err) + + discovered, err := bazaar.ExtractDiscoveredResourceFromPaymentPayload(payloadJSON, nil, false) + require.NoError(t, err) + require.NotNil(t, discovered) + assert.Equal(t, "http://example.com/search", discovered.ResourceURL) + assert.Empty(t, discovered.RouteTemplate) + }) +} + +// TestDynamicRoutesCatalogConsolidation verifies that two requests to the same +// parameterized route produce the same canonical ResourceURL, so a catalog keyed by ResourceURL +// contains exactly one entry regardless of how many distinct concrete parameter values arrive. +func TestDynamicRoutesCatalogConsolidation(t *testing.T) { + extension := declareEmptyGETExtension(t) + + makePayloadJSON := func(userID string) []byte { + enrichedExt := map[string]interface{}{ + "info": extension.Info, + "schema": extension.Schema, + "routeTemplate": "/users/:userId", + } + b, err := json.Marshal(map[string]interface{}{ + "x402Version": 2, + "scheme": "exact", + "network": "eip155:8453", + "payload": map[string]interface{}{}, + "accepted": map[string]interface{}{}, + "resource": map[string]interface{}{"url": "http://api.example.com/users/" + userID}, + "extensions": map[string]interface{}{ + bazaar.BAZAAR.Key(): enrichedExt, + }, + }) + require.NoError(t, err) + return b + } + + // First request: /users/123 + discovered1, err := bazaar.ExtractDiscoveredResourceFromPaymentPayload(makePayloadJSON("123"), nil, false) + require.NoError(t, err) + require.NotNil(t, discovered1) + + // Second request: /users/456 — different concrete ID, same route + discovered2, err := bazaar.ExtractDiscoveredResourceFromPaymentPayload(makePayloadJSON("456"), nil, false) + require.NoError(t, err) + require.NotNil(t, discovered2) + + // Both must produce the same canonical URL so catalog sees a single entry + assert.Equal(t, "http://api.example.com/users/:userId", discovered1.ResourceURL) + assert.Equal(t, "http://api.example.com/users/:userId", discovered2.ResourceURL) + assert.Equal(t, discovered1.ResourceURL, discovered2.ResourceURL, + "requests to the same parameterized route should consolidate to one catalog entry") +} diff --git a/go/extensions/bazaar/facilitator.go b/go/extensions/bazaar/facilitator.go index 1e251bc307..ef54483a27 100644 --- a/go/extensions/bazaar/facilitator.go +++ b/go/extensions/bazaar/facilitator.go @@ -4,6 +4,8 @@ import ( "encoding/json" "fmt" "net/url" + "regexp" + "strings" x402 "github.com/coinbase/x402/go" "github.com/coinbase/x402/go/extensions/types" @@ -93,6 +95,7 @@ type DiscoveredResource struct { DiscoveryInfo *types.DiscoveryInfo Description string MimeType string + RouteTemplate string } // ExtractDiscoveredResourceFromPaymentPayload extracts a discovered resource from a client's payment payload and requirements. @@ -142,6 +145,7 @@ func ExtractDiscoveredResourceFromPaymentPayload( var resourceURL string var description string var mimeType string + var routeTemplate string version := versionCheck.X402Version switch version { @@ -162,6 +166,18 @@ func ExtractDiscoveredResourceFromPaymentPayload( // Extract discovery info from extensions if payload.Extensions != nil { if bazaarExt, ok := payload.Extensions[types.BAZAAR.Key()]; ok { + // routeTemplate uses :param syntax (e.g. "/users/:userId", "/weather/:country/:city"). + // Must start with "/", must not contain ".." or "://". + var rawTemplate string + if m, ok := bazaarExt.(map[string]interface{}); ok { + if v, ok := m["routeTemplate"]; ok { + rawTemplate, _ = v.(string) + } + } + if isValidRouteTemplate(rawTemplate) { + routeTemplate = rawTemplate + } + extensionJSON, err := json.Marshal(bazaarExt) if err != nil { return nil, fmt.Errorf("failed to marshal bazaar extension: %w", err) @@ -221,8 +237,7 @@ func ExtractDiscoveredResourceFromPaymentPayload( return nil, fmt.Errorf("failed to extract method from discovery info") } - // Strip query params and hash for discovery cataloging - normalizedURL := stripQueryParams(resourceURL) + normalizedURL := normalizeResourceURL(resourceURL, routeTemplate) return &DiscoveredResource{ ResourceURL: normalizedURL, @@ -231,9 +246,48 @@ func ExtractDiscoveredResourceFromPaymentPayload( Method: method, X402Version: version, DiscoveryInfo: discoveryInfo, + RouteTemplate: routeTemplate, }, nil } +// routeTemplateRegex validates the overall shape of a routeTemplate: +// must start with "/" and contain only safe URL path characters and :param identifiers. +// Expected format: "/users/:userId", "/weather/:country/:city", "/api/v1/items". +var routeTemplateRegex = regexp.MustCompile(`^/[a-zA-Z0-9_/:.\-~%]+$`) + +// isValidRouteTemplate checks whether a routeTemplate value is structurally valid. +// +// Expected format: ":param" segments using colon-prefixed identifiers +// (e.g. "/users/:userId", "/weather/:country/:city"). +// +// The facilitator is a trust boundary: the client controls the payment payload and can modify +// routeTemplate before submission. A malicious value could cause the facilitator to catalog the +// payment under an arbitrary URL (catalog poisoning). This enforces minimal structural requirements: +// - Must be a non-empty string starting with "/" +// - Must match the safe URL path character set (alphanumeric, _, :, /, ., -, ~, %) +// - Must not contain ".." (path traversal) +// - Must not contain "://" (URL injection) +func isValidRouteTemplate(s string) bool { + if s == "" { + return false + } + if !routeTemplateRegex.MatchString(s) { + return false + } + // Decode percent-encoding before traversal checks so that %2e%2e is caught. + decoded, err := url.PathUnescape(s) + if err != nil { + return false + } + if strings.Contains(decoded, "..") { + return false + } + if strings.Contains(decoded, "://") { + return false + } + return true +} + // stripQueryParams removes query parameters and fragments from a URL for cataloging func stripQueryParams(rawURL string) string { parsed, err := url.Parse(rawURL) @@ -245,6 +299,22 @@ func stripQueryParams(rawURL string) string { return parsed.String() } +// normalizeResourceURL returns the canonical URL for discovery cataloging. +// If routeTemplate is non-empty (dynamic route), it replaces the URL path with the +// template and strips query/fragment. Otherwise it just strips query/fragment. +func normalizeResourceURL(rawURL, routeTemplate string) string { + if routeTemplate != "" { + parsed, err := url.Parse(rawURL) + if err == nil { + parsed.Path = routeTemplate + parsed.RawQuery = "" + parsed.Fragment = "" + return parsed.String() + } + } + return stripQueryParams(rawURL) +} + // ExtractDiscoveredResourceFromPaymentRequired extracts a discovered resource from a 402 PaymentRequired response. // This is useful for clients/facilitators that receive a 402 response and want to discover resource capabilities. // @@ -293,6 +363,7 @@ func ExtractDiscoveredResourceFromPaymentRequired( var resourceURL string var description string var mimeType string + var routeTemplate string version := versionCheck.X402Version switch version { @@ -313,6 +384,18 @@ func ExtractDiscoveredResourceFromPaymentRequired( // First check PaymentRequired.extensions for bazaar extension if paymentRequired.Extensions != nil { if bazaarExt, ok := paymentRequired.Extensions[types.BAZAAR.Key()]; ok { + // routeTemplate uses :param syntax (e.g. "/users/:userId", "/weather/:country/:city"). + // Must start with "/", must not contain ".." or "://". + var rawTemplate string + if m, ok := bazaarExt.(map[string]interface{}); ok { + if v, ok := m["routeTemplate"]; ok { + rawTemplate, _ = v.(string) + } + } + if isValidRouteTemplate(rawTemplate) { + routeTemplate = rawTemplate + } + extensionJSON, err := json.Marshal(bazaarExt) if err != nil { return nil, fmt.Errorf("failed to marshal bazaar extension: %w", err) @@ -378,8 +461,7 @@ func ExtractDiscoveredResourceFromPaymentRequired( return nil, fmt.Errorf("failed to extract method from discovery info") } - // Strip query params and hash for discovery cataloging - normalizedURL := stripQueryParams(resourceURL) + normalizedURL := normalizeResourceURL(resourceURL, routeTemplate) return &DiscoveredResource{ ResourceURL: normalizedURL, @@ -388,6 +470,7 @@ func ExtractDiscoveredResourceFromPaymentRequired( Method: method, X402Version: version, DiscoveryInfo: discoveryInfo, + RouteTemplate: routeTemplate, }, nil } diff --git a/go/extensions/bazaar/facilitator_client.go b/go/extensions/bazaar/facilitator_client.go new file mode 100644 index 0000000000..90be3b9ab6 --- /dev/null +++ b/go/extensions/bazaar/facilitator_client.go @@ -0,0 +1,181 @@ +package bazaar + +import ( + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "net/url" + "strconv" + + x402http "github.com/coinbase/x402/go/http" +) + +// ListDiscoveryResourcesParams contains optional filtering and pagination parameters +// for listing discovery resources from a facilitator's bazaar. +type ListDiscoveryResourcesParams struct { + // Type filters by protocol type (e.g., "http", "mcp"). + Type string + + // Limit is the number of discovered x402 resources to return per page. + Limit int + + // Offset is the offset of the first discovered x402 resource to return. + Offset int +} + +// DiscoveryResource represents a discovered x402 resource from the bazaar. +type DiscoveryResource struct { + // Resource is the URL or identifier of the discovered resource. + Resource string `json:"resource"` + + // Type is the protocol type of the resource (e.g., "http"). + Type string `json:"type"` + + // X402Version is the x402 protocol version supported by this resource. + X402Version int `json:"x402Version"` + + // Accepts is an array of accepted payment methods for this resource. + Accepts []json.RawMessage `json:"accepts"` + + // LastUpdated is an ISO 8601 timestamp of when the resource was last updated. + LastUpdated string `json:"lastUpdated"` + + // Metadata contains additional metadata about the resource. + Metadata map[string]any `json:"metadata,omitempty"` +} + +// Pagination contains pagination information for a discovery resources response. +type Pagination struct { + // Limit is the maximum number of results returned. + Limit int `json:"limit"` + + // Offset is the number of results skipped. + Offset int `json:"offset"` + + // Total is the total count of resources matching the query. + Total int `json:"total"` +} + +// DiscoveryResourcesResponse is the response from listing discovery resources. +type DiscoveryResourcesResponse struct { + // X402Version is the x402 protocol version of this response. + X402Version int `json:"x402Version"` + + // Items is the list of discovered resources. + Items []DiscoveryResource `json:"items"` + + // Pagination contains pagination information for the response. + Pagination Pagination `json:"pagination"` +} + +// BazaarFacilitatorClient wraps an HTTPFacilitatorClient with bazaar discovery +// query functionality. It preserves all original facilitator client capabilities +// (Verify, Settle, GetSupported) and adds the ability to list discovered x402 +// resources from the facilitator's bazaar. +type BazaarFacilitatorClient struct { + *x402http.HTTPFacilitatorClient +} + +// WithBazaar extends a facilitator client with bazaar discovery query functionality. +// +// Example: +// +// client := bazaar.WithBazaar(http.NewHTTPFacilitatorClient(nil)) +// resources, err := client.ListDiscoveryResources(ctx, &bazaar.ListDiscoveryResourcesParams{ +// Type: "http", +// Limit: 20, +// }) +func WithBazaar(client *x402http.HTTPFacilitatorClient) *BazaarFacilitatorClient { + return &BazaarFacilitatorClient{HTTPFacilitatorClient: client} +} + +// ListDiscoveryResources queries the facilitator's /discovery/resources endpoint +// to list x402 discovery resources from the bazaar. +// +// Params may be nil to list all resources without filtering. +func (c *BazaarFacilitatorClient) ListDiscoveryResources( + ctx context.Context, + params *ListDiscoveryResourcesParams, +) (*DiscoveryResourcesResponse, error) { + // Build URL with query parameters + endpoint, err := c.buildDiscoveryURL(params) + if err != nil { + return nil, fmt.Errorf("failed to build discovery URL: %w", err) + } + + // Create request + req, err := http.NewRequestWithContext(ctx, "GET", endpoint, nil) + if err != nil { + return nil, fmt.Errorf("failed to create discovery request: %w", err) + } + + req.Header.Set("Content-Type", "application/json") + + // Add auth headers if available + authProvider := c.GetAuthProvider() + if authProvider != nil { + authHeaders, err := authProvider.GetAuthHeaders(ctx) + if err != nil { + return nil, fmt.Errorf("failed to get auth headers: %w", err) + } + for k, v := range authHeaders.Discovery { + req.Header.Set(k, v) + } + } + + // Make request + resp, err := c.HTTPClient().Do(req) + if err != nil { + return nil, fmt.Errorf("discovery request failed: %w", err) + } + defer resp.Body.Close() + + // Read response body + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("failed to read response body: %w", err) + } + + // Check for error response + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("facilitator listDiscoveryResources failed (%d): %s", resp.StatusCode, string(body)) + } + + // Parse response + var result DiscoveryResourcesResponse + if err := json.Unmarshal(body, &result); err != nil { + return nil, fmt.Errorf("failed to decode discovery response: %w", err) + } + + return &result, nil +} + +// buildDiscoveryURL constructs the full /discovery/resources URL with query parameters. +func (c *BazaarFacilitatorClient) buildDiscoveryURL(params *ListDiscoveryResourcesParams) (string, error) { + base := c.URL() + "/discovery/resources" + + if params == nil { + return base, nil + } + + u, err := url.Parse(base) + if err != nil { + return "", err + } + + q := u.Query() + if params.Type != "" { + q.Set("type", params.Type) + } + if params.Limit > 0 { + q.Set("limit", strconv.Itoa(params.Limit)) + } + if params.Offset > 0 { + q.Set("offset", strconv.Itoa(params.Offset)) + } + + u.RawQuery = q.Encode() + return u.String(), nil +} diff --git a/go/extensions/bazaar/facilitator_client_test.go b/go/extensions/bazaar/facilitator_client_test.go new file mode 100644 index 0000000000..c23ec4308b --- /dev/null +++ b/go/extensions/bazaar/facilitator_client_test.go @@ -0,0 +1,491 @@ +package bazaar + +import ( + "context" + "encoding/json" + "net/http" + "net/http/httptest" + "testing" + + x402http "github.com/coinbase/x402/go/http" +) + +// testAuthProvider is a test helper that returns fixed auth headers. +type testAuthProvider struct { + headers x402http.AuthHeaders + err error +} + +func (p *testAuthProvider) GetAuthHeaders(_ context.Context) (x402http.AuthHeaders, error) { + return p.headers, p.err +} + +func TestWithBazaar(t *testing.T) { + client := x402http.NewHTTPFacilitatorClient(nil) + bazaarClient := WithBazaar(client) + + if bazaarClient == nil { + t.Fatal("Expected non-nil bazaar client") + } + if bazaarClient.HTTPFacilitatorClient != client { + t.Error("Expected embedded client to match original") + } +} + +func TestWithBazaar_PreservesOriginalMethods(t *testing.T) { + client := x402http.NewHTTPFacilitatorClient(&x402http.FacilitatorConfig{ + URL: "https://example.com/facilitator", + }) + bazaarClient := WithBazaar(client) + + // Verify the wrapped client preserves the URL + if bazaarClient.URL() != "https://example.com/facilitator" { + t.Errorf("Expected URL https://example.com/facilitator, got %s", bazaarClient.URL()) + } +} + +func TestListDiscoveryResources_Success(t *testing.T) { + ctx := context.Background() + + expectedResponse := DiscoveryResourcesResponse{ + X402Version: 2, + Items: []DiscoveryResource{ + { + Resource: "https://api.example.com/data", + Type: "http", + X402Version: 2, + Accepts: []json.RawMessage{json.RawMessage(`{"scheme":"exact","network":"eip155:1"}`)}, + LastUpdated: "2026-03-01T00:00:00Z", + Metadata: map[string]any{"category": "data"}, + }, + }, + Pagination: Pagination{ + Limit: 20, + Offset: 0, + Total: 1, + }, + } + + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/discovery/resources" { + t.Errorf("Expected path /discovery/resources, got %s", r.URL.Path) + } + if r.Method != "GET" { + t.Errorf("Expected GET method, got %s", r.Method) + } + if r.Header.Get("Content-Type") != "application/json" { + t.Errorf("Expected Content-Type application/json, got %s", r.Header.Get("Content-Type")) + } + + w.Header().Set("Content-Type", "application/json") + _ = json.NewEncoder(w).Encode(expectedResponse) + })) + defer server.Close() + + client := WithBazaar(x402http.NewHTTPFacilitatorClient(&x402http.FacilitatorConfig{ + URL: server.URL, + })) + + result, err := client.ListDiscoveryResources(ctx, nil) + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + + if result.X402Version != 2 { + t.Errorf("Expected x402Version 2, got %d", result.X402Version) + } + if len(result.Items) != 1 { + t.Fatalf("Expected 1 item, got %d", len(result.Items)) + } + if result.Items[0].Resource != "https://api.example.com/data" { + t.Errorf("Expected resource URL https://api.example.com/data, got %s", result.Items[0].Resource) + } + if result.Items[0].Type != "http" { + t.Errorf("Expected type http, got %s", result.Items[0].Type) + } + if result.Items[0].X402Version != 2 { + t.Errorf("Expected item x402Version 2, got %d", result.Items[0].X402Version) + } + if result.Items[0].LastUpdated != "2026-03-01T00:00:00Z" { + t.Errorf("Expected lastUpdated 2026-03-01T00:00:00Z, got %s", result.Items[0].LastUpdated) + } + if result.Items[0].Metadata["category"] != "data" { + t.Errorf("Expected metadata category=data, got %v", result.Items[0].Metadata["category"]) + } + if result.Pagination.Limit != 20 { + t.Errorf("Expected pagination limit 20, got %d", result.Pagination.Limit) + } + if result.Pagination.Total != 1 { + t.Errorf("Expected pagination total 1, got %d", result.Pagination.Total) + } +} + +func TestListDiscoveryResources_WithParams(t *testing.T) { + ctx := context.Background() + + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + query := r.URL.Query() + + if query.Get("type") != "http" { + t.Errorf("Expected type=http, got %s", query.Get("type")) + } + if query.Get("limit") != "10" { + t.Errorf("Expected limit=10, got %s", query.Get("limit")) + } + if query.Get("offset") != "5" { + t.Errorf("Expected offset=5, got %s", query.Get("offset")) + } + + w.Header().Set("Content-Type", "application/json") + _ = json.NewEncoder(w).Encode(DiscoveryResourcesResponse{ + X402Version: 2, + Items: []DiscoveryResource{}, + Pagination: Pagination{Limit: 10, Offset: 5, Total: 0}, + }) + })) + defer server.Close() + + client := WithBazaar(x402http.NewHTTPFacilitatorClient(&x402http.FacilitatorConfig{ + URL: server.URL, + })) + + result, err := client.ListDiscoveryResources(ctx, &ListDiscoveryResourcesParams{ + Type: "http", + Limit: 10, + Offset: 5, + }) + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + if result.Pagination.Limit != 10 { + t.Errorf("Expected pagination limit 10, got %d", result.Pagination.Limit) + } + if result.Pagination.Offset != 5 { + t.Errorf("Expected pagination offset 5, got %d", result.Pagination.Offset) + } +} + +func TestListDiscoveryResources_NoParams(t *testing.T) { + ctx := context.Background() + + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.RawQuery != "" { + t.Errorf("Expected no query params, got %s", r.URL.RawQuery) + } + + w.Header().Set("Content-Type", "application/json") + _ = json.NewEncoder(w).Encode(DiscoveryResourcesResponse{ + X402Version: 2, + Items: []DiscoveryResource{}, + Pagination: Pagination{Limit: 20, Offset: 0, Total: 0}, + }) + })) + defer server.Close() + + client := WithBazaar(x402http.NewHTTPFacilitatorClient(&x402http.FacilitatorConfig{ + URL: server.URL, + })) + + _, err := client.ListDiscoveryResources(ctx, nil) + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } +} + +func TestListDiscoveryResources_PartialParams(t *testing.T) { + ctx := context.Background() + + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + query := r.URL.Query() + + if query.Get("type") != "mcp" { + t.Errorf("Expected type=mcp, got %s", query.Get("type")) + } + // limit and offset should not be set when zero + if query.Has("limit") { + t.Errorf("Expected no limit param, got %s", query.Get("limit")) + } + if query.Has("offset") { + t.Errorf("Expected no offset param, got %s", query.Get("offset")) + } + + w.Header().Set("Content-Type", "application/json") + _ = json.NewEncoder(w).Encode(DiscoveryResourcesResponse{ + X402Version: 2, + Items: []DiscoveryResource{}, + Pagination: Pagination{Limit: 20, Offset: 0, Total: 0}, + }) + })) + defer server.Close() + + client := WithBazaar(x402http.NewHTTPFacilitatorClient(&x402http.FacilitatorConfig{ + URL: server.URL, + })) + + _, err := client.ListDiscoveryResources(ctx, &ListDiscoveryResourcesParams{ + Type: "mcp", + }) + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } +} + +func TestListDiscoveryResources_ErrorResponse(t *testing.T) { + ctx := context.Background() + + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusInternalServerError) + _, _ = w.Write([]byte("internal server error")) + })) + defer server.Close() + + client := WithBazaar(x402http.NewHTTPFacilitatorClient(&x402http.FacilitatorConfig{ + URL: server.URL, + })) + + _, err := client.ListDiscoveryResources(ctx, nil) + if err == nil { + t.Fatal("Expected error for 500 response") + } + + expected := "facilitator listDiscoveryResources failed (500): internal server error" + if err.Error() != expected { + t.Errorf("Expected error message %q, got %q", expected, err.Error()) + } +} + +func TestListDiscoveryResources_NotFound(t *testing.T) { + ctx := context.Background() + + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusNotFound) + _, _ = w.Write([]byte("not found")) + })) + defer server.Close() + + client := WithBazaar(x402http.NewHTTPFacilitatorClient(&x402http.FacilitatorConfig{ + URL: server.URL, + })) + + _, err := client.ListDiscoveryResources(ctx, nil) + if err == nil { + t.Fatal("Expected error for 404 response") + } + + expected := "facilitator listDiscoveryResources failed (404): not found" + if err.Error() != expected { + t.Errorf("Expected error message %q, got %q", expected, err.Error()) + } +} + +func TestListDiscoveryResources_InvalidJSON(t *testing.T) { + ctx := context.Background() + + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write([]byte(`{"x402Version":`)) + })) + defer server.Close() + + client := WithBazaar(x402http.NewHTTPFacilitatorClient(&x402http.FacilitatorConfig{ + URL: server.URL, + })) + + _, err := client.ListDiscoveryResources(ctx, nil) + if err == nil { + t.Fatal("Expected error for invalid JSON response") + } +} + +func TestListDiscoveryResources_WithAuthHeaders(t *testing.T) { + ctx := context.Background() + + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + auth := r.Header.Get("Authorization") + if auth != "Bearer test-token" { + t.Errorf("Expected Authorization header 'Bearer test-token', got %q", auth) + } + + apiKey := r.Header.Get("X-Api-Key") + if apiKey != "my-key" { + t.Errorf("Expected X-Api-Key header 'my-key', got %q", apiKey) + } + + w.Header().Set("Content-Type", "application/json") + _ = json.NewEncoder(w).Encode(DiscoveryResourcesResponse{ + X402Version: 2, + Items: []DiscoveryResource{}, + Pagination: Pagination{Limit: 20, Offset: 0, Total: 0}, + }) + })) + defer server.Close() + + authProvider := &testAuthProvider{ + headers: x402http.AuthHeaders{ + Discovery: map[string]string{ + "Authorization": "Bearer test-token", + "X-Api-Key": "my-key", + }, + }, + } + + client := WithBazaar(x402http.NewHTTPFacilitatorClient(&x402http.FacilitatorConfig{ + URL: server.URL, + AuthProvider: authProvider, + })) + + _, err := client.ListDiscoveryResources(ctx, nil) + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } +} + +func TestListDiscoveryResources_NoAuthProvider(t *testing.T) { + ctx := context.Background() + + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Should not have Authorization header when no auth provider + if r.Header.Get("Authorization") != "" { + t.Errorf("Expected no Authorization header, got %q", r.Header.Get("Authorization")) + } + + w.Header().Set("Content-Type", "application/json") + _ = json.NewEncoder(w).Encode(DiscoveryResourcesResponse{ + X402Version: 2, + Items: []DiscoveryResource{}, + Pagination: Pagination{Limit: 20, Offset: 0, Total: 0}, + }) + })) + defer server.Close() + + client := WithBazaar(x402http.NewHTTPFacilitatorClient(&x402http.FacilitatorConfig{ + URL: server.URL, + })) + + _, err := client.ListDiscoveryResources(ctx, nil) + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } +} + +func TestListDiscoveryResources_AuthProviderError(t *testing.T) { + ctx := context.Background() + + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + t.Error("Request should not have been made when auth provider fails") + w.WriteHeader(http.StatusOK) + })) + defer server.Close() + + authProvider := &testAuthProvider{ + err: http.ErrAbortHandler, + } + + client := WithBazaar(x402http.NewHTTPFacilitatorClient(&x402http.FacilitatorConfig{ + URL: server.URL, + AuthProvider: authProvider, + })) + + _, err := client.ListDiscoveryResources(ctx, nil) + if err == nil { + t.Fatal("Expected error when auth provider fails") + } +} + +func TestListDiscoveryResources_MultipleItems(t *testing.T) { + ctx := context.Background() + + expectedResponse := DiscoveryResourcesResponse{ + X402Version: 2, + Items: []DiscoveryResource{ + { + Resource: "https://api.example.com/endpoint1", + Type: "http", + X402Version: 2, + Accepts: []json.RawMessage{json.RawMessage(`{"scheme":"exact"}`)}, + LastUpdated: "2026-03-01T00:00:00Z", + }, + { + Resource: "https://api.example.com/endpoint2", + Type: "http", + X402Version: 1, + Accepts: []json.RawMessage{json.RawMessage(`{"scheme":"exact"}`)}, + LastUpdated: "2026-02-28T00:00:00Z", + }, + { + Resource: "mcp://tools/search", + Type: "mcp", + X402Version: 2, + Accepts: []json.RawMessage{json.RawMessage(`{"scheme":"exact"}`)}, + LastUpdated: "2026-03-01T12:00:00Z", + }, + }, + Pagination: Pagination{ + Limit: 20, + Offset: 0, + Total: 3, + }, + } + + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.Header().Set("Content-Type", "application/json") + _ = json.NewEncoder(w).Encode(expectedResponse) + })) + defer server.Close() + + client := WithBazaar(x402http.NewHTTPFacilitatorClient(&x402http.FacilitatorConfig{ + URL: server.URL, + })) + + result, err := client.ListDiscoveryResources(ctx, nil) + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + + if len(result.Items) != 3 { + t.Fatalf("Expected 3 items, got %d", len(result.Items)) + } + if result.Items[0].Resource != "https://api.example.com/endpoint1" { + t.Errorf("Expected first resource https://api.example.com/endpoint1, got %s", result.Items[0].Resource) + } + if result.Items[2].Type != "mcp" { + t.Errorf("Expected third resource type mcp, got %s", result.Items[2].Type) + } + if result.Pagination.Total != 3 { + t.Errorf("Expected total 3, got %d", result.Pagination.Total) + } +} + +func TestListDiscoveryResources_ContextCancellation(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + cancel() // Cancel immediately + + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + t.Error("Request should not have been made with cancelled context") + w.WriteHeader(http.StatusOK) + })) + defer server.Close() + + client := WithBazaar(x402http.NewHTTPFacilitatorClient(&x402http.FacilitatorConfig{ + URL: server.URL, + })) + + _, err := client.ListDiscoveryResources(ctx, nil) + if err == nil { + t.Fatal("Expected error with cancelled context") + } +} + +func TestListDiscoveryResources_ConnectionError(t *testing.T) { + ctx := context.Background() + + // Use a URL that will fail to connect + client := WithBazaar(x402http.NewHTTPFacilitatorClient(&x402http.FacilitatorConfig{ + URL: "http://127.0.0.1:1", // Port 1 should fail + })) + + _, err := client.ListDiscoveryResources(ctx, nil) + if err == nil { + t.Fatal("Expected error for connection failure") + } +} diff --git a/go/extensions/bazaar/facilitator_test.go b/go/extensions/bazaar/facilitator_test.go new file mode 100644 index 0000000000..d7a8155685 --- /dev/null +++ b/go/extensions/bazaar/facilitator_test.go @@ -0,0 +1,108 @@ +package bazaar + +// Internal tests for unexported facilitator helpers. +// Uses package bazaar (not bazaar_test) to access unexported functions. + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestIsValidRouteTemplate(t *testing.T) { + t.Run("returns false for empty input", func(t *testing.T) { + assert.False(t, isValidRouteTemplate("")) + }) + + t.Run("returns false for paths not starting with /", func(t *testing.T) { + assert.False(t, isValidRouteTemplate("users/123")) + assert.False(t, isValidRouteTemplate("relative/path")) + assert.False(t, isValidRouteTemplate("no-slash")) + }) + + t.Run("returns false for paths containing ..", func(t *testing.T) { + assert.False(t, isValidRouteTemplate("/users/../admin")) + assert.False(t, isValidRouteTemplate("/../etc/passwd")) + assert.False(t, isValidRouteTemplate("/users/..")) + }) + + t.Run("returns false for paths containing ://", func(t *testing.T) { + assert.False(t, isValidRouteTemplate("http://evil.com/path")) + assert.False(t, isValidRouteTemplate("/users/http://evil")) + assert.False(t, isValidRouteTemplate("javascript://foo")) + }) + + t.Run("returns true for valid paths", func(t *testing.T) { + assert.True(t, isValidRouteTemplate("/users/:userId")) + assert.True(t, isValidRouteTemplate("/api/v1/items")) + assert.True(t, isValidRouteTemplate("/products/:productId/reviews/:reviewId")) + assert.True(t, isValidRouteTemplate("/weather/:country/:city")) + }) + + t.Run("returns false for paths with spaces or invalid characters", func(t *testing.T) { + assert.False(t, isValidRouteTemplate("/users/ bad")) + assert.False(t, isValidRouteTemplate("/path with spaces")) + }) + + t.Run("edge case: /users/..hidden is rejected (contains ..)", func(t *testing.T) { + assert.False(t, isValidRouteTemplate("/users/..hidden")) + }) + + t.Run("rejects percent-encoded traversal sequences", func(t *testing.T) { + assert.False(t, isValidRouteTemplate("/users/%2e%2e/admin")) + assert.False(t, isValidRouteTemplate("/users/%2E%2E/admin")) + }) +} + +func TestExtractPathParams(t *testing.T) { + t.Run("returns empty map when URL path has fewer segments than pattern (bracket)", func(t *testing.T) { + result := extractPathParams("/users/[userId]", "/api/other", true) + assert.Equal(t, map[string]string{}, result) + }) + + t.Run("extracts single param from matching path (bracket)", func(t *testing.T) { + result := extractPathParams("/users/[userId]", "/users/123", true) + assert.Equal(t, map[string]string{"userId": "123"}, result) + }) + + t.Run("extracts multiple params from matching path (bracket)", func(t *testing.T) { + result := extractPathParams("/users/[userId]/posts/[postId]", "/users/42/posts/7", true) + assert.Equal(t, map[string]string{"userId": "42", "postId": "7"}, result) + }) + + t.Run("extracts single param from matching path (colon)", func(t *testing.T) { + result := extractPathParams("/users/:userId", "/users/123", false) + assert.Equal(t, map[string]string{"userId": "123"}, result) + }) + + t.Run("extracts multiple params from matching path (colon)", func(t *testing.T) { + result := extractPathParams("/users/:userId/posts/:postId", "/users/42/posts/7", false) + assert.Equal(t, map[string]string{"userId": "42", "postId": "7"}, result) + }) + + t.Run("returns empty map when URL path mismatches (colon)", func(t *testing.T) { + result := extractPathParams("/users/:userId", "/api/other", false) + assert.Equal(t, map[string]string{}, result) + }) +} + +func TestNormalizeResourceURL(t *testing.T) { + t.Run("uses routeTemplate as canonical path when present", func(t *testing.T) { + result := normalizeResourceURL("https://api.example.com/users/123?foo=bar#frag", "/users/:userId") + assert.Equal(t, "https://api.example.com/users/:userId", result) + }) + + t.Run("strips query params and fragment when no routeTemplate", func(t *testing.T) { + result := normalizeResourceURL("https://api.example.com/search?q=test#section", "") + assert.Equal(t, "https://api.example.com/search", result) + }) + + t.Run("returns original URL on parse error with routeTemplate", func(t *testing.T) { + // url.Parse rarely fails but we exercise the fallback branch. + result := normalizeResourceURL("://invalid", "/route") + // Fallback: stripQueryParams is called, which may also fail on invalid URL, + // returning the original. + assert.NotEmpty(t, result) + }) + +} diff --git a/go/extensions/bazaar/resource_service.go b/go/extensions/bazaar/resource_service.go index 9a70d7d6ed..f56089c051 100644 --- a/go/extensions/bazaar/resource_service.go +++ b/go/extensions/bazaar/resource_service.go @@ -53,13 +53,25 @@ import ( // Example: map[string]interface{}{"success": true, "id": "123"}, // }, // ) +// +// DeclareDiscoveryExtensionOpts holds optional parameters for DeclareDiscoveryExtension. +type DeclareDiscoveryExtensionOpts struct { + PathParamsSchema types.JSONSchema +} + func DeclareDiscoveryExtension( method interface{}, // QueryParamMethods or BodyMethods input interface{}, inputSchema types.JSONSchema, bodyType types.BodyType, output *types.OutputConfig, + opts ...DeclareDiscoveryExtensionOpts, ) (types.DiscoveryExtension, error) { + var pathParamsSchema types.JSONSchema + if len(opts) > 0 { + pathParamsSchema = opts[0].PathParamsSchema + } + // Convert method to string var methodStr string switch m := method.(type) { @@ -74,12 +86,12 @@ func DeclareDiscoveryExtension( } if types.IsQueryMethod(methodStr) { - return createQueryDiscoveryExtension(types.QueryParamMethods(methodStr), input, inputSchema, output) + return createQueryDiscoveryExtension(types.QueryParamMethods(methodStr), input, inputSchema, pathParamsSchema, output) } else if types.IsBodyMethod(methodStr) { if bodyType == "" { bodyType = types.BodyTypeJSON } - return createBodyDiscoveryExtension(types.BodyMethods(methodStr), input, inputSchema, bodyType, output) + return createBodyDiscoveryExtension(types.BodyMethods(methodStr), input, inputSchema, pathParamsSchema, bodyType, output) } return types.DiscoveryExtension{}, fmt.Errorf("unsupported HTTP method: %s", methodStr) @@ -90,6 +102,7 @@ func createQueryDiscoveryExtension( method types.QueryParamMethods, input interface{}, inputSchema types.JSONSchema, + pathParamsSchema types.JSONSchema, output *types.OutputConfig, ) (types.DiscoveryExtension, error) { // Convert input to map if provided @@ -135,7 +148,10 @@ func createQueryDiscoveryExtension( "enum": []string{string(method)}, }, }, - "required": []string{"type", "method"}, + "required": []string{"type", "method"}, + // pathParams and method are not declared here at schema build time — + // the server extension's EnrichDeclaration adds them to both info and schema + // atomically at request time, keeping data and schema consistent. "additionalProperties": false, }, } @@ -153,6 +169,16 @@ func createQueryDiscoveryExtension( } } + if len(pathParamsSchema) > 0 { + inputProps := schemaProperties["input"].(map[string]interface{}) + props := inputProps["properties"].(map[string]interface{}) + pp := map[string]interface{}{"type": "object"} + for k, v := range pathParamsSchema { + pp[k] = v + } + props["pathParams"] = pp + } + // Add output schema if provided if output != nil && output.Example != nil { outputSchema := map[string]interface{}{ @@ -199,6 +225,7 @@ func createBodyDiscoveryExtension( method types.BodyMethods, input interface{}, inputSchema types.JSONSchema, + pathParamsSchema types.JSONSchema, bodyType types.BodyType, output *types.OutputConfig, ) (types.DiscoveryExtension, error) { @@ -243,11 +270,24 @@ func createBodyDiscoveryExtension( }, "body": inputSchema, }, - "required": []string{"type", "method", "bodyType", "body"}, + "required": []string{"type", "method", "bodyType", "body"}, + // pathParams and method are not declared here at schema build time — + // the server extension's EnrichDeclaration adds them to both info and schema + // atomically at request time, keeping data and schema consistent. "additionalProperties": false, }, } + if len(pathParamsSchema) > 0 { + inputProps := schemaProperties["input"].(map[string]interface{}) + props := inputProps["properties"].(map[string]interface{}) + pp := map[string]interface{}{"type": "object"} + for k, v := range pathParamsSchema { + pp[k] = v + } + props["pathParams"] = pp + } + // Add output schema if provided if output != nil && output.Example != nil { outputSchema := map[string]interface{}{ diff --git a/go/extensions/bazaar/server.go b/go/extensions/bazaar/server.go index 2a24a9c8b1..863b808b18 100644 --- a/go/extensions/bazaar/server.go +++ b/go/extensions/bazaar/server.go @@ -1,16 +1,131 @@ package bazaar import ( + "fmt" + "regexp" + "strings" + "sync" + "github.com/coinbase/x402/go/extensions/types" "github.com/coinbase/x402/go/http" ) +// bracketParamRegex matches [paramName] route segments (Next.js style). +var bracketParamRegex = regexp.MustCompile(`\[([^\]]+)\]`) + +// colonParamRegex is a package-local alias for the shared regex in extensions/types. +var colonParamRegex = types.ColonParamRegex + +// patternCache caches compiled capture regexes and param names per route pattern +// to avoid recompilation on every request. +type patternCacheEntry struct { + regex *regexp.Regexp + paramNames []string +} + +var patternCache sync.Map // map[string]*patternCacheEntry + +// normalizeWildcardPattern converts wildcard * segments to :var1, :var2, etc. +func normalizeWildcardPattern(pattern string) string { + if !strings.Contains(pattern, "*") { + return pattern + } + segments := strings.Split(pattern, "/") + counter := 0 + for i, seg := range segments { + if seg == "*" { + counter++ + segments[i] = fmt.Sprintf(":var%d", counter) + } + } + return strings.Join(segments, "/") +} + type bazaarResourceServerExtension struct{} func (e *bazaarResourceServerExtension) Key() string { return types.BAZAAR.Key() } +// extractDynamicRouteInfo converts a parameterized route pattern into a :param template +// and extracts concrete param values from the URL path in a single call. +// Supports both [param] (Next.js) and :param (Express) syntax. The output routeTemplate +// always uses :param syntax regardless of input format. +// Returns an empty routeTemplate and nil pathParams when routePattern has no param segments. +func extractDynamicRouteInfo(routePattern, urlPath string) (routeTemplate string, pathParams map[string]string) { + hasBracket := bracketParamRegex.MatchString(routePattern) + hasColon := colonParamRegex.MatchString(routePattern) + if !hasBracket && !hasColon { + return "", nil + } + // When both [param] and :param are present, normalize brackets to colons first + // so all params are extracted uniformly. + normalizedPattern := routePattern + if hasBracket { + normalizedPattern = bracketParamRegex.ReplaceAllString(routePattern, ":$1") + } + routeTemplate = normalizedPattern + pathParams = extractPathParams(normalizedPattern, urlPath, false) + return +} + +// getOrCompilePattern returns a cached capture regex and param names for the given +// route pattern, compiling and caching on first access. +func getOrCompilePattern(routePattern string, isBracket bool) *patternCacheEntry { + if cached, ok := patternCache.Load(routePattern); ok { + return cached.(*patternCacheEntry) + } + + paramRegex := colonParamRegex + if isBracket { + paramRegex = bracketParamRegex + } + matches := paramRegex.FindAllStringSubmatch(routePattern, -1) + paramNames := make([]string, 0, len(matches)) + for _, m := range matches { + paramNames = append(paramNames, m[1]) + } + + parts := paramRegex.Split(routePattern, -1) + regexParts := make([]string, 0, len(parts)+len(paramNames)) + for i, part := range parts { + regexParts = append(regexParts, regexp.QuoteMeta(part)) + if i < len(paramNames) { + regexParts = append(regexParts, "([^/]+)") + } + } + captureRegex, err := regexp.Compile("^" + strings.Join(regexParts, "") + "$") + if err != nil { + return &patternCacheEntry{paramNames: paramNames} + } + + entry := &patternCacheEntry{regex: captureRegex, paramNames: paramNames} + patternCache.Store(routePattern, entry) + return entry +} + +// extractPathParams extracts concrete path parameter values by matching a URL path +// against a route pattern containing [paramName] or :paramName segments. +func extractPathParams(routePattern, urlPath string, isBracket bool) map[string]string { + entry := getOrCompilePattern(routePattern, isBracket) + if entry.regex == nil { + return map[string]string{} + } + + submatches := entry.regex.FindStringSubmatch(urlPath) + if submatches == nil { + return map[string]string{} + } + + result := make(map[string]string, len(entry.paramNames)) + for i, name := range entry.paramNames { + if i+1 < len(submatches) { + result[name] = submatches[i+1] + } + } + return result +} + func (e *bazaarResourceServerExtension) EnrichDeclaration( declaration interface{}, transportContext interface{}, @@ -52,6 +167,46 @@ func (e *bazaarResourceServerExtension) EnrichDeclaration( } } + // Dynamic routes: translate [param]/:param → :param for the routeTemplate catalog key; + // pathParams carries runtime values (distinct from pathParamsSchema in the declaration). + // Wildcard * segments are auto-converted to :var1, :var2, etc. for catalog normalization. + var urlPath string + if httpContext.Adapter != nil { + urlPath = httpContext.Adapter.GetPath() + } + normalizedPattern := normalizeWildcardPattern(httpContext.RoutePattern) + routeTemplate, pathParams := extractDynamicRouteInfo(normalizedPattern, urlPath) + if routeTemplate != "" { + // Widen map[string]string to map[string]interface{} for the wire-level PathParams field + pathParamsIface := make(map[string]interface{}, len(pathParams)) + for k, v := range pathParams { + pathParamsIface[k] = v + } + + // Update input with pathParams + if queryInput, ok := extension.Info.Input.(types.QueryInput); ok { + queryInput.PathParams = pathParamsIface + extension.Info.Input = queryInput + } else if bodyInput, ok := extension.Info.Input.(types.BodyInput); ok { + bodyInput.PathParams = pathParamsIface + extension.Info.Input = bodyInput + } + + // Ensure pathParams is allowed in the schema (additionalProperties: false would reject it) + if schemaProps, ok := extension.Schema["properties"].(map[string]interface{}); ok { + if inputSchema, ok := schemaProps["input"].(map[string]interface{}); ok { + if props, ok := inputSchema["properties"].(map[string]interface{}); ok { + if _, hasPathParams := props["pathParams"]; !hasPathParams { + props["pathParams"] = map[string]interface{}{"type": "object"} + } + } + } + } + + extension.RouteTemplate = routeTemplate + return extension + } + return extension } diff --git a/go/extensions/erc20approvalgassponsor/resolve_signer_test.go b/go/extensions/erc20approvalgassponsor/resolve_signer_test.go new file mode 100644 index 0000000000..f455a59db5 --- /dev/null +++ b/go/extensions/erc20approvalgassponsor/resolve_signer_test.go @@ -0,0 +1,68 @@ +package erc20approvalgassponsor + +import ( + "context" + "math/big" + "testing" + + evm "github.com/coinbase/x402/go/mechanisms/evm" +) + +type mockApprovalSigner struct { + id string +} + +func (m *mockApprovalSigner) GetAddresses() []string { + return []string{"0x0000000000000000000000000000000000000001"} +} +func (m *mockApprovalSigner) ReadContract(_ context.Context, _ string, _ []byte, _ string, _ ...interface{}) (interface{}, error) { + return big.NewInt(0), nil +} +func (m *mockApprovalSigner) VerifyTypedData(_ context.Context, _ string, _ evm.TypedDataDomain, _ map[string][]evm.TypedDataField, _ string, _ map[string]interface{}, _ []byte) (bool, error) { + return true, nil +} +func (m *mockApprovalSigner) WriteContract(_ context.Context, _ string, _ []byte, _ string, _ ...interface{}) (string, error) { + return "0xtx", nil +} +func (m *mockApprovalSigner) SendTransaction(_ context.Context, _ string, _ []byte) (string, error) { + return "0xtx", nil +} +func (m *mockApprovalSigner) WaitForTransactionReceipt(_ context.Context, _ string) (*evm.TransactionReceipt, error) { + return &evm.TransactionReceipt{Status: evm.TxStatusSuccess}, nil +} +func (m *mockApprovalSigner) GetBalance(_ context.Context, _ string, _ string) (*big.Int, error) { + return big.NewInt(0), nil +} +func (m *mockApprovalSigner) GetChainID(_ context.Context) (*big.Int, error) { + return big.NewInt(8453), nil +} +func (m *mockApprovalSigner) GetCode(_ context.Context, _ string) ([]byte, error) { + return []byte{}, nil +} +func (m *mockApprovalSigner) SendTransactions(_ context.Context, _ []TransactionRequest) ([]string, error) { + return []string{"0xtx"}, nil +} + +func TestResolveSigner_UsesNetworkResolverFirst(t *testing.T) { + defaultSigner := &mockApprovalSigner{id: "default"} + baseSigner := &mockApprovalSigner{id: "base"} + ext := &Erc20ApprovalFacilitatorExtension{ + Signer: baseSigner, + SignerForNetwork: func(network string) Erc20ApprovalGasSponsoringSigner { + if network == "eip155:8453" { + return defaultSigner + } + return nil + }, + } + + resolved := ext.ResolveSigner("eip155:8453") + if resolved == nil || resolved.(*mockApprovalSigner).id != "default" { + t.Fatalf("expected network-specific signer, got %#v", resolved) + } + + resolved = ext.ResolveSigner("eip155:1") + if resolved == nil || resolved.(*mockApprovalSigner).id != "base" { + t.Fatalf("expected fallback base signer, got %#v", resolved) + } +} diff --git a/go/extensions/erc20approvalgassponsor/types.go b/go/extensions/erc20approvalgassponsor/types.go index b60a4e32db..a62d049f51 100644 --- a/go/extensions/erc20approvalgassponsor/types.go +++ b/go/extensions/erc20approvalgassponsor/types.go @@ -50,19 +50,65 @@ type Extension struct { Schema map[string]interface{} `json:"schema"` } -// Erc20ApprovalGasSponsoringSigner extends FacilitatorEvmSigner with raw transaction broadcasting. +// WriteContractCall encapsulates arguments for a WriteContract call, +// used by SendTransactions to describe an unsigned contract call operation. +type WriteContractCall struct { + Address string + ABI []byte + Function string + Args []interface{} +} + +// TransactionRequest represents a single transaction to be executed by the signer. +// Either Serialized (pre-signed raw transaction) or Call (unsigned intent) must be set. +type TransactionRequest struct { + // Serialized is a pre-signed raw transaction hex (0x-prefixed). + // When non-empty, the signer broadcasts it as-is via sendRawTransaction. + Serialized string + // Call is an unsigned contract write for the signer to sign and execute. + // Used when Serialized is empty. + Call *WriteContractCall +} + +// Erc20ApprovalGasSponsoringSigner extends FacilitatorEvmSigner with multi-transaction execution. +// The signer owns the execution strategy (sequential, batched, or atomic bundling via +// Flashbots, multicall, or smart account batching). type Erc20ApprovalGasSponsoringSigner interface { evm.FacilitatorEvmSigner - SendRawTransaction(ctx context.Context, signedTx string) (string, error) + SendTransactions(ctx context.Context, transactions []TransactionRequest) ([]string, error) +} + +// Erc20ApprovalGasSponsoringSimulator is an optional extension of Erc20ApprovalGasSponsoringSigner with multi-transaction simulation. +// The signer owns the simulation strategy. +type Erc20ApprovalGasSponsoringSimulator interface { + Erc20ApprovalGasSponsoringSigner + SimulateTransactions(ctx context.Context, transactions []TransactionRequest) (bool, error) } // Erc20ApprovalFacilitatorExtension carries the signer; registered with the facilitator. // It implements x402.FacilitatorExtension so it can be registered and retrieved via FacilitatorContext. type Erc20ApprovalFacilitatorExtension struct { Signer Erc20ApprovalGasSponsoringSigner + // Optional network-aware signer resolver. When provided, this takes precedence + // over Signer and allows different settlement signers per network. + SignerForNetwork func(network string) Erc20ApprovalGasSponsoringSigner } // Key returns the extension identifier. func (e *Erc20ApprovalFacilitatorExtension) Key() string { return ERC20ApprovalGasSponsoring.Key() } + +// ResolveSigner returns the signer to use for a given network. +// SignerForNetwork takes precedence when configured. +func (e *Erc20ApprovalFacilitatorExtension) ResolveSigner(network string) Erc20ApprovalGasSponsoringSigner { + if e == nil { + return nil + } + if e.SignerForNetwork != nil { + if signer := e.SignerForNetwork(network); signer != nil { + return signer + } + } + return e.Signer +} diff --git a/go/extensions/types/types.go b/go/extensions/types/types.go index 11b175c266..d445732067 100644 --- a/go/extensions/types/types.go +++ b/go/extensions/types/types.go @@ -10,6 +10,10 @@ import ( // BAZAAR is the extension identifier for the Bazaar discovery extension. var BAZAAR = x402.NewFacilitatorExtension("bazaar") +// ColonParamRegex matches :paramName route segments (Express style). +// Shared across http/server.go and extensions/bazaar/server.go to avoid drift. +var ColonParamRegex = regexp.MustCompile(`:([a-zA-Z_][a-zA-Z0-9_]*)`) + // Extension identifier constant for the Payment Identifier extension const PAYMENT_IDENTIFIER = "payment-identifier" @@ -60,6 +64,7 @@ type QueryInput struct { Type string `json:"type"` // "http" Method QueryParamMethods `json:"method"` QueryParams map[string]interface{} `json:"queryParams,omitempty"` + PathParams map[string]interface{} `json:"pathParams,omitempty"` Headers map[string]string `json:"headers,omitempty"` } @@ -76,6 +81,7 @@ type BodyInput struct { BodyType BodyType `json:"bodyType"` Body interface{} `json:"body"` QueryParams map[string]interface{} `json:"queryParams,omitempty"` + PathParams map[string]interface{} `json:"pathParams,omitempty"` Headers map[string]string `json:"headers,omitempty"` } @@ -145,8 +151,9 @@ type BodyDiscoveryExtension struct { // DiscoveryExtension is a union type that can be either Query or Body discovery extension type DiscoveryExtension struct { - Info DiscoveryInfo `json:"info"` - Schema JSONSchema `json:"schema"` + Info DiscoveryInfo `json:"info"` + Schema JSONSchema `json:"schema"` + RouteTemplate string `json:"routeTemplate,omitempty"` } // DeclareQueryDiscoveryConfig is the configuration for declaring a query discovery extension diff --git a/go/facilitator.go b/go/facilitator.go index d4fb4728c0..fdb5014555 100644 --- a/go/facilitator.go +++ b/go/facilitator.go @@ -435,7 +435,8 @@ func (f *x402Facilitator) verifyV1(ctx context.Context, payload types.PaymentPay } } - return nil, NewVerifyError(ErrNoFacilitatorForNetwork, "", fmt.Sprintf("no facilitator for scheme %s on network %s", scheme, network)) + registered := f.registeredV1Summary() + return nil, NewVerifyError(ErrNoFacilitatorForNetwork, "", fmt.Sprintf("no facilitator for scheme %q on network %q; registered: %s", scheme, network, registered)) } // verifyV2 verifies a V2 payment (internal, typed) @@ -460,7 +461,8 @@ func (f *x402Facilitator) verifyV2(ctx context.Context, payload types.PaymentPay } } - return nil, NewVerifyError(ErrNoFacilitatorForNetwork, "", fmt.Sprintf("no facilitator for scheme %s on network %s", scheme, network)) + registered := f.registeredV2Summary() + return nil, NewVerifyError(ErrNoFacilitatorForNetwork, "", fmt.Sprintf("no facilitator for scheme %q on network %q; registered: %s", scheme, network, registered)) } // settleV1 settles a V1 payment (internal, typed) @@ -485,7 +487,8 @@ func (f *x402Facilitator) settleV1(ctx context.Context, payload types.PaymentPay } } - return nil, NewSettleError(ErrNoFacilitatorForNetwork, "", network, "", fmt.Sprintf("no facilitator for scheme %s on network %s", scheme, network)) + registered := f.registeredV1Summary() + return nil, NewSettleError(ErrNoFacilitatorForNetwork, "", network, "", fmt.Sprintf("no facilitator for scheme %q on network %q; registered: %s", scheme, network, registered)) } // settleV2 settles a V2 payment (internal, typed) @@ -510,7 +513,38 @@ func (f *x402Facilitator) settleV2(ctx context.Context, payload types.PaymentPay } } - return nil, NewSettleError(ErrNoFacilitatorForNetwork, "", network, "", fmt.Sprintf("no facilitator for scheme %s on network %s", scheme, network)) + registered := f.registeredV2Summary() + return nil, NewSettleError(ErrNoFacilitatorForNetwork, "", network, "", fmt.Sprintf("no facilitator for scheme %q on network %q; registered: %s", scheme, network, registered)) +} + +// registeredV1Summary returns a human-readable list of registered V1 scheme/network pairs. +func (f *x402Facilitator) registeredV1Summary() string { + if len(f.schemesV1) == 0 { + return "(none)" + } + var parts []string + for _, data := range f.schemesV1 { + facilitator := data.facilitator.(SchemeNetworkFacilitatorV1) + for network := range data.networks { + parts = append(parts, fmt.Sprintf("%s@%s", facilitator.Scheme(), network)) + } + } + return strings.Join(parts, ", ") +} + +// registeredV2Summary returns a human-readable list of registered V2 scheme/network pairs. +func (f *x402Facilitator) registeredV2Summary() string { + if len(f.schemes) == 0 { + return "(none)" + } + var parts []string + for _, data := range f.schemes { + facilitator := data.facilitator.(SchemeNetworkFacilitator) + for network := range data.networks { + parts = append(parts, fmt.Sprintf("%s@%s", facilitator.Scheme(), network)) + } + } + return strings.Join(parts, ", ") } // GetSupported returns supported payment kinds diff --git a/go/facilitator_hooks_test.go b/go/facilitator_hooks_test.go index 06644dddac..0767a91f52 100644 --- a/go/facilitator_hooks_test.go +++ b/go/facilitator_hooks_test.go @@ -23,8 +23,6 @@ func TestFacilitatorBeforeVerifyHook_Abort(t *testing.T) { }) // Try to verify (should be aborted by hook) - // Note: Hooks are not fully integrated yet - this test validates hook registration works - // TODO: Integrate hooks into Verify execution payload := types.PaymentPayload{X402Version: 2, Payload: map[string]interface{}{}} requirements := types.PaymentRequirements{Scheme: "exact", Network: "eip155:8453"} diff --git a/go/go.mod b/go/go.mod index ee54c97400..7f498071a9 100644 --- a/go/go.mod +++ b/go/go.mod @@ -15,6 +15,13 @@ require ( github.com/xeipuuv/gojsonschema v1.2.0 ) +require github.com/google/uuid v1.6.0 + +require ( + github.com/google/uuid v1.6.0 + github.com/labstack/echo/v4 v4.15.1 +) + require ( filippo.io/edwards25519 v1.0.0-rc.1 // indirect github.com/Microsoft/go-winio v0.6.2 // indirect @@ -45,15 +52,15 @@ require ( github.com/goccy/go-json v0.10.4 // indirect github.com/goccy/go-yaml v1.18.0 // indirect github.com/google/jsonschema-go v0.4.2 // indirect - github.com/google/uuid v1.6.0 // indirect github.com/gorilla/websocket v1.4.2 // indirect github.com/holiman/uint256 v1.3.2 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/compress v1.16.0 // indirect github.com/klauspost/cpuid/v2 v2.3.0 // indirect + github.com/labstack/gommon v0.4.2 // indirect github.com/leodido/go-urn v1.4.0 // indirect github.com/logrusorgru/aurora v2.0.3+incompatible // indirect - github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mitchellh/go-testing-interface v1.14.1 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect @@ -70,6 +77,8 @@ require ( github.com/tklauser/numcpus v0.6.1 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/ugorji/go/codec v1.3.0 // indirect + github.com/valyala/bytebufferpool v1.0.0 // indirect + github.com/valyala/fasttemplate v1.2.2 // indirect github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f // indirect github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect github.com/yosida95/uritemplate/v3 v3.0.2 // indirect @@ -79,16 +88,16 @@ require ( go.uber.org/ratelimit v0.2.0 // indirect go.uber.org/zap v1.21.0 // indirect golang.org/x/arch v0.20.0 // indirect - golang.org/x/crypto v0.41.0 // indirect - golang.org/x/mod v0.27.0 // indirect - golang.org/x/net v0.43.0 // indirect + golang.org/x/crypto v0.46.0 // indirect + golang.org/x/mod v0.30.0 // indirect + golang.org/x/net v0.48.0 // indirect golang.org/x/oauth2 v0.30.0 // indirect - golang.org/x/sync v0.16.0 // indirect - golang.org/x/sys v0.36.0 // indirect - golang.org/x/term v0.34.0 // indirect - golang.org/x/text v0.28.0 // indirect - golang.org/x/time v0.9.0 // indirect - golang.org/x/tools v0.36.0 // indirect + golang.org/x/sync v0.19.0 // indirect + golang.org/x/sys v0.39.0 // indirect + golang.org/x/term v0.38.0 // indirect + golang.org/x/text v0.32.0 // indirect + golang.org/x/time v0.14.0 // indirect + golang.org/x/tools v0.39.0 // indirect google.golang.org/protobuf v1.36.9 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go/go.sum b/go/go.sum index b1a3f1d716..8232f4e582 100644 --- a/go/go.sum +++ b/go/go.sum @@ -159,15 +159,18 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/labstack/echo/v4 v4.15.1 h1:S9keusg26gZpjMmPqB5hOEvNKnmd1lNmcHrbbH2lnFs= +github.com/labstack/echo/v4 v4.15.1/go.mod h1:xmw1clThob0BSVRX1CRQkGQ/vjwcpOMjQZSZa9fKA/c= +github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0= +github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU= github.com/leanovate/gopter v0.2.11 h1:vRjThO1EKPb/1NsDXuDrzldR28RLkBflWYcU9CvzWu4= github.com/leanovate/gopter v0.2.11/go.mod h1:aK3tzZP/C+p1m3SPRE4SYZFGP7jjkuSI4f7Xvpt0S9c= github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= github.com/logrusorgru/aurora v2.0.3+incompatible h1:tOpm7WcpBTn4fjmVfgpQq0EfczGlG91VSDkswnjF5A8= github.com/logrusorgru/aurora v2.0.3+incompatible/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4= -github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= -github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= -github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= +github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU= @@ -266,6 +269,10 @@ github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4= github.com/urfave/cli/v2 v2.27.5 h1:WoHEJLdsXr6dDWoJgMq/CboDmyY/8HMMH1fTECbih+w= github.com/urfave/cli/v2 v2.27.5/go.mod h1:3Sevf16NykTbInEnD0yKkjDAeZDS0A6bzhBH5hrMvTQ= +github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= +github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= +github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo= +github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4= github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM= @@ -307,15 +314,15 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20220214200702-86341886e292/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4= -golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc= +golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU= +golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0= golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df h1:UA2aFVmmsIlefxMk29Dp2juaUSth8Pyn3Tq5Y5mJGME= golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc= golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ= -golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc= +golang.org/x/mod v0.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk= +golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= @@ -323,15 +330,15 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE= -golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg= +golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU= +golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY= golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI= golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= -golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= +golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -342,34 +349,33 @@ golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k= -golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= +golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.34.0 h1:O/2T7POpk0ZZ7MAzMeWFSg6S5IpWd/RXDlM9hgM3DR4= -golang.org/x/term v0.34.0/go.mod h1:5jC53AEywhIVebHgPVeg0mj8OD3VO9OzclacVrqpaAw= +golang.org/x/term v0.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q= +golang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng= -golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU= -golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY= -golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU= +golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY= +golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= +golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg= -golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s= +golang.org/x/tools v0.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ= +golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/go/http/echo/README.md b/go/http/echo/README.md new file mode 100644 index 0000000000..21cbe52697 --- /dev/null +++ b/go/http/echo/README.md @@ -0,0 +1,350 @@ +# x402 Echo Middleware + +Echo middleware integration for the x402 Payment Protocol. This package provides middleware for adding x402 payment requirements to your Echo applications. + +## Installation + +```bash +go get github.com/coinbase/x402/go +``` + +## Quick Start + +```go +package main + +import ( + x402http "github.com/coinbase/x402/go/http" + echomw "github.com/coinbase/x402/go/http/echo" + evm "github.com/coinbase/x402/go/mechanisms/evm/exact/server" + "github.com/labstack/echo/v4" +) + +func main() { + e := echo.New() + + facilitator := x402http.NewHTTPFacilitatorClient(&x402http.FacilitatorConfig{ + URL: "https://facilitator.x402.org", + }) + + routes := x402http.RoutesConfig{ + "GET /protected": { + Accepts: x402http.PaymentOptions{ + { + Scheme: "exact", + PayTo: "0xYourAddress", + Price: "$0.10", + Network: "eip155:84532", + }, + }, + Description: "Access to premium content", + }, + } + + e.Use(echomw.PaymentMiddlewareFromConfig(routes, + echomw.WithFacilitatorClient(facilitator), + echomw.WithScheme("eip155:*", evm.NewExactEvmScheme()), + )) + + e.GET("/protected", func(c echo.Context) error { + return c.JSON(200, map[string]interface{}{"message": "This content is behind a paywall"}) + }) + + e.Logger.Fatal(e.Start(":8080")) +} +``` + +## Configuration + +There are two approaches to configuring the middleware: + +### 1. PaymentMiddlewareFromConfig (Functional Options) + +Use `PaymentMiddlewareFromConfig` with functional options: + +```go +e.Use(echomw.PaymentMiddlewareFromConfig(routes, + echomw.WithFacilitatorClient(facilitator), + echomw.WithScheme("eip155:*", evm.NewExactEvmScheme()), +)) +``` + +### 2. PaymentMiddlewareFromHTTPServer (Pre-configured Server) + +Use `PaymentMiddlewareFromHTTPServer` when you need to configure the server separately (e.g., with lifecycle hooks): + +```go +server := x402.Newx402ResourceServer( + x402.WithFacilitatorClient(facilitator), +). + Register("eip155:*", evm.NewExactEvmScheme()). + OnAfterSettle(func(ctx x402.SettleResultContext) error { + log.Printf("Payment settled: %s", ctx.Result.Transaction) + return nil + }) + +httpServer := x402http.Wrappedx402HTTPResourceServer(routes, server) + +e.Use(echomw.PaymentMiddlewareFromHTTPServer(httpServer)) +``` + +### Middleware Options + +- `WithFacilitatorClient(client)` - Add a facilitator client +- `WithScheme(network, server)` - Register a payment scheme +- `WithPaywallConfig(config)` - Configure paywall UI +- `WithSyncFacilitatorOnStart(bool)` - Sync with facilitator on startup (default: true) +- `WithTimeout(duration)` - Set payment operation timeout (default: 30s) +- `WithErrorHandler(handler)` - Custom error handler +- `WithSettlementHandler(handler)` - Settlement callback + +## Route Configuration + +Define which routes require payment: + +```go +routes := x402http.RoutesConfig{ + "GET /api/data": { + Accepts: x402http.PaymentOptions{ + { + Scheme: "exact", + PayTo: "0xYourAddress", + Price: "$0.10", + Network: "eip155:84532", + }, + }, + Description: "API data access", + }, + "POST /api/compute": { + Accepts: x402http.PaymentOptions{ + { + Scheme: "exact", + PayTo: "0xYourAddress", + Price: "$1.00", + Network: "eip155:8453", + }, + }, + Description: "Compute operation", + }, +} +``` + +Routes support wildcards: +- `"GET /api/premium/*"` - Matches all GET requests under `/api/premium/` +- `"* /api/data"` - Matches all HTTP methods to `/api/data` + +## Paywall Configuration + +Configure the paywall UI for browser requests: + +```go +paywallConfig := &x402http.PaywallConfig{ + AppName: "My API Service", + AppLogo: "https://myapp.com/logo.svg", + Testnet: true, +} + +e.Use(echomw.PaymentMiddlewareFromConfig(routes, + echomw.WithFacilitatorClient(facilitator), + echomw.WithScheme("eip155:*", evm.NewExactEvmScheme()), + echomw.WithPaywallConfig(paywallConfig), +)) +``` + +The paywall includes: +- EVM wallet support (MetaMask, Coinbase Wallet, etc.) +- Solana wallet support (Phantom, Solflare, etc.) +- USDC balance checking and chain switching +- Onramp integration for mainnet + +## Advanced Usage + +### Multiple Payment Schemes + +Register schemes for different networks: + +```go +import ( + evm "github.com/coinbase/x402/go/mechanisms/evm/exact/server" + svm "github.com/coinbase/x402/go/mechanisms/svm/exact/server" +) + +e.Use(echomw.PaymentMiddlewareFromConfig(routes, + echomw.WithFacilitatorClient(facilitator), + echomw.WithScheme("eip155:*", evm.NewExactEvmScheme()), + echomw.WithScheme("solana:*", svm.NewExactSvmScheme()), +)) +``` + +### Custom Facilitator Client + +Configure with custom authentication: + +```go +facilitator := x402http.NewHTTPFacilitatorClient(&x402http.FacilitatorConfig{ + URL: "https://your-facilitator.com", + CreateAuthHeaders: func() (*x402http.FacilitatorAuthHeaders, error) { + return &x402http.FacilitatorAuthHeaders{ + Verify: map[string]string{"Authorization": "Bearer verify-token"}, + Settle: map[string]string{"Authorization": "Bearer settle-token"}, + }, nil + }, +}) +``` + +### Settlement Handler + +Track successful payments: + +```go +e.Use(echomw.PaymentMiddlewareFromConfig(routes, + echomw.WithFacilitatorClient(facilitator), + echomw.WithScheme("eip155:*", evm.NewExactEvmScheme()), + echomw.WithSettlementHandler(func(c echo.Context, settlement *x402.SettleResponse) { + log.Printf("Payment settled - Payer: %s, Tx: %s", + settlement.Payer, + settlement.Transaction, + ) + c.Response().Header().Set("X-Payment-Receipt", settlement.Transaction) + }), +)) +``` + +### Settlement Overrides (Upto Scheme) + +For the upto scheme, route handlers specify the actual settlement amount via `SetSettlementOverrides`: + +```go +e.GET("/api/metered", func(c echo.Context) error { + usage := calculateUsage(c) + echomw.SetSettlementOverrides(c, &x402.SettlementOverrides{Amount: usage}) + + return c.JSON(http.StatusOK, map[string]interface{}{"result": "ok"}) +}) +``` + +### Error Handler + +Custom error handling: + +```go +e.Use(echomw.PaymentMiddlewareFromConfig(routes, + echomw.WithFacilitatorClient(facilitator), + echomw.WithScheme("eip155:*", evm.NewExactEvmScheme()), + echomw.WithErrorHandler(func(c echo.Context, err error) { + log.Printf("Payment error: %v", err) + c.JSON(http.StatusPaymentRequired, map[string]interface{}{ + "error": err.Error(), + }) + }), +)) +``` + +## Convenience Functions + +### X402Payment (Config Struct) + +With struct-based configuration: + +```go +e.Use(echomw.X402Payment(echomw.Config{ + Routes: routes, + Facilitator: facilitator, + Schemes: []echomw.SchemeConfig{{Network: "eip155:*", Server: evm.NewExactEvmScheme()}}, + Timeout: 30 * time.Second, +})) +``` + +### SimpleX402Payment + +Apply payment requirements to all routes: + +```go +e.Use(echomw.SimpleX402Payment( + "0xYourAddress", + "$0.10", + "eip155:84532", + "https://facilitator.x402.org", +)) +``` + +## Complete Example + +```go +package main + +import ( + "log" + "net/http" + "time" + + x402 "github.com/coinbase/x402/go" + x402http "github.com/coinbase/x402/go/http" + echomw "github.com/coinbase/x402/go/http/echo" + evm "github.com/coinbase/x402/go/mechanisms/evm/exact/server" + "github.com/labstack/echo/v4" +) + +func main() { + e := echo.New() + + facilitator := x402http.NewHTTPFacilitatorClient(&x402http.FacilitatorConfig{ + URL: "https://facilitator.x402.org", + }) + + routes := x402http.RoutesConfig{ + "GET /api/data": { + Accepts: x402http.PaymentOptions{ + { + Scheme: "exact", + PayTo: "0xYourAddress", + Price: "$0.10", + Network: "eip155:84532", + }, + }, + Description: "Basic data access", + }, + "POST /api/compute": { + Accepts: x402http.PaymentOptions{ + { + Scheme: "exact", + PayTo: "0xYourAddress", + Price: "$1.00", + Network: "eip155:8453", + }, + }, + Description: "Compute operation", + }, + } + + paywallConfig := &x402http.PaywallConfig{ + AppName: "My API Service", + AppLogo: "/logo.svg", + Testnet: true, + } + + e.Use(echomw.PaymentMiddlewareFromConfig(routes, + echomw.WithFacilitatorClient(facilitator), + echomw.WithScheme("eip155:*", evm.NewExactEvmScheme()), + echomw.WithPaywallConfig(paywallConfig), + echomw.WithTimeout(60*time.Second), + echomw.WithSettlementHandler(func(c echo.Context, settlement *x402.SettleResponse) { + log.Printf("Payment settled: %s", settlement.Transaction) + }), + )) + + e.GET("/api/data", func(c echo.Context) error { + return c.JSON(http.StatusOK, map[string]interface{}{"data": "Protected content"}) + }) + + e.POST("/api/compute", func(c echo.Context) error { + return c.JSON(http.StatusOK, map[string]interface{}{"result": "Computation complete"}) + }) + + e.GET("/", func(c echo.Context) error { + return c.JSON(http.StatusOK, map[string]interface{}{"message": "Welcome"}) + }) + + e.Logger.Fatal(e.Start(":8080")) +} +``` diff --git a/go/http/echo/builder.go b/go/http/echo/builder.go new file mode 100644 index 0000000000..35189f55dd --- /dev/null +++ b/go/http/echo/builder.go @@ -0,0 +1,171 @@ +package echo + +import ( + "time" + + x402 "github.com/coinbase/x402/go" + x402http "github.com/coinbase/x402/go/http" + "github.com/labstack/echo/v4" +) + +// Config provides struct-based configuration for x402 payment middleware. +// This is a cleaner alternative to the variadic options pattern. +type Config struct { + // Routes maps HTTP patterns to payment requirements + Routes x402http.RoutesConfig + + // Facilitator is a single facilitator client (most common case) + // Use this OR Facilitators (not both) + Facilitator x402.FacilitatorClient + + // Facilitators is an array of facilitator clients (for fallback/redundancy) + // Use this OR Facilitator (not both) + Facilitators []x402.FacilitatorClient + + // Schemes to register with the server + Schemes []SchemeConfig + + // PaywallConfig for browser-based payment UI (optional) + PaywallConfig *x402http.PaywallConfig + + // SyncFacilitatorOnStart fetches supported kinds from facilitators on startup + // Default: true + SyncFacilitatorOnStart bool + + // Timeout for payment operations + // Default: 30 seconds + Timeout time.Duration + + // ErrorHandler for custom error handling (optional) + ErrorHandler func(echo.Context, error) + + // SettlementHandler called after successful settlement (optional) + SettlementHandler func(echo.Context, *x402.SettleResponse) +} + +// SchemeConfig configures a payment scheme for a network. +type SchemeConfig struct { + Network x402.Network + Server x402.SchemeNetworkServer +} + +// X402Payment creates payment middleware using struct-based configuration. +// This is a cleaner, more readable alternative to PaymentMiddlewareFromConfig with variadic options. +// +// Example: +// +// e.Use(echomw.X402Payment(echomw.Config{ +// Routes: routes, +// Facilitator: facilitatorClient, +// Schemes: []echomw.SchemeConfig{ +// {Network: "eip155:*", Server: evm.NewExactEvmServer()}, +// {Network: "solana:*", Server: svm.NewExactSvmServer()}, +// }, +// SyncFacilitatorOnStart: true, +// Timeout: 30 * time.Second, +// })) +func X402Payment(config Config) echo.MiddlewareFunc { + // Set defaults + if config.Timeout == 0 { + config.Timeout = 30 * time.Second + } + + // Default to sync unless explicitly disabled + syncOnStart := config.SyncFacilitatorOnStart + if !syncOnStart && config.Facilitator == nil && len(config.Facilitators) == 0 { + // If no explicit setting and no facilitators, default to false + syncOnStart = false + } else if config.Facilitator != nil || len(config.Facilitators) > 0 { + // If facilitators provided but SyncFacilitatorOnStart not explicitly set, default to true + if config.Timeout != 0 { + // User set something, so they're configuring - default to true + syncOnStart = true + } + } + + // Normalize facilitators list + var facilitators []x402.FacilitatorClient + if config.Facilitator != nil { + facilitators = append(facilitators, config.Facilitator) + } + facilitators = append(facilitators, config.Facilitators...) + + // Convert to middleware options + opts := []MiddlewareOption{ + WithSyncFacilitatorOnStart(syncOnStart), + WithTimeout(config.Timeout), + } + + // Add facilitators + for _, facilitator := range facilitators { + opts = append(opts, WithFacilitatorClient(facilitator)) + } + + // Add schemes + for _, scheme := range config.Schemes { + opts = append(opts, WithScheme(scheme.Network, scheme.Server)) + } + + // Add optional handlers + if config.PaywallConfig != nil { + opts = append(opts, WithPaywallConfig(config.PaywallConfig)) + } + if config.ErrorHandler != nil { + opts = append(opts, WithErrorHandler(config.ErrorHandler)) + } + if config.SettlementHandler != nil { + opts = append(opts, WithSettlementHandler(config.SettlementHandler)) + } + + // Delegate to PaymentMiddlewareFromConfig (reuse all logic) + return PaymentMiddlewareFromConfig(config.Routes, opts...) +} + +// SimpleX402Payment creates middleware with minimal configuration. +// Uses a single route pattern and facilitator for the simplest possible setup. +// +// Args: +// +// payTo: Payment recipient address +// price: Payment amount (e.g., "$0.001") +// network: Payment network +// facilitatorURL: Facilitator server URL +// +// Returns: +// +// Echo middleware function +// +// Example: +// +// e.Use(echomw.SimpleX402Payment( +// "0x123...", +// "$0.001", +// "eip155:8453", +// "https://facilitator.example.com", +// )) +func SimpleX402Payment(payTo string, price string, network x402.Network, facilitatorURL string) echo.MiddlewareFunc { + // Create facilitator client + facilitator := x402http.NewHTTPFacilitatorClient(&x402http.FacilitatorConfig{ + URL: facilitatorURL, + }) + + // Create routes for all endpoints + routes := x402http.RoutesConfig{ + "*": { + Accepts: []x402http.PaymentOption{ + { + Scheme: "exact", + PayTo: payTo, + Price: x402.Price(price), + Network: network, + }, + }, + }, + } + + return X402Payment(Config{ + Routes: routes, + Facilitator: facilitator, + SyncFacilitatorOnStart: true, + }) +} diff --git a/go/http/echo/middleware.go b/go/http/echo/middleware.go new file mode 100644 index 0000000000..81a5eb501e --- /dev/null +++ b/go/http/echo/middleware.go @@ -0,0 +1,483 @@ +package echo + +import ( + "bytes" + "context" + "fmt" + "net/http" + "sync" + "time" + + x402 "github.com/coinbase/x402/go" + "github.com/coinbase/x402/go/extensions/bazaar" + x402http "github.com/coinbase/x402/go/http" + "github.com/labstack/echo/v4" +) + +// SetSettlementOverrides sets settlement overrides on the Echo response for partial settlement. +// The middleware extracts these before settlement and strips the header from the client response. +func SetSettlementOverrides(c echo.Context, overrides *x402.SettlementOverrides) { + c.Response().Header().Set(x402http.SettlementOverridesHeader, x402http.MarshalSettlementOverrides(overrides)) +} + +// ============================================================================ +// Echo Adapter Implementation +// ============================================================================ + +// EchoAdapter implements HTTPAdapter for Echo framework +type EchoAdapter struct { + ctx echo.Context +} + +// NewEchoAdapter creates a new Echo adapter +func NewEchoAdapter(ctx echo.Context) *EchoAdapter { + return &EchoAdapter{ctx: ctx} +} + +// GetHeader gets a request header +func (a *EchoAdapter) GetHeader(name string) string { + return a.ctx.Request().Header.Get(name) +} + +// GetMethod gets the HTTP method +func (a *EchoAdapter) GetMethod() string { + return a.ctx.Request().Method +} + +// GetPath gets the request path +func (a *EchoAdapter) GetPath() string { + return a.ctx.Request().URL.Path +} + +// GetURL gets the full request URL +func (a *EchoAdapter) GetURL() string { + req := a.ctx.Request() + scheme := "http" + if req.TLS != nil { + scheme = "https" + } + host := req.Host + if host == "" { + host = req.Header.Get("Host") + } + return fmt.Sprintf("%s://%s%s", scheme, host, req.RequestURI) +} + +// GetAcceptHeader gets the Accept header +func (a *EchoAdapter) GetAcceptHeader() string { + return a.ctx.Request().Header.Get("Accept") +} + +// GetUserAgent gets the User-Agent header +func (a *EchoAdapter) GetUserAgent() string { + return a.ctx.Request().Header.Get("User-Agent") +} + +// ============================================================================ +// Middleware Configuration +// ============================================================================ + +// MiddlewareConfig configures the payment middleware +type MiddlewareConfig struct { + // Routes configuration + Routes x402http.RoutesConfig + + // Facilitator client(s) + FacilitatorClients []x402.FacilitatorClient + + // Scheme registrations + Schemes []SchemeRegistration + + // Paywall configuration + PaywallConfig *x402http.PaywallConfig + + // Sync with facilitator on start + SyncFacilitatorOnStart bool + + // Custom error handler + ErrorHandler func(echo.Context, error) + + // Custom settlement handler + SettlementHandler func(echo.Context, *x402.SettleResponse) + + // Context timeout for payment operations + Timeout time.Duration +} + +// SchemeRegistration registers a scheme with the server +type SchemeRegistration struct { + Network x402.Network + Server x402.SchemeNetworkServer +} + +// MiddlewareOption configures the middleware +type MiddlewareOption func(*MiddlewareConfig) + +// WithFacilitatorClient adds a facilitator client +func WithFacilitatorClient(client x402.FacilitatorClient) MiddlewareOption { + return func(c *MiddlewareConfig) { + c.FacilitatorClients = append(c.FacilitatorClients, client) + } +} + +// WithScheme registers a scheme server +func WithScheme(network x402.Network, schemeServer x402.SchemeNetworkServer) MiddlewareOption { + return func(c *MiddlewareConfig) { + c.Schemes = append(c.Schemes, SchemeRegistration{ + Network: network, + Server: schemeServer, + }) + } +} + +// WithPaywallConfig sets the paywall configuration +func WithPaywallConfig(config *x402http.PaywallConfig) MiddlewareOption { + return func(c *MiddlewareConfig) { + c.PaywallConfig = config + } +} + +// WithSyncFacilitatorOnStart sets whether to sync with facilitator on startup +func WithSyncFacilitatorOnStart(sync bool) MiddlewareOption { + return func(c *MiddlewareConfig) { + c.SyncFacilitatorOnStart = sync + } +} + +// WithErrorHandler sets a custom error handler +func WithErrorHandler(handler func(echo.Context, error)) MiddlewareOption { + return func(c *MiddlewareConfig) { + c.ErrorHandler = handler + } +} + +// WithSettlementHandler sets a custom settlement handler +func WithSettlementHandler(handler func(echo.Context, *x402.SettleResponse)) MiddlewareOption { + return func(c *MiddlewareConfig) { + c.SettlementHandler = handler + } +} + +// WithTimeout sets the context timeout for payment operations +func WithTimeout(timeout time.Duration) MiddlewareOption { + return func(c *MiddlewareConfig) { + c.Timeout = timeout + } +} + +// ============================================================================ +// Payment Middleware +// ============================================================================ + +// PaymentMiddleware creates Echo middleware for x402 payment handling using a pre-configured server. +func PaymentMiddleware(routes x402http.RoutesConfig, server *x402.X402ResourceServer, opts ...MiddlewareOption) echo.MiddlewareFunc { + config := &MiddlewareConfig{ + Routes: routes, + SyncFacilitatorOnStart: true, + Timeout: 30 * time.Second, + } + + // Apply options + for _, opt := range opts { + opt(config) + } + + // Wrap the resource server with HTTP functionality + httpServer := x402http.Wrappedx402HTTPResourceServer(routes, server) + + httpServer.RegisterExtension(bazaar.BazaarResourceServerExtension) + + // Initialize if requested - queries facilitator /supported to populate facilitatorClients map + if config.SyncFacilitatorOnStart { + ctx, cancel := context.WithTimeout(context.Background(), config.Timeout) + defer cancel() + if err := httpServer.Initialize(ctx); err != nil { + fmt.Printf("Warning: failed to initialize x402 server: %v\n", err) + } + } + + // Create middleware handler using shared logic + return createMiddlewareHandler(httpServer, config) +} + +// PaymentMiddlewareFromHTTPServer creates Echo middleware using a pre-configured HTTPServer. +// This allows registering hooks (e.g., OnProtectedRequest) on the server before attaching to the router. +// +// Example: +// +// resourceServer := x402.Newx402ResourceServer( +// x402.WithFacilitatorClient(facilitator), +// ).Register("eip155:*", evm.NewExactEvmScheme()) +// +// httpServer := x402http.Wrappedx402HTTPResourceServer(routes, resourceServer). +// OnProtectedRequest(requestHook) +// +// e.Use(echomw.PaymentMiddlewareFromHTTPServer(httpServer)) +func PaymentMiddlewareFromHTTPServer(httpServer *x402http.HTTPServer, opts ...MiddlewareOption) echo.MiddlewareFunc { + config := &MiddlewareConfig{ + SyncFacilitatorOnStart: true, + Timeout: 30 * time.Second, + } + + // Apply options + for _, opt := range opts { + opt(config) + } + + httpServer.RegisterExtension(bazaar.BazaarResourceServerExtension) + + // Initialize if requested - queries facilitator /supported to populate facilitatorClients map + if config.SyncFacilitatorOnStart { + ctx, cancel := context.WithTimeout(context.Background(), config.Timeout) + defer cancel() + if err := httpServer.Initialize(ctx); err != nil { + fmt.Printf("Warning: failed to initialize x402 server: %v\n", err) + } + } + + // Create middleware handler using shared logic + return createMiddlewareHandler(httpServer, config) +} + +// PaymentMiddlewareFromConfig creates Echo middleware for x402 payment handling. +// This creates the server internally from the provided options. +func PaymentMiddlewareFromConfig(routes x402http.RoutesConfig, opts ...MiddlewareOption) echo.MiddlewareFunc { + config := &MiddlewareConfig{ + Routes: routes, + FacilitatorClients: []x402.FacilitatorClient{}, + Schemes: []SchemeRegistration{}, + SyncFacilitatorOnStart: true, + Timeout: 30 * time.Second, + } + + // Apply options + for _, opt := range opts { + opt(config) + } + + serverOpts := []x402.ResourceServerOption{} + for _, client := range config.FacilitatorClients { + serverOpts = append(serverOpts, x402.WithFacilitatorClient(client)) + } + + httpServer := x402http.Newx402HTTPResourceServer(config.Routes, serverOpts...) + + httpServer.RegisterExtension(bazaar.BazaarResourceServerExtension) + + // Register schemes + for _, scheme := range config.Schemes { + httpServer.Register(scheme.Network, scheme.Server) + } + + // Initialize if requested - queries facilitator /supported to populate facilitatorClients map + if config.SyncFacilitatorOnStart { + ctx, cancel := context.WithTimeout(context.Background(), config.Timeout) + defer cancel() + if err := httpServer.Initialize(ctx); err != nil { + fmt.Printf("Warning: failed to initialize x402 server: %v\n", err) + } + } + + // Create middleware handler + return createMiddlewareHandler(httpServer, config) +} + +// createMiddlewareHandler creates the actual Echo middleware function. +func createMiddlewareHandler(server *x402http.HTTPServer, config *MiddlewareConfig) echo.MiddlewareFunc { + return func(next echo.HandlerFunc) echo.HandlerFunc { + return func(c echo.Context) error { + // Create adapter and request context + adapter := NewEchoAdapter(c) + reqCtx := x402http.HTTPRequestContext{ + Adapter: adapter, + Path: c.Request().URL.Path, + Method: c.Request().Method, + } + + // Check if route requires payment before waiting for initialization + if !server.RequiresPayment(reqCtx) { + return next(c) + } + + // Create context with timeout + ctx, cancel := context.WithTimeout(c.Request().Context(), config.Timeout) + defer cancel() + + result := server.ProcessHTTPRequest(ctx, reqCtx, config.PaywallConfig) + + // Handle result + switch result.Type { + case x402http.ResultNoPaymentRequired: + // No payment required, continue to next handler + return next(c) + + case x402http.ResultPaymentError: + // Payment required but not provided or invalid + return handlePaymentError(c, result.Response) + + case x402http.ResultPaymentVerified: + // Payment verified, continue with settlement handling + return handlePaymentVerified(c, next, server, ctx, reqCtx, result, config) + + default: + return next(c) + } + } + } +} + +// handlePaymentError handles payment error responses +func handlePaymentError(c echo.Context, response *x402http.HTTPResponseInstructions) error { + // Set headers + for key, value := range response.Headers { + c.Response().Header().Set(key, value) + } + + // Send response body + if response.IsHTML { + return c.HTMLBlob(response.Status, []byte(response.Body.(string))) + } + return c.JSON(response.Status, response.Body) +} + +// handlePaymentVerified handles verified payments with settlement +func handlePaymentVerified(c echo.Context, next echo.HandlerFunc, server *x402http.HTTPServer, ctx context.Context, reqCtx x402http.HTTPRequestContext, result x402http.HTTPProcessResult, config *MiddlewareConfig) error { + // Capture response for settlement + origWriter := c.Response().Writer + capture := &responseCapture{ + ResponseWriter: origWriter, + body: &bytes.Buffer{}, + statusCode: http.StatusOK, + } + c.Response().Writer = capture + + // Set payment data in context for downstream handlers + if result.PaymentPayload != nil { + c.Set("x402_payload", *result.PaymentPayload) + } + if result.PaymentRequirements != nil { + c.Set("x402_requirements", *result.PaymentRequirements) + } + + // Continue to protected handler + err := next(c) + + // Restore original writer + c.Response().Writer = origWriter + c.Response().Committed = false + + // If handler returned error, propagate it + if err != nil { + return err + } + + // Don't settle if response failed + if capture.statusCode >= 400 { + // Write captured error response + origWriter.WriteHeader(capture.statusCode) + _, _ = origWriter.Write(capture.body.Bytes()) + return nil + } + + settleResult := server.ProcessSettlement( + ctx, + *result.PaymentPayload, + *result.PaymentRequirements, + nil, + &x402http.HTTPTransportContext{ + Request: &reqCtx, + ResponseBody: capture.body.Bytes(), + ResponseHeaders: capture.Header(), + }, + ) + + // Check settlement success + if !settleResult.Success { + // Always set PAYMENT-RESPONSE header on settlement failure + for key, value := range settleResult.Headers { + origWriter.Header().Set(key, value) + } + switch { + case config.ErrorHandler != nil: + errorReason := settleResult.ErrorReason + if errorReason == "" { + errorReason = "Settlement failed" + } + config.ErrorHandler(c, fmt.Errorf("settlement failed: %s", errorReason)) + case settleResult.Response != nil: + return handlePaymentError(c, settleResult.Response) + default: + return c.JSON(http.StatusPaymentRequired, map[string]interface{}{}) + } + return nil + } + + // Add settlement headers + for key, value := range settleResult.Headers { + origWriter.Header().Set(key, value) + } + + // Call settlement handler if configured + if config.SettlementHandler != nil { + settleResponse := &x402.SettleResponse{ + Success: true, + Transaction: settleResult.Transaction, + Network: settleResult.Network, + Payer: settleResult.Payer, + } + config.SettlementHandler(c, settleResponse) + } + + // Write captured response + origWriter.WriteHeader(capture.statusCode) + _, _ = origWriter.Write(capture.body.Bytes()) + return nil +} + +// ============================================================================ +// Response Capture +// ============================================================================ + +// responseCapture captures the response for settlement processing +type responseCapture struct { + http.ResponseWriter + body *bytes.Buffer + statusCode int + written bool + mu sync.Mutex +} + +// WriteHeader captures the status code +func (w *responseCapture) WriteHeader(code int) { + w.mu.Lock() + defer w.mu.Unlock() + + w.writeHeaderLocked(code) +} + +// writeHeaderLocked sets the status code (must be called with lock held) +func (w *responseCapture) writeHeaderLocked(code int) { + if !w.written { + w.statusCode = code + w.written = true + } +} + +// Write captures the response body +func (w *responseCapture) Write(data []byte) (int, error) { + w.mu.Lock() + defer w.mu.Unlock() + + if !w.written { + w.writeHeaderLocked(http.StatusOK) + } + return w.body.Write(data) +} + +// WriteString captures string responses +func (w *responseCapture) WriteString(s string) (int, error) { + return w.Write([]byte(s)) +} + +// Flush is a no-op to prevent premature flushing to the wire before settlement. +func (w *responseCapture) Flush() {} diff --git a/go/http/echo/middleware_test.go b/go/http/echo/middleware_test.go new file mode 100644 index 0000000000..6500357a33 --- /dev/null +++ b/go/http/echo/middleware_test.go @@ -0,0 +1,1339 @@ +package echo + +import ( + "bytes" + "context" + "encoding/base64" + "encoding/json" + "net/http" + "net/http/httptest" + "testing" + "time" + + x402 "github.com/coinbase/x402/go" + x402http "github.com/coinbase/x402/go/http" + "github.com/coinbase/x402/go/types" + "github.com/labstack/echo/v4" +) + +// ============================================================================ +// Mock Implementations +// ============================================================================ + +// mockSchemeServer implements x402.SchemeNetworkServer for testing +type mockSchemeServer struct { + scheme string +} + +func (m *mockSchemeServer) Scheme() string { + return m.scheme +} + +func (m *mockSchemeServer) ParsePrice(price x402.Price, network x402.Network) (x402.AssetAmount, error) { + return x402.AssetAmount{ + Asset: "USDC", + Amount: "1000000", + }, nil +} + +func (m *mockSchemeServer) EnhancePaymentRequirements(ctx context.Context, base types.PaymentRequirements, supported types.SupportedKind, extensions []string) (types.PaymentRequirements, error) { + return base, nil +} + +// mockFacilitatorClient implements x402.FacilitatorClient for testing +type mockFacilitatorClient struct { + verifyFunc func(ctx context.Context, payloadBytes []byte, requirementsBytes []byte) (*x402.VerifyResponse, error) + settleFunc func(ctx context.Context, payloadBytes []byte, requirementsBytes []byte) (*x402.SettleResponse, error) + supportedFunc func(ctx context.Context) (x402.SupportedResponse, error) +} + +func (m *mockFacilitatorClient) Verify(ctx context.Context, payloadBytes []byte, requirementsBytes []byte) (*x402.VerifyResponse, error) { + if m.verifyFunc != nil { + return m.verifyFunc(ctx, payloadBytes, requirementsBytes) + } + return &x402.VerifyResponse{IsValid: true, Payer: "0xmock"}, nil +} + +func (m *mockFacilitatorClient) Settle(ctx context.Context, payloadBytes []byte, requirementsBytes []byte) (*x402.SettleResponse, error) { + if m.settleFunc != nil { + return m.settleFunc(ctx, payloadBytes, requirementsBytes) + } + return &x402.SettleResponse{Success: true, Transaction: "0xtx", Network: "eip155:1", Payer: "0xmock"}, nil +} + +func (m *mockFacilitatorClient) GetSupported(ctx context.Context) (x402.SupportedResponse, error) { + if m.supportedFunc != nil { + return m.supportedFunc(ctx) + } + return x402.SupportedResponse{ + Kinds: []x402.SupportedKind{ + {X402Version: 2, Scheme: "exact", Network: "eip155:1"}, + }, + Extensions: []string{}, + Signers: make(map[string][]string), + }, nil +} + +func (m *mockFacilitatorClient) Identifier() string { + return "mock" +} + +// ============================================================================ +// Test Helpers +// ============================================================================ + +// createTestEcho creates an Echo instance for testing +func createTestEcho() *echo.Echo { + e := echo.New() + return e +} + +// createPaymentHeader creates a base64-encoded payment header for testing +// +//nolint:unparam // payTo is always "0xtest" in current tests but keeping param for flexibility +func createPaymentHeader(payTo string) string { + payload := x402.PaymentPayload{ + X402Version: 2, + Payload: map[string]interface{}{"sig": "test"}, + Accepted: x402.PaymentRequirements{ + Scheme: "exact", + Network: "eip155:1", + Asset: "USDC", + Amount: "1000000", + PayTo: payTo, + MaxTimeoutSeconds: 300, + Extra: map[string]interface{}{ + "resourceUrl": "http://example.com/api", + }, + }, + } + + payloadJSON, _ := json.Marshal(payload) + return base64.StdEncoding.EncodeToString(payloadJSON) +} + +// ============================================================================ +// EchoAdapter Tests +// ============================================================================ + +func TestEchoAdapter_GetHeader(t *testing.T) { + e := createTestEcho() + var adapter *EchoAdapter + + e.GET("/test", func(c echo.Context) error { + adapter = NewEchoAdapter(c) + return c.NoContent(http.StatusOK) + }) + + req := httptest.NewRequest("GET", "/test", nil) + req.Header.Set("X-Custom-Header", "test-value") + req.Header.Set("payment-signature", "sig-data") + + w := httptest.NewRecorder() + e.ServeHTTP(w, req) + + if adapter.GetHeader("X-Custom-Header") != "test-value" { + t.Error("Expected X-Custom-Header to be 'test-value'") + } + + if adapter.GetHeader("Payment-Signature") != "sig-data" { + t.Error("Expected payment-signature header") + } +} + +func TestEchoAdapter_GetMethod(t *testing.T) { + tests := []struct { + method string + expected string + }{ + {"GET", "GET"}, + {"POST", "POST"}, + {"PUT", "PUT"}, + {"DELETE", "DELETE"}, + } + + for _, tt := range tests { + t.Run(tt.method, func(t *testing.T) { + e := createTestEcho() + var adapter *EchoAdapter + + e.Add(tt.method, "/test", func(c echo.Context) error { + adapter = NewEchoAdapter(c) + return c.NoContent(http.StatusOK) + }) + + req := httptest.NewRequest(tt.method, "/test", nil) + w := httptest.NewRecorder() + e.ServeHTTP(w, req) + + if adapter.GetMethod() != tt.expected { + t.Errorf("Expected method %s, got %s", tt.expected, adapter.GetMethod()) + } + }) + } +} + +func TestEchoAdapter_GetPath(t *testing.T) { + e := createTestEcho() + var adapter *EchoAdapter + + e.GET("/api/users/:id", func(c echo.Context) error { + adapter = NewEchoAdapter(c) + return c.NoContent(http.StatusOK) + }) + + req := httptest.NewRequest("GET", "/api/users/123", nil) + w := httptest.NewRecorder() + e.ServeHTTP(w, req) + + if adapter.GetPath() != "/api/users/123" { + t.Errorf("Expected path '/api/users/123', got '%s'", adapter.GetPath()) + } +} + +func TestEchoAdapter_GetURL(t *testing.T) { + tests := []struct { + name string + target string + expected string + }{ + { + name: "with query params", + target: "/api/test?id=1", + expected: "http://example.com/api/test?id=1", + }, + { + name: "without query params", + target: "/api/test", + expected: "http://example.com/api/test", + }, + { + name: "with multiple query params", + target: "/api/test?id=1&foo=bar", + expected: "http://example.com/api/test?id=1&foo=bar", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + e := createTestEcho() + var adapter *EchoAdapter + + e.GET("/api/test", func(c echo.Context) error { + adapter = NewEchoAdapter(c) + return c.NoContent(http.StatusOK) + }) + + req := httptest.NewRequest("GET", tt.target, nil) + req.Host = "example.com" + w := httptest.NewRecorder() + e.ServeHTTP(w, req) + + if adapter.GetURL() != tt.expected { + t.Errorf("Expected URL '%s', got '%s'", tt.expected, adapter.GetURL()) + } + }) + } +} + +func TestEchoAdapter_GetAcceptHeader(t *testing.T) { + e := createTestEcho() + var adapter *EchoAdapter + + e.GET("/test", func(c echo.Context) error { + adapter = NewEchoAdapter(c) + return c.NoContent(http.StatusOK) + }) + + req := httptest.NewRequest("GET", "/test", nil) + req.Header.Set("Accept", "text/html") + + w := httptest.NewRecorder() + e.ServeHTTP(w, req) + + if adapter.GetAcceptHeader() != "text/html" { + t.Errorf("Expected Accept header 'text/html', got '%s'", adapter.GetAcceptHeader()) + } +} + +func TestEchoAdapter_GetUserAgent(t *testing.T) { + e := createTestEcho() + var adapter *EchoAdapter + + e.GET("/test", func(c echo.Context) error { + adapter = NewEchoAdapter(c) + return c.NoContent(http.StatusOK) + }) + + req := httptest.NewRequest("GET", "/test", nil) + req.Header.Set("User-Agent", "Mozilla/5.0") + + w := httptest.NewRecorder() + e.ServeHTTP(w, req) + + if adapter.GetUserAgent() != "Mozilla/5.0" { + t.Errorf("Expected User-Agent 'Mozilla/5.0', got '%s'", adapter.GetUserAgent()) + } +} + +// ============================================================================ +// PaymentMiddleware Tests +// ============================================================================ + +func TestPaymentMiddleware_CallsNextWhenNoPaymentRequired(t *testing.T) { + routes := x402http.RoutesConfig{ + "GET /api": x402http.RouteConfig{ + Accepts: x402http.PaymentOptions{ + { + Scheme: "exact", + PayTo: "0xtest", + Price: "$1.00", + Network: "eip155:1", + }, + }, + }, + } + + e := createTestEcho() + e.Use(PaymentMiddlewareFromConfig(routes, WithSyncFacilitatorOnStart(false))) + + nextCalled := false + e.GET("/public", func(c echo.Context) error { + nextCalled = true + return c.JSON(http.StatusOK, map[string]interface{}{"message": "success"}) + }) + + req := httptest.NewRequest("GET", "/public", nil) + w := httptest.NewRecorder() + e.ServeHTTP(w, req) + + if !nextCalled { + t.Error("Expected next() to be called for non-protected route") + } + if w.Code != http.StatusOK { + t.Errorf("Expected status 200, got %d", w.Code) + } +} + +func TestPaymentMiddleware_Returns402JSONForPaymentError(t *testing.T) { + mockClient := &mockFacilitatorClient{ + supportedFunc: func(ctx context.Context) (x402.SupportedResponse, error) { + return x402.SupportedResponse{ + Kinds: []x402.SupportedKind{ + {X402Version: 2, Scheme: "exact", Network: "eip155:1"}, + }, + Extensions: []string{}, + Signers: make(map[string][]string), + }, nil + }, + } + + mockServer := &mockSchemeServer{scheme: "exact"} + + routes := x402http.RoutesConfig{ + "GET /api": x402http.RouteConfig{ + Accepts: x402http.PaymentOptions{ + { + Scheme: "exact", + PayTo: "0xtest", + Price: "$1.00", + Network: "eip155:1", + }, + }, + Description: "API access", + }, + } + + e := createTestEcho() + e.Use(PaymentMiddlewareFromConfig(routes, + WithFacilitatorClient(mockClient), + WithScheme("eip155:1", mockServer), + WithSyncFacilitatorOnStart(true), + WithTimeout(5*time.Second), + )) + + e.GET("/api", func(c echo.Context) error { + return c.JSON(http.StatusOK, map[string]interface{}{"data": "protected"}) + }) + + req := httptest.NewRequest("GET", "/api", nil) + req.Header.Set("Accept", "application/json") + + w := httptest.NewRecorder() + e.ServeHTTP(w, req) + + if w.Code != http.StatusPaymentRequired { + t.Errorf("Expected status 402, got %d", w.Code) + } + + if w.Header().Get("PAYMENT-REQUIRED") == "" { + t.Error("Expected PAYMENT-REQUIRED header") + } +} + +func TestPaymentMiddleware_Returns402HTMLForBrowserRequest(t *testing.T) { + mockClient := &mockFacilitatorClient{ + supportedFunc: func(ctx context.Context) (x402.SupportedResponse, error) { + return x402.SupportedResponse{ + Kinds: []x402.SupportedKind{ + {X402Version: 2, Scheme: "exact", Network: "eip155:1"}, + }, + Extensions: []string{}, + Signers: make(map[string][]string), + }, nil + }, + } + + mockServer := &mockSchemeServer{scheme: "exact"} + + routes := x402http.RoutesConfig{ + "*": x402http.RouteConfig{ + Accepts: x402http.PaymentOptions{ + { + Scheme: "exact", + PayTo: "0xtest", + Price: "$5.00", + Network: "eip155:1", + }, + }, + Description: "Premium content", + }, + } + + paywallConfig := &x402http.PaywallConfig{ + AppName: "Test App", + } + + e := createTestEcho() + e.Use(PaymentMiddlewareFromConfig(routes, + WithFacilitatorClient(mockClient), + WithScheme("eip155:1", mockServer), + WithPaywallConfig(paywallConfig), + WithSyncFacilitatorOnStart(true), + WithTimeout(5*time.Second), + )) + + e.GET("/content", func(c echo.Context) error { + return c.JSON(http.StatusOK, map[string]interface{}{"data": "protected"}) + }) + + req := httptest.NewRequest("GET", "/content", nil) + req.Header.Set("Accept", "text/html") + req.Header.Set("User-Agent", "Mozilla/5.0") + + w := httptest.NewRecorder() + e.ServeHTTP(w, req) + + if w.Code != http.StatusPaymentRequired { + t.Errorf("Expected status 402, got %d", w.Code) + } + + contentType := w.Header().Get("Content-Type") + if !bytes.Contains([]byte(contentType), []byte("text/html")) { + t.Errorf("Expected Content-Type to contain 'text/html', got '%s'", contentType) + } + + body := w.Body.String() + if !bytes.Contains([]byte(body), []byte("Payment Required")) { + t.Error("Expected 'Payment Required' in HTML body") + } + if !bytes.Contains([]byte(body), []byte("Test App")) { + t.Error("Expected app name in HTML body") + } +} + +func TestPaymentMiddleware_SettlesAndReturnsResponseForVerifiedPayment(t *testing.T) { + settleCalled := false + + mockClient := &mockFacilitatorClient{ + verifyFunc: func(ctx context.Context, payloadBytes []byte, requirementsBytes []byte) (*x402.VerifyResponse, error) { + return &x402.VerifyResponse{IsValid: true, Payer: "0xpayer"}, nil + }, + settleFunc: func(ctx context.Context, payloadBytes []byte, requirementsBytes []byte) (*x402.SettleResponse, error) { + settleCalled = true + return &x402.SettleResponse{ + Success: true, + Transaction: "0xtx", + Network: "eip155:1", + Payer: "0xpayer", + }, nil + }, + supportedFunc: func(ctx context.Context) (x402.SupportedResponse, error) { + return x402.SupportedResponse{ + Kinds: []x402.SupportedKind{ + {X402Version: 2, Scheme: "exact", Network: "eip155:1"}, + }, + Extensions: []string{}, + Signers: make(map[string][]string), + }, nil + }, + } + + mockServer := &mockSchemeServer{scheme: "exact"} + + routes := x402http.RoutesConfig{ + "POST /api": x402http.RouteConfig{ + Accepts: x402http.PaymentOptions{ + { + Scheme: "exact", + PayTo: "0xtest", + Price: "$1.00", + Network: "eip155:1", + }, + }, + }, + } + + e := createTestEcho() + e.Use(PaymentMiddlewareFromConfig(routes, + WithFacilitatorClient(mockClient), + WithScheme("eip155:1", mockServer), + WithSyncFacilitatorOnStart(true), + WithTimeout(5*time.Second), + )) + + e.POST("/api", func(c echo.Context) error { + return c.JSON(http.StatusOK, map[string]interface{}{"data": "protected-data"}) + }) + + req := httptest.NewRequest("POST", "/api", nil) + req.Header.Set("PAYMENT-SIGNATURE", createPaymentHeader("0xtest")) + req.Host = "example.com" + + w := httptest.NewRecorder() + e.ServeHTTP(w, req) + + if w.Code != http.StatusOK { + t.Errorf("Expected status 200, got %d. Body: %s", w.Code, w.Body.String()) + } + + if !settleCalled { + t.Error("Expected settlement to be called") + } + + if w.Header().Get("PAYMENT-RESPONSE") == "" { + t.Error("Expected PAYMENT-RESPONSE header") + } +} + +func TestPaymentMiddleware_SkipsSettlementWhenHandlerReturns400OrHigher(t *testing.T) { + settleCalled := false + + mockClient := &mockFacilitatorClient{ + verifyFunc: func(ctx context.Context, payloadBytes []byte, requirementsBytes []byte) (*x402.VerifyResponse, error) { + return &x402.VerifyResponse{IsValid: true, Payer: "0xpayer"}, nil + }, + settleFunc: func(ctx context.Context, payloadBytes []byte, requirementsBytes []byte) (*x402.SettleResponse, error) { + settleCalled = true + return &x402.SettleResponse{Success: true, Transaction: "0xtx"}, nil + }, + supportedFunc: func(ctx context.Context) (x402.SupportedResponse, error) { + return x402.SupportedResponse{ + Kinds: []x402.SupportedKind{ + {X402Version: 2, Scheme: "exact", Network: "eip155:1"}, + }, + Extensions: []string{}, + Signers: make(map[string][]string), + }, nil + }, + } + + mockServer := &mockSchemeServer{scheme: "exact"} + + routes := x402http.RoutesConfig{ + "POST /api": x402http.RouteConfig{ + Accepts: x402http.PaymentOptions{ + { + Scheme: "exact", + PayTo: "0xtest", + Price: "$1.00", + Network: "eip155:1", + }, + }, + }, + } + + e := createTestEcho() + e.Use(PaymentMiddlewareFromConfig(routes, + WithFacilitatorClient(mockClient), + WithScheme("eip155:1", mockServer), + WithSyncFacilitatorOnStart(true), + WithTimeout(5*time.Second), + )) + + e.POST("/api", func(c echo.Context) error { + // Handler returns error status + return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": "internal error"}) + }) + + req := httptest.NewRequest("POST", "/api", nil) + req.Header.Set("PAYMENT-SIGNATURE", createPaymentHeader("0xtest")) + req.Host = "example.com" + + w := httptest.NewRecorder() + e.ServeHTTP(w, req) + + if w.Code != http.StatusInternalServerError { + t.Errorf("Expected status 500, got %d", w.Code) + } + + if settleCalled { + t.Error("Settlement should NOT be called when handler returns >= 400") + } +} + +func TestPaymentMiddleware_Returns402WhenSettlementFails(t *testing.T) { + mockClient := &mockFacilitatorClient{ + verifyFunc: func(ctx context.Context, payloadBytes []byte, requirementsBytes []byte) (*x402.VerifyResponse, error) { + return &x402.VerifyResponse{IsValid: true, Payer: "0xpayer"}, nil + }, + settleFunc: func(ctx context.Context, payloadBytes []byte, requirementsBytes []byte) (*x402.SettleResponse, error) { + return &x402.SettleResponse{ + Success: false, + ErrorReason: "Insufficient funds", + }, nil + }, + supportedFunc: func(ctx context.Context) (x402.SupportedResponse, error) { + return x402.SupportedResponse{ + Kinds: []x402.SupportedKind{ + {X402Version: 2, Scheme: "exact", Network: "eip155:1"}, + }, + Extensions: []string{}, + Signers: make(map[string][]string), + }, nil + }, + } + + mockServer := &mockSchemeServer{scheme: "exact"} + + routes := x402http.RoutesConfig{ + "POST /api": x402http.RouteConfig{ + Accepts: x402http.PaymentOptions{ + { + Scheme: "exact", + PayTo: "0xtest", + Price: "$1.00", + Network: "eip155:1", + }, + }, + }, + } + + e := createTestEcho() + e.Use(PaymentMiddlewareFromConfig(routes, + WithFacilitatorClient(mockClient), + WithScheme("eip155:1", mockServer), + WithSyncFacilitatorOnStart(true), + WithTimeout(5*time.Second), + )) + + e.POST("/api", func(c echo.Context) error { + return c.JSON(http.StatusOK, map[string]interface{}{"data": "protected-data"}) + }) + + req := httptest.NewRequest("POST", "/api", nil) + req.Header.Set("PAYMENT-SIGNATURE", createPaymentHeader("0xtest")) + req.Host = "example.com" + + w := httptest.NewRecorder() + e.ServeHTTP(w, req) + + if w.Code != http.StatusPaymentRequired { + t.Errorf("Expected status 402, got %d", w.Code) + } + + // Empty body by default on settlement failure + var response map[string]interface{} + if err := json.Unmarshal(w.Body.Bytes(), &response); err != nil { + t.Fatalf("Failed to parse response: %v", err) + } + if len(response) != 0 { + t.Errorf("Expected empty body {}, got %v", response) + } + + // PAYMENT-RESPONSE header must be included on settlement failure + if w.Header().Get("PAYMENT-RESPONSE") == "" { + t.Error("Expected PAYMENT-RESPONSE header on settlement failure") + } +} + +func TestPaymentMiddleware_CustomErrorHandler(t *testing.T) { + customHandlerCalled := false + + mockClient := &mockFacilitatorClient{ + verifyFunc: func(ctx context.Context, payloadBytes []byte, requirementsBytes []byte) (*x402.VerifyResponse, error) { + return &x402.VerifyResponse{IsValid: true, Payer: "0xpayer"}, nil + }, + settleFunc: func(ctx context.Context, payloadBytes []byte, requirementsBytes []byte) (*x402.SettleResponse, error) { + return &x402.SettleResponse{ + Success: false, + ErrorReason: "Settlement rejected", + }, nil + }, + supportedFunc: func(ctx context.Context) (x402.SupportedResponse, error) { + return x402.SupportedResponse{ + Kinds: []x402.SupportedKind{ + {X402Version: 2, Scheme: "exact", Network: "eip155:1"}, + }, + Extensions: []string{}, + Signers: make(map[string][]string), + }, nil + }, + } + + mockServer := &mockSchemeServer{scheme: "exact"} + + routes := x402http.RoutesConfig{ + "POST /api": x402http.RouteConfig{ + Accepts: x402http.PaymentOptions{ + { + Scheme: "exact", + PayTo: "0xtest", + Price: "$1.00", + Network: "eip155:1", + }, + }, + }, + } + + customErrorHandler := func(c echo.Context, err error) { + customHandlerCalled = true + // Reset committed state so we can write + c.Response().Committed = false + _ = c.JSON(http.StatusPaymentRequired, map[string]interface{}{ + "custom_error": err.Error(), + }) + } + + e := createTestEcho() + e.Use(PaymentMiddlewareFromConfig(routes, + WithFacilitatorClient(mockClient), + WithScheme("eip155:1", mockServer), + WithErrorHandler(customErrorHandler), + WithSyncFacilitatorOnStart(true), + WithTimeout(5*time.Second), + )) + + e.POST("/api", func(c echo.Context) error { + return c.JSON(http.StatusOK, map[string]interface{}{"data": "protected-data"}) + }) + + req := httptest.NewRequest("POST", "/api", nil) + req.Header.Set("PAYMENT-SIGNATURE", createPaymentHeader("0xtest")) + req.Host = "example.com" + + w := httptest.NewRecorder() + e.ServeHTTP(w, req) + + if !customHandlerCalled { + t.Error("Expected custom error handler to be called") + } + + var response map[string]interface{} + if err := json.Unmarshal(w.Body.Bytes(), &response); err != nil { + t.Fatalf("Failed to parse response: %v", err) + } + + if response["custom_error"] == nil { + t.Error("Expected custom_error in response") + } +} + +func TestPaymentMiddleware_CustomSettlementHandler(t *testing.T) { + settlementHandlerCalled := false + var capturedSettleResponse *x402.SettleResponse + + mockClient := &mockFacilitatorClient{ + verifyFunc: func(ctx context.Context, payloadBytes []byte, requirementsBytes []byte) (*x402.VerifyResponse, error) { + return &x402.VerifyResponse{IsValid: true, Payer: "0xpayer"}, nil + }, + settleFunc: func(ctx context.Context, payloadBytes []byte, requirementsBytes []byte) (*x402.SettleResponse, error) { + return &x402.SettleResponse{ + Success: true, + Transaction: "0xtx123", + Network: "eip155:1", + Payer: "0xpayer", + }, nil + }, + supportedFunc: func(ctx context.Context) (x402.SupportedResponse, error) { + return x402.SupportedResponse{ + Kinds: []x402.SupportedKind{ + {X402Version: 2, Scheme: "exact", Network: "eip155:1"}, + }, + Extensions: []string{}, + Signers: make(map[string][]string), + }, nil + }, + } + + mockServer := &mockSchemeServer{scheme: "exact"} + + routes := x402http.RoutesConfig{ + "POST /api": x402http.RouteConfig{ + Accepts: x402http.PaymentOptions{ + { + Scheme: "exact", + PayTo: "0xtest", + Price: "$1.00", + Network: "eip155:1", + }, + }, + }, + } + + customSettlementHandler := func(c echo.Context, settleResponse *x402.SettleResponse) { + settlementHandlerCalled = true + capturedSettleResponse = settleResponse + // Add custom header + c.Response().Header().Set("X-Transaction-ID", settleResponse.Transaction) + } + + e := createTestEcho() + e.Use(PaymentMiddlewareFromConfig(routes, + WithFacilitatorClient(mockClient), + WithScheme("eip155:1", mockServer), + WithSettlementHandler(customSettlementHandler), + WithSyncFacilitatorOnStart(true), + WithTimeout(5*time.Second), + )) + + e.POST("/api", func(c echo.Context) error { + return c.JSON(http.StatusOK, map[string]interface{}{"data": "protected-data"}) + }) + + req := httptest.NewRequest("POST", "/api", nil) + req.Header.Set("PAYMENT-SIGNATURE", createPaymentHeader("0xtest")) + req.Host = "example.com" + + w := httptest.NewRecorder() + e.ServeHTTP(w, req) + + if w.Code != http.StatusOK { + t.Errorf("Expected status 200, got %d", w.Code) + } + + if !settlementHandlerCalled { + t.Error("Expected custom settlement handler to be called") + } + + if capturedSettleResponse == nil { + t.Fatal("Expected settle response to be captured") + } + + if capturedSettleResponse.Transaction != "0xtx123" { + t.Errorf("Expected transaction '0xtx123', got '%s'", capturedSettleResponse.Transaction) + } + + if w.Header().Get("X-Transaction-ID") != "0xtx123" { + t.Error("Expected custom X-Transaction-ID header") + } +} + +func TestPaymentMiddleware_WithTimeout(t *testing.T) { + mockClient := &mockFacilitatorClient{ + supportedFunc: func(ctx context.Context) (x402.SupportedResponse, error) { + return x402.SupportedResponse{ + Kinds: []x402.SupportedKind{ + {X402Version: 2, Scheme: "exact", Network: "eip155:1"}, + }, + Extensions: []string{}, + Signers: make(map[string][]string), + }, nil + }, + } + + mockServer := &mockSchemeServer{scheme: "exact"} + + routes := x402http.RoutesConfig{ + "*": x402http.RouteConfig{ + Accepts: x402http.PaymentOptions{ + { + Scheme: "exact", + PayTo: "0xtest", + Price: "$1.00", + Network: "eip155:1", + }, + }, + }, + } + + timeout := 10 * time.Second + + e := createTestEcho() + e.Use(PaymentMiddlewareFromConfig(routes, + WithFacilitatorClient(mockClient), + WithScheme("eip155:1", mockServer), + WithTimeout(timeout), + WithSyncFacilitatorOnStart(true), + )) + + e.GET("/test", func(c echo.Context) error { + return c.JSON(http.StatusOK, map[string]interface{}{"message": "success"}) + }) + + // Verify the middleware is created and requires payment + req := httptest.NewRequest("GET", "/test", nil) + w := httptest.NewRecorder() + e.ServeHTTP(w, req) + + // Route should require payment + if w.Code != http.StatusPaymentRequired { + t.Errorf("Expected status 402, got %d", w.Code) + } +} + +// ============================================================================ +// PaymentMiddlewareFromHTTPServer Tests +// ============================================================================ + +func TestPaymentMiddlewareFromHTTPServer_Returns402ForProtectedRoute(t *testing.T) { + mockClient := &mockFacilitatorClient{ + supportedFunc: func(ctx context.Context) (x402.SupportedResponse, error) { + return x402.SupportedResponse{ + Kinds: []x402.SupportedKind{ + {X402Version: 2, Scheme: "exact", Network: "eip155:1"}, + }, + Extensions: []string{}, + Signers: make(map[string][]string), + }, nil + }, + } + + routes := x402http.RoutesConfig{ + "GET /api": x402http.RouteConfig{ + Accepts: x402http.PaymentOptions{ + { + Scheme: "exact", + PayTo: "0xtest", + Price: "$1.00", + Network: "eip155:1", + }, + }, + }, + } + + // Build the resource server externally + resourceServer := x402.Newx402ResourceServer( + x402.WithFacilitatorClient(mockClient), + ) + resourceServer.Register("eip155:1", &mockSchemeServer{scheme: "exact"}) + + // Wrap with HTTP server + httpServer := x402http.Wrappedx402HTTPResourceServer(routes, resourceServer) + + // Use PaymentMiddlewareFromHTTPServer + e := createTestEcho() + e.Use(PaymentMiddlewareFromHTTPServer(httpServer, WithTimeout(5*time.Second))) + + e.GET("/api", func(c echo.Context) error { + return c.JSON(http.StatusOK, map[string]interface{}{"data": "protected"}) + }) + + req := httptest.NewRequest("GET", "/api", nil) + req.Header.Set("Accept", "application/json") + w := httptest.NewRecorder() + e.ServeHTTP(w, req) + + if w.Code != http.StatusPaymentRequired { + t.Errorf("Expected status 402, got %d", w.Code) + } +} + +func TestPaymentMiddlewareFromHTTPServer_PassesThroughNonProtectedRoute(t *testing.T) { + routes := x402http.RoutesConfig{ + "GET /api": x402http.RouteConfig{ + Accepts: x402http.PaymentOptions{ + { + Scheme: "exact", + PayTo: "0xtest", + Price: "$1.00", + Network: "eip155:1", + }, + }, + }, + } + + resourceServer := x402.Newx402ResourceServer() + httpServer := x402http.Wrappedx402HTTPResourceServer(routes, resourceServer) + + e := createTestEcho() + e.Use(PaymentMiddlewareFromHTTPServer(httpServer, WithSyncFacilitatorOnStart(false))) + + nextCalled := false + e.GET("/public", func(c echo.Context) error { + nextCalled = true + return c.JSON(http.StatusOK, map[string]interface{}{"message": "public"}) + }) + + req := httptest.NewRequest("GET", "/public", nil) + w := httptest.NewRecorder() + e.ServeHTTP(w, req) + + if !nextCalled { + t.Error("Expected next() to be called for non-protected route") + } + if w.Code != http.StatusOK { + t.Errorf("Expected status 200, got %d", w.Code) + } +} + +func TestPaymentMiddlewareFromHTTPServer_HookGrantsAccess(t *testing.T) { + mockClient := &mockFacilitatorClient{ + supportedFunc: func(ctx context.Context) (x402.SupportedResponse, error) { + return x402.SupportedResponse{ + Kinds: []x402.SupportedKind{ + {X402Version: 2, Scheme: "exact", Network: "eip155:1"}, + }, + Extensions: []string{}, + Signers: make(map[string][]string), + }, nil + }, + } + + routes := x402http.RoutesConfig{ + "GET /api": x402http.RouteConfig{ + Accepts: x402http.PaymentOptions{ + { + Scheme: "exact", + PayTo: "0xtest", + Price: "$1.00", + Network: "eip155:1", + }, + }, + }, + } + + resourceServer := x402.Newx402ResourceServer( + x402.WithFacilitatorClient(mockClient), + ) + resourceServer.Register("eip155:1", &mockSchemeServer{scheme: "exact"}) + + // Register a hook that grants free access + httpServer := x402http.Wrappedx402HTTPResourceServer(routes, resourceServer). + OnProtectedRequest(func(ctx context.Context, reqCtx x402http.HTTPRequestContext, routeConfig x402http.RouteConfig) (*x402http.ProtectedRequestHookResult, error) { + return &x402http.ProtectedRequestHookResult{GrantAccess: true}, nil + }) + + e := createTestEcho() + e.Use(PaymentMiddlewareFromHTTPServer(httpServer, WithTimeout(5*time.Second))) + + nextCalled := false + e.GET("/api", func(c echo.Context) error { + nextCalled = true + return c.JSON(http.StatusOK, map[string]interface{}{"data": "free-access"}) + }) + + // Request without payment header - hook should grant access + req := httptest.NewRequest("GET", "/api", nil) + w := httptest.NewRecorder() + e.ServeHTTP(w, req) + + if w.Code != http.StatusOK { + t.Errorf("Expected status 200 (hook granted access), got %d. Body: %s", w.Code, w.Body.String()) + } + if !nextCalled { + t.Error("Expected next handler to be called when hook grants access") + } +} + +func TestPaymentMiddlewareFromHTTPServer_HookAbortsRequest(t *testing.T) { + mockClient := &mockFacilitatorClient{ + supportedFunc: func(ctx context.Context) (x402.SupportedResponse, error) { + return x402.SupportedResponse{ + Kinds: []x402.SupportedKind{ + {X402Version: 2, Scheme: "exact", Network: "eip155:1"}, + }, + Extensions: []string{}, + Signers: make(map[string][]string), + }, nil + }, + } + + routes := x402http.RoutesConfig{ + "GET /api": x402http.RouteConfig{ + Accepts: x402http.PaymentOptions{ + { + Scheme: "exact", + PayTo: "0xtest", + Price: "$1.00", + Network: "eip155:1", + }, + }, + }, + } + + resourceServer := x402.Newx402ResourceServer( + x402.WithFacilitatorClient(mockClient), + ) + resourceServer.Register("eip155:1", &mockSchemeServer{scheme: "exact"}) + + // Register a hook that aborts the request + httpServer := x402http.Wrappedx402HTTPResourceServer(routes, resourceServer). + OnProtectedRequest(func(ctx context.Context, reqCtx x402http.HTTPRequestContext, routeConfig x402http.RouteConfig) (*x402http.ProtectedRequestHookResult, error) { + return &x402http.ProtectedRequestHookResult{Abort: true, Reason: "IP blocked"}, nil + }) + + e := createTestEcho() + e.Use(PaymentMiddlewareFromHTTPServer(httpServer, WithTimeout(5*time.Second))) + + e.GET("/api", func(c echo.Context) error { + t.Error("Handler should not be called when hook aborts") + return nil + }) + + req := httptest.NewRequest("GET", "/api", nil) + w := httptest.NewRecorder() + e.ServeHTTP(w, req) + + if w.Code != http.StatusForbidden { + t.Errorf("Expected status 403 (hook aborted), got %d", w.Code) + } +} + +// ============================================================================ +// X402Payment (Builder Pattern) Tests +// ============================================================================ + +func TestX402Payment_CreatesWorkingMiddleware(t *testing.T) { + mockClient := &mockFacilitatorClient{ + supportedFunc: func(ctx context.Context) (x402.SupportedResponse, error) { + return x402.SupportedResponse{ + Kinds: []x402.SupportedKind{ + {X402Version: 2, Scheme: "exact", Network: "eip155:1"}, + }, + Extensions: []string{}, + Signers: make(map[string][]string), + }, nil + }, + } + + mockServer := &mockSchemeServer{scheme: "exact"} + + routes := x402http.RoutesConfig{ + "GET /api": x402http.RouteConfig{ + Accepts: x402http.PaymentOptions{ + { + Scheme: "exact", + PayTo: "0xtest", + Price: "$1.00", + Network: "eip155:1", + }, + }, + }, + } + + e := createTestEcho() + e.Use(X402Payment(Config{ + Routes: routes, + Facilitator: mockClient, + Schemes: []SchemeConfig{ + {Network: "eip155:1", Server: mockServer}, + }, + SyncFacilitatorOnStart: true, + Timeout: 5 * time.Second, + })) + + e.GET("/api", func(c echo.Context) error { + return c.JSON(http.StatusOK, map[string]interface{}{"data": "protected"}) + }) + + // Test non-protected route passes through + e.GET("/public", func(c echo.Context) error { + return c.JSON(http.StatusOK, map[string]interface{}{"message": "public"}) + }) + + req := httptest.NewRequest("GET", "/public", nil) + w := httptest.NewRecorder() + e.ServeHTTP(w, req) + + if w.Code != http.StatusOK { + t.Errorf("Expected status 200 for public route, got %d", w.Code) + } + + // Test protected route requires payment + req = httptest.NewRequest("GET", "/api", nil) + w = httptest.NewRecorder() + e.ServeHTTP(w, req) + + if w.Code != http.StatusPaymentRequired { + t.Errorf("Expected status 402 for protected route, got %d", w.Code) + } +} + +func TestX402Payment_RegistersMultipleFacilitators(t *testing.T) { + mockClient1 := &mockFacilitatorClient{ + supportedFunc: func(ctx context.Context) (x402.SupportedResponse, error) { + return x402.SupportedResponse{ + Kinds: []x402.SupportedKind{ + {X402Version: 2, Scheme: "exact", Network: "eip155:1"}, + }, + Extensions: []string{}, + Signers: make(map[string][]string), + }, nil + }, + } + mockClient2 := &mockFacilitatorClient{ + supportedFunc: func(ctx context.Context) (x402.SupportedResponse, error) { + return x402.SupportedResponse{ + Kinds: []x402.SupportedKind{ + {X402Version: 2, Scheme: "exact", Network: "eip155:1"}, + }, + Extensions: []string{}, + Signers: make(map[string][]string), + }, nil + }, + } + + mockServer := &mockSchemeServer{scheme: "exact"} + + routes := x402http.RoutesConfig{ + "*": x402http.RouteConfig{ + Accepts: x402http.PaymentOptions{ + { + Scheme: "exact", + PayTo: "0xtest", + Price: "$1.00", + Network: "eip155:1", + }, + }, + }, + } + + // This should not panic and properly register multiple facilitators + e := createTestEcho() + e.Use(X402Payment(Config{ + Routes: routes, + Facilitators: []x402.FacilitatorClient{mockClient1, mockClient2}, + Schemes: []SchemeConfig{ + {Network: "eip155:1", Server: mockServer}, + }, + SyncFacilitatorOnStart: true, + })) + + e.GET("/test", func(c echo.Context) error { + return c.JSON(http.StatusOK, map[string]interface{}{"message": "success"}) + }) + + req := httptest.NewRequest("GET", "/test", nil) + w := httptest.NewRecorder() + e.ServeHTTP(w, req) + + if w.Code != http.StatusPaymentRequired { + t.Errorf("Expected status 402, got %d", w.Code) + } +} + +func TestX402Payment_RegistersMultipleSchemes(t *testing.T) { + mockServer1 := &mockSchemeServer{scheme: "exact"} + mockServer2 := &mockSchemeServer{scheme: "exact"} + + routes := x402http.RoutesConfig{ + "*": x402http.RouteConfig{ + Accepts: x402http.PaymentOptions{ + { + Scheme: "exact", + PayTo: "0xtest", + Price: "$1.00", + Network: "eip155:1", + }, + }, + }, + } + + // This should not panic + e := createTestEcho() + e.Use(X402Payment(Config{ + Routes: routes, + Schemes: []SchemeConfig{ + {Network: "eip155:1", Server: mockServer1}, + {Network: "eip155:8453", Server: mockServer2}, + }, + SyncFacilitatorOnStart: false, + })) + + e.GET("/test", func(c echo.Context) error { + return c.JSON(http.StatusOK, map[string]interface{}{"message": "success"}) + }) + + req := httptest.NewRequest("GET", "/test", nil) + w := httptest.NewRecorder() + e.ServeHTTP(w, req) + + if w.Code != http.StatusPaymentRequired { + t.Errorf("Expected status 402, got %d", w.Code) + } +} + +// ============================================================================ +// responseCapture Tests +// ============================================================================ + +func TestResponseCapture_CapturesStatusCode(t *testing.T) { + capture := &responseCapture{ + ResponseWriter: httptest.NewRecorder(), + body: &bytes.Buffer{}, + statusCode: http.StatusOK, + } + + capture.WriteHeader(http.StatusCreated) + + if capture.statusCode != http.StatusCreated { + t.Errorf("Expected status 201, got %d", capture.statusCode) + } +} + +func TestResponseCapture_CapturesBody(t *testing.T) { + capture := &responseCapture{ + ResponseWriter: httptest.NewRecorder(), + body: &bytes.Buffer{}, + statusCode: http.StatusOK, + } + + data := []byte(`{"message":"test"}`) + n, err := capture.Write(data) + + if err != nil { + t.Fatalf("Write failed: %v", err) + } + if n != len(data) { + t.Errorf("Expected to write %d bytes, wrote %d", len(data), n) + } + if capture.body.String() != `{"message":"test"}` { + t.Errorf("Expected body '%s', got '%s'", `{"message":"test"}`, capture.body.String()) + } +} + +func TestResponseCapture_WriteString(t *testing.T) { + capture := &responseCapture{ + ResponseWriter: httptest.NewRecorder(), + body: &bytes.Buffer{}, + statusCode: http.StatusOK, + } + + n, err := capture.WriteString("hello world") + + if err != nil { + t.Fatalf("WriteString failed: %v", err) + } + if n != 11 { + t.Errorf("Expected to write 11 bytes, wrote %d", n) + } + if capture.body.String() != "hello world" { + t.Errorf("Expected body 'hello world', got '%s'", capture.body.String()) + } +} + +func TestResponseCapture_WriteHeaderOnlyOnce(t *testing.T) { + capture := &responseCapture{ + ResponseWriter: httptest.NewRecorder(), + body: &bytes.Buffer{}, + statusCode: http.StatusOK, + } + + capture.WriteHeader(http.StatusCreated) + capture.WriteHeader(http.StatusAccepted) // Should be ignored + + if capture.statusCode != http.StatusCreated { + t.Errorf("Expected status 201 (first call), got %d", capture.statusCode) + } +} diff --git a/go/http/evm_paywall_template.go b/go/http/evm_paywall_template.go index 24e183877f..1f03ef1070 100644 --- a/go/http/evm_paywall_template.go +++ b/go/http/evm_paywall_template.go @@ -2,4 +2,4 @@ package http // EVMPaywallTemplate is the pre-built EVM paywall template with inlined CSS and JS -const EVMPaywallTemplate = "\n \n \n Payment Required\n \n
\n \n \n " +const EVMPaywallTemplate = "\n \n \n Payment Required\n \n
\n \n \n " diff --git a/go/http/facilitator_client.go b/go/http/facilitator_client.go index ca868799f9..1ca017c3cc 100644 --- a/go/http/facilitator_client.go +++ b/go/http/facilitator_client.go @@ -7,6 +7,7 @@ import ( "fmt" "io" "net/http" + "strings" "time" x402 "github.com/coinbase/x402/go" @@ -37,6 +38,7 @@ type AuthHeaders struct { Verify map[string]string Settle map[string]string Supported map[string]string + Discovery map[string]string } // FacilitatorConfig configures the HTTP facilitator client @@ -66,6 +68,146 @@ const getSupportedRetries = 3 // getSupportedRetryBaseDelay is the base delay for exponential backoff on retries const getSupportedRetryBaseDelay = 1 * time.Second +// FacilitatorResponseError indicates a facilitator returned malformed success payload data. +type FacilitatorResponseError struct { + message string + cause error +} + +func (e *FacilitatorResponseError) Error() string { + return e.message +} + +func (e *FacilitatorResponseError) Unwrap() error { + return e.cause +} + +type verifyResponseEnvelope struct { + IsValid *bool `json:"isValid"` + InvalidReason string `json:"invalidReason,omitempty"` + InvalidMessage string `json:"invalidMessage,omitempty"` + Payer string `json:"payer,omitempty"` +} + +type settleResponseEnvelope struct { + Success *bool `json:"success"` + ErrorReason string `json:"errorReason,omitempty"` + ErrorMessage string `json:"errorMessage,omitempty"` + Payer string `json:"payer,omitempty"` + Transaction *string `json:"transaction"` + Network *x402.Network `json:"network"` +} + +type supportedKindEnvelope struct { + X402Version *int `json:"x402Version"` + Scheme string `json:"scheme"` + Network string `json:"network"` + Extra map[string]interface{} `json:"extra,omitempty"` +} + +type supportedResponseEnvelope struct { + Kinds []supportedKindEnvelope `json:"kinds"` + Extensions []string `json:"extensions"` + Signers map[string][]string `json:"signers"` +} + +func responseExcerpt(body []byte, limit int) string { + text := strings.TrimSpace(string(body)) + if text == "" { + return "" + } + + compact := strings.Join(strings.Fields(text), " ") + if len(compact) <= limit { + return compact + } + + return compact[:limit-3] + "..." +} + +func newFacilitatorResponseError(operation string, kind string, body []byte, cause error) error { + return &FacilitatorResponseError{ + message: fmt.Sprintf("facilitator %s returned invalid %s: %s", operation, kind, responseExcerpt(body, 200)), + cause: cause, + } +} + +func parseVerifySuccessResponse(body []byte) (*x402.VerifyResponse, error) { + var response verifyResponseEnvelope + if err := json.Unmarshal(body, &response); err != nil { + return nil, newFacilitatorResponseError("verify", "JSON", body, err) + } + if response.IsValid == nil { + return nil, newFacilitatorResponseError("verify", "data", body, fmt.Errorf("missing isValid")) + } + + return &x402.VerifyResponse{ + IsValid: *response.IsValid, + InvalidReason: response.InvalidReason, + InvalidMessage: response.InvalidMessage, + Payer: response.Payer, + }, nil +} + +func parseSettleSuccessResponse(body []byte) (*x402.SettleResponse, error) { + var response settleResponseEnvelope + if err := json.Unmarshal(body, &response); err != nil { + return nil, newFacilitatorResponseError("settle", "JSON", body, err) + } + if response.Success == nil || response.Transaction == nil || response.Network == nil { + return nil, newFacilitatorResponseError("settle", "data", body, fmt.Errorf("missing required fields")) + } + + return &x402.SettleResponse{ + Success: *response.Success, + ErrorReason: response.ErrorReason, + ErrorMessage: response.ErrorMessage, + Payer: response.Payer, + Transaction: *response.Transaction, + Network: *response.Network, + }, nil +} + +func parseSupportedSuccessResponse(body []byte) (x402.SupportedResponse, error) { + var response supportedResponseEnvelope + if err := json.Unmarshal(body, &response); err != nil { + return x402.SupportedResponse{}, newFacilitatorResponseError("getSupported", "JSON", body, err) + } + kinds := make([]x402.SupportedKind, 0, len(response.Kinds)) + for _, kind := range response.Kinds { + if kind.X402Version == nil || kind.Scheme == "" || kind.Network == "" { + return x402.SupportedResponse{}, newFacilitatorResponseError( + "getSupported", + "data", + body, + fmt.Errorf("invalid supported response fields"), + ) + } + kinds = append(kinds, x402.SupportedKind{ + X402Version: *kind.X402Version, + Scheme: kind.Scheme, + Network: kind.Network, + Extra: kind.Extra, + }) + } + + extensions := response.Extensions + if extensions == nil { + extensions = []string{} + } + + signers := response.Signers + if signers == nil { + signers = map[string][]string{} + } + + return x402.SupportedResponse{ + Kinds: kinds, + Extensions: extensions, + Signers: signers, + }, nil +} + // NewHTTPFacilitatorClient creates a new HTTP facilitator client func NewHTTPFacilitatorClient(config *FacilitatorConfig) *HTTPFacilitatorClient { if config == nil { @@ -101,6 +243,21 @@ func NewHTTPFacilitatorClient(config *FacilitatorConfig) *HTTPFacilitatorClient } } +// URL returns the base URL of the facilitator service. +func (c *HTTPFacilitatorClient) URL() string { + return c.url +} + +// HTTPClient returns the underlying HTTP client. +func (c *HTTPFacilitatorClient) HTTPClient() *http.Client { + return c.httpClient +} + +// GetAuthProvider returns the authentication provider, or nil if not configured. +func (c *HTTPFacilitatorClient) GetAuthProvider() AuthProvider { + return c.authProvider +} + // ============================================================================ // FacilitatorClient Implementation (Network Boundary - uses bytes) // ============================================================================ @@ -167,14 +324,14 @@ func (c *HTTPFacilitatorClient) GetSupported(ctx context.Context) (x402.Supporte // Success if resp.StatusCode == http.StatusOK { - var supportedResponse x402.SupportedResponse - if err := json.Unmarshal(responseBody, &supportedResponse); err != nil { - return x402.SupportedResponse{}, fmt.Errorf("failed to decode supported response: %w", err) - } - return supportedResponse, nil + return parseSupportedSuccessResponse(responseBody) } - lastErr = fmt.Errorf("facilitator supported failed (%d): %s", resp.StatusCode, string(responseBody)) + lastErr = fmt.Errorf( + "facilitator supported failed (%d): %s", + resp.StatusCode, + responseExcerpt(responseBody, 200), + ) // Retry on 429 with exponential backoff, except on the last attempt if resp.StatusCode == http.StatusTooManyRequests && attempt < getSupportedRetries-1 { @@ -250,18 +407,9 @@ func (c *HTTPFacilitatorClient) verifyHTTP(ctx context.Context, version int, pay return nil, fmt.Errorf("failed to read response body: %w", err) } - var verifyResponse x402.VerifyResponse - if err := json.Unmarshal(responseBody, &verifyResponse); err != nil { - return nil, x402.NewVerifyError( - x402.ErrInvalidResponse, - "", - fmt.Sprintf("failed to unmarshal verify response: %s", err.Error()), - ) - } - - // For non-200 responses, return an error with the details from the response if resp.StatusCode != http.StatusOK { - if verifyResponse.InvalidReason != "" { + var verifyResponse verifyResponseEnvelope + if err := json.Unmarshal(responseBody, &verifyResponse); err == nil && verifyResponse.InvalidReason != "" { return nil, x402.NewVerifyError( verifyResponse.InvalidReason, verifyResponse.Payer, @@ -271,7 +419,7 @@ func (c *HTTPFacilitatorClient) verifyHTTP(ctx context.Context, version int, pay return nil, fmt.Errorf("facilitator verify failed (%d): %s", resp.StatusCode, string(responseBody)) } - return &verifyResponse, nil + return parseVerifySuccessResponse(responseBody) } func (c *HTTPFacilitatorClient) settleHTTP(ctx context.Context, version int, payloadBytes, requirementsBytes []byte) (*x402.SettleResponse, error) { @@ -326,24 +474,27 @@ func (c *HTTPFacilitatorClient) settleHTTP(ctx context.Context, version int, pay return nil, fmt.Errorf("failed to read response body: %w", err) } - var settleResponse x402.SettleResponse - if err := json.Unmarshal(responseBody, &settleResponse); err != nil { - return nil, fmt.Errorf("facilitator settle failed (%d): %s", resp.StatusCode, string(responseBody)) - } - - // For non-200 responses, return an error with the details from the response if resp.StatusCode != http.StatusOK { - if settleResponse.ErrorReason != "" { + var settleResponse settleResponseEnvelope + if err := json.Unmarshal(responseBody, &settleResponse); err == nil && settleResponse.ErrorReason != "" { + network := x402.Network("") + if settleResponse.Network != nil { + network = *settleResponse.Network + } + transaction := "" + if settleResponse.Transaction != nil { + transaction = *settleResponse.Transaction + } return nil, x402.NewSettleError( settleResponse.ErrorReason, settleResponse.Payer, - settleResponse.Network, - settleResponse.Transaction, + network, + transaction, fmt.Sprintf("facilitator returned %d", resp.StatusCode), ) } return nil, fmt.Errorf("facilitator settle failed (%d): %s", resp.StatusCode, string(responseBody)) } - return &settleResponse, nil + return parseSettleSuccessResponse(responseBody) } diff --git a/go/http/facilitator_client_test.go b/go/http/facilitator_client_test.go index bd9d4f61f0..2cb610ba69 100644 --- a/go/http/facilitator_client_test.go +++ b/go/http/facilitator_client_test.go @@ -7,6 +7,7 @@ import ( "fmt" "net/http" "net/http/httptest" + "strings" "sync/atomic" "testing" "time" @@ -249,15 +250,12 @@ func TestHTTPFacilitatorClientVerifyInvalidResponse(t *testing.T) { t.Fatal("Expected error for invalid verify response") } - var verifyErr *x402.VerifyError - if !errors.As(err, &verifyErr) { - t.Fatalf("Expected VerifyError, got: %T (%v)", err, err) - } - if verifyErr.InvalidReason != x402.ErrInvalidResponse { - t.Errorf("Expected InvalidReason %q, got %q", x402.ErrInvalidResponse, verifyErr.InvalidReason) + var responseErr *FacilitatorResponseError + if !errors.As(err, &responseErr) { + t.Fatalf("Expected FacilitatorResponseError, got: %T (%v)", err, err) } - if verifyErr.InvalidMessage == "" { - t.Error("Expected InvalidMessage to be set for invalid response") + if !strings.Contains(responseErr.Error(), "facilitator verify returned invalid JSON") { + t.Errorf("Expected invalid JSON message, got %q", responseErr.Error()) } } @@ -317,6 +315,50 @@ func TestHTTPFacilitatorClientSettle(t *testing.T) { } } +func TestHTTPFacilitatorClientSettleInvalidResponse(t *testing.T) { + ctx := context.Background() + + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write([]byte(`{"success": true}`)) + })) + defer server.Close() + + client := NewHTTPFacilitatorClient(&FacilitatorConfig{ + URL: server.URL, + }) + + requirements := x402.PaymentRequirements{ + Scheme: "exact", + Network: "eip155:1", + Asset: "USDC", + Amount: "1000000", + PayTo: "0xrecipient", + } + + payload := x402.PaymentPayload{ + X402Version: 2, + Accepted: requirements, + Payload: map[string]interface{}{"sig": "test"}, + } + + payloadBytes, _ := json.Marshal(payload) + requirementsBytes, _ := json.Marshal(requirements) + + _, err := client.Settle(ctx, payloadBytes, requirementsBytes) + if err == nil { + t.Fatal("Expected error for invalid settle response") + } + + var responseErr *FacilitatorResponseError + if !errors.As(err, &responseErr) { + t.Fatalf("Expected FacilitatorResponseError, got: %T (%v)", err, err) + } + if !strings.Contains(responseErr.Error(), "facilitator settle returned invalid data") { + t.Errorf("Expected invalid data message, got %q", responseErr.Error()) + } +} + func TestHTTPFacilitatorClientGetSupported(t *testing.T) { ctx := context.Background() @@ -373,6 +415,33 @@ func TestHTTPFacilitatorClientGetSupported(t *testing.T) { } } +func TestHTTPFacilitatorClientGetSupportedInvalidResponse(t *testing.T) { + ctx := context.Background() + + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write([]byte(`{"kinds":[{"scheme":"exact"}]}`)) + })) + defer server.Close() + + client := NewHTTPFacilitatorClient(&FacilitatorConfig{ + URL: server.URL, + }) + + _, err := client.GetSupported(ctx) + if err == nil { + t.Fatal("Expected error for invalid supported response") + } + + var responseErr *FacilitatorResponseError + if !errors.As(err, &responseErr) { + t.Fatalf("Expected FacilitatorResponseError, got: %T (%v)", err, err) + } + if !strings.Contains(responseErr.Error(), "facilitator getSupported returned invalid data") { + t.Errorf("Expected invalid data message, got %q", responseErr.Error()) + } +} + func TestHTTPFacilitatorClientWithAuth(t *testing.T) { ctx := context.Background() diff --git a/go/http/gin/README.md b/go/http/gin/README.md index f4c9de69d0..bdde156947 100644 --- a/go/http/gin/README.md +++ b/go/http/gin/README.md @@ -199,6 +199,19 @@ r.Use(ginmw.PaymentMiddlewareFromConfig(routes, )) ``` +### Settlement Overrides (Upto Scheme) + +For the upto scheme, route handlers specify the actual settlement amount via `SetSettlementOverrides`: + +```go +r.GET("/api/metered", func(c *gin.Context) { + usage := calculateUsage(c) + ginmw.SetSettlementOverrides(c, &x402.SettlementOverrides{Amount: usage}) + + c.JSON(http.StatusOK, gin.H{"result": "ok"}) +}) +``` + ### Error Handler Custom error handling: diff --git a/go/http/gin/middleware.go b/go/http/gin/middleware.go index 3d480580fe..05feaae1bc 100644 --- a/go/http/gin/middleware.go +++ b/go/http/gin/middleware.go @@ -14,6 +14,12 @@ import ( "github.com/gin-gonic/gin" ) +// SetSettlementOverrides sets settlement overrides on the Gin response for partial settlement. +// The middleware extracts these before settlement and strips the header from the client response. +func SetSettlementOverrides(c *gin.Context, overrides *x402.SettlementOverrides) { + c.Header(x402http.SettlementOverridesHeader, x402http.MarshalSettlementOverrides(overrides)) +} + // ============================================================================ // Gin Adapter Implementation // ============================================================================ @@ -193,6 +199,45 @@ func PaymentMiddleware(routes x402http.RoutesConfig, server *x402.X402ResourceSe return createMiddlewareHandler(httpServer, config) } +// PaymentMiddlewareFromHTTPServer creates Gin middleware using a pre-configured HTTPServer. +// This allows registering hooks (e.g., OnProtectedRequest) on the server before attaching to the router. +// +// Example: +// +// resourceServer := x402.Newx402ResourceServer( +// x402.WithFacilitatorClient(facilitator), +// ).Register("eip155:*", evm.NewExactEvmScheme()) +// +// httpServer := x402http.Wrappedx402HTTPResourceServer(routes, resourceServer). +// OnProtectedRequest(requestHook) +// +// r.Use(ginmw.PaymentMiddlewareFromHTTPServer(httpServer)) +func PaymentMiddlewareFromHTTPServer(httpServer *x402http.HTTPServer, opts ...MiddlewareOption) gin.HandlerFunc { + config := &MiddlewareConfig{ + SyncFacilitatorOnStart: true, + Timeout: 30 * time.Second, + } + + // Apply options + for _, opt := range opts { + opt(config) + } + + httpServer.RegisterExtension(bazaar.BazaarResourceServerExtension) + + // Initialize if requested - queries facilitator /supported to populate facilitatorClients map + if config.SyncFacilitatorOnStart { + ctx, cancel := context.WithTimeout(context.Background(), config.Timeout) + defer cancel() + if err := httpServer.Initialize(ctx); err != nil { + fmt.Printf("Warning: failed to initialize x402 server: %v\n", err) + } + } + + // Create middleware handler using shared logic + return createMiddlewareHandler(httpServer, config) +} + // PaymentMiddlewareFromConfig creates Gin middleware for x402 payment handling. // This creates the server internally from the provided options. func PaymentMiddlewareFromConfig(routes x402http.RoutesConfig, opts ...MiddlewareOption) gin.HandlerFunc { @@ -271,7 +316,7 @@ func createMiddlewareHandler(server *x402http.HTTPServer, config *MiddlewareConf case x402http.ResultPaymentVerified: // Payment verified, continue with settlement handling - handlePaymentVerified(c, server, ctx, result, config) + handlePaymentVerified(c, server, ctx, reqCtx, result, config) } } } @@ -298,7 +343,7 @@ func handlePaymentError(c *gin.Context, response *x402http.HTTPResponseInstructi } // handlePaymentVerified handles verified payments with settlement -func handlePaymentVerified(c *gin.Context, server *x402http.HTTPServer, ctx context.Context, result x402http.HTTPProcessResult, config *MiddlewareConfig) { +func handlePaymentVerified(c *gin.Context, server *x402http.HTTPServer, ctx context.Context, reqCtx x402http.HTTPRequestContext, result x402http.HTTPProcessResult, config *MiddlewareConfig) { // Capture response for settlement writer := &responseCapture{ ResponseWriter: c.Writer, @@ -334,26 +379,36 @@ func handlePaymentVerified(c *gin.Context, server *x402http.HTTPServer, ctx cont return } - // Process settlement settleResult := server.ProcessSettlement( ctx, *result.PaymentPayload, *result.PaymentRequirements, + nil, + &x402http.HTTPTransportContext{ + Request: &reqCtx, + ResponseBody: writer.body.Bytes(), + ResponseHeaders: writer.Header(), + }, ) // Check settlement success if !settleResult.Success { - errorReason := settleResult.ErrorReason - if errorReason == "" { - errorReason = "Settlement failed" + // Always set PAYMENT-RESPONSE header on settlement failure + for key, value := range settleResult.Headers { + c.Header(key, value) } - if config.ErrorHandler != nil { + switch { + case config.ErrorHandler != nil: + errorReason := settleResult.ErrorReason + if errorReason == "" { + errorReason = "Settlement failed" + } config.ErrorHandler(c, fmt.Errorf("settlement failed: %s", errorReason)) - } else { - c.JSON(http.StatusPaymentRequired, gin.H{ - "error": "Settlement failed", - "details": errorReason, - }) + case settleResult.Response != nil: + handlePaymentError(c, settleResult.Response, config) + default: + // Fallback if Response is nil + c.JSON(http.StatusPaymentRequired, map[string]interface{}{}) } return } @@ -423,3 +478,34 @@ func (w *responseCapture) Write(data []byte) (int, error) { func (w *responseCapture) WriteString(s string) (int, error) { return w.Write([]byte(s)) } + +// Flush is a no-op to prevent premature flushing to the wire before settlement. +// Gin's default Flush calls WriteHeaderNow then flushes the TCP connection, +// which would commit HTTP headers before settlement can add PAYMENT-RESPONSE. +func (w *responseCapture) Flush() {} + +// WriteHeaderNow is a no-op to prevent premature header commit before settlement. +// Gin's default WriteHeaderNow writes the status line + headers to the underlying +// http.ResponseWriter, which cannot be undone. +func (w *responseCapture) WriteHeaderNow() {} + +// Status returns the captured status code instead of the embedded writer's. +func (w *responseCapture) Status() int { + w.mu.Lock() + defer w.mu.Unlock() + return w.statusCode +} + +// Size returns the captured body length instead of the embedded writer's. +func (w *responseCapture) Size() int { + w.mu.Lock() + defer w.mu.Unlock() + return w.body.Len() +} + +// Written returns whether any write has been captured. +func (w *responseCapture) Written() bool { + w.mu.Lock() + defer w.mu.Unlock() + return w.written +} diff --git a/go/http/gin/middleware_test.go b/go/http/gin/middleware_test.go index 2367beb190..0e2efd045f 100644 --- a/go/http/gin/middleware_test.go +++ b/go/http/gin/middleware_test.go @@ -6,6 +6,7 @@ import ( "context" "encoding/base64" "encoding/json" + "fmt" "net" "net/http" "net/http/httptest" @@ -646,16 +647,18 @@ func TestPaymentMiddleware_Returns402WhenSettlementFails(t *testing.T) { t.Errorf("Expected status 402, got %d", w.Code) } + // Empty body by default on settlement failure var response map[string]interface{} if err := json.Unmarshal(w.Body.Bytes(), &response); err != nil { t.Fatalf("Failed to parse response: %v", err) } - - if response["error"] != "Settlement failed" { - t.Errorf("Expected error 'Settlement failed', got '%v'", response["error"]) + if len(response) != 0 { + t.Errorf("Expected empty body {}, got %v", response) } - if response["details"] != "Insufficient funds" { - t.Errorf("Expected details 'Insufficient funds', got '%v'", response["details"]) + + // AYMENT-RESPONSE header must be included on settlement failure + if w.Header().Get("PAYMENT-RESPONSE") == "" { + t.Error("Expected PAYMENT-RESPONSE header on settlement failure") } } @@ -737,6 +740,11 @@ func TestPaymentMiddleware_CustomErrorHandler(t *testing.T) { if response["custom_error"] == nil { t.Error("Expected custom_error in response") } + + // PAYMENT-RESPONSE header must be set even when using custom error handler + if w.Header().Get("PAYMENT-RESPONSE") == "" { + t.Error("Expected PAYMENT-RESPONSE header when using custom error handler") + } } func TestPaymentMiddleware_CustomSettlementHandler(t *testing.T) { @@ -882,6 +890,213 @@ func TestPaymentMiddleware_WithTimeout(t *testing.T) { } } +// ============================================================================ +// PaymentMiddlewareFromHTTPServer Tests +// ============================================================================ + +func TestPaymentMiddlewareFromHTTPServer_Returns402ForProtectedRoute(t *testing.T) { + mockClient := &mockFacilitatorClient{ + supportedFunc: func(ctx context.Context) (x402.SupportedResponse, error) { + return x402.SupportedResponse{ + Kinds: []x402.SupportedKind{ + {X402Version: 2, Scheme: "exact", Network: "eip155:1"}, + }, + Extensions: []string{}, + Signers: make(map[string][]string), + }, nil + }, + } + + routes := x402http.RoutesConfig{ + "GET /api": x402http.RouteConfig{ + Accepts: x402http.PaymentOptions{ + { + Scheme: "exact", + PayTo: "0xtest", + Price: "$1.00", + Network: "eip155:1", + }, + }, + }, + } + + // Build the resource server externally + resourceServer := x402.Newx402ResourceServer( + x402.WithFacilitatorClient(mockClient), + ) + resourceServer.Register("eip155:1", &mockSchemeServer{scheme: "exact"}) + + // Wrap with HTTP server + httpServer := x402http.Wrappedx402HTTPResourceServer(routes, resourceServer) + + // Use PaymentMiddlewareFromHTTPServer + router := createTestRouter() + router.Use(PaymentMiddlewareFromHTTPServer(httpServer, WithTimeout(5*time.Second))) + + router.GET("/api", func(c *gin.Context) { + c.JSON(http.StatusOK, gin.H{"data": "protected"}) + }) + + req := httptest.NewRequest("GET", "/api", nil) + req.Header.Set("Accept", "application/json") + w := httptest.NewRecorder() + router.ServeHTTP(w, req) + + if w.Code != http.StatusPaymentRequired { + t.Errorf("Expected status 402, got %d", w.Code) + } +} + +func TestPaymentMiddlewareFromHTTPServer_PassesThroughNonProtectedRoute(t *testing.T) { + routes := x402http.RoutesConfig{ + "GET /api": x402http.RouteConfig{ + Accepts: x402http.PaymentOptions{ + { + Scheme: "exact", + PayTo: "0xtest", + Price: "$1.00", + Network: "eip155:1", + }, + }, + }, + } + + resourceServer := x402.Newx402ResourceServer() + httpServer := x402http.Wrappedx402HTTPResourceServer(routes, resourceServer) + + router := createTestRouter() + router.Use(PaymentMiddlewareFromHTTPServer(httpServer, WithSyncFacilitatorOnStart(false))) + + nextCalled := false + router.GET("/public", func(c *gin.Context) { + nextCalled = true + c.JSON(http.StatusOK, gin.H{"message": "public"}) + }) + + req := httptest.NewRequest("GET", "/public", nil) + w := httptest.NewRecorder() + router.ServeHTTP(w, req) + + if !nextCalled { + t.Error("Expected next() to be called for non-protected route") + } + if w.Code != http.StatusOK { + t.Errorf("Expected status 200, got %d", w.Code) + } +} + +func TestPaymentMiddlewareFromHTTPServer_HookGrantsAccess(t *testing.T) { + mockClient := &mockFacilitatorClient{ + supportedFunc: func(ctx context.Context) (x402.SupportedResponse, error) { + return x402.SupportedResponse{ + Kinds: []x402.SupportedKind{ + {X402Version: 2, Scheme: "exact", Network: "eip155:1"}, + }, + Extensions: []string{}, + Signers: make(map[string][]string), + }, nil + }, + } + + routes := x402http.RoutesConfig{ + "GET /api": x402http.RouteConfig{ + Accepts: x402http.PaymentOptions{ + { + Scheme: "exact", + PayTo: "0xtest", + Price: "$1.00", + Network: "eip155:1", + }, + }, + }, + } + + resourceServer := x402.Newx402ResourceServer( + x402.WithFacilitatorClient(mockClient), + ) + resourceServer.Register("eip155:1", &mockSchemeServer{scheme: "exact"}) + + // Register a hook that grants free access + httpServer := x402http.Wrappedx402HTTPResourceServer(routes, resourceServer). + OnProtectedRequest(func(ctx context.Context, reqCtx x402http.HTTPRequestContext, routeConfig x402http.RouteConfig) (*x402http.ProtectedRequestHookResult, error) { + return &x402http.ProtectedRequestHookResult{GrantAccess: true}, nil + }) + + router := createTestRouter() + router.Use(PaymentMiddlewareFromHTTPServer(httpServer, WithTimeout(5*time.Second))) + + nextCalled := false + router.GET("/api", func(c *gin.Context) { + nextCalled = true + c.JSON(http.StatusOK, gin.H{"data": "free-access"}) + }) + + // Request without payment header - hook should grant access + req := httptest.NewRequest("GET", "/api", nil) + w := httptest.NewRecorder() + router.ServeHTTP(w, req) + + if w.Code != http.StatusOK { + t.Errorf("Expected status 200 (hook granted access), got %d. Body: %s", w.Code, w.Body.String()) + } + if !nextCalled { + t.Error("Expected next handler to be called when hook grants access") + } +} + +func TestPaymentMiddlewareFromHTTPServer_HookAbortsRequest(t *testing.T) { + mockClient := &mockFacilitatorClient{ + supportedFunc: func(ctx context.Context) (x402.SupportedResponse, error) { + return x402.SupportedResponse{ + Kinds: []x402.SupportedKind{ + {X402Version: 2, Scheme: "exact", Network: "eip155:1"}, + }, + Extensions: []string{}, + Signers: make(map[string][]string), + }, nil + }, + } + + routes := x402http.RoutesConfig{ + "GET /api": x402http.RouteConfig{ + Accepts: x402http.PaymentOptions{ + { + Scheme: "exact", + PayTo: "0xtest", + Price: "$1.00", + Network: "eip155:1", + }, + }, + }, + } + + resourceServer := x402.Newx402ResourceServer( + x402.WithFacilitatorClient(mockClient), + ) + resourceServer.Register("eip155:1", &mockSchemeServer{scheme: "exact"}) + + // Register a hook that aborts the request + httpServer := x402http.Wrappedx402HTTPResourceServer(routes, resourceServer). + OnProtectedRequest(func(ctx context.Context, reqCtx x402http.HTTPRequestContext, routeConfig x402http.RouteConfig) (*x402http.ProtectedRequestHookResult, error) { + return &x402http.ProtectedRequestHookResult{Abort: true, Reason: "IP blocked"}, nil + }) + + router := createTestRouter() + router.Use(PaymentMiddlewareFromHTTPServer(httpServer, WithTimeout(5*time.Second))) + + router.GET("/api", func(c *gin.Context) { + t.Error("Handler should not be called when hook aborts") + }) + + req := httptest.NewRequest("GET", "/api", nil) + w := httptest.NewRecorder() + router.ServeHTTP(w, req) + + if w.Code != http.StatusForbidden { + t.Errorf("Expected status 403 (hook aborted), got %d", w.Code) + } +} + // ============================================================================ // X402Payment (Builder Pattern) Tests // ============================================================================ @@ -1138,11 +1353,15 @@ func TestResponseCapture_WriteHeaderOnlyOnce(t *testing.T) { } } -// mockGinResponseWriter implements gin.ResponseWriter for testing +// mockGinResponseWriter implements gin.ResponseWriter for testing. +// It tracks Flush and WriteHeaderNow calls so tests can verify that +// responseCapture prevents them from reaching the embedded writer. type mockGinResponseWriter struct { *httptest.ResponseRecorder - status int - size int + status int + size int + flushCount int + writeHeaderNowCnt int } func (m *mockGinResponseWriter) Status() int { @@ -1162,7 +1381,9 @@ func (m *mockGinResponseWriter) WriteHeader(code int) { m.ResponseRecorder.WriteHeader(code) } -func (m *mockGinResponseWriter) WriteHeaderNow() {} +func (m *mockGinResponseWriter) WriteHeaderNow() { + m.writeHeaderNowCnt++ +} func (m *mockGinResponseWriter) Write(data []byte) (int, error) { n, err := m.ResponseRecorder.Write(data) @@ -1183,9 +1404,191 @@ func (m *mockGinResponseWriter) CloseNotify() <-chan bool { } func (m *mockGinResponseWriter) Flush() { + m.flushCount++ m.ResponseRecorder.Flush() } func (m *mockGinResponseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) { return nil, nil, nil } + +// ============================================================================ +// responseCapture: Flush / WriteHeaderNow / Status / Size / Written tests +// ============================================================================ + +func TestResponseCapture_FlushIsNoOp(t *testing.T) { + mock := &mockGinResponseWriter{ + ResponseRecorder: httptest.NewRecorder(), + } + capture := &responseCapture{ + ResponseWriter: mock, + body: &bytes.Buffer{}, + statusCode: http.StatusOK, + } + + capture.Write([]byte("some data")) + capture.Flush() + capture.Flush() + + if mock.flushCount != 0 { + t.Errorf("Expected Flush to NOT reach embedded writer, got %d calls", mock.flushCount) + } + if capture.body.String() != "some data" { + t.Errorf("Expected buffered body 'some data', got '%s'", capture.body.String()) + } +} + +func TestResponseCapture_WriteHeaderNowIsNoOp(t *testing.T) { + mock := &mockGinResponseWriter{ + ResponseRecorder: httptest.NewRecorder(), + } + capture := &responseCapture{ + ResponseWriter: mock, + body: &bytes.Buffer{}, + statusCode: http.StatusOK, + } + + capture.WriteHeaderNow() + capture.WriteHeaderNow() + + if mock.writeHeaderNowCnt != 0 { + t.Errorf("Expected WriteHeaderNow to NOT reach embedded writer, got %d calls", mock.writeHeaderNowCnt) + } +} + +func TestResponseCapture_StatusSizeWritten(t *testing.T) { + mock := &mockGinResponseWriter{ + ResponseRecorder: httptest.NewRecorder(), + } + capture := &responseCapture{ + ResponseWriter: mock, + body: &bytes.Buffer{}, + statusCode: http.StatusOK, + } + + if capture.Status() != http.StatusOK { + t.Errorf("Expected initial status 200, got %d", capture.Status()) + } + if capture.Size() != 0 { + t.Errorf("Expected initial size 0, got %d", capture.Size()) + } + if capture.Written() { + t.Error("Expected Written() == false before any write") + } + + capture.WriteHeader(http.StatusCreated) + capture.Write([]byte("hello")) + + if capture.Status() != http.StatusCreated { + t.Errorf("Expected status 201, got %d", capture.Status()) + } + if capture.Size() != 5 { + t.Errorf("Expected size 5, got %d", capture.Size()) + } + if !capture.Written() { + t.Error("Expected Written() == true after WriteHeader") + } + + if mock.status != 0 { + t.Errorf("Expected embedded writer status unchanged (0), got %d", mock.status) + } + if mock.size != 0 { + t.Errorf("Expected embedded writer size unchanged (0), got %d", mock.size) + } +} + +// ============================================================================ +// Streaming integration test +// ============================================================================ + +func TestPaymentMiddleware_StreamingDoesNotLeakHeaders(t *testing.T) { + settleCalled := false + + mockClient := &mockFacilitatorClient{ + verifyFunc: func(ctx context.Context, payloadBytes []byte, requirementsBytes []byte) (*x402.VerifyResponse, error) { + return &x402.VerifyResponse{IsValid: true, Payer: "0xpayer"}, nil + }, + settleFunc: func(ctx context.Context, payloadBytes []byte, requirementsBytes []byte) (*x402.SettleResponse, error) { + settleCalled = true + return &x402.SettleResponse{ + Success: true, + Transaction: "0xtx", + Network: "eip155:1", + Payer: "0xpayer", + }, nil + }, + supportedFunc: func(ctx context.Context) (x402.SupportedResponse, error) { + return x402.SupportedResponse{ + Kinds: []x402.SupportedKind{ + {X402Version: 2, Scheme: "exact", Network: "eip155:1"}, + }, + Extensions: []string{}, + Signers: make(map[string][]string), + }, nil + }, + } + + mockScheme := &mockSchemeServer{scheme: "exact"} + + routes := x402http.RoutesConfig{ + "GET /stream": x402http.RouteConfig{ + Accepts: x402http.PaymentOptions{ + { + Scheme: "exact", + PayTo: "0xtest", + Price: "$1.00", + Network: "eip155:1", + }, + }, + MimeType: "text/event-stream", + }, + } + + router := createTestRouter() + router.Use(PaymentMiddlewareFromConfig(routes, + WithFacilitatorClient(mockClient), + WithScheme("eip155:1", mockScheme), + WithSyncFacilitatorOnStart(true), + WithTimeout(5*time.Second), + )) + + router.GET("/stream", func(c *gin.Context) { + c.Header("Content-Type", "text/event-stream") + c.Header("Cache-Control", "no-cache") + for i := 0; i < 3; i++ { + chunk := fmt.Sprintf("data: {\"n\":%d}\n\n", i) + _, _ = c.Writer.Write([]byte(chunk)) + if f, ok := c.Writer.(http.Flusher); ok { + f.Flush() + } + } + }) + + req := httptest.NewRequest("GET", "/stream", nil) + req.Header.Set("PAYMENT-SIGNATURE", createPaymentHeader("0xtest")) + req.Host = "example.com" + + w := httptest.NewRecorder() + router.ServeHTTP(w, req) + + if w.Code != http.StatusOK { + t.Errorf("Expected status 200, got %d. Body: %s", w.Code, w.Body.String()) + } + + if !settleCalled { + t.Error("Expected settlement to be called for streaming response") + } + + paymentResponse := w.Header().Get("PAYMENT-RESPONSE") + if paymentResponse == "" { + t.Error("Expected PAYMENT-RESPONSE header in final response; streaming Flush must not commit headers early") + } + + body := w.Body.String() + for i := 0; i < 3; i++ { + expected := fmt.Sprintf("data: {\"n\":%d}\n\n", i) + if !bytes.Contains([]byte(body), []byte(expected)) { + t.Errorf("Expected body to contain chunk %d (%q), got: %s", i, expected, body) + } + } +} diff --git a/go/http/nethttp/adapter.go b/go/http/nethttp/adapter.go new file mode 100644 index 0000000000..8aa0888571 --- /dev/null +++ b/go/http/nethttp/adapter.go @@ -0,0 +1,54 @@ +package nethttp + +import ( + "fmt" + "net/http" +) + +// NetHTTPAdapter implements HTTPAdapter for the standard net/http library. +type NetHTTPAdapter struct { + r *http.Request +} + +// NewNetHTTPAdapter creates a new adapter wrapping the given HTTP request. +func NewNetHTTPAdapter(r *http.Request) *NetHTTPAdapter { + return &NetHTTPAdapter{r: r} +} + +// GetHeader gets a request header by name. +func (a *NetHTTPAdapter) GetHeader(name string) string { + return a.r.Header.Get(name) +} + +// GetMethod gets the HTTP method. +func (a *NetHTTPAdapter) GetMethod() string { + return a.r.Method +} + +// GetPath gets the request path. +func (a *NetHTTPAdapter) GetPath() string { + return a.r.URL.Path +} + +// GetURL gets the full request URL. +func (a *NetHTTPAdapter) GetURL() string { + scheme := "http" + if a.r.TLS != nil { + scheme = "https" + } + host := a.r.Host + if host == "" { + host = a.r.Header.Get("Host") + } + return fmt.Sprintf("%s://%s%s", scheme, host, a.r.URL.RequestURI()) +} + +// GetAcceptHeader gets the Accept header. +func (a *NetHTTPAdapter) GetAcceptHeader() string { + return a.r.Header.Get("Accept") +} + +// GetUserAgent gets the User-Agent header. +func (a *NetHTTPAdapter) GetUserAgent() string { + return a.r.UserAgent() +} diff --git a/go/http/nethttp/builder.go b/go/http/nethttp/builder.go new file mode 100644 index 0000000000..416c552bfa --- /dev/null +++ b/go/http/nethttp/builder.go @@ -0,0 +1,151 @@ +package nethttp + +import ( + "net/http" + "time" + + x402 "github.com/coinbase/x402/go" + x402http "github.com/coinbase/x402/go/http" +) + +// Config provides struct-based configuration for x402 payment middleware. +// This is a cleaner alternative to the variadic options pattern. +type Config struct { + // Routes maps HTTP patterns to payment requirements. + Routes x402http.RoutesConfig + + // Facilitator is a single facilitator client (most common case). + // Use this OR Facilitators (not both). + Facilitator x402.FacilitatorClient + + // Facilitators is an array of facilitator clients (for fallback/redundancy). + // Use this OR Facilitator (not both). + Facilitators []x402.FacilitatorClient + + // Schemes to register with the server. + Schemes []SchemeConfig + + // PaywallConfig for browser-based payment UI (optional). + PaywallConfig *x402http.PaywallConfig + + // SyncFacilitatorOnStart fetches supported kinds from facilitators on startup. + // Default: true + SyncFacilitatorOnStart bool + + // Timeout for payment operations. + // Default: 30 seconds + Timeout time.Duration + + // ErrorHandler for custom error handling (optional). + ErrorHandler func(w http.ResponseWriter, r *http.Request, err error) + + // SettlementHandler called after successful settlement (optional). + SettlementHandler func(w http.ResponseWriter, r *http.Request, resp *x402.SettleResponse) +} + +// SchemeConfig configures a payment scheme for a network. +type SchemeConfig struct { + Network x402.Network + Server x402.SchemeNetworkServer +} + +// X402Payment creates payment middleware using struct-based configuration. +// This is a cleaner, more readable alternative to PaymentMiddlewareFromConfig with variadic options. +// +// Example: +// +// mux := http.NewServeMux() +// handler := nethttp.X402Payment(nethttp.Config{ +// Routes: routes, +// Facilitator: facilitatorClient, +// Schemes: []nethttp.SchemeConfig{ +// {Network: "eip155:*", Server: evm.NewExactEvmServer()}, +// }, +// SyncFacilitatorOnStart: true, +// Timeout: 30 * time.Second, +// })(mux) +func X402Payment(config Config) func(http.Handler) http.Handler { + if config.Timeout == 0 { + config.Timeout = 30 * time.Second + } + + // Default to sync when facilitators provided + syncOnStart := config.SyncFacilitatorOnStart + if !syncOnStart && config.Facilitator == nil && len(config.Facilitators) == 0 { + syncOnStart = false + } else if config.Facilitator != nil || len(config.Facilitators) > 0 { + if config.Timeout != 0 { + syncOnStart = true + } + } + + // Normalize facilitators list + var facilitators []x402.FacilitatorClient + if config.Facilitator != nil { + facilitators = append(facilitators, config.Facilitator) + } + facilitators = append(facilitators, config.Facilitators...) + + // Convert to middleware options + opts := []MiddlewareOption{ + WithSyncFacilitatorOnStart(syncOnStart), + WithTimeout(config.Timeout), + } + + for _, facilitator := range facilitators { + opts = append(opts, WithFacilitatorClient(facilitator)) + } + + for _, scheme := range config.Schemes { + opts = append(opts, WithScheme(scheme.Network, scheme.Server)) + } + + if config.PaywallConfig != nil { + opts = append(opts, WithPaywallConfig(config.PaywallConfig)) + } + if config.ErrorHandler != nil { + opts = append(opts, WithErrorHandler(config.ErrorHandler)) + } + if config.SettlementHandler != nil { + opts = append(opts, WithSettlementHandler(config.SettlementHandler)) + } + + return PaymentMiddlewareFromConfig(config.Routes, opts...) +} + +// SimpleX402Payment creates middleware with minimal configuration. +// Uses a single route pattern and facilitator for the simplest possible setup. +// +// Example: +// +// mux := http.NewServeMux() +// handler := nethttp.SimpleX402Payment( +// "0x123...", +// "$0.001", +// "eip155:8453", +// "https://facilitator.example.com", +// )(mux) +func SimpleX402Payment(payTo string, price string, network x402.Network, facilitatorURL string) func(http.Handler) http.Handler { + facilitator := x402http.NewHTTPFacilitatorClient(&x402http.FacilitatorConfig{ + URL: facilitatorURL, + }) + + routes := x402http.RoutesConfig{ + "*": { + Accepts: []x402http.PaymentOption{ + { + Scheme: "exact", + PayTo: payTo, + Price: x402.Price(price), + Network: network, + }, + }, + }, + } + + return X402Payment(Config{ + Routes: routes, + Facilitator: facilitator, + SyncFacilitatorOnStart: true, + }) +} diff --git a/go/http/nethttp/context.go b/go/http/nethttp/context.go new file mode 100644 index 0000000000..b9fdd14951 --- /dev/null +++ b/go/http/nethttp/context.go @@ -0,0 +1,50 @@ +package nethttp + +import ( + "context" + + "github.com/coinbase/x402/go/types" +) + +// contextKey is a private type for context keys defined in this package. +type contextKey string + +const ( + // payloadContextKey is the context key for the payment payload. + payloadContextKey contextKey = "x402_payload" + + // requirementsContextKey is the context key for the payment requirements. + requirementsContextKey contextKey = "x402_requirements" +) + +// PayloadFromContext retrieves the payment payload from the request context. +// Returns nil and false if no payload is present. +func PayloadFromContext(ctx context.Context) (*types.PaymentPayload, bool) { + val := ctx.Value(payloadContextKey) + if val == nil { + return nil, false + } + payload, ok := val.(*types.PaymentPayload) + return payload, ok +} + +// RequirementsFromContext retrieves the payment requirements from the request context. +// Returns nil and false if no requirements are present. +func RequirementsFromContext(ctx context.Context) (*types.PaymentRequirements, bool) { + val := ctx.Value(requirementsContextKey) + if val == nil { + return nil, false + } + reqs, ok := val.(*types.PaymentRequirements) + return reqs, ok +} + +// withPayload returns a new context with the payment payload attached. +func withPayload(ctx context.Context, payload *types.PaymentPayload) context.Context { + return context.WithValue(ctx, payloadContextKey, payload) +} + +// withRequirements returns a new context with the payment requirements attached. +func withRequirements(ctx context.Context, reqs *types.PaymentRequirements) context.Context { + return context.WithValue(ctx, requirementsContextKey, reqs) +} diff --git a/go/http/nethttp/middleware.go b/go/http/nethttp/middleware.go new file mode 100644 index 0000000000..6ce5588f6f --- /dev/null +++ b/go/http/nethttp/middleware.go @@ -0,0 +1,391 @@ +package nethttp + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "net/http" + "sync" + "time" + + x402 "github.com/coinbase/x402/go" + "github.com/coinbase/x402/go/extensions/bazaar" + x402http "github.com/coinbase/x402/go/http" +) + +// SetSettlementOverrides sets settlement overrides on the response for partial settlement. +// The middleware extracts these before settlement and strips the header from the client response. +func SetSettlementOverrides(w http.ResponseWriter, overrides *x402.SettlementOverrides) { + w.Header().Set(x402http.SettlementOverridesHeader, x402http.MarshalSettlementOverrides(overrides)) +} + +// ============================================================================ +// Middleware Configuration +// ============================================================================ + +// MiddlewareConfig configures the payment middleware. +type MiddlewareConfig struct { + // Routes configuration + Routes x402http.RoutesConfig + + // Facilitator client(s) + FacilitatorClients []x402.FacilitatorClient + + // Scheme registrations + Schemes []SchemeRegistration + + // Paywall configuration + PaywallConfig *x402http.PaywallConfig + + // Sync with facilitator on start + SyncFacilitatorOnStart bool + + // Custom error handler + ErrorHandler func(w http.ResponseWriter, r *http.Request, err error) + + // Custom settlement handler + SettlementHandler func(w http.ResponseWriter, r *http.Request, resp *x402.SettleResponse) + + // Context timeout for payment operations + Timeout time.Duration +} + +// SchemeRegistration registers a scheme with the server. +type SchemeRegistration struct { + Network x402.Network + Server x402.SchemeNetworkServer +} + +// MiddlewareOption configures the middleware. +type MiddlewareOption func(*MiddlewareConfig) + +// WithFacilitatorClient adds a facilitator client. +func WithFacilitatorClient(client x402.FacilitatorClient) MiddlewareOption { + return func(c *MiddlewareConfig) { + c.FacilitatorClients = append(c.FacilitatorClients, client) + } +} + +// WithScheme registers a scheme server. +func WithScheme(network x402.Network, schemeServer x402.SchemeNetworkServer) MiddlewareOption { + return func(c *MiddlewareConfig) { + c.Schemes = append(c.Schemes, SchemeRegistration{ + Network: network, + Server: schemeServer, + }) + } +} + +// WithPaywallConfig sets the paywall configuration. +func WithPaywallConfig(config *x402http.PaywallConfig) MiddlewareOption { + return func(c *MiddlewareConfig) { + c.PaywallConfig = config + } +} + +// WithSyncFacilitatorOnStart sets whether to sync with facilitator on startup. +func WithSyncFacilitatorOnStart(sync bool) MiddlewareOption { + return func(c *MiddlewareConfig) { + c.SyncFacilitatorOnStart = sync + } +} + +// WithErrorHandler sets a custom error handler. +func WithErrorHandler(handler func(w http.ResponseWriter, r *http.Request, err error)) MiddlewareOption { + return func(c *MiddlewareConfig) { + c.ErrorHandler = handler + } +} + +// WithSettlementHandler sets a custom settlement handler. +func WithSettlementHandler(handler func(w http.ResponseWriter, r *http.Request, resp *x402.SettleResponse)) MiddlewareOption { + return func(c *MiddlewareConfig) { + c.SettlementHandler = handler + } +} + +// WithTimeout sets the context timeout for payment operations. +func WithTimeout(timeout time.Duration) MiddlewareOption { + return func(c *MiddlewareConfig) { + c.Timeout = timeout + } +} + +// ============================================================================ +// Payment Middleware +// ============================================================================ + +// PaymentMiddleware creates net/http middleware for x402 payment handling using a pre-configured server. +func PaymentMiddleware(routes x402http.RoutesConfig, server *x402.X402ResourceServer, opts ...MiddlewareOption) func(http.Handler) http.Handler { + config := &MiddlewareConfig{ + Routes: routes, + SyncFacilitatorOnStart: true, + Timeout: 30 * time.Second, + } + + for _, opt := range opts { + opt(config) + } + + httpServer := x402http.Wrappedx402HTTPResourceServer(routes, server) + + httpServer.RegisterExtension(bazaar.BazaarResourceServerExtension) + + if config.SyncFacilitatorOnStart { + ctx, cancel := context.WithTimeout(context.Background(), config.Timeout) + defer cancel() + if err := httpServer.Initialize(ctx); err != nil { + fmt.Printf("Warning: failed to initialize x402 server: %v\n", err) + } + } + + return createMiddlewareHandler(httpServer, config) +} + +// PaymentMiddlewareFromConfig creates net/http middleware for x402 payment handling. +// This creates the server internally from the provided options. +func PaymentMiddlewareFromConfig(routes x402http.RoutesConfig, opts ...MiddlewareOption) func(http.Handler) http.Handler { + config := &MiddlewareConfig{ + Routes: routes, + FacilitatorClients: []x402.FacilitatorClient{}, + Schemes: []SchemeRegistration{}, + SyncFacilitatorOnStart: true, + Timeout: 30 * time.Second, + } + + for _, opt := range opts { + opt(config) + } + + serverOpts := []x402.ResourceServerOption{} + for _, client := range config.FacilitatorClients { + serverOpts = append(serverOpts, x402.WithFacilitatorClient(client)) + } + + httpServer := x402http.Newx402HTTPResourceServer(config.Routes, serverOpts...) + + httpServer.RegisterExtension(bazaar.BazaarResourceServerExtension) + + for _, scheme := range config.Schemes { + httpServer.Register(scheme.Network, scheme.Server) + } + + if config.SyncFacilitatorOnStart { + ctx, cancel := context.WithTimeout(context.Background(), config.Timeout) + defer cancel() + if err := httpServer.Initialize(ctx); err != nil { + fmt.Printf("Warning: failed to initialize x402 server: %v\n", err) + } + } + + return createMiddlewareHandler(httpServer, config) +} + +// PaymentMiddlewareFromHTTPServer creates net/http middleware using a pre-configured HTTPServer. +// This allows registering hooks (e.g., OnProtectedRequest) on the server before attaching to the router. +// +// Example: +// +// resourceServer := x402.Newx402ResourceServer( +// x402.WithFacilitatorClient(facilitator), +// ).Register("eip155:*", evm.NewExactEvmScheme()) +// +// httpServer := x402http.Wrappedx402HTTPResourceServer(routes, resourceServer). +// OnProtectedRequest(requestHook) +// +// handler := nethttp.PaymentMiddlewareFromHTTPServer(httpServer)(mux) +func PaymentMiddlewareFromHTTPServer(httpServer *x402http.HTTPServer, opts ...MiddlewareOption) func(http.Handler) http.Handler { + config := &MiddlewareConfig{ + SyncFacilitatorOnStart: true, + Timeout: 30 * time.Second, + } + + for _, opt := range opts { + opt(config) + } + + httpServer.RegisterExtension(bazaar.BazaarResourceServerExtension) + + if config.SyncFacilitatorOnStart { + ctx, cancel := context.WithTimeout(context.Background(), config.Timeout) + defer cancel() + if err := httpServer.Initialize(ctx); err != nil { + fmt.Printf("Warning: failed to initialize x402 server: %v\n", err) + } + } + + return createMiddlewareHandler(httpServer, config) +} + +// createMiddlewareHandler creates the actual http.Handler middleware function. +func createMiddlewareHandler(server *x402http.HTTPServer, config *MiddlewareConfig) func(http.Handler) http.Handler { + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + adapter := NewNetHTTPAdapter(r) + reqCtx := x402http.HTTPRequestContext{ + Adapter: adapter, + Path: r.URL.Path, + Method: r.Method, + } + + // Check if route requires payment + if !server.RequiresPayment(reqCtx) { + next.ServeHTTP(w, r) + return + } + + // Create context with timeout + ctx, cancel := context.WithTimeout(r.Context(), config.Timeout) + defer cancel() + + result := server.ProcessHTTPRequest(ctx, reqCtx, config.PaywallConfig) + + switch result.Type { + case x402http.ResultNoPaymentRequired: + next.ServeHTTP(w, r) + + case x402http.ResultPaymentError: + handlePaymentError(w, result.Response) + + case x402http.ResultPaymentVerified: + handlePaymentVerified(w, r, next, server, ctx, reqCtx, result, config) + } + }) + } +} + +// handlePaymentError writes a payment error response (typically 402). +func handlePaymentError(w http.ResponseWriter, response *x402http.HTTPResponseInstructions) { + for key, value := range response.Headers { + w.Header().Set(key, value) + } + + if response.IsHTML { + w.Header().Set("Content-Type", "text/html; charset=utf-8") + w.WriteHeader(response.Status) + fmt.Fprint(w, response.Body) + } else { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(response.Status) + _ = json.NewEncoder(w).Encode(response.Body) + } +} + +// handlePaymentVerified handles verified payments with response capture and settlement. +func handlePaymentVerified(w http.ResponseWriter, r *http.Request, next http.Handler, server *x402http.HTTPServer, ctx context.Context, reqCtx x402http.HTTPRequestContext, result x402http.HTTPProcessResult, config *MiddlewareConfig) { + // Capture downstream handler response + capture := &responseCapture{ + ResponseWriter: w, + body: &bytes.Buffer{}, + statusCode: http.StatusOK, + } + + // Set payment data in request context for downstream handlers + if result.PaymentPayload != nil { + r = r.WithContext(context.WithValue(r.Context(), payloadContextKey, result.PaymentPayload)) //nolint:contextcheck // context is derived from r.Context() + } + if result.PaymentRequirements != nil { + r = r.WithContext(context.WithValue(r.Context(), requirementsContextKey, result.PaymentRequirements)) //nolint:contextcheck // context is derived from r.Context() + } + + // Call downstream handler with captured writer + next.ServeHTTP(capture, r) + + // Don't settle if response failed + if capture.statusCode >= 400 { + w.WriteHeader(capture.statusCode) + _, _ = w.Write(capture.body.Bytes()) + return + } + + settleResult := server.ProcessSettlement( + ctx, + *result.PaymentPayload, + *result.PaymentRequirements, + nil, + &x402http.HTTPTransportContext{ + Request: &reqCtx, + ResponseBody: capture.body.Bytes(), + ResponseHeaders: capture.Header(), + }, + ) + + if !settleResult.Success { + // Always set PAYMENT-RESPONSE header on settlement failure + for key, value := range settleResult.Headers { + w.Header().Set(key, value) + } + switch { + case config.ErrorHandler != nil: + errorReason := settleResult.ErrorReason + if errorReason == "" { + errorReason = "Settlement failed" + } + config.ErrorHandler(w, r, fmt.Errorf("settlement failed: %s", errorReason)) + case settleResult.Response != nil: + handlePaymentError(w, settleResult.Response) + default: + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusPaymentRequired) + _ = json.NewEncoder(w).Encode(map[string]any{}) + } + return + } + + // Add settlement headers + for key, value := range settleResult.Headers { + w.Header().Set(key, value) + } + + // Call settlement handler if configured + if config.SettlementHandler != nil { + settleResponse := &x402.SettleResponse{ + Success: true, + Transaction: settleResult.Transaction, + Network: settleResult.Network, + Payer: settleResult.Payer, + } + config.SettlementHandler(w, r, settleResponse) + } + + // Write captured response + w.WriteHeader(capture.statusCode) + _, _ = w.Write(capture.body.Bytes()) +} + +// ============================================================================ +// Response Capture +// ============================================================================ + +// responseCapture captures the response for settlement processing. +type responseCapture struct { + http.ResponseWriter + body *bytes.Buffer + statusCode int + written bool + mu sync.Mutex +} + +// WriteHeader captures the status code without writing to the underlying writer. +func (w *responseCapture) WriteHeader(code int) { + w.mu.Lock() + defer w.mu.Unlock() + + if !w.written { + w.statusCode = code + w.written = true + } +} + +// Write captures the response body without writing to the underlying writer. +func (w *responseCapture) Write(data []byte) (int, error) { + w.mu.Lock() + defer w.mu.Unlock() + + if !w.written { + w.statusCode = http.StatusOK + w.written = true + } + return w.body.Write(data) +} diff --git a/go/http/nethttp/middleware_test.go b/go/http/nethttp/middleware_test.go new file mode 100644 index 0000000000..0d4f6928f7 --- /dev/null +++ b/go/http/nethttp/middleware_test.go @@ -0,0 +1,1317 @@ +package nethttp + +import ( + "bytes" + "context" + "encoding/base64" + "encoding/json" + "net/http" + "net/http/httptest" + "testing" + "time" + + x402 "github.com/coinbase/x402/go" + x402http "github.com/coinbase/x402/go/http" + "github.com/coinbase/x402/go/types" +) + +// ============================================================================ +// Mock Implementations +// ============================================================================ + +// mockSchemeServer implements x402.SchemeNetworkServer for testing. +type mockSchemeServer struct { + scheme string +} + +func (m *mockSchemeServer) Scheme() string { + return m.scheme +} + +func (m *mockSchemeServer) ParsePrice(price x402.Price, network x402.Network) (x402.AssetAmount, error) { + return x402.AssetAmount{ + Asset: "USDC", + Amount: "1000000", + }, nil +} + +func (m *mockSchemeServer) EnhancePaymentRequirements(ctx context.Context, base types.PaymentRequirements, supported types.SupportedKind, extensions []string) (types.PaymentRequirements, error) { + return base, nil +} + +// mockFacilitatorClient implements x402.FacilitatorClient for testing. +type mockFacilitatorClient struct { + verifyFunc func(ctx context.Context, payloadBytes []byte, requirementsBytes []byte) (*x402.VerifyResponse, error) + settleFunc func(ctx context.Context, payloadBytes []byte, requirementsBytes []byte) (*x402.SettleResponse, error) + supportedFunc func(ctx context.Context) (x402.SupportedResponse, error) +} + +func (m *mockFacilitatorClient) Verify(ctx context.Context, payloadBytes []byte, requirementsBytes []byte) (*x402.VerifyResponse, error) { + if m.verifyFunc != nil { + return m.verifyFunc(ctx, payloadBytes, requirementsBytes) + } + return &x402.VerifyResponse{IsValid: true, Payer: "0xmock"}, nil +} + +func (m *mockFacilitatorClient) Settle(ctx context.Context, payloadBytes []byte, requirementsBytes []byte) (*x402.SettleResponse, error) { + if m.settleFunc != nil { + return m.settleFunc(ctx, payloadBytes, requirementsBytes) + } + return &x402.SettleResponse{Success: true, Transaction: "0xtx", Network: "eip155:1", Payer: "0xmock"}, nil +} + +func (m *mockFacilitatorClient) GetSupported(ctx context.Context) (x402.SupportedResponse, error) { + if m.supportedFunc != nil { + return m.supportedFunc(ctx) + } + return x402.SupportedResponse{ + Kinds: []x402.SupportedKind{ + {X402Version: 2, Scheme: "exact", Network: "eip155:1"}, + }, + Extensions: []string{}, + Signers: make(map[string][]string), + }, nil +} + +func (m *mockFacilitatorClient) Identifier() string { + return "mock" +} + +// ============================================================================ +// Test Helpers +// ============================================================================ + +// createPaymentHeader creates a base64-encoded payment header for testing. +// +//nolint:unparam // payTo is always "0xtest" in current tests but keeping param for flexibility +func createPaymentHeader(payTo string) string { + payload := x402.PaymentPayload{ + X402Version: 2, + Payload: map[string]any{"sig": "test"}, + Accepted: x402.PaymentRequirements{ + Scheme: "exact", + Network: "eip155:1", + Asset: "USDC", + Amount: "1000000", + PayTo: payTo, + MaxTimeoutSeconds: 300, + Extra: map[string]any{ + "resourceUrl": "http://example.com/api", + }, + }, + } + + payloadJSON, _ := json.Marshal(payload) + return base64.StdEncoding.EncodeToString(payloadJSON) +} + +// defaultSupportedFunc returns a standard supported response function for tests. +func defaultSupportedFunc() func(ctx context.Context) (x402.SupportedResponse, error) { + return func(ctx context.Context) (x402.SupportedResponse, error) { + return x402.SupportedResponse{ + Kinds: []x402.SupportedKind{ + {X402Version: 2, Scheme: "exact", Network: "eip155:1"}, + }, + Extensions: []string{}, + Signers: make(map[string][]string), + }, nil + } +} + +// ============================================================================ +// NetHTTPAdapter Tests +// ============================================================================ + +func TestNetHTTPAdapter_GetHeader(t *testing.T) { + req := httptest.NewRequest("GET", "/test", nil) + req.Header.Set("X-Custom-Header", "test-value") + req.Header.Set("payment-signature", "sig-data") + + adapter := NewNetHTTPAdapter(req) + + if adapter.GetHeader("X-Custom-Header") != "test-value" { + t.Error("Expected X-Custom-Header to be 'test-value'") + } + + if adapter.GetHeader("payment-signature") != "sig-data" { + t.Error("Expected payment-signature header") + } +} + +func TestNetHTTPAdapter_GetMethod(t *testing.T) { + tests := []struct { + method string + expected string + }{ + {"GET", "GET"}, + {"POST", "POST"}, + {"PUT", "PUT"}, + {"DELETE", "DELETE"}, + } + + for _, tt := range tests { + t.Run(tt.method, func(t *testing.T) { + req := httptest.NewRequest(tt.method, "/test", nil) + adapter := NewNetHTTPAdapter(req) + + if adapter.GetMethod() != tt.expected { + t.Errorf("Expected method %s, got %s", tt.expected, adapter.GetMethod()) + } + }) + } +} + +func TestNetHTTPAdapter_GetPath(t *testing.T) { + req := httptest.NewRequest("GET", "/api/users/123", nil) + adapter := NewNetHTTPAdapter(req) + + if adapter.GetPath() != "/api/users/123" { + t.Errorf("Expected path '/api/users/123', got '%s'", adapter.GetPath()) + } +} + +func TestNetHTTPAdapter_GetURL(t *testing.T) { + tests := []struct { + name string + target string + expected string + }{ + { + name: "with query params", + target: "/api/test?id=1", + expected: "http://example.com/api/test?id=1", + }, + { + name: "without query params", + target: "/api/test", + expected: "http://example.com/api/test", + }, + { + name: "with multiple query params", + target: "/api/test?id=1&foo=bar", + expected: "http://example.com/api/test?id=1&foo=bar", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + req := httptest.NewRequest("GET", tt.target, nil) + req.Host = "example.com" + adapter := NewNetHTTPAdapter(req) + + if adapter.GetURL() != tt.expected { + t.Errorf("Expected URL '%s', got '%s'", tt.expected, adapter.GetURL()) + } + }) + } +} + +func TestNetHTTPAdapter_GetAcceptHeader(t *testing.T) { + req := httptest.NewRequest("GET", "/test", nil) + req.Header.Set("Accept", "text/html") + + adapter := NewNetHTTPAdapter(req) + + if adapter.GetAcceptHeader() != "text/html" { + t.Errorf("Expected Accept header 'text/html', got '%s'", adapter.GetAcceptHeader()) + } +} + +func TestNetHTTPAdapter_GetUserAgent(t *testing.T) { + req := httptest.NewRequest("GET", "/test", nil) + req.Header.Set("User-Agent", "Mozilla/5.0") + + adapter := NewNetHTTPAdapter(req) + + if adapter.GetUserAgent() != "Mozilla/5.0" { + t.Errorf("Expected User-Agent 'Mozilla/5.0', got '%s'", adapter.GetUserAgent()) + } +} + +// ============================================================================ +// PaymentMiddleware Tests +// ============================================================================ + +func TestPaymentMiddleware_CallsNextWhenNoPaymentRequired(t *testing.T) { + routes := x402http.RoutesConfig{ + "GET /api": x402http.RouteConfig{ + Accepts: x402http.PaymentOptions{ + { + Scheme: "exact", + PayTo: "0xtest", + Price: "$1.00", + Network: "eip155:1", + }, + }, + }, + } + + nextCalled := false + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + nextCalled = true + w.WriteHeader(http.StatusOK) + _ = json.NewEncoder(w).Encode(map[string]string{"message": "success"}) + }) + + middleware := PaymentMiddlewareFromConfig(routes, WithSyncFacilitatorOnStart(false)) + wrapped := middleware(handler) + + req := httptest.NewRequest("GET", "/public", nil) + w := httptest.NewRecorder() + wrapped.ServeHTTP(w, req) + + if !nextCalled { + t.Error("Expected next handler to be called for non-protected route") + } + if w.Code != http.StatusOK { + t.Errorf("Expected status 200, got %d", w.Code) + } +} + +func TestPaymentMiddleware_Returns402JSONForPaymentError(t *testing.T) { + mockClient := &mockFacilitatorClient{supportedFunc: defaultSupportedFunc()} + mockServer := &mockSchemeServer{scheme: "exact"} + + routes := x402http.RoutesConfig{ + "GET /api": x402http.RouteConfig{ + Accepts: x402http.PaymentOptions{ + { + Scheme: "exact", + PayTo: "0xtest", + Price: "$1.00", + Network: "eip155:1", + }, + }, + Description: "API access", + }, + } + + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + _ = json.NewEncoder(w).Encode(map[string]string{"data": "protected"}) + }) + + middleware := PaymentMiddlewareFromConfig(routes, + WithFacilitatorClient(mockClient), + WithScheme("eip155:1", mockServer), + WithSyncFacilitatorOnStart(true), + WithTimeout(5*time.Second), + ) + wrapped := middleware(handler) + + req := httptest.NewRequest("GET", "/api", nil) + req.Header.Set("Accept", "application/json") + + w := httptest.NewRecorder() + wrapped.ServeHTTP(w, req) + + if w.Code != http.StatusPaymentRequired { + t.Errorf("Expected status 402, got %d", w.Code) + } + + if w.Header().Get("PAYMENT-REQUIRED") == "" { + t.Error("Expected PAYMENT-REQUIRED header") + } +} + +func TestPaymentMiddleware_Returns402HTMLForBrowserRequest(t *testing.T) { + mockClient := &mockFacilitatorClient{supportedFunc: defaultSupportedFunc()} + mockServer := &mockSchemeServer{scheme: "exact"} + + routes := x402http.RoutesConfig{ + "*": x402http.RouteConfig{ + Accepts: x402http.PaymentOptions{ + { + Scheme: "exact", + PayTo: "0xtest", + Price: "$5.00", + Network: "eip155:1", + }, + }, + Description: "Premium content", + }, + } + + paywallConfig := &x402http.PaywallConfig{ + AppName: "Test App", + } + + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + _ = json.NewEncoder(w).Encode(map[string]string{"data": "protected"}) + }) + + middleware := PaymentMiddlewareFromConfig(routes, + WithFacilitatorClient(mockClient), + WithScheme("eip155:1", mockServer), + WithPaywallConfig(paywallConfig), + WithSyncFacilitatorOnStart(true), + WithTimeout(5*time.Second), + ) + wrapped := middleware(handler) + + req := httptest.NewRequest("GET", "/content", nil) + req.Header.Set("Accept", "text/html") + req.Header.Set("User-Agent", "Mozilla/5.0") + + w := httptest.NewRecorder() + wrapped.ServeHTTP(w, req) + + if w.Code != http.StatusPaymentRequired { + t.Errorf("Expected status 402, got %d", w.Code) + } + + contentType := w.Header().Get("Content-Type") + if !bytes.Contains([]byte(contentType), []byte("text/html")) { + t.Errorf("Expected Content-Type to contain 'text/html', got '%s'", contentType) + } + + body := w.Body.String() + if !bytes.Contains([]byte(body), []byte("Payment Required")) { + t.Error("Expected 'Payment Required' in HTML body") + } + if !bytes.Contains([]byte(body), []byte("Test App")) { + t.Error("Expected app name in HTML body") + } +} + +func TestPaymentMiddleware_SettlesAndReturnsResponseForVerifiedPayment(t *testing.T) { + settleCalled := false + + mockClient := &mockFacilitatorClient{ + verifyFunc: func(ctx context.Context, payloadBytes []byte, requirementsBytes []byte) (*x402.VerifyResponse, error) { + return &x402.VerifyResponse{IsValid: true, Payer: "0xpayer"}, nil + }, + settleFunc: func(ctx context.Context, payloadBytes []byte, requirementsBytes []byte) (*x402.SettleResponse, error) { + settleCalled = true + return &x402.SettleResponse{ + Success: true, + Transaction: "0xtx", + Network: "eip155:1", + Payer: "0xpayer", + }, nil + }, + supportedFunc: defaultSupportedFunc(), + } + + mockServer := &mockSchemeServer{scheme: "exact"} + + routes := x402http.RoutesConfig{ + "POST /api": x402http.RouteConfig{ + Accepts: x402http.PaymentOptions{ + { + Scheme: "exact", + PayTo: "0xtest", + Price: "$1.00", + Network: "eip155:1", + }, + }, + }, + } + + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _ = json.NewEncoder(w).Encode(map[string]string{"data": "protected-data"}) + }) + + middleware := PaymentMiddlewareFromConfig(routes, + WithFacilitatorClient(mockClient), + WithScheme("eip155:1", mockServer), + WithSyncFacilitatorOnStart(true), + WithTimeout(5*time.Second), + ) + wrapped := middleware(handler) + + req := httptest.NewRequest("POST", "/api", nil) + req.Header.Set("PAYMENT-SIGNATURE", createPaymentHeader("0xtest")) + req.Host = "example.com" + + w := httptest.NewRecorder() + wrapped.ServeHTTP(w, req) + + if w.Code != http.StatusOK { + t.Errorf("Expected status 200, got %d. Body: %s", w.Code, w.Body.String()) + } + + if !settleCalled { + t.Error("Expected settlement to be called") + } + + if w.Header().Get("PAYMENT-RESPONSE") == "" { + t.Error("Expected PAYMENT-RESPONSE header") + } +} + +func TestPaymentMiddleware_SkipsSettlementWhenHandlerReturns400OrHigher(t *testing.T) { + settleCalled := false + + mockClient := &mockFacilitatorClient{ + verifyFunc: func(ctx context.Context, payloadBytes []byte, requirementsBytes []byte) (*x402.VerifyResponse, error) { + return &x402.VerifyResponse{IsValid: true, Payer: "0xpayer"}, nil + }, + settleFunc: func(ctx context.Context, payloadBytes []byte, requirementsBytes []byte) (*x402.SettleResponse, error) { + settleCalled = true + return &x402.SettleResponse{Success: true, Transaction: "0xtx"}, nil + }, + supportedFunc: defaultSupportedFunc(), + } + + mockServer := &mockSchemeServer{scheme: "exact"} + + routes := x402http.RoutesConfig{ + "POST /api": x402http.RouteConfig{ + Accepts: x402http.PaymentOptions{ + { + Scheme: "exact", + PayTo: "0xtest", + Price: "$1.00", + Network: "eip155:1", + }, + }, + }, + } + + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusInternalServerError) + _ = json.NewEncoder(w).Encode(map[string]string{"error": "internal error"}) + }) + + middleware := PaymentMiddlewareFromConfig(routes, + WithFacilitatorClient(mockClient), + WithScheme("eip155:1", mockServer), + WithSyncFacilitatorOnStart(true), + WithTimeout(5*time.Second), + ) + wrapped := middleware(handler) + + req := httptest.NewRequest("POST", "/api", nil) + req.Header.Set("PAYMENT-SIGNATURE", createPaymentHeader("0xtest")) + req.Host = "example.com" + + w := httptest.NewRecorder() + wrapped.ServeHTTP(w, req) + + if w.Code != http.StatusInternalServerError { + t.Errorf("Expected status 500, got %d", w.Code) + } + + if settleCalled { + t.Error("Settlement should NOT be called when handler returns >= 400") + } +} + +func TestPaymentMiddleware_Returns402WhenSettlementFails(t *testing.T) { + mockClient := &mockFacilitatorClient{ + verifyFunc: func(ctx context.Context, payloadBytes []byte, requirementsBytes []byte) (*x402.VerifyResponse, error) { + return &x402.VerifyResponse{IsValid: true, Payer: "0xpayer"}, nil + }, + settleFunc: func(ctx context.Context, payloadBytes []byte, requirementsBytes []byte) (*x402.SettleResponse, error) { + return &x402.SettleResponse{ + Success: false, + ErrorReason: "Insufficient funds", + }, nil + }, + supportedFunc: defaultSupportedFunc(), + } + + mockServer := &mockSchemeServer{scheme: "exact"} + + routes := x402http.RoutesConfig{ + "POST /api": x402http.RouteConfig{ + Accepts: x402http.PaymentOptions{ + { + Scheme: "exact", + PayTo: "0xtest", + Price: "$1.00", + Network: "eip155:1", + }, + }, + }, + } + + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _ = json.NewEncoder(w).Encode(map[string]string{"data": "protected-data"}) + }) + + middleware := PaymentMiddlewareFromConfig(routes, + WithFacilitatorClient(mockClient), + WithScheme("eip155:1", mockServer), + WithSyncFacilitatorOnStart(true), + WithTimeout(5*time.Second), + ) + wrapped := middleware(handler) + + req := httptest.NewRequest("POST", "/api", nil) + req.Header.Set("PAYMENT-SIGNATURE", createPaymentHeader("0xtest")) + req.Host = "example.com" + + w := httptest.NewRecorder() + wrapped.ServeHTTP(w, req) + + if w.Code != http.StatusPaymentRequired { + t.Errorf("Expected status 402, got %d", w.Code) + } + + // Empty body by default on settlement failure + var response map[string]any + if err := json.Unmarshal(w.Body.Bytes(), &response); err != nil { + t.Fatalf("Failed to parse response: %v", err) + } + if len(response) != 0 { + t.Errorf("Expected empty body {}, got %v", response) + } + + // PAYMENT-RESPONSE header must be included on settlement failure + if w.Header().Get("PAYMENT-RESPONSE") == "" { + t.Error("Expected PAYMENT-RESPONSE header on settlement failure") + } +} + +func TestPaymentMiddleware_CustomErrorHandler(t *testing.T) { + customHandlerCalled := false + + mockClient := &mockFacilitatorClient{ + verifyFunc: func(ctx context.Context, payloadBytes []byte, requirementsBytes []byte) (*x402.VerifyResponse, error) { + return &x402.VerifyResponse{IsValid: true, Payer: "0xpayer"}, nil + }, + settleFunc: func(ctx context.Context, payloadBytes []byte, requirementsBytes []byte) (*x402.SettleResponse, error) { + return &x402.SettleResponse{ + Success: false, + ErrorReason: "Settlement rejected", + }, nil + }, + supportedFunc: defaultSupportedFunc(), + } + + mockServer := &mockSchemeServer{scheme: "exact"} + + routes := x402http.RoutesConfig{ + "POST /api": x402http.RouteConfig{ + Accepts: x402http.PaymentOptions{ + { + Scheme: "exact", + PayTo: "0xtest", + Price: "$1.00", + Network: "eip155:1", + }, + }, + }, + } + + customErrorHandler := func(w http.ResponseWriter, r *http.Request, err error) { + customHandlerCalled = true + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusPaymentRequired) + _ = json.NewEncoder(w).Encode(map[string]string{ + "custom_error": err.Error(), + }) + } + + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _ = json.NewEncoder(w).Encode(map[string]string{"data": "protected-data"}) + }) + + middleware := PaymentMiddlewareFromConfig(routes, + WithFacilitatorClient(mockClient), + WithScheme("eip155:1", mockServer), + WithErrorHandler(customErrorHandler), + WithSyncFacilitatorOnStart(true), + WithTimeout(5*time.Second), + ) + wrapped := middleware(handler) + + req := httptest.NewRequest("POST", "/api", nil) + req.Header.Set("PAYMENT-SIGNATURE", createPaymentHeader("0xtest")) + req.Host = "example.com" + + w := httptest.NewRecorder() + wrapped.ServeHTTP(w, req) + + if !customHandlerCalled { + t.Error("Expected custom error handler to be called") + } + + var response map[string]any + if err := json.Unmarshal(w.Body.Bytes(), &response); err != nil { + t.Fatalf("Failed to parse response: %v", err) + } + + if response["custom_error"] == nil { + t.Error("Expected custom_error in response") + } +} + +func TestPaymentMiddleware_CustomSettlementHandler(t *testing.T) { + settlementHandlerCalled := false + var capturedSettleResponse *x402.SettleResponse + + mockClient := &mockFacilitatorClient{ + verifyFunc: func(ctx context.Context, payloadBytes []byte, requirementsBytes []byte) (*x402.VerifyResponse, error) { + return &x402.VerifyResponse{IsValid: true, Payer: "0xpayer"}, nil + }, + settleFunc: func(ctx context.Context, payloadBytes []byte, requirementsBytes []byte) (*x402.SettleResponse, error) { + return &x402.SettleResponse{ + Success: true, + Transaction: "0xtx123", + Network: "eip155:1", + Payer: "0xpayer", + }, nil + }, + supportedFunc: defaultSupportedFunc(), + } + + mockServer := &mockSchemeServer{scheme: "exact"} + + routes := x402http.RoutesConfig{ + "POST /api": x402http.RouteConfig{ + Accepts: x402http.PaymentOptions{ + { + Scheme: "exact", + PayTo: "0xtest", + Price: "$1.00", + Network: "eip155:1", + }, + }, + }, + } + + customSettlementHandler := func(w http.ResponseWriter, r *http.Request, settleResponse *x402.SettleResponse) { + settlementHandlerCalled = true + capturedSettleResponse = settleResponse + w.Header().Set("X-Transaction-ID", settleResponse.Transaction) + } + + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _ = json.NewEncoder(w).Encode(map[string]string{"data": "protected-data"}) + }) + + middleware := PaymentMiddlewareFromConfig(routes, + WithFacilitatorClient(mockClient), + WithScheme("eip155:1", mockServer), + WithSettlementHandler(customSettlementHandler), + WithSyncFacilitatorOnStart(true), + WithTimeout(5*time.Second), + ) + wrapped := middleware(handler) + + req := httptest.NewRequest("POST", "/api", nil) + req.Header.Set("PAYMENT-SIGNATURE", createPaymentHeader("0xtest")) + req.Host = "example.com" + + w := httptest.NewRecorder() + wrapped.ServeHTTP(w, req) + + if w.Code != http.StatusOK { + t.Errorf("Expected status 200, got %d", w.Code) + } + + if !settlementHandlerCalled { + t.Error("Expected custom settlement handler to be called") + } + + if capturedSettleResponse == nil { + t.Fatal("Expected settle response to be captured") + } + + if capturedSettleResponse.Transaction != "0xtx123" { + t.Errorf("Expected transaction '0xtx123', got '%s'", capturedSettleResponse.Transaction) + } + + if w.Header().Get("X-Transaction-ID") != "0xtx123" { + t.Error("Expected custom X-Transaction-ID header") + } +} + +func TestPaymentMiddleware_WithTimeout(t *testing.T) { + mockClient := &mockFacilitatorClient{supportedFunc: defaultSupportedFunc()} + mockServer := &mockSchemeServer{scheme: "exact"} + + routes := x402http.RoutesConfig{ + "*": x402http.RouteConfig{ + Accepts: x402http.PaymentOptions{ + { + Scheme: "exact", + PayTo: "0xtest", + Price: "$1.00", + Network: "eip155:1", + }, + }, + }, + } + + timeout := 10 * time.Second + + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + _ = json.NewEncoder(w).Encode(map[string]string{"message": "success"}) + }) + + middleware := PaymentMiddlewareFromConfig(routes, + WithFacilitatorClient(mockClient), + WithScheme("eip155:1", mockServer), + WithTimeout(timeout), + WithSyncFacilitatorOnStart(true), + ) + wrapped := middleware(handler) + + req := httptest.NewRequest("GET", "/test", nil) + w := httptest.NewRecorder() + wrapped.ServeHTTP(w, req) + + if w.Code != http.StatusPaymentRequired { + t.Errorf("Expected status 402, got %d", w.Code) + } +} + +// ============================================================================ +// X402Payment (Builder Pattern) Tests +// ============================================================================ + +func TestX402Payment_CreatesWorkingMiddleware(t *testing.T) { + mockClient := &mockFacilitatorClient{supportedFunc: defaultSupportedFunc()} + mockServer := &mockSchemeServer{scheme: "exact"} + + routes := x402http.RoutesConfig{ + "GET /api": x402http.RouteConfig{ + Accepts: x402http.PaymentOptions{ + { + Scheme: "exact", + PayTo: "0xtest", + Price: "$1.00", + Network: "eip155:1", + }, + }, + }, + } + + protectedHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + _ = json.NewEncoder(w).Encode(map[string]string{"data": "protected"}) + }) + + middleware := X402Payment(Config{ + Routes: routes, + Facilitator: mockClient, + Schemes: []SchemeConfig{ + {Network: "eip155:1", Server: mockServer}, + }, + SyncFacilitatorOnStart: true, + Timeout: 5 * time.Second, + }) + wrapped := middleware(protectedHandler) + + // Test non-protected route passes through + req := httptest.NewRequest("GET", "/public", nil) + w := httptest.NewRecorder() + wrapped.ServeHTTP(w, req) + + if w.Code != http.StatusOK { + t.Errorf("Expected status 200 for public route, got %d", w.Code) + } + + // Test protected route requires payment + req = httptest.NewRequest("GET", "/api", nil) + w = httptest.NewRecorder() + wrapped.ServeHTTP(w, req) + + if w.Code != http.StatusPaymentRequired { + t.Errorf("Expected status 402 for protected route, got %d", w.Code) + } +} + +func TestX402Payment_RegistersMultipleFacilitators(t *testing.T) { + mockClient1 := &mockFacilitatorClient{supportedFunc: defaultSupportedFunc()} + mockClient2 := &mockFacilitatorClient{supportedFunc: defaultSupportedFunc()} + mockServer := &mockSchemeServer{scheme: "exact"} + + routes := x402http.RoutesConfig{ + "*": x402http.RouteConfig{ + Accepts: x402http.PaymentOptions{ + { + Scheme: "exact", + PayTo: "0xtest", + Price: "$1.00", + Network: "eip155:1", + }, + }, + }, + } + + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + _ = json.NewEncoder(w).Encode(map[string]string{"message": "success"}) + }) + + middleware := X402Payment(Config{ + Routes: routes, + Facilitators: []x402.FacilitatorClient{mockClient1, mockClient2}, + Schemes: []SchemeConfig{ + {Network: "eip155:1", Server: mockServer}, + }, + SyncFacilitatorOnStart: true, + }) + wrapped := middleware(handler) + + req := httptest.NewRequest("GET", "/test", nil) + w := httptest.NewRecorder() + wrapped.ServeHTTP(w, req) + + if w.Code != http.StatusPaymentRequired { + t.Errorf("Expected status 402, got %d", w.Code) + } +} + +func TestX402Payment_RegistersMultipleSchemes(t *testing.T) { + mockServer1 := &mockSchemeServer{scheme: "exact"} + mockServer2 := &mockSchemeServer{scheme: "exact"} + + routes := x402http.RoutesConfig{ + "*": x402http.RouteConfig{ + Accepts: x402http.PaymentOptions{ + { + Scheme: "exact", + PayTo: "0xtest", + Price: "$1.00", + Network: "eip155:1", + }, + }, + }, + } + + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + _ = json.NewEncoder(w).Encode(map[string]string{"message": "success"}) + }) + + middleware := X402Payment(Config{ + Routes: routes, + Schemes: []SchemeConfig{ + {Network: "eip155:1", Server: mockServer1}, + {Network: "eip155:8453", Server: mockServer2}, + }, + SyncFacilitatorOnStart: false, + }) + wrapped := middleware(handler) + + req := httptest.NewRequest("GET", "/test", nil) + w := httptest.NewRecorder() + wrapped.ServeHTTP(w, req) + + if w.Code != http.StatusPaymentRequired { + t.Errorf("Expected status 402, got %d", w.Code) + } +} + +// ============================================================================ +// Context Helper Tests +// ============================================================================ + +func TestPayloadFromContext_ReturnsPayload(t *testing.T) { + payload := &types.PaymentPayload{ + X402Version: 2, + Payload: map[string]any{"sig": "test"}, + } + + ctx := withPayload(context.Background(), payload) + got, ok := PayloadFromContext(ctx) + + if !ok { + t.Fatal("Expected payload to be found in context") + } + if got.X402Version != 2 { + t.Errorf("Expected X402Version 2, got %d", got.X402Version) + } +} + +func TestPayloadFromContext_ReturnsFalseWhenMissing(t *testing.T) { + _, ok := PayloadFromContext(context.Background()) + if ok { + t.Error("Expected payload not to be found in empty context") + } +} + +func TestRequirementsFromContext_ReturnsRequirements(t *testing.T) { + reqs := &types.PaymentRequirements{ + Scheme: "exact", + Network: "eip155:1", + } + + ctx := withRequirements(context.Background(), reqs) + got, ok := RequirementsFromContext(ctx) + + if !ok { + t.Fatal("Expected requirements to be found in context") + } + if got.Scheme != "exact" { + t.Errorf("Expected scheme 'exact', got '%s'", got.Scheme) + } +} + +func TestRequirementsFromContext_ReturnsFalseWhenMissing(t *testing.T) { + _, ok := RequirementsFromContext(context.Background()) + if ok { + t.Error("Expected requirements not to be found in empty context") + } +} + +// ============================================================================ +// responseCapture Tests +// ============================================================================ + +func TestResponseCapture_CapturesStatusCode(t *testing.T) { + capture := &responseCapture{ + ResponseWriter: httptest.NewRecorder(), + body: &bytes.Buffer{}, + statusCode: http.StatusOK, + } + + capture.WriteHeader(http.StatusCreated) + + if capture.statusCode != http.StatusCreated { + t.Errorf("Expected status 201, got %d", capture.statusCode) + } +} + +func TestResponseCapture_CapturesBody(t *testing.T) { + capture := &responseCapture{ + ResponseWriter: httptest.NewRecorder(), + body: &bytes.Buffer{}, + statusCode: http.StatusOK, + } + + data := []byte(`{"message":"test"}`) + n, err := capture.Write(data) + + if err != nil { + t.Fatalf("Write failed: %v", err) + } + if n != len(data) { + t.Errorf("Expected to write %d bytes, wrote %d", len(data), n) + } + if capture.body.String() != `{"message":"test"}` { + t.Errorf("Expected body '%s', got '%s'", `{"message":"test"}`, capture.body.String()) + } +} + +func TestResponseCapture_WriteHeaderOnlyOnce(t *testing.T) { + capture := &responseCapture{ + ResponseWriter: httptest.NewRecorder(), + body: &bytes.Buffer{}, + statusCode: http.StatusOK, + } + + capture.WriteHeader(http.StatusCreated) + capture.WriteHeader(http.StatusAccepted) // Should be ignored + + if capture.statusCode != http.StatusCreated { + t.Errorf("Expected status 201 (first call), got %d", capture.statusCode) + } +} + +func TestPaymentMiddleware_PayloadAvailableInDownstreamHandler(t *testing.T) { + var capturedPayload *types.PaymentPayload + + mockClient := &mockFacilitatorClient{ + verifyFunc: func(ctx context.Context, payloadBytes []byte, requirementsBytes []byte) (*x402.VerifyResponse, error) { + return &x402.VerifyResponse{IsValid: true, Payer: "0xpayer"}, nil + }, + settleFunc: func(ctx context.Context, payloadBytes []byte, requirementsBytes []byte) (*x402.SettleResponse, error) { + return &x402.SettleResponse{ + Success: true, + Transaction: "0xtx", + Network: "eip155:1", + Payer: "0xpayer", + }, nil + }, + supportedFunc: defaultSupportedFunc(), + } + + mockServer := &mockSchemeServer{scheme: "exact"} + + routes := x402http.RoutesConfig{ + "POST /api": x402http.RouteConfig{ + Accepts: x402http.PaymentOptions{ + { + Scheme: "exact", + PayTo: "0xtest", + Price: "$1.00", + Network: "eip155:1", + }, + }, + }, + } + + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + payload, ok := PayloadFromContext(r.Context()) + if ok { + capturedPayload = payload + } + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte(`{"ok":true}`)) + }) + + middleware := PaymentMiddlewareFromConfig(routes, + WithFacilitatorClient(mockClient), + WithScheme("eip155:1", mockServer), + WithSyncFacilitatorOnStart(true), + WithTimeout(5*time.Second), + ) + wrapped := middleware(handler) + + req := httptest.NewRequest("POST", "/api", nil) + req.Header.Set("PAYMENT-SIGNATURE", createPaymentHeader("0xtest")) + req.Host = "example.com" + + w := httptest.NewRecorder() + wrapped.ServeHTTP(w, req) + + if w.Code != http.StatusOK { + t.Errorf("Expected status 200, got %d. Body: %s", w.Code, w.Body.String()) + } + + if capturedPayload == nil { + t.Fatal("Expected payment payload to be available in downstream handler context") + } + + if capturedPayload.X402Version != 2 { + t.Errorf("Expected X402Version 2, got %d", capturedPayload.X402Version) + } +} + +// ============================================================================ +// PaymentMiddlewareFromHTTPServer Tests +// ============================================================================ + +func TestPaymentMiddlewareFromHTTPServer_Returns402ForProtectedRoute(t *testing.T) { + mockClient := &mockFacilitatorClient{supportedFunc: defaultSupportedFunc()} + mockServer := &mockSchemeServer{scheme: "exact"} + + routes := x402http.RoutesConfig{ + "GET /api": x402http.RouteConfig{ + Accepts: x402http.PaymentOptions{ + { + Scheme: "exact", + PayTo: "0xtest", + Price: "$1.00", + Network: "eip155:1", + }, + }, + }, + } + + // Create resource server and wrap as HTTPServer (same pattern as user would) + resourceServer := x402.Newx402ResourceServer( + x402.WithFacilitatorClient(mockClient), + ) + resourceServer.Register("eip155:1", mockServer) + + httpServer := x402http.Wrappedx402HTTPResourceServer(routes, resourceServer) + + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + _ = json.NewEncoder(w).Encode(map[string]string{"data": "protected"}) + }) + + middleware := PaymentMiddlewareFromHTTPServer(httpServer, + WithSyncFacilitatorOnStart(true), + WithTimeout(5*time.Second), + ) + wrapped := middleware(handler) + + // Protected route should require payment + req := httptest.NewRequest("GET", "/api", nil) + w := httptest.NewRecorder() + wrapped.ServeHTTP(w, req) + + if w.Code != http.StatusPaymentRequired { + t.Errorf("Expected status 402 for protected route, got %d", w.Code) + } + + // Non-protected route should pass through + req = httptest.NewRequest("GET", "/public", nil) + w = httptest.NewRecorder() + wrapped.ServeHTTP(w, req) + + if w.Code != http.StatusOK { + t.Errorf("Expected status 200 for public route, got %d", w.Code) + } +} + +// ============================================================================ +// Settlement Override Round-Trip Tests +// ============================================================================ + +// TestPaymentMiddleware_SettlementOverrideViaHeader verifies the full path: +// SetSettlementOverrides (handler) → responseCapture.Header() → +// HTTPTransportContext.ResponseHeaders → ProcessSettlement → facilitator. +// +// This test would have caught the header canonicalization bug (issue #1): +// net/http stores headers as Title-Case ("Settlement-Overrides") but the +// old code used a raw map lookup with the lowercase key ("settlement-overrides"), +// silently missing the override every time. +func TestPaymentMiddleware_SettlementOverrideViaHeader(t *testing.T) { + var capturedRequirementsBytes []byte + + mockClient := &mockFacilitatorClient{ + verifyFunc: func(ctx context.Context, payloadBytes []byte, requirementsBytes []byte) (*x402.VerifyResponse, error) { + return &x402.VerifyResponse{IsValid: true, Payer: "0xpayer"}, nil + }, + settleFunc: func(ctx context.Context, payloadBytes []byte, requirementsBytes []byte) (*x402.SettleResponse, error) { + capturedRequirementsBytes = requirementsBytes + return &x402.SettleResponse{ + Success: true, + Transaction: "0xtx", + Network: "eip155:1", + Payer: "0xpayer", + }, nil + }, + supportedFunc: defaultSupportedFunc(), + } + + mockServer := &mockSchemeServer{scheme: "exact"} + + routes := x402http.RoutesConfig{ + "POST /api": x402http.RouteConfig{ + Accepts: x402http.PaymentOptions{ + { + Scheme: "exact", + PayTo: "0xtest", + Price: "$1.00", // mockSchemeServer.ParsePrice returns Amount "1000000" + Network: "eip155:1", + }, + }, + }, + } + + // Handler calls SetSettlementOverrides to request partial settlement of 500. + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + SetSettlementOverrides(w, &x402.SettlementOverrides{Amount: "500"}) + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _ = json.NewEncoder(w).Encode(map[string]string{"data": "ok"}) + }) + + middleware := PaymentMiddlewareFromConfig(routes, + WithFacilitatorClient(mockClient), + WithScheme("eip155:1", mockServer), + WithSyncFacilitatorOnStart(true), + WithTimeout(5*time.Second), + ) + wrapped := middleware(handler) + + req := httptest.NewRequest("POST", "/api", nil) + req.Header.Set("PAYMENT-SIGNATURE", createPaymentHeader("0xtest")) + req.Host = "example.com" + + w := httptest.NewRecorder() + wrapped.ServeHTTP(w, req) + + if w.Code != http.StatusOK { + t.Fatalf("Expected status 200, got %d. Body: %s", w.Code, w.Body.String()) + } + + if w.Header().Get("PAYMENT-RESPONSE") == "" { + t.Error("Expected PAYMENT-RESPONSE header (settlement must succeed)") + } + + // The settlement-overrides header must be stripped from the client response. + // If the canonicalization bug were present, ProcessSettlement would fail to find + // and delete the canonical "Settlement-Overrides" key, and the header would leak. + if w.Header().Get(x402http.SettlementOverridesHeader) != "" { + t.Error("settlement-overrides header must be stripped from the client response by the middleware") + } + + // Verify the overridden amount reached the facilitator. + if capturedRequirementsBytes == nil { + t.Fatal("settle was never called; payment was not processed") + } + var settledReqs struct { + Amount string `json:"amount"` + } + if err := json.Unmarshal(capturedRequirementsBytes, &settledReqs); err != nil { + t.Fatalf("failed to unmarshal captured requirements: %v", err) + } + if settledReqs.Amount != "500" { + t.Errorf("expected settle to be called with amount \"500\" (override), got %q", settledReqs.Amount) + } +} + +func TestPaymentMiddlewareFromHTTPServer_SettlesVerifiedPayment(t *testing.T) { + settleCalled := false + + mockClient := &mockFacilitatorClient{ + verifyFunc: func(ctx context.Context, payloadBytes []byte, requirementsBytes []byte) (*x402.VerifyResponse, error) { + return &x402.VerifyResponse{IsValid: true, Payer: "0xpayer"}, nil + }, + settleFunc: func(ctx context.Context, payloadBytes []byte, requirementsBytes []byte) (*x402.SettleResponse, error) { + settleCalled = true + return &x402.SettleResponse{ + Success: true, + Transaction: "0xtx", + Network: "eip155:1", + Payer: "0xpayer", + }, nil + }, + supportedFunc: defaultSupportedFunc(), + } + mockServer := &mockSchemeServer{scheme: "exact"} + + routes := x402http.RoutesConfig{ + "POST /api": x402http.RouteConfig{ + Accepts: x402http.PaymentOptions{ + { + Scheme: "exact", + PayTo: "0xtest", + Price: "$1.00", + Network: "eip155:1", + }, + }, + }, + } + + resourceServer := x402.Newx402ResourceServer( + x402.WithFacilitatorClient(mockClient), + ) + resourceServer.Register("eip155:1", mockServer) + + httpServer := x402http.Wrappedx402HTTPResourceServer(routes, resourceServer) + + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _ = json.NewEncoder(w).Encode(map[string]string{"data": "protected-data"}) + }) + + middleware := PaymentMiddlewareFromHTTPServer(httpServer, + WithSyncFacilitatorOnStart(true), + WithTimeout(5*time.Second), + ) + wrapped := middleware(handler) + + req := httptest.NewRequest("POST", "/api", nil) + req.Header.Set("PAYMENT-SIGNATURE", createPaymentHeader("0xtest")) + req.Host = "example.com" + + w := httptest.NewRecorder() + wrapped.ServeHTTP(w, req) + + if w.Code != http.StatusOK { + t.Errorf("Expected status 200, got %d. Body: %s", w.Code, w.Body.String()) + } + + if !settleCalled { + t.Error("Expected settlement to be called") + } + + if w.Header().Get("PAYMENT-RESPONSE") == "" { + t.Error("Expected PAYMENT-RESPONSE header") + } +} diff --git a/go/http/paywall.go b/go/http/paywall.go new file mode 100644 index 0000000000..a6191f7f4c --- /dev/null +++ b/go/http/paywall.go @@ -0,0 +1,126 @@ +package http + +import ( + "strings" + + "github.com/coinbase/x402/go/types" +) + +// ============================================================================ +// Paywall Provider Interfaces +// ============================================================================ + +// PaywallProvider generates HTML for browser-facing 402 responses. +// Register a custom implementation via RegisterPaywallProvider to override +// the built-in EVM/SVM templates. +type PaywallProvider interface { + GenerateHTML(paymentRequired types.PaymentRequired, config *PaywallConfig) string +} + +// PaywallNetworkHandler handles paywall HTML generation for a specific network family. +// Used with PaywallBuilder to compose network-specific handlers into a single PaywallProvider. +type PaywallNetworkHandler interface { + // Supports returns true if this handler can generate HTML for the given payment requirement. + Supports(requirement types.PaymentRequirements) bool + + // GenerateHTML generates the paywall HTML for the given requirement. + GenerateHTML(requirement types.PaymentRequirements, paymentRequired types.PaymentRequired, config *PaywallConfig) string +} + +// ============================================================================ +// Built-in Network Handlers +// ============================================================================ + +// EVMPaywallHandler generates paywall HTML for EVM-compatible networks (eip155:*). +type EVMPaywallHandler struct{} + +// Supports returns true for EVM networks (eip155:* CAIP-2 identifiers). +func (h *EVMPaywallHandler) Supports(requirement types.PaymentRequirements) bool { + return strings.HasPrefix(requirement.Network, "eip155:") +} + +// GenerateHTML generates paywall HTML using the built-in EVM template. +func (h *EVMPaywallHandler) GenerateHTML(_ types.PaymentRequirements, paymentRequired types.PaymentRequired, config *PaywallConfig) string { + return injectPaywallConfig(EVMPaywallTemplate, paymentRequired, config) +} + +// SVMPaywallHandler generates paywall HTML for Solana networks (solana:*). +type SVMPaywallHandler struct{} + +// Supports returns true for Solana networks (solana:* CAIP-2 identifiers). +func (h *SVMPaywallHandler) Supports(requirement types.PaymentRequirements) bool { + return strings.HasPrefix(requirement.Network, "solana:") +} + +// GenerateHTML generates paywall HTML using the built-in SVM template. +func (h *SVMPaywallHandler) GenerateHTML(_ types.PaymentRequirements, paymentRequired types.PaymentRequired, config *PaywallConfig) string { + return injectPaywallConfig(SVMPaywallTemplate, paymentRequired, config) +} + +// ============================================================================ +// Paywall Builder +// ============================================================================ + +// PaywallBuilder composes multiple PaywallNetworkHandlers into a single PaywallProvider. +// Use NewPaywallBuilder to create a builder, add network handlers, and call Build. +type PaywallBuilder struct { + handlers []PaywallNetworkHandler + config *PaywallConfig +} + +// NewPaywallBuilder creates a new PaywallBuilder. +func NewPaywallBuilder() *PaywallBuilder { + return &PaywallBuilder{} +} + +// WithNetwork adds a network handler to the builder. +func (b *PaywallBuilder) WithNetwork(handler PaywallNetworkHandler) *PaywallBuilder { + b.handlers = append(b.handlers, handler) + return b +} + +// WithConfig sets default paywall configuration for the builder. +func (b *PaywallBuilder) WithConfig(config *PaywallConfig) *PaywallBuilder { + b.config = config + return b +} + +// Build creates a PaywallProvider that dispatches to the first matching network handler. +func (b *PaywallBuilder) Build() PaywallProvider { + return &compositePaywallProvider{ + handlers: b.handlers, + config: b.config, + } +} + +// compositePaywallProvider dispatches to the first handler that supports the payment requirement. +type compositePaywallProvider struct { + handlers []PaywallNetworkHandler + config *PaywallConfig +} + +func (p *compositePaywallProvider) GenerateHTML(paymentRequired types.PaymentRequired, config *PaywallConfig) string { + // Use builder config as fallback if no per-call config provided + effectiveConfig := config + if effectiveConfig == nil { + effectiveConfig = p.config + } + + for _, req := range paymentRequired.Accepts { + for _, handler := range p.handlers { + if handler.Supports(req) { + return handler.GenerateHTML(req, paymentRequired, effectiveConfig) + } + } + } + + return "" +} + +// DefaultPaywallProvider creates a PaywallProvider with built-in EVM and SVM handlers. +func DefaultPaywallProvider() PaywallProvider { + return NewPaywallBuilder(). + WithNetwork(&EVMPaywallHandler{}). + WithNetwork(&SVMPaywallHandler{}). + Build() +} diff --git a/go/http/paywall_test.go b/go/http/paywall_test.go new file mode 100644 index 0000000000..ae1684bdd5 --- /dev/null +++ b/go/http/paywall_test.go @@ -0,0 +1,325 @@ +package http + +import ( + "strings" + "testing" + + "github.com/coinbase/x402/go/types" +) + +// mockPaywallProvider is a test PaywallProvider that returns configurable HTML. +type mockPaywallProvider struct { + html string +} + +func (m *mockPaywallProvider) GenerateHTML(_ types.PaymentRequired, _ *PaywallConfig) string { + return m.html +} + +// mockNetworkHandler is a test PaywallNetworkHandler for a configurable network prefix. +type mockNetworkHandler struct { + prefix string + html string +} + +func (m *mockNetworkHandler) Supports(req types.PaymentRequirements) bool { + return strings.HasPrefix(req.Network, m.prefix) +} + +func (m *mockNetworkHandler) GenerateHTML(_ types.PaymentRequirements, _ types.PaymentRequired, _ *PaywallConfig) string { + return m.html +} + +func makePaymentRequired(network string) types.PaymentRequired { + return types.PaymentRequired{ + X402Version: 2, + Accepts: []types.PaymentRequirements{ + { + Scheme: "exact", + Network: network, + Asset: "USDC", + Amount: "1000000", + PayTo: "0xtest", + }, + }, + Resource: &types.ResourceInfo{ + URL: "/api/test", + Description: "Test API", + }, + } +} + +// --- EVMPaywallHandler tests --- + +func TestEVMPaywallHandler_Supports(t *testing.T) { + handler := &EVMPaywallHandler{} + + tests := []struct { + network string + want bool + }{ + {"eip155:1", true}, + {"eip155:8453", true}, + {"eip155:84532", true}, + {"solana:mainnet", false}, + {"solana:devnet", false}, + {"aptos:mainnet", false}, + {"", false}, + } + + for _, tt := range tests { + t.Run(tt.network, func(t *testing.T) { + req := types.PaymentRequirements{Network: tt.network} + got := handler.Supports(req) + if got != tt.want { + t.Errorf("EVMPaywallHandler.Supports(%q) = %v, want %v", tt.network, got, tt.want) + } + }) + } +} + +// --- SVMPaywallHandler tests --- + +func TestSVMPaywallHandler_Supports(t *testing.T) { + handler := &SVMPaywallHandler{} + + tests := []struct { + network string + want bool + }{ + {"solana:mainnet", true}, + {"solana:devnet", true}, + {"eip155:1", false}, + {"eip155:8453", false}, + {"aptos:mainnet", false}, + {"", false}, + } + + for _, tt := range tests { + t.Run(tt.network, func(t *testing.T) { + req := types.PaymentRequirements{Network: tt.network} + got := handler.Supports(req) + if got != tt.want { + t.Errorf("SVMPaywallHandler.Supports(%q) = %v, want %v", tt.network, got, tt.want) + } + }) + } +} + +// --- PaywallBuilder tests --- + +func TestPaywallBuilder_Build(t *testing.T) { + provider := NewPaywallBuilder(). + WithNetwork(&mockNetworkHandler{prefix: "eip155:", html: ""}). + WithNetwork(&mockNetworkHandler{prefix: "solana:", html: ""}). + Build() + + t.Run("matches EVM network", func(t *testing.T) { + got := provider.GenerateHTML(makePaymentRequired("eip155:8453"), nil) + if got != "" { + t.Errorf("expected , got %q", got) + } + }) + + t.Run("matches Solana network", func(t *testing.T) { + got := provider.GenerateHTML(makePaymentRequired("solana:mainnet"), nil) + if got != "" { + t.Errorf("expected , got %q", got) + } + }) + + t.Run("no match returns empty string", func(t *testing.T) { + got := provider.GenerateHTML(makePaymentRequired("aptos:mainnet"), nil) + if got != "" { + t.Errorf("expected empty string for unsupported network, got %q", got) + } + }) +} + +func TestPaywallBuilder_WithConfig(t *testing.T) { + var capturedConfig *PaywallConfig + + handler := &configCapturingHandler{ + prefix: "eip155:", + onGenerate: func(config *PaywallConfig) { + capturedConfig = config + }, + } + + builderConfig := &PaywallConfig{AppName: "TestApp", Testnet: true} + provider := NewPaywallBuilder(). + WithNetwork(handler). + WithConfig(builderConfig). + Build() + + t.Run("uses builder config when no per-call config", func(t *testing.T) { + provider.GenerateHTML(makePaymentRequired("eip155:1"), nil) + if capturedConfig == nil || capturedConfig.AppName != "TestApp" { + t.Errorf("expected builder config to be used, got %+v", capturedConfig) + } + }) + + t.Run("per-call config overrides builder config", func(t *testing.T) { + callConfig := &PaywallConfig{AppName: "CallApp"} + provider.GenerateHTML(makePaymentRequired("eip155:1"), callConfig) + if capturedConfig == nil || capturedConfig.AppName != "CallApp" { + t.Errorf("expected per-call config to override, got %+v", capturedConfig) + } + }) +} + +type configCapturingHandler struct { + prefix string + onGenerate func(config *PaywallConfig) +} + +func (h *configCapturingHandler) Supports(req types.PaymentRequirements) bool { + return strings.HasPrefix(req.Network, h.prefix) +} + +func (h *configCapturingHandler) GenerateHTML(_ types.PaymentRequirements, _ types.PaymentRequired, config *PaywallConfig) string { + if h.onGenerate != nil { + h.onGenerate(config) + } + return "" +} + +// --- DefaultPaywallProvider tests --- + +func TestDefaultPaywallProvider(t *testing.T) { + provider := DefaultPaywallProvider() + + t.Run("EVM network returns non-empty HTML", func(t *testing.T) { + got := provider.GenerateHTML(makePaymentRequired("eip155:8453"), nil) + if got == "" { + t.Error("expected non-empty HTML for EVM network") + } + if !strings.Contains(got, "window.x402") { + t.Error("expected window.x402 config injection in HTML") + } + }) + + t.Run("Solana network returns non-empty HTML", func(t *testing.T) { + got := provider.GenerateHTML(makePaymentRequired("solana:mainnet"), nil) + if got == "" { + t.Error("expected non-empty HTML for Solana network") + } + if !strings.Contains(got, "window.x402") { + t.Error("expected window.x402 config injection in HTML") + } + }) + + t.Run("unsupported network returns empty", func(t *testing.T) { + got := provider.GenerateHTML(makePaymentRequired("aptos:mainnet"), nil) + if got != "" { + t.Errorf("expected empty string for unsupported network, got length %d", len(got)) + } + }) +} + +// --- RegisterPaywallProvider integration tests --- + +func TestRegisterPaywallProvider(t *testing.T) { + routes := RoutesConfig{ + "GET /api/test": { + Accepts: PaymentOptions{ + { + Scheme: "exact", + PayTo: "0xtest", + Price: "$1.00", + Network: "eip155:8453", + }, + }, + }, + } + + t.Run("returns server for chaining", func(t *testing.T) { + server := Newx402HTTPResourceServer(routes) + result := server.RegisterPaywallProvider(&mockPaywallProvider{html: ""}) + if result != server { + t.Error("expected RegisterPaywallProvider to return the same server instance") + } + }) + + t.Run("registered provider is used in generatePaywallHTMLV2", func(t *testing.T) { + server := Newx402HTTPResourceServer(routes) + server.RegisterPaywallProvider(&mockPaywallProvider{html: ""}) + + got := server.generatePaywallHTMLV2(makePaymentRequired("eip155:8453"), nil, "") + if got != "" { + t.Errorf("expected , got %q", got) + } + }) + + t.Run("CustomPaywallHTML takes priority over provider", func(t *testing.T) { + server := Newx402HTTPResourceServer(routes) + server.RegisterPaywallProvider(&mockPaywallProvider{html: ""}) + + got := server.generatePaywallHTMLV2(makePaymentRequired("eip155:8453"), nil, "") + if got != "" { + t.Errorf("expected , got %q", got) + } + }) + + t.Run("no provider falls back to built-in template", func(t *testing.T) { + server := Newx402HTTPResourceServer(routes) + + got := server.generatePaywallHTMLV2(makePaymentRequired("eip155:8453"), nil, "") + if got == "" { + t.Error("expected non-empty HTML from built-in template") + } + if !strings.Contains(got, "window.x402") { + t.Error("expected built-in template with window.x402 injection") + } + }) +} + +// --- injectPaywallConfig tests --- + +func TestInjectPaywallConfig(t *testing.T) { + template := "" + paymentReq := makePaymentRequired("eip155:8453") + + t.Run("injects window.x402 config", func(t *testing.T) { + got := injectPaywallConfig(template, paymentReq, nil) + if !strings.Contains(got, "window.x402") { + t.Error("expected window.x402 in output") + } + if !strings.Contains(got, "") { + t.Error("expected to remain in output") + } + }) + + t.Run("includes PaywallConfig values", func(t *testing.T) { + config := &PaywallConfig{ + AppName: "TestApp", + AppLogo: "https://example.com/logo.png", + Testnet: true, + } + got := injectPaywallConfig(template, paymentReq, config) + if !strings.Contains(got, "TestApp") { + t.Error("expected appName in output") + } + if !strings.Contains(got, "https://example.com/logo.png") { + t.Error("expected appLogo in output") + } + if !strings.Contains(got, "testnet: true") { + t.Error("expected testnet: true in output") + } + }) + + t.Run("escapes HTML in config values", func(t *testing.T) { + config := &PaywallConfig{AppName: ``} + got := injectPaywallConfig(template, paymentReq, config) + if strings.Contains(got, ``) { + t.Error("expected HTML-escaped appName, got raw script tag") + } + }) + + t.Run("uses resource URL as currentUrl fallback", func(t *testing.T) { + got := injectPaywallConfig(template, paymentReq, nil) + if !strings.Contains(got, "/api/test") { + t.Error("expected resource URL as currentUrl fallback") + } + }) +} diff --git a/go/http/server.go b/go/http/server.go index 15455d4663..d66ae11b88 100644 --- a/go/http/server.go +++ b/go/http/server.go @@ -6,12 +6,15 @@ import ( "encoding/json" "fmt" "html" + "log" + "net/http" "net/url" "regexp" "strconv" "strings" x402 "github.com/coinbase/x402/go" + exttypes "github.com/coinbase/x402/go/extensions/types" "github.com/coinbase/x402/go/types" ) @@ -19,6 +22,7 @@ import ( var ( multiSlashRegex = regexp.MustCompile(`/+`) paramRegex = regexp.MustCompile(`\\\[([^\]]+)\\\]`) + colonParamRegex = exttypes.ColonParamRegex ) // ============================================================================ @@ -118,21 +122,48 @@ type RoutesConfig map[string]RouteConfig // CompiledRoute is a parsed route ready for matching type CompiledRoute struct { - Verb string - Regex *regexp.Regexp - Config RouteConfig + Verb string + Regex *regexp.Regexp + Config RouteConfig + Pattern string } // ============================================================================ // Request/Response Types // ============================================================================ +// ProtectedRequestHookResult represents the result of a protected request hook. +// A nil result means the hook has no opinion and the next hook (or payment flow) should proceed. +type ProtectedRequestHookResult struct { + // GrantAccess bypasses payment and grants free access to the resource. + GrantAccess bool + // Abort denies the request with a 403 status and the provided Reason. + Abort bool + Reason string +} + +// ProtectedRequestHook is called on every request to a protected route, before payment processing. +// It receives the request context and the matched route configuration. +// Return nil to continue to the next hook or payment flow. +// Return a result with GrantAccess=true to bypass payment. +// Return a result with Abort=true to deny the request with a 403 status. +type ProtectedRequestHook func(ctx context.Context, reqCtx HTTPRequestContext, routeConfig RouteConfig) (*ProtectedRequestHookResult, error) + // HTTPRequestContext encapsulates an HTTP request type HTTPRequestContext struct { Adapter HTTPAdapter Path string Method string PaymentHeader string + RoutePattern string +} + +// HTTPTransportContext carries request and response data through settlement processing. +// ResponseHeaders must be an http.Header — use Header.Get/Del to preserve canonicalization. +type HTTPTransportContext struct { + Request *HTTPRequestContext + ResponseBody []byte + ResponseHeaders http.Header } // HTTPResponseInstructions tells the framework how to respond @@ -166,6 +197,9 @@ type ProcessSettleResult struct { Transaction string Network x402.Network Payer string + // Response contains HTTP instructions for the failure case (status 402, body, etc). + // Set when Success is false; nil when Success is true. + Response *HTTPResponseInstructions } // ============================================================================ @@ -213,7 +247,9 @@ func (e *RouteConfigurationError) Error() string { // x402HTTPResourceServer provides HTTP-specific payment handling type x402HTTPResourceServer struct { *x402.X402ResourceServer - compiledRoutes []CompiledRoute + compiledRoutes []CompiledRoute + paywallProvider PaywallProvider + protectedRequestHooks []ProtectedRequestHook } // Newx402HTTPResourceServer creates a new HTTP resource server @@ -236,17 +272,35 @@ func Wrappedx402HTTPResourceServer(routes RoutesConfig, resourceServer *x402.X40 // Compile routes for pattern, config := range normalizedRoutes { - verb, regex := parseRoutePattern(pattern) + verb, path, regex := parseRoutePattern(pattern) server.compiledRoutes = append(server.compiledRoutes, CompiledRoute{ - Verb: verb, - Regex: regex, - Config: config, + Verb: verb, + Regex: regex, + Config: config, + Pattern: path, }) } return server } +// RegisterPaywallProvider registers a custom PaywallProvider for generating paywall HTML. +// The provider takes precedence over the built-in EVM/SVM templates but is overridden +// by per-route CustomPaywallHTML. Returns the server for method chaining. +func (s *x402HTTPResourceServer) RegisterPaywallProvider(provider PaywallProvider) *x402HTTPResourceServer { + s.paywallProvider = provider + return s +} + +// OnProtectedRequest registers a hook that runs on every request to a protected route, +// before payment processing. Hooks are executed in registration order; the first hook +// to return a non-nil result determines the outcome. +// Returns the server instance for method chaining. +func (s *x402HTTPResourceServer) OnProtectedRequest(hook ProtectedRequestHook) *x402HTTPResourceServer { + s.protectedRequestHooks = append(s.protectedRequestHooks, hook) + return s +} + // Initialize initializes the server by populating facilitator data and validating route configuration. // It calls the parent server's Initialize to fetch facilitator support, then validates that all // configured routes have matching scheme registrations and facilitator support. @@ -266,6 +320,16 @@ func (s *x402HTTPResourceServer) validateRouteConfiguration() error { var errors []RouteValidationError for _, route := range s.compiledRoutes { + // Warn if wildcard routes are used with discovery extensions + if strings.Contains(route.Pattern, "*") && route.Config.Extensions != nil { + if _, hasBazaar := route.Config.Extensions["bazaar"]; hasBazaar { + log.Printf("[x402] Route %q %s: Wildcard (*) patterns with bazaar discovery extensions "+ + "will auto-generate parameter names (var1, var2, ...). "+ + "Consider using named parameters instead (e.g. /weather/:city) for better discovery metadata.", + route.Verb, route.Pattern) + } + } + for _, option := range route.Config.Accepts { // Check 1: Is the scheme registered for this network? if !s.HasRegisteredScheme(option.Network, option.Scheme) { @@ -353,6 +417,7 @@ func (s *x402HTTPResourceServer) BuildPaymentRequirementsFromOptions(ctx context Price: resolvedPrice, Network: option.Network, MaxTimeoutSeconds: option.MaxTimeoutSeconds, + Extra: option.Extra, } // Use existing BuildPaymentRequirementsFromConfig for each option @@ -369,11 +434,46 @@ func (s *x402HTTPResourceServer) BuildPaymentRequirementsFromOptions(ctx context // ProcessHTTPRequest handles an HTTP request and returns processing result func (s *x402HTTPResourceServer) ProcessHTTPRequest(ctx context.Context, reqCtx HTTPRequestContext, paywallConfig *PaywallConfig) HTTPProcessResult { + if reqCtx.Method == "" { + reqCtx.Method = reqCtx.Adapter.GetMethod() + } + // Find matching route - routeConfig := s.getRouteConfig(reqCtx.Path, reqCtx.Method) + routeConfig, routePattern := s.getRouteConfig(reqCtx.Path, reqCtx.Method) if routeConfig == nil { return HTTPProcessResult{Type: ResultNoPaymentRequired} } + reqCtx.RoutePattern = routePattern + + // Execute protected request hooks before any payment processing + for _, hook := range s.protectedRequestHooks { + result, err := hook(ctx, reqCtx, *routeConfig) + if err != nil { + return HTTPProcessResult{ + Type: ResultPaymentError, + Response: &HTTPResponseInstructions{ + Status: 500, + Headers: map[string]string{"Content-Type": "application/json"}, + Body: map[string]string{"error": fmt.Sprintf("protected request hook error: %v", err)}, + }, + } + } + if result != nil { + if result.GrantAccess { + return HTTPProcessResult{Type: ResultNoPaymentRequired} + } + if result.Abort { + return HTTPProcessResult{ + Type: ResultPaymentError, + Response: &HTTPResponseInstructions{ + Status: 403, + Headers: map[string]string{"Content-Type": "application/json"}, + Body: map[string]string{"error": result.Reason}, + }, + } + } + } + } // Get payment options from route config paymentOptions := routeConfig.Accepts @@ -422,10 +522,9 @@ func (s *x402HTTPResourceServer) ProcessHTTPRequest(ctx context.Context, reqCtx } extensions := routeConfig.Extensions - // TODO: Add EnrichExtensions method if needed - // if extensions != nil && len(extensions) > 0 { - // extensions = s.EnrichExtensions(extensions, reqCtx) - // } + if len(extensions) > 0 { + extensions = s.EnrichExtensions(extensions, reqCtx) + } if typedPayload == nil { paymentRequired := s.CreatePaymentRequiredResponse( @@ -542,34 +641,61 @@ func (s *x402HTTPResourceServer) ProcessHTTPRequest(ctx context.Context, reqCtx // RequiresPayment checks if a request requires payment based on route configuration func (s *x402HTTPResourceServer) RequiresPayment(reqCtx HTTPRequestContext) bool { - routeConfig := s.getRouteConfig(reqCtx.Path, reqCtx.Method) + method := reqCtx.Method + if method == "" { + method = reqCtx.Adapter.GetMethod() + } + routeConfig, _ := s.getRouteConfig(reqCtx.Path, method) return routeConfig != nil } -// ProcessSettlement handles settlement after successful response -func (s *x402HTTPResourceServer) ProcessSettlement(ctx context.Context, payload types.PaymentPayload, requirements types.PaymentRequirements) *ProcessSettleResult { - // Settle payment (type-safe, no marshal needed) - settleResult, err := s.SettlePayment(ctx, payload, requirements) - if err != nil { - return &ProcessSettleResult{ - Success: false, - ErrorReason: err.Error(), +// SettlementOverridesHeader is the HTTP header name for settlement overrides. +// The value is the canonical HTTP header form (Title-Case) so it works correctly +// with both http.Header methods and direct map access. +const SettlementOverridesHeader = "Settlement-Overrides" + +// MarshalSettlementOverrides serializes overrides to the JSON string suitable for +// the SettlementOverridesHeader value. Returns an empty string on marshal failure +// (which cannot happen for a well-formed SettlementOverrides value). +func MarshalSettlementOverrides(overrides *x402.SettlementOverrides) string { + data, _ := json.Marshal(overrides) + return string(data) +} + +// ProcessSettlement handles settlement after successful response. +// If overrides is non-nil, it takes precedence. Otherwise, falls back to reading +// the settlement-overrides header from the transport context's ResponseHeaders +// (set by the route handler via SetSettlementOverrides). The header is deleted +// from ResponseHeaders to prevent it from being sent to the client. +func (s *x402HTTPResourceServer) ProcessSettlement(ctx context.Context, payload types.PaymentPayload, requirements types.PaymentRequirements, overrides *x402.SettlementOverrides, transportContext *HTTPTransportContext) *ProcessSettleResult { + resolved := overrides + if resolved == nil && transportContext != nil && transportContext.ResponseHeaders != nil { + if val := transportContext.ResponseHeaders.Get(SettlementOverridesHeader); val != "" { + var parsed x402.SettlementOverrides + if err := json.Unmarshal([]byte(val), &parsed); err == nil { + resolved = &parsed + } + transportContext.ResponseHeaders.Del(SettlementOverridesHeader) } } + settleResult, err := s.SettlePayment(ctx, payload, requirements, resolved) + if err != nil { + return s.buildSettlementFailureResult(err.Error(), x402.Network(requirements.Network), "", nil) + } + if !settleResult.Success { - return &ProcessSettleResult{ - Success: false, - ErrorReason: settleResult.ErrorReason, - } + return s.buildSettlementFailureResult(settleResult.ErrorReason, settleResult.Network, settleResult.Payer, settleResult) } headers, err := s.createSettlementHeaders(settleResult) if err != nil { - return &ProcessSettleResult{ - Success: false, - ErrorReason: fmt.Sprintf("failed to create settlement headers: %v", err), - } + return s.buildSettlementFailureResult( + fmt.Sprintf("failed to create settlement headers: %v", err), + x402.Network(requirements.Network), + settleResult.Payer, + nil, + ) } return &ProcessSettleResult{ @@ -581,12 +707,53 @@ func (s *x402HTTPResourceServer) ProcessSettlement(ctx context.Context, payload } } +// buildSettlementFailureResult creates a ProcessSettleResult for settlement failure. +// It includes PAYMENT-RESPONSE header and empty body by default. +func (s *x402HTTPResourceServer) buildSettlementFailureResult(errorReason string, network x402.Network, payer string, settleResult *x402.SettleResponse) *ProcessSettleResult { + failureResponse := x402.SettleResponse{ + Success: false, + ErrorReason: errorReason, + Transaction: "", + Network: network, + Payer: payer, + } + if settleResult != nil { + failureResponse.Network = settleResult.Network + failureResponse.Payer = settleResult.Payer + } + + headers, err := s.createSettlementHeaders(&failureResponse) + if err != nil { + // Fallback: return minimal result without PAYMENT-RESPONSE if encoding fails + return &ProcessSettleResult{ + Success: false, + ErrorReason: errorReason, + Response: &HTTPResponseInstructions{ + Status: 402, + Headers: map[string]string{}, + Body: map[string]interface{}{}, + }, + } + } + + return &ProcessSettleResult{ + Success: false, + ErrorReason: errorReason, + Headers: headers, + Response: &HTTPResponseInstructions{ + Status: 402, + Headers: headers, + Body: map[string]interface{}{}, + }, + } +} + // ============================================================================ // Helper Methods // ============================================================================ -// getRouteConfig finds matching route configuration -func (s *x402HTTPResourceServer) getRouteConfig(path, method string) *RouteConfig { +// getRouteConfig finds matching route configuration and returns the route pattern +func (s *x402HTTPResourceServer) getRouteConfig(path, method string) (*RouteConfig, string) { normalizedPath := normalizePath(path) upperMethod := strings.ToUpper(method) @@ -594,11 +761,11 @@ func (s *x402HTTPResourceServer) getRouteConfig(path, method string) *RouteConfi if route.Regex.MatchString(normalizedPath) && (route.Verb == "*" || route.Verb == upperMethod) { config := route.Config // Make a copy - return &config + return &config, route.Pattern } } - return nil + return nil, "" } // extractPaymentV2 extracts V2 payment from headers (V2 only) @@ -741,12 +908,20 @@ func (s *x402HTTPResourceServer) createSettlementHeaders(response *x402.SettleRe }, nil } -// generatePaywallHTMLV2 generates HTML paywall for V2 PaymentRequired +// generatePaywallHTMLV2 generates HTML paywall for V2 PaymentRequired. +// Fallback chain: 1) customHTML, 2) registered PaywallProvider, 3) built-in templates. func (s *x402HTTPResourceServer) generatePaywallHTMLV2(paymentRequired types.PaymentRequired, config *PaywallConfig, customHTML string) string { + // Tier 1: Per-route custom HTML (highest priority) if customHTML != "" { return customHTML } + // Tier 2: Registered PaywallProvider + if s.paywallProvider != nil { + return s.paywallProvider.GenerateHTML(paymentRequired, config) + } + + // Tier 3: Built-in EVM/SVM templates (default fallback) // Convert V2 to generic format to reuse existing HTML generation genericRequired := x402.PaymentRequired{ X402Version: paymentRequired.X402Version, @@ -825,7 +1000,7 @@ func (s *x402HTTPResourceServer) generatePaywallHTML(paymentRequired x402.Paymen // Select template based on network template := s.selectPaywallTemplate(paymentRequired) - return strings.Replace(template, "", configScript+"", 1) + return strings.Replace(template, "", configScript+"\n", 1) } // selectPaywallTemplate chooses the appropriate paywall template based on the network @@ -859,12 +1034,65 @@ func (s *x402HTTPResourceServer) getDisplayAmount(paymentRequired x402.PaymentRe return 0.0 } +// injectPaywallConfig injects a window.x402 configuration script into a paywall HTML template. +// Used by built-in PaywallNetworkHandler implementations to hydrate templates with payment data. +func injectPaywallConfig(template string, paymentRequired types.PaymentRequired, config *PaywallConfig) string { + // Calculate display amount (assuming USDC with 6 decimals) + var displayAmount float64 + if len(paymentRequired.Accepts) > 0 { + amount, err := strconv.ParseFloat(paymentRequired.Accepts[0].Amount, 64) + if err == nil { + displayAmount = amount / 1000000 + } + } + + appName := "" + appLogo := "" + testnet := false + currentURL := "" + + if config != nil { + appName = config.AppName + appLogo = config.AppLogo + testnet = config.Testnet + currentURL = config.CurrentURL + } + + if currentURL == "" && paymentRequired.Resource != nil { + currentURL = paymentRequired.Resource.URL + } + + requirementsJSON, _ := json.Marshal(paymentRequired) + + configScript := fmt.Sprintf(``, + string(requirementsJSON), + html.EscapeString(appName), + html.EscapeString(appLogo), + displayAmount, + testnet, + displayAmount, + html.EscapeString(currentURL), + ) + + return strings.Replace(template, "", configScript+"\n", 1) +} + // ============================================================================ // Utility Functions // ============================================================================ // parseRoutePattern parses a route pattern like "GET /api/*" -func parseRoutePattern(pattern string) (string, *regexp.Regexp) { +func parseRoutePattern(pattern string) (string, string, *regexp.Regexp) { parts := strings.Fields(pattern) var verb, path string @@ -879,13 +1107,14 @@ func parseRoutePattern(pattern string) (string, *regexp.Regexp) { // Convert pattern to regex regexPattern := "^" + regexp.QuoteMeta(path) regexPattern = strings.ReplaceAll(regexPattern, `\*`, `.*?`) - // Handle parameters like [id] + // Handle parameters: [param] (Next.js style) and :param (Express style) regexPattern = paramRegex.ReplaceAllString(regexPattern, `[^/]+`) + regexPattern = colonParamRegex.ReplaceAllString(regexPattern, `[^/]+`) regexPattern += "$" regex := regexp.MustCompile(regexPattern) - return verb, regex + return verb, path, regex } // normalizePath normalizes a URL path for matching diff --git a/go/http/server_extensions_test.go b/go/http/server_extensions_test.go new file mode 100644 index 0000000000..48c1e76545 --- /dev/null +++ b/go/http/server_extensions_test.go @@ -0,0 +1,288 @@ +// package http_test is an external test package. External test packages may import packages +// that import the package under test (go/http), which is the only way to break the otherwise +// circular dependency: go/http → bazaar → go/http. +package http_test + +import ( + "context" + "testing" + + x402 "github.com/coinbase/x402/go" + "github.com/coinbase/x402/go/extensions/bazaar" + "github.com/coinbase/x402/go/extensions/eip2612gassponsor" + "github.com/coinbase/x402/go/extensions/paymentidentifier" + gohttp "github.com/coinbase/x402/go/http" + "github.com/coinbase/x402/go/types" +) + +// extTestHTTPAdapter is a minimal HTTPAdapter for use in this external test package. +type extTestHTTPAdapter struct { + headers map[string]string + method string + path string + url string + accept string + agent string +} + +func (m *extTestHTTPAdapter) GetHeader(name string) string { + if m.headers == nil { + return "" + } + return m.headers[name] +} +func (m *extTestHTTPAdapter) GetMethod() string { return m.method } +func (m *extTestHTTPAdapter) GetPath() string { return m.path } +func (m *extTestHTTPAdapter) GetURL() string { return m.url } +func (m *extTestHTTPAdapter) GetAcceptHeader() string { return m.accept } +func (m *extTestHTTPAdapter) GetUserAgent() string { return m.agent } + +// extTestSchemeServer is a minimal SchemeServer mock. +type extTestSchemeServer struct{ scheme string } + +func (m *extTestSchemeServer) Scheme() string { return m.scheme } +func (m *extTestSchemeServer) ParsePrice(_ x402.Price, _ x402.Network) (x402.AssetAmount, error) { + return x402.AssetAmount{Asset: "USDC", Amount: "1000000"}, nil +} +func (m *extTestSchemeServer) EnhancePaymentRequirements(_ context.Context, base types.PaymentRequirements, _ types.SupportedKind, _ []string) (types.PaymentRequirements, error) { + return base, nil +} + +// extTestFacilitatorClient is a minimal FacilitatorClient mock. +type extTestFacilitatorClient struct { + supported func(context.Context) (x402.SupportedResponse, error) +} + +func (m *extTestFacilitatorClient) Verify(_ context.Context, _, _ []byte) (*x402.VerifyResponse, error) { + return &x402.VerifyResponse{IsValid: true, Payer: "0xmock"}, nil +} +func (m *extTestFacilitatorClient) Settle(_ context.Context, _, _ []byte) (*x402.SettleResponse, error) { + return &x402.SettleResponse{Success: true, Transaction: "0xmock", Network: "eip155:1", Payer: "0xmock"}, nil +} +func (m *extTestFacilitatorClient) GetSupported(ctx context.Context) (x402.SupportedResponse, error) { + if m.supported != nil { + return m.supported(ctx) + } + return x402.SupportedResponse{ + Kinds: []x402.SupportedKind{{X402Version: 2, Scheme: "exact", Network: "eip155:1"}}, + Extensions: []string{}, + Signers: make(map[string][]string), + }, nil +} +func (m *extTestFacilitatorClient) Identifier() string { return "mock" } + +// TestProcessHTTPRequestWithExtensions is a regression test for the EnrichExtensions +// activation. Before this PR, the EnrichExtensions call in ProcessHTTPRequest was disabled +// (commented out). This test verifies that routes with bazaar discovery extensions configured +// still return correct 402 responses after activating the enrichment path. +// +// This test lives in package http_test (external) rather than package http so that it can +// import go/extensions/bazaar without creating an import cycle (bazaar → go/http → bazaar). +func TestProcessHTTPRequestWithExtensions(t *testing.T) { + ctx := context.Background() + + bazaarDecl, err := bazaar.DeclareDiscoveryExtension( + bazaar.MethodGET, + map[string]interface{}{}, + bazaar.JSONSchema{"properties": map[string]interface{}{}}, + "", + nil, + ) + if err != nil { + t.Fatalf("DeclareDiscoveryExtension: %v", err) + } + + routes := gohttp.RoutesConfig{ + "GET /api/data": { + Accepts: gohttp.PaymentOptions{ + { + Scheme: "exact", + PayTo: "0xtest", + Price: "$1.00", + Network: "eip155:1", + }, + }, + Description: "Data endpoint with bazaar extension", + // Extensions are enriched at request time via EnrichExtensions, + // which was previously disabled and is now active in this PR. + Extensions: map[string]interface{}{ + bazaar.BAZAAR.Key(): bazaarDecl, + }, + }, + } + + mockServer := &extTestSchemeServer{scheme: "exact"} + mockClient := &extTestFacilitatorClient{ + supported: func(ctx context.Context) (x402.SupportedResponse, error) { + return x402.SupportedResponse{ + Kinds: []x402.SupportedKind{{X402Version: 2, Scheme: "exact", Network: "eip155:1"}}, + Extensions: []string{}, + Signers: make(map[string][]string), + }, nil + }, + } + + server := gohttp.Newx402HTTPResourceServer( + routes, + x402.WithFacilitatorClient(mockClient), + x402.WithSchemeServer("eip155:1", mockServer), + ) + _ = server.Initialize(ctx) + + adapter := &extTestHTTPAdapter{ + method: "GET", + path: "/api/data", + url: "http://example.com/api/data", + accept: "application/json", + } + + reqCtx := gohttp.HTTPRequestContext{ + Adapter: adapter, + Path: "/api/data", + Method: "GET", + } + + // EnrichExtensions is now active — verify it does not break the 402 response path + result := server.ProcessHTTPRequest(ctx, reqCtx, nil) + + if result.Type != gohttp.ResultPaymentError { + t.Errorf("Expected payment error, got %s", result.Type) + } + if result.Response == nil { + t.Fatal("Expected response instructions") + } + if result.Response.Status != 402 { + t.Errorf("Expected status 402, got %d", result.Response.Status) + } + if result.Response.Headers["PAYMENT-REQUIRED"] == "" { + t.Error("Expected PAYMENT-REQUIRED header to be set after enrichment") + } +} + +// TestProcessHTTPRequestWithPaymentIdentifierExtension is a regression test confirming that +// EnrichExtensions activation does not break routes that declare the paymentidentifier extension. +// paymentidentifier.EnrichDeclaration is a no-op; this test ensures the declaration passes through +// unchanged and the 402 response is still produced correctly. +func TestProcessHTTPRequestWithPaymentIdentifierExtension(t *testing.T) { + ctx := context.Background() + + piDecl := paymentidentifier.DeclarePaymentIdentifierExtension(false) + + routes := gohttp.RoutesConfig{ + "GET /api/data": { + Accepts: gohttp.PaymentOptions{ + { + Scheme: "exact", + PayTo: "0xtest", + Price: "$1.00", + Network: "eip155:1", + }, + }, + Description: "Data endpoint with payment-identifier extension", + Extensions: map[string]interface{}{ + paymentidentifier.PAYMENT_IDENTIFIER: piDecl, + }, + }, + } + + mockServer := &extTestSchemeServer{scheme: "exact"} + mockClient := &extTestFacilitatorClient{} + + server := gohttp.Newx402HTTPResourceServer( + routes, + x402.WithFacilitatorClient(mockClient), + x402.WithSchemeServer("eip155:1", mockServer), + ) + _ = server.Initialize(ctx) + + adapter := &extTestHTTPAdapter{ + method: "GET", + path: "/api/data", + url: "http://example.com/api/data", + accept: "application/json", + } + + reqCtx := gohttp.HTTPRequestContext{ + Adapter: adapter, + Path: "/api/data", + Method: "GET", + } + + result := server.ProcessHTTPRequest(ctx, reqCtx, nil) + + if result.Type != gohttp.ResultPaymentError { + t.Errorf("Expected payment error, got %s", result.Type) + } + if result.Response == nil { + t.Fatal("Expected response instructions") + } + if result.Response.Status != 402 { + t.Errorf("Expected status 402, got %d", result.Response.Status) + } + if result.Response.Headers["PAYMENT-REQUIRED"] == "" { + t.Error("Expected PAYMENT-REQUIRED header after paymentidentifier extension enrichment") + } +} + +// TestProcessHTTPRequestWithEip2612GasSponsorExtension is a regression test confirming that +// EnrichExtensions activation does not break routes that declare the eip2612gassponsor extension. +// The extension declaration passes through EnrichExtensions unchanged (no server-side enricher is +// registered for it), and the 402 response must still be produced correctly. +func TestProcessHTTPRequestWithEip2612GasSponsorExtension(t *testing.T) { + ctx := context.Background() + + gasExt := eip2612gassponsor.DeclareEip2612GasSponsoringExtension() + + routes := gohttp.RoutesConfig{ + "GET /api/data": { + Accepts: gohttp.PaymentOptions{ + { + Scheme: "exact", + PayTo: "0xtest", + Price: "$1.00", + Network: "eip155:1", + }, + }, + Description: "Data endpoint with eip2612gassponsor extension", + Extensions: gasExt, + }, + } + + mockServer := &extTestSchemeServer{scheme: "exact"} + mockClient := &extTestFacilitatorClient{} + + server := gohttp.Newx402HTTPResourceServer( + routes, + x402.WithFacilitatorClient(mockClient), + x402.WithSchemeServer("eip155:1", mockServer), + ) + _ = server.Initialize(ctx) + + adapter := &extTestHTTPAdapter{ + method: "GET", + path: "/api/data", + url: "http://example.com/api/data", + accept: "application/json", + } + + reqCtx := gohttp.HTTPRequestContext{ + Adapter: adapter, + Path: "/api/data", + Method: "GET", + } + + result := server.ProcessHTTPRequest(ctx, reqCtx, nil) + + if result.Type != gohttp.ResultPaymentError { + t.Errorf("Expected payment error, got %s", result.Type) + } + if result.Response == nil { + t.Fatal("Expected response instructions") + } + if result.Response.Status != 402 { + t.Errorf("Expected status 402, got %d", result.Response.Status) + } + if result.Response.Headers["PAYMENT-REQUIRED"] == "" { + t.Error("Expected PAYMENT-REQUIRED header after eip2612gassponsor extension enrichment") + } +} diff --git a/go/http/server_test.go b/go/http/server_test.go index 7b05fd5524..23a26e8015 100644 --- a/go/http/server_test.go +++ b/go/http/server_test.go @@ -5,6 +5,7 @@ import ( "encoding/base64" "encoding/json" "errors" + "net/http" "strings" "testing" @@ -189,6 +190,67 @@ func TestProcessHTTPRequestPaymentRequired(t *testing.T) { } } +func TestBuildPaymentRequirementsFromOptionsPreservesOptionExtra(t *testing.T) { + ctx := context.Background() + + routes := RoutesConfig{ + "GET /api": { + Accepts: PaymentOptions{ + { + Scheme: "exact", + PayTo: "0xtest", + Price: "$1.00", + Network: "eip155:1", + Extra: map[string]interface{}{ + "assetTransferMethod": "permit2", + "merchantNote": "route-level-extra", + }, + }, + }, + }, + } + + mockServer := &mockSchemeServer{scheme: "exact"} + mockClient := &mockFacilitatorClient{ + supported: func(ctx context.Context) (x402.SupportedResponse, error) { + return x402.SupportedResponse{ + Kinds: []x402.SupportedKind{{X402Version: 2, Scheme: "exact", Network: "eip155:1"}}, + Extensions: []string{}, + Signers: make(map[string][]string), + }, nil + }, + } + + server := Newx402HTTPResourceServer( + routes, + x402.WithFacilitatorClient(mockClient), + x402.WithSchemeServer("eip155:1", mockServer), + ) + if err := server.Initialize(ctx); err != nil { + t.Fatalf("Failed to initialize server: %v", err) + } + + reqCtx := HTTPRequestContext{ + Adapter: &mockHTTPAdapter{method: "GET", path: "/api", url: "http://example.com/api"}, + Path: "/api", + Method: "GET", + } + + requirements, err := server.BuildPaymentRequirementsFromOptions(ctx, routes["GET /api"].Accepts, reqCtx) + if err != nil { + t.Fatalf("Failed to build payment requirements: %v", err) + } + if len(requirements) != 1 { + t.Fatalf("Expected 1 requirement, got %d", len(requirements)) + } + if requirements[0].Extra["assetTransferMethod"] != "permit2" { + t.Fatalf("Expected assetTransferMethod passthrough, got %v", requirements[0].Extra["assetTransferMethod"]) + } + if requirements[0].Extra["merchantNote"] != "route-level-extra" { + t.Fatalf("Expected merchant extra passthrough, got %v", requirements[0].Extra["merchantNote"]) + } +} + func TestProcessHTTPRequestWithBrowser(t *testing.T) { ctx := context.Background() @@ -405,7 +467,7 @@ func TestProcessSettlement(t *testing.T) { } // Test settlement processing - result := server.ProcessSettlement(ctx, payload, requirements) + result := server.ProcessSettlement(ctx, payload, requirements, nil, nil) if !result.Success { t.Fatalf("Unexpected failure: %v", result.ErrorReason) } @@ -417,53 +479,319 @@ func TestProcessSettlement(t *testing.T) { } } +func TestProcessSettlement_Failure(t *testing.T) { + ctx := context.Background() + + mockClient := &mockFacilitatorClient{ + settle: func(ctx context.Context, payloadBytes []byte, requirementsBytes []byte) (*x402.SettleResponse, error) { + return &x402.SettleResponse{ + Success: false, + ErrorReason: "insufficient_funds", + Network: "eip155:1", + Payer: "0xpayer", + }, nil + }, + } + + server := Newx402HTTPResourceServer( + RoutesConfig{}, + x402.WithFacilitatorClient(mockClient), + ) + _ = server.Initialize(ctx) + + requirements := types.PaymentRequirements{ + Scheme: "exact", + Network: "eip155:1", + Asset: "USDC", + Amount: "1000000", + PayTo: "0xtest", + } + + payload := types.PaymentPayload{ + X402Version: 2, + Accepted: requirements, + Payload: map[string]interface{}{}, + } + + result := server.ProcessSettlement(ctx, payload, requirements, nil, nil) + if result.Success { + t.Fatal("Expected settlement failure") + } + if result.Headers == nil || result.Headers["PAYMENT-RESPONSE"] == "" { + t.Error("Expected PAYMENT-RESPONSE header on settlement failure") + } + if result.Response == nil { + t.Fatal("Expected Response to be set on settlement failure") + } + if result.Response.Status != 402 { + t.Errorf("Expected status 402, got %d", result.Response.Status) + } + body, ok := result.Response.Body.(map[string]interface{}) + if !ok || len(body) != 0 { + t.Errorf("Expected empty body {}, got %v", result.Response.Body) + } +} + +func TestProcessSettlement_OverridesFromTransportContext(t *testing.T) { + ctx := context.Background() + + var capturedRequirements []byte + mockClient := &mockFacilitatorClient{ + settle: func(ctx context.Context, payloadBytes []byte, requirementsBytes []byte) (*x402.SettleResponse, error) { + capturedRequirements = requirementsBytes + return &x402.SettleResponse{ + Success: true, + Transaction: "0xtx", + Payer: "0xpayer", + Network: "eip155:8453", + }, nil + }, + } + + server := Newx402HTTPResourceServer(RoutesConfig{}, x402.WithFacilitatorClient(mockClient)) + _ = server.Initialize(ctx) + + requirements := types.PaymentRequirements{ + Scheme: "exact", + Network: "eip155:1", + Amount: "1000000", + PayTo: "0xtest", + } + payload := types.PaymentPayload{ + X402Version: 2, + Accepted: requirements, + Payload: map[string]interface{}{}, + } + + t.Run("reads overrides from response headers", func(t *testing.T) { + capturedRequirements = nil + h := http.Header{} + h.Set(SettlementOverridesHeader, `{"amount":"500"}`) + tc := &HTTPTransportContext{ + ResponseHeaders: h, + } + + result := server.ProcessSettlement(ctx, payload, requirements, nil, tc) + if !result.Success { + t.Fatalf("unexpected failure: %v", result.ErrorReason) + } + + var settled types.PaymentRequirements + if err := json.Unmarshal(capturedRequirements, &settled); err != nil { + t.Fatalf("failed to unmarshal captured requirements: %v", err) + } + if settled.Amount != "500" { + t.Errorf("expected overridden amount 500, got %s", settled.Amount) + } + }) + + t.Run("explicit overrides take precedence over header", func(t *testing.T) { + capturedRequirements = nil + h := http.Header{} + h.Set(SettlementOverridesHeader, `{"amount":"500"}`) + tc := &HTTPTransportContext{ + ResponseHeaders: h, + } + explicit := &x402.SettlementOverrides{Amount: "200"} + + result := server.ProcessSettlement(ctx, payload, requirements, explicit, tc) + if !result.Success { + t.Fatalf("unexpected failure: %v", result.ErrorReason) + } + + var settled types.PaymentRequirements + if err := json.Unmarshal(capturedRequirements, &settled); err != nil { + t.Fatalf("failed to unmarshal captured requirements: %v", err) + } + if settled.Amount != "200" { + t.Errorf("expected explicit override amount 200, got %s", settled.Amount) + } + }) + + t.Run("malformed header is ignored", func(t *testing.T) { + capturedRequirements = nil + h := http.Header{} + h.Set(SettlementOverridesHeader, "not-valid-json{{{") + tc := &HTTPTransportContext{ + ResponseHeaders: h, + } + + result := server.ProcessSettlement(ctx, payload, requirements, nil, tc) + if !result.Success { + t.Fatalf("unexpected failure: %v", result.ErrorReason) + } + + var settled types.PaymentRequirements + if err := json.Unmarshal(capturedRequirements, &settled); err != nil { + t.Fatalf("failed to unmarshal captured requirements: %v", err) + } + if settled.Amount != "1000000" { + t.Errorf("expected original amount 1000000, got %s", settled.Amount) + } + }) + + t.Run("header is deleted after extraction", func(t *testing.T) { + h := http.Header{} + h.Set(SettlementOverridesHeader, `{"amount":"500"}`) + h.Set("Content-Type", "application/json") + tc := &HTTPTransportContext{ + ResponseHeaders: h, + } + + server.ProcessSettlement(ctx, payload, requirements, nil, tc) + + if tc.ResponseHeaders.Get(SettlementOverridesHeader) != "" { + t.Error("expected settlement-overrides header to be deleted from transport context") + } + if tc.ResponseHeaders.Get("Content-Type") == "" { + t.Error("expected other headers to remain") + } + }) + + t.Run("nil transport context is safe", func(t *testing.T) { + result := server.ProcessSettlement(ctx, payload, requirements, nil, nil) + if !result.Success { + t.Fatalf("unexpected failure: %v", result.ErrorReason) + } + }) + + t.Run("nil response headers is safe", func(t *testing.T) { + tc := &HTTPTransportContext{ + Request: &HTTPRequestContext{Path: "/test", Method: "GET"}, + ResponseHeaders: nil, + } + result := server.ProcessSettlement(ctx, payload, requirements, nil, tc) + if !result.Success { + t.Fatalf("unexpected failure: %v", result.ErrorReason) + } + }) + + t.Run("percent override via header", func(t *testing.T) { + capturedRequirements = nil + h := http.Header{} + h.Set(SettlementOverridesHeader, `{"amount":"50%"}`) + tc := &HTTPTransportContext{ + ResponseHeaders: h, + } + + result := server.ProcessSettlement(ctx, payload, requirements, nil, tc) + if !result.Success { + t.Fatalf("unexpected failure: %v", result.ErrorReason) + } + + var settled types.PaymentRequirements + if err := json.Unmarshal(capturedRequirements, &settled); err != nil { + t.Fatalf("failed to unmarshal: %v", err) + } + // 50% of 1000000 = 500000 + if settled.Amount != "500000" { + t.Errorf("expected 500000, got %s", settled.Amount) + } + }) + + t.Run("dollar override via header with default decimals", func(t *testing.T) { + capturedRequirements = nil + h := http.Header{} + h.Set(SettlementOverridesHeader, `{"amount":"$0.001"}`) + tc := &HTTPTransportContext{ + ResponseHeaders: h, + } + + result := server.ProcessSettlement(ctx, payload, requirements, nil, tc) + if !result.Success { + t.Fatalf("unexpected failure: %v", result.ErrorReason) + } + + var settled types.PaymentRequirements + if err := json.Unmarshal(capturedRequirements, &settled); err != nil { + t.Fatalf("failed to unmarshal: %v", err) + } + // $0.001 with 6 decimals = 1000 + if settled.Amount != "1000" { + t.Errorf("expected 1000, got %s", settled.Amount) + } + }) + +} + func TestParseRoutePattern(t *testing.T) { tests := []struct { pattern string expectVerb string + expectPath string testPath string shouldMatch bool }{ { pattern: "GET /api", expectVerb: "GET", + expectPath: "/api", testPath: "/api", shouldMatch: true, }, { pattern: "POST /api/*", expectVerb: "POST", + expectPath: "/api/*", testPath: "/api/users", shouldMatch: true, }, { pattern: "/public", expectVerb: "*", + expectPath: "/public", testPath: "/public", shouldMatch: true, }, { pattern: "*", expectVerb: "*", + expectPath: "*", testPath: "/anything", shouldMatch: true, }, { pattern: "GET /api/[id]", expectVerb: "GET", + expectPath: "/api/[id]", testPath: "/api/123", shouldMatch: true, }, + { + pattern: "GET /api/users/:userId", + expectVerb: "GET", + expectPath: "/api/users/:userId", + testPath: "/api/users/456", + shouldMatch: true, + }, + { + pattern: "GET /api/users/:userId/posts/:postId", + expectVerb: "GET", + expectPath: "/api/users/:userId/posts/:postId", + testPath: "/api/users/42/posts/7", + shouldMatch: true, + }, + { + pattern: "/api/:version/items", + expectVerb: "*", + expectPath: "/api/:version/items", + testPath: "/api/v2/items", + shouldMatch: true, + }, } for _, tt := range tests { t.Run(tt.pattern, func(t *testing.T) { - verb, regex := parseRoutePattern(tt.pattern) + verb, path, regex := parseRoutePattern(tt.pattern) if verb != tt.expectVerb { t.Errorf("Expected verb %s, got %s", tt.expectVerb, verb) } + if path != tt.expectPath { + t.Errorf("Expected path %s, got %s", tt.expectPath, path) + } + normalized := normalizePath(tt.testPath) if regex.MatchString(normalized) != tt.shouldMatch { t.Errorf("Expected match=%v for path %s", tt.shouldMatch, tt.testPath) @@ -832,3 +1160,248 @@ func (m *mockFacilitatorClient) GetSupported(ctx context.Context) (x402.Supporte func (m *mockFacilitatorClient) Identifier() string { return "mock" } + +// ============================================================================ +// OnProtectedRequest Hook Tests +// ============================================================================ + +func TestOnProtectedRequest_GrantAccess(t *testing.T) { + routes := RoutesConfig{ + "GET /api": { + Accepts: PaymentOptions{{Scheme: "exact", PayTo: "0xtest", Price: "$1.00", Network: "eip155:1"}}, + }, + } + + server := Newx402HTTPResourceServer(routes). + OnProtectedRequest(func(ctx context.Context, reqCtx HTTPRequestContext, route RouteConfig) (*ProtectedRequestHookResult, error) { + return &ProtectedRequestHookResult{GrantAccess: true}, nil + }) + + reqCtx := HTTPRequestContext{ + Adapter: &mockHTTPAdapter{method: "GET", path: "/api", url: "http://example.com/api"}, + Path: "/api", + Method: "GET", + } + + result := server.ProcessHTTPRequest(context.Background(), reqCtx, nil) + if result.Type != ResultNoPaymentRequired { + t.Errorf("Expected no-payment-required, got %s", result.Type) + } +} + +func TestOnProtectedRequest_Abort(t *testing.T) { + routes := RoutesConfig{ + "GET /api": { + Accepts: PaymentOptions{{Scheme: "exact", PayTo: "0xtest", Price: "$1.00", Network: "eip155:1"}}, + }, + } + + server := Newx402HTTPResourceServer(routes). + OnProtectedRequest(func(ctx context.Context, reqCtx HTTPRequestContext, route RouteConfig) (*ProtectedRequestHookResult, error) { + return &ProtectedRequestHookResult{Abort: true, Reason: "forbidden"}, nil + }) + + reqCtx := HTTPRequestContext{ + Adapter: &mockHTTPAdapter{method: "GET", path: "/api", url: "http://example.com/api"}, + Path: "/api", + Method: "GET", + } + + result := server.ProcessHTTPRequest(context.Background(), reqCtx, nil) + if result.Type != ResultPaymentError { + t.Errorf("Expected payment-error, got %s", result.Type) + } + if result.Response == nil { + t.Fatal("Expected response instructions") + } + if result.Response.Status != 403 { + t.Errorf("Expected status 403, got %d", result.Response.Status) + } + body, ok := result.Response.Body.(map[string]string) + if !ok { + t.Fatal("Expected body to be map[string]string") + } + if body["error"] != "forbidden" { + t.Errorf("Expected error 'forbidden', got '%s'", body["error"]) + } +} + +func TestOnProtectedRequest_Continue(t *testing.T) { + ctx := context.Background() + + routes := RoutesConfig{ + "GET /api": { + Accepts: PaymentOptions{{Scheme: "exact", PayTo: "0xtest", Price: "$1.00", Network: "eip155:1"}}, + }, + } + + mockServer := &mockSchemeServer{scheme: "exact"} + mockClient := &mockFacilitatorClient{ + supported: func(ctx context.Context) (x402.SupportedResponse, error) { + return x402.SupportedResponse{ + Kinds: []x402.SupportedKind{{X402Version: 2, Scheme: "exact", Network: "eip155:1"}}, + Extensions: []string{}, + Signers: make(map[string][]string), + }, nil + }, + } + + // Hook returns nil — should continue to payment flow + server := Newx402HTTPResourceServer(routes, x402.WithFacilitatorClient(mockClient), x402.WithSchemeServer("eip155:1", mockServer)). + OnProtectedRequest(func(ctx context.Context, reqCtx HTTPRequestContext, route RouteConfig) (*ProtectedRequestHookResult, error) { + return nil, nil + }) + _ = server.Initialize(ctx) + + reqCtx := HTTPRequestContext{ + Adapter: &mockHTTPAdapter{method: "GET", path: "/api", url: "http://example.com/api", accept: "application/json"}, + Path: "/api", + Method: "GET", + } + + result := server.ProcessHTTPRequest(ctx, reqCtx, nil) + // Without a payment header, should get 402 + if result.Type != ResultPaymentError { + t.Errorf("Expected payment-error (402), got %s", result.Type) + } + if result.Response != nil && result.Response.Status != 402 { + t.Errorf("Expected status 402, got %d", result.Response.Status) + } +} + +func TestOnProtectedRequest_MultipleHooks_FirstNonNilWins(t *testing.T) { + routes := RoutesConfig{ + "GET /api": { + Accepts: PaymentOptions{{Scheme: "exact", PayTo: "0xtest", Price: "$1.00", Network: "eip155:1"}}, + }, + } + + callOrder := []string{} + + server := Newx402HTTPResourceServer(routes). + OnProtectedRequest(func(ctx context.Context, reqCtx HTTPRequestContext, route RouteConfig) (*ProtectedRequestHookResult, error) { + callOrder = append(callOrder, "hook1") + return nil, nil // no opinion + }). + OnProtectedRequest(func(ctx context.Context, reqCtx HTTPRequestContext, route RouteConfig) (*ProtectedRequestHookResult, error) { + callOrder = append(callOrder, "hook2") + return &ProtectedRequestHookResult{GrantAccess: true}, nil + }). + OnProtectedRequest(func(ctx context.Context, reqCtx HTTPRequestContext, route RouteConfig) (*ProtectedRequestHookResult, error) { + callOrder = append(callOrder, "hook3") + return &ProtectedRequestHookResult{Abort: true, Reason: "should not reach"}, nil + }) + + reqCtx := HTTPRequestContext{ + Adapter: &mockHTTPAdapter{method: "GET", path: "/api", url: "http://example.com/api"}, + Path: "/api", + Method: "GET", + } + + result := server.ProcessHTTPRequest(context.Background(), reqCtx, nil) + if result.Type != ResultNoPaymentRequired { + t.Errorf("Expected no-payment-required, got %s", result.Type) + } + if len(callOrder) != 2 || callOrder[0] != "hook1" || callOrder[1] != "hook2" { + t.Errorf("Expected [hook1, hook2], got %v", callOrder) + } +} + +func TestOnProtectedRequest_HookError(t *testing.T) { + routes := RoutesConfig{ + "GET /api": { + Accepts: PaymentOptions{{Scheme: "exact", PayTo: "0xtest", Price: "$1.00", Network: "eip155:1"}}, + }, + } + + server := Newx402HTTPResourceServer(routes). + OnProtectedRequest(func(ctx context.Context, reqCtx HTTPRequestContext, route RouteConfig) (*ProtectedRequestHookResult, error) { + return nil, errors.New("hook failed") + }) + + reqCtx := HTTPRequestContext{ + Adapter: &mockHTTPAdapter{method: "GET", path: "/api", url: "http://example.com/api"}, + Path: "/api", + Method: "GET", + } + + result := server.ProcessHTTPRequest(context.Background(), reqCtx, nil) + if result.Type != ResultPaymentError { + t.Errorf("Expected payment-error, got %s", result.Type) + } + if result.Response == nil { + t.Fatal("Expected response instructions") + } + if result.Response.Status != 500 { + t.Errorf("Expected status 500, got %d", result.Response.Status) + } +} + +func TestOnProtectedRequest_ContextPassing(t *testing.T) { + routes := RoutesConfig{ + "GET /api/data": { + Accepts: PaymentOptions{{Scheme: "exact", PayTo: "0xtest", Price: "$2.00", Network: "eip155:1"}}, + Description: "Data endpoint", + }, + } + + var capturedReqCtx HTTPRequestContext + var capturedRoute RouteConfig + + server := Newx402HTTPResourceServer(routes). + OnProtectedRequest(func(ctx context.Context, reqCtx HTTPRequestContext, route RouteConfig) (*ProtectedRequestHookResult, error) { + capturedReqCtx = reqCtx + capturedRoute = route + return &ProtectedRequestHookResult{GrantAccess: true}, nil + }) + + reqCtx := HTTPRequestContext{ + Adapter: &mockHTTPAdapter{method: "GET", path: "/api/data", url: "http://example.com/api/data"}, + Path: "/api/data", + Method: "GET", + } + + server.ProcessHTTPRequest(context.Background(), reqCtx, nil) + + if capturedReqCtx.Path != "/api/data" { + t.Errorf("Expected path '/api/data', got '%s'", capturedReqCtx.Path) + } + if capturedReqCtx.Method != "GET" { + t.Errorf("Expected method 'GET', got '%s'", capturedReqCtx.Method) + } + if capturedRoute.Description != "Data endpoint" { + t.Errorf("Expected description 'Data endpoint', got '%s'", capturedRoute.Description) + } + if len(capturedRoute.Accepts) != 1 || capturedRoute.Accepts[0].Price != "$2.00" { + t.Errorf("Expected route accepts with price $2.00, got %+v", capturedRoute.Accepts) + } +} + +func TestOnProtectedRequest_UnmatchedRoute_HookNotCalled(t *testing.T) { + routes := RoutesConfig{ + "GET /api": { + Accepts: PaymentOptions{{Scheme: "exact", PayTo: "0xtest", Price: "$1.00", Network: "eip155:1"}}, + }, + } + + hookCalled := false + server := Newx402HTTPResourceServer(routes). + OnProtectedRequest(func(ctx context.Context, reqCtx HTTPRequestContext, route RouteConfig) (*ProtectedRequestHookResult, error) { + hookCalled = true + return &ProtectedRequestHookResult{Abort: true, Reason: "should not be called"}, nil + }) + + reqCtx := HTTPRequestContext{ + Adapter: &mockHTTPAdapter{method: "GET", path: "/public", url: "http://example.com/public"}, + Path: "/public", + Method: "GET", + } + + result := server.ProcessHTTPRequest(context.Background(), reqCtx, nil) + if result.Type != ResultNoPaymentRequired { + t.Errorf("Expected no-payment-required, got %s", result.Type) + } + if hookCalled { + t.Error("Hook should not be called for unmatched routes") + } +} diff --git a/go/http/svm_paywall_template.go b/go/http/svm_paywall_template.go index e58e9ec8eb..1ca1b8e095 100644 --- a/go/http/svm_paywall_template.go +++ b/go/http/svm_paywall_template.go @@ -2,4 +2,4 @@ package http // SVMPaywallTemplate is the pre-built SVM paywall template with inlined CSS and JS -const SVMPaywallTemplate = "\n \n \n Payment Required\n \n
\n \n \n " +const SVMPaywallTemplate = "\n \n \n Payment Required\n \n
\n \n \n " diff --git a/go/interfaces.go b/go/interfaces.go index bee7bc1bc6..b0668d5a2f 100644 --- a/go/interfaces.go +++ b/go/interfaces.go @@ -164,6 +164,14 @@ type SchemeNetworkServer interface { ) (types.PaymentRequirements, error) } +// AssetDecimalsProvider is an optional interface that SchemeNetworkServer implementations +// can satisfy to report the decimal precision of the asset for a given network. +// SettlePayment uses this to convert dollar-format settlement overrides to atomic units. +// Falls back to 6 decimals when the scheme does not implement this interface. +type AssetDecimalsProvider interface { + GetAssetDecimals(asset string, network Network) int +} + // SchemeNetworkFacilitator is implemented by facilitator-side payment mechanisms (V2) type SchemeNetworkFacilitator interface { Scheme() string diff --git a/go/mcp/server.go b/go/mcp/server.go index 8ee877616a..9cbfdb543e 100644 --- a/go/mcp/server.go +++ b/go/mcp/server.go @@ -138,7 +138,7 @@ func (w *PaymentWrapper) Wrap(handler ToolHandler) ToolHandler { } // Settle payment -- return tool error result, NOT Go error - settleResp, err := w.server.SettlePayment(ctx, payload, w.config.Accepts[0]) + settleResp, err := w.server.SettlePayment(ctx, payload, w.config.Accepts[0], nil) if err != nil { return w.settlementFailedResult( fmt.Sprintf("Settlement error: %v", err)), nil diff --git a/go/mechanisms/evm/DEFAULT_ASSET.md b/go/mechanisms/evm/DEFAULT_ASSET.md index d500045f30..5313a93c1b 100644 --- a/go/mechanisms/evm/DEFAULT_ASSET.md +++ b/go/mechanisms/evm/DEFAULT_ASSET.md @@ -13,7 +13,7 @@ To add support for a new EVM chain, add an entry to the `NetworkConfigs` map in ```go NetworkConfigs = map[string]NetworkConfig{ // ... existing chains ... - + // Your New Chain "eip155:YOUR_CHAIN_ID": { ChainID: big.NewInt(YOUR_CHAIN_ID), @@ -22,6 +22,8 @@ NetworkConfigs = map[string]NetworkConfig{ Name: "Token Name", // Must match EIP-712 domain name Version: "1", // Must match EIP-712 domain version Decimals: 6, // Token decimals (e.g. 6 for USDC) + // AssetTransferMethod: AssetTransferMethodPermit2, // Uncomment if token doesn't support EIP-3009 + // SupportsEip2612: true, // Set if permit2 token implements EIP-2612 permit() }, }, } @@ -36,12 +38,21 @@ NetworkConfigs = map[string]NetworkConfig{ | `Name` | EIP-712 domain name (must match the token's domain separator) | | `Version` | EIP-712 domain version (must match the token's domain separator) | | `Decimals` | Token decimal places (typically 6 for USDC) | +| `AssetTransferMethod` | *(Optional)* Transfer method override: set to `AssetTransferMethodPermit2` for tokens that don't support EIP-3009. Omit for EIP-3009 tokens (default behavior). | +| `SupportsEip2612` | *(Optional)* Set to `true` for Permit2 tokens that implement EIP-2612 `permit()`. When true, clients can use gasless EIP-2612 permits for Permit2 approval. When false/absent on a Permit2 token, clients fall back to ERC-20 approval gas sponsoring. Ignored for EIP-3009 tokens. | + +## Asset Transfer Methods + +x402 supports two methods for transferring assets: + +| Method | Use Case | Recommendation | +|--------|----------|----------------| +| **EIP-3009** | Tokens with native `transferWithAuthorization` (e.g., USDC) | **Recommended** (Simplest, truly gasless) | +| **Permit2** | Any ERC-20 token | **Universal Fallback** (Requires one-time approval) | -## Current Limitation +### Default Behavior -> ⚠️ **EIP-3009 Required**: Currently, only stablecoins implementing [EIP-3009](https://eips.ethereum.org/EIPS/eip-3009) (`transferWithAuthorization`) are supported. -> -> Generic ERC-20 support via EIP-2612/Permit2 is planned but not yet implemented. +If no `AssetTransferMethod` is specified, the system defaults to **EIP-3009**. This maintains backward compatibility with existing deployments. ## Asset Selection Policy @@ -52,7 +63,6 @@ The default asset is chosen **per chain** based on the following guidelines: 2. **No official stance**: If the chain team has not taken a public position on a preferred stablecoin, we encourage the team behind that chain to make the selection and submit a PR. 3. **Community PRs welcome**: Chain teams and community members may submit PRs to add their chain's default asset, provided: - - The stablecoin implements EIP-3009 - The selection aligns with the chain's ecosystem preferences - The EIP-712 domain parameters are correctly specified @@ -60,7 +70,29 @@ The default asset is chosen **per chain** based on the following guidelines: To add a new chain's default asset: -1. Verify the stablecoin implements EIP-3009 -2. Obtain the correct EIP-712 domain `name` and `version` from the token contract -3. Add the entry to `NetworkConfigs` in `constants.go` -4. Submit a PR with the chain name and rationale for the asset selection +1. Obtain the correct EIP-712 domain `name` and `version` from the token contract +2. Check whether the token supports EIP-3009 (`transferWithAuthorization`): + - If yes: omit `AssetTransferMethod` (EIP-3009 is the default) + - If no: set `AssetTransferMethod: AssetTransferMethodPermit2` +3. For Permit2 tokens, check whether the token supports EIP-2612 (`permit()`): + - If yes: set `SupportsEip2612: true` so clients can use gasless EIP-2612 permits for Permit2 approval + - If no: omit `SupportsEip2612` — clients will fall back to ERC-20 approval gas sponsoring +4. Add the entry to `NetworkConfigs` in `constants.go` +5. Submit a PR with the chain name and rationale for the asset selection + +## Cross-SDK Checklist + +When adding a new chain's default asset, update all three SDKs to maintain parity: + +| SDK | File to edit | What to add | +|-----|-------------|-------------| +| **Go** | `go/mechanisms/evm/constants.go` | Entry in `NetworkConfigs` map | +| **TypeScript** | `typescript/packages/mechanisms/evm/src/exact/server/scheme.ts` | Entry in `stablecoins` map inside `getDefaultAsset()` | +| **Python** | `python/x402/mechanisms/evm/constants.py` | Entry in `NETWORK_CONFIGS` dict | + +All three must use: +- The same CAIP-2 network key (e.g., `eip155:YOUR_CHAIN_ID`) +- The same token contract address +- The same EIP-712 domain `name` and `version` +- The same `decimals` value +- The same asset transfer method (EIP-3009 default, or Permit2 if specified) diff --git a/go/mechanisms/evm/README.md b/go/mechanisms/evm/README.md index ea1ba8daa6..1b0f180e3f 100644 --- a/go/mechanisms/evm/README.md +++ b/go/mechanisms/evm/README.md @@ -8,7 +8,7 @@ This package provides scheme implementations for EVM-based blockchains (Ethereum ## Exact Payment Scheme -The **exact** scheme implementation enables fixed-amount payments using EIP-3009 `transferWithAuthorization` for USDC and compatible tokens. +The **exact** scheme implementation enables fixed-amount payments using EIP-3009 `transferWithAuthorization` or Permit2 for USDC and compatible tokens. ### Export Paths @@ -22,8 +22,10 @@ github.com/coinbase/x402/go/mechanisms/evm/exact/client ``` **Exports:** -- `NewExactEvmScheme(signer)` - Creates client-side EVM exact payment mechanism +- `NewExactEvmScheme(signer, config)` - Creates client-side EVM exact payment mechanism - Used for creating payment payloads that clients sign +- Pass `nil` config for signer-only mode +- Use config to provide explicit RPC URLs for extension enrichment (`RPCURL` or `RPCByChainID`) #### For Servers @@ -63,6 +65,8 @@ Networks with default assets configured: - **Base Mainnet**: `eip155:8453` (USDC) - **Base Sepolia**: `eip155:84532` (USDC) +- **MegaETH Mainnet**: `eip155:4326` (MegaUSD) +- **Monad Mainnet**: `eip155:143` (USDC) To add default asset support for additional chains, see [DEFAULT_ASSET.md](./DEFAULT_ASSET.md). @@ -70,8 +74,8 @@ To add default asset support for additional chains, see [DEFAULT_ASSET.md](./DEF The **exact** scheme implements fixed-amount payments: -- **Standard**: EIP-3009 `transferWithAuthorization` -- **Token**: USDC and EIP-3009 compatible tokens +- **Standard**: EIP-3009 `transferWithAuthorization` or Permit2 (per-asset configuration) +- **Token**: USDC and other stablecoins (EIP-3009 or any ERC-20 via Permit2) - **Gas**: Paid by facilitator - **Confirmation**: On-chain settlement with transaction hash diff --git a/go/mechanisms/evm/constants.go b/go/mechanisms/evm/constants.go index cb75136cf8..d616f9449f 100644 --- a/go/mechanisms/evm/constants.go +++ b/go/mechanisms/evm/constants.go @@ -5,8 +5,9 @@ import ( ) const ( - // Scheme identifier + // Scheme identifiers SchemeExact = "exact" + SchemeUpto = "upto" // Default token decimals for USDC DefaultDecimals = 6 @@ -15,9 +16,11 @@ const ( FunctionTransferWithAuthorization = "transferWithAuthorization" FunctionReceiveWithAuthorization = "receiveWithAuthorization" FunctionAuthorizationState = "authorizationState" + FunctionTryAggregate = "tryAggregate" // Permit2 function names - FunctionSettle = "settle" + FunctionSettle = "settle" + FunctionSettleWithPermit = "settleWithPermit" // Transaction status TxStatusSuccess = 1 @@ -41,13 +44,17 @@ const ( // Same address on all EVM chains via CREATE2 deployment. PERMIT2Address = "0x000000000022D473030F116dDEE9F6B43aC78BA3" + // MULTICALL3Address is the canonical Multicall3 deployment address. + // Same address on all EVM chains via CREATE2 deployment. + MULTICALL3Address = "0xcA11bde05977b3631167028862bE2a173976CA11" + // X402ExactPermit2ProxyAddress is the x402 exact payment proxy. // Vanity address: 0x4020...0001 for easy recognition. X402ExactPermit2ProxyAddress = "0x402085c248EeA27D92E8b30b2C58ed07f9E20001" // X402UptoPermit2ProxyAddress is the x402 upto payment proxy. // Vanity address: 0x4020...0002 for easy recognition. - X402UptoPermit2ProxyAddress = "0x402039b3d6E6BEC5A02c2C9fd937ac17A6940002" + X402UptoPermit2ProxyAddress = "0x4020A4f3b7b90ccA423B9fabCc0CE57C6C240002" // Permit2DeadlineBuffer is the time buffer (in seconds) added when checking // deadline expiration to account for block propagation time. @@ -55,14 +62,23 @@ const ( // ERC20ApproveGasLimit is the gas limit for a standard ERC-20 approve() transaction. ERC20ApproveGasLimit = 70000 + + // DefaultMaxFeePerGas is the fallback max fee per gas (1 gwei) for gas cost estimation. + DefaultMaxFeePerGas = 1_000_000_000 ) var ( // Network chain IDs - ChainIDBase = big.NewInt(8453) - ChainIDBaseSepolia = big.NewInt(84532) - ChainIDMegaETH = big.NewInt(4326) - ChainIDMonad = big.NewInt(143) + ChainIDBase = big.NewInt(8453) + ChainIDBaseSepolia = big.NewInt(84532) + ChainIDMegaETH = big.NewInt(4326) + ChainIDMonad = big.NewInt(143) + ChainIDMezoTestnet = big.NewInt(31611) + ChainIDStable = big.NewInt(988) + ChainIDStableTestnet = big.NewInt(2201) + ChainIDPolygon = big.NewInt(137) + ChainIDArbOne = big.NewInt(42161) + ChainIDArbSepolia = big.NewInt(421614) // Network configurations // See DEFAULT_ASSET.md for guidelines on adding new chains @@ -72,8 +88,9 @@ var ( // - If the chain has officially endorsed a stablecoin, that asset should be used // - If no official stance exists, the chain team should make the selection // - // NOTE: Currently only EIP-3009 supporting stablecoins can be used. - // Generic ERC-20 support via EIP-2612/Permit2 is planned but not yet implemented. + // Both EIP-3009 (transferWithAuthorization) and Permit2 asset transfer methods are supported. + // EIP-3009 is the default. Set AssetTransferMethod to AssetTransferMethodPermit2 for tokens + // that don't support EIP-3009. See DEFAULT_ASSET.md for details. NetworkConfigs = map[string]NetworkConfig{ // Base Mainnet "eip155:8453": { @@ -95,14 +112,16 @@ var ( Decimals: DefaultDecimals, }, }, - // MegaETH Mainnet + // MegaETH Mainnet (uses Permit2 instead of EIP-3009, supports EIP-2612) "eip155:4326": { ChainID: ChainIDMegaETH, DefaultAsset: AssetInfo{ - Address: "0xFAfDdbb3FC7688494971a79cc65DCa3EF82079E7", // USDM (MegaUSD) - Name: "MegaUSD", - Version: "1", - Decimals: 18, + Address: "0xFAfDdbb3FC7688494971a79cc65DCa3EF82079E7", // USDM (MegaUSD) + Name: "MegaUSD", + Version: "1", + Decimals: 18, + AssetTransferMethod: AssetTransferMethodPermit2, + SupportsEip2612: true, }, }, // Monad Mainnet @@ -115,6 +134,68 @@ var ( Decimals: DefaultDecimals, }, }, + // Mezo Testnet (uses Permit2 instead of EIP-3009, supports EIP-2612) + "eip155:31611": { + ChainID: ChainIDMezoTestnet, + DefaultAsset: AssetInfo{ + Address: "0x118917a40FAF1CD7a13dB0Ef56C86De7973Ac503", // mUSD on Mezo Testnet + Name: "Mezo USD", + Version: "1", + Decimals: 18, + AssetTransferMethod: AssetTransferMethodPermit2, + SupportsEip2612: true, + }, + }, + // Stable Mainnet + "eip155:988": { + ChainID: ChainIDStable, + DefaultAsset: AssetInfo{ + Address: "0x779Ded0c9e1022225f8E0630b35a9b54bE713736", // USDT0 on Stable + Name: "USDT0", + Version: "1", + Decimals: DefaultDecimals, + }, + }, + // Stable Testnet + "eip155:2201": { + ChainID: ChainIDStableTestnet, + DefaultAsset: AssetInfo{ + Address: "0x78Cf24370174180738C5B8E352B6D14c83a6c9A9", // USDT0 on Stable Testnet + Name: "USDT0", + Version: "1", + Decimals: DefaultDecimals, + }, + }, + // Polygon Mainnet + "eip155:137": { + ChainID: ChainIDPolygon, + DefaultAsset: AssetInfo{ + Address: "0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359", // USDC on Polygon + Name: "USD Coin", + Version: "2", + Decimals: DefaultDecimals, + }, + }, + // Arbitrum One + "eip155:42161": { + ChainID: ChainIDArbOne, + DefaultAsset: AssetInfo{ + Address: "0xaf88d065e77c8cC2239327C5EDb3A432268e5831", // USDC on ArbOne + Name: "USD Coin", + Version: "2", + Decimals: DefaultDecimals, + }, + }, + // Arbitrum Sepolia + "eip155:421614": { + ChainID: ChainIDArbSepolia, + DefaultAsset: AssetInfo{ + Address: "0x75faf114eafb1BDbe2F0316DF893fd58CE46AA4d", // USDC on ArbSepolia + Name: "USD Coin", + Version: "2", + Decimals: DefaultDecimals, + }, + }, } // EIP-3009 ABI for transferWithAuthorization with v,r,s (EOA signatures) @@ -174,6 +255,36 @@ var ( } ]`) + // Multicall3TryAggregateABI batches arbitrary eth_call requests. + Multicall3TryAggregateABI = []byte(`[ + { + "inputs": [ + {"name": "requireSuccess", "type": "bool"}, + { + "name": "calls", + "type": "tuple[]", + "components": [ + {"name": "target", "type": "address"}, + {"name": "callData", "type": "bytes"} + ] + } + ], + "name": "tryAggregate", + "outputs": [ + { + "name": "returnData", + "type": "tuple[]", + "components": [ + {"name": "success", "type": "bool"}, + {"name": "returnData", "type": "bytes"} + ] + } + ], + "stateMutability": "payable", + "type": "function" + } + ]`) + // ERC20AllowanceABI for checking Permit2 approval ERC20AllowanceABI = []byte(`[ { @@ -215,6 +326,28 @@ var ( } ]`) + // ERC20NameABI for checking EIP-712 domain name diagnostics. + ERC20NameABI = []byte(`[ + { + "inputs": [], + "name": "name", + "outputs": [{"name": "", "type": "string"}], + "stateMutability": "view", + "type": "function" + } + ]`) + + // ERC20VersionABI for checking EIP-712 domain version diagnostics. + ERC20VersionABI = []byte(`[ + { + "inputs": [], + "name": "version", + "outputs": [{"name": "", "type": "string"}], + "stateMutability": "view", + "type": "function" + } + ]`) + // X402ExactPermit2ProxySettleABI for calling settle on x402ExactPermit2Proxy X402ExactPermit2ProxySettleABI = []byte(`[ { @@ -253,6 +386,133 @@ var ( } ]`) + // X402ExactPermit2ProxyPermit2ABI for verifying proxy deployment + X402ExactPermit2ProxyPermit2ABI = []byte(`[ + { + "inputs": [], + "name": "PERMIT2", + "outputs": [{"name": "", "type": "address"}], + "stateMutability": "view", + "type": "function" + } + ]`) + + // Multicall3GetEthBalanceABI for querying native ETH balance via Multicall3. + Multicall3GetEthBalanceABI = []byte(`[ + { + "inputs": [ + {"name": "addr", "type": "address"} + ], + "name": "getEthBalance", + "outputs": [{"name": "balance", "type": "uint256"}], + "stateMutability": "view", + "type": "function" + } + ]`) + + // X402UptoPermit2ProxySettleABI for calling settle on x402UptoPermit2Proxy. + // Differs from exact: takes an additional `amount` param and witness includes `facilitator`. + X402UptoPermit2ProxySettleABI = []byte(`[ + { + "type": "function", + "name": "settle", + "inputs": [ + { + "name": "permit", + "type": "tuple", + "components": [ + { + "name": "permitted", + "type": "tuple", + "components": [ + {"name": "token", "type": "address"}, + {"name": "amount", "type": "uint256"} + ] + }, + {"name": "nonce", "type": "uint256"}, + {"name": "deadline", "type": "uint256"} + ] + }, + {"name": "amount", "type": "uint256"}, + {"name": "owner", "type": "address"}, + { + "name": "witness", + "type": "tuple", + "components": [ + {"name": "to", "type": "address"}, + {"name": "facilitator", "type": "address"}, + {"name": "validAfter", "type": "uint256"} + ] + }, + {"name": "signature", "type": "bytes"} + ], + "outputs": [], + "stateMutability": "nonpayable" + } + ]`) + + // X402UptoPermit2ProxySettleWithPermitABI for calling settleWithPermit on x402UptoPermit2Proxy (EIP-2612 extension). + X402UptoPermit2ProxySettleWithPermitABI = []byte(`[ + { + "type": "function", + "name": "settleWithPermit", + "inputs": [ + { + "name": "permit2612", + "type": "tuple", + "components": [ + {"name": "value", "type": "uint256"}, + {"name": "deadline", "type": "uint256"}, + {"name": "r", "type": "bytes32"}, + {"name": "s", "type": "bytes32"}, + {"name": "v", "type": "uint8"} + ] + }, + { + "name": "permit", + "type": "tuple", + "components": [ + { + "name": "permitted", + "type": "tuple", + "components": [ + {"name": "token", "type": "address"}, + {"name": "amount", "type": "uint256"} + ] + }, + {"name": "nonce", "type": "uint256"}, + {"name": "deadline", "type": "uint256"} + ] + }, + {"name": "amount", "type": "uint256"}, + {"name": "owner", "type": "address"}, + { + "name": "witness", + "type": "tuple", + "components": [ + {"name": "to", "type": "address"}, + {"name": "facilitator", "type": "address"}, + {"name": "validAfter", "type": "uint256"} + ] + }, + {"name": "signature", "type": "bytes"} + ], + "outputs": [], + "stateMutability": "nonpayable" + } + ]`) + + // X402UptoPermit2ProxyPermit2ABI for verifying upto proxy deployment + X402UptoPermit2ProxyPermit2ABI = []byte(`[ + { + "inputs": [], + "name": "PERMIT2", + "outputs": [{"name": "", "type": "address"}], + "stateMutability": "view", + "type": "function" + } + ]`) + // EIP2612NoncesABI for querying EIP-2612 nonces EIP2612NoncesABI = []byte(`[ { @@ -315,9 +575,6 @@ var ( } ]`) - // FunctionSettleWithPermit is the function name for EIP-2612 settlement - FunctionSettleWithPermit = "settleWithPermit" - // EIP712DomainTypes defines the standard EIP-712 domain type for Permit2. // Permit2 uses name + chainId + verifyingContract (no version field). EIP712DomainTypes = []TypedDataField{ @@ -387,4 +644,36 @@ func GetEIP2612EIP712Types() map[string][]TypedDataField { } } -// Note: MaxUint256() is defined in utils.go +// UptoPermit2WitnessTypes defines the EIP-712 types for the upto Permit2 witness. +// The upto witness includes a `facilitator` field absent from the exact witness. +// Only the address matching witness.facilitator can call settle() on-chain. +// Field order MUST match the on-chain x402UptoPermit2Proxy contract and TypeScript implementation. +var UptoPermit2WitnessTypes = map[string][]TypedDataField{ + "PermitWitnessTransferFrom": { + {Name: "permitted", Type: "TokenPermissions"}, + {Name: "spender", Type: "address"}, + {Name: "nonce", Type: "uint256"}, + {Name: "deadline", Type: "uint256"}, + {Name: "witness", Type: "Witness"}, + }, + "TokenPermissions": { + {Name: "token", Type: "address"}, + {Name: "amount", Type: "uint256"}, + }, + "Witness": { + {Name: "to", Type: "address"}, + {Name: "facilitator", Type: "address"}, + {Name: "validAfter", Type: "uint256"}, + }, +} + +// GetUptoPermit2EIP712Types returns the complete EIP-712 types map for upto Permit2 signing. +// This combines the EIP712Domain with the upto-specific Permit2 types (including facilitator in witness). +func GetUptoPermit2EIP712Types() map[string][]TypedDataField { + return map[string][]TypedDataField{ + "EIP712Domain": EIP712DomainTypes, + "PermitWitnessTransferFrom": UptoPermit2WitnessTypes["PermitWitnessTransferFrom"], + "TokenPermissions": UptoPermit2WitnessTypes["TokenPermissions"], + "Witness": UptoPermit2WitnessTypes["Witness"], + } +} diff --git a/go/mechanisms/evm/eip2612.go b/go/mechanisms/evm/eip2612.go new file mode 100644 index 0000000000..23e5e7e238 --- /dev/null +++ b/go/mechanisms/evm/eip2612.go @@ -0,0 +1,58 @@ +package evm + +import ( + "errors" + "math/big" + "strings" + "time" + + "github.com/coinbase/x402/go/extensions/eip2612gassponsor" +) + +// ValidateEip2612PermitForPayment validates EIP-2612 extension data for a Permit2 payment. +// Returns an empty string if valid, or an error-reason string on failure. +func ValidateEip2612PermitForPayment(info *eip2612gassponsor.Info, payer string, tokenAddress string) string { + if !eip2612gassponsor.ValidateEip2612GasSponsoringInfo(info) { + return "invalid_eip2612_extension_format" + } + + if !strings.EqualFold(info.From, payer) { + return "eip2612_from_mismatch" + } + + if !strings.EqualFold(info.Asset, tokenAddress) { + return "eip2612_asset_mismatch" + } + + if !strings.EqualFold(info.Spender, PERMIT2Address) { + return "eip2612_spender_not_permit2" + } + + // Use a 6-second buffer consistent with Permit2's on-chain deadline check. + now := time.Now().Unix() + deadline, ok := new(big.Int).SetString(info.Deadline, 10) + if !ok || deadline.Int64() < now+Permit2DeadlineBuffer { + return "eip2612_deadline_expired" + } + + return "" +} + +// SplitEip2612Signature splits a 65-byte hex-encoded signature into v, r, s components. +func SplitEip2612Signature(signature string) (uint8, [32]byte, [32]byte, error) { + sigBytes, err := HexToBytes(signature) + if err != nil { + return 0, [32]byte{}, [32]byte{}, err + } + + if len(sigBytes) != 65 { + return 0, [32]byte{}, [32]byte{}, errors.New("signature must be 65 bytes") + } + + var r, s [32]byte + copy(r[:], sigBytes[0:32]) + copy(s[:], sigBytes[32:64]) + v := sigBytes[64] + + return v, r, s, nil +} diff --git a/go/mechanisms/evm/eip712.go b/go/mechanisms/evm/eip712.go index d46b48edfb..7843c80373 100644 --- a/go/mechanisms/evm/eip712.go +++ b/go/mechanisms/evm/eip712.go @@ -173,6 +173,66 @@ func HashEIP3009Authorization( return HashTypedData(domain, types, "TransferWithAuthorization", message) } +// BuildUptoPermit2WitnessMap returns the witness map for upto EIP-712 message construction. +// Includes the facilitator field absent from the exact witness. +func BuildUptoPermit2WitnessMap(to string, facilitator string, validAfter *big.Int) map[string]interface{} { + return map[string]interface{}{ + "to": to, + "facilitator": facilitator, + "validAfter": validAfter, + } +} + +// HashUptoPermit2Authorization hashes a PermitWitnessTransferFrom message for the upto Permit2 scheme. +// Uses upto-specific witness types that include the facilitator address. +func HashUptoPermit2Authorization( + authorization UptoPermit2Authorization, + chainID *big.Int, +) ([]byte, error) { + domain := TypedDataDomain{ + Name: "Permit2", + ChainID: chainID, + VerifyingContract: PERMIT2Address, + } + + types := GetUptoPermit2EIP712Types() + + amount, ok := new(big.Int).SetString(authorization.Permitted.Amount, 10) + if !ok { + return nil, fmt.Errorf("invalid permitted amount: %s", authorization.Permitted.Amount) + } + nonce, ok := new(big.Int).SetString(authorization.Nonce, 10) + if !ok { + return nil, fmt.Errorf("invalid nonce: %s", authorization.Nonce) + } + deadline, ok := new(big.Int).SetString(authorization.Deadline, 10) + if !ok { + return nil, fmt.Errorf("invalid deadline: %s", authorization.Deadline) + } + validAfter, ok := new(big.Int).SetString(authorization.Witness.ValidAfter, 10) + if !ok { + return nil, fmt.Errorf("invalid validAfter: %s", authorization.Witness.ValidAfter) + } + + token := common.HexToAddress(authorization.Permitted.Token).Hex() + spender := common.HexToAddress(authorization.Spender).Hex() + to := common.HexToAddress(authorization.Witness.To).Hex() + facilitator := common.HexToAddress(authorization.Witness.Facilitator).Hex() + + message := map[string]interface{}{ + "permitted": map[string]interface{}{ + "token": token, + "amount": amount, + }, + "spender": spender, + "nonce": nonce, + "deadline": deadline, + "witness": BuildUptoPermit2WitnessMap(to, facilitator, validAfter), + } + + return HashTypedData(domain, types, "PermitWitnessTransferFrom", message) +} + // BuildPermit2WitnessMap returns the witness map used in EIP-712 message construction. // Centralizing this ensures eip712.go and exact/client/permit2.go stay in sync when // the witness struct changes. diff --git a/go/mechanisms/evm/exact/client/eip2612.go b/go/mechanisms/evm/exact/client/eip2612.go index 8e77e088c9..9e1c00ff39 100644 --- a/go/mechanisms/evm/exact/client/eip2612.go +++ b/go/mechanisms/evm/exact/client/eip2612.go @@ -19,7 +19,7 @@ import ( // proxy contract enforces permit2612.value == permittedAmount. func SignEip2612Permit( ctx context.Context, - signer evm.ClientEvmSigner, + signer evm.ClientEvmSignerWithReadContract, tokenAddress string, tokenName string, tokenVersion string, diff --git a/go/mechanisms/evm/exact/client/erc20_approval.go b/go/mechanisms/evm/exact/client/erc20_approval.go index 57600a4020..99f626871e 100644 --- a/go/mechanisms/evm/exact/client/erc20_approval.go +++ b/go/mechanisms/evm/exact/client/erc20_approval.go @@ -47,8 +47,12 @@ func SignErc20ApprovalTransaction( return nil, fmt.Errorf("failed to get transaction count: %w", err) } - // Get fee estimates - maxFeePerGas, maxPriorityFeePerGas, _ := signer.EstimateFeesPerGas(ctx) + // EstimateFeesPerGas returns usable fallback values even on RPC error, + // but guard against nil to avoid panic in DynamicFeeTx construction. + maxFeePerGas, maxPriorityFeePerGas, feeErr := signer.EstimateFeesPerGas(ctx) + if feeErr != nil && (maxFeePerGas == nil || maxPriorityFeePerGas == nil) { + return nil, fmt.Errorf("failed to estimate fees and no fallback available: %w", feeErr) + } // Build EIP-1559 transaction toAddr := common.HexToAddress(normalizedToken) diff --git a/go/mechanisms/evm/exact/client/rpc.go b/go/mechanisms/evm/exact/client/rpc.go new file mode 100644 index 0000000000..5963beb174 --- /dev/null +++ b/go/mechanisms/evm/exact/client/rpc.go @@ -0,0 +1,32 @@ +package client + +import ( + "context" + + "github.com/coinbase/x402/go/mechanisms/evm" +) + +// ExactEvmChainConfig configures RPC behavior for one chain. +type ExactEvmChainConfig = evm.RPCChainConfig + +// ExactEvmSchemeConfig configures RPC behavior for Exact EVM clients. +// If both RPCByChainID and RPCURL are set, chain-specific entries take precedence. +type ExactEvmSchemeConfig = evm.RPCConfig + +func (c *ExactEvmScheme) resolveRPCURL(network string) string { + return evm.ResolveRPCURL(c.config, network) +} + +func (c *ExactEvmScheme) resolveReadSigner( + ctx context.Context, + network string, +) (evm.ClientEvmSignerWithReadContract, error) { + return evm.ResolveReadSigner(ctx, c.signer, c.resolveRPCURL(network)) +} + +func (c *ExactEvmScheme) resolveTxSigner( + ctx context.Context, + network string, +) (evm.ClientEvmSignerWithTxSigning, error) { + return evm.ResolveTxSigner(ctx, c.signer, c.resolveRPCURL(network)) +} diff --git a/go/mechanisms/evm/exact/client/rpc_test.go b/go/mechanisms/evm/exact/client/rpc_test.go new file mode 100644 index 0000000000..414b5a89e5 --- /dev/null +++ b/go/mechanisms/evm/exact/client/rpc_test.go @@ -0,0 +1,132 @@ +package client + +import ( + "context" + "math/big" + "testing" + + goethtypes "github.com/ethereum/go-ethereum/core/types" + + "github.com/coinbase/x402/go/mechanisms/evm" +) + +type rpcTestSigner struct { + address string + signCalls int + nonceCalls int + estimateCalls int +} + +func (s *rpcTestSigner) Address() string { + if s.address == "" { + return "0x1234567890123456789012345678901234567890" + } + return s.address +} + +func (s *rpcTestSigner) SignTypedData( + ctx context.Context, + domain evm.TypedDataDomain, + types map[string][]evm.TypedDataField, + primaryType string, + message map[string]interface{}, +) ([]byte, error) { + return []byte{1, 2, 3}, nil +} + +func (s *rpcTestSigner) SignTransaction(ctx context.Context, tx *goethtypes.Transaction) ([]byte, error) { + s.signCalls++ + return []byte{0x01}, nil +} + +func (s *rpcTestSigner) GetTransactionCount(ctx context.Context, address string) (uint64, error) { + s.nonceCalls++ + return 7, nil +} + +func (s *rpcTestSigner) EstimateFeesPerGas(ctx context.Context) (*big.Int, *big.Int, error) { + s.estimateCalls++ + return big.NewInt(100), big.NewInt(10), nil +} + +func TestResolveRPCURL(t *testing.T) { + scheme := &ExactEvmScheme{ + config: &ExactEvmSchemeConfig{ + RPCURL: "https://default.example", + RPCByChainID: map[int64]ExactEvmChainConfig{ + 8453: {RPCURL: "https://base.example"}, + }, + }, + } + + if got := scheme.resolveRPCURL("eip155:8453"); got != "https://base.example" { + t.Fatalf("expected chain-specific rpc, got %q", got) + } + if got := scheme.resolveRPCURL("eip155:137"); got != "https://default.example" { + t.Fatalf("expected default rpc, got %q", got) + } +} + +func TestResolveTxSignerUsesSignerCapabilitiesFirst(t *testing.T) { + ctx := context.Background() + signer := &rpcTestSigner{} + scheme := &ExactEvmScheme{ + signer: signer, + } + + txSigner, err := scheme.resolveTxSigner(ctx, "eip155:8453") + if err != nil { + t.Fatalf("resolveTxSigner failed: %v", err) + } + if txSigner == nil { + t.Fatal("expected tx signer") + } + + _, err = txSigner.SignTransaction(ctx, nil) + if err != nil { + t.Fatalf("SignTransaction failed: %v", err) + } + _, err = txSigner.GetTransactionCount(ctx, signer.Address()) + if err != nil { + t.Fatalf("GetTransactionCount failed: %v", err) + } + _, _, err = txSigner.EstimateFeesPerGas(ctx) + if err != nil { + t.Fatalf("EstimateFeesPerGas failed: %v", err) + } + + if signer.signCalls != 1 || signer.nonceCalls != 1 || signer.estimateCalls != 1 { + t.Fatalf("expected signer methods to be used, got sign=%d nonce=%d fee=%d", signer.signCalls, signer.nonceCalls, signer.estimateCalls) + } +} + +func TestResolveTxSignerReturnsNilWithoutRequiredCapabilities(t *testing.T) { + ctx := context.Background() + scheme := &ExactEvmScheme{ + signer: &mockMinimalClientSigner{}, + } + + txSigner, err := scheme.resolveTxSigner(ctx, "eip155:8453") + if err != nil { + t.Fatalf("resolveTxSigner failed: %v", err) + } + if txSigner != nil { + t.Fatal("expected nil tx signer when signTransaction capability is missing") + } +} + +type mockMinimalClientSigner struct{} + +func (m *mockMinimalClientSigner) Address() string { + return "0x1234567890123456789012345678901234567890" +} + +func (m *mockMinimalClientSigner) SignTypedData( + ctx context.Context, + domain evm.TypedDataDomain, + types map[string][]evm.TypedDataField, + primaryType string, + message map[string]interface{}, +) ([]byte, error) { + return []byte{0x01}, nil +} diff --git a/go/mechanisms/evm/exact/client/scheme.go b/go/mechanisms/evm/exact/client/scheme.go index daddea3107..2a3aef2f1f 100644 --- a/go/mechanisms/evm/exact/client/scheme.go +++ b/go/mechanisms/evm/exact/client/scheme.go @@ -17,14 +17,16 @@ import ( // ExactEvmScheme implements the SchemeNetworkClient interface for EVM exact payments (V2) type ExactEvmScheme struct { signer evm.ClientEvmSigner + config *ExactEvmSchemeConfig } // NewExactEvmScheme creates a new ExactEvmScheme. -// The signer must implement ReadContract for EIP-2612 gas sponsoring support. -// Use NewClientSignerFromPrivateKeyWithClient to create a signer with RPC connectivity. -func NewExactEvmScheme(signer evm.ClientEvmSigner) *ExactEvmScheme { +// Base flows only require a signer that can sign typed data. +// Extension enrichment paths use optional runtime capabilities. +func NewExactEvmScheme(signer evm.ClientEvmSigner, config *ExactEvmSchemeConfig) *ExactEvmScheme { return &ExactEvmScheme{ signer: signer, + config: config, } } @@ -40,20 +42,15 @@ func (c *ExactEvmScheme) CreatePaymentPayload( ctx context.Context, requirements types.PaymentRequirements, ) (types.PaymentPayload, error) { - // Check asset transfer method assetTransferMethod := evm.AssetTransferMethodEIP3009 // default if requirements.Extra != nil { if method, ok := requirements.Extra["assetTransferMethod"].(string); ok { assetTransferMethod = evm.AssetTransferMethod(method) } } - - // Route based on method if assetTransferMethod == evm.AssetTransferMethodPermit2 { return CreatePermit2Payload(ctx, c.signer, requirements) } - - // Default to EIP-3009 return c.createEIP3009Payload(ctx, requirements) } @@ -66,26 +63,23 @@ func (c *ExactEvmScheme) CreatePaymentPayloadWithExtensions( requirements types.PaymentRequirements, extensions map[string]interface{}, ) (types.PaymentPayload, error) { - // Check asset transfer method assetTransferMethod := evm.AssetTransferMethodEIP3009 if requirements.Extra != nil { if method, ok := requirements.Extra["assetTransferMethod"].(string); ok { assetTransferMethod = evm.AssetTransferMethod(method) } } - if assetTransferMethod == evm.AssetTransferMethodPermit2 { result, err := CreatePermit2Payload(ctx, c.signer, requirements) if err != nil { return types.PaymentPayload{}, err } - // Try EIP-2612 permit first (preferred for compatible tokens) extData, err := c.trySignEip2612Permit(ctx, requirements, result, extensions) - if err == nil && extData != nil { + if extData != nil { result.Extensions = extData - } else { - // Fallback: ERC-20 approval (for tokens without EIP-2612) + } else if err == nil { + // EIP-2612 not applicable — try ERC-20 approval fallback erc20ExtData, erc20Err := c.trySignErc20Approval(ctx, requirements, extensions) if erc20Err == nil && erc20ExtData != nil { result.Extensions = erc20ExtData @@ -95,7 +89,6 @@ func (c *ExactEvmScheme) CreatePaymentPayloadWithExtensions( return result, nil } - // Default to EIP-3009 return c.createEIP3009Payload(ctx, requirements) } @@ -106,7 +99,6 @@ func (c *ExactEvmScheme) trySignEip2612Permit( result types.PaymentPayload, extensions map[string]interface{}, ) (map[string]interface{}, error) { - // Check if server advertises eip2612GasSponsoring if extensions == nil { return nil, nil } @@ -114,7 +106,6 @@ func (c *ExactEvmScheme) trySignEip2612Permit( return nil, nil } - // Check that required token metadata is available tokenName, _ := requirements.Extra["name"].(string) tokenVersion, _ := requirements.Extra["version"].(string) if tokenName == "" || tokenVersion == "" { @@ -128,8 +119,16 @@ func (c *ExactEvmScheme) trySignEip2612Permit( tokenAddress := evm.NormalizeAddress(requirements.Asset) + readSigner, err := c.resolveReadSigner(ctx, requirements.Network) + if err != nil { + return nil, err + } + if readSigner == nil { + return nil, nil + } + // Check if user already has sufficient Permit2 allowance - allowanceResult, err := c.signer.ReadContract( + allowanceResult, err := readSigner.ReadContract( ctx, tokenAddress, evm.ERC20AllowanceABI, @@ -161,7 +160,7 @@ func (c *ExactEvmScheme) trySignEip2612Permit( // Sign the EIP-2612 permit with the exact Permit2 permitted amount // (the contract enforces permit2612.value == permit.permitted.amount) - info, err := SignEip2612Permit(ctx, c.signer, tokenAddress, tokenName, tokenVersion, chainID, deadline, requirements.Amount) + info, err := SignEip2612Permit(ctx, readSigner, tokenAddress, tokenName, tokenVersion, chainID, deadline, requirements.Amount) if err != nil { return nil, err } @@ -181,7 +180,6 @@ func (c *ExactEvmScheme) trySignErc20Approval( requirements types.PaymentRequirements, extensions map[string]interface{}, ) (map[string]interface{}, error) { - // Check if server advertises erc20ApprovalGasSponsoring if extensions == nil { return nil, nil } @@ -189,9 +187,11 @@ func (c *ExactEvmScheme) trySignErc20Approval( return nil, nil } - // Signer must support transaction signing - txSigner, ok := c.signer.(evm.ClientEvmSignerWithTxSigning) - if !ok { + txSigner, err := c.resolveTxSigner(ctx, requirements.Network) + if err != nil { + return nil, err + } + if txSigner == nil { return nil, nil } @@ -202,20 +202,22 @@ func (c *ExactEvmScheme) trySignErc20Approval( tokenAddress := evm.NormalizeAddress(requirements.Asset) - // Check if user already has sufficient Permit2 allowance - allowanceResult, err := c.signer.ReadContract( - ctx, - tokenAddress, - evm.ERC20AllowanceABI, - "allowance", - common.HexToAddress(c.signer.Address()), - common.HexToAddress(evm.PERMIT2Address), - ) - if err == nil { - if allowanceBig, ok := allowanceResult.(*big.Int); ok { - requiredAmount, ok := new(big.Int).SetString(requirements.Amount, 10) - if ok && allowanceBig.Cmp(requiredAmount) >= 0 { - return nil, nil // Already approved + // If read capability exists, skip signing when Permit2 allowance is already sufficient. + if readSigner, hasRead := c.signer.(evm.ClientEvmSignerWithReadContract); hasRead { + allowanceResult, err := readSigner.ReadContract( + ctx, + tokenAddress, + evm.ERC20AllowanceABI, + "allowance", + common.HexToAddress(c.signer.Address()), + common.HexToAddress(evm.PERMIT2Address), + ) + if err == nil { + if allowanceBig, ok := allowanceResult.(*big.Int); ok { + requiredAmount, ok := new(big.Int).SetString(requirements.Amount, 10) + if ok && allowanceBig.Cmp(requiredAmount) >= 0 { + return nil, nil // Already approved + } } } } diff --git a/go/mechanisms/evm/exact/facilitator/eip3009.go b/go/mechanisms/evm/exact/facilitator/eip3009.go new file mode 100644 index 0000000000..dd749e6f02 --- /dev/null +++ b/go/mechanisms/evm/exact/facilitator/eip3009.go @@ -0,0 +1,214 @@ +package facilitator + +import ( + "context" + "errors" + "fmt" + "math/big" + "strings" + "time" + + x402 "github.com/coinbase/x402/go" + "github.com/coinbase/x402/go/mechanisms/evm" + "github.com/coinbase/x402/go/types" +) + +// verifyEIP3009 verifies an EIP-3009 payment payload. +func (f *ExactEvmScheme) verifyEIP3009( + ctx context.Context, + payload types.PaymentPayload, + requirements types.PaymentRequirements, + simulate bool, +) (*x402.VerifyResponse, error) { + if payload.Accepted.Scheme != evm.SchemeExact { + return nil, x402.NewVerifyError(ErrInvalidScheme, "", fmt.Sprintf("invalid scheme: %s", payload.Accepted.Scheme)) + } + + if payload.Accepted.Network != requirements.Network { + return nil, x402.NewVerifyError(ErrNetworkMismatch, "", fmt.Sprintf("network mismatch: %s != %s", payload.Accepted.Network, requirements.Network)) + } + + evmPayload, err := evm.PayloadFromMap(payload.Payload) + if err != nil { + return nil, x402.NewVerifyError(ErrInvalidPayload, "", fmt.Sprintf("failed to parse EVM payload: %s", err.Error())) + } + + if evmPayload.Signature == "" { + return nil, x402.NewVerifyError(ErrMissingSignature, "", "missing signature") + } + + chainID, err := evm.GetEvmChainId(string(requirements.Network)) + if err != nil { + return nil, x402.NewVerifyError(ErrFailedToGetNetworkConfig, "", err.Error()) + } + + tokenAddress := evm.NormalizeAddress(requirements.Asset) + + if !strings.EqualFold(evmPayload.Authorization.To, requirements.PayTo) { + return nil, x402.NewVerifyError(ErrRecipientMismatch, "", fmt.Sprintf("recipient mismatch: %s != %s", evmPayload.Authorization.To, requirements.PayTo)) + } + + parsedAuthorization, err := ParseEIP3009Authorization(evmPayload.Authorization) + if err != nil { + return nil, x402.NewVerifyError(ErrInvalidPayload, evmPayload.Authorization.From, err.Error()) + } + + requiredValue, ok := new(big.Int).SetString(requirements.Amount, 10) + if !ok { + return nil, x402.NewVerifyError(ErrInvalidRequiredAmount, "", fmt.Sprintf("invalid required amount: %s", requirements.Amount)) + } + + if parsedAuthorization.Value.Cmp(requiredValue) != 0 { + return nil, x402.NewVerifyError(ErrAuthorizationValueMismatch, evmPayload.Authorization.From, fmt.Sprintf("authorization value mismatch: %s != %s", parsedAuthorization.Value.String(), requiredValue.String())) + } + + now := time.Now().Unix() + if parsedAuthorization.ValidBefore.Cmp(big.NewInt(now+6)) < 0 { + return nil, x402.NewVerifyError(ErrValidBeforeExpired, evmPayload.Authorization.From, fmt.Sprintf("valid before expired: %s", parsedAuthorization.ValidBefore.String())) + } + + if parsedAuthorization.ValidAfter.Cmp(big.NewInt(now)) > 0 { + return nil, x402.NewVerifyError(ErrValidAfterInFuture, evmPayload.Authorization.From, fmt.Sprintf("valid after in future: %s", parsedAuthorization.ValidAfter.String())) + } + + tokenName, _ := requirements.Extra["name"].(string) + tokenVersion, _ := requirements.Extra["version"].(string) + if tokenName == "" || tokenVersion == "" { + return nil, x402.NewVerifyError(ErrMissingEip712Domain, evmPayload.Authorization.From, "missing EIP-712 domain name/version in requirements.extra") + } + + signatureBytes, err := evm.HexToBytes(evmPayload.Signature) + if err != nil { + return nil, x402.NewVerifyError(ErrInvalidSignatureFormat, evmPayload.Authorization.From, err.Error()) + } + + classification, err := ClassifyEIP3009Signature( + ctx, + f.signer, + evmPayload.Authorization, + signatureBytes, + chainID, + tokenAddress, + tokenName, + tokenVersion, + ) + if err != nil { + return nil, x402.NewVerifyError(ErrFailedToVerifySignature, evmPayload.Authorization.From, err.Error()) + } + + if !classification.Valid && classification.IsUndeployed && !HasEIP6492Deployment(classification.SigData) { + return nil, x402.NewVerifyError(ErrUndeployedSmartWallet, evmPayload.Authorization.From, "") + } + + if !classification.Valid && !classification.IsSmartWallet { + return nil, x402.NewVerifyError(ErrInvalidSignature, evmPayload.Authorization.From, fmt.Sprintf("invalid signature: %s", evmPayload.Signature)) + } + + if simulate { + simulationSucceeded, err := SimulateEIP3009Transfer( + ctx, + f.signer, + tokenAddress, + parsedAuthorization, + classification.SigData, + ) + if err != nil { + return nil, x402.NewVerifyError(ErrInvalidPayload, evmPayload.Authorization.From, err.Error()) + } + if !simulationSucceeded { + reason := DiagnoseEIP3009SimulationFailure( + ctx, + f.signer, + tokenAddress, + evmPayload.Authorization, + requiredValue, + tokenName, + tokenVersion, + ) + return nil, x402.NewVerifyError(reason, evmPayload.Authorization.From, "") + } + } + + return &x402.VerifyResponse{ + IsValid: true, + Payer: evmPayload.Authorization.From, + }, nil +} + +// settleEIP3009 settles an EIP-3009 payment on-chain. +func (f *ExactEvmScheme) settleEIP3009( + ctx context.Context, + payload types.PaymentPayload, + requirements types.PaymentRequirements, +) (*x402.SettleResponse, error) { + network := x402.Network(payload.Accepted.Network) + + verifyResp, err := f.verifyEIP3009(ctx, payload, requirements, f.config.SimulateInSettle) + if err != nil { + ve := &x402.VerifyError{} + if errors.As(err, &ve) { + return nil, x402.NewSettleError(ve.InvalidReason, ve.Payer, network, "", ve.InvalidMessage) + } + return nil, x402.NewSettleError(ErrVerificationFailed, "", network, "", err.Error()) + } + + evmPayload, err := evm.PayloadFromMap(payload.Payload) + if err != nil { + return nil, x402.NewSettleError(ErrInvalidPayload, verifyResp.Payer, network, "", err.Error()) + } + + tokenAddress := evm.NormalizeAddress(requirements.Asset) + + signatureBytes, err := evm.HexToBytes(evmPayload.Signature) + if err != nil { + return nil, x402.NewSettleError(ErrInvalidSignatureFormat, verifyResp.Payer, network, "", err.Error()) + } + + sigData, err := evm.ParseERC6492Signature(signatureBytes) + if err != nil { + return nil, x402.NewSettleError(ErrFailedToParseSignature, verifyResp.Payer, network, "", err.Error()) + } + + if HasEIP6492Deployment(sigData) { + code, err := f.signer.GetCode(ctx, evmPayload.Authorization.From) + if err != nil { + return nil, x402.NewSettleError(ErrFailedToCheckDeployment, verifyResp.Payer, network, "", err.Error()) + } + + if len(code) == 0 { + if !f.config.DeployERC4337WithEIP6492 { + return nil, x402.NewSettleError(ErrUndeployedSmartWallet, verifyResp.Payer, network, "", "") + } + + if err := DeploySmartWallet(ctx, f.signer, sigData); err != nil { + return nil, x402.NewSettleError(ErrSmartWalletDeploymentFailed, verifyResp.Payer, network, "", err.Error()) + } + } + } + + parsedAuthorization, err := ParseEIP3009Authorization(evmPayload.Authorization) + if err != nil { + return nil, x402.NewSettleError(ErrInvalidPayload, verifyResp.Payer, network, "", err.Error()) + } + + txHash, err := ExecuteTransferWithAuthorization(ctx, f.signer, tokenAddress, parsedAuthorization, sigData) + if err != nil { + return nil, x402.NewSettleError(ErrFailedToExecuteTransfer, verifyResp.Payer, network, "", err.Error()) + } + + receipt, err := f.signer.WaitForTransactionReceipt(ctx, txHash) + if err != nil { + return nil, x402.NewSettleError(ErrFailedToGetReceipt, verifyResp.Payer, network, txHash, err.Error()) + } + + if receipt.Status != evm.TxStatusSuccess { + return nil, x402.NewSettleError(ErrTransactionFailed, verifyResp.Payer, network, txHash, "") + } + + return &x402.SettleResponse{ + Success: true, + Transaction: txHash, + Network: network, + Payer: verifyResp.Payer, + }, nil +} diff --git a/go/mechanisms/evm/exact/facilitator/eip3009_helpers.go b/go/mechanisms/evm/exact/facilitator/eip3009_helpers.go new file mode 100644 index 0000000000..5f072035ad --- /dev/null +++ b/go/mechanisms/evm/exact/facilitator/eip3009_helpers.go @@ -0,0 +1,448 @@ +package facilitator + +import ( + "context" + "fmt" + "math/big" + "strings" + + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/common" + + "github.com/coinbase/x402/go/mechanisms/evm" +) + +// ParsedEIP3009Authorization contains the parsed transfer arguments used by verify and settle. +type ParsedEIP3009Authorization struct { + From common.Address + To common.Address + Value *big.Int + ValidAfter *big.Int + ValidBefore *big.Int + Nonce [32]byte +} + +// EIP3009SignatureClassification captures how the signature should be treated. +type EIP3009SignatureClassification struct { + Valid bool + IsSmartWallet bool + IsUndeployed bool + SigData *evm.ERC6492SignatureData +} + +// ParseEIP3009Authorization parses authorization fields into contract-call arguments. +func ParseEIP3009Authorization( + authorization evm.ExactEIP3009Authorization, +) (*ParsedEIP3009Authorization, error) { + value, ok := new(big.Int).SetString(authorization.Value, 10) + if !ok { + return nil, fmt.Errorf("invalid authorization value: %s", authorization.Value) + } + + validAfter, ok := new(big.Int).SetString(authorization.ValidAfter, 10) + if !ok { + return nil, fmt.Errorf("invalid validAfter: %s", authorization.ValidAfter) + } + + validBefore, ok := new(big.Int).SetString(authorization.ValidBefore, 10) + if !ok { + return nil, fmt.Errorf("invalid validBefore: %s", authorization.ValidBefore) + } + + nonceBytes, err := evm.HexToBytes(authorization.Nonce) + if err != nil { + return nil, fmt.Errorf("invalid nonce: %w", err) + } + if len(nonceBytes) != 32 { + return nil, fmt.Errorf("invalid nonce length: got %d bytes, want 32", len(nonceBytes)) + } + + var nonce [32]byte + copy(nonce[:], nonceBytes) + + return &ParsedEIP3009Authorization{ + From: common.HexToAddress(authorization.From), + To: common.HexToAddress(authorization.To), + Value: value, + ValidAfter: validAfter, + ValidBefore: validBefore, + Nonce: nonce, + }, nil +} + +// ClassifyEIP3009Signature checks the signature directly when possible, while preserving +// smart-wallet signatures for simulation-first verification. +func ClassifyEIP3009Signature( + ctx context.Context, + signer evm.FacilitatorEvmSigner, + authorization evm.ExactEIP3009Authorization, + signature []byte, + chainID *big.Int, + tokenAddress string, + tokenName string, + tokenVersion string, +) (*EIP3009SignatureClassification, error) { + hash, err := evm.HashEIP3009Authorization( + authorization, + chainID, + tokenAddress, + tokenName, + tokenVersion, + ) + if err != nil { + return nil, err + } + + var hash32 [32]byte + copy(hash32[:], hash) + + valid, sigData, err := evm.VerifyUniversalSignature( + ctx, + signer, + authorization.From, + hash32, + signature, + true, + ) + if err != nil { + return nil, err + } + if sigData == nil { + sigData = &evm.ERC6492SignatureData{InnerSignature: signature} + } + + classification := &EIP3009SignatureClassification{ + Valid: valid, + SigData: sigData, + } + + if HasEIP6492Deployment(sigData) || len(sigData.InnerSignature) != 65 { + classification.IsSmartWallet = true + } + if valid { + return classification, nil + } + + code, err := signer.GetCode(ctx, authorization.From) + if err != nil { + return nil, err + } + if len(code) > 0 { + classification.IsSmartWallet = true + return classification, nil + } + + if HasEIP6492Deployment(sigData) { + classification.IsSmartWallet = true + classification.IsUndeployed = true + return classification, nil + } + + if len(sigData.InnerSignature) != 65 { + classification.IsSmartWallet = true + classification.IsUndeployed = true + } + + return classification, nil +} + +// SimulateEIP3009Transfer runs the transfer via eth_call. +func SimulateEIP3009Transfer( + ctx context.Context, + signer evm.FacilitatorEvmSigner, + tokenAddress string, + parsed *ParsedEIP3009Authorization, + sigData *evm.ERC6492SignatureData, +) (bool, error) { + if sigData == nil { + return false, fmt.Errorf("missing signature data") + } + + if HasEIP6492Deployment(sigData) { + transferCalldata, err := buildTransferWithAuthorizationBytesCalldata(parsed, sigData.InnerSignature) + if err != nil { + return false, err + } + + results, err := evm.Multicall(ctx, signer, []evm.MulticallCall{ + { + Address: common.BytesToAddress(sigData.Factory[:]).Hex(), + CallData: sigData.FactoryCalldata, + }, + { + Address: tokenAddress, + CallData: transferCalldata, + }, + }) + if err != nil { + return false, err + } + if len(results) < 2 { + return false, nil + } + + return results[1].Success(), nil + } + + if len(sigData.InnerSignature) == 65 { + v, r, s := splitSignatureParts(sigData.InnerSignature) + _, err := signer.ReadContract( + ctx, + tokenAddress, + evm.TransferWithAuthorizationVRSABI, + evm.FunctionTransferWithAuthorization, + parsed.From, + parsed.To, + parsed.Value, + parsed.ValidAfter, + parsed.ValidBefore, + parsed.Nonce, + v, + r, + s, + ) + if err != nil { + return false, err + } + + return true, nil + } + + _, err := signer.ReadContract( + ctx, + tokenAddress, + evm.TransferWithAuthorizationBytesABI, + evm.FunctionTransferWithAuthorization, + parsed.From, + parsed.To, + parsed.Value, + parsed.ValidAfter, + parsed.ValidBefore, + parsed.Nonce, + sigData.InnerSignature, + ) + if err != nil { + return false, err + } + + return true, nil +} + +// DiagnoseEIP3009SimulationFailure resolves a failed simulation into the most specific error. +func DiagnoseEIP3009SimulationFailure( + ctx context.Context, + signer evm.FacilitatorEvmSigner, + tokenAddress string, + authorization evm.ExactEIP3009Authorization, + requiredAmount *big.Int, + tokenName string, + tokenVersion string, +) string { + results, err := evm.Multicall(ctx, signer, []evm.MulticallCall{ + { + Address: tokenAddress, + ABI: evm.ERC20BalanceOfABI, + FunctionName: "balanceOf", + Args: []interface{}{common.HexToAddress(authorization.From)}, + }, + { + Address: tokenAddress, + ABI: evm.ERC20NameABI, + FunctionName: "name", + }, + { + Address: tokenAddress, + ABI: evm.ERC20VersionABI, + FunctionName: "version", + }, + { + Address: tokenAddress, + ABI: evm.AuthorizationStateABI, + FunctionName: evm.FunctionAuthorizationState, + Args: []interface{}{common.HexToAddress(authorization.From), mustNonce(authorization.Nonce)}, + }, + }) + if err != nil || len(results) < 4 { + return ErrEip3009SimulationFailed + } + + authStateResult := results[3] + if !authStateResult.Success() { + return ErrEip3009NotSupported + } + + if nonceUsed, ok := authStateResult.Result.(bool); ok && nonceUsed { + return ErrNonceAlreadyUsed + } + + nameResult := results[1] + if tokenName != "" && nameResult.Success() { + if actualName, ok := nameResult.Result.(string); ok && actualName != tokenName { + return ErrEip3009TokenNameMismatch + } + } + + versionResult := results[2] + if tokenVersion != "" && versionResult.Success() { + if actualVersion, ok := versionResult.Result.(string); ok && actualVersion != tokenVersion { + return ErrEip3009TokenVersionMismatch + } + } + + balanceResult := results[0] + if balanceResult.Success() { + if balance := asBigInt(balanceResult.Result); balance != nil && balance.Cmp(requiredAmount) < 0 { + return ErrInsufficientBalance + } + } + + return ErrEip3009SimulationFailed +} + +// ExecuteTransferWithAuthorization executes the actual transfer onchain. +func ExecuteTransferWithAuthorization( + ctx context.Context, + signer evm.FacilitatorEvmSigner, + tokenAddress string, + parsed *ParsedEIP3009Authorization, + sigData *evm.ERC6492SignatureData, +) (string, error) { + if sigData == nil { + return "", fmt.Errorf("missing signature data") + } + + if len(sigData.InnerSignature) == 65 { + v, r, s := splitSignatureParts(sigData.InnerSignature) + return signer.WriteContract( + ctx, + tokenAddress, + evm.TransferWithAuthorizationVRSABI, + evm.FunctionTransferWithAuthorization, + parsed.From, + parsed.To, + parsed.Value, + parsed.ValidAfter, + parsed.ValidBefore, + parsed.Nonce, + v, + r, + s, + ) + } + + return signer.WriteContract( + ctx, + tokenAddress, + evm.TransferWithAuthorizationBytesABI, + evm.FunctionTransferWithAuthorization, + parsed.From, + parsed.To, + parsed.Value, + parsed.ValidAfter, + parsed.ValidBefore, + parsed.Nonce, + sigData.InnerSignature, + ) +} + +// DeploySmartWallet sends the ERC-6492 factory deployment transaction when enabled. +func DeploySmartWallet( + ctx context.Context, + signer evm.FacilitatorEvmSigner, + sigData *evm.ERC6492SignatureData, +) error { + if !HasEIP6492Deployment(sigData) { + return nil + } + + txHash, err := signer.SendTransaction( + ctx, + common.BytesToAddress(sigData.Factory[:]).Hex(), + sigData.FactoryCalldata, + ) + if err != nil { + return fmt.Errorf("factory deployment transaction failed: %w", err) + } + + receipt, err := signer.WaitForTransactionReceipt(ctx, txHash) + if err != nil { + return fmt.Errorf("failed to wait for deployment: %w", err) + } + if receipt.Status != evm.TxStatusSuccess { + return fmt.Errorf("deployment transaction reverted") + } + + return nil +} + +func buildTransferWithAuthorizationBytesCalldata( + parsed *ParsedEIP3009Authorization, + signature []byte, +) ([]byte, error) { + return packCallData( + evm.TransferWithAuthorizationBytesABI, + evm.FunctionTransferWithAuthorization, + parsed.From, + parsed.To, + parsed.Value, + parsed.ValidAfter, + parsed.ValidBefore, + parsed.Nonce, + signature, + ) +} + +func packCallData(abiBytes []byte, functionName string, args ...interface{}) ([]byte, error) { + contractABI, err := abi.JSON(strings.NewReader(string(abiBytes))) + if err != nil { + return nil, err + } + + data, err := contractABI.Pack(functionName, args...) + if err != nil { + return nil, err + } + + return data, nil +} + +func splitSignatureParts(signature []byte) (uint8, [32]byte, [32]byte) { + var r [32]byte + var s [32]byte + copy(r[:], signature[0:32]) + copy(s[:], signature[32:64]) + + v := signature[64] + if v == 0 || v == 1 { + v += 27 + } + + return v, r, s +} + +func HasEIP6492Deployment(sigData *evm.ERC6492SignatureData) bool { + if sigData == nil { + return false + } + + var zeroFactory [20]byte + return sigData.Factory != zeroFactory && len(sigData.FactoryCalldata) > 0 +} + +func mustNonce(nonce string) [32]byte { + nonceBytes, _ := evm.HexToBytes(nonce) + var nonceArray [32]byte + copy(nonceArray[:], nonceBytes) + return nonceArray +} + +func asBigInt(value interface{}) *big.Int { + switch v := value.(type) { + case *big.Int: + return v + case big.Int: + return &v + default: + return nil + } +} diff --git a/go/mechanisms/evm/exact/facilitator/errors.go b/go/mechanisms/evm/exact/facilitator/errors.go index 46dd1bb4bc..4295e3d92f 100644 --- a/go/mechanisms/evm/exact/facilitator/errors.go +++ b/go/mechanisms/evm/exact/facilitator/errors.go @@ -1,27 +1,33 @@ package facilitator +import "github.com/coinbase/x402/go/mechanisms/evm" + // Facilitator error constants for the exact EVM scheme const ( // EIP-3009 Verify errors - ErrInvalidScheme = "invalid_exact_evm_scheme" - ErrNetworkMismatch = "invalid_exact_evm_network_mismatch" - ErrInvalidPayload = "invalid_exact_evm_payload" - ErrMissingSignature = "invalid_exact_evm_payload_missing_signature" - ErrFailedToGetNetworkConfig = "invalid_exact_evm_failed_to_get_network_config" - ErrMissingEip712Domain = "invalid_exact_evm_missing_eip712_domain" - ErrRecipientMismatch = "invalid_exact_evm_recipient_mismatch" - ErrInvalidAuthorizationValue = "invalid_exact_evm_authorization_value" - ErrInvalidRequiredAmount = "invalid_exact_evm_required_amount" - ErrInsufficientAmount = "invalid_exact_evm_insufficient_amount" - ErrFailedToCheckNonce = "invalid_exact_evm_failed_to_check_nonce" - ErrNonceAlreadyUsed = "invalid_exact_evm_nonce_already_used" - ErrFailedToGetBalance = "invalid_exact_evm_failed_to_get_balance" - ErrInsufficientBalance = "invalid_exact_evm_insufficient_balance" - ErrInvalidSignatureFormat = "invalid_exact_evm_signature_format" - ErrFailedToVerifySignature = "invalid_exact_evm_failed_to_verify_signature" - ErrInvalidSignature = "invalid_exact_evm_signature" - ErrValidBeforeExpired = "invalid_exact_evm_payload_authorization_valid_before" - ErrValidAfterInFuture = "invalid_exact_evm_payload_authorization_valid_after" + ErrInvalidScheme = "invalid_exact_evm_scheme" + ErrNetworkMismatch = "invalid_exact_evm_network_mismatch" + ErrInvalidPayload = "invalid_exact_evm_payload" + ErrMissingSignature = "invalid_exact_evm_payload_missing_signature" + ErrFailedToGetNetworkConfig = "invalid_exact_evm_failed_to_get_network_config" + ErrMissingEip712Domain = "invalid_exact_evm_missing_eip712_domain" + ErrRecipientMismatch = "invalid_exact_evm_recipient_mismatch" + ErrInvalidAuthorizationValue = "invalid_exact_evm_authorization_value" + ErrInvalidRequiredAmount = "invalid_exact_evm_required_amount" + ErrAuthorizationValueMismatch = "invalid_exact_evm_payload_authorization_value_mismatch" + ErrFailedToCheckNonce = "invalid_exact_evm_failed_to_check_nonce" + ErrNonceAlreadyUsed = "invalid_exact_evm_nonce_already_used" + ErrFailedToGetBalance = "invalid_exact_evm_failed_to_get_balance" + ErrInsufficientBalance = "invalid_exact_evm_insufficient_balance" + ErrInvalidSignatureFormat = "invalid_exact_evm_signature_format" + ErrFailedToVerifySignature = "invalid_exact_evm_failed_to_verify_signature" + ErrInvalidSignature = "invalid_exact_evm_signature" + ErrValidBeforeExpired = "invalid_exact_evm_payload_authorization_valid_before" + ErrValidAfterInFuture = "invalid_exact_evm_payload_authorization_valid_after" + ErrEip3009TokenNameMismatch = "invalid_exact_evm_token_name_mismatch" + ErrEip3009TokenVersionMismatch = "invalid_exact_evm_token_version_mismatch" + ErrEip3009NotSupported = "invalid_exact_evm_eip3009_not_supported" + ErrEip3009SimulationFailed = "invalid_exact_evm_transaction_simulation_failed" // EIP-3009 Settle errors ErrVerificationFailed = "invalid_exact_evm_verification_failed" @@ -36,25 +42,32 @@ const ( ErrSmartWalletDeploymentFailed = "smart_wallet_deployment_failed" ErrUnsupportedPayloadType = "unsupported_payload_type" - // Permit2 verify errors - ErrPermit2InvalidSpender = "invalid_permit2_spender" - ErrPermit2RecipientMismatch = "invalid_permit2_recipient_mismatch" - ErrPermit2DeadlineExpired = "permit2_deadline_expired" - ErrPermit2NotYetValid = "permit2_not_yet_valid" - ErrPermit2InsufficientAmount = "permit2_insufficient_amount" - ErrPermit2TokenMismatch = "permit2_token_mismatch" - ErrPermit2InvalidSignature = "invalid_permit2_signature" - ErrPermit2AllowanceRequired = "permit2_allowance_required" + // Permit2 verify errors — canonical values live in evm.ErrPermit2* + ErrPermit2InvalidSpender = evm.ErrPermit2InvalidSpender + ErrPermit2RecipientMismatch = evm.ErrPermit2RecipientMismatch + ErrPermit2DeadlineExpired = evm.ErrPermit2DeadlineExpired + ErrPermit2NotYetValid = evm.ErrPermit2NotYetValid + ErrPermit2AmountMismatch = evm.ErrPermit2AmountMismatch + ErrPermit2TokenMismatch = evm.ErrPermit2TokenMismatch + ErrPermit2InvalidSignature = evm.ErrPermit2InvalidSignature + ErrPermit2AllowanceRequired = evm.ErrPermit2AllowanceRequired // Permit2 settle errors (from contract reverts) - ErrPermit2InvalidAmount = "permit2_invalid_amount" - ErrPermit2InvalidDestination = "permit2_invalid_destination" - ErrPermit2InvalidOwner = "permit2_invalid_owner" - ErrPermit2PaymentTooEarly = "permit2_payment_too_early" - ErrPermit2InvalidNonce = "permit2_invalid_nonce" - ErrPermit2612AmountMismatch = "permit2_2612_amount_mismatch" + ErrPermit2InvalidAmount = evm.ErrPermit2InvalidAmount + ErrPermit2InvalidDestination = evm.ErrPermit2InvalidDestination + ErrPermit2InvalidOwner = evm.ErrPermit2InvalidOwner + ErrPermit2PaymentTooEarly = evm.ErrPermit2PaymentTooEarly + ErrPermit2InvalidNonce = evm.ErrPermit2InvalidNonce + ErrPermit2612AmountMismatch = evm.ErrPermit2612AmountMismatch + + // Permit2 simulation errors + ErrPermit2SimulationFailed = evm.ErrPermit2SimulationFailed + ErrPermit2InsufficientBalance = evm.ErrPermit2InsufficientBalance + ErrPermit2ProxyNotDeployed = evm.ErrPermit2ProxyNotDeployed + ErrErc20ApprovalTxFailed = "erc20_approval_tx_failed" // ERC-20 approval gas sponsoring errors + ErrErc20ApprovalInsufficientEth = evm.ErrErc20ApprovalInsufficientEth ErrErc20ApprovalInvalidFormat = "invalid_erc20_approval_extension_format" ErrErc20ApprovalFromMismatch = "erc20_approval_from_mismatch" ErrErc20ApprovalAssetMismatch = "erc20_approval_asset_mismatch" @@ -65,5 +78,5 @@ const ( ErrErc20ApprovalWrongCalldata = "erc20_approval_tx_wrong_spender" ErrErc20ApprovalSignerMismatch = "erc20_approval_tx_signer_mismatch" ErrErc20ApprovalInvalidSig = "erc20_approval_tx_invalid_signature" - ErrErc20ApprovalBroadcastFailed = "erc20_approval_broadcast_failed" + ErrErc20ApprovalBroadcastFailed = evm.ErrErc20ApprovalBroadcastFailed ) diff --git a/go/mechanisms/evm/exact/facilitator/permit2.go b/go/mechanisms/evm/exact/facilitator/permit2.go index e21ff78899..8b4a655010 100644 --- a/go/mechanisms/evm/exact/facilitator/permit2.go +++ b/go/mechanisms/evm/exact/facilitator/permit2.go @@ -7,8 +7,6 @@ import ( "strings" "time" - "github.com/ethereum/go-ethereum/common" - x402 "github.com/coinbase/x402/go" "github.com/coinbase/x402/go/extensions/eip2612gassponsor" "github.com/coinbase/x402/go/extensions/erc20approvalgassponsor" @@ -16,6 +14,19 @@ import ( "github.com/coinbase/x402/go/types" ) +// VerifyPermit2Options controls optional behaviour for VerifyPermit2. +type VerifyPermit2Options struct { + // Simulate enables onchain simulation. Defaults to true when zero-value. + Simulate *bool +} + +func (o *VerifyPermit2Options) shouldSimulate() bool { + if o == nil || o.Simulate == nil { + return true + } + return *o.Simulate +} + // VerifyPermit2 verifies a Permit2 payment payload. func VerifyPermit2( ctx context.Context, @@ -24,6 +35,7 @@ func VerifyPermit2( requirements types.PaymentRequirements, permit2Payload *evm.ExactPermit2Payload, facilCtx *x402.FacilitatorContext, + opts *VerifyPermit2Options, ) (*x402.VerifyResponse, error) { payer := permit2Payload.Permit2Authorization.From @@ -60,8 +72,7 @@ func VerifyPermit2( if !ok { return nil, x402.NewVerifyError(ErrInvalidPayload, payer, "invalid deadline format") } - deadlineThreshold := big.NewInt(now + evm.Permit2DeadlineBuffer) - if deadline.Cmp(deadlineThreshold) < 0 { + if deadline.Cmp(big.NewInt(now+evm.Permit2DeadlineBuffer)) < 0 { return nil, x402.NewVerifyError(ErrPermit2DeadlineExpired, payer, "deadline expired") } @@ -70,8 +81,7 @@ func VerifyPermit2( if !ok { return nil, x402.NewVerifyError(ErrInvalidPayload, payer, "invalid validAfter format") } - nowBig := big.NewInt(now) - if validAfter.Cmp(nowBig) > 0 { + if validAfter.Cmp(big.NewInt(now)) > 0 { return nil, x402.NewVerifyError(ErrPermit2NotYetValid, payer, "not yet valid") } @@ -84,8 +94,8 @@ func VerifyPermit2( if !ok { return nil, x402.NewVerifyError(ErrInvalidRequiredAmount, payer, "invalid required amount format") } - if authAmount.Cmp(requiredAmount) < 0 { - return nil, x402.NewVerifyError(ErrPermit2InsufficientAmount, payer, "insufficient amount") + if authAmount.Cmp(requiredAmount) != 0 { + return nil, x402.NewVerifyError(ErrPermit2AmountMismatch, payer, "amount mismatch") } // Verify token matches @@ -99,54 +109,96 @@ func VerifyPermit2( return nil, x402.NewVerifyError(ErrInvalidSignatureFormat, payer, err.Error()) } - valid, err := verifyPermit2Signature(ctx, signer, permit2Payload.Permit2Authorization, signatureBytes, chainID) - if err != nil || !valid { - return nil, x402.NewVerifyError(ErrPermit2InvalidSignature, payer, "invalid signature") + sigValid, sigErr := verifyPermit2Signature(ctx, signer, permit2Payload.Permit2Authorization, signatureBytes, chainID) + if sigErr != nil || !sigValid { + // Check if payer is a deployed smart contract + // ERC-1271 signatures may not be verifiable by all signer implementations + code, codeErr := signer.GetCode(ctx, payer) + if codeErr != nil || len(code) == 0 { + return nil, x402.NewVerifyError(ErrPermit2InvalidSignature, payer, "invalid signature") + } + // Deployed smart contract: fall through to simulation } - // Check Permit2 allowance - allowance, err := signer.ReadContract(ctx, tokenAddress, evm.ERC20AllowanceABI, "allowance", - common.HexToAddress(payer), common.HexToAddress(evm.PERMIT2Address)) - if err == nil { - if allowanceBig, ok := allowance.(*big.Int); ok && allowanceBig.Cmp(requiredAmount) < 0 { - // Allowance insufficient - try EIP-2612 first, then ERC-20 approval extension - eip2612Info, _ := eip2612gassponsor.ExtractEip2612GasSponsoringInfo(payload.Extensions) - if eip2612Info != nil { - // Validate the EIP-2612 extension data - if validErr := validateEip2612PermitForPayment(eip2612Info, payer, tokenAddress); validErr != "" { - return nil, x402.NewVerifyError(validErr, payer, "eip2612 validation failed") - } - // EIP-2612 extension is valid, allowance will be set during settlement - } else { - // Try ERC-20 approval extension - erc20Info, _ := erc20approvalgassponsor.ExtractInfo(payload.Extensions) - if erc20Info != nil && facilCtx != nil { - ext, ok := facilCtx.GetExtension(erc20approvalgassponsor.ERC20ApprovalGasSponsoring.Key()).(*erc20approvalgassponsor.Erc20ApprovalFacilitatorExtension) - if ok && ext != nil && ext.Signer != nil { - if reason, msg := ValidateErc20ApprovalForPayment(erc20Info, payer, tokenAddress); reason != "" { - return nil, x402.NewVerifyError(reason, payer, msg) - } - // ERC-20 approval valid, tx will be broadcast during settlement - } else { - return nil, x402.NewVerifyError(ErrPermit2AllowanceRequired, payer, "permit2 allowance required") + // Early return when simulation is disabled + if !opts.shouldSimulate() { + return &x402.VerifyResponse{IsValid: true, Payer: payer}, nil + } + + // EIP-2612 gas sponsoring (atomic settleWithPermit via contract) + eip2612Info, _ := eip2612gassponsor.ExtractEip2612GasSponsoringInfo(payload.Extensions) + if eip2612Info != nil { + if validErr := validateEip2612PermitForPayment(eip2612Info, payer, tokenAddress); validErr != "" { + return nil, x402.NewVerifyError(validErr, payer, "eip2612 validation failed") + } + + simOk, simErr := SimulatePermit2SettleWithPermit(ctx, signer, permit2Payload, eip2612Info.Signature, eip2612Info.Amount, eip2612Info.Deadline) + if simErr != nil || !simOk { + resp := DiagnosePermit2SimulationFailure(ctx, signer, tokenAddress, permit2Payload, requirements.Amount) + return nil, x402.NewVerifyError(resp.InvalidReason, payer, "simulation failed") + } + return &x402.VerifyResponse{IsValid: true, Payer: payer}, nil + } + + // ERC-20 approval gas sponsoring + erc20Info, _ := erc20approvalgassponsor.ExtractInfo(payload.Extensions) + if erc20Info != nil && facilCtx != nil { + ext, ok := facilCtx.GetExtension(erc20approvalgassponsor.ERC20ApprovalGasSponsoring.Key()).(*erc20approvalgassponsor.Erc20ApprovalFacilitatorExtension) + var extensionSigner erc20approvalgassponsor.Erc20ApprovalGasSponsoringSigner + if ok && ext != nil { + extensionSigner = ext.ResolveSigner(payload.Accepted.Network) + } + + if extensionSigner != nil { + if reason, msg := ValidateErc20ApprovalForPayment(erc20Info, payer, tokenAddress); reason != "" { + return nil, x402.NewVerifyError(reason, payer, msg) + } + + // If the signer supports SimulateTransactions, use it for the approve+settle bundle + if simulator, ok := extensionSigner.(erc20approvalgassponsor.Erc20ApprovalGasSponsoringSimulator); ok { + simArgs, buildErr := BuildPermit2SettleArgs(permit2Payload) + if buildErr == nil { + simOk, simErr := simulator.SimulateTransactions(ctx, []erc20approvalgassponsor.TransactionRequest{ + {Serialized: erc20Info.SignedTransaction}, + {Call: &erc20approvalgassponsor.WriteContractCall{ + Address: evm.X402ExactPermit2ProxyAddress, + ABI: evm.X402ExactPermit2ProxySettleABI, + Function: evm.FunctionSettle, + Args: []interface{}{simArgs.permitStruct(), simArgs.Owner, simArgs.witnessStruct(), simArgs.Signature}, + }}, + }) + if simErr == nil && simOk { + return &x402.VerifyResponse{IsValid: true, Payer: payer}, nil } - } else { - return nil, x402.NewVerifyError(ErrPermit2AllowanceRequired, payer, "permit2 allowance required") } + resp := DiagnosePermit2SimulationFailure(ctx, signer, tokenAddress, permit2Payload, requirements.Amount) + return nil, x402.NewVerifyError(resp.InvalidReason, payer, "simulation failed") + } + + // Fallback: signer does not support simulation; check prerequisites only + prereqResp := CheckPermit2Prerequisites(ctx, signer, tokenAddress, payer, requirements.Amount) + if !prereqResp.IsValid { + return nil, x402.NewVerifyError(prereqResp.InvalidReason, payer, "prerequisites check failed") } + return &x402.VerifyResponse{IsValid: true, Payer: payer}, nil } } - // Check balance - balance, err := signer.GetBalance(ctx, payer, tokenAddress) - if err == nil && balance.Cmp(requiredAmount) < 0 { - return nil, x402.NewVerifyError(ErrInsufficientBalance, payer, "insufficient balance") + // Standard settle (allowance already on-chain) + simOk, simErr := SimulatePermit2Settle(ctx, signer, permit2Payload) + if simErr != nil || !simOk { + resp := DiagnosePermit2SimulationFailure(ctx, signer, tokenAddress, permit2Payload, requirements.Amount) + return nil, x402.NewVerifyError(resp.InvalidReason, payer, "simulation failed") } - return &x402.VerifyResponse{ - IsValid: true, - Payer: payer, - }, nil + return &x402.VerifyResponse{IsValid: true, Payer: payer}, nil +} + +// Permit2FacilitatorConfig holds optional settlement-time configuration. +type Permit2FacilitatorConfig struct { + // SimulateInSettle re-runs simulation during settle + // When false (default), the settle path skips simulation since verify already ran it + SimulateInSettle bool } // SettlePermit2 settles a Permit2 payment by calling x402ExactPermit2Proxy.settle(). @@ -157,12 +209,17 @@ func SettlePermit2( requirements types.PaymentRequirements, permit2Payload *evm.ExactPermit2Payload, facilCtx *x402.FacilitatorContext, + config *Permit2FacilitatorConfig, ) (*x402.SettleResponse, error) { network := x402.Network(payload.Accepted.Network) payer := permit2Payload.Permit2Authorization.From - // Re-verify before settling - verifyResp, err := VerifyPermit2(ctx, signer, payload, requirements, permit2Payload, facilCtx) + simulate := false + if config != nil { + simulate = config.SimulateInSettle + } + + verifyResp, err := VerifyPermit2(ctx, signer, payload, requirements, permit2Payload, facilCtx, &VerifyPermit2Options{Simulate: &simulate}) if err != nil { ve := &x402.VerifyError{} if errors.As(err, &ve) { @@ -171,58 +228,14 @@ func SettlePermit2( return nil, x402.NewSettleError(ErrVerificationFailed, payer, network, "", err.Error()) } - // Parse values for contract call (validated during verify, but check again for safety) - amount, ok := new(big.Int).SetString(permit2Payload.Permit2Authorization.Permitted.Amount, 10) - if !ok { - return nil, x402.NewSettleError(ErrInvalidPayload, payer, network, "", "invalid permitted amount") - } - nonce, ok := new(big.Int).SetString(permit2Payload.Permit2Authorization.Nonce, 10) - if !ok { - return nil, x402.NewSettleError(ErrInvalidPayload, payer, network, "", "invalid nonce") - } - deadline, ok := new(big.Int).SetString(permit2Payload.Permit2Authorization.Deadline, 10) - if !ok { - return nil, x402.NewSettleError(ErrInvalidPayload, payer, network, "", "invalid deadline") - } - validAfter, ok := new(big.Int).SetString(permit2Payload.Permit2Authorization.Witness.ValidAfter, 10) - if !ok { - return nil, x402.NewSettleError(ErrInvalidPayload, payer, network, "", "invalid validAfter") - } - signatureBytes, err := evm.HexToBytes(permit2Payload.Signature) - if err != nil { - return nil, x402.NewSettleError(ErrInvalidSignatureFormat, payer, network, "", "invalid signature format") - } - - // Create struct args for the settle call - // The ABI expects: settle(permit, owner, witness, signature) - permitStruct := struct { - Permitted struct { - Token common.Address - Amount *big.Int - } - Nonce *big.Int - Deadline *big.Int - }{ - Permitted: struct { - Token common.Address - Amount *big.Int - }{ - Token: common.HexToAddress(permit2Payload.Permit2Authorization.Permitted.Token), - Amount: amount, - }, - Nonce: nonce, - Deadline: deadline, + args, buildErr := BuildPermit2SettleArgs(permit2Payload) + if buildErr != nil { + return nil, x402.NewSettleError(ErrInvalidPayload, payer, network, "", buildErr.Error()) } - witnessStruct := struct { - To common.Address - ValidAfter *big.Int - }{ - To: common.HexToAddress(permit2Payload.Permit2Authorization.Witness.To), - ValidAfter: validAfter, - } + permitStruct := args.permitStruct() + witnessStruct := args.witnessStruct() - // Check for EIP-2612 gas sponsoring extension eip2612Info, _ := eip2612gassponsor.ExtractEip2612GasSponsoringInfo(payload.Extensions) erc20Info, _ := erc20approvalgassponsor.ExtractInfo(payload.Extensions) @@ -266,65 +279,57 @@ func SettlePermit2( evm.FunctionSettleWithPermit, permit2612Struct, permitStruct, - common.HexToAddress(payer), + args.Owner, witnessStruct, - signatureBytes, + args.Signature, ) + case erc20Info != nil && facilCtx != nil: - // ERC-20 approval path: broadcast pre-signed approve tx, then settle + // Branch: ERC-20 approval gas sponsoring (broadcast approval + settle via extension signer) ext, ok := facilCtx.GetExtension(erc20approvalgassponsor.ERC20ApprovalGasSponsoring.Key()).(*erc20approvalgassponsor.Erc20ApprovalFacilitatorExtension) - if ok && ext != nil && ext.Signer != nil { - // 1. Broadcast the pre-signed approve transaction - approveTxHash, broadcastErr := ext.Signer.SendRawTransaction(ctx, erc20Info.SignedTransaction) - if broadcastErr != nil { - return nil, x402.NewSettleError(ErrErc20ApprovalBroadcastFailed, payer, network, "", broadcastErr.Error()) + var extensionSigner erc20approvalgassponsor.Erc20ApprovalGasSponsoringSigner + if ok && ext != nil { + extensionSigner = ext.ResolveSigner(payload.Accepted.Network) + } + if extensionSigner != nil { + settle := erc20approvalgassponsor.WriteContractCall{ + Address: evm.X402ExactPermit2ProxyAddress, + ABI: evm.X402ExactPermit2ProxySettleABI, + Function: evm.FunctionSettle, + Args: []interface{}{permitStruct, args.Owner, witnessStruct, args.Signature}, } - - // 2. Wait for approve tx confirmation - approveReceipt, receiptErr := ext.Signer.WaitForTransactionReceipt(ctx, approveTxHash) - if receiptErr != nil || approveReceipt.Status != evm.TxStatusSuccess { - msg := "approve tx failed" - if receiptErr != nil { - msg = receiptErr.Error() - } - return nil, x402.NewSettleError(ErrErc20ApprovalBroadcastFailed, payer, network, approveTxHash, msg) + txHashes, sendErr := extensionSigner.SendTransactions(ctx, []erc20approvalgassponsor.TransactionRequest{ + {Serialized: erc20Info.SignedTransaction}, + {Call: &settle}, + }) + if sendErr != nil { + err = sendErr + } else if len(txHashes) > 0 { + txHash = txHashes[len(txHashes)-1] } - - // 3. Call settle via extension signer - txHash, err = ext.Signer.WriteContract( - ctx, - evm.X402ExactPermit2ProxyAddress, - evm.X402ExactPermit2ProxySettleABI, - evm.FunctionSettle, - permitStruct, - common.HexToAddress(payer), - witnessStruct, - signatureBytes, - ) } else { - // Extension not properly configured, fall through to standard settle txHash, err = signer.WriteContract( ctx, evm.X402ExactPermit2ProxyAddress, evm.X402ExactPermit2ProxySettleABI, evm.FunctionSettle, permitStruct, - common.HexToAddress(payer), + args.Owner, witnessStruct, - signatureBytes, + args.Signature, ) } + default: - // Standard settle - no gas sponsoring extension txHash, err = signer.WriteContract( ctx, evm.X402ExactPermit2ProxyAddress, evm.X402ExactPermit2ProxySettleABI, evm.FunctionSettle, permitStruct, - common.HexToAddress(payer), + args.Owner, witnessStruct, - signatureBytes, + args.Signature, ) } @@ -334,7 +339,15 @@ func SettlePermit2( } // Wait for transaction confirmation - receipt, err := signer.WaitForTransactionReceipt(ctx, txHash) + receiptWaitSigner := signer + if erc20Info != nil && facilCtx != nil { + if ext, ok := facilCtx.GetExtension(erc20approvalgassponsor.ERC20ApprovalGasSponsoring.Key()).(*erc20approvalgassponsor.Erc20ApprovalFacilitatorExtension); ok && ext != nil { + if extensionSigner := ext.ResolveSigner(payload.Accepted.Network); extensionSigner != nil { + receiptWaitSigner = extensionSigner + } + } + } + receipt, err := receiptWaitSigner.WaitForTransactionReceipt(ctx, txHash) if err != nil { return nil, x402.NewSettleError(ErrFailedToGetReceipt, payer, network, txHash, err.Error()) } @@ -372,57 +385,8 @@ func verifyPermit2Signature( return valid, err } -// validateEip2612PermitForPayment validates the EIP-2612 extension data. -// Returns an empty string if valid, or an error reason string. -func validateEip2612PermitForPayment(info *eip2612gassponsor.Info, payer string, tokenAddress string) string { - if !eip2612gassponsor.ValidateEip2612GasSponsoringInfo(info) { - return "invalid_eip2612_extension_format" - } - - // Verify from matches payer - if !strings.EqualFold(info.From, payer) { - return "eip2612_from_mismatch" - } - - // Verify asset matches token - if !strings.EqualFold(info.Asset, tokenAddress) { - return "eip2612_asset_mismatch" - } - - // Verify spender is Permit2 - if !strings.EqualFold(info.Spender, evm.PERMIT2Address) { - return "eip2612_spender_not_permit2" - } - - // Verify deadline not expired - // Use 6 second buffer consistent with Permit2 deadline check - now := time.Now().Unix() - deadline, ok := new(big.Int).SetString(info.Deadline, 10) - if !ok || deadline.Int64() < now+evm.Permit2DeadlineBuffer { - return "eip2612_deadline_expired" - } - - return "" -} - -// splitEip2612Signature splits a 65-byte hex signature into v, r, s. -func splitEip2612Signature(signature string) (uint8, [32]byte, [32]byte, error) { - sigBytes, err := evm.HexToBytes(signature) - if err != nil { - return 0, [32]byte{}, [32]byte{}, err - } - - if len(sigBytes) != 65 { - return 0, [32]byte{}, [32]byte{}, errors.New("signature must be 65 bytes") - } - - var r, s [32]byte - copy(r[:], sigBytes[0:32]) - copy(s[:], sigBytes[32:64]) - v := sigBytes[64] - - return v, r, s, nil -} +var validateEip2612PermitForPayment = evm.ValidateEip2612PermitForPayment +var splitEip2612Signature = evm.SplitEip2612Signature // parsePermit2Error extracts meaningful error codes from contract reverts. func parsePermit2Error(err error) string { @@ -442,6 +406,8 @@ func parsePermit2Error(err error) string { return ErrPermit2InvalidSignature case strings.Contains(msg, "InvalidNonce"): return ErrPermit2InvalidNonce + case strings.Contains(msg, "erc20_approval_tx_failed"): + return ErrErc20ApprovalBroadcastFailed default: return ErrFailedToExecuteTransfer } diff --git a/go/mechanisms/evm/exact/facilitator/permit2_helpers.go b/go/mechanisms/evm/exact/facilitator/permit2_helpers.go new file mode 100644 index 0000000000..8ee9a44ced --- /dev/null +++ b/go/mechanisms/evm/exact/facilitator/permit2_helpers.go @@ -0,0 +1,301 @@ +package facilitator + +import ( + "context" + "math/big" + + "github.com/ethereum/go-ethereum/common" + + x402 "github.com/coinbase/x402/go" + "github.com/coinbase/x402/go/mechanisms/evm" +) + +// Permit2SettleArgs holds the parsed and typed arguments for settle() / settleWithPermit(). +type Permit2SettleArgs struct { + Permit struct { + Permitted struct { + Token common.Address + Amount *big.Int + } + Nonce *big.Int + Deadline *big.Int + } + Owner common.Address + Witness struct { + To common.Address + ValidAfter *big.Int + } + Signature []byte +} + +// BuildPermit2SettleArgs converts a raw ExactPermit2Payload into typed contract-call +// arguments, deduplicating the struct construction shared by verify simulation and settle. +func BuildPermit2SettleArgs(permit2Payload *evm.ExactPermit2Payload) (*Permit2SettleArgs, error) { + amount, ok := new(big.Int).SetString(permit2Payload.Permit2Authorization.Permitted.Amount, 10) + if !ok { + return nil, errParse("permitted amount") + } + nonce, ok := new(big.Int).SetString(permit2Payload.Permit2Authorization.Nonce, 10) + if !ok { + return nil, errParse("nonce") + } + deadline, ok := new(big.Int).SetString(permit2Payload.Permit2Authorization.Deadline, 10) + if !ok { + return nil, errParse("deadline") + } + validAfter, ok := new(big.Int).SetString(permit2Payload.Permit2Authorization.Witness.ValidAfter, 10) + if !ok { + return nil, errParse("validAfter") + } + signatureBytes, err := evm.HexToBytes(permit2Payload.Signature) + if err != nil { + return nil, err + } + + args := &Permit2SettleArgs{} + args.Permit.Permitted.Token = common.HexToAddress(permit2Payload.Permit2Authorization.Permitted.Token) + args.Permit.Permitted.Amount = amount + args.Permit.Nonce = nonce + args.Permit.Deadline = deadline + args.Owner = common.HexToAddress(permit2Payload.Permit2Authorization.From) + args.Witness.To = common.HexToAddress(permit2Payload.Permit2Authorization.Witness.To) + args.Witness.ValidAfter = validAfter + args.Signature = signatureBytes + return args, nil +} + +// SimulatePermit2Settle runs settle() via eth_call (ReadContract). +// Returns true if the simulation succeeded. +func SimulatePermit2Settle( + ctx context.Context, + signer evm.FacilitatorEvmSigner, + permit2Payload *evm.ExactPermit2Payload, +) (bool, error) { + args, err := BuildPermit2SettleArgs(permit2Payload) + if err != nil { + return false, err + } + + permitStruct := args.permitStruct() + witnessStruct := args.witnessStruct() + + _, err = signer.ReadContract( + ctx, + evm.X402ExactPermit2ProxyAddress, + evm.X402ExactPermit2ProxySettleABI, + evm.FunctionSettle, + permitStruct, + args.Owner, + witnessStruct, + args.Signature, + ) + if err != nil { + return false, err + } + return true, nil +} + +// SimulatePermit2SettleWithPermit runs settleWithPermit() via eth_call. +// The contract atomically calls token.permit() then PERMIT2.permitTransferFrom(), +// so simulation covers allowance + balance + nonces. +func SimulatePermit2SettleWithPermit( + ctx context.Context, + signer evm.FacilitatorEvmSigner, + permit2Payload *evm.ExactPermit2Payload, + eip2612Signature, eip2612Amount, eip2612DeadlineStr string, +) (bool, error) { + args, err := BuildPermit2SettleArgs(permit2Payload) + if err != nil { + return false, err + } + + v, r, s, splitErr := splitEip2612Signature(eip2612Signature) + if splitErr != nil { + return false, splitErr + } + + eip2612Value, ok := new(big.Int).SetString(eip2612Amount, 10) + if !ok { + return false, errParse("eip2612 amount") + } + eip2612Deadline, ok := new(big.Int).SetString(eip2612DeadlineStr, 10) + if !ok { + return false, errParse("eip2612 deadline") + } + + permit2612Struct := struct { + Value *big.Int + Deadline *big.Int + R [32]byte + S [32]byte + V uint8 + }{ + Value: eip2612Value, + Deadline: eip2612Deadline, + R: r, + S: s, + V: v, + } + + permitStruct := args.permitStruct() + witnessStruct := args.witnessStruct() + + _, err = signer.ReadContract( + ctx, + evm.X402ExactPermit2ProxyAddress, + evm.X402ExactPermit2ProxySettleWithPermitABI, + evm.FunctionSettleWithPermit, + permit2612Struct, + permitStruct, + args.Owner, + witnessStruct, + args.Signature, + ) + if err != nil { + return false, err + } + return true, nil +} + +// DiagnosePermit2SimulationFailure runs a multicall diagnostic to return the most +// specific error reason after a simulation failure. +func DiagnosePermit2SimulationFailure( + ctx context.Context, + signer evm.FacilitatorEvmSigner, + tokenAddress string, + permit2Payload *evm.ExactPermit2Payload, + amountRequired string, +) *x402.VerifyResponse { + payer := permit2Payload.Permit2Authorization.From + + results, err := evm.Multicall(ctx, signer, []evm.MulticallCall{ + { + Address: evm.X402ExactPermit2ProxyAddress, + ABI: evm.X402ExactPermit2ProxyPermit2ABI, + FunctionName: "PERMIT2", + }, + { + Address: tokenAddress, + ABI: evm.ERC20BalanceOfABI, + FunctionName: "balanceOf", + Args: []interface{}{common.HexToAddress(payer)}, + }, + { + Address: tokenAddress, + ABI: evm.ERC20AllowanceABI, + FunctionName: "allowance", + Args: []interface{}{common.HexToAddress(payer), common.HexToAddress(evm.PERMIT2Address)}, + }, + }) + if err != nil || len(results) < 3 { + return &x402.VerifyResponse{IsValid: false, InvalidReason: ErrPermit2SimulationFailed, Payer: payer} + } + + if !results[0].Success() { + return &x402.VerifyResponse{IsValid: false, InvalidReason: ErrPermit2ProxyNotDeployed, Payer: payer} + } + + reqAmount, ok := new(big.Int).SetString(amountRequired, 10) + if !ok { + return &x402.VerifyResponse{IsValid: false, InvalidReason: ErrPermit2SimulationFailed, Payer: payer} + } + + if results[1].Success() { + if balance := asBigInt(results[1].Result); balance != nil && balance.Cmp(reqAmount) < 0 { + return &x402.VerifyResponse{IsValid: false, InvalidReason: ErrPermit2InsufficientBalance, Payer: payer} + } + } + + if results[2].Success() { + if allowance := asBigInt(results[2].Result); allowance != nil && allowance.Cmp(reqAmount) < 0 { + return &x402.VerifyResponse{IsValid: false, InvalidReason: ErrPermit2AllowanceRequired, Payer: payer} + } + } + + return &x402.VerifyResponse{IsValid: false, InvalidReason: ErrPermit2SimulationFailed, Payer: payer} +} + +// CheckPermit2Prerequisites checks proxy deployment and payer token balance. +func CheckPermit2Prerequisites( + ctx context.Context, + signer evm.FacilitatorEvmSigner, + tokenAddress string, + payer string, + amountRequired string, +) *x402.VerifyResponse { + results, err := evm.Multicall(ctx, signer, []evm.MulticallCall{ + { + Address: evm.X402ExactPermit2ProxyAddress, + ABI: evm.X402ExactPermit2ProxyPermit2ABI, + FunctionName: "PERMIT2", + }, + { + Address: tokenAddress, + ABI: evm.ERC20BalanceOfABI, + FunctionName: "balanceOf", + Args: []interface{}{common.HexToAddress(payer)}, + }, + }) + if err != nil || len(results) < 2 { + // Fail open for prerequisites-only check + return &x402.VerifyResponse{IsValid: true, Payer: payer} + } + + if !results[0].Success() { + return &x402.VerifyResponse{IsValid: false, InvalidReason: ErrPermit2ProxyNotDeployed, Payer: payer} + } + + reqAmount, ok := new(big.Int).SetString(amountRequired, 10) + if ok && results[1].Success() { + if balance := asBigInt(results[1].Result); balance != nil && balance.Cmp(reqAmount) < 0 { + return &x402.VerifyResponse{IsValid: false, InvalidReason: ErrPermit2InsufficientBalance, Payer: payer} + } + } + + return &x402.VerifyResponse{IsValid: true, Payer: payer} +} + +// permitStruct returns the ABI-compatible tuple for the permit parameter. +func (a *Permit2SettleArgs) permitStruct() interface{} { + return struct { + Permitted struct { + Token common.Address + Amount *big.Int + } + Nonce *big.Int + Deadline *big.Int + }{ + Permitted: struct { + Token common.Address + Amount *big.Int + }{ + Token: a.Permit.Permitted.Token, + Amount: a.Permit.Permitted.Amount, + }, + Nonce: a.Permit.Nonce, + Deadline: a.Permit.Deadline, + } +} + +// witnessStruct returns the ABI-compatible tuple for the witness parameter. +func (a *Permit2SettleArgs) witnessStruct() interface{} { + return struct { + To common.Address + ValidAfter *big.Int + }{ + To: a.Witness.To, + ValidAfter: a.Witness.ValidAfter, + } +} + +func errParse(field string) error { + return &parseError{field: field} +} + +type parseError struct { + field string +} + +func (e *parseError) Error() string { + return "invalid " + e.field +} diff --git a/go/mechanisms/evm/exact/facilitator/scheme.go b/go/mechanisms/evm/exact/facilitator/scheme.go index a9dbe8c365..2ccc92740d 100644 --- a/go/mechanisms/evm/exact/facilitator/scheme.go +++ b/go/mechanisms/evm/exact/facilitator/scheme.go @@ -2,13 +2,7 @@ package facilitator import ( "context" - "errors" "fmt" - "math/big" - "strings" - "time" - - "github.com/ethereum/go-ethereum/common" x402 "github.com/coinbase/x402/go" "github.com/coinbase/x402/go/mechanisms/evm" @@ -20,6 +14,8 @@ type ExactEvmSchemeConfig struct { // DeployERC4337WithEIP6492 enables automatic deployment of ERC-4337 smart wallets // via EIP-6492 when encountering undeployed contract signatures during settlement DeployERC4337WithEIP6492 bool + // SimulateInSettle reruns transfer simulation during settle. Verify always simulates. + SimulateInSettle bool } // ExactEvmScheme implements the SchemeNetworkFacilitator interface for EVM exact payments (V2) @@ -78,148 +74,17 @@ func (f *ExactEvmScheme) Verify( requirements types.PaymentRequirements, fctx *x402.FacilitatorContext, ) (*x402.VerifyResponse, error) { - // Check if this is a Permit2 payload and route accordingly - if evm.IsPermit2Payload(payload.Payload) { + isPermit2 := evm.IsPermit2Payload(payload.Payload) + + if isPermit2 { permit2Payload, err := evm.Permit2PayloadFromMap(payload.Payload) if err != nil { return nil, x402.NewVerifyError(ErrInvalidPayload, "", fmt.Sprintf("failed to parse Permit2 payload: %s", err.Error())) } - return VerifyPermit2(ctx, f.signer, payload, requirements, permit2Payload, fctx) + return VerifyPermit2(ctx, f.signer, payload, requirements, permit2Payload, fctx, nil) } - // Default to EIP-3009 verification - return f.verifyEIP3009(ctx, payload, requirements) -} - -// verifyEIP3009 verifies an EIP-3009 payment payload. -func (f *ExactEvmScheme) verifyEIP3009( - ctx context.Context, - payload types.PaymentPayload, - requirements types.PaymentRequirements, -) (*x402.VerifyResponse, error) { - // Validate scheme (v2 has scheme in Accepted field) - if payload.Accepted.Scheme != evm.SchemeExact { - return nil, x402.NewVerifyError(ErrInvalidScheme, "", fmt.Sprintf("invalid scheme: %s", payload.Accepted.Scheme)) - } - - // Validate network (v2 has network in Accepted field) - if payload.Accepted.Network != requirements.Network { - return nil, x402.NewVerifyError(ErrNetworkMismatch, "", fmt.Sprintf("network mismatch: %s != %s", payload.Accepted.Network, requirements.Network)) - } - - // Parse EVM payload - evmPayload, err := evm.PayloadFromMap(payload.Payload) - if err != nil { - return nil, x402.NewVerifyError(ErrInvalidPayload, "", fmt.Sprintf("failed to parse EVM payload: %s", err.Error())) - } - - // Validate signature exists - if evmPayload.Signature == "" { - return nil, x402.NewVerifyError(ErrMissingSignature, "", "missing signature") - } - - // Parse chain ID from network identifier - chainID, err := evm.GetEvmChainId(string(requirements.Network)) - if err != nil { - return nil, x402.NewVerifyError(ErrFailedToGetNetworkConfig, "", err.Error()) - } - - tokenAddress := evm.NormalizeAddress(requirements.Asset) - - // Validate authorization matches requirements - if !strings.EqualFold(evmPayload.Authorization.To, requirements.PayTo) { - return nil, x402.NewVerifyError(ErrRecipientMismatch, "", fmt.Sprintf("recipient mismatch: %s != %s", evmPayload.Authorization.To, requirements.PayTo)) - } - - // Parse and validate amount - authValue, ok := new(big.Int).SetString(evmPayload.Authorization.Value, 10) - if !ok { - return nil, x402.NewVerifyError(ErrInvalidAuthorizationValue, "", fmt.Sprintf("invalid authorization value: %s", evmPayload.Authorization.Value)) - } - - // Requirements.Amount is already in the smallest unit - requiredValue, ok := new(big.Int).SetString(requirements.Amount, 10) - if !ok { - return nil, x402.NewVerifyError(ErrInvalidRequiredAmount, "", fmt.Sprintf("invalid required amount: %s", requirements.Amount)) - } - - if authValue.Cmp(requiredValue) < 0 { - return nil, x402.NewVerifyError(ErrInsufficientAmount, evmPayload.Authorization.From, fmt.Sprintf("insufficient amount: %s < %s", authValue.String(), requiredValue.String())) - } - - // Check validBefore is in the future (with 6 second buffer for block time) - now := time.Now().Unix() - validBefore, ok := new(big.Int).SetString(evmPayload.Authorization.ValidBefore, 10) - if !ok { - return nil, x402.NewVerifyError(ErrInvalidPayload, evmPayload.Authorization.From, "invalid validBefore format") - } - if validBefore.Cmp(big.NewInt(now+6)) < 0 { - return nil, x402.NewVerifyError(ErrValidBeforeExpired, evmPayload.Authorization.From, - fmt.Sprintf("valid before expired: %s", validBefore.String())) - } - - // Check validAfter is not in the future - validAfter, ok := new(big.Int).SetString(evmPayload.Authorization.ValidAfter, 10) - if !ok { - return nil, x402.NewVerifyError(ErrInvalidPayload, evmPayload.Authorization.From, "invalid validAfter format") - } - if validAfter.Cmp(big.NewInt(now)) > 0 { - return nil, x402.NewVerifyError(ErrValidAfterInFuture, evmPayload.Authorization.From, - fmt.Sprintf("valid after in future: %s", validAfter.String())) - } - - // Check if nonce has been used - nonceUsed, err := f.checkNonceUsed(ctx, evmPayload.Authorization.From, evmPayload.Authorization.Nonce, tokenAddress) - if err != nil { - return nil, x402.NewVerifyError(ErrFailedToCheckNonce, evmPayload.Authorization.From, err.Error()) - } - if nonceUsed { - return nil, x402.NewVerifyError(ErrNonceAlreadyUsed, evmPayload.Authorization.From, fmt.Sprintf("nonce already used: %s", evmPayload.Authorization.Nonce)) - } - - // Check balance - balance, err := f.signer.GetBalance(ctx, evmPayload.Authorization.From, tokenAddress) - if err != nil { - return nil, x402.NewVerifyError(ErrFailedToGetBalance, evmPayload.Authorization.From, err.Error()) - } - if balance.Cmp(authValue) < 0 { - return nil, x402.NewVerifyError(ErrInsufficientBalance, evmPayload.Authorization.From, fmt.Sprintf("insufficient balance: %s < %s", balance.String(), authValue.String())) - } - - // Extract EIP-712 domain parameters from requirements - tokenName, _ := requirements.Extra["name"].(string) - tokenVersion, _ := requirements.Extra["version"].(string) - if tokenName == "" || tokenVersion == "" { - return nil, x402.NewVerifyError(ErrMissingEip712Domain, evmPayload.Authorization.From, "missing EIP-712 domain name/version in requirements.extra") - } - - // Verify signature - signatureBytes, err := evm.HexToBytes(evmPayload.Signature) - if err != nil { - return nil, x402.NewVerifyError(ErrInvalidSignatureFormat, evmPayload.Authorization.From, err.Error()) - } - - valid, err := f.verifySignature( - ctx, - evmPayload.Authorization, - signatureBytes, - chainID, - tokenAddress, - tokenName, - tokenVersion, - ) - if err != nil { - return nil, x402.NewVerifyError(ErrFailedToVerifySignature, evmPayload.Authorization.From, err.Error()) - } - - if !valid { - return nil, x402.NewVerifyError(ErrInvalidSignature, evmPayload.Authorization.From, fmt.Sprintf("invalid signature: %s", evmPayload.Signature)) - } - - return &x402.VerifyResponse{ - IsValid: true, - Payer: evmPayload.Authorization.From, - }, nil + return f.verifyEIP3009(ctx, payload, requirements, true) } // Settle settles a V2 payment on-chain. @@ -230,291 +95,18 @@ func (f *ExactEvmScheme) Settle( requirements types.PaymentRequirements, fctx *x402.FacilitatorContext, ) (*x402.SettleResponse, error) { - // Check if this is a Permit2 payload and route accordingly - if evm.IsPermit2Payload(payload.Payload) { + isPermit2 := evm.IsPermit2Payload(payload.Payload) + + if isPermit2 { permit2Payload, err := evm.Permit2PayloadFromMap(payload.Payload) if err != nil { network := x402.Network(payload.Accepted.Network) return nil, x402.NewSettleError(ErrInvalidPayload, "", network, "", fmt.Sprintf("failed to parse Permit2 payload: %s", err.Error())) } - return SettlePermit2(ctx, f.signer, payload, requirements, permit2Payload, fctx) + return SettlePermit2(ctx, f.signer, payload, requirements, permit2Payload, fctx, &Permit2FacilitatorConfig{ + SimulateInSettle: f.config.SimulateInSettle, + }) } - // Default to EIP-3009 settlement return f.settleEIP3009(ctx, payload, requirements) } - -// settleEIP3009 settles an EIP-3009 payment on-chain. -func (f *ExactEvmScheme) settleEIP3009( - ctx context.Context, - payload types.PaymentPayload, - requirements types.PaymentRequirements, -) (*x402.SettleResponse, error) { - network := x402.Network(payload.Accepted.Network) - - // First verify the payment - verifyResp, err := f.verifyEIP3009(ctx, payload, requirements) - if err != nil { - // Convert VerifyError to SettleError - ve := &x402.VerifyError{} - if errors.As(err, &ve) { - return nil, x402.NewSettleError(ve.InvalidReason, ve.Payer, network, "", ve.InvalidMessage) - } - return nil, x402.NewSettleError(ErrVerificationFailed, "", network, "", err.Error()) - } - - // Parse EVM payload - evmPayload, err := evm.PayloadFromMap(payload.Payload) - if err != nil { - return nil, x402.NewSettleError(ErrInvalidPayload, verifyResp.Payer, network, "", err.Error()) - } - - tokenAddress := evm.NormalizeAddress(requirements.Asset) - - // Parse signature - signatureBytes, err := evm.HexToBytes(evmPayload.Signature) - if err != nil { - return nil, x402.NewSettleError(ErrInvalidSignatureFormat, verifyResp.Payer, network, "", err.Error()) - } - - // Parse ERC-6492 signature to extract inner signature if needed - sigData, err := evm.ParseERC6492Signature(signatureBytes) - if err != nil { - return nil, x402.NewSettleError(ErrFailedToParseSignature, verifyResp.Payer, network, "", err.Error()) - } - - // Check if wallet needs deployment (undeployed smart wallet with ERC-6492) - zeroFactory := [20]byte{} - if sigData.Factory != zeroFactory && len(sigData.FactoryCalldata) > 0 { - code, err := f.signer.GetCode(ctx, evmPayload.Authorization.From) - if err != nil { - return nil, x402.NewSettleError(ErrFailedToCheckDeployment, verifyResp.Payer, network, "", err.Error()) - } - - if len(code) == 0 { - // Wallet not deployed - if f.config.DeployERC4337WithEIP6492 { - // Deploy wallet - err := f.deploySmartWallet(ctx, sigData) - if err != nil { - return nil, x402.NewSettleError(ErrSmartWalletDeploymentFailed, verifyResp.Payer, network, "", err.Error()) - } - } else { - // Deployment not enabled - fail settlement - return nil, x402.NewSettleError(ErrUndeployedSmartWallet, verifyResp.Payer, network, "", "") - } - } - } - - // Use inner signature for settlement - signatureBytes = sigData.InnerSignature - - // Parse values (validated during verify, but check again for safety) - value, ok := new(big.Int).SetString(evmPayload.Authorization.Value, 10) - if !ok { - return nil, x402.NewSettleError(ErrInvalidPayload, verifyResp.Payer, network, "", "invalid authorization value") - } - validAfter, ok := new(big.Int).SetString(evmPayload.Authorization.ValidAfter, 10) - if !ok { - return nil, x402.NewSettleError(ErrInvalidPayload, verifyResp.Payer, network, "", "invalid validAfter") - } - validBefore, ok := new(big.Int).SetString(evmPayload.Authorization.ValidBefore, 10) - if !ok { - return nil, x402.NewSettleError(ErrInvalidPayload, verifyResp.Payer, network, "", "invalid validBefore") - } - nonceBytes, err := evm.HexToBytes(evmPayload.Authorization.Nonce) - if err != nil { - return nil, x402.NewSettleError(ErrInvalidPayload, verifyResp.Payer, network, "", "invalid nonce format") - } - - // Determine signature type: ECDSA (65 bytes) or smart wallet (longer) - isECDSA := len(signatureBytes) == 65 - - var txHash string - if isECDSA { - // For EOA wallets, use v,r,s overload - r := signatureBytes[0:32] - s := signatureBytes[32:64] - v := signatureBytes[64] - if v == 0 || v == 1 { - v += 27 - } - - txHash, err = f.signer.WriteContract( - ctx, - tokenAddress, - evm.TransferWithAuthorizationVRSABI, - evm.FunctionTransferWithAuthorization, - common.HexToAddress(evmPayload.Authorization.From), - common.HexToAddress(evmPayload.Authorization.To), - value, - validAfter, - validBefore, - [32]byte(nonceBytes), - v, - [32]byte(r), - [32]byte(s), - ) - } else { - // For smart wallets, use bytes signature overload - txHash, err = f.signer.WriteContract( - ctx, - tokenAddress, - evm.TransferWithAuthorizationBytesABI, - evm.FunctionTransferWithAuthorization, - common.HexToAddress(evmPayload.Authorization.From), - common.HexToAddress(evmPayload.Authorization.To), - value, - validAfter, - validBefore, - [32]byte(nonceBytes), - signatureBytes, - ) - } - - if err != nil { - return nil, x402.NewSettleError(ErrFailedToExecuteTransfer, verifyResp.Payer, network, "", err.Error()) - } - - // Wait for transaction confirmation - receipt, err := f.signer.WaitForTransactionReceipt(ctx, txHash) - if err != nil { - return nil, x402.NewSettleError(ErrFailedToGetReceipt, verifyResp.Payer, network, txHash, err.Error()) - } - - if receipt.Status != evm.TxStatusSuccess { - return nil, x402.NewSettleError(ErrTransactionFailed, verifyResp.Payer, network, txHash, "") - } - - return &x402.SettleResponse{ - Success: true, - Transaction: txHash, - Network: network, - Payer: verifyResp.Payer, - }, nil -} - -// deploySmartWallet deploys an ERC-4337 smart wallet using the ERC-6492 factory -// -// This function sends the pre-encoded factory calldata directly as a transaction. -// The factoryCalldata already contains the complete encoded function call with selector. -// -// Args: -// -// ctx: Context for cancellation -// sigData: Parsed ERC-6492 signature containing factory address and calldata -// -// Returns: -// -// error if deployment fails -func (f *ExactEvmScheme) deploySmartWallet( - ctx context.Context, - sigData *evm.ERC6492SignatureData, -) error { - factoryAddr := common.BytesToAddress(sigData.Factory[:]) - - // Send the factory calldata directly - it already contains the encoded function call - txHash, err := f.signer.SendTransaction( - ctx, - factoryAddr.Hex(), - sigData.FactoryCalldata, - ) - if err != nil { - return fmt.Errorf("factory deployment transaction failed: %w", err) - } - - // Wait for deployment transaction - receipt, err := f.signer.WaitForTransactionReceipt(ctx, txHash) - if err != nil { - return fmt.Errorf("failed to wait for deployment: %w", err) - } - - if receipt.Status != evm.TxStatusSuccess { - return fmt.Errorf("deployment transaction reverted") - } - - return nil -} - -// checkNonceUsed checks if a nonce has already been used -func (f *ExactEvmScheme) checkNonceUsed(ctx context.Context, from string, nonce string, tokenAddress string) (bool, error) { - nonceBytes, err := evm.HexToBytes(nonce) - if err != nil { - return false, err - } - - result, err := f.signer.ReadContract( - ctx, - tokenAddress, - evm.AuthorizationStateABI, - evm.FunctionAuthorizationState, - common.HexToAddress(from), - [32]byte(nonceBytes), - ) - if err != nil { - return false, err - } - - used, ok := result.(bool) - if !ok { - return false, fmt.Errorf("unexpected result type from authorizationState") - } - - return used, nil -} - -// verifySignature verifies the EIP-712 signature -func (f *ExactEvmScheme) verifySignature( - ctx context.Context, - authorization evm.ExactEIP3009Authorization, - signature []byte, - chainID *big.Int, - verifyingContract string, - tokenName string, - tokenVersion string, -) (bool, error) { - // Hash the EIP-712 typed data - hash, err := evm.HashEIP3009Authorization( - authorization, - chainID, - verifyingContract, - tokenName, - tokenVersion, - ) - if err != nil { - return false, err - } - - // Convert hash to [32]byte - var hash32 [32]byte - copy(hash32[:], hash) - - // Use universal verification (supports EOA, EIP-1271, and ERC-6492) - valid, sigData, err := evm.VerifyUniversalSignature( - ctx, - f.signer, - authorization.From, - hash32, - signature, - true, // allowUndeployed in verify() - ) - - if err != nil { - return false, err - } - - // If undeployed wallet with deployment info, it will be deployed in settle() - if sigData != nil { - zeroFactory := [20]byte{} - if sigData.Factory != zeroFactory { - _, err := f.signer.GetCode(ctx, authorization.From) - if err != nil { - return false, err - } - // Wallet may not be deployed - this is OK in verify() if has deployment info - // Actual deployment happens in settle() if configured - } - } - - return valid, nil -} diff --git a/go/mechanisms/evm/exact/server/scheme.go b/go/mechanisms/evm/exact/server/scheme.go index b31193d5b2..b554b1324f 100644 --- a/go/mechanisms/evm/exact/server/scheme.go +++ b/go/mechanisms/evm/exact/server/scheme.go @@ -30,6 +30,16 @@ func (s *ExactEvmScheme) Scheme() string { return evm.SchemeExact } +// GetAssetDecimals implements AssetDecimalsProvider. Returns the decimal precision for the +// given asset on the given network, falling back to 6 if the asset is not recognized. +func (s *ExactEvmScheme) GetAssetDecimals(asset string, network x402.Network) int { + info, err := evm.GetAssetInfo(string(network), asset) + if err != nil || info == nil { + return 6 + } + return info.Decimals +} + // RegisterMoneyParser registers a custom money parser in the parser chain. // Multiple parsers can be registered - they will be tried in registration order. // Each parser receives a decimal amount (e.g., 1.50 for $1.50). @@ -138,11 +148,8 @@ func (s *ExactEvmScheme) ParsePrice(price x402.Price, network x402.Network) (x40 func (s *ExactEvmScheme) parseMoneyToDecimal(price x402.Price) (float64, error) { switch v := price.(type) { case string: - // Remove currency symbols cleanPrice := strings.TrimSpace(v) cleanPrice = strings.TrimPrefix(cleanPrice, "$") - cleanPrice = strings.TrimSuffix(cleanPrice, " USD") - cleanPrice = strings.TrimSuffix(cleanPrice, " USDC") cleanPrice = strings.TrimSpace(cleanPrice) // Parse as float @@ -176,6 +183,23 @@ func (s *ExactEvmScheme) defaultMoneyConversion(amount float64, network x402.Net return x402.AssetAmount{}, err } + if config.DefaultAsset.Address == "" { + return x402.AssetAmount{}, fmt.Errorf("no default stablecoin configured for network %s; use RegisterMoneyParser or specify an explicit AssetAmount", networkStr) + } + + // EIP-3009 tokens always need name/version for their transferWithAuthorization domain. + // Permit2 tokens only need them if the token supports EIP-2612 (for gasless permit signing). + // Omitting name/version for permit2 tokens signals the client to skip EIP-2612 and use ERC-20 approval gas sponsoring instead. + extra := map[string]interface{}{} + includeEip712Domain := config.DefaultAsset.AssetTransferMethod == "" || config.DefaultAsset.SupportsEip2612 + if includeEip712Domain { + extra["name"] = config.DefaultAsset.Name + extra["version"] = config.DefaultAsset.Version + } + if config.DefaultAsset.AssetTransferMethod != "" { + extra["assetTransferMethod"] = string(config.DefaultAsset.AssetTransferMethod) + } + // Check if amount appears to already be in smallest unit // (e.g., 1500000 for $1.50 USDC is likely already in smallest unit, not $1.5M) oneUnit := float64(1) @@ -188,7 +212,7 @@ func (s *ExactEvmScheme) defaultMoneyConversion(amount float64, network x402.Net return x402.AssetAmount{ Asset: config.DefaultAsset.Address, Amount: fmt.Sprintf("%.0f", amount), - Extra: make(map[string]interface{}), + Extra: extra, }, nil } @@ -202,7 +226,7 @@ func (s *ExactEvmScheme) defaultMoneyConversion(amount float64, network x402.Net return x402.AssetAmount{ Asset: config.DefaultAsset.Address, Amount: parsedAmount.String(), - Extra: make(map[string]interface{}), + Extra: extra, }, nil } @@ -247,13 +271,15 @@ func (s *ExactEvmScheme) EnhancePaymentRequirements( requirements.Extra = make(map[string]interface{}) } - // Add token name and version for EIP-712 signing - // ONLY add if not already present (client may have specified exact values) - if _, ok := requirements.Extra["name"]; !ok { - requirements.Extra["name"] = assetInfo.Name - } - if _, ok := requirements.Extra["version"]; !ok { - requirements.Extra["version"] = assetInfo.Version + // EIP-3009 tokens always need name/version; permit2 tokens only if they support EIP-2612 + includeEip712Domain := assetInfo.AssetTransferMethod == "" || assetInfo.SupportsEip2612 + if includeEip712Domain { + if _, ok := requirements.Extra["name"]; !ok { + requirements.Extra["name"] = assetInfo.Name + } + if _, ok := requirements.Extra["version"]; !ok { + requirements.Extra["version"] = assetInfo.Version + } } // Copy extensions from supportedKind if provided diff --git a/go/mechanisms/evm/exact/server/server_money_parser_test.go b/go/mechanisms/evm/exact/server/server_money_parser_test.go index bf8c2e46c1..c429533d52 100644 --- a/go/mechanisms/evm/exact/server/server_money_parser_test.go +++ b/go/mechanisms/evm/exact/server/server_money_parser_test.go @@ -200,8 +200,6 @@ func TestRegisterMoneyParser_StringPrices(t *testing.T) { }{ {"Dollar format", "$100", "0xDAI"}, // > 50, uses DAI {"Plain decimal", "25.50", baseMainnetUSDC}, // <= 50, uses USDC - {"With USD suffix", "75 USD", "0xDAI"}, - {"With USDC suffix", "10 USDC", baseMainnetUSDC}, } for _, tt := range tests { diff --git a/go/mechanisms/evm/exact/v1/facilitator/errors.go b/go/mechanisms/evm/exact/v1/facilitator/errors.go index 628666a585..2c2297b4a7 100644 --- a/go/mechanisms/evm/exact/v1/facilitator/errors.go +++ b/go/mechanisms/evm/exact/v1/facilitator/errors.go @@ -13,7 +13,7 @@ const ( ErrRecipientMismatch = "invalid_exact_evm_payload_recipient_mismatch" ErrInvalidAuthorizationValue = "invalid_exact_evm_payload_authorization_value" ErrInvalidRequiredAmount = "invalid_exact_evm_required_amount" - ErrAuthorizationValueInsufficient = "invalid_exact_evm_payload_authorization_value_insufficient" + ErrAuthorizationValueMismatch = "invalid_exact_evm_payload_authorization_value_mismatch" ErrAuthorizationValidBeforeExpired = "invalid_exact_evm_payload_authorization_valid_before" ErrAuthorizationValidAfterInFuture = "invalid_exact_evm_payload_authorization_valid_after" ErrInsufficientFunds = "invalid_exact_evm_insufficient_funds" diff --git a/go/mechanisms/evm/exact/v1/facilitator/scheme.go b/go/mechanisms/evm/exact/v1/facilitator/scheme.go index 825f4a273f..ebd1c7d2d1 100644 --- a/go/mechanisms/evm/exact/v1/facilitator/scheme.go +++ b/go/mechanisms/evm/exact/v1/facilitator/scheme.go @@ -13,6 +13,7 @@ import ( x402 "github.com/coinbase/x402/go" "github.com/coinbase/x402/go/mechanisms/evm" + exactfacilitator "github.com/coinbase/x402/go/mechanisms/evm/exact/facilitator" evmv1 "github.com/coinbase/x402/go/mechanisms/evm/v1" "github.com/coinbase/x402/go/types" ) @@ -22,6 +23,8 @@ type ExactEvmSchemeV1Config struct { // DeployERC4337WithEIP6492 enables automatic deployment of ERC-4337 smart wallets // via EIP-6492 when encountering undeployed contract signatures during settlement DeployERC4337WithEIP6492 bool + // SimulateInSettle reruns transfer simulation during settle. Verify always simulates. + SimulateInSettle bool } // ExactEvmSchemeV1 implements the SchemeNetworkFacilitatorV1 interface for EVM exact payments (V1) @@ -74,10 +77,20 @@ func (f *ExactEvmSchemeV1) GetSigners(_ x402.Network) []string { // Verify verifies a V1 payment payload against requirements func (f *ExactEvmSchemeV1) Verify( + ctx context.Context, + payload types.PaymentPayloadV1, + requirements types.PaymentRequirementsV1, + fctx *x402.FacilitatorContext, +) (*x402.VerifyResponse, error) { + return f.verify(ctx, payload, requirements, fctx, true) +} + +func (f *ExactEvmSchemeV1) verify( ctx context.Context, payload types.PaymentPayloadV1, requirements types.PaymentRequirementsV1, _ *x402.FacilitatorContext, + simulate bool, ) (*x402.VerifyResponse, error) { // Validate scheme (v1 has scheme at top level) if payload.Scheme != evm.SchemeExact || requirements.Scheme != evm.SchemeExact { @@ -133,10 +146,9 @@ func (f *ExactEvmSchemeV1) Verify( return nil, x402.NewVerifyError(ErrRecipientMismatch, evmPayload.Authorization.From, fmt.Sprintf("recipient mismatch: %s != %s", evmPayload.Authorization.To, requirements.PayTo)) } - // Parse and validate amount - authValue, ok := new(big.Int).SetString(evmPayload.Authorization.Value, 10) - if !ok || evmPayload.Authorization.Value == "" { - return nil, x402.NewVerifyError(ErrInvalidAuthorizationValue, evmPayload.Authorization.From, fmt.Sprintf("invalid value: %s", evmPayload.Authorization.Value)) + parsedAuthorization, err := exactfacilitator.ParseEIP3009Authorization(evmPayload.Authorization) + if err != nil { + return nil, x402.NewVerifyError(ErrInvalidPayload, evmPayload.Authorization.From, err.Error()) } // V1: Use MaxAmountRequired field @@ -147,27 +159,19 @@ func (f *ExactEvmSchemeV1) Verify( return nil, x402.NewVerifyError(ErrInvalidRequiredAmount, evmPayload.Authorization.From, fmt.Sprintf("invalid required amount: %s", amountStr)) } - if authValue.Cmp(requiredValue) < 0 { - return nil, x402.NewVerifyError(ErrAuthorizationValueInsufficient, evmPayload.Authorization.From, fmt.Sprintf("authorization value insufficient: %s < %s", authValue.String(), requiredValue.String())) + if parsedAuthorization.Value.Cmp(requiredValue) != 0 { + return nil, x402.NewVerifyError(ErrAuthorizationValueMismatch, evmPayload.Authorization.From, fmt.Sprintf("authorization value mismatch: %s != %s", parsedAuthorization.Value.String(), requiredValue.String())) } // V1 specific: Check validBefore is in the future (with 6 second buffer for block time) now := time.Now().Unix() - validBefore, _ := new(big.Int).SetString(evmPayload.Authorization.ValidBefore, 10) - if validBefore.Cmp(big.NewInt(now+6)) < 0 { - return nil, x402.NewVerifyError(ErrAuthorizationValidBeforeExpired, evmPayload.Authorization.From, fmt.Sprintf("valid before expired: %s < %s", validBefore.String(), big.NewInt(now+6).String())) + if parsedAuthorization.ValidBefore.Cmp(big.NewInt(now+6)) < 0 { + return nil, x402.NewVerifyError(ErrAuthorizationValidBeforeExpired, evmPayload.Authorization.From, fmt.Sprintf("valid before expired: %s < %s", parsedAuthorization.ValidBefore.String(), big.NewInt(now+6).String())) } // V1 specific: Check validAfter is not in the future - validAfter, _ := new(big.Int).SetString(evmPayload.Authorization.ValidAfter, 10) - if validAfter.Cmp(big.NewInt(now)) > 0 { - return nil, x402.NewVerifyError(ErrAuthorizationValidAfterInFuture, evmPayload.Authorization.From, fmt.Sprintf("valid after in future: %s > %s", validAfter.String(), big.NewInt(now).String())) - } - - // Check balance - balance, err := f.signer.GetBalance(ctx, evmPayload.Authorization.From, tokenAddress) - if err == nil && balance.Cmp(requiredValue) < 0 { - return nil, x402.NewVerifyError(ErrInsufficientFunds, evmPayload.Authorization.From, fmt.Sprintf("insufficient funds: wallet balance %s < %s", balance.String(), requiredValue.String())) + if parsedAuthorization.ValidAfter.Cmp(big.NewInt(now)) > 0 { + return nil, x402.NewVerifyError(ErrAuthorizationValidAfterInFuture, evmPayload.Authorization.From, fmt.Sprintf("valid after in future: %s > %s", parsedAuthorization.ValidAfter.String(), big.NewInt(now).String())) } // Extract token info from requirements (already unmarshaled earlier) @@ -180,8 +184,9 @@ func (f *ExactEvmSchemeV1) Verify( return nil, x402.NewVerifyError(ErrInvalidSignatureFormat, evmPayload.Authorization.From, err.Error()) } - valid, err := f.verifySignature( + classification, err := exactfacilitator.ClassifyEIP3009Signature( ctx, + f.signer, evmPayload.Authorization, signatureBytes, chainID, @@ -193,10 +198,39 @@ func (f *ExactEvmSchemeV1) Verify( return nil, x402.NewVerifyError(ErrFailedToVerifySignature, evmPayload.Authorization.From, err.Error()) } - if !valid { + if !classification.Valid && classification.IsUndeployed && !exactfacilitator.HasEIP6492Deployment(classification.SigData) { + return nil, x402.NewVerifyError(ErrUndeployedSmartWallet, evmPayload.Authorization.From, "") + } + + if !classification.Valid && !classification.IsSmartWallet { return nil, x402.NewVerifyError(ErrInvalidSignature, evmPayload.Authorization.From, "invalid signature") } + if simulate { + simulationSucceeded, err := exactfacilitator.SimulateEIP3009Transfer( + ctx, + f.signer, + tokenAddress, + parsedAuthorization, + classification.SigData, + ) + if err != nil { + return nil, x402.NewVerifyError(ErrInvalidPayload, evmPayload.Authorization.From, err.Error()) + } + if !simulationSucceeded { + reason := exactfacilitator.DiagnoseEIP3009SimulationFailure( + ctx, + f.signer, + tokenAddress, + evmPayload.Authorization, + requiredValue, + tokenName, + tokenVersion, + ) + return nil, x402.NewVerifyError(reason, evmPayload.Authorization.From, reason) + } + } + return &x402.VerifyResponse{ IsValid: true, Payer: evmPayload.Authorization.From, @@ -213,7 +247,7 @@ func (f *ExactEvmSchemeV1) Settle( network := x402.Network(payload.Network) // First verify the payment - verifyResp, err := f.Verify(ctx, payload, requirements, fctx) + verifyResp, err := f.verify(ctx, payload, requirements, fctx, f.config.SimulateInSettle) if err != nil { // Convert VerifyError to SettleError ve := &x402.VerifyError{} @@ -266,60 +300,12 @@ func (f *ExactEvmSchemeV1) Settle( } } - // Use inner signature for settlement - signatureBytes = sigData.InnerSignature - - // Parse values - value, _ := new(big.Int).SetString(evmPayload.Authorization.Value, 10) - validAfter, _ := new(big.Int).SetString(evmPayload.Authorization.ValidAfter, 10) - validBefore, _ := new(big.Int).SetString(evmPayload.Authorization.ValidBefore, 10) - nonceBytes, _ := evm.HexToBytes(evmPayload.Authorization.Nonce) - - // Determine signature type: ECDSA (65 bytes) or smart wallet (longer) - isECDSA := len(signatureBytes) == 65 - - var txHash string - if isECDSA { - // For EOA wallets, use v,r,s overload - r := signatureBytes[0:32] - s := signatureBytes[32:64] - v := signatureBytes[64] - if v == 0 || v == 1 { - v += 27 - } - - txHash, err = f.signer.WriteContract( - ctx, - tokenAddress, - evm.TransferWithAuthorizationVRSABI, - evm.FunctionTransferWithAuthorization, - common.HexToAddress(evmPayload.Authorization.From), - common.HexToAddress(evmPayload.Authorization.To), - value, - validAfter, - validBefore, - [32]byte(nonceBytes), - v, - [32]byte(r), - [32]byte(s), - ) - } else { - // For smart wallets, use bytes signature overload - txHash, err = f.signer.WriteContract( - ctx, - tokenAddress, - evm.TransferWithAuthorizationBytesABI, - evm.FunctionTransferWithAuthorization, - common.HexToAddress(evmPayload.Authorization.From), - common.HexToAddress(evmPayload.Authorization.To), - value, - validAfter, - validBefore, - [32]byte(nonceBytes), - signatureBytes, - ) + parsedAuthorization, err := exactfacilitator.ParseEIP3009Authorization(evmPayload.Authorization) + if err != nil { + return nil, x402.NewSettleError(ErrInvalidPayload, verifyResp.Payer, network, "", err.Error()) } + txHash, err := exactfacilitator.ExecuteTransferWithAuthorization(ctx, f.signer, tokenAddress, parsedAuthorization, sigData) if err != nil { return nil, x402.NewSettleError(ErrTransactionFailed, verifyResp.Payer, network, "", err.Error()) } @@ -342,62 +328,6 @@ func (f *ExactEvmSchemeV1) Settle( }, nil } -// verifySignature verifies the EIP-712 signature -func (f *ExactEvmSchemeV1) verifySignature( - ctx context.Context, - authorization evm.ExactEIP3009Authorization, - signature []byte, - chainID *big.Int, - verifyingContract string, - tokenName string, - tokenVersion string, -) (bool, error) { - // Hash the EIP-712 typed data - hash, err := evm.HashEIP3009Authorization( - authorization, - chainID, - verifyingContract, - tokenName, - tokenVersion, - ) - if err != nil { - return false, err - } - - // Convert hash to [32]byte - var hash32 [32]byte - copy(hash32[:], hash) - - // Use universal verification (supports EOA, EIP-1271, and ERC-6492) - valid, sigData, err := evm.VerifyUniversalSignature( - ctx, - f.signer, - authorization.From, - hash32, - signature, - true, // allowUndeployed in verify() - ) - - if err != nil { - return false, err - } - - // If undeployed wallet with deployment info, it will be deployed in settle() - if sigData != nil { - zeroFactory := [20]byte{} - if sigData.Factory != zeroFactory { - _, err := f.signer.GetCode(ctx, authorization.From) - if err != nil { - return false, err - } - // Wallet may not be deployed - this is OK in verify() if has deployment info - // Actual deployment happens in settle() if configured - } - } - - return valid, nil -} - // deploySmartWallet deploys an ERC-4337 smart wallet using the ERC-6492 factory // // This function sends the pre-encoded factory calldata directly as a transaction. diff --git a/go/mechanisms/evm/multicall.go b/go/mechanisms/evm/multicall.go new file mode 100644 index 0000000000..c22141204b --- /dev/null +++ b/go/mechanisms/evm/multicall.go @@ -0,0 +1,202 @@ +package evm + +import ( + "context" + "fmt" + "reflect" + "strings" + + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/common" +) + +// MulticallCall describes one batched call. +// Use CallData for pre-encoded raw calls, or ABI/FunctionName/Args for typed calls. +type MulticallCall struct { + Address string + ABI []byte + FunctionName string + Args []interface{} + CallData []byte +} + +// MulticallResult is the decoded outcome of one batched call. +type MulticallResult struct { + Status string + Result interface{} + Error error +} + +type multicallTryAggregateCall struct { + Target common.Address + CallData []byte +} + +type multicallTryAggregateResult struct { + Success bool + ReturnData []byte +} + +// Success reports whether the call completed successfully. +func (r MulticallResult) Success() bool { + return r.Status == "success" +} + +// Multicall batches typed and raw eth_call requests via Multicall3. +func Multicall( + ctx context.Context, + signer FacilitatorEvmSigner, + calls []MulticallCall, +) ([]MulticallResult, error) { + if len(calls) == 0 { + return nil, nil + } + + aggregateCalls := make([]multicallTryAggregateCall, 0, len(calls)) + for _, call := range calls { + callData, err := multicallCallData(call) + if err != nil { + return nil, err + } + + aggregateCalls = append(aggregateCalls, multicallTryAggregateCall{ + Target: common.HexToAddress(call.Address), + CallData: callData, + }) + } + + rawResults, err := signer.ReadContract( + ctx, + MULTICALL3Address, + Multicall3TryAggregateABI, + FunctionTryAggregate, + false, + aggregateCalls, + ) + if err != nil { + return nil, err + } + + decodedResults, err := normalizeMulticallResults(rawResults) + if err != nil { + return nil, err + } + if len(decodedResults) != len(calls) { + return nil, fmt.Errorf("multicall result length mismatch: got %d, want %d", len(decodedResults), len(calls)) + } + + results := make([]MulticallResult, 0, len(calls)) + for i, raw := range decodedResults { + if !raw.Success { + results = append(results, MulticallResult{ + Status: "failure", + Error: fmt.Errorf("multicall: call reverted"), + }) + continue + } + + call := calls[i] + if len(call.CallData) > 0 { + results = append(results, MulticallResult{Status: "success"}) + continue + } + + decoded, err := decodeMulticallResult(call, raw.ReturnData) + if err != nil { + results = append(results, MulticallResult{ + Status: "failure", + Error: err, + }) + continue + } + + results = append(results, MulticallResult{ + Status: "success", + Result: decoded, + }) + } + + return results, nil +} + +func multicallCallData(call MulticallCall) ([]byte, error) { + if len(call.CallData) > 0 { + return call.CallData, nil + } + + if len(call.ABI) == 0 || call.FunctionName == "" { + return nil, fmt.Errorf("multicall typed call requires ABI and function name") + } + + contractABI, err := abi.JSON(strings.NewReader(string(call.ABI))) + if err != nil { + return nil, fmt.Errorf("failed to parse ABI for multicall: %w", err) + } + + data, err := contractABI.Pack(call.FunctionName, call.Args...) + if err != nil { + return nil, fmt.Errorf("failed to pack multicall input for %s: %w", call.FunctionName, err) + } + + return data, nil +} + +func decodeMulticallResult(call MulticallCall, returnData []byte) (interface{}, error) { + contractABI, err := abi.JSON(strings.NewReader(string(call.ABI))) + if err != nil { + return nil, fmt.Errorf("failed to parse ABI for multicall decode: %w", err) + } + + outputs, err := contractABI.Unpack(call.FunctionName, returnData) + if err != nil { + return nil, fmt.Errorf("failed to decode multicall result for %s: %w", call.FunctionName, err) + } + + if len(outputs) == 0 { + return nil, nil + } + if len(outputs) == 1 { + return outputs[0], nil + } + + return outputs, nil +} + +func normalizeMulticallResults(raw interface{}) ([]multicallTryAggregateResult, error) { + value := reflect.ValueOf(raw) + if !value.IsValid() { + return nil, fmt.Errorf("multicall returned nil results") + } + if value.Kind() != reflect.Slice { + return nil, fmt.Errorf("multicall returned %T, want slice", raw) + } + + results := make([]multicallTryAggregateResult, 0, value.Len()) + for i := 0; i < value.Len(); i++ { + entry := value.Index(i) + if entry.Kind() == reflect.Interface || entry.Kind() == reflect.Pointer { + entry = entry.Elem() + } + if !entry.IsValid() || entry.Kind() != reflect.Struct { + return nil, fmt.Errorf("multicall entry %d has unexpected type %s", i, entry.Kind()) + } + + successField := entry.FieldByName("Success") + returnDataField := entry.FieldByName("ReturnData") + if !successField.IsValid() || !returnDataField.IsValid() { + return nil, fmt.Errorf("multicall entry %d missing expected fields", i) + } + + returnData, ok := returnDataField.Interface().([]byte) + if !ok { + return nil, fmt.Errorf("multicall entry %d returnData has unexpected type %T", i, returnDataField.Interface()) + } + + results = append(results, multicallTryAggregateResult{ + Success: successField.Bool(), + ReturnData: returnData, + }) + } + + return results, nil +} diff --git a/go/mechanisms/evm/permit2_errors.go b/go/mechanisms/evm/permit2_errors.go new file mode 100644 index 0000000000..5073212770 --- /dev/null +++ b/go/mechanisms/evm/permit2_errors.go @@ -0,0 +1,33 @@ +package evm + +// Shared Permit2 error constants used by both the exact and upto facilitators. +// Both schemes write these strings to JSON responses and facilitate cross-SDK parity, +// so the values must never change without a coordinated update across all SDKs. +const ( + // Verification errors + ErrPermit2InvalidSpender = "invalid_permit2_spender" + ErrPermit2RecipientMismatch = "invalid_permit2_recipient_mismatch" + ErrPermit2DeadlineExpired = "permit2_deadline_expired" + ErrPermit2NotYetValid = "permit2_not_yet_valid" + ErrPermit2AmountMismatch = "permit2_amount_mismatch" + ErrPermit2TokenMismatch = "permit2_token_mismatch" + ErrPermit2InvalidSignature = "invalid_permit2_signature" + ErrPermit2AllowanceRequired = "permit2_allowance_required" + + // Settle errors (from contract reverts) + ErrPermit2InvalidAmount = "permit2_invalid_amount" + ErrPermit2InvalidDestination = "permit2_invalid_destination" + ErrPermit2InvalidOwner = "permit2_invalid_owner" + ErrPermit2PaymentTooEarly = "permit2_payment_too_early" + ErrPermit2InvalidNonce = "permit2_invalid_nonce" + ErrPermit2612AmountMismatch = "permit2_2612_amount_mismatch" + + // Simulation errors + ErrPermit2SimulationFailed = "permit2_simulation_failed" + ErrPermit2InsufficientBalance = "permit2_insufficient_balance" + ErrPermit2ProxyNotDeployed = "permit2_proxy_not_deployed" + + // ERC-20 approval gas-sponsoring errors + ErrErc20ApprovalInsufficientEth = "erc20_approval_insufficient_eth_for_gas" + ErrErc20ApprovalBroadcastFailed = "erc20_approval_broadcast_failed" +) diff --git a/go/mechanisms/evm/rpc.go b/go/mechanisms/evm/rpc.go new file mode 100644 index 0000000000..7f119eecea --- /dev/null +++ b/go/mechanisms/evm/rpc.go @@ -0,0 +1,279 @@ +package evm + +import ( + "context" + "fmt" + "math/big" + "strings" + "sync" + + "github.com/ethereum/go-ethereum" + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/common" + goethtypes "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/ethclient" +) + +// RPCChainConfig configures RPC behavior for a specific chain. +type RPCChainConfig struct { + RPCURL string +} + +// RPCConfig configures RPC behavior for EVM clients that need on-chain reads or fee estimation. +// Chain-specific entries in RPCByChainID take precedence over the top-level RPCURL. +type RPCConfig struct { + RPCURL string + RPCByChainID map[int64]RPCChainConfig +} + +// ResolveRPCURL returns the appropriate RPC URL for the given network. +func ResolveRPCURL(config *RPCConfig, network string) string { + if config == nil { + return "" + } + if len(config.RPCByChainID) > 0 { + chainID, err := GetEvmChainId(network) + if err == nil { + if chainConfig, ok := config.RPCByChainID[chainID.Int64()]; ok && chainConfig.RPCURL != "" { + return chainConfig.RPCURL + } + } + } + return config.RPCURL +} + +// rpcClientCache is a process-wide cache of ethclient.Client instances keyed by RPC URL. +var rpcClientCache sync.Map // map[string]*ethclient.Client + +func getOrCreateRPCClient(ctx context.Context, rpcURL string) (*ethclient.Client, error) { + if existing, ok := rpcClientCache.Load(rpcURL); ok { + if cachedClient, ok := existing.(*ethclient.Client); ok { + return cachedClient, nil + } + } + client, err := ethclient.DialContext(ctx, rpcURL) + if err != nil { + return nil, err + } + actual, loaded := rpcClientCache.LoadOrStore(rpcURL, client) + if loaded { + // Another goroutine stored first; close our duplicate connection. + client.Close() + return actual.(*ethclient.Client), nil + } + return client, nil +} + +type rpcCapabilities struct { + client *ethclient.Client +} + +func newRPCCapabilities(ctx context.Context, rpcURL string) (*rpcCapabilities, error) { + client, err := getOrCreateRPCClient(ctx, rpcURL) + if err != nil { + return nil, err + } + return &rpcCapabilities{client: client}, nil +} + +func (r *rpcCapabilities) ReadContract( + ctx context.Context, + contractAddress string, + abiBytes []byte, + functionName string, + args ...interface{}, +) (interface{}, error) { + contractABI, err := abi.JSON(strings.NewReader(string(abiBytes))) + if err != nil { + return nil, fmt.Errorf("failed to parse ABI: %w", err) + } + + data, err := contractABI.Pack(functionName, args...) + if err != nil { + return nil, fmt.Errorf("failed to pack method call: %w", err) + } + + addr := common.HexToAddress(contractAddress) + msg := ethereum.CallMsg{ + To: &addr, + Data: data, + } + + result, err := r.client.CallContract(ctx, msg, nil) + if err != nil { + return nil, fmt.Errorf("contract call failed: %w", err) + } + + outputs, err := contractABI.Unpack(functionName, result) + if err != nil { + return nil, fmt.Errorf("failed to unpack result: %w", err) + } + if len(outputs) == 0 { + return nil, nil + } + if len(outputs) == 1 { + return outputs[0], nil + } + return outputs, nil +} + +func (r *rpcCapabilities) GetTransactionCount(ctx context.Context, address string) (uint64, error) { + nonce, err := r.client.PendingNonceAt(ctx, common.HexToAddress(address)) + if err != nil { + return 0, fmt.Errorf("failed to get pending nonce: %w", err) + } + return nonce, nil +} + +func (r *rpcCapabilities) EstimateFeesPerGas(ctx context.Context) (*big.Int, *big.Int, error) { + gwei := big.NewInt(1_000_000_000) + fallbackMax := new(big.Int).Mul(big.NewInt(1), gwei) + fallbackTip := new(big.Int).Div(gwei, big.NewInt(10)) + + tip, err := r.client.SuggestGasTipCap(ctx) + if err != nil { + return fallbackMax, fallbackTip, err + } + + header, err := r.client.HeaderByNumber(ctx, nil) + if err != nil { + maxFee := new(big.Int).Add(tip, gwei) + return maxFee, tip, err + } + + baseFee := header.BaseFee + if baseFee == nil { + baseFee = gwei + } + maxFee := new(big.Int).Add(new(big.Int).Mul(big.NewInt(2), baseFee), tip) + return maxFee, tip, nil +} + +// resolvedReadSigner wraps a base signer with an RPC-backed ReadContract implementation. +type resolvedReadSigner struct { + base ClientEvmSigner + reader func(ctx context.Context, address string, abi []byte, functionName string, args ...interface{}) (interface{}, error) +} + +func (s *resolvedReadSigner) Address() string { return s.base.Address() } + +func (s *resolvedReadSigner) SignTypedData( + ctx context.Context, + domain TypedDataDomain, + types map[string][]TypedDataField, + primaryType string, + message map[string]interface{}, +) ([]byte, error) { + return s.base.SignTypedData(ctx, domain, types, primaryType, message) +} + +func (s *resolvedReadSigner) ReadContract( + ctx context.Context, + address string, + abiBytes []byte, + functionName string, + args ...interface{}, +) (interface{}, error) { + return s.reader(ctx, address, abiBytes, functionName, args...) +} + +// resolvedTxSigner wraps a base signer with RPC-backed nonce and fee estimation. +type resolvedTxSigner struct { + base ClientEvmSigner + signTx func(ctx context.Context, tx *goethtypes.Transaction) ([]byte, error) + getNonce func(ctx context.Context, address string) (uint64, error) + estimateFees func(ctx context.Context) (maxFeePerGas, maxPriorityFeePerGas *big.Int, err error) +} + +func (s *resolvedTxSigner) Address() string { return s.base.Address() } + +func (s *resolvedTxSigner) SignTypedData( + ctx context.Context, + domain TypedDataDomain, + types map[string][]TypedDataField, + primaryType string, + message map[string]interface{}, +) ([]byte, error) { + return s.base.SignTypedData(ctx, domain, types, primaryType, message) +} + +func (s *resolvedTxSigner) SignTransaction(ctx context.Context, tx *goethtypes.Transaction) ([]byte, error) { + return s.signTx(ctx, tx) +} + +func (s *resolvedTxSigner) GetTransactionCount(ctx context.Context, address string) (uint64, error) { + return s.getNonce(ctx, address) +} + +func (s *resolvedTxSigner) EstimateFeesPerGas(ctx context.Context) (*big.Int, *big.Int, error) { + return s.estimateFees(ctx) +} + +// ResolveReadSigner returns a ClientEvmSignerWithReadContract. If the signer already +// implements ReadContract, it is returned as-is. Otherwise an RPC-backed wrapper is +// created using rpcURL; if rpcURL is empty, nil is returned without error. +func ResolveReadSigner( + ctx context.Context, + signer ClientEvmSigner, + rpcURL string, +) (ClientEvmSignerWithReadContract, error) { + if signerWithRead, ok := signer.(ClientEvmSignerWithReadContract); ok { + return signerWithRead, nil + } + if rpcURL == "" { + return nil, nil + } + rpcCaps, err := newRPCCapabilities(ctx, rpcURL) + if err != nil { + return nil, err + } + return &resolvedReadSigner{base: signer, reader: rpcCaps.ReadContract}, nil +} + +// ResolveTxSigner returns a ClientEvmSignerWithTxSigning. Nonce and fee-estimation +// capabilities are taken from the signer when available, and fall back to the provided +// rpcURL. Returns nil without error when the signer cannot sign transactions or when +// RPC capabilities are needed but rpcURL is empty. +func ResolveTxSigner( + ctx context.Context, + signer ClientEvmSigner, + rpcURL string, +) (ClientEvmSignerWithTxSigning, error) { + signSigner, ok := signer.(ClientEvmSignerWithSignTransaction) + if !ok { + return nil, nil + } + + var getNonceFn func(ctx context.Context, address string) (uint64, error) + if nonceSigner, hasNonce := signer.(ClientEvmSignerWithGetTransactionCount); hasNonce { + getNonceFn = nonceSigner.GetTransactionCount + } + + var estimateFeesFn func(ctx context.Context) (maxFeePerGas, maxPriorityFeePerGas *big.Int, err error) + if feeSigner, hasFees := signer.(ClientEvmSignerWithEstimateFeesPerGas); hasFees { + estimateFeesFn = feeSigner.EstimateFeesPerGas + } + + if getNonceFn == nil || estimateFeesFn == nil { + if rpcURL == "" { + return nil, nil + } + rpcCaps, err := newRPCCapabilities(ctx, rpcURL) + if err != nil { + return nil, err + } + if getNonceFn == nil { + getNonceFn = rpcCaps.GetTransactionCount + } + if estimateFeesFn == nil { + estimateFeesFn = rpcCaps.EstimateFeesPerGas + } + } + + return &resolvedTxSigner{ + base: signer, + signTx: signSigner.SignTransaction, + getNonce: getNonceFn, + estimateFees: estimateFeesFn, + }, nil +} diff --git a/go/mechanisms/evm/types.go b/go/mechanisms/evm/types.go index 1de1a713b4..dc1540d7ea 100644 --- a/go/mechanisms/evm/types.go +++ b/go/mechanisms/evm/types.go @@ -189,32 +189,55 @@ func IsEIP3009Payload(data map[string]interface{}) bool { // Required for the ERC-20 approval gas sponsoring extension, where the client signs // (but does not broadcast) an approve(Permit2, MaxUint256) transaction. type ClientEvmSignerWithTxSigning interface { + ClientEvmSignerWithSignTransaction + ClientEvmSignerWithGetTransactionCount + ClientEvmSignerWithEstimateFeesPerGas +} + +// ClientEvmSignerWithSignTransaction extends ClientEvmSigner with raw tx signing. +type ClientEvmSignerWithSignTransaction interface { ClientEvmSigner // SignTransaction signs an EIP-1559 transaction and returns the RLP-encoded bytes. SignTransaction(ctx context.Context, tx *goethtypes.Transaction) ([]byte, error) +} + +// ClientEvmSignerWithGetTransactionCount extends ClientEvmSigner with nonce lookup. +type ClientEvmSignerWithGetTransactionCount interface { + ClientEvmSigner // GetTransactionCount returns the pending nonce for an address. GetTransactionCount(ctx context.Context, address string) (uint64, error) +} + +// ClientEvmSignerWithEstimateFeesPerGas extends ClientEvmSigner with fee estimation. +type ClientEvmSignerWithEstimateFeesPerGas interface { + ClientEvmSigner // EstimateFeesPerGas returns the EIP-1559 maxFeePerGas and maxPriorityFeePerGas. EstimateFeesPerGas(ctx context.Context) (maxFeePerGas, maxPriorityFeePerGas *big.Int, err error) } -// ClientEvmSigner defines the interface for client-side EVM signing operations. +// ClientEvmSignerWithReadContract extends ClientEvmSigner with on-chain read capability. +// Used by extension enrichment paths (EIP-2612 nonce lookup, allowance checks). +type ClientEvmSignerWithReadContract interface { + ClientEvmSigner + + // ReadContract reads data from a smart contract. + ReadContract(ctx context.Context, address string, abi []byte, functionName string, args ...interface{}) (interface{}, error) +} + +// ClientEvmSigner defines the minimal interface for client-side EVM signing operations. // -// Typically created via NewClientSignerFromPrivateKeyWithClient which provides -// both signing and on-chain read capability. The ReadContract method is required -// for EIP-2612 gas sponsoring (querying nonces and checking allowances). +// Base payment signing only requires address + typed-data signing. +// Optional extension flows can use additional capability interfaces like +// ClientEvmSignerWithReadContract and ClientEvmSignerWithTxSigning. type ClientEvmSigner interface { // Address returns the signer's Ethereum address Address() string // SignTypedData signs EIP-712 typed data SignTypedData(ctx context.Context, domain TypedDataDomain, types map[string][]TypedDataField, primaryType string, message map[string]interface{}) ([]byte, error) - - // ReadContract reads data from a smart contract - ReadContract(ctx context.Context, address string, abi []byte, functionName string, args ...interface{}) (interface{}, error) } // FacilitatorEvmSigner defines the interface for facilitator EVM operations @@ -274,10 +297,12 @@ type TransactionReceipt struct { // AssetInfo contains information about an ERC20 token type AssetInfo struct { - Address string - Name string - Version string - Decimals int + Address string + Name string + Version string + Decimals int + AssetTransferMethod AssetTransferMethod + SupportsEip2612 bool } // NetworkConfig contains network-specific configuration @@ -337,6 +362,166 @@ func PayloadFromMap(data map[string]interface{}) (*ExactEIP3009Payload, error) { return payload, nil } +// UptoPermit2Witness represents the witness data for x402UptoPermit2Proxy. +// Differs from Permit2Witness by including a Facilitator address field. +// Only the address matching Facilitator can call settle() on-chain. +type UptoPermit2Witness struct { + To string `json:"to"` // Destination address for funds (hex) + Facilitator string `json:"facilitator"` // Facilitator address authorized to settle (hex) + ValidAfter string `json:"validAfter"` // Unix timestamp (decimal string) +} + +// UptoPermit2Authorization represents the Permit2 authorization parameters for the upto scheme. +type UptoPermit2Authorization struct { + From string `json:"from"` // Signer/owner address (hex) + Permitted Permit2TokenPermissions `json:"permitted"` // Token and amount permitted (max charge) + Spender string `json:"spender"` // Must be x402UptoPermit2Proxy address + Nonce string `json:"nonce"` // uint256 nonce as decimal string + Deadline string `json:"deadline"` // Unix timestamp as decimal string + Witness UptoPermit2Witness `json:"witness"` // Witness data including facilitator +} + +// UptoPermit2Payload represents the upto Permit2 payment payload sent by clients. +type UptoPermit2Payload struct { + Signature string `json:"signature"` // EIP-712 signature (hex) + Permit2Authorization UptoPermit2Authorization `json:"permit2Authorization"` // Authorization parameters +} + +// ToMap converts an UptoPermit2Payload to a map for JSON marshaling. +func (p *UptoPermit2Payload) ToMap() map[string]interface{} { + return map[string]interface{}{ + "signature": p.Signature, + "permit2Authorization": map[string]interface{}{ + "from": p.Permit2Authorization.From, + "permitted": map[string]interface{}{ + "token": p.Permit2Authorization.Permitted.Token, + "amount": p.Permit2Authorization.Permitted.Amount, + }, + "spender": p.Permit2Authorization.Spender, + "nonce": p.Permit2Authorization.Nonce, + "deadline": p.Permit2Authorization.Deadline, + "witness": map[string]interface{}{ + "to": p.Permit2Authorization.Witness.To, + "facilitator": p.Permit2Authorization.Witness.Facilitator, + "validAfter": p.Permit2Authorization.Witness.ValidAfter, + }, + }, + } +} + +// UptoPermit2PayloadFromMap creates an UptoPermit2Payload from a map. +// Returns an error if required fields are missing or malformed. +func UptoPermit2PayloadFromMap(data map[string]interface{}) (*UptoPermit2Payload, error) { + payload := &UptoPermit2Payload{} + + if sig, ok := data["signature"].(string); ok { + payload.Signature = sig + } + + auth, ok := data["permit2Authorization"].(map[string]interface{}) + if !ok { + return nil, fmt.Errorf("missing or invalid permit2Authorization field") + } + + if from, ok := auth["from"].(string); ok { + payload.Permit2Authorization.From = from + } else { + return nil, fmt.Errorf("missing or invalid permit2Authorization.from field") + } + + if spender, ok := auth["spender"].(string); ok { + payload.Permit2Authorization.Spender = spender + } else { + return nil, fmt.Errorf("missing or invalid permit2Authorization.spender field") + } + + if nonce, ok := auth["nonce"].(string); ok { + payload.Permit2Authorization.Nonce = nonce + } else { + return nil, fmt.Errorf("missing or invalid permit2Authorization.nonce field") + } + + if deadline, ok := auth["deadline"].(string); ok { + payload.Permit2Authorization.Deadline = deadline + } else { + return nil, fmt.Errorf("missing or invalid permit2Authorization.deadline field") + } + + permitted, ok := auth["permitted"].(map[string]interface{}) + if !ok { + return nil, fmt.Errorf("missing or invalid permit2Authorization.permitted field") + } + + if token, ok := permitted["token"].(string); ok { + payload.Permit2Authorization.Permitted.Token = token + } else { + return nil, fmt.Errorf("missing or invalid permit2Authorization.permitted.token field") + } + + if amount, ok := permitted["amount"].(string); ok { + payload.Permit2Authorization.Permitted.Amount = amount + } else { + return nil, fmt.Errorf("missing or invalid permit2Authorization.permitted.amount field") + } + + witness, ok := auth["witness"].(map[string]interface{}) + if !ok { + return nil, fmt.Errorf("missing or invalid permit2Authorization.witness field") + } + + if to, ok := witness["to"].(string); ok { + payload.Permit2Authorization.Witness.To = to + } else { + return nil, fmt.Errorf("missing or invalid permit2Authorization.witness.to field") + } + + if facilitator, ok := witness["facilitator"].(string); ok { + payload.Permit2Authorization.Witness.Facilitator = facilitator + } else { + return nil, fmt.Errorf("missing or invalid permit2Authorization.witness.facilitator field") + } + + if validAfter, ok := witness["validAfter"].(string); ok { + payload.Permit2Authorization.Witness.ValidAfter = validAfter + } else { + return nil, fmt.Errorf("missing or invalid permit2Authorization.witness.validAfter field") + } + + return payload, nil +} + +// IsUptoPermit2Payload checks if a payload map is an upto Permit2 payload. +// Validates structural presence of all required fields including witness.facilitator. +func IsUptoPermit2Payload(data map[string]interface{}) bool { + if _, ok := data["signature"].(string); !ok { + return false + } + auth, ok := data["permit2Authorization"].(map[string]interface{}) + if !ok { + return false + } + if _, ok := auth["from"].(string); !ok { + return false + } + if _, ok := auth["spender"].(string); !ok { + return false + } + witness, ok := auth["witness"].(map[string]interface{}) + if !ok { + return false + } + if _, ok := witness["facilitator"].(string); !ok { + return false + } + if _, ok := witness["to"].(string); !ok { + return false + } + if _, ok := witness["validAfter"].(string); !ok { + return false + } + return true +} + // ERC6492SignatureData represents the parsed components of an ERC-6492 signature // ERC-6492 allows signatures from undeployed smart contract accounts by wrapping // the signature with deployment information (factory address and calldata) diff --git a/go/mechanisms/evm/upto/client/errors.go b/go/mechanisms/evm/upto/client/errors.go new file mode 100644 index 0000000000..fb3acd162d --- /dev/null +++ b/go/mechanisms/evm/upto/client/errors.go @@ -0,0 +1,7 @@ +package client + +const ( + ErrInvalidAmount = "invalid_upto_evm_client_amount" + ErrFailedToSignPermit2Authorization = "invalid_upto_evm_client_failed_to_sign_permit2_authorization" + ErrMissingFacilitatorAddress = "invalid_upto_evm_client_missing_facilitator_address" +) diff --git a/go/mechanisms/evm/upto/client/permit2.go b/go/mechanisms/evm/upto/client/permit2.go new file mode 100644 index 0000000000..bbb8ff75cc --- /dev/null +++ b/go/mechanisms/evm/upto/client/permit2.go @@ -0,0 +1,124 @@ +package client + +import ( + "context" + "fmt" + "math/big" + "time" + + "github.com/coinbase/x402/go/mechanisms/evm" + "github.com/coinbase/x402/go/types" +) + +// CreateUptoPermit2Payload creates a Permit2 payload using the x402UptoPermit2Proxy witness pattern. +// The upto witness includes a facilitator address, which must be provided in requirements.Extra. +func CreateUptoPermit2Payload( + ctx context.Context, + signer evm.ClientEvmSigner, + requirements types.PaymentRequirements, +) (types.PaymentPayload, error) { + facilitatorAddress, _ := requirements.Extra["facilitatorAddress"].(string) + if facilitatorAddress == "" { + return types.PaymentPayload{}, fmt.Errorf( + "%s: upto scheme requires facilitatorAddress in paymentRequirements.extra; "+ + "ensure the server is configured with an upto facilitator that provides getExtra()", + ErrMissingFacilitatorAddress, + ) + } + + networkStr := string(requirements.Network) + + chainID, err := evm.GetEvmChainId(networkStr) + if err != nil { + return types.PaymentPayload{}, err + } + + nonce, err := evm.CreatePermit2Nonce() + if err != nil { + return types.PaymentPayload{}, err + } + + now := time.Now().Unix() + validAfter := fmt.Sprintf("%d", now-600) + deadline := fmt.Sprintf("%d", now+int64(requirements.MaxTimeoutSeconds)) + + tokenAddress := evm.NormalizeAddress(requirements.Asset) + payTo := evm.NormalizeAddress(requirements.PayTo) + normalizedFacilitator := evm.NormalizeAddress(facilitatorAddress) + + authorization := evm.UptoPermit2Authorization{ + From: signer.Address(), + Permitted: evm.Permit2TokenPermissions{ + Token: tokenAddress, + Amount: requirements.Amount, + }, + Spender: evm.X402UptoPermit2ProxyAddress, + Nonce: nonce, + Deadline: deadline, + Witness: evm.UptoPermit2Witness{ + To: payTo, + Facilitator: normalizedFacilitator, + ValidAfter: validAfter, + }, + } + + signature, err := signUptoPermit2Authorization(ctx, signer, authorization, chainID) + if err != nil { + return types.PaymentPayload{}, fmt.Errorf(ErrFailedToSignPermit2Authorization+": %w", err) + } + + uptoPayload := &evm.UptoPermit2Payload{ + Signature: evm.BytesToHex(signature), + Permit2Authorization: authorization, + } + + return types.PaymentPayload{ + X402Version: 2, + Payload: uptoPayload.ToMap(), + }, nil +} + +func signUptoPermit2Authorization( + ctx context.Context, + signer evm.ClientEvmSigner, + authorization evm.UptoPermit2Authorization, + chainID *big.Int, +) ([]byte, error) { + domain := evm.TypedDataDomain{ + Name: "Permit2", + ChainID: chainID, + VerifyingContract: evm.PERMIT2Address, + } + + types := evm.GetUptoPermit2EIP712Types() + + amount, ok := new(big.Int).SetString(authorization.Permitted.Amount, 10) + if !ok { + return nil, fmt.Errorf("invalid permitted amount: %s", authorization.Permitted.Amount) + } + nonce, ok := new(big.Int).SetString(authorization.Nonce, 10) + if !ok { + return nil, fmt.Errorf("invalid nonce: %s", authorization.Nonce) + } + deadline, ok := new(big.Int).SetString(authorization.Deadline, 10) + if !ok { + return nil, fmt.Errorf("invalid deadline: %s", authorization.Deadline) + } + validAfter, ok := new(big.Int).SetString(authorization.Witness.ValidAfter, 10) + if !ok { + return nil, fmt.Errorf("invalid validAfter: %s", authorization.Witness.ValidAfter) + } + + message := map[string]interface{}{ + "permitted": map[string]interface{}{ + "token": authorization.Permitted.Token, + "amount": amount, + }, + "spender": authorization.Spender, + "nonce": nonce, + "deadline": deadline, + "witness": evm.BuildUptoPermit2WitnessMap(authorization.Witness.To, authorization.Witness.Facilitator, validAfter), + } + + return signer.SignTypedData(ctx, domain, types, "PermitWitnessTransferFrom", message) +} diff --git a/go/mechanisms/evm/upto/client/rpc.go b/go/mechanisms/evm/upto/client/rpc.go new file mode 100644 index 0000000000..55105b3c67 --- /dev/null +++ b/go/mechanisms/evm/upto/client/rpc.go @@ -0,0 +1,32 @@ +package client + +import ( + "context" + + "github.com/coinbase/x402/go/mechanisms/evm" +) + +// UptoEvmChainConfig configures RPC behavior for one chain. +type UptoEvmChainConfig = evm.RPCChainConfig + +// UptoEvmSchemeConfig configures RPC behavior for Upto EVM clients. +// If both RPCByChainID and RPCURL are set, chain-specific entries take precedence. +type UptoEvmSchemeConfig = evm.RPCConfig + +func (c *UptoEvmScheme) resolveRPCURL(network string) string { + return evm.ResolveRPCURL(c.config, network) +} + +func (c *UptoEvmScheme) resolveReadSigner( + ctx context.Context, + network string, +) (evm.ClientEvmSignerWithReadContract, error) { + return evm.ResolveReadSigner(ctx, c.signer, c.resolveRPCURL(network)) +} + +func (c *UptoEvmScheme) resolveTxSigner( + ctx context.Context, + network string, +) (evm.ClientEvmSignerWithTxSigning, error) { + return evm.ResolveTxSigner(ctx, c.signer, c.resolveRPCURL(network)) +} diff --git a/go/mechanisms/evm/upto/client/rpc_test.go b/go/mechanisms/evm/upto/client/rpc_test.go new file mode 100644 index 0000000000..3bec8d9a3c --- /dev/null +++ b/go/mechanisms/evm/upto/client/rpc_test.go @@ -0,0 +1,141 @@ +package client + +import ( + "context" + "math/big" + "testing" + + goethtypes "github.com/ethereum/go-ethereum/core/types" + + "github.com/coinbase/x402/go/mechanisms/evm" +) + +type rpcTestSigner struct { + address string + signCalls int + nonceCalls int + estimateCalls int +} + +func (s *rpcTestSigner) Address() string { + if s.address == "" { + return "0x1234567890123456789012345678901234567890" + } + return s.address +} + +func (s *rpcTestSigner) SignTypedData( + ctx context.Context, + domain evm.TypedDataDomain, + types map[string][]evm.TypedDataField, + primaryType string, + message map[string]interface{}, +) ([]byte, error) { + return []byte{1, 2, 3}, nil +} + +func (s *rpcTestSigner) SignTransaction(ctx context.Context, tx *goethtypes.Transaction) ([]byte, error) { + s.signCalls++ + return []byte{0x01}, nil +} + +func (s *rpcTestSigner) GetTransactionCount(ctx context.Context, address string) (uint64, error) { + s.nonceCalls++ + return 7, nil +} + +func (s *rpcTestSigner) EstimateFeesPerGas(ctx context.Context) (*big.Int, *big.Int, error) { + s.estimateCalls++ + return big.NewInt(100), big.NewInt(10), nil +} + +func TestResolveRPCURL(t *testing.T) { + scheme := &UptoEvmScheme{ + config: &UptoEvmSchemeConfig{ + RPCURL: "https://default.example", + RPCByChainID: map[int64]UptoEvmChainConfig{ + 8453: {RPCURL: "https://base.example"}, + }, + }, + } + + if got := scheme.resolveRPCURL("eip155:8453"); got != "https://base.example" { + t.Fatalf("expected chain-specific rpc, got %q", got) + } + if got := scheme.resolveRPCURL("eip155:137"); got != "https://default.example" { + t.Fatalf("expected default rpc, got %q", got) + } +} + +func TestResolveTxSignerUsesSignerCapabilitiesFirst(t *testing.T) { + ctx := context.Background() + signer := &rpcTestSigner{} + scheme := &UptoEvmScheme{ + signer: signer, + } + + txSigner, err := scheme.resolveTxSigner(ctx, "eip155:8453") + if err != nil { + t.Fatalf("resolveTxSigner failed: %v", err) + } + if txSigner == nil { + t.Fatal("expected tx signer") + } + + _, err = txSigner.SignTransaction(ctx, nil) + if err != nil { + t.Fatalf("SignTransaction failed: %v", err) + } + _, err = txSigner.GetTransactionCount(ctx, signer.Address()) + if err != nil { + t.Fatalf("GetTransactionCount failed: %v", err) + } + _, _, err = txSigner.EstimateFeesPerGas(ctx) + if err != nil { + t.Fatalf("EstimateFeesPerGas failed: %v", err) + } + + if signer.signCalls != 1 || signer.nonceCalls != 1 || signer.estimateCalls != 1 { + t.Fatalf("expected signer methods to be used, got sign=%d nonce=%d fee=%d", signer.signCalls, signer.nonceCalls, signer.estimateCalls) + } +} + +func TestResolveTxSignerReturnsNilWithoutRequiredCapabilities(t *testing.T) { + ctx := context.Background() + scheme := &UptoEvmScheme{ + signer: &mockMinimalClientSigner{}, + } + + txSigner, err := scheme.resolveTxSigner(ctx, "eip155:8453") + if err != nil { + t.Fatalf("resolveTxSigner failed: %v", err) + } + if txSigner != nil { + t.Fatal("expected nil tx signer when signTransaction capability is missing") + } +} + +type mockMinimalClientSigner struct{} + +func (m *mockMinimalClientSigner) Address() string { + return "0x1234567890123456789012345678901234567890" +} + +func (m *mockMinimalClientSigner) SignTypedData( + ctx context.Context, + domain evm.TypedDataDomain, + types map[string][]evm.TypedDataField, + primaryType string, + message map[string]interface{}, +) ([]byte, error) { + return []byte{0x01}, nil +} + +func TestScheme_ReturnsUpto(t *testing.T) { + signer := &mockMinimalClientSigner{} + scheme := NewUptoEvmScheme(signer, nil) + + if scheme.Scheme() != "upto" { + t.Fatalf("expected scheme 'upto', got %q", scheme.Scheme()) + } +} diff --git a/go/mechanisms/evm/upto/client/scheme.go b/go/mechanisms/evm/upto/client/scheme.go new file mode 100644 index 0000000000..dcd865a63e --- /dev/null +++ b/go/mechanisms/evm/upto/client/scheme.go @@ -0,0 +1,199 @@ +package client + +import ( + "context" + "fmt" + "math/big" + "time" + + "github.com/ethereum/go-ethereum/common" + + "github.com/coinbase/x402/go/extensions/eip2612gassponsor" + "github.com/coinbase/x402/go/extensions/erc20approvalgassponsor" + "github.com/coinbase/x402/go/mechanisms/evm" + exactclient "github.com/coinbase/x402/go/mechanisms/evm/exact/client" + "github.com/coinbase/x402/go/types" +) + +// UptoEvmScheme implements SchemeNetworkClient and ExtensionAwareClient for EVM upto payments. +// Always uses Permit2 (no EIP-3009 path). +type UptoEvmScheme struct { + signer evm.ClientEvmSigner + config *UptoEvmSchemeConfig +} + +func NewUptoEvmScheme(signer evm.ClientEvmSigner, config *UptoEvmSchemeConfig) *UptoEvmScheme { + return &UptoEvmScheme{ + signer: signer, + config: config, + } +} + +func (c *UptoEvmScheme) Scheme() string { + return evm.SchemeUpto +} + +// CreatePaymentPayload creates a V2 payment payload for the upto scheme (always Permit2). +func (c *UptoEvmScheme) CreatePaymentPayload( + ctx context.Context, + requirements types.PaymentRequirements, +) (types.PaymentPayload, error) { + return CreateUptoPermit2Payload(ctx, c.signer, requirements) +} + +// CreatePaymentPayloadWithExtensions creates a payment payload with extension support. +func (c *UptoEvmScheme) CreatePaymentPayloadWithExtensions( + ctx context.Context, + requirements types.PaymentRequirements, + extensions map[string]interface{}, +) (types.PaymentPayload, error) { + result, err := CreateUptoPermit2Payload(ctx, c.signer, requirements) + if err != nil { + return types.PaymentPayload{}, err + } + + extData, err := c.trySignEip2612Permit(ctx, requirements, result, extensions) + if extData != nil { + result.Extensions = extData + } else if err == nil { + erc20ExtData, erc20Err := c.trySignErc20Approval(ctx, requirements, extensions) + if erc20Err == nil && erc20ExtData != nil { + result.Extensions = erc20ExtData + } + } + + return result, nil +} + +func (c *UptoEvmScheme) trySignEip2612Permit( + ctx context.Context, + requirements types.PaymentRequirements, + result types.PaymentPayload, + extensions map[string]interface{}, +) (map[string]interface{}, error) { + if extensions == nil { + return nil, nil + } + if _, ok := extensions[eip2612gassponsor.EIP2612GasSponsoring.Key()]; !ok { + return nil, nil + } + + tokenName, _ := requirements.Extra["name"].(string) + tokenVersion, _ := requirements.Extra["version"].(string) + if tokenName == "" || tokenVersion == "" { + return nil, nil + } + + chainID, err := evm.GetEvmChainId(string(requirements.Network)) + if err != nil { + return nil, err + } + + tokenAddress := evm.NormalizeAddress(requirements.Asset) + + readSigner, err := c.resolveReadSigner(ctx, string(requirements.Network)) + if err != nil { + return nil, err + } + if readSigner == nil { + return nil, nil + } + + allowanceResult, err := readSigner.ReadContract( + ctx, + tokenAddress, + evm.ERC20AllowanceABI, + "allowance", + common.HexToAddress(c.signer.Address()), + common.HexToAddress(evm.PERMIT2Address), + ) + if err == nil { + if allowanceBig, ok := allowanceResult.(*big.Int); ok { + requiredAmount, ok := new(big.Int).SetString(requirements.Amount, 10) + if ok && allowanceBig.Cmp(requiredAmount) >= 0 { + return nil, nil + } + } + } + + deadline := "" + if result.Payload != nil { + if auth, ok := result.Payload["permit2Authorization"].(map[string]interface{}); ok { + if d, ok := auth["deadline"].(string); ok { + deadline = d + } + } + } + if deadline == "" { + deadline = fmt.Sprintf("%d", time.Now().Unix()+int64(requirements.MaxTimeoutSeconds)) + } + + info, err := exactclient.SignEip2612Permit(ctx, readSigner, tokenAddress, tokenName, tokenVersion, chainID, deadline, requirements.Amount) + if err != nil { + return nil, err + } + + return map[string]interface{}{ + eip2612gassponsor.EIP2612GasSponsoring.Key(): map[string]interface{}{ + "info": info, + }, + }, nil +} + +func (c *UptoEvmScheme) trySignErc20Approval( + ctx context.Context, + requirements types.PaymentRequirements, + extensions map[string]interface{}, +) (map[string]interface{}, error) { + if extensions == nil { + return nil, nil + } + if _, ok := extensions[erc20approvalgassponsor.ERC20ApprovalGasSponsoring.Key()]; !ok { + return nil, nil + } + + txSigner, err := c.resolveTxSigner(ctx, string(requirements.Network)) + if err != nil { + return nil, err + } + if txSigner == nil { + return nil, nil + } + + chainID, err := evm.GetEvmChainId(string(requirements.Network)) + if err != nil { + return nil, err + } + + tokenAddress := evm.NormalizeAddress(requirements.Asset) + + if readSigner, hasRead := c.signer.(evm.ClientEvmSignerWithReadContract); hasRead { + allowanceResult, err := readSigner.ReadContract( + ctx, + tokenAddress, + evm.ERC20AllowanceABI, + "allowance", + common.HexToAddress(c.signer.Address()), + common.HexToAddress(evm.PERMIT2Address), + ) + if err == nil { + if allowanceBig, ok := allowanceResult.(*big.Int); ok { + requiredAmount, ok := new(big.Int).SetString(requirements.Amount, 10) + if ok && allowanceBig.Cmp(requiredAmount) >= 0 { + return nil, nil + } + } + } + } + + info, err := exactclient.SignErc20ApprovalTransaction(ctx, txSigner, tokenAddress, chainID) + if err != nil { + return nil, err + } + + return map[string]interface{}{ + erc20approvalgassponsor.ERC20ApprovalGasSponsoring.Key(): map[string]interface{}{ + "info": info, + }, + }, nil +} diff --git a/go/mechanisms/evm/upto/facilitator/errors.go b/go/mechanisms/evm/upto/facilitator/errors.go new file mode 100644 index 0000000000..e124d0ec3a --- /dev/null +++ b/go/mechanisms/evm/upto/facilitator/errors.go @@ -0,0 +1,43 @@ +package facilitator + +import "github.com/coinbase/x402/go/mechanisms/evm" + +// Upto-specific error constants. +const ( + ErrUptoInvalidScheme = "invalid_upto_evm_scheme" + ErrUptoNetworkMismatch = "invalid_upto_evm_network_mismatch" + ErrUptoInvalidPayload = "invalid_upto_evm_payload" + ErrUptoSettlementExceedsAmount = "invalid_upto_evm_payload_settlement_exceeds_amount" + ErrUptoAmountExceedsPermitted = "upto_amount_exceeds_permitted" + ErrUptoUnauthorizedFacilitator = "upto_unauthorized_facilitator" + ErrUptoFacilitatorMismatch = "upto_facilitator_mismatch" + ErrUptoVerificationFailed = "invalid_upto_evm_verification_failed" + ErrUptoFailedToGetNetworkConfig = "invalid_upto_evm_failed_to_get_network_config" + ErrUptoFailedToGetReceipt = "invalid_upto_evm_failed_to_get_receipt" + ErrUptoTransactionFailed = "invalid_upto_evm_transaction_failed" + + // Shared Permit2 error constants — canonical values live in evm.ErrPermit2* + ErrPermit2InvalidSpender = evm.ErrPermit2InvalidSpender + ErrPermit2RecipientMismatch = evm.ErrPermit2RecipientMismatch + ErrPermit2DeadlineExpired = evm.ErrPermit2DeadlineExpired + ErrPermit2NotYetValid = evm.ErrPermit2NotYetValid + ErrPermit2AmountMismatch = evm.ErrPermit2AmountMismatch + ErrPermit2TokenMismatch = evm.ErrPermit2TokenMismatch + ErrPermit2InvalidSignature = evm.ErrPermit2InvalidSignature + ErrPermit2InvalidAmount = evm.ErrPermit2InvalidAmount + ErrPermit2InvalidDestination = evm.ErrPermit2InvalidDestination + ErrPermit2InvalidOwner = evm.ErrPermit2InvalidOwner + ErrPermit2PaymentTooEarly = evm.ErrPermit2PaymentTooEarly + ErrPermit2InvalidNonce = evm.ErrPermit2InvalidNonce + ErrPermit2612AmountMismatch = evm.ErrPermit2612AmountMismatch + ErrPermit2SimulationFailed = evm.ErrPermit2SimulationFailed + ErrPermit2InsufficientBalance = evm.ErrPermit2InsufficientBalance + ErrPermit2ProxyNotDeployed = evm.ErrPermit2ProxyNotDeployed + ErrPermit2AllowanceRequired = evm.ErrPermit2AllowanceRequired + + ErrErc20ApprovalInsufficientEth = evm.ErrErc20ApprovalInsufficientEth + ErrErc20ApprovalBroadcastFailed = evm.ErrErc20ApprovalBroadcastFailed + + ErrInvalidSignatureFormat = "invalid_upto_evm_signature_format" + ErrInvalidRequiredAmount = "invalid_upto_evm_required_amount" +) diff --git a/go/mechanisms/evm/upto/facilitator/permit2.go b/go/mechanisms/evm/upto/facilitator/permit2.go new file mode 100644 index 0000000000..4ae438165c --- /dev/null +++ b/go/mechanisms/evm/upto/facilitator/permit2.go @@ -0,0 +1,414 @@ +package facilitator + +import ( + "context" + "errors" + "math/big" + "strings" + "time" + + x402 "github.com/coinbase/x402/go" + "github.com/coinbase/x402/go/extensions/eip2612gassponsor" + "github.com/coinbase/x402/go/extensions/erc20approvalgassponsor" + "github.com/coinbase/x402/go/mechanisms/evm" + exactfacilitator "github.com/coinbase/x402/go/mechanisms/evm/exact/facilitator" + "github.com/coinbase/x402/go/types" +) + +// VerifyUptoPermit2 verifies an upto Permit2 payment payload against the given requirements. +// simulate controls whether to run an eth_call simulation as part of verification. +func VerifyUptoPermit2( + ctx context.Context, + signer evm.FacilitatorEvmSigner, + payload types.PaymentPayload, + requirements types.PaymentRequirements, + permit2Payload *evm.UptoPermit2Payload, + facilCtx *x402.FacilitatorContext, + simulate bool, +) (*x402.VerifyResponse, error) { + payer := permit2Payload.Permit2Authorization.From + + if payload.Accepted.Scheme != evm.SchemeUpto || requirements.Scheme != evm.SchemeUpto { + return nil, x402.NewVerifyError(ErrUptoInvalidScheme, payer, "scheme mismatch") + } + + if payload.Accepted.Network != requirements.Network { + return nil, x402.NewVerifyError(ErrUptoNetworkMismatch, payer, "network mismatch") + } + + chainID, err := evm.GetEvmChainId(string(requirements.Network)) + if err != nil { + return nil, x402.NewVerifyError(ErrUptoFailedToGetNetworkConfig, payer, err.Error()) + } + + tokenAddress := evm.NormalizeAddress(requirements.Asset) + + if !strings.EqualFold(permit2Payload.Permit2Authorization.Spender, evm.X402UptoPermit2ProxyAddress) { + return nil, x402.NewVerifyError(ErrPermit2InvalidSpender, payer, "invalid spender") + } + + if !strings.EqualFold(permit2Payload.Permit2Authorization.Witness.To, requirements.PayTo) { + return nil, x402.NewVerifyError(ErrPermit2RecipientMismatch, payer, "recipient mismatch") + } + + // Verify the facilitator address in the witness matches one of our signer addresses + facilitatorAddresses := signer.GetAddresses() + witnessFacilitator := permit2Payload.Permit2Authorization.Witness.Facilitator + isFacilitatorMatch := false + for _, addr := range facilitatorAddresses { + if strings.EqualFold(addr, witnessFacilitator) { + isFacilitatorMatch = true + break + } + } + if !isFacilitatorMatch { + return nil, x402.NewVerifyError(ErrUptoFacilitatorMismatch, payer, "facilitator mismatch") + } + + now := time.Now().Unix() + deadline, ok := new(big.Int).SetString(permit2Payload.Permit2Authorization.Deadline, 10) + if !ok { + return nil, x402.NewVerifyError(ErrUptoInvalidPayload, payer, "invalid deadline format") + } + if deadline.Cmp(big.NewInt(now+evm.Permit2DeadlineBuffer)) < 0 { + return nil, x402.NewVerifyError(ErrPermit2DeadlineExpired, payer, "deadline expired") + } + + validAfter, ok := new(big.Int).SetString(permit2Payload.Permit2Authorization.Witness.ValidAfter, 10) + if !ok { + return nil, x402.NewVerifyError(ErrUptoInvalidPayload, payer, "invalid validAfter format") + } + if validAfter.Cmp(big.NewInt(now)) > 0 { + return nil, x402.NewVerifyError(ErrPermit2NotYetValid, payer, "not yet valid") + } + + authAmount, ok := new(big.Int).SetString(permit2Payload.Permit2Authorization.Permitted.Amount, 10) + if !ok { + return nil, x402.NewVerifyError(ErrUptoInvalidPayload, payer, "invalid permitted amount format") + } + requiredAmount, ok := new(big.Int).SetString(requirements.Amount, 10) + if !ok { + return nil, x402.NewVerifyError(ErrInvalidRequiredAmount, payer, "invalid required amount format") + } + if authAmount.Cmp(requiredAmount) != 0 { + return nil, x402.NewVerifyError(ErrPermit2AmountMismatch, payer, "amount mismatch") + } + + if !strings.EqualFold(permit2Payload.Permit2Authorization.Permitted.Token, requirements.Asset) { + return nil, x402.NewVerifyError(ErrPermit2TokenMismatch, payer, "token mismatch") + } + + signatureBytes, err := evm.HexToBytes(permit2Payload.Signature) + if err != nil { + return nil, x402.NewVerifyError(ErrInvalidSignatureFormat, payer, err.Error()) + } + + sigValid, sigErr := verifyUptoPermit2Signature(ctx, signer, permit2Payload.Permit2Authorization, signatureBytes, chainID) + if sigErr != nil || !sigValid { + code, codeErr := signer.GetCode(ctx, payer) + if codeErr != nil || len(code) == 0 { + return nil, x402.NewVerifyError(ErrPermit2InvalidSignature, payer, "invalid signature") + } + } + + if !simulate { + return &x402.VerifyResponse{IsValid: true, Payer: payer}, nil + } + + // Simulate against requirements.amount (worst-case charge). From must equal the + // facilitator address: the upto proxy enforces msg.sender == witness.facilitator. + eip2612Info, _ := eip2612gassponsor.ExtractEip2612GasSponsoringInfo(payload.Extensions) + if eip2612Info != nil { + if validErr := validateEip2612PermitForPayment(eip2612Info, payer, tokenAddress); validErr != "" { + return nil, x402.NewVerifyError(validErr, payer, "eip2612 validation failed") + } + + simOk, simErr := SimulateUptoPermit2SettleWithPermit(ctx, signer, permit2Payload, requiredAmount, eip2612Info.Signature, eip2612Info.Amount, eip2612Info.Deadline) + if simErr != nil || !simOk { + resp := DiagnoseUptoPermit2SimulationFailure(ctx, signer, tokenAddress, permit2Payload, requirements.Amount) + return nil, x402.NewVerifyError(resp.InvalidReason, payer, "simulation failed") + } + return &x402.VerifyResponse{IsValid: true, Payer: payer}, nil + } + + erc20Info, _ := erc20approvalgassponsor.ExtractInfo(payload.Extensions) + if erc20Info != nil && facilCtx != nil { + ext, ok := facilCtx.GetExtension(erc20approvalgassponsor.ERC20ApprovalGasSponsoring.Key()).(*erc20approvalgassponsor.Erc20ApprovalFacilitatorExtension) + var extensionSigner erc20approvalgassponsor.Erc20ApprovalGasSponsoringSigner + if ok && ext != nil { + extensionSigner = ext.ResolveSigner(payload.Accepted.Network) + } + + if extensionSigner != nil { + if reason, msg := exactfacilitator.ValidateErc20ApprovalForPayment(erc20Info, payer, tokenAddress); reason != "" { + return nil, x402.NewVerifyError(reason, payer, msg) + } + + if simulator, ok := extensionSigner.(erc20approvalgassponsor.Erc20ApprovalGasSponsoringSimulator); ok { + simArgs, buildErr := BuildUptoPermit2SettleArgs(permit2Payload, requiredAmount) + if buildErr == nil { + simOk, simErr := simulator.SimulateTransactions(ctx, []erc20approvalgassponsor.TransactionRequest{ + {Serialized: erc20Info.SignedTransaction}, + {Call: &erc20approvalgassponsor.WriteContractCall{ + Address: evm.X402UptoPermit2ProxyAddress, + ABI: evm.X402UptoPermit2ProxySettleABI, + Function: evm.FunctionSettle, + Args: []interface{}{simArgs.permitStruct(), simArgs.SettlementAmount, simArgs.Owner, simArgs.witnessStruct(), simArgs.Signature}, + }}, + }) + if simErr == nil && simOk { + return &x402.VerifyResponse{IsValid: true, Payer: payer}, nil + } + } + resp := DiagnoseUptoPermit2SimulationFailure(ctx, signer, tokenAddress, permit2Payload, requirements.Amount) + return nil, x402.NewVerifyError(resp.InvalidReason, payer, "simulation failed") + } + + prereqResp := CheckUptoPermit2Prerequisites(ctx, signer, tokenAddress, payer, requirements.Amount) + if !prereqResp.IsValid { + return nil, x402.NewVerifyError(prereqResp.InvalidReason, payer, "prerequisites check failed") + } + return &x402.VerifyResponse{IsValid: true, Payer: payer}, nil + } + } + + simOk, simErr := SimulateUptoPermit2Settle(ctx, signer, permit2Payload, requiredAmount) + if simErr != nil || !simOk { + resp := DiagnoseUptoPermit2SimulationFailure(ctx, signer, tokenAddress, permit2Payload, requirements.Amount) + return nil, x402.NewVerifyError(resp.InvalidReason, payer, "simulation failed") + } + + return &x402.VerifyResponse{IsValid: true, Payer: payer}, nil +} + +// SettleUptoPermit2 settles an upto Permit2 payment by calling x402UptoPermit2Proxy.settle(). +// simulateInSettle controls whether to run an eth_call simulation as part of pre-settle verification. +func SettleUptoPermit2( + ctx context.Context, + signer evm.FacilitatorEvmSigner, + payload types.PaymentPayload, + requirements types.PaymentRequirements, + permit2Payload *evm.UptoPermit2Payload, + facilCtx *x402.FacilitatorContext, + simulateInSettle bool, +) (*x402.SettleResponse, error) { + network := x402.Network(payload.Accepted.Network) + payer := permit2Payload.Permit2Authorization.From + + settlementAmount, ok := new(big.Int).SetString(requirements.Amount, 10) + if !ok { + return nil, x402.NewSettleError(ErrUptoInvalidPayload, payer, network, "", "invalid settlement amount") + } + + // Re-verify with permitted.amount as requirements.Amount (the authorized max) + verifyRequirements := requirements + verifyRequirements.Amount = permit2Payload.Permit2Authorization.Permitted.Amount + + verifyResp, err := VerifyUptoPermit2(ctx, signer, payload, verifyRequirements, permit2Payload, facilCtx, simulateInSettle) + if err != nil { + ve := &x402.VerifyError{} + if errors.As(err, &ve) { + return nil, x402.NewSettleError(ve.InvalidReason, ve.Payer, network, "", ve.InvalidMessage) + } + return nil, x402.NewSettleError(ErrUptoVerificationFailed, payer, network, "", err.Error()) + } + + // Zero settlement — no on-chain tx needed + if settlementAmount.Sign() == 0 { + return &x402.SettleResponse{ + Success: true, + Transaction: "", + Network: network, + Payer: verifyResp.Payer, + Amount: "0", + }, nil + } + + // Guard: settlement amount must not exceed authorized maximum + permittedAmount, ok := new(big.Int).SetString(permit2Payload.Permit2Authorization.Permitted.Amount, 10) + if !ok { + return nil, x402.NewSettleError(ErrUptoInvalidPayload, payer, network, "", "invalid permitted amount") + } + if settlementAmount.Cmp(permittedAmount) > 0 { + return nil, x402.NewSettleError(ErrUptoSettlementExceedsAmount, payer, network, "", "settlement exceeds permitted amount") + } + + args, buildErr := BuildUptoPermit2SettleArgs(permit2Payload, settlementAmount) + if buildErr != nil { + return nil, x402.NewSettleError(ErrUptoInvalidPayload, payer, network, "", buildErr.Error()) + } + + permitStruct := args.permitStruct() + witnessStruct := args.witnessStruct() + + eip2612Info, _ := eip2612gassponsor.ExtractEip2612GasSponsoringInfo(payload.Extensions) + erc20Info, _ := erc20approvalgassponsor.ExtractInfo(payload.Extensions) + + var txHash string + + switch { + case eip2612Info != nil: + v, r, s, splitErr := splitEip2612Signature(eip2612Info.Signature) + if splitErr != nil { + return nil, x402.NewSettleError(ErrUptoInvalidPayload, payer, network, "", "invalid eip2612 signature format") + } + + eip2612Value, ok := new(big.Int).SetString(eip2612Info.Amount, 10) + if !ok { + return nil, x402.NewSettleError(ErrUptoInvalidPayload, payer, network, "", "invalid eip2612 amount") + } + eip2612Deadline, ok := new(big.Int).SetString(eip2612Info.Deadline, 10) + if !ok { + return nil, x402.NewSettleError(ErrUptoInvalidPayload, payer, network, "", "invalid eip2612 deadline") + } + + permit2612Struct := EIP2612PermitData{ + Value: eip2612Value, + Deadline: eip2612Deadline, + R: r, + S: s, + V: v, + } + + txHash, err = signer.WriteContract( + ctx, + evm.X402UptoPermit2ProxyAddress, + evm.X402UptoPermit2ProxySettleWithPermitABI, + evm.FunctionSettleWithPermit, + permit2612Struct, + permitStruct, + args.SettlementAmount, + args.Owner, + witnessStruct, + args.Signature, + ) + + case erc20Info != nil && facilCtx != nil: + ext, ok := facilCtx.GetExtension(erc20approvalgassponsor.ERC20ApprovalGasSponsoring.Key()).(*erc20approvalgassponsor.Erc20ApprovalFacilitatorExtension) + var extensionSigner erc20approvalgassponsor.Erc20ApprovalGasSponsoringSigner + if ok && ext != nil { + extensionSigner = ext.ResolveSigner(payload.Accepted.Network) + } + if extensionSigner != nil { + settle := erc20approvalgassponsor.WriteContractCall{ + Address: evm.X402UptoPermit2ProxyAddress, + ABI: evm.X402UptoPermit2ProxySettleABI, + Function: evm.FunctionSettle, + Args: []interface{}{permitStruct, args.SettlementAmount, args.Owner, witnessStruct, args.Signature}, + } + txHashes, sendErr := extensionSigner.SendTransactions(ctx, []erc20approvalgassponsor.TransactionRequest{ + {Serialized: erc20Info.SignedTransaction}, + {Call: &settle}, + }) + if sendErr != nil { + err = sendErr + } else if len(txHashes) > 0 { + txHash = txHashes[len(txHashes)-1] + } + } else { + txHash, err = signer.WriteContract( + ctx, + evm.X402UptoPermit2ProxyAddress, + evm.X402UptoPermit2ProxySettleABI, + evm.FunctionSettle, + permitStruct, + args.SettlementAmount, + args.Owner, + witnessStruct, + args.Signature, + ) + } + + default: + txHash, err = signer.WriteContract( + ctx, + evm.X402UptoPermit2ProxyAddress, + evm.X402UptoPermit2ProxySettleABI, + evm.FunctionSettle, + permitStruct, + args.SettlementAmount, + args.Owner, + witnessStruct, + args.Signature, + ) + } + + if err != nil { + errorReason := parseUptoPermit2Error(err) + return nil, x402.NewSettleError(errorReason, payer, network, "", err.Error()) + } + + receiptWaitSigner := signer + if erc20Info != nil && facilCtx != nil { + if ext, ok := facilCtx.GetExtension(erc20approvalgassponsor.ERC20ApprovalGasSponsoring.Key()).(*erc20approvalgassponsor.Erc20ApprovalFacilitatorExtension); ok && ext != nil { + if extensionSigner := ext.ResolveSigner(payload.Accepted.Network); extensionSigner != nil { + receiptWaitSigner = extensionSigner + } + } + } + receipt, err := receiptWaitSigner.WaitForTransactionReceipt(ctx, txHash) + if err != nil { + return nil, x402.NewSettleError(ErrUptoFailedToGetReceipt, payer, network, txHash, err.Error()) + } + + if receipt.Status != evm.TxStatusSuccess { + return nil, x402.NewSettleError(ErrUptoTransactionFailed, payer, network, txHash, "") + } + + return &x402.SettleResponse{ + Success: true, + Transaction: txHash, + Network: network, + Payer: verifyResp.Payer, + Amount: settlementAmount.String(), + }, nil +} + +func verifyUptoPermit2Signature( + ctx context.Context, + signer evm.FacilitatorEvmSigner, + authorization evm.UptoPermit2Authorization, + signature []byte, + chainID *big.Int, +) (bool, error) { + hash, err := evm.HashUptoPermit2Authorization(authorization, chainID) + if err != nil { + return false, err + } + + var hash32 [32]byte + copy(hash32[:], hash) + + valid, _, err := evm.VerifyUniversalSignature(ctx, signer, authorization.From, hash32, signature, true) + return valid, err +} + +var validateEip2612PermitForPayment = evm.ValidateEip2612PermitForPayment + +func parseUptoPermit2Error(err error) string { + msg := err.Error() + switch { + case strings.Contains(msg, "Permit2612AmountMismatch"): + return ErrPermit2612AmountMismatch + case strings.Contains(msg, "InvalidAmount"): + return ErrPermit2InvalidAmount + case strings.Contains(msg, "InvalidDestination"): + return ErrPermit2InvalidDestination + case strings.Contains(msg, "InvalidOwner"): + return ErrPermit2InvalidOwner + case strings.Contains(msg, "PaymentTooEarly"): + return ErrPermit2PaymentTooEarly + case strings.Contains(msg, "InvalidSignature"), strings.Contains(msg, "SignatureExpired"): + return ErrPermit2InvalidSignature + case strings.Contains(msg, "InvalidNonce"): + return ErrPermit2InvalidNonce + case strings.Contains(msg, "erc20_approval_tx_failed"): + return ErrErc20ApprovalBroadcastFailed + case strings.Contains(msg, "AmountExceedsPermitted"): + return ErrUptoAmountExceedsPermitted + case strings.Contains(msg, "UnauthorizedFacilitator"): + return ErrUptoUnauthorizedFacilitator + default: + return ErrUptoTransactionFailed + } +} diff --git a/go/mechanisms/evm/upto/facilitator/permit2_helpers.go b/go/mechanisms/evm/upto/facilitator/permit2_helpers.go new file mode 100644 index 0000000000..7a2417e666 --- /dev/null +++ b/go/mechanisms/evm/upto/facilitator/permit2_helpers.go @@ -0,0 +1,336 @@ +package facilitator + +import ( + "context" + "math/big" + + "github.com/ethereum/go-ethereum/common" + + x402 "github.com/coinbase/x402/go" + "github.com/coinbase/x402/go/mechanisms/evm" +) + +// EIP2612PermitData holds the parsed EIP-2612 permit fields for settleWithPermit() calls. +type EIP2612PermitData struct { + Value *big.Int + Deadline *big.Int + R [32]byte + S [32]byte + V uint8 +} + +// UptoPermit2SettleArgs holds the parsed and typed arguments for upto settle()/settleWithPermit(). +// Differs from exact: witness includes Facilitator, and settle takes a separate Amount. +type UptoPermit2SettleArgs struct { + Permit struct { + Permitted struct { + Token common.Address + Amount *big.Int + } + Nonce *big.Int + Deadline *big.Int + } + SettlementAmount *big.Int + Owner common.Address + Witness struct { + To common.Address + Facilitator common.Address + ValidAfter *big.Int + } + Signature []byte +} + +// BuildUptoPermit2SettleArgs converts a raw UptoPermit2Payload into typed contract-call arguments. +func BuildUptoPermit2SettleArgs(permit2Payload *evm.UptoPermit2Payload, settlementAmount *big.Int) (*UptoPermit2SettleArgs, error) { + amount, ok := new(big.Int).SetString(permit2Payload.Permit2Authorization.Permitted.Amount, 10) + if !ok { + return nil, errParse("permitted amount") + } + nonce, ok := new(big.Int).SetString(permit2Payload.Permit2Authorization.Nonce, 10) + if !ok { + return nil, errParse("nonce") + } + deadline, ok := new(big.Int).SetString(permit2Payload.Permit2Authorization.Deadline, 10) + if !ok { + return nil, errParse("deadline") + } + validAfter, ok := new(big.Int).SetString(permit2Payload.Permit2Authorization.Witness.ValidAfter, 10) + if !ok { + return nil, errParse("validAfter") + } + signatureBytes, err := evm.HexToBytes(permit2Payload.Signature) + if err != nil { + return nil, err + } + + args := &UptoPermit2SettleArgs{} + args.Permit.Permitted.Token = common.HexToAddress(permit2Payload.Permit2Authorization.Permitted.Token) + args.Permit.Permitted.Amount = amount + args.Permit.Nonce = nonce + args.Permit.Deadline = deadline + args.SettlementAmount = settlementAmount + args.Owner = common.HexToAddress(permit2Payload.Permit2Authorization.From) + args.Witness.To = common.HexToAddress(permit2Payload.Permit2Authorization.Witness.To) + args.Witness.Facilitator = common.HexToAddress(permit2Payload.Permit2Authorization.Witness.Facilitator) + args.Witness.ValidAfter = validAfter + args.Signature = signatureBytes + return args, nil +} + +func (a *UptoPermit2SettleArgs) permitStruct() interface{} { + return struct { + Permitted struct { + Token common.Address + Amount *big.Int + } + Nonce *big.Int + Deadline *big.Int + }{ + Permitted: struct { + Token common.Address + Amount *big.Int + }{ + Token: a.Permit.Permitted.Token, + Amount: a.Permit.Permitted.Amount, + }, + Nonce: a.Permit.Nonce, + Deadline: a.Permit.Deadline, + } +} + +func (a *UptoPermit2SettleArgs) witnessStruct() interface{} { + return struct { + To common.Address + Facilitator common.Address + ValidAfter *big.Int + }{ + To: a.Witness.To, + Facilitator: a.Witness.Facilitator, + ValidAfter: a.Witness.ValidAfter, + } +} + +// SimulateUptoPermit2Settle runs settle() via eth_call (ReadContract) on the upto proxy. +func SimulateUptoPermit2Settle( + ctx context.Context, + signer evm.FacilitatorEvmSigner, + permit2Payload *evm.UptoPermit2Payload, + settlementAmount *big.Int, +) (bool, error) { + args, err := BuildUptoPermit2SettleArgs(permit2Payload, settlementAmount) + if err != nil { + return false, err + } + + _, err = signer.ReadContract( + ctx, + evm.X402UptoPermit2ProxyAddress, + evm.X402UptoPermit2ProxySettleABI, + evm.FunctionSettle, + args.permitStruct(), + args.SettlementAmount, + args.Owner, + args.witnessStruct(), + args.Signature, + ) + if err != nil { + return false, err + } + return true, nil +} + +// SimulateUptoPermit2SettleWithPermit runs settleWithPermit() via eth_call on the upto proxy. +func SimulateUptoPermit2SettleWithPermit( + ctx context.Context, + signer evm.FacilitatorEvmSigner, + permit2Payload *evm.UptoPermit2Payload, + settlementAmount *big.Int, + eip2612Signature, eip2612Amount, eip2612DeadlineStr string, +) (bool, error) { + args, err := BuildUptoPermit2SettleArgs(permit2Payload, settlementAmount) + if err != nil { + return false, err + } + + v, r, s, splitErr := splitEip2612Signature(eip2612Signature) + if splitErr != nil { + return false, splitErr + } + + eip2612Value, ok := new(big.Int).SetString(eip2612Amount, 10) + if !ok { + return false, errParse("eip2612 amount") + } + eip2612Deadline, ok := new(big.Int).SetString(eip2612DeadlineStr, 10) + if !ok { + return false, errParse("eip2612 deadline") + } + + permit2612Struct := EIP2612PermitData{ + Value: eip2612Value, + Deadline: eip2612Deadline, + R: r, + S: s, + V: v, + } + + _, err = signer.ReadContract( + ctx, + evm.X402UptoPermit2ProxyAddress, + evm.X402UptoPermit2ProxySettleWithPermitABI, + evm.FunctionSettleWithPermit, + permit2612Struct, + args.permitStruct(), + args.SettlementAmount, + args.Owner, + args.witnessStruct(), + args.Signature, + ) + if err != nil { + return false, err + } + return true, nil +} + +// uptoPermit2Multicall3 runs the standard 3-call multicall shared by the diagnostic and +// prerequisites helpers: [PERMIT2(), balanceOf(payer), thirdCall]. Returns (results, +// reqAmount, error). results is nil when the multicall itself fails; reqAmount is nil when +// amountRequired cannot be parsed. +func uptoPermit2Multicall3( + ctx context.Context, + signer evm.FacilitatorEvmSigner, + tokenAddress string, + payer string, + amountRequired string, + thirdCall evm.MulticallCall, +) ([]evm.MulticallResult, *big.Int, error) { + results, err := evm.Multicall(ctx, signer, []evm.MulticallCall{ + { + Address: evm.X402UptoPermit2ProxyAddress, + ABI: evm.X402UptoPermit2ProxyPermit2ABI, + FunctionName: "PERMIT2", + }, + { + Address: tokenAddress, + ABI: evm.ERC20BalanceOfABI, + FunctionName: "balanceOf", + Args: []interface{}{common.HexToAddress(payer)}, + }, + thirdCall, + }) + if err != nil || len(results) < 3 { + return nil, nil, err + } + reqAmount, _ := new(big.Int).SetString(amountRequired, 10) + return results, reqAmount, nil +} + +// DiagnoseUptoPermit2SimulationFailure runs a multicall diagnostic to return the most +// specific error reason after an upto simulation failure. +func DiagnoseUptoPermit2SimulationFailure( + ctx context.Context, + signer evm.FacilitatorEvmSigner, + tokenAddress string, + permit2Payload *evm.UptoPermit2Payload, + amountRequired string, +) *x402.VerifyResponse { + payer := permit2Payload.Permit2Authorization.From + + results, reqAmount, _ := uptoPermit2Multicall3(ctx, signer, tokenAddress, payer, amountRequired, evm.MulticallCall{ + Address: tokenAddress, + ABI: evm.ERC20AllowanceABI, + FunctionName: "allowance", + Args: []interface{}{common.HexToAddress(payer), common.HexToAddress(evm.PERMIT2Address)}, + }) + if results == nil { + return &x402.VerifyResponse{IsValid: false, InvalidReason: ErrPermit2SimulationFailed, Payer: payer} + } + + if !results[0].Success() { + return &x402.VerifyResponse{IsValid: false, InvalidReason: ErrPermit2ProxyNotDeployed, Payer: payer} + } + + if reqAmount == nil { + return &x402.VerifyResponse{IsValid: false, InvalidReason: ErrPermit2SimulationFailed, Payer: payer} + } + + if results[1].Success() { + if balance := asBigInt(results[1].Result); balance != nil && balance.Cmp(reqAmount) < 0 { + return &x402.VerifyResponse{IsValid: false, InvalidReason: ErrPermit2InsufficientBalance, Payer: payer} + } + } + + if results[2].Success() { + if allowance := asBigInt(results[2].Result); allowance != nil && allowance.Cmp(reqAmount) < 0 { + return &x402.VerifyResponse{IsValid: false, InvalidReason: ErrPermit2AllowanceRequired, Payer: payer} + } + } + + return &x402.VerifyResponse{IsValid: false, InvalidReason: ErrPermit2SimulationFailed, Payer: payer} +} + +// CheckUptoPermit2Prerequisites checks proxy deployment, payer token balance and payer ETH balance for gas. +func CheckUptoPermit2Prerequisites( + ctx context.Context, + signer evm.FacilitatorEvmSigner, + tokenAddress string, + payer string, + amountRequired string, +) *x402.VerifyResponse { + results, reqAmount, _ := uptoPermit2Multicall3(ctx, signer, tokenAddress, payer, amountRequired, evm.MulticallCall{ + Address: evm.MULTICALL3Address, + ABI: evm.Multicall3GetEthBalanceABI, + FunctionName: "getEthBalance", + Args: []interface{}{common.HexToAddress(payer)}, + }) + if results == nil { + return &x402.VerifyResponse{IsValid: true, Payer: payer} // fail-open on multicall error + } + + if !results[0].Success() { + return &x402.VerifyResponse{IsValid: false, InvalidReason: ErrPermit2ProxyNotDeployed, Payer: payer} + } + + if reqAmount != nil && results[1].Success() { + if balance := asBigInt(results[1].Result); balance != nil && balance.Cmp(reqAmount) < 0 { + return &x402.VerifyResponse{IsValid: false, InvalidReason: ErrPermit2InsufficientBalance, Payer: payer} + } + } + + if results[2].Success() { + minEthForGas := new(big.Int).Mul( + big.NewInt(int64(evm.ERC20ApproveGasLimit)), + big.NewInt(int64(evm.DefaultMaxFeePerGas)), + ) + if ethBalance := asBigInt(results[2].Result); ethBalance != nil && ethBalance.Cmp(minEthForGas) < 0 { + return &x402.VerifyResponse{IsValid: false, InvalidReason: ErrErc20ApprovalInsufficientEth, Payer: payer} + } + } + + return &x402.VerifyResponse{IsValid: true, Payer: payer} +} + +func errParse(field string) error { + return &parseError{field: field} +} + +type parseError struct { + field string +} + +func (e *parseError) Error() string { + return "invalid " + e.field +} + +func asBigInt(value interface{}) *big.Int { + switch v := value.(type) { + case *big.Int: + return v + case big.Int: + return &v + default: + return nil + } +} + +var splitEip2612Signature = evm.SplitEip2612Signature diff --git a/go/mechanisms/evm/upto/facilitator/permit2_test.go b/go/mechanisms/evm/upto/facilitator/permit2_test.go new file mode 100644 index 0000000000..4bd5624826 --- /dev/null +++ b/go/mechanisms/evm/upto/facilitator/permit2_test.go @@ -0,0 +1,920 @@ +package facilitator + +import ( + "context" + "errors" + "fmt" + "math/big" + "testing" + "time" + + x402 "github.com/coinbase/x402/go" + "github.com/coinbase/x402/go/extensions/eip2612gassponsor" + "github.com/coinbase/x402/go/mechanisms/evm" + "github.com/coinbase/x402/go/types" +) + +// ─── Mock facilitator signer ──────────────────────────────────────────────── + +type mockFacilitatorSigner struct { + addresses []string + readContractResult interface{} + readContractError error + writeContractTx string + writeContractError error + getCodeResult []byte + getCodeError error + verifyResult bool + verifyError error + receiptResult *evm.TransactionReceipt + receiptError error +} + +func newMockSigner(addresses ...string) *mockFacilitatorSigner { + if len(addresses) == 0 { + addresses = []string{testFacilitatorAddr} + } + return &mockFacilitatorSigner{ + addresses: addresses, + writeContractTx: "0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef", + // Payer appears as a deployed contract so sig-recovery failures fall through to simulation + getCodeResult: []byte{0x60, 0x60}, + receiptResult: &evm.TransactionReceipt{Status: evm.TxStatusSuccess, TxHash: "0xdeadbeef"}, + } +} + +func (m *mockFacilitatorSigner) GetAddresses() []string { return m.addresses } + +func (m *mockFacilitatorSigner) ReadContract(ctx context.Context, address string, abi []byte, functionName string, args ...interface{}) (interface{}, error) { + if m.readContractError != nil { + return nil, m.readContractError + } + return m.readContractResult, nil +} + +func (m *mockFacilitatorSigner) VerifyTypedData(ctx context.Context, address string, domain evm.TypedDataDomain, types map[string][]evm.TypedDataField, primaryType string, message map[string]interface{}, signature []byte) (bool, error) { + return m.verifyResult, m.verifyError +} + +func (m *mockFacilitatorSigner) WriteContract(ctx context.Context, address string, abi []byte, functionName string, args ...interface{}) (string, error) { + if m.writeContractError != nil { + return "", m.writeContractError + } + return m.writeContractTx, nil +} + +func (m *mockFacilitatorSigner) SendTransaction(ctx context.Context, to string, data []byte) (string, error) { + return m.writeContractTx, m.writeContractError +} + +func (m *mockFacilitatorSigner) WaitForTransactionReceipt(ctx context.Context, txHash string) (*evm.TransactionReceipt, error) { + if m.receiptError != nil { + return nil, m.receiptError + } + return m.receiptResult, nil +} + +func (m *mockFacilitatorSigner) GetBalance(ctx context.Context, address string, tokenAddress string) (*big.Int, error) { + return big.NewInt(999_000_000), nil +} + +func (m *mockFacilitatorSigner) GetChainID(ctx context.Context) (*big.Int, error) { + return big.NewInt(84532), nil +} + +func (m *mockFacilitatorSigner) GetCode(ctx context.Context, address string) ([]byte, error) { + if m.getCodeError != nil { + return nil, m.getCodeError + } + return m.getCodeResult, nil +} + +// ─── Test addresses (valid 40-hex-char Ethereum addresses) ────────────────── + +const ( + // All addresses are proper 0x + 40 hex chars + testFacilitatorAddr = "0xf1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1" + testPayerAddr = "0xa0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0" + testPayToAddr = "0xb1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1" + testTokenAddr = "0x036cbd53842c5426634e7929541ec2318f3dcf7e" + testNetwork = "eip155:84532" + testAmount = "1000" + // 65-byte dummy hex signature (0x + 130 hex chars) + dummySig = "0x" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + // r (32 bytes) + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" + // s (32 bytes) + "1b" // v = 27 +) + +func futureDeadline() string { + return fmt.Sprintf("%d", time.Now().Unix()+300) +} + +func pastTimestamp() string { + return fmt.Sprintf("%d", time.Now().Unix()-600) +} + +// buildValidUptoPayload constructs a syntactically valid UptoPermit2Payload. +// ECDSA recovery on the dummy sig will fail, but since the payer is mocked as +// a deployed contract (getCodeResult non-empty), verification falls through to +// simulation where readContractError controls the outcome. +func buildValidUptoPayload(facilitatorAddr string) *evm.UptoPermit2Payload { + return &evm.UptoPermit2Payload{ + Signature: dummySig, + Permit2Authorization: evm.UptoPermit2Authorization{ + From: testPayerAddr, + Permitted: evm.Permit2TokenPermissions{ + Token: testTokenAddr, + Amount: testAmount, + }, + Spender: evm.X402UptoPermit2ProxyAddress, + Nonce: "12345", + Deadline: futureDeadline(), + Witness: evm.UptoPermit2Witness{ + To: testPayToAddr, + Facilitator: facilitatorAddr, + ValidAfter: pastTimestamp(), + }, + }, + } +} + +func buildValidPayload(facilitatorAddr string) types.PaymentPayload { + p := buildValidUptoPayload(facilitatorAddr) + return types.PaymentPayload{ + X402Version: 2, + Payload: p.ToMap(), + Accepted: types.PaymentRequirements{ + Scheme: evm.SchemeUpto, + Network: testNetwork, + Amount: testAmount, + Asset: testTokenAddr, + PayTo: testPayToAddr, + }, + } +} + +func buildValidRequirements() types.PaymentRequirements { + return types.PaymentRequirements{ + Scheme: evm.SchemeUpto, + Network: testNetwork, + Amount: testAmount, + Asset: testTokenAddr, + PayTo: testPayToAddr, + } +} + +// ─── VerifyUptoPermit2 — input validation tests ────────────────────────────── + +func TestVerifyUptoPermit2_SchemeMismatch_Payload(t *testing.T) { + signer := newMockSigner() + p := buildValidUptoPayload(testFacilitatorAddr) + payload := buildValidPayload(testFacilitatorAddr) + payload.Accepted.Scheme = "exact" + + _, err := VerifyUptoPermit2(context.Background(), signer, payload, buildValidRequirements(), p, nil, false) + assertVerifyError(t, err, ErrUptoInvalidScheme) +} + +func TestVerifyUptoPermit2_SchemeMismatch_Requirements(t *testing.T) { + signer := newMockSigner() + p := buildValidUptoPayload(testFacilitatorAddr) + payload := buildValidPayload(testFacilitatorAddr) + req := buildValidRequirements() + req.Scheme = "exact" + + _, err := VerifyUptoPermit2(context.Background(), signer, payload, req, p, nil, false) + assertVerifyError(t, err, ErrUptoInvalidScheme) +} + +func TestVerifyUptoPermit2_NetworkMismatch(t *testing.T) { + signer := newMockSigner() + p := buildValidUptoPayload(testFacilitatorAddr) + payload := buildValidPayload(testFacilitatorAddr) + payload.Accepted.Network = "eip155:8453" + + _, err := VerifyUptoPermit2(context.Background(), signer, payload, buildValidRequirements(), p, nil, false) + assertVerifyError(t, err, ErrUptoNetworkMismatch) +} + +func TestVerifyUptoPermit2_InvalidSpender(t *testing.T) { + signer := newMockSigner() + p := buildValidUptoPayload(testFacilitatorAddr) + p.Permit2Authorization.Spender = "0x0000000000000000000000000000000000000001" + + _, err := VerifyUptoPermit2(context.Background(), signer, buildValidPayload(testFacilitatorAddr), buildValidRequirements(), p, nil, false) + assertVerifyError(t, err, ErrPermit2InvalidSpender) +} + +func TestVerifyUptoPermit2_RecipientMismatch(t *testing.T) { + signer := newMockSigner() + p := buildValidUptoPayload(testFacilitatorAddr) + p.Permit2Authorization.Witness.To = "0x0000000000000000000000000000000000000001" + + _, err := VerifyUptoPermit2(context.Background(), signer, buildValidPayload(testFacilitatorAddr), buildValidRequirements(), p, nil, false) + assertVerifyError(t, err, ErrPermit2RecipientMismatch) +} + +func TestVerifyUptoPermit2_FacilitatorMismatch(t *testing.T) { + signer := newMockSigner(testFacilitatorAddr) + // Witness references a different facilitator address + p := buildValidUptoPayload("0x0000000000000000000000000000000000000001") + + _, err := VerifyUptoPermit2(context.Background(), signer, buildValidPayload(testFacilitatorAddr), buildValidRequirements(), p, nil, false) + assertVerifyError(t, err, ErrUptoFacilitatorMismatch) +} + +func TestVerifyUptoPermit2_FacilitatorMatch_CaseInsensitive(t *testing.T) { + // Facilitator address is uppercase in signer but lowercase in witness — should still match + upperFacilitator := "0xF1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1" + lowerFacilitator := "0xf1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1f1" + signer := newMockSigner(upperFacilitator) + + p := buildValidUptoPayload(lowerFacilitator) + // simulation should succeed (readContractError == nil) + _, err := VerifyUptoPermit2(context.Background(), signer, buildValidPayload(lowerFacilitator), buildValidRequirements(), p, nil, true) + if err != nil { + t.Fatalf("expected facilitator case-insensitive match to succeed, got: %v", err) + } +} + +func TestVerifyUptoPermit2_DeadlineExpired(t *testing.T) { + signer := newMockSigner() + p := buildValidUptoPayload(testFacilitatorAddr) + p.Permit2Authorization.Deadline = "1000000000" // year 2001 — expired + + _, err := VerifyUptoPermit2(context.Background(), signer, buildValidPayload(testFacilitatorAddr), buildValidRequirements(), p, nil, false) + assertVerifyError(t, err, ErrPermit2DeadlineExpired) +} + +func TestVerifyUptoPermit2_NotYetValid(t *testing.T) { + signer := newMockSigner() + p := buildValidUptoPayload(testFacilitatorAddr) + p.Permit2Authorization.Witness.ValidAfter = fmt.Sprintf("%d", time.Now().Unix()+9999) + + _, err := VerifyUptoPermit2(context.Background(), signer, buildValidPayload(testFacilitatorAddr), buildValidRequirements(), p, nil, false) + assertVerifyError(t, err, ErrPermit2NotYetValid) +} + +func TestVerifyUptoPermit2_AmountMismatch(t *testing.T) { + signer := newMockSigner() + p := buildValidUptoPayload(testFacilitatorAddr) + p.Permit2Authorization.Permitted.Amount = "9999" // != requirements.Amount "1000" + + _, err := VerifyUptoPermit2(context.Background(), signer, buildValidPayload(testFacilitatorAddr), buildValidRequirements(), p, nil, false) + assertVerifyError(t, err, ErrPermit2AmountMismatch) +} + +func TestVerifyUptoPermit2_TokenMismatch(t *testing.T) { + signer := newMockSigner() + p := buildValidUptoPayload(testFacilitatorAddr) + p.Permit2Authorization.Permitted.Token = "0x0000000000000000000000000000000000000001" + + _, err := VerifyUptoPermit2(context.Background(), signer, buildValidPayload(testFacilitatorAddr), buildValidRequirements(), p, nil, false) + assertVerifyError(t, err, ErrPermit2TokenMismatch) +} + +func TestVerifyUptoPermit2_InvalidSignatureHex(t *testing.T) { + signer := newMockSigner() + p := buildValidUptoPayload(testFacilitatorAddr) + p.Signature = "not-hex" + + _, err := VerifyUptoPermit2(context.Background(), signer, buildValidPayload(testFacilitatorAddr), buildValidRequirements(), p, nil, false) + assertVerifyError(t, err, ErrInvalidSignatureFormat) +} + +func TestVerifyUptoPermit2_InvalidSig_PayerIsEOA(t *testing.T) { + // ECDSA recovery fails, payer has no contract code → invalid sig + signer := newMockSigner() + signer.getCodeResult = []byte{} // EOA + + p := buildValidUptoPayload(testFacilitatorAddr) + _, err := VerifyUptoPermit2(context.Background(), signer, buildValidPayload(testFacilitatorAddr), buildValidRequirements(), p, nil, false) + assertVerifyError(t, err, ErrPermit2InvalidSignature) +} + +func TestVerifyUptoPermit2_SimulationDisabled_ReturnsValid(t *testing.T) { + // Even though ECDSA recovery fails, payer is a contract → falls to simulation + // Simulation is disabled → return valid immediately + signer := newMockSigner() + // getCodeResult is non-empty by default (contract) + + p := buildValidUptoPayload(testFacilitatorAddr) + resp, err := VerifyUptoPermit2(context.Background(), signer, buildValidPayload(testFacilitatorAddr), buildValidRequirements(), p, nil, false) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if !resp.IsValid { + t.Error("expected valid when simulation is disabled") + } +} + +func TestVerifyUptoPermit2_SimulationSucceeds(t *testing.T) { + // Payer is a contract, sim succeeds (readContractError nil) → valid + signer := newMockSigner() + + p := buildValidUptoPayload(testFacilitatorAddr) + resp, err := VerifyUptoPermit2(context.Background(), signer, buildValidPayload(testFacilitatorAddr), buildValidRequirements(), p, nil, true) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if !resp.IsValid { + t.Errorf("expected valid, got %s", resp.InvalidReason) + } + if resp.Payer != testPayerAddr { + t.Errorf("expected payer %s, got %s", testPayerAddr, resp.Payer) + } +} + +func TestVerifyUptoPermit2_ViabilityCheck_FailOpenOnMulticallError(t *testing.T) { + signer := newMockSigner() + p := buildValidUptoPayload(testFacilitatorAddr) + resp, err := VerifyUptoPermit2(context.Background(), signer, buildValidPayload(testFacilitatorAddr), buildValidRequirements(), p, nil, true) + if err != nil { + t.Fatalf("unexpected error in standard viability path: %v", err) + } + if !resp.IsValid { + t.Errorf("expected valid when viability multicall succeeds, got %s", resp.InvalidReason) + } +} + +func TestVerifyUptoPermit2_WithEIP2612Extension_FromMismatch(t *testing.T) { + signer := newMockSigner() + p := buildValidUptoPayload(testFacilitatorAddr) + payload := buildValidPayload(testFacilitatorAddr) + + // Attach EIP-2612 extension with wrong 'from' + payload.Extensions = map[string]interface{}{ + eip2612gassponsor.EIP2612GasSponsoring.Key(): map[string]interface{}{ + "info": map[string]interface{}{ + "from": "0x0000000000000000000000000000000000000001", + "asset": testTokenAddr, + "spender": evm.PERMIT2Address, + "amount": "115792089237316195423570985008687907853269984665640564039457584007913129639935", + "nonce": "0", + "deadline": futureDeadline(), + "signature": dummySig, + "version": "1", + }, + }, + } + + _, err := VerifyUptoPermit2(context.Background(), signer, payload, buildValidRequirements(), p, nil, true) + if err == nil { + t.Fatal("expected error from EIP-2612 from mismatch") + } + var ve *x402.VerifyError + if !errors.As(err, &ve) { + t.Fatalf("expected VerifyError, got %T: %v", err, err) + } + if ve.InvalidReason != "eip2612_from_mismatch" { + t.Errorf("expected eip2612_from_mismatch, got %s", ve.InvalidReason) + } +} + +func TestVerifyUptoPermit2_WithEIP2612Extension_Valid_SimSucceeds(t *testing.T) { + // When a valid EIP-2612 extension is present and simulation succeeds → valid + signer := newMockSigner() + p := buildValidUptoPayload(testFacilitatorAddr) + payload := buildValidPayload(testFacilitatorAddr) + + payload.Extensions = map[string]interface{}{ + eip2612gassponsor.EIP2612GasSponsoring.Key(): map[string]interface{}{ + "info": map[string]interface{}{ + "from": testPayerAddr, + "asset": testTokenAddr, + "spender": evm.PERMIT2Address, + "amount": "115792089237316195423570985008687907853269984665640564039457584007913129639935", + "nonce": "0", + "deadline": futureDeadline(), + "signature": dummySig, + "version": "1", + }, + }, + } + + // readContract: first call is settleWithPermit sim (succeeds with nil error) + resp, err := VerifyUptoPermit2(context.Background(), signer, payload, buildValidRequirements(), p, nil, true) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if !resp.IsValid { + t.Errorf("expected valid, got %s", resp.InvalidReason) + } +} + +// ─── SettleUptoPermit2 tests ───────────────────────────────────────────────── + +func TestSettleUptoPermit2_ZeroSettlement(t *testing.T) { + signer := newMockSigner() + p := buildValidUptoPayload(testFacilitatorAddr) + payload := buildValidPayload(testFacilitatorAddr) + + req := buildValidRequirements() + req.Amount = "0" // settle zero — no on-chain tx + + resp, err := SettleUptoPermit2(context.Background(), signer, payload, req, p, nil, false) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if !resp.Success { + t.Errorf("expected success for zero settlement, got %s", resp.ErrorReason) + } + if resp.Transaction != "" { + t.Errorf("expected empty txHash, got %s", resp.Transaction) + } + if resp.Amount != "0" { + t.Errorf("expected Amount='0', got %q", resp.Amount) + } +} + +func TestSettleUptoPermit2_ExceedsPermittedAmount(t *testing.T) { + signer := newMockSigner() + p := buildValidUptoPayload(testFacilitatorAddr) + payload := buildValidPayload(testFacilitatorAddr) + + req := buildValidRequirements() + req.Amount = "99999" // more than permitted "1000" + + _, err := SettleUptoPermit2(context.Background(), signer, payload, req, p, nil, false) + assertSettleError(t, err, ErrUptoSettlementExceedsAmount) +} + +func TestSettleUptoPermit2_FullAmount(t *testing.T) { + signer := newMockSigner() + resp, err := SettleUptoPermit2( + context.Background(), signer, + buildValidPayload(testFacilitatorAddr), + buildValidRequirements(), + buildValidUptoPayload(testFacilitatorAddr), + nil, + false, + ) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if !resp.Success { + t.Errorf("expected success, got %s", resp.ErrorReason) + } + if resp.Amount != testAmount { + t.Errorf("expected Amount=%q, got %q", testAmount, resp.Amount) + } +} + +func TestSettleUptoPermit2_PartialAmount(t *testing.T) { + signer := newMockSigner() + req := buildValidRequirements() + req.Amount = "500" // 500 of 1000 permitted + + resp, err := SettleUptoPermit2(context.Background(), signer, buildValidPayload(testFacilitatorAddr), req, buildValidUptoPayload(testFacilitatorAddr), nil, false) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if !resp.Success { + t.Errorf("expected success, got %s", resp.ErrorReason) + } + if resp.Amount != "500" { + t.Errorf("expected Amount='500', got %q", resp.Amount) + } +} + +func TestSettleUptoPermit2_InvalidSettlementAmount(t *testing.T) { + signer := newMockSigner() + req := buildValidRequirements() + req.Amount = "not-a-number" + + _, err := SettleUptoPermit2(context.Background(), signer, buildValidPayload(testFacilitatorAddr), req, buildValidUptoPayload(testFacilitatorAddr), nil, false) + if err == nil { + t.Fatal("expected error on invalid settlement amount") + } +} + +func TestSettleUptoPermit2_WriteContractFails(t *testing.T) { + signer := newMockSigner() + signer.writeContractError = errors.New("out of gas") + + _, err := SettleUptoPermit2(context.Background(), signer, buildValidPayload(testFacilitatorAddr), buildValidRequirements(), buildValidUptoPayload(testFacilitatorAddr), nil, false) + if err == nil { + t.Fatal("expected error on WriteContract failure") + } +} + +func TestSettleUptoPermit2_ReceiptStatusFailed(t *testing.T) { + signer := newMockSigner() + signer.receiptResult = &evm.TransactionReceipt{Status: evm.TxStatusFailed, TxHash: "0xfail"} + + _, err := SettleUptoPermit2(context.Background(), signer, buildValidPayload(testFacilitatorAddr), buildValidRequirements(), buildValidUptoPayload(testFacilitatorAddr), nil, false) + assertSettleError(t, err, ErrUptoTransactionFailed) +} + +func TestSettleUptoPermit2_ReceiptError(t *testing.T) { + signer := newMockSigner() + signer.receiptError = errors.New("timeout") + + _, err := SettleUptoPermit2(context.Background(), signer, buildValidPayload(testFacilitatorAddr), buildValidRequirements(), buildValidUptoPayload(testFacilitatorAddr), nil, false) + assertSettleError(t, err, ErrUptoFailedToGetReceipt) +} + +func TestSettleUptoPermit2_VerifyFails_EOAPayer(t *testing.T) { + // Payer is EOA → sig verify fails → settle re-verify returns invalid + signer := newMockSigner() + signer.getCodeResult = []byte{} // EOA + + _, err := SettleUptoPermit2(context.Background(), signer, buildValidPayload(testFacilitatorAddr), buildValidRequirements(), buildValidUptoPayload(testFacilitatorAddr), nil, false) + if err == nil { + t.Fatal("expected error when verify fails during settle") + } +} + +func TestSettleUptoPermit2_WithEIP2612_ZeroSettlement(t *testing.T) { + // EIP-2612 extension present but settlement is zero — should skip on-chain tx + signer := newMockSigner() + p := buildValidUptoPayload(testFacilitatorAddr) + payload := buildValidPayload(testFacilitatorAddr) + payload.Extensions = map[string]interface{}{ + eip2612gassponsor.EIP2612GasSponsoring.Key(): map[string]interface{}{ + "info": map[string]interface{}{ + "from": testPayerAddr, + "asset": testTokenAddr, + "spender": evm.PERMIT2Address, + "amount": "115792089237316195423570985008687907853269984665640564039457584007913129639935", + "nonce": "0", + "deadline": futureDeadline(), + "signature": dummySig, + "version": "1", + }, + }, + } + + req := buildValidRequirements() + req.Amount = "0" + + resp, err := SettleUptoPermit2(context.Background(), signer, payload, req, p, nil, false) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if !resp.Success || resp.Amount != "0" { + t.Errorf("expected zero-settlement success, got success=%v amount=%q", resp.Success, resp.Amount) + } +} + +// ─── BuildUptoPermit2SettleArgs tests ──────────────────────────────────────── + +func TestBuildUptoPermit2SettleArgs_Success(t *testing.T) { + p := buildValidUptoPayload(testFacilitatorAddr) + settlement := big.NewInt(500) + + args, err := BuildUptoPermit2SettleArgs(p, settlement) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if args.SettlementAmount.Cmp(settlement) != 0 { + t.Errorf("expected settlementAmount=%s, got %s", settlement, args.SettlementAmount) + } + if args.Permit.Permitted.Amount.String() != testAmount { + t.Errorf("expected permitted amount=%s, got %s", testAmount, args.Permit.Permitted.Amount) + } + // Witness.Facilitator should be the facilitator address + expectedFacilitator := args.Witness.Facilitator.Hex() + if expectedFacilitator == "0x0000000000000000000000000000000000000000" { + t.Error("facilitator address should not be zero") + } +} + +func TestBuildUptoPermit2SettleArgs_InvalidPermittedAmount(t *testing.T) { + p := buildValidUptoPayload(testFacilitatorAddr) + p.Permit2Authorization.Permitted.Amount = "not-a-number" + + _, err := BuildUptoPermit2SettleArgs(p, big.NewInt(1)) + if err == nil { + t.Fatal("expected error on invalid permitted amount") + } +} + +func TestBuildUptoPermit2SettleArgs_InvalidNonce(t *testing.T) { + p := buildValidUptoPayload(testFacilitatorAddr) + p.Permit2Authorization.Nonce = "not-a-nonce" + + _, err := BuildUptoPermit2SettleArgs(p, big.NewInt(1)) + if err == nil { + t.Fatal("expected error on invalid nonce") + } +} + +func TestBuildUptoPermit2SettleArgs_InvalidDeadline(t *testing.T) { + p := buildValidUptoPayload(testFacilitatorAddr) + p.Permit2Authorization.Deadline = "not-a-deadline" + + _, err := BuildUptoPermit2SettleArgs(p, big.NewInt(1)) + if err == nil { + t.Fatal("expected error on invalid deadline") + } +} + +func TestBuildUptoPermit2SettleArgs_InvalidValidAfter(t *testing.T) { + p := buildValidUptoPayload(testFacilitatorAddr) + p.Permit2Authorization.Witness.ValidAfter = "not-a-timestamp" + + _, err := BuildUptoPermit2SettleArgs(p, big.NewInt(1)) + if err == nil { + t.Fatal("expected error on invalid validAfter") + } +} + +func TestBuildUptoPermit2SettleArgs_InvalidSignature(t *testing.T) { + p := buildValidUptoPayload(testFacilitatorAddr) + p.Signature = "not-hex" + + _, err := BuildUptoPermit2SettleArgs(p, big.NewInt(1)) + if err == nil { + t.Fatal("expected error on invalid signature hex") + } +} + +// ─── validateEip2612PermitForPayment tests ─────────────────────────────────── + +func TestValidateEip2612PermitForPayment_Valid(t *testing.T) { + info := makeValidEip2612Info(testPayerAddr, testTokenAddr) + if got := validateEip2612PermitForPayment(info, testPayerAddr, testTokenAddr); got != "" { + t.Errorf("expected valid, got: %s", got) + } +} + +func TestValidateEip2612PermitForPayment_FromMismatch(t *testing.T) { + info := makeValidEip2612Info(testPayerAddr, testTokenAddr) + info.From = "0x0000000000000000000000000000000000000001" + if got := validateEip2612PermitForPayment(info, testPayerAddr, testTokenAddr); got != "eip2612_from_mismatch" { + t.Errorf("expected eip2612_from_mismatch, got: %s", got) + } +} + +func TestValidateEip2612PermitForPayment_AssetMismatch(t *testing.T) { + info := makeValidEip2612Info(testPayerAddr, testTokenAddr) + info.Asset = "0x0000000000000000000000000000000000000001" + if got := validateEip2612PermitForPayment(info, testPayerAddr, testTokenAddr); got != "eip2612_asset_mismatch" { + t.Errorf("expected eip2612_asset_mismatch, got: %s", got) + } +} + +func TestValidateEip2612PermitForPayment_WrongSpender(t *testing.T) { + info := makeValidEip2612Info(testPayerAddr, testTokenAddr) + info.Spender = "0x0000000000000000000000000000000000000001" + if got := validateEip2612PermitForPayment(info, testPayerAddr, testTokenAddr); got != "eip2612_spender_not_permit2" { + t.Errorf("expected eip2612_spender_not_permit2, got: %s", got) + } +} + +func TestValidateEip2612PermitForPayment_ExpiredDeadline(t *testing.T) { + info := makeValidEip2612Info(testPayerAddr, testTokenAddr) + info.Deadline = "1000000000" // 2001 + if got := validateEip2612PermitForPayment(info, testPayerAddr, testTokenAddr); got != "eip2612_deadline_expired" { + t.Errorf("expected eip2612_deadline_expired, got: %s", got) + } +} + +// makeValidEip2612Info creates a well-formed EIP-2612 info for testing. +func makeValidEip2612Info(from, asset string) *eip2612gassponsor.Info { + return &eip2612gassponsor.Info{ + From: from, + Asset: asset, + Spender: evm.PERMIT2Address, + Amount: "115792089237316195423570985008687907853269984665640564039457584007913129639935", + Nonce: "0", + Deadline: futureDeadline(), + Signature: dummySig, + Version: "1", + } +} + +// ─── splitEip2612Signature tests ───────────────────────────────────────────── + +func TestSplitEip2612Signature_Valid(t *testing.T) { + sig := "0x" + + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + // r (32 bytes) + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" + // s (32 bytes) + "1c" // v = 28 + + v, r, s, err := splitEip2612Signature(sig) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if v != 28 { + t.Errorf("expected v=28, got %d", v) + } + for i, b := range r { + if b != 0xaa { + t.Fatalf("r[%d] expected 0xaa, got 0x%02x", i, b) + } + } + for i, b := range s { + if b != 0xbb { + t.Fatalf("s[%d] expected 0xbb, got 0x%02x", i, b) + } + } +} + +func TestSplitEip2612Signature_TooShort(t *testing.T) { + if _, _, _, err := splitEip2612Signature("0xaabb"); err == nil { + t.Fatal("expected error for short signature") + } +} + +func TestSplitEip2612Signature_InvalidHex(t *testing.T) { + if _, _, _, err := splitEip2612Signature("not-hex"); err == nil { + t.Fatal("expected error for non-hex input") + } +} + +// ─── parseUptoPermit2Error tests ───────────────────────────────────────────── + +func TestParseUptoPermit2Error(t *testing.T) { + cases := []struct { + msg string + expected string + }{ + {"Permit2612AmountMismatch blah", ErrPermit2612AmountMismatch}, + {"execution reverted: InvalidAmount", ErrPermit2InvalidAmount}, + {"execution reverted: InvalidDestination", ErrPermit2InvalidDestination}, + {"execution reverted: InvalidOwner", ErrPermit2InvalidOwner}, + {"execution reverted: PaymentTooEarly", ErrPermit2PaymentTooEarly}, + {"execution reverted: InvalidSignature", ErrPermit2InvalidSignature}, + {"execution reverted: SignatureExpired", ErrPermit2InvalidSignature}, + {"execution reverted: InvalidNonce", ErrPermit2InvalidNonce}, + {"erc20_approval_tx_failed: something", ErrErc20ApprovalBroadcastFailed}, + {"execution reverted: AmountExceedsPermitted", ErrUptoAmountExceedsPermitted}, + {"execution reverted: UnauthorizedFacilitator", ErrUptoUnauthorizedFacilitator}, + {"unknown revert reason", ErrUptoTransactionFailed}, + } + + for _, tc := range cases { + t.Run(tc.msg, func(t *testing.T) { + got := parseUptoPermit2Error(errors.New(tc.msg)) + if got != tc.expected { + t.Errorf("expected %s, got %s", tc.expected, got) + } + }) + } +} + +// ─── UptoEvmScheme wrapper tests ───────────────────────────────────────────── + +func TestUptoEvmScheme_Scheme(t *testing.T) { + if s := NewUptoEvmScheme(newMockSigner(), nil).Scheme(); s != evm.SchemeUpto { + t.Errorf("expected %q, got %q", evm.SchemeUpto, s) + } +} + +func TestUptoEvmScheme_CaipFamily(t *testing.T) { + if cf := NewUptoEvmScheme(newMockSigner(), nil).CaipFamily(); cf != "eip155:*" { + t.Errorf("expected eip155:*, got %s", cf) + } +} + +func TestUptoEvmScheme_GetExtra_ReturnsFacilitatorAddress(t *testing.T) { + s := NewUptoEvmScheme(newMockSigner(testFacilitatorAddr), nil) + extra := s.GetExtra(x402.Network(testNetwork)) + if extra == nil { + t.Fatal("expected extra, got nil") + } + if extra["facilitatorAddress"] != testFacilitatorAddr { + t.Errorf("expected facilitatorAddress=%s, got %v", testFacilitatorAddr, extra["facilitatorAddress"]) + } +} + +func TestUptoEvmScheme_GetExtra_NoAddresses(t *testing.T) { + s := NewUptoEvmScheme(&mockFacilitatorSigner{addresses: []string{}}, nil) + if extra := s.GetExtra(x402.Network(testNetwork)); extra != nil { + t.Errorf("expected nil extra with no addresses, got %v", extra) + } +} + +func TestUptoEvmScheme_GetExtra_MultipleAddresses_UsesOneFromPool(t *testing.T) { + addr1 := "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + addr2 := "0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" + pool := map[string]bool{addr1: true, addr2: true} + s := NewUptoEvmScheme(newMockSigner(addr1, addr2), nil) + extra := s.GetExtra(x402.Network(testNetwork)) + got, ok := extra["facilitatorAddress"].(string) + if !ok || !pool[got] { + t.Errorf("expected one of pool addresses, got %v", extra["facilitatorAddress"]) + } +} + +func TestUptoEvmScheme_GetSigners(t *testing.T) { + s := NewUptoEvmScheme(newMockSigner(testFacilitatorAddr), nil) + signers := s.GetSigners(x402.Network(testNetwork)) + if len(signers) != 1 || signers[0] != testFacilitatorAddr { + t.Errorf("unexpected signers: %v", signers) + } +} + +func TestUptoEvmScheme_Config_DefaultSimulateInSettle(t *testing.T) { + s := NewUptoEvmScheme(newMockSigner(), nil) + if s.config.SimulateInSettle { + t.Error("default SimulateInSettle should be false") + } +} + +func TestUptoEvmScheme_Config_CustomSimulateInSettle(t *testing.T) { + s := NewUptoEvmScheme(newMockSigner(), &UptoEvmSchemeConfig{SimulateInSettle: true}) + if !s.config.SimulateInSettle { + t.Error("expected SimulateInSettle=true from config") + } +} + +func TestUptoEvmScheme_Verify_UnsupportedPayload(t *testing.T) { + s := NewUptoEvmScheme(newMockSigner(), nil) + payload := types.PaymentPayload{ + X402Version: 2, + Payload: map[string]interface{}{"authorization": map[string]interface{}{"from": testPayerAddr}}, + Accepted: buildValidRequirements(), + } + if _, err := s.Verify(context.Background(), payload, buildValidRequirements(), nil); err == nil { + t.Fatal("expected error for EIP-3009 payload passed to upto scheme") + } +} + +func TestUptoEvmScheme_Settle_UnsupportedPayload(t *testing.T) { + s := NewUptoEvmScheme(newMockSigner(), nil) + payload := types.PaymentPayload{ + X402Version: 2, + Payload: map[string]interface{}{"authorization": map[string]interface{}{}}, + Accepted: buildValidRequirements(), + } + if _, err := s.Settle(context.Background(), payload, buildValidRequirements(), nil); err == nil { + t.Fatal("expected error for unsupported payload in settle") + } +} + +func TestUptoEvmScheme_Verify_Valid(t *testing.T) { + signer := newMockSigner() + s := NewUptoEvmScheme(signer, nil) + + p := buildValidUptoPayload(testFacilitatorAddr) + payload := types.PaymentPayload{ + X402Version: 2, + Payload: p.ToMap(), + Accepted: buildValidRequirements(), + } + + resp, err := s.Verify(context.Background(), payload, buildValidRequirements(), nil) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if !resp.IsValid { + t.Errorf("expected valid, got %s", resp.InvalidReason) + } +} + +func TestUptoEvmScheme_Settle_Valid(t *testing.T) { + signer := newMockSigner() + s := NewUptoEvmScheme(signer, nil) + + p := buildValidUptoPayload(testFacilitatorAddr) + payload := types.PaymentPayload{ + X402Version: 2, + Payload: p.ToMap(), + Accepted: buildValidRequirements(), + } + + resp, err := s.Settle(context.Background(), payload, buildValidRequirements(), nil) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if !resp.Success { + t.Errorf("expected success, got %s", resp.ErrorReason) + } + if resp.Amount != testAmount { + t.Errorf("expected Amount=%q, got %q", testAmount, resp.Amount) + } +} + +// ─── Helpers ────────────────────────────────────────────────────────────────── + +func assertVerifyError(t *testing.T, err error, want string) { + t.Helper() + if err == nil { + t.Fatalf("expected error %q, got nil", want) + } + var ve *x402.VerifyError + if !errors.As(err, &ve) { + t.Fatalf("expected *x402.VerifyError, got %T: %v", err, err) + } + if ve.InvalidReason != want { + t.Errorf("expected InvalidReason=%q, got %q", want, ve.InvalidReason) + } +} + +func assertSettleError(t *testing.T, err error, want string) { + t.Helper() + if err == nil { + t.Fatalf("expected error %q, got nil", want) + } + var se *x402.SettleError + if !errors.As(err, &se) { + t.Fatalf("expected *x402.SettleError, got %T: %v", err, err) + } + if se.ErrorReason != want { + t.Errorf("expected ErrorReason=%q, got %q", want, se.ErrorReason) + } +} diff --git a/go/mechanisms/evm/upto/facilitator/scheme.go b/go/mechanisms/evm/upto/facilitator/scheme.go new file mode 100644 index 0000000000..9feb489675 --- /dev/null +++ b/go/mechanisms/evm/upto/facilitator/scheme.go @@ -0,0 +1,99 @@ +package facilitator + +import ( + "context" + "fmt" + "math/rand" + + x402 "github.com/coinbase/x402/go" + "github.com/coinbase/x402/go/mechanisms/evm" + "github.com/coinbase/x402/go/types" +) + +type UptoEvmSchemeConfig struct { + SimulateInSettle bool +} + +// UptoEvmScheme implements the SchemeNetworkFacilitator interface for EVM upto payments (V2). +// Only supports Permit2 (no EIP-3009 path). +type UptoEvmScheme struct { + signer evm.FacilitatorEvmSigner + config UptoEvmSchemeConfig +} + +func NewUptoEvmScheme(signer evm.FacilitatorEvmSigner, config *UptoEvmSchemeConfig) *UptoEvmScheme { + cfg := UptoEvmSchemeConfig{} + if config != nil { + cfg = *config + } + return &UptoEvmScheme{ + signer: signer, + config: cfg, + } +} + +func (f *UptoEvmScheme) Scheme() string { + return evm.SchemeUpto +} + +// CaipFamily returns the CAIP family pattern this facilitator supports +func (f *UptoEvmScheme) CaipFamily() string { + return "eip155:*" +} + +// GetExtra returns mechanism-specific extra data for the supported kinds endpoint. +// For upto, returns the facilitatorAddress so clients include it in their signed witness. +func (f *UptoEvmScheme) GetExtra(_ x402.Network) map[string]interface{} { + addresses := f.signer.GetAddresses() + if len(addresses) == 0 { + return nil + } + return map[string]interface{}{ + "facilitatorAddress": addresses[rand.Intn(len(addresses))], + } +} + +// GetSigners returns signer addresses used by this facilitator. +func (f *UptoEvmScheme) GetSigners(_ x402.Network) []string { + return f.signer.GetAddresses() +} + +// Verify verifies a V2 upto payment payload against requirements. +func (f *UptoEvmScheme) Verify( + ctx context.Context, + payload types.PaymentPayload, + requirements types.PaymentRequirements, + fctx *x402.FacilitatorContext, +) (*x402.VerifyResponse, error) { + if !evm.IsUptoPermit2Payload(payload.Payload) { + return nil, x402.NewVerifyError(ErrUptoInvalidPayload, "", "unsupported payload type: expected upto permit2 payload") + } + + permit2Payload, err := evm.UptoPermit2PayloadFromMap(payload.Payload) + if err != nil { + return nil, x402.NewVerifyError(ErrUptoInvalidPayload, "", fmt.Sprintf("failed to parse upto Permit2 payload: %s", err.Error())) + } + + return VerifyUptoPermit2(ctx, f.signer, payload, requirements, permit2Payload, fctx, true) +} + +// Settle settles a V2 upto payment on-chain. +func (f *UptoEvmScheme) Settle( + ctx context.Context, + payload types.PaymentPayload, + requirements types.PaymentRequirements, + fctx *x402.FacilitatorContext, +) (*x402.SettleResponse, error) { + if !evm.IsUptoPermit2Payload(payload.Payload) { + network := x402.Network(payload.Accepted.Network) + return nil, x402.NewSettleError(ErrUptoInvalidPayload, "", network, "", "unsupported payload type: expected upto permit2 payload") + } + + permit2Payload, err := evm.UptoPermit2PayloadFromMap(payload.Payload) + if err != nil { + network := x402.Network(payload.Accepted.Network) + return nil, x402.NewSettleError(ErrUptoInvalidPayload, "", network, "", fmt.Sprintf("failed to parse upto Permit2 payload: %s", err.Error())) + } + + return SettleUptoPermit2(ctx, f.signer, payload, requirements, permit2Payload, fctx, f.config.SimulateInSettle) +} diff --git a/go/mechanisms/evm/upto/server/errors.go b/go/mechanisms/evm/upto/server/errors.go new file mode 100644 index 0000000000..3864d0d9ac --- /dev/null +++ b/go/mechanisms/evm/upto/server/errors.go @@ -0,0 +1,16 @@ +package server + +const ( + ErrAmountMustBeString = "invalid_upto_evm_server_amount_must_be_string" + ErrAssetAddressRequired = "invalid_upto_evm_server_asset_address_required" + ErrFailedToParsePrice = "invalid_upto_evm_server_failed_to_parse_price" + ErrUnsupportedPriceType = "invalid_upto_evm_server_unsupported_price_type" + ErrFailedToConvertAmount = "invalid_upto_evm_server_failed_to_convert_amount" + ErrNoAssetSpecified = "invalid_upto_evm_server_no_asset_specified" + ErrFailedToParseAmount = "invalid_upto_evm_server_failed_to_parse_amount" + ErrInvalidPayToAddress = "invalid_upto_evm_server_invalid_payto_address" + ErrAmountRequired = "invalid_upto_evm_server_amount_required" + ErrInvalidAmount = "invalid_upto_evm_server_invalid_amount" + ErrInvalidAsset = "invalid_upto_evm_server_invalid_asset" + ErrInvalidTokenAmount = "invalid_upto_evm_server_invalid_token_amount" +) diff --git a/go/mechanisms/evm/upto/server/scheme.go b/go/mechanisms/evm/upto/server/scheme.go new file mode 100644 index 0000000000..c24c6f1b62 --- /dev/null +++ b/go/mechanisms/evm/upto/server/scheme.go @@ -0,0 +1,258 @@ +package server + +import ( + "context" + "errors" + "fmt" + "math/big" + "strconv" + "strings" + + x402 "github.com/coinbase/x402/go" + "github.com/coinbase/x402/go/mechanisms/evm" + "github.com/coinbase/x402/go/types" +) + +// UptoEvmScheme implements the SchemeNetworkServer interface for EVM upto payments (V2). +// Always uses Permit2 (no EIP-3009 path). +type UptoEvmScheme struct { + moneyParsers []x402.MoneyParser +} + +func NewUptoEvmScheme() *UptoEvmScheme { + return &UptoEvmScheme{ + moneyParsers: []x402.MoneyParser{}, + } +} + +func (s *UptoEvmScheme) Scheme() string { + return evm.SchemeUpto +} + +// GetAssetDecimals implements AssetDecimalsProvider. Returns the decimal precision for the +// given asset on the given network, falling back to 6 if the asset is not recognized. +func (s *UptoEvmScheme) GetAssetDecimals(asset string, network x402.Network) int { + info, err := evm.GetAssetInfo(string(network), asset) + if err != nil || info == nil { + return 6 + } + return info.Decimals +} + +func (s *UptoEvmScheme) RegisterMoneyParser(parser x402.MoneyParser) *UptoEvmScheme { + s.moneyParsers = append(s.moneyParsers, parser) + return s +} + +func (s *UptoEvmScheme) ParsePrice(price x402.Price, network x402.Network) (x402.AssetAmount, error) { + if priceMap, ok := price.(map[string]interface{}); ok { + if amountVal, hasAmount := priceMap["amount"]; hasAmount { + amountStr, ok := amountVal.(string) + if !ok { + return x402.AssetAmount{}, errors.New(ErrAmountMustBeString) + } + + asset := "" + if assetVal, hasAsset := priceMap["asset"]; hasAsset { + if assetStr, ok := assetVal.(string); ok { + asset = assetStr + } + } + + if asset == "" { + return x402.AssetAmount{}, errors.New(ErrAssetAddressRequired) + } + + extra := make(map[string]interface{}) + if extraVal, hasExtra := priceMap["extra"]; hasExtra { + if extraMap, ok := extraVal.(map[string]interface{}); ok { + extra = extraMap + } + } + + return x402.AssetAmount{ + Amount: amountStr, + Asset: asset, + Extra: extra, + }, nil + } + } + + decimalAmount, err := s.parseMoneyToDecimal(price) + if err != nil { + return x402.AssetAmount{}, err + } + + for _, parser := range s.moneyParsers { + result, err := parser(decimalAmount, network) + if err != nil { + continue + } + if result != nil { + return *result, nil + } + } + + return s.defaultMoneyConversion(decimalAmount, network) +} + +func (s *UptoEvmScheme) parseMoneyToDecimal(price x402.Price) (float64, error) { + switch v := price.(type) { + case string: + cleanPrice := strings.TrimSpace(v) + cleanPrice = strings.TrimPrefix(cleanPrice, "$") + cleanPrice = strings.TrimSpace(cleanPrice) + + amount, err := strconv.ParseFloat(cleanPrice, 64) + if err != nil { + return 0, fmt.Errorf(ErrFailedToParsePrice+": '%s': %w", v, err) + } + return amount, nil + + case float64: + return v, nil + + case int: + return float64(v), nil + + case int64: + return float64(v), nil + + default: + return 0, fmt.Errorf(ErrUnsupportedPriceType+": %T", price) + } +} + +func (s *UptoEvmScheme) defaultMoneyConversion(amount float64, network x402.Network) (x402.AssetAmount, error) { + networkStr := string(network) + + config, err := evm.GetNetworkConfig(networkStr) + if err != nil { + return x402.AssetAmount{}, err + } + + if config.DefaultAsset.Address == "" { + return x402.AssetAmount{}, fmt.Errorf("no default stablecoin configured for network %s; use RegisterMoneyParser or specify an explicit AssetAmount", networkStr) + } + + extra := map[string]interface{}{ + "name": config.DefaultAsset.Name, + "version": config.DefaultAsset.Version, + "assetTransferMethod": "permit2", + } + + oneUnit := float64(1) + for i := 0; i < config.DefaultAsset.Decimals; i++ { + oneUnit *= 10 + } + + if amount >= oneUnit && amount == float64(int64(amount)) { + return x402.AssetAmount{ + Asset: config.DefaultAsset.Address, + Amount: fmt.Sprintf("%.0f", amount), + Extra: extra, + }, nil + } + + amountStr := fmt.Sprintf("%.6f", amount) + parsedAmount, err := evm.ParseAmount(amountStr, config.DefaultAsset.Decimals) + if err != nil { + return x402.AssetAmount{}, fmt.Errorf(ErrFailedToConvertAmount+": %w", err) + } + + return x402.AssetAmount{ + Asset: config.DefaultAsset.Address, + Amount: parsedAmount.String(), + Extra: extra, + }, nil +} + +// EnhancePaymentRequirements adds upto payment requirements. +func (s *UptoEvmScheme) EnhancePaymentRequirements( + ctx context.Context, + requirements types.PaymentRequirements, + supportedKind types.SupportedKind, + extensionKeys []string, +) (types.PaymentRequirements, error) { + networkStr := string(requirements.Network) + + var assetInfo *evm.AssetInfo + var err error + if requirements.Asset != "" { + assetInfo, err = evm.GetAssetInfo(networkStr, requirements.Asset) + if err != nil { + return requirements, err + } + } else { + assetInfo, err = evm.GetAssetInfo(networkStr, "") + if err != nil { + return requirements, fmt.Errorf(ErrNoAssetSpecified+": %w", err) + } + requirements.Asset = assetInfo.Address + } + + if requirements.Amount != "" && strings.Contains(requirements.Amount, ".") { + amount, err := evm.ParseAmount(requirements.Amount, assetInfo.Decimals) + if err != nil { + return requirements, fmt.Errorf(ErrFailedToParseAmount+": %w", err) + } + requirements.Amount = amount.String() + } + + if requirements.Extra == nil { + requirements.Extra = make(map[string]interface{}) + } + + // Upto always includes name/version and always sets permit2 + if _, ok := requirements.Extra["name"]; !ok { + requirements.Extra["name"] = assetInfo.Name + } + if _, ok := requirements.Extra["version"]; !ok { + requirements.Extra["version"] = assetInfo.Version + } + requirements.Extra["assetTransferMethod"] = "permit2" + + // Copy facilitatorAddress from supportedKind.Extra if present + if supportedKind.Extra != nil { + if facilitatorAddr, ok := supportedKind.Extra["facilitatorAddress"].(string); ok && facilitatorAddr != "" { + requirements.Extra["facilitatorAddress"] = evm.NormalizeAddress(facilitatorAddr) + } + } + + // Copy extensions from supportedKind if provided + if supportedKind.Extra != nil { + for _, key := range extensionKeys { + if val, ok := supportedKind.Extra[key]; ok { + requirements.Extra[key] = val + } + } + } + + return requirements, nil +} + +// ValidatePaymentRequirements validates that requirements are valid for this scheme. +func (s *UptoEvmScheme) ValidatePaymentRequirements(requirements x402.PaymentRequirements) error { + if !evm.IsValidAddress(requirements.PayTo) { + return fmt.Errorf(ErrInvalidPayToAddress+": %s", requirements.PayTo) + } + + if requirements.Amount == "" { + return errors.New(ErrAmountRequired) + } + + amount, ok := new(big.Int).SetString(requirements.Amount, 10) + if !ok || amount.Sign() <= 0 { + return fmt.Errorf(ErrInvalidAmount+": %s", requirements.Amount) + } + + if requirements.Asset != "" && !evm.IsValidAddress(requirements.Asset) { + networkStr := string(requirements.Network) + _, err := evm.GetAssetInfo(networkStr, requirements.Asset) + if err != nil { + return fmt.Errorf(ErrInvalidAsset+": %s", requirements.Asset) + } + } + + return nil +} diff --git a/go/mechanisms/evm/upto/server/server_money_parser_test.go b/go/mechanisms/evm/upto/server/server_money_parser_test.go new file mode 100644 index 0000000000..b26b858230 --- /dev/null +++ b/go/mechanisms/evm/upto/server/server_money_parser_test.go @@ -0,0 +1,178 @@ +package server + +import ( + "context" + "fmt" + "testing" + + x402 "github.com/coinbase/x402/go" +) + +const baseMainnetUSDC = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913" + +func TestParsePrice_DefaultNoCustomParsers(t *testing.T) { + server := NewUptoEvmScheme() + + result, err := server.ParsePrice(10.0, "eip155:8453") + if err != nil { + t.Fatalf("Expected no error, got %v", err) + } + + if result.Asset != baseMainnetUSDC { + t.Errorf("Expected default USDC, got %s", result.Asset) + } + + expectedAmount := "10000000" + if result.Amount != expectedAmount { + t.Errorf("Expected amount %s, got %s", expectedAmount, result.Amount) + } + + // Upto always includes assetTransferMethod: "permit2" in default conversion + if result.Extra["assetTransferMethod"] != "permit2" { + t.Errorf("Expected assetTransferMethod='permit2', got %v", result.Extra["assetTransferMethod"]) + } + + // Upto always includes name and version + if result.Extra["name"] == nil { + t.Error("Expected name in extra, got nil") + } + if result.Extra["version"] == nil { + t.Error("Expected version in extra, got nil") + } +} + +func TestParsePrice_CustomParser(t *testing.T) { + server := NewUptoEvmScheme() + + server.RegisterMoneyParser(func(amount float64, network x402.Network) (*x402.AssetAmount, error) { + if amount > 100 { + return &x402.AssetAmount{ + Amount: fmt.Sprintf("%.0f", amount*1e18), + Asset: "0x6B175474E89094C44Da98b954EedeAC495271d0F", + Extra: map[string]interface{}{"token": "DAI"}, + }, nil + } + return nil, nil + }) + + result1, err := server.ParsePrice(150.0, "eip155:8453") + if err != nil { + t.Fatalf("Expected no error, got %v", err) + } + if result1.Extra["token"] != "DAI" { + t.Errorf("Expected token='DAI', got %v", result1.Extra["token"]) + } + + result2, err := server.ParsePrice(50.0, "eip155:8453") + if err != nil { + t.Fatalf("Expected no error, got %v", err) + } + if result2.Asset != baseMainnetUSDC { + t.Errorf("Expected USDC asset, got %s", result2.Asset) + } +} + +func TestParsePrice_StringPrices(t *testing.T) { + server := NewUptoEvmScheme() + + tests := []struct { + name string + price string + expectedAsset string + }{ + {"Dollar format", "$10.50", baseMainnetUSDC}, + {"Plain decimal", "25.50", baseMainnetUSDC}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result, err := server.ParsePrice(tt.price, "eip155:8453") + if err != nil { + t.Fatalf("Expected no error, got %v", err) + } + if result.Asset != tt.expectedAsset { + t.Errorf("Expected asset %s, got %s", tt.expectedAsset, result.Asset) + } + }) + } +} + +func TestParsePrice_AssetAmountPassthrough(t *testing.T) { + server := NewUptoEvmScheme() + + price := map[string]interface{}{ + "amount": "1000000", + "asset": "0x036CbD53842c5426634e7929541eC2318f3dCF7e", + "extra": map[string]interface{}{ + "assetTransferMethod": "permit2", + }, + } + + result, err := server.ParsePrice(price, "eip155:84532") + if err != nil { + t.Fatalf("Expected no error, got %v", err) + } + + if result.Amount != "1000000" { + t.Errorf("Expected amount 1000000, got %s", result.Amount) + } + if result.Asset != "0x036CbD53842c5426634e7929541eC2318f3dCF7e" { + t.Errorf("Expected asset pass-through, got %s", result.Asset) + } +} + +func TestRegisterMoneyParser_Chainability(t *testing.T) { + server := NewUptoEvmScheme() + + result := server. + RegisterMoneyParser(func(amount float64, network x402.Network) (*x402.AssetAmount, error) { + return nil, nil + }). + RegisterMoneyParser(func(amount float64, network x402.Network) (*x402.AssetAmount, error) { + return nil, nil + }) + + if result != server { + t.Error("Expected RegisterMoneyParser to return server for chaining") + } +} + +func TestEnhancePaymentRequirements_SetsPermit2(t *testing.T) { + server := NewUptoEvmScheme() + + requirements := x402.PaymentRequirements{ + Scheme: "upto", + Network: "eip155:84532", + Amount: "1000", + Asset: "0x036CbD53842c5426634e7929541eC2318f3dCF7e", + PayTo: "0x1234567890123456789012345678901234567890", + } + + supportedKind := x402.SupportedKind{ + Scheme: "upto", + Network: "eip155:84532", + Extra: map[string]interface{}{ + "facilitatorAddress": "0xABCDEF1234567890ABCDEF1234567890ABCDEF12", + }, + } + + enhanced, err := server.EnhancePaymentRequirements(context.TODO(), requirements, supportedKind, nil) + if err != nil { + t.Fatalf("Expected no error, got %v", err) + } + + if enhanced.Extra["assetTransferMethod"] != "permit2" { + t.Errorf("Expected assetTransferMethod='permit2', got %v", enhanced.Extra["assetTransferMethod"]) + } + + if enhanced.Extra["facilitatorAddress"] == nil { + t.Error("Expected facilitatorAddress in extra") + } + + if enhanced.Extra["name"] == nil { + t.Error("Expected name in extra") + } + if enhanced.Extra["version"] == nil { + t.Error("Expected version in extra") + } +} diff --git a/go/mechanisms/evm/v1/network.go b/go/mechanisms/evm/v1/network.go index 1757ec862c..85259ddb73 100644 --- a/go/mechanisms/evm/v1/network.go +++ b/go/mechanisms/evm/v1/network.go @@ -27,6 +27,8 @@ var NetworkChainIDs = map[string]*big.Int{ "skale-base-sepolia": big.NewInt(324705682), "megaeth": big.NewInt(4326), "monad": big.NewInt(143), + "stable": big.NewInt(988), + "stable-testnet": big.NewInt(2201), } // NetworkConfigs maps v1 legacy network names to their full configuration. @@ -68,6 +70,33 @@ var NetworkConfigs = map[string]evm.NetworkConfig{ Decimals: evm.DefaultDecimals, }, }, + "stable": { + ChainID: big.NewInt(988), + DefaultAsset: evm.AssetInfo{ + Address: "0x779Ded0c9e1022225f8E0630b35a9b54bE713736", + Name: "USDT0", + Version: "1", + Decimals: evm.DefaultDecimals, + }, + }, + "stable-testnet": { + ChainID: big.NewInt(2201), + DefaultAsset: evm.AssetInfo{ + Address: "0x78Cf24370174180738C5B8E352B6D14c83a6c9A9", + Name: "USDT0", + Version: "1", + Decimals: evm.DefaultDecimals, + }, + }, + "polygon": { + ChainID: big.NewInt(137), + DefaultAsset: evm.AssetInfo{ + Address: "0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359", + Name: "USD Coin", + Version: "2", + Decimals: evm.DefaultDecimals, + }, + }, } // Networks is the list of all v1 network names. diff --git a/go/mechanisms/evm/verify_universal.go b/go/mechanisms/evm/verify_universal.go index 2119f6bf12..5c38f22dc0 100644 --- a/go/mechanisms/evm/verify_universal.go +++ b/go/mechanisms/evm/verify_universal.go @@ -16,12 +16,13 @@ import ( // - ERC-6492: Counterfactual signatures (undeployed contracts with deployment info) // // The verification flow: -// 1. Parse ERC-6492 wrapper if present to extract inner signature -// 2. If inner signature is exactly 65 bytes AND no factory: EOA path (optimization - skips GetCode) -// 3. Otherwise: check if contract is deployed (GetCode) -// 4. If undeployed + has deployment info + allowUndeployed: accept (deploy in settle) -// 5. If undeployed without deployment info: fallback to EOA verification -// 6. If deployed: use EIP-1271 verification +// 1. Parse ERC-6492 wrapper if present to extract inner signature +// 2. If inner signature is exactly 65 bytes AND no factory: EOA path (optimization - skips GetCode) +// 3. Otherwise: check if contract is deployed (GetCode) +// 4. If undeployed + has deployment info + allowUndeployed: classify as counterfactual, +// but do not treat it as valid until a later onchain simulation succeeds +// 5. If undeployed without deployment info: fallback to EOA verification +// 6. If deployed: use EIP-1271 verification // // Args: // @@ -62,7 +63,10 @@ func VerifyUniversalSignature( // EOA signature - use ECDSA recovery directly (avoids GetCode call) signerAddr := common.HexToAddress(signerAddress) valid, err := VerifyEOASignature(hash[:], sigData.InnerSignature, signerAddr) - return valid, sigData, err + if err == nil { + return valid, sigData, nil + } + // EOA verification failed - could be smart wallet with 65-byte sig, fall through to check GetCode } // Step 4: Potential smart wallet signature - check if contract is deployed @@ -84,16 +88,19 @@ func VerifyUniversalSignature( if !allowUndeployed { return false, nil, errors.New(ErrUndeployedSmartWallet + ": undeployed not allowed") } - // Valid ERC-6492 signature - allow it through - // Actual deployment happens in settle() if configured - return true, sigData, nil + // Preserve deployment info for callers, but require a later simulation + // to prove the inner signature would succeed onchain. + return false, sigData, nil } // No deployment info - try EOA verification as fallback // This handles the case where someone sends a non-65-byte signature from an EOA signerAddr := common.HexToAddress(signerAddress) valid, err := VerifyEOASignature(hash[:], sigData.InnerSignature, signerAddr) - return valid, sigData, err + if err != nil { + return false, sigData, err + } + return valid, sigData, nil } // Step 6: Deployed smart contract - use EIP-1271 verification diff --git a/go/mechanisms/evm/verify_universal_test.go b/go/mechanisms/evm/verify_universal_test.go index 0fd6ac4b71..09d93224d4 100644 --- a/go/mechanisms/evm/verify_universal_test.go +++ b/go/mechanisms/evm/verify_universal_test.go @@ -171,8 +171,8 @@ func TestVerifyUniversalSignature_ERC6492(t *testing.T) { if err != nil { t.Errorf("unexpected error: %v", err) } - if !valid { - t.Error("expected valid ERC-6492 signature") + if valid { + t.Error("expected ERC-6492 signature to require later simulation") } if sigData == nil { t.Fatal("expected sigData to be non-nil") @@ -223,7 +223,7 @@ func TestVerifyUniversalSignature_ERC6492(t *testing.T) { ) if err == nil { - t.Error("expected error for undeployed wallet without deployment info") + t.Error("expected error for invalid EOA signature length when used as fallback") } if valid { t.Error("expected invalid result") @@ -245,7 +245,7 @@ func TestVerifyUniversalSignature_EdgeCases(t *testing.T) { mock, "0x1234", testHash, - make([]byte, 65), + make([]byte, 100), true, ) diff --git a/go/mechanisms/svm/README.md b/go/mechanisms/svm/README.md index 1a15759d59..2b05f7ace4 100644 --- a/go/mechanisms/svm/README.md +++ b/go/mechanisms/svm/README.md @@ -69,6 +69,24 @@ The **exact** scheme implements fixed-amount payments: - **Fees**: Rent and transaction fees paid by facilitator - **Confirmation**: On-chain settlement with transaction signature +## Duplicate Settlement Protection + +This package includes a built-in `SettlementCache` that prevents a known race condition on Solana where the same payment transaction could be settled multiple times before on-chain confirmation. The `NewExactSvmScheme` facilitator constructor accepts an optional `*SettlementCache` parameter — when the same cache instance is passed to both V1 and V2 facilitator schemes, cross-version duplicate detection is enabled. + +The cache rejects concurrent `/settle` calls that carry the same transaction payload, returning a `duplicate_settlement` error for the second and subsequent attempts. Entries are automatically evicted after 120 seconds (approximately twice the Solana blockhash lifetime). + +```go +import svm "github.com/coinbase/x402/go/mechanisms/svm" + +cache := svm.NewSettlementCache() + +// Share the same cache across V1 and V2 schemes +v2Scheme := facilitator.NewExactSvmScheme(signer, cache) +v1Scheme := v1facilitator.NewExactSvmSchemeV1(signer, cache) +``` + +For full details on the race condition and mitigation strategy, see the [Exact SVM Scheme Specification](../../specs/schemes/exact/scheme_exact_svm.md#duplicate-settlement-mitigation-recommended). + ## Future Schemes This directory currently contains only the **exact** scheme implementation. As new payment schemes are developed for Solana networks, they will be added here alongside the exact implementation: diff --git a/go/mechanisms/svm/constants.go b/go/mechanisms/svm/constants.go index b0ed5d8f21..597f8f23e1 100644 --- a/go/mechanisms/svm/constants.go +++ b/go/mechanisms/svm/constants.go @@ -44,6 +44,10 @@ const ( // ConfirmRetryDelay is the base delay between confirmation attempts ConfirmRetryDelay = 1 * time.Second + // SettlementTTL is how long a transaction is held in the duplicate settlement cache. + // Covers the Solana blockhash lifetime (~60-90s) with margin. + SettlementTTL = 120 * time.Second + // CAIP-2 network identifiers (V2) SolanaMainnetCAIP2 = "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp" SolanaDevnetCAIP2 = "solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1" diff --git a/go/mechanisms/svm/exact/facilitator/duplicate_tx_test.go b/go/mechanisms/svm/exact/facilitator/duplicate_tx_test.go index 430eb420fa..5c96be2ba3 100644 --- a/go/mechanisms/svm/exact/facilitator/duplicate_tx_test.go +++ b/go/mechanisms/svm/exact/facilitator/duplicate_tx_test.go @@ -2,6 +2,7 @@ package facilitator import ( "testing" + "time" "github.com/coinbase/x402/go/mechanisms/svm" "github.com/stretchr/testify/assert" @@ -32,3 +33,56 @@ func TestErrorCodesForMitigationPlanning(t *testing.T) { assert.Equal(t, "invalid_exact_solana_payload_transaction_instructions_length", err) }) } + +func TestDuplicateSettlementCache(t *testing.T) { + t.Run("should reject duplicate transaction", func(t *testing.T) { + cache := svm.NewSettlementCache() + + cache.Mu().Lock() + cache.Entries()["txBase64A=="] = time.Now() + cache.Mu().Unlock() + + assert.True(t, cache.IsDuplicate("txBase64A=="), "same transaction key should be detected as duplicate") + }) + + t.Run("should not conflict with distinct transactions", func(t *testing.T) { + cache := svm.NewSettlementCache() + + cache.Mu().Lock() + cache.Entries()["txBase64A=="] = time.Now() + cache.Mu().Unlock() + + assert.False(t, cache.IsDuplicate("txBase64B=="), "different transaction key should not be a duplicate") + }) + + t.Run("should prune expired entries", func(t *testing.T) { + cache := svm.NewSettlementCache() + + cache.Mu().Lock() + cache.Entries()["expiredTx=="] = time.Now().Add(-150 * time.Second) + cache.Entries()["freshTx=="] = time.Now() + cache.Mu().Unlock() + + // IsDuplicate triggers pruning internally + assert.False(t, cache.IsDuplicate("newTx=="), "new tx should not be a duplicate") + + cache.Mu().Lock() + _, expiredExists := cache.Entries()["expiredTx=="] + _, freshExists := cache.Entries()["freshTx=="] + cache.Mu().Unlock() + + assert.False(t, expiredExists, "expired entry should be pruned") + assert.True(t, freshExists, "fresh entry should survive pruning") + }) + + t.Run("duplicate settlement error constant is correct", func(t *testing.T) { + assert.Equal(t, "duplicate_settlement", ErrDuplicateSettlement) + }) + + t.Run("constructor wires the shared cache into the scheme", func(t *testing.T) { + cache := svm.NewSettlementCache() + scheme := NewExactSvmScheme(nil, cache) + assert.Same(t, cache, scheme.settlementCache, + "scheme should hold the exact cache instance that was injected") + }) +} diff --git a/go/mechanisms/svm/exact/facilitator/errors.go b/go/mechanisms/svm/exact/facilitator/errors.go index b4c4309f9c..9142123ac7 100644 --- a/go/mechanisms/svm/exact/facilitator/errors.go +++ b/go/mechanisms/svm/exact/facilitator/errors.go @@ -30,4 +30,5 @@ const ( ErrFeePayerMismatch = "invalid_exact_solana_fee_payer_mismatch" ErrTransactionFailed = "invalid_exact_solana_transaction_failed" ErrTransactionConfirmationFailed = "invalid_exact_solana_transaction_confirmation_failed" + ErrDuplicateSettlement = "duplicate_settlement" ) diff --git a/go/mechanisms/svm/exact/facilitator/scheme.go b/go/mechanisms/svm/exact/facilitator/scheme.go index 8ba6a02181..7c846dbf85 100644 --- a/go/mechanisms/svm/exact/facilitator/scheme.go +++ b/go/mechanisms/svm/exact/facilitator/scheme.go @@ -18,13 +18,23 @@ import ( // ExactSvmScheme implements the SchemeNetworkFacilitator interface for SVM (Solana) exact payments (V2) type ExactSvmScheme struct { - signer svm.FacilitatorSvmSigner + signer svm.FacilitatorSvmSigner + settlementCache *svm.SettlementCache } -// NewExactSvmScheme creates a new ExactSvmScheme -func NewExactSvmScheme(signer svm.FacilitatorSvmSigner) *ExactSvmScheme { +// NewExactSvmScheme creates a new ExactSvmScheme. +// An optional SettlementCache may be provided to share deduplication state +// across V1 and V2 instances; if nil a new cache is created. +func NewExactSvmScheme(signer svm.FacilitatorSvmSigner, cache ...*svm.SettlementCache) *ExactSvmScheme { + var c *svm.SettlementCache + if len(cache) > 0 && cache[0] != nil { + c = cache[0] + } else { + c = svm.NewSettlementCache() + } return &ExactSvmScheme{ - signer: signer, + signer: signer, + settlementCache: c, } } @@ -244,6 +254,12 @@ func (f *ExactSvmScheme) Settle( return nil, x402.NewSettleError(ErrInvalidPayloadTransaction, verifyResp.Payer, network, "", err.Error()) } + // Duplicate settlement check: reject if this transaction is already being settled. + txKey := solanaPayload.Transaction + if f.settlementCache.IsDuplicate(txKey) { + return nil, x402.NewSettleError(ErrDuplicateSettlement, verifyResp.Payer, network, "", "duplicate transaction") + } + // Decode transaction tx, err := svm.DecodeTransaction(solanaPayload.Transaction) if err != nil { diff --git a/go/mechanisms/svm/exact/v1/facilitator/duplicate_tx_test.go b/go/mechanisms/svm/exact/v1/facilitator/duplicate_tx_test.go new file mode 100644 index 0000000000..68fabf6d6f --- /dev/null +++ b/go/mechanisms/svm/exact/v1/facilitator/duplicate_tx_test.go @@ -0,0 +1,116 @@ +package facilitator + +import ( + "testing" + "time" + + "github.com/coinbase/x402/go/mechanisms/svm" + "github.com/stretchr/testify/assert" +) + +func TestDuplicateSettlementCacheV1(t *testing.T) { + t.Run("should reject duplicate transaction", func(t *testing.T) { + cache := svm.NewSettlementCache() + + cache.Mu().Lock() + cache.Entries()["txBase64A=="] = time.Now() + cache.Mu().Unlock() + + assert.True(t, cache.IsDuplicate("txBase64A=="), "same transaction key should be detected as duplicate") + }) + + t.Run("should not conflict with distinct transactions", func(t *testing.T) { + cache := svm.NewSettlementCache() + + cache.Mu().Lock() + cache.Entries()["txBase64A=="] = time.Now() + cache.Mu().Unlock() + + assert.False(t, cache.IsDuplicate("txBase64B=="), "different transaction key should not be a duplicate") + }) + + t.Run("should prune expired entries", func(t *testing.T) { + cache := svm.NewSettlementCache() + + cache.Mu().Lock() + cache.Entries()["expiredTx=="] = time.Now().Add(-150 * time.Second) + cache.Entries()["freshTx=="] = time.Now() + cache.Mu().Unlock() + + // IsDuplicate triggers pruning internally + assert.False(t, cache.IsDuplicate("newTx=="), "new tx should not be a duplicate") + + cache.Mu().Lock() + _, expiredExists := cache.Entries()["expiredTx=="] + _, freshExists := cache.Entries()["freshTx=="] + cache.Mu().Unlock() + + assert.False(t, expiredExists, "expired entry should be pruned") + assert.True(t, freshExists, "fresh entry should survive pruning") + }) + + t.Run("duplicate settlement error constant is correct", func(t *testing.T) { + assert.Equal(t, "duplicate_settlement", ErrDuplicateSettlement) + }) + + t.Run("constructor wires the shared cache into the scheme", func(t *testing.T) { + cache := svm.NewSettlementCache() + scheme := NewExactSvmSchemeV1(nil, cache) + assert.Same(t, cache, scheme.settlementCache, + "scheme should hold the exact cache instance that was injected") + }) + + t.Run("should prune only expired entries and keep fresh ones", func(t *testing.T) { + cache := svm.NewSettlementCache() + + cache.Mu().Lock() + cache.Entries()["expired-1"] = time.Now().Add(-150 * time.Second) + cache.Entries()["expired-2"] = time.Now().Add(-130 * time.Second) + cache.Entries()["fresh-1"] = time.Now() + cache.Entries()["fresh-2"] = time.Now() + cache.Mu().Unlock() + + // Trigger prune + cache.IsDuplicate("trigger") + + cache.Mu().Lock() + _, expired1 := cache.Entries()["expired-1"] + _, expired2 := cache.Entries()["expired-2"] + _, fresh1 := cache.Entries()["fresh-1"] + _, fresh2 := cache.Entries()["fresh-2"] + _, trigger := cache.Entries()["trigger"] + cache.Mu().Unlock() + + assert.False(t, expired1, "expired entry should be pruned") + assert.False(t, expired2, "expired entry should be pruned") + assert.True(t, fresh1, "fresh entry should survive pruning") + assert.True(t, fresh2, "fresh entry should survive pruning") + assert.True(t, trigger, "newly inserted entry should be present") + }) + + t.Run("should prune all entries when all expired", func(t *testing.T) { + cache := svm.NewSettlementCache() + + cache.Mu().Lock() + cache.Entries()["old-1"] = time.Now().Add(-200 * time.Second) + cache.Entries()["old-2"] = time.Now().Add(-200 * time.Second) + cache.Entries()["old-3"] = time.Now().Add(-200 * time.Second) + cache.Mu().Unlock() + + assert.False(t, cache.IsDuplicate("old-1"), "expired entry should not be a duplicate") + assert.False(t, cache.IsDuplicate("old-2"), "expired entry should not be a duplicate") + assert.False(t, cache.IsDuplicate("old-3"), "expired entry should not be a duplicate") + }) + + t.Run("should not prune any entries when all fresh", func(t *testing.T) { + cache := svm.NewSettlementCache() + + assert.False(t, cache.IsDuplicate("new-1")) + assert.False(t, cache.IsDuplicate("new-2")) + assert.False(t, cache.IsDuplicate("new-3")) + + assert.True(t, cache.IsDuplicate("new-1"), "fresh entry should still be cached") + assert.True(t, cache.IsDuplicate("new-2"), "fresh entry should still be cached") + assert.True(t, cache.IsDuplicate("new-3"), "fresh entry should still be cached") + }) +} diff --git a/go/mechanisms/svm/exact/v1/facilitator/errors.go b/go/mechanisms/svm/exact/v1/facilitator/errors.go index 761f88c01a..980162f4c1 100644 --- a/go/mechanisms/svm/exact/v1/facilitator/errors.go +++ b/go/mechanisms/svm/exact/v1/facilitator/errors.go @@ -31,4 +31,5 @@ const ( ErrFeePayerMismatch = "invalid_exact_solana_fee_payer_mismatch" ErrTransactionFailed = "invalid_exact_solana_transaction_failed" ErrTransactionConfirmationFailed = "invalid_exact_solana_transaction_confirmation_failed" + ErrDuplicateSettlement = "duplicate_settlement" ) diff --git a/go/mechanisms/svm/exact/v1/facilitator/scheme.go b/go/mechanisms/svm/exact/v1/facilitator/scheme.go index b8282bcddc..1d289c980d 100644 --- a/go/mechanisms/svm/exact/v1/facilitator/scheme.go +++ b/go/mechanisms/svm/exact/v1/facilitator/scheme.go @@ -19,13 +19,23 @@ import ( // ExactSvmSchemeV1 implements the SchemeNetworkFacilitator interface for SVM (Solana) exact payments (V1) type ExactSvmSchemeV1 struct { - signer svm.FacilitatorSvmSigner + signer svm.FacilitatorSvmSigner + settlementCache *svm.SettlementCache } -// NewExactSvmSchemeV1 creates a new ExactSvmSchemeV1 -func NewExactSvmSchemeV1(signer svm.FacilitatorSvmSigner) *ExactSvmSchemeV1 { +// NewExactSvmSchemeV1 creates a new ExactSvmSchemeV1. +// An optional SettlementCache may be provided to share deduplication state +// across V1 and V2 instances; if nil a new cache is created. +func NewExactSvmSchemeV1(signer svm.FacilitatorSvmSigner, cache ...*svm.SettlementCache) *ExactSvmSchemeV1 { + var c *svm.SettlementCache + if len(cache) > 0 && cache[0] != nil { + c = cache[0] + } else { + c = svm.NewSettlementCache() + } return &ExactSvmSchemeV1{ - signer: signer, + signer: signer, + settlementCache: c, } } @@ -241,6 +251,12 @@ func (f *ExactSvmSchemeV1) Settle( return nil, x402.NewSettleError(ErrInvalidPayloadTransaction, verifyResp.Payer, network, "", err.Error()) } + // Duplicate settlement check: reject if this transaction is already being settled. + txKey := svmPayload.Transaction + if f.settlementCache.IsDuplicate(txKey) { + return nil, x402.NewSettleError(ErrDuplicateSettlement, verifyResp.Payer, network, "", "duplicate transaction") + } + // Decode transaction tx, err := svm.DecodeTransaction(svmPayload.Transaction) if err != nil { diff --git a/go/mechanisms/svm/settlement_cache.go b/go/mechanisms/svm/settlement_cache.go new file mode 100644 index 0000000000..f18731eecb --- /dev/null +++ b/go/mechanisms/svm/settlement_cache.go @@ -0,0 +1,58 @@ +package svm + +import ( + "sync" + "time" +) + +// SettlementCache is a thread-safe in-memory cache for deduplicating concurrent +// settlement requests. A single instance should be shared across V1 and V2 +// facilitator scheme instances so that a transaction submitted through one +// protocol version is also blocked on the other. +type SettlementCache struct { + mu sync.Mutex + entries map[string]time.Time +} + +// NewSettlementCache creates a new, empty SettlementCache. +func NewSettlementCache() *SettlementCache { + return &SettlementCache{ + entries: make(map[string]time.Time), + } +} + +// IsDuplicate returns true if key is already pending settlement (duplicate). +// Otherwise it records the key as newly pending and returns false. +// Callers should reject the settlement when this returns true. +func (c *SettlementCache) IsDuplicate(key string) bool { + c.mu.Lock() + defer c.mu.Unlock() + + c.prune() + + if _, exists := c.entries[key]; exists { + return true + } + c.entries[key] = time.Now() + return false +} + +// Entries returns a snapshot of the underlying map — use only in tests. +func (c *SettlementCache) Entries() map[string]time.Time { + return c.entries +} + +// Mu returns the mutex — use only in tests. +func (c *SettlementCache) Mu() *sync.Mutex { + return &c.mu +} + +// prune removes entries older than the settlement TTL. Caller must hold mu. +func (c *SettlementCache) prune() { + cutoff := time.Now().Add(-SettlementTTL) + for key, ts := range c.entries { + if ts.Before(cutoff) { + delete(c.entries, key) + } + } +} diff --git a/go/server.go b/go/server.go index 13f0fce154..f2890b8c95 100644 --- a/go/server.go +++ b/go/server.go @@ -4,12 +4,61 @@ import ( "context" "encoding/json" "fmt" + "math/big" + "regexp" + "strconv" + "strings" "sync" "time" "github.com/coinbase/x402/go/types" ) +var ( + percentRegex = regexp.MustCompile(`^(\d+(?:\.\d{0,2})?)%$`) + dollarRegex = regexp.MustCompile(`^\$(\d+(?:\.\d+)?)$`) +) + +// ResolveSettlementOverrideAmount resolves a settlement override amount string +// to a final atomic-unit string. Supports three formats: +// - Raw atomic units: "1000" +// - Percent of requirements.Amount: "50%" (up to 2 decimal places, floored) +// - Dollar price: "$0.05" (converted using the provided decimals) +func ResolveSettlementOverrideAmount(rawAmount string, requirements types.PaymentRequirements, decimals int) (string, error) { + if m := percentRegex.FindStringSubmatch(rawAmount); m != nil { + parts := strings.SplitN(m[1], ".", 2) + intPart, _ := strconv.ParseInt(parts[0], 10, 64) + decPart := int64(0) + if len(parts) == 2 { + padded := (parts[1] + "00")[:2] + decPart, _ = strconv.ParseInt(padded, 10, 64) + } + scaledPercent := big.NewInt(intPart*100 + decPart) + base, ok := new(big.Int).SetString(requirements.Amount, 10) + if !ok { + return "", fmt.Errorf("invalid requirements amount: %s", requirements.Amount) + } + result := new(big.Int).Mul(base, scaledPercent) + result.Div(result, big.NewInt(10000)) + return result.String(), nil + } + + if m := dollarRegex.FindStringSubmatch(rawAmount); m != nil { + dollarFloat, ok := new(big.Float).SetPrec(256).SetString(m[1]) + if !ok { + return "", fmt.Errorf("invalid dollar amount: %s", rawAmount) + } + multiplier := new(big.Float).SetPrec(256).SetInt( + new(big.Int).Exp(big.NewInt(10), big.NewInt(int64(decimals)), nil), + ) + atomicFloat := new(big.Float).SetPrec(256).Mul(dollarFloat, multiplier) + atomicInt, _ := atomicFloat.Int(nil) // truncates toward zero (floor for positive values) + return atomicInt.String(), nil + } + + return rawAmount, nil +} + // x402ResourceServer manages payment requirements and verification for protected resources // V2 ONLY - This server only produces and accepts V2 payments type x402ResourceServer struct { @@ -246,6 +295,22 @@ func (s *x402ResourceServer) OnSettleFailure(hook OnSettleFailureHook) *x402Reso // Core Payment Methods (V2 Only) // ============================================================================ +func mergeExtraFields(parsedExtra map[string]interface{}, configExtra map[string]interface{}) map[string]interface{} { + if len(parsedExtra) == 0 && len(configExtra) == 0 { + return nil + } + + merged := make(map[string]interface{}, len(parsedExtra)+len(configExtra)) + for key, value := range parsedExtra { + merged[key] = value + } + for key, value := range configExtra { + merged[key] = value + } + + return merged +} + // BuildPaymentRequirements creates payment requirements for a resource func (s *x402ResourceServer) BuildPaymentRequirements( ctx context.Context, @@ -288,7 +353,7 @@ func (s *x402ResourceServer) BuildPaymentRequirements( Amount: assetAmount.Amount, PayTo: config.PayTo, MaxTimeoutSeconds: maxTimeout, - Extra: assetAmount.Extra, + Extra: mergeExtraFields(assetAmount.Extra, config.Extra), } // Enhance with scheme-specific details @@ -381,24 +446,46 @@ func (s *x402ResourceServer) VerifyPayment(ctx context.Context, payload types.Pa return verifyResult, nil } -// SettlePayment settles a V2 payment -func (s *x402ResourceServer) SettlePayment(ctx context.Context, payload types.PaymentPayload, requirements types.PaymentRequirements) (*SettleResponse, error) { +// SettlePayment settles a V2 payment. +// If overrides is non-nil and overrides.Amount is set, the effective requirements amount +// is replaced before settlement (partial settlement for upto scheme). +func (s *x402ResourceServer) SettlePayment(ctx context.Context, payload types.PaymentPayload, requirements types.PaymentRequirements, overrides *SettlementOverrides) (*SettleResponse, error) { + effectiveRequirements := requirements + if overrides != nil && overrides.Amount != "" { + decimals := 6 + s.mu.RLock() + network := Network(requirements.Network) + if networkSchemes, ok := s.schemes[network]; ok { + if scheme, ok := networkSchemes[requirements.Scheme]; ok { + if dp, ok := scheme.(AssetDecimalsProvider); ok { + decimals = dp.GetAssetDecimals(requirements.Asset, network) + } + } + } + s.mu.RUnlock() + resolved, err := ResolveSettlementOverrideAmount(overrides.Amount, requirements, decimals) + if err != nil { + return nil, NewSettleError("invalid_settlement_override", "", Network(requirements.Network), "", err.Error()) + } + effectiveRequirements.Amount = resolved + } + // Marshal to bytes early for hooks (escape hatch for extensions) payloadBytes, err := json.Marshal(payload) if err != nil { - return nil, NewSettleError("failed_to_marshal_payload", "", Network(requirements.Network), "", err.Error()) + return nil, NewSettleError("failed_to_marshal_payload", "", Network(effectiveRequirements.Network), "", err.Error()) } - requirementsBytes, err := json.Marshal(requirements) + requirementsBytes, err := json.Marshal(effectiveRequirements) if err != nil { - return nil, NewSettleError("failed_to_marshal_requirements", "", Network(requirements.Network), "", err.Error()) + return nil, NewSettleError("failed_to_marshal_requirements", "", Network(effectiveRequirements.Network), "", err.Error()) } // Execute beforeSettle hooks hookCtx := SettleContext{ Ctx: ctx, Payload: payload, - Requirements: requirements, + Requirements: effectiveRequirements, PayloadBytes: payloadBytes, RequirementsBytes: requirementsBytes, } @@ -409,13 +496,13 @@ func (s *x402ResourceServer) SettlePayment(ctx context.Context, payload types.Pa return nil, err } if result != nil && result.Abort { - return nil, NewSettleError(result.Reason, "", Network(requirements.Network), "", "") + return nil, NewSettleError(result.Reason, "", Network(effectiveRequirements.Network), "", "") } } s.mu.RLock() - scheme := requirements.Scheme - network := Network(requirements.Network) + scheme := effectiveRequirements.Scheme + network := Network(effectiveRequirements.Network) facilitator := s.facilitatorClients[network][scheme] s.mu.RUnlock() @@ -448,6 +535,26 @@ func (s *x402ResourceServer) SettlePayment(ctx context.Context, payload types.Pa return settleResult, nil } +// EnrichExtensions enriches declared extensions using registered extension hooks. +func (s *x402ResourceServer) EnrichExtensions( + declaredExtensions map[string]interface{}, + transportContext interface{}, +) map[string]interface{} { + s.mu.RLock() + defer s.mu.RUnlock() + + enriched := make(map[string]interface{}) + for key, declaration := range declaredExtensions { + ext, ok := s.registeredExtensions[key] + if ok { + enriched[key] = ext.EnrichDeclaration(declaration, transportContext) + } else { + enriched[key] = declaration + } + } + return enriched +} + // CreatePaymentRequiredResponse creates a V2 PaymentRequired response func (s *x402ResourceServer) CreatePaymentRequiredResponse( requirements []types.PaymentRequirements, diff --git a/go/server_hooks_test.go b/go/server_hooks_test.go index 753a0ac315..9e18bdc42c 100644 --- a/go/server_hooks_test.go +++ b/go/server_hooks_test.go @@ -272,6 +272,7 @@ func TestBeforeSettleHook_Abort(t *testing.T) { context.Background(), payload, requirements, + nil, ) if err == nil { @@ -329,6 +330,7 @@ func TestAfterSettleHook(t *testing.T) { context.Background(), payload, requirements, + nil, ) if err != nil { @@ -381,6 +383,7 @@ func TestOnSettleFailureHook_Recover(t *testing.T) { context.Background(), payload, requirements, + nil, ) if err != nil { diff --git a/go/server_test.go b/go/server_test.go index b9b70b030a..f1da18f4bf 100644 --- a/go/server_test.go +++ b/go/server_test.go @@ -232,6 +232,10 @@ func TestServerBuildPaymentRequirements(t *testing.T) { Price: "$5.00", Network: "eip155:1", MaxTimeoutSeconds: 600, + Extra: map[string]interface{}{ + "assetTransferMethod": "permit2", + "merchantNote": "custom-scheme-data", + }, } // BuildPaymentRequirements now requires supportedKind @@ -260,6 +264,15 @@ func TestServerBuildPaymentRequirements(t *testing.T) { if requirements.Extra["enhanced"] != true { t.Fatal("Expected requirements to be enhanced") } + if requirements.Extra["decimals"] != 6 { + t.Fatalf("Expected parsed extra to be preserved, got %v", requirements.Extra["decimals"]) + } + if requirements.Extra["assetTransferMethod"] != "permit2" { + t.Fatalf("Expected config extra to be merged, got %v", requirements.Extra["assetTransferMethod"]) + } + if requirements.Extra["merchantNote"] != "custom-scheme-data" { + t.Fatalf("Expected merchant extra to be merged, got %v", requirements.Extra["merchantNote"]) + } } func TestServerBuildPaymentRequirementsNoScheme(t *testing.T) { @@ -416,7 +429,7 @@ func TestServerSettlePayment(t *testing.T) { } // Server uses typed API now - response, err := server.SettlePayment(ctx, payload, requirements) + response, err := server.SettlePayment(ctx, payload, requirements, nil) if err != nil { t.Fatalf("Unexpected error: %v", err) } @@ -594,3 +607,109 @@ func TestSupportedCache(t *testing.T) { } } */ + +func TestResolveSettlementOverrideAmount(t *testing.T) { + baseReqs := types.PaymentRequirements{ + Amount: "2000", + } + + t.Run("raw atomic units", func(t *testing.T) { + tests := []struct { + input string + expected string + }{ + {"1000", "1000"}, + {"0", "0"}, + {"999999", "999999"}, + } + for _, tt := range tests { + result, err := ResolveSettlementOverrideAmount(tt.input, baseReqs, 6) + if err != nil { + t.Errorf("ResolveSettlementOverrideAmount(%q) error: %v", tt.input, err) + } + if result != tt.expected { + t.Errorf("ResolveSettlementOverrideAmount(%q) = %q, want %q", tt.input, result, tt.expected) + } + } + }) + + t.Run("percent format", func(t *testing.T) { + tests := []struct { + input string + amount string + expected string + }{ + {"50%", "2000", "1000"}, + {"100%", "2000", "2000"}, + {"0%", "2000", "0"}, + {"25%", "2000", "500"}, + {"33.33%", "3000", "999"}, + {"10.5%", "1000", "105"}, + } + for _, tt := range tests { + reqs := types.PaymentRequirements{Amount: tt.amount} + result, err := ResolveSettlementOverrideAmount(tt.input, reqs, 6) + if err != nil { + t.Errorf("ResolveSettlementOverrideAmount(%q, amount=%s) error: %v", tt.input, tt.amount, err) + } + if result != tt.expected { + t.Errorf("ResolveSettlementOverrideAmount(%q, amount=%s) = %q, want %q", tt.input, tt.amount, result, tt.expected) + } + } + }) + + t.Run("dollar price with default 6 decimals", func(t *testing.T) { + tests := []struct { + input string + expected string + }{ + {"$1.00", "1000000"}, + {"$0.05", "50000"}, + {"$0.001", "1000"}, + {"$0", "0"}, + } + for _, tt := range tests { + result, err := ResolveSettlementOverrideAmount(tt.input, baseReqs, 6) + if err != nil { + t.Errorf("ResolveSettlementOverrideAmount(%q) error: %v", tt.input, err) + } + if result != tt.expected { + t.Errorf("ResolveSettlementOverrideAmount(%q) = %q, want %q", tt.input, result, tt.expected) + } + } + }) + + t.Run("dollar price with 8 decimals", func(t *testing.T) { + reqs := types.PaymentRequirements{Amount: "2000"} + result, err := ResolveSettlementOverrideAmount("$0.05", reqs, 8) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if result != "5000000" { + t.Errorf("expected 5000000 (8 decimals), got %s", result) + } + }) + + t.Run("dollar price result uses requirements asset regardless of decimals", func(t *testing.T) { + reqs := types.PaymentRequirements{Amount: "2000", Asset: "0xSomeToken"} + result, err := ResolveSettlementOverrideAmount("$0.001", reqs, 6) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + // Only the amount changes; the asset remains whatever is in requirements + if result != "1000" { + t.Errorf("expected 1000, got %s", result) + } + }) + + t.Run("dollar price with 6 decimals", func(t *testing.T) { + reqs := types.PaymentRequirements{Amount: "2000", Asset: "0xUnknownToken"} + result, err := ResolveSettlementOverrideAmount("$0.05", reqs, 6) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if result != "50000" { + t.Errorf("expected 50000 (6 decimals), got %s", result) + } + }) +} diff --git a/go/test/integration/core_test.go b/go/test/integration/core_test.go index 8535315e4a..c6cedbe970 100644 --- a/go/test/integration/core_test.go +++ b/go/test/integration/core_test.go @@ -79,7 +79,7 @@ func TestCoreIntegration(t *testing.T) { // Server does work here... // Server - settles payment (typed) - settleResponse, err := server.SettlePayment(ctx, payload, *accepted) + settleResponse, err := server.SettlePayment(ctx, payload, *accepted, nil) if err != nil { t.Fatalf("Failed to settle payment: %v", err) } diff --git a/go/test/integration/evm_test.go b/go/test/integration/evm_test.go index 5cbc3303b2..3993ae4ea6 100644 --- a/go/test/integration/evm_test.go +++ b/go/test/integration/evm_test.go @@ -24,9 +24,12 @@ import ( x402 "github.com/coinbase/x402/go" "github.com/coinbase/x402/go/mechanisms/evm" - evmclient "github.com/coinbase/x402/go/mechanisms/evm/exact/client" - evmfacilitator "github.com/coinbase/x402/go/mechanisms/evm/exact/facilitator" - evmserver "github.com/coinbase/x402/go/mechanisms/evm/exact/server" + exactevmclient "github.com/coinbase/x402/go/mechanisms/evm/exact/client" + exactevmfacilitator "github.com/coinbase/x402/go/mechanisms/evm/exact/facilitator" + exactevmserver "github.com/coinbase/x402/go/mechanisms/evm/exact/server" + uptoevmclient "github.com/coinbase/x402/go/mechanisms/evm/upto/client" + uptoevmfacilitator "github.com/coinbase/x402/go/mechanisms/evm/upto/facilitator" + uptoevmserver "github.com/coinbase/x402/go/mechanisms/evm/upto/server" evmsigners "github.com/coinbase/x402/go/signers/evm" "github.com/coinbase/x402/go/types" ) @@ -36,6 +39,49 @@ func newRealClientEvmSigner(privateKeyHex string) (evm.ClientEvmSigner, error) { return evmsigners.NewClientSignerFromPrivateKey(privateKeyHex) } +// callContractAndDecode performs a generic eth_call and returns the decoded result. +// Used by integration test signers to support any contract read (tryAggregate, transferWithAuthorization, etc.). +func callContractAndDecode( + ctx context.Context, + ethClient *ethclient.Client, + contractAddress string, + abiBytes []byte, + functionName string, + args ...interface{}, +) (interface{}, error) { + contractABI, err := abi.JSON(strings.NewReader(string(abiBytes))) + if err != nil { + return nil, fmt.Errorf("failed to parse ABI: %w", err) + } + + callData, err := contractABI.Pack(functionName, args...) + if err != nil { + return nil, fmt.Errorf("failed to pack %s: %w", functionName, err) + } + + addr := common.HexToAddress(contractAddress) + result, err := ethClient.CallContract(ctx, ethereum.CallMsg{ + To: &addr, + Data: callData, + }, nil) + if err != nil { + return nil, fmt.Errorf("eth_call failed: %w", err) + } + + outputs, err := contractABI.Unpack(functionName, result) + if err != nil { + return nil, fmt.Errorf("failed to unpack %s result: %w", functionName, err) + } + + if len(outputs) == 0 { + return nil, nil + } + if len(outputs) == 1 { + return outputs[0], nil + } + return outputs, nil +} + // Real EVM facilitator signer type realFacilitatorEvmSigner struct { privateKey *ecdsa.PrivateKey @@ -100,15 +146,41 @@ func (s *realFacilitatorEvmSigner) GetCode(ctx context.Context, address string) func (s *realFacilitatorEvmSigner) ReadContract( ctx context.Context, contractAddress string, - abi []byte, + abiBytes []byte, functionName string, args ...interface{}, ) (interface{}, error) { - // For integration tests with authorizationState, assume nonce not used - if functionName == "authorizationState" { - return false, nil + // Set From to the facilitator's own address, matching TypeScript's viem WalletClient + // which always includes from=account.address in eth_call. This is required for + // contracts that check msg.sender (e.g. the upto proxy settle() function). + contractABI, err := abi.JSON(strings.NewReader(string(abiBytes))) + if err != nil { + return nil, fmt.Errorf("failed to parse ABI: %w", err) } - return nil, fmt.Errorf("read contract not fully implemented for integration tests") + callData, err := contractABI.Pack(functionName, args...) + if err != nil { + return nil, fmt.Errorf("failed to pack %s: %w", functionName, err) + } + addr := common.HexToAddress(contractAddress) + result, err := s.ethClient.CallContract(ctx, ethereum.CallMsg{ + From: s.address, + To: &addr, + Data: callData, + }, nil) + if err != nil { + return nil, fmt.Errorf("eth_call failed: %w", err) + } + outputs, err := contractABI.Unpack(functionName, result) + if err != nil { + return nil, fmt.Errorf("failed to unpack %s result: %w", functionName, err) + } + if len(outputs) == 0 { + return nil, nil + } + if len(outputs) == 1 { + return outputs[0], nil + } + return outputs, nil } func (s *realFacilitatorEvmSigner) WriteContract( @@ -161,7 +233,9 @@ func (s *realFacilitatorEvmSigner) sendTxWithRetry(ctx context.Context, to commo err = s.ethClient.SendTransaction(ctx, signedTx) if err != nil { - if strings.Contains(err.Error(), "replacement transaction underpriced") && attempt < maxRetries { + if (strings.Contains(err.Error(), "replacement transaction underpriced") || + strings.Contains(err.Error(), "nonce too low") || + strings.Contains(err.Error(), "already known")) && attempt < maxRetries { time.Sleep(time.Duration(2*(attempt+1)) * time.Second) continue } @@ -342,7 +416,7 @@ func TestEVMIntegrationV2(t *testing.T) { // Setup client with EVM v2 scheme client := x402.Newx402Client() - evmClient := evmclient.NewExactEvmScheme(clientSigner) + evmClient := exactevmclient.NewExactEvmScheme(clientSigner, nil) // Register for Base Sepolia client.Register("eip155:84532", evmClient) @@ -355,10 +429,10 @@ func TestEVMIntegrationV2(t *testing.T) { // Setup facilitator with EVM v2 scheme facilitator := x402.Newx402Facilitator() // Enable smart wallet deployment via EIP-6492 - evmConfig := &evmfacilitator.ExactEvmSchemeConfig{ + evmConfig := &exactevmfacilitator.ExactEvmSchemeConfig{ DeployERC4337WithEIP6492: true, } - evmFacilitator := evmfacilitator.NewExactEvmScheme(facilitatorSigner, evmConfig) + evmFacilitator := exactevmfacilitator.NewExactEvmScheme(facilitatorSigner, evmConfig) // Register for Base Sepolia facilitator.Register([]x402.Network{"eip155:84532"}, evmFacilitator) @@ -366,7 +440,7 @@ func TestEVMIntegrationV2(t *testing.T) { facilitatorClient := &localEvmFacilitatorClient{facilitator: facilitator} // Setup resource server with EVM v2 - evmServer := evmserver.NewExactEvmScheme() + evmServer := exactevmserver.NewExactEvmScheme() server := x402.Newx402ResourceServer( x402.WithFacilitatorClient(facilitatorClient), ) @@ -462,7 +536,7 @@ func TestEVMIntegrationV2(t *testing.T) { // Server does work here... // Server - settles payment (typed) - settleResponse, err := server.SettlePayment(ctx, paymentPayload, *accepted) + settleResponse, err := server.SettlePayment(ctx, paymentPayload, *accepted, nil) if err != nil { t.Fatalf("Failed to settle payment: %v", err) } @@ -526,28 +600,43 @@ func TestEVMIntegrationV2Permit2(t *testing.T) { // Setup client with EVM v2 scheme client := x402.Newx402Client() - evmClient := evmclient.NewExactEvmScheme(clientSigner) + evmClient := exactevmclient.NewExactEvmScheme(clientSigner, nil) client.Register("eip155:84532", evmClient) // Create facilitator signer with Permit2 support - facilitatorSigner, err := newPermit2FacilitatorEvmSigner(facilitatorPrivateKey, "https://sepolia.base.org") + facilitatorSigner, err := newPermit2FacilitatorEvmSigner(ctx, facilitatorPrivateKey, "https://sepolia.base.org") if err != nil { t.Fatalf("Failed to create facilitator signer: %v", err) } // Setup facilitator with EVM v2 scheme facilitator := x402.Newx402Facilitator() - evmConfig := &evmfacilitator.ExactEvmSchemeConfig{ + evmConfig := &exactevmfacilitator.ExactEvmSchemeConfig{ DeployERC4337WithEIP6492: true, } - evmFacilitator := evmfacilitator.NewExactEvmScheme(facilitatorSigner, evmConfig) + evmFacilitator := exactevmfacilitator.NewExactEvmScheme(facilitatorSigner, evmConfig) facilitator.Register([]x402.Network{"eip155:84532"}, evmFacilitator) // Create facilitator client wrapper facilitatorClient := &localEvmFacilitatorClient{facilitator: facilitator} // Setup resource server with EVM v2 - evmServer := evmserver.NewExactEvmScheme() + evmServer := exactevmserver.NewExactEvmScheme() + evmServer.RegisterMoneyParser(func(amount float64, network x402.Network) (*x402.AssetAmount, error) { + if string(network) != "eip155:84532" { + return nil, nil + } + + return &x402.AssetAmount{ + Asset: "0x036CbD53842c5426634e7929541eC2318f3dCF7e", // USDC on Base Sepolia + Amount: fmt.Sprintf("%.0f", amount*1e6), + Extra: map[string]interface{}{ + "assetTransferMethod": "permit2", + "name": "USDC", + "version": "2", + }, + }, nil + }) server := x402.Newx402ResourceServer( x402.WithFacilitatorClient(facilitatorClient), ) @@ -559,21 +648,19 @@ func TestEVMIntegrationV2Permit2(t *testing.T) { t.Fatalf("Failed to initialize server: %v", err) } - // Server - builds PaymentRequired response with Permit2 method - accepts := []types.PaymentRequirements{ - { - Scheme: evm.SchemeExact, - Network: "eip155:84532", - Asset: "0x036CbD53842c5426634e7929541eC2318f3dCF7e", // USDC on Base Sepolia - Amount: "1000", // 0.001 USDC - PayTo: resourceServerAddress, - MaxTimeoutSeconds: 300, - Extra: map[string]interface{}{ - "assetTransferMethod": "permit2", // Request Permit2 flow - "name": "USDC", - "version": "2", - }, - }, + // Server - builds PaymentRequired response with Permit2 method via money parser + accepts, err := server.BuildPaymentRequirementsFromConfig(ctx, x402.ResourceConfig{ + Scheme: evm.SchemeExact, + Network: "eip155:84532", + PayTo: resourceServerAddress, + Price: "$0.001", + MaxTimeoutSeconds: 300, + }) + if err != nil { + t.Fatalf("Failed to build payment requirements: %v", err) + } + if accepts[0].Extra["assetTransferMethod"] != "permit2" { + t.Fatalf("Expected Permit2 payment requirements, got extra=%v", accepts[0].Extra) } resource := &types.ResourceInfo{ URL: "https://api.example.com/permit2", @@ -657,7 +744,7 @@ func TestEVMIntegrationV2Permit2(t *testing.T) { } // Server - settles payment - settleResponse, err := server.SettlePayment(ctx, paymentPayload, *accepted) + settleResponse, err := server.SettlePayment(ctx, paymentPayload, *accepted, nil) if err != nil { t.Fatalf("Failed to settle payment: %v", err) } @@ -677,7 +764,7 @@ func TestEVMIntegrationV2Permit2(t *testing.T) { } // newPermit2FacilitatorEvmSigner creates a facilitator signer with Permit2 support -func newPermit2FacilitatorEvmSigner(privateKeyHex string, rpcURL string) (*permit2FacilitatorEvmSigner, error) { +func newPermit2FacilitatorEvmSigner(ctx context.Context, privateKeyHex string, rpcURL string) (*permit2FacilitatorEvmSigner, error) { privateKeyHex = strings.TrimPrefix(privateKeyHex, "0x") privateKey, err := crypto.HexToECDSA(privateKeyHex) @@ -692,7 +779,6 @@ func newPermit2FacilitatorEvmSigner(privateKeyHex string, rpcURL string) (*permi return nil, fmt.Errorf("failed to connect to RPC: %w", err) } - ctx := context.Background() chainID, err := client.ChainID(ctx) if err != nil { return nil, fmt.Errorf("failed to get chain ID: %w", err) @@ -733,40 +819,62 @@ func (s *permit2FacilitatorEvmSigner) GetCode(ctx context.Context, address strin return s.ethClient.CodeAt(ctx, addr, nil) } -func (s *permit2FacilitatorEvmSigner) ReadContract( +func (s *permit2FacilitatorEvmSigner) readContractWithFrom( ctx context.Context, + from common.Address, contractAddress string, abiBytes []byte, functionName string, args ...interface{}, ) (interface{}, error) { - // For authorizationState, assume nonce not used (random nonces are unique) - if functionName == "authorizationState" { - return false, nil + contractABI, err := abi.JSON(strings.NewReader(string(abiBytes))) + if err != nil { + return nil, fmt.Errorf("failed to parse ABI: %w", err) } + callData, err := contractABI.Pack(functionName, args...) + if err != nil { + return nil, fmt.Errorf("failed to pack %s: %w", functionName, err) + } + addr := common.HexToAddress(contractAddress) + result, err := s.ethClient.CallContract(ctx, ethereum.CallMsg{ + From: from, + To: &addr, + Data: callData, + }, nil) + if err != nil { + return nil, fmt.Errorf("eth_call failed: %w", err) + } + outputs, err := contractABI.Unpack(functionName, result) + if err != nil { + return nil, fmt.Errorf("failed to unpack %s result: %w", functionName, err) + } + if len(outputs) == 0 { + return nil, nil + } + if len(outputs) == 1 { + return outputs[0], nil + } + return outputs, nil +} - // For allowance, read real on-chain value (fall back to MaxUint256 on any error) +func (s *permit2FacilitatorEvmSigner) ReadContract( + ctx context.Context, + contractAddress string, + abiBytes []byte, + functionName string, + args ...interface{}, +) (interface{}, error) { + // Set From to the facilitator's own address, matching TypeScript's viem WalletClient + // which always includes from=account.address in eth_call. if functionName == "allowance" { - contractABI, parseErr := abi.JSON(strings.NewReader(string(abiBytes))) - if parseErr != nil { - return evm.MaxUint256(), nil //nolint:nilerr // fallback to assume approved - } - callData, packErr := contractABI.Pack(functionName, args...) - if packErr != nil { - return evm.MaxUint256(), nil //nolint:nilerr // fallback to assume approved - } - addr := common.HexToAddress(contractAddress) - result, callErr := s.ethClient.CallContract(ctx, ethereum.CallMsg{ - To: &addr, - Data: callData, - }, nil) - if callErr != nil { + result, err := s.readContractWithFrom(ctx, s.address, contractAddress, abiBytes, functionName, args...) + if err != nil { return evm.MaxUint256(), nil //nolint:nilerr // fallback to assume approved } - return new(big.Int).SetBytes(result), nil + return result, nil } - return nil, fmt.Errorf("read contract not fully implemented for integration tests") + return s.readContractWithFrom(ctx, s.address, contractAddress, abiBytes, functionName, args...) } func (s *permit2FacilitatorEvmSigner) WriteContract( @@ -827,7 +935,9 @@ func (s *permit2FacilitatorEvmSigner) sendTxWithRetry(ctx context.Context, to co err = s.ethClient.SendTransaction(ctx, signedTx) if err != nil { - if strings.Contains(err.Error(), "replacement transaction underpriced") && attempt < maxRetries { + if (strings.Contains(err.Error(), "replacement transaction underpriced") || + strings.Contains(err.Error(), "nonce too low") || + strings.Contains(err.Error(), "already known")) && attempt < maxRetries { time.Sleep(time.Duration(2*(attempt+1)) * time.Second) continue } @@ -1410,7 +1520,7 @@ func TestEVMIntegrationV1(t *testing.T) { // Setup resource server with EVM v1 // V1 doesn't have separate server, uses V2 server - evmServerV1 := evmserver.NewExactEvmScheme() + evmServerV1 := exactevmserver.NewExactEvmScheme() server := x402.Newx402ResourceServer( x402.WithFacilitatorClient(facilitatorClient), ) @@ -1540,3 +1650,335 @@ func TestEVMIntegrationV1(t *testing.T) { }) } */ + +func TestEVMIntegrationV2UptoPermit2(t *testing.T) { + clientPrivateKey := os.Getenv("EVM_CLIENT_PRIVATE_KEY") + facilitatorPrivateKey := os.Getenv("EVM_FACILITATOR_PRIVATE_KEY") + resourceServerAddress := os.Getenv("EVM_RESOURCE_SERVER_ADDRESS") + + if clientPrivateKey == "" || facilitatorPrivateKey == "" || resourceServerAddress == "" { + t.Skip("Skipping EVM upto Permit2 integration test: EVM_CLIENT_PRIVATE_KEY, EVM_FACILITATOR_PRIVATE_KEY, and EVM_RESOURCE_SERVER_ADDRESS must be set") + } + + ctx := context.Background() + rpcURL := "https://sepolia.base.org" + + t.Run("Upto EVM V2 Permit2 - Full Flow", func(t *testing.T) { + waitForPendingTransactions(t, ctx, facilitatorPrivateKey, rpcURL) + + revokePermit2Approval(t, ctx, clientPrivateKey, + "0x036CbD53842c5426634e7929541eC2318f3dCF7e", + rpcURL, + ) + + clientEthClient, err := ethclient.Dial(rpcURL) + if err != nil { + t.Fatalf("Failed to connect to Base Sepolia: %v", err) + } + defer clientEthClient.Close() + clientSigner, err := evmsigners.NewClientSignerFromPrivateKeyWithClient(clientPrivateKey, clientEthClient) + if err != nil { + t.Fatalf("Failed to create client signer: %v", err) + } + + client := x402.Newx402Client() + uptoClient := uptoevmclient.NewUptoEvmScheme(clientSigner, nil) + client.Register("eip155:84532", uptoClient) + + facilitatorSigner, err := newPermit2FacilitatorEvmSigner(ctx, facilitatorPrivateKey, rpcURL) + if err != nil { + t.Fatalf("Failed to create facilitator signer: %v", err) + } + + facilitator := x402.Newx402Facilitator() + uptoFacilitator := uptoevmfacilitator.NewUptoEvmScheme(facilitatorSigner, nil) + facilitator.Register([]x402.Network{"eip155:84532"}, uptoFacilitator) + + facilitatorClient := &localEvmFacilitatorClient{facilitator: facilitator} + + uptoServer := uptoevmserver.NewUptoEvmScheme() + server := x402.Newx402ResourceServer( + x402.WithFacilitatorClient(facilitatorClient), + ) + server.Register("eip155:84532", uptoServer) + + err = server.Initialize(ctx) + if err != nil { + t.Fatalf("Failed to initialize server: %v", err) + } + + accepts, err := server.BuildPaymentRequirementsFromConfig(ctx, x402.ResourceConfig{ + Scheme: evm.SchemeUpto, + Network: "eip155:84532", + PayTo: resourceServerAddress, + Price: "$0.001", + MaxTimeoutSeconds: 300, + }) + if err != nil { + t.Fatalf("Failed to build payment requirements: %v", err) + } + if accepts[0].Extra["assetTransferMethod"] != "permit2" { + t.Fatalf("Expected Permit2 payment requirements, got extra=%v", accepts[0].Extra) + } + if accepts[0].Extra["facilitatorAddress"] == nil { + t.Fatal("Expected facilitatorAddress in payment requirements extra") + } + + resource := &types.ResourceInfo{ + URL: "https://api.example.com/upto-permit2", + Description: "Upto Permit2 API Access", + MimeType: "application/json", + } + + serverExtensions := map[string]interface{}{ + "eip2612GasSponsoring": map[string]interface{}{ + "info": map[string]interface{}{"description": "EIP-2612 gas sponsoring", "version": "1"}, + "schema": map[string]interface{}{}, + }, + } + paymentRequiredResponse := server.CreatePaymentRequiredResponse(accepts, resource, "", serverExtensions) + + if paymentRequiredResponse.X402Version != 2 { + t.Errorf("Expected X402Version 2, got %d", paymentRequiredResponse.X402Version) + } + + selected, err := client.SelectPaymentRequirements(accepts) + if err != nil { + t.Fatalf("Failed to select payment requirements: %v", err) + } + + paymentPayload, err := client.CreatePaymentPayload(ctx, selected, resource, paymentRequiredResponse.Extensions) + if err != nil { + t.Fatalf("Failed to create payment payload: %v", err) + } + + if !evm.IsUptoPermit2Payload(paymentPayload.Payload) { + t.Error("Expected upto Permit2 payload") + } + + uptoPayload, err := evm.UptoPermit2PayloadFromMap(paymentPayload.Payload) + if err != nil { + t.Fatalf("Failed to parse upto Permit2 payload: %v", err) + } + + if uptoPayload.Permit2Authorization.Spender != evm.X402UptoPermit2ProxyAddress { + t.Errorf("Expected spender %s, got %s", evm.X402UptoPermit2ProxyAddress, uptoPayload.Permit2Authorization.Spender) + } + + if uptoPayload.Permit2Authorization.Witness.Facilitator == "" { + t.Error("Expected facilitator in witness") + } + + accepted := server.FindMatchingRequirements(accepts, paymentPayload) + if accepted == nil { + t.Fatal("No matching payment requirements found") + } + + verifyResponse, err := server.VerifyPayment(ctx, paymentPayload, *accepted) + if err != nil { + t.Fatalf("Failed to verify payment: %v", err) + } + if !verifyResponse.IsValid { + t.Fatalf("Payment verification failed: %s", verifyResponse.InvalidReason) + } + + settleResponse, err := server.SettlePayment(ctx, paymentPayload, *accepted, nil) + if err != nil { + t.Fatalf("Failed to settle payment: %v", err) + } + if !settleResponse.Success { + t.Fatalf("Payment settlement failed: %s", settleResponse.ErrorReason) + } + if settleResponse.Transaction == "" { + t.Error("Expected transaction hash in settlement response") + } + }) + + t.Run("Upto EVM V2 Permit2 - Partial Settlement", func(t *testing.T) { + waitForPendingTransactions(t, ctx, facilitatorPrivateKey, rpcURL) + + revokePermit2Approval(t, ctx, clientPrivateKey, + "0x036CbD53842c5426634e7929541eC2318f3dCF7e", + rpcURL, + ) + + clientEthClient, err := ethclient.Dial(rpcURL) + if err != nil { + t.Fatalf("Failed to connect to Base Sepolia: %v", err) + } + defer clientEthClient.Close() + clientSigner, err := evmsigners.NewClientSignerFromPrivateKeyWithClient(clientPrivateKey, clientEthClient) + if err != nil { + t.Fatalf("Failed to create client signer: %v", err) + } + + client := x402.Newx402Client() + client.Register("eip155:84532", uptoevmclient.NewUptoEvmScheme(clientSigner, nil)) + + facilitatorSigner, err := newPermit2FacilitatorEvmSigner(ctx, facilitatorPrivateKey, rpcURL) + if err != nil { + t.Fatalf("Failed to create facilitator signer: %v", err) + } + + facilitator := x402.Newx402Facilitator() + facilitator.Register([]x402.Network{"eip155:84532"}, uptoevmfacilitator.NewUptoEvmScheme(facilitatorSigner, nil)) + + facilitatorClient := &localEvmFacilitatorClient{facilitator: facilitator} + + server := x402.Newx402ResourceServer(x402.WithFacilitatorClient(facilitatorClient)) + server.Register("eip155:84532", uptoevmserver.NewUptoEvmScheme()) + + err = server.Initialize(ctx) + if err != nil { + t.Fatalf("Failed to initialize server: %v", err) + } + + // Build requirements with max amount of 1000 (0.001 USDC) + accepts, err := server.BuildPaymentRequirementsFromConfig(ctx, x402.ResourceConfig{ + Scheme: evm.SchemeUpto, + Network: "eip155:84532", + PayTo: resourceServerAddress, + Price: "$0.001", + MaxTimeoutSeconds: 300, + }) + if err != nil { + t.Fatalf("Failed to build payment requirements: %v", err) + } + + resource := &types.ResourceInfo{ + URL: "https://api.example.com/upto-partial", + Description: "Upto Partial Settlement Test", + MimeType: "application/json", + } + + serverExtensions := map[string]interface{}{ + "eip2612GasSponsoring": map[string]interface{}{ + "info": map[string]interface{}{"description": "EIP-2612 gas sponsoring", "version": "1"}, + "schema": map[string]interface{}{}, + }, + } + paymentRequiredResponse := server.CreatePaymentRequiredResponse(accepts, resource, "", serverExtensions) + + selected, err := client.SelectPaymentRequirements(accepts) + if err != nil { + t.Fatalf("Failed to select payment requirements: %v", err) + } + + paymentPayload, err := client.CreatePaymentPayload(ctx, selected, resource, paymentRequiredResponse.Extensions) + if err != nil { + t.Fatalf("Failed to create payment payload: %v", err) + } + + accepted := server.FindMatchingRequirements(accepts, paymentPayload) + if accepted == nil { + t.Fatal("No matching payment requirements found") + } + + verifyResponse, err := server.VerifyPayment(ctx, paymentPayload, *accepted) + if err != nil { + t.Fatalf("Failed to verify payment: %v", err) + } + if !verifyResponse.IsValid { + t.Fatalf("Payment verification failed: %s", verifyResponse.InvalidReason) + } + + // Settle with partial amount (500 out of 1000 authorized max) + overrides := &x402.SettlementOverrides{Amount: "500"} + settleResponse, err := server.SettlePayment(ctx, paymentPayload, *accepted, overrides) + if err != nil { + t.Fatalf("Failed to settle partial payment: %v", err) + } + if !settleResponse.Success { + t.Fatalf("Partial payment settlement failed: %s", settleResponse.ErrorReason) + } + if settleResponse.Transaction == "" { + t.Error("Expected transaction hash for partial settlement") + } + if settleResponse.Amount != "500" { + t.Errorf("Expected settle amount '500', got '%s'", settleResponse.Amount) + } + }) + + t.Run("Upto EVM V2 Permit2 - Zero Settlement", func(t *testing.T) { + clientEthClient, err := ethclient.Dial(rpcURL) + if err != nil { + t.Fatalf("Failed to connect to Base Sepolia: %v", err) + } + defer clientEthClient.Close() + clientSigner, err := evmsigners.NewClientSignerFromPrivateKeyWithClient(clientPrivateKey, clientEthClient) + if err != nil { + t.Fatalf("Failed to create client signer: %v", err) + } + + client := x402.Newx402Client() + client.Register("eip155:84532", uptoevmclient.NewUptoEvmScheme(clientSigner, nil)) + + facilitatorSigner, err := newPermit2FacilitatorEvmSigner(ctx, facilitatorPrivateKey, rpcURL) + if err != nil { + t.Fatalf("Failed to create facilitator signer: %v", err) + } + + facilitator := x402.Newx402Facilitator() + facilitator.Register([]x402.Network{"eip155:84532"}, uptoevmfacilitator.NewUptoEvmScheme(facilitatorSigner, nil)) + + facilitatorClient := &localEvmFacilitatorClient{facilitator: facilitator} + + server := x402.Newx402ResourceServer(x402.WithFacilitatorClient(facilitatorClient)) + server.Register("eip155:84532", uptoevmserver.NewUptoEvmScheme()) + + err = server.Initialize(ctx) + if err != nil { + t.Fatalf("Failed to initialize server: %v", err) + } + + accepts, err := server.BuildPaymentRequirementsFromConfig(ctx, x402.ResourceConfig{ + Scheme: evm.SchemeUpto, + Network: "eip155:84532", + PayTo: resourceServerAddress, + Price: "$0.001", + MaxTimeoutSeconds: 300, + }) + if err != nil { + t.Fatalf("Failed to build payment requirements: %v", err) + } + + resource := &types.ResourceInfo{ + URL: "https://api.example.com/upto-zero", + Description: "Upto Zero Settlement Test", + MimeType: "application/json", + } + + paymentRequiredResponse := server.CreatePaymentRequiredResponse(accepts, resource, "", nil) + + selected, err := client.SelectPaymentRequirements(accepts) + if err != nil { + t.Fatalf("Failed to select payment requirements: %v", err) + } + + paymentPayload, err := client.CreatePaymentPayload(ctx, selected, resource, paymentRequiredResponse.Extensions) + if err != nil { + t.Fatalf("Failed to create payment payload: %v", err) + } + + accepted := server.FindMatchingRequirements(accepts, paymentPayload) + if accepted == nil { + t.Fatal("No matching payment requirements found") + } + + // Settle with zero amount — no on-chain tx + overrides := &x402.SettlementOverrides{Amount: "0"} + settleResponse, err := server.SettlePayment(ctx, paymentPayload, *accepted, overrides) + if err != nil { + t.Fatalf("Failed to settle zero payment: %v", err) + } + if !settleResponse.Success { + t.Fatalf("Zero settlement failed: %s", settleResponse.ErrorReason) + } + if settleResponse.Transaction != "" { + t.Error("Expected empty transaction hash for zero settlement") + } + if settleResponse.Amount != "0" { + t.Errorf("Expected settle amount '0', got '%s'", settleResponse.Amount) + } + }) +} diff --git a/go/test/integration/http_test.go b/go/test/integration/http_test.go index c6af8da20a..7397977872 100644 --- a/go/test/integration/http_test.go +++ b/go/test/integration/http_test.go @@ -216,6 +216,8 @@ func TestHTTPIntegration(t *testing.T) { ctx, *httpProcessResult2.PaymentPayload, *httpProcessResult2.PaymentRequirements, + nil, + nil, ) if !settlementResult.Success { t.Fatalf("Failed to process settlement: %v", settlementResult.ErrorReason) diff --git a/go/test/integration/mcp_evm_test.go b/go/test/integration/mcp_evm_test.go index c2ce1a54d2..ce5c75c1c4 100644 --- a/go/test/integration/mcp_evm_test.go +++ b/go/test/integration/mcp_evm_test.go @@ -74,7 +74,7 @@ func TestMCPEVMIntegration(t *testing.T) { } paymentClient := x402.Newx402Client() - evmClientScheme := evmclient.NewExactEvmScheme(clientSigner) + evmClientScheme := evmclient.NewExactEvmScheme(clientSigner, nil) paymentClient.Register(TEST_NETWORK, evmClientScheme) // Get client address diff --git a/go/test/integration/svm_test.go b/go/test/integration/svm_test.go index 1335574073..960bdc4ecd 100644 --- a/go/test/integration/svm_test.go +++ b/go/test/integration/svm_test.go @@ -386,7 +386,7 @@ func TestSVMIntegrationV2(t *testing.T) { // Server does work here... // Server - settles payment (REAL ON-CHAIN TRANSACTION, typed) - settleResponse, err := server.SettlePayment(ctx, paymentPayload, *accepted) + settleResponse, err := server.SettlePayment(ctx, paymentPayload, *accepted, nil) if err != nil { t.Fatalf("Failed to settle payment: %v", err) } diff --git a/go/test/unit/evm_client_facilitator_test.go b/go/test/unit/evm_client_facilitator_test.go index 391b10c336..6837e0c13d 100644 --- a/go/test/unit/evm_client_facilitator_test.go +++ b/go/test/unit/evm_client_facilitator_test.go @@ -2,16 +2,20 @@ package unit_test import ( "context" + "encoding/json" "fmt" "math/big" "strings" "testing" + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/crypto" "github.com/coinbase/x402/go/mechanisms/evm" evmclient "github.com/coinbase/x402/go/mechanisms/evm/exact/client" evmfacilitator "github.com/coinbase/x402/go/mechanisms/evm/exact/facilitator" + evmv1facilitator "github.com/coinbase/x402/go/mechanisms/evm/exact/v1/facilitator" "github.com/coinbase/x402/go/types" ) @@ -72,12 +76,15 @@ type mockFacilitatorSigner struct { chainID *big.Int writeContractTxHash string writeContractError error + sendTransactionError error receiptStatus uint64 receiptError error readContractError error + readContractFn func(contractAddress string, abi []byte, functionName string, args ...interface{}) (interface{}, error) verifyTypedDataResult bool verifyTypedDataError error code []byte + getCodeError error authorizationStateUsed bool lastWriteFunctionName string } @@ -101,6 +108,9 @@ func (m *mockFacilitatorSigner) GetChainID(ctx context.Context) (*big.Int, error } func (m *mockFacilitatorSigner) GetCode(ctx context.Context, address string) ([]byte, error) { + if m.getCodeError != nil { + return nil, m.getCodeError + } return m.code, nil } @@ -111,6 +121,9 @@ func (m *mockFacilitatorSigner) ReadContract( functionName string, args ...interface{}, ) (interface{}, error) { + if m.readContractFn != nil { + return m.readContractFn(contractAddress, abi, functionName, args...) + } if m.readContractError != nil { return nil, m.readContractError } @@ -150,6 +163,9 @@ func (m *mockFacilitatorSigner) WriteContract( } func (m *mockFacilitatorSigner) SendTransaction(ctx context.Context, to string, data []byte) (string, error) { + if m.sendTransactionError != nil { + return "", m.sendTransactionError + } return "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", nil } @@ -190,7 +206,7 @@ func (m *mockFacilitatorSigner) VerifyTypedData( // TestExactEvmSchemeScheme tests the Scheme() method func TestExactEvmSchemeScheme(t *testing.T) { signer := &mockClientSigner{} - client := evmclient.NewExactEvmScheme(signer) + client := evmclient.NewExactEvmScheme(signer, nil) if client.Scheme() != evm.SchemeExact { t.Errorf("Expected scheme %s, got %s", evm.SchemeExact, client.Scheme()) @@ -201,7 +217,7 @@ func TestExactEvmSchemeScheme(t *testing.T) { func TestCreatePaymentPayloadEIP3009(t *testing.T) { ctx := context.Background() signer := &mockClientSigner{address: "0xClientAddress1234567890123456789012"} - client := evmclient.NewExactEvmScheme(signer) + client := evmclient.NewExactEvmScheme(signer, nil) t.Run("Creates valid EIP-3009 payload", func(t *testing.T) { requirements := types.PaymentRequirements{ @@ -299,7 +315,7 @@ func TestCreatePaymentPayloadEIP3009(t *testing.T) { func TestCreatePaymentPayloadPermit2(t *testing.T) { ctx := context.Background() signer := &mockClientSigner{address: "0xClientAddress1234567890123456789012"} - client := evmclient.NewExactEvmScheme(signer) + client := evmclient.NewExactEvmScheme(signer, nil) t.Run("Creates valid Permit2 payload", func(t *testing.T) { requirements := types.PaymentRequirements{ @@ -486,6 +502,119 @@ func mockSignature65Bytes() string { return "0x" + strings.Repeat("00", 65) } +type mockMulticallResult struct { + Success bool + ReturnData []byte +} + +func defaultEIP3009Requirements() types.PaymentRequirements { + return types.PaymentRequirements{ + Scheme: evm.SchemeExact, + Network: "eip155:84532", + Asset: "0x036CbD53842c5426634e7929541eC2318f3dCF7e", + Amount: "1000000", + PayTo: "0x9876543210987654321098765432109876543210", + Extra: map[string]interface{}{ + "name": "USDC", + "version": "2", + }, + } +} + +func defaultEIP3009Payload(signature string) types.PaymentPayload { + return types.PaymentPayload{ + X402Version: 2, + Accepted: types.PaymentRequirements{ + Scheme: evm.SchemeExact, + Network: "eip155:84532", + }, + Payload: map[string]interface{}{ + "signature": signature, + "authorization": map[string]interface{}{ + "from": "0x1234567890123456789012345678901234567890", + "to": "0x9876543210987654321098765432109876543210", + "value": "1000000", + "validAfter": "0", + "validBefore": "99999999999", + "nonce": "0x0000000000000000000000000000000000000000000000000000000000000001", + }, + }, + } +} + +func defaultEIP3009RequirementsV1(t *testing.T) types.PaymentRequirementsV1 { + t.Helper() + + extra, err := json.Marshal(map[string]interface{}{ + "name": "USDC", + "version": "2", + }) + if err != nil { + t.Fatalf("failed to marshal v1 extra: %v", err) + } + + raw := json.RawMessage(extra) + return types.PaymentRequirementsV1{ + Scheme: evm.SchemeExact, + Network: "base-sepolia", + MaxAmountRequired: "1000000", + PayTo: "0x9876543210987654321098765432109876543210", + Asset: "0x036CbD53842c5426634e7929541eC2318f3dCF7e", + Extra: &raw, + } +} + +func defaultEIP3009PayloadV1(signature string) types.PaymentPayloadV1 { + return types.PaymentPayloadV1{ + X402Version: 1, + Scheme: evm.SchemeExact, + Network: "base-sepolia", + Payload: defaultEIP3009Payload(signature).Payload, + } +} + +func packABIOutput(t *testing.T, abiBytes []byte, functionName string, values ...interface{}) []byte { + t.Helper() + + contractABI, err := abi.JSON(strings.NewReader(string(abiBytes))) + if err != nil { + t.Fatalf("failed to parse ABI for %s: %v", functionName, err) + } + + data, err := contractABI.Methods[functionName].Outputs.Pack(values...) + if err != nil { + t.Fatalf("failed to pack output for %s: %v", functionName, err) + } + + return data +} + +func wrapERC6492SignatureForTest(t *testing.T, factory common.Address, factoryData []byte, originalSig []byte) string { + t.Helper() + + addressTy, err := abi.NewType("address", "", nil) + if err != nil { + t.Fatalf("failed to create address ABI type: %v", err) + } + bytesTy, err := abi.NewType("bytes", "", nil) + if err != nil { + t.Fatalf("failed to create bytes ABI type: %v", err) + } + + arguments := abi.Arguments{ + {Type: addressTy}, + {Type: bytesTy}, + {Type: bytesTy}, + } + + packed, err := arguments.Pack(factory, factoryData, originalSig) + if err != nil { + t.Fatalf("failed to pack ERC-6492 signature: %v", err) + } + + return "0x" + fmt.Sprintf("%x%x", packed, common.Hex2Bytes(evm.ERC6492MagicValue[2:])) +} + // TestVerifyPermit2InvalidInputs tests validation in VerifyPermit2 func TestVerifyPermit2InvalidInputs(t *testing.T) { ctx := context.Background() @@ -525,7 +654,7 @@ func TestVerifyPermit2InvalidInputs(t *testing.T) { }, } - _, err := evmfacilitator.VerifyPermit2(ctx, signer, validPayload, validRequirements, permit2Payload, nil) + _, err := evmfacilitator.VerifyPermit2(ctx, signer, validPayload, validRequirements, permit2Payload, nil, nil) if err == nil { t.Error("Expected error for invalid spender") } @@ -550,7 +679,7 @@ func TestVerifyPermit2InvalidInputs(t *testing.T) { }, } - _, err := evmfacilitator.VerifyPermit2(ctx, signer, validPayload, validRequirements, permit2Payload, nil) + _, err := evmfacilitator.VerifyPermit2(ctx, signer, validPayload, validRequirements, permit2Payload, nil, nil) if err == nil { t.Error("Expected error for recipient mismatch") } @@ -572,7 +701,7 @@ func TestVerifyPermit2InvalidInputs(t *testing.T) { }, } - _, err := evmfacilitator.VerifyPermit2(ctx, signer, validPayload, validRequirements, permit2Payload, nil) + _, err := evmfacilitator.VerifyPermit2(ctx, signer, validPayload, validRequirements, permit2Payload, nil, nil) if err == nil { t.Error("Expected error for expired deadline") } @@ -597,7 +726,7 @@ func TestVerifyPermit2InvalidInputs(t *testing.T) { }, } - _, err := evmfacilitator.VerifyPermit2(ctx, signer, validPayload, validRequirements, permit2Payload, nil) + _, err := evmfacilitator.VerifyPermit2(ctx, signer, validPayload, validRequirements, permit2Payload, nil, nil) if err == nil { t.Error("Expected error for not-yet-valid payment") } @@ -619,7 +748,7 @@ func TestVerifyPermit2InvalidInputs(t *testing.T) { }, } - _, err := evmfacilitator.VerifyPermit2(ctx, signer, validPayload, validRequirements, permit2Payload, nil) + _, err := evmfacilitator.VerifyPermit2(ctx, signer, validPayload, validRequirements, permit2Payload, nil, nil) if err == nil { t.Error("Expected error for insufficient amount") } @@ -641,7 +770,7 @@ func TestVerifyPermit2InvalidInputs(t *testing.T) { }, } - _, err := evmfacilitator.VerifyPermit2(ctx, signer, validPayload, validRequirements, permit2Payload, nil) + _, err := evmfacilitator.VerifyPermit2(ctx, signer, validPayload, validRequirements, permit2Payload, nil, nil) if err == nil { t.Error("Expected error for token mismatch") } @@ -663,7 +792,7 @@ func TestVerifyPermit2InvalidInputs(t *testing.T) { }, } - _, err := evmfacilitator.VerifyPermit2(ctx, signer, validPayload, validRequirements, permit2Payload, nil) + _, err := evmfacilitator.VerifyPermit2(ctx, signer, validPayload, validRequirements, permit2Payload, nil, nil) if err == nil { t.Error("Expected error for invalid deadline format") } @@ -693,7 +822,7 @@ func TestVerifyPermit2InvalidInputs(t *testing.T) { }, } - _, err := evmfacilitator.VerifyPermit2(ctx, signer, wrongSchemePayload, validRequirements, permit2Payload, nil) + _, err := evmfacilitator.VerifyPermit2(ctx, signer, wrongSchemePayload, validRequirements, permit2Payload, nil, nil) if err == nil { t.Error("Expected error for scheme mismatch") } @@ -723,7 +852,7 @@ func TestVerifyPermit2InvalidInputs(t *testing.T) { }, } - _, err := evmfacilitator.VerifyPermit2(ctx, signer, wrongNetworkPayload, validRequirements, permit2Payload, nil) + _, err := evmfacilitator.VerifyPermit2(ctx, signer, wrongNetworkPayload, validRequirements, permit2Payload, nil, nil) if err == nil { t.Error("Expected error for network mismatch") } @@ -804,6 +933,309 @@ func TestVerifyEIP3009TimingValidation(t *testing.T) { }) } +func TestVerifyEIP3009RejectsOverpayment(t *testing.T) { + ctx := context.Background() + requirements := defaultEIP3009Requirements() + payload := defaultEIP3009Payload(mockSignature65Bytes()) + payload.Payload["authorization"].(map[string]interface{})["value"] = "1000001" + + signer := &mockFacilitatorSigner{ + verifyTypedDataResult: true, + readContractFn: func(contractAddress string, abiBytes []byte, functionName string, args ...interface{}) (interface{}, error) { + if functionName == evm.FunctionTransferWithAuthorization { + return nil, nil + } + return nil, fmt.Errorf("unsupported function: %s", functionName) + }, + } + scheme := evmfacilitator.NewExactEvmScheme(signer, nil) + + _, err := scheme.Verify(ctx, payload, requirements, nil) + if err == nil { + t.Fatal("expected overpayment mismatch error") + } + if !strings.Contains(err.Error(), evmfacilitator.ErrAuthorizationValueMismatch) { + t.Fatalf("expected %q, got %v", evmfacilitator.ErrAuthorizationValueMismatch, err) + } +} + +func TestVerifyEIP3009V1RejectsOverpayment(t *testing.T) { + ctx := context.Background() + requirements := defaultEIP3009RequirementsV1(t) + payload := defaultEIP3009PayloadV1(mockSignature65Bytes()) + payload.Payload["authorization"].(map[string]interface{})["value"] = "1000001" + + signer := &mockFacilitatorSigner{ + verifyTypedDataResult: true, + readContractFn: func(contractAddress string, abiBytes []byte, functionName string, args ...interface{}) (interface{}, error) { + if functionName == evm.FunctionTransferWithAuthorization { + return nil, nil + } + return nil, fmt.Errorf("unsupported function: %s", functionName) + }, + } + scheme := evmv1facilitator.NewExactEvmSchemeV1(signer, nil) + + _, err := scheme.Verify(ctx, payload, requirements, nil) + if err == nil { + t.Fatal("expected overpayment mismatch error") + } + if !strings.Contains(err.Error(), evmv1facilitator.ErrAuthorizationValueMismatch) { + t.Fatalf("expected %q, got %v", evmv1facilitator.ErrAuthorizationValueMismatch, err) + } +} + +func TestVerifyEIP3009SimulationParity(t *testing.T) { + ctx := context.Background() + requirements := defaultEIP3009Requirements() + + t.Run("Rejects wrong token name from simulation diagnostics", func(t *testing.T) { + factory := common.HexToAddress("0x1111111111111111111111111111111111111111") + payload := defaultEIP3009Payload("0x") + payload.Payload["signature"] = wrapERC6492SignatureForTest(t, factory, []byte{0xde, 0xad}, make([]byte, 65)) + + multicallCount := 0 + signer := &mockFacilitatorSigner{ + readContractFn: func(contractAddress string, abiBytes []byte, functionName string, args ...interface{}) (interface{}, error) { + if functionName != evm.FunctionTryAggregate { + return nil, fmt.Errorf("unsupported function: %s", functionName) + } + multicallCount++ + if multicallCount == 1 { + return []mockMulticallResult{ + {Success: true, ReturnData: []byte{}}, + {Success: false, ReturnData: []byte{}}, + }, nil + } + return []mockMulticallResult{ + {Success: true, ReturnData: packABIOutput(t, evm.ERC20BalanceOfABI, "balanceOf", big.NewInt(1_000_000))}, + {Success: true, ReturnData: packABIOutput(t, evm.ERC20NameABI, "name", "Wrong Name")}, + {Success: true, ReturnData: packABIOutput(t, evm.ERC20VersionABI, "version", "2")}, + {Success: true, ReturnData: packABIOutput(t, evm.AuthorizationStateABI, evm.FunctionAuthorizationState, false)}, + }, nil + }, + } + scheme := evmfacilitator.NewExactEvmScheme(signer, nil) + + _, err := scheme.Verify(ctx, payload, requirements, nil) + if err == nil { + t.Fatal("expected token name mismatch") + } + if !strings.Contains(err.Error(), evmfacilitator.ErrEip3009TokenNameMismatch) { + t.Fatalf("expected %q, got %v", evmfacilitator.ErrEip3009TokenNameMismatch, err) + } + }) + + t.Run("Rejects wrong token version from simulation diagnostics", func(t *testing.T) { + factory := common.HexToAddress("0x1111111111111111111111111111111111111111") + payload := defaultEIP3009Payload("0x") + payload.Payload["signature"] = wrapERC6492SignatureForTest(t, factory, []byte{0xde, 0xad}, make([]byte, 65)) + + multicallCount := 0 + signer := &mockFacilitatorSigner{ + readContractFn: func(contractAddress string, abiBytes []byte, functionName string, args ...interface{}) (interface{}, error) { + if functionName != evm.FunctionTryAggregate { + return nil, fmt.Errorf("unsupported function: %s", functionName) + } + multicallCount++ + if multicallCount == 1 { + return []mockMulticallResult{ + {Success: true, ReturnData: []byte{}}, + {Success: false, ReturnData: []byte{}}, + }, nil + } + return []mockMulticallResult{ + {Success: true, ReturnData: packABIOutput(t, evm.ERC20BalanceOfABI, "balanceOf", big.NewInt(1_000_000))}, + {Success: true, ReturnData: packABIOutput(t, evm.ERC20NameABI, "name", "USDC")}, + {Success: true, ReturnData: packABIOutput(t, evm.ERC20VersionABI, "version", "999")}, + {Success: true, ReturnData: packABIOutput(t, evm.AuthorizationStateABI, evm.FunctionAuthorizationState, false)}, + }, nil + }, + } + scheme := evmfacilitator.NewExactEvmScheme(signer, nil) + + _, err := scheme.Verify(ctx, payload, requirements, nil) + if err == nil { + t.Fatal("expected token version mismatch") + } + if !strings.Contains(err.Error(), evmfacilitator.ErrEip3009TokenVersionMismatch) { + t.Fatalf("expected %q, got %v", evmfacilitator.ErrEip3009TokenVersionMismatch, err) + } + }) + + t.Run("Accepts deployed smart wallet when simulation succeeds", func(t *testing.T) { + signer := &mockFacilitatorSigner{ + code: []byte{0x60, 0x80}, + readContractFn: func(contractAddress string, abiBytes []byte, functionName string, args ...interface{}) (interface{}, error) { + switch functionName { + case "isValidSignature": + return []byte{0x16, 0x26, 0xba, 0x7e}, nil + case evm.FunctionTransferWithAuthorization: + return nil, nil + default: + return nil, fmt.Errorf("unsupported function: %s", functionName) + } + }, + } + scheme := evmfacilitator.NewExactEvmScheme(signer, nil) + + verifyResp, err := scheme.Verify(ctx, defaultEIP3009Payload(mockSignature65Bytes()), requirements, nil) + if err != nil { + t.Fatalf("expected verification success, got %v", err) + } + if !verifyResp.IsValid { + t.Fatal("expected valid verification response") + } + }) + + t.Run("Rejects undeployed ERC-6492 when deploy+transfer simulation fails", func(t *testing.T) { + factory := common.HexToAddress("0x1111111111111111111111111111111111111111") + payload := defaultEIP3009Payload("0x") + payload.Payload["signature"] = wrapERC6492SignatureForTest(t, factory, []byte{0xde, 0xad}, make([]byte, 65)) + + multicallCount := 0 + signer := &mockFacilitatorSigner{ + readContractFn: func(contractAddress string, abiBytes []byte, functionName string, args ...interface{}) (interface{}, error) { + if functionName != evm.FunctionTryAggregate { + return nil, fmt.Errorf("unsupported function: %s", functionName) + } + multicallCount++ + if multicallCount == 1 { + return []mockMulticallResult{ + {Success: true, ReturnData: []byte{}}, + {Success: false, ReturnData: []byte{}}, + }, nil + } + return []mockMulticallResult{ + {Success: true, ReturnData: packABIOutput(t, evm.ERC20BalanceOfABI, "balanceOf", big.NewInt(1_000_000))}, + {Success: true, ReturnData: packABIOutput(t, evm.ERC20NameABI, "name", "USDC")}, + {Success: true, ReturnData: packABIOutput(t, evm.ERC20VersionABI, "version", "2")}, + {Success: true, ReturnData: packABIOutput(t, evm.AuthorizationStateABI, evm.FunctionAuthorizationState, false)}, + }, nil + }, + } + scheme := evmfacilitator.NewExactEvmScheme(signer, nil) + + _, err := scheme.Verify(ctx, payload, requirements, nil) + if err == nil { + t.Fatal("expected simulation failure") + } + if !strings.Contains(err.Error(), evmfacilitator.ErrEip3009SimulationFailed) { + t.Fatalf("expected %q, got %v", evmfacilitator.ErrEip3009SimulationFailed, err) + } + }) + + t.Run("Accepts undeployed ERC-6492 when deploy+transfer simulation succeeds", func(t *testing.T) { + factory := common.HexToAddress("0x2222222222222222222222222222222222222222") + payload := defaultEIP3009Payload("0x") + payload.Payload["signature"] = wrapERC6492SignatureForTest(t, factory, []byte{0xbe, 0xef}, make([]byte, 65)) + + signer := &mockFacilitatorSigner{ + readContractFn: func(contractAddress string, abiBytes []byte, functionName string, args ...interface{}) (interface{}, error) { + if functionName != evm.FunctionTryAggregate { + return nil, fmt.Errorf("unsupported function: %s", functionName) + } + return []mockMulticallResult{ + {Success: true, ReturnData: []byte{}}, + {Success: true, ReturnData: []byte{}}, + }, nil + }, + } + scheme := evmfacilitator.NewExactEvmScheme(signer, nil) + + verifyResp, err := scheme.Verify(ctx, payload, requirements, nil) + if err != nil { + t.Fatalf("expected verification success, got %v", err) + } + if !verifyResp.IsValid { + t.Fatal("expected valid verification response") + } + }) +} + +func TestSettleEIP3009SimulateInSettleToggle(t *testing.T) { + ctx := context.Background() + requirements := defaultEIP3009Requirements() + payload := defaultEIP3009Payload(mockSignature65Bytes()) + + runCase := func(simulateInSettle bool) int { + simulations := 0 + signer := &mockFacilitatorSigner{ + code: []byte{0x60, 0x80}, + readContractFn: func(contractAddress string, abiBytes []byte, functionName string, args ...interface{}) (interface{}, error) { + switch functionName { + case "isValidSignature": + return []byte{0x16, 0x26, 0xba, 0x7e}, nil + case evm.FunctionTransferWithAuthorization: + simulations++ + return nil, nil + default: + return nil, fmt.Errorf("unsupported function: %s", functionName) + } + }, + } + scheme := evmfacilitator.NewExactEvmScheme(signer, &evmfacilitator.ExactEvmSchemeConfig{ + SimulateInSettle: simulateInSettle, + }) + + if _, err := scheme.Verify(ctx, payload, requirements, nil); err != nil { + t.Fatalf("verify failed: %v", err) + } + if _, err := scheme.Settle(ctx, payload, requirements, nil); err != nil { + t.Fatalf("settle failed: %v", err) + } + + return simulations + } + + if got := runCase(false); got != 1 { + t.Fatalf("expected 1 simulation when SimulateInSettle=false, got %d", got) + } + if got := runCase(true); got != 2 { + t.Fatalf("expected 2 simulations when SimulateInSettle=true, got %d", got) + } +} + +func TestVerifyEIP3009V1UsesSimulationDiagnostics(t *testing.T) { + ctx := context.Background() + requirements := defaultEIP3009RequirementsV1(t) + + factory := common.HexToAddress("0x1111111111111111111111111111111111111111") + payload := defaultEIP3009PayloadV1("0x") + payload.Payload["signature"] = wrapERC6492SignatureForTest(t, factory, []byte{0xde, 0xad}, make([]byte, 65)) + + multicallCount := 0 + signer := &mockFacilitatorSigner{ + readContractFn: func(contractAddress string, abiBytes []byte, functionName string, args ...interface{}) (interface{}, error) { + if functionName != evm.FunctionTryAggregate { + return nil, fmt.Errorf("unsupported function: %s", functionName) + } + multicallCount++ + if multicallCount == 1 { + return []mockMulticallResult{ + {Success: true, ReturnData: []byte{}}, + {Success: false, ReturnData: []byte{}}, + }, nil + } + return []mockMulticallResult{ + {Success: true, ReturnData: packABIOutput(t, evm.ERC20BalanceOfABI, "balanceOf", big.NewInt(1_000_000))}, + {Success: true, ReturnData: packABIOutput(t, evm.ERC20NameABI, "name", "Wrong Name")}, + {Success: true, ReturnData: packABIOutput(t, evm.ERC20VersionABI, "version", "2")}, + {Success: true, ReturnData: packABIOutput(t, evm.AuthorizationStateABI, evm.FunctionAuthorizationState, false)}, + }, nil + }, + } + scheme := evmv1facilitator.NewExactEvmSchemeV1(signer, nil) + + _, err := scheme.Verify(ctx, payload, requirements, nil) + if err == nil { + t.Fatal("expected token name mismatch") + } + if !strings.Contains(err.Error(), evmfacilitator.ErrEip3009TokenNameMismatch) { + t.Fatalf("expected %q, got %v", evmfacilitator.ErrEip3009TokenNameMismatch, err) + } +} + // TestExactEvmFacilitatorScheme tests the scheme initialization func TestExactEvmFacilitatorScheme(t *testing.T) { signer := &mockFacilitatorSigner{} @@ -838,7 +1270,7 @@ func TestCreatePaymentPayloadWithExtensions_EIP2612(t *testing.T) { t.Run("Creates EIP-2612 extension when server advertises and allowance is 0", func(t *testing.T) { signer := &mockClientSigner{address: "0xClientAddress1234567890123456789012"} - client := evmclient.NewExactEvmScheme(signer) + client := evmclient.NewExactEvmScheme(signer, nil) requirements := types.PaymentRequirements{ Scheme: evm.SchemeExact, @@ -879,7 +1311,7 @@ func TestCreatePaymentPayloadWithExtensions_EIP2612(t *testing.T) { t.Run("No extension when server does not advertise eip2612GasSponsoring", func(t *testing.T) { signer := &mockClientSigner{address: "0xClientAddress1234567890123456789012"} - client := evmclient.NewExactEvmScheme(signer) + client := evmclient.NewExactEvmScheme(signer, nil) requirements := types.PaymentRequirements{ Scheme: evm.SchemeExact, @@ -909,7 +1341,7 @@ func TestCreatePaymentPayloadWithExtensions_EIP2612(t *testing.T) { t.Run("No extension when token metadata missing", func(t *testing.T) { signer := &mockClientSigner{address: "0xClientAddress1234567890123456789012"} - client := evmclient.NewExactEvmScheme(signer) + client := evmclient.NewExactEvmScheme(signer, nil) requirements := types.PaymentRequirements{ Scheme: evm.SchemeExact, @@ -1035,7 +1467,7 @@ func TestSettlePermit2_EIP2612Routing(t *testing.T) { }, } - _, err := evmfacilitator.SettlePermit2(ctx, signer, payload, validRequirements, permit2Payload, nil) + _, err := evmfacilitator.SettlePermit2(ctx, signer, payload, validRequirements, permit2Payload, nil, nil) if err != nil { t.Fatalf("Unexpected error: %v", err) } @@ -1069,7 +1501,7 @@ func TestSettlePermit2_EIP2612Routing(t *testing.T) { // No extensions } - _, err := evmfacilitator.SettlePermit2(ctx, signer, payload, validRequirements, permit2Payload, nil) + _, err := evmfacilitator.SettlePermit2(ctx, signer, payload, validRequirements, permit2Payload, nil, nil) if err != nil { t.Fatalf("Unexpected error: %v", err) } @@ -1163,7 +1595,7 @@ func TestSettlePermit2_ContractRevertErrors(t *testing.T) { }, } - _, err := evmfacilitator.SettlePermit2(ctx, signer, payload, validRequirements, permit2Payload, nil) + _, err := evmfacilitator.SettlePermit2(ctx, signer, payload, validRequirements, permit2Payload, nil, nil) if err == nil { t.Fatal("Expected error from SettlePermit2") } diff --git a/go/test/unit/evm_test.go b/go/test/unit/evm_test.go index a8ddb80ba0..e0363dc0aa 100644 --- a/go/test/unit/evm_test.go +++ b/go/test/unit/evm_test.go @@ -86,7 +86,7 @@ func TestEVMVersionMismatch(t *testing.T) { // Setup V2 client clientSigner := &mockClientEvmSigner{} client := x402.Newx402Client() - evmClient := evmclient.NewExactEvmScheme(clientSigner) + evmClient := evmclient.NewExactEvmScheme(clientSigner, nil) client.Register("eip155:8453", evmClient) // V2 requirements (typed) @@ -129,7 +129,7 @@ func TestEVMDualVersionSupport(t *testing.T) { client.RegisterV1("base", evmClientV1) // Register V2 implementation with CAIP-2 - evmClient := evmclient.NewExactEvmScheme(clientSigner) + evmClient := evmclient.NewExactEvmScheme(clientSigner, nil) client.Register("eip155:8453", evmClient) // V1 requirements use legacy network name @@ -195,7 +195,7 @@ func TestEVMDualVersionSupport(t *testing.T) { client.RegisterV1("base", evmClientV1) // Register V2 implementation with CAIP-2 - evmClient := evmclient.NewExactEvmScheme(clientSigner) + evmClient := evmclient.NewExactEvmScheme(clientSigner, nil) client.Register("eip155:8453", evmClient) // V2 requirements use CAIP-2 diff --git a/go/test/unit/evm_v1_utils_test.go b/go/test/unit/evm_v1_utils_test.go index 3d709784ff..dfd278bb91 100644 --- a/go/test/unit/evm_v1_utils_test.go +++ b/go/test/unit/evm_v1_utils_test.go @@ -81,8 +81,27 @@ func TestV1GetNetworkConfig(t *testing.T) { } }) + t.Run("polygon has default asset", func(t *testing.T) { + config, err := evmv1.GetNetworkConfig("polygon") + if err != nil { + t.Fatalf("Failed to get config: %v", err) + } + + if config.ChainID.Int64() != 137 { + t.Errorf("Expected chain ID 137, got %d", config.ChainID.Int64()) + } + + if config.DefaultAsset.Address != "0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359" { + t.Errorf("Expected USDC address, got %s", config.DefaultAsset.Address) + } + + if config.DefaultAsset.Decimals != 6 { + t.Errorf("Expected 6 decimals, got %d", config.DefaultAsset.Decimals) + } + }) + t.Run("network without config returns error", func(t *testing.T) { - _, err := evmv1.GetNetworkConfig("polygon") + _, err := evmv1.GetNetworkConfig("iotex") if err == nil { t.Error("Expected error for network without configured default asset") } @@ -123,8 +142,19 @@ func TestV1GetAssetInfo(t *testing.T) { } }) + t.Run("polygon empty asset uses default USDC", func(t *testing.T) { + info, err := evmv1.GetAssetInfo("polygon", "") + if err != nil { + t.Fatalf("Failed to get asset info: %v", err) + } + + if info.Address != "0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359" { + t.Errorf("Expected Polygon USDC address, got %s", info.Address) + } + }) + t.Run("network without config fails for empty asset", func(t *testing.T) { - _, err := evmv1.GetAssetInfo("polygon", "") + _, err := evmv1.GetAssetInfo("iotex", "") if err == nil { t.Error("Expected error for network without default asset") } diff --git a/go/types.go b/go/types.go index cec884ce98..97ae1fdc1d 100644 --- a/go/types.go +++ b/go/types.go @@ -81,30 +81,45 @@ type ( // VerifyResponse contains the verification result // If verification fails, an error (typically *VerifyError) is returned and this will be nil type VerifyResponse struct { - IsValid bool `json:"isValid"` - InvalidReason string `json:"invalidReason,omitempty"` - InvalidMessage string `json:"invalidMessage,omitempty"` - Payer string `json:"payer,omitempty"` + IsValid bool `json:"isValid"` + InvalidReason string `json:"invalidReason,omitempty"` + InvalidMessage string `json:"invalidMessage,omitempty"` + Payer string `json:"payer,omitempty"` + Extensions map[string]interface{} `json:"extensions,omitempty"` } // SettleResponse contains the settlement result // If settlement fails, an error (typically *SettleError) is returned and this will be nil type SettleResponse struct { - Success bool `json:"success"` - ErrorReason string `json:"errorReason,omitempty"` - ErrorMessage string `json:"errorMessage,omitempty"` - Payer string `json:"payer,omitempty"` - Transaction string `json:"transaction"` - Network Network `json:"network"` + Success bool `json:"success"` + ErrorReason string `json:"errorReason,omitempty"` + ErrorMessage string `json:"errorMessage,omitempty"` + Payer string `json:"payer,omitempty"` + Transaction string `json:"transaction"` + Network Network `json:"network"` + Amount string `json:"amount,omitempty"` + Extensions map[string]interface{} `json:"extensions,omitempty"` +} + +// SettlementOverrides allows overriding settlement parameters. +// Used to support partial settlement (e.g., upto scheme billing by actual usage). +type SettlementOverrides struct { + // Amount to settle. Supports three formats: + // - Raw atomic units: "1000" settles exactly 1000 atomic units. + // - Percent: "50%" settles 50% of PaymentRequirements.Amount (up to 2 decimal places, floored). + // - Dollar price: "$0.05" converts to atomic units using Extra["decimals"] (default 6). + // The resolved amount must be <= the authorized maximum in PaymentRequirements. + Amount string `json:"amount,omitempty"` } // ResourceConfig defines payment configuration for a protected resource type ResourceConfig struct { - Scheme string `json:"scheme"` - PayTo string `json:"payTo"` - Price Price `json:"price"` - Network Network `json:"network"` - MaxTimeoutSeconds int `json:"maxTimeoutSeconds,omitempty"` + Scheme string `json:"scheme"` + PayTo string `json:"payTo"` + Price Price `json:"price"` + Network Network `json:"network"` + MaxTimeoutSeconds int `json:"maxTimeoutSeconds,omitempty"` + Extra map[string]interface{} `json:"extra,omitempty"` } // ============================================================================ diff --git a/python/x402/CHANGELOG.md b/python/x402/CHANGELOG.md index 558598b814..20d25d31f8 100644 --- a/python/x402/CHANGELOG.md +++ b/python/x402/CHANGELOG.md @@ -2,6 +2,43 @@ +## [2.5.0] - 2026-03-19 + +### Fixed + +- Fixed Python HTTP middleware to return `502` instead of `500` when the facilitator responds with invalid JSON or schema-invalid data. ([#545](https://github.com/coinbase/x402/pull/545)) + +### Added + +- Added Permit2 support to the Python SDK exact EVM mechanism with full TS/Go parity. The client routes to Permit2 (`PermitWitnessTransferFrom`) when `assetTransferMethod == "permit2"` in payment requirements extra, and the facilitator verifies and settles via the `x402ExactPermit2Proxy` contract. Includes `eip2612GasSponsoring` and `erc20ApprovalGasSponsoring` extension support for gasless Permit2 approval flows, universal signature verification via `signer.verify_typed_data` (EOA + EIP-1271 + ERC-6492), and `settleWithPermit` settlement path. Added E2E `/protected-permit2`, `/protected-permit2-eip2612`, and `/protected-permit2-erc20` endpoints to Flask server, and updated httpx client for cross-language Permit2 testing. ([#689](https://github.com/coinbase/x402/pull/689)) + + +## [2.4.0] - 2026-03-16 + +### Fixed + +- Fixed paywall config injection targeting causing SVG parse errors in the browser ([#1550](https://github.com/coinbase/x402/pull/1550)) + +### Added + +- Simulate transaction in verify and (optional) settle; Added multicall utility for efficient rpc calls; Fixed undeployed smart wallet handling to prevent facilitator grieving and account for implementation dependent verifyTypedData; Enforce strict amount equality per spec in evm exact; Fix extra field passthrough in resource configs ([#1474](https://github.com/coinbase/x402/pull/1474)) + + +## [2.3.0] - 2026-03-06 + +### Fixed + +- Add in-memory SettlementCache to prevent duplicate SVM transaction settlement during on-chain confirmation window ([#svm-duplicate-settlement](https://github.com/coinbase/x402/pull/svm-duplicate-settlement)) +- Added serialize_by_alias=True to BaseX402Model so model_dump_json() produces spec-compliant camelCase by default ([#1120](https://github.com/coinbase/x402/pull/1120)) +- Auto-wrap eth_account LocalAccount in EthAccountSigner when passed to ExactEvmScheme or ExactEvmSchemeV1 ([#1121](https://github.com/coinbase/x402/pull/1121)) +- Added assetTransferMethod and supportsEip2612 flag to defaultAssets ([#1359](https://github.com/coinbase/x402/pull/1359)) +- Added dynamic function for servers to generate custom response for settlement failures defaulting to empty ([#1430](https://github.com/coinbase/x402/pull/1430)) + +### Added + +- Separated v1 legacy network name resolution from v2 CAIP-2 resolution; get_evm_chain_id now only accepts eip155:CHAIN_ID format, v1 code uses evm.v1.utils ([#split-v1-v2-networks](https://github.com/coinbase/x402/pull/split-v1-v2-networks)) + + ## [2.2.0] - 2026-02-20 ### Fixed diff --git a/python/x402/__init__.py b/python/x402/__init__.py index 7932c40bed..619b316efe 100644 --- a/python/x402/__init__.py +++ b/python/x402/__init__.py @@ -137,7 +137,7 @@ x402ResourceServerSync, ) -__version__ = "0.1.0" +__version__ = "2.5.0" __all__ = [ # Version diff --git a/python/x402/changelog.d/1120.bugfix.md b/python/x402/changelog.d/1120.bugfix.md deleted file mode 100644 index bbe9671188..0000000000 --- a/python/x402/changelog.d/1120.bugfix.md +++ /dev/null @@ -1 +0,0 @@ -Added serialize_by_alias=True to BaseX402Model so model_dump_json() produces spec-compliant camelCase by default diff --git a/python/x402/changelog.d/1121.bugfix.md b/python/x402/changelog.d/1121.bugfix.md deleted file mode 100644 index 9d7fe5daa6..0000000000 --- a/python/x402/changelog.d/1121.bugfix.md +++ /dev/null @@ -1 +0,0 @@ -Auto-wrap eth_account LocalAccount in EthAccountSigner when passed to ExactEvmScheme or ExactEvmSchemeV1 diff --git a/python/x402/changelog.d/1584.bugfix.md b/python/x402/changelog.d/1584.bugfix.md new file mode 100644 index 0000000000..15b102d338 --- /dev/null +++ b/python/x402/changelog.d/1584.bugfix.md @@ -0,0 +1 @@ +Fixed race condition in lazy facilitator initialization for FastAPI and Flask middleware under concurrent requests. diff --git a/python/x402/changelog.d/1762.bugfix.md b/python/x402/changelog.d/1762.bugfix.md new file mode 100644 index 0000000000..bb194ca8d5 --- /dev/null +++ b/python/x402/changelog.d/1762.bugfix.md @@ -0,0 +1 @@ +Fix extra: null incompatibility between python facilitator and TS zod schema diff --git a/python/x402/changelog.d/424.feature.md b/python/x402/changelog.d/424.feature.md new file mode 100644 index 0000000000..2fd38e1354 --- /dev/null +++ b/python/x402/changelog.d/424.feature.md @@ -0,0 +1 @@ +Added dynamic route support to the Bazaar discovery extension — servers can now declare ``[param]`` route segments that consolidate to a single catalog entry per route template, with automatic ``pathParams`` enrichment and ``:param``-style ``routeTemplate`` in discovery output. diff --git a/python/x402/changelog.d/add-arbitrum-one-and-arbitrum-sepolia-default-stablecoin.md b/python/x402/changelog.d/add-arbitrum-one-and-arbitrum-sepolia-default-stablecoin.md new file mode 100644 index 0000000000..9db05707e8 --- /dev/null +++ b/python/x402/changelog.d/add-arbitrum-one-and-arbitrum-sepolia-default-stablecoin.md @@ -0,0 +1 @@ +Add Arbitrum One (chain ID 42161) and Arbitrum Sepolid (chain ID 421614) support with USDC as the default stablecoin \ No newline at end of file diff --git a/python/x402/changelog.d/mezo-testnet-default-asset.feature.md b/python/x402/changelog.d/mezo-testnet-default-asset.feature.md new file mode 100644 index 0000000000..2afafe9f2d --- /dev/null +++ b/python/x402/changelog.d/mezo-testnet-default-asset.feature.md @@ -0,0 +1 @@ +Add Mezo Testnet (chain ID 31611) support with mUSD as the default stablecoin diff --git a/python/x402/changelog.d/polygon-support.feature.md b/python/x402/changelog.d/polygon-support.feature.md new file mode 100644 index 0000000000..dec21aa58e --- /dev/null +++ b/python/x402/changelog.d/polygon-support.feature.md @@ -0,0 +1 @@ +Add Polygon mainnet (chain ID 137) support with USDC as the default stablecoin diff --git a/python/x402/changelog.d/split-v1-v2-networks.feature.md b/python/x402/changelog.d/split-v1-v2-networks.feature.md deleted file mode 100644 index feaffda9d9..0000000000 --- a/python/x402/changelog.d/split-v1-v2-networks.feature.md +++ /dev/null @@ -1 +0,0 @@ -Separated v1 legacy network name resolution from v2 CAIP-2 resolution; get_evm_chain_id now only accepts eip155:CHAIN_ID format, v1 code uses evm.v1.utils diff --git a/python/x402/changelog.d/stable-support.feature.md b/python/x402/changelog.d/stable-support.feature.md new file mode 100644 index 0000000000..3af2817ed9 --- /dev/null +++ b/python/x402/changelog.d/stable-support.feature.md @@ -0,0 +1 @@ +Add Stable mainnet (chain ID 988) support with USDT0 as the default stablecoin diff --git a/python/x402/changelog.d/stable-testnet-support.feature.md b/python/x402/changelog.d/stable-testnet-support.feature.md new file mode 100644 index 0000000000..e9fb7c99c8 --- /dev/null +++ b/python/x402/changelog.d/stable-testnet-support.feature.md @@ -0,0 +1 @@ +Add Stable testnet (chain ID 2201) support with USDT0 as the default stablecoin diff --git a/python/x402/client_base.py b/python/x402/client_base.py index 4bc6ed7c8e..aa5dbbf7e0 100644 --- a/python/x402/client_base.py +++ b/python/x402/client_base.py @@ -5,6 +5,7 @@ from __future__ import annotations +import inspect from collections.abc import Awaitable, Callable, Generator from dataclasses import dataclass, field from typing import Any, Literal @@ -303,8 +304,21 @@ def _create_payment_payload_v2_core( client = schemes[selected.scheme] - # 5. Create inner payload - inner_payload = client.create_payment_payload(selected) + # 5. Create inner payload (pass extensions for enrichment if scheme supports it) + server_extensions = payment_required.extensions + sig = inspect.signature(client.create_payment_payload) + if "extensions" in sig.parameters: + inner_payload = client.create_payment_payload( + selected, extensions=server_extensions + ) + else: + inner_payload = client.create_payment_payload(selected) + + # 5b. Extract scheme-generated extensions (e.g. gas sponsoring) + scheme_extensions = inner_payload.pop("__extensions", None) + final_extensions = extensions or payment_required.extensions or {} + if scheme_extensions: + final_extensions = {**final_extensions, **scheme_extensions} # 6. Wrap into full PaymentPayload payload = PaymentPayload( @@ -312,7 +326,7 @@ def _create_payment_payload_v2_core( payload=inner_payload, accepted=selected, resource=resource or payment_required.resource, - extensions=extensions or payment_required.extensions, + extensions=final_extensions or None, ) # 7. Execute after hooks diff --git a/python/x402/extensions/bazaar/facilitator.py b/python/x402/extensions/bazaar/facilitator.py index a0b233c704..f03ba51f29 100644 --- a/python/x402/extensions/bazaar/facilitator.py +++ b/python/x402/extensions/bazaar/facilitator.py @@ -9,9 +9,10 @@ from __future__ import annotations import logging +import re from dataclasses import dataclass, field from typing import TYPE_CHECKING, Any -from urllib.parse import urlparse, urlunparse +from urllib.parse import unquote, urlparse, urlunparse from .types import ( BAZAAR, @@ -38,6 +39,46 @@ logger = logging.getLogger(__name__) +# Valid routeTemplate pattern: must start with "/", contain only safe URL path characters +# and :param identifiers. Expected format: "/users/:userId", "/weather/:country/:city". +_ROUTE_TEMPLATE_RE = re.compile(r"^/[a-zA-Z0-9_/:.\-~%]+$") + + +def _is_valid_route_template(value: str | None) -> bool: + """Check whether a routeTemplate value is structurally valid. + + Expected format: ":param" segments using colon-prefixed identifiers + (e.g. "/users/:userId", "/weather/:country/:city"). + + The facilitator is a trust boundary: clients control the payment payload and can + modify routeTemplate before submission. A malicious value could cause the facilitator + to catalog the payment under an arbitrary URL (catalog poisoning). + + Enforces: + - Must be a non-empty string starting with "/" + - Must match the safe URL path character set (alphanumeric, _, :, /, ., -, ~, %) + - Must not contain ".." (path traversal) + - Must not contain "://" (URL injection) + + Args: + value: The raw routeTemplate string from the client payload. + + Returns: + True if the value is a valid routeTemplate, False otherwise. + """ + if not value: + return False + if not _ROUTE_TEMPLATE_RE.match(value): + return False + # Decode percent-encoding before traversal checks so that %2e%2e is caught. + decoded = unquote(value) + if ".." in decoded: + return False + if "://" in decoded: + return False + return True + + @dataclass class ValidationResult: """Result of validating a discovery extension.""" @@ -56,6 +97,7 @@ class DiscoveredResource: discovery_info: DiscoveryInfo description: str | None = None mime_type: str | None = None + route_template: str | None = None @dataclass @@ -177,6 +219,7 @@ def extract_discovery_info( discovery_info: DiscoveryInfo | None = None resource_url: str = "" + route_template: str | None = None version = payload_dict.get("x402Version", 1) if version == 2: @@ -189,6 +232,11 @@ def extract_discovery_info( bazaar_ext = extensions[BAZAAR.key] if bazaar_ext and isinstance(bazaar_ext, dict): + # routeTemplate uses :param syntax (e.g. "/users/:userId", "/weather/:country/:city"). + # Must start with "/", must not contain ".." or "://". + raw_template = bazaar_ext.get("routeTemplate") + if _is_valid_route_template(raw_template): + route_template = raw_template try: extension = parse_discovery_extension(bazaar_ext) @@ -223,7 +271,11 @@ def extract_discovery_info( method = _get_method_from_info(discovery_info) # Strip query params (?) and hash sections (#) for discovery cataloging parsed = urlparse(resource_url) - normalized_url = urlunparse((parsed.scheme, parsed.netloc, parsed.path, "", "", "")) + # If a routeTemplate is present (dynamic route), use it as the canonical path + if route_template: + normalized_url = urlunparse((parsed.scheme, parsed.netloc, route_template, "", "", "")) + else: + normalized_url = urlunparse((parsed.scheme, parsed.netloc, parsed.path, "", "", "")) # Extract description and mime_type from resource info (V2) or requirements (V1) description: str | None = None @@ -247,6 +299,7 @@ def extract_discovery_info( discovery_info=discovery_info, description=description, mime_type=mime_type, + route_template=route_template, ) diff --git a/python/x402/extensions/bazaar/resource_service.py b/python/x402/extensions/bazaar/resource_service.py index ba6b8ce767..11ca3fcdbd 100644 --- a/python/x402/extensions/bazaar/resource_service.py +++ b/python/x402/extensions/bazaar/resource_service.py @@ -36,6 +36,7 @@ class DeclareQueryDiscoveryConfig: input: dict[str, Any] | None = None input_schema: dict[str, Any] | None = None + path_params_schema: dict[str, Any] | None = None output: OutputConfig | None = None @@ -45,6 +46,7 @@ class DeclareBodyDiscoveryConfig: input: dict[str, Any] | None = None input_schema: dict[str, Any] | None = None + path_params_schema: dict[str, Any] | None = None body_type: BodyType = "json" output: OutputConfig | None = None @@ -52,6 +54,7 @@ class DeclareBodyDiscoveryConfig: def _create_query_discovery_extension( input_data: dict[str, Any] | None = None, input_schema: dict[str, Any] | None = None, + path_params_schema: dict[str, Any] | None = None, output: OutputConfig | None = None, ) -> QueryDiscoveryExtension: """Create a query discovery extension. @@ -59,6 +62,7 @@ def _create_query_discovery_extension( Args: input_data: Example query parameters. input_schema: JSON schema for query parameters. + path_params_schema: JSON schema for URL path parameters. output: Output specification with example. Returns: @@ -87,7 +91,10 @@ def _create_query_discovery_extension( "type": {"type": "string", "const": "http"}, "method": {"type": "string", "enum": ["GET", "HEAD", "DELETE"]}, }, - "required": ["type"], + "required": ["type", "method"], + # pathParams are not declared here at schema build time — + # the server extension's enrich_declaration adds pathParams to both info and schema + # atomically at request time, keeping data and schema consistent. "additionalProperties": False, } } @@ -99,6 +106,12 @@ def _create_query_discovery_extension( **input_schema, } + if path_params_schema: + schema_properties["input"]["properties"]["pathParams"] = { + "type": "object", + **path_params_schema, + } + # Add output schema if provided if output and output.example is not None: output_schema: dict[str, Any] = { @@ -128,6 +141,7 @@ def _create_query_discovery_extension( def _create_body_discovery_extension( input_data: dict[str, Any] | None = None, input_schema: dict[str, Any] | None = None, + path_params_schema: dict[str, Any] | None = None, body_type: BodyType = "json", output: OutputConfig | None = None, ) -> BodyDiscoveryExtension: @@ -136,6 +150,7 @@ def _create_body_discovery_extension( Args: input_data: Example request body. input_schema: JSON schema for request body. + path_params_schema: JSON schema for URL path parameters. body_type: Content type of body (json, form-data, text). output: Output specification with example. @@ -168,11 +183,20 @@ def _create_body_discovery_extension( "bodyType": {"type": "string", "enum": ["json", "form-data", "text"]}, "body": input_schema, }, - "required": ["type", "bodyType", "body"], + "required": ["type", "method", "bodyType", "body"], + # pathParams are not declared here at schema build time — + # the server extension's enrich_declaration adds pathParams to both info and schema + # atomically at request time, keeping data and schema consistent. "additionalProperties": False, } } + if path_params_schema: + schema_properties["input"]["properties"]["pathParams"] = { + "type": "object", + **path_params_schema, + } + # Add output schema if provided if output and output.example is not None: output_schema: dict[str, Any] = { @@ -202,6 +226,7 @@ def _create_body_discovery_extension( def declare_discovery_extension( input: dict[str, Any] | None = None, # noqa: A002 input_schema: dict[str, Any] | None = None, + path_params_schema: dict[str, Any] | None = None, body_type: BodyType | None = None, output: OutputConfig | None = None, ) -> dict[str, Any]: @@ -218,6 +243,7 @@ def declare_discovery_extension( input: Example input data (query params for GET/HEAD/DELETE, body for POST/PUT/PATCH). input_schema: JSON Schema for the input. + path_params_schema: JSON Schema for URL path parameters (e.g. :city slugs). body_type: For POST/PUT/PATCH, specify "json", "form-data", or "text". When provided, creates a body extension. When None, creates a query extension. output: Output configuration with example and optional schema. @@ -236,6 +262,15 @@ def declare_discovery_extension( } ) + # For a GET endpoint with path params + extension = declare_discovery_extension( + path_params_schema={ + "properties": {"city": {"type": "string"}}, + "required": ["city"] + }, + output=OutputConfig(example={"city": "sf", "weather": "foggy"}) + ) + # For a POST endpoint with JSON body extension = declare_discovery_extension( input={"name": "John", "age": 30}, @@ -257,6 +292,7 @@ def declare_discovery_extension( extension = _create_body_discovery_extension( input_data=input, input_schema=input_schema, + path_params_schema=path_params_schema, body_type=body_type, # type: ignore[arg-type] output=output, ) @@ -264,6 +300,7 @@ def declare_discovery_extension( extension = _create_query_discovery_extension( input_data=input, input_schema=input_schema, + path_params_schema=path_params_schema, output=output, ) diff --git a/python/x402/extensions/bazaar/server.py b/python/x402/extensions/bazaar/server.py index 7cdedcc56d..b229c735e6 100644 --- a/python/x402/extensions/bazaar/server.py +++ b/python/x402/extensions/bazaar/server.py @@ -6,10 +6,31 @@ from __future__ import annotations +import re from typing import Any from .types import BAZAAR +# Compiled once at module level. +_BRACKET_PARAM_RE = re.compile(r"\[([^\]]+)\]") # [paramName] (Next.js style) +_COLON_PARAM_RE = re.compile(r":([a-zA-Z_]\w*)") # :paramName (Express style) + +# Cache compiled capture regexes per route pattern to avoid per-request recompilation. +_pattern_cache: dict[str, tuple[re.Pattern[str], list[str]]] = {} + + +def _normalize_wildcard_pattern(pattern: str) -> str: + """Convert wildcard segments to :var1, :var2, etc. for discovery normalization.""" + if "*" not in pattern: + return pattern + counter = 0 + segments = pattern.split("/") + for i, seg in enumerate(segments): + if seg == "*": + counter += 1 + segments[i] = f":var{counter}" + return "/".join(segments) + def _is_http_request_context(ctx: Any) -> bool: """Check if context is an HTTP request context. @@ -23,6 +44,74 @@ def _is_http_request_context(ctx: Any) -> bool: return hasattr(ctx, "method") and isinstance(getattr(ctx, "method", None), str) +def _extract_dynamic_route_info( + route_pattern: str, url_path: str +) -> tuple[str, dict[str, str]] | None: + """Convert a parameterized route pattern to a :param template and extract concrete values. + + Supports both [param] (Next.js) and :param (Express) syntax. The output routeTemplate + always uses :param syntax regardless of input format. + + Args: + route_pattern: Route pattern (e.g. "/users/[userId]" or "/users/:userId") + url_path: Concrete URL path (e.g. "/users/123") + + Returns: + (routeTemplate, pathParams) tuple, or None if route_pattern has no param segments. + """ + has_bracket = bool(_BRACKET_PARAM_RE.search(route_pattern)) + has_colon = bool(_COLON_PARAM_RE.search(route_pattern)) + if not has_bracket and not has_colon: + return None + # When both [param] and :param are present, normalize brackets to colons first + # so all params are extracted uniformly. + normalized = _BRACKET_PARAM_RE.sub(r":\1", route_pattern) if has_bracket else route_pattern + path_params = _extract_path_params(normalized, url_path, is_bracket=False) + return normalized, path_params + + +def _get_or_compile_pattern( + route_pattern: str, *, is_bracket: bool +) -> tuple[re.Pattern[str], list[str]]: + """Return a cached (regex, param_names) pair for the route pattern, compiling on first access.""" + if route_pattern in _pattern_cache: + return _pattern_cache[route_pattern] + + split_re = _BRACKET_PARAM_RE if is_bracket else _COLON_PARAM_RE + parts = split_re.split(route_pattern) + regex_parts: list[str] = [] + param_names: list[str] = [] + for i, part in enumerate(parts): + if i % 2 == 0: + regex_parts.append(re.escape(part)) + else: + param_names.append(part) + regex_parts.append("([^/]+)") + + compiled = re.compile("^" + "".join(regex_parts) + "$") + _pattern_cache[route_pattern] = (compiled, param_names) + return compiled, param_names + + +def _extract_path_params(route_pattern: str, url_path: str, *, is_bracket: bool) -> dict[str, str]: + """Extract concrete path parameter values by matching a URL path against a route pattern. + + Args: + route_pattern: Route pattern with [paramName] or :paramName segments + url_path: Concrete URL path (e.g. "/users/123") + is_bracket: True if pattern uses [param] syntax, False for :param + + Returns: + Dict mapping param names to their concrete values. + """ + compiled, param_names = _get_or_compile_pattern(route_pattern, is_bracket=is_bracket) + match = compiled.match(url_path) + if not match: + return {} + + return {name: match.group(i + 1) for i, name in enumerate(param_names)} + + class BazaarResourceServerExtension: """Resource server extension that enriches discovery extensions with HTTP method. @@ -107,6 +196,39 @@ def enrich_declaration( schema["properties"] = properties ext["schema"] = schema + # Check for dynamic route pattern. + # Wildcard * segments are auto-converted to :var1, :var2, etc. for catalog normalization. + raw_route_pattern = getattr(transport_context, "route_pattern", None) + route_pattern = ( + _normalize_wildcard_pattern(raw_route_pattern) if raw_route_pattern else None + ) + dynamic = ( + _extract_dynamic_route_info(route_pattern, transport_context.adapter.get_path()) + if route_pattern + else None + ) + if dynamic is not None: + route_template, path_params = dynamic + input_data = ext.get("info", {}).get("input", {}) + if isinstance(input_data, dict): + input_data["pathParams"] = path_params + info = ext.get("info", {}) + if isinstance(info, dict): + info["input"] = input_data + ext["info"] = info + ext["routeTemplate"] = route_template + + # Ensure pathParams is allowed in the schema (additionalProperties: false would reject it) + schema = ext.get("schema", {}) + if isinstance(schema, dict): + props = schema.get("properties", {}) + if isinstance(props, dict): + input_schema = props.get("input", {}) + if isinstance(input_schema, dict): + input_props = input_schema.get("properties", {}) + if isinstance(input_props, dict) and "pathParams" not in input_props: + input_props["pathParams"] = {"type": "object"} + return ext diff --git a/python/x402/extensions/bazaar/types.py b/python/x402/extensions/bazaar/types.py index a29618ec71..aa2e2e478d 100644 --- a/python/x402/extensions/bazaar/types.py +++ b/python/x402/extensions/bazaar/types.py @@ -57,6 +57,7 @@ class QueryInput(BaseModel): type: Literal["http"] = "http" method: QueryParamMethods | None = None query_params: dict[str, Any] | None = Field(default=None, alias="queryParams") + path_params: dict[str, Any] | None = Field(default=None, alias="pathParams") headers: dict[str, str] | None = None model_config = {"extra": "allow", "populate_by_name": True} @@ -70,6 +71,7 @@ class BodyInput(BaseModel): body_type: BodyType = Field(default="json", alias="bodyType") body: dict[str, Any] | Any = Field(default_factory=dict) query_params: dict[str, Any] | None = Field(default=None, alias="queryParams") + path_params: dict[str, Any] | None = Field(default=None, alias="pathParams") headers: dict[str, str] | None = None model_config = {"extra": "allow", "populate_by_name": True} diff --git a/python/x402/extensions/eip2612_gas_sponsoring/__init__.py b/python/x402/extensions/eip2612_gas_sponsoring/__init__.py new file mode 100644 index 0000000000..3d9ee1e2f7 --- /dev/null +++ b/python/x402/extensions/eip2612_gas_sponsoring/__init__.py @@ -0,0 +1,19 @@ +"""EIP-2612 Gas Sponsoring Extension for x402 Permit2 flows.""" + +from .facilitator import ( + extract_eip2612_gas_sponsoring_info, + validate_eip2612_gas_sponsoring_info, + validate_eip2612_permit_for_payment, +) +from .server import declare_eip2612_gas_sponsoring_extension +from .types import EIP2612_GAS_SPONSORING, EIP2612_GAS_SPONSORING_KEY, Eip2612GasSponsoringInfo + +__all__ = [ + "EIP2612_GAS_SPONSORING", + "EIP2612_GAS_SPONSORING_KEY", + "Eip2612GasSponsoringInfo", + "declare_eip2612_gas_sponsoring_extension", + "extract_eip2612_gas_sponsoring_info", + "validate_eip2612_gas_sponsoring_info", + "validate_eip2612_permit_for_payment", +] diff --git a/python/x402/extensions/eip2612_gas_sponsoring/client.py b/python/x402/extensions/eip2612_gas_sponsoring/client.py new file mode 100644 index 0000000000..bc68716dee --- /dev/null +++ b/python/x402/extensions/eip2612_gas_sponsoring/client.py @@ -0,0 +1,86 @@ +"""Client-side EIP-2612 permit signing for Permit2 approval sponsoring.""" + +from __future__ import annotations + +from typing import Any + +from ...mechanisms.evm.constants import ( + EIP2612_NONCES_ABI, + EIP2612_PERMIT_TYPES, + PERMIT2_ADDRESS, +) +from ...mechanisms.evm.types import TypedDataField +from .types import Eip2612GasSponsoringInfo + + +def sign_eip2612_permit( + signer: Any, + token_address: str, + token_name: str, + token_version: str, + chain_id: int, + deadline: str, + amount: str, +) -> Eip2612GasSponsoringInfo: + """Sign an EIP-2612 permit authorizing Permit2 to spend tokens. + + The signer must implement read_contract (to query nonces) and + sign_typed_data. + + Args: + signer: Client signer with read_contract and sign_typed_data. + token_address: ERC-20 token contract address. + token_name: Token name for EIP-712 domain. + token_version: Token version for EIP-712 domain. + chain_id: Chain ID. + deadline: Deadline timestamp as decimal string. + amount: Amount to approve as decimal string. + + Returns: + Eip2612GasSponsoringInfo ready to attach to payload extensions. + """ + nonce = signer.read_contract( + token_address, + EIP2612_NONCES_ABI, + "nonces", + signer.address, + ) + + domain_dict: dict[str, Any] = { + "name": token_name, + "version": token_version, + "chainId": chain_id, + "verifyingContract": token_address, + } + + message = { + "owner": signer.address, + "spender": PERMIT2_ADDRESS, + "value": int(amount), + "nonce": int(nonce), + "deadline": int(deadline), + } + + typed_fields: dict[str, list[TypedDataField]] = { + type_name: [TypedDataField(name=f["name"], type=f["type"]) for f in fields] + for type_name, fields in EIP2612_PERMIT_TYPES.items() + } + + sig_bytes = signer.sign_typed_data( + domain_dict, + typed_fields, + "Permit", + message, + ) + signature = "0x" + sig_bytes.hex() + + return Eip2612GasSponsoringInfo( + from_address=signer.address, + asset=token_address, + spender=PERMIT2_ADDRESS, + amount=amount, + nonce=str(int(nonce)), + deadline=deadline, + signature=signature, + version="1", + ) diff --git a/python/x402/extensions/eip2612_gas_sponsoring/facilitator.py b/python/x402/extensions/eip2612_gas_sponsoring/facilitator.py new file mode 100644 index 0000000000..e99356142d --- /dev/null +++ b/python/x402/extensions/eip2612_gas_sponsoring/facilitator.py @@ -0,0 +1,86 @@ +"""Facilitator-side extraction and validation for EIP-2612 Gas Sponsoring.""" + +from __future__ import annotations + +import re +import time + +from ...mechanisms.evm.constants import PERMIT2_ADDRESS, PERMIT2_DEADLINE_BUFFER +from ...schemas import PaymentPayload +from .types import EIP2612_GAS_SPONSORING_KEY, Eip2612GasSponsoringInfo + +_HEX_ADDRESS = re.compile(r"^0x[a-fA-F0-9]{40}$") +_DECIMAL_STRING = re.compile(r"^[0-9]+$") +_HEX_STRING = re.compile(r"^0x[a-fA-F0-9]+$") +_VERSION_STRING = re.compile(r"^[0-9]+(\.[0-9]+)*$") + + +def extract_eip2612_gas_sponsoring_info( + payload: PaymentPayload, +) -> Eip2612GasSponsoringInfo | None: + """Extract EIP-2612 gas sponsoring info from a payment payload. + + Returns None if the extension is not present or malformed. + """ + extensions = payload.extensions + if not extensions: + return None + + ext_data = extensions.get(EIP2612_GAS_SPONSORING_KEY) + if not isinstance(ext_data, dict): + return None + + info = ext_data.get("info") + if not isinstance(info, dict): + return None + + required = ["from", "asset", "spender", "amount", "nonce", "deadline", "signature"] + if not all(isinstance(info.get(k), str) for k in required): + return None + + return Eip2612GasSponsoringInfo.from_dict(info) + + +def validate_eip2612_gas_sponsoring_info(info: Eip2612GasSponsoringInfo) -> bool: + """Validate info fields against the JSON Schema patterns.""" + return ( + bool(_HEX_ADDRESS.match(info.from_address)) + and bool(_HEX_ADDRESS.match(info.asset)) + and bool(_HEX_ADDRESS.match(info.spender)) + and bool(_DECIMAL_STRING.match(info.amount)) + and bool(_DECIMAL_STRING.match(info.nonce)) + and bool(_DECIMAL_STRING.match(info.deadline)) + and bool(_HEX_STRING.match(info.signature)) + and bool(_VERSION_STRING.match(info.version)) + ) + + +def validate_eip2612_permit_for_payment( + info: Eip2612GasSponsoringInfo, + payer: str, + token_address: str, +) -> str: + """Validate EIP-2612 extension data for a specific payment. + + Returns empty string if valid, or an error reason string. + """ + if not validate_eip2612_gas_sponsoring_info(info): + return "invalid_eip2612_extension_format" + + if info.from_address.lower() != payer.lower(): + return "eip2612_from_mismatch" + + if info.asset.lower() != token_address.lower(): + return "eip2612_asset_mismatch" + + if info.spender.lower() != PERMIT2_ADDRESS.lower(): + return "eip2612_spender_not_permit2" + + now = int(time.time()) + try: + if int(info.deadline) < now + PERMIT2_DEADLINE_BUFFER: + return "eip2612_deadline_expired" + except (ValueError, TypeError): + return "eip2612_deadline_expired" + + return "" diff --git a/python/x402/extensions/eip2612_gas_sponsoring/schema.py b/python/x402/extensions/eip2612_gas_sponsoring/schema.py new file mode 100644 index 0000000000..541c0d49a3 --- /dev/null +++ b/python/x402/extensions/eip2612_gas_sponsoring/schema.py @@ -0,0 +1,58 @@ +"""JSON Schema for the EIP-2612 Gas Sponsoring extension info payload.""" + +eip2612_gas_sponsoring_schema: dict = { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "from": { + "type": "string", + "pattern": "^0x[a-fA-F0-9]{40}$", + "description": "The address of the sender.", + }, + "asset": { + "type": "string", + "pattern": "^0x[a-fA-F0-9]{40}$", + "description": "The address of the ERC-20 token contract.", + }, + "spender": { + "type": "string", + "pattern": "^0x[a-fA-F0-9]{40}$", + "description": "The address of the spender (Canonical Permit2).", + }, + "amount": { + "type": "string", + "pattern": "^[0-9]+$", + "description": "The amount to approve (uint256).", + }, + "nonce": { + "type": "string", + "pattern": "^[0-9]+$", + "description": "The current EIP-2612 nonce of the sender.", + }, + "deadline": { + "type": "string", + "pattern": "^[0-9]+$", + "description": "The timestamp at which the signature expires.", + }, + "signature": { + "type": "string", + "pattern": "^0x[a-fA-F0-9]+$", + "description": "The 65-byte concatenated signature (r, s, v) as a hex string.", + }, + "version": { + "type": "string", + "pattern": r"^[0-9]+(\.[0-9]+)*$", + "description": "Schema version identifier.", + }, + }, + "required": [ + "from", + "asset", + "spender", + "amount", + "nonce", + "deadline", + "signature", + "version", + ], +} diff --git a/python/x402/extensions/eip2612_gas_sponsoring/server.py b/python/x402/extensions/eip2612_gas_sponsoring/server.py new file mode 100644 index 0000000000..3bf3e70736 --- /dev/null +++ b/python/x402/extensions/eip2612_gas_sponsoring/server.py @@ -0,0 +1,27 @@ +"""Resource server declaration for the EIP-2612 Gas Sponsoring extension.""" + +from __future__ import annotations + +from typing import Any + +from .schema import eip2612_gas_sponsoring_schema +from .types import EIP2612_GAS_SPONSORING_KEY + + +def declare_eip2612_gas_sponsoring_extension() -> dict[str, Any]: + """Declare the eip2612GasSponsoring extension for inclusion in PaymentRequired. + + Returns a dict keyed by the extension key, ready to merge into + PaymentRequired.extensions. + """ + return { + EIP2612_GAS_SPONSORING_KEY: { + "info": { + "description": ( + "The facilitator accepts EIP-2612 gasless Permit to Permit2 canonical contract." + ), + "version": "1", + }, + "schema": eip2612_gas_sponsoring_schema, + } + } diff --git a/python/x402/extensions/eip2612_gas_sponsoring/types.py b/python/x402/extensions/eip2612_gas_sponsoring/types.py new file mode 100644 index 0000000000..eeebc54321 --- /dev/null +++ b/python/x402/extensions/eip2612_gas_sponsoring/types.py @@ -0,0 +1,56 @@ +"""Types for the EIP-2612 Gas Sponsoring extension.""" + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any + +from ...interfaces import FacilitatorExtension + +EIP2612_GAS_SPONSORING_KEY = "eip2612GasSponsoring" + +EIP2612_GAS_SPONSORING = FacilitatorExtension(key=EIP2612_GAS_SPONSORING_KEY) +"""Singleton extension instance for registering with x402Facilitator. + +Unlike erc20ApprovalGasSponsoring, this extension needs no special signer — +the facilitator's main EVM signer handles settleWithPermit directly. +""" + + +@dataclass +class Eip2612GasSponsoringInfo: + """EIP-2612 permit data sent by the client for gasless Permit2 approval.""" + + from_address: str + asset: str + spender: str + amount: str + nonce: str + deadline: str + signature: str + version: str = "1" + + def to_dict(self) -> dict[str, Any]: + return { + "from": self.from_address, + "asset": self.asset, + "spender": self.spender, + "amount": self.amount, + "nonce": self.nonce, + "deadline": self.deadline, + "signature": self.signature, + "version": self.version, + } + + @classmethod + def from_dict(cls, data: dict[str, Any]) -> Eip2612GasSponsoringInfo: + return cls( + from_address=data.get("from", ""), + asset=data.get("asset", ""), + spender=data.get("spender", ""), + amount=data.get("amount", ""), + nonce=data.get("nonce", ""), + deadline=data.get("deadline", ""), + signature=data.get("signature", ""), + version=data.get("version", "1"), + ) diff --git a/python/x402/extensions/erc20_approval_gas_sponsoring/__init__.py b/python/x402/extensions/erc20_approval_gas_sponsoring/__init__.py new file mode 100644 index 0000000000..8bdbc86b90 --- /dev/null +++ b/python/x402/extensions/erc20_approval_gas_sponsoring/__init__.py @@ -0,0 +1,29 @@ +"""ERC-20 Approval Gas Sponsoring Extension for x402 Permit2 flows.""" + +from .facilitator import ( + extract_erc20_approval_gas_sponsoring_info, + validate_erc20_approval_for_payment, + validate_erc20_approval_gas_sponsoring_info, +) +from .server import declare_erc20_approval_gas_sponsoring_extension +from .types import ( + ERC20_APPROVAL_GAS_SPONSORING_KEY, + Erc20ApprovalFacilitatorExtension, + Erc20ApprovalGasSponsoringInfo, + Erc20ApprovalGasSponsoringSigner, + TransactionRequest, + WriteContractCall, +) + +__all__ = [ + "ERC20_APPROVAL_GAS_SPONSORING_KEY", + "Erc20ApprovalFacilitatorExtension", + "Erc20ApprovalGasSponsoringInfo", + "Erc20ApprovalGasSponsoringSigner", + "TransactionRequest", + "WriteContractCall", + "declare_erc20_approval_gas_sponsoring_extension", + "extract_erc20_approval_gas_sponsoring_info", + "validate_erc20_approval_for_payment", + "validate_erc20_approval_gas_sponsoring_info", +] diff --git a/python/x402/extensions/erc20_approval_gas_sponsoring/client.py b/python/x402/extensions/erc20_approval_gas_sponsoring/client.py new file mode 100644 index 0000000000..9f66e16e6e --- /dev/null +++ b/python/x402/extensions/erc20_approval_gas_sponsoring/client.py @@ -0,0 +1,87 @@ +"""Client-side ERC-20 approval transaction signing for Permit2 approval sponsoring.""" + +from __future__ import annotations + +from typing import Any + +from ...mechanisms.evm.constants import ( + ERC20_APPROVE_ABI, + ERC20_APPROVE_GAS_LIMIT, + PERMIT2_ADDRESS, +) +from .types import Erc20ApprovalGasSponsoringInfo + +MAX_UINT256 = 2**256 - 1 + + +def sign_erc20_approval_transaction( + signer: Any, + token_address: str, + chain_id: int, +) -> Erc20ApprovalGasSponsoringInfo: + """Sign an ERC-20 approve(Permit2, MaxUint256) transaction. + + The signer must implement: + - address: str property + - sign_transaction(tx_dict) -> hex string + - get_transaction_count(address) -> int + - estimate_fees_per_gas() -> (max_fee, max_priority_fee) (optional) + + Args: + signer: Client signer with transaction signing capabilities. + token_address: ERC-20 token contract address. + chain_id: Chain ID. + + Returns: + Erc20ApprovalGasSponsoringInfo ready to attach to payload extensions. + """ + try: + from web3 import Web3 + + w3 = Web3() + contract = w3.eth.contract( + address=Web3.to_checksum_address(token_address), + abi=ERC20_APPROVE_ABI, + ) + calldata = contract.encode_abi( + abi_element_identifier="approve", + args=[Web3.to_checksum_address(PERMIT2_ADDRESS), MAX_UINT256], + ) + except ImportError: + calldata = ( + "0x095ea7b3" + PERMIT2_ADDRESS[2:].lower().zfill(64) + hex(MAX_UINT256)[2:].zfill(64) + ) + + nonce = signer.get_transaction_count(signer.address) + + max_fee = 1_000_000_000 + max_priority_fee = 100_000_000 + if hasattr(signer, "estimate_fees_per_gas"): + try: + fees = signer.estimate_fees_per_gas() + max_fee = fees[0] if isinstance(fees, tuple) else fees["maxFeePerGas"] + max_priority_fee = fees[1] if isinstance(fees, tuple) else fees["maxPriorityFeePerGas"] + except Exception: + pass + + tx = { + "to": token_address, + "data": calldata, + "nonce": nonce, + "gas": ERC20_APPROVE_GAS_LIMIT, + "maxFeePerGas": max_fee, + "maxPriorityFeePerGas": max_priority_fee, + "chainId": chain_id, + "type": 2, + } + + signed_tx_hex = signer.sign_transaction(tx) + + return Erc20ApprovalGasSponsoringInfo( + from_address=signer.address, + asset=token_address, + spender=PERMIT2_ADDRESS, + amount=str(MAX_UINT256), + signed_transaction=signed_tx_hex, + version="1", + ) diff --git a/python/x402/extensions/erc20_approval_gas_sponsoring/facilitator.py b/python/x402/extensions/erc20_approval_gas_sponsoring/facilitator.py new file mode 100644 index 0000000000..0f3c76b530 --- /dev/null +++ b/python/x402/extensions/erc20_approval_gas_sponsoring/facilitator.py @@ -0,0 +1,155 @@ +"""Facilitator-side extraction and validation for ERC-20 Approval Gas Sponsoring.""" + +from __future__ import annotations + +import re + +from ...mechanisms.evm.constants import PERMIT2_ADDRESS +from ...schemas import PaymentPayload +from .types import ERC20_APPROVAL_GAS_SPONSORING_KEY, Erc20ApprovalGasSponsoringInfo + +_HEX_ADDRESS = re.compile(r"^0x[a-fA-F0-9]{40}$") +_DECIMAL_STRING = re.compile(r"^[0-9]+$") +_HEX_STRING = re.compile(r"^0x[a-fA-F0-9]+$") +_VERSION_STRING = re.compile(r"^[0-9]+(\.[0-9]+)*$") + +# ERC-20 approve(address,uint256) selector +_APPROVE_SELECTOR = "095ea7b3" + + +def extract_erc20_approval_gas_sponsoring_info( + payload: PaymentPayload, +) -> Erc20ApprovalGasSponsoringInfo | None: + """Extract ERC-20 approval gas sponsoring info from a payment payload. + + Returns None if the extension is not present or malformed. + """ + extensions = payload.extensions + if not extensions: + return None + + ext_data = extensions.get(ERC20_APPROVAL_GAS_SPONSORING_KEY) + if not isinstance(ext_data, dict): + return None + + info = ext_data.get("info") + if not isinstance(info, dict): + return None + + required = ["from", "asset", "spender", "amount", "signedTransaction"] + if not all(isinstance(info.get(k), str) for k in required): + return None + + return Erc20ApprovalGasSponsoringInfo.from_dict(info) + + +def validate_erc20_approval_gas_sponsoring_info( + info: Erc20ApprovalGasSponsoringInfo, +) -> bool: + """Validate info fields against the JSON Schema patterns.""" + return ( + bool(_HEX_ADDRESS.match(info.from_address)) + and bool(_HEX_ADDRESS.match(info.asset)) + and bool(_HEX_ADDRESS.match(info.spender)) + and bool(_DECIMAL_STRING.match(info.amount)) + and bool(_HEX_STRING.match(info.signed_transaction)) + and bool(_VERSION_STRING.match(info.version)) + ) + + +def validate_erc20_approval_for_payment( + info: Erc20ApprovalGasSponsoringInfo, + payer: str, + token_address: str, +) -> tuple[str, str]: + """Validate ERC-20 approval extension data for a specific payment. + + Returns ("", "") if valid, or (reason, message) on failure. + Performs schema validation, address matching, and signed tx decoding. + """ + if not validate_erc20_approval_gas_sponsoring_info(info): + return "invalid_erc20_approval_extension_format", "format validation failed" + + if info.from_address.lower() != payer.lower(): + return "erc20_approval_from_mismatch", "from does not match payer" + + if info.asset.lower() != token_address.lower(): + return "erc20_approval_asset_mismatch", "asset does not match token" + + if info.spender.lower() != PERMIT2_ADDRESS.lower(): + return "erc20_approval_spender_not_permit2", "spender is not Permit2" + + # Decode and validate the signed transaction + try: + reason, msg = _validate_signed_approval_tx(info.signed_transaction, payer, token_address) + if reason: + return reason, msg + except Exception as e: + return "erc20_approval_tx_parse_failed", str(e)[:200] + + return "", "" + + +def _validate_signed_approval_tx( + signed_tx_hex: str, + payer: str, + token_address: str, +) -> tuple[str, str]: + """Decode and validate a signed ERC-20 approve transaction. + + Checks: target address, function selector, spender in calldata, + and recovered signer. + """ + try: + from eth_account import Account + except ImportError: + return "erc20_approval_tx_validation_unavailable", "eth_account not installed" + + tx_bytes = bytes.fromhex(signed_tx_hex[2:] if signed_tx_hex.startswith("0x") else signed_tx_hex) + + try: + recovered = Account.recover_transaction(signed_tx_hex) + except Exception: + return "erc20_approval_tx_invalid_signature", "failed to recover signer" + + if recovered.lower() != payer.lower(): + return "erc20_approval_tx_signer_mismatch", "recovered signer does not match payer" + + try: + from eth_account.typed_transactions import TypedTransaction + from hexbytes import HexBytes + + tx_obj = TypedTransaction.from_bytes(HexBytes(tx_bytes)) + tx_dict = tx_obj.transaction.dictionary + + to_addr = tx_dict.get("to", b"") + if isinstance(to_addr, bytes): + to_addr = "0x" + to_addr.hex() + to_addr = str(to_addr) + + if to_addr.lower() != token_address.lower(): + return "erc20_approval_tx_wrong_target", "tx target is not the token" + + data = tx_dict.get("data", b"") + if isinstance(data, bytes): + data_hex = data.hex() + else: + data_hex = str(data) + + if not data_hex.startswith(_APPROVE_SELECTOR): + return "erc20_approval_tx_wrong_selector", "not an approve() call" + + # Decode spender from calldata (bytes 4..36 = 32-byte padded address) + if len(data_hex) < 72: + return "erc20_approval_tx_invalid_calldata", "calldata too short" + + spender_hex = "0x" + data_hex[32:72] + if spender_hex.lower() != PERMIT2_ADDRESS.lower(): + return "erc20_approval_tx_wrong_spender", "approve spender is not Permit2" + + except ImportError: + return "erc20_approval_tx_validation_unavailable", "typed transaction parsing not available" + except Exception as e: + return "erc20_approval_tx_parse_failed", str(e)[:200] + + return "", "" diff --git a/python/x402/extensions/erc20_approval_gas_sponsoring/schema.py b/python/x402/extensions/erc20_approval_gas_sponsoring/schema.py new file mode 100644 index 0000000000..15a7771d88 --- /dev/null +++ b/python/x402/extensions/erc20_approval_gas_sponsoring/schema.py @@ -0,0 +1,46 @@ +"""JSON Schema for the ERC-20 Approval Gas Sponsoring extension info payload.""" + +erc20_approval_gas_sponsoring_schema: dict = { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "from": { + "type": "string", + "pattern": "^0x[a-fA-F0-9]{40}$", + "description": "The address of the sender.", + }, + "asset": { + "type": "string", + "pattern": "^0x[a-fA-F0-9]{40}$", + "description": "The ERC-20 token contract address to approve.", + }, + "spender": { + "type": "string", + "pattern": "^0x[a-fA-F0-9]{40}$", + "description": "The address of the spender (Canonical Permit2).", + }, + "amount": { + "type": "string", + "pattern": "^[0-9]+$", + "description": "Approval amount (uint256).", + }, + "signedTransaction": { + "type": "string", + "pattern": "^0x[a-fA-F0-9]+$", + "description": "RLP-encoded signed transaction calling ERC20.approve().", + }, + "version": { + "type": "string", + "pattern": r"^[0-9]+(\.[0-9]+)*$", + "description": "Schema version identifier.", + }, + }, + "required": [ + "from", + "asset", + "spender", + "amount", + "signedTransaction", + "version", + ], +} diff --git a/python/x402/extensions/erc20_approval_gas_sponsoring/server.py b/python/x402/extensions/erc20_approval_gas_sponsoring/server.py new file mode 100644 index 0000000000..fbac2f71cb --- /dev/null +++ b/python/x402/extensions/erc20_approval_gas_sponsoring/server.py @@ -0,0 +1,28 @@ +"""Resource server declaration for the ERC-20 Approval Gas Sponsoring extension.""" + +from __future__ import annotations + +from typing import Any + +from .schema import erc20_approval_gas_sponsoring_schema +from .types import ERC20_APPROVAL_GAS_SPONSORING_KEY + + +def declare_erc20_approval_gas_sponsoring_extension() -> dict[str, Any]: + """Declare the erc20ApprovalGasSponsoring extension for PaymentRequired. + + Returns a dict keyed by the extension key, ready to merge into + PaymentRequired.extensions. + """ + return { + ERC20_APPROVAL_GAS_SPONSORING_KEY: { + "info": { + "description": ( + "The facilitator accepts a raw signed approval transaction " + "and will sponsor the gas fees." + ), + "version": "1", + }, + "schema": erc20_approval_gas_sponsoring_schema, + } + } diff --git a/python/x402/extensions/erc20_approval_gas_sponsoring/types.py b/python/x402/extensions/erc20_approval_gas_sponsoring/types.py new file mode 100644 index 0000000000..3d6ab85f4f --- /dev/null +++ b/python/x402/extensions/erc20_approval_gas_sponsoring/types.py @@ -0,0 +1,105 @@ +"""Types for the ERC-20 Approval Gas Sponsoring extension.""" + +from __future__ import annotations + +from dataclasses import dataclass, field +from typing import Any, Protocol + +ERC20_APPROVAL_GAS_SPONSORING_KEY = "erc20ApprovalGasSponsoring" + + +@dataclass +class Erc20ApprovalGasSponsoringInfo: + """ERC-20 approval data sent by the client for gasless Permit2 approval.""" + + from_address: str + asset: str + spender: str + amount: str + signed_transaction: str + version: str = "1" + + def to_dict(self) -> dict[str, Any]: + return { + "from": self.from_address, + "asset": self.asset, + "spender": self.spender, + "amount": self.amount, + "signedTransaction": self.signed_transaction, + "version": self.version, + } + + @classmethod + def from_dict(cls, data: dict[str, Any]) -> Erc20ApprovalGasSponsoringInfo: + return cls( + from_address=data.get("from", ""), + asset=data.get("asset", ""), + spender=data.get("spender", ""), + amount=data.get("amount", ""), + signed_transaction=data.get("signedTransaction", ""), + version=data.get("version", "1"), + ) + + +@dataclass +class WriteContractCall: + """An unsigned contract call for the extension signer to execute.""" + + address: str + abi: list[dict[str, Any]] + function: str + args: list[Any] = field(default_factory=list) + + +TransactionRequest = str | WriteContractCall + + +class Erc20ApprovalGasSponsoringSigner(Protocol): + """Extension signer capable of broadcasting approval + settle atomically. + + Extends FacilitatorEvmSigner with send_transactions for batched execution. + """ + + def send_transactions(self, transactions: list[TransactionRequest]) -> list[str]: + """Send a batch of transactions (pre-signed raw hex or unsigned calls). + + Args: + transactions: List of either raw hex tx strings or WriteContractCall. + + Returns: + List of transaction hashes, one per input. + """ + ... + + def wait_for_transaction_receipt(self, tx_hash: str) -> Any: + """Wait for a transaction to be mined.""" + ... + + +class Erc20ApprovalFacilitatorExtension: + """Facilitator extension for ERC-20 approval gas sponsoring. + + Wraps a signer (or per-network signer resolver) that can broadcast + the approval + settle bundle. + + Implements the FacilitatorExtension interface (key attribute) without + inheriting from the frozen dataclass. + """ + + key: str = ERC20_APPROVAL_GAS_SPONSORING_KEY + + def __init__( + self, + signer: Erc20ApprovalGasSponsoringSigner | None = None, + signer_for_network: Any = None, + ): + self._signer = signer + self._signer_for_network = signer_for_network + + def resolve_signer(self, network: str) -> Erc20ApprovalGasSponsoringSigner | None: + """Resolve the signer for a given network.""" + if self._signer_for_network is not None: + result = self._signer_for_network(network) + if result is not None: + return result + return self._signer diff --git a/python/x402/http/__init__.py b/python/x402/http/__init__.py index bbb2c126d1..ba34300a4d 100644 --- a/python/x402/http/__init__.py +++ b/python/x402/http/__init__.py @@ -30,6 +30,7 @@ HTTPAdapter, HTTPProcessResult, HTTPRequestContext, + HTTPResponseBody, HTTPResponseInstructions, PaymentOption, PaywallConfig, @@ -38,8 +39,8 @@ RouteConfigurationError, RoutesConfig, RouteValidationError, + SettlementFailedResponseBody, UnpaidResponseBody, - UnpaidResponseResult, ) from .utils import ( decode_payment_required_header, @@ -115,8 +116,9 @@ "CompiledRoute", "DynamicPayTo", "DynamicPrice", + "HTTPResponseBody", + "SettlementFailedResponseBody", "UnpaidResponseBody", - "UnpaidResponseResult", "RouteValidationError", "RouteConfigurationError", # Utils diff --git a/python/x402/http/facilitator_client.py b/python/x402/http/facilitator_client.py index 0f0440fc87..7d762dfec2 100644 --- a/python/x402/http/facilitator_client.py +++ b/python/x402/http/facilitator_client.py @@ -7,7 +7,9 @@ from __future__ import annotations import json -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING, Any, TypeVar + +from pydantic import ValidationError from ..schemas import ( PaymentPayload, @@ -24,6 +26,7 @@ FacilitatorClient, FacilitatorClientSync, FacilitatorConfig, + FacilitatorResponseError, HTTPFacilitatorClientBase, ) @@ -35,6 +38,7 @@ "HTTPFacilitatorClient", "HTTPFacilitatorClientSync", "FacilitatorConfig", + "FacilitatorResponseError", "FacilitatorClient", "FacilitatorClientSync", "AuthProvider", @@ -42,6 +46,46 @@ "CreateHeadersAuthProvider", ] +_ResponseModelT = TypeVar( + "_ResponseModelT", + VerifyResponse, + SettleResponse, + SupportedResponse, +) + + +def _response_excerpt(response: Any, limit: int = 200) -> str: + """Build a compact response preview for parse errors.""" + text = str(getattr(response, "text", "") or "").strip() + if not text: + return "" + + compact = " ".join(text.split()) + if len(compact) <= limit: + return compact + return f"{compact[: limit - 3]}..." + + +def _parse_facilitator_response( + response: Any, + model_cls: type[_ResponseModelT], + operation: str, +) -> _ResponseModelT: + """Parse facilitator JSON into a validated response model.""" + try: + response_data = response.json() + except (json.JSONDecodeError, ValueError, TypeError) as exc: + raise FacilitatorResponseError( + f"Facilitator {operation} returned invalid JSON: {_response_excerpt(response)}" + ) from exc + + try: + return model_cls.model_validate(response_data) + except (ValidationError, ValueError, TypeError) as exc: + raise FacilitatorResponseError( + f"Facilitator {operation} returned invalid data: {_response_excerpt(response)}" + ) from exc + # ============================================================================ # Async HTTP Facilitator Client (Default) @@ -167,7 +211,7 @@ def get_supported(self) -> SupportedResponse: f"Facilitator get_supported failed ({response.status_code}): {response.text}" ) - return SupportedResponse.model_validate(response.json()) + return _parse_facilitator_response(response, SupportedResponse, "supported") # ========================================================================= # Bytes-Based Methods (Network Boundary) @@ -244,7 +288,7 @@ async def _verify_http( if response.status_code != 200: raise ValueError(f"Facilitator verify failed ({response.status_code}): {response.text}") - return VerifyResponse.model_validate(response.json()) + return _parse_facilitator_response(response, VerifyResponse, "verify") async def _settle_http( self, @@ -265,7 +309,7 @@ async def _settle_http( if response.status_code != 200: raise ValueError(f"Facilitator settle failed ({response.status_code}): {response.text}") - return SettleResponse.model_validate(response.json()) + return _parse_facilitator_response(response, SettleResponse, "settle") # ============================================================================ @@ -383,7 +427,7 @@ def get_supported(self) -> SupportedResponse: f"Facilitator get_supported failed ({response.status_code}): {response.text}" ) - return SupportedResponse.model_validate(response.json()) + return _parse_facilitator_response(response, SupportedResponse, "supported") # ========================================================================= # Bytes-Based Methods (Network Boundary) @@ -460,7 +504,7 @@ def _verify_http( if response.status_code != 200: raise ValueError(f"Facilitator verify failed ({response.status_code}): {response.text}") - return VerifyResponse.model_validate(response.json()) + return _parse_facilitator_response(response, VerifyResponse, "verify") def _settle_http( self, @@ -481,4 +525,4 @@ def _settle_http( if response.status_code != 200: raise ValueError(f"Facilitator settle failed ({response.status_code}): {response.text}") - return SettleResponse.model_validate(response.json()) + return _parse_facilitator_response(response, SettleResponse, "settle") diff --git a/python/x402/http/facilitator_client_base.py b/python/x402/http/facilitator_client_base.py index aaab9ab797..9926f8b12c 100644 --- a/python/x402/http/facilitator_client_base.py +++ b/python/x402/http/facilitator_client_base.py @@ -66,6 +66,10 @@ def get_auth_headers(self) -> AuthHeaders: ) +class FacilitatorResponseError(ValueError): + """Facilitator returned malformed or schema-invalid response data.""" + + # ============================================================================ # FacilitatorClient Protocols # ============================================================================ diff --git a/python/x402/http/middleware/fastapi.py b/python/x402/http/middleware/fastapi.py index 9911f06493..84d2a8dff9 100644 --- a/python/x402/http/middleware/fastapi.py +++ b/python/x402/http/middleware/fastapi.py @@ -5,6 +5,7 @@ from __future__ import annotations +import asyncio from collections.abc import Awaitable, Callable from typing import TYPE_CHECKING, Any @@ -18,6 +19,7 @@ "FastAPI middleware requires fastapi and starlette. Install with: uv add x402[fastapi]" ) from e +from ..facilitator_client_base import FacilitatorResponseError from ..types import ( HTTPAdapter, HTTPRequestContext, @@ -187,6 +189,14 @@ def get_body(self) -> Any: # ============================================================================ +def _facilitator_error_response(error: FacilitatorResponseError) -> JSONResponse: + """Map invalid facilitator responses to a stable HTTP error.""" + return JSONResponse( + content={"error": str(error)}, + status_code=502, + ) + + def payment_middleware( routes: RoutesConfig, server: x402ResourceServer, @@ -248,8 +258,9 @@ async def x402_middleware(request, call_next): if paywall_provider: http_server.register_paywall_provider(paywall_provider) - # Lazy initialization state + # Lazy initialization state with async lock for concurrency safety init_done = False + init_lock = asyncio.Lock() async def middleware( request: Request, @@ -272,13 +283,21 @@ async def middleware( if not http_server.requires_payment(context): return await call_next(request) - # Initialize on first protected request + # Initialize on first protected request (double-checked locking) if sync_facilitator_on_start and not init_done: - http_server.initialize() - init_done = True + async with init_lock: + if not init_done: + try: + http_server.initialize() + except FacilitatorResponseError as error: + return _facilitator_error_response(error) + init_done = True # Process payment request - result = await http_server.process_http_request(context, paywall_config) + try: + result = await http_server.process_http_request(context, paywall_config) + except FacilitatorResponseError as error: + return _facilitator_error_response(error) if result.type == "no-payment-required": return await call_next(request) @@ -327,15 +346,26 @@ async def middleware( settle_result = await http_server.process_settlement( result.payment_payload, result.payment_requirements, + context=context, ) if not settle_result.success: + # Use response from process_settlement (includes PAYMENT-RESPONSE + # header and empty body by default) + resp = settle_result.response + if resp is None: + return JSONResponse(content={}, status_code=402) + if resp.is_html: + return Response( + content=resp.body, + status_code=resp.status, + headers=resp.headers, + media_type="text/html", + ) return JSONResponse( - content={ - "error": "Settlement failed", - "details": settle_result.error_reason, - }, - status_code=402, + content=resp.body or {}, + status_code=resp.status, + headers=resp.headers, ) # Add settlement headers @@ -349,14 +379,10 @@ async def middleware( media_type=response.media_type, ) - except Exception as e: - return JSONResponse( - content={ - "error": "Settlement failed", - "details": str(e), - }, - status_code=402, - ) + except FacilitatorResponseError as error: + return _facilitator_error_response(error) + except Exception: + return JSONResponse(content={}, status_code=402) # Fallthrough - should not happen return await call_next(request) diff --git a/python/x402/http/middleware/flask.py b/python/x402/http/middleware/flask.py index 7e0c7d1cd5..9987fcb8e0 100644 --- a/python/x402/http/middleware/flask.py +++ b/python/x402/http/middleware/flask.py @@ -7,6 +7,7 @@ from __future__ import annotations import json +import threading from collections.abc import Callable, Iterator from typing import TYPE_CHECKING, Any @@ -17,6 +18,7 @@ "Flask middleware requires the flask package. Install with: uv add x402[flask]" ) from e +from ..facilitator_client_base import FacilitatorResponseError from ..types import ( HTTPAdapter, HTTPRequestContext, @@ -186,6 +188,19 @@ def get_body(self) -> Any: # ============================================================================ +def _facilitator_error_wsgi_response( + start_response: Callable[..., Any], + error: FacilitatorResponseError, +) -> list[bytes]: + """Map invalid facilitator responses to a stable HTTP error.""" + body = json.dumps({"error": str(error)}).encode("utf-8") + start_response( + "502 Bad Gateway", + [("Content-Type", "application/json")], + ) + return [body] + + class ResponseWrapper: """Wrapper to capture and buffer WSGI response for settlement. @@ -320,6 +335,7 @@ def __init__( self._paywall_config = paywall_config self._sync_on_start = sync_facilitator_on_start self._init_done = False + self._init_lock = threading.Lock() self._original_wsgi = app.wsgi_app if paywall_provider: @@ -358,13 +374,21 @@ def _wsgi_middleware( if not self._http_server.requires_payment(context): return self._original_wsgi(environ, start_response) - # Initialize on first protected request + # Initialize on first protected request (double-checked locking) if self._sync_on_start and not self._init_done: - self._http_server.initialize() - self._init_done = True + with self._init_lock: + if not self._init_done: + try: + self._http_server.initialize() + except FacilitatorResponseError as error: + return _facilitator_error_wsgi_response(start_response, error) + self._init_done = True # Process payment request synchronously (no asyncio overhead) - result = self._http_server.process_http_request(context, self._paywall_config) + try: + result = self._http_server.process_http_request(context, self._paywall_config) + except FacilitatorResponseError as error: + return _facilitator_error_wsgi_response(start_response, error) if result.type == "no-payment-required": return self._original_wsgi(environ, start_response) @@ -418,6 +442,7 @@ def _wsgi_middleware( settle_result = self._http_server.process_settlement( result.payment_payload, result.payment_requirements, + context=context, ) if settle_result.success: @@ -425,32 +450,37 @@ def _wsgi_middleware( for key, value in settle_result.headers.items(): response_wrapper.add_header(key, value) else: - # Settlement failed - error_body = json.dumps( - { - "error": "Settlement failed", - "details": settle_result.error_reason, - } - ).encode("utf-8") - start_response( - "402 Payment Required", - [("Content-Type", "application/json")], - ) - return [error_body] - - except Exception as e: - # Settlement error - error_body = json.dumps( - { - "error": "Settlement failed", - "details": str(e), - } - ).encode("utf-8") + # Settlement failed - use response from process_settlement + # (includes PAYMENT-RESPONSE header and empty body by default) + response = settle_result.response + if response is None: + status = "402 Payment Required" + headers = [("Content-Type", "application/json")] + body = json.dumps({}).encode("utf-8") + else: + status = f"{response.status} Payment Required" + headers = list(response.headers.items()) + if response.is_html: + body = ( + response.body.encode("utf-8") + if isinstance(response.body, str) + else response.body + ) + else: + body = json.dumps(response.body or {}).encode("utf-8") + start_response(status, headers) + return [body] + + except FacilitatorResponseError as error: + return _facilitator_error_wsgi_response(start_response, error) + + except Exception: + # Settlement error - return empty body with 402 start_response( "402 Payment Required", [("Content-Type", "application/json")], ) - return [error_body] + return [json.dumps({}).encode("utf-8")] # Send buffered response response_wrapper.send_response(body_chunks) diff --git a/python/x402/http/paywall/__init__.py b/python/x402/http/paywall/__init__.py index 1d02a3f5f1..26147ba100 100644 --- a/python/x402/http/paywall/__init__.py +++ b/python/x402/http/paywall/__init__.py @@ -166,7 +166,7 @@ def generate_html( window.x402 = {htmlsafe_json_dumps(x402_config)}; """ - return template.replace("", f"{config_script}\n") + return template.replace("", f"{config_script}\n", 1) def _fallback_html( self, @@ -243,7 +243,7 @@ def generate_html( window.x402 = {htmlsafe_json_dumps(x402_config)}; """ - return template.replace("", f"{config_script}\n") + return template.replace("", f"{config_script}\n", 1) def _fallback_html( self, diff --git a/python/x402/http/paywall/evm_paywall_template.py b/python/x402/http/paywall/evm_paywall_template.py index c9bb46d72f..cb301fe539 100644 --- a/python/x402/http/paywall/evm_paywall_template.py +++ b/python/x402/http/paywall/evm_paywall_template.py @@ -1,2 +1,2 @@ # THIS FILE IS AUTO-GENERATED - DO NOT EDIT -EVM_PAYWALL_TEMPLATE = '\n \n \n Payment Required\n \n
\n \n \n ' +EVM_PAYWALL_TEMPLATE = '\n \n \n Payment Required\n \n
\n \n \n ' diff --git a/python/x402/http/paywall/svm_paywall_template.py b/python/x402/http/paywall/svm_paywall_template.py index 29eff3263c..de853d5bf5 100644 --- a/python/x402/http/paywall/svm_paywall_template.py +++ b/python/x402/http/paywall/svm_paywall_template.py @@ -1,2 +1,2 @@ # THIS FILE IS AUTO-GENERATED - DO NOT EDIT -SVM_PAYWALL_TEMPLATE = '\n \n \n Payment Required\n \n
\n \n \n ' +SVM_PAYWALL_TEMPLATE = '\n \n \n Payment Required\n \n
\n \n \n ' diff --git a/python/x402/http/types.py b/python/x402/http/types.py index cf02da2f69..c9043eb5e4 100644 --- a/python/x402/http/types.py +++ b/python/x402/http/types.py @@ -77,6 +77,7 @@ class HTTPRequestContext: path: str method: str payment_header: str | None = None + route_pattern: str | None = None @dataclass @@ -115,6 +116,7 @@ class ProcessSettleResult: transaction: str | None = None network: str | None = None payer: str | None = None + response: HTTPResponseInstructions | None = None # Only set when success=False # ============================================================================ @@ -140,14 +142,20 @@ class PaywallConfig: @dataclass -class UnpaidResponseResult: - """Custom unpaid response body.""" +class HTTPResponseBody: + """Custom response body (used by unpaid and settlement-failed hooks).""" content_type: str body: Any -UnpaidResponseBody = Callable[[HTTPRequestContext], UnpaidResponseResult] +UnpaidResponseBody = Callable[[HTTPRequestContext], HTTPResponseBody] + + +SettlementFailedResponseBody = Callable[ + [HTTPRequestContext, ProcessSettleResult], + HTTPResponseBody | Awaitable[HTTPResponseBody], +] @dataclass @@ -172,6 +180,7 @@ class RouteConfig: mime_type: str | None = None custom_paywall_html: str | None = None unpaid_response_body: UnpaidResponseBody | None = None + settlement_failed_response_body: SettlementFailedResponseBody | None = None extensions: dict[str, Any] | None = None hook_timeout_seconds: float | None = None @@ -186,6 +195,7 @@ class CompiledRoute: verb: str regex: re.Pattern[str] config: RouteConfig + pattern: str = "" # ============================================================================ diff --git a/python/x402/http/x402_http_server.py b/python/x402/http/x402_http_server.py index 548b863912..4799aed5a0 100644 --- a/python/x402/http/x402_http_server.py +++ b/python/x402/http/x402_http_server.py @@ -10,12 +10,14 @@ import inspect from typing import TYPE_CHECKING, Any -from ..schemas import PaymentPayload, PaymentRequirements +from ..schemas import PaymentPayload, PaymentRequirements, SettleResponse +from ..schemas.errors import SettleError from ..schemas.v1 import PaymentPayloadV1 from ..server import ResourceConfig from .types import ( HTTPProcessResult, HTTPRequestContext, + HTTPResponseInstructions, PaymentOption, PaywallConfig, ProcessSettleResult, @@ -125,6 +127,7 @@ async def process_settlement( self, payment_payload: PaymentPayload | PaymentPayloadV1, requirements: PaymentRequirements, + context: HTTPRequestContext | None = None, ) -> ProcessSettleResult: """Process settlement after successful response (async). @@ -133,9 +136,10 @@ async def process_settlement( Args: payment_payload: The verified payment payload. requirements: The matching payment requirements. + context: Optional HTTP request context for route config lookup and hooks. Returns: - ProcessSettleResult with headers if success. + ProcessSettleResult with headers if success, or response if failure. """ try: settle_response = await self._server.settle_payment( @@ -144,10 +148,18 @@ async def process_settlement( ) if not settle_response.success: - return ProcessSettleResult( + failure = ProcessSettleResult( success=False, error_reason=settle_response.error_reason or "Settlement failed", + headers=self._create_settlement_headers(settle_response, requirements), + transaction=settle_response.transaction, + network=settle_response.network, + payer=settle_response.payer, ) + failure.response = await self._build_settlement_failure_response_async( + failure, context + ) + return failure return ProcessSettleResult( success=True, @@ -157,8 +169,76 @@ async def process_settlement( payer=settle_response.payer, ) + except SettleError as e: + settle_response = SettleResponse( + success=False, + error_reason=e.error_reason, + error_message=e.error_message or e.error_reason, + transaction=e.transaction or "", + network=requirements.network, + payer=e.payer, + ) + failure = ProcessSettleResult( + success=False, + error_reason=e.error_reason, + headers=self._create_settlement_headers(settle_response, requirements), + transaction=settle_response.transaction, + network=settle_response.network, + payer=settle_response.payer, + ) + failure.response = await self._build_settlement_failure_response_async(failure, context) + return failure + except Exception as e: - return ProcessSettleResult(success=False, error_reason=str(e)) + settle_response = SettleResponse( + success=False, + error_reason=str(e), + error_message=str(e), + transaction="", + network=requirements.network, + ) + failure = ProcessSettleResult( + success=False, + error_reason=str(e), + headers=self._create_settlement_headers(settle_response, requirements), + transaction="", + network=requirements.network, + ) + failure.response = await self._build_settlement_failure_response_async(failure, context) + return failure + + async def _build_settlement_failure_response_async( + self, + failure: ProcessSettleResult, + context: HTTPRequestContext | None, + ) -> HTTPResponseInstructions: + """Build HTTPResponseInstructions for settlement failure (async). + + Awaits settlement_failed_response_body hook if it returns a coroutine. + """ + settlement_headers = failure.headers + route_config = self._get_route_config(context.path, context.method) if context else None + + custom_body = None + if route_config and route_config.settlement_failed_response_body: + hook_result = route_config.settlement_failed_response_body(context, failure) + if asyncio.iscoroutine(hook_result): + custom_body = await hook_result + else: + custom_body = hook_result + + content_type = custom_body.content_type if custom_body else "application/json" + body = custom_body.body if custom_body else {} + + return HTTPResponseInstructions( + status=402, + headers={ + "Content-Type": content_type, + **settlement_headers, + }, + body=body, + is_html=content_type.startswith("text/html"), + ) async def _build_payment_requirements_from_options( self, @@ -192,6 +272,7 @@ async def _build_payment_requirements_from_options( price=price, network=option.network, max_timeout_seconds=option.max_timeout_seconds, + extra=option.extra, ) requirements = self._server.build_payment_requirements(config) @@ -351,6 +432,7 @@ def _build_payment_requirements_from_options_sync( price=price, network=option.network, max_timeout_seconds=option.max_timeout_seconds, + extra=option.extra, ) requirements = self._server.build_payment_requirements(config) diff --git a/python/x402/http/x402_http_server_base.py b/python/x402/http/x402_http_server_base.py index 65e6fb889b..189cfdfbf3 100644 --- a/python/x402/http/x402_http_server_base.py +++ b/python/x402/http/x402_http_server_base.py @@ -5,7 +5,9 @@ from __future__ import annotations +import dataclasses import html +import logging import re from collections.abc import Generator from typing import TYPE_CHECKING, Any, Literal, Protocol @@ -18,8 +20,13 @@ ResourceInfo, SettleResponse, ) +from ..schemas.errors import SettleError from ..schemas.v1 import PaymentPayloadV1 -from .constants import PAYMENT_REQUIRED_HEADER, PAYMENT_SIGNATURE_HEADER +from .constants import ( + PAYMENT_REQUIRED_HEADER, + PAYMENT_RESPONSE_HEADER, + PAYMENT_SIGNATURE_HEADER, +) from .types import ( RESULT_NO_PAYMENT_REQUIRED, RESULT_PAYMENT_ERROR, @@ -47,6 +54,8 @@ if TYPE_CHECKING: from ..server import x402ResourceServer, x402ResourceServerSync +logger = logging.getLogger("x402") + # ============================================================================ # Paywall Provider Protocol # ============================================================================ @@ -136,8 +145,10 @@ def _compile_routes(self, routes: RoutesConfig) -> None: raise ValueError(f"Invalid route config for pattern {pattern}") for pattern, config in normalized.items(): - verb, regex = self._parse_route_pattern(pattern) - self._compiled_routes.append(CompiledRoute(verb=verb, regex=regex, config=config)) + verb, path, regex = self._parse_route_pattern(pattern) + self._compiled_routes.append( + CompiledRoute(verb=verb, regex=regex, config=config, pattern=path) + ) def _parse_route_config(self, config: dict[str, Any]) -> RouteConfig: """Parse a raw dict into a RouteConfig.""" @@ -175,6 +186,10 @@ def _parse_route_config(self, config: dict[str, Any]) -> RouteConfig: unpaid_response_body=config.get( "unpaidResponseBody", config.get("unpaid_response_body") ), + settlement_failed_response_body=config.get( + "settlementFailedResponseBody", + config.get("settlement_failed_response_body"), + ), extensions=config.get("extensions"), hook_timeout_seconds=config.get("hook_timeout_seconds"), ) @@ -226,17 +241,20 @@ def requires_payment(self, context: HTTPRequestContext) -> bool: Returns: True if route requires payment. """ - return self._get_route_config(context.path, context.method) is not None + method = context.method or context.adapter.get_method() + # _get_route_config returns tuple[RouteConfig, str] | None; 'is not None' is the + # correct check for a union-with-None return type and does not rely on tuple truthiness. + return self._get_route_config(context.path, method) is not None - def _get_route_config(self, path: str, method: str) -> RouteConfig | None: - """Find matching route configuration.""" + def _get_route_config(self, path: str, method: str) -> tuple[RouteConfig, str] | None: + """Find matching route configuration, returning (config, pattern) or None.""" normalized_path = self._normalize_path(path) upper_method = method.upper() for route in self._compiled_routes: if route.regex.match(normalized_path): if route.verb == "*" or route.verb == upper_method: - return route.config + return route.config, route.pattern return None @@ -257,10 +275,15 @@ def _process_request_core( Returns HTTPProcessResult. """ + if not context.method: + context = dataclasses.replace(context, method=context.adapter.get_method()) + # Find matching route - route_config = self._get_route_config(context.path, context.method) - if route_config is None: + route_match = self._get_route_config(context.path, context.method) + if route_match is None: return HTTPProcessResult(type=RESULT_NO_PAYMENT_REQUIRED) + route_config, route_pattern = route_match + context = dataclasses.replace(context, route_pattern=route_pattern) # Extract payment from headers payment_payload = self._extract_payment(context.adapter) @@ -402,6 +425,7 @@ def process_settlement( self, payment_payload: PaymentPayload | PaymentPayloadV1, requirements: PaymentRequirements, + context: HTTPRequestContext | None = None, ) -> ProcessSettleResult: """Process settlement after successful response. @@ -410,9 +434,10 @@ def process_settlement( Args: payment_payload: The verified payment payload. requirements: The matching payment requirements. + context: Optional HTTP request context for route config lookup and hooks. Returns: - ProcessSettleResult with headers if success. + ProcessSettleResult with headers if success, or response if failure. """ try: settle_response = self._server.settle_payment( @@ -421,10 +446,16 @@ def process_settlement( ) if not settle_response.success: - return ProcessSettleResult( + failure = ProcessSettleResult( success=False, error_reason=settle_response.error_reason or "Settlement failed", + headers=self._create_settlement_headers(settle_response, requirements), + transaction=settle_response.transaction, + network=settle_response.network, + payer=settle_response.payer, ) + failure.response = self._build_settlement_failure_response(failure, context) + return failure return ProcessSettleResult( success=True, @@ -434,8 +465,43 @@ def process_settlement( payer=settle_response.payer, ) + except SettleError as e: + settle_response = SettleResponse( + success=False, + error_reason=e.error_reason, + error_message=e.error_message or e.error_reason, + transaction=e.transaction or "", + network=requirements.network, + payer=e.payer, + ) + failure = ProcessSettleResult( + success=False, + error_reason=e.error_reason, + headers=self._create_settlement_headers(settle_response, requirements), + transaction=settle_response.transaction, + network=settle_response.network, + payer=settle_response.payer, + ) + failure.response = self._build_settlement_failure_response(failure, context) + return failure + except Exception as e: - return ProcessSettleResult(success=False, error_reason=str(e)) + settle_response = SettleResponse( + success=False, + error_reason=str(e), + error_message=str(e), + transaction="", + network=requirements.network, + ) + failure = ProcessSettleResult( + success=False, + error_reason=str(e), + headers=self._create_settlement_headers(settle_response, requirements), + transaction="", + network=requirements.network, + ) + failure.response = self._build_settlement_failure_response(failure, context) + return failure # ========================================================================= # Internal Methods @@ -507,12 +573,43 @@ def _create_settlement_headers( requirements: PaymentRequirements, ) -> dict[str, str]: """Create settlement response headers.""" - from .constants import PAYMENT_RESPONSE_HEADER - return { PAYMENT_RESPONSE_HEADER: encode_payment_response_header(settle_response), } + def _build_settlement_failure_response( + self, + failure: ProcessSettleResult, + context: HTTPRequestContext | None, + ) -> HTTPResponseInstructions: + """Build HTTPResponseInstructions for settlement failure. + + Uses settlement_failed_response_body hook if configured, otherwise defaults to empty body. + Merges settlement headers (including PAYMENT-RESPONSE) into the response. + """ + settlement_headers = failure.headers + if context and not context.method: + context = dataclasses.replace(context, method=context.adapter.get_method()) + route_match = self._get_route_config(context.path, context.method) if context else None + route_config = route_match[0] if route_match else None + + custom_body = None + if route_config and route_config.settlement_failed_response_body: + custom_body = route_config.settlement_failed_response_body(context, failure) + + content_type = custom_body.content_type if custom_body else "application/json" + body = custom_body.body if custom_body else {} + + return HTTPResponseInstructions( + status=402, + headers={ + "Content-Type": content_type, + **settlement_headers, + }, + body=body, + is_html=content_type.startswith("text/html"), + ) + def _validate_route_configuration(self) -> list[RouteValidationError]: """Validate all payment options have registered schemes.""" errors: list[RouteValidationError] = [] @@ -520,6 +617,21 @@ def _validate_route_configuration(self) -> list[RouteValidationError]: for route in self._compiled_routes: pattern = f"{route.verb} {route.regex.pattern}" + # Warn if wildcard routes are used with discovery extensions + if ( + "*" in route.pattern + and route.config.extensions + and "bazaar" in route.config.extensions + ): + logger.warning( + 'Route "%s %s": Wildcard (*) patterns with bazaar discovery extensions ' + "will auto-generate parameter names (var1, var2, ...). " + "Consider using named parameters instead (e.g. /weather/:city) " + "for better discovery metadata.", + route.verb, + route.pattern, + ) + # Get options as list options = route.config.accepts if isinstance(options, PaymentOption): @@ -555,8 +667,8 @@ def _validate_route_configuration(self) -> list[RouteValidationError]: return errors @staticmethod - def _parse_route_pattern(pattern: str) -> tuple[str, re.Pattern[str]]: - """Parse route pattern into verb and regex.""" + def _parse_route_pattern(pattern: str) -> tuple[str, str, re.Pattern[str]]: + """Parse route pattern into verb, raw path, and regex.""" parts = pattern.split(None, 1) # Split on whitespace if len(parts) == 2: @@ -570,9 +682,10 @@ def _parse_route_pattern(pattern: str) -> tuple[str, re.Pattern[str]]: regex_pattern = "^" + re.escape(path) regex_pattern = regex_pattern.replace(r"\*", ".*?") # Wildcards regex_pattern = re.sub(r"\\\[([^\]]+)\\\]", r"[^/]+", regex_pattern) # [param] + regex_pattern = re.sub(r":([a-zA-Z_]\w*)", r"[^/]+", regex_pattern) # :param regex_pattern += "$" - return verb, re.compile(regex_pattern, re.IGNORECASE) + return verb, path, re.compile(regex_pattern, re.IGNORECASE) @staticmethod def _normalize_path(path: str) -> str: @@ -669,7 +782,7 @@ def _inject_paywall_config( f"" ) - return template.replace("", config_script + "") + return template.replace("", config_script + "\n", 1) def _generate_fallback_html( self, diff --git a/python/x402/mcp/tests/test_server.py b/python/x402/mcp/tests/test_server.py index 859d99ce2f..b8f7fbf803 100644 --- a/python/x402/mcp/tests/test_server.py +++ b/python/x402/mcp/tests/test_server.py @@ -2,6 +2,8 @@ from unittest.mock import MagicMock, Mock +import pytest + from x402.mcp import ( ResourceInfo, ) @@ -11,6 +13,8 @@ from x402.mcp import ( create_payment_wrapper_sync as create_payment_wrapper, ) +from x402.mcp.types import MCPToolResult +from x402.mcp.types import SyncPaymentWrapperHooks as PaymentWrapperHooks from x402.schemas import PaymentPayload, PaymentRequirements, SettleResponse @@ -581,3 +585,345 @@ def handler(args, context): ) assert call_order == ["before", "handler", "after", "settlement"] + + +def test_no_meta_key_returns_402(): + """Test that missing _meta key returns payment-required error.""" + server = MockResourceServer() + config = PaymentWrapperConfig( + accepts=[ + PaymentRequirements( + scheme="exact", + network="eip155:84532", + amount="1000", + asset="USDC", + pay_to="0xrecipient", + max_timeout_seconds=300, + ) + ], + ) + + wrapped = create_payment_wrapper(server, config)(lambda args, ctx: {"content": []}) + result = wrapped({}, {"toolName": "test"}) + + assert result.is_error is True + + +def test_non_dict_meta_returns_402(): + """Test that non-dict _meta returns payment-required error.""" + server = MockResourceServer() + config = PaymentWrapperConfig( + accepts=[ + PaymentRequirements( + scheme="exact", + network="eip155:84532", + amount="1000", + asset="USDC", + pay_to="0xrecipient", + max_timeout_seconds=300, + ) + ], + ) + + wrapped = create_payment_wrapper(server, config)(lambda args, ctx: {"content": []}) + result = wrapped({}, {"_meta": "bad", "toolName": "test"}) + + assert result.is_error is True + + +def test_no_matching_requirements(): + """Test that mismatched network returns 402 without verification.""" + server = MockResourceServer() + config = PaymentWrapperConfig( + accepts=[ + PaymentRequirements( + scheme="exact", + network="eip155:84532", + amount="1000", + asset="USDC", + pay_to="0xrecipient", + max_timeout_seconds=300, + ) + ], + ) + + wrapped = create_payment_wrapper(server, config)(lambda args, ctx: {"content": []}) + + payload = PaymentPayload( + x402_version=2, + accepted={ + "scheme": "exact", + "network": "eip155:1", + "amount": "1000", + "asset": "USDC", + "pay_to": "0xrecipient", + "max_timeout_seconds": 300, + }, + payload={"signature": "0x123"}, + ) + extra = { + "_meta": { + "x402/payment": (payload.model_dump() if hasattr(payload, "model_dump") else payload) + }, + "toolName": "test", + } + + result = wrapped({}, extra) + + assert result.is_error is True + assert not server.verify_payment.called + + +def test_handler_returns_mcp_tool_result(): + """Test that handler returning MCPToolResult directly is used as-is.""" + server = MockResourceServer() + config = PaymentWrapperConfig( + accepts=[ + PaymentRequirements( + scheme="exact", + network="eip155:84532", + amount="1000", + asset="USDC", + pay_to="0xrecipient", + max_timeout_seconds=300, + ) + ], + ) + + def handler(args, context): + return MCPToolResult( + content=[{"type": "text", "text": "direct result"}], + is_error=False, + ) + + wrapped = create_payment_wrapper(server, config)(handler) + + payload = PaymentPayload( + x402_version=2, + accepted={ + "scheme": "exact", + "network": "eip155:84532", + "amount": "1000", + "asset": "USDC", + "pay_to": "0xrecipient", + "max_timeout_seconds": 300, + }, + payload={"signature": "0x123"}, + ) + extra = { + "_meta": { + "x402/payment": (payload.model_dump() if hasattr(payload, "model_dump") else payload) + }, + "toolName": "test", + } + + result = wrapped({}, extra) + + assert result.is_error is False + assert result.content[0]["text"] == "direct result" + + +def test_handler_returns_non_dict(): + """Test that handler returning a non-dict gets stringified.""" + server = MockResourceServer() + config = PaymentWrapperConfig( + accepts=[ + PaymentRequirements( + scheme="exact", + network="eip155:84532", + amount="1000", + asset="USDC", + pay_to="0xrecipient", + max_timeout_seconds=300, + ) + ], + ) + + wrapped = create_payment_wrapper(server, config)(lambda args, ctx: 42) + + payload = PaymentPayload( + x402_version=2, + accepted={ + "scheme": "exact", + "network": "eip155:84532", + "amount": "1000", + "asset": "USDC", + "pay_to": "0xrecipient", + "max_timeout_seconds": 300, + }, + payload={"signature": "0x123"}, + ) + extra = { + "_meta": { + "x402/payment": (payload.model_dump() if hasattr(payload, "model_dump") else payload) + }, + "toolName": "test", + } + + result = wrapped({}, extra) + + assert result.is_error is False + assert result.content[0]["text"] == "42" + + +def test_handler_dict_with_structured_content(): + """Test that dict result with structuredContent key is preserved.""" + server = MockResourceServer() + config = PaymentWrapperConfig( + accepts=[ + PaymentRequirements( + scheme="exact", + network="eip155:84532", + amount="1000", + asset="USDC", + pay_to="0xrecipient", + max_timeout_seconds=300, + ) + ], + ) + + def handler(args, context): + return { + "content": [{"type": "text", "text": "ok"}], + "isError": False, + "structuredContent": {"key": "value"}, + } + + wrapped = create_payment_wrapper(server, config)(handler) + + payload = PaymentPayload( + x402_version=2, + accepted={ + "scheme": "exact", + "network": "eip155:84532", + "amount": "1000", + "asset": "USDC", + "pay_to": "0xrecipient", + "max_timeout_seconds": 300, + }, + payload={"signature": "0x123"}, + ) + extra = { + "_meta": { + "x402/payment": (payload.model_dump() if hasattr(payload, "model_dump") else payload) + }, + "toolName": "test", + } + + result = wrapped({}, extra) + + assert result.structured_content == {"key": "value"} + + +def test_empty_accepts_raises(): + """Test that empty accepts raises ValueError.""" + with pytest.raises(ValueError, match="at least one"): + PaymentWrapperConfig(accepts=[]) + + +def test_verification_failure_no_reason(): + """Test that verification failure without reason still returns 402.""" + server = MockResourceServer() + server.verify_payment = Mock(return_value=Mock(is_valid=False, invalid_reason=None)) + config = PaymentWrapperConfig( + accepts=[ + PaymentRequirements( + scheme="exact", + network="eip155:84532", + amount="1000", + asset="USDC", + pay_to="0xrecipient", + max_timeout_seconds=300, + ) + ], + ) + + wrapped = create_payment_wrapper(server, config)(lambda args, ctx: {"content": []}) + + payload = PaymentPayload( + x402_version=2, + accepted={ + "scheme": "exact", + "network": "eip155:84532", + "amount": "1000", + "asset": "USDC", + "pay_to": "0xrecipient", + "max_timeout_seconds": 300, + }, + payload={"signature": "0x123"}, + ) + extra = { + "_meta": { + "x402/payment": (payload.model_dump() if hasattr(payload, "model_dump") else payload) + }, + "toolName": "test", + } + + result = wrapped({}, extra) + + assert result.is_error is True + + +def test_hook_context_carries_expected_fields(): + """Test that hook context objects carry tool_name, arguments, and payment data.""" + server = MockResourceServer() + captured_before = [] + captured_after = [] + captured_settlement = [] + + config = PaymentWrapperConfig( + accepts=[ + PaymentRequirements( + scheme="exact", + network="eip155:84532", + amount="1000", + asset="USDC", + pay_to="0xrecipient", + max_timeout_seconds=300, + ) + ], + hooks=PaymentWrapperHooks( + on_before_execution=lambda ctx: captured_before.append(ctx) or True, + on_after_execution=lambda ctx: captured_after.append(ctx), + on_after_settlement=lambda ctx: captured_settlement.append(ctx), + ), + ) + + wrapped = create_payment_wrapper(server, config)( + lambda args, ctx: {"content": [{"type": "text", "text": "data"}]} + ) + + payload = PaymentPayload( + x402_version=2, + accepted={ + "scheme": "exact", + "network": "eip155:84532", + "amount": "1000", + "asset": "USDC", + "pay_to": "0xrecipient", + "max_timeout_seconds": 300, + }, + payload={"signature": "0x123"}, + ) + extra = { + "_meta": { + "x402/payment": (payload.model_dump() if hasattr(payload, "model_dump") else payload) + }, + "toolName": "test", + } + + wrapped({"city": "NYC"}, extra) + + before_ctx = captured_before[0] + assert before_ctx.tool_name == "test" + assert before_ctx.arguments == {"city": "NYC"} + assert before_ctx.payment_requirements is not None + assert before_ctx.payment_payload is not None + + after_ctx = captured_after[0] + assert after_ctx.result is not None + assert after_ctx.result.content[0]["text"] == "data" + + settle_ctx = captured_settlement[0] + assert settle_ctx.settlement is not None + assert settle_ctx.settlement.success is True diff --git a/python/x402/mechanisms/evm/__init__.py b/python/x402/mechanisms/evm/__init__.py index fbaa5786df..ddd4196c3a 100644 --- a/python/x402/mechanisms/evm/__init__.py +++ b/python/x402/mechanisms/evm/__init__.py @@ -8,10 +8,10 @@ DEFAULT_VALIDITY_PERIOD, EIP1271_MAGIC_VALUE, ERC6492_MAGIC_VALUE, + ERR_AUTHORIZATION_VALUE_MISMATCH, ERR_FAILED_TO_GET_ASSET_INFO, ERR_FAILED_TO_GET_NETWORK_CONFIG, ERR_FAILED_TO_VERIFY_SIGNATURE, - ERR_INSUFFICIENT_AMOUNT, ERR_INSUFFICIENT_BALANCE, ERR_INVALID_SIGNATURE, ERR_MISSING_EIP712_DOMAIN, @@ -56,7 +56,7 @@ from .signer import ClientEvmSigner, FacilitatorEvmSigner # Signer implementations -from .signers import EthAccountSigner, FacilitatorWeb3Signer +from .signers import EthAccountSigner, EthAccountSignerWithRPC, FacilitatorWeb3Signer # Types from .types import ( @@ -91,7 +91,6 @@ # V1 legacy constants (re-exported for backward compatibility) from .v1.constants import ( - NETWORK_ALIASES, V1_NETWORK_CHAIN_IDS, V1_NETWORKS, ) @@ -113,14 +112,13 @@ "ERC6492_MAGIC_VALUE", "EIP1271_MAGIC_VALUE", "NETWORK_CONFIGS", - "NETWORK_ALIASES", "V1_NETWORKS", "V1_NETWORK_CHAIN_IDS", "ERR_INVALID_SIGNATURE", "ERR_UNDEPLOYED_SMART_WALLET", "ERR_SMART_WALLET_DEPLOYMENT_FAILED", "ERR_RECIPIENT_MISMATCH", - "ERR_INSUFFICIENT_AMOUNT", + "ERR_AUTHORIZATION_VALUE_MISMATCH", "ERR_VALID_BEFORE_EXPIRED", "ERR_VALID_AFTER_FUTURE", "ERR_NONCE_ALREADY_USED", @@ -155,6 +153,7 @@ "FacilitatorEvmSigner", # Signer implementations "EthAccountSigner", + "EthAccountSignerWithRPC", "FacilitatorWeb3Signer", # Utilities "get_evm_chain_id", diff --git a/python/x402/mechanisms/evm/constants.py b/python/x402/mechanisms/evm/constants.py index 27bcca554e..d3f772e27c 100644 --- a/python/x402/mechanisms/evm/constants.py +++ b/python/x402/mechanisms/evm/constants.py @@ -31,16 +31,185 @@ # EIP-1271 magic value (returned by isValidSignature on success) EIP1271_MAGIC_VALUE = bytes.fromhex("1626ba7e") +# Permit2 contract address (same on all EVM chains via CREATE2) +PERMIT2_ADDRESS = "0x000000000022D473030F116dDEE9F6B43aC78BA3" + +# x402ExactPermit2Proxy contract address +X402_EXACT_PERMIT2_PROXY_ADDRESS = "0x402085c248EeA27D92E8b30b2C58ed07f9E20001" + +# Permit2 EIP-712 witness types for PermitWitnessTransferFrom +# Note: Types must be in alphabetical order after primary type (TokenPermissions < Witness) +PERMIT2_WITNESS_TYPES: dict[str, list[dict[str, str]]] = { + "PermitWitnessTransferFrom": [ + {"name": "permitted", "type": "TokenPermissions"}, + {"name": "spender", "type": "address"}, + {"name": "nonce", "type": "uint256"}, + {"name": "deadline", "type": "uint256"}, + {"name": "witness", "type": "Witness"}, + ], + "TokenPermissions": [ + {"name": "token", "type": "address"}, + {"name": "amount", "type": "uint256"}, + ], + "Witness": [ + {"name": "to", "type": "address"}, + {"name": "validAfter", "type": "uint256"}, + ], +} + +# x402ExactPermit2Proxy settle ABI +X402_EXACT_PERMIT2_PROXY_ABI = [ + { + "type": "function", + "name": "settle", + "inputs": [ + { + "name": "permit", + "type": "tuple", + "components": [ + { + "name": "permitted", + "type": "tuple", + "components": [ + {"name": "token", "type": "address"}, + {"name": "amount", "type": "uint256"}, + ], + }, + {"name": "nonce", "type": "uint256"}, + {"name": "deadline", "type": "uint256"}, + ], + }, + {"name": "owner", "type": "address"}, + { + "name": "witness", + "type": "tuple", + "components": [ + {"name": "to", "type": "address"}, + {"name": "validAfter", "type": "uint256"}, + ], + }, + {"name": "signature", "type": "bytes"}, + ], + "outputs": [], + "stateMutability": "nonpayable", + } +] + +# x402ExactPermit2Proxy settleWithPermit ABI (EIP-2612 extension path) +X402_EXACT_PERMIT2_PROXY_SETTLE_WITH_PERMIT_ABI = [ + { + "type": "function", + "name": "settleWithPermit", + "inputs": [ + { + "name": "permit2612", + "type": "tuple", + "components": [ + {"name": "value", "type": "uint256"}, + {"name": "deadline", "type": "uint256"}, + {"name": "r", "type": "bytes32"}, + {"name": "s", "type": "bytes32"}, + {"name": "v", "type": "uint8"}, + ], + }, + { + "name": "permit", + "type": "tuple", + "components": [ + { + "name": "permitted", + "type": "tuple", + "components": [ + {"name": "token", "type": "address"}, + {"name": "amount", "type": "uint256"}, + ], + }, + {"name": "nonce", "type": "uint256"}, + {"name": "deadline", "type": "uint256"}, + ], + }, + {"name": "owner", "type": "address"}, + { + "name": "witness", + "type": "tuple", + "components": [ + {"name": "to", "type": "address"}, + {"name": "validAfter", "type": "uint256"}, + ], + }, + {"name": "signature", "type": "bytes"}, + ], + "outputs": [], + "stateMutability": "nonpayable", + } +] + +# EIP-2612 nonces ABI +EIP2612_NONCES_ABI = [ + { + "type": "function", + "name": "nonces", + "inputs": [{"name": "owner", "type": "address"}], + "outputs": [{"type": "uint256"}], + "stateMutability": "view", + } +] + +# EIP-2612 EIP-712 Permit types +EIP2612_PERMIT_TYPES: dict[str, list[dict[str, str]]] = { + "Permit": [ + {"name": "owner", "type": "address"}, + {"name": "spender", "type": "address"}, + {"name": "value", "type": "uint256"}, + {"name": "nonce", "type": "uint256"}, + {"name": "deadline", "type": "uint256"}, + ] +} + +# Gas limit for a standard ERC-20 approve() transaction +ERC20_APPROVE_GAS_LIMIT = 70_000 + +# Permit2 deadline buffer (seconds) for verification +PERMIT2_DEADLINE_BUFFER = 6 + +# ERC-20 allowance ABI +ERC20_ALLOWANCE_ABI = [ + { + "type": "function", + "name": "allowance", + "inputs": [ + {"name": "owner", "type": "address"}, + {"name": "spender", "type": "address"}, + ], + "outputs": [{"type": "uint256"}], + "stateMutability": "view", + } +] + +# ERC-20 approve ABI +ERC20_APPROVE_ABI = [ + { + "type": "function", + "name": "approve", + "inputs": [ + {"name": "spender", "type": "address"}, + {"name": "amount", "type": "uint256"}, + ], + "outputs": [{"type": "bool"}], + "stateMutability": "nonpayable", + } +] + # Error codes ERR_INVALID_SIGNATURE = "invalid_exact_evm_payload_signature" ERR_UNDEPLOYED_SMART_WALLET = "invalid_exact_evm_payload_undeployed_smart_wallet" ERR_SMART_WALLET_DEPLOYMENT_FAILED = "smart_wallet_deployment_failed" ERR_RECIPIENT_MISMATCH = "invalid_exact_evm_payload_recipient_mismatch" -ERR_INSUFFICIENT_AMOUNT = "invalid_exact_evm_payload_authorization_value" +ERR_AUTHORIZATION_VALUE_MISMATCH = "invalid_exact_evm_payload_authorization_value_mismatch" ERR_VALID_BEFORE_EXPIRED = "invalid_exact_evm_payload_authorization_valid_before" ERR_VALID_AFTER_FUTURE = "invalid_exact_evm_payload_authorization_valid_after" -ERR_NONCE_ALREADY_USED = "nonce_already_used" -ERR_INSUFFICIENT_BALANCE = "insufficient_balance" +ERR_NONCE_ALREADY_USED = "invalid_exact_evm_nonce_already_used" +ERR_INSUFFICIENT_BALANCE = "invalid_exact_evm_insufficient_balance" ERR_MISSING_EIP712_DOMAIN = "missing_eip712_domain" ERR_NETWORK_MISMATCH = "network_mismatch" ERR_UNSUPPORTED_SCHEME = "unsupported_scheme" @@ -48,10 +217,24 @@ ERR_FAILED_TO_GET_ASSET_INFO = "invalid_exact_evm_failed_to_get_asset_info" ERR_FAILED_TO_VERIFY_SIGNATURE = "invalid_exact_evm_failed_to_verify_signature" ERR_TRANSACTION_FAILED = "transaction_failed" +ERR_TOKEN_NAME_MISMATCH = "invalid_exact_evm_token_name_mismatch" +ERR_TOKEN_VERSION_MISMATCH = "invalid_exact_evm_token_version_mismatch" +ERR_EIP3009_NOT_SUPPORTED = "invalid_exact_evm_eip3009_not_supported" +ERR_TRANSACTION_SIMULATION_FAILED = "invalid_exact_evm_transaction_simulation_failed" +# Permit2-specific error codes +ERR_PERMIT2_INVALID_SPENDER = "invalid_permit2_spender" +ERR_PERMIT2_RECIPIENT_MISMATCH = "invalid_permit2_recipient_mismatch" +ERR_PERMIT2_DEADLINE_EXPIRED = "permit2_deadline_expired" +ERR_PERMIT2_NOT_YET_VALID = "permit2_not_yet_valid" +ERR_PERMIT2_AMOUNT_MISMATCH = "invalid_exact_evm_payload_amount_mismatch" +ERR_PERMIT2_TOKEN_MISMATCH = "permit2_token_mismatch" +ERR_PERMIT2_INVALID_SIGNATURE = "invalid_permit2_signature" +ERR_PERMIT2_ALLOWANCE_REQUIRED = "permit2_allowance_required" -class AssetInfo(TypedDict): - """Information about a token asset.""" + +class _AssetInfoRequired(TypedDict): + """Required fields for a token asset.""" address: str name: str @@ -59,34 +242,27 @@ class AssetInfo(TypedDict): decimals: int -class NetworkConfig(TypedDict): - """Configuration for an EVM network.""" +class AssetInfo(_AssetInfoRequired, total=False): + """Information about a token asset.""" + + asset_transfer_method: str + supports_eip2612: bool + + +class _NetworkConfigRequired(TypedDict): + """Required fields for an EVM network configuration.""" chain_id: int + + +class NetworkConfig(_NetworkConfigRequired, total=False): + """Configuration for an EVM network.""" + default_asset: AssetInfo - supported_assets: dict[str, AssetInfo] # Network configurations NETWORK_CONFIGS: dict[str, NetworkConfig] = { - # Ethereum Mainnet - "eip155:1": { - "chain_id": 1, - "default_asset": { - "address": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", - "name": "USD Coin", - "version": "2", - "decimals": 6, - }, - "supported_assets": { - "USDC": { - "address": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", - "name": "USD Coin", - "version": "2", - "decimals": 6, - }, - }, - }, # Base Mainnet "eip155:8453": { "chain_id": 8453, @@ -96,14 +272,6 @@ class NetworkConfig(TypedDict): "version": "2", "decimals": 6, }, - "supported_assets": { - "USDC": { - "address": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", - "name": "USD Coin", - "version": "2", - "decimals": 6, - }, - }, }, # Base Sepolia (Testnet) "eip155:84532": { @@ -114,13 +282,59 @@ class NetworkConfig(TypedDict): "version": "2", "decimals": 6, }, - "supported_assets": { - "USDC": { - "address": "0x036CbD53842c5426634e7929541eC2318f3dCF7e", - "name": "USDC", - "version": "2", - "decimals": 6, - }, + }, + # MegaETH Mainnet (uses Permit2 instead of EIP-3009, supports EIP-2612) + "eip155:4326": { + "chain_id": 4326, + "default_asset": { + "address": "0xFAfDdbb3FC7688494971a79cc65DCa3EF82079E7", + "name": "MegaUSD", + "version": "1", + "decimals": 18, + "asset_transfer_method": "permit2", + "supports_eip2612": True, + }, + }, + # Monad Mainnet + "eip155:143": { + "chain_id": 143, + "default_asset": { + "address": "0x754704Bc059F8C67012fEd69BC8A327a5aafb603", + "name": "USD Coin", + "version": "2", + "decimals": 6, + }, + }, + # Mezo Testnet (uses Permit2 instead of EIP-3009, supports EIP-2612) + "eip155:31611": { + "chain_id": 31611, + "default_asset": { + "address": "0x118917a40FAF1CD7a13dB0Ef56C86De7973Ac503", + "name": "Mezo USD", + "version": "1", + "decimals": 18, + "asset_transfer_method": "permit2", + "supports_eip2612": True, + }, + }, + # Stable Mainnet + "eip155:988": { + "chain_id": 988, + "default_asset": { + "address": "0x779Ded0c9e1022225f8E0630b35a9b54bE713736", + "name": "USDT0", + "version": "1", + "decimals": 6, + }, + }, + # Stable Testnet + "eip155:2201": { + "chain_id": 2201, + "default_asset": { + "address": "0x78Cf24370174180738C5B8E352B6D14c83a6c9A9", + "name": "USDT0", + "version": "1", + "decimals": 6, }, }, # Polygon Mainnet @@ -141,64 +355,30 @@ class NetworkConfig(TypedDict): }, }, }, - # Avalanche C-Chain - "eip155:43114": { - "chain_id": 43114, + # Arbitrum One + "eip155:42161": { + "chain_id": 42161, "default_asset": { - "address": "0xB97EF9Ef8734C71904D8002F8b6Bc66Dd9c48a6E", + "address": "0xaf88d065e77c8cC2239327C5EDb3A432268e5831", "name": "USD Coin", "version": "2", "decimals": 6, }, - "supported_assets": { - "USDC": { - "address": "0xB97EF9Ef8734C71904D8002F8b6Bc66Dd9c48a6E", - "name": "USD Coin", - "version": "2", - "decimals": 6, - }, - }, - }, - # MegaETH Mainnet - "eip155:4326": { - "chain_id": 4326, - "default_asset": { - "address": "0xFAfDdbb3FC7688494971a79cc65DCa3EF82079E7", - "name": "MegaUSD", - "version": "1", - "decimals": 18, - }, - "supported_assets": { - "USDM": { - "address": "0xFAfDdbb3FC7688494971a79cc65DCa3EF82079E7", - "name": "MegaUSD", - "version": "1", - "decimals": 18, - }, - }, }, - # Monad Mainnet - "eip155:143": { - "chain_id": 143, + # Arbitrum Sepolia + "eip155:421614": { + "chain_id": 421614, "default_asset": { - "address": "0x754704Bc059F8C67012fEd69BC8A327a5aafb603", + "address": "0x75faf114eafb1BDbe2F0316DF893fd58CE46AA4d", "name": "USD Coin", "version": "2", "decimals": 6, }, - "supported_assets": { - "USDC": { - "address": "0x754704Bc059F8C67012fEd69BC8A327a5aafb603", - "name": "USD Coin", - "version": "2", - "decimals": 6, - }, - }, }, } # V1 legacy constants are in x402.mechanisms.evm.v1.constants -# (NETWORK_ALIASES, V1_NETWORKS, V1_NETWORK_CHAIN_IDS) +# (V1_NETWORKS, V1_NETWORK_CHAIN_IDS, V1_DEFAULT_ASSETS) # EIP-3009 ABIs TRANSFER_WITH_AUTHORIZATION_VRS_ABI = [ @@ -262,6 +442,26 @@ class NetworkConfig(TypedDict): } ] +NAME_ABI = [ + { + "inputs": [], + "name": "name", + "outputs": [{"name": "", "type": "string"}], + "stateMutability": "view", + "type": "function", + } +] + +VERSION_ABI = [ + { + "inputs": [], + "name": "version", + "outputs": [{"name": "", "type": "string"}], + "stateMutability": "view", + "type": "function", + } +] + IS_VALID_SIGNATURE_ABI = [ { "inputs": [ @@ -274,3 +474,34 @@ class NetworkConfig(TypedDict): "type": "function", } ] + +MULTICALL3_ADDRESS = "0xcA11bde05977b3631167028862bE2a173976CA11" + +MULTICALL3_TRY_AGGREGATE_ABI = [ + { + "inputs": [ + {"name": "requireSuccess", "type": "bool"}, + { + "name": "calls", + "type": "tuple[]", + "components": [ + {"name": "target", "type": "address"}, + {"name": "callData", "type": "bytes"}, + ], + }, + ], + "name": "tryAggregate", + "outputs": [ + { + "name": "returnData", + "type": "tuple[]", + "components": [ + {"name": "success", "type": "bool"}, + {"name": "returnData", "type": "bytes"}, + ], + } + ], + "stateMutability": "payable", + "type": "function", + } +] diff --git a/python/x402/mechanisms/evm/exact/client.py b/python/x402/mechanisms/evm/exact/client.py index 66157a8212..5eabc10a39 100644 --- a/python/x402/mechanisms/evm/exact/client.py +++ b/python/x402/mechanisms/evm/exact/client.py @@ -6,16 +6,22 @@ from typing import Any from ....schemas import PaymentRequirements -from ..constants import SCHEME_EXACT +from ..constants import ERC20_ALLOWANCE_ABI, PERMIT2_ADDRESS, SCHEME_EXACT from ..eip712 import build_typed_data_for_signing -from ..signer import ClientEvmSigner +from ..signer import ( + ClientEvmSigner, + ClientEvmSignerWithReadContract, + ClientEvmSignerWithSignTransaction, +) from ..types import ExactEIP3009Authorization, ExactEIP3009Payload, TypedDataField from ..utils import ( create_nonce, create_validity_window, get_asset_info, get_evm_chain_id, + normalize_address, ) +from .permit2_utils import create_permit2_payload def _wrap_if_local_account(signer: Any) -> ClientEvmSigner: @@ -38,6 +44,10 @@ class ExactEvmScheme: Implements SchemeNetworkClient protocol. Returns the inner payload dict, which x402Client wraps into a full PaymentPayload. + For Permit2 flows, if the server advertises gas sponsoring extensions + and the signer has the required capabilities, the scheme automatically + signs extension data when Permit2 allowance is insufficient. + Attributes: scheme: The scheme identifier ("exact"). """ @@ -57,16 +67,32 @@ def __init__(self, signer: ClientEvmSigner): def create_payment_payload( self, requirements: PaymentRequirements, + extensions: dict[str, Any] | None = None, ) -> dict[str, Any]: - """Create signed EIP-3009 inner payload. + """Create signed payment inner payload. + + Routes to Permit2 or EIP-3009 based on requirements.extra.assetTransferMethod. + For Permit2, enriches with gas sponsoring extensions when advertised. Args: requirements: Payment requirements from server. + extensions: Server-declared extensions from PaymentRequired. Returns: - Inner payload dict (authorization + signature). + Inner payload dict. x402Client wraps this with x402_version, accepted, resource, extensions. """ + extra = requirements.extra or {} + if extra.get("assetTransferMethod") == "permit2": + result = create_permit2_payload(self._signer, requirements) + + if extensions: + ext_data = self._try_sign_extensions(requirements, result, extensions) + if ext_data: + result["__extensions"] = ext_data + + return result + nonce = create_nonce() valid_after, valid_before = create_validity_window( timedelta(seconds=requirements.max_timeout_seconds or 3600) @@ -85,9 +111,130 @@ def create_payment_payload( payload = ExactEIP3009Payload(authorization=authorization, signature=signature) - # Return inner payload dict - x402Client wraps this return payload.to_dict() + def _try_sign_extensions( + self, + requirements: PaymentRequirements, + result: dict[str, Any], + extensions: dict[str, Any], + ) -> dict[str, Any] | None: + """Try to sign gas sponsoring extensions for Permit2 flows.""" + + # Try EIP-2612 first + eip2612_ext = self._try_sign_eip2612(requirements, result, extensions) + if eip2612_ext: + return eip2612_ext + + # Try ERC-20 approval fallback + erc20_ext = self._try_sign_erc20_approval(requirements, extensions) + if erc20_ext: + return erc20_ext + + return None + + def _try_sign_eip2612( + self, + requirements: PaymentRequirements, + result: dict[str, Any], + extensions: dict[str, Any], + ) -> dict[str, Any] | None: + """Try to sign an EIP-2612 permit for gasless Permit2 approval.""" + from ....extensions.eip2612_gas_sponsoring import EIP2612_GAS_SPONSORING_KEY + from ....extensions.eip2612_gas_sponsoring.client import sign_eip2612_permit + + if EIP2612_GAS_SPONSORING_KEY not in extensions: + return None + + if not isinstance(self._signer, ClientEvmSignerWithReadContract): + return None + + extra = requirements.extra or {} + token_name = extra.get("name") + token_version = extra.get("version") + if not token_name or not token_version: + return None + + chain_id = get_evm_chain_id(str(requirements.network)) + token_address = normalize_address(requirements.asset) + + try: + allowance = self._signer.read_contract( + token_address, + ERC20_ALLOWANCE_ABI, + "allowance", + self._signer.address, + PERMIT2_ADDRESS, + ) + if int(allowance) >= int(requirements.amount): + return None + except Exception: + pass # Allowance check failed, proceed with signing + + permit2_auth = result.get("permit2Authorization", {}) + deadline = permit2_auth.get("deadline", "") + if not deadline: + import time + + deadline = str(int(time.time()) + (requirements.max_timeout_seconds or 3600)) + + info = sign_eip2612_permit( + self._signer, + token_address, + token_name, + token_version, + chain_id, + deadline, + requirements.amount, + ) + + return {EIP2612_GAS_SPONSORING_KEY: {"info": info.to_dict()}} + + def _try_sign_erc20_approval( + self, + requirements: PaymentRequirements, + extensions: dict[str, Any], + ) -> dict[str, Any] | None: + """Try to sign an ERC-20 approval tx for gasless Permit2 approval.""" + from ....extensions.erc20_approval_gas_sponsoring import ( + ERC20_APPROVAL_GAS_SPONSORING_KEY, + ) + from ....extensions.erc20_approval_gas_sponsoring.client import ( + sign_erc20_approval_transaction, + ) + + if ERC20_APPROVAL_GAS_SPONSORING_KEY not in extensions: + return None + + if not isinstance(self._signer, ClientEvmSignerWithSignTransaction): + return None + + chain_id = get_evm_chain_id(str(requirements.network)) + token_address = normalize_address(requirements.asset) + + # Skip if allowance is already sufficient + if isinstance(self._signer, ClientEvmSignerWithReadContract): + try: + allowance = self._signer.read_contract( + token_address, + ERC20_ALLOWANCE_ABI, + "allowance", + self._signer.address, + PERMIT2_ADDRESS, + ) + if int(allowance) >= int(requirements.amount): + return None + except Exception: + pass + + info = sign_erc20_approval_transaction( + self._signer, + token_address, + chain_id, + ) + + return {ERC20_APPROVAL_GAS_SPONSORING_KEY: {"info": info.to_dict()}} + def _sign_authorization( self, authorization: ExactEIP3009Authorization, diff --git a/python/x402/mechanisms/evm/exact/eip3009_utils.py b/python/x402/mechanisms/evm/exact/eip3009_utils.py new file mode 100644 index 0000000000..0b0885e461 --- /dev/null +++ b/python/x402/mechanisms/evm/exact/eip3009_utils.py @@ -0,0 +1,315 @@ +"""Shared EIP-3009 helpers for exact EVM facilitators.""" + +from __future__ import annotations + +from dataclasses import dataclass + +from ..constants import ( + AUTHORIZATION_STATE_ABI, + BALANCE_OF_ABI, + ERR_EIP3009_NOT_SUPPORTED, + ERR_INSUFFICIENT_BALANCE, + ERR_NONCE_ALREADY_USED, + ERR_TOKEN_NAME_MISMATCH, + ERR_TOKEN_VERSION_MISMATCH, + ERR_TRANSACTION_SIMULATION_FAILED, + FUNCTION_TRANSFER_WITH_AUTHORIZATION, + NAME_ABI, + TRANSFER_WITH_AUTHORIZATION_BYTES_ABI, + TRANSFER_WITH_AUTHORIZATION_VRS_ABI, + VERSION_ABI, +) +from ..eip712 import build_typed_data_for_signing +from ..erc6492 import has_deployment_info, parse_erc6492_signature +from ..multicall import MulticallCall, encode_contract_call, multicall +from ..signer import FacilitatorEvmSigner +from ..types import ERC6492SignatureData, ExactEIP3009Authorization +from ..utils import bytes_to_hex, hex_to_bytes + + +@dataclass +class ParsedEIP3009Authorization: + """Parsed authorization values ready for contract calls.""" + + from_address: str + to: str + value: int + valid_after: int + valid_before: int + nonce: bytes + + +@dataclass +class EIP3009SignatureClassification: + """How the facilitator should treat a signature before simulation.""" + + valid: bool + is_smart_wallet: bool + is_undeployed: bool + sig_data: ERC6492SignatureData + + +def parse_eip3009_authorization( + authorization: ExactEIP3009Authorization, +) -> ParsedEIP3009Authorization: + """Parse string-encoded authorization fields into contract-call values.""" + nonce = hex_to_bytes(authorization.nonce) + if len(nonce) != 32: + raise ValueError(f"invalid nonce length: got {len(nonce)} bytes, want 32") + + return ParsedEIP3009Authorization( + from_address=authorization.from_address, + to=authorization.to, + value=int(authorization.value), + valid_after=int(authorization.valid_after), + valid_before=int(authorization.valid_before), + nonce=nonce, + ) + + +def classify_eip3009_signature( + signer: FacilitatorEvmSigner, + authorization: ExactEIP3009Authorization, + signature: bytes, + chain_id: int, + token_address: str, + token_name: str, + token_version: str, +) -> EIP3009SignatureClassification: + """Classify the signature before deciding whether simulation may rescue it.""" + sig_data = parse_erc6492_signature(signature) + domain, types, primary_type, message = build_typed_data_for_signing( + authorization, + chain_id, + token_address, + token_name, + token_version, + ) + + is_smart_wallet = has_deployment_info(sig_data) or len(sig_data.inner_signature) != 65 + valid = signer.verify_typed_data( + authorization.from_address, + domain, + types, + primary_type, + message, + sig_data.inner_signature, + ) + if valid: + return EIP3009SignatureClassification( + valid=True, + is_smart_wallet=is_smart_wallet, + is_undeployed=False, + sig_data=sig_data, + ) + + code = signer.get_code(authorization.from_address) + if len(code) > 0: + return EIP3009SignatureClassification( + valid=False, + is_smart_wallet=True, + is_undeployed=False, + sig_data=sig_data, + ) + + if has_deployment_info(sig_data): + return EIP3009SignatureClassification( + valid=False, + is_smart_wallet=True, + is_undeployed=True, + sig_data=sig_data, + ) + + return EIP3009SignatureClassification( + valid=False, + is_smart_wallet=is_smart_wallet, + is_undeployed=is_smart_wallet, + sig_data=sig_data, + ) + + +def simulate_eip3009_transfer( + signer: FacilitatorEvmSigner, + token_address: str, + parsed: ParsedEIP3009Authorization, + sig_data: ERC6492SignatureData, +) -> bool: + """Simulate `transferWithAuthorization` and return whether it succeeds.""" + if has_deployment_info(sig_data): + transfer_calldata = encode_contract_call( + TRANSFER_WITH_AUTHORIZATION_BYTES_ABI, + FUNCTION_TRANSFER_WITH_AUTHORIZATION, + parsed.from_address, + parsed.to, + parsed.value, + parsed.valid_after, + parsed.valid_before, + parsed.nonce, + sig_data.inner_signature, + ) + try: + results = multicall( + signer, + [ + MulticallCall( + address=bytes_to_hex(sig_data.factory), + call_data=sig_data.factory_calldata, + ), + MulticallCall(address=token_address, call_data=transfer_calldata), + ], + ) + except Exception: + return False + return len(results) >= 2 and results[1].success + + if len(sig_data.inner_signature) == 65: + v, r, s = _split_signature_parts(sig_data.inner_signature) + try: + signer.read_contract( + token_address, + TRANSFER_WITH_AUTHORIZATION_VRS_ABI, + FUNCTION_TRANSFER_WITH_AUTHORIZATION, + parsed.from_address, + parsed.to, + parsed.value, + parsed.valid_after, + parsed.valid_before, + parsed.nonce, + v, + r, + s, + ) + except Exception: + return False + return True + + try: + signer.read_contract( + token_address, + TRANSFER_WITH_AUTHORIZATION_BYTES_ABI, + FUNCTION_TRANSFER_WITH_AUTHORIZATION, + parsed.from_address, + parsed.to, + parsed.value, + parsed.valid_after, + parsed.valid_before, + parsed.nonce, + sig_data.inner_signature, + ) + except Exception: + return False + return True + + +def diagnose_eip3009_simulation_failure( + signer: FacilitatorEvmSigner, + token_address: str, + authorization: ExactEIP3009Authorization, + required_amount: int, + token_name: str, + token_version: str, +) -> str: + """Map a failed transfer simulation to the most specific invalid reason.""" + try: + results = multicall( + signer, + [ + MulticallCall( + address=token_address, + abi=BALANCE_OF_ABI, + function_name="balanceOf", + args=(authorization.from_address,), + ), + MulticallCall(address=token_address, abi=NAME_ABI, function_name="name"), + MulticallCall( + address=token_address, + abi=VERSION_ABI, + function_name="version", + ), + MulticallCall( + address=token_address, + abi=AUTHORIZATION_STATE_ABI, + function_name="authorizationState", + args=(authorization.from_address, hex_to_bytes(authorization.nonce)), + ), + ], + ) + except Exception: + return ERR_TRANSACTION_SIMULATION_FAILED + + if len(results) < 4: + return ERR_TRANSACTION_SIMULATION_FAILED + + authorization_state = results[3] + if not authorization_state.success: + return ERR_EIP3009_NOT_SUPPORTED + if bool(authorization_state.result): + return ERR_NONCE_ALREADY_USED + + name_result = results[1] + if token_name and name_result.success and isinstance(name_result.result, str): + if name_result.result != token_name: + return ERR_TOKEN_NAME_MISMATCH + + version_result = results[2] + if token_version and version_result.success and isinstance(version_result.result, str): + if version_result.result != token_version: + return ERR_TOKEN_VERSION_MISMATCH + + balance_result = results[0] + if balance_result.success: + try: + if int(balance_result.result) < required_amount: + return ERR_INSUFFICIENT_BALANCE + except (TypeError, ValueError): + pass + + return ERR_TRANSACTION_SIMULATION_FAILED + + +def execute_transfer_with_authorization( + signer: FacilitatorEvmSigner, + token_address: str, + parsed: ParsedEIP3009Authorization, + sig_data: ERC6492SignatureData, +) -> str: + """Execute `transferWithAuthorization` using the correct ABI overload.""" + if len(sig_data.inner_signature) == 65: + v, r, s = _split_signature_parts(sig_data.inner_signature) + return signer.write_contract( + token_address, + TRANSFER_WITH_AUTHORIZATION_VRS_ABI, + FUNCTION_TRANSFER_WITH_AUTHORIZATION, + parsed.from_address, + parsed.to, + parsed.value, + parsed.valid_after, + parsed.valid_before, + parsed.nonce, + v, + r, + s, + ) + + return signer.write_contract( + token_address, + TRANSFER_WITH_AUTHORIZATION_BYTES_ABI, + FUNCTION_TRANSFER_WITH_AUTHORIZATION, + parsed.from_address, + parsed.to, + parsed.value, + parsed.valid_after, + parsed.valid_before, + parsed.nonce, + sig_data.inner_signature, + ) + + +def _split_signature_parts(signature: bytes) -> tuple[int, bytes, bytes]: + if len(signature) != 65: + raise ValueError(f"invalid ECDSA signature length: expected 65, got {len(signature)}") + + v = signature[64] + if v in (0, 1): + v += 27 + return (v, signature[:32], signature[32:64]) diff --git a/python/x402/mechanisms/evm/exact/facilitator.py b/python/x402/mechanisms/evm/exact/facilitator.py index ecf3b070c3..e7b61010d5 100644 --- a/python/x402/mechanisms/evm/exact/facilitator.py +++ b/python/x402/mechanisms/evm/exact/facilitator.py @@ -12,15 +12,12 @@ VerifyResponse, ) from ..constants import ( - AUTHORIZATION_STATE_ABI, + ERR_AUTHORIZATION_VALUE_MISMATCH, ERR_FAILED_TO_GET_NETWORK_CONFIG, ERR_FAILED_TO_VERIFY_SIGNATURE, - ERR_INSUFFICIENT_AMOUNT, - ERR_INSUFFICIENT_BALANCE, ERR_INVALID_SIGNATURE, ERR_MISSING_EIP712_DOMAIN, ERR_NETWORK_MISMATCH, - ERR_NONCE_ALREADY_USED, ERR_RECIPIENT_MISMATCH, ERR_SMART_WALLET_DEPLOYMENT_FAILED, ERR_TRANSACTION_FAILED, @@ -29,16 +26,20 @@ ERR_VALID_AFTER_FUTURE, ERR_VALID_BEFORE_EXPIRED, SCHEME_EXACT, - TRANSFER_WITH_AUTHORIZATION_BYTES_ABI, - TRANSFER_WITH_AUTHORIZATION_VRS_ABI, TX_STATUS_SUCCESS, ) -from ..eip712 import hash_eip3009_authorization from ..erc6492 import has_deployment_info, parse_erc6492_signature +from ..exact.eip3009_utils import ( + classify_eip3009_signature, + diagnose_eip3009_simulation_failure, + execute_transfer_with_authorization, + parse_eip3009_authorization, + simulate_eip3009_transfer, +) +from ..exact.permit2_utils import settle_permit2, verify_permit2 from ..signer import FacilitatorEvmSigner -from ..types import ERC6492SignatureData, ExactEIP3009Payload +from ..types import ERC6492SignatureData, ExactEIP3009Payload, is_permit2_payload from ..utils import bytes_to_hex, get_evm_chain_id, hex_to_bytes, normalize_address -from ..verify import verify_universal_signature @dataclass @@ -48,6 +49,9 @@ class ExactEvmSchemeConfig: deploy_erc4337_with_eip6492: bool = False """Enable automatic smart wallet deployment via EIP-6492.""" + simulate_in_settle: bool = False + """Rerun transfer simulation during settle.""" + class ExactEvmScheme: """EVM facilitator implementation for the Exact payment scheme (V2). @@ -103,6 +107,16 @@ def verify( payload: PaymentPayload, requirements: PaymentRequirements, context=None, + ) -> VerifyResponse: + if is_permit2_payload(payload.payload): + return verify_permit2(self._signer, payload, requirements, context) + return self._verify(payload, requirements, simulate=True) + + def _verify( + self, + payload: PaymentPayload, + requirements: PaymentRequirements, + simulate: bool, ) -> VerifyResponse: """Verify EIP-3009 payment payload. @@ -110,7 +124,7 @@ def verify( - Scheme and network match - Signature is valid (EOA, EIP-1271, or ERC-6492) - Recipient matches requirements.pay_to - - Amount >= requirements.amount + - Amount exactly matches requirements.amount - Validity window is correct - Nonce hasn't been used - Payer has sufficient balance @@ -163,9 +177,11 @@ def verify( ) # Validate amount - if int(evm_payload.authorization.value) < int(requirements.amount): + if int(evm_payload.authorization.value) != int(requirements.amount): return VerifyResponse( - is_valid=False, invalid_reason=ERR_INSUFFICIENT_AMOUNT, payer=payer + is_valid=False, + invalid_reason=ERR_AUTHORIZATION_VALUE_MISMATCH, + payer=payer, ) # Validate timing @@ -183,46 +199,30 @@ def verify( is_valid=False, invalid_reason=ERR_VALID_AFTER_FUTURE, payer=payer ) - # Check nonce - try: - nonce_used = self._check_nonce_used( - payer, evm_payload.authorization.nonce, token_address - ) - if nonce_used: - return VerifyResponse( - is_valid=False, invalid_reason=ERR_NONCE_ALREADY_USED, payer=payer - ) - except Exception: - pass # Continue if nonce check fails - - # Check balance - try: - balance = self._signer.get_balance(payer, token_address) - if balance < int(evm_payload.authorization.value): - return VerifyResponse( - is_valid=False, invalid_reason=ERR_INSUFFICIENT_BALANCE, payer=payer - ) - except Exception: - pass # Continue if balance check fails - # Verify signature if not evm_payload.signature: return VerifyResponse(is_valid=False, invalid_reason=ERR_INVALID_SIGNATURE, payer=payer) - signature = hex_to_bytes(evm_payload.signature) - hash_bytes = hash_eip3009_authorization( - evm_payload.authorization, - chain_id, - token_address, - extra["name"], - extra["version"], - ) - try: - valid, _ = verify_universal_signature( - self._signer, payer, hash_bytes, signature, allow_undeployed=True + signature = hex_to_bytes(evm_payload.signature) + classification = classify_eip3009_signature( + self._signer, + evm_payload.authorization, + signature, + chain_id, + token_address, + extra["name"], + extra["version"], ) - if not valid: + if not classification.valid and classification.is_undeployed: + if not has_deployment_info(classification.sig_data): + return VerifyResponse( + is_valid=False, + invalid_reason=ERR_UNDEPLOYED_SMART_WALLET, + payer=payer, + ) + + if not classification.valid and not classification.is_smart_wallet: return VerifyResponse( is_valid=False, invalid_reason=ERR_INVALID_SIGNATURE, payer=payer ) @@ -234,6 +234,38 @@ def verify( payer=payer, ) + if not simulate: + return VerifyResponse(is_valid=True, payer=payer) + + try: + parsed_authorization = parse_eip3009_authorization(evm_payload.authorization) + except Exception as e: + return VerifyResponse( + is_valid=False, + invalid_reason=ERR_FAILED_TO_VERIFY_SIGNATURE, + invalid_message=str(e), + payer=payer, + ) + + if not simulate_eip3009_transfer( + self._signer, + token_address, + parsed_authorization, + classification.sig_data, + ): + return VerifyResponse( + is_valid=False, + invalid_reason=diagnose_eip3009_simulation_failure( + self._signer, + token_address, + evm_payload.authorization, + int(requirements.amount), + extra["name"], + extra["version"], + ), + payer=payer, + ) + return VerifyResponse(is_valid=True, payer=payer) def settle( @@ -242,8 +274,10 @@ def settle( requirements: PaymentRequirements, context=None, ) -> SettleResponse: - """Settle EIP-3009 payment on-chain. + """Settle payment on-chain. + Routes to Permit2 or EIP-3009 settlement based on payload type. + For EIP-3009: - Re-verifies payment - Deploys smart wallet if configured and needed (ERC-6492) - Calls transferWithAuthorization (v,r,s or bytes overload) @@ -256,8 +290,15 @@ def settle( Returns: SettleResponse with success, transaction, and payer. """ + if is_permit2_payload(payload.payload): + return settle_permit2(self._signer, payload, requirements, context) + # First verify - verify_result = self.verify(payload, requirements, context) + verify_result = self._verify( + payload, + requirements, + simulate=self._config.simulate_in_settle, + ) if not verify_result.is_valid: return SettleResponse( success=False, @@ -272,8 +313,19 @@ def settle( network = str(requirements.network) token_address = normalize_address(requirements.asset) - signature = hex_to_bytes(evm_payload.signature) - sig_data = parse_erc6492_signature(signature) + try: + signature = hex_to_bytes(evm_payload.signature or "") + sig_data = parse_erc6492_signature(signature) + parsed_authorization = parse_eip3009_authorization(evm_payload.authorization) + except Exception as e: + return SettleResponse( + success=False, + error_reason=ERR_TRANSACTION_FAILED, + error_message=str(e), + network=network, + payer=payer, + transaction="", + ) # Deploy smart wallet if needed if has_deployment_info(sig_data): @@ -300,43 +352,13 @@ def settle( transaction="", ) - # Use inner signature for settlement - inner_sig = sig_data.inner_signature - is_ecdsa = len(inner_sig) == 65 - try: - if is_ecdsa: - # EOA: v,r,s overload - r, s, v = inner_sig[:32], inner_sig[32:64], inner_sig[64] - tx_hash = self._signer.write_contract( - token_address, - TRANSFER_WITH_AUTHORIZATION_VRS_ABI, - "transferWithAuthorization", - payer, - evm_payload.authorization.to, - int(evm_payload.authorization.value), - int(evm_payload.authorization.valid_after), - int(evm_payload.authorization.valid_before), - hex_to_bytes(evm_payload.authorization.nonce), - v, - r, - s, - ) - else: - # Smart wallet: bytes overload - tx_hash = self._signer.write_contract( - token_address, - TRANSFER_WITH_AUTHORIZATION_BYTES_ABI, - "transferWithAuthorization", - payer, - evm_payload.authorization.to, - int(evm_payload.authorization.value), - int(evm_payload.authorization.valid_after), - int(evm_payload.authorization.valid_before), - hex_to_bytes(evm_payload.authorization.nonce), - inner_sig, - ) - + tx_hash = execute_transfer_with_authorization( + self._signer, + token_address, + parsed_authorization, + sig_data, + ) receipt = self._signer.wait_for_transaction_receipt(tx_hash) if receipt.status != TX_STATUS_SUCCESS: return SettleResponse( @@ -364,26 +386,6 @@ def settle( transaction="", ) - def _check_nonce_used(self, from_addr: str, nonce: str, token: str) -> bool: - """Check if EIP-3009 nonce has been used. - - Args: - from_addr: Authorizer address. - nonce: Nonce hex string. - token: Token contract address. - - Returns: - True if nonce has been used. - """ - result = self._signer.read_contract( - token, - AUTHORIZATION_STATE_ABI, - "authorizationState", - from_addr, - hex_to_bytes(nonce), - ) - return bool(result) - def _deploy_smart_wallet(self, sig_data: ERC6492SignatureData) -> None: """Deploy ERC-4337 smart wallet via ERC-6492 factory. diff --git a/python/x402/mechanisms/evm/exact/permit2_utils.py b/python/x402/mechanisms/evm/exact/permit2_utils.py new file mode 100644 index 0000000000..d5d10fddf3 --- /dev/null +++ b/python/x402/mechanisms/evm/exact/permit2_utils.py @@ -0,0 +1,740 @@ +"""Permit2 helpers for the exact EVM payment scheme.""" + +from __future__ import annotations + +import logging +import time +from typing import Any + +logger = logging.getLogger("x402.permit2") + +try: + from eth_utils import to_checksum_address +except ImportError as e: + raise ImportError( + "EVM mechanism requires ethereum packages. Install with: pip install x402[evm]" + ) from e + +from ....interfaces import FacilitatorContext # noqa: E402 +from ....schemas import ( # noqa: E402 + PaymentPayload, + PaymentRequirements, + SettleResponse, + VerifyResponse, +) +from ..constants import ( # noqa: E402 + BALANCE_OF_ABI, + ERC20_ALLOWANCE_ABI, + ERR_INSUFFICIENT_BALANCE, + ERR_NETWORK_MISMATCH, + ERR_PERMIT2_ALLOWANCE_REQUIRED, + ERR_PERMIT2_AMOUNT_MISMATCH, + ERR_PERMIT2_DEADLINE_EXPIRED, + ERR_PERMIT2_INVALID_SIGNATURE, + ERR_PERMIT2_INVALID_SPENDER, + ERR_PERMIT2_NOT_YET_VALID, + ERR_PERMIT2_RECIPIENT_MISMATCH, + ERR_PERMIT2_TOKEN_MISMATCH, + ERR_TRANSACTION_FAILED, + ERR_UNSUPPORTED_SCHEME, + PERMIT2_ADDRESS, + PERMIT2_WITNESS_TYPES, + SCHEME_EXACT, + TX_STATUS_SUCCESS, + X402_EXACT_PERMIT2_PROXY_ABI, + X402_EXACT_PERMIT2_PROXY_ADDRESS, + X402_EXACT_PERMIT2_PROXY_SETTLE_WITH_PERMIT_ABI, +) +from ..signer import ClientEvmSigner, FacilitatorEvmSigner # noqa: E402 +from ..types import ( # noqa: E402 + ExactPermit2Authorization, + ExactPermit2Payload, + ExactPermit2TokenPermissions, + ExactPermit2Witness, + TypedDataField, +) +from ..utils import ( # noqa: E402 + create_permit2_nonce, + get_evm_chain_id, + hex_to_bytes, + normalize_address, +) + + +def create_permit2_payload( + signer: ClientEvmSigner, + requirements: PaymentRequirements, +) -> dict[str, Any]: + """Create a signed Permit2 PermitWitnessTransferFrom payload. + + The spender is always x402ExactPermit2Proxy, which enforces that funds + can only be sent to the witness.to address (requirements.pay_to). + + Args: + signer: EVM signer for signing the Permit2 authorization. + requirements: Payment requirements from server. + + Returns: + Inner payload dict (permit2Authorization + signature). + """ + now = int(time.time()) + nonce = create_permit2_nonce() + + # Lower time bound - allow clock skew + valid_after = str(now - 600) + # Upper time bound - permit2 deadline + deadline = str(now + (requirements.max_timeout_seconds or 3600)) + + permit2_authorization = ExactPermit2Authorization( + from_address=signer.address, + permitted=ExactPermit2TokenPermissions( + token=normalize_address(requirements.asset), + amount=requirements.amount, + ), + spender=X402_EXACT_PERMIT2_PROXY_ADDRESS, + nonce=nonce, + deadline=deadline, + witness=ExactPermit2Witness( + to=normalize_address(requirements.pay_to), + valid_after=valid_after, + ), + ) + + signature = _sign_permit2_authorization(signer, permit2_authorization, requirements) + + payload = ExactPermit2Payload( + permit2_authorization=permit2_authorization, + signature=signature, + ) + return payload.to_dict() + + +def _sign_permit2_authorization( + signer: ClientEvmSigner, + permit2_authorization: ExactPermit2Authorization, + requirements: PaymentRequirements, +) -> str: + """Sign a Permit2 PermitWitnessTransferFrom using EIP-712. + + The Permit2 domain has NO version field — only name, chainId, verifyingContract. + We pass the domain as a raw dict to support signers whose protocol expects + TypedDataDomain (which requires version), using the dict fallback path in + EthAccountSigner.sign_typed_data(). + + Args: + signer: EVM signer. + permit2_authorization: The authorization to sign. + requirements: Payment requirements (used for chain ID). + + Returns: + Hex-encoded signature with 0x prefix. + """ + chain_id = get_evm_chain_id(str(requirements.network)) + domain_dict, typed_fields, primary_type, message = _build_permit2_typed_data( + permit2_authorization, chain_id + ) + + sig_bytes = signer.sign_typed_data( + domain_dict, # type: ignore[arg-type] + typed_fields, + primary_type, + message, + ) + return "0x" + sig_bytes.hex() + + +def verify_permit2( + signer: FacilitatorEvmSigner, + payload: PaymentPayload, + requirements: PaymentRequirements, + context: FacilitatorContext | None = None, +) -> VerifyResponse: + """Verify a Permit2 payment payload. + + Verification cascade (cheap to expensive): + 1. Scheme check + 2. Network check + 3. Spender check (must be x402ExactPermit2Proxy) + 4. Recipient check (witness.to must match requirements.pay_to) + 5. Deadline check (must not be expired) + 6. validAfter check (must not be in the future) + 7. Amount check + 8. Token check + 9. Signature verification + 10. Allowance check (with extension fallbacks) + 11. Balance check + + Args: + signer: Facilitator EVM signer for on-chain reads. + payload: Payment payload from client. + requirements: Payment requirements. + context: Optional facilitator context for extension lookup. + + Returns: + VerifyResponse with is_valid and payer. + """ + permit2_payload = ExactPermit2Payload.from_dict(payload.payload) + payer = permit2_payload.permit2_authorization.from_address + + # 1. Scheme check + if payload.accepted.scheme != SCHEME_EXACT: + return VerifyResponse(is_valid=False, invalid_reason=ERR_UNSUPPORTED_SCHEME, payer=payer) + + # 2. Network check + if payload.accepted.network != requirements.network: + return VerifyResponse(is_valid=False, invalid_reason=ERR_NETWORK_MISMATCH, payer=payer) + + chain_id = get_evm_chain_id(str(requirements.network)) + token_address = normalize_address(requirements.asset) + + # 3. Spender check + try: + spender_norm = normalize_address(permit2_payload.permit2_authorization.spender) + proxy_norm = normalize_address(X402_EXACT_PERMIT2_PROXY_ADDRESS) + except Exception: + return VerifyResponse( + is_valid=False, invalid_reason=ERR_PERMIT2_INVALID_SPENDER, payer=payer + ) + + if spender_norm != proxy_norm: + return VerifyResponse( + is_valid=False, invalid_reason=ERR_PERMIT2_INVALID_SPENDER, payer=payer + ) + + # 4. Recipient check + try: + witness_to = normalize_address(permit2_payload.permit2_authorization.witness.to) + pay_to = normalize_address(requirements.pay_to) + except Exception: + return VerifyResponse( + is_valid=False, invalid_reason=ERR_PERMIT2_RECIPIENT_MISMATCH, payer=payer + ) + + if witness_to != pay_to: + return VerifyResponse( + is_valid=False, invalid_reason=ERR_PERMIT2_RECIPIENT_MISMATCH, payer=payer + ) + + now = int(time.time()) + + # 5-7. Parse numeric fields from untrusted input before comparison + try: + deadline_val = int(permit2_payload.permit2_authorization.deadline) + valid_after_val = int(permit2_payload.permit2_authorization.witness.valid_after) + amount_val = int(permit2_payload.permit2_authorization.permitted.amount) + except (ValueError, TypeError): + return VerifyResponse( + is_valid=False, invalid_reason="invalid_permit2_payload_format", payer=payer + ) + + # 5. Deadline check (6 second buffer) + if deadline_val < now + 6: + return VerifyResponse( + is_valid=False, invalid_reason=ERR_PERMIT2_DEADLINE_EXPIRED, payer=payer + ) + + # 6. validAfter check + if valid_after_val > now: + return VerifyResponse(is_valid=False, invalid_reason=ERR_PERMIT2_NOT_YET_VALID, payer=payer) + + # 7. Amount check + if amount_val != int(requirements.amount): + return VerifyResponse( + is_valid=False, invalid_reason=ERR_PERMIT2_AMOUNT_MISMATCH, payer=payer + ) + + # 8. Token check + try: + permitted_token = normalize_address(permit2_payload.permit2_authorization.permitted.token) + except Exception: + return VerifyResponse( + is_valid=False, invalid_reason=ERR_PERMIT2_TOKEN_MISMATCH, payer=payer + ) + + if permitted_token != token_address: + return VerifyResponse( + is_valid=False, invalid_reason=ERR_PERMIT2_TOKEN_MISMATCH, payer=payer + ) + + # 9. Signature verification + if not permit2_payload.signature: + logger.warning("Permit2 verify: missing signature") + return VerifyResponse( + is_valid=False, invalid_reason=ERR_PERMIT2_INVALID_SIGNATURE, payer=payer + ) + + try: + sig_bytes = hex_to_bytes(permit2_payload.signature) + logger.info( + "Permit2 verify: checking signature for payer=%s chain_id=%s sig_len=%d", + payer, + chain_id, + len(sig_bytes), + ) + is_valid_sig = _verify_permit2_signature( + signer, + payer, + permit2_payload.permit2_authorization, + chain_id, + sig_bytes, + ) + if not is_valid_sig: + logger.warning("Permit2 verify: signature verification returned False") + return VerifyResponse( + is_valid=False, invalid_reason=ERR_PERMIT2_INVALID_SIGNATURE, payer=payer + ) + logger.info("Permit2 verify: signature OK") + except Exception as e: + logger.warning("Permit2 verify: signature exception: %s", e, exc_info=True) + return VerifyResponse( + is_valid=False, invalid_reason=ERR_PERMIT2_INVALID_SIGNATURE, payer=payer + ) + + # 10. Allowance check — with extension fallbacks + allowance_result = _verify_permit2_allowance( + signer, payload, requirements, payer, token_address, context + ) + if allowance_result is not None: + logger.warning( + "Permit2 verify: allowance check failed: %s", allowance_result.invalid_reason + ) + return allowance_result + logger.info("Permit2 verify: allowance OK") + + # 11. Balance check (fail closed — RPC failure rejects rather than allowing underfunded payments) + try: + balance = signer.read_contract(token_address, BALANCE_OF_ABI, "balanceOf", payer) + if int(balance) < int(requirements.amount): + return VerifyResponse( + is_valid=False, invalid_reason=ERR_INSUFFICIENT_BALANCE, payer=payer + ) + except Exception: + logger.warning("Permit2 verify: balance check failed for payer=%s", payer, exc_info=True) + return VerifyResponse(is_valid=False, invalid_reason="balance_check_failed", payer=payer) + + return VerifyResponse(is_valid=True, payer=payer) + + +def _verify_permit2_allowance( + signer: FacilitatorEvmSigner, + payload: PaymentPayload, + requirements: PaymentRequirements, + payer: str, + token_address: str, + context: FacilitatorContext | None, +) -> VerifyResponse | None: + """Check Permit2 allowance with extension fallbacks. + + Returns a VerifyResponse if verification should stop (failure), + or None to continue with remaining checks. + + Fallback order (matching TS/Go): + 1. On-chain allowance sufficient -> None (continue) + 2. EIP-2612 gas sponsoring extension valid -> None (continue) + 3. ERC-20 approval gas sponsoring extension valid -> None (continue) + 4. Fail with permit2_allowance_required + """ + from ....extensions.eip2612_gas_sponsoring import ( + extract_eip2612_gas_sponsoring_info, + validate_eip2612_permit_for_payment, + ) + from ....extensions.erc20_approval_gas_sponsoring import ( + ERC20_APPROVAL_GAS_SPONSORING_KEY, + Erc20ApprovalFacilitatorExtension, + extract_erc20_approval_gas_sponsoring_info, + validate_erc20_approval_for_payment, + ) + + needs_extension = True + try: + allowance = signer.read_contract( + token_address, + ERC20_ALLOWANCE_ABI, + "allowance", + payer, + PERMIT2_ADDRESS, + ) + if int(allowance) >= int(requirements.amount): + needs_extension = False + except Exception: + logger.warning("Permit2 verify: allowance check failed for payer=%s", payer, exc_info=True) + + if not needs_extension: + return None + + # Try EIP-2612 gas sponsoring extension first + eip2612_info = extract_eip2612_gas_sponsoring_info(payload) + if eip2612_info is not None: + reason = validate_eip2612_permit_for_payment(eip2612_info, payer, token_address) + if reason: + return VerifyResponse(is_valid=False, invalid_reason=reason, payer=payer) + return None # Valid EIP-2612 extension, allowance will be set atomically + + # Try ERC-20 approval gas sponsoring extension + erc20_info = extract_erc20_approval_gas_sponsoring_info(payload) + if erc20_info is not None and context is not None: + ext = context.get_extension(ERC20_APPROVAL_GAS_SPONSORING_KEY) + if isinstance(ext, Erc20ApprovalFacilitatorExtension): + extension_signer = ext.resolve_signer(str(payload.accepted.network)) + if extension_signer is not None: + reason, _msg = validate_erc20_approval_for_payment(erc20_info, payer, token_address) + if reason: + return VerifyResponse(is_valid=False, invalid_reason=reason, payer=payer) + return None # Valid ERC-20 approval extension + + return VerifyResponse( + is_valid=False, invalid_reason=ERR_PERMIT2_ALLOWANCE_REQUIRED, payer=payer + ) + + +def settle_permit2( + signer: FacilitatorEvmSigner, + payload: PaymentPayload, + requirements: PaymentRequirements, + context: FacilitatorContext | None = None, +) -> SettleResponse: + """Settle a Permit2 payment on-chain. + + Routes to the appropriate settlement path: + 1. EIP-2612 extension -> settleWithPermit (atomic single tx) + 2. ERC-20 approval extension -> send_transactions (approval + settle) + 3. Standard -> settle directly (allowance already on-chain) + + Args: + signer: Facilitator EVM signer for on-chain writes. + payload: Verified payment payload. + requirements: Payment requirements. + context: Optional facilitator context for extension lookup. + + Returns: + SettleResponse with success, transaction, and payer. + """ + from ....extensions.eip2612_gas_sponsoring import extract_eip2612_gas_sponsoring_info + from ....extensions.erc20_approval_gas_sponsoring import ( + ERC20_APPROVAL_GAS_SPONSORING_KEY, + Erc20ApprovalFacilitatorExtension, + extract_erc20_approval_gas_sponsoring_info, + ) + + permit2_payload = ExactPermit2Payload.from_dict(payload.payload) + payer = permit2_payload.permit2_authorization.from_address + network = str(requirements.network) + + # Re-verify before settling + verify_result = verify_permit2(signer, payload, requirements, context) + if not verify_result.is_valid: + return SettleResponse( + success=False, + error_reason=verify_result.invalid_reason, + network=network, + payer=payer, + transaction="", + ) + + # Branch: EIP-2612 gas sponsoring (atomic settleWithPermit) + eip2612_info = extract_eip2612_gas_sponsoring_info(payload) + if eip2612_info is not None: + return _settle_permit2_with_eip2612(signer, payload, permit2_payload, eip2612_info) + + # Branch: ERC-20 approval gas sponsoring (broadcast approval + settle) + erc20_info = extract_erc20_approval_gas_sponsoring_info(payload) + if erc20_info is not None and context is not None: + ext = context.get_extension(ERC20_APPROVAL_GAS_SPONSORING_KEY) + if isinstance(ext, Erc20ApprovalFacilitatorExtension): + extension_signer = ext.resolve_signer(str(payload.accepted.network)) + if extension_signer is not None: + return _settle_permit2_with_erc20_approval( + extension_signer, payload, permit2_payload, erc20_info + ) + + # Branch: standard settle (allowance already on-chain) + return _settle_permit2_direct(signer, payload, permit2_payload) + + +def _build_permit2_settle_args( + permit2_payload: ExactPermit2Payload, +) -> tuple: + """Build common settle call arguments from a Permit2 payload. + + Returns (permit_tuple, owner_addr, witness_tuple, sig_bytes). + """ + sig_bytes = hex_to_bytes(permit2_payload.signature or "") + permit_tuple = ( + ( + to_checksum_address(permit2_payload.permit2_authorization.permitted.token), + int(permit2_payload.permit2_authorization.permitted.amount), + ), + int(permit2_payload.permit2_authorization.nonce), + int(permit2_payload.permit2_authorization.deadline), + ) + owner_addr = to_checksum_address(permit2_payload.permit2_authorization.from_address) + witness_tuple = ( + to_checksum_address(permit2_payload.permit2_authorization.witness.to), + int(permit2_payload.permit2_authorization.witness.valid_after), + ) + return permit_tuple, owner_addr, witness_tuple, sig_bytes + + +def _settle_permit2_direct( + signer: FacilitatorEvmSigner, + payload: PaymentPayload, + permit2_payload: ExactPermit2Payload, +) -> SettleResponse: + """Standard Permit2 settle — allowance is already on-chain.""" + payer = permit2_payload.permit2_authorization.from_address + network = str(payload.accepted.network) + + try: + permit_tuple, owner_addr, witness_tuple, sig_bytes = _build_permit2_settle_args( + permit2_payload + ) + + tx_hash = signer.write_contract( + X402_EXACT_PERMIT2_PROXY_ADDRESS, + X402_EXACT_PERMIT2_PROXY_ABI, + "settle", + permit_tuple, + owner_addr, + witness_tuple, + sig_bytes, + ) + + receipt = signer.wait_for_transaction_receipt(tx_hash) + if receipt.status != TX_STATUS_SUCCESS: + return SettleResponse( + success=False, + error_reason=ERR_TRANSACTION_FAILED, + transaction=tx_hash, + network=network, + payer=payer, + ) + + return SettleResponse( + success=True, + transaction=tx_hash, + network=network, + payer=payer, + ) + + except Exception as e: + return _map_settle_error(e, network, payer) + + +def _settle_permit2_with_eip2612( + signer: FacilitatorEvmSigner, + payload: PaymentPayload, + permit2_payload: ExactPermit2Payload, + eip2612_info: Any, +) -> SettleResponse: + """Settle via settleWithPermit — includes the EIP-2612 permit atomically.""" + payer = permit2_payload.permit2_authorization.from_address + network = str(payload.accepted.network) + + try: + permit_tuple, owner_addr, witness_tuple, sig_bytes = _build_permit2_settle_args( + permit2_payload + ) + + sig_hex = eip2612_info.signature + sig_raw = hex_to_bytes(sig_hex) + if len(sig_raw) != 65: + return _map_settle_error( + ValueError("EIP-2612 signature must be 65 bytes"), network, payer + ) + r = sig_raw[:32] + s = sig_raw[32:64] + v = sig_raw[64] + + permit2612_tuple = ( + int(eip2612_info.amount), + int(eip2612_info.deadline), + r, + s, + v, + ) + + tx_hash = signer.write_contract( + X402_EXACT_PERMIT2_PROXY_ADDRESS, + X402_EXACT_PERMIT2_PROXY_SETTLE_WITH_PERMIT_ABI, + "settleWithPermit", + permit2612_tuple, + permit_tuple, + owner_addr, + witness_tuple, + sig_bytes, + ) + + receipt = signer.wait_for_transaction_receipt(tx_hash) + if receipt.status != TX_STATUS_SUCCESS: + return SettleResponse( + success=False, + error_reason=ERR_TRANSACTION_FAILED, + transaction=tx_hash, + network=network, + payer=payer, + ) + + return SettleResponse( + success=True, + transaction=tx_hash, + network=network, + payer=payer, + ) + + except Exception as e: + return _map_settle_error(e, network, payer) + + +def _settle_permit2_with_erc20_approval( + extension_signer: Any, + payload: PaymentPayload, + permit2_payload: ExactPermit2Payload, + erc20_info: Any, +) -> SettleResponse: + """Settle via extension signer's send_transactions (approval + settle).""" + payer = permit2_payload.permit2_authorization.from_address + network = str(payload.accepted.network) + + try: + permit_tuple, owner_addr, witness_tuple, sig_bytes = _build_permit2_settle_args( + permit2_payload + ) + + from ....extensions.erc20_approval_gas_sponsoring.types import WriteContractCall + + tx_hashes = extension_signer.send_transactions( + [ + erc20_info.signed_transaction, + WriteContractCall( + address=X402_EXACT_PERMIT2_PROXY_ADDRESS, + abi=X402_EXACT_PERMIT2_PROXY_ABI, + function="settle", + args=[permit_tuple, owner_addr, witness_tuple, sig_bytes], + ), + ] + ) + + settle_tx_hash = tx_hashes[-1] if tx_hashes else "" + receipt = extension_signer.wait_for_transaction_receipt(settle_tx_hash) + if receipt.status != TX_STATUS_SUCCESS: + return SettleResponse( + success=False, + error_reason=ERR_TRANSACTION_FAILED, + transaction=settle_tx_hash, + network=network, + payer=payer, + ) + + return SettleResponse( + success=True, + transaction=settle_tx_hash, + network=network, + payer=payer, + ) + + except Exception as e: + return _map_settle_error(e, network, payer) + + +def _map_settle_error(error: Exception, network: str, payer: str) -> SettleResponse: + """Map contract revert errors to structured SettleResponse.""" + error_msg = str(error) + error_reason = ERR_TRANSACTION_FAILED + if "Permit2612AmountMismatch" in error_msg: + error_reason = "permit2_2612_amount_mismatch" + elif "InvalidAmount" in error_msg: + error_reason = "invalid_permit2_amount" + elif "InvalidDestination" in error_msg: + error_reason = "invalid_permit2_destination" + elif "InvalidOwner" in error_msg: + error_reason = "invalid_permit2_owner" + elif "PaymentTooEarly" in error_msg: + error_reason = "permit2_payment_too_early" + elif "InvalidSignature" in error_msg or "SignatureExpired" in error_msg: + error_reason = ERR_PERMIT2_INVALID_SIGNATURE + elif "InvalidNonce" in error_msg: + error_reason = "permit2_invalid_nonce" + elif "erc20_approval_tx_failed" in error_msg: + error_reason = "erc20_approval_tx_failed" + + return SettleResponse( + success=False, + error_reason=error_reason, + error_message=error_msg[:500], + network=network, + payer=payer, + transaction="", + ) + + +def _build_permit2_typed_data( + permit2_authorization: ExactPermit2Authorization, + chain_id: int, +) -> tuple[dict[str, Any], dict[str, list[TypedDataField]], str, dict[str, Any]]: + """Build EIP-712 typed data components for Permit2 signature verification. + + Returns (domain_dict, types, primary_type, message) suitable for both + client signing and facilitator verification. + """ + domain_dict: dict[str, Any] = { + "name": "Permit2", + "chainId": chain_id, + "verifyingContract": PERMIT2_ADDRESS, + } + + message = { + "permitted": { + "token": permit2_authorization.permitted.token, + "amount": int(permit2_authorization.permitted.amount), + }, + "spender": permit2_authorization.spender, + "nonce": int(permit2_authorization.nonce), + "deadline": int(permit2_authorization.deadline), + "witness": { + "to": permit2_authorization.witness.to, + "validAfter": int(permit2_authorization.witness.valid_after), + }, + } + + typed_fields: dict[str, list[TypedDataField]] = { + type_name: [TypedDataField(name=f["name"], type=f["type"]) for f in fields] + for type_name, fields in PERMIT2_WITNESS_TYPES.items() + } + + return domain_dict, typed_fields, "PermitWitnessTransferFrom", message + + +def _verify_permit2_signature( + signer: FacilitatorEvmSigner, + payer: str, + permit2_authorization: ExactPermit2Authorization, + chain_id: int, + signature: bytes, +) -> bool: + """Verify a Permit2 EIP-712 signature. + + Delegates to signer.verify_typed_data which supports EOA, EIP-1271, + and ERC-6492 verification (matching TS/Go universal signature verification). + + Args: + signer: Facilitator signer with verify_typed_data capability. + payer: Expected signer address. + permit2_authorization: The authorization that was signed. + chain_id: Chain ID. + signature: Signature bytes. + + Returns: + True if signature is valid. + """ + domain_dict, typed_fields, primary_type, message = _build_permit2_typed_data( + permit2_authorization, chain_id + ) + + return signer.verify_typed_data( + payer, + domain_dict, # type: ignore[arg-type] + typed_fields, + primary_type, + message, + signature, + ) diff --git a/python/x402/mechanisms/evm/exact/register.py b/python/x402/mechanisms/evm/exact/register.py index 00656224f8..2d871980ee 100644 --- a/python/x402/mechanisms/evm/exact/register.py +++ b/python/x402/mechanisms/evm/exact/register.py @@ -105,6 +105,7 @@ def register_exact_evm_facilitator( signer: "FacilitatorEvmSigner", networks: str | list[str], deploy_erc4337_with_eip6492: bool = False, + simulate_in_settle: bool = False, ) -> FacilitatorT: """Register EVM exact payment schemes to x402Facilitator. @@ -117,6 +118,7 @@ def register_exact_evm_facilitator( signer: EVM signer for verification/settlement. networks: Network(s) to register. deploy_erc4337_with_eip6492: Enable smart wallet deployment. + simulate_in_settle: Rerun verify-time simulation inside settle. Returns: Facilitator for chaining. @@ -126,7 +128,10 @@ def register_exact_evm_facilitator( from .v1.facilitator import ExactEvmSchemeV1 as ExactEvmFacilitatorSchemeV1 from .v1.facilitator import ExactEvmSchemeV1Config - config = ExactEvmSchemeConfig(deploy_erc4337_with_eip6492=deploy_erc4337_with_eip6492) + config = ExactEvmSchemeConfig( + deploy_erc4337_with_eip6492=deploy_erc4337_with_eip6492, + simulate_in_settle=simulate_in_settle, + ) scheme = ExactEvmFacilitatorScheme(signer, config) if isinstance(networks, str): @@ -134,7 +139,10 @@ def register_exact_evm_facilitator( facilitator.register(networks, scheme) # Register V1 - v1_config = ExactEvmSchemeV1Config(deploy_erc4337_with_eip6492=deploy_erc4337_with_eip6492) + v1_config = ExactEvmSchemeV1Config( + deploy_erc4337_with_eip6492=deploy_erc4337_with_eip6492, + simulate_in_settle=simulate_in_settle, + ) v1_scheme = ExactEvmFacilitatorSchemeV1(signer, v1_config) facilitator.register_v1(V1_NETWORKS, v1_scheme) diff --git a/python/x402/mechanisms/evm/exact/server.py b/python/x402/mechanisms/evm/exact/server.py index 72d6743787..3534c1f348 100644 --- a/python/x402/mechanisms/evm/exact/server.py +++ b/python/x402/mechanisms/evm/exact/server.py @@ -105,6 +105,7 @@ def enhance_payment_requirements( - Fills in default asset if not specified - Adds EIP-712 domain parameters (name, version) to extra + - Adds assetTransferMethod to extra when present on the asset - Converts decimal amounts to smallest unit Args: @@ -119,42 +120,81 @@ def enhance_payment_requirements( # Default asset if not requirements.asset: - requirements.asset = config["default_asset"]["address"] - - asset_info = get_asset_info(str(requirements.network), requirements.asset) + default = config.get("default_asset") + if not default or not default.get("address"): + raise ValueError( + f"No default stablecoin configured for network {requirements.network}; " + "use register_money_parser or specify an explicit asset address" + ) + requirements.asset = default["address"] + + try: + asset_info = get_asset_info(str(requirements.network), requirements.asset) + except ValueError: + asset_info = None # Ensure amount is in smallest unit if "." in requirements.amount: + if asset_info is None: + raise ValueError( + f"Token {requirements.asset} is not a registered asset for network " + f"{requirements.network}; provide amount in atomic units" + ) requirements.amount = str(parse_amount(requirements.amount, asset_info["decimals"])) # Add EIP-712 domain params if requirements.extra is None: requirements.extra = {} - if "name" not in requirements.extra: - requirements.extra["name"] = asset_info["name"] - if "version" not in requirements.extra: - requirements.extra["version"] = asset_info["version"] + if asset_info is not None: + atm = asset_info.get("asset_transfer_method") + include_eip712_domain = not atm or asset_info.get("supports_eip2612", False) + + if include_eip712_domain: + if "name" not in requirements.extra: + requirements.extra["name"] = asset_info["name"] + if "version" not in requirements.extra: + requirements.extra["version"] = asset_info["version"] + if "assetTransferMethod" not in requirements.extra and atm: + requirements.extra["assetTransferMethod"] = atm return requirements def _default_money_conversion(self, amount: float, network: str) -> AssetAmount: - """Convert decimal amount to USDC AssetAmount. + """Convert decimal amount to network's default stablecoin AssetAmount. Args: amount: Decimal amount (e.g., 1.50). network: Network identifier. Returns: - AssetAmount in USDC. + AssetAmount for the network's default stablecoin. + + Raises: + ValueError: If no default stablecoin is configured for the network. """ config = get_network_config(network) - asset = config["default_asset"] + asset = config.get("default_asset") + + if not asset or not asset.get("address"): + raise ValueError( + f"No default stablecoin configured for network {network}; " + "use register_money_parser or specify an explicit AssetAmount" + ) - # Convert to smallest unit (6 decimals for USDC) token_amount = int(amount * (10 ** asset["decimals"])) + atm = asset.get("asset_transfer_method") + include_eip712_domain = not atm or asset.get("supports_eip2612", False) + + extra: dict = {} + if include_eip712_domain: + extra["name"] = asset["name"] + extra["version"] = asset["version"] + if atm: + extra["assetTransferMethod"] = atm + return AssetAmount( amount=str(token_amount), asset=asset["address"], - extra={"name": asset["name"], "version": asset["version"]}, + extra=extra, ) diff --git a/python/x402/mechanisms/evm/exact/v1/facilitator.py b/python/x402/mechanisms/evm/exact/v1/facilitator.py index 538d68c068..412bece471 100644 --- a/python/x402/mechanisms/evm/exact/v1/facilitator.py +++ b/python/x402/mechanisms/evm/exact/v1/facilitator.py @@ -8,10 +8,9 @@ from .....schemas import Network, SettleResponse, VerifyResponse from .....schemas.v1 import PaymentPayloadV1, PaymentRequirementsV1 from ...constants import ( + ERR_AUTHORIZATION_VALUE_MISMATCH, ERR_FAILED_TO_GET_NETWORK_CONFIG, ERR_FAILED_TO_VERIFY_SIGNATURE, - ERR_INSUFFICIENT_AMOUNT, - ERR_INSUFFICIENT_BALANCE, ERR_INVALID_SIGNATURE, ERR_MISSING_EIP712_DOMAIN, ERR_NETWORK_MISMATCH, @@ -23,17 +22,20 @@ ERR_VALID_AFTER_FUTURE, ERR_VALID_BEFORE_EXPIRED, SCHEME_EXACT, - TRANSFER_WITH_AUTHORIZATION_BYTES_ABI, - TRANSFER_WITH_AUTHORIZATION_VRS_ABI, TX_STATUS_SUCCESS, ) -from ...eip712 import hash_eip3009_authorization from ...erc6492 import has_deployment_info, parse_erc6492_signature from ...signer import FacilitatorEvmSigner from ...types import ERC6492SignatureData, ExactEIP3009Payload from ...utils import bytes_to_hex, hex_to_bytes, normalize_address from ...v1.utils import get_evm_chain_id -from ...verify import verify_universal_signature +from ..eip3009_utils import ( + classify_eip3009_signature, + diagnose_eip3009_simulation_failure, + execute_transfer_with_authorization, + parse_eip3009_authorization, + simulate_eip3009_transfer, +) @dataclass @@ -43,6 +45,9 @@ class ExactEvmSchemeV1Config: deploy_erc4337_with_eip6492: bool = False """Enable automatic smart wallet deployment via EIP-6492.""" + simulate_in_settle: bool = False + """Rerun transfer simulation during settle.""" + class ExactEvmSchemeV1: """EVM facilitator implementation for Exact payment scheme (V1). @@ -102,12 +107,20 @@ def verify( payload: PaymentPayloadV1, requirements: PaymentRequirementsV1, context=None, + ) -> VerifyResponse: + return self._verify(payload, requirements, simulate=True) + + def _verify( + self, + payload: PaymentPayloadV1, + requirements: PaymentRequirementsV1, + simulate: bool, ) -> VerifyResponse: """Verify EIP-3009 payment payload (V1). V1 validation differences: - scheme/network at top level of payload - - Uses maxAmountRequired for amount check + - Uses maxAmountRequired for exact amount validation - extra is JSON-encoded Args: @@ -161,9 +174,11 @@ def verify( ) # V1: Use maxAmountRequired - if int(evm_payload.authorization.value) < int(requirements.max_amount_required): + if int(evm_payload.authorization.value) != int(requirements.max_amount_required): return VerifyResponse( - is_valid=False, invalid_reason=ERR_INSUFFICIENT_AMOUNT, payer=payer + is_valid=False, + invalid_reason=ERR_AUTHORIZATION_VALUE_MISMATCH, + payer=payer, ) # V1: Check validBefore is in future (6 second buffer) @@ -179,34 +194,30 @@ def verify( is_valid=False, invalid_reason=ERR_VALID_AFTER_FUTURE, payer=payer ) - # Check balance - try: - balance = self._signer.get_balance(payer, token_address) - if balance < int(requirements.max_amount_required): - return VerifyResponse( - is_valid=False, invalid_reason=ERR_INSUFFICIENT_BALANCE, payer=payer - ) - except Exception: - pass # Continue if balance check fails - # Verify signature if not evm_payload.signature: return VerifyResponse(is_valid=False, invalid_reason=ERR_INVALID_SIGNATURE, payer=payer) - signature = hex_to_bytes(evm_payload.signature) - hash_bytes = hash_eip3009_authorization( - evm_payload.authorization, - chain_id, - token_address, - extra["name"], - extra["version"], - ) - try: - valid, _ = verify_universal_signature( - self._signer, payer, hash_bytes, signature, allow_undeployed=True + signature = hex_to_bytes(evm_payload.signature) + classification = classify_eip3009_signature( + self._signer, + evm_payload.authorization, + signature, + chain_id, + token_address, + extra["name"], + extra["version"], ) - if not valid: + if not classification.valid and classification.is_undeployed: + if not has_deployment_info(classification.sig_data): + return VerifyResponse( + is_valid=False, + invalid_reason=ERR_UNDEPLOYED_SMART_WALLET, + payer=payer, + ) + + if not classification.valid and not classification.is_smart_wallet: return VerifyResponse( is_valid=False, invalid_reason=ERR_INVALID_SIGNATURE, payer=payer ) @@ -218,6 +229,38 @@ def verify( payer=payer, ) + if not simulate: + return VerifyResponse(is_valid=True, payer=payer) + + try: + parsed_authorization = parse_eip3009_authorization(evm_payload.authorization) + except Exception as e: + return VerifyResponse( + is_valid=False, + invalid_reason=ERR_FAILED_TO_VERIFY_SIGNATURE, + invalid_message=str(e), + payer=payer, + ) + + if not simulate_eip3009_transfer( + self._signer, + token_address, + parsed_authorization, + classification.sig_data, + ): + return VerifyResponse( + is_valid=False, + invalid_reason=diagnose_eip3009_simulation_failure( + self._signer, + token_address, + evm_payload.authorization, + int(requirements.max_amount_required), + extra["name"], + extra["version"], + ), + payer=payer, + ) + return VerifyResponse(is_valid=True, payer=payer) def settle( @@ -238,7 +281,11 @@ def settle( SettleResponse with success, transaction, and payer. """ # First verify - verify_result = self.verify(payload, requirements, context) + verify_result = self._verify( + payload, + requirements, + simulate=self._config.simulate_in_settle, + ) if not verify_result.is_valid: return SettleResponse( success=False, @@ -253,8 +300,19 @@ def settle( network = requirements.network token_address = normalize_address(requirements.asset) - signature = hex_to_bytes(evm_payload.signature) - sig_data = parse_erc6492_signature(signature) + try: + signature = hex_to_bytes(evm_payload.signature or "") + sig_data = parse_erc6492_signature(signature) + parsed_authorization = parse_eip3009_authorization(evm_payload.authorization) + except Exception as e: + return SettleResponse( + success=False, + error_reason=ERR_TRANSACTION_FAILED, + error_message=str(e), + network=network, + payer=payer, + transaction="", + ) # Deploy smart wallet if needed if has_deployment_info(sig_data): @@ -281,40 +339,13 @@ def settle( transaction="", ) - inner_sig = sig_data.inner_signature - is_ecdsa = len(inner_sig) == 65 - try: - if is_ecdsa: - r, s, v = inner_sig[:32], inner_sig[32:64], inner_sig[64] - tx_hash = self._signer.write_contract( - token_address, - TRANSFER_WITH_AUTHORIZATION_VRS_ABI, - "transferWithAuthorization", - payer, - evm_payload.authorization.to, - int(evm_payload.authorization.value), - int(evm_payload.authorization.valid_after), - int(evm_payload.authorization.valid_before), - hex_to_bytes(evm_payload.authorization.nonce), - v, - r, - s, - ) - else: - tx_hash = self._signer.write_contract( - token_address, - TRANSFER_WITH_AUTHORIZATION_BYTES_ABI, - "transferWithAuthorization", - payer, - evm_payload.authorization.to, - int(evm_payload.authorization.value), - int(evm_payload.authorization.valid_after), - int(evm_payload.authorization.valid_before), - hex_to_bytes(evm_payload.authorization.nonce), - inner_sig, - ) - + tx_hash = execute_transfer_with_authorization( + self._signer, + token_address, + parsed_authorization, + sig_data, + ) receipt = self._signer.wait_for_transaction_receipt(tx_hash) if receipt.status != TX_STATUS_SUCCESS: return SettleResponse( diff --git a/python/x402/mechanisms/evm/multicall.py b/python/x402/mechanisms/evm/multicall.py new file mode 100644 index 0000000000..8a99689286 --- /dev/null +++ b/python/x402/mechanisms/evm/multicall.py @@ -0,0 +1,172 @@ +"""Helpers for batching `eth_call` requests through Multicall3.""" + +from __future__ import annotations + +from dataclasses import dataclass, field +from typing import Any + +try: + from eth_abi import decode, encode + from eth_utils import keccak +except ImportError as e: + raise ImportError( + "EVM mechanism requires ethereum packages. Install with: pip install x402[evm]" + ) from e + +from .constants import MULTICALL3_ADDRESS, MULTICALL3_TRY_AGGREGATE_ABI +from .signer import FacilitatorEvmSigner + + +@dataclass +class MulticallCall: + """One call executed through Multicall3.""" + + address: str + abi: list[dict[str, Any]] | None = None + function_name: str = "" + args: tuple[Any, ...] = field(default_factory=tuple) + call_data: bytes = b"" + + +@dataclass +class MulticallResult: + """Decoded result for a single multicall entry.""" + + success: bool + result: Any = None + error: Exception | None = None + + +def encode_contract_call( + abi: list[dict[str, Any]], + function_name: str, + *args: Any, +) -> bytes: + """Encode calldata for a contract function.""" + function = _get_function_abi(abi, function_name) + input_types = [_canonical_type(item) for item in function.get("inputs", [])] + signature = f"{function_name}({','.join(input_types)})" + selector = keccak(text=signature)[:4] + return selector + encode(input_types, list(args)) + + +def multicall( + signer: FacilitatorEvmSigner, + calls: list[MulticallCall], +) -> list[MulticallResult]: + """Batch calls through Multicall3 and decode the results.""" + if not calls: + return [] + + aggregate_calls = [] + for call in calls: + call_data = call.call_data + if not call_data: + if not call.abi or not call.function_name: + raise ValueError("typed multicall entries require ABI and function name") + call_data = encode_contract_call(call.abi, call.function_name, *call.args) + aggregate_calls.append((call.address, call_data)) + + raw_results = signer.read_contract( + MULTICALL3_ADDRESS, + MULTICALL3_TRY_AGGREGATE_ABI, + "tryAggregate", + False, + aggregate_calls, + ) + normalized = _normalize_results(raw_results) + + if len(normalized) != len(calls): + raise ValueError( + f"multicall result length mismatch: got {len(normalized)}, want {len(calls)}" + ) + + results: list[MulticallResult] = [] + for raw_result, call in zip(normalized, calls, strict=True): + success, return_data = raw_result + if not success: + results.append( + MulticallResult( + success=False, + error=RuntimeError("multicall: call reverted"), + ) + ) + continue + + if call.call_data: + results.append(MulticallResult(success=True)) + continue + + try: + decoded = _decode_contract_result(call.abi or [], call.function_name, return_data) + except Exception as exc: + results.append(MulticallResult(success=False, error=exc)) + continue + + results.append(MulticallResult(success=True, result=decoded)) + + return results + + +def _decode_contract_result( + abi: list[dict[str, Any]], + function_name: str, + return_data: bytes, +) -> Any: + function = _get_function_abi(abi, function_name) + output_types = [_canonical_type(item) for item in function.get("outputs", [])] + if not output_types: + return None + + decoded = decode(output_types, return_data) + if len(decoded) == 1: + return decoded[0] + return list(decoded) + + +def _get_function_abi(abi: list[dict[str, Any]], function_name: str) -> dict[str, Any]: + for entry in abi: + if entry.get("type") == "function" and entry.get("name") == function_name: + return entry + raise ValueError(f"Function {function_name} not found in ABI") + + +def _canonical_type(abi_item: dict[str, Any]) -> str: + item_type = abi_item["type"] + if not item_type.startswith("tuple"): + return item_type + + suffix = item_type[len("tuple") :] + components = ",".join(_canonical_type(component) for component in abi_item["components"]) + return f"({components}){suffix}" + + +def _normalize_results(raw_results: Any) -> list[tuple[bool, bytes]]: + if not isinstance(raw_results, list | tuple): + raise ValueError(f"multicall returned {type(raw_results)!r}, want sequence") + + normalized: list[tuple[bool, bytes]] = [] + for index, entry in enumerate(raw_results): + if isinstance(entry, dict): + success = bool(entry["success"]) + return_data = entry["returnData"] + else: + if hasattr(entry, "success") and hasattr(entry, "returnData"): + success = bool(entry.success) + return_data = entry.returnData + elif isinstance(entry, list | tuple) and len(entry) == 2: + success = bool(entry[0]) + return_data = entry[1] + else: + raise ValueError(f"multicall entry {index} has unexpected type {type(entry)!r}") + + if isinstance(return_data, str): + return_data = bytes.fromhex(return_data.removeprefix("0x")) + if not isinstance(return_data, bytes): + raise ValueError( + f"multicall entry {index} returnData has unexpected type {type(return_data)!r}" + ) + + normalized.append((success, return_data)) + + return normalized diff --git a/python/x402/mechanisms/evm/signer.py b/python/x402/mechanisms/evm/signer.py index 147155a2dc..948b2fef74 100644 --- a/python/x402/mechanisms/evm/signer.py +++ b/python/x402/mechanisms/evm/signer.py @@ -1,6 +1,6 @@ """EVM signer protocol definitions.""" -from typing import Any, Protocol +from typing import Any, Protocol, runtime_checkable from .types import TransactionReceipt, TypedDataDomain, TypedDataField @@ -176,3 +176,59 @@ def get_code(self, address: str) -> bytes: Bytecode (empty if EOA). """ ... + + +@runtime_checkable +class ClientEvmSignerWithReadContract(Protocol): + """Extension of ClientEvmSigner that adds on-chain read capability. + + Required for EIP-2612 gas sponsoring (needs to read nonces from token). + """ + + @property + def address(self) -> str: ... + + def sign_typed_data( + self, + domain: TypedDataDomain, + types: dict[str, list[TypedDataField]], + primary_type: str, + message: dict[str, Any], + ) -> bytes: ... + + def read_contract( + self, + address: str, + abi: list[dict[str, Any]], + function_name: str, + *args: Any, + ) -> Any: + """Read data from a smart contract.""" + ... + + +@runtime_checkable +class ClientEvmSignerWithSignTransaction(Protocol): + """Extension of ClientEvmSigner that adds raw transaction signing. + + Required for ERC-20 approval gas sponsoring (signs approve tx off-chain). + """ + + @property + def address(self) -> str: ... + + def sign_typed_data( + self, + domain: TypedDataDomain, + types: dict[str, list[TypedDataField]], + primary_type: str, + message: dict[str, Any], + ) -> bytes: ... + + def sign_transaction(self, tx: dict[str, Any]) -> str: + """Sign an EIP-1559 transaction and return the RLP-encoded hex string.""" + ... + + def get_transaction_count(self, address: str) -> int: + """Get the pending nonce for an address.""" + ... diff --git a/python/x402/mechanisms/evm/signers.py b/python/x402/mechanisms/evm/signers.py index 847b68032a..9774ecf8f5 100644 --- a/python/x402/mechanisms/evm/signers.py +++ b/python/x402/mechanisms/evm/signers.py @@ -6,8 +6,11 @@ from __future__ import annotations +import logging from typing import Any +logger = logging.getLogger("x402.signers") + try: from eth_account import Account from eth_account.messages import encode_typed_data @@ -19,8 +22,8 @@ "EVM signers require eth_account and web3. Install with: pip install x402[evm]" ) from e -from .constants import EIP1271_MAGIC_VALUE, IS_VALID_SIGNATURE_ABI, TX_STATUS_SUCCESS -from .types import TransactionReceipt, TypedDataDomain, TypedDataField +from .constants import EIP1271_MAGIC_VALUE, IS_VALID_SIGNATURE_ABI, TX_STATUS_SUCCESS # noqa: E402 +from .types import TransactionReceipt, TypedDataDomain, TypedDataField # noqa: E402 # ERC20 ABI for balance checks _ERC20_BALANCE_ABI = [ @@ -117,6 +120,14 @@ def sign_typed_data( else: domain_dict = domain + logger.info( + "EthAccountSigner.sign_typed_data: primaryType=%s domain_keys=%s type_names=%s", + primary_type, + list(domain_dict.keys()), + list(types_dict.keys()), + ) + logger.debug("EthAccountSigner.sign_typed_data: domain=%s message=%s", domain_dict, message) + # Sign typed data using eth_account signed = self._account.sign_typed_data( domain_data=domain_dict, @@ -126,6 +137,102 @@ def sign_typed_data( return bytes(signed.signature) +class EthAccountSignerWithRPC(EthAccountSigner): + """Client-side EVM signer with RPC capabilities for gas sponsoring extensions. + + Extends EthAccountSigner with read_contract, sign_transaction, and + get_transaction_count — the capabilities needed for EIP-2612 and + ERC-20 approval gas sponsoring. + + Equivalent to TS's toClientEvmSigner(account, publicClient). + + Example: + ```python + from eth_account import Account + from x402.mechanisms.evm.signers import EthAccountSignerWithRPC + + account = Account.from_key("0x...") + signer = EthAccountSignerWithRPC(account, rpc_url="https://sepolia.base.org") + + # Supports Permit2 with gas sponsoring extensions + from x402 import x402Client + from x402.mechanisms.evm.exact import register_exact_evm_client + + client = x402Client() + register_exact_evm_client(client, signer) + ``` + """ + + def __init__(self, account: LocalAccount, rpc_url: str) -> None: + """Initialize signer with eth_account LocalAccount and RPC connection. + + Args: + account: eth_account LocalAccount instance. + rpc_url: Ethereum RPC endpoint URL for on-chain reads. + """ + super().__init__(account) + self._w3 = Web3(Web3.HTTPProvider(rpc_url)) + + def read_contract( + self, + address: str, + abi: list[dict[str, Any]], + function_name: str, + *args: Any, + ) -> Any: + """Read data from a smart contract. + + Args: + address: Contract address. + abi: Contract ABI. + function_name: Function to call. + *args: Function arguments. + + Returns: + Function return value. + """ + contract = self._w3.eth.contract( + address=Web3.to_checksum_address(address), + abi=abi, + ) + return getattr(contract.functions, function_name)(*args).call() + + def sign_transaction(self, tx: dict[str, Any]) -> str: + """Sign an EIP-1559 transaction and return the RLP-encoded hex string. + + Args: + tx: Transaction dict with fields like to, data, nonce, gas, etc. + + Returns: + Hex-encoded signed transaction with 0x prefix. + """ + signed = self._w3.eth.account.sign_transaction(tx, self._account.key) + return "0x" + signed.raw_transaction.hex() + + def get_transaction_count(self, address: str) -> int: + """Get the pending nonce for an address. + + Args: + address: Account address. + + Returns: + Pending transaction count. + """ + return self._w3.eth.get_transaction_count(Web3.to_checksum_address(address)) + + def estimate_fees_per_gas(self) -> tuple[int, int]: + """Estimate EIP-1559 fee parameters from the network. + + Returns: + Tuple of (maxFeePerGas, maxPriorityFeePerGas) in wei. + """ + latest = self._w3.eth.get_block("latest") + base_fee = latest.get("baseFeePerGas", 1_000_000_000) + max_priority_fee = self._w3.eth.max_priority_fee + max_fee = base_fee * 2 + max_priority_fee + return (max_fee, max_priority_fee) + + class FacilitatorWeb3Signer: """Facilitator-side EVM signer using web3.py. @@ -248,14 +355,30 @@ def verify_typed_data( Returns: True if signature is valid. """ - # Build full types including EIP712Domain + # Build domain dict — handle both TypedDataDomain and raw dict (Permit2 has no version) + if isinstance(domain, dict): + domain_dict = domain + else: + domain_dict = { + "name": domain.name, + "chainId": domain.chain_id, + "verifyingContract": domain.verifying_contract, + } + if domain.version: + domain_dict["version"] = domain.version + + # Derive EIP712Domain type from actual domain keys + domain_field_map = { + "name": {"name": "name", "type": "string"}, + "version": {"name": "version", "type": "string"}, + "chainId": {"name": "chainId", "type": "uint256"}, + "verifyingContract": {"name": "verifyingContract", "type": "address"}, + "salt": {"name": "salt", "type": "bytes32"}, + } + eip712_domain_type = [domain_field_map[k] for k in domain_dict if k in domain_field_map] + full_types: dict[str, list[dict[str, str]]] = { - "EIP712Domain": [ - {"name": "name", "type": "string"}, - {"name": "version", "type": "string"}, - {"name": "chainId", "type": "uint256"}, - {"name": "verifyingContract", "type": "address"}, - ] + "EIP712Domain": eip712_domain_type, } for type_name, fields in types.items(): full_types[type_name] = [ @@ -272,19 +395,27 @@ def verify_typed_data( typed_data = { "types": full_types, "primaryType": primary_type, - "domain": { - "name": domain.name, - "version": domain.version, - "chainId": domain.chain_id, - "verifyingContract": domain.verifying_contract, - }, + "domain": domain_dict, "message": msg_copy, } + logger.info( + "verify_typed_data: primaryType=%s domain_keys=%s type_names=%s", + primary_type, + list(domain_dict.keys()), + list(full_types.keys()), + ) + logger.debug("verify_typed_data: full typed_data=%s", typed_data) + # Try EOA signature verification first - recovered = Account.recover_message( - encode_typed_data(full_message=typed_data), - signature=signature, + signable = encode_typed_data(full_message=typed_data) + recovered = Account.recover_message(signable, signature=signature) + + logger.info( + "verify_typed_data: expected=%s recovered=%s match=%s", + address.lower(), + recovered.lower(), + recovered.lower() == address.lower(), ) if recovered.lower() == address.lower(): @@ -313,7 +444,7 @@ def verify_typed_data( return False except Exception as e: - print(f"Signature verification error: {e}") + logger.error("Signature verification error: %s", e, exc_info=True) return False def write_contract( diff --git a/python/x402/mechanisms/evm/types.py b/python/x402/mechanisms/evm/types.py index e6d18f557a..39c66a281c 100644 --- a/python/x402/mechanisms/evm/types.py +++ b/python/x402/mechanisms/evm/types.py @@ -72,6 +72,123 @@ def from_dict(cls, data: dict[str, Any]) -> "ExactEIP3009Payload": ExactEvmPayloadV2 = ExactEIP3009Payload +@dataclass +class ExactPermit2Witness: + """Witness data for Permit2 PermitWitnessTransferFrom.""" + + to: str # Recipient address + valid_after: str # Unix timestamp as string (lower time bound) + + +@dataclass +class ExactPermit2TokenPermissions: + """Token permissions for Permit2.""" + + token: str # ERC-20 token address + amount: str # Amount in smallest unit as string + + +@dataclass +class ExactPermit2Authorization: + """Permit2 PermitWitnessTransferFrom data.""" + + from_address: str # 'from' field (payer address) + permitted: ExactPermit2TokenPermissions + spender: str # x402ExactPermit2Proxy address + nonce: str # Random uint256 as decimal string + deadline: str # Unix timestamp as string (upper time bound) + witness: ExactPermit2Witness + + +@dataclass +class ExactPermit2Payload: + """Exact payment payload for Permit2 flow.""" + + permit2_authorization: ExactPermit2Authorization + signature: str | None = None + + def to_dict(self) -> dict[str, Any]: + """Convert to dictionary for JSON serialization. + + Returns: + Dict with permit2Authorization and signature fields. + """ + result: dict[str, Any] = { + "permit2Authorization": { + "from": self.permit2_authorization.from_address, + "permitted": { + "token": self.permit2_authorization.permitted.token, + "amount": self.permit2_authorization.permitted.amount, + }, + "spender": self.permit2_authorization.spender, + "nonce": self.permit2_authorization.nonce, + "deadline": self.permit2_authorization.deadline, + "witness": { + "to": self.permit2_authorization.witness.to, + "validAfter": self.permit2_authorization.witness.valid_after, + }, + } + } + if self.signature: + result["signature"] = self.signature + return result + + @classmethod + def from_dict(cls, data: dict[str, Any]) -> "ExactPermit2Payload": + """Create from dictionary. + + Args: + data: Dict with permit2Authorization and optional signature. + + Returns: + ExactPermit2Payload instance. + """ + auth = data.get("permit2Authorization", {}) + permitted = auth.get("permitted", {}) + witness = auth.get("witness", {}) + return cls( + permit2_authorization=ExactPermit2Authorization( + from_address=auth.get("from", ""), + permitted=ExactPermit2TokenPermissions( + token=permitted.get("token", ""), + amount=permitted.get("amount", ""), + ), + spender=auth.get("spender", ""), + nonce=auth.get("nonce", ""), + deadline=auth.get("deadline", ""), + witness=ExactPermit2Witness( + to=witness.get("to", ""), + valid_after=witness.get("validAfter", ""), + ), + ), + signature=data.get("signature"), + ) + + +def is_permit2_payload(payload: dict[str, Any]) -> bool: + """Check if a raw payload dict is a Permit2 payload. + + Args: + payload: Raw payload dictionary. + + Returns: + True if the payload contains permit2Authorization key. + """ + return "permit2Authorization" in payload + + +def is_eip3009_payload(payload: dict[str, Any]) -> bool: + """Check if a raw payload dict is an EIP-3009 payload. + + Args: + payload: Raw payload dictionary. + + Returns: + True if the payload contains authorization key. + """ + return "authorization" in payload + + @dataclass class TypedDataDomain: """EIP-712 domain separator.""" diff --git a/python/x402/mechanisms/evm/utils.py b/python/x402/mechanisms/evm/utils.py index d515f25833..1d29cc2580 100644 --- a/python/x402/mechanisms/evm/utils.py +++ b/python/x402/mechanisms/evm/utils.py @@ -45,6 +45,9 @@ def get_evm_chain_id(network: str) -> int: def get_network_config(network: str) -> NetworkConfig: """Get configuration for a CAIP-2 network identifier (eip155:CHAIN_ID). + Returns a full config for known networks, or a minimal config (chain_id only) + for any valid but unknown eip155 network. + Args: network: Network identifier in CAIP-2 format. @@ -52,64 +55,60 @@ def get_network_config(network: str) -> NetworkConfig: Network configuration. Raises: - ValueError: If network is not configured. + ValueError: If the network format is invalid or not an eip155 network. """ if network in NETWORK_CONFIGS: return NETWORK_CONFIGS[network] - raise ValueError(f"No configuration for network: {network} (expected eip155:CHAIN_ID)") + if network.startswith("eip155:"): + try: + chain_id = int(network.split(":")[1]) + return {"chain_id": chain_id} + except (IndexError, ValueError) as e: + raise ValueError(f"Invalid CAIP-2 network format: {network}") from e + + raise ValueError(f"Unsupported network format: {network} (expected eip155:CHAIN_ID)") + +def get_asset_info(network: str, asset_address: str) -> AssetInfo: + """Get asset info by address. -def get_asset_info(network: str, asset_symbol_or_address: str) -> AssetInfo: - """Get asset info by symbol or address. + Returns the full default asset info if the address matches the network's default asset. Args: - network: Network identifier. - asset_symbol_or_address: Asset symbol (e.g., "USDC") or address. + network: Network identifier in CAIP-2 format. + asset_address: Asset contract address (0x...). Returns: Asset information. Raises: - ValueError: If asset is not found. + ValueError: If the address does not match any registered asset for the network. """ config = get_network_config(network) + default = config.get("default_asset") - # Check if it's an address - if asset_symbol_or_address.startswith("0x"): - # Search by address - for asset in config["supported_assets"].values(): - if asset["address"].lower() == asset_symbol_or_address.lower(): - return asset - # Return default with provided address if not found - return { - "address": asset_symbol_or_address, - "name": config["default_asset"]["name"], - "version": config["default_asset"]["version"], - "decimals": config["default_asset"]["decimals"], - } + if default and default["address"].lower() == asset_address.lower(): + return default - # Search by symbol - symbol = asset_symbol_or_address.upper() - if symbol in config["supported_assets"]: - return config["supported_assets"][symbol] - - raise ValueError(f"Asset {asset_symbol_or_address} not found on {network}") + raise ValueError(f"Token {asset_address} is not a registered asset for network {network}.") def is_valid_network(network: str) -> bool: - """Check if network is supported. + """Check if network is a valid eip155 network identifier. Args: network: Network identifier. Returns: - True if network is supported. + True if the network is a valid eip155:CHAIN_ID format. """ + if not network.startswith("eip155:"): + return False try: - get_network_config(network) + int(network.split(":")[1]) return True - except ValueError: + except (IndexError, ValueError): return False @@ -122,6 +121,18 @@ def create_nonce() -> str: return "0x" + os.urandom(32).hex() +def create_permit2_nonce() -> str: + """Generate random uint256 nonce as decimal string for Permit2. + + Permit2 uses uint256 nonces (not bytes32), so the nonce is returned + as a decimal string rather than a hex string. + + Returns: + Decimal string representation of a random uint256. + """ + return str(int.from_bytes(os.urandom(32), "big")) + + def normalize_address(address: str) -> str: """Normalize Ethereum address to checksummed format. diff --git a/python/x402/mechanisms/evm/v1/constants.py b/python/x402/mechanisms/evm/v1/constants.py index f9c57d266e..ce668bc64b 100644 --- a/python/x402/mechanisms/evm/v1/constants.py +++ b/python/x402/mechanisms/evm/v1/constants.py @@ -1,18 +1,60 @@ """V1 legacy network constants for EVM mechanisms.""" -# Network aliases (legacy names to CAIP-2) -NETWORK_ALIASES: dict[str, str] = { - "base": "eip155:8453", - "base-mainnet": "eip155:8453", - "base-sepolia": "eip155:84532", - "ethereum": "eip155:1", - "mainnet": "eip155:1", - "polygon": "eip155:137", - "avalanche": "eip155:43114", - "megaeth": "eip155:4326", - "monad": "eip155:143", +from ..constants import AssetInfo + +# Default assets keyed by v1 legacy network name. +V1_DEFAULT_ASSETS: dict[str, AssetInfo] = { + "ethereum": { + "address": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "name": "USD Coin", + "version": "2", + "decimals": 6, + }, + "base": { + "address": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", + "name": "USD Coin", + "version": "2", + "decimals": 6, + }, + "base-sepolia": { + "address": "0x036CbD53842c5426634e7929541eC2318f3dCF7e", + "name": "USDC", + "version": "2", + "decimals": 6, + }, + "polygon": { + "address": "0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359", + "name": "USD Coin", + "version": "2", + "decimals": 6, + }, + "avalanche": { + "address": "0xB97EF9Ef8734C71904D8002F8b6Bc66Dd9c48a6E", + "name": "USD Coin", + "version": "2", + "decimals": 6, + }, + "monad": { + "address": "0x754704Bc059F8C67012fEd69BC8A327a5aafb603", + "name": "USD Coin", + "version": "2", + "decimals": 6, + }, + "stable": { + "address": "0x779Ded0c9e1022225f8E0630b35a9b54bE713736", + "name": "USDT0", + "version": "1", + "decimals": 6, + }, + "stable-testnet": { + "address": "0x78Cf24370174180738C5B8E352B6D14c83a6c9A9", + "name": "USDT0", + "version": "1", + "decimals": 6, + }, } + # V1 supported networks (legacy name-based) V1_NETWORKS = [ "abstract", @@ -32,6 +74,8 @@ "skale-base-sepolia", "megaeth", "monad", + "stable", + "stable-testnet", ] # V1 network name to chain ID mapping @@ -54,4 +98,6 @@ "skale-base-sepolia": 1444673419, "megaeth": 4326, "monad": 143, + "stable": 988, + "stable-testnet": 2201, } diff --git a/python/x402/mechanisms/evm/v1/utils.py b/python/x402/mechanisms/evm/v1/utils.py index e143f49dcb..7994e9a788 100644 --- a/python/x402/mechanisms/evm/v1/utils.py +++ b/python/x402/mechanisms/evm/v1/utils.py @@ -1,7 +1,7 @@ """V1 legacy network utilities for EVM mechanisms.""" -from ..constants import NETWORK_CONFIGS, AssetInfo -from .constants import NETWORK_ALIASES, V1_NETWORK_CHAIN_IDS +from ..constants import AssetInfo +from .constants import V1_DEFAULT_ASSETS, V1_NETWORK_CHAIN_IDS def get_evm_chain_id(network: str) -> int: @@ -16,62 +16,32 @@ def get_evm_chain_id(network: str) -> int: Raises: ValueError: If network is not a known v1 network. """ - if network in NETWORK_ALIASES: - caip2 = NETWORK_ALIASES[network] - return int(caip2.split(":")[1]) - if network in V1_NETWORK_CHAIN_IDS: return V1_NETWORK_CHAIN_IDS[network] raise ValueError(f"Unknown v1 network: {network}") -def get_asset_info(network: str, asset_symbol_or_address: str) -> AssetInfo: - """Get asset info for a v1 network. - - Normalizes the v1 network name to CAIP-2 and looks up in shared NETWORK_CONFIGS. +def get_asset_info(network: str, asset_address: str) -> AssetInfo: + """Get asset info for a v1 network by legacy network name. Args: - network: V1 network name. - asset_symbol_or_address: Asset symbol (e.g., "USDC") or address. + network: V1 legacy network name (e.g., "base", "polygon"). + asset_address: Asset contract address (0x...). Returns: Asset information. Raises: - ValueError: If asset is not found. + ValueError: If the network has no known default asset, or the address does not + match the registered asset for the network. """ - caip2_network = _normalize_to_caip2(network) - - if caip2_network not in NETWORK_CONFIGS: - raise ValueError(f"No configuration for v1 network: {network}") - - config = NETWORK_CONFIGS[caip2_network] - - if asset_symbol_or_address.startswith("0x"): - for asset in config["supported_assets"].values(): - if asset["address"].lower() == asset_symbol_or_address.lower(): - return asset - return { - "address": asset_symbol_or_address, - "name": config["default_asset"]["name"], - "version": config["default_asset"]["version"], - "decimals": config["default_asset"]["decimals"], - } + default = V1_DEFAULT_ASSETS.get(network) - symbol = asset_symbol_or_address.upper() - if symbol and symbol in config["supported_assets"]: - return config["supported_assets"][symbol] + if default is None: + raise ValueError(f"No default asset for v1 network: {network}") - return config["default_asset"] + if default["address"].lower() == asset_address.lower(): + return default - -def _normalize_to_caip2(network: str) -> str: - """Convert a v1 network name to CAIP-2 format.""" - if network in NETWORK_ALIASES: - return NETWORK_ALIASES[network] - - if network in V1_NETWORK_CHAIN_IDS: - return f"eip155:{V1_NETWORK_CHAIN_IDS[network]}" - - raise ValueError(f"Unknown v1 network: {network}") + raise ValueError(f"Token {asset_address} is not a registered asset for v1 network {network}.") diff --git a/python/x402/mechanisms/svm/README.md b/python/x402/mechanisms/svm/README.md index 02c02e9a55..ab82614002 100644 --- a/python/x402/mechanisms/svm/README.md +++ b/python/x402/mechanisms/svm/README.md @@ -121,3 +121,13 @@ The Exact scheme creates a partially-signed transaction: Automatic ATA derivation for source and destination addresses. +## Duplicate Settlement Protection + +This package includes a built-in `SettlementCache` that prevents a known race condition on Solana where the same payment transaction could be settled multiple times before on-chain confirmation. When the facilitator scheme is registered via `register_exact_svm_facilitator()`, a single `SettlementCache` instance is automatically shared across both V1 and V2 scheme versions. + +The cache rejects concurrent `/settle` calls that carry the same transaction payload, returning a `duplicate_settlement` error for the second and subsequent attempts. Entries are automatically evicted after 120 seconds (approximately twice the Solana blockhash lifetime). + +**No additional configuration is required** — duplicate settlement protection is enabled by default when using the standard registration helpers. + +For full details on the race condition and mitigation strategy, see the [Exact SVM Scheme Specification](../../../../specs/schemes/exact/scheme_exact_svm.md#duplicate-settlement-mitigation-recommended). + diff --git a/python/x402/mechanisms/svm/__init__.py b/python/x402/mechanisms/svm/__init__.py index fb9c6c0d31..08947a3ffc 100644 --- a/python/x402/mechanisms/svm/__init__.py +++ b/python/x402/mechanisms/svm/__init__.py @@ -52,6 +52,9 @@ NetworkConfig, ) +# Settlement cache (shared across V1/V2 facilitator instances) +from .settlement_cache import SettlementCache + # Signer protocols from .signer import ClientSvmSigner, FacilitatorSvmSigner @@ -139,6 +142,8 @@ "ExactSvmPayloadV1", "ExactSvmPayloadV2", "TransactionInfo", + # Settlement cache + "SettlementCache", # Signer protocols "ClientSvmSigner", "FacilitatorSvmSigner", diff --git a/python/x402/mechanisms/svm/constants.py b/python/x402/mechanisms/svm/constants.py index f82443afcb..bbe8748428 100644 --- a/python/x402/mechanisms/svm/constants.py +++ b/python/x402/mechanisms/svm/constants.py @@ -85,6 +85,11 @@ ERR_FEE_PAYER_TRANSFERRING = "invalid_exact_svm_payload_transaction_fee_payer_transferring_funds" ERR_SIMULATION_FAILED = "transaction_simulation_failed" ERR_TRANSACTION_FAILED = "transaction_failed" +ERR_DUPLICATE_SETTLEMENT = "duplicate_settlement" + +# How long a transaction is held in the duplicate settlement cache (seconds). +# Covers the Solana blockhash lifetime (~60-90s) with margin. +SETTLEMENT_TTL_SECONDS = 120.0 class AssetInfo(TypedDict): diff --git a/python/x402/mechanisms/svm/exact/facilitator.py b/python/x402/mechanisms/svm/exact/facilitator.py index cc0e0cecd4..42b90ce1e8 100644 --- a/python/x402/mechanisms/svm/exact/facilitator.py +++ b/python/x402/mechanisms/svm/exact/facilitator.py @@ -1,5 +1,7 @@ """SVM facilitator implementation for the Exact payment scheme (V2).""" +from __future__ import annotations + import random from typing import Any @@ -20,6 +22,7 @@ from ..constants import ( COMPUTE_BUDGET_PROGRAM_ADDRESS, ERR_AMOUNT_INSUFFICIENT, + ERR_DUPLICATE_SETTLEMENT, ERR_FEE_PAYER_MISSING, ERR_FEE_PAYER_NOT_MANAGED, ERR_FEE_PAYER_TRANSFERRING, @@ -44,6 +47,7 @@ TOKEN_2022_PROGRAM_ADDRESS, TOKEN_PROGRAM_ADDRESS, ) +from ..settlement_cache import SettlementCache from ..signer import FacilitatorSvmSigner from ..types import ExactSvmPayload from ..utils import ( @@ -66,13 +70,19 @@ class ExactSvmScheme: scheme = SCHEME_EXACT caip_family = "solana:*" - def __init__(self, signer: FacilitatorSvmSigner): + def __init__( + self, + signer: FacilitatorSvmSigner, + settlement_cache: SettlementCache | None = None, + ): """Create ExactSvmScheme facilitator. Args: signer: SVM signer for verification and settlement. + settlement_cache: Optional shared settlement cache (one is created if omitted). """ self._signer = signer + self._settlement_cache = settlement_cache or SettlementCache() def get_extra(self, network: Network) -> dict[str, Any] | None: """Get mechanism-specific extra data for the supported kinds endpoint. @@ -121,7 +131,7 @@ def verify( - Token program is known (Token or Token-2022) - Mint matches requirements.asset - Destination ATA matches requirements.pay_to - - Amount >= requirements.amount + - Amount >= requirements.amount - Authority is not the facilitator (prevent self-transfer) - Simulates transaction to catch runtime errors @@ -362,6 +372,17 @@ def settle( transaction="", ) + # Duplicate settlement check: reject if this transaction is already being settled. + tx_key = svm_payload.transaction + if self._settlement_cache.is_duplicate(tx_key): + return SettleResponse( + success=False, + error_reason=ERR_DUPLICATE_SETTLEMENT, + network=network, + payer=verify_result.payer or "", + transaction="", + ) + signature = "" try: # Extract feePayer from requirements (already validated in verify) diff --git a/python/x402/mechanisms/svm/exact/register.py b/python/x402/mechanisms/svm/exact/register.py index 39ce7ce3eb..afb4b16c56 100644 --- a/python/x402/mechanisms/svm/exact/register.py +++ b/python/x402/mechanisms/svm/exact/register.py @@ -111,6 +111,9 @@ def register_exact_svm_facilitator( - V2: Specified networks - V1: All supported SVM networks + A single settlement cache is shared across V1 and V2 so that a duplicate + transaction submitted through one protocol version is also caught by the other. + Args: facilitator: x402Facilitator instance. signer: SVM signer for verification/settlement. @@ -119,17 +122,20 @@ def register_exact_svm_facilitator( Returns: Facilitator for chaining. """ + from ..settlement_cache import SettlementCache from .facilitator import ExactSvmScheme as ExactSvmFacilitatorScheme from .v1.facilitator import ExactSvmSchemeV1 as ExactSvmFacilitatorSchemeV1 - scheme = ExactSvmFacilitatorScheme(signer) + settlement_cache = SettlementCache() + + scheme = ExactSvmFacilitatorScheme(signer, settlement_cache) if isinstance(networks, str): networks = [networks] facilitator.register(networks, scheme) # Register V1 - v1_scheme = ExactSvmFacilitatorSchemeV1(signer) + v1_scheme = ExactSvmFacilitatorSchemeV1(signer, settlement_cache) facilitator.register_v1(V1_NETWORKS, v1_scheme) return facilitator diff --git a/python/x402/mechanisms/svm/exact/v1/facilitator.py b/python/x402/mechanisms/svm/exact/v1/facilitator.py index e6c2cd0b9d..389ac2f9a9 100644 --- a/python/x402/mechanisms/svm/exact/v1/facilitator.py +++ b/python/x402/mechanisms/svm/exact/v1/facilitator.py @@ -1,5 +1,7 @@ """SVM facilitator implementation for Exact payment scheme (V1 legacy).""" +from __future__ import annotations + import random from typing import Any @@ -15,6 +17,7 @@ from ...constants import ( COMPUTE_BUDGET_PROGRAM_ADDRESS, ERR_AMOUNT_INSUFFICIENT, + ERR_DUPLICATE_SETTLEMENT, ERR_FEE_PAYER_MISSING, ERR_FEE_PAYER_NOT_MANAGED, ERR_FEE_PAYER_TRANSFERRING, @@ -39,6 +42,7 @@ TOKEN_2022_PROGRAM_ADDRESS, TOKEN_PROGRAM_ADDRESS, ) +from ...settlement_cache import SettlementCache from ...signer import FacilitatorSvmSigner from ...types import ExactSvmPayload from ...utils import ( @@ -64,13 +68,19 @@ class ExactSvmSchemeV1: scheme = SCHEME_EXACT caip_family = "solana:*" - def __init__(self, signer: FacilitatorSvmSigner): + def __init__( + self, + signer: FacilitatorSvmSigner, + settlement_cache: SettlementCache | None = None, + ): """Create ExactSvmSchemeV1 facilitator. Args: signer: SVM signer for verification/settlement. + settlement_cache: Optional shared settlement cache (one is created if omitted). """ self._signer = signer + self._settlement_cache = settlement_cache or SettlementCache() def get_extra(self, network: Network) -> dict[str, Any] | None: """Get mechanism-specific extra data. @@ -340,6 +350,17 @@ def settle( transaction="", ) + # Duplicate settlement check: reject if this transaction is already being settled. + tx_key = svm_payload.transaction + if self._settlement_cache.is_duplicate(tx_key): + return SettleResponse( + success=False, + error_reason=ERR_DUPLICATE_SETTLEMENT, + network=network, + payer=verify_result.payer or "", + transaction="", + ) + try: extra = requirements.extra or {} fee_payer = extra["feePayer"] diff --git a/python/x402/mechanisms/svm/settlement_cache.py b/python/x402/mechanisms/svm/settlement_cache.py new file mode 100644 index 0000000000..2d839eb6a8 --- /dev/null +++ b/python/x402/mechanisms/svm/settlement_cache.py @@ -0,0 +1,53 @@ +"""Thread-safe in-memory cache for deduplicating concurrent settlement requests. + +A single instance should be shared across V1 and V2 facilitator scheme +instances so that a transaction submitted through one protocol version is +also deduplicated on the other. +""" + +import threading +import time + +from .constants import SETTLEMENT_TTL_SECONDS + + +class SettlementCache: + """In-memory cache for deduplicating concurrent settlement requests. + + Thread-safe: all public methods acquire an internal lock. + """ + + def __init__(self) -> None: + self._entries: dict[str, float] = {} + self._lock = threading.Lock() + + def is_duplicate(self, key: str) -> bool: + """Return ``True`` if *key* is already pending settlement (duplicate). + + When ``False`` the key is recorded as newly pending. + Callers should reject the settlement when this returns ``True``. + """ + with self._lock: + self._prune() + if key in self._entries: + return True + self._entries[key] = time.monotonic() + return False + + # Exposed for testing (e.g. backdating entries to simulate TTL expiry). + @property + def entries(self) -> dict[str, float]: + """Direct access to the underlying dict — use only in tests.""" + return self._entries + + def _prune(self) -> None: + """Remove entries older than the settlement TTL. Caller must hold _lock.""" + cutoff = time.monotonic() - SETTLEMENT_TTL_SECONDS + expired = [] + for k, ts in self._entries.items(): + if ts < cutoff: + expired.append(k) + else: + break + for k in expired: + del self._entries[k] diff --git a/python/x402/mechanisms/svm/utils.py b/python/x402/mechanisms/svm/utils.py index 76231190b6..37170f3bae 100644 --- a/python/x402/mechanisms/svm/utils.py +++ b/python/x402/mechanisms/svm/utils.py @@ -122,19 +122,17 @@ def get_asset_info(network: str, asset_address: str | None = None) -> AssetInfo: Returns: Asset information. + + Raises: + ValueError: If the address does not match the registered asset for the network. """ config = get_network_config(network) default_asset = config["default_asset"] - if asset_address and asset_address != default_asset["address"]: - # Return with provided address but default metadata - return { - "address": asset_address, - "name": default_asset["name"], - "decimals": default_asset["decimals"], - } + if not asset_address or asset_address == default_asset["address"]: + return default_asset - return default_asset + raise ValueError(f"Token {asset_address} is not a registered asset for network {network}.") def convert_to_token_amount(decimal_amount: str, decimals: int) -> str: diff --git a/python/x402/pyproject.toml b/python/x402/pyproject.toml index 4f19f9d13f..4affe3ab24 100644 --- a/python/x402/pyproject.toml +++ b/python/x402/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "x402" -version = "2.2.0" +version = "2.5.0" description = "x402 Payment Protocol SDK for Python" readme = "README.md" license = { text = "MIT" } @@ -39,7 +39,7 @@ evm = [ "eth-abi>=5.0.0", "eth-keys>=0.5.0", "eth-utils>=4.0.0", - "eth-account>=0.12.0", + "eth-account>=0.13.0", "web3>=7.0.0", ] svm = [ @@ -77,7 +77,7 @@ dev = [ "eth-abi>=5.0.0", "eth-keys>=0.5.0", "eth-utils>=4.0.0", - "eth-account>=0.12.0", + "eth-account>=0.13.0", "web3>=7.0.0", # SVM dependencies "solders>=0.27.0", diff --git a/python/x402/schemas/config.py b/python/x402/schemas/config.py index 22d22a6705..7aa14de12b 100644 --- a/python/x402/schemas/config.py +++ b/python/x402/schemas/config.py @@ -22,6 +22,7 @@ class ResourceConfig(BaseX402Model): price: Price network: Network max_timeout_seconds: int | None = None + extra: dict[str, Any] | None = None class FacilitatorConfig(TypedDict, total=False): diff --git a/python/x402/schemas/responses.py b/python/x402/schemas/responses.py index 6ff72c7569..bb5e5a7837 100644 --- a/python/x402/schemas/responses.py +++ b/python/x402/schemas/responses.py @@ -12,10 +12,12 @@ class VerifyRequest(BaseX402Model): """Request to verify a payment. Attributes: + x402_version: Protocol version (1 or 2). payment_payload: The payment payload to verify. payment_requirements: The requirements to verify against. """ + x402_version: int payment_payload: PaymentPayload payment_requirements: PaymentRequirements @@ -40,10 +42,12 @@ class SettleRequest(BaseX402Model): """Request to settle a payment. Attributes: + x402_version: Protocol version (1 or 2). payment_payload: The payment payload to settle. payment_requirements: The requirements for settlement. """ + x402_version: int payment_payload: PaymentPayload payment_requirements: PaymentRequirements diff --git a/python/x402/server_base.py b/python/x402/server_base.py index 75e06e4f13..150edc3377 100644 --- a/python/x402/server_base.py +++ b/python/x402/server_base.py @@ -325,7 +325,10 @@ def build_payment_requirements( amount=asset_amount.amount, pay_to=config.pay_to, max_timeout_seconds=config.max_timeout_seconds or 300, - extra=asset_amount.extra or {}, + extra={ + **(asset_amount.extra or {}), + **(config.extra or {}), + }, ) # Enhance with scheme-specific details diff --git a/python/x402/tests/integrations/test_http_integration.py b/python/x402/tests/integrations/test_http_integration.py index e0ef45e4a8..0bb88fcc43 100644 --- a/python/x402/tests/integrations/test_http_integration.py +++ b/python/x402/tests/integrations/test_http_integration.py @@ -22,6 +22,7 @@ ) from x402.http import ( HTTPRequestContext, + ProcessSettleResult, decode_payment_required_header, x402HTTPClient, x402HTTPClientSync, @@ -315,6 +316,46 @@ def create(routes: dict) -> HTTPComponentsFixture: return Factory # type: ignore + def test_route_option_extra_is_preserved( + self, + components_factory: Any, + ) -> None: + """Route-level extra should flow into built payment requirements.""" + routes = { + "GET /api/permit2": { + "accepts": { + "scheme": "cash", + "payTo": "merchant@example.com", + "price": "$0.10", + "network": "x402:cash", + "extra": { + "assetTransferMethod": "permit2", + "merchantNote": "route-level-extra", + }, + }, + }, + } + + components = components_factory.create(routes) + adapter = MockHTTPAdapter( + path="/api/permit2", + method="GET", + ) + context = HTTPRequestContext( + adapter=adapter, + path="/api/permit2", + method="GET", + ) + + result = components.process_http_request(context) + payment_required = decode_payment_required_header( + result.response.headers["PAYMENT-REQUIRED"] + ) + + assert payment_required.accepts[0].extra is not None + assert payment_required.accepts[0].extra["assetTransferMethod"] == "permit2" + assert payment_required.accepts[0].extra["merchantNote"] == "route-level-extra" + def test_dynamic_price_from_query_params( self, components_factory: Any, @@ -542,3 +583,147 @@ def dynamic_price(context: HTTPRequestContext) -> Price: assert payment_required2.accepts[0].pay_to == "market-data-provider@example.com" assert payment_required2.accepts[0].amount == "1.50" # 0.5 * 3 + + +class TestSettlementFailureWithContext: + """Regression test: _build_settlement_failure_response must unpack the tuple + from _get_route_config (which returns tuple[RouteConfig, str] | None). + + Before the fix, passing a context that matched a route would raise + AttributeError because the code accessed .settlement_failed_response_body + on the tuple instead of unpacking it first. + """ + + @pytest.fixture(params=["sync", "async"]) + def components_factory(self, request: pytest.FixtureRequest) -> type[HTTPComponentsFixture]: + """Returns factory for creating components with custom routes.""" + + class Factory: + @staticmethod + def create(routes: dict) -> HTTPComponentsFixture: + if request.param == "sync": + return _create_sync_http_components(routes) + return _create_async_http_components(routes) + + return Factory # type: ignore + + def test_settlement_failure_with_route_context_does_not_raise( + self, + components_factory: Any, + ) -> None: + """_build_settlement_failure_response should not crash when context matches a route.""" + routes = { + "GET /api/protected": { + "accepts": { + "scheme": "cash", + "payTo": "merchant@example.com", + "price": "$0.10", + "network": "x402:cash", + }, + "description": "Protected endpoint", + }, + } + components = components_factory.create(routes) + + adapter = MockHTTPAdapter(path="/api/protected", method="GET") + context = HTTPRequestContext(adapter=adapter, path="/api/protected", method="GET") + + failure = ProcessSettleResult( + success=False, + error_reason="test failure", + headers={"PAYMENT-RESPONSE": "encoded"}, + ) + + # Directly call the internal method to exercise the tuple-unpacking fix. + # Before the fix, this would raise AttributeError on the tuple. + response = components.http_server._build_settlement_failure_response(failure, context) + + assert response.status == 402 + assert response.body == {} + assert "PAYMENT-RESPONSE" in response.headers + + +class TestColonParamRouteMatching: + """Tests that :param (Express-style) route patterns match requests correctly.""" + + @pytest.fixture(params=["sync", "async"]) + def components_factory(self, request: pytest.FixtureRequest) -> type[HTTPComponentsFixture]: + class Factory: + @staticmethod + def create(routes: dict) -> HTTPComponentsFixture: + if request.param == "sync": + return _create_sync_http_components(routes) + return _create_async_http_components(routes) + + return Factory # type: ignore + + def test_colon_param_route_matches_request( + self, + components_factory: Any, + ) -> None: + """Routes configured with :param syntax should match concrete requests.""" + routes = { + "GET /api/users/:userId": { + "accepts": { + "scheme": "cash", + "payTo": "merchant@example.com", + "price": "$0.10", + "network": "x402:cash", + }, + }, + } + components = components_factory.create(routes) + + adapter = MockHTTPAdapter(path="/api/users/123", method="GET") + context = HTTPRequestContext(adapter=adapter, path="/api/users/123", method="GET") + + result = components.process_http_request(context) + assert result.type == "payment-error" + assert result.response.status == 402 + + def test_colon_param_multiple_segments( + self, + components_factory: Any, + ) -> None: + """Multiple :param segments should all be matched.""" + routes = { + "GET /api/users/:userId/posts/:postId": { + "accepts": { + "scheme": "cash", + "payTo": "merchant@example.com", + "price": "$0.10", + "network": "x402:cash", + }, + }, + } + components = components_factory.create(routes) + + adapter = MockHTTPAdapter(path="/api/users/42/posts/7", method="GET") + context = HTTPRequestContext(adapter=adapter, path="/api/users/42/posts/7", method="GET") + + result = components.process_http_request(context) + assert result.type == "payment-error" + assert result.response.status == 402 + + def test_colon_param_no_match_for_different_path( + self, + components_factory: Any, + ) -> None: + """:param routes should not match structurally different paths.""" + routes = { + "GET /api/users/:userId": { + "accepts": { + "scheme": "cash", + "payTo": "merchant@example.com", + "price": "$0.10", + "network": "x402:cash", + }, + }, + } + components = components_factory.create(routes) + + adapter = MockHTTPAdapter(path="/api/posts/123", method="GET") + context = HTTPRequestContext(adapter=adapter, path="/api/posts/123", method="GET") + + result = components.process_http_request(context) + assert result.type == "no-payment-required" diff --git a/python/x402/tests/unit/core/test_server.py b/python/x402/tests/unit/core/test_server.py index 45dadadb54..400bc1f75f 100644 --- a/python/x402/tests/unit/core/test_server.py +++ b/python/x402/tests/unit/core/test_server.py @@ -4,6 +4,7 @@ from x402 import x402ResourceServer, x402ResourceServerSync from x402.schemas import ( + ResourceConfig, SettleResponse, SupportedKind, SupportedResponse, @@ -410,6 +411,64 @@ def enrich_declaration(self, declared, context): assert "test" in server._extensions +# ============================================================================= +# Build Requirements Tests +# ============================================================================= + + +class TestBuildPaymentRequirements: + """Tests for build_payment_requirements.""" + + def test_merges_resource_config_extra_with_parsed_price_extra(self): + """Merchant config extra should be preserved when requirements are built.""" + + class SchemeWithParsedExtra(MockSchemeServer): + def parse_price(self, price, network): + from dataclasses import dataclass + + @dataclass + class AssetAmount: + asset: str + amount: str + extra: dict | None = None + + return AssetAmount( + asset="0x0000000000000000000000000000000000000000", + amount="1000000", + extra={"parsed": "value"}, + ) + + kinds = [ + SupportedKind( + x402_version=2, + scheme="exact", + network="eip155:8453", + ) + ] + server = x402ResourceServerSync(MockFacilitatorClientSync(kinds)) + server.register("eip155:8453", SchemeWithParsedExtra("exact")) + server.initialize() + + requirements = server.build_payment_requirements( + ResourceConfig( + scheme="exact", + pay_to="0xmerchant", + price="$1.00", + network="eip155:8453", + extra={ + "assetTransferMethod": "permit2", + "merchantNote": "custom-scheme-data", + }, + ) + ) + + assert len(requirements) == 1 + assert requirements[0].extra is not None + assert requirements[0].extra.get("parsed") == "value" + assert requirements[0].extra.get("assetTransferMethod") == "permit2" + assert requirements[0].extra.get("merchantNote") == "custom-scheme-data" + + # ============================================================================= # Error Handling Tests # ============================================================================= diff --git a/python/x402/tests/unit/extensions/__init__.py b/python/x402/tests/unit/extensions/__init__.py index 2a219f87ea..e69de29bb2 100644 --- a/python/x402/tests/unit/extensions/__init__.py +++ b/python/x402/tests/unit/extensions/__init__.py @@ -1 +0,0 @@ -"""Unit tests for x402 extensions.""" diff --git a/python/x402/tests/unit/extensions/bazaar/test_facilitator.py b/python/x402/tests/unit/extensions/bazaar/test_facilitator.py index 9b330504f6..af79e1fa46 100644 --- a/python/x402/tests/unit/extensions/bazaar/test_facilitator.py +++ b/python/x402/tests/unit/extensions/bazaar/test_facilitator.py @@ -10,33 +10,91 @@ validate_and_extract, validate_discovery_extension, ) +from x402.extensions.bazaar.facilitator import _is_valid_route_template + + +class TestIsValidRouteTemplate: + """Direct unit tests for the _is_valid_route_template helper.""" + + def test_returns_false_for_none_input(self) -> None: + assert _is_valid_route_template(None) is False + + def test_returns_false_for_empty_string(self) -> None: + assert _is_valid_route_template("") is False + + def test_returns_false_for_paths_not_starting_with_slash(self) -> None: + assert _is_valid_route_template("users/123") is False + assert _is_valid_route_template("relative/path") is False + assert _is_valid_route_template("no-slash") is False + + def test_returns_false_for_paths_containing_dotdot(self) -> None: + assert _is_valid_route_template("/users/../admin") is False + assert _is_valid_route_template("/../etc/passwd") is False + assert _is_valid_route_template("/users/..") is False + + def test_returns_false_for_paths_containing_scheme(self) -> None: + assert _is_valid_route_template("http://evil.com/path") is False + assert _is_valid_route_template("/users/http://evil") is False + assert _is_valid_route_template("javascript://foo") is False + + def test_returns_true_for_valid_paths(self) -> None: + assert _is_valid_route_template("/users/:userId") is True + assert _is_valid_route_template("/api/v1/items") is True + assert _is_valid_route_template("/products/:productId/reviews/:reviewId") is True + assert _is_valid_route_template("/weather/:country/:city") is True + + def test_returns_false_for_paths_with_spaces_or_invalid_chars(self) -> None: + assert _is_valid_route_template("/users/ bad") is False + assert _is_valid_route_template("/path with spaces") is False + + def test_dotdot_segment_prefix_is_rejected(self) -> None: + assert _is_valid_route_template("/users/..hidden") is False + + def test_rejects_percent_encoded_traversal_sequences(self) -> None: + assert _is_valid_route_template("/users/%2e%2e/admin") is False + assert _is_valid_route_template("/users/%2E%2E/admin") is False class TestValidateDiscoveryExtension: """Tests for validate_discovery_extension function.""" def test_valid_query_extension(self) -> None: - """Test validating a valid query extension.""" + """Test validating a valid query extension (enriched with method per spec).""" ext = declare_discovery_extension( input={"query": "test"}, input_schema={"properties": {"query": {"type": "string"}}}, ) + inner = ext[BAZAAR.key] + inner["info"]["input"]["method"] = "GET" - result = validate_discovery_extension(ext[BAZAAR.key]) + result = validate_discovery_extension(inner) assert result.valid is True assert len(result.errors) == 0 def test_valid_body_extension(self) -> None: - """Test validating a valid body extension.""" + """Test validating a valid body extension (enriched with method per spec).""" ext = declare_discovery_extension( input={"data": "test"}, input_schema={"properties": {"data": {"type": "string"}}}, body_type="json", ) + inner = ext[BAZAAR.key] + inner["info"]["input"]["method"] = "POST" - result = validate_discovery_extension(ext[BAZAAR.key]) + result = validate_discovery_extension(inner) assert result.valid is True + def test_method_required_enforcement(self) -> None: + """Test that validation fails when method is absent per spec.""" + ext = declare_discovery_extension( + input={"query": "test"}, + input_schema={"properties": {"query": {"type": "string"}}}, + ) + + result = validate_discovery_extension(ext[BAZAAR.key]) + assert result.valid is False + assert any("method" in e for e in result.errors) + class TestExtractDiscoveryInfo: """Tests for extract_discovery_info function.""" @@ -52,6 +110,7 @@ def test_extract_v2_query_extension(self) -> None: ext_dict = ext[BAZAAR.key] if hasattr(ext_dict, "model_dump"): ext_dict = ext_dict.model_dump(by_alias=True) + ext_dict["info"]["input"]["method"] = "GET" payload = { "x402Version": 2, @@ -78,6 +137,7 @@ def test_extract_v2_body_extension(self) -> None: ext_dict = ext[BAZAAR.key] if hasattr(ext_dict, "model_dump"): ext_dict = ext_dict.model_dump(by_alias=True) + ext_dict["info"]["input"]["method"] = "POST" payload = { "x402Version": 2, @@ -127,6 +187,7 @@ def test_strip_query_params_from_v2_resource_url(self) -> None: ext_dict = ext[BAZAAR.key] if hasattr(ext_dict, "model_dump"): ext_dict = ext_dict.model_dump(by_alias=True) + ext_dict["info"]["input"]["method"] = "GET" payload = { "x402Version": 2, @@ -150,6 +211,7 @@ def test_strip_hash_sections_from_v2_resource_url(self) -> None: ext_dict = ext[BAZAAR.key] if hasattr(ext_dict, "model_dump"): ext_dict = ext_dict.model_dump(by_alias=True) + ext_dict["info"]["input"]["method"] = "GET" payload = { "x402Version": 2, @@ -173,6 +235,7 @@ def test_strip_query_params_and_hash_from_v2_resource_url(self) -> None: ext_dict = ext[BAZAAR.key] if hasattr(ext_dict, "model_dump"): ext_dict = ext_dict.model_dump(by_alias=True) + ext_dict["info"]["input"]["method"] = "GET" payload = { "x402Version": 2, @@ -264,8 +327,10 @@ def test_extract_valid_extension(self) -> None: ext = declare_discovery_extension( input={"q": "test"}, ) + inner = ext[BAZAAR.key] + inner["info"]["input"]["method"] = "GET" - info = extract_discovery_info_from_extension(ext[BAZAAR.key]) + info = extract_discovery_info_from_extension(inner) assert isinstance(info, QueryDiscoveryInfo) def test_extract_without_validation(self) -> None: @@ -286,8 +351,10 @@ def test_valid_extension(self) -> None: ext = declare_discovery_extension( input={"query": "test"}, ) + inner = ext[BAZAAR.key] + inner["info"]["input"]["method"] = "GET" - result = validate_and_extract(ext[BAZAAR.key]) + result = validate_and_extract(inner) assert result.valid is True assert result.info is not None assert len(result.errors) == 0 @@ -298,7 +365,65 @@ def test_returns_info_on_success(self) -> None: input={"name": "test"}, body_type="json", ) + inner = ext[BAZAAR.key] + inner["info"]["input"]["method"] = "POST" - result = validate_and_extract(ext[BAZAAR.key]) + result = validate_and_extract(inner) assert result.valid is True assert isinstance(result.info, BodyDiscoveryInfo) + + +class TestDynamicRoutesFacilitator: + """Tests for dynamic route handling in the facilitator.""" + + def test_route_template_used_for_canonical_url(self) -> None: + """When routeTemplate is present, it should override the concrete URL path.""" + ext = declare_discovery_extension(input={}) + declaration = ext[BAZAAR.key] + if hasattr(declaration, "model_dump"): + declaration = declaration.model_dump(by_alias=True) + # Inject routeTemplate as if the server extension enriched it + declaration["routeTemplate"] = "/users/:userId" + declaration["info"]["input"]["pathParams"] = {"userId": "123"} + + payload = { + "x402Version": 2, + "scheme": "exact", + "network": "eip155:8453", + "payload": {}, + "accepted": {}, + "resource": {"url": "http://example.com/users/123"}, + "extensions": {BAZAAR.key: declaration}, + } + + discovered = extract_discovery_info(payload, {}, validate=False) + + assert discovered is not None + assert discovered.resource_url == "http://example.com/users/:userId" + assert discovered.route_template == "/users/:userId" + + def test_static_route_uses_concrete_url(self) -> None: + """Without routeTemplate, the stripped concrete URL should be used.""" + ext = declare_discovery_extension( + input={"query": "test"}, + input_schema={"properties": {"query": {"type": "string"}}}, + ) + declaration = ext[BAZAAR.key] + if hasattr(declaration, "model_dump"): + declaration = declaration.model_dump(by_alias=True) + + payload = { + "x402Version": 2, + "scheme": "exact", + "network": "eip155:8453", + "payload": {}, + "accepted": {}, + "resource": {"url": "http://example.com/search?q=test"}, + "extensions": {BAZAAR.key: declaration}, + } + + discovered = extract_discovery_info(payload, {}, validate=False) + + assert discovered is not None + assert discovered.resource_url == "http://example.com/search" + assert discovered.route_template is None diff --git a/python/x402/tests/unit/extensions/bazaar/test_server.py b/python/x402/tests/unit/extensions/bazaar/test_server.py index d3d380069e..fdebeb5f2c 100644 --- a/python/x402/tests/unit/extensions/bazaar/test_server.py +++ b/python/x402/tests/unit/extensions/bazaar/test_server.py @@ -4,7 +4,9 @@ BAZAAR, bazaar_resource_server_extension, declare_discovery_extension, + validate_discovery_extension, ) +from x402.http.types import HTTPRequestContext class MockHTTPRequest: @@ -57,6 +59,53 @@ def test_enrich_post_method(self) -> None: assert enriched["info"]["input"]["method"] == "POST" + def test_enrich_then_validate_get(self) -> None: + """Test that declaring without method, then enriching, produces a valid extension.""" + ext = declare_discovery_extension( + input={"query": "test"}, + input_schema={"properties": {"query": {"type": "string"}}}, + ) + declaration = ext[BAZAAR.key] + + if hasattr(declaration, "model_dump"): + declaration = declaration.model_dump(by_alias=True) + + # Pre-enrichment: validation should fail (method missing) + pre_result = validate_discovery_extension(declaration) + assert pre_result.valid is False + + context = MockHTTPRequest(method="GET") + enriched = bazaar_resource_server_extension.enrich_declaration(declaration, context) + + # Post-enrichment: validation should pass + post_result = validate_discovery_extension(enriched) + assert post_result.valid is True, ( + f"enriched GET extension should pass: {post_result.errors}" + ) + + def test_enrich_then_validate_post(self) -> None: + """Test that declaring without method, then enriching, produces a valid extension.""" + ext = declare_discovery_extension( + input={"data": "test"}, + input_schema={"properties": {"data": {"type": "string"}}}, + body_type="json", + ) + declaration = ext[BAZAAR.key] + + if hasattr(declaration, "model_dump"): + declaration = declaration.model_dump(by_alias=True) + + pre_result = validate_discovery_extension(declaration) + assert pre_result.valid is False + + context = MockHTTPRequest(method="POST") + enriched = bazaar_resource_server_extension.enrich_declaration(declaration, context) + + post_result = validate_discovery_extension(enriched) + assert post_result.valid is True, ( + f"enriched POST extension should pass: {post_result.errors}" + ) + def test_enrich_no_context(self) -> None: """Test enriching without HTTP context returns unchanged.""" ext = declare_discovery_extension( @@ -133,3 +182,187 @@ def test_enrich_preserves_existing_data(self) -> None: query_params = enriched["info"]["input"].get("queryParams") if query_params: assert "city" in query_params or "city" in str(query_params) + + +class MockAdapter: + """Mock HTTP adapter with configurable path.""" + + def __init__(self, path: str) -> None: + self._path = path + + def get_path(self) -> str: + return self._path + + +class TestBazaarDynamicRoutes: + """Tests for dynamic route pattern handling in BazaarResourceServerExtension.""" + + def _prepare_declaration(self, ext: dict) -> dict: + declaration = ext[BAZAAR.key] + if hasattr(declaration, "model_dump"): + declaration = declaration.model_dump(by_alias=True) + return declaration + + def test_static_route_leaves_extension_unchanged(self) -> None: + """Static routes should not produce a routeTemplate.""" + ext = declare_discovery_extension(input={"query": "test"}) + declaration = self._prepare_declaration(ext) + + context = HTTPRequestContext( + method="GET", adapter=MockAdapter("/users"), path="/users", route_pattern="/users" + ) + enriched = bazaar_resource_server_extension.enrich_declaration(declaration, context) + + assert "routeTemplate" not in enriched + + def test_dynamic_route_produces_route_template(self) -> None: + """Dynamic route should produce routeTemplate with :param syntax.""" + ext = declare_discovery_extension(input={}) + declaration = self._prepare_declaration(ext) + + context = HTTPRequestContext( + method="GET", + adapter=MockAdapter("/users/123"), + path="/users/123", + route_pattern="/users/[userId]", + ) + enriched = bazaar_resource_server_extension.enrich_declaration(declaration, context) + + assert enriched.get("routeTemplate") == "/users/:userId" + + def test_path_params_extracted_from_concrete_url(self) -> None: + """Path params should be extracted from the concrete URL path.""" + ext = declare_discovery_extension(input={}) + declaration = self._prepare_declaration(ext) + + context = HTTPRequestContext( + method="GET", + adapter=MockAdapter("/users/123"), + path="/users/123", + route_pattern="/users/[userId]", + ) + enriched = bazaar_resource_server_extension.enrich_declaration(declaration, context) + + path_params = enriched["info"]["input"].get("pathParams") + assert path_params == {"userId": "123"} + + def test_multiple_path_params_extracted(self) -> None: + """Multiple path params should all be extracted.""" + ext = declare_discovery_extension(input={}) + declaration = self._prepare_declaration(ext) + + context = HTTPRequestContext( + method="GET", + adapter=MockAdapter("/users/42/posts/7"), + path="/users/42/posts/7", + route_pattern="/users/[userId]/posts/[postId]", + ) + enriched = bazaar_resource_server_extension.enrich_declaration(declaration, context) + + assert enriched.get("routeTemplate") == "/users/:userId/posts/:postId" + path_params = enriched["info"]["input"].get("pathParams") + assert path_params == {"userId": "42", "postId": "7"} + + def test_mismatched_pattern_and_path_returns_empty_params(self) -> None: + """extractPathParams returns {} gracefully when pattern and URL path are structurally different. + + This can occur in production if middleware and extension patterns diverge (e.g. the + route is mounted at a different prefix than the extension expects). + """ + ext = declare_discovery_extension(input={}) + declaration = self._prepare_declaration(ext) + + # Pattern expects /users/[userId] but adapter path is /api/other — fewer segments. + context = HTTPRequestContext( + method="GET", + adapter=MockAdapter("/api/other"), + path="/api/other", + route_pattern="/users/[userId]", + ) + enriched = bazaar_resource_server_extension.enrich_declaration(declaration, context) + + # routeTemplate is still produced (pattern contains a bracket param), but pathParams is empty + assert enriched.get("routeTemplate") == "/users/:userId" + path_params = enriched["info"]["input"].get("pathParams") + assert path_params == {} + + def test_colon_param_route_produces_route_template(self) -> None: + """Routes with :param syntax should produce routeTemplate.""" + ext = declare_discovery_extension(input={}) + declaration = self._prepare_declaration(ext) + + context = HTTPRequestContext( + method="GET", + adapter=MockAdapter("/users/123"), + path="/users/123", + route_pattern="/users/:userId", + ) + enriched = bazaar_resource_server_extension.enrich_declaration(declaration, context) + + assert enriched.get("routeTemplate") == "/users/:userId" + + def test_colon_param_extracts_path_params(self) -> None: + """:param patterns should extract pathParams from the URL.""" + ext = declare_discovery_extension(input={}) + declaration = self._prepare_declaration(ext) + + context = HTTPRequestContext( + method="GET", + adapter=MockAdapter("/users/42/posts/7"), + path="/users/42/posts/7", + route_pattern="/users/:userId/posts/:postId", + ) + enriched = bazaar_resource_server_extension.enrich_declaration(declaration, context) + + assert enriched.get("routeTemplate") == "/users/:userId/posts/:postId" + path_params = enriched["info"]["input"].get("pathParams") + assert path_params == {"userId": "42", "postId": "7"} + + def test_mixed_bracket_and_colon_params(self) -> None: + """Mixed [param] and :param should normalize to :param and extract all values.""" + ext = declare_discovery_extension(input={}) + declaration = self._prepare_declaration(ext) + + context = HTTPRequestContext( + method="GET", + adapter=MockAdapter("/users/42/posts/7"), + path="/users/42/posts/7", + route_pattern="/users/[userId]/posts/:postId", + ) + enriched = bazaar_resource_server_extension.enrich_declaration(declaration, context) + + assert enriched.get("routeTemplate") == "/users/:userId/posts/:postId" + path_params = enriched["info"]["input"].get("pathParams") + assert path_params == {"userId": "42", "postId": "7"} + + def test_wildcard_auto_converts_to_var_params(self) -> None: + """Wildcard * segments should auto-convert to :var1, :var2, etc.""" + ext = declare_discovery_extension(input={}) + declaration = self._prepare_declaration(ext) + + context = HTTPRequestContext( + method="GET", + adapter=MockAdapter("/weather/san-francisco"), + path="/weather/san-francisco", + route_pattern="/weather/*", + ) + enriched = bazaar_resource_server_extension.enrich_declaration(declaration, context) + + assert enriched.get("routeTemplate") == "/weather/:var1" + path_params = enriched["info"]["input"].get("pathParams") + assert path_params == {"var1": "san-francisco"} + + def test_multiple_wildcards_auto_convert(self) -> None: + """Multiple * segments should become :var1, :var2, :var3, etc.""" + ext = declare_discovery_extension(input={}) + declaration = self._prepare_declaration(ext) + + context = HTTPRequestContext( + method="GET", + adapter=MockAdapter("/api/users/42/posts/7"), + path="/api/users/42/posts/7", + route_pattern="/api/*/*/posts/*", + ) + enriched = bazaar_resource_server_extension.enrich_declaration(declaration, context) + + assert enriched.get("routeTemplate") == "/api/:var1/:var2/posts/:var3" diff --git a/python/x402/tests/unit/extensions/test_eip2612_gas_sponsoring.py b/python/x402/tests/unit/extensions/test_eip2612_gas_sponsoring.py new file mode 100644 index 0000000000..b4a6909a94 --- /dev/null +++ b/python/x402/tests/unit/extensions/test_eip2612_gas_sponsoring.py @@ -0,0 +1,143 @@ +"""Tests for the EIP-2612 Gas Sponsoring extension.""" + +from __future__ import annotations + +import time +from typing import Any + +from x402.extensions.eip2612_gas_sponsoring import ( + EIP2612_GAS_SPONSORING_KEY, + Eip2612GasSponsoringInfo, + declare_eip2612_gas_sponsoring_extension, + extract_eip2612_gas_sponsoring_info, + validate_eip2612_gas_sponsoring_info, + validate_eip2612_permit_for_payment, +) +from x402.mechanisms.evm.constants import PERMIT2_ADDRESS +from x402.schemas import PaymentPayload, PaymentRequirements, ResourceInfo + +TOKEN_ADDRESS = "0x036CbD53842c5426634e7929541eC2318f3dCF7e" +PAYER = "0x1234567890123456789012345678901234567890" + + +def _make_info(**overrides: Any) -> Eip2612GasSponsoringInfo: + defaults = { + "from_address": PAYER, + "asset": TOKEN_ADDRESS, + "spender": PERMIT2_ADDRESS, + "amount": str(2**256 - 1), + "nonce": "0", + "deadline": str(int(time.time()) + 3600), + "signature": "0x" + "aa" * 65, + "version": "1", + } + defaults.update(overrides) + return Eip2612GasSponsoringInfo(**defaults) + + +def _make_payload(info: Eip2612GasSponsoringInfo | None = None) -> PaymentPayload: + ext = {} + if info is not None: + ext = {EIP2612_GAS_SPONSORING_KEY: {"info": info.to_dict()}} + return PaymentPayload( + x402_version=2, + resource=ResourceInfo(url="http://example.com", description="test", mime_type="text"), + accepted=PaymentRequirements( + scheme="exact", + network="eip155:84532", + asset=TOKEN_ADDRESS, + amount="1000", + pay_to="0x0987654321098765432109876543210987654321", + max_timeout_seconds=3600, + extra={"assetTransferMethod": "permit2"}, + ), + payload={"permit2Authorization": {"from": PAYER}}, + extensions=ext, + ) + + +class TestDeclaration: + def test_declare_returns_correct_key(self): + result = declare_eip2612_gas_sponsoring_extension() + assert EIP2612_GAS_SPONSORING_KEY in result + ext = result[EIP2612_GAS_SPONSORING_KEY] + assert "info" in ext + assert "schema" in ext + assert ext["info"]["version"] == "1" + + +class TestSerialization: + def test_roundtrip(self): + info = _make_info() + d = info.to_dict() + restored = Eip2612GasSponsoringInfo.from_dict(d) + assert restored.from_address == info.from_address + assert restored.asset == info.asset + assert restored.spender == info.spender + assert restored.amount == info.amount + assert restored.nonce == info.nonce + assert restored.deadline == info.deadline + assert restored.signature == info.signature + assert restored.version == info.version + + def test_to_dict_uses_camel_case(self): + info = _make_info() + d = info.to_dict() + assert "from" in d + assert "from_address" not in d + + +class TestExtraction: + def test_extract_from_payload(self): + info = _make_info() + payload = _make_payload(info) + result = extract_eip2612_gas_sponsoring_info(payload) + assert result is not None + assert result.from_address == PAYER + + def test_extract_returns_none_when_missing(self): + payload = _make_payload(None) + result = extract_eip2612_gas_sponsoring_info(payload) + assert result is None + + +class TestValidation: + def test_valid_info(self): + info = _make_info() + assert validate_eip2612_gas_sponsoring_info(info) is True + + def test_invalid_address(self): + info = _make_info(from_address="not-an-address") + assert validate_eip2612_gas_sponsoring_info(info) is False + + def test_invalid_amount(self): + info = _make_info(amount="abc") + assert validate_eip2612_gas_sponsoring_info(info) is False + + +class TestPaymentValidation: + def test_valid_permit(self): + info = _make_info() + assert validate_eip2612_permit_for_payment(info, PAYER, TOKEN_ADDRESS) == "" + + def test_from_mismatch(self): + info = _make_info() + assert "from_mismatch" in validate_eip2612_permit_for_payment( + info, "0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef", TOKEN_ADDRESS + ) + + def test_asset_mismatch(self): + info = _make_info() + assert "asset_mismatch" in validate_eip2612_permit_for_payment( + info, PAYER, "0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef" + ) + + def test_spender_not_permit2(self): + info = _make_info(spender="0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef") + assert "spender_not_permit2" in validate_eip2612_permit_for_payment( + info, PAYER, TOKEN_ADDRESS + ) + + def test_expired_deadline(self): + info = _make_info(deadline=str(int(time.time()) - 100)) + assert "deadline_expired" in validate_eip2612_permit_for_payment(info, PAYER, TOKEN_ADDRESS) diff --git a/python/x402/tests/unit/extensions/test_erc20_approval_gas_sponsoring.py b/python/x402/tests/unit/extensions/test_erc20_approval_gas_sponsoring.py new file mode 100644 index 0000000000..82ce5eaf1a --- /dev/null +++ b/python/x402/tests/unit/extensions/test_erc20_approval_gas_sponsoring.py @@ -0,0 +1,111 @@ +"""Tests for the ERC-20 Approval Gas Sponsoring extension.""" + +from __future__ import annotations + +from typing import Any + +from x402.extensions.erc20_approval_gas_sponsoring import ( + ERC20_APPROVAL_GAS_SPONSORING_KEY, + Erc20ApprovalGasSponsoringInfo, + declare_erc20_approval_gas_sponsoring_extension, + extract_erc20_approval_gas_sponsoring_info, + validate_erc20_approval_gas_sponsoring_info, +) +from x402.mechanisms.evm.constants import PERMIT2_ADDRESS +from x402.schemas import PaymentPayload, PaymentRequirements, ResourceInfo + +TOKEN_ADDRESS = "0x036CbD53842c5426634e7929541eC2318f3dCF7e" +PAYER = "0x1234567890123456789012345678901234567890" + + +def _make_info(**overrides: Any) -> Erc20ApprovalGasSponsoringInfo: + defaults = { + "from_address": PAYER, + "asset": TOKEN_ADDRESS, + "spender": PERMIT2_ADDRESS, + "amount": str(2**256 - 1), + "signed_transaction": "0x" + "ff" * 100, + "version": "1", + } + defaults.update(overrides) + return Erc20ApprovalGasSponsoringInfo(**defaults) + + +def _make_payload(info: Erc20ApprovalGasSponsoringInfo | None = None) -> PaymentPayload: + ext = {} + if info is not None: + ext = {ERC20_APPROVAL_GAS_SPONSORING_KEY: {"info": info.to_dict()}} + return PaymentPayload( + x402_version=2, + resource=ResourceInfo(url="http://example.com", description="test", mime_type="text"), + accepted=PaymentRequirements( + scheme="exact", + network="eip155:84532", + asset=TOKEN_ADDRESS, + amount="1000", + pay_to="0x0987654321098765432109876543210987654321", + max_timeout_seconds=3600, + extra={"assetTransferMethod": "permit2"}, + ), + payload={"permit2Authorization": {"from": PAYER}}, + extensions=ext, + ) + + +class TestDeclaration: + def test_declare_returns_correct_key(self): + result = declare_erc20_approval_gas_sponsoring_extension() + assert ERC20_APPROVAL_GAS_SPONSORING_KEY in result + ext = result[ERC20_APPROVAL_GAS_SPONSORING_KEY] + assert "info" in ext + assert "schema" in ext + assert ext["info"]["version"] == "1" + + +class TestSerialization: + def test_roundtrip(self): + info = _make_info() + d = info.to_dict() + restored = Erc20ApprovalGasSponsoringInfo.from_dict(d) + assert restored.from_address == info.from_address + assert restored.asset == info.asset + assert restored.spender == info.spender + assert restored.amount == info.amount + assert restored.signed_transaction == info.signed_transaction + assert restored.version == info.version + + def test_to_dict_uses_camel_case(self): + info = _make_info() + d = info.to_dict() + assert "from" in d + assert "signedTransaction" in d + assert "from_address" not in d + assert "signed_transaction" not in d + + +class TestExtraction: + def test_extract_from_payload(self): + info = _make_info() + payload = _make_payload(info) + result = extract_erc20_approval_gas_sponsoring_info(payload) + assert result is not None + assert result.from_address == PAYER + + def test_extract_returns_none_when_missing(self): + payload = _make_payload(None) + result = extract_erc20_approval_gas_sponsoring_info(payload) + assert result is None + + +class TestValidation: + def test_valid_info(self): + info = _make_info() + assert validate_erc20_approval_gas_sponsoring_info(info) is True + + def test_invalid_address(self): + info = _make_info(from_address="not-an-address") + assert validate_erc20_approval_gas_sponsoring_info(info) is False + + def test_invalid_signed_transaction(self): + info = _make_info(signed_transaction="not-hex") + assert validate_erc20_approval_gas_sponsoring_info(info) is False diff --git a/python/x402/tests/unit/http/middleware/test_fastapi.py b/python/x402/tests/unit/http/middleware/test_fastapi.py index 91a0cb0764..972adfb1de 100644 --- a/python/x402/tests/unit/http/middleware/test_fastapi.py +++ b/python/x402/tests/unit/http/middleware/test_fastapi.py @@ -2,15 +2,17 @@ from __future__ import annotations -from unittest.mock import MagicMock +from unittest.mock import AsyncMock, MagicMock, patch import pytest # Skip all tests if fastapi not installed pytest.importorskip("fastapi") -from fastapi import Request +from fastapi import FastAPI, Request +from fastapi.testclient import TestClient from starlette.datastructures import Headers, QueryParams +from x402.http.facilitator_client_base import FacilitatorResponseError from x402.http.middleware.fastapi import ( FastAPIAdapter, PaymentMiddlewareASGI, @@ -18,7 +20,10 @@ payment_middleware, ) from x402.http.types import ( + HTTPProcessResult, + HTTPResponseInstructions, PaymentOption, + ProcessSettleResult, RouteConfig, ) from x402.schemas import PaymentPayload, PaymentRequirements @@ -318,6 +323,347 @@ async def call_next(req): assert response == expected_response +# ============================================================================= +# Integration-style Tests +# ============================================================================= + + +class TestFastAPIMiddlewareIntegration: + """Integration-style tests for FastAPI payment middleware.""" + + def test_settlement_success_adds_headers(self): + """Test that settlement success adds PAYMENT-RESPONSE header.""" + app = FastAPI() + + @app.get("/api/protected") + def protected_route(): + return {"data": "Protected content"} + + mock_server = MagicMock() + routes = { + "GET /api/protected": RouteConfig( + accepts=PaymentOption( + scheme="exact", + pay_to="0x1234567890123456789012345678901234567890", + price="$0.01", + network="eip155:8453", + ), + ) + } + + payment_payload = make_v2_payload() + payment_requirements = make_payment_requirements() + + with patch("x402.http.middleware.fastapi.x402HTTPResourceServer") as mock_http_server: + mock_http_server_instance = MagicMock() + mock_http_server_instance.requires_payment.return_value = True + mock_http_server_instance.process_http_request = AsyncMock( + return_value=HTTPProcessResult( + type="payment-verified", + payment_payload=payment_payload, + payment_requirements=payment_requirements, + ) + ) + mock_http_server_instance.process_settlement = AsyncMock( + return_value=ProcessSettleResult( + success=True, + headers={"PAYMENT-RESPONSE": "settlement_encoded"}, + ) + ) + mock_http_server.return_value = mock_http_server_instance + + @app.middleware("http") + async def x402_middleware(request: Request, call_next): + return await payment_middleware( + routes, mock_server, sync_facilitator_on_start=False + )(request, call_next) + + with TestClient(app) as client: + response = client.get( + "/api/protected", + headers={"PAYMENT-SIGNATURE": "valid_payment"}, + ) + assert response.status_code == 200 + assert response.json() == {"data": "Protected content"} + assert "PAYMENT-RESPONSE" in response.headers + + def test_settlement_failure_returns_402(self): + """Test that settlement failure returns 402 with empty body and PAYMENT-RESPONSE header.""" + app = FastAPI() + + @app.get("/api/protected") + def protected_route(): + return {"data": "Protected content"} + + mock_server = MagicMock() + routes = { + "GET /api/protected": RouteConfig( + accepts=PaymentOption( + scheme="exact", + pay_to="0x1234567890123456789012345678901234567890", + price="$0.01", + network="eip155:8453", + ), + ) + } + + payment_payload = make_v2_payload() + payment_requirements = make_payment_requirements() + + with patch("x402.http.middleware.fastapi.x402HTTPResourceServer") as mock_http_server: + mock_http_server_instance = MagicMock() + mock_http_server_instance.requires_payment.return_value = True + mock_http_server_instance.process_http_request = AsyncMock( + return_value=HTTPProcessResult( + type="payment-verified", + payment_payload=payment_payload, + payment_requirements=payment_requirements, + ) + ) + mock_http_server_instance.process_settlement = AsyncMock( + return_value=ProcessSettleResult( + success=False, + error_reason="Insufficient funds", + response=HTTPResponseInstructions( + status=402, + headers={ + "Content-Type": "application/json", + "PAYMENT-RESPONSE": "base64encoded", + }, + body={}, + ), + ) + ) + mock_http_server.return_value = mock_http_server_instance + + @app.middleware("http") + async def x402_middleware(request: Request, call_next): + return await payment_middleware( + routes, mock_server, sync_facilitator_on_start=False + )(request, call_next) + + with TestClient(app) as client: + response = client.get("/api/protected") + assert response.status_code == 402 + assert response.json() == {} + assert "PAYMENT-RESPONSE" in response.headers + + def test_invalid_facilitator_verify_response_returns_502(self): + """Test that invalid facilitator data during verify returns 502 instead of 500.""" + app = FastAPI() + + @app.get("/api/protected") + def protected_route(): + return {"data": "Protected content"} + + mock_server = MagicMock() + routes = { + "GET /api/protected": RouteConfig( + accepts=PaymentOption( + scheme="exact", + pay_to="0x1234567890123456789012345678901234567890", + price="$0.01", + network="eip155:8453", + ), + ) + } + + with patch("x402.http.middleware.fastapi.x402HTTPResourceServer") as mock_http_server: + mock_http_server_instance = MagicMock() + mock_http_server_instance.requires_payment.return_value = True + mock_http_server_instance.process_http_request = AsyncMock( + side_effect=FacilitatorResponseError( + "Facilitator verify returned invalid JSON: not-json" + ) + ) + mock_http_server.return_value = mock_http_server_instance + + @app.middleware("http") + async def x402_middleware(request: Request, call_next): + return await payment_middleware( + routes, mock_server, sync_facilitator_on_start=False + )(request, call_next) + + with TestClient(app) as client: + response = client.get("/api/protected") + assert response.status_code == 502 + assert response.json() == { + "error": "Facilitator verify returned invalid JSON: not-json" + } + + def test_invalid_facilitator_settlement_response_returns_502(self): + """Test that invalid facilitator data during settlement returns 502.""" + app = FastAPI() + + @app.get("/api/protected") + def protected_route(): + return {"data": "Protected content"} + + mock_server = MagicMock() + routes = { + "GET /api/protected": RouteConfig( + accepts=PaymentOption( + scheme="exact", + pay_to="0x1234567890123456789012345678901234567890", + price="$0.01", + network="eip155:8453", + ), + ) + } + + payment_payload = make_v2_payload() + payment_requirements = make_payment_requirements() + + with patch("x402.http.middleware.fastapi.x402HTTPResourceServer") as mock_http_server: + mock_http_server_instance = MagicMock() + mock_http_server_instance.requires_payment.return_value = True + mock_http_server_instance.process_http_request = AsyncMock( + return_value=HTTPProcessResult( + type="payment-verified", + payment_payload=payment_payload, + payment_requirements=payment_requirements, + ) + ) + mock_http_server_instance.process_settlement = AsyncMock( + side_effect=FacilitatorResponseError( + "Facilitator settle returned invalid data: {'success': true}" + ) + ) + mock_http_server.return_value = mock_http_server_instance + + @app.middleware("http") + async def x402_middleware(request: Request, call_next): + return await payment_middleware( + routes, mock_server, sync_facilitator_on_start=False + )(request, call_next) + + with TestClient(app) as client: + response = client.get("/api/protected") + assert response.status_code == 502 + assert response.json() == { + "error": "Facilitator settle returned invalid data: {'success': true}" + } + + +# ============================================================================= +# Concurrency Tests +# ============================================================================= + + +class TestFastAPIMiddlewareConcurrency: + """Tests for concurrency-safe lazy facilitator initialization.""" + + @pytest.mark.asyncio + async def test_concurrent_requests_initialize_only_once(self): + """Test that concurrent requests only trigger one initialization call.""" + import asyncio + + app = FastAPI() + + @app.get("/api/protected") + def protected_route(): + return {"data": "Protected content"} + + mock_server = MagicMock() + routes = { + "GET /api/protected": RouteConfig( + accepts=PaymentOption( + scheme="exact", + pay_to="0x1234567890123456789012345678901234567890", + price="$0.01", + network="eip155:8453", + ), + ) + } + + init_call_count = 0 + + with patch("x402.http.middleware.fastapi.x402HTTPResourceServer") as mock_http_server: + mock_http_server_instance = MagicMock() + mock_http_server_instance.requires_payment.return_value = True + mock_http_server_instance.process_http_request = AsyncMock( + return_value=HTTPProcessResult( + type="payment-error", + response=HTTPResponseInstructions( + status=402, + headers={"PAYMENT-REQUIRED": "encoded"}, + body={"error": "Payment required"}, + ), + ) + ) + + def slow_initialize(): + nonlocal init_call_count + init_call_count += 1 + + mock_http_server_instance.initialize.side_effect = slow_initialize + mock_http_server.return_value = mock_http_server_instance + + mw = payment_middleware(routes, mock_server, sync_facilitator_on_start=True) + + request1 = make_mock_fastapi_request(path="/api/protected") + request2 = make_mock_fastapi_request(path="/api/protected") + request3 = make_mock_fastapi_request(path="/api/protected") + + async def call_next(req): + return MagicMock() + + await asyncio.gather( + mw(request1, call_next), + mw(request2, call_next), + mw(request3, call_next), + ) + + assert init_call_count == 1, ( + f"Expected initialize() to be called exactly once, got {init_call_count}" + ) + + @pytest.mark.asyncio + async def test_init_error_does_not_block_subsequent_requests(self): + """Test that a failed init allows subsequent requests to retry.""" + mock_server = MagicMock() + routes = { + "GET /api/protected": RouteConfig( + accepts=PaymentOption( + scheme="exact", + pay_to="0x1234567890123456789012345678901234567890", + price="$0.01", + network="eip155:8453", + ), + ) + } + + call_count = 0 + + with patch("x402.http.middleware.fastapi.x402HTTPResourceServer") as mock_http_server: + mock_http_server_instance = MagicMock() + mock_http_server_instance.requires_payment.return_value = True + + def failing_initialize(): + nonlocal call_count + call_count += 1 + raise FacilitatorResponseError("Connection refused") + + mock_http_server_instance.initialize.side_effect = failing_initialize + mock_http_server.return_value = mock_http_server_instance + + mw = payment_middleware(routes, mock_server, sync_facilitator_on_start=True) + + request = make_mock_fastapi_request(path="/api/protected") + + async def call_next(req): + return MagicMock() + + # First request fails + response1 = await mw(request, call_next) + assert response1.status_code == 502 + + # Second request should also attempt init since first failed + response2 = await mw(request, call_next) + assert response2.status_code == 502 + assert call_count == 2 + + # ============================================================================= # ASGI Middleware Class Tests # ============================================================================= diff --git a/python/x402/tests/unit/http/middleware/test_flask.py b/python/x402/tests/unit/http/middleware/test_flask.py index bf538ff210..044d066a12 100644 --- a/python/x402/tests/unit/http/middleware/test_flask.py +++ b/python/x402/tests/unit/http/middleware/test_flask.py @@ -3,6 +3,7 @@ from __future__ import annotations import json +import threading from typing import Any from unittest.mock import MagicMock, patch @@ -13,6 +14,7 @@ from flask import Flask from werkzeug.datastructures import Headers, ImmutableMultiDict +from x402.http.facilitator_client_base import FacilitatorResponseError from x402.http.middleware.flask import ( FlaskAdapter, PaymentMiddleware, @@ -358,6 +360,117 @@ def test_stores_original_wsgi(self): assert middleware._original_wsgi == original_wsgi +class TestFlaskMiddlewareConcurrency: + """Tests for concurrency-safe lazy facilitator initialization.""" + + def test_concurrent_threads_initialize_only_once(self): + """Test that concurrent threads only trigger one initialization call.""" + import concurrent.futures + + app = Flask(__name__) + + @app.route("/api/protected") + def protected_route(): + return "Protected content" + + mock_server = MagicMock() + routes = { + "GET /api/protected": RouteConfig( + accepts=PaymentOption( + scheme="exact", + pay_to="0x1234567890123456789012345678901234567890", + price="$0.01", + network="eip155:8453", + ), + ) + } + + init_call_count = 0 + count_lock = threading.Lock() + + with patch("x402.http.middleware.flask.x402HTTPResourceServerSync") as mock_http_server: + mock_http_server_instance = MagicMock() + mock_http_server_instance.requires_payment.return_value = True + mock_http_server_instance.process_http_request.return_value = HTTPProcessResult( + type="payment-error", + response=HTTPResponseInstructions( + status=402, + headers={"PAYMENT-REQUIRED": "encoded"}, + body={"error": "Payment required"}, + ), + ) + + def counting_initialize(): + nonlocal init_call_count + with count_lock: + init_call_count += 1 + + mock_http_server_instance.initialize.side_effect = counting_initialize + mock_http_server.return_value = mock_http_server_instance + + PaymentMiddleware(app, routes, mock_server, sync_facilitator_on_start=True) + + def make_request(): + with app.test_client() as client: + return client.get("/api/protected") + + with concurrent.futures.ThreadPoolExecutor(max_workers=5) as executor: + futures = [executor.submit(make_request) for _ in range(5)] + responses = [f.result() for f in futures] + + assert init_call_count == 1, ( + f"Expected initialize() to be called exactly once, got {init_call_count}" + ) + for resp in responses: + assert resp.status_code == 402 + + def test_init_error_does_not_block_subsequent_requests(self): + """Test that a failed init allows subsequent requests to retry.""" + app = Flask(__name__) + + @app.route("/api/protected") + def protected_route(): + return "Protected content" + + mock_server = MagicMock() + routes = { + "GET /api/protected": RouteConfig( + accepts=PaymentOption( + scheme="exact", + pay_to="0x1234567890123456789012345678901234567890", + price="$0.01", + network="eip155:8453", + ), + ) + } + + call_count = 0 + + with patch("x402.http.middleware.flask.x402HTTPResourceServerSync") as mock_http_server: + mock_http_server_instance = MagicMock() + mock_http_server_instance.requires_payment.return_value = True + + def failing_initialize(): + nonlocal call_count + call_count += 1 + raise FacilitatorResponseError("Connection refused") + + mock_http_server_instance.initialize.side_effect = failing_initialize + mock_http_server.return_value = mock_http_server_instance + + PaymentMiddleware(app, routes, mock_server, sync_facilitator_on_start=True) + + with app.test_client() as client: + # First request fails + response1 = client.get("/api/protected") + assert response1.status_code == 502 + + # Second request retries init since first failed + response2 = client.get("/api/protected") + assert response2.status_code == 502 + assert call_count == 2 + + class TestPaymentMiddlewareFunction: """Tests for payment_middleware convenience function.""" @@ -522,7 +635,7 @@ def protected_route(): assert "PAYMENT-RESPONSE" in response.headers def test_settlement_failure_returns_402(self): - """Test that settlement failure returns 402.""" + """Test that settlement failure returns 402 with empty body and PAYMENT-RESPONSE header.""" app = Flask(__name__) @app.route("/api/protected") @@ -546,6 +659,14 @@ def protected_route(): mock_http_server_instance.process_settlement.return_value = ProcessSettleResult( success=False, error_reason="Insufficient funds", + response=HTTPResponseInstructions( + status=402, + headers={ + "Content-Type": "application/json", + "PAYMENT-RESPONSE": "base64encoded", + }, + body={}, + ), ) mock_http_server.return_value = mock_http_server_instance @@ -555,5 +676,87 @@ def protected_route(): response = client.get("/api/protected") assert response.status_code == 402 data = json.loads(response.data) - assert data["error"] == "Settlement failed" - assert data["details"] == "Insufficient funds" + assert data == {} + assert "PAYMENT-RESPONSE" in response.headers + + def test_invalid_facilitator_verify_response_returns_502(self): + """Test that invalid facilitator data during verify returns 502 instead of 500.""" + app = Flask(__name__) + + @app.route("/api/protected") + def protected_route(): + return "Protected content" + + mock_server = MagicMock() + routes = { + "GET /api/protected": RouteConfig( + accepts=PaymentOption( + scheme="exact", + pay_to="0x1234567890123456789012345678901234567890", + price="$0.01", + network="eip155:8453", + ), + ) + } + + with patch("x402.http.middleware.flask.x402HTTPResourceServerSync") as mock_http_server: + mock_http_server_instance = MagicMock() + mock_http_server_instance.requires_payment.return_value = True + mock_http_server_instance.process_http_request.side_effect = FacilitatorResponseError( + "Facilitator verify returned invalid JSON: not-json" + ) + mock_http_server.return_value = mock_http_server_instance + + PaymentMiddleware(app, routes, mock_server, sync_facilitator_on_start=False) + + with app.test_client() as client: + response = client.get("/api/protected") + assert response.status_code == 502 + assert response.get_json() == { + "error": "Facilitator verify returned invalid JSON: not-json" + } + + def test_invalid_facilitator_settlement_response_returns_502(self): + """Test that invalid facilitator data during settlement returns 502.""" + app = Flask(__name__) + + @app.route("/api/protected") + def protected_route(): + return "Protected content" + + mock_server = MagicMock() + routes = { + "GET /api/protected": RouteConfig( + accepts=PaymentOption( + scheme="exact", + pay_to="0x1234567890123456789012345678901234567890", + price="$0.01", + network="eip155:8453", + ), + ) + } + + payment_payload = make_v2_payload() + payment_requirements = make_payment_requirements() + + with patch("x402.http.middleware.flask.x402HTTPResourceServerSync") as mock_http_server: + mock_http_server_instance = MagicMock() + mock_http_server_instance.requires_payment.return_value = True + mock_http_server_instance.process_http_request.return_value = HTTPProcessResult( + type="payment-verified", + payment_payload=payment_payload, + payment_requirements=payment_requirements, + ) + mock_http_server_instance.process_settlement.side_effect = FacilitatorResponseError( + "Facilitator settle returned invalid data: {'success': true}" + ) + mock_http_server.return_value = mock_http_server_instance + + PaymentMiddleware(app, routes, mock_server, sync_facilitator_on_start=False) + + with app.test_client() as client: + response = client.get("/api/protected") + assert response.status_code == 502 + assert response.get_json() == { + "error": "Facilitator settle returned invalid data: {'success': true}" + } diff --git a/python/x402/tests/unit/http/test_facilitator_client.py b/python/x402/tests/unit/http/test_facilitator_client.py new file mode 100644 index 0000000000..03d3ef5b58 --- /dev/null +++ b/python/x402/tests/unit/http/test_facilitator_client.py @@ -0,0 +1,117 @@ +"""Unit tests for x402.http.facilitator_client.""" + +from __future__ import annotations + +import json +from unittest.mock import AsyncMock, MagicMock + +import pytest + +from x402.http.facilitator_client import ( + HTTPFacilitatorClient, + HTTPFacilitatorClientSync, +) +from x402.http.facilitator_client_base import ( + FacilitatorConfig, + FacilitatorResponseError, +) +from x402.schemas import PaymentPayload, PaymentRequirements + + +def make_payment_requirements() -> PaymentRequirements: + """Helper to create valid PaymentRequirements.""" + return PaymentRequirements( + scheme="exact", + network="eip155:8453", + asset="0x0000000000000000000000000000000000000000", + amount="1000000", + pay_to="0x1234567890123456789012345678901234567890", + max_timeout_seconds=300, + ) + + +def make_v2_payload(signature: str = "0xmock") -> PaymentPayload: + """Helper to create valid V2 PaymentPayload.""" + return PaymentPayload( + x402_version=2, + payload={"signature": signature}, + accepted=make_payment_requirements(), + ) + + +@pytest.mark.asyncio +async def test_async_verify_raises_facilitator_response_error_for_invalid_json(): + """Async verify should surface invalid JSON as facilitator boundary error.""" + response = MagicMock(status_code=200, text="not-json") + response.json.side_effect = json.JSONDecodeError("Expecting value", "not-json", 0) + + http_client = MagicMock() + http_client.post = AsyncMock(return_value=response) + + client = HTTPFacilitatorClient( + FacilitatorConfig(url="https://facilitator.test", http_client=http_client) + ) + + with pytest.raises( + FacilitatorResponseError, + match="Facilitator verify returned invalid JSON", + ): + await client.verify(make_v2_payload(), make_payment_requirements()) + + +@pytest.mark.asyncio +async def test_async_settle_raises_facilitator_response_error_for_invalid_schema(): + """Async settle should surface schema drift as facilitator boundary error.""" + response = MagicMock(status_code=200, text='{"success": true}') + response.json.return_value = {"success": True} + + http_client = MagicMock() + http_client.post = AsyncMock(return_value=response) + + client = HTTPFacilitatorClient( + FacilitatorConfig(url="https://facilitator.test", http_client=http_client) + ) + + with pytest.raises( + FacilitatorResponseError, + match="Facilitator settle returned invalid data", + ): + await client.settle(make_v2_payload(), make_payment_requirements()) + + +def test_sync_verify_raises_facilitator_response_error_for_invalid_json(): + """Sync verify should surface invalid JSON as facilitator boundary error.""" + response = MagicMock(status_code=200, text="not-json") + response.json.side_effect = json.JSONDecodeError("Expecting value", "not-json", 0) + + http_client = MagicMock() + http_client.post.return_value = response + + client = HTTPFacilitatorClientSync( + FacilitatorConfig(url="https://facilitator.test", http_client=http_client) + ) + + with pytest.raises( + FacilitatorResponseError, + match="Facilitator verify returned invalid JSON", + ): + client.verify(make_v2_payload(), make_payment_requirements()) + + +def test_sync_settle_raises_facilitator_response_error_for_invalid_schema(): + """Sync settle should surface schema drift as facilitator boundary error.""" + response = MagicMock(status_code=200, text='{"success": true}') + response.json.return_value = {"success": True} + + http_client = MagicMock() + http_client.post.return_value = response + + client = HTTPFacilitatorClientSync( + FacilitatorConfig(url="https://facilitator.test", http_client=http_client) + ) + + with pytest.raises( + FacilitatorResponseError, + match="Facilitator settle returned invalid data", + ): + client.settle(make_v2_payload(), make_payment_requirements()) diff --git a/python/x402/tests/unit/mechanisms/evm/test_client.py b/python/x402/tests/unit/mechanisms/evm/test_client.py index 179a80ae12..01dada6b10 100644 --- a/python/x402/tests/unit/mechanisms/evm/test_client.py +++ b/python/x402/tests/unit/mechanisms/evm/test_client.py @@ -8,9 +8,9 @@ pytest.skip("EVM client requires eth_account", allow_module_level=True) -from x402.mechanisms.evm import get_asset_info from x402.mechanisms.evm.exact import ExactEvmClientScheme from x402.mechanisms.evm.signers import EthAccountSigner +from x402.mechanisms.evm.utils import get_asset_info from x402.schemas import PaymentRequirements @@ -62,7 +62,7 @@ def test_should_accept_v2_requirements_with_amount_field(self): requirements = PaymentRequirements( scheme="exact", network=network, - asset=get_asset_info(network, "USDC")["address"], + asset="0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", # USDC on Base amount="500000", # V2 uses 'amount' pay_to="0x0987654321098765432109876543210987654321", max_timeout_seconds=3600, @@ -86,7 +86,7 @@ def test_requirements_must_have_eip712_domain(self): requirements = PaymentRequirements( scheme="exact", network=network, - asset=get_asset_info(network, "USDC")["address"], + asset="0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", # USDC on Base amount="100000", pay_to="0x0987654321098765432109876543210987654321", max_timeout_seconds=3600, @@ -162,7 +162,7 @@ def test_raw_local_account_can_sign_payload(self): requirements = PaymentRequirements( scheme="exact", network=network, - asset=get_asset_info(network, "USDC")["address"], + asset=get_asset_info(network, "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913")["address"], amount="500000", pay_to="0x0987654321098765432109876543210987654321", max_timeout_seconds=3600, diff --git a/python/x402/tests/unit/mechanisms/evm/test_facilitator.py b/python/x402/tests/unit/mechanisms/evm/test_facilitator.py index ba0157c505..be4bc71346 100644 --- a/python/x402/tests/unit/mechanisms/evm/test_facilitator.py +++ b/python/x402/tests/unit/mechanisms/evm/test_facilitator.py @@ -1,30 +1,220 @@ -"""Tests for ExactEvmScheme facilitator.""" - -from x402.mechanisms.evm import get_asset_info +"""Tests for the exact EVM facilitator's simulation-based verification flow.""" + +from __future__ import annotations + +import time + +import pytest + +try: + from eth_abi import encode as abi_encode +except ImportError: + pytest.skip("eth-abi not available", allow_module_level=True) + +from x402.mechanisms.evm import ERC6492_MAGIC_VALUE, get_network_config +from x402.mechanisms.evm.constants import ( + ERR_AUTHORIZATION_VALUE_MISMATCH, + ERR_INSUFFICIENT_BALANCE, + ERR_INVALID_SIGNATURE, + ERR_NONCE_ALREADY_USED, + ERR_TOKEN_NAME_MISMATCH, + ERR_TOKEN_VERSION_MISMATCH, + ERR_TRANSACTION_SIMULATION_FAILED, + ERR_UNDEPLOYED_SMART_WALLET, +) from x402.mechanisms.evm.exact import ExactEvmFacilitatorScheme, ExactEvmSchemeConfig +from x402.mechanisms.evm.exact.v1.facilitator import ExactEvmSchemeV1 +from x402.mechanisms.evm.types import TransactionReceipt from x402.schemas import PaymentPayload, PaymentRequirements, ResourceInfo +from x402.schemas.v1 import PaymentPayloadV1, PaymentRequirementsV1 + +NETWORK = "eip155:8453" +TOKEN_ADDRESS = get_network_config(NETWORK)["default_asset"]["address"] +PAYER = "0x1234567890123456789012345678901234567890" +RECIPIENT = "0x0987654321098765432109876543210987654321" +FACILITATOR = "0x1111111111111111111111111111111111111111" +FACTORY = "0x2222222222222222222222222222222222222222" +NONCE = "0x" + "11" * 32 + + +def make_payment_payload( + *, + signature: str = "0x" + "00" * 65, + accepted_scheme: str = "exact", + accepted_network: str = NETWORK, + pay_to: str = RECIPIENT, + amount: str = "100000", + extra: dict | None = None, + authorization_overrides: dict | None = None, +) -> PaymentPayload: + now = int(time.time()) + authorization = { + "from": PAYER, + "to": RECIPIENT, + "value": amount, + "validAfter": str(now - 60), + "validBefore": str(now + 600), + "nonce": NONCE, + } + if authorization_overrides: + authorization.update(authorization_overrides) + + return PaymentPayload( + x402_version=2, + resource=ResourceInfo( + url="http://example.com/protected", + description="Test resource", + mime_type="application/json", + ), + accepted=PaymentRequirements( + scheme=accepted_scheme, + network=accepted_network, + asset=TOKEN_ADDRESS, + amount=amount, + pay_to=pay_to, + max_timeout_seconds=3600, + extra=extra if extra is not None else {"name": "USD Coin", "version": "2"}, + ), + payload={"authorization": authorization, "signature": signature}, + ) + + +def make_requirements( + *, + scheme: str = "exact", + network: str = NETWORK, + amount: str = "100000", + pay_to: str = RECIPIENT, + extra: dict | None = None, +) -> PaymentRequirements: + return PaymentRequirements( + scheme=scheme, + network=network, + asset=TOKEN_ADDRESS, + amount=amount, + pay_to=pay_to, + max_timeout_seconds=3600, + extra=extra if extra is not None else {"name": "USD Coin", "version": "2"}, + ) + + +def make_payment_payload_v1( + *, + signature: str = "0x" + "00" * 65, + scheme: str = "exact", + network: str = "base", + pay_to: str = RECIPIENT, + amount: str = "100000", + extra: dict | None = None, + authorization_overrides: dict | None = None, +) -> PaymentPayloadV1: + now = int(time.time()) + authorization = { + "from": PAYER, + "to": RECIPIENT, + "value": amount, + "validAfter": str(now - 60), + "validBefore": str(now + 600), + "nonce": NONCE, + } + if authorization_overrides: + authorization.update(authorization_overrides) + + return PaymentPayloadV1( + x402_version=1, + scheme=scheme, + network=network, + payload={"authorization": authorization, "signature": signature}, + ) + + +def make_requirements_v1( + *, + scheme: str = "exact", + network: str = "base", + amount: str = "100000", + pay_to: str = RECIPIENT, + extra: dict | None = None, +) -> PaymentRequirementsV1: + return PaymentRequirementsV1( + scheme=scheme, + network=network, + asset=TOKEN_ADDRESS, + max_amount_required=amount, + pay_to=pay_to, + max_timeout_seconds=3600, + resource="http://example.com/protected", + extra=extra if extra is not None else {"name": "USD Coin", "version": "2"}, + ) + + +def encode_result(abi_type: str, value): + return abi_encode([abi_type], [value]) + + +def make_diagnostic_results( + *, + balance: int = 100000, + name: str = "USD Coin", + version: str = "2", + nonce_used: bool = False, + authorization_state_supported: bool = True, +) -> list[tuple[bool, bytes]]: + return [ + (True, encode_result("uint256", balance)), + (True, encode_result("string", name)), + (True, encode_result("string", version)), + ( + authorization_state_supported, + encode_result("bool", nonce_used) if authorization_state_supported else b"", + ), + ] + + +def make_erc6492_signature(inner_signature: bytes) -> str: + payload = abi_encode( + ["address", "bytes", "bytes"], [FACTORY, b"\xde\xad\xbe\xef", inner_signature] + ) + return "0x" + (payload + ERC6492_MAGIC_VALUE).hex() class MockFacilitatorSigner: - """Mock facilitator signer for testing.""" + """Mock signer that exposes just enough behavior for facilitator tests.""" - def __init__(self, addresses: list[str] | None = None): - self._addresses = addresses or ["0xFacilitator123456789012345678901234567890"] + def __init__( + self, + *, + addresses: list[str] | None = None, + typed_data_valid: bool = True, + code: bytes = b"", + transfer_simulation_should_revert: bool = False, + multicall_results: list[tuple[bool, bytes]] | None = None, + deploy_tx_hash: str = "0x" + "12" * 32, + ): + self._addresses = addresses or [FACILITATOR] + self.typed_data_valid = typed_data_valid + self.code = code + self.transfer_simulation_should_revert = transfer_simulation_should_revert + self.multicall_results = multicall_results or [] + self.deploy_tx_hash = deploy_tx_hash + self.transfer_simulation_calls = 0 + self.write_calls = 0 + self.send_calls = 0 def get_addresses(self) -> list[str]: return self._addresses - def read_contract( - self, - address: str, - abi: list[dict], - function_name: str, - *args, - ) -> bool: - # Mock authorizationState - return False (nonce not used) - if function_name == "authorizationState": - return False - return False + def read_contract(self, address: str, abi: list[dict], function_name: str, *args): + if function_name == "tryAggregate": + return self.multicall_results + + if function_name == "transferWithAuthorization": + self.transfer_simulation_calls += 1 + if self.transfer_simulation_should_revert: + raise RuntimeError("simulation reverted") + return None + + raise AssertionError(f"unexpected read_contract call: {function_name}") def verify_typed_data( self, @@ -35,400 +225,323 @@ def verify_typed_data( message: dict, signature: bytes, ) -> bool: - # Mock verification - always return True for testing - return True + return self.typed_data_valid - def write_contract( - self, - address: str, - abi: list[dict], - function_name: str, - *args, - ) -> str: - return "0x" + "00" * 32 # Mock transaction hash + def write_contract(self, address: str, abi: list[dict], function_name: str, *args) -> str: + self.write_calls += 1 + return "0x" + "34" * 32 def send_transaction(self, to: str, data: bytes) -> str: - return "0x" + "00" * 32 - - def wait_for_transaction_receipt(self, tx_hash: str): - from x402.mechanisms.evm.types import TransactionReceipt + self.send_calls += 1 + return self.deploy_tx_hash + def wait_for_transaction_receipt(self, tx_hash: str) -> TransactionReceipt: return TransactionReceipt(status=1, block_number=1, tx_hash=tx_hash) def get_balance(self, address: str, token_address: str) -> int: - return 1000000000 # Mock balance + return 1_000_000_000 def get_chain_id(self) -> int: return 8453 def get_code(self, address: str) -> bytes: - return b"" # Mock EOA (no code) + return self.code class TestExactEvmSchemeConstructor: - """Test ExactEvmScheme facilitator constructor.""" - - def test_should_create_instance_with_correct_scheme(self): - """Should create instance with correct scheme.""" + def test_creates_instance_with_config(self): signer = MockFacilitatorSigner() - facilitator = ExactEvmFacilitatorScheme(signer) - - assert facilitator.scheme == "exact" + config = ExactEvmSchemeConfig( + deploy_erc4337_with_eip6492=True, + simulate_in_settle=True, + ) - def test_should_create_instance_with_config(self): - """Should create instance with config.""" - signer = MockFacilitatorSigner() - config = ExactEvmSchemeConfig(deploy_erc4337_with_eip6492=True) facilitator = ExactEvmFacilitatorScheme(signer, config) assert facilitator.scheme == "exact" assert facilitator._config.deploy_erc4337_with_eip6492 is True + assert facilitator._config.simulate_in_settle is True class TestVerify: - """Test verify method.""" - - def test_should_reject_if_scheme_does_not_match(self): - """Should reject if scheme does not match.""" + def test_rejects_wrong_scheme(self): signer = MockFacilitatorSigner() facilitator = ExactEvmFacilitatorScheme(signer) - network = "eip155:8453" - - payload = PaymentPayload( - x402_version=2, - resource=ResourceInfo( - url="http://example.com/protected", - description="Test resource", - mime_type="application/json", - ), - accepted=PaymentRequirements( - scheme="wrong", # Wrong scheme - network=network, - asset=get_asset_info(network, "USDC")["address"], - amount="100000", - pay_to="0x0987654321098765432109876543210987654321", - max_timeout_seconds=3600, - extra={"name": "USD Coin", "version": "2"}, - ), - payload={ - "authorization": { - "from": "0x1234567890123456789012345678901234567890", - "to": "0x0987654321098765432109876543210987654321", - "value": "100000", - "validAfter": "1000000000", - "validBefore": "1000003600", - "nonce": "0x" + "00" * 32, - }, - "signature": "0x" + "00" * 65, - }, - ) - requirements = PaymentRequirements( - scheme="exact", - network=network, - asset=get_asset_info(network, "USDC")["address"], - amount="100000", - pay_to="0x0987654321098765432109876543210987654321", - max_timeout_seconds=3600, - extra={"name": "USD Coin", "version": "2"}, + result = facilitator.verify( + make_payment_payload(accepted_scheme="wrong"), + make_requirements(), ) - result = facilitator.verify(payload, requirements) - assert result.is_valid is False assert "unsupported_scheme" in result.invalid_reason - def test_should_reject_if_network_does_not_match(self): - """Should reject if network does not match.""" + def test_rejects_wrong_network(self): signer = MockFacilitatorSigner() facilitator = ExactEvmFacilitatorScheme(signer) - payload = PaymentPayload( - x402_version=2, - resource=ResourceInfo( - url="http://example.com/protected", - description="Test resource", - mime_type="application/json", - ), - accepted=PaymentRequirements( - scheme="exact", - network="eip155:1", # Ethereum Mainnet - asset=get_asset_info("eip155:1", "USDC")["address"], - amount="100000", - pay_to="0x0987654321098765432109876543210987654321", - max_timeout_seconds=3600, - extra={"name": "USD Coin", "version": "2"}, - ), - payload={ - "authorization": { - "from": "0x1234567890123456789012345678901234567890", - "to": "0x0987654321098765432109876543210987654321", - "value": "100000", - "validAfter": "1000000000", - "validBefore": "1000003600", - "nonce": "0x" + "00" * 32, - }, - "signature": "0x" + "00" * 65, - }, + result = facilitator.verify( + make_payment_payload(accepted_network="eip155:1"), + make_requirements(), ) - requirements = PaymentRequirements( - scheme="exact", - network="eip155:8453", # Base Mainnet - asset=get_asset_info("eip155:8453", "USDC")["address"], - amount="100000", - pay_to="0x0987654321098765432109876543210987654321", - max_timeout_seconds=3600, - extra={"name": "USD Coin", "version": "2"}, - ) + assert result.is_valid is False + assert "network_mismatch" in result.invalid_reason + + def test_rejects_missing_eip712_domain(self): + signer = MockFacilitatorSigner() + facilitator = ExactEvmFacilitatorScheme(signer) - result = facilitator.verify(payload, requirements) + result = facilitator.verify( + make_payment_payload(extra={}), + make_requirements(extra={}), + ) - # Network check happens early assert result.is_valid is False - assert "network_mismatch" in result.invalid_reason + assert "missing_eip712_domain" in result.invalid_reason - def test_should_reject_if_eip712_domain_is_missing(self): - """Should reject if EIP-712 domain is missing.""" + def test_rejects_recipient_mismatch(self): signer = MockFacilitatorSigner() facilitator = ExactEvmFacilitatorScheme(signer) - network = "eip155:8453" - - payload = PaymentPayload( - x402_version=2, - resource=ResourceInfo( - url="http://example.com/protected", - description="Test resource", - mime_type="application/json", - ), - accepted=PaymentRequirements( - scheme="exact", - network=network, - asset=get_asset_info(network, "USDC")["address"], - amount="100000", - pay_to="0x0987654321098765432109876543210987654321", - max_timeout_seconds=3600, - extra={}, # Missing EIP-712 domain + + result = facilitator.verify( + make_payment_payload( + authorization_overrides={"to": FACILITATOR}, + pay_to=RECIPIENT, ), - payload={ - "authorization": { - "from": "0x1234567890123456789012345678901234567890", - "to": "0x0987654321098765432109876543210987654321", - "value": "100000", - "validAfter": "1000000000", - "validBefore": "1000003600", - "nonce": "0x" + "00" * 32, - }, - "signature": "0x" + "00" * 65, - }, + make_requirements(), ) - requirements = PaymentRequirements( - scheme="exact", - network=network, - asset=get_asset_info(network, "USDC")["address"], - amount="100000", - pay_to="0x0987654321098765432109876543210987654321", - max_timeout_seconds=3600, - extra={}, # Missing EIP-712 domain - ) + assert result.is_valid is False + assert "recipient_mismatch" in result.invalid_reason + + def test_rejects_amount_mismatch(self): + signer = MockFacilitatorSigner() + facilitator = ExactEvmFacilitatorScheme(signer) - result = facilitator.verify(payload, requirements) + result = facilitator.verify( + make_payment_payload(amount="50000"), + make_requirements(amount="100000"), + ) assert result.is_valid is False - assert "missing_eip712_domain" in result.invalid_reason + assert result.invalid_reason == ERR_AUTHORIZATION_VALUE_MISMATCH - def test_should_reject_if_recipient_does_not_match(self): - """Should reject if recipient does not match.""" + def test_rejects_overpayment_amount_mismatch(self): signer = MockFacilitatorSigner() facilitator = ExactEvmFacilitatorScheme(signer) - network = "eip155:8453" - - payload = PaymentPayload( - x402_version=2, - resource=ResourceInfo( - url="http://example.com/protected", - description="Test resource", - mime_type="application/json", - ), - accepted=PaymentRequirements( - scheme="exact", - network=network, - asset=get_asset_info(network, "USDC")["address"], - amount="100000", - pay_to="0x0987654321098765432109876543210987654321", - max_timeout_seconds=3600, - extra={"name": "USD Coin", "version": "2"}, - ), - payload={ - "authorization": { - "from": "0x1234567890123456789012345678901234567890", - "to": "0xWrongRecipient1234567890123456789012345678", # Wrong recipient - "value": "100000", - "validAfter": "1000000000", - "validBefore": "1000003600", - "nonce": "0x" + "00" * 32, - }, - "signature": "0x" + "00" * 65, - }, + + result = facilitator.verify( + make_payment_payload(amount="150000"), + make_requirements(amount="100000"), ) - requirements = PaymentRequirements( - scheme="exact", - network=network, - asset=get_asset_info(network, "USDC")["address"], - amount="100000", - pay_to="0x0987654321098765432109876543210987654321", - max_timeout_seconds=3600, - extra={"name": "USD Coin", "version": "2"}, + assert result.is_valid is False + assert result.invalid_reason == ERR_AUTHORIZATION_VALUE_MISMATCH + + def test_reports_name_mismatch_from_simulation_diagnostic(self): + signer = MockFacilitatorSigner( + typed_data_valid=False, + code=b"\x01", + transfer_simulation_should_revert=True, + multicall_results=make_diagnostic_results(name="Wrong Coin"), ) + facilitator = ExactEvmFacilitatorScheme(signer) - result = facilitator.verify(payload, requirements) + result = facilitator.verify( + make_payment_payload(signature="0x" + "22" * 66), + make_requirements(), + ) assert result.is_valid is False - assert "recipient_mismatch" in result.invalid_reason + assert result.invalid_reason == ERR_TOKEN_NAME_MISMATCH + + def test_reports_version_mismatch_from_simulation_diagnostic(self): + signer = MockFacilitatorSigner( + typed_data_valid=False, + code=b"\x01", + transfer_simulation_should_revert=True, + multicall_results=make_diagnostic_results(version="3"), + ) + facilitator = ExactEvmFacilitatorScheme(signer) - def test_should_reject_if_amount_is_insufficient(self): - """Should reject if amount is insufficient.""" - signer = MockFacilitatorSigner() + result = facilitator.verify( + make_payment_payload(signature="0x" + "22" * 66), + make_requirements(), + ) + + assert result.is_valid is False + assert result.invalid_reason == ERR_TOKEN_VERSION_MISMATCH + + def test_deployed_erc1271_falls_back_to_simulation(self): + signer = MockFacilitatorSigner( + typed_data_valid=False, + code=b"\x01", + ) facilitator = ExactEvmFacilitatorScheme(signer) - network = "eip155:8453" - - payload = PaymentPayload( - x402_version=2, - resource=ResourceInfo( - url="http://example.com/protected", - description="Test resource", - mime_type="application/json", - ), - accepted=PaymentRequirements( - scheme="exact", - network=network, - asset=get_asset_info(network, "USDC")["address"], - amount="100000", - pay_to="0x0987654321098765432109876543210987654321", - max_timeout_seconds=3600, - extra={"name": "USD Coin", "version": "2"}, - ), - payload={ - "authorization": { - "from": "0x1234567890123456789012345678901234567890", - "to": "0x0987654321098765432109876543210987654321", - "value": "50000", # Less than required - "validAfter": "1000000000", - "validBefore": "1000003600", - "nonce": "0x" + "00" * 32, - }, - "signature": "0x" + "00" * 65, - }, + + result = facilitator.verify( + make_payment_payload(signature="0x" + "22" * 66), + make_requirements(), ) - requirements = PaymentRequirements( - scheme="exact", - network=network, - asset=get_asset_info(network, "USDC")["address"], - amount="100000", - pay_to="0x0987654321098765432109876543210987654321", - max_timeout_seconds=3600, - extra={"name": "USD Coin", "version": "2"}, + assert result.is_valid is True + assert result.payer == PAYER + assert signer.transfer_simulation_calls == 1 + + def test_undeployed_erc6492_passes_when_deploy_and_transfer_simulate(self): + signer = MockFacilitatorSigner( + typed_data_valid=False, + code=b"", + multicall_results=[(True, b""), (True, b"")], + ) + facilitator = ExactEvmFacilitatorScheme(signer) + + result = facilitator.verify( + make_payment_payload(signature=make_erc6492_signature(b"\x33" * 66)), + make_requirements(), + ) + + assert result.is_valid is True + assert result.payer == PAYER + + def test_undeployed_erc6492_rejects_when_simulation_fails(self): + signer = MockFacilitatorSigner( + typed_data_valid=False, + code=b"", + multicall_results=[(True, b""), (False, b"")], ) + facilitator = ExactEvmFacilitatorScheme(signer) - result = facilitator.verify(payload, requirements) + result = facilitator.verify( + make_payment_payload(signature=make_erc6492_signature(b"\x33" * 66)), + make_requirements(), + ) assert result.is_valid is False - assert "authorization_value" in result.invalid_reason + assert result.invalid_reason == ERR_TRANSACTION_SIMULATION_FAILED + def test_undeployed_smart_wallet_without_deployment_info_is_rejected(self): + signer = MockFacilitatorSigner(typed_data_valid=False, code=b"") + facilitator = ExactEvmFacilitatorScheme(signer) -class TestSettle: - """Test settle method.""" + result = facilitator.verify( + make_payment_payload(signature="0x" + "22" * 66), + make_requirements(), + ) - def test_should_fail_settlement_if_verification_fails(self): - """Should fail settlement if verification fails.""" - signer = MockFacilitatorSigner() + assert result.is_valid is False + assert result.invalid_reason == ERR_UNDEPLOYED_SMART_WALLET + + def test_reports_nonce_used_from_simulation_diagnostic(self): + signer = MockFacilitatorSigner( + typed_data_valid=False, + code=b"\x01", + transfer_simulation_should_revert=True, + multicall_results=make_diagnostic_results(nonce_used=True), + ) facilitator = ExactEvmFacilitatorScheme(signer) - network = "eip155:8453" - - payload = PaymentPayload( - x402_version=2, - resource=ResourceInfo( - url="http://example.com/protected", - description="Test resource", - mime_type="application/json", - ), - accepted=PaymentRequirements( - scheme="wrong", # Wrong scheme - network=network, - asset=get_asset_info(network, "USDC")["address"], - amount="100000", - pay_to="0x0987654321098765432109876543210987654321", - max_timeout_seconds=3600, - extra={"name": "USD Coin", "version": "2"}, - ), - payload={ - "authorization": { - "from": "0x1234567890123456789012345678901234567890", - "to": "0x0987654321098765432109876543210987654321", - "value": "100000", - "validAfter": "1000000000", - "validBefore": "1000003600", - "nonce": "0x" + "00" * 32, - }, - "signature": "0x" + "00" * 65, - }, + + result = facilitator.verify( + make_payment_payload(signature="0x" + "22" * 66), + make_requirements(), ) - requirements = PaymentRequirements( - scheme="exact", - network=network, - asset=get_asset_info(network, "USDC")["address"], - amount="100000", - pay_to="0x0987654321098765432109876543210987654321", - max_timeout_seconds=3600, - extra={"name": "USD Coin", "version": "2"}, + assert result.is_valid is False + assert result.invalid_reason == ERR_NONCE_ALREADY_USED + + def test_reports_insufficient_balance_from_simulation_diagnostic(self): + signer = MockFacilitatorSigner( + typed_data_valid=False, + code=b"\x01", + transfer_simulation_should_revert=True, + multicall_results=make_diagnostic_results(balance=1), + ) + facilitator = ExactEvmFacilitatorScheme(signer) + + result = facilitator.verify( + make_payment_payload(signature="0x" + "22" * 66), + make_requirements(amount="100000"), + ) + + assert result.is_valid is False + assert result.invalid_reason == ERR_INSUFFICIENT_BALANCE + + def test_eoa_invalid_signature_is_rejected_immediately(self): + signer = MockFacilitatorSigner(typed_data_valid=False, code=b"") + facilitator = ExactEvmFacilitatorScheme(signer) + + result = facilitator.verify( + make_payment_payload(signature="0x" + "00" * 65), + make_requirements(), ) - result = facilitator.settle(payload, requirements) + assert result.is_valid is False + assert result.invalid_reason == ERR_INVALID_SIGNATURE + + +class TestSettle: + def test_fails_settlement_if_verification_fails(self): + signer = MockFacilitatorSigner() + facilitator = ExactEvmFacilitatorScheme(signer) + + result = facilitator.settle( + make_payment_payload(accepted_scheme="wrong"), + make_requirements(), + ) assert result.success is False assert "unsupported_scheme" in result.error_reason - assert result.network == network + assert result.network == NETWORK + + def test_can_rerun_simulation_during_settle(self): + signer = MockFacilitatorSigner(typed_data_valid=True) + facilitator = ExactEvmFacilitatorScheme( + signer, + ExactEvmSchemeConfig(simulate_in_settle=True), + ) + result = facilitator.settle( + make_payment_payload(signature="0x" + "00" * 65), + make_requirements(), + ) -class TestFacilitatorSchemeAttributes: - """Test facilitator scheme attributes.""" + assert result.success is True + assert signer.transfer_simulation_calls == 1 + assert signer.write_calls == 1 - def test_scheme_attribute_is_exact(self): - """scheme attribute should be 'exact'.""" + +class TestVerifyV1: + def test_rejects_overpayment_amount_mismatch(self): signer = MockFacilitatorSigner() - facilitator = ExactEvmFacilitatorScheme(signer) + facilitator = ExactEvmSchemeV1(signer) + + result = facilitator.verify( + make_payment_payload_v1(amount="150000"), + make_requirements_v1(amount="100000"), + ) + assert result.is_valid is False + assert result.invalid_reason == ERR_AUTHORIZATION_VALUE_MISMATCH + + +class TestFacilitatorSchemeAttributes: + def test_scheme_attribute_is_exact(self): + facilitator = ExactEvmFacilitatorScheme(MockFacilitatorSigner()) assert facilitator.scheme == "exact" def test_caip_family_attribute(self): - """caip_family attribute should be 'eip155:*'.""" - signer = MockFacilitatorSigner() - facilitator = ExactEvmFacilitatorScheme(signer) - + facilitator = ExactEvmFacilitatorScheme(MockFacilitatorSigner()) assert facilitator.caip_family == "eip155:*" def test_get_extra_returns_none(self): - """get_extra should return None for EVM.""" - signer = MockFacilitatorSigner() - facilitator = ExactEvmFacilitatorScheme(signer) - - extra = facilitator.get_extra("eip155:8453") - - assert extra is None + facilitator = ExactEvmFacilitatorScheme(MockFacilitatorSigner()) + assert facilitator.get_extra(NETWORK) is None def test_get_signers_returns_signer_addresses(self): - """get_signers should return list of signer addresses.""" addresses = [ - "0xSigner1111111111111111111111111111111111111111", - "0xSigner2222222222222222222222222222222222222222", + "0x1111111111111111111111111111111111111111", + "0x2222222222222222222222222222222222222222", ] - signer = MockFacilitatorSigner(addresses) - facilitator = ExactEvmFacilitatorScheme(signer) - - result = facilitator.get_signers("eip155:8453") - - assert result == addresses + facilitator = ExactEvmFacilitatorScheme(MockFacilitatorSigner(addresses=addresses)) + assert facilitator.get_signers(NETWORK) == addresses diff --git a/python/x402/tests/unit/mechanisms/evm/test_index.py b/python/x402/tests/unit/mechanisms/evm/test_index.py index 598834f135..a5d896da65 100644 --- a/python/x402/tests/unit/mechanisms/evm/test_index.py +++ b/python/x402/tests/unit/mechanisms/evm/test_index.py @@ -165,26 +165,26 @@ def test_should_return_config_for_supported_networks(self): assert config is not None assert config["chain_id"] == 8453 assert "default_asset" in config - assert "supported_assets" in config def test_should_reject_legacy_names(self): """Should reject legacy network names (use evm.v1.utils for v1).""" with pytest.raises(ValueError, match="expected eip155:CHAIN_ID"): get_network_config("base") - def test_should_raise_for_unsupported_networks(self): - """Should raise ValueError for unsupported networks.""" - with pytest.raises(ValueError, match="No configuration"): - get_network_config("eip155:99999") + def test_should_return_minimal_config_for_unknown_networks(self): + """Should return a minimal config with chain_id for valid but unconfigured networks.""" + config = get_network_config("eip155:99999") + assert config["chain_id"] == 99999 class TestGetAssetInfo: """Test get_asset_info function.""" def test_should_return_asset_info_by_symbol(self): - """Should return asset info by symbol.""" + """Should return asset info for known address (get_asset_info takes address, not symbol).""" network = "eip155:8453" - asset_info = get_asset_info(network, "USDC") + usdc_address = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913" + asset_info = get_asset_info(network, usdc_address) assert asset_info["address"].startswith("0x") assert asset_info["name"] == "USD Coin" @@ -193,15 +193,16 @@ def test_should_return_asset_info_by_symbol(self): def test_should_return_asset_info_by_address(self): """Should return asset info by address.""" network = "eip155:8453" - usdc_address = get_asset_info(network, "USDC")["address"] + usdc_address = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913" asset_info = get_asset_info(network, usdc_address) assert asset_info["address"].lower() == usdc_address.lower() def test_should_raise_for_unknown_asset(self): - """Should raise ValueError for unknown asset.""" - with pytest.raises(ValueError, match="Asset.*not found"): - get_asset_info("eip155:8453", "UNKNOWN") + """Should raise ValueError for an unregistered asset address.""" + unknown_address = "0x1234567890123456789012345678901234567890" + with pytest.raises(ValueError, match="not a registered asset"): + get_asset_info("eip155:8453", unknown_address) class TestIsValidNetwork: @@ -217,8 +218,8 @@ def test_should_return_false_for_legacy_names(self): assert is_valid_network("base") is False def test_should_return_false_for_unsupported_networks(self): - """Should return False for unsupported networks.""" - assert is_valid_network("eip155:99999") is False + """Should return False for non-eip155 or malformed networks; True for any valid eip155 format.""" + assert is_valid_network("eip155:99999") is True # Valid format, any chain ID assert is_valid_network("unknown-network") is False diff --git a/python/x402/tests/unit/mechanisms/evm/test_permit2.py b/python/x402/tests/unit/mechanisms/evm/test_permit2.py new file mode 100644 index 0000000000..1f573886e9 --- /dev/null +++ b/python/x402/tests/unit/mechanisms/evm/test_permit2.py @@ -0,0 +1,609 @@ +"""Tests for the Permit2 payment flow in the exact EVM scheme.""" + +from __future__ import annotations + +import time +from typing import Any +from unittest.mock import MagicMock, patch + +from x402.mechanisms.evm.constants import ( + ERR_INSUFFICIENT_BALANCE, + ERR_NETWORK_MISMATCH, + ERR_PERMIT2_ALLOWANCE_REQUIRED, + ERR_PERMIT2_AMOUNT_MISMATCH, + ERR_PERMIT2_DEADLINE_EXPIRED, + ERR_PERMIT2_INVALID_SIGNATURE, + ERR_PERMIT2_INVALID_SPENDER, + ERR_PERMIT2_NOT_YET_VALID, + ERR_PERMIT2_RECIPIENT_MISMATCH, + ERR_PERMIT2_TOKEN_MISMATCH, + ERR_UNSUPPORTED_SCHEME, + X402_EXACT_PERMIT2_PROXY_ADDRESS, +) +from x402.mechanisms.evm.exact import ExactEvmClientScheme, ExactEvmFacilitatorScheme +from x402.mechanisms.evm.types import ( + ExactPermit2Authorization, + ExactPermit2Payload, + ExactPermit2TokenPermissions, + ExactPermit2Witness, + TransactionReceipt, + is_permit2_payload, +) +from x402.schemas import PaymentPayload, PaymentRequirements, ResourceInfo + +NETWORK = "eip155:84532" # Base Sepolia +TOKEN_ADDRESS = "0x036CbD53842c5426634e7929541eC2318f3dCF7e" +PAYER = "0x1234567890123456789012345678901234567890" +RECIPIENT = "0x0987654321098765432109876543210987654321" +FACILITATOR = "0x1111111111111111111111111111111111111111" +AMOUNT = "1000" + + +def make_permit2_authorization( + *, + from_address: str = PAYER, + token: str = TOKEN_ADDRESS, + amount: str = AMOUNT, + spender: str = X402_EXACT_PERMIT2_PROXY_ADDRESS, + nonce: str = "12345678901234567890", + deadline_offset: int = 3600, + valid_after_offset: int = -600, + witness_to: str = RECIPIENT, +) -> ExactPermit2Authorization: + now = int(time.time()) + return ExactPermit2Authorization( + from_address=from_address, + permitted=ExactPermit2TokenPermissions(token=token, amount=amount), + spender=spender, + nonce=nonce, + deadline=str(now + deadline_offset), + witness=ExactPermit2Witness( + to=witness_to, + valid_after=str(now + valid_after_offset), + ), + ) + + +def make_permit2_payload_dict( + auth: ExactPermit2Authorization | None = None, + signature: str = "0x" + "aa" * 65, +) -> dict[str, Any]: + if auth is None: + auth = make_permit2_authorization() + payload = ExactPermit2Payload(permit2_authorization=auth, signature=signature) + return payload.to_dict() + + +def make_payment_payload( + *, + payload_dict: dict[str, Any] | None = None, + accepted_scheme: str = "exact", + accepted_network: str = NETWORK, + pay_to: str = RECIPIENT, + amount: str = AMOUNT, +) -> PaymentPayload: + if payload_dict is None: + payload_dict = make_permit2_payload_dict() + return PaymentPayload( + x402_version=2, + resource=ResourceInfo( + url="http://example.com/protected-permit2", + description="Test resource", + mime_type="application/json", + ), + accepted=PaymentRequirements( + scheme=accepted_scheme, + network=accepted_network, + asset=TOKEN_ADDRESS, + amount=amount, + pay_to=pay_to, + max_timeout_seconds=3600, + extra={"assetTransferMethod": "permit2"}, + ), + payload=payload_dict, + ) + + +def make_requirements( + *, + scheme: str = "exact", + network: str = NETWORK, + amount: str = AMOUNT, + pay_to: str = RECIPIENT, +) -> PaymentRequirements: + return PaymentRequirements( + scheme=scheme, + network=network, + asset=TOKEN_ADDRESS, + amount=amount, + pay_to=pay_to, + max_timeout_seconds=3600, + extra={"assetTransferMethod": "permit2"}, + ) + + +class MockFacilitatorSigner: + """Mock signer for permit2 facilitator tests.""" + + def __init__( + self, + *, + sig_valid: bool = True, + allowance: int = int(AMOUNT) * 2, + balance: int = int(AMOUNT) * 10, + tx_success: bool = True, + ): + self._sig_valid = sig_valid + self._allowance = allowance + self._balance = balance + self._tx_success = tx_success + self.write_calls: list[tuple] = [] + + def get_addresses(self) -> list[str]: + return [FACILITATOR] + + def read_contract(self, address: str, abi: list[dict], function_name: str, *args) -> Any: + if function_name == "allowance": + return self._allowance + if function_name == "balanceOf": + return self._balance + raise AssertionError(f"unexpected read_contract: {function_name}") + + def verify_typed_data(self, *args: Any, **kwargs: Any) -> bool: + return self._sig_valid + + def write_contract(self, address: str, abi: list[dict], function_name: str, *args) -> str: + self.write_calls.append((address, function_name, args)) + return "0x" + "ab" * 32 + + def send_transaction(self, to: str, data: bytes) -> str: + return "0x" + "cd" * 32 + + def wait_for_transaction_receipt(self, tx_hash: str) -> TransactionReceipt: + status = 1 if self._tx_success else 0 + return TransactionReceipt(status=status, block_number=1, tx_hash=tx_hash) + + def get_balance(self, address: str, token_address: str) -> int: + return self._balance + + def get_chain_id(self) -> int: + return 84532 + + def get_code(self, address: str) -> bytes: + return b"" + + +# ============================================================================ +# Type detection tests +# ============================================================================ + + +class TestIsPermit2Payload: + def test_detects_permit2_payload(self): + payload = make_permit2_payload_dict() + assert is_permit2_payload(payload) is True + + def test_rejects_eip3009_payload(self): + payload = {"authorization": {"from": PAYER}, "signature": "0x"} + assert is_permit2_payload(payload) is False + + def test_rejects_empty_dict(self): + assert is_permit2_payload({}) is False + + +# ============================================================================ +# ExactPermit2Payload serialization +# ============================================================================ + + +class TestExactPermit2PayloadSerialization: + def test_to_dict_and_from_dict_roundtrip(self): + auth = make_permit2_authorization() + original = ExactPermit2Payload(permit2_authorization=auth, signature="0x" + "ff" * 65) + + d = original.to_dict() + restored = ExactPermit2Payload.from_dict(d) + + assert restored.permit2_authorization.from_address == auth.from_address + assert restored.permit2_authorization.spender == auth.spender + assert restored.permit2_authorization.nonce == auth.nonce + assert restored.permit2_authorization.deadline == auth.deadline + assert restored.permit2_authorization.permitted.token == auth.permitted.token + assert restored.permit2_authorization.permitted.amount == auth.permitted.amount + assert restored.permit2_authorization.witness.to == auth.witness.to + assert restored.permit2_authorization.witness.valid_after == auth.witness.valid_after + assert restored.signature == original.signature + + def test_to_dict_keys_are_camel_case(self): + auth = make_permit2_authorization() + payload = ExactPermit2Payload(permit2_authorization=auth, signature="0xsig") + d = payload.to_dict() + + assert "permit2Authorization" in d + p2 = d["permit2Authorization"] + assert "from" in p2 + assert "permitted" in p2 + assert "spender" in p2 + assert "nonce" in p2 + assert "deadline" in p2 + assert "witness" in p2 + assert "to" in p2["witness"] + assert "validAfter" in p2["witness"] + + +# ============================================================================ +# Facilitator verify_permit2 tests +# ============================================================================ + + +class TestVerifyPermit2: + def _make_facilitator(self, **kwargs) -> ExactEvmFacilitatorScheme: + signer = MockFacilitatorSigner(**kwargs) + return ExactEvmFacilitatorScheme(signer) + + def _verify(self, facilitator: ExactEvmFacilitatorScheme, **payload_kwargs): + payload = make_payment_payload(**payload_kwargs) + requirements = make_requirements() + return facilitator.verify(payload, requirements) + + def test_verify_accepts_valid_payment(self): + facilitator = self._make_facilitator(sig_valid=True) + with patch( + "x402.mechanisms.evm.exact.permit2_utils._verify_permit2_signature", + return_value=True, + ): + result = self._verify(facilitator) + assert result.is_valid is True + assert result.payer == PAYER + + def test_verify_rejects_wrong_scheme(self): + facilitator = self._make_facilitator() + payload = make_payment_payload(accepted_scheme="upto") + requirements = make_requirements() + result = facilitator.verify(payload, requirements) + assert result.is_valid is False + assert result.invalid_reason == ERR_UNSUPPORTED_SCHEME + + def test_verify_rejects_network_mismatch(self): + facilitator = self._make_facilitator() + payload = make_payment_payload(accepted_network="eip155:8453") + requirements = make_requirements(network="eip155:84532") + result = facilitator.verify(payload, requirements) + assert result.is_valid is False + assert result.invalid_reason == ERR_NETWORK_MISMATCH + + def test_verify_rejects_invalid_spender(self): + facilitator = self._make_facilitator() + auth = make_permit2_authorization(spender="0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef") + payload = make_payment_payload(payload_dict=make_permit2_payload_dict(auth)) + requirements = make_requirements() + result = facilitator.verify(payload, requirements) + assert result.is_valid is False + assert result.invalid_reason == ERR_PERMIT2_INVALID_SPENDER + + def test_verify_rejects_recipient_mismatch(self): + facilitator = self._make_facilitator() + wrong_recipient = "0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef" + auth = make_permit2_authorization(witness_to=wrong_recipient) + payload = make_payment_payload(payload_dict=make_permit2_payload_dict(auth)) + requirements = make_requirements(pay_to=RECIPIENT) + result = facilitator.verify(payload, requirements) + assert result.is_valid is False + assert result.invalid_reason == ERR_PERMIT2_RECIPIENT_MISMATCH + + def test_verify_rejects_expired_deadline(self): + facilitator = self._make_facilitator() + auth = make_permit2_authorization(deadline_offset=-100) # expired + payload = make_payment_payload(payload_dict=make_permit2_payload_dict(auth)) + requirements = make_requirements() + result = facilitator.verify(payload, requirements) + assert result.is_valid is False + assert result.invalid_reason == ERR_PERMIT2_DEADLINE_EXPIRED + + def test_verify_rejects_valid_after_in_future(self): + facilitator = self._make_facilitator() + auth = make_permit2_authorization(valid_after_offset=3600) # 1 hour from now + payload = make_payment_payload(payload_dict=make_permit2_payload_dict(auth)) + requirements = make_requirements() + result = facilitator.verify(payload, requirements) + assert result.is_valid is False + assert result.invalid_reason == ERR_PERMIT2_NOT_YET_VALID + + def test_verify_rejects_amount_mismatch(self): + facilitator = self._make_facilitator() + auth = make_permit2_authorization(amount="999") # wrong amount + payload = make_payment_payload(payload_dict=make_permit2_payload_dict(auth)) + requirements = make_requirements(amount=AMOUNT) + result = facilitator.verify(payload, requirements) + assert result.is_valid is False + assert result.invalid_reason == ERR_PERMIT2_AMOUNT_MISMATCH + + def test_verify_rejects_token_mismatch(self): + facilitator = self._make_facilitator() + wrong_token = "0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef" + auth = make_permit2_authorization(token=wrong_token) + payload = make_payment_payload(payload_dict=make_permit2_payload_dict(auth)) + requirements = make_requirements() + result = facilitator.verify(payload, requirements) + assert result.is_valid is False + assert result.invalid_reason == ERR_PERMIT2_TOKEN_MISMATCH + + def test_verify_rejects_invalid_signature(self): + facilitator = self._make_facilitator() + with patch( + "x402.mechanisms.evm.exact.permit2_utils._verify_permit2_signature", + return_value=False, + ): + result = self._verify(facilitator) + assert result.is_valid is False + assert result.invalid_reason == ERR_PERMIT2_INVALID_SIGNATURE + + def test_verify_rejects_missing_signature(self): + facilitator = self._make_facilitator() + auth = make_permit2_authorization() + payload_dict = ExactPermit2Payload(permit2_authorization=auth, signature=None).to_dict() + payload = make_payment_payload(payload_dict=payload_dict) + requirements = make_requirements() + result = facilitator.verify(payload, requirements) + assert result.is_valid is False + assert result.invalid_reason == ERR_PERMIT2_INVALID_SIGNATURE + + def test_verify_rejects_insufficient_allowance(self): + facilitator = self._make_facilitator(allowance=0) + with patch( + "x402.mechanisms.evm.exact.permit2_utils._verify_permit2_signature", + return_value=True, + ): + result = self._verify(facilitator) + assert result.is_valid is False + assert result.invalid_reason == ERR_PERMIT2_ALLOWANCE_REQUIRED + + def test_verify_rejects_insufficient_balance(self): + facilitator = self._make_facilitator(allowance=int(AMOUNT) * 10, balance=0) + with patch( + "x402.mechanisms.evm.exact.permit2_utils._verify_permit2_signature", + return_value=True, + ): + result = self._verify(facilitator) + assert result.is_valid is False + assert result.invalid_reason == ERR_INSUFFICIENT_BALANCE + + +# ============================================================================ +# Facilitator settle_permit2 tests +# ============================================================================ + + +class TestSettlePermit2: + def _make_facilitator(self, **kwargs) -> ExactEvmFacilitatorScheme: + signer = MockFacilitatorSigner(**kwargs) + return ExactEvmFacilitatorScheme(signer) + + def test_settle_calls_proxy_contract(self): + signer = MockFacilitatorSigner(sig_valid=True) + facilitator = ExactEvmFacilitatorScheme(signer) + payload = make_payment_payload() + requirements = make_requirements() + + with patch( + "x402.mechanisms.evm.exact.permit2_utils._verify_permit2_signature", + return_value=True, + ): + result = facilitator.settle(payload, requirements) + + assert result.success is True + assert len(signer.write_calls) == 1 + address, function_name, _ = signer.write_calls[0] + assert address == X402_EXACT_PERMIT2_PROXY_ADDRESS + assert function_name == "settle" + + def test_settle_returns_transaction_hash(self): + signer = MockFacilitatorSigner() + facilitator = ExactEvmFacilitatorScheme(signer) + payload = make_payment_payload() + requirements = make_requirements() + + with patch( + "x402.mechanisms.evm.exact.permit2_utils._verify_permit2_signature", + return_value=True, + ): + result = facilitator.settle(payload, requirements) + + assert result.success is True + assert result.transaction is not None + assert len(result.transaction) > 0 + + def test_settle_fails_if_verify_fails(self): + facilitator = self._make_facilitator(allowance=0) + payload = make_payment_payload() + requirements = make_requirements() + result = facilitator.settle(payload, requirements) + assert result.success is False + + def test_settle_fails_on_transaction_failure(self): + signer = MockFacilitatorSigner(tx_success=False) + facilitator = ExactEvmFacilitatorScheme(signer) + payload = make_payment_payload() + requirements = make_requirements() + + with patch( + "x402.mechanisms.evm.exact.permit2_utils._verify_permit2_signature", + return_value=True, + ): + result = facilitator.settle(payload, requirements) + + assert result.success is False + + +# ============================================================================ +# Client routing tests +# ============================================================================ + + +class TestClientPermit2Routing: + def test_routes_to_permit2_when_asset_transfer_method_is_permit2(self): + mock_signer = MagicMock() + mock_signer.address = PAYER + mock_signer.sign_typed_data.return_value = b"\x00" * 65 + + client_scheme = ExactEvmClientScheme(mock_signer) + requirements = make_requirements() + + result = client_scheme.create_payment_payload(requirements) + + assert "permit2Authorization" in result + assert "authorization" not in result + assert result["permit2Authorization"]["from"] == PAYER + assert result["permit2Authorization"]["spender"] == X402_EXACT_PERMIT2_PROXY_ADDRESS + + def test_routes_to_eip3009_when_no_asset_transfer_method(self): + mock_signer = MagicMock() + mock_signer.address = PAYER + mock_signer.sign_typed_data.return_value = b"\x00" * 65 + + client_scheme = ExactEvmClientScheme(mock_signer) + requirements = PaymentRequirements( + scheme="exact", + network=NETWORK, + asset=TOKEN_ADDRESS, + amount=AMOUNT, + pay_to=RECIPIENT, + max_timeout_seconds=3600, + extra={"name": "USD Coin", "version": "2"}, + ) + + result = client_scheme.create_payment_payload(requirements) + + assert "authorization" in result + assert "permit2Authorization" not in result + + def test_permit2_payload_has_correct_structure(self): + mock_signer = MagicMock() + mock_signer.address = PAYER + mock_signer.sign_typed_data.return_value = b"\xff" * 65 + + client_scheme = ExactEvmClientScheme(mock_signer) + requirements = make_requirements() + + result = client_scheme.create_payment_payload(requirements) + + p2 = result["permit2Authorization"] + assert p2["from"] == PAYER + assert p2["spender"] == X402_EXACT_PERMIT2_PROXY_ADDRESS + assert p2["witness"]["to"] == RECIPIENT + assert "nonce" in p2 + assert "deadline" in p2 + assert "permitted" in p2 + assert p2["permitted"]["token"] == "0x036CbD53842c5426634e7929541eC2318f3dCF7e" + assert p2["permitted"]["amount"] == AMOUNT + + +# ============================================================================ +# Extension-aware verify tests +# ============================================================================ + + +class TestVerifyPermit2WithExtensions: + """Tests for verify_permit2 with gas sponsoring extension fallbacks.""" + + def _make_facilitator(self, **kwargs) -> ExactEvmFacilitatorScheme: + signer = MockFacilitatorSigner(**kwargs) + return ExactEvmFacilitatorScheme(signer) + + def test_verify_accepts_with_eip2612_extension_when_allowance_insufficient(self): + """When allowance is 0 but valid EIP-2612 extension is present, verify passes.""" + from x402.extensions.eip2612_gas_sponsoring import EIP2612_GAS_SPONSORING_KEY + from x402.mechanisms.evm.constants import PERMIT2_ADDRESS + + facilitator = self._make_facilitator(allowance=0, sig_valid=True) + + now = int(time.time()) + eip2612_info = { + "from": PAYER, + "asset": TOKEN_ADDRESS, + "spender": PERMIT2_ADDRESS, + "amount": str(2**256 - 1), + "nonce": "0", + "deadline": str(now + 3600), + "signature": "0x" + "aa" * 65, + "version": "1", + } + + payload_dict = make_permit2_payload_dict() + payload = PaymentPayload( + x402_version=2, + resource=ResourceInfo( + url="http://example.com/test", + description="Test", + mime_type="application/json", + ), + accepted=PaymentRequirements( + scheme="exact", + network=NETWORK, + asset=TOKEN_ADDRESS, + amount=AMOUNT, + pay_to=RECIPIENT, + max_timeout_seconds=3600, + extra={"assetTransferMethod": "permit2"}, + ), + payload=payload_dict, + extensions={EIP2612_GAS_SPONSORING_KEY: {"info": eip2612_info}}, + ) + requirements = make_requirements() + + with patch( + "x402.mechanisms.evm.exact.permit2_utils._verify_permit2_signature", + return_value=True, + ): + result = facilitator.verify(payload, requirements) + + assert result.is_valid is True + + def test_verify_rejects_with_invalid_eip2612_extension(self): + """When allowance is 0 and EIP-2612 extension has wrong spender, verify fails.""" + from x402.extensions.eip2612_gas_sponsoring import EIP2612_GAS_SPONSORING_KEY + + facilitator = self._make_facilitator(allowance=0, sig_valid=True) + + now = int(time.time()) + eip2612_info = { + "from": PAYER, + "asset": TOKEN_ADDRESS, + "spender": "0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef", + "amount": str(2**256 - 1), + "nonce": "0", + "deadline": str(now + 3600), + "signature": "0x" + "aa" * 65, + "version": "1", + } + + payload_dict = make_permit2_payload_dict() + payload = PaymentPayload( + x402_version=2, + resource=ResourceInfo( + url="http://example.com/test", + description="Test", + mime_type="application/json", + ), + accepted=PaymentRequirements( + scheme="exact", + network=NETWORK, + asset=TOKEN_ADDRESS, + amount=AMOUNT, + pay_to=RECIPIENT, + max_timeout_seconds=3600, + extra={"assetTransferMethod": "permit2"}, + ), + payload=payload_dict, + extensions={EIP2612_GAS_SPONSORING_KEY: {"info": eip2612_info}}, + ) + requirements = make_requirements() + + with patch( + "x402.mechanisms.evm.exact.permit2_utils._verify_permit2_signature", + return_value=True, + ): + result = facilitator.verify(payload, requirements) + + assert result.is_valid is False + assert "spender_not_permit2" in result.invalid_reason diff --git a/python/x402/tests/unit/mechanisms/evm/test_server.py b/python/x402/tests/unit/mechanisms/evm/test_server.py index 7365dbddde..0cbbd9741e 100644 --- a/python/x402/tests/unit/mechanisms/evm/test_server.py +++ b/python/x402/tests/unit/mechanisms/evm/test_server.py @@ -3,7 +3,6 @@ import pytest from x402.mechanisms.evm import ( - get_asset_info, get_network_config, ) from x402.mechanisms.evm.exact import ExactEvmServerScheme @@ -24,7 +23,7 @@ def test_should_parse_dollar_string_prices(self): result = server.parse_price("$0.10", network) assert result.amount == "100000" # 0.10 USDC = 100000 smallest units - assert result.asset == get_asset_info(network, "USDC")["address"] + assert result.asset == get_network_config(network)["default_asset"]["address"] assert result.extra == {"name": "USD Coin", "version": "2"} def test_should_parse_simple_number_string_prices(self): @@ -35,7 +34,7 @@ def test_should_parse_simple_number_string_prices(self): result = server.parse_price("0.10", network) assert result.amount == "100000" - assert result.asset == get_asset_info(network, "USDC")["address"] + assert result.asset == get_network_config(network)["default_asset"]["address"] def test_should_parse_number_prices(self): """Should parse number prices.""" @@ -45,7 +44,7 @@ def test_should_parse_number_prices(self): result = server.parse_price(0.1, network) assert result.amount == "100000" - assert result.asset == get_asset_info(network, "USDC")["address"] + assert result.asset == get_network_config(network)["default_asset"]["address"] def test_should_handle_larger_amounts(self): """Should handle larger amounts.""" @@ -68,15 +67,13 @@ def test_should_handle_whole_numbers(self): class TestEthereumMainnetNetwork: """Test Ethereum Mainnet network.""" - def test_should_use_ethereum_usdc_address(self): - """Should use Ethereum Mainnet USDC address.""" + def test_should_raise_for_network_without_default_stablecoin(self): + """Should raise ValueError when network has no default stablecoin configured.""" server = ExactEvmServerScheme() network = "eip155:1" - result = server.parse_price("1.00", network) - - assert result.asset == get_asset_info(network, "USDC")["address"] - assert result.amount == "1000000" + with pytest.raises(ValueError, match="No default stablecoin"): + server.parse_price("1.00", network) class TestBaseSepoliaNetwork: """Test Base Sepolia network.""" @@ -88,7 +85,7 @@ def test_should_use_sepolia_usdc_address(self): result = server.parse_price("1.00", network) - assert result.asset == get_asset_info(network, "USDC")["address"] + assert result.asset == get_network_config(network)["default_asset"]["address"] assert result.amount == "1000000" class TestPreParsedPriceObjects: @@ -151,7 +148,7 @@ def test_should_add_eip712_domain_to_payment_requirements(self): requirements = PaymentRequirements( scheme="exact", network=network, - asset=get_asset_info(network, "USDC")["address"], + asset=get_network_config(network)["default_asset"]["address"], amount="100000", pay_to="0x1234567890123456789012345678901234567890", max_timeout_seconds=3600, @@ -181,7 +178,7 @@ def test_should_preserve_existing_extra_fields(self): requirements = PaymentRequirements( scheme="exact", network=network, - asset=get_asset_info(network, "USDC")["address"], + asset=get_network_config(network)["default_asset"]["address"], amount="100000", pay_to="0x1234567890123456789012345678901234567890", max_timeout_seconds=3600, @@ -209,7 +206,7 @@ def test_should_convert_decimal_amounts_to_smallest_unit(self): requirements = PaymentRequirements( scheme="exact", network=network, - asset=get_asset_info(network, "USDC")["address"], + asset=get_network_config(network)["default_asset"]["address"], amount="1.5", # Decimal amount pay_to="0x1234567890123456789012345678901234567890", max_timeout_seconds=3600, @@ -286,7 +283,7 @@ def custom_parser(amount: float, network: str) -> AssetAmount | None: # Small amount should fall back to default (USDC) result2 = server.parse_price(50, network) - assert result2.asset == get_asset_info(network, "USDC")["address"] + assert result2.asset == get_network_config(network)["default_asset"]["address"] assert result2.amount == "50000000" # 50 * 1e6 def test_should_receive_decimal_number_not_raw_string(self): @@ -351,7 +348,7 @@ def null_parser(amount: float, network: str) -> AssetAmount | None: result = server.parse_price(1, network) # Should use default USDC - assert result.asset == get_asset_info(network, "USDC")["address"] + assert result.asset == get_network_config(network)["default_asset"]["address"] assert result.amount == "1000000" class TestMultipleParsersChainOfResponsibility: @@ -426,7 +423,7 @@ def test_should_use_default_if_all_parsers_return_null(self): result = server.parse_price(1, network) # Should use default USDC - assert result.asset == get_asset_info(network, "USDC")["address"] + assert result.asset == get_network_config(network)["default_asset"]["address"] assert result.amount == "1000000" class TestErrorHandling: @@ -552,7 +549,10 @@ def network_parser(amount: float, network: str) -> AssetAmount | None: assert sepolia_result.asset == "0xTestToken123456789012345678901234567890" mainnet_result = server.parse_price(10, "eip155:8453") - assert mainnet_result.asset == get_asset_info("eip155:8453", "USDC")["address"] + assert ( + mainnet_result.asset + == get_network_config("eip155:8453")["default_asset"]["address"] + ) def test_should_support_tiered_pricing(self): """Should support tiered pricing.""" @@ -588,7 +588,7 @@ def standard_parser(amount: float, network: str) -> AssetAmount | None: assert standard.extra.get("tier") == "standard" basic = server.parse_price(50, network) - assert basic.asset == get_asset_info(network, "USDC")["address"] + assert basic.asset == get_network_config(network)["default_asset"]["address"] class TestIntegrationWithParsePriceFlow: """Test integration with parsePrice flow.""" diff --git a/python/x402/tests/unit/mechanisms/evm/test_v1_utils.py b/python/x402/tests/unit/mechanisms/evm/test_v1_utils.py index 571fa7721f..1e5418b267 100644 --- a/python/x402/tests/unit/mechanisms/evm/test_v1_utils.py +++ b/python/x402/tests/unit/mechanisms/evm/test_v1_utils.py @@ -29,9 +29,11 @@ def test_should_resolve_monad(self): def test_should_resolve_avalanche(self): assert get_evm_chain_id("avalanche") == 43114 - def test_should_resolve_aliases(self): - assert get_evm_chain_id("base-mainnet") == 8453 - assert get_evm_chain_id("mainnet") == 1 + def test_should_reject_undefined_aliases(self): + with pytest.raises(ValueError, match="Unknown v1 network"): + get_evm_chain_id("base-mainnet") + with pytest.raises(ValueError, match="Unknown v1 network"): + get_evm_chain_id("mainnet") def test_should_reject_caip2_format(self): with pytest.raises(ValueError, match="Unknown v1 network"): @@ -46,7 +48,8 @@ class TestV1GetAssetInfo: """Test v1 get_asset_info function.""" def test_should_return_default_asset_for_base(self): - info = get_asset_info("base", "USDC") + usdc_address = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913" + info = get_asset_info("base", usdc_address) assert info["address"].startswith("0x") assert info["decimals"] == 6 @@ -55,5 +58,10 @@ def test_should_return_asset_by_address(self): assert info["decimals"] == 6 def test_should_raise_for_unknown_v1_network(self): - with pytest.raises(ValueError, match="Unknown v1 network"): - get_asset_info("eip155:8453", "USDC") + with pytest.raises(ValueError, match="No default asset for v1 network"): + get_asset_info("eip155:8453", "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913") + + def test_should_raise_for_unregistered_asset_address(self): + unknown_address = "0x1234567890123456789012345678901234567890" + with pytest.raises(ValueError, match="not a registered asset"): + get_asset_info("base", unknown_address) diff --git a/python/x402/tests/unit/mechanisms/evm/test_verify.py b/python/x402/tests/unit/mechanisms/evm/test_verify.py new file mode 100644 index 0000000000..b0fe6c3eb8 --- /dev/null +++ b/python/x402/tests/unit/mechanisms/evm/test_verify.py @@ -0,0 +1,93 @@ +"""Tests for universal signature verification helpers.""" + +import pytest + +try: + from eth_abi import encode as eth_abi_encode +except ImportError: + pytest.skip("eth-abi not available", allow_module_level=True) + +from x402.mechanisms.evm.verify import verify_universal_signature + +# ERC-6492 magic bytes suffix +ERC6492_MAGIC = bytes.fromhex("6492649264926492649264926492649264926492649264926492649264926492") + + +def make_erc6492_sig(factory: bytes, calldata: bytes, inner_sig: bytes) -> bytes: + """Build a valid ERC-6492 wrapped signature for testing. + + Format: abi.encode(address, bytes, bytes) + magic + """ + encoded = eth_abi_encode(["address", "bytes", "bytes"], [factory, calldata, inner_sig]) + return encoded + ERC6492_MAGIC + + +FACTORY_ADDR = bytes.fromhex("1111111111111111111111111111111111111111") +FACTORY_CALLDATA = bytes.fromhex("deadbeef") +GARBAGE_INNER_SIG = b"\x00" * 65 # All-zero 65-byte "signature" — forged/invalid +WALLET_ADDRESS = "0x1234567890123456789012345678901234567890" +TEST_HASH = b"\x01" * 32 + + +class MockFacilitatorSigner: + """Minimal mock facilitator signer for verify tests.""" + + def __init__(self, read_contract_result=None, read_contract_raises=None, code=b""): + self._read_contract_result = read_contract_result + self._read_contract_raises = read_contract_raises + self._code = code + + def get_code(self, address: str) -> bytes: + return self._code + + def read_contract(self, address, abi, function_name, *args): + if self._read_contract_raises is not None: + raise self._read_contract_raises + return self._read_contract_result + + +class TestVerifyUniversalSignature: + """Generic verification should stay generic and not perform EIP-3009 simulation.""" + + def test_allow_undeployed_true_accepts_erc6492_wrapper(self): + erc6492_sig = make_erc6492_sig(FACTORY_ADDR, FACTORY_CALLDATA, GARBAGE_INNER_SIG) + signer = MockFacilitatorSigner( + code=b"", + ) + + valid, sig_data = verify_universal_signature( + signer, + WALLET_ADDRESS, + TEST_HASH, + erc6492_sig, + allow_undeployed=True, + ) + + assert valid is True + assert sig_data.factory == FACTORY_ADDR + + def test_allow_undeployed_false_raises(self): + erc6492_sig = make_erc6492_sig(FACTORY_ADDR, FACTORY_CALLDATA, GARBAGE_INNER_SIG) + signer = MockFacilitatorSigner( + code=b"", + ) + + with pytest.raises(ValueError, match="not allowed"): + verify_universal_signature( + signer, + WALLET_ADDRESS, + TEST_HASH, + erc6492_sig, + allow_undeployed=False, + ) + + def test_non_erc6492_non_eoa_signature_returns_false_for_undeployed_wallet(self): + signer = MockFacilitatorSigner(code=b"") + valid, _ = verify_universal_signature( + signer, + WALLET_ADDRESS, + TEST_HASH, + b"\x99" * 66, + allow_undeployed=True, + ) + assert valid is False diff --git a/python/x402/tests/unit/mechanisms/svm/test_facilitator.py b/python/x402/tests/unit/mechanisms/svm/test_facilitator.py index 49a27701a6..d5eadf5eb1 100644 --- a/python/x402/tests/unit/mechanisms/svm/test_facilitator.py +++ b/python/x402/tests/unit/mechanisms/svm/test_facilitator.py @@ -1,12 +1,14 @@ """Tests for ExactSvmScheme facilitator.""" +from unittest.mock import patch + from x402.mechanisms.svm import ( SOLANA_DEVNET_CAIP2, SOLANA_MAINNET_CAIP2, USDC_DEVNET_ADDRESS, ) from x402.mechanisms.svm.exact import ExactSvmFacilitatorScheme -from x402.schemas import PaymentPayload, PaymentRequirements, ResourceInfo +from x402.schemas import PaymentPayload, PaymentRequirements, ResourceInfo, VerifyResponse class MockFacilitatorSigner: @@ -292,6 +294,154 @@ def test_get_signers_returns_signer_addresses(self): assert result == addresses +class TestDuplicateSettlementCache: + """Test duplicate settlement cache in settle method.""" + + def _make_payload(self, transaction: str) -> PaymentPayload: + return PaymentPayload( + x402_version=2, + resource=ResourceInfo( + url="http://example.com/protected", + description="Test resource", + mime_type="application/json", + ), + accepted=PaymentRequirements( + scheme="exact", + network=SOLANA_DEVNET_CAIP2, + asset=USDC_DEVNET_ADDRESS, + amount="100000", + pay_to="PayToAddress11111111111111111111111111", + max_timeout_seconds=3600, + extra={"feePayer": "FeePayer1111111111111111111111111111"}, + ), + payload={"transaction": transaction}, + ) + + def _make_requirements(self) -> PaymentRequirements: + return PaymentRequirements( + scheme="exact", + network=SOLANA_DEVNET_CAIP2, + asset=USDC_DEVNET_ADDRESS, + amount="100000", + pay_to="PayToAddress11111111111111111111111111", + max_timeout_seconds=3600, + extra={"feePayer": "FeePayer1111111111111111111111111111"}, + ) + + def test_should_reject_duplicate_settlement(self): + """Second settle call with the same transaction should be rejected.""" + signer = MockFacilitatorSigner() + facilitator = ExactSvmFacilitatorScheme(signer) + requirements = self._make_requirements() + payload = self._make_payload("sameTransactionBase64==") + + with patch.object( + facilitator, + "verify", + return_value=VerifyResponse(is_valid=True, payer="PayerAddress"), + ): + result1 = facilitator.settle(payload, requirements) + assert result1.success is True + + result2 = facilitator.settle(payload, requirements) + assert result2.success is False + assert result2.error_reason == "duplicate_settlement" + + def test_should_allow_distinct_transactions(self): + """Two different transactions should both settle successfully.""" + signer = MockFacilitatorSigner() + facilitator = ExactSvmFacilitatorScheme(signer) + requirements = self._make_requirements() + + with patch.object( + facilitator, + "verify", + return_value=VerifyResponse(is_valid=True, payer="PayerAddress"), + ): + result1 = facilitator.settle(self._make_payload("transactionA=="), requirements) + assert result1.success is True + + result2 = facilitator.settle(self._make_payload("transactionB=="), requirements) + assert result2.success is True + + def test_should_evict_cache_entries_after_ttl(self): + """Cache entries should be pruned after TTL so they no longer block locally. + + NOTE: In production the Solana RPC would still reject a re-submitted + transaction that already landed on-chain. This test only verifies that + the in-memory cache correctly prunes expired entries. + """ + signer = MockFacilitatorSigner() + facilitator = ExactSvmFacilitatorScheme(signer) + requirements = self._make_requirements() + payload = self._make_payload("expiringTransaction==") + + with patch.object( + facilitator, + "verify", + return_value=VerifyResponse(is_valid=True, payer="PayerAddress"), + ): + result1 = facilitator.settle(payload, requirements) + assert result1.success is True + + # Simulate TTL expiration by backdating the cache entry + for key in facilitator._settlement_cache.entries: + facilitator._settlement_cache.entries[key] -= 121.0 + + result2 = facilitator.settle(payload, requirements) + assert result2.success is True + + def test_shared_cache_blocks_cross_version_duplicates(self): + """V1 and V2 sharing a cache should catch cross-version duplicates.""" + from x402.mechanisms.svm.exact.v1.facilitator import ( + ExactSvmSchemeV1 as ExactSvmFacilitatorSchemeV1, + ) + from x402.mechanisms.svm.settlement_cache import SettlementCache + from x402.schemas.v1 import PaymentPayloadV1, PaymentRequirementsV1 + + signer = MockFacilitatorSigner() + shared_cache = SettlementCache() + v2 = ExactSvmFacilitatorScheme(signer, shared_cache) + v1 = ExactSvmFacilitatorSchemeV1(signer, shared_cache) + + # Settle via V2 first + with patch.object( + v2, + "verify", + return_value=VerifyResponse(is_valid=True, payer="PayerAddress"), + ): + result1 = v2.settle( + self._make_payload("crossVersionTx=="), + self._make_requirements(), + ) + assert result1.success is True + + # Same tx via V1 should be rejected by the shared cache + v1_payload = PaymentPayloadV1( + scheme="exact", + network="solana-devnet", + payload={"transaction": "crossVersionTx=="}, + ) + v1_requirements = PaymentRequirementsV1( + scheme="exact", + network="solana-devnet", + asset=USDC_DEVNET_ADDRESS, + max_amount_required="100000", + pay_to="PayToAddress11111111111111111111111111", + max_timeout_seconds=3600, + resource="https://example.com", + extra={"feePayer": "FeePayer1111111111111111111111111111"}, + ) + with patch.object( + v1, + "verify", + return_value=VerifyResponse(is_valid=True, payer="PayerAddress"), + ): + result2 = v1.settle(v1_payload, v1_requirements) + assert result2.success is False + assert result2.error_reason == "duplicate_settlement" + + class TestVerifyFeePayer: """Test fee payer verification in verify method.""" @@ -333,3 +483,79 @@ def test_should_reject_if_fee_payer_not_managed(self): assert result.is_valid is False assert result.invalid_reason == "fee_payer_not_managed_by_facilitator" + + +class TestSettlementCachePruneOptimization: + """Verify the early-break prune optimization preserves insertion-order semantics.""" + + def test_prunes_only_expired_entries_preserves_fresh_ones(self): + """Entries older than TTL are pruned; newer entries survive.""" + from x402.mechanisms.svm.settlement_cache import SettlementCache + + cache = SettlementCache() + + cache.is_duplicate("tx-a") + cache.is_duplicate("tx-b") + cache.is_duplicate("tx-c") + + # Backdate tx-a past TTL (121s), leave tx-b and tx-c fresh + base = cache.entries["tx-a"] + cache.entries["tx-a"] = base - 121.0 + + assert cache.is_duplicate("tx-a") is False, "expired entry should have been pruned" + assert cache.is_duplicate("tx-b") is True, "fresh entry should still be cached" + assert cache.is_duplicate("tx-c") is True, "fresh entry should still be cached" + + def test_prunes_all_entries_when_all_expired(self): + """When every entry is expired, all should be pruned.""" + from x402.mechanisms.svm.settlement_cache import SettlementCache + + cache = SettlementCache() + + cache.is_duplicate("tx-1") + cache.is_duplicate("tx-2") + cache.is_duplicate("tx-3") + + for k in list(cache.entries): + cache.entries[k] -= 121.0 + + assert cache.is_duplicate("tx-1") is False + assert cache.is_duplicate("tx-2") is False + assert cache.is_duplicate("tx-3") is False + + def test_prunes_nothing_when_all_fresh(self): + """When no entries are expired, none should be pruned.""" + from x402.mechanisms.svm.settlement_cache import SettlementCache + + cache = SettlementCache() + + cache.is_duplicate("tx-x") + cache.is_duplicate("tx-y") + cache.is_duplicate("tx-z") + + assert cache.is_duplicate("tx-x") is True + assert cache.is_duplicate("tx-y") is True + assert cache.is_duplicate("tx-z") is True + + def test_early_break_preserves_ordered_entries(self): + """Insertion-order iteration means the break fires at the first fresh entry.""" + from x402.mechanisms.svm.settlement_cache import SettlementCache + + cache = SettlementCache() + + # Insert A, B, C in order with small gaps + cache.is_duplicate("tx-old-1") + cache.is_duplicate("tx-old-2") + cache.is_duplicate("tx-fresh") + + # Expire only the first two + for k in ("tx-old-1", "tx-old-2"): + cache.entries[k] -= 121.0 + + # Trigger prune + cache.is_duplicate("tx-new") + + assert "tx-old-1" not in cache.entries, "first expired entry should be pruned" + assert "tx-old-2" not in cache.entries, "second expired entry should be pruned" + assert "tx-fresh" in cache.entries, "fresh entry after expired ones should survive" + assert "tx-new" in cache.entries, "newly inserted entry should be present" diff --git a/python/x402/uv.lock b/python/x402/uv.lock index cb061ba93e..d9b2c5ceb9 100644 --- a/python/x402/uv.lock +++ b/python/x402/uv.lock @@ -1,5 +1,5 @@ version = 1 -revision = 3 +revision = 2 requires-python = ">=3.10" [[package]] @@ -3407,7 +3407,7 @@ wheels = [ [[package]] name = "x402" -version = "2.2.0" +version = "2.5.0" source = { editable = "." } dependencies = [ { name = "nest-asyncio" }, @@ -3509,7 +3509,7 @@ dev = [ [package.metadata] requires-dist = [ { name = "eth-abi", marker = "extra == 'evm'", specifier = ">=5.0.0" }, - { name = "eth-account", marker = "extra == 'evm'", specifier = ">=0.12.0" }, + { name = "eth-account", marker = "extra == 'evm'", specifier = ">=0.13.0" }, { name = "eth-keys", marker = "extra == 'evm'", specifier = ">=0.5.0" }, { name = "eth-utils", marker = "extra == 'evm'", specifier = ">=4.0.0" }, { name = "fastapi", extras = ["standard"], marker = "extra == 'fastapi'", specifier = ">=0.115.0" }, @@ -3536,7 +3536,7 @@ provides-extras = ["httpx", "requests", "flask", "fastapi", "evm", "svm", "mcp", dev = [ { name = "black", specifier = ">=23.0.0" }, { name = "eth-abi", specifier = ">=5.0.0" }, - { name = "eth-account", specifier = ">=0.12.0" }, + { name = "eth-account", specifier = ">=0.13.0" }, { name = "eth-keys", specifier = ">=0.5.0" }, { name = "eth-utils", specifier = ">=4.0.0" }, { name = "fastapi", extras = ["standard"], specifier = ">=0.115.0" }, diff --git a/specs/extensions/bazaar.md b/specs/extensions/bazaar.md index 7db7f5a2cf..96324b16ed 100644 --- a/specs/extensions/bazaar.md +++ b/specs/extensions/bazaar.md @@ -362,6 +362,43 @@ When a facilitator receives a `PaymentPayload` containing the `bazaar` extension How a facilitator stores, indexes, and exposes discovered resources is an implementation detail. Facilitators may choose to catalog resources in a database, expose them via a discovery API, or process them in any manner they see fit. +### Settlement Response Header + +After processing a `PaymentPayload`, a facilitator **MAY** append an `EXTENSION-RESPONSES` HTTP header to the settlement response to communicate extension-specific outcomes to the client. + +**Header name:** `EXTENSION-RESPONSES` + +**Header value:** A base64-encoded JSON object keyed by extension name. The `bazaar` key contains the bazaar extension's response: + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `bazaar.status` | string | Yes | One of `"success"`, `"processing"`, or `"rejected"` | +| `bazaar.rejectedReason` | string | No | Human-readable explanation. Only present when `status` is `"rejected"` | + +**Status values:** + +| Value | Meaning | +|-------|---------| +| `"success"` | The discovery info was validated and successfully cataloged | +| `"processing"` | The discovery info was accepted and is being cataloged asynchronously | +| `"rejected"` | The discovery info was rejected (e.g., failed schema validation). See `rejectedReason` for details | + +**Example (success):** + +``` +EXTENSION-RESPONSES: eyJiYXphYXIiOnsic3RhdHVzIjoic3VjY2VzcyJ9fQ== +``` +*(base64 of `{"bazaar":{"status":"success"}}`)* + +**Example (rejected):** + +``` +EXTENSION-RESPONSES: eyJiYXphYXIiOnsic3RhdHVzIjoicmVqZWN0ZWQiLCJyZWplY3RlZFJlYXNvbiI6ImluZm8gZmFpbGVkIHNjaGVtYSB2YWxpZGF0aW9uIn19 +``` +*(base64 of `{"bazaar":{"status":"rejected","rejectedReason":"info failed schema validation"}}`)* + +Clients that understand the `bazaar` extension SHOULD read the `bazaar` key of this header to confirm cataloging succeeded and surface any rejection reason for debugging. + --- ## Client Behavior @@ -370,6 +407,65 @@ Clients are expected to echo the `bazaar` extension from `PaymentRequired` into --- +## Dynamic Routes and `routeTemplate` + +HTTP endpoints can use parameterized route patterns (e.g. `/users/[userId]`). When a route has +parameter segments, the server extension enriches the extension with two additional fields: + +- **`info.input.pathParams`** — concrete parameter values for this specific request (e.g. `{ "userId": "123" }`) +- **`routeTemplate`** — the canonical template with `:param` syntax (e.g. `/users/:userId`) + +The `routeTemplate` field at the **top level** of the extension object is the catalog key contract between +server and facilitator. Facilitators use it to map all concrete requests (e.g. `/users/123`, `/users/456`) +to a single canonical catalog entry. + +### `routeTemplate` Wire Format + +- The server writes patterns using `[paramName]` syntax internally (matches the route framework convention). +- The extension delivers `routeTemplate` externally using `:paramName` syntax, consistent with REST conventions. +- The field is **absent** for static routes; facilitators MUST treat an absent `routeTemplate` as "use the concrete URL path". + +Example of an enriched extension for a dynamic route: + +```json +{ + "info": { + "input": { + "type": "http", + "method": "GET", + "pathParams": { "userId": "123" } + } + }, + "schema": { ... }, + "routeTemplate": "/users/:userId" +} +``` + +### `routeTemplate` Validation Rules + +The facilitator MUST validate `routeTemplate` before using it as a catalog key. The expected format +uses colon-prefixed parameter identifiers (e.g. `/users/:userId`, `/weather/:country/:city`). +All SDK implementations use the function `isValidRouteTemplate` (TypeScript, Go) or +`_is_valid_route_template` (Python) which applies the following rules identically. +**All three copies must stay in sync.** + +| Rule | Reason | +|------|--------| +| Must be a non-empty string | Empty/absent means "no template" | +| Must start with `/` | Prevents relative paths and external URLs | +| Must match `^/[a-zA-Z0-9_/:.\-~%]+$` | Only allows safe URL path characters and `:param` identifiers | +| Must not contain `..` | Prevents path traversal (`/users/../admin`) | +| Must not contain `://` | Prevents URL injection (`http://evil.com`) | + +All implementations decode percent-encoding (e.g. `%2e%2e` -> `..`) before applying the traversal +and scheme checks. A value that fails any rule is discarded; the facilitator falls back to the +concrete URL path for cataloging. + +> **SDK implementers:** If you add a fourth SDK, copy these validation rules exactly, including +> the percent-decoding step before the `..` and `://` checks. + +--- + ## Backwards Compatibility The `bazaar` extension was formalized in x402 v2. Discovery functionality unofficially existed in x402 v1 through the `outputSchema` field. diff --git a/specs/extensions/eip2612_gas_sponsoring.md b/specs/extensions/eip2612_gas_sponsoring.md index 49e138458e..00579025df 100644 --- a/specs/extensions/eip2612_gas_sponsoring.md +++ b/specs/extensions/eip2612_gas_sponsoring.md @@ -128,13 +128,12 @@ To utilize this extension, the client must generate a valid EIP-2612 signature a "amount": "10000" }, "from": "0x857b06519E91e3A54538791bDbb0E22373e36b66", - "spender": "0xx402Permit2ProxyAddress", - "nonce": "0xf3746613c2d920b5fdabc0856f2aeb2d4f88ee6037b8cc5d04a71a4462f13480", + "spender": "0x402085c248EeA27D92E8b30b2C58ed07f9E20001", + "nonce": "33247007178036348590600198031289925668252061821958005840077069883511451257277", "deadline": "1740672154", "witness": { "to": "0x209693Bc6afc0C5328bA36FaF03C514EF312287C", - "validAfter": "1740672089", - "extra": {} + "validAfter": "1740672089" } } }, @@ -143,7 +142,7 @@ To utilize this extension, the client must generate a valid EIP-2612 signature a "info": { "from": "0x857b06519E91e3A54538791bDbb0E22373e36b66", "asset": "0x036CbD53842c5426634e7929541eC2318f3dCF7e", - "spender": "0xCanonicalPermit2", + "spender": "0x000000000022D473030F116dDEE9F6B43aC78BA3", "amount": "115792089237316195423570985008687907853269984665640564039457584007913129639935", "nonce": "0", "deadline": "1740672154", diff --git a/specs/extensions/erc20_gas_sponsoring.md b/specs/extensions/erc20_gas_sponsoring.md index 5ac5e515fa..7e079035c2 100644 --- a/specs/extensions/erc20_gas_sponsoring.md +++ b/specs/extensions/erc20_gas_sponsoring.md @@ -154,13 +154,12 @@ Incorrect fees or nonce values invalidate the signed transaction. "amount": "10000" }, "from": "0x857b06519E91e3A54538791bDbb0E22373e36b66", - "spender": "0xx402Permit2ProxyAddress", - "nonce": "0xf3746613c2d920b5fdabc0856f2aeb2d4f88ee6037b8cc5d04a71a4462f13480", + "spender": "0x402085c248EeA27D92E8b30b2C58ed07f9E20001", + "nonce": "33247007178036348590600198031289925668252061821958005840077069883511451257277", "deadline": "1740672154", "witness": { "to": "0x209693Bc6afc0C5328bA36FaF03C514EF312287C", - "validAfter": "1740672089", - "extra": {} + "validAfter": "1740672089" } } }, @@ -169,7 +168,7 @@ Incorrect fees or nonce values invalidate the signed transaction. "info": { "from": "0x857b06519E91e3A54538791bDbb0E22373e36b66", "asset": "0x036CbD53842c5426634e7929541eC2318f3dCF7e", - "spender": "0xCanonicalPermit2", + "spender": "0x000000000022D473030F116dDEE9F6B43aC78BA3", "amount": "115792089237316195423570985008687907853269984665640564039457584007913129639935", "signedTransaction": "0x505cbf0d9a4a227e0c52c6c2d6a7588d6acca34008c8e8986a12832597641d6293af148b571c73608a2ce6496642e377d6da8dbbf5836e9bd15092f9ecab05ded3", "version": "1" diff --git a/specs/extensions/extension-offer-and-receipt.md b/specs/extensions/extension-offer-and-receipt.md new file mode 100644 index 0000000000..bb9fd12754 --- /dev/null +++ b/specs/extensions/extension-offer-and-receipt.md @@ -0,0 +1,882 @@ +# Offer and Receipt Extension + +**1. Overview** + +The Offer and Receipt Extension adds **server-side signatures** to x402, enabling: + +1. **Signed offers**: the resource server can cryptographically commit to the payment terms it presents in `accepts[]`. +2. **Signed receipts**: after successful payment and service delivery, the resource server can return a signed receipt confirming the transaction. + +This extension supports downstream use cases including: + +- dispute evidence and auditability, +- user-review attestations (e.g., "I paid and received service"), +- verifiable proof of commercial interactions for reputation systems. + +The signed offer and receipt payloads are **x402 version-agnostic** and work identically for both x402 v1 and v2. + +**2. Status, Evolution, and Forward Compatibility** + +This extension is specified as an optional, composable addition to x402. The x402 ecosystem may introduce additional extensions over time. + +Accordingly: + +- **Wire shape and field placement are not considered stable** and may change to align with x402 canonical extension architecture once standardized. +- **Behavioral requirements are stable**: the payload structures, signature formats, and verification rules in this document are normative and MUST be implemented as written, independent of serialization details. +- Implementers SHOULD design with forward compatibility in mind and SHOULD treat unknown extension-specific fields as unsupported rather than attempting best-effort interpretation. + +**3. Signed Artifact Structure** + +This extension defines exactly two signed artifacts: + +1. **Offer** — placed in the `extensions` field, corresponding to `accepts[]` entries +2. **Receipt** — returned only on success + +Both artifacts use the same top-level structure, differing only in their payload fields. + +**3.1 Common Object Shape** + +Both `offer` and `receipt` objects MUST have the following structure: + +| Field | Type | Required | Description | +| ------------ | ------- | ------------ | -------------------------------------------------------- | +| `format` | string | Yes | `"eip712"` or `"jws"` | +| `payload` | object | EIP-712 only | The canonical payload fields (omit for JWS) | +| `signature` | string | Yes | The signature (format-specific encoding) | +| `acceptIndex`| integer | No | Index into `accepts[]` (offers only) | + +See §4.1.1 for `acceptIndex` usage and verification requirements. + +**3.1.1 Format-Specific Rules** + +**When `format = "eip712"`:** +- `payload` is REQUIRED and contains the canonical payload fields +- `signature` is a hex-encoded ECDSA signature (`0x`-prefixed, 65 bytes: r+s+v) +- `network` MUST be `eip155:` and `payTo` MUST be a valid EVM address + +**When `format = "jws"`:** +- `payload` MUST be omitted (the JWS compact string already contains the payload) +- `signature` is a JWS Compact Serialization string (`header.payload.signature`) + +The `payload` field is omitted for JWS to avoid duplication and ambiguity — the payload is already encoded inside the JWS compact string. + +**3.2 EIP-712 Domain** + +All EIP-712 signatures in this extension use the following domain structure: + +```javascript +{ + name: "", + version: "1", + chainId: 1 +} +``` + +Where `name` is: +- `"x402 offer"` for signed offers +- `"x402 receipt"` for receipts + +The `chainId` is hardcoded to `1` (Ethereum mainnet) for all EIP-712 signatures in this extension. This is intentional: EIP-712 is used here purely as an off-chain signing format, not for on-chain transaction submission. The payment network is already identified by the `network` field in the payload. Using a constant `chainId` ensures EIP-712 signing works uniformly regardless of the payment network (including non-EVM networks like Solana). + +> **Versioning note:** EIP-712 artifacts have two distinct version fields: +> - **Domain `version`** (string `"1"`): Indicates the EIP-712 schema version. Changing the canonical `types` or `primaryType` requires bumping this version. +> - **Payload `version`** (integer `1`): Indicates the offer/receipt semantic version. This field is part of the signed payload and travels with the artifact for use outside x402. + +**3.2.1 EIP-712 Schema Is Normative and Not Transmitted** + +For `format = "eip712"`, the signing digest is computed using the EIP-712 domain, the message (the artifact payload), and the canonical `types` and `primaryType` defined in this specification. + +- The canonical `types` and `primaryType` definitions MUST NOT be included in transmitted x402 messages (offers/receipts). +- Signers MUST use the canonical `types` and `primaryType` definitions from this specification when producing EIP-712 signatures. +- Verifiers MUST obtain and use the same canonical `types` and `primaryType` definitions from this specification when verifying EIP-712 signatures. +- Because EIP-712 hashes the schema into the signature, any change to the canonical `types` or `primaryType` constitutes a breaking change and MUST be accompanied by explicit versioning (e.g., bumping the EIP-712 domain `version` or publishing a new spec version). + +> **Non-normative note:** Conceptually, EIP-712 maps to JWS as follows: `domain` ≈ signing context (like a header), `message` ≈ payload, `signature` ≈ signature. The EIP-712 schema (`types` and `primaryType`) is "implicit" only in the sense that it is not transmitted on the wire — it is not optional. + +> **Interoperability note:** Some ecosystems represent EIP-712 signatures as `{ domain, message, signature }`. This extension transmits EIP-712 artifacts as `{ format, payload, signature }`, where `payload` corresponds to the EIP-712 `message`. Implementations may wrap or translate these fields for use in external proof or attestation formats. + +**3.3 JWS Header Requirements** + +For JWS format, the header MUST include: + +| Field | Type | Required | Description | +| ----- | ------ | -------- | ------------------------------------------- | +| `alg` | string | Yes | Signing algorithm (e.g., `ES256K`, `EdDSA`) | +| `kid` | string | Yes | Key identifier (DID URL) for key lookup | + + +**4. Signed Offer** + +A signed offer is a cryptographic commitment by the resource server to the payment terms presented in an `accepts[]` entry. + +**4.1 Placement** + +Signed offers are placed in the `extensions` field of the payment requirements response, following the v2 extension structure: + +``` +extensions["offer-receipt"].info.offers[] +``` + +Each offer in the `info.offers` array corresponds to an entry in `accepts[]`. Servers SHOULD maintain the same ordering between `offers[]` and `accepts[]` as a convenience, but clients MUST match offers to `accepts[]` entries by comparing payload fields (`network`, `asset`, `payTo`, `amount`, etc.) rather than relying on array index ordering. For JWS format, clients extract the payload by base64url-decoding the JWS payload component. + +See §6.1 for complete examples. + +**4.1.1 acceptIndex Handling** + +Servers SHOULD include `acceptIndex` as an unsigned convenience field to help clients match offers to `accepts[]` entries. It is NOT part of the signed payload and MUST NOT be relied upon for integrity or binding. + +**Within the x402 session (clients):** + +When `acceptIndex` is present, clients SHOULD: +- Check that `acceptIndex` is in-range for the `accepts[]` array +- Validate that `accepts[acceptIndex]` terms match the signed payload fields (`network`, `asset`, `payTo`, `amount`, etc.) + +Clients MUST NOT treat `acceptIndex` as authoritative — field matching against the signed payload is the source of truth. + +**Outside the x402 session (external verifiers):** + +When an offer is stored or transmitted outside the x402 negotiation context (e.g., in attestations or reputation systems), `acceptIndex` MAY be omitted without affecting signature verification. External verifiers SHOULD ignore `acceptIndex` since the corresponding `accepts[]` list is not available. + +**4.2 Offer Payload Fields** + +Each element of the offers[] array contains the following fields: + +| Field | Type | Required | Description | +| ------------- | ------ | -------- | ------------------------------------------------------------------ | +| `version` | number | Yes | Offer payload schema version (currently `1`) | +| `resourceUrl` | string | Yes | The paid resource URL | +| `scheme` | string | Yes | Payment scheme identifier (e.g., "exact") | +| `network` | string | Yes | Blockchain network identifier (CAIP-2 format, e.g., "eip155:8453") | +| `asset` | string | Yes | Token contract address or "native" | +| `payTo` | string | Yes | Recipient wallet address | +| `amount` | string | Yes | Required payment amount | +| `validUntil` | number | Optional | Unix timestamp (seconds) when the offer expires | + +**Note**: For x402 v1, servers copy `maxAmountRequired` to `amount` when constructing the offer payload. Servers MUST convert v1 network identifiers (e.g., "base-sepolia") to CAIP-2 format (e.g., "eip155:84532") in the offer payload. + +**4.3 EIP-712 Types for Offer (Normative Schema)** + +The following `types` and `primaryType` are the canonical EIP-712 schema for offers. Per §3.2.1, these definitions are used for signing and verification but MUST NOT be transmitted on the wire. + +```javascript +{ + "primaryType": "Offer", + "types": { + "EIP712Domain": [ + { "name": "name", "type": "string" }, + { "name": "version", "type": "string" }, + { "name": "chainId", "type": "uint256" } + ], + "Offer": [ + { "name": "version", "type": "uint256" }, + { "name": "resourceUrl", "type": "string" }, + { "name": "scheme", "type": "string" }, + { "name": "network", "type": "string" }, + { "name": "asset", "type": "string" }, + { "name": "payTo", "type": "string" }, + { "name": "amount", "type": "string" }, + { "name": "validUntil", "type": "uint256" } + ] + } +} +``` + +For the optional `validUntil` field, implementations MUST set unused fields to `0`. This rule applies only to EIP-712 signing, where fixed schemas require all fields to be present. Verifiers MUST treat zero-value optional fields as equivalent to absence. + +**4.4 Offer Examples** + +**EIP-712 format:** + +```json +{ + "format": "eip712", + "acceptIndex": 0, + "payload": { + "version": 1, + "resourceUrl": "https://api.example.com/premium-data", + "scheme": "exact", + "network": "eip155:8453", + "asset": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", + "payTo": "0x209693Bc6afc0C5328bA36FaF03C514EF312287C", + "amount": "10000", + "validUntil": 1703123516 + }, + "signature": "0x1234567890abcdef..." +} +``` + +**JWS format:** + +```json +{ + "format": "jws", + "acceptIndex": 0, + "signature": "eyJhbGciOiJFUzI1NksiLCJraWQiOiJkaWQ6d2ViOmFwaS5leGFtcGxlLmNvbSNrZXktMSJ9.eyJ2ZXJzaW9uIjoxLCJyZXNvdXJjZVVybCI6Imh0dHBzOi8vYXBpLmV4YW1wbGUuY29tL3ByZW1pdW0tZGF0YSIsInNjaGVtZSI6ImV4YWN0IiwibmV0d29yayI6ImVpcDE1NTo4NDUzIiwiYXNzZXQiOiIweDgzMzU4OWZDRDZlRGI2RTA4ZjRjN0MzMkQ0ZjcxYjU0YmRBMDI5MTMiLCJwYXlUbyI6IjB4MjA5NjkzQmM2YWZjMEM1MzI4YkEzNkZhRjAzQzUxNEVGMzEyMjg3QyIsImFtb3VudCI6IjEwMDAwIiwidmFsaWRVbnRpbCI6MTcwMzEyMzUxNn0.sig" +} +``` + +**4.5 Offer Verification** + +**For EIP-712:** +1. Extract `offer.payload` and `offer.signature` +2. Check `payload.version` to select the appropriate EIP-712 types (currently only version `1` is defined; see §4.3) +3. Construct the EIP-712 typed data hash using the domain (`name: "x402 offer"`, `version: "1"`, `chainId: 1`) and the types for the payload version. The `offer.payload` object MUST be used exactly as transmitted; verifiers MUST NOT reconstruct or infer payload fields from surrounding x402 context. +4. Verify the signature and recover the signer address +5. Confirm the signer is authorized to sign for the service identified by `payload.resourceUrl` (see §4.5.1) + +**For JWS:** +1. Parse the JWS compact string from `offer.signature` +2. Extract `kid` from the JWS header; extract the payload by base64url-decoding the JWS payload component +3. Check the payload's `version` to determine how to interpret the remaining fields (currently only version `1` is defined) +4. Resolve `kid` to a public key +5. Verify the JWS signature over the complete payload +6. Confirm the key is authorized to sign for the service identified by the payload's `resourceUrl` (see §4.5.1) + +**4.5.1 Signer Authorization** + +Verifiers MUST confirm that the signing key is authorized to act on behalf of the service identified by `resourceUrl`. This specification does not mandate a specific authorization mechanism. Common approaches include: + +- **`payTo` address signing**: The simplest approach — the service signs with the private key corresponding to the `payTo` address. Verifiers accept the signature if the recovered signer matches `payTo`. +- **External key registry**: An external system (e.g., DID documents, on-chain attestations, or other key binding mechanisms) maps the signing key or `kid` to the service identity. + +**4.6 Offer Expiration** + +If `validUntil` is present and non-zero, the resource server MAY reject payment attempts where: + +``` +now > validUntil +``` + +This allows servers to limit how long they commit to specific pricing or terms. Clients SHOULD check expiration before paying to avoid rejected payments, but the enforcement decision rests with the resource server. + + +**5. Receipt** + +A receipt is a signed statement returned by the resource server **only on success**, confirming that payment was received and service was delivered. + +**5.1 Placement** + +On success, the `SettlementResponse` MAY include a receipt in the `extensions` field, following the v2 extension structure: + +``` +extensions["offer-receipt"].info.receipt +``` + +This placement is the same for both x402 v1 and v2. + +See §6.2 and §6.3 for complete examples. + +**5.2 Receipt Payload Fields** + +The canonical receipt payload contains the following fields: + +| Field | Type | Required | Description | +| ------------- | ------ | -------- | ------------------------------------------------------------------ | +| `version` | number | Yes | Receipt payload schema version (currently `1`) | +| `network` | string | Yes | Blockchain network identifier (CAIP-2 format, e.g., "eip155:8453") | +| `resourceUrl` | string | Yes | The paid resource URL | +| `payer` | string | Yes | Payer identifier (commonly a wallet address) | +| `issuedAt` | number | Yes | Unix timestamp (seconds) when receipt was issued | +| `transaction` | string | Optional | Blockchain transaction hash | + +The receipt is **privacy-minimal** by default and intentionally omits transaction references to reduce correlation risk. Servers MAY include the optional `transaction` field when stronger verifiability is preferred over privacy. If `transaction` is included, verifiers can look up the payment amount on-chain. + +**Note**: Servers MUST convert v1 network identifiers (e.g., "base-sepolia") to CAIP-2 format (e.g., "eip155:84532") in the receipt payload. + +**5.3 EIP-712 Types for Receipt (Normative Schema)** + +The following `types` and `primaryType` are the canonical EIP-712 schema for receipts. Per §3.2.1, these definitions are used for signing and verification but MUST NOT be transmitted on the wire. + +```javascript +{ + "primaryType": "Receipt", + "types": { + "EIP712Domain": [ + { "name": "name", "type": "string" }, + { "name": "version", "type": "string" }, + { "name": "chainId", "type": "uint256" } + ], + "Receipt": [ + { "name": "version", "type": "uint256" }, + { "name": "network", "type": "string" }, + { "name": "resourceUrl", "type": "string" }, + { "name": "payer", "type": "string" }, + { "name": "issuedAt", "type": "uint256" }, + { "name": "transaction", "type": "string" } + ] + } +} +``` + +For the optional `transaction` field, implementations MUST set unused fields to empty string `""`. This rule applies only to EIP-712 signing, where fixed schemas require all fields to be present. Verifiers MUST treat empty-string optional fields as equivalent to absence. + +**5.4 Receipt Examples** + +**EIP-712 format (privacy-minimal):** + +```json +{ + "format": "eip712", + "payload": { + "version": 1, + "network": "eip155:8453", + "resourceUrl": "https://api.example.com/premium-data", + "payer": "0x857b06519E91e3A54538791bDbb0E22373e36b66", + "issuedAt": 1703123456, + "transaction": "" + }, + "signature": "0x1234567890abcdef..." +} +``` + +**EIP-712 format (with transaction for verifiability):** + +```json +{ + "format": "eip712", + "payload": { + "version": 1, + "network": "eip155:8453", + "resourceUrl": "https://api.example.com/premium-data", + "payer": "0x857b06519E91e3A54538791bDbb0E22373e36b66", + "issuedAt": 1703123456, + "transaction": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef" + }, + "signature": "0x1234567890abcdef..." +} +``` + +**JWS format:** + +```json +{ + "format": "jws", + "signature": "eyJhbGciOiJFUzI1NksiLCJraWQiOiJkaWQ6d2ViOmFwaS5leGFtcGxlLmNvbSNrZXktMSJ9.eyJ2ZXJzaW9uIjoxLCJuZXR3b3JrIjoiZWlwMTU1Ojg0NTMiLCJyZXNvdXJjZVVybCI6Imh0dHBzOi8vYXBpLmV4YW1wbGUuY29tL3ByZW1pdW0tZGF0YSIsInBheWVyIjoiMHg4NTdiMDY1MTlFOTFlM0E1NDUzOGI5MWJEYmIwRTIyMzczZTM2YjY2IiwiaXNzdWVkQXQiOjE3MDMxMjM0NTZ9.sig" +} +``` + +**5.5 Receipt Verification** + +**For EIP-712:** +1. Extract `receipt.payload` and `receipt.signature` +2. Check `payload.version` to select the appropriate EIP-712 types (currently only version `1` is defined; see §5.3) +3. Construct the EIP-712 typed data hash using the domain (`name: "x402 receipt"`, `version: "1"`, `chainId: 1`) and the types for the payload version. The `receipt.payload` object MUST be used exactly as transmitted; verifiers MUST NOT reconstruct or infer payload fields from surrounding x402 context. +4. Verify the signature and recover the signer address +5. Confirm the signer is authorized to sign for the service identified by `payload.resourceUrl` (see §4.5.1) +6. Confirm `issuedAt` is within acceptable verifier policy +7. If `transaction` is present and non-empty, verifiers MAY check the blockchain to confirm the transaction exists and matches expected parameters + +**For JWS:** +1. Parse the JWS compact string from `receipt.signature` +2. Extract `kid` from the JWS header; extract the payload by base64url-decoding the JWS payload component +3. Check the payload's `version` to determine how to interpret the remaining fields (currently only version `1` is defined) +4. Resolve `kid` to a public key +5. Verify the JWS signature over the complete payload +6. Confirm the key is authorized to sign for the service identified by the payload's `resourceUrl` (see §4.5.1) +7. Confirm `issuedAt` (from the payload) is within acceptable verifier policy +8. If `transaction` is present, verifiers MAY check the blockchain to confirm the transaction exists + + +**6. Protocol Integration Examples** + +This section provides complete examples showing how signed offers and receipts integrate with x402 protocol messages. A server would typically use one signature format consistently (EIP-712 or JWS), so examples are shown separately. + +Note: x402 v1 uses human-readable network identifiers (e.g., "base") in the protocol messages, but the offer and receipt payloads MUST use CAIP-2 format (e.g., "eip155:8453") for portability and EIP-712 domain construction. + +**6.1 Payment Requirements with Signed Offers (EIP-712, x402 v2)** + +```json +{ + "x402Version": 2, + "resource": { + "url": "https://api.example.com/premium-data", + "description": "Access to premium market data", + "mimeType": "application/json" + }, + "accepts": [ + { + "scheme": "exact", + "network": "eip155:8453", + "amount": "10000", + "asset": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", + "payTo": "0x209693Bc6afc0C5328bA36FaF03C514EF312287C", + "maxTimeoutSeconds": 60 + } + ], + "extensions": { + "offer-receipt": { + "info": { + "offers": [ + { + "format": "eip712", + "acceptIndex": 0, + "payload": { + "version": 1, + "resourceUrl": "https://api.example.com/premium-data", + "scheme": "exact", + "network": "eip155:8453", + "asset": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", + "payTo": "0x209693Bc6afc0C5328bA36FaF03C514EF312287C", + "amount": "10000", + "validUntil": 1703123516 + }, + "signature": "0x1234567890abcdef..." + } + ] + }, + "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "offers": { + "type": "array", + "items": { + "type": "object", + "properties": { + "format": { "type": "string", "const": "eip712" }, + "acceptIndex": { "type": "integer" }, + "payload": { + "type": "object", + "properties": { + "version": { "type": "integer" }, + "resourceUrl": { "type": "string" }, + "scheme": { "type": "string" }, + "network": { "type": "string" }, + "asset": { "type": "string" }, + "payTo": { "type": "string" }, + "amount": { "type": "string" }, + "validUntil": { "type": "integer" } + }, + "required": ["version", "resourceUrl", "scheme", "network", "asset", "payTo", "amount"] + }, + "signature": { "type": "string" } + }, + "required": ["format", "payload", "signature"] + } + } + }, + "required": ["offers"] + } + } + } +} +``` + +**6.2 Payment Requirements with Signed Offers (EIP-712, x402 v1)** + +```json +{ + "x402Version": 1, + "accepts": [ + { + "scheme": "exact", + "network": "base", + "maxAmountRequired": "10000", + "asset": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", + "payTo": "0x209693Bc6afc0C5328bA36FaF03C514EF312287C", + "resource": "https://api.example.com/premium-data", + "description": "Access to premium market data", + "mimeType": "application/json", + "maxTimeoutSeconds": 60 + } + ], + "extensions": { + "offer-receipt": { + "info": { + "offers": [ + { + "format": "eip712", + "acceptIndex": 0, + "payload": { + "version": 1, + "resourceUrl": "https://api.example.com/premium-data", + "scheme": "exact", + "network": "eip155:8453", + "asset": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", + "payTo": "0x209693Bc6afc0C5328bA36FaF03C514EF312287C", + "amount": "10000", + "validUntil": 1703123516 + }, + "signature": "0x1234567890abcdef..." + } + ] + }, + "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "offers": { + "type": "array", + "items": { + "type": "object", + "properties": { + "format": { "type": "string", "const": "eip712" }, + "acceptIndex": { "type": "integer" }, + "payload": { + "type": "object", + "properties": { + "version": { "type": "integer" }, + "resourceUrl": { "type": "string" }, + "scheme": { "type": "string" }, + "network": { "type": "string" }, + "asset": { "type": "string" }, + "payTo": { "type": "string" }, + "amount": { "type": "string" }, + "validUntil": { "type": "integer" } + }, + "required": ["version", "resourceUrl", "scheme", "network", "asset", "payTo", "amount"] + }, + "signature": { "type": "string" } + }, + "required": ["format", "payload", "signature"] + } + } + }, + "required": ["offers"] + } + } + } +} +``` + +**6.3 Payment Requirements with Signed Offers (JWS, x402 v2)** + +```json +{ + "x402Version": 2, + "resource": { + "url": "https://api.example.com/premium-data", + "description": "Access to premium market data", + "mimeType": "application/json" + }, + "accepts": [ + { + "scheme": "exact", + "network": "eip155:8453", + "amount": "10000", + "asset": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", + "payTo": "0x209693Bc6afc0C5328bA36FaF03C514EF312287C", + "maxTimeoutSeconds": 60 + } + ], + "extensions": { + "offer-receipt": { + "info": { + "offers": [ + { + "format": "jws", + "acceptIndex": 0, + "signature": "eyJhbGciOiJFUzI1NksiLCJraWQiOiJkaWQ6d2ViOmFwaS5leGFtcGxlLmNvbSNrZXktMSJ9.eyJ2ZXJzaW9uIjoxLCJyZXNvdXJjZVVybCI6Imh0dHBzOi8vYXBpLmV4YW1wbGUuY29tL3ByZW1pdW0tZGF0YSIsInNjaGVtZSI6ImV4YWN0IiwibmV0d29yayI6ImVpcDE1NTo4NDUzIiwiYXNzZXQiOiIweDgzMzU4OWZDRDZlRGI2RTA4ZjRjN0MzMkQ0ZjcxYjU0YmRBMDI5MTMiLCJwYXlUbyI6IjB4MjA5NjkzQmM2YWZjMEM1MzI4YkEzNkZhRjAzQzUxNEVGMzEyMjg3QyIsImFtb3VudCI6IjEwMDAwIiwidmFsaWRVbnRpbCI6MTcwMzEyMzUxNn0.sig" + } + ] + }, + "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "offers": { + "type": "array", + "items": { + "type": "object", + "properties": { + "format": { "type": "string", "const": "jws" }, + "acceptIndex": { "type": "integer" }, + "signature": { "type": "string", "description": "JWS compact serialization containing the offer payload" } + }, + "required": ["format", "signature"] + } + } + }, + "required": ["offers"] + } + } + } +} +``` + +**6.4 Payment Requirements with Signed Offers (JWS, x402 v1)** + +```json +{ + "x402Version": 1, + "accepts": [ + { + "scheme": "exact", + "network": "base", + "maxAmountRequired": "10000", + "asset": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", + "payTo": "0x209693Bc6afc0C5328bA36FaF03C514EF312287C", + "resource": "https://api.example.com/premium-data", + "description": "Access to premium market data", + "mimeType": "application/json", + "maxTimeoutSeconds": 60 + } + ], + "extensions": { + "offer-receipt": { + "info": { + "offers": [ + { + "format": "jws", + "acceptIndex": 0, + "signature": "eyJhbGciOiJFUzI1NksiLCJraWQiOiJkaWQ6d2ViOmFwaS5leGFtcGxlLmNvbSNrZXktMSJ9.eyJ2ZXJzaW9uIjoxLCJyZXNvdXJjZVVybCI6Imh0dHBzOi8vYXBpLmV4YW1wbGUuY29tL3ByZW1pdW0tZGF0YSIsInNjaGVtZSI6ImV4YWN0IiwibmV0d29yayI6ImVpcDE1NTo4NDUzIiwiYXNzZXQiOiIweDgzMzU4OWZDRDZlRGI2RTA4ZjRjN0MzMkQ0ZjcxYjU0YmRBMDI5MTMiLCJwYXlUbyI6IjB4MjA5NjkzQmM2YWZjMEM1MzI4YkEzNkZhRjAzQzUxNEVGMzEyMjg3QyIsImFtb3VudCI6IjEwMDAwIiwidmFsaWRVbnRpbCI6MTcwMzEyMzUxNn0.sig" + } + ] + }, + "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "offers": { + "type": "array", + "items": { + "type": "object", + "properties": { + "format": { "type": "string", "const": "jws" }, + "acceptIndex": { "type": "integer" }, + "signature": { "type": "string", "description": "JWS compact serialization containing the offer payload" } + }, + "required": ["format", "signature"] + } + } + }, + "required": ["offers"] + } + } + } +} +``` + +**6.5 Success Response with Receipt (EIP-712, x402 v2)** + +```json +{ + "success": true, + "transaction": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", + "network": "eip155:8453", + "payer": "0x857b06519E91e3A54538791bDbb0E22373e36b66", + "extensions": { + "offer-receipt": { + "info": { + "receipt": { + "format": "eip712", + "payload": { + "version": 1, + "network": "eip155:8453", + "resourceUrl": "https://api.example.com/premium-data", + "payer": "0x857b06519E91e3A54538791bDbb0E22373e36b66", + "issuedAt": 1703123456, + "transaction": "" + }, + "signature": "0x1234567890abcdef..." + } + }, + "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "receipt": { + "type": "object", + "properties": { + "format": { "type": "string", "const": "eip712" }, + "payload": { + "type": "object", + "properties": { + "version": { "type": "integer" }, + "network": { "type": "string" }, + "resourceUrl": { "type": "string" }, + "payer": { "type": "string" }, + "issuedAt": { "type": "integer" }, + "transaction": { "type": "string" } + }, + "required": ["version", "network", "resourceUrl", "payer", "issuedAt"] + }, + "signature": { "type": "string" } + }, + "required": ["format", "payload", "signature"] + } + }, + "required": ["receipt"] + } + } + } +} +``` + +**6.6 Success Response with Receipt (EIP-712, x402 v1)** + +```json +{ + "success": true, + "transaction": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", + "network": "base", + "payer": "0x857b06519E91e3A54538791bDbb0E22373e36b66", + "extensions": { + "offer-receipt": { + "info": { + "receipt": { + "format": "eip712", + "payload": { + "version": 1, + "network": "eip155:8453", + "resourceUrl": "https://api.example.com/premium-data", + "payer": "0x857b06519E91e3A54538791bDbb0E22373e36b66", + "issuedAt": 1703123456, + "transaction": "" + }, + "signature": "0x1234567890abcdef..." + } + }, + "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "receipt": { + "type": "object", + "properties": { + "format": { "type": "string", "const": "eip712" }, + "payload": { + "type": "object", + "properties": { + "version": { "type": "integer" }, + "network": { "type": "string" }, + "resourceUrl": { "type": "string" }, + "payer": { "type": "string" }, + "issuedAt": { "type": "integer" }, + "transaction": { "type": "string" } + }, + "required": ["version", "network", "resourceUrl", "payer", "issuedAt"] + }, + "signature": { "type": "string" } + }, + "required": ["format", "payload", "signature"] + } + }, + "required": ["receipt"] + } + } + } +} +``` + +**6.7 Success Response with Receipt (JWS, x402 v2)** + +```json +{ + "success": true, + "transaction": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", + "network": "eip155:8453", + "payer": "0x857b06519E91e3A54538791bDbb0E22373e36b66", + "extensions": { + "offer-receipt": { + "info": { + "receipt": { + "format": "jws", + "signature": "eyJhbGciOiJFUzI1NksiLCJraWQiOiJkaWQ6d2ViOmFwaS5leGFtcGxlLmNvbSNrZXktMSJ9.eyJ2ZXJzaW9uIjoxLCJuZXR3b3JrIjoiZWlwMTU1Ojg0NTMiLCJyZXNvdXJjZVVybCI6Imh0dHBzOi8vYXBpLmV4YW1wbGUuY29tL3ByZW1pdW0tZGF0YSIsInBheWVyIjoiMHg4NTdiMDY1MTlFOTFlM0E1NDUzOGI5MWJEYmIwRTIyMzczZTM2YjY2IiwiaXNzdWVkQXQiOjE3MDMxMjM0NTYsInRyYW5zYWN0aW9uIjoiIn0.sig" + } + }, + "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "receipt": { + "type": "object", + "properties": { + "format": { "type": "string", "const": "jws" }, + "signature": { "type": "string", "description": "JWS compact serialization containing the receipt payload" } + }, + "required": ["format", "signature"] + } + }, + "required": ["receipt"] + } + } + } +} +``` + +**6.8 Success Response with Receipt (JWS, x402 v1)** + +```json +{ + "success": true, + "transaction": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", + "network": "base", + "payer": "0x857b06519E91e3A54538791bDbb0E22373e36b66", + "extensions": { + "offer-receipt": { + "info": { + "receipt": { + "format": "jws", + "signature": "eyJhbGciOiJFUzI1NksiLCJraWQiOiJkaWQ6d2ViOmFwaS5leGFtcGxlLmNvbSNrZXktMSJ9.eyJ2ZXJzaW9uIjoxLCJuZXR3b3JrIjoiZWlwMTU1Ojg0NTMiLCJyZXNvdXJjZVVybCI6Imh0dHBzOi8vYXBpLmV4YW1wbGUuY29tL3ByZW1pdW0tZGF0YSIsInBheWVyIjoiMHg4NTdiMDY1MTlFOTFlM0E1NDUzOGI5MWJEYmIwRTIyMzczZTM2YjY2IiwiaXNzdWVkQXQiOjE3MDMxMjM0NTYsInRyYW5zYWN0aW9uIjoiIn0.sig" + } + }, + "schema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "receipt": { + "type": "object", + "properties": { + "format": { "type": "string", "const": "jws" }, + "signature": { "type": "string", "description": "JWS compact serialization containing the receipt payload" } + }, + "required": ["format", "signature"] + } + }, + "required": ["receipt"] + } + } + } +} +``` + +**7. Key Discovery and Trust** + +This extension does not mandate a specific trust system for mapping the server's signing key to an identity. See §4.5.1 for signer authorization options. + +For EIP-712 signatures, the signer address is recovered from the signature. The simplest deployment uses the `payTo` address as the signing key. + +For JWS signatures, the `kid` header field provides the key identifier for lookup. + +**8. Use Cases (Non-Normative)** + +This extension defines signed offers and signed receipts that can be carried alongside x402 flows. These artifacts are designed to be portable and independently verifiable, enabling optional trust and audit layers without changing payment execution or settlement semantics. + +- **Attestation-backed discovery and trust for paid endpoints**: Signed offers and receipts can be embedded as evidence in attestations (e.g., user reviews). Those attestations can support discovery, filtering, and reputation scoring for paid API/service endpoints — an area that typically lacks the trust provided by user reviews in app stores and ecommerce sites. + +- **Auditability and dispute/feedback evidence**: Signed artifacts provide verifiable evidence of what terms were presented and, when applicable, that service was delivered. This supports auditing, customer support, and dispute workflows, including scenarios involving automated purchasers (agents) and enterprise procurement. + +- **Agent-to-agent commerce**: Autonomous agents making purchasing decisions need machine-verifiable proof of terms and delivery. Signed offers let an agent's principal (human or system) audit what deals the agent accepted; receipts prove the agent received the promised service. + +- **Why offers matter even without receipts**: A signed offer can be used as evidence even when no receipt is available (e.g., the user did not complete payment, the service did not return a receipt, or the user wants to provide feedback about pricing/terms). Offers prove the server's stated terms at a point in time; receipts prove successful service delivery. + +**9. Integration with Proof Systems** + +The `offer` and `receipt` objects defined in this extension are designed to be usable as proof artifacts in attestation systems. These objects are intentionally self-contained so they can be lifted verbatim into external proof or attestation formats without reconstruction. + +**10. Security Considerations** + +- Implementations MUST ensure canonicalization rules are applied consistently (JCS for JWS payloads, EIP-712 rules for EIP-712). +- Servers MUST NOT include the `signature` field in the payload being signed to avoid circularity. +- Servers should consider replay implications of long-lived signed offers; including `validUntil` can reduce risk. +- Receipts and offers are transferable artifacts; possession of a valid server signature is sufficient for verification. Transport-layer security (HTTPS) is essential. + +**11. Privacy Considerations** + +- Receipts are minimal by default — they omit transaction references to reduce correlation risk. +- Servers MAY include the optional `transaction` field when verifiability is more important than privacy for their use case. +- Offers reveal economic terms (amount, asset, payTo address). +- Attestations MAY include either offers, receipts, or both. +- Implementations SHOULD consider privacy implications when deciding which artifacts to include in public attestations. + +**12. Version History** + +| Version | Date | Changes | Author | +| ------- | ---------- | -------------------------------------------------------------- | ---------- | +| 0.6 | 2026-02-04 | Make EIP-712 chain-agnostic: chainId=1, payTo type=string. | Alfred Tom | +| 0.5 | 2026-01-29 | First approved release. | Alfred Tom | +| 0.4 | 2026-01-26 | Add acceptIndex as unsigned envelope field. | Alfred Tom | +| 0.3 | 2026-01-22 | Add validUntil for offer expiration. Move version to payload. | Alfred Tom | +| 0.2 | 2026-01-20 | Move offers/receipt to extensions. Add network to receipt. | Alfred Tom | +| 0.1 | 2025-12-22 | Initial extension draft. | Alfred Tom | diff --git a/specs/extensions/specs/extensions/diagnostic.md b/specs/extensions/specs/extensions/diagnostic.md new file mode 100644 index 0000000000..0581a4aa73 --- /dev/null +++ b/specs/extensions/specs/extensions/diagnostic.md @@ -0,0 +1,369 @@ +**Extension name:** `diagnostic` +**Status:** Proposed +**Author:** @jonathanbulkeley +**Closes:** #1860 + +--- + +## Summary + +This extension adds an optional `diagnostic` field to x402 402 responses, providing a machine-readable vocabulary for communicating payment failure state. It enables autonomous agents to distinguish between retriable errors, non-retriable errors, and situations requiring human escalation — and enables client SDKs to implement appropriate handling for each case. + +--- + +## Motivation + +The current x402 specification defines what a 402 response looks like when payment is required. It does not define any mechanism for communicating *why* payment is failing when a client repeatedly attempts and fails to pay. + +Every 402 response looks identical regardless of whether it is: + +- A first request from a new client legitimately discovering payment requirements +- A payment attempt where the invoice expired before settlement +- A payment attempt where the wallet has insufficient funds +- A broken agent that has made thousands of requests with zero successful payments + +From the server's perspective these are completely different situations. From the protocol's perspective they are indistinguishable. + +### Real-world case + +A production x402 oracle (myceliasignal.com) received 8,000+ daily requests from a client over 18 days with zero successful payments. The client had previously paid successfully. Something in its payment stack broke. The server had no protocol mechanism to signal the issue. The client received 144,000+ identical 402 responses. The operator was unaware. + +This is not an edge case. As agentic payment volume grows, broken payment logic will become a regular occurrence. Without a diagnostic vocabulary, every failure is invisible until someone manually inspects server logs. + +--- + +## Extension Format + +This extension follows the x402 v2 extension pattern. When present, the `diagnostic` object appears inside the `extensions` field of the 402 response body. + +### Response example + +```json +{ + "x402Version": 2, + "error": "Payment required", + "resource": { + "url": "https://api.example.com/resource" + }, + "accepts": ["x402"], + "extensions": { + "diagnostic": { + "info": { + "code": "PAYMENT_ATTEMPTS_EXCEEDED", + "message": "8,432 requests received with no valid payment in 18 days.", + "attempts": 8432, + "firstAttempt": "2026-03-10T00:00:00Z", + "suggestion": "Check payment handler configuration and wallet balance.", + "escalate": true + }, + "schema": "https://raw.githubusercontent.com/coinbase/x402/main/specs/extensions/diagnostic.schema.json" + } + } +} +``` + +--- + +## Field Reference + +### `extensions.diagnostic` + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `info` | object | yes | Diagnostic information object | +| `schema` | string | no | URL to the JSON schema for validation. When this spec is merged, the canonical URL will be `https://raw.githubusercontent.com/coinbase/x402/main/specs/extensions/diagnostic.schema.json`. Until then this field SHOULD be omitted. | + +### `extensions.diagnostic.info` + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `code` | string | yes | Machine-readable diagnostic code (see codes below) | +| `message` | string | no | Human-readable description of the failure state | +| `attempts` | integer | no | Number of requests received without a successful payment | +| `firstAttempt` | string (ISO 8601) | no | Timestamp of the first request in the current failure sequence | +| `suggestion` | string | no | Actionable suggestion for recovery | +| `escalate` | boolean | no | When `true`, autonomous resolution is unlikely — the client should halt and surface to a human operator | + +--- + +## Diagnostic Codes + +| Code | Meaning | Retriable | Escalate | +|------|---------|-----------|----------| +| `PAYMENT_REQUIRED` | Standard first-request 402, no prior attempts | Yes | No | +| `INVOICE_EXPIRED` | Payment was attempted but the invoice expired before settlement | Yes | No | +| `PAYMENT_UNVERIFIED` | Payment header was present but signature verification failed | No | Recommended | +| `PAYMENT_ATTEMPTS_EXCEEDED` | Many requests received with no successful payment | No | Yes | +| `WALLET_INSUFFICIENT_FUNDS` | On-chain balance is too low to cover the required payment | No | Yes | +| `OPERATOR_ALERT` | Server cannot determine root cause — escalate to human operator | No | Yes | + +### Code semantics + +**`PAYMENT_REQUIRED`** +The default state. No diagnostic history. The client should proceed with the normal payment flow. Servers MAY omit the `diagnostic` field entirely for first requests; including it with this code is optional but provides consistency for clients that always parse the extension. + +**`INVOICE_EXPIRED`** +The client attempted payment but the payment window expired before settlement. In x402 (USDC on Base), this corresponds to an expired EIP-3009 `validBefore` timestamp. In L402 (Lightning), this corresponds to a Lightning invoice that expired before it was paid. The client should request a fresh payment challenge and retry. Retriable once — if a second attempt also expires, escalation is recommended. Common causes: slow routing, delayed client processing, or a client clock skew issue. + +**`PAYMENT_UNVERIFIED`** +A payment header (`X-PAYMENT`) was present in the request but signature verification failed. The client should not retry automatically — the payment logic itself is likely broken. Surface to the operator for review. + +**`PAYMENT_ATTEMPTS_EXCEEDED`** +The server has received a high volume of requests from this client with no successful payments. The client's payment handler is likely broken or the wallet is empty and not being refilled. The client should halt, stop retrying, and alert the operator. + +**`WALLET_INSUFFICIENT_FUNDS`** +The facilitator returned an `insufficient_funds` signal during payment verification. The client's wallet does not have enough funds to cover the payment. The client should halt and surface the wallet balance to the operator. + +**`OPERATOR_ALERT`** +A general escalation code for situations where the server cannot determine the specific root cause but is confident that autonomous resolution is unlikely. The client should treat this identically to `PAYMENT_ATTEMPTS_EXCEEDED` — halt, stop retrying, and alert the operator. + +--- + +## `escalate` Flag + +The `escalate: true` flag is the primary signal for client SDKs to implement human escalation. When a client receives `escalate: true`: + +1. **Stop retrying** the current endpoint immediately +2. **Emit a structured escalation event** with the full diagnostic payload attached (see Client Implementation below) +3. **Block further requests** to this endpoint until an operator reviews and explicitly re-enables it + +The `escalate` flag is distinct from the `code` field. A server MAY set `escalate: true` with any code, not only the codes listed as "Escalate: Yes" above. The codes provide semantic meaning; the flag provides the behavioral signal. + +--- + +## Client Implementation + +### Parsing and routing + +Client SDKs SHOULD parse `extensions.diagnostic.info.code` and implement the following behavior: + +| Code | Recommended client behavior | +|------|---------------------------| +| `PAYMENT_REQUIRED` | Proceed with normal payment flow | +| `INVOICE_EXPIRED` | Request fresh payment challenge, retry once — if second attempt also expires, surface to operator | +| `PAYMENT_UNVERIFIED` | Do not retry, surface to operator via approval queue | +| `PAYMENT_ATTEMPTS_EXCEEDED` | Halt, emit escalation event, block endpoint | +| `WALLET_INSUFFICIENT_FUNDS` | Halt, surface wallet balance to operator, block endpoint | +| `OPERATOR_ALERT` | Halt, emit escalation event, block endpoint | + +### Escalation event schema + +When `escalate: true` is received, client SDKs SHOULD emit a structured escalation event to the operator's notification surface. Recommended event shape: + +```json +{ + "type": "x402_escalation", + "endpoint": "https://api.example.com/resource", + "amount": 0.01, + "currency": "USDC", + "correlation_id": "req_8432", + "diagnostic": { + "code": "PAYMENT_ATTEMPTS_EXCEEDED", + "attempts": 8432, + "firstAttempt": "2026-03-10T00:00:00Z", + "escalate": true + }, + "timestamp": "2026-03-29T00:00:00Z" +} +``` + +The `correlation_id` field ties the escalation event back to the specific request chain, making it actionable for the operator rather than just informational. + +### Graceful degradation + +Clients that do not implement diagnostic parsing MUST continue to function correctly — the `extensions` field is ignored by clients that do not recognise it. The extension is purely additive. + +--- + +## Server Implementation + +### When to emit diagnostics + +Servers MAY emit the `diagnostic` extension on any 402 response. Recommended thresholds: + +- Emit `PAYMENT_REQUIRED` (or no diagnostic) for first requests and early retries +- Emit `INVOICE_EXPIRED` when a payment attempt is detected but invoice has passed its validity window +- Emit `PAYMENT_UNVERIFIED` when `X-PAYMENT` is present but signature verification fails +- Emit `PAYMENT_ATTEMPTS_EXCEEDED` after a configurable threshold of failed attempts (suggested: 100+ attempts with no successful payment). Servers SHOULD increase the urgency of the `message` field as attempts grow — e.g. note the duration as well as the count. Servers SHOULD NOT emit this code on first contact or early retries; a threshold prevents false positives from clients that legitimately retry a small number of times. + +### Attempt tracking thresholds (suggested) + +| Attempts | Recommended code | `escalate` | +|----------|-----------------|-----------| +| 1–9 | No diagnostic (or `PAYMENT_REQUIRED`) | false | +| 10–99 | No diagnostic | false | +| 100–999 | `PAYMENT_ATTEMPTS_EXCEEDED` | true | +| 1000+ | `PAYMENT_ATTEMPTS_EXCEEDED` | true | + +These are suggestions only. Servers MAY use lower thresholds for high-value endpoints or higher thresholds for high-volume low-cost endpoints. +- Emit `WALLET_INSUFFICIENT_FUNDS` when the facilitator returns `insufficient_funds` +- Emit `OPERATOR_ALERT` at server discretion for other persistent failure patterns + +### Attempt tracking + +Servers that implement `PAYMENT_ATTEMPTS_EXCEEDED` SHOULD track attempts per client identifier. Recommended identifiers (in order of preference): + +1. `payer` address from the `X-PAYMENT` header (most precise — ties to a specific wallet) +2. IP address (fallback — less precise, subject to spoofing) + +Servers SHOULD NOT penalise clients for `insufficient_funds` responses — these indicate an empty wallet, not a bad actor. The `WALLET_INSUFFICIENT_FUNDS` code communicates this state without implying malicious intent. + +### Security considerations + +**Information disclosure:** The `attempts` and `firstAttempt` fields reveal server-side tracking state. Servers SHOULD only emit these fields for clients that have already made multiple requests — do not reveal tracking state on first contact. + +**Privacy:** Servers SHOULD NOT include personally identifiable information in `message` or `suggestion` fields. These fields are machine-readable and may be logged by client SDKs. + +**Spoofing:** The diagnostic extension is advisory only. Clients MUST NOT use diagnostic codes to make security-critical decisions. A malicious server could emit misleading codes; clients should treat diagnostics as operational hints, not authoritative signals. + +--- + +## Out-of-Band Operator Contact (Future Extension) + +This extension solves in-band signaling — the server tells the client what is wrong. A complementary problem remains: when the client runtime is too broken to read the diagnostic, there is no protocol mechanism for the server to reach the human operator directly. + +A future extension could define a standard `X-Operator-Contact` or `X-Operator-Webhook` header that clients include in payment requests, providing the server with a direct escalation channel. This is intentionally out of scope for this extension to keep the initial surface area minimal. Privacy implications (exposing operator webhooks to every server an agent pays) require careful design and a separate discussion. + +--- + +## Backward Compatibility + +This extension is fully backward compatible: + +- The `extensions` field is optional in x402 v2 responses +- Existing clients that do not parse `extensions.diagnostic` continue to function unchanged +- Servers can implement the extension incrementally — emitting diagnostics for some failure patterns before others +- No changes to the payment flow, headers, or settlement process + +--- + +## Reference Implementation + +### Server (Python — x402_proxy.py pattern) + +```python +from collections import defaultdict +from datetime import datetime, timezone + +_attempt_counts = defaultdict(int) +_first_attempt = {} + +def get_diagnostic(payer_address: str, failure_reason: str) -> dict | None: + key = payer_address or "unknown" + _attempt_counts[key] += 1 + if key not in _first_attempt: + _first_attempt[key] = datetime.now(timezone.utc).isoformat() + + attempts = _attempt_counts[key] + + if failure_reason == "insufficient_funds": + return { + "code": "WALLET_INSUFFICIENT_FUNDS", + "message": "Wallet balance too low to cover payment.", + "attempts": attempts, + "firstAttempt": _first_attempt[key], + "suggestion": "Top up your wallet and retry.", + "escalate": True + } + elif failure_reason == "signature_invalid": + return { + "code": "PAYMENT_UNVERIFIED", + "message": "Payment signature verification failed.", + "attempts": attempts, + "firstAttempt": _first_attempt[key], + "suggestion": "Check payment signing configuration.", + "escalate": True + } + elif attempts >= 100: + return { + "code": "PAYMENT_ATTEMPTS_EXCEEDED", + "message": f"{attempts} requests received with no valid payment since {_first_attempt[key]}.", + "attempts": attempts, + "firstAttempt": _first_attempt[key], + "suggestion": "Check payment handler configuration and wallet balance.", + "escalate": True + } + + return None # No diagnostic for early retries + + +def build_402_response(payer_address: str = None, failure_reason: str = None) -> dict: + response = { + "x402Version": 2, + "error": "Payment required", + "accepts": ["x402"], + # ... payment requirements ... + } + + diagnostic = get_diagnostic(payer_address, failure_reason) + if diagnostic: + response["extensions"] = { + "diagnostic": { + "info": diagnostic, + "schema": "https://raw.githubusercontent.com/coinbase/x402/main/specs/extensions/diagnostic.schema.json" + } + } + + return response +``` + +### Client (TypeScript — SDK integration pattern) + +```typescript +interface DiagnosticInfo { + code: 'PAYMENT_REQUIRED' | 'INVOICE_EXPIRED' | 'PAYMENT_UNVERIFIED' | + 'PAYMENT_ATTEMPTS_EXCEEDED' | 'WALLET_INSUFFICIENT_FUNDS' | 'OPERATOR_ALERT'; + message?: string; + attempts?: number; + firstAttempt?: string; + suggestion?: string; + escalate?: boolean; +} + +async function handle402Response( + response: Response, + correlationId: string, + onEscalate: (event: object) => void +): Promise<'retry' | 'halt'> { + const body = await response.json(); + const diagnostic: DiagnosticInfo | undefined = + body?.extensions?.diagnostic?.info; + + if (!diagnostic) { + // No diagnostic — proceed with normal payment flow + return 'retry'; + } + + if (diagnostic.escalate) { + onEscalate({ + type: 'x402_escalation', + endpoint: body?.resource?.url, + correlation_id: correlationId, + diagnostic, + timestamp: new Date().toISOString(), + }); + return 'halt'; + } + + switch (diagnostic.code) { + case 'PAYMENT_REQUIRED': + return 'retry'; + case 'INVOICE_EXPIRED': + // Request fresh invoice and retry once + return 'retry'; + case 'PAYMENT_UNVERIFIED': + case 'PAYMENT_ATTEMPTS_EXCEEDED': + case 'WALLET_INSUFFICIENT_FUNDS': + case 'OPERATOR_ALERT': + return 'halt'; + default: + return 'retry'; + } +} +``` + +--- + +## Acknowledgements + +This extension was developed from production experience operating a pay-per-query oracle (myceliasignal.com) and refined through discussion in issue #1860. Thanks to @0xAxiom for filing the first spec PR (#1866) which demonstrated the extension format and provided a strong implementation foundation; to @hermesnousagent for the receipt trail and `correlation_id` additions; and to @up2itnow0822 (agentwallet-sdk) for the code-to-behavior mapping that informed the client implementation guidance. diff --git a/specs/schemes/exact/scheme_exact_evm.md b/specs/schemes/exact/scheme_exact_evm.md index 70ed5f715c..605033bcf2 100644 --- a/specs/schemes/exact/scheme_exact_evm.md +++ b/specs/schemes/exact/scheme_exact_evm.md @@ -6,14 +6,15 @@ The `exact` scheme on EVM executes a transfer where the Facilitator (server) pay This is implemented via one of two asset transfer methods, depending on the token's capabilities: -| AssetTransferMethod | Use Case | Recommendation | -| :------------------ | :----------------------------------------------------------- | :--------------------------------------------- | -| **1. EIP-3009** | Tokens with native `transferWithAuthorization` (e.g., USDC). | **Recommended** (Simplest, truly gasless). | -| **2. Permit2** | Tokens without EIP-3009. Uses a Proxy + Permit2. | **Universal Fallback** (Works for any ERC-20). | +| AssetTransferMethod | Use Case | Recommendation | Usage Semantics | +| :------------------ | :----------------------------------------------------------- | :--------------------------------------------- | :---------------------------------- | +| **1. EIP-3009** | Tokens with native `transferWithAuthorization` (e.g., USDC). | **Recommended** (Simplest, truly gasless). | One-time use | +| **2. Permit2** | Tokens without EIP-3009. Uses a Proxy + Permit2. | **Universal Fallback** (Works for any ERC-20). | One-time use | +| **3. ERC-7710** | Smart accounts with delegation support. | **Smart Account Option** (Paid from ERC-7710 compatible account). | One-time use and multi-use | If no `assetTransferMethod` is specified in the payload, the implementation should prioritize `eip3009` (if compatible) and then `permit2`. -In both cases, the Facilitator cannot modify the amount or destination. They serve only as the transaction broadcaster. +In all cases, the Facilitator cannot modify the amount or destination. They serve only as the transaction broadcaster. --- @@ -144,13 +145,12 @@ The `payload` field must contain: "amount": "10000" }, "from": "0x857b06519E91e3A54538791bDbb0E22373e36b66", - "spender": "0x4020CD856C882D5fb903D99CE35316A085Bb0001", // Canonical x402ExactPermit2Proxy address - "nonce": "0xf3746613c2d920b5fdabc0856f2aeb2d4f88ee6037b8cc5d04a71a4462f13480", + "spender": "0x402085c248EeA27D92E8b30b2C58ed07f9E20001", // Canonical x402ExactPermit2Proxy address + "nonce": "33247007178036348590600198031289925668252061821958005840077069883511451257277", "deadline": "1740672154", "witness": { "to": "0x209693Bc6afc0C5328bA36FaF03C514EF312287C", - "validAfter": "1740672089", - "extra": {} + "validAfter": "1740672089" } } }, @@ -163,8 +163,6 @@ The verifier must execute these checks in order: 1. **Verify** `payload.signature` is valid and recovers to the `permit2Authorization.from`. - - Note that `extra` must be converted to its ABI encoded version. - 2. **Verify** that the `client` has enabled the Permit2 approval. - if ERC20.allowance(from, Permit2_Address) < amount: @@ -180,7 +178,9 @@ The verifier must execute these checks in order: 6. **Verify** the Token and Network match the requirement. -7. **Simulation:** +7. **Simulation (Recommended):** + + Simulation is recommended but implementations may defer to re-verify-before-settle. - _Standard:_ Simulate `x402ExactPermit2Proxy.settle`. - _With "Sponsored ERC20 Approval" (Extension):_ Simulate batch `transfer` -> `approve` -> `settle`. @@ -201,6 +201,114 @@ Settlement is performed by calling the `x402ExactPermit2Proxy`. --- +## 3. AssetTransferMethod: `ERC-7710` + +This asset transfer method uses [ERC-7710](https://eips.ethereum.org/EIPS/eip-7710) smart contract delegation to authorize transfers from accounts that support the standard. It is particularly suited for smart contract accounts (e.g., ERC-4337 accounts, ERC-7579 modular accounts) that have enabled delegation capabilities. + +### Prerequisites + +For ERC-7710 to work, the following must be true: + +1. **Delegator Account**: The payer's account must be a smart contract that supports ERC-7710 delegation (e.g., a modular smart account with delegation capabilities). +2. **Delegation Manager**: A `DelegationManager` contract implementing the `ERC7710Manager` interface must be deployed on the network. +3. **Active Delegation**: The payer must have created a delegation authorizing the delegate to execute token transfers on their behalf, with appropriate caveats (amount limits, recipient restrictions, etc.). + +### Phase 1: Obtaining a Delegation + +The process of obtaining a delegation is outside the scope of x402. Delegations may be obtained through: + +- [ERC-7715](https://eips.ethereum.org/EIPS/eip-7715) permission requests +- Direct wallet interactions +- Pre-configured session keys +- Other delegation protocols + +The key requirement is that the client is able to issue a delegation to the facilitator that permits the required token transfer. + +### Phase 2: `PAYMENT-SIGNATURE` Header Payload + +The `payload` field must contain: + +- `delegationManager`: The address of the ERC-7710 Delegation Manager contract. +- `permissionContext`: The delegation proof/context required by the specific Delegation Manager implementation. +- `delegator`: The address of the account that created the delegation. + +**Example PaymentPayload:** + +```json +{ + "x402Version": 2, + "resource": { + "url": "https://api.example.com/premium-data", + "description": "Access to premium market data", + "mimeType": "application/json" + }, + "accepted": { + "scheme": "exact", + "network": "eip155:84532", + "amount": "10000", + "asset": "0x036CbD53842c5426634e7929541eC2318f3dCF7e", + "payTo": "0x209693Bc6afc0C5328bA36FaF03C514EF312287C", + "maxTimeoutSeconds": 60, + "extra": { + "assetTransferMethod": "erc7710", + "name": "USDC", + "version": "2" + } + }, + "payload": { + "delegationManager": "0xDelegationManagerAddress", + "permissionContext": "0x...", + "delegator": "0x857b06519E91e3A54538791bDbb0E22373e36b66" + } +} +``` + +**Note:** The structure of `permissionContext` is determined by the specific Delegation Manager implementation. Common implementations (e.g., MetaMask Delegation Framework) use EIP-712 signed delegation chains. + +### Phase 3: Verification Logic + +Unlike EIP-3009 and Permit2, ERC-7710 verification is performed entirely through simulation. The `permissionContext` is opaque to the facilitator but verifiable by simulating the intended action. + +The facilitator: + +1. **Constructs** the `executionCallData` encoding an ERC-20 `transfer(payTo, amount)` call for the required payment. + +2. **Constructs** the `mode` appropriate for the execution (typically `0x00...` for single call mode per ERC-7579). + +3. **Simulates** `delegationManager.redeemDelegations([permissionContext], [mode], [executionCallData])` to verify: + - The delegation is valid and authorizes the intended transfer. + - The delegator has sufficient balance of the asset. + - The transaction will succeed when executed. + +If the simulation succeeds, the payment is considered valid. The simulation serves as the sole verification mechanism—no trusted list of Delegation Manager implementations is required. + +**Security Considerations**: + +1. **Race Condition Risk**: A facilitator may be vulnerable to a race condition where the client invalidates their delegation between simulation and transaction execution, causing the facilitator to pay gas for a failed transaction. This risk can be mitigated by: + - Submitting transactions via a private mempool to reduce the window for front-running. + - Building trust signals for client accounts (e.g., reputation systems) that can be used to flag or ban abusive behavior. + +2. **Malicious Delegation Manager Gas Consumption**: A malicious or poorly implemented Delegation Manager could attempt to consume excessive gas during execution. To mitigate this risk: + - Facilitators should always set an explicit gas limit on their `redeemDelegations` call, as is standard practice for all Ethereum transactions. + - Pre-execution simulation helps identify whether a transaction is likely to use a reasonable amount of gas. + - If simulation reveals unexpectedly high gas consumption, this may indicate a "trap door" implementation designed to drain facilitator funds, and the transaction should be rejected. + +### Phase 4: Settlement Logic + +Settlement is performed by calling `redeemDelegations` on the Delegation Manager: + +```solidity +delegationManager.redeemDelegations( + [permissionContext], // bytes[] - delegation proof + [mode], // bytes32[] - execution mode + [executionCallData] // bytes[] - encoded transfer call +); +``` + +The Delegation Manager validates the delegation authority and calls the delegator account to execute the token transfer. The delegator account then performs `token.transfer(payTo, amount)`. + +--- + ## Implementer Notes - **Permit2 Dependency:** Both the Permit2 contract and the x402ExactPermit2Proxy are audited, battle-tested contracts. However, integrators inherit their security properties and any future vulnerabilities discovered in either dependency. @@ -209,6 +317,14 @@ Settlement is performed by calling the `x402ExactPermit2Proxy`. ## Annex +### ERC-7710 Delegation Managers + +ERC-7710 does not define a canonical Delegation Manager. Implementations may vary in their delegation structure, caveat enforcement, and permission context format. Notable implementations include: + +- **MetaMask Delegation Framework**: A full-featured implementation supporting EIP-712 signed delegation chains, caveat enforcement, and batch processing. See [gator.metamask.io](https://gator.metamask.io/) for documentation. + +Since verification is performed entirely through simulation, facilitators do not need to maintain a trusted list of Delegation Manager implementations. + ### Canonical Permit2 The Canonical Permit2 contract address can be found at [https://docs.uniswap.org/contracts/v4/deployments](https://docs.uniswap.org/contracts/v4/deployments). @@ -219,7 +335,7 @@ This contract acts as the authorized Spender. It validates the Witness data to e > **Requirement**: This contract will be deployed to the same address across all supported EVM chains using `CREATE2` to ensure consistent behavior and simpler integration. -**Canonical Address:** `0x4020CD856C882D5fb903D99CE35316A085Bb0001` +**Canonical Address:** `0x402085c248EeA27D92E8b30b2C58ed07f9E20001` ```solidity // SPDX-License-Identifier: MIT @@ -237,20 +353,18 @@ contract x402ExactPermit2Proxy { event x402PermitTransfer(address from, address to, uint256 amount, address asset); - // EIP-712 Type Definition + // EIP-712 Type Definition (post-audit: extra removed from Witness) string public constant WITNESS_TYPE_STRING = - "Witness witness)Witness(bytes extra,address to,uint256 validAfter)TokenPermissions(address token,uint256 amount)"; + "Witness witness)TokenPermissions(address token,uint256 amount)Witness(address to,uint256 validAfter)"; bytes32 public constant WITNESS_TYPEHASH = - keccak256("Witness(bytes extra,address to,uint256 validAfter)"); + keccak256("Witness(address to,uint256 validAfter)"); struct Witness { address to; uint256 validAfter; - bytes extra; } - // New Struct to group EIP-2612 parameters and reduce stack depth struct EIP2612Permit { uint256 value; uint256 deadline; @@ -268,21 +382,18 @@ contract x402ExactPermit2Proxy { */ function settle( ISignatureTransfer.PermitTransferFrom calldata permit, - uint256 amount, address owner, Witness calldata witness, bytes calldata signature ) external { - _settleInternal(permit, amount, owner, witness, signature); + _settleInternal(permit, owner, witness, signature); } /** * @notice Extension: Settles a transfer using an EIP-2612 Permit for the allowance - * @dev Deconstructs the 2612 signature bytes to call the token contract */ - function settleWith2612( - EIP2612Permit calldata permit2612, // Deduplicated/Grouped params - uint256 amount, + function settleWithPermit( + EIP2612Permit calldata permit2612, ISignatureTransfer.PermitTransferFrom calldata permit, address owner, Witness calldata witness, @@ -298,29 +409,25 @@ contract x402ExactPermit2Proxy { ); // 2. Execute Permit2 Settlement - _settleInternal(permit, amount, owner, witness, signature); + _settleInternal(permit, owner, witness, signature); } function _settleInternal( ISignatureTransfer.PermitTransferFrom calldata permit, - uint256 amount, address owner, Witness calldata witness, bytes calldata signature ) internal { require(block.timestamp >= witness.validAfter, "Too early"); - require(amount <= permit.permitted.amount, "Amount higher than permitted"); ISignatureTransfer.SignatureTransferDetails memory transferDetails = ISignatureTransfer.SignatureTransferDetails({ to: witness.to, - requestedAmount: amount + requestedAmount: permit.permitted.amount }); - // Reconstruct hash to enforce witness integrity bytes32 witnessHash = keccak256(abi.encode( WITNESS_TYPEHASH, - keccak256(witness.extra), witness.to, witness.validAfter )); diff --git a/specs/schemes/exact/scheme_exact_stellar.md b/specs/schemes/exact/scheme_exact_stellar.md index 6eeecbccd8..87a6980b8f 100644 --- a/specs/schemes/exact/scheme_exact_stellar.md +++ b/specs/schemes/exact/scheme_exact_stellar.md @@ -28,14 +28,16 @@ The protocol flow for `exact` on Stellar is client-driven with facilitator-spons 4. **Client** signs the authorization entries (not the full transaction) with their wallet, setting expiration to `currentLedger + ledgerTimeout`, where `ledgerTimeout = ceil(maxTimeoutSeconds / estimatedLedgerSeconds)`; implementations should use the current network estimate for `estimatedLedgerSeconds` when available (fallback to `5` seconds). 5. **Client** serializes the transaction with signed auth entries and encodes it as XDR (base64). 6. **Client** sends a new request to the resource server with the `PaymentPayload` containing the base64-encoded transaction. -7. **Resource Server** forwards the `PaymentPayload` and `PaymentRequirements` to the **Facilitator Server's** `/settle` endpoint. - - NOTE: `/verify` is optional and intended for pre-flight checks only. `/settle` MUST perform full verification independently and MUST NOT assume prior verification. +7. **Resource Server** forwards the `PaymentPayload` and `PaymentRequirements` to the **Facilitator Server's** `/verify` endpoint. 8. **Facilitator** decodes the transaction XDR and validates the transaction's: structure, auth entries, signature expiration, amount, payer, and recipient. -9. **Facilitator** rebuilds the transaction with its own account as the source, preserving all operations and auth entries. -10. **Facilitator** simulates the transaction to verify it succeeds and emits the expected transfer events. -11. **Facilitator** signs the rebuilt transaction with its own key and submits it to the Stellar network via RPC `sendTransaction`. -12. **Facilitator** polls for transaction confirmation and responds with a `SettlementResponse` to the **Resource Server**. -13. **Resource Server** grants the **Client** access to the resource in its response upon successful settlement. +9. **Facilitator** returns a `VerifyResponse` to the **Resource Server**. +10. **Resource Server**, upon successful verification, forwards the payload to the facilitator's `/settle` endpoint. + - NOTE: `/settle` MUST perform full verification independently and MUST NOT assume prior verification. +11. **Facilitator** rebuilds the transaction with its own account as the source, preserving all operations and auth entries. +12. **Facilitator** simulates the transaction to verify it succeeds and emits the expected transfer events. +13. **Facilitator** signs the rebuilt transaction with its own key and submits it to the Stellar network via RPC `sendTransaction`. +14. **Facilitator** polls for transaction confirmation and responds with a `SettlementResponse` to the **Resource Server**. +15. **Resource Server** grants the **Client** access to the resource in its response upon successful settlement. ## `PaymentRequirements` for `exact` diff --git a/specs/schemes/exact/scheme_exact_svm.md b/specs/schemes/exact/scheme_exact_svm.md index bdf1e5e235..d22402b859 100644 --- a/specs/schemes/exact/scheme_exact_svm.md +++ b/specs/schemes/exact/scheme_exact_svm.md @@ -40,13 +40,15 @@ In addition to the standard x402 `PaymentRequirements` fields, the `exact` schem "payTo": "2wKupLR9q6wXYppw8Gr2NvWxKBUqm4PPJKkQfoxHDBg4", "maxTimeoutSeconds": 60, "extra": { - "feePayer": "EwWqGE4ZFKLofuestmU4LDdK7XM1N4ALgdZccwYugwGd" + "feePayer": "EwWqGE4ZFKLofuestmU4LDdK7XM1N4ALgdZccwYugwGd", + "memo": "pi_3abc123def456" } } ``` - `asset`: The public key of the token mint. - `extra.feePayer`: The public key of the account that will pay for the transaction fees. This is typically the facilitator's public key. +- `extra.memo` (optional): A seller-defined UTF-8 string to include in the transaction's Memo instruction. When present, the client MUST use this value as the Memo instruction data instead of a random nonce. Maximum 256 bytes. This enables sellers to attach payment references (e.g., invoice IDs) to on-chain transactions for reconciliation without requiring unique deposit addresses. ## PaymentPayload `payload` Field @@ -78,7 +80,8 @@ Full `PaymentPayload` object: "payTo": "2wKupLR9q6wXYppw8Gr2NvWxKBUqm4PPJKkQfoxHDBg4", "maxTimeoutSeconds": 60, "extra": { - "feePayer": "EwWqGE4ZFKLofuestmU4LDdK7XM1N4ALgdZccwYugwGd" + "feePayer": "EwWqGE4ZFKLofuestmU4LDdK7XM1N4ALgdZccwYugwGd", + "memo": "pi_3abc123def456" } }, "payload": { @@ -106,16 +109,18 @@ A facilitator verifying an `exact`-scheme SVM payment MUST enforce all of the fo 1. Instruction layout -- The decompiled transaction MUST contain 3 to 5 instructions in this order: +- The decompiled transaction MUST contain 3 to 6 instructions in this order: 1. Compute Budget: Set Compute Unit Limit 2. Compute Budget: Set Compute Unit Price 3. SPL Token or Token-2022 TransferChecked - 4. (Optional) Lighthouse program instruction (Phantom wallet protection) - 5. (Optional) Lighthouse program instruction (Solflare wallet protection) + 4. (Optional) Lighthouse or Memo program instruction + 5. (Optional) Lighthouse or Memo program instruction + 6. (Optional) Memo program instruction -- If a 4th or 5th instruction is present, the program MUST be the Lighthouse program (`L2TExMFKdjpN9kozasaurPirfHy9P8sbXoAN1qA3S95`). -- Phantom wallet injects 1 Lighthouse instruction; Solflare injects 2. -- These Lighthouse instructions are wallet-injected user protection mechanisms and MUST be allowed to support these wallets. +- Allowed optional programs: Lighthouse (`L2TExMFKdjpN9kozasaurPirfHy9P8sbXoAN1qA3S95`) and SPL Memo (`MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr`). +- Phantom wallet injects 1 Lighthouse instruction; Solflare injects 2. These are wallet-injected user protection mechanisms and MUST be allowed. +- The Memo instruction ensures transaction uniqueness across concurrent payments with identical parameters. Clients MUST include a Memo instruction containing either the value of `extra.memo` (when present) or a random nonce (at least 16 bytes, hex-encoded for UTF-8 compliance). +- If `extra.memo` is present in `PaymentRequirements`, the facilitator MUST verify that exactly one Memo instruction exists and that its data matches the value of `extra.memo` encoded as UTF-8. 2. Fee payer (facilitator) safety @@ -143,3 +148,22 @@ A facilitator verifying an `exact`-scheme SVM payment MUST enforce all of the fo - The `amount` in TransferChecked MUST equal `PaymentRequirements.amount` exactly. These checks are security-critical to ensure the fee payer cannot be tricked into transferring their own funds or sponsoring unintended actions. Implementations MAY introduce stricter limits (e.g., lower compute price caps) but MUST NOT relax the above constraints. + +## Duplicate Settlement Mitigation (RECOMMENDED) + +### Vulnerability + +A race condition exists in the settlement flow: if the same payment transaction is submitted to the facilitator's `/settle` endpoint multiple times before the first submission is confirmed on-chain, each call may return a successful response. + +Although Solana's transaction deduplication ensures the transfer only executes once on-chain, the RPC returns "success", and hence the facilitator could return `success` to each caller. A malicious client can exploit this to obtain access to multiple resources while only paying once. + +### Recommended Mitigation + +Merchants and/or Facilitators SHOULD maintain a short-term, in-memory cache of transaction payloads that are currently being settled. Before proceeding with settlement, the merchant/facilitator checks whether the transaction has already been seen: + +1. After verification succeeds, derive a cache key from the transaction payload (e.g., the base64-encoded transaction string). +2. If the key is already present in the cache, reject the settlement with a `"duplicate_settlement"` error. +3. If the key is not present, insert it into the cache and proceed with signing and submission. +4. Evict entries older than 120 seconds (approximately twice the Solana blockhash lifetime of ~60–90 seconds). After this window, the transaction's blockhash will have expired and it cannot land on-chain regardless. + +This approach requires no external storage or long-lived state — only an in-process map with time-based eviction. It preserves the facilitator's otherwise stateless design while closing the duplicate settlement attack vector. \ No newline at end of file diff --git a/specs/schemes/upto/scheme_upto.md b/specs/schemes/upto/scheme_upto.md new file mode 100644 index 0000000000..f4890982b7 --- /dev/null +++ b/specs/schemes/upto/scheme_upto.md @@ -0,0 +1,74 @@ +# Scheme: `upto` + +## Summary + +`upto` is a scheme that authorizes a transfer of up to a **maximum amount** of funds from a client to a resource server. The actual amount charged is determined at settlement time based on resource consumption during the request. + +This scheme is ideal for usage-based pricing models where the final cost is not known until after the resource has been consumed. + +## Example Use Cases + +- Paying for LLM token generation (charge per token generated) +- Bandwidth or data transfer metering (charge per byte transferred in a single request) +- Dynamic compute pricing (charge based on actual resources consumed) + +## Core Properties (MUST) + +The `upto` scheme MUST enforce the following properties across ALL network implementations: + +### 1. Single-Use Authorization + +Each authorization MUST be settled at most once. After settlement (regardless of amount), the authorization is consumed and cannot be reused. + +- Rationale: Provides a clear audit trail, simpler mental model, and matches x402's request-response pattern. +- Implementation: On EVM, Permit2's nonce mechanism enforces this. Other networks MUST implement equivalent replay protection. + +### 2. Time-Bound Authorization + +Each authorization MUST have explicit validity time constraints: + +- **Start time** (`validAfter`): Authorization is not valid before this timestamp +- **End time** (`deadline`): Authorization expires after this timestamp + +- Rationale: Limits exposure window for unused authorizations and ensures timely settlement. +- Implementation: On EVM, Permit2's `deadline` and witness `validAfter` enforce this. Other networks MUST implement equivalent time bounds. + +### 3. Recipient Binding + +The authorization MUST cryptographically bind the recipient address. The server/facilitator cannot redirect funds to a different address than what the client signed. + +- Rationale: Prevents malicious facilitators from stealing funds. +- Implementation: On EVM, the Permit2 witness pattern binds `witness.to`. Other networks MUST implement equivalent recipient binding. + +### 4. Maximum Amount Enforcement + +The settled amount MUST be less than or equal to the authorized maximum. + +- The settled `amount` MUST be `<=` the authorized maximum +- The settled `amount` MAY be `0` (no charge if no usage occurred) + +### 5. Phase-Dependent `amount` Semantics in `PaymentRequirements` + +In the x402 protocol, the verify and settle requests share the same `PaymentPayload` and `PaymentRequirements` types. In the `upto` scheme, the `amount` field of `PaymentRequirements` is **phase-dependent** for server-to-facilitator communication: + +- At **verification** time, `amount` represents the **maximum** amount the client authorizes. +- At **settlement** time, `amount` represents the **actual amount to settle**, which MUST be less than or equal to the previously authorized maximum. + +The actual settled amount is communicated by the resource server to the facilitator via the `amount` field in the settlement-time `PaymentRequirements`. This allows the resource server to determine the final charge based on actual resource consumption (e.g., tokens generated, bytes transferred) and communicate it to the facilitator without requiring additional fields or a separate settlement type. + +- Rationale: Reusing the existing `PaymentRequirements` type for both phases keeps the protocol simple and avoids introducing settlement-specific message types. The `amount` field naturally maps to "how much" in both contexts — "how much is authorized" at verification time and "how much to charge" at settlement time. +- Implementation: The resource server MUST set the `amount` field in the `PaymentRequirements` passed to the facilitator's settle endpoint to the desired settlement amount. The facilitator MUST verify that this amount does not exceed the authorized maximum from the client's signed authorization. + +## Out of Scope + +The following patterns are NOT supported by `upto` and would require different schemes: + +- **Multi-settlement / streaming**: Settling the same authorization multiple times (e.g., pay-per-chunk streaming) +- **Recurring payments**: Automatic periodic charges without new authorizations +- **Open-ended allowances**: Authorizations without time bounds or single-use constraints + +## Network-Specific Implementation + +Network-specific rules and implementation details are defined in the per-network scheme documents: + +- EVM chains: See [`scheme_upto_evm.md`](./scheme_upto_evm.md) diff --git a/specs/schemes/upto/scheme_upto_evm.md b/specs/schemes/upto/scheme_upto_evm.md new file mode 100644 index 0000000000..470c9be1d8 --- /dev/null +++ b/specs/schemes/upto/scheme_upto_evm.md @@ -0,0 +1,282 @@ +# Scheme: `upto` on `EVM` + +## Summary + +The `upto` scheme on EVM enables usage-based payments where the Client (user) authorizes a **maximum amount**, and the Facilitator (server) settles for the **actual amount used** at the end of the request. This is ideal for variable-cost resources like LLM token generation, bandwidth metering, or time-based access. + +This scheme uses the **Permit2** asset transfer method exclusively, leveraging the `permitWitnessTransferFrom` function to allow settling for any amount up to the signed maximum. + +| AssetTransferMethod | Use Case | Notes | +| :------------------ | :-------------------------------------------------------------- | :---------------------------------------------- | +| **Permit2** | All ERC-20 tokens. Client signs max, server settles actual. | Uses existing `x402Permit2Proxy` contract. | + +> **Note**: EIP-3009 (`transferWithAuthorization`) is **not supported** for the `upto` scheme because it requires exact amounts at signature time. + +--- + +## Use Cases + +- **LLM Token Generation**: Client authorizes up to $5, actual charge based on tokens generated +- **Bandwidth/Data Transfer**: Pay per byte transferred in a single request, up to a cap +- **Dynamic Compute**: Authorize max cost, charge based on actual compute resources consumed + +--- + +## 1. AssetTransferMethod: `Permit2` + +This scheme uses the `permitWitnessTransferFrom` from the [canonical **Permit2** contract](#canonical-permit2) combined with the [`x402Permit2Proxy`](#reference-implementation-x402permit2proxy) to enforce receiver address security via the "Witness" pattern. + +The `permit.permitted.amount` represents the **maximum** authorized amount, while the actual settlement amount is determined by the server at settlement time. + +### Phase 1: One-Time Gas Approval + +Permit2 requires the user to approve the [**Permit2 Contract** (Canonical Address)](#canonical-permit2) to spend their tokens. This is a one-time setup. The specification supports three approval methods: + +#### Option A: Direct User Approval (Standard) + +The user submits a standard on-chain `approve(Permit2)` transaction paying their own gas. + +- _Prerequisite:_ User must have Native Gas currency. + +#### Option B: Sponsored ERC20 Approval (Extension: [`erc20ApprovalGasSponsoring`](../../extensions/erc20_gas_sponsoring.md)) + +The Facilitator pays the gas for the approval transaction on the user's behalf. + +- _Prerequisite:_ Server supports this extension. +- _Flow:_ Facilitator batches the following transactions: `from.transfer(gas_amount)` -> `ERC20.approve(Permit2)` -> `settle`. + +#### Option C: EIP2612 Permit (Extension: [`eip2612GasSponsoring`](../../extensions/eip2612_gas_sponsoring.md)) + +If the token supports EIP-2612, the user signs a permit authorizing Permit2. + +- _Prerequisite:_ Token supports EIP-2612. +- _Flow:_ Facilitator calls `x402Permit2Proxy.settleWithPermit()` + +### Phase 2: `PAYMENT-SIGNATURE` Header Payload + +The `payload` field must contain: + +- `signature`: The signature for `permitWitnessTransferFrom`. +- `permit2Authorization`: Parameters to reconstruct the message. + +**Important Logic:** The `permit2Authorization.permitted.amount` represents the **maximum** amount the client is willing to pay. The actual amount charged will be determined at settlement and will be less than or equal to this maximum. + +> **Requirement**: The `x402Permit2Proxy` contract will be deployed to the same address across all supported EVM chains using `CREATE2` to ensure consistent behavior and simpler integration. + +**Facilitator Address Discovery:** The facilitator announces its address via the `/supported` endpoint in the `extra` field of each supported scheme. The client MUST include this `facilitatorAddress` in the `permit2Authorization.witness.facilitator` field when constructing the payment signature. This binds the authorization to a specific facilitator, preventing unauthorized settlement by other parties. + +**Example PaymentRequired (402 Response):** + +```json +{ + "x402Version": 2, + "error": "PAYMENT-SIGNATURE header is required", + "resource": { + "url": "https://api.example.com/llm/generate", + "description": "LLM text generation endpoint", + "mimeType": "application/json" + }, + "accepts": [ + { + "scheme": "upto", + "network": "eip155:84532", + "amount": "5000000", + "asset": "0x036CbD53842c5426634e7929541eC2318f3dCF7e", + "payTo": "0x209693Bc6afc0C5328bA36FaF03C514EF312287C", + "maxTimeoutSeconds": 300, + "extra": { + "name": "USDC", + "version": "2", + "facilitatorAddress": "0xFacilitatorAddress1234567890123456789012" + } + } + ] +} +``` + +**Example PaymentPayload (Client Request):** + +```json +{ + "x402Version": 2, + "resource": { + "url": "https://api.example.com/llm/generate", + "description": "LLM text generation endpoint", + "mimeType": "application/json" + }, + "accepted": { + "scheme": "upto", + "network": "eip155:84532", + "amount": "5000000", + "asset": "0x036CbD53842c5426634e7929541eC2318f3dCF7e", + "payTo": "0x209693Bc6afc0C5328bA36FaF03C514EF312287C", + "maxTimeoutSeconds": 300, + "extra": { + "name": "USDC", + "version": "2" + } + }, + "payload": { + "signature": "0x2d6a7588d6acca505cbf0d9a4a227e0c52c6c34008c8e8986a1283259764173608a2ce6496642e377d6da8dbbf5836e9bd15092f9ecab05ded3d6293af148b571c", + "permit2Authorization": { + "permitted": { + "token": "0x036CbD53842c5426634e7929541eC2318f3dCF7e", + "amount": "5000000" + }, + "from": "0x857b06519E91e3A54538791bDbb0E22373e36b66", + "spender": "0x4020A4f3b7b90ccA423B9fabCc0CE57C6C240002", + "nonce": "0xf3746613c2d920b5fdabc0856f2aeb2d4f88ee6037b8cc5d04a71a4462f13480", + "deadline": "1740672154", + "witness": { + "to": "0x209693Bc6afc0C5328bA36FaF03C514EF312287C", + "facilitator": "0xFacilitatorAddress1234567890123456789012", + "validAfter": "1740672089" + } + } + } +} +``` + +### Phase 3: Verification Logic + +The verifier must execute these checks in order: + +1. **Verify** `payload.signature` is valid and recovers to the `permit2Authorization.from`. + + - Note that `extra` must be converted to its ABI encoded version. + +2. **Verify** that the `client` has enabled the Permit2 approval. + + - if ERC20.allowance(from, Permit2_Address) < amount: + - Check for **Sponsored ERC20 Approval** (Extension): Refers to [`erc20ApprovalGasSponsoring`](../../extensions/erc20_gas_sponsoring.md). + - Check for **EIP2612 Permit** (Extension): Refers to [`eip2612GasSponsoring`](../../extensions/eip2612_gas_sponsoring.md). + - **If neither exists:** Return `412 Precondition Failed` (Error Code: `PERMIT2_ALLOWANCE_REQUIRED`). This signals the client that a one-time Direct Approval transaction is required before retrying. + +3. **Verify** the `client` has sufficient balance of the `asset` to cover `amount`. + +4. **Verify** the `permit2Authorization.permitted.amount` equals the `amount` from requirements. + +5. **Verify** the `deadline` (not expired) and `witness.validAfter` (active). + +6. **Verify** the Token and Network match the requirement. + +7. **Simulation:** + + - _Standard:_ Simulate `x402Permit2Proxy.settle` with the full `amount` (worst case). + - _With "Sponsored ERC20 Approval" (Extension):_ Simulate batch `transfer` -> `approve` -> `settle`. + - _With "EIP2612 Permit" (Extension):_ Simulate `x402Permit2Proxy.settleWithPermit`. + +### Phase 4: Settlement Logic + +Settlement is performed by calling the `x402Permit2Proxy` with the **actual amount** to charge. + +The server determines the actual amount based on resource consumption during the request (tokens generated, bytes transferred, time elapsed, etc.). + +**Settlement Amount Rules:** + +- The settled `amount` MUST be `<=` the authorized maximum +- The settled `amount` MAY be `0` (no charge if no usage occurred) +- The settled `amount` is determined by the resource server, not the client + +**Settlement Process:** + +1. **Standard Settlement:** + Call `x402Permit2Proxy.settle(permit, actualAmount, owner, witness, signature)` where `actualAmount <= permit.permitted.amount`. + +2. **With Sponsored ERC20 Approval (Extension):** + If `erc20ApprovalGasSponsoring` is used, the facilitator must construct a batched transaction that executes the sponsored `ERC20.approve` call strictly before the `x402Permit2Proxy.settle` call. + +3. **With EIP-2612 Permit (Extension):** + If `eip2612GasSponsoring` is used, call `x402Permit2Proxy.settleWithPermit`. + +4. **Zero Settlement:** + If the settled `amount = 0`, no on-chain transaction is required. The authorization simply expires unused. + +**Example SettlementResponse:** + +```json +{ + "success": true, + "transaction": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", + "network": "eip155:84532", + "payer": "0x857b06519E91e3A54538791bDbb0E22373e36b66", + "amount": "2350000" +} +``` + +--- + +## 2. PaymentRequirements Schema + +The `upto` scheme uses the following `PaymentRequirements` schema: + +| Field Name | Type | Required | Description | +| ------------------- | -------- | -------- | ----------------------------------------------------------------------------- | +| `scheme` | `string` | Required | Must be `"upto"` | +| `network` | `string` | Required | Blockchain network identifier in CAIP-2 format (e.g., "eip155:84532") | +| `amount` | `string` | Required | Phase-dependent: maximum amount at verification, actual amount at settlement | +| `asset` | `string` | Required | Token contract address | +| `payTo` | `string` | Required | Recipient wallet address | +| `maxTimeoutSeconds` | `number` | Required | Maximum time allowed for payment completion | +| `extra` | `object` | Optional | Scheme-specific additional information (must include `name`, `version`, and `facilitatorAddress`) | + +> **Note**: In the `upto` scheme, the `amount` field of `PaymentRequirements` is phase-dependent for server-to-facilitator communication: +> +> - At _verification_ time, `amount` represents the **maximum** amount the client authorizes. +> - At _settlement_ time, `amount` represents the **actual amount to settle**, which MUST be less than or equal to the previously authorized maximum. +> +> The actual settled amount is communicated by the resource server to the facilitator via the `amount` field in the settlement-time payment requirements. This allows the server to determine the final charge based on actual resource consumption without requiring additional fields or a separate settlement type. + +--- + +## 3. SettlementResponse Schema Extension + +The `upto` scheme extends the base [`SettlementResponse`](../../x402-specification-v2.md#53-settlementresponse-schema) with the actual settled amount: + +| Field Name | Type | Required | Description | +| --------------- | --------- | -------- | --------------------------------------------------------------------- | +| `success` | `boolean` | Required | Indicates whether the payment settlement was successful | +| `errorReason` | `string` | Optional | Error reason if settlement failed (omitted if successful) | +| `payer` | `string` | Optional | Address of the payer's wallet | +| `transaction` | `string` | Required | Blockchain transaction hash (empty string if $0 settlement) | +| `network` | `string` | Required | Blockchain network identifier in CAIP-2 format | +| `amount` | `string` | Required | Actual amount charged in atomic token units (may be 0) | + +--- + +## 4. Error Codes + +The `upto` scheme uses the standard x402 error codes defined in the [x402 specification](../../x402-specification-v2.md#9-error-handling). + +### Scheme-Specific Error Code + +The `upto` scheme defines one additional error code: + +- **`invalid_upto_evm_payload_settlement_exceeds_amount`**: Attempted to settle for more than the authorized `amount` + +--- + +## Annex + +### Canonical Permit2 + +The Canonical Permit2 contract address can be found at [https://docs.uniswap.org/contracts/v4/deployments](https://docs.uniswap.org/contracts/v4/deployments). + +### Reference Implementation: `x402Permit2Proxy` + +The `upto` scheme uses its own `x402UptoPermit2Proxy` contract (deployed at `0x4020A4f3b7b90ccA423B9fabCc0CE57C6C240002`), which is structurally similar to the `x402ExactPermit2Proxy` used by the [exact scheme](../exact/scheme_exact_evm.md#reference-implementation-x402permit2proxy) but includes a `facilitator` field in the witness struct for access control. The contract's `settle` function accepts an `amount` parameter that can be less than or equal to `permit.permitted.amount`, which enables the variable settlement amounts required by the `upto` scheme. + +--- + +## Security Considerations + +1. **Maximum Amount Authorization**: Clients should carefully consider the `amount` they authorize. While servers can only charge up to this amount, clients bear the risk of the full amount being charged. + +2. **Server Trust**: The `upto` scheme requires clients to trust that servers will charge fair amounts based on actual usage. Malicious servers could charge up to `amount` regardless of actual usage. + +3. **Signature Reuse Prevention**: The Permit2 nonce mechanism prevents signature reuse. Each authorization can only be settled once. + +4. **Time Constraints**: Authorizations have explicit valid time windows (`deadline`, `validAfter`) to limit their lifetime and reduce exposure. + +5. **Zero Settlement**: Allowing $0 settlements means unused authorizations naturally expire without on-chain transactions, reducing gas costs and blockchain bloat. + diff --git a/specs/transports-v2/http.md b/specs/transports-v2/http.md index fb61bb87a4..a735f80032 100644 --- a/specs/transports-v2/http.md +++ b/specs/transports-v2/http.md @@ -18,9 +18,7 @@ HTTP/1.1 402 Payment Required Content-Type: application/json PAYMENT-REQUIRED: eyJ4NDAyVmVyc2lvbiI6MiwiZXJyb3IiOiJQQVlNRU5ULVNJR05BVFVSRSBoZWFkZXIgaXMgcmVxdWlyZWQiLCJyZXNvdXJjZSI6eyJ1cmwiOiJodHRwczovL2FwaS5leGFtcGxlLmNvbS9wcmVtaXVtLWRhdGEiLCJkZXNjcmlwdGlvbiI6IkFjY2VzcyB0byBwcmVtaXVtIG1hcmtldCBkYXRhIiwibWltZVR5cGUiOiJhcHBsaWNhdGlvbi9qc29uIn0sImFjY2VwdHMiOlt7InNjaGVtZSI6ImV4YWN0IiwibmV0d29yayI6ImVpcDE1NTo4NDUzMiIsImFtb3VudCI6IjEwMDAwIiwiYXNzZXQiOiIweDAzNkNiRDUzODQyYzU0MjY2MzRlNzkyOTU0MWVDMjMxOGYzZENGN2UiLCJwYXlUbyI6IjB4MjA5NjkzQmM2YWZjMEM1MzI4YkEzNkZhRjAzQzUxNEVGMzEyMjg3QyIsIm1heFRpbWVvdXRTZWNvbmRzIjo2MCwiZXh0cmEiOnsibmFtZSI6IlVTREMiLCJ2ZXJzaW9uIjoiMiJ9fV19 -{ - "error": "Payment required" -} +{} ``` The base64 header decodes to: @@ -145,10 +143,18 @@ HTTP/1.1 402 Payment Required Content-Type: application/json PAYMENT-RESPONSE: eyJzdWNjZXNzIjpmYWxzZSwiZXJyb3JSZWFzb24iOiJpbnN1ZmZpY2llbnRfZnVuZHMiLCJ0cmFuc2FjdGlvbiI6IiIsIm5ldHdvcmsiOiJlaXAxNTU6ODQ1MzIiLCJwYXllciI6IjB4ODU3YjA2NTE5RTkxZTNBNTQ1Mzg3OTFiRGJiMEUyMjM3M2UzNmI2NiJ9 +{} +``` + +The base64 response header decodes to: + +```json { - "x402Version": 2, - "error": "Payment failed: insufficient funds", - "accepts": [...] + "success": false, + "errorReason": "insufficient_funds", + "transaction": "", + "network": "eip155:84532", + "payer": "0x857b06519E91e3A54538791bDbb0E22373e36b66" } ``` @@ -160,6 +166,10 @@ PAYMENT-RESPONSE: eyJzdWNjZXNzIjpmYWxzZSwiZXJyb3JSZWFzb24iOiJpbnN1ZmZpY2llbnRfZn | `PAYMENT-SIGNATURE` | Client → Server | Base64-encoded `PaymentPayload` object | | `PAYMENT-RESPONSE` | Server → Client | Base64-encoded `SettlementResponse` object | +## Response Body + +Response bodies are a server implementation concern. All x402 protocol information is communicated through headers (`PAYMENT-REQUIRED`, `PAYMENT-SIGNATURE`, `PAYMENT-RESPONSE`). + ## Error Handling HTTP transport maps x402 errors to standard HTTP status codes: diff --git a/specs/x402-specification-v1.md b/specs/x402-specification-v1.md index 48a71e2298..32191136ab 100644 --- a/specs/x402-specification-v1.md +++ b/specs/x402-specification-v1.md @@ -280,6 +280,7 @@ Verifies a payment authorization without executing the transaction on the blockc ```json { + "x402Version": 1, "paymentPayload": { /* PaymentPayload schema */ }, @@ -293,6 +294,7 @@ Example with actual data: ```json { + "x402Version": 1, "paymentPayload": { "x402Version": 1, "scheme": "exact", diff --git a/specs/x402-specification-v2.md b/specs/x402-specification-v2.md index 00cdd18cac..bde67e2584 100644 --- a/specs/x402-specification-v2.md +++ b/specs/x402-specification-v2.md @@ -238,7 +238,8 @@ The `SettleResponse` schema contains the following fields: | `payer` | `string` | Optional | Address of the payer's wallet | | `transaction` | `string` | Required | Blockchain transaction hash (empty string if settlement failed) | | `network` | `string` | Required | Blockchain network identifier in CAIP-2 format | -| `extensions` | `object` | Optional | Protocol extensions data | +| `amount` | `string` | Optional | The actual amount settled in atomic units (omitted if not applicable) | +| `extensions` | `object` | Optional | Protocol extensions data | **5.4 VerifyResponse Schema** @@ -290,7 +291,7 @@ The facilitator performs the following verification steps: 1. **Signature Validation**: Verify the EIP-712 signature is valid and properly signed by the payer 2. **Balance Verification**: Confirm the payer has sufficient token balance for the transfer -3. **Amount Validation**: Ensure the payment amount meets or exceeds the required amount +3. **Amount Validation**: Ensure the payment amount exactly matches the required amount 4. **Time Window Check**: Verify the authorization is within its valid time range 5. **Parameter Matching**: Confirm authorization parameters match the original payment requirements 6. **Transaction Simulation**: Simulate the `transferWithAuthorization` transaction to ensure it would succeed @@ -323,6 +324,7 @@ Verifies a payment authorization without executing the transaction on the blockc ```json { + "x402Version": 2, "paymentPayload": { /* PaymentPayload schema */ }, @@ -336,6 +338,7 @@ Example with actual data: ```json { + "x402Version": 2, "paymentPayload": { "x402Version": 2, "resource": { @@ -405,7 +408,9 @@ Example with actual data: Executes a verified payment by broadcasting the transaction to the blockchain. -**Request:** Same as `/verify` endpoint +**Request:** Same structure as `/verify` endpoint (contains `paymentPayload` and `paymentRequirements`). + +> **Note**: While the request structure is identical, some payment schemes may assign different semantics to fields at settlement time versus verification time. For example, in the `upto` scheme, the `amount` field in `paymentRequirements` represents the maximum authorized amount at verification time, but the actual amount to settle at settlement time. See individual scheme specifications for details. **Successful Response:** @@ -579,7 +584,7 @@ The x402 protocol defines standard error codes that may be returned by facilitat - **`insufficient_funds`**: Client does not have enough tokens to complete the payment - **`invalid_exact_evm_payload_authorization_valid_after`**: Payment authorization is not yet valid (before validAfter timestamp) - **`invalid_exact_evm_payload_authorization_valid_before`**: Payment authorization has expired (after validBefore timestamp) -- **`invalid_exact_evm_payload_authorization_value`**: Payment amount is insufficient for the required payment +- **`invalid_exact_evm_payload_authorization_value_mismatch`**: Payment amount does not exactly match the required amount - **`invalid_exact_evm_payload_signature`**: Payment authorization signature is invalid or improperly signed - **`invalid_exact_evm_payload_recipient_mismatch`**: Recipient address does not match payment requirements - **`invalid_network`**: Specified blockchain network is not supported diff --git a/typescript/.changeset/add-arbitrum-one-and-arbitrum-sepolia-default-stablecoin.md b/typescript/.changeset/add-arbitrum-one-and-arbitrum-sepolia-default-stablecoin.md new file mode 100644 index 0000000000..f668d66039 --- /dev/null +++ b/typescript/.changeset/add-arbitrum-one-and-arbitrum-sepolia-default-stablecoin.md @@ -0,0 +1,5 @@ +--- +"@x402/evm": patch +--- + +Add Arbitrum One (chain ID 42161) and Arbitrum Sepolid (chain ID 421614) support with USDC as the default stablecoin \ No newline at end of file diff --git a/typescript/.changeset/add-fastify-adapter.md b/typescript/.changeset/add-fastify-adapter.md new file mode 100644 index 0000000000..3742b2ec99 --- /dev/null +++ b/typescript/.changeset/add-fastify-adapter.md @@ -0,0 +1,5 @@ +--- +"@x402/fastify": minor +--- + +Added Fastify framework adapter for x402 payment middleware diff --git a/typescript/.changeset/add-mezo-testnet-default-asset.md b/typescript/.changeset/add-mezo-testnet-default-asset.md new file mode 100644 index 0000000000..68f95c4bdf --- /dev/null +++ b/typescript/.changeset/add-mezo-testnet-default-asset.md @@ -0,0 +1,5 @@ +--- +"@x402/evm": patch +--- + +Add Mezo Testnet (chain ID 31611) support with mUSD as the default stablecoin diff --git a/typescript/.changeset/add-polygon-support.md b/typescript/.changeset/add-polygon-support.md new file mode 100644 index 0000000000..c25ea927a4 --- /dev/null +++ b/typescript/.changeset/add-polygon-support.md @@ -0,0 +1,5 @@ +--- +"@x402/evm": minor +--- + +Add Polygon mainnet (chain ID 137) support with USDC as the default stablecoin diff --git a/typescript/.changeset/add-stable-support.md b/typescript/.changeset/add-stable-support.md new file mode 100644 index 0000000000..cafc97c9d8 --- /dev/null +++ b/typescript/.changeset/add-stable-support.md @@ -0,0 +1,5 @@ +--- +"@x402/evm": minor +--- + +Add Stable mainnet (chain ID 988) support with USDT0 as the default stablecoin diff --git a/typescript/.changeset/add-stable-testnet-support.md b/typescript/.changeset/add-stable-testnet-support.md new file mode 100644 index 0000000000..abad1a4804 --- /dev/null +++ b/typescript/.changeset/add-stable-testnet-support.md @@ -0,0 +1,5 @@ +--- +"@x402/evm": minor +--- + +Add Stable testnet (chain ID 2201) support with USDT0 as the default stablecoin diff --git a/typescript/.changeset/config.json b/typescript/.changeset/config.json index 893f241ab4..83c6b62309 100644 --- a/typescript/.changeset/config.json +++ b/typescript/.changeset/config.json @@ -3,9 +3,30 @@ "changelog": "@changesets/cli/changelog", "commit": false, "fixed": [], - "linked": [], + "linked": [ + [ + "@x402/core", + "@x402/extensions", + "@x402/evm", + "@x402/aptos", + "@x402/stellar", + "@x402/svm", + "@x402/express", + "@x402/hono", + "@x402/next", + "@x402/paywall", + "@x402/fetch", + "@x402/axios", + "@x402/mcp" + ] + ], "access": "public", "baseBranch": "main", "updateInternalDependencies": "patch", - "ignore": ["site"] -} + "ignore": [ + "site" + ], + "___experimentalUnsafeOptions_WILL_CHANGE_IN_PATCH": { + "onlyUpdatePeerDependentsWhenOutOfRange": true + } +} \ No newline at end of file diff --git a/typescript/.changeset/export-hook-types.md b/typescript/.changeset/export-hook-types.md new file mode 100644 index 0000000000..ddaaeacf3c --- /dev/null +++ b/typescript/.changeset/export-hook-types.md @@ -0,0 +1,5 @@ +--- +'@x402/core': patch +--- + +Export all hook types and hook context interfaces from the server entry point diff --git a/typescript/.changeset/fix-facilitator-redirect.md b/typescript/.changeset/fix-facilitator-redirect.md new file mode 100644 index 0000000000..39b9902c64 --- /dev/null +++ b/typescript/.changeset/fix-facilitator-redirect.md @@ -0,0 +1,5 @@ +--- +'@x402/core': patch +--- + +Fixed HTTPFacilitatorClient not following 308 redirects from facilitator endpoints. Normalized base URL to strip trailing slashes and explicitly set `redirect: "follow"` on all fetch calls for cross-runtime compatibility. diff --git a/typescript/.changeset/fix-settlement-failure-header.md b/typescript/.changeset/fix-settlement-failure-header.md deleted file mode 100644 index e3e5767651..0000000000 --- a/typescript/.changeset/fix-settlement-failure-header.md +++ /dev/null @@ -1,8 +0,0 @@ ---- -'@x402/core': patch -'@x402/express': patch -'@x402/hono': patch -'@x402/next': patch ---- - -Include PAYMENT-RESPONSE header on settlement failure responses diff --git a/typescript/.changeset/settlement-overrides.md b/typescript/.changeset/settlement-overrides.md new file mode 100644 index 0000000000..2d90176e25 --- /dev/null +++ b/typescript/.changeset/settlement-overrides.md @@ -0,0 +1,7 @@ +--- +'@x402/core': minor +'@x402/express': minor +'@x402/hono': minor +--- + +Add SettlementOverrides support for partial settlement (upto scheme). Route handlers can call setSettlementOverrides() to settle less than the authorized maximum, enabling usage-based billing. diff --git a/typescript/.changeset/tall-windows-rescue.md b/typescript/.changeset/tall-windows-rescue.md new file mode 100644 index 0000000000..4fc919c649 --- /dev/null +++ b/typescript/.changeset/tall-windows-rescue.md @@ -0,0 +1,5 @@ +--- +'@x402/fastify': patch +--- + +Applied monkey-patch on reply.raw write operations and buffered response to prevent content leak from direct raw writes bypassing Fastify's onSend lifecycle diff --git a/typescript/.changeset/types-optionality-fix.md b/typescript/.changeset/types-optionality-fix.md deleted file mode 100644 index acc61f8ed0..0000000000 --- a/typescript/.changeset/types-optionality-fix.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -"@x402/core": patch -"@x402/paywall": patch ---- - -Make ResourceInfo.description, ResourceInfo.mimeType, and PaymentPayload.resource optional to match v2 spec diff --git a/typescript/.changeset/upto-client-sdk.md b/typescript/.changeset/upto-client-sdk.md new file mode 100644 index 0000000000..2c5d911156 --- /dev/null +++ b/typescript/.changeset/upto-client-sdk.md @@ -0,0 +1,5 @@ +--- +'@x402/evm': minor +--- + +Add upto payment scheme TypeScript SDK with client, facilitator, and server support for permit2-based "up to" payments on EVM chains. diff --git a/typescript/.changeset/yummy-readers-cut.md b/typescript/.changeset/yummy-readers-cut.md new file mode 100644 index 0000000000..bcadc37c36 --- /dev/null +++ b/typescript/.changeset/yummy-readers-cut.md @@ -0,0 +1,5 @@ +--- +"@x402/evm": patch +--- + +Updated x402UptoPermit2Proxy canonical address to 0x4020A4f3b7b90ccA423B9fabCc0CE57C6C240002, deployed with deterministic bytecode for reproducible cross-chain CREATE2 addresses diff --git a/typescript/package.json b/typescript/package.json index 602ca6fca1..8612926dc2 100644 --- a/typescript/package.json +++ b/typescript/package.json @@ -21,7 +21,7 @@ "lint:check": "turbo run lint:check", "format:check": "turbo run format:check", "test": "turbo run test", - "test:integration": "pnpm --filter @x402/core --filter @x402/evm --filter @x402/svm --filter @x402/aptos test:integration", + "test:integration": "pnpm --filter @x402/core --filter @x402/evm --filter @x402/svm --filter @x402/aptos --filter @x402/stellar test:integration", "test:all": "pnpm test && pnpm test:integration" }, "keywords": [], @@ -34,4 +34,4 @@ "@changesets/cli": "^2.28.1", "@changesets/changelog-github": "^0.5.1" } -} +} \ No newline at end of file diff --git a/typescript/packages/core/CHANGELOG.md b/typescript/packages/core/CHANGELOG.md index 2ccb20544d..0367e55735 100644 --- a/typescript/packages/core/CHANGELOG.md +++ b/typescript/packages/core/CHANGELOG.md @@ -1,5 +1,29 @@ # @x402/core Changelog +## 2.8.0 + +### Minor Changes + +- 067f297: Added `routePattern` to `HTTPRequestContext` and `pattern` to `CompiledRoute` to thread the matched route pattern through to server extensions, enabling dynamic route support in discovery extensions. +- 4c1e44f: Treat malformed facilitator success payloads as upstream facilitator errors and return 502 responses from framework middleware instead of flattening them into payment failures. +- 5135fab: Accept null in extra and extension fields + +## 2.7.0 + +### Minor Changes + +- 8931cb3: Added support for Express-style `:param` dynamic route parameters in route matching. Routes like `/api/users/:id` and `/api/chapters/:seriesId/:chapterId` now match correctly alongside the existing `[param]` (Next.js) and `*` (wildcard) patterns. + +## 2.6.0 + +### Minor Changes + +- f41baed: Added `x402Version` field to `VerifyRequest`, `SettleRequest`, `VerifyRequestV1`, and `SettleRequestV1` types to match what all SDK implementations already send in facilitator request bodies. +- aeef1bf: Added dynamic function for servers to generate custom response for settlement failures defaulting to empty +- 2564781: Include PAYMENT-RESPONSE header on settlement failure responses +- b341973: Remove duplicate server-local `ResourceInfo` interface; use the wire-format `ResourceInfo` from `types/payments.ts` directly throughout the server module. +- 29fe09a: Make ResourceInfo.description, ResourceInfo.mimeType, and PaymentPayload.resource optional to match v2 spec + ## 2.5.0 ### Minor Changes diff --git a/typescript/packages/core/package.json b/typescript/packages/core/package.json index 226e53c023..e1467004bf 100644 --- a/typescript/packages/core/package.json +++ b/typescript/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "@x402/core", - "version": "2.5.0", + "version": "2.8.0", "main": "./dist/cjs/index.js", "module": "./dist/esm/index.js", "types": "./dist/cjs/index.d.ts", diff --git a/typescript/packages/core/src/http/httpFacilitatorClient.ts b/typescript/packages/core/src/http/httpFacilitatorClient.ts index 29ce532e5a..37bd2a0674 100644 --- a/typescript/packages/core/src/http/httpFacilitatorClient.ts +++ b/typescript/packages/core/src/http/httpFacilitatorClient.ts @@ -5,7 +5,9 @@ import { SupportedResponse, VerifyError, SettleError, + FacilitatorResponseError, } from "../types/facilitator"; +import { z } from "../schemas"; const DEFAULT_FACILITATOR_URL = "https://x402.org/facilitator"; @@ -60,6 +62,121 @@ const GET_SUPPORTED_RETRIES = 3; /** Base delay in ms for exponential backoff on retries */ const GET_SUPPORTED_RETRY_DELAY_MS = 1000; +const verifyResponseSchema: z.ZodType = z.object({ + isValid: z.boolean(), + invalidReason: z + .string() + .nullish() + .transform(v => v ?? undefined), + invalidMessage: z + .string() + .nullish() + .transform(v => v ?? undefined), + payer: z + .string() + .nullish() + .transform(v => v ?? undefined), + extensions: z + .record(z.string(), z.unknown()) + .nullish() + .transform(v => v ?? undefined), +}); + +const settleResponseSchema: z.ZodType = z.object({ + success: z.boolean(), + errorReason: z + .string() + .nullish() + .transform(v => v ?? undefined), + errorMessage: z + .string() + .nullish() + .transform(v => v ?? undefined), + payer: z + .string() + .nullish() + .transform(v => v ?? undefined), + transaction: z.string(), + network: z.custom(value => typeof value === "string"), + extensions: z + .record(z.string(), z.unknown()) + .nullish() + .transform(v => v ?? undefined), +}); + +const supportedKindSchema: z.ZodType = + z.object({ + x402Version: z.number(), + scheme: z.string(), + network: z.custom( + value => typeof value === "string", + ), + extra: z + .record(z.string(), z.unknown()) + .nullish() + .transform(v => v ?? undefined), + }); + +const supportedResponseSchema: z.ZodType = z.object({ + kinds: z.array(supportedKindSchema), + extensions: z.array(z.string()).default([]), + signers: z.record(z.string(), z.array(z.string())).default({}), +}); + +/** + * Produces a compact excerpt of a facilitator response body for error messages. + * + * @param text - The raw response body text + * @param limit - The maximum number of characters to include + * @returns A normalized excerpt suitable for logs and thrown errors + */ +function responseExcerpt(text: string, limit: number = 200): string { + const compact = text.trim().replace(/\s+/g, " "); + if (!compact) { + return ""; + } + + if (compact.length <= limit) { + return compact; + } + + return `${compact.slice(0, limit - 3)}...`; +} + +/** + * Parses and validates a successful facilitator response body. + * + * @param response - The HTTP response returned by the facilitator + * @param schema - The schema used to validate the response payload + * @param operation - The facilitator operation name for error reporting + * @returns The validated facilitator payload + */ +async function parseSuccessResponse( + response: Response, + schema: z.ZodType, + operation: string, +): Promise { + const text = await response.text(); + + let data: unknown; + try { + data = JSON.parse(text); + } catch { + throw new FacilitatorResponseError( + `Facilitator ${operation} returned invalid JSON: ${responseExcerpt(text)}`, + ); + } + + const parsed = schema.safeParse(data); + if (!parsed.success) { + throw new FacilitatorResponseError( + `Facilitator ${operation} returned invalid data: ${responseExcerpt(text)}`, + ); + } + + return parsed.data; +} + /** * HTTP-based client for interacting with x402 facilitator services * Handles HTTP communication with facilitator endpoints @@ -74,7 +191,9 @@ export class HTTPFacilitatorClient implements FacilitatorClient { * @param config - Configuration options for the facilitator client */ constructor(config?: FacilitatorConfig) { - this.url = config?.url || DEFAULT_FACILITATOR_URL; + // Normalize URL: strip trailing slashes to prevent redirect loops (e.g. 308) + // when constructing endpoint paths like `${url}/supported` + this.url = (config?.url || DEFAULT_FACILITATOR_URL).replace(/\/+$/, ""); this._createAuthHeaders = config?.createAuthHeaders; } @@ -101,6 +220,7 @@ export class HTTPFacilitatorClient implements FacilitatorClient { const response = await fetch(`${this.url}/verify`, { method: "POST", headers, + redirect: "follow", body: JSON.stringify({ x402Version: paymentPayload.x402Version, paymentPayload: this.toJsonSafe(paymentPayload), @@ -108,17 +228,25 @@ export class HTTPFacilitatorClient implements FacilitatorClient { }), }); - const data = await response.json(); + if (!response.ok) { + const text = await response.text(); + let data: unknown; + try { + data = JSON.parse(text); + } catch { + throw new Error(`Facilitator verify failed (${response.status}): ${responseExcerpt(text)}`); + } - if (typeof data === "object" && data !== null && "isValid" in data) { - const verifyResponse = data as VerifyResponse; - if (!response.ok) { - throw new VerifyError(response.status, verifyResponse); + if (typeof data === "object" && data !== null && "isValid" in data) { + throw new VerifyError(response.status, data as VerifyResponse); } - return verifyResponse; + + throw new Error( + `Facilitator verify failed (${response.status}): ${responseExcerpt(JSON.stringify(data))}`, + ); } - throw new Error(`Facilitator verify failed (${response.status}): ${JSON.stringify(data)}`); + return parseSuccessResponse(response, verifyResponseSchema, "verify"); } /** @@ -144,6 +272,7 @@ export class HTTPFacilitatorClient implements FacilitatorClient { const response = await fetch(`${this.url}/settle`, { method: "POST", headers, + redirect: "follow", body: JSON.stringify({ x402Version: paymentPayload.x402Version, paymentPayload: this.toJsonSafe(paymentPayload), @@ -151,17 +280,25 @@ export class HTTPFacilitatorClient implements FacilitatorClient { }), }); - const data = await response.json(); + if (!response.ok) { + const text = await response.text(); + let data: unknown; + try { + data = JSON.parse(text); + } catch { + throw new Error(`Facilitator settle failed (${response.status}): ${responseExcerpt(text)}`); + } - if (typeof data === "object" && data !== null && "success" in data) { - const settleResponse = data as SettleResponse; - if (!response.ok) { - throw new SettleError(response.status, settleResponse); + if (typeof data === "object" && data !== null && "success" in data) { + throw new SettleError(response.status, data as SettleResponse); } - return settleResponse; + + throw new Error( + `Facilitator settle failed (${response.status}): ${responseExcerpt(JSON.stringify(data))}`, + ); } - throw new Error(`Facilitator settle failed (${response.status}): ${JSON.stringify(data)}`); + return parseSuccessResponse(response, settleResponseSchema, "settle"); } /** @@ -185,14 +322,17 @@ export class HTTPFacilitatorClient implements FacilitatorClient { const response = await fetch(`${this.url}/supported`, { method: "GET", headers, + redirect: "follow", }); if (response.ok) { - return (await response.json()) as SupportedResponse; + return parseSuccessResponse(response, supportedResponseSchema, "supported"); } const errorText = await response.text().catch(() => response.statusText); - lastError = new Error(`Facilitator getSupported failed (${response.status}): ${errorText}`); + lastError = new Error( + `Facilitator getSupported failed (${response.status}): ${responseExcerpt(errorText)}`, + ); // Retry on 429 rate limit errors with exponential backoff if (response.status === 429 && attempt < GET_SUPPORTED_RETRIES - 1) { diff --git a/typescript/packages/core/src/http/index.ts b/typescript/packages/core/src/http/index.ts index a464eacee8..1b156719bd 100644 --- a/typescript/packages/core/src/http/index.ts +++ b/typescript/packages/core/src/http/index.ts @@ -94,7 +94,8 @@ export { DynamicPayTo, DynamicPrice, UnpaidResponseBody, - UnpaidResponseResult, + HTTPResponseBody, + SettlementFailedResponseBody, ProcessSettleResultResponse, ProcessSettleSuccessResponse, ProcessSettleFailureResponse, @@ -107,4 +108,5 @@ export { FacilitatorClient, FacilitatorConfig, } from "./httpFacilitatorClient"; +export { FacilitatorResponseError, getFacilitatorResponseError } from "../types"; export { x402HTTPClient, PaymentRequiredContext, PaymentRequiredHook } from "./x402HTTPClient"; diff --git a/typescript/packages/core/src/http/x402HTTPResourceServer.ts b/typescript/packages/core/src/http/x402HTTPResourceServer.ts index 1f7ed67997..7f621a8572 100644 --- a/typescript/packages/core/src/http/x402HTTPResourceServer.ts +++ b/typescript/packages/core/src/http/x402HTTPResourceServer.ts @@ -1,4 +1,4 @@ -import { x402ResourceServer } from "../server"; +import { x402ResourceServer, SettlementOverrides } from "../server"; import { decodePaymentSignatureHeader, encodePaymentRequiredHeader, @@ -9,12 +9,15 @@ import { PaymentRequired, SettleResponse, SettleError, + FacilitatorResponseError, Price, Network, PaymentRequirements, } from "../types"; import { x402Version } from ".."; +export const SETTLEMENT_OVERRIDES_HEADER = "Settlement-Overrides"; + /** * Framework-agnostic HTTP adapter interface * Implementations provide framework-specific HTTP operations @@ -80,9 +83,9 @@ export type DynamicPayTo = (context: HTTPRequestContext) => string | Promise Price | Promise; /** - * Result of the unpaid response callback containing content type and body. + * Result of response body callbacks containing content type and body. */ -export interface UnpaidResponseResult { +export interface HTTPResponseBody { /** * The content type for the response (e.g., 'application/json', 'text/plain'). */ @@ -100,7 +103,16 @@ export interface UnpaidResponseResult { */ export type UnpaidResponseBody = ( context: HTTPRequestContext, -) => UnpaidResponseResult | Promise; +) => HTTPResponseBody | Promise; + +/** + * Dynamic function to generate a custom response for settlement failures. + * Receives the HTTP request context and settle failure result, returns the content type and body. + */ +export type SettlementFailedResponseBody = ( + context: HTTPRequestContext, + settleResult: Omit, +) => HTTPResponseBody | Promise; /** * A single payment option for a route @@ -146,6 +158,16 @@ export interface RouteConfig { */ unpaidResponseBody?: UnpaidResponseBody; + /** + * Optional callback to generate a custom response for settlement failures. + * If not provided, defaults to { contentType: 'application/json', body: {} }. + * + * @param context - The HTTP request context + * @param settleResult - The settlement failure result + * @returns An object containing both contentType and body for the 402 response + */ + settlementFailedResponseBody?: SettlementFailedResponseBody; + // Extensions extensions?: Record; } @@ -176,6 +198,7 @@ export interface CompiledRoute { verb: string; regex: RegExp; config: RouteConfig; + pattern: string; } /** @@ -186,6 +209,7 @@ export interface HTTPRequestContext { path: string; method: string; paymentHeader?: string; + routePattern?: string; } /** @@ -196,6 +220,8 @@ export interface HTTPTransportContext { request: HTTPRequestContext; /** The response body buffer */ responseBody?: Buffer; + /** Response headers set by the route handler (used for settlement overrides) */ + responseHeaders?: Record; } /** @@ -235,6 +261,7 @@ export type ProcessSettleFailureResponse = SettleResponse & { errorReason: string; errorMessage?: string; headers: Record; + response: HTTPResponseInstructions; }; export type ProcessSettleResultResponse = @@ -310,6 +337,7 @@ export class x402HTTPResourceServer { verb: parsed.verb, regex: parsed.regex, config, + pattern: parsed.path, }); } } @@ -394,17 +422,21 @@ export class x402HTTPResourceServer { context: HTTPRequestContext, paywallConfig?: PaywallConfig, ): Promise { - const { adapter, path, method } = context; + const method = context.method || context.adapter.getMethod(); + context = { ...context, method }; + const { adapter, path } = context; // Find matching route - const routeConfig = this.getRouteConfig(path, method); - if (!routeConfig) { + const routeMatch = this.getRouteConfig(path, method); + if (!routeMatch) { return { type: "no-payment-required" }; // No payment required for this route } + const { config: routeConfig, pattern: routePattern } = routeMatch; + const enrichedContext: HTTPRequestContext = { ...context, routePattern }; // Execute request hooks before any payment processing for (const hook of this.protectedRequestHooks) { - const result = await hook(context, routeConfig); + const result = await hook(enrichedContext, routeConfig); if (result && "grantAccess" in result) { return { type: "no-payment-required" }; } @@ -428,7 +460,7 @@ export class x402HTTPResourceServer { // Create resource info, using config override if provided const resourceInfo = { - url: routeConfig.resource || context.adapter.getUrl(), + url: routeConfig.resource || enrichedContext.adapter.getUrl(), description: routeConfig.description || "", mimeType: routeConfig.mimeType || "", }; @@ -437,16 +469,16 @@ export class x402HTTPResourceServer { // (this method handles resolving dynamic functions internally) let requirements = await this.ResourceServer.buildPaymentRequirementsFromOptions( paymentOptions, - context, + enrichedContext, ); let extensions = routeConfig.extensions; if (extensions) { - extensions = this.ResourceServer.enrichExtensions(extensions, context); + extensions = this.ResourceServer.enrichExtensions(extensions, enrichedContext); } // createPaymentRequiredResponse already handles extension enrichment in the core layer - const transportContext: HTTPTransportContext = { request: context }; + const transportContext: HTTPTransportContext = { request: enrichedContext }; const paymentRequired = await this.ResourceServer.createPaymentRequiredResponse( requirements, resourceInfo, @@ -459,7 +491,7 @@ export class x402HTTPResourceServer { if (!paymentPayload) { // Resolve custom unpaid response body if provided const unpaidBody = routeConfig.unpaidResponseBody - ? await routeConfig.unpaidResponseBody(context) + ? await routeConfig.unpaidResponseBody(enrichedContext) : undefined; return { @@ -522,6 +554,9 @@ export class x402HTTPResourceServer { declaredExtensions: routeConfig.extensions, }; } catch (error) { + if (error instanceof FacilitatorResponseError) { + throw error; + } const errorResponse = await this.ResourceServer.createPaymentRequiredResponse( requirements, resourceInfo, @@ -543,6 +578,7 @@ export class x402HTTPResourceServer { * @param requirements - The matching payment requirements * @param declaredExtensions - Optional declared extensions (for per-key enrichment) * @param transportContext - Optional HTTP transport context + * @param settlementOverrides - Optional settlement overrides (e.g., partial settlement amount) * @returns ProcessSettleResultResponse - SettleResponse with headers if success or errorReason if failure */ async processSettlement( @@ -550,24 +586,49 @@ export class x402HTTPResourceServer { requirements: PaymentRequirements, declaredExtensions?: Record, transportContext?: HTTPTransportContext, + settlementOverrides?: SettlementOverrides, ): Promise { + if (transportContext?.request && !transportContext.request.method) { + transportContext = { + ...transportContext, + request: { + ...transportContext.request, + method: transportContext.request.adapter.getMethod(), + }, + }; + } try { + // Resolve overrides: explicit param takes precedence, fall back to response header + let resolvedOverrides = settlementOverrides; + if (!resolvedOverrides && transportContext?.responseHeaders?.[SETTLEMENT_OVERRIDES_HEADER]) { + try { + resolvedOverrides = JSON.parse( + transportContext.responseHeaders[SETTLEMENT_OVERRIDES_HEADER], + ); + } catch { + // Ignore malformed header + } + } + const settleResponse = await this.ResourceServer.settlePayment( paymentPayload, requirements, declaredExtensions, transportContext, + resolvedOverrides, ); if (!settleResponse.success) { - return { + const failure = { ...settleResponse, - success: false, + success: false as const, errorReason: settleResponse.errorReason || "Settlement failed", errorMessage: settleResponse.errorMessage || settleResponse.errorReason || "Settlement failed", headers: this.createSettlementHeaders(settleResponse), }; + const response = await this.buildSettlementFailureResponse(failure, transportContext); + return { ...failure, response }; } return { @@ -577,6 +638,9 @@ export class x402HTTPResourceServer { requirements, }; } catch (error) { + if (error instanceof FacilitatorResponseError) { + throw error; + } if (error instanceof SettleError) { const errorReason = error.errorReason || error.message; const settleResponse: SettleResponse = { @@ -587,12 +651,14 @@ export class x402HTTPResourceServer { network: error.network, transaction: error.transaction, }; - return { + const failure = { ...settleResponse, success: false as const, errorReason, headers: this.createSettlementHeaders(settleResponse), }; + const response = await this.buildSettlementFailureResponse(failure, transportContext); + return { ...failure, response }; } const errorReason = error instanceof Error ? error.message : "Settlement failed"; const settleResponse: SettleResponse = { @@ -602,12 +668,14 @@ export class x402HTTPResourceServer { network: requirements.network as Network, transaction: "", }; - return { + const failure = { ...settleResponse, success: false as const, errorReason, headers: this.createSettlementHeaders(settleResponse), }; + const response = await this.buildSettlementFailureResponse(failure, transportContext); + return { ...failure, response }; } } @@ -618,8 +686,43 @@ export class x402HTTPResourceServer { * @returns True if the route requires payment, false otherwise */ requiresPayment(context: HTTPRequestContext): boolean { - const routeConfig = this.getRouteConfig(context.path, context.method); - return routeConfig !== undefined; + const method = context.method || context.adapter.getMethod(); + return this.getRouteConfig(context.path, method) !== undefined; + } + + /** + * Build HTTPResponseInstructions for settlement failure. + * Uses settlementFailedResponseBody hook if configured, otherwise defaults to empty body. + * + * @param failure - Settlement failure result with headers + * @param transportContext - Optional HTTP transport context for the request + * @returns HTTP response instructions for the 402 settlement failure response + */ + private async buildSettlementFailureResponse( + failure: Omit, + transportContext?: HTTPTransportContext, + ): Promise { + const settlementHeaders = failure.headers; + const routeConfig = transportContext + ? this.getRouteConfig(transportContext.request.path, transportContext.request.method) + : undefined; + + const customBody = routeConfig?.config.settlementFailedResponseBody + ? await routeConfig.config.settlementFailedResponseBody(transportContext!.request, failure) + : undefined; + + const contentType = customBody ? customBody.contentType : "application/json"; + const body = customBody ? customBody.body : {}; + + return { + status: 402, + headers: { + "Content-Type": contentType, + ...settlementHeaders, + }, + body, + isHtml: contentType.includes("text/html"), + }; } /** @@ -649,6 +752,21 @@ export class x402HTTPResourceServer { : [["*", this.routesConfig as RouteConfig] as [string, RouteConfig]]; for (const [pattern, config] of normalizedRoutes) { + // Warn if wildcard routes are used with discovery extensions + const pathPart = pattern.includes(" ") ? pattern.split(/\s+/)[1] : pattern; + if ( + pathPart && + pathPart.includes("*") && + config.extensions && + "bazaar" in config.extensions + ) { + console.warn( + `[x402] Route "${pattern}": Wildcard (*) patterns with bazaar discovery extensions ` + + `will auto-generate parameter names (var1, var2, ...). ` + + `Consider using named parameters instead (e.g. /weather/:city) for better discovery metadata.`, + ); + } + const paymentOptions = this.normalizePaymentOptions(config); for (const option of paymentOptions) { @@ -692,9 +810,12 @@ export class x402HTTPResourceServer { * * @param path - Request path * @param method - HTTP method - * @returns Route configuration or undefined if no match + * @returns Route configuration and pattern, or undefined if no match */ - private getRouteConfig(path: string, method: string): RouteConfig | undefined { + private getRouteConfig( + path: string, + method: string, + ): { config: RouteConfig; pattern: string } | undefined { const normalizedPath = this.normalizePath(path); const upperMethod = method.toUpperCase(); @@ -703,7 +824,8 @@ export class x402HTTPResourceServer { route.regex.test(normalizedPath) && (route.verb === "*" || route.verb === upperMethod), ); - return matchingRoute?.config; + if (!matchingRoute) return undefined; + return { config: matchingRoute.config, pattern: matchingRoute.pattern }; } /** @@ -754,7 +876,7 @@ export class x402HTTPResourceServer { isWebBrowser: boolean, paywallConfig?: PaywallConfig, customHtml?: string, - unpaidResponse?: UnpaidResponseResult, + unpaidResponse?: HTTPResponseBody, ): HTTPResponseInstructions { // Use 412 Precondition Failed for permit2_allowance_required error // This signals client needs to approve Permit2 before retrying @@ -816,24 +938,26 @@ export class x402HTTPResourceServer { /** * Parse route pattern into verb and regex * - * @param pattern - Route pattern like "GET /api/*" or "/api/[id]" + * @param pattern - Route pattern like "GET /api/*", "/api/[id]", or "/api/:id" * @returns Parsed pattern with verb and regex */ - private parseRoutePattern(pattern: string): { verb: string; regex: RegExp } { + private parseRoutePattern(pattern: string): { verb: string; regex: RegExp; path: string } { const [verb, path] = pattern.includes(" ") ? pattern.split(/\s+/) : ["*", pattern]; const regex = new RegExp( `^${ path + .replace(/\\/g, "\\\\") // Escape backslashes first .replace(/[$()+.?^{|}]/g, "\\$&") // Escape regex special chars .replace(/\*/g, ".*?") // Wildcards - .replace(/\[([^\]]+)\]/g, "[^/]+") // Parameters + .replace(/\[([^\]]+)\]/g, "[^/]+") // Parameters (Next.js style [param]) + .replace(/:([a-zA-Z_][a-zA-Z0-9_]*)/g, "[^/]+") // Parameters (Express style :param) .replace(/\//g, "\\/") // Escape slashes }$`, "i", ); - return { verb: verb.toUpperCase(), regex }; + return { verb: verb.toUpperCase(), regex, path }; } /** diff --git a/typescript/packages/core/src/server/index.ts b/typescript/packages/core/src/server/index.ts index 11f96623a2..086e4325a9 100644 --- a/typescript/packages/core/src/server/index.ts +++ b/typescript/packages/core/src/server/index.ts @@ -1,10 +1,31 @@ export { x402ResourceServer } from "./x402ResourceServer"; -export type { ResourceConfig, ResourceInfo, SettleResultContext } from "./x402ResourceServer"; +export type { + ResourceConfig, + PaymentRequiredContext, + VerifyContext, + VerifyResultContext, + VerifyFailureContext, + SettleContext, + SettleResultContext, + SettleFailureContext, + SettlementOverrides, + BeforeVerifyHook, + AfterVerifyHook, + OnVerifyFailureHook, + BeforeSettleHook, + AfterSettleHook, + OnSettleFailureHook, +} from "./x402ResourceServer"; export { HTTPFacilitatorClient } from "../http/httpFacilitatorClient"; export type { FacilitatorClient, FacilitatorConfig } from "../http/httpFacilitatorClient"; +export { FacilitatorResponseError, getFacilitatorResponseError } from "../types"; -export { x402HTTPResourceServer, RouteConfigurationError } from "../http/x402HTTPResourceServer"; +export { + x402HTTPResourceServer, + RouteConfigurationError, + SETTLEMENT_OVERRIDES_HEADER, +} from "../http/x402HTTPResourceServer"; export type { HTTPRequestContext, HTTPTransportContext, @@ -17,9 +38,11 @@ export type { HTTPAdapter, RoutesConfig, UnpaidResponseBody, - UnpaidResponseResult, + HTTPResponseBody, + SettlementFailedResponseBody, ProcessSettleResultResponse, ProcessSettleSuccessResponse, ProcessSettleFailureResponse, RouteValidationError, + ProtectedRequestHook, } from "../http/x402HTTPResourceServer"; diff --git a/typescript/packages/core/src/server/x402ResourceServer.ts b/typescript/packages/core/src/server/x402ResourceServer.ts index a5eb4769b2..14f8aab79e 100644 --- a/typescript/packages/core/src/server/x402ResourceServer.ts +++ b/typescript/packages/core/src/server/x402ResourceServer.ts @@ -5,7 +5,12 @@ import { SupportedResponse, SupportedKind, } from "../types/facilitator"; -import { PaymentPayload, PaymentRequirements, PaymentRequired } from "../types/payments"; +import { + PaymentPayload, + PaymentRequirements, + PaymentRequired, + ResourceInfo, +} from "../types/payments"; import { SchemeNetworkServer } from "../types/mechanisms"; import { Price, Network, ResourceServerExtension, VerifyError } from "../types"; import { deepEqual, findByNetworkAndScheme } from "../utils"; @@ -25,15 +30,6 @@ export interface ResourceConfig { extra?: Record; // Scheme-specific additional data } -/** - * Resource information for PaymentRequired response - */ -export interface ResourceInfo { - url: string; - description: string; - mimeType: string; -} - /** * Lifecycle Hook Context Interfaces */ @@ -97,6 +93,74 @@ export type OnSettleFailureHook = ( context: SettleFailureContext, ) => Promise; +/** + * Optional overrides for settlement parameters. + * Used to support partial settlement (e.g., upto scheme billing by actual usage). + * + * Note: Overriding the amount to a value different from the agreed-upon + * `PaymentRequirements.amount` is only valid in schemes that explicitly support + * partial settlement, such as the `upto` scheme. Using this with standard + * x402 schemes (e.g., `exact`) will likely cause settlement verification to fail. + */ +export interface SettlementOverrides { + /** + * Amount to settle. Supports three formats: + * + * - **Raw atomic units** — e.g., `"1000"` settles exactly 1000 atomic units. + * - **Percent** — e.g., `"50%"` settles 50% of `PaymentRequirements.amount`. + * Supports up to two decimal places (e.g., `"33.33%"`). The result is floored + * to the nearest atomic unit. + * - **Dollar price** — e.g., `"$0.05"` converts a USD-denominated price to + * atomic units. Decimals are determined from the registered scheme's + * `getAssetDecimals` method, falling back to 6 (standard for USDC stablecoins). + * The result is rounded to the nearest atomic unit. + * + * The resolved amount must be <= the authorized maximum in `PaymentRequirements`. + * + * Note: Setting this to an amount other than `PaymentRequirements.amount` is + * only valid in schemes that support partial settlement, such as `upto`. + */ + amount?: string; +} + +/** + * Resolves a settlement override amount string to a final atomic-unit string. + * + * Supports three input formats (see {@link SettlementOverrides.amount}): + * - Raw atomic units: `"1000"` + * - Percent of `PaymentRequirements.amount`: `"50%"` + * - Dollar price: `"$0.05"` (converted using the provided decimals) + * + * @param rawAmount - The override amount string (e.g., `"1000"`, `"50%"`, `"$0.05"`) + * @param requirements - The payment requirements containing the base amount + * @param decimals - Decimal precision to use for dollar-format conversion (default 6) + * @returns The resolved amount as an atomic-unit string + */ +export function resolveSettlementOverrideAmount( + rawAmount: string, + requirements: PaymentRequirements, + decimals: number = 6, +): string { + // Percent format: "50%" or "33.33%" + const percentMatch = rawAmount.match(/^(\d+(?:\.\d{0,2})?)%$/); + if (percentMatch) { + const [intPart, decPart = ""] = percentMatch[1].split("."); + const scaledPercent = BigInt(intPart) * 100n + BigInt(decPart.padEnd(2, "0").slice(0, 2)); + const base = BigInt(requirements.amount); + return ((base * scaledPercent) / 10000n).toString(); + } + + // Dollar price format: "$0.05" + const dollarMatch = rawAmount.match(/^\$(\d+(?:\.\d+)?)$/); + if (dollarMatch) { + const dollars = parseFloat(dollarMatch[1]); + return Math.round(dollars * 10 ** decimals).toString(); + } + + // Raw atomic units (existing behavior) + return rawAmount; +} + /** * Core x402 protocol server for resource protection * Transport-agnostic implementation of the x402 payment protocol @@ -303,6 +367,7 @@ export class x402ResourceServer { // Clear existing mappings this.supportedResponsesMap.clear(); this.facilitatorClientsMap.clear(); + let lastError: Error | undefined; // Fetch supported kinds from all facilitator clients // Process in order to give precedence to earlier facilitators @@ -345,15 +410,23 @@ export class x402ResourceServer { } } } catch (error) { + lastError = error as Error; // Log error but continue with other facilitators console.warn(`Failed to fetch supported kinds from facilitator: ${error}`); } } if (this.supportedResponsesMap.size === 0) { - throw new Error( - "Failed to initialize: no supported payment kinds loaded from any facilitator.", - ); + throw lastError + ? new Error( + "Failed to initialize: no supported payment kinds loaded from any facilitator.", + { + cause: lastError, + }, + ) + : new Error( + "Failed to initialize: no supported payment kinds loaded from any facilitator.", + ); } } @@ -700,6 +773,7 @@ export class x402ResourceServer { * @param requirements - The payment requirements * @param declaredExtensions - Optional declared extensions (for per-key enrichment) * @param transportContext - Optional transport-specific context (e.g., HTTP request/response, MCP tool context) + * @param settlementOverrides - Optional overrides for settlement parameters (e.g., partial settlement amount) * @returns Settlement response */ async settlePayment( @@ -707,10 +781,27 @@ export class x402ResourceServer { requirements: PaymentRequirements, declaredExtensions?: Record, transportContext?: unknown, + settlementOverrides?: SettlementOverrides, ): Promise { + // Apply settlement overrides (e.g., partial settlement for upto scheme) + let effectiveRequirements = requirements; + if (settlementOverrides?.amount !== undefined) { + const scheme = findByNetworkAndScheme( + this.registeredServerSchemes, + requirements.scheme, + requirements.network as Network, + ); + const decimals = + scheme?.getAssetDecimals?.(requirements.asset ?? "", requirements.network as Network) ?? 6; + effectiveRequirements = { + ...requirements, + amount: resolveSettlementOverrideAmount(settlementOverrides.amount, requirements, decimals), + }; + } + const context: SettleContext = { paymentPayload, - requirements, + requirements: effectiveRequirements, }; // Execute beforeSettle hooks @@ -744,8 +835,8 @@ export class x402ResourceServer { // Find the facilitator that supports this payment type const facilitatorClient = this.getFacilitatorClient( paymentPayload.x402Version, - requirements.network, - requirements.scheme, + effectiveRequirements.network, + effectiveRequirements.scheme, ); let settleResult: SettleResponse; @@ -756,7 +847,7 @@ export class x402ResourceServer { for (const client of this.facilitatorClients) { try { - settleResult = await client.settle(paymentPayload, requirements); + settleResult = await client.settle(paymentPayload, effectiveRequirements); break; } catch (error) { lastError = error as Error; @@ -767,13 +858,13 @@ export class x402ResourceServer { throw ( lastError || new Error( - `No facilitator supports ${requirements.scheme} on ${requirements.network} for v${paymentPayload.x402Version}`, + `No facilitator supports ${effectiveRequirements.scheme} on ${effectiveRequirements.network} for v${paymentPayload.x402Version}`, ) ); } } else { // Use the specific facilitator that supports this payment - settleResult = await facilitatorClient.settle(paymentPayload, requirements); + settleResult = await facilitatorClient.settle(paymentPayload, effectiveRequirements); } // Execute afterSettle hooks diff --git a/typescript/packages/core/src/types/facilitator.ts b/typescript/packages/core/src/types/facilitator.ts index b37c9e8754..e330967f03 100644 --- a/typescript/packages/core/src/types/facilitator.ts +++ b/typescript/packages/core/src/types/facilitator.ts @@ -2,6 +2,7 @@ import { PaymentPayload, PaymentRequirements } from "./payments"; import { Network } from "./"; export type VerifyRequest = { + x402Version: number; paymentPayload: PaymentPayload; paymentRequirements: PaymentRequirements; }; @@ -15,6 +16,7 @@ export type VerifyResponse = { }; export type SettleRequest = { + x402Version: number; paymentPayload: PaymentPayload; paymentRequirements: PaymentRequirements; }; @@ -26,6 +28,8 @@ export type SettleResponse = { payer?: string; transaction: string; network: Network; + /** Actual amount settled in atomic token units. Present for schemes like `upto` where settlement amount may differ from the authorized maximum. */ + amount?: string; extensions?: Record; }; @@ -99,3 +103,37 @@ export class SettleError extends Error { this.network = response.network; } } + +/** + * Error thrown when a facilitator returns malformed success payload data. + */ +export class FacilitatorResponseError extends Error { + /** + * Creates a FacilitatorResponseError for malformed facilitator responses. + * + * @param message - The boundary error message + */ + constructor(message: string) { + super(message); + this.name = "FacilitatorResponseError"; + } +} + +/** + * Walks an error cause chain to find the first facilitator response error. + * + * @param error - The thrown value to inspect + * @returns The nested facilitator response error, if present + */ +export function getFacilitatorResponseError(error: unknown): FacilitatorResponseError | null { + let current = error; + + while (current instanceof Error) { + if (current instanceof FacilitatorResponseError) { + return current; + } + current = current.cause; + } + + return null; +} diff --git a/typescript/packages/core/src/types/index.ts b/typescript/packages/core/src/types/index.ts index 35a4425ed2..6ef8e7fe37 100644 --- a/typescript/packages/core/src/types/index.ts +++ b/typescript/packages/core/src/types/index.ts @@ -5,7 +5,12 @@ export type { SettleResponse, SupportedResponse, } from "./facilitator"; -export { VerifyError, SettleError } from "./facilitator"; +export { + VerifyError, + SettleError, + FacilitatorResponseError, + getFacilitatorResponseError, +} from "./facilitator"; export type { PaymentRequirements, PaymentPayload, diff --git a/typescript/packages/core/src/types/mechanisms.ts b/typescript/packages/core/src/types/mechanisms.ts index 02f9e8ab49..fb3fa46a92 100644 --- a/typescript/packages/core/src/types/mechanisms.ts +++ b/typescript/packages/core/src/types/mechanisms.ts @@ -148,6 +148,17 @@ export interface SchemeNetworkServer { */ parsePrice(price: Price, network: Network): Promise; + /** + * Optional: Return the decimal precision of the asset for a given network. + * Used by `resolveSettlementOverrideAmount` to convert dollar-format overrides to atomic units. + * Defaults to 6 when not implemented. + * + * @param asset - The asset address or symbol + * @param network - The network identifier + * @returns Number of decimal places for the asset + */ + getAssetDecimals?(asset: string, network: Network): number; + /** * Build payment requirements for this scheme/network combination * diff --git a/typescript/packages/core/src/types/v1/index.ts b/typescript/packages/core/src/types/v1/index.ts index da674995c5..8de7e425b4 100644 --- a/typescript/packages/core/src/types/v1/index.ts +++ b/typescript/packages/core/src/types/v1/index.ts @@ -30,11 +30,13 @@ export type PaymentPayloadV1 = { // Facilitator Requests/Responses export type VerifyRequestV1 = { + x402Version: number; paymentPayload: PaymentPayloadV1; paymentRequirements: PaymentRequirementsV1; }; export type SettleRequestV1 = { + x402Version: number; paymentPayload: PaymentPayloadV1; paymentRequirements: PaymentRequirementsV1; }; diff --git a/typescript/packages/core/test/integrations/upto.test.ts b/typescript/packages/core/test/integrations/upto.test.ts new file mode 100644 index 0000000000..0800d50bad --- /dev/null +++ b/typescript/packages/core/test/integrations/upto.test.ts @@ -0,0 +1,328 @@ +import { beforeEach, describe, expect, it } from "vitest"; +import { x402Client, x402HTTPClient } from "../../src/client"; +import { x402Facilitator } from "../../src/facilitator"; +import { + HTTPAdapter, + HTTPResponseInstructions, + x402HTTPResourceServer, + x402ResourceServer, +} from "../../src/server"; +import { + buildCashPaymentRequirements, + CashFacilitatorClient, + CashSchemeNetworkClient, + CashSchemeNetworkFacilitator, + CashSchemeNetworkServer, +} from "../mocks"; +import { Network, PaymentPayload, PaymentRequirements } from "../../src/types"; +import { SettlementOverrides } from "../../src/server/x402ResourceServer"; +import { SETTLEMENT_OVERRIDES_HEADER } from "../../src/http/x402HTTPResourceServer"; + +describe("Upto Integration Tests", () => { + describe("x402Client / x402ResourceServer — Upto-style partial settlement", () => { + let client: x402Client; + let server: x402ResourceServer; + + beforeEach(async () => { + client = new x402Client().register("x402:cash", new CashSchemeNetworkClient("Alice")); + + const facilitator = new x402Facilitator().register( + "x402:cash", + new CashSchemeNetworkFacilitator(), + ); + + const facilitatorClient = new CashFacilitatorClient(facilitator); + server = new x402ResourceServer(facilitatorClient); + server.register("x402:cash", new CashSchemeNetworkServer()); + await server.initialize(); + }); + + it("should settle with full amount when no overrides provided", async () => { + const accepts = [buildCashPaymentRequirements("Merchant", "USD", "1000")]; + const resource = { + url: "https://api.example.com/generate", + description: "AI generation", + mimeType: "application/json", + }; + const paymentRequired = await server.createPaymentRequiredResponse(accepts, resource); + + const paymentPayload = await client.createPaymentPayload(paymentRequired); + const accepted = server.findMatchingRequirements(accepts, paymentPayload); + expect(accepted).toBeDefined(); + + const verifyResponse = await server.verifyPayment(paymentPayload, accepted!); + expect(verifyResponse.isValid).toBe(true); + + // No overrides — settles for the full 1000 + const settleResponse = await server.settlePayment(paymentPayload, accepted!); + expect(settleResponse.success).toBe(true); + expect(settleResponse.transaction).toContain("1000"); + }); + + it("should settle with reduced amount when overrides specify partial amount", async () => { + const accepts = [buildCashPaymentRequirements("Merchant", "USD", "1000")]; + const resource = { + url: "https://api.example.com/generate", + description: "AI generation", + mimeType: "application/json", + }; + const paymentRequired = await server.createPaymentRequiredResponse(accepts, resource); + + const paymentPayload = await client.createPaymentPayload(paymentRequired); + const accepted = server.findMatchingRequirements(accepts, paymentPayload); + expect(accepted).toBeDefined(); + + const verifyResponse = await server.verifyPayment(paymentPayload, accepted!); + expect(verifyResponse.isValid).toBe(true); + + // Partial settlement — only charge 400 of authorized 1000 + const overrides: SettlementOverrides = { amount: "400" }; + const settleResponse = await server.settlePayment( + paymentPayload, + accepted!, + undefined, + undefined, + overrides, + ); + expect(settleResponse.success).toBe(true); + // The mock cash facilitator includes the amount in the transaction string + expect(settleResponse.transaction).toContain("400"); + expect(settleResponse.transaction).not.toContain("1000"); + }); + + it("should settle with zero amount when overrides specify zero", async () => { + const accepts = [buildCashPaymentRequirements("Merchant", "USD", "1000")]; + const resource = { + url: "https://api.example.com/generate", + description: "AI generation", + mimeType: "application/json", + }; + const paymentRequired = await server.createPaymentRequiredResponse(accepts, resource); + + const paymentPayload = await client.createPaymentPayload(paymentRequired); + const accepted = server.findMatchingRequirements(accepts, paymentPayload); + expect(accepted).toBeDefined(); + + await server.verifyPayment(paymentPayload, accepted!); + + // Zero settlement — free usage this time + const overrides: SettlementOverrides = { amount: "0" }; + const settleResponse = await server.settlePayment( + paymentPayload, + accepted!, + undefined, + undefined, + overrides, + ); + expect(settleResponse.success).toBe(true); + expect(settleResponse.transaction).toContain("0 USD"); + }); + + it("should not modify original requirements when overrides are applied", async () => { + const accepts = [buildCashPaymentRequirements("Merchant", "USD", "1000")]; + const resource = { + url: "https://api.example.com/generate", + description: "AI generation", + mimeType: "application/json", + }; + const paymentRequired = await server.createPaymentRequiredResponse(accepts, resource); + + const paymentPayload = await client.createPaymentPayload(paymentRequired); + const accepted = server.findMatchingRequirements(accepts, paymentPayload); + expect(accepted).toBeDefined(); + + const originalAmount = accepted!.amount; + + await server.settlePayment(paymentPayload, accepted!, undefined, undefined, { + amount: "250", + }); + + // Original requirements object should not be mutated + expect(accepted!.amount).toBe(originalAmount); + }); + }); + + describe("x402HTTPResourceServer — Upto processSettlement with overrides", () => { + let client: x402HTTPClient; + let httpServer: x402HTTPResourceServer; + + const routes = { + "/api/generate": { + accepts: { + scheme: "cash", + payTo: "merchant@example.com", + price: "$10.00", + network: "x402:cash" as Network, + }, + description: "AI generation with upto billing", + mimeType: "application/json", + }, + }; + + function createMockAdapter(): HTTPAdapter { + return { + getHeader: () => undefined, + getMethod: () => "GET", + getPath: () => "/api/generate", + getUrl: () => "https://example.com/api/generate", + getAcceptHeader: () => "application/json", + getUserAgent: () => "TestClient/1.0", + }; + } + + beforeEach(async () => { + const facilitator = new x402Facilitator().register( + "x402:cash", + new CashSchemeNetworkFacilitator(), + ); + + const facilitatorClient = new CashFacilitatorClient(facilitator); + + const paymentClient = new x402Client().register( + "x402:cash", + new CashSchemeNetworkClient("Alice"), + ); + client = new x402HTTPClient(paymentClient) as x402HTTPClient; + + const ResourceServer = new x402ResourceServer(facilitatorClient); + ResourceServer.register("x402:cash", new CashSchemeNetworkServer()); + await ResourceServer.initialize(); + + httpServer = new x402HTTPResourceServer(ResourceServer, routes); + }); + + it("should settle with overrides passed explicitly to processSettlement", async () => { + // Get PaymentRequired + const context = { adapter: createMockAdapter(), path: "/api/generate", method: "GET" }; + const httpResult = await httpServer.processHTTPRequest(context); + expect(httpResult.type).toBe("payment-error"); + + const initial402 = ( + httpResult as { type: "payment-error"; response: HTTPResponseInstructions } + ).response; + + // Client creates payment + const paymentRequired = client.getPaymentRequiredResponse( + name => initial402.headers[name], + initial402.body, + ); + const paymentPayload = await client.createPaymentPayload(paymentRequired); + const requestHeaders = await client.encodePaymentSignatureHeader(paymentPayload); + + // Submit payment + context.adapter.getHeader = (name: string) => { + if (name === "PAYMENT-SIGNATURE") return requestHeaders["PAYMENT-SIGNATURE"]; + return undefined; + }; + const verified = await httpServer.processHTTPRequest(context); + expect(verified.type).toBe("payment-verified"); + + const { paymentPayload: verifiedPayload, paymentRequirements: verifiedRequirements } = + verified as { + type: "payment-verified"; + paymentPayload: PaymentPayload; + paymentRequirements: PaymentRequirements; + }; + + // Settle with partial override + const result = await httpServer.processSettlement( + verifiedPayload, + verifiedRequirements, + undefined, + undefined, + { amount: "3" }, + ); + expect(result.success).toBe(true); + if (result.success) { + expect(result.headers["PAYMENT-RESPONSE"]).toBeDefined(); + } + }); + + it("should extract overrides from transport context responseHeaders", async () => { + // Get PaymentRequired + const context = { adapter: createMockAdapter(), path: "/api/generate", method: "GET" }; + const httpResult = await httpServer.processHTTPRequest(context); + const initial402 = ( + httpResult as { type: "payment-error"; response: HTTPResponseInstructions } + ).response; + + // Client creates payment + const paymentRequired = client.getPaymentRequiredResponse( + name => initial402.headers[name], + initial402.body, + ); + const paymentPayload = await client.createPaymentPayload(paymentRequired); + const requestHeaders = await client.encodePaymentSignatureHeader(paymentPayload); + + context.adapter.getHeader = (name: string) => { + if (name === "PAYMENT-SIGNATURE") return requestHeaders["PAYMENT-SIGNATURE"]; + return undefined; + }; + const verified = await httpServer.processHTTPRequest(context); + + const { paymentPayload: verifiedPayload, paymentRequirements: verifiedRequirements } = + verified as { + type: "payment-verified"; + paymentPayload: PaymentPayload; + paymentRequirements: PaymentRequirements; + }; + + // Pass overrides via transport context responseHeaders (simulating middleware extraction) + const result = await httpServer.processSettlement( + verifiedPayload, + verifiedRequirements, + undefined, + { + request: context, + responseHeaders: { + [SETTLEMENT_OVERRIDES_HEADER]: JSON.stringify({ amount: "5" }), + }, + }, + ); + expect(result.success).toBe(true); + }); + + it("explicit overrides should take precedence over header overrides", async () => { + const context = { adapter: createMockAdapter(), path: "/api/generate", method: "GET" }; + const httpResult = await httpServer.processHTTPRequest(context); + const initial402 = ( + httpResult as { type: "payment-error"; response: HTTPResponseInstructions } + ).response; + + const paymentRequired = client.getPaymentRequiredResponse( + name => initial402.headers[name], + initial402.body, + ); + const paymentPayload = await client.createPaymentPayload(paymentRequired); + const requestHeaders = await client.encodePaymentSignatureHeader(paymentPayload); + + context.adapter.getHeader = (name: string) => { + if (name === "PAYMENT-SIGNATURE") return requestHeaders["PAYMENT-SIGNATURE"]; + return undefined; + }; + const verified = await httpServer.processHTTPRequest(context); + + const { paymentPayload: verifiedPayload, paymentRequirements: verifiedRequirements } = + verified as { + type: "payment-verified"; + paymentPayload: PaymentPayload; + paymentRequirements: PaymentRequirements; + }; + + // Both explicit and header overrides — explicit wins + const result = await httpServer.processSettlement( + verifiedPayload, + verifiedRequirements, + undefined, + { + request: context, + responseHeaders: { + [SETTLEMENT_OVERRIDES_HEADER]: JSON.stringify({ amount: "999" }), + }, + }, + { amount: "2" }, // explicit takes precedence + ); + expect(result.success).toBe(true); + }); + }); +}); diff --git a/typescript/packages/core/test/mocks/generic/MockFacilitatorClient.ts b/typescript/packages/core/test/mocks/generic/MockFacilitatorClient.ts index 1dda1e36cd..4cef0090b1 100644 --- a/typescript/packages/core/test/mocks/generic/MockFacilitatorClient.ts +++ b/typescript/packages/core/test/mocks/generic/MockFacilitatorClient.ts @@ -63,10 +63,13 @@ export class MockFacilitatorClient implements FacilitatorClient { payloadOrRequest: PaymentPayload | VerifyRequest, requirements?: PaymentRequirements, ): Promise { - const payload = "payload" in payloadOrRequest ? payloadOrRequest.payload : payloadOrRequest; + const payload = + "paymentPayload" in payloadOrRequest ? payloadOrRequest.paymentPayload : payloadOrRequest; const reqs = - requirements || - ("requirements" in payloadOrRequest ? payloadOrRequest.requirements : undefined)!; + requirements ?? + ("paymentRequirements" in payloadOrRequest + ? payloadOrRequest.paymentRequirements + : undefined)!; this.verifyCalls.push({ payload, requirements: reqs }); @@ -93,10 +96,13 @@ export class MockFacilitatorClient implements FacilitatorClient { payloadOrRequest: PaymentPayload | SettleRequest, requirements?: PaymentRequirements, ): Promise { - const payload = "payload" in payloadOrRequest ? payloadOrRequest.payload : payloadOrRequest; + const payload = + "paymentPayload" in payloadOrRequest ? payloadOrRequest.paymentPayload : payloadOrRequest; const reqs = - requirements || - ("requirements" in payloadOrRequest ? payloadOrRequest.requirements : undefined)!; + requirements ?? + ("paymentRequirements" in payloadOrRequest + ? payloadOrRequest.paymentRequirements + : undefined)!; this.settleCalls.push({ payload, requirements: reqs }); diff --git a/typescript/packages/core/test/mocks/generic/MockSchemeServer.ts b/typescript/packages/core/test/mocks/generic/MockSchemeServer.ts index 8ec46cfcbf..803124b1d4 100644 --- a/typescript/packages/core/test/mocks/generic/MockSchemeServer.ts +++ b/typescript/packages/core/test/mocks/generic/MockSchemeServer.ts @@ -9,6 +9,7 @@ export class MockSchemeNetworkServer implements SchemeNetworkServer { public readonly scheme: string; private parsePriceResult: AssetAmount | Error; private enhanceResult: PaymentRequirements | Error | null = null; + private assetDecimalsResult: number | null = null; // Call tracking public parsePriceCalls: Array<{ price: Price; network: Network }> = []; @@ -71,7 +72,19 @@ export class MockSchemeNetworkServer implements SchemeNetworkServer { return this.enhanceResult || paymentRequirements; } + getAssetDecimals(_asset: string, _network: Network): number { + return this.assetDecimalsResult ?? 6; + } + // Helper methods for test configuration + /** + * + * @param result + */ + setAssetDecimalsResult(result: number): void { + this.assetDecimalsResult = result; + } + /** * * @param result diff --git a/typescript/packages/core/test/unit/http/httpFacilitatorClient.test.ts b/typescript/packages/core/test/unit/http/httpFacilitatorClient.test.ts new file mode 100644 index 0000000000..ef2214edbf --- /dev/null +++ b/typescript/packages/core/test/unit/http/httpFacilitatorClient.test.ts @@ -0,0 +1,267 @@ +import { afterEach, describe, expect, it, vi } from "vitest"; +import { HTTPFacilitatorClient } from "../../../src/http/httpFacilitatorClient"; +import { FacilitatorResponseError, SettleError, VerifyError } from "../../../src/types"; +import { PaymentPayload, PaymentRequirements } from "../../../src/types/payments"; + +const paymentRequirements: PaymentRequirements = { + scheme: "exact", + network: "eip155:8453", + asset: "0x0000000000000000000000000000000000000000", + amount: "1000000", + payTo: "0x1234567890123456789012345678901234567890", + maxTimeoutSeconds: 300, + extra: {}, +}; + +const paymentPayload: PaymentPayload = { + x402Version: 2, + accepted: paymentRequirements, + payload: { signature: "0xmock" }, +}; + +describe("HTTPFacilitatorClient", () => { + afterEach(() => { + vi.restoreAllMocks(); + vi.unstubAllGlobals(); + }); + + it("throws FacilitatorResponseError for invalid verify JSON on success", async () => { + vi.stubGlobal("fetch", vi.fn().mockResolvedValue(new Response("not-json", { status: 200 }))); + + const client = new HTTPFacilitatorClient({ url: "https://facilitator.test" }); + const error = await client + .verify(paymentPayload, paymentRequirements) + .catch(caught => caught as Error); + + expect(error).toBeInstanceOf(FacilitatorResponseError); + expect(error.message).toContain("Facilitator verify returned invalid JSON"); + }); + + it("throws FacilitatorResponseError for invalid settle data on success", async () => { + vi.stubGlobal( + "fetch", + vi.fn().mockResolvedValue(new Response(JSON.stringify({ success: true }), { status: 200 })), + ); + + const client = new HTTPFacilitatorClient({ url: "https://facilitator.test" }); + const error = await client + .settle(paymentPayload, paymentRequirements) + .catch(caught => caught as Error); + + expect(error).toBeInstanceOf(FacilitatorResponseError); + expect(error.message).toContain("Facilitator settle returned invalid data"); + }); + + it("throws FacilitatorResponseError for invalid supported data on success", async () => { + vi.stubGlobal( + "fetch", + vi + .fn() + .mockResolvedValue( + new Response(JSON.stringify({ kinds: [{ scheme: "exact" }] }), { status: 200 }), + ), + ); + + const client = new HTTPFacilitatorClient({ url: "https://facilitator.test" }); + const error = await client.getSupported().catch(caught => caught as Error); + + expect(error).toBeInstanceOf(FacilitatorResponseError); + expect(error.message).toContain("Facilitator supported returned invalid data"); + }); + + it("preserves VerifyError semantics for valid non-200 verify responses", async () => { + vi.stubGlobal( + "fetch", + vi.fn().mockResolvedValue( + new Response( + JSON.stringify({ + isValid: false, + invalidReason: "invalid_signature", + invalidMessage: "signature mismatch", + }), + { status: 400 }, + ), + ), + ); + + const client = new HTTPFacilitatorClient({ url: "https://facilitator.test" }); + + await expect(client.verify(paymentPayload, paymentRequirements)).rejects.toThrow(VerifyError); + }); + + it("preserves SettleError semantics for valid non-200 settle responses", async () => { + vi.stubGlobal( + "fetch", + vi.fn().mockResolvedValue( + new Response( + JSON.stringify({ + success: false, + errorReason: "insufficient_allowance", + transaction: "", + network: "eip155:8453", + }), + { status: 400 }, + ), + ), + ); + + const client = new HTTPFacilitatorClient({ url: "https://facilitator.test" }); + + await expect(client.settle(paymentPayload, paymentRequirements)).rejects.toThrow(SettleError); + }); + + it("parses verify 200 when optional string fields are JSON null", async () => { + vi.stubGlobal( + "fetch", + vi.fn().mockResolvedValue( + new Response( + JSON.stringify({ + isValid: true, + invalidReason: null, + invalidMessage: null, + payer: null, + }), + { status: 200 }, + ), + ), + ); + + const client = new HTTPFacilitatorClient({ url: "https://facilitator.test" }); + const result = await client.verify(paymentPayload, paymentRequirements); + + expect(result.isValid).toBe(true); + expect(result.invalidReason).toBeUndefined(); + expect(result.invalidMessage).toBeUndefined(); + expect(result.payer).toBeUndefined(); + }); + + it("parses settle 200 when optional string fields are JSON null", async () => { + vi.stubGlobal( + "fetch", + vi.fn().mockResolvedValue( + new Response( + JSON.stringify({ + success: true, + transaction: "0xabc", + network: "eip155:8453", + errorReason: null, + errorMessage: null, + payer: null, + }), + { status: 200 }, + ), + ), + ); + + const client = new HTTPFacilitatorClient({ url: "https://facilitator.test" }); + const result = await client.settle(paymentPayload, paymentRequirements); + + expect(result.success).toBe(true); + expect(result.transaction).toBe("0xabc"); + expect(result.network).toBe("eip155:8453"); + expect(result.errorReason).toBeUndefined(); + expect(result.errorMessage).toBeUndefined(); + expect(result.payer).toBeUndefined(); + }); + + describe("URL normalization", () => { + it("strips trailing slashes from the configured URL", () => { + const client = new HTTPFacilitatorClient({ url: "https://x402.org/facilitator/" }); + expect(client.url).toBe("https://x402.org/facilitator"); + }); + + it("strips multiple trailing slashes", () => { + const client = new HTTPFacilitatorClient({ url: "https://x402.org/facilitator///" }); + expect(client.url).toBe("https://x402.org/facilitator"); + }); + + it("leaves URLs without trailing slash unchanged", () => { + const client = new HTTPFacilitatorClient({ url: "https://x402.org/facilitator" }); + expect(client.url).toBe("https://x402.org/facilitator"); + }); + + it("uses default URL when no config is provided", () => { + const client = new HTTPFacilitatorClient(); + expect(client.url).toBe("https://x402.org/facilitator"); + }); + }); + + describe("redirect handling", () => { + it("passes redirect: follow to fetch on getSupported", async () => { + const mockFetch = vi.fn().mockResolvedValue( + new Response( + JSON.stringify({ + kinds: [{ x402Version: 2, scheme: "exact", network: "eip155:8453" }], + }), + { status: 200 }, + ), + ); + vi.stubGlobal("fetch", mockFetch); + + const client = new HTTPFacilitatorClient({ url: "https://facilitator.test" }); + await client.getSupported(); + + expect(mockFetch).toHaveBeenCalledWith( + "https://facilitator.test/supported", + expect.objectContaining({ redirect: "follow" }), + ); + }); + + it("passes redirect: follow to fetch on verify", async () => { + const mockFetch = vi + .fn() + .mockResolvedValue(new Response(JSON.stringify({ isValid: true }), { status: 200 })); + vi.stubGlobal("fetch", mockFetch); + + const client = new HTTPFacilitatorClient({ url: "https://facilitator.test" }); + await client.verify(paymentPayload, paymentRequirements); + + expect(mockFetch).toHaveBeenCalledWith( + "https://facilitator.test/verify", + expect.objectContaining({ redirect: "follow" }), + ); + }); + + it("passes redirect: follow to fetch on settle", async () => { + const mockFetch = vi.fn().mockResolvedValue( + new Response( + JSON.stringify({ + success: true, + transaction: "0xabc", + network: "eip155:8453", + }), + { status: 200 }, + ), + ); + vi.stubGlobal("fetch", mockFetch); + + const client = new HTTPFacilitatorClient({ url: "https://facilitator.test" }); + await client.settle(paymentPayload, paymentRequirements); + + expect(mockFetch).toHaveBeenCalledWith( + "https://facilitator.test/settle", + expect.objectContaining({ redirect: "follow" }), + ); + }); + + it("constructs correct endpoint URLs after trailing slash normalization", async () => { + const mockFetch = vi.fn().mockResolvedValue( + new Response( + JSON.stringify({ + kinds: [{ x402Version: 2, scheme: "exact", network: "eip155:8453" }], + }), + { status: 200 }, + ), + ); + vi.stubGlobal("fetch", mockFetch); + + const client = new HTTPFacilitatorClient({ url: "https://x402.org/facilitator/" }); + await client.getSupported(); + + expect(mockFetch).toHaveBeenCalledWith( + "https://x402.org/facilitator/supported", + expect.anything(), + ); + }); + }); +}); diff --git a/typescript/packages/core/test/unit/http/x402HTTPResourceServer.errors.test.ts b/typescript/packages/core/test/unit/http/x402HTTPResourceServer.errors.test.ts new file mode 100644 index 0000000000..0d7e3c1c6d --- /dev/null +++ b/typescript/packages/core/test/unit/http/x402HTTPResourceServer.errors.test.ts @@ -0,0 +1,116 @@ +import { beforeEach, describe, expect, it } from "vitest"; +import { x402HTTPResourceServer, HTTPAdapter } from "../../../src/http/x402HTTPResourceServer"; +import { x402ResourceServer } from "../../../src/server/x402ResourceServer"; +import { FacilitatorResponseError, Network } from "../../../src/types"; +import { + MockFacilitatorClient, + MockSchemeNetworkServer, + buildPaymentPayload, + buildPaymentRequirements, + buildSupportedResponse, +} from "../../mocks"; +import { encodePaymentSignatureHeader } from "../../../src/http"; + +class MockHTTPAdapter implements HTTPAdapter { + constructor(private readonly headers: Record = {}) {} + + getHeader(name: string): string | undefined { + return this.headers[name.toLowerCase()]; + } + + getMethod(): string { + return "GET"; + } + + getPath(): string { + return "/api/test"; + } + + getUrl(): string { + return "https://example.com/api/test"; + } + + getAcceptHeader(): string { + return "application/json"; + } + + getUserAgent(): string { + return "Vitest"; + } +} + +describe("x402HTTPResourceServer facilitator response errors", () => { + let resourceServer: x402ResourceServer; + let facilitator: MockFacilitatorClient; + let httpServer: x402HTTPResourceServer; + const network = "eip155:8453" as Network; + + beforeEach(async () => { + facilitator = new MockFacilitatorClient( + buildSupportedResponse({ + kinds: [{ x402Version: 2, scheme: "exact", network }], + }), + ); + + resourceServer = new x402ResourceServer(facilitator); + resourceServer.register(network, new MockSchemeNetworkServer("exact")); + await resourceServer.initialize(); + + httpServer = new x402HTTPResourceServer(resourceServer, { + "/api/test": { + accepts: { + scheme: "exact", + payTo: "0xabc", + price: "$1.00", + network, + }, + }, + }); + }); + + it("rethrows FacilitatorResponseError during verification", async () => { + facilitator.setVerifyResponse( + new FacilitatorResponseError("Facilitator verify returned invalid JSON: not-json"), + ); + + const accepted = buildPaymentRequirements({ + scheme: "exact", + network, + payTo: "0xabc", + asset: "USDC", + amount: "1000000", + }); + const payload = buildPaymentPayload({ + x402Version: 2, + accepted, + }); + + await expect( + httpServer.processHTTPRequest({ + adapter: new MockHTTPAdapter({ + "payment-signature": encodePaymentSignatureHeader(payload), + }), + path: "/api/test", + method: "GET", + paymentHeader: encodePaymentSignatureHeader(payload), + }), + ).rejects.toThrow(FacilitatorResponseError); + }); + + it("rethrows FacilitatorResponseError during settlement", async () => { + facilitator.setSettleResponse( + new FacilitatorResponseError('Facilitator settle returned invalid data: {"success":true}'), + ); + + const accepted = buildPaymentRequirements({ + scheme: "exact", + network, + payTo: "0xabc", + asset: "USDC", + amount: "1000000", + }); + await expect( + httpServer.processSettlement(buildPaymentPayload({ x402Version: 2, accepted }), accepted), + ).rejects.toThrow(FacilitatorResponseError); + }); +}); diff --git a/typescript/packages/core/test/unit/http/x402HTTPResourceService.test.ts b/typescript/packages/core/test/unit/http/x402HTTPResourceService.test.ts index 3c31df05e5..2dda52f143 100644 --- a/typescript/packages/core/test/unit/http/x402HTTPResourceService.test.ts +++ b/typescript/packages/core/test/unit/http/x402HTTPResourceService.test.ts @@ -372,6 +372,84 @@ describe("x402HTTPResourceServer", () => { expect(result.type).toBe("payment-error"); // Route matched }); + it("should match Express-style :param dynamic routes", async () => { + const routes = { + "/api/chapters/:seriesId/:chapterId": { + accepts: { + scheme: "exact", + payTo: "0xabc", + price: "$1.00" as Price, + network: "eip155:8453" as Network, + }, + }, + }; + + const httpServer = new x402HTTPResourceServer(ResourceServer, routes); + + const adapter = new MockHTTPAdapter(); + const context: HTTPRequestContext = { + adapter, + path: "/api/chapters/abc123/chapter-7", + method: "GET", + }; + + const result = await httpServer.processHTTPRequest(context); + + expect(result.type).toBe("payment-error"); // Route matched + }); + + it("should match Express-style :param with HTTP method prefix", async () => { + const routes = { + "GET /api/users/:id": { + accepts: { + scheme: "exact", + payTo: "0xabc", + price: "$1.00" as Price, + network: "eip155:8453" as Network, + }, + }, + }; + + const httpServer = new x402HTTPResourceServer(ResourceServer, routes); + + const adapter = new MockHTTPAdapter(); + const context: HTTPRequestContext = { + adapter, + path: "/api/users/42", + method: "GET", + }; + + const result = await httpServer.processHTTPRequest(context); + + expect(result.type).toBe("payment-error"); // Route matched + }); + + it("should not match :param against paths with extra segments", async () => { + const routes = { + "/api/users/:id": { + accepts: { + scheme: "exact", + payTo: "0xabc", + price: "$1.00" as Price, + network: "eip155:8453" as Network, + }, + }, + }; + + const httpServer = new x402HTTPResourceServer(ResourceServer, routes); + + const adapter = new MockHTTPAdapter(); + const context: HTTPRequestContext = { + adapter, + path: "/api/users/42/posts", + method: "GET", + }; + + const result = await httpServer.processHTTPRequest(context); + + expect(result.type).toBe("no-payment-required"); + }); + it("should return no-payment-required for unmatched routes", async () => { const routes = { "/api/protected": { @@ -748,6 +826,167 @@ describe("x402HTTPResourceServer", () => { expect(result.headers["PAYMENT-RESPONSE"]).toBeDefined(); } }); + + it("should forward explicit settlementOverrides to settlePayment", async () => { + const routes = { + "/api/test": { + accepts: { + scheme: "exact", + payTo: "0xabc", + price: "$1.00" as Price, + network: "eip155:8453" as Network, + }, + }, + }; + + const httpServer = new x402HTTPResourceServer(ResourceServer, routes); + + const payload = buildPaymentPayload(); + const requirements = buildPaymentRequirements({ + scheme: "exact", + network: "eip155:8453" as Network, + amount: "1000000", + }); + + const result = await httpServer.processSettlement( + payload, + requirements, + undefined, + undefined, + { amount: "500000" }, + ); + + expect(result.success).toBe(true); + // Verify the facilitator received the overridden amount + expect(mockFacilitator.settleCalls[0].requirements.amount).toBe("500000"); + }); + + it("should extract overrides from responseHeaders in transport context", async () => { + const { SETTLEMENT_OVERRIDES_HEADER } = await import( + "../../../src/http/x402HTTPResourceServer" + ); + + const routes = { + "/api/test": { + accepts: { + scheme: "exact", + payTo: "0xabc", + price: "$1.00" as Price, + network: "eip155:8453" as Network, + }, + }, + }; + + const httpServer = new x402HTTPResourceServer(ResourceServer, routes); + + const payload = buildPaymentPayload(); + const requirements = buildPaymentRequirements({ + scheme: "exact", + network: "eip155:8453" as Network, + amount: "1000000", + }); + + const result = await httpServer.processSettlement(payload, requirements, undefined, { + request: { + adapter: new MockHTTPAdapter(), + path: "/api/test", + method: "GET", + }, + responseHeaders: { + [SETTLEMENT_OVERRIDES_HEADER]: JSON.stringify({ amount: "300000" }), + }, + }); + + expect(result.success).toBe(true); + expect(mockFacilitator.settleCalls[0].requirements.amount).toBe("300000"); + }); + + it("should ignore malformed overrides header gracefully", async () => { + const { SETTLEMENT_OVERRIDES_HEADER } = await import( + "../../../src/http/x402HTTPResourceServer" + ); + + const routes = { + "/api/test": { + accepts: { + scheme: "exact", + payTo: "0xabc", + price: "$1.00" as Price, + network: "eip155:8453" as Network, + }, + }, + }; + + const httpServer = new x402HTTPResourceServer(ResourceServer, routes); + + const payload = buildPaymentPayload(); + const requirements = buildPaymentRequirements({ + scheme: "exact", + network: "eip155:8453" as Network, + amount: "1000000", + }); + + const result = await httpServer.processSettlement(payload, requirements, undefined, { + request: { + adapter: new MockHTTPAdapter(), + path: "/api/test", + method: "GET", + }, + responseHeaders: { + [SETTLEMENT_OVERRIDES_HEADER]: "not-valid-json{{{", + }, + }); + + // Should succeed with original amount (malformed header is ignored) + expect(result.success).toBe(true); + expect(mockFacilitator.settleCalls[0].requirements.amount).toBe("1000000"); + }); + + it("should prefer explicit overrides over header overrides", async () => { + const { SETTLEMENT_OVERRIDES_HEADER } = await import( + "../../../src/http/x402HTTPResourceServer" + ); + + const routes = { + "/api/test": { + accepts: { + scheme: "exact", + payTo: "0xabc", + price: "$1.00" as Price, + network: "eip155:8453" as Network, + }, + }, + }; + + const httpServer = new x402HTTPResourceServer(ResourceServer, routes); + + const payload = buildPaymentPayload(); + const requirements = buildPaymentRequirements({ + scheme: "exact", + network: "eip155:8453" as Network, + amount: "1000000", + }); + + const result = await httpServer.processSettlement( + payload, + requirements, + undefined, + { + request: { + adapter: new MockHTTPAdapter(), + path: "/api/test", + method: "GET", + }, + responseHeaders: { + [SETTLEMENT_OVERRIDES_HEADER]: JSON.stringify({ amount: "999999" }), + }, + }, + { amount: "100000" }, // explicit takes precedence + ); + + expect(result.success).toBe(true); + expect(mockFacilitator.settleCalls[0].requirements.amount).toBe("100000"); + }); }); describe("Browser detection", () => { diff --git a/typescript/packages/core/test/unit/server/x402ResourceServer.test.ts b/typescript/packages/core/test/unit/server/x402ResourceServer.test.ts index b904637ff1..1966efa662 100644 --- a/typescript/packages/core/test/unit/server/x402ResourceServer.test.ts +++ b/typescript/packages/core/test/unit/server/x402ResourceServer.test.ts @@ -1,5 +1,8 @@ import { describe, it, expect, beforeEach } from "vitest"; -import { x402ResourceServer } from "../../../src/server/x402ResourceServer"; +import { + x402ResourceServer, + resolveSettlementOverrideAmount, +} from "../../../src/server/x402ResourceServer"; import { MockFacilitatorClient, MockSchemeNetworkServer, @@ -820,6 +823,242 @@ describe("x402ResourceServer", () => { expect(result.success).toBe(true); expect(mockClient.settleCalls.length).toBe(1); }); + + it("should use original amount when no overrides provided", async () => { + const mockClient = new MockFacilitatorClient( + buildSupportedResponse({ + kinds: [{ x402Version: 2, scheme: "exact", network: "eip155:8453" as Network }], + }), + undefined, + buildSettleResponse({ success: true }), + ); + + const server = new x402ResourceServer(mockClient); + + const payload = buildPaymentPayload(); + const requirements = buildPaymentRequirements({ + scheme: "exact", + network: "eip155:8453" as Network, + amount: "1000000", + }); + + await server.settlePayment(payload, requirements); + + expect(mockClient.settleCalls[0].requirements.amount).toBe("1000000"); + }); + + it("should override amount when settlementOverrides.amount is provided", async () => { + const mockClient = new MockFacilitatorClient( + buildSupportedResponse({ + kinds: [{ x402Version: 2, scheme: "exact", network: "eip155:8453" as Network }], + }), + undefined, + buildSettleResponse({ success: true }), + ); + + const server = new x402ResourceServer(mockClient); + + const payload = buildPaymentPayload(); + const requirements = buildPaymentRequirements({ + scheme: "exact", + network: "eip155:8453" as Network, + amount: "1000000", + }); + + await server.settlePayment(payload, requirements, undefined, undefined, { amount: "500000" }); + + // Facilitator should receive the overridden amount + expect(mockClient.settleCalls[0].requirements.amount).toBe("500000"); + }); + + it("should not mutate original requirements when overrides applied", async () => { + const mockClient = new MockFacilitatorClient( + buildSupportedResponse({ + kinds: [{ x402Version: 2, scheme: "exact", network: "eip155:8453" as Network }], + }), + undefined, + buildSettleResponse({ success: true }), + ); + + const server = new x402ResourceServer(mockClient); + + const payload = buildPaymentPayload(); + const requirements = buildPaymentRequirements({ + scheme: "exact", + network: "eip155:8453" as Network, + amount: "1000000", + }); + + await server.settlePayment(payload, requirements, undefined, undefined, { amount: "250000" }); + + // Original requirements must not be mutated + expect(requirements.amount).toBe("1000000"); + }); + + it("should use original amount when overrides has undefined amount", async () => { + const mockClient = new MockFacilitatorClient( + buildSupportedResponse({ + kinds: [{ x402Version: 2, scheme: "exact", network: "eip155:8453" as Network }], + }), + undefined, + buildSettleResponse({ success: true }), + ); + + const server = new x402ResourceServer(mockClient); + + const payload = buildPaymentPayload(); + const requirements = buildPaymentRequirements({ + scheme: "exact", + network: "eip155:8453" as Network, + amount: "1000000", + }); + + await server.settlePayment(payload, requirements, undefined, undefined, {}); + + expect(mockClient.settleCalls[0].requirements.amount).toBe("1000000"); + }); + + it("should allow settling for zero amount", async () => { + const mockClient = new MockFacilitatorClient( + buildSupportedResponse({ + kinds: [{ x402Version: 2, scheme: "exact", network: "eip155:8453" as Network }], + }), + undefined, + buildSettleResponse({ success: true }), + ); + + const server = new x402ResourceServer(mockClient); + + const payload = buildPaymentPayload(); + const requirements = buildPaymentRequirements({ + scheme: "exact", + network: "eip155:8453" as Network, + amount: "1000000", + }); + + await server.settlePayment(payload, requirements, undefined, undefined, { amount: "0" }); + + expect(mockClient.settleCalls[0].requirements.amount).toBe("0"); + }); + + it("should resolve percent override through settlePayment", async () => { + const mockClient = new MockFacilitatorClient( + buildSupportedResponse({ + kinds: [{ x402Version: 2, scheme: "exact", network: "eip155:8453" as Network }], + }), + undefined, + buildSettleResponse({ success: true }), + ); + + const server = new x402ResourceServer(mockClient); + const payload = buildPaymentPayload(); + const requirements = buildPaymentRequirements({ + scheme: "exact", + network: "eip155:8453" as Network, + amount: "2000", + }); + + await server.settlePayment(payload, requirements, undefined, undefined, { amount: "50%" }); + + expect(mockClient.settleCalls[0].requirements.amount).toBe("1000"); + }); + + it("should resolve dollar override through settlePayment with default decimals", async () => { + const mockClient = new MockFacilitatorClient( + buildSupportedResponse({ + kinds: [{ x402Version: 2, scheme: "exact", network: "eip155:8453" as Network }], + }), + undefined, + buildSettleResponse({ success: true }), + ); + + const server = new x402ResourceServer(mockClient); + const payload = buildPaymentPayload(); + const requirements = buildPaymentRequirements({ + scheme: "exact", + network: "eip155:8453" as Network, + amount: "1000000", + }); + + await server.settlePayment(payload, requirements, undefined, undefined, { amount: "$0.001" }); + + expect(mockClient.settleCalls[0].requirements.amount).toBe("1000"); + }); + + it("should resolve dollar override using scheme getAssetDecimals", async () => { + const mockClient = new MockFacilitatorClient( + buildSupportedResponse({ + kinds: [{ x402Version: 2, scheme: "exact", network: "eip155:8453" as Network }], + }), + undefined, + buildSettleResponse({ success: true }), + ); + + const server = new x402ResourceServer(mockClient); + const mockScheme = new MockSchemeNetworkServer("exact"); + mockScheme.setAssetDecimalsResult(8); + server.register("eip155:8453" as Network, mockScheme); + + const payload = buildPaymentPayload(); + const requirements = buildPaymentRequirements({ + scheme: "exact", + network: "eip155:8453" as Network, + amount: "1000000", + }); + + await server.settlePayment(payload, requirements, undefined, undefined, { amount: "$0.05" }); + + expect(mockClient.settleCalls[0].requirements.amount).toBe("5000000"); + }); + + it("should not mutate asset when dollar override is used", async () => { + const mockClient = new MockFacilitatorClient( + buildSupportedResponse({ + kinds: [{ x402Version: 2, scheme: "exact", network: "eip155:8453" as Network }], + }), + undefined, + buildSettleResponse({ success: true }), + ); + + const server = new x402ResourceServer(mockClient); + const payload = buildPaymentPayload(); + const requirements = buildPaymentRequirements({ + scheme: "exact", + network: "eip155:8453" as Network, + amount: "1000000", + asset: "0xOriginalToken", + }); + + await server.settlePayment(payload, requirements, undefined, undefined, { + amount: "$0.10", + }); + + // Only amount changes, asset stays the same + expect(mockClient.settleCalls[0].requirements.amount).toBe("100000"); + expect(mockClient.settleCalls[0].requirements.asset).toBe("0xOriginalToken"); + }); + + it("should pass overridden requirements to beforeSettle hooks", async () => { + const mockClient = new MockFacilitatorClient( + buildSupportedResponse(), + buildVerifyResponse({ isValid: true }), + buildSettleResponse({ success: true }), + ); + + const server = new x402ResourceServer(mockClient); + + let hookAmount: string | undefined; + server.onBeforeSettle(async context => { + hookAmount = context.requirements.amount; + }); + + const payload = buildPaymentPayload(); + const requirements = buildPaymentRequirements({ amount: "1000000" }); + + await server.settlePayment(payload, requirements, undefined, undefined, { amount: "300000" }); + + expect(hookAmount).toBe("300000"); + }); }); describe("findMatchingRequirements", () => { @@ -1047,3 +1286,69 @@ describe("x402ResourceServer", () => { }); }); }); + +describe("resolveSettlementOverrideAmount", () => { + const baseRequirements = buildPaymentRequirements({ amount: "2000" }); + + describe("raw atomic units", () => { + it("passes through a plain numeric string unchanged", () => { + expect(resolveSettlementOverrideAmount("1000", baseRequirements)).toBe("1000"); + }); + + it("passes through '0'", () => { + expect(resolveSettlementOverrideAmount("0", baseRequirements)).toBe("0"); + }); + }); + + describe("percent format", () => { + it("resolves '50%' to half of requirements.amount", () => { + expect(resolveSettlementOverrideAmount("50%", baseRequirements)).toBe("1000"); + }); + + it("resolves '100%' to the full requirements.amount", () => { + expect(resolveSettlementOverrideAmount("100%", baseRequirements)).toBe("2000"); + }); + + it("resolves '0%' to 0", () => { + expect(resolveSettlementOverrideAmount("0%", baseRequirements)).toBe("0"); + }); + + it("resolves '25%' correctly", () => { + expect(resolveSettlementOverrideAmount("25%", baseRequirements)).toBe("500"); + }); + + it("resolves '33.33%' and floors to nearest atomic unit", () => { + const reqs = buildPaymentRequirements({ amount: "3000" }); + // 3000 * 3333 / 10000 = 999.9 → floored to 999 + expect(resolveSettlementOverrideAmount("33.33%", reqs)).toBe("999"); + }); + + it("resolves '10.5%' correctly", () => { + const reqs = buildPaymentRequirements({ amount: "1000" }); + // 1000 * 1050 / 10000 = 105 + expect(resolveSettlementOverrideAmount("10.5%", reqs)).toBe("105"); + }); + }); + + describe("dollar price format", () => { + it("converts '$1.00' using default 6 decimals", () => { + expect(resolveSettlementOverrideAmount("$1.00", baseRequirements)).toBe("1000000"); + }); + + it("converts '$0.05' using default 6 decimals", () => { + expect(resolveSettlementOverrideAmount("$0.05", baseRequirements)).toBe("50000"); + }); + + it("converts '$0.05' using 8 decimals when provided", () => { + expect(resolveSettlementOverrideAmount("$0.05", baseRequirements, 8)).toBe("5000000"); + }); + + it("converts '$0.001' using default 6 decimals", () => { + expect(resolveSettlementOverrideAmount("$0.001", baseRequirements)).toBe("1000"); + }); + + it("converts '$0' to '0'", () => { + expect(resolveSettlementOverrideAmount("$0", baseRequirements)).toBe("0"); + }); + }); +}); diff --git a/typescript/packages/extensions/CHANGELOG.md b/typescript/packages/extensions/CHANGELOG.md index cb75350723..085352cfbe 100644 --- a/typescript/packages/extensions/CHANGELOG.md +++ b/typescript/packages/extensions/CHANGELOG.md @@ -1,5 +1,39 @@ # @x402/extensions Changelog +## 2.8.0 + +### Minor Changes + +- 4f2f4f3: Added auth-only route support in createSIWxRequestHook via accepts: [] detection +- 067f297: Added dynamic route support to the Bazaar discovery extension — servers can now declare `[param]` route segments that consolidate to a single catalog entry per route template, with automatic `pathParams` enrichment and `:param`-style `routeTemplate` in discovery output. + +### Patch Changes + +- Updated dependencies [067f297] +- Updated dependencies [4c1e44f] +- Updated dependencies [5135fab] + - @x402/core@2.8.0 + +## 2.7.0 + +### Minor Changes + +- 8b731cb: Replaced `sendRawApprovalAndSettle` with a generic `sendTransactions` signer method that accepts an array of pre-signed serialized transactions or unsigned call intents. The signer owns execution strategy (sequential, batched, or atomic bundling). Closed fail-open verification paths, aligned Permit2 amount check to exact match, and added `signerForNetwork` to the extensions package. +- f2bbb5c: Added offer-receipt extension to enable signed offers and receipts in x402 payment flows + +### Patch Changes + +- 34d2442: Removed dependencie on node’s crypto module +- Updated dependencies [8931cb3] + - @x402/core@2.7.0 + +## 2.6.0 + +### Minor Changes + +- Updated dependencies + - @x402/core@2.6.0 + ## 2.5.0 ### Minor Changes diff --git a/typescript/packages/extensions/README.md b/typescript/packages/extensions/README.md index 91d4c56078..f937322d0a 100644 --- a/typescript/packages/extensions/README.md +++ b/typescript/packages/extensions/README.md @@ -471,7 +471,7 @@ The Sign-In-With-X extension implements [CAIP-122](https://chainagnostic.org/CAI 1. Server returns 402 with `sign-in-with-x` extension containing challenge parameters 2. Client signs the CAIP-122 message with their wallet 3. Client sends signed proof in `SIGN-IN-WITH-X` header -4. Server verifies signature and grants access if wallet has previous payment +4. Server verifies signature and grants access either because the route is auth-only or because the wallet has previously paid ### Server Usage @@ -495,7 +495,7 @@ const resourceServer = new x402ResourceServer(facilitatorClient) .registerExtension(siwxResourceServerExtension) // Refreshes nonce/timestamps per request .onAfterSettle(createSIWxSettleHook({ storage })); // Records payments -// 2. Declare SIWX support in routes (network/domain/uri derived automatically) +// 2. Declare SIWX support in routes const routes = { "GET /data": { accepts: [{scheme: "exact", price: "$0.01", network: "eip155:8453", payTo}], @@ -503,11 +503,19 @@ const routes = { statement: 'Sign in to access your purchased content', }), }, + "GET /profile": { + accepts: [], + extensions: declareSIWxExtension({ + network: ["eip155:8453", "solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1"], + statement: 'Sign in to view your profile', + expirationSeconds: 300, + }), + }, }; // 3. Verify incoming SIWX proofs const httpServer = new x402HTTPResourceServer(resourceServer, routes) - .onProtectedRequest(createSIWxRequestHook({ storage })); // Grants access if paid + .onProtectedRequest(createSIWxRequestHook({ storage })); // Grants access when SIWX auth is sufficient // Optional: Enable smart wallet support (EIP-1271/EIP-6492) import { createPublicClient, http } from 'viem'; @@ -524,7 +532,7 @@ const httpServerWithSmartWallets = new x402HTTPResourceServer(resourceServer, ro The hooks automatically: - **siwxResourceServerExtension**: Derives `network` from `accepts`, `domain`/`uri` from request URL, refreshes `nonce`/`issuedAt`/`expirationTime` per request - **createSIWxSettleHook**: Records payment when settlement succeeds -- **createSIWxRequestHook**: Validates and verifies SIWX proofs, grants access if wallet has paid +- **createSIWxRequestHook**: Validates and verifies SIWX proofs, grants access for auth-only routes or when the wallet has paid #### Manual Usage (Advanced) @@ -534,18 +542,15 @@ import { parseSIWxHeader, validateSIWxMessage, verifySIWxSignature, - SIGN_IN_WITH_X, } from '@x402/extensions/sign-in-with-x'; // 1. Declare in PaymentRequired response -const extensions = { - [SIGN_IN_WITH_X]: declareSIWxExtension({ - domain: 'api.example.com', - resourceUri: 'https://api.example.com/data', - network: 'eip155:8453', - statement: 'Sign in to access your purchased content', - }), -}; +const extensions = declareSIWxExtension({ + domain: 'api.example.com', + resourceUri: 'https://api.example.com/data', + network: 'eip155:8453', + statement: 'Sign in to access your purchased content', +}); // 2. Verify incoming proof async function handleRequest(request: Request) { @@ -571,10 +576,8 @@ async function handleRequest(request: Request) { } // verification.address is the verified wallet - // Check if this wallet has paid before - const hasPaid = await checkPaymentHistory(verification.address); - if (hasPaid) { - // Grant access without payment + if (await isAuthOnlyRoute(request) || await checkPaymentHistory(verification.address)) { + // Grant access } } ``` @@ -612,15 +615,15 @@ import { // 1. Get extension and network from 402 response const paymentRequired = await response.json(); const extension = paymentRequired.extensions['sign-in-with-x']; -const paymentNetwork = paymentRequired.accepts[0].network; // e.g., "eip155:8453" +const paymentNetwork = paymentRequired.accepts[0]?.network; // undefined for auth-only routes // 2. Find matching chain in supportedChains -const matchingChain = extension.supportedChains.find( - chain => chain.chainId === paymentNetwork -); +const matchingChain = paymentNetwork + ? extension.supportedChains.find(chain => chain.chainId === paymentNetwork) + : extension.supportedChains[0]; if (!matchingChain) { - // Payment network not supported for SIWX + // No chain supported by this signer / route combination throw new Error('Chain not supported'); } @@ -664,6 +667,8 @@ declareSIWxExtension({ - `resourceUri` → from request URL - `domain` → parsed from resourceUri +For auth-only routes declared with `accepts: []`, `network` cannot be inferred from payment requirements and should be provided explicitly. + **Multi-chain support:** When `network` is an array (or multiple networks in `accepts`), `supportedChains` will contain one entry per network. #### `parseSIWxHeader(header)` diff --git a/typescript/packages/extensions/package.json b/typescript/packages/extensions/package.json index 993aee45e6..abc827908f 100644 --- a/typescript/packages/extensions/package.json +++ b/typescript/packages/extensions/package.json @@ -1,6 +1,6 @@ { "name": "@x402/extensions", - "version": "2.5.0", + "version": "2.8.0", "main": "./dist/cjs/index.js", "module": "./dist/esm/index.js", "types": "./dist/cjs/index.d.ts", @@ -43,9 +43,11 @@ "vitest": "^3.0.5" }, "dependencies": { + "@noble/curves": "^1.9.0", "@scure/base": "^1.2.6", "@x402/core": "workspace:~", "ajv": "^8.17.1", + "jose": "^5.9.6", "siwe": "^2.3.2", "tweetnacl": "^1.0.3", "viem": "^2.43.5", @@ -82,6 +84,16 @@ "default": "./dist/cjs/sign-in-with-x/index.js" } }, + "./offer-receipt": { + "import": { + "types": "./dist/esm/offer-receipt/index.d.mts", + "default": "./dist/esm/offer-receipt/index.mjs" + }, + "require": { + "types": "./dist/cjs/offer-receipt/index.d.ts", + "default": "./dist/cjs/offer-receipt/index.js" + } + }, "./payment-identifier": { "import": { "types": "./dist/esm/payment-identifier/index.d.mts", diff --git a/typescript/packages/extensions/src/bazaar/facilitator.ts b/typescript/packages/extensions/src/bazaar/facilitator.ts index fe7e443c25..201489a868 100644 --- a/typescript/packages/extensions/src/bazaar/facilitator.ts +++ b/typescript/packages/extensions/src/bazaar/facilitator.ts @@ -16,6 +16,60 @@ import type { DiscoveredMCPResource } from "./mcp/types"; import { BAZAAR } from "./types"; import { extractDiscoveryInfoV1 } from "./v1/facilitator"; +/** + * Valid routeTemplate pattern: must start with "/", contain only safe URL path characters + * and :param identifiers, and not include traversal sequences or scheme markers. + * + * Allowed: /users/:userId, /weather/:country/:city, /api/v1/items + */ +const ROUTE_TEMPLATE_REGEX = /^\/[a-zA-Z0-9_/:.\-~%]+$/; + +/** + * Checks whether a routeTemplate value is structurally valid. + * + * Expected format: "/:param" segments using colon-prefixed identifiers + * (e.g. "/users/:userId", "/weather/:country/:city"). + * + * The facilitator is a trust boundary: clients control the payment payload and + * can modify routeTemplate before submission. A malicious value could cause the + * facilitator to catalog the payment under an arbitrary URL (catalog poisoning). + * This function enforces minimal structural requirements: + * - Must be a non-empty string starting with "/" + * - Must match the safe URL path character set (alphanumeric, _, :, /, ., -, ~, %) + * - Must not contain ".." (path traversal) + * - Must not contain "://" (URL injection) + * + * @param value - The raw routeTemplate string from the client payload + * @returns true if the value is a valid routeTemplate, false otherwise + * + * @internal Exported for facilitator use. + */ +export function isValidRouteTemplate(value: string | undefined): value is string { + if (!value) return false; + if (!ROUTE_TEMPLATE_REGEX.test(value)) return false; + // Decode percent-encoding before traversal checks so that %2e%2e is caught. + let decoded: string; + try { + decoded = decodeURIComponent(value); + } catch { + return false; + } + if (decoded.includes("..")) return false; + if (decoded.includes("://")) return false; + return true; +} + +/** + * Validates a routeTemplate and returns it if valid, undefined otherwise. + * + * @param value - The raw routeTemplate string to validate + * @returns The validated value, or undefined if invalid + * @deprecated Use `isValidRouteTemplate` instead. + */ +export function validateRouteTemplate(value: string | undefined): string | undefined { + return isValidRouteTemplate(value) ? value : undefined; +} + /** * Validation result for discovery extensions */ @@ -124,6 +178,8 @@ export function extractDiscoveryInfo( let discoveryInfo: DiscoveryInfo | null = null; let resourceUrl: string; + let routeTemplate: string | undefined; + if (paymentPayload.x402Version === 2) { resourceUrl = paymentPayload.resource?.url ?? ""; @@ -132,6 +188,15 @@ export function extractDiscoveryInfo( if (bazaarExtension && typeof bazaarExtension === "object") { try { + // routeTemplate uses :param syntax (e.g. "/users/:userId", "/weather/:country/:city"). + // Must start with "/", must not contain ".." or "://". + // Validate before use: the client controls this field in the payment payload. + const rawExt = bazaarExtension as Record; + const rawTemplate = + typeof rawExt.routeTemplate === "string" ? rawExt.routeTemplate : undefined; + if (isValidRouteTemplate(rawTemplate)) { + routeTemplate = rawTemplate; + } const extension = bazaarExtension as DiscoveryExtension; if (validate) { @@ -165,7 +230,10 @@ export function extractDiscoveryInfo( // Strip query params (?) and hash sections (#) for discovery cataloging const url = new URL(resourceUrl); - const normalizedResourceUrl = `${url.origin}${url.pathname}`; + // If a routeTemplate is present (dynamic route), use it as the canonical path + const canonicalUrl = routeTemplate + ? `${url.origin}${routeTemplate}` + : `${url.origin}${url.pathname}`; // Extract description and mimeType from resource info (v2) or requirements (v1) let description: string | undefined; @@ -181,7 +249,7 @@ export function extractDiscoveryInfo( } const base = { - resourceUrl: normalizedResourceUrl, + resourceUrl: canonicalUrl, description, mimeType, x402Version: paymentPayload.x402Version, @@ -189,10 +257,11 @@ export function extractDiscoveryInfo( }; if (discoveryInfo.input.type === "mcp") { + // MCP routes are not parameterized; routeTemplate is not applicable. return { ...base, toolName: (discoveryInfo as McpDiscoveryInfo).input.toolName }; } - return { ...base, method: discoveryInfo.input.method }; + return { ...base, routeTemplate, method: discoveryInfo.input.method }; } /** diff --git a/typescript/packages/extensions/src/bazaar/http/resourceService.ts b/typescript/packages/extensions/src/bazaar/http/resourceService.ts index 85b19fb712..f23d9470ef 100644 --- a/typescript/packages/extensions/src/bazaar/http/resourceService.ts +++ b/typescript/packages/extensions/src/bazaar/http/resourceService.ts @@ -17,12 +17,16 @@ import type { * @param root0.input - Query parameters * @param root0.inputSchema - JSON schema for query parameters * @param root0.output - Output specification with example + * @param root0.pathParams - Concrete path parameter values extracted from the request + * @param root0.pathParamsSchema - JSON schema for path parameters * @returns QueryDiscoveryExtension with info and schema */ export function createQueryDiscoveryExtension({ method, input = {}, inputSchema = { properties: {} }, + pathParams, + pathParamsSchema, output, }: DeclareQueryDiscoveryExtensionConfig): QueryDiscoveryExtension { return { @@ -31,6 +35,7 @@ export function createQueryDiscoveryExtension({ type: "http" as const, ...(method ? { method } : {}), ...(input ? { queryParams: input } : {}), + ...(pathParams ? { pathParams } : {}), }, ...(output?.example ? { @@ -64,8 +69,19 @@ export function createQueryDiscoveryExtension({ }, } : {}), + ...(pathParamsSchema + ? { + pathParams: { + type: "object" as const, + ...(typeof pathParamsSchema === "object" ? pathParamsSchema : {}), + }, + } + : {}), }, - required: ["type"] as ("type" | "method")[], + required: ["type", "method"] as ("type" | "method")[], + // pathParams are not declared here at schema build time -- + // the server extension's enrichDeclaration adds them to both info and schema + // atomically at request time, keeping data and schema consistent. additionalProperties: false, }, ...(output?.example @@ -100,12 +116,16 @@ export function createQueryDiscoveryExtension({ * @param root0.inputSchema - JSON schema for request body * @param root0.bodyType - Content type of body (json, form-data, text) * @param root0.output - Output specification with example + * @param root0.pathParams - Concrete path parameter values extracted from the request + * @param root0.pathParamsSchema - JSON schema for path parameters * @returns BodyDiscoveryExtension with info and schema */ export function createBodyDiscoveryExtension({ method, input = {}, inputSchema = { properties: {} }, + pathParams, + pathParamsSchema, bodyType, output, }: DeclareBodyDiscoveryExtensionConfig): BodyDiscoveryExtension { @@ -116,6 +136,7 @@ export function createBodyDiscoveryExtension({ ...(method ? { method } : {}), bodyType, body: input, + ...(pathParams ? { pathParams } : {}), }, ...(output?.example ? { @@ -146,8 +167,24 @@ export function createBodyDiscoveryExtension({ enum: ["json", "form-data", "text"], }, body: inputSchema, + ...(pathParamsSchema + ? { + pathParams: { + type: "object" as const, + ...(typeof pathParamsSchema === "object" ? pathParamsSchema : {}), + }, + } + : {}), }, - required: ["type", "bodyType", "body"] as ("type" | "method" | "bodyType" | "body")[], + required: ["type", "method", "bodyType", "body"] as ( + | "type" + | "method" + | "bodyType" + | "body" + )[], + // pathParams are not declared here at schema build time -- + // the server extension's enrichDeclaration adds them to both info and schema + // atomically at request time, keeping data and schema consistent. additionalProperties: false, }, ...(output?.example diff --git a/typescript/packages/extensions/src/bazaar/http/types.ts b/typescript/packages/extensions/src/bazaar/http/types.ts index 07de9b6871..be47d0d6b0 100644 --- a/typescript/packages/extensions/src/bazaar/http/types.ts +++ b/typescript/packages/extensions/src/bazaar/http/types.ts @@ -5,6 +5,13 @@ import type { BodyMethods, QueryParamMethods } from "@x402/core/http"; import type { DiscoveryInfo } from "../types"; +/** Shared schema definition for an object-typed parameter map (queryParams, pathParams, etc.) */ +interface ParamMapSchemaProperty { + type: "object"; + properties?: Record; + additionalProperties?: boolean; +} + /** * Discovery info for query parameter methods (GET, HEAD, DELETE) */ @@ -14,6 +21,7 @@ export interface QueryDiscoveryInfo { /** Absent at declaration time; set by bazaarResourceServerExtension.enrichDeclaration */ method?: QueryParamMethods; queryParams?: Record; + pathParams?: Record; headers?: Record; }; output?: { @@ -34,6 +42,7 @@ export interface BodyDiscoveryInfo { bodyType: "json" | "form-data" | "text"; body: Record; queryParams?: Record; + pathParams?: Record; headers?: Record; }; output?: { @@ -48,6 +57,7 @@ export interface BodyDiscoveryInfo { */ export interface QueryDiscoveryExtension { info: QueryDiscoveryInfo; + routeTemplate?: string; schema: { $schema: "https://json-schema.org/draft/2020-12/schema"; @@ -64,12 +74,8 @@ export interface QueryDiscoveryExtension { type: "string"; enum: QueryParamMethods[]; }; - queryParams?: { - type: "object"; - properties?: Record; - required?: string[]; - additionalProperties?: boolean; - }; + queryParams?: ParamMapSchemaProperty & { required?: string[] }; + pathParams?: ParamMapSchemaProperty; headers?: { type: "object"; additionalProperties: { @@ -96,6 +102,7 @@ export interface QueryDiscoveryExtension { */ export interface BodyDiscoveryExtension { info: BodyDiscoveryInfo; + routeTemplate?: string; schema: { $schema: "https://json-schema.org/draft/2020-12/schema"; @@ -117,12 +124,8 @@ export interface BodyDiscoveryExtension { enum: ["json", "form-data", "text"]; }; body: Record; - queryParams?: { - type: "object"; - properties?: Record; - required?: string[]; - additionalProperties?: boolean; - }; + queryParams?: ParamMapSchemaProperty & { required?: string[] }; + pathParams?: ParamMapSchemaProperty; headers?: { type: "object"; additionalProperties: { @@ -148,6 +151,8 @@ export interface DeclareQueryDiscoveryExtensionConfig { method?: QueryParamMethods; input?: Record; inputSchema?: Record; + pathParams?: Record; + pathParamsSchema?: Record; output?: { example?: unknown; schema?: Record; @@ -158,6 +163,8 @@ export interface DeclareBodyDiscoveryExtensionConfig { method?: BodyMethods; input?: Record; inputSchema?: Record; + pathParams?: Record; + pathParamsSchema?: Record; bodyType: "json" | "form-data" | "text"; output?: { example?: unknown; @@ -171,6 +178,7 @@ export interface DiscoveredHTTPResource { mimeType?: string; /** Present after server extension enrichment; may be absent for pre-enrichment data */ method?: string; + routeTemplate?: string; x402Version: number; discoveryInfo: DiscoveryInfo; } diff --git a/typescript/packages/extensions/src/bazaar/index.ts b/typescript/packages/extensions/src/bazaar/index.ts index f0e88ed261..235f713bfd 100644 --- a/typescript/packages/extensions/src/bazaar/index.ts +++ b/typescript/packages/extensions/src/bazaar/index.ts @@ -112,6 +112,8 @@ export { bazaarResourceServerExtension } from "./server"; // Export facilitator functions (for facilitators) export { validateDiscoveryExtension, + isValidRouteTemplate, + validateRouteTemplate, extractDiscoveryInfo, extractDiscoveryInfoFromExtension, validateAndExtract, diff --git a/typescript/packages/extensions/src/bazaar/server.ts b/typescript/packages/extensions/src/bazaar/server.ts index 6047cf7f57..b7a42a7f0e 100644 --- a/typescript/packages/extensions/src/bazaar/server.ts +++ b/typescript/packages/extensions/src/bazaar/server.ts @@ -2,6 +2,16 @@ import type { ResourceServerExtension } from "@x402/core/types"; import type { HTTPRequestContext } from "@x402/core/http"; import { BAZAAR } from "./types"; +// Non-global: safe for test/split (no stateful lastIndex side-effects). +const BRACKET_PARAM_REGEX = /\[([^\]]+)\]/; +// Global variant required for String.replace to substitute ALL occurrences. +// JS String.replace with a non-global regex replaces only the first match. +// (String.replaceAll with a non-global regex would work in ES2021+, but the +// target lib is ES2020 — keep this separate constant to avoid that constraint.) +const BRACKET_PARAM_REGEX_ALL = /\[([^\]]+)\]/g; + +const COLON_PARAM_REGEX = /:([a-zA-Z_][a-zA-Z0-9_]*)/; + /** * Type guard to check if context is an HTTP request context. * @@ -12,6 +22,102 @@ function isHTTPRequestContext(ctx: unknown): ctx is HTTPRequestContext { return ctx !== null && typeof ctx === "object" && "method" in ctx && "adapter" in ctx; } +/** + * Converts wildcard segments in a route pattern to named :varN parameters + * so they can be treated as dynamic routes for discovery catalog normalization. + * + * @param pattern - Route pattern that may contain wildcard segments + * @returns The pattern with wildcard segments replaced by :var1, :var2, etc. + */ +function normalizeWildcardPattern(pattern: string): string { + if (!pattern.includes("*")) { + return pattern; + } + let counter = 0; + return pattern + .split("/") + .map(seg => { + if (seg === "*") { + counter++; + return `:var${counter}`; + } + return seg; + }) + .join("/"); +} + +/** + * Converts a parameterized route pattern into a :param template and extracts concrete + * param values from the URL path in a single call. + * + * Supports both [param] (Next.js) and :param (Express) syntax. The output routeTemplate + * always uses :param syntax regardless of input format. + * + * @param routePattern - Route pattern (e.g. "/users/[userId]" or "/users/:userId") + * @param urlPath - Concrete URL path (e.g. "/users/123") + * @returns Object with routeTemplate and pathParams, or null if no params detected + */ +function extractDynamicRouteInfo( + routePattern: string, + urlPath: string, +): { routeTemplate: string; pathParams: Record } | null { + const hasBracket = BRACKET_PARAM_REGEX.test(routePattern); + const hasColon = COLON_PARAM_REGEX.test(routePattern); + if (!hasBracket && !hasColon) { + return null; + } + // When both [param] and :param are present, normalize brackets to colons first + // so all params are extracted uniformly. + const normalizedPattern = hasBracket + ? routePattern.replace(BRACKET_PARAM_REGEX_ALL, ":$1") + : routePattern; + const pathParams = extractPathParams(normalizedPattern, urlPath, false); + return { routeTemplate: normalizedPattern, pathParams }; +} + +/** + * Extracts concrete path parameter values by matching a URL path against a route pattern. + * + * @param routePattern - Route pattern with [paramName] or :paramName segments + * @param urlPath - Concrete URL path (e.g. "/users/123") + * @param isBracket - True if pattern uses [param] syntax, false for :param + * @returns Record mapping param names to their values + */ +function extractPathParams( + routePattern: string, + urlPath: string, + isBracket: boolean, +): Record { + const paramNames: string[] = []; + const splitRegex = isBracket ? BRACKET_PARAM_REGEX : COLON_PARAM_REGEX; + // Split on param markers so literal segments can be regex-escaped independently. + // Without escaping, a route like /api/v1.0/[id] would produce a regex where '.' matches + // any character (e.g. /api/v1X0/123 would incorrectly match). + const parts = routePattern.split(splitRegex); + const regexParts: string[] = []; + parts.forEach((part, i) => { + if (i % 2 === 0) { + // Literal segment – escape all regex metacharacters + regexParts.push(part.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")); + } else { + // Param name + paramNames.push(part); + regexParts.push("([^/]+)"); + } + }); + + const regex = new RegExp(`^${regexParts.join("")}$`); + const match = urlPath.match(regex); + + if (!match) return {}; + + const result: Record = {}; + paramNames.forEach((name, idx) => { + result[name] = match[idx + 1]; + }); + return result; +} + interface ExtensionDeclaration { [key: string]: unknown; info?: { @@ -63,7 +169,7 @@ export const bazaarResourceServerExtension: ResourceServerExtension = { }, }; - return { + const enrichedResult = { ...extension, info: { ...(extension.info || {}), @@ -89,5 +195,45 @@ export const bazaarResourceServerExtension: ResourceServerExtension = { }, }, }; + + // Dynamic routes: translate [param]/:param → :param for the routeTemplate catalog key; + // pathParams carries runtime values (distinct from pathParamsSchema in the declaration). + // Wildcard * segments are auto-converted to :var1, :var2, etc. for catalog normalization. + const rawRoutePattern = (transportContext as HTTPRequestContext).routePattern; + const routePattern = rawRoutePattern ? normalizeWildcardPattern(rawRoutePattern) : undefined; + const dynamicRoute = routePattern + ? extractDynamicRouteInfo(routePattern, transportContext.adapter.getPath()) + : null; + if (dynamicRoute) { + const inputSchemaProps = enrichedResult.schema?.properties?.input?.properties || {}; + const hasPathParamsInSchema = "pathParams" in inputSchemaProps; + return { + ...enrichedResult, + routeTemplate: dynamicRoute.routeTemplate, + info: { + ...enrichedResult.info, + input: { ...enrichedResult.info.input, pathParams: dynamicRoute.pathParams }, + }, + ...(!hasPathParamsInSchema + ? { + schema: { + ...enrichedResult.schema, + properties: { + ...enrichedResult.schema?.properties, + input: { + ...enrichedResult.schema?.properties?.input, + properties: { + ...inputSchemaProps, + pathParams: { type: "object" }, + }, + }, + }, + }, + } + : {}), + }; + } + + return enrichedResult; }, }; diff --git a/typescript/packages/extensions/src/erc20-approval-gas-sponsoring/index.ts b/typescript/packages/extensions/src/erc20-approval-gas-sponsoring/index.ts index 3b73c68ab3..d12339b05d 100644 --- a/typescript/packages/extensions/src/erc20-approval-gas-sponsoring/index.ts +++ b/typescript/packages/extensions/src/erc20-approval-gas-sponsoring/index.ts @@ -39,6 +39,7 @@ // Export types export type { + TransactionRequest, Erc20ApprovalGasSponsoringInfo, Erc20ApprovalGasSponsoringServerInfo, Erc20ApprovalGasSponsoringExtension, diff --git a/typescript/packages/extensions/src/erc20-approval-gas-sponsoring/types.ts b/typescript/packages/extensions/src/erc20-approval-gas-sponsoring/types.ts index 69a13e651c..5876aa725a 100644 --- a/typescript/packages/extensions/src/erc20-approval-gas-sponsoring/types.ts +++ b/typescript/packages/extensions/src/erc20-approval-gas-sponsoring/types.ts @@ -9,13 +9,22 @@ import type { FacilitatorExtension } from "@x402/core/types"; +/** + * A single transaction to be executed by the signer. + * - `0x${string}`: a pre-signed serialized transaction (broadcast as-is via sendRawTransaction) + * - `{ to, data, gas? }`: an unsigned call intent (signer signs and broadcasts) + */ +export type TransactionRequest = + | `0x${string}` + | { to: `0x${string}`; data: `0x${string}`; gas?: bigint }; + /** * Signer capability carried by the ERC-20 approval extension when registered in a facilitator. * - * Mirrors FacilitatorEvmSigner (from @x402/evm) plus `sendRawTransaction`. - * The extension signer owns the full approve+settle flow: it broadcasts the - * pre-signed approval transaction AND executes the Permit2 settle call, enabling - * production implementations to bundle both atomically (e.g., Flashbots, multicall). + * Mirrors FacilitatorEvmSigner (from @x402/evm) plus `sendTransactions`. + * The signer owns execution of multiple transactions, enabling production implementations + * to bundle them atomically (e.g., Flashbots, multicall, smart account batching) + * while simpler implementations can execute them sequentially. * * The method signatures are duplicated here (rather than extending FacilitatorEvmSigner) * to avoid a circular dependency between @x402/extensions and @x402/evm. @@ -41,11 +50,12 @@ export interface Erc20ApprovalGasSponsoringSigner { abi: readonly unknown[]; functionName: string; args: readonly unknown[]; + gas?: bigint; }): Promise<`0x${string}`>; sendTransaction(args: { to: `0x${string}`; data: `0x${string}` }): Promise<`0x${string}`>; waitForTransactionReceipt(args: { hash: `0x${string}` }): Promise<{ status: string }>; getCode(args: { address: `0x${string}` }): Promise<`0x${string}` | undefined>; - sendRawTransaction(args: { serializedTransaction: `0x${string}` }): Promise<`0x${string}`>; + sendTransactions(transactions: TransactionRequest[]): Promise<`0x${string}`[]>; } /** @@ -61,54 +71,48 @@ export const ERC20_APPROVAL_GAS_SPONSORING_VERSION = "1"; /** * Extended extension object registered in a facilitator via registerExtension(). * Carries the signer that owns the full approve+settle flow for ERC-20 tokens - * that lack EIP-2612. The signer must have all FacilitatorEvmSigner capabilities - * plus `sendRawTransaction` for broadcasting the pre-signed approval tx. + * that lack EIP-2612. * * @example * ```typescript * import { createErc20ApprovalGasSponsoringExtension } from '@x402/extensions'; * * facilitator.registerExtension( - * createErc20ApprovalGasSponsoringExtension(evmSigner, viemClient), + * createErc20ApprovalGasSponsoringExtension(signer), * ); * ``` */ export interface Erc20ApprovalGasSponsoringFacilitatorExtension extends FacilitatorExtension { key: "erc20ApprovalGasSponsoring"; - /** Signer with broadcast + settle capability. Optional — settlement fails gracefully if absent. */ + /** Default signer with approve+settle capability. Optional — settlement fails gracefully if absent. */ signer?: Erc20ApprovalGasSponsoringSigner; + /** Network-specific signer resolver. Takes precedence over `signer` when provided. */ + signerForNetwork?: (network: string) => Erc20ApprovalGasSponsoringSigner | undefined; } /** - * Signer input for {@link createErc20ApprovalGasSponsoringExtension}. + * Base signer shape without `sendTransactions`. * Matches the FacilitatorEvmSigner shape from @x402/evm (duplicated to avoid circular dep). */ export type Erc20ApprovalGasSponsoringBaseSigner = Omit< Erc20ApprovalGasSponsoringSigner, - "sendRawTransaction" + "sendTransactions" >; /** * Create an ERC-20 approval gas sponsoring extension ready to register in a facilitator. * - * @param signer - The EVM facilitator signer (e.g. from `toFacilitatorEvmSigner()`) - * @param client - Object providing `sendRawTransaction` (e.g. a viem WalletClient) - * @param client.sendRawTransaction - Broadcasts a signed transaction to the network + * @param signer - A complete signer with `sendTransactions` already implemented. + * The signer decides how to execute the transactions (sequentially, batched, or atomically). + * @param signerForNetwork - Optional network-specific signer resolver. When provided, + * takes precedence over `signer` and allows different settlement signers per network. * @returns A fully configured extension to pass to `facilitator.registerExtension()` */ export function createErc20ApprovalGasSponsoringExtension( - signer: Erc20ApprovalGasSponsoringBaseSigner, - client: { - sendRawTransaction: (args: { serializedTransaction: `0x${string}` }) => Promise<`0x${string}`>; - }, + signer: Erc20ApprovalGasSponsoringSigner, + signerForNetwork?: (network: string) => Erc20ApprovalGasSponsoringSigner | undefined, ): Erc20ApprovalGasSponsoringFacilitatorExtension { - return { - ...ERC20_APPROVAL_GAS_SPONSORING, - signer: { - ...signer, - sendRawTransaction: client.sendRawTransaction.bind(client), - }, - }; + return { ...ERC20_APPROVAL_GAS_SPONSORING, signer, signerForNetwork }; } /** diff --git a/typescript/packages/extensions/src/index.ts b/typescript/packages/extensions/src/index.ts index dbb00e5359..a8e18bfd60 100644 --- a/typescript/packages/extensions/src/index.ts +++ b/typescript/packages/extensions/src/index.ts @@ -8,6 +8,9 @@ export { bazaarResourceServerExtension } from "./bazaar/server"; // Sign-in-with-x extension export * from "./sign-in-with-x"; +// Offer/Receipt extension +export * from "./offer-receipt"; + // Payment-identifier extension export * from "./payment-identifier"; export { paymentIdentifierResourceServerExtension } from "./payment-identifier/resourceServer"; diff --git a/typescript/packages/extensions/src/offer-receipt/README.md b/typescript/packages/extensions/src/offer-receipt/README.md new file mode 100644 index 0000000000..d1e785ac63 --- /dev/null +++ b/typescript/packages/extensions/src/offer-receipt/README.md @@ -0,0 +1,218 @@ +# x402 Offer/Receipt Extension + +Enables signed offers and receipts for the x402 payment protocol (v1.0). + +## Overview + +``` +┌─────────┐ ┌─────────────────┐ ┌─────────────┐ +│ Client │ │ Resource Server │ │ Facilitator │ +└────┬────┘ └────────┬────────┘ └──────┬──────┘ + │ │ │ + │ GET /resource │ │ + │ ──────────────────────────────────►│ │ + │ │ │ + │ 402 + PaymentRequirements │ │ + │ + SignedOffer(s) │ │ + │ ◄──────────────────────────────────│ │ + │ │ │ + │ GET /resource + Payment Header │ │ + │ ──────────────────────────────────►│ │ + │ │ │ + │ │ Verify + Settle │ + │ │ ────────────────────────────────────►│ + │ │ │ + │ │ Settlement Response │ + │ │ ◄────────────────────────────────────│ + │ │ │ + │ 200 + Resource + SignedReceipt │ │ + │ ◄──────────────────────────────────│ │ + │ │ │ +``` + +The **Offer** is signed by the resource server and included in the 402 response. Each `accepts[]` entry has a corresponding signed offer, proving those specific payment requirements are authentic. + +The **Receipt** is signed by the resource server after successful payment and included in the success response. It proves service was delivered. + +## Why Receipts? + +Receipts are **portable proofs of paid service**. They enable: + +- **Verified user reviews**: Like a "Verified Purchase" badge +- **Audit trails**: Cryptographic proof of service delivery +- **Dispute resolution**: Evidence that service was delivered after payment +- **Agent memory**: AI agents can prove past interactions with services + +## Why Offers? + +Signed offers: +- Give clients a fallback for proof of interaction if a signed receipt is not sent +- Prove the offer came from the resource server +- Prevent clients from creating their own offer and claiming it came from a server + +## Installation + +```bash +npm install @x402/extensions +``` + +## Server Usage + +To enable offer/receipt signing on your resource server: + +```typescript +import { x402ResourceServer } from "@x402/core/server"; +import { + createOfferReceiptExtension, + createJWSOfferReceiptIssuer, + declareOfferReceipt, +} from "@x402/extensions/offer-receipt"; + +// Create an issuer (JWS or EIP-712) +const issuer = createJWSOfferReceiptIssuer("did:web:api.example.com#key-1", jwsSigner); + +// Register the extension +const server = new x402ResourceServer(facilitator) + .registerExtension(createOfferReceiptExtension(issuer)); + +// Declare in route config +const routes = { + "GET /api/data": { + accepts: { payTo, scheme: "exact", price: "$0.01", network: "eip155:8453" }, + extensions: { + ...declareOfferReceipt({ includeTxHash: false }) + } + } +}; +``` + +### Signature Formats + +Two formats are supported: + +- **JWS** - Best for server-side signing with managed keys (HSM, KMS, etc.) +- **EIP-712** - Best for wallet-based signing (MetaMask, WalletConnect, etc.) + +## Client Usage + +### Using wrapFetchWithPayment + +The `wrapFetchWithPayment` wrapper can be used with offers and receipts by capturing offers in the `onPaymentRequired` hook and extracting the receipt from the response. Note that this approach does not control which `accepts[]` entry is selected - the client's selector/policies determine that independently. + +```typescript +import { wrapFetchWithPayment, x402Client, x402HTTPClient } from "@x402/fetch"; +import { registerExactEvmScheme } from "@x402/evm/exact/client"; +import { registerExactSvmScheme } from "@x402/svm/exact/client"; +import { privateKeyToAccount } from "viem/accounts"; +import { createKeyPairSignerFromBytes } from "@solana/kit"; +import { base58 } from "@scure/base"; +import { + extractOffersFromPaymentRequired, + decodeSignedOffers, + extractReceiptFromResponse, + type DecodedOffer, +} from "@x402/extensions/offer-receipt"; + +// Set up signers +const evmSigner = privateKeyToAccount(evmPrivateKey); +const svmSigner = await createKeyPairSignerFromBytes(base58.decode(svmPrivateKey)); + +// Configure x402 client +const client = new x402Client(); +registerExactEvmScheme(client, { signer: evmSigner }); +registerExactSvmScheme(client, { signer: svmSigner }); + +const httpClient = new x402HTTPClient(client); + +// Store offers for later matching with receipt +let capturedOffers: DecodedOffer[] = []; + +// Capture offers in onPaymentRequired hook +httpClient.onPaymentRequired(async ({ paymentRequired }) => { + const offers = extractOffersFromPaymentRequired(paymentRequired); + capturedOffers = decodeSignedOffers(offers); +}); + +// Create payment-enabled fetch +const fetchWithPay = wrapFetchWithPayment(fetch, httpClient); + +// Make request (payment handled automatically) +const response = await fetchWithPay(url); + +// Extract receipt from response headers +const receipt = extractReceiptFromResponse(response); + +// Match receipt to captured offer using receipt payload fields +// (receipt contains network, amount, etc. to identify which offer was accepted) +``` + +### Raw Flow + +For full control over offer selection, use the raw flow. See the [Offer/Receipt Example](../../../../../examples/typescript/clients/offer-receipt/) for a complete working implementation. + +The example demonstrates: +1. Making a request and receiving a 402 with signed offers +2. Extracting and decoding offers to inspect payment options +3. Selecting an offer and finding the matching `accepts[]` entry +4. Making payment and receiving a signed receipt +5. Verifying the receipt payload + +### Future: wrapFetchWithPaymentExtended + +We may add a `wrapFetchWithPaymentExtended` wrapper that selects payment options based on signed offers rather than the `accepts[]` array directly. This would guarantee that the selected payment option has a corresponding signed offer, which is the correct approach when attestation proofs are important. + +## Using Receipts as Proofs + +Signed receipts serve as cryptographic proofs of commercial transactions. These proofs can be submitted to downstream trust and reputation platforms: + +- **[OMATrust](https://github.com/oma3dao/omatrust-docs)** - Decentralized reputation system for verified user reviews and service attestations +- **[PEAC Protocol](https://github.com/peacprotocol/peac)** - Payment Evidence and Attestation Chain for commercial transaction proofs + +Integration libraries for these platforms will be added in future releases. + +## Payload Structure + +For detailed payload field definitions, see the [Extension Specification](../../../../../specs/extensions/extension-offer-and-receipt.md): +- §4.2 Offer Payload Fields +- §5.2 Receipt Payload Fields + +## Security Considerations + +The `extractPayload()` functions extract payloads without verifying the signature or checking signer authorization. This is by design — signer authorization requires resolving key bindings (did:web documents, attestations, etc.) which varies by deployment and is outside the scope of x402 client utilities. + +For production use, downstream trust systems verify: +1. The signature is valid (EIP-712 or JWS) +2. The signing key is authorized for the resource domain + +### Key-to-Domain Binding + +To establish trust, bind the signing key's DID to the resource domain using: + +1. **`did:web` DID Document** - Serve at `https://example.com/.well-known/did.json` +2. **DNS TXT Record** - Add a TXT record binding a DID to the domain +3. **Key Binding Attestation** - Create an attestation specifying the key's purpose and authorized domain + +### Key Management + +For production deployments: + +- **JWS signing**: Use HSM or KMS-backed keys. The `kid` in the JWS header should be a DID URL that resolves to the public key. +- **EIP-712 signing**: The signing wallet should be the `payTo` address, or have an on-chain/off-chain authorization linking it to the service. +- **Key rotation**: Update DID documents or attestations when rotating keys. Old receipts remain valid if the key was authorized at issuance time. + +## Files + +| File | Description | +|------|-------------| +| [types.ts](./types.ts) | Type definitions for offers, receipts, and signers | +| [signing.ts](./signing.ts) | Signing utilities and offer/receipt creation | +| [server.ts](./server.ts) | Server extension and signer factories | +| [client.ts](./client.ts) | Client-side extraction utilities | + +## Examples + +- [Offer/Receipt Client Example](../../../../../examples/typescript/clients/offer-receipt/) - Complete example showing offer/receipt extraction + +## Related + +- [Extension Specification](../../../../../specs/extensions/extension-offer-and-receipt.md) diff --git a/typescript/packages/extensions/src/offer-receipt/client.ts b/typescript/packages/extensions/src/offer-receipt/client.ts new file mode 100644 index 0000000000..0ad22adf5f --- /dev/null +++ b/typescript/packages/extensions/src/offer-receipt/client.ts @@ -0,0 +1,193 @@ +/** + * Client-side utilities for extracting offers and receipts from x402 responses + * + * Provides utilities for clients who want to access signed offers and receipts + * from x402 payment flows. Useful for verified reviews, audit trails, and dispute resolution. + * + * @see README.md for usage examples (raw and wrapper flows) + * @see examples/typescript/clients/offer-receipt/ for complete example + */ + +import { decodePaymentResponseHeader } from "@x402/core/http"; +import type { PaymentRequired, PaymentRequirements, SettleResponse } from "@x402/core/types"; +import { OFFER_RECEIPT, type OfferPayload, type SignedOffer, type SignedReceipt } from "./types"; +import { extractOfferPayload, extractReceiptPayload } from "./signing"; + +/** + * A signed offer with its decoded payload fields at the top level. + * Combines the signed offer metadata with the decoded payload for easy access. + */ +export interface DecodedOffer extends OfferPayload { + /** The original signed offer (for passing to other functions or downstream systems) */ + signedOffer: SignedOffer; + /** The signature format used */ + format: "jws" | "eip712"; + /** Index into accepts[] array (hint for matching), may be undefined */ + acceptIndex?: number; +} + +/** + * Structure of offer-receipt extension data in PaymentRequired.extensions + */ +interface OfferReceiptExtensionInfo { + info?: { + offers?: SignedOffer[]; + receipt?: SignedReceipt; + }; +} + +// ============================================================================ +// Exported Functions +// ============================================================================ + +/** + * Verify that a receipt's payload matches the offer and payer. + * + * This performs basic payload field verification: + * - resourceUrl matches the offer + * - network matches the offer + * - payer matches one of the client's wallet addresses + * - issuedAt is recent (within maxAgeSeconds) + * + * NOTE: This does NOT verify the signature or key binding. See the comment + * in the offer-receipt example for guidance on full verification. + * + * @param receipt - The signed receipt from the server + * @param offer - The decoded offer that was accepted + * @param payerAddresses - Array of the client's wallet addresses (EVM, SVM, etc.) + * @param maxAgeSeconds - Maximum age of receipt in seconds (default: 3600 = 1 hour) + * @returns true if all checks pass, false otherwise + */ +export function verifyReceiptMatchesOffer( + receipt: SignedReceipt, + offer: DecodedOffer, + payerAddresses: string[], + maxAgeSeconds: number = 3600, +): boolean { + const payload = extractReceiptPayload(receipt); + + const resourceUrlMatch = payload.resourceUrl === offer.resourceUrl; + const networkMatch = payload.network === offer.network; + const payerMatch = payerAddresses.some( + addr => payload.payer.toLowerCase() === addr.toLowerCase(), + ); + const issuedRecently = Math.floor(Date.now() / 1000) - payload.issuedAt < maxAgeSeconds; + + return resourceUrlMatch && networkMatch && payerMatch && issuedRecently; +} + +/** + * Extract signed offers from a PaymentRequired response. + * + * Call this immediately after receiving a 402 response to save the offers. + * If the settlement response doesn't include a receipt, you'll still have + * the offers for attestation purposes. + * + * @param paymentRequired - The PaymentRequired object from the 402 response + * @returns Array of signed offers, or empty array if none present + */ +export function extractOffersFromPaymentRequired(paymentRequired: PaymentRequired): SignedOffer[] { + const extData = paymentRequired.extensions?.[OFFER_RECEIPT] as + | OfferReceiptExtensionInfo + | undefined; + return extData?.info?.offers ?? []; +} + +/** + * Decode all signed offers and return them with payload fields at the top level. + * + * Use this to inspect offer details (network, amount, etc.) for selection. + * JWS decoding is cheap (base64 decode, no crypto), so decoding all offers + * upfront is fine even with multiple offers. + * + * @param offers - Array of signed offers from extractOffersFromPaymentRequired + * @returns Array of decoded offers with payload fields at top level + */ +export function decodeSignedOffers(offers: SignedOffer[]): DecodedOffer[] { + return offers.map(offer => { + const payload = extractOfferPayload(offer); + return { + // Spread payload fields at top level + ...payload, + // Include metadata + signedOffer: offer, + format: offer.format, + acceptIndex: offer.acceptIndex, + }; + }); +} + +/** + * Find the accepts[] entry that matches a signed or decoded offer. + * + * Use this after selecting an offer to get the PaymentRequirements + * object needed for createPaymentPayload. + * + * Uses the offer's acceptIndex as a hint for faster lookup, but verifies + * the payload matches in case indices got out of sync. + * + * @param offer - A DecodedOffer (from decodeSignedOffers) or SignedOffer + * @param accepts - Array of payment requirements from paymentRequired.accepts + * @returns The matching PaymentRequirements, or undefined if not found + */ +export function findAcceptsObjectFromSignedOffer( + offer: DecodedOffer | SignedOffer, + accepts: PaymentRequirements[], +): PaymentRequirements | undefined { + // Check if it's a DecodedOffer (has signedOffer property) or SignedOffer + const isDecoded = "signedOffer" in offer; + const payload = isDecoded ? offer : extractOfferPayload(offer); + const acceptIndex = isDecoded ? offer.acceptIndex : offer.acceptIndex; + + // Use acceptIndex as a hint - check that index first + if (acceptIndex !== undefined && acceptIndex < accepts.length) { + const hinted = accepts[acceptIndex]; + if ( + hinted.network === payload.network && + hinted.scheme === payload.scheme && + hinted.asset === payload.asset && + hinted.payTo === payload.payTo && + hinted.amount === payload.amount + ) { + return hinted; + } + } + + // Fall back to searching all accepts + return accepts.find( + req => + req.network === payload.network && + req.scheme === payload.scheme && + req.asset === payload.asset && + req.payTo === payload.payTo && + req.amount === payload.amount, + ); +} + +/** + * Extract signed receipt from a successful payment response. + * + * Call this after a successful payment to get the server's signed receipt. + * The receipt proves the service was delivered after payment. + * + * @param response - The Response object from the successful request + * @returns The signed receipt, or undefined if not present + */ +export function extractReceiptFromResponse(response: Response): SignedReceipt | undefined { + const paymentResponseHeader = + response.headers.get("PAYMENT-RESPONSE") || response.headers.get("X-PAYMENT-RESPONSE"); + + if (!paymentResponseHeader) { + return undefined; + } + + try { + const settlementResponse = decodePaymentResponseHeader(paymentResponseHeader) as SettleResponse; + const receiptExtData = settlementResponse.extensions?.[OFFER_RECEIPT] as + | OfferReceiptExtensionInfo + | undefined; + return receiptExtData?.info?.receipt; + } catch { + return undefined; + } +} diff --git a/typescript/packages/extensions/src/offer-receipt/did.ts b/typescript/packages/extensions/src/offer-receipt/did.ts new file mode 100644 index 0000000000..3984417dca --- /dev/null +++ b/typescript/packages/extensions/src/offer-receipt/did.ts @@ -0,0 +1,252 @@ +/** + * DID Resolution Utilities + * + * Extracts public keys from DID key identifiers. Supports did:key, did:jwk, did:web. + * Uses @noble/curves and @scure/base for cryptographic operations. + */ + +import * as jose from "jose"; +import { base58 } from "@scure/base"; +import { secp256k1 } from "@noble/curves/secp256k1"; +import { p256 } from "@noble/curves/nist"; + +// Multicodec prefixes for supported key types +const MULTICODEC_ED25519_PUB = 0xed; +const MULTICODEC_SECP256K1_PUB = 0xe7; +const MULTICODEC_P256_PUB = 0x1200; + +/** + * Extract a public key from a DID key identifier (kid). + * Supports did:key, did:jwk, did:web. + * + * @param kid - The key identifier (DID URL, e.g., did:key:z6Mk..., did:web:example.com#key-1) + * @returns The extracted public key + */ +export async function extractPublicKeyFromKid(kid: string): Promise { + const [didPart, fragment] = kid.split("#"); + const parts = didPart.split(":"); + + if (parts.length < 3 || parts[0] !== "did") { + throw new Error(`Invalid DID format: ${kid}`); + } + + const method = parts[1]; + const identifier = parts.slice(2).join(":"); + + switch (method) { + case "key": + return extractKeyFromDidKey(identifier); + case "jwk": + return extractKeyFromDidJwk(identifier); + case "web": + return resolveDidWeb(identifier, fragment); + default: + throw new Error( + `Unsupported DID method "${method}". Supported: did:key, did:jwk, did:web. ` + + `Provide the public key directly for other methods.`, + ); + } +} + +/** + * Extract public key from did:key identifier (multibase-encoded) + * + * @param identifier - The did:key identifier (without the "did:key:" prefix) + * @returns The extracted public key + */ +async function extractKeyFromDidKey(identifier: string): Promise { + if (!identifier.startsWith("z")) { + throw new Error(`Unsupported multibase encoding. Expected 'z' (base58-btc).`); + } + + const decoded = base58.decode(identifier.slice(1)); + const { codec, keyBytes } = readMulticodec(decoded); + + switch (codec) { + case MULTICODEC_ED25519_PUB: + return importAsymmetricJWK({ + kty: "OKP", + crv: "Ed25519", + x: jose.base64url.encode(keyBytes), + }); + + case MULTICODEC_SECP256K1_PUB: { + const point = secp256k1.Point.fromHex(keyBytes); + const uncompressed = point.toBytes(false); + return importAsymmetricJWK({ + kty: "EC", + crv: "secp256k1", + x: jose.base64url.encode(uncompressed.slice(1, 33)), + y: jose.base64url.encode(uncompressed.slice(33, 65)), + }); + } + + case MULTICODEC_P256_PUB: { + const point = p256.Point.fromHex(keyBytes); + const uncompressed = point.toBytes(false); + return importAsymmetricJWK({ + kty: "EC", + crv: "P-256", + x: jose.base64url.encode(uncompressed.slice(1, 33)), + y: jose.base64url.encode(uncompressed.slice(33, 65)), + }); + } + + default: + throw new Error( + `Unsupported key type in did:key (multicodec: 0x${codec.toString(16)}). ` + + `Supported: Ed25519, secp256k1, P-256.`, + ); + } +} + +/** + * Extract public key from did:jwk identifier (base64url-encoded JWK) + * + * @param identifier - The did:jwk identifier (without the "did:jwk:" prefix) + * @returns The extracted public key + */ +async function extractKeyFromDidJwk(identifier: string): Promise { + const jwkJson = new TextDecoder().decode(jose.base64url.decode(identifier)); + const jwk = JSON.parse(jwkJson) as jose.JWK; + return importAsymmetricJWK(jwk); +} + +/** + * Resolve did:web by fetching DID document from .well-known/did.json + * + * @param identifier - The did:web identifier (without the "did:web:" prefix) + * @param fragment - Optional fragment to identify specific key + * @returns The extracted public key + */ +async function resolveDidWeb(identifier: string, fragment?: string): Promise { + const parts = identifier.split(":"); + const domain = decodeURIComponent(parts[0]); + const path = parts.slice(1).map(decodeURIComponent).join("/"); + + // did:web spec allows HTTP for localhost (https://w3c-ccg.github.io/did-method-web/#read-resolve) + const host = domain.split(":")[0]; + const scheme = host === "localhost" || host === "127.0.0.1" ? "http" : "https"; + + const url = path + ? `${scheme}://${domain}/${path}/did.json` + : `${scheme}://${domain}/.well-known/did.json`; + + let didDocument: DIDDocument; + try { + const response = await fetch(url, { + headers: { Accept: "application/did+json, application/json" }, + }); + if (!response.ok) { + throw new Error(`HTTP ${response.status}`); + } + didDocument = (await response.json()) as DIDDocument; + } catch (error) { + throw new Error( + `Failed to resolve did:web:${identifier}: ${error instanceof Error ? error.message : error}`, + ); + } + + const fullDid = `did:web:${identifier}`; + const keyId = fragment ? `${fullDid}#${fragment}` : undefined; + const method = findVerificationMethod(didDocument, keyId); + + if (!method) { + throw new Error(`No verification method found for ${keyId || fullDid}`); + } + + if (method.publicKeyJwk) { + return importAsymmetricJWK(method.publicKeyJwk); + } + if (method.publicKeyMultibase) { + return extractKeyFromDidKey(method.publicKeyMultibase); + } + + throw new Error(`Verification method ${method.id} has no supported key format`); +} + +/** + * Read multicodec varint prefix from bytes + * + * @param bytes - The encoded bytes + * @returns The codec identifier and remaining key bytes + */ +function readMulticodec(bytes: Uint8Array): { codec: number; keyBytes: Uint8Array } { + let codec = 0; + let shift = 0; + let offset = 0; + + for (const byte of bytes) { + codec |= (byte & 0x7f) << shift; + offset++; + if ((byte & 0x80) === 0) break; + shift += 7; + } + + return { codec, keyBytes: bytes.slice(offset) }; +} + +/** + * Import an asymmetric JWK as a KeyLike + * + * @param jwk - The JWK to import + * @returns The imported key + */ +async function importAsymmetricJWK(jwk: jose.JWK): Promise { + const key = await jose.importJWK(jwk); + if (key instanceof Uint8Array) { + throw new Error("Symmetric keys are not supported"); + } + return key; +} + +interface DIDDocument { + id: string; + verificationMethod?: VerificationMethod[]; + assertionMethod?: (string | VerificationMethod)[]; + authentication?: (string | VerificationMethod)[]; +} + +interface VerificationMethod { + id: string; + type: string; + controller: string; + publicKeyJwk?: jose.JWK; + publicKeyMultibase?: string; +} + +/** + * Find a verification method in a DID document + * + * @param doc - The DID document + * @param keyId - Optional specific key ID to find + * @returns The verification method or undefined + */ +function findVerificationMethod(doc: DIDDocument, keyId?: string): VerificationMethod | undefined { + const methods = doc.verificationMethod || []; + + if (keyId) { + return methods.find(m => m.id === keyId); + } + + // Prefer assertionMethod, then authentication, then any + for (const ref of doc.assertionMethod || []) { + if (typeof ref === "string") { + const m = methods.find(m => m.id === ref); + if (m) return m; + } else { + return ref; + } + } + + for (const ref of doc.authentication || []) { + if (typeof ref === "string") { + const m = methods.find(m => m.id === ref); + if (m) return m; + } else { + return ref; + } + } + + return methods[0]; +} diff --git a/typescript/packages/extensions/src/offer-receipt/index.ts b/typescript/packages/extensions/src/offer-receipt/index.ts new file mode 100644 index 0000000000..47af659004 --- /dev/null +++ b/typescript/packages/extensions/src/offer-receipt/index.ts @@ -0,0 +1,96 @@ +/** + * x402 Offer/Receipt Extension + */ + +// Types +export { + OFFER_RECEIPT, + type SignatureFormat, + type Signer, + type JWSSigner, + type EIP712Signer, + type OfferPayload, + type SignedOffer, + type JWSSignedOffer, + type EIP712SignedOffer, + type ReceiptPayload, + type SignedReceipt, + type JWSSignedReceipt, + type EIP712SignedReceipt, + type OfferReceiptDeclaration, + type OfferReceiptIssuer, + type OfferInput, + type ReceiptInput, + isJWSSignedOffer, + isEIP712SignedOffer, + isJWSSignedReceipt, + isEIP712SignedReceipt, + isJWSSigner, + isEIP712Signer, +} from "./types"; + +// Signing utilities and offer/receipt creation +export { + // Canonicalization + canonicalize, + hashCanonical, + getCanonicalBytes, + // JWS + createJWS, + extractJWSHeader, + extractJWSPayload, + // EIP-712 + createOfferDomain, + createReceiptDomain, + OFFER_TYPES, + RECEIPT_TYPES, + prepareOfferForEIP712, + prepareReceiptForEIP712, + hashOfferTypedData, + hashReceiptTypedData, + signOfferEIP712, + signReceiptEIP712, + type SignTypedDataFn, + // Network utilities + extractEIP155ChainId, + convertNetworkStringToCAIP2, + extractChainIdFromCAIP2, + // Offer creation + createOfferJWS, + createOfferEIP712, + extractOfferPayload, + // Receipt creation + createReceiptJWS, + createReceiptEIP712, + extractReceiptPayload, +} from "./signing"; + +// Server extension and factory functions +export { + createOfferReceiptExtension, + declareOfferReceiptExtension, + createJWSOfferReceiptIssuer, + createEIP712OfferReceiptIssuer, +} from "./server"; + +// Client utilities for extracting offers/receipts +export { + decodeSignedOffers, + extractOffersFromPaymentRequired, + extractReceiptFromResponse, + findAcceptsObjectFromSignedOffer, + verifyReceiptMatchesOffer, + type DecodedOffer, +} from "./client"; + +// Verification utilities (exported from signing.ts) +export { + verifyOfferSignatureEIP712, + verifyReceiptSignatureEIP712, + verifyOfferSignatureJWS, + verifyReceiptSignatureJWS, + type EIP712VerificationResult, +} from "./signing"; + +// DID resolution utilities +export { extractPublicKeyFromKid } from "./did"; diff --git a/typescript/packages/extensions/src/offer-receipt/server.ts b/typescript/packages/extensions/src/offer-receipt/server.ts new file mode 100644 index 0000000000..7f954a07d4 --- /dev/null +++ b/typescript/packages/extensions/src/offer-receipt/server.ts @@ -0,0 +1,323 @@ +/** + * Offer-Receipt Extension for x402ResourceServer + * + * This module provides the ResourceServerExtension implementation that uses + * the extension hooks (enrichPaymentRequiredResponse, enrichSettlementResponse) + * to add signed offers and receipts to x402 payment flows. + * + * Based on: x402/specs/extensions/extension-offer-and-receipt.md (v1.0) + */ + +import type { + ResourceServerExtension, + PaymentRequiredContext, + SettleResultContext, +} from "@x402/core/types"; +import type { PaymentRequirements } from "@x402/core/types"; +import type { HTTPTransportContext } from "@x402/core/http"; +import { + OFFER_RECEIPT, + type OfferReceiptIssuer, + type OfferReceiptDeclaration, + type OfferInput, + type SignedOffer, + type SignedReceipt, + type JWSSigner, +} from "./types"; +import { + createOfferJWS, + createOfferEIP712, + createReceiptJWS, + createReceiptEIP712, + type SignTypedDataFn, +} from "./signing"; + +// ============================================================================ +// JSON Schemas for Extension Responses +// ============================================================================ + +/** + * JSON Schema for offer extension data (§6.1) + */ +const OFFER_SCHEMA = { + $schema: "https://json-schema.org/draft/2020-12/schema", + type: "object", + properties: { + offers: { + type: "array", + items: { + type: "object", + properties: { + format: { type: "string" }, + acceptIndex: { type: "integer" }, + payload: { + type: "object", + properties: { + version: { type: "integer" }, + resourceUrl: { type: "string" }, + scheme: { type: "string" }, + network: { type: "string" }, + asset: { type: "string" }, + payTo: { type: "string" }, + amount: { type: "string" }, + validUntil: { type: "integer" }, + }, + required: ["version", "resourceUrl", "scheme", "network", "asset", "payTo", "amount"], + }, + signature: { type: "string" }, + }, + required: ["format", "signature"], + }, + }, + }, + required: ["offers"], +}; + +/** + * JSON Schema for receipt extension data (§6.5) + */ +const RECEIPT_SCHEMA = { + $schema: "https://json-schema.org/draft/2020-12/schema", + type: "object", + properties: { + receipt: { + type: "object", + properties: { + format: { type: "string" }, + payload: { + type: "object", + properties: { + version: { type: "integer" }, + network: { type: "string" }, + resourceUrl: { type: "string" }, + payer: { type: "string" }, + issuedAt: { type: "integer" }, + transaction: { type: "string" }, + }, + required: ["version", "network", "resourceUrl", "payer", "issuedAt"], + }, + signature: { type: "string" }, + }, + required: ["format", "signature"], + }, + }, + required: ["receipt"], +}; + +// ============================================================================ +// Extension Factory +// ============================================================================ + +/** + * Convert PaymentRequirements to OfferInput + * + * @param requirements - The payment requirements + * @param acceptIndex - Index into accepts[] array + * @param offerValiditySeconds - Optional validity duration override + * @returns The offer input object + */ +function requirementsToOfferInput( + requirements: PaymentRequirements, + acceptIndex: number, + offerValiditySeconds?: number, +): OfferInput { + return { + acceptIndex, + scheme: requirements.scheme, + network: requirements.network, + asset: requirements.asset, + payTo: requirements.payTo, + amount: requirements.amount, + offerValiditySeconds: offerValiditySeconds ?? requirements.maxTimeoutSeconds, + }; +} + +/** + * Creates an offer-receipt extension for use with x402ResourceServer. + * + * The extension uses the hook system to: + * 1. Add signed offers to each PaymentRequirements in 402 responses + * 2. Add signed receipts to settlement responses after successful payment + * + * @param issuer - The issuer to use for creating and signing offers and receipts + * @returns ResourceServerExtension that can be registered with x402ResourceServer + */ +export function createOfferReceiptExtension(issuer: OfferReceiptIssuer): ResourceServerExtension { + return { + key: OFFER_RECEIPT, + + // Add signed offers to 402 PaymentRequired response + enrichPaymentRequiredResponse: async ( + declaration: unknown, + context: PaymentRequiredContext, + ): Promise => { + const config = declaration as OfferReceiptDeclaration | undefined; + + // Get resource URL from transport context or payment required response + const resourceUrl = + context.paymentRequiredResponse.resource?.url || + (context.transportContext as HTTPTransportContext)?.request?.adapter?.getUrl?.(); + + if (!resourceUrl) { + console.warn("[offer-receipt] No resource URL available for signing offers"); + return undefined; + } + + // Sign offers for each payment requirement + const offers: SignedOffer[] = []; + + for (let i = 0; i < context.requirements.length; i++) { + const requirement = context.requirements[i]; + try { + const offerInput = requirementsToOfferInput(requirement, i, config?.offerValiditySeconds); + const signedOffer = await issuer.issueOffer(resourceUrl, offerInput); + offers.push(signedOffer); + } catch (error) { + console.error(`[offer-receipt] Failed to sign offer for requirement ${i}:`, error); + } + } + + if (offers.length === 0) { + return undefined; + } + + // Return extension data per spec structure + return { + info: { + offers, + }, + schema: OFFER_SCHEMA, + }; + }, + + // Add signed receipt to settlement response + enrichSettlementResponse: async ( + declaration: unknown, + context: SettleResultContext, + ): Promise => { + const config = declaration as OfferReceiptDeclaration | undefined; + + // Skip if settlement failed + if (!context.result.success) { + return undefined; + } + + // Get payer from settlement result + const payer = context.result.payer; + if (!payer) { + console.warn("[offer-receipt] No payer available for signing receipt"); + return undefined; + } + + // Get network and transaction from settlement result + const network = context.result.network; + if (!network) { + console.warn("[offer-receipt] No network available for signing receipt"); + return undefined; + } + const transaction = context.result.transaction; + + // Get resource URL from transport context + const resourceUrl = ( + context.transportContext as HTTPTransportContext + )?.request?.adapter?.getUrl?.(); + + if (!resourceUrl) { + console.warn("[offer-receipt] No resource URL available for signing receipt"); + return undefined; + } + + // Determine whether to include transaction hash (default: false for privacy) + const includeTxHash = config?.includeTxHash === true; + + try { + const signedReceipt: SignedReceipt = await issuer.issueReceipt( + resourceUrl, + payer, + network, + includeTxHash ? transaction || undefined : undefined, + ); + // Return extension data per spec structure + return { + info: { + receipt: signedReceipt, + }, + schema: RECEIPT_SCHEMA, + }; + } catch (error) { + console.error("[offer-receipt] Failed to sign receipt:", error); + return undefined; + } + }, + }; +} + +/** + * Declare offer-receipt extension for a route + * + * Use this in route configuration to enable offer-receipt for a specific endpoint. + * + * @param config - Optional configuration for the extension + * @returns Extension declaration object to spread into route config + */ +export function declareOfferReceiptExtension( + config?: OfferReceiptDeclaration, +): Record { + return { + [OFFER_RECEIPT]: { + includeTxHash: config?.includeTxHash, + offerValiditySeconds: config?.offerValiditySeconds, + }, + }; +} + +// ============================================================================ +// Issuer Factory Functions +// ============================================================================ + +/** + * Create an OfferReceiptIssuer that uses JWS format + * + * @param kid - Key identifier DID (e.g., did:web:api.example.com#key-1) + * @param jwsSigner - JWS signer with sign() function and algorithm + * @returns OfferReceiptIssuer for use with createOfferReceiptExtension + */ +export function createJWSOfferReceiptIssuer(kid: string, jwsSigner: JWSSigner): OfferReceiptIssuer { + return { + kid, + format: "jws", + + async issueOffer(resourceUrl: string, input: OfferInput) { + return createOfferJWS(resourceUrl, input, jwsSigner); + }, + + async issueReceipt(resourceUrl: string, payer: string, network: string, transaction?: string) { + return createReceiptJWS({ resourceUrl, payer, network, transaction }, jwsSigner); + }, + }; +} + +/** + * Create an OfferReceiptIssuer that uses EIP-712 format + * + * @param kid - Key identifier DID (e.g., did:pkh:eip155:1:0x...) + * @param signTypedData - Function to sign EIP-712 typed data + * @returns OfferReceiptIssuer for use with createOfferReceiptExtension + */ +export function createEIP712OfferReceiptIssuer( + kid: string, + signTypedData: SignTypedDataFn, +): OfferReceiptIssuer { + return { + kid, + format: "eip712", + + async issueOffer(resourceUrl: string, input: OfferInput) { + return createOfferEIP712(resourceUrl, input, signTypedData); + }, + + async issueReceipt(resourceUrl: string, payer: string, network: string, transaction?: string) { + return createReceiptEIP712({ resourceUrl, payer, network, transaction }, signTypedData); + }, + }; +} diff --git a/typescript/packages/extensions/src/offer-receipt/signing.ts b/typescript/packages/extensions/src/offer-receipt/signing.ts new file mode 100644 index 0000000000..54d3864a4c --- /dev/null +++ b/typescript/packages/extensions/src/offer-receipt/signing.ts @@ -0,0 +1,852 @@ +/** + * Signing utilities for x402 Offer/Receipt Extension + * + * This module provides: + * - JCS (JSON Canonicalization Scheme) per RFC 8785 + * - JWS (JSON Web Signature) signing and extraction + * - EIP-712 typed data signing + * - Offer/Receipt creation utilities + * - Signature verification utilities + * + * Based on: x402/specs/extensions/extension-offer-and-receipt.md (v1.0) §3 + */ + +import * as jose from "jose"; +import { hashTypedData, recoverTypedDataAddress, type Hex, type TypedDataDomain } from "viem"; +import type { + JWSSigner, + OfferPayload, + ReceiptPayload, + SignedOffer, + SignedReceipt, + OfferInput, + ReceiptInput, +} from "./types"; +import { + isJWSSignedOffer, + isEIP712SignedOffer, + isJWSSignedReceipt, + isEIP712SignedReceipt, + type JWSSignedOffer, + type EIP712SignedOffer, + type JWSSignedReceipt, + type EIP712SignedReceipt, +} from "./types"; +import { extractPublicKeyFromKid } from "./did"; + +// ============================================================================ +// JCS Canonicalization (RFC 8785) +// ============================================================================ + +/** + * Canonicalize a JSON object using JCS (RFC 8785) + * + * Rules: + * 1. Object keys are sorted lexicographically by UTF-16 code units + * 2. No whitespace between tokens + * 3. Numbers use shortest representation (no trailing zeros) + * 4. Strings use minimal escaping + * 5. null, true, false are lowercase literals + * + * @param value - The object to canonicalize + * @returns The canonicalized JSON string + */ +export function canonicalize(value: unknown): string { + return serializeValue(value); +} + +/** + * Serialize a value to canonical JSON + * + * @param value - The value to serialize + * @returns The serialized string + */ +function serializeValue(value: unknown): string { + if (value === null) return "null"; + if (value === undefined) return "null"; + + const type = typeof value; + if (type === "boolean") return value ? "true" : "false"; + if (type === "number") return serializeNumber(value as number); + if (type === "string") return serializeString(value as string); + if (Array.isArray(value)) return serializeArray(value); + if (type === "object") return serializeObject(value as Record); + + throw new Error(`Cannot canonicalize value of type ${type}`); +} + +/** + * Serialize a number to canonical JSON + * + * @param num - The number to serialize + * @returns The serialized string + */ +function serializeNumber(num: number): string { + if (!Number.isFinite(num)) throw new Error("Cannot canonicalize Infinity or NaN"); + if (Object.is(num, -0)) return "0"; + return String(num); +} + +/** + * Serialize a string to canonical JSON + * + * @param str - The string to serialize + * @returns The serialized string with proper escaping + */ +function serializeString(str: string): string { + let result = '"'; + for (let i = 0; i < str.length; i++) { + const char = str[i]; + const code = str.charCodeAt(i); + if (code < 0x20) { + result += "\\u" + code.toString(16).padStart(4, "0"); + } else if (char === '"') { + result += '\\"'; + } else if (char === "\\") { + result += "\\\\"; + } else { + result += char; + } + } + return result + '"'; +} + +/** + * Serialize an array to canonical JSON + * + * @param arr - The array to serialize + * @returns The serialized string + */ +function serializeArray(arr: unknown[]): string { + return "[" + arr.map(serializeValue).join(",") + "]"; +} + +/** + * Serialize an object to canonical JSON with sorted keys + * + * @param obj - The object to serialize + * @returns The serialized string with sorted keys + */ +function serializeObject(obj: Record): string { + const keys = Object.keys(obj).sort((a, b) => (a < b ? -1 : a > b ? 1 : 0)); + const pairs: string[] = []; + for (const key of keys) { + const value = obj[key]; + if (value !== undefined) { + pairs.push(serializeString(key) + ":" + serializeValue(value)); + } + } + return "{" + pairs.join(",") + "}"; +} + +/** + * Hash a canonicalized object using SHA-256 + * + * @param obj - The object to hash + * @returns The SHA-256 hash as Uint8Array + */ +export async function hashCanonical(obj: unknown): Promise { + const canonical = canonicalize(obj); + const data = new TextEncoder().encode(canonical); + const hashBuffer = await crypto.subtle.digest("SHA-256", data); + return new Uint8Array(hashBuffer); +} + +/** + * Get canonical bytes of an object (UTF-8 encoded) + * + * @param obj - The object to encode + * @returns The UTF-8 encoded canonical JSON + */ +export function getCanonicalBytes(obj: unknown): Uint8Array { + return new TextEncoder().encode(canonicalize(obj)); +} + +// ============================================================================ +// JWS Signing (§3.3) +// ============================================================================ + +/** + * Create a JWS Compact Serialization from a payload + * + * Assembles the full JWS structure (header.payload.signature) using the + * signer's algorithm and kid. The signer only needs to sign bytes and + * return the base64url-encoded signature. + * + * @param payload - The payload object to sign + * @param signer - The JWS signer + * @returns The JWS compact serialization string + */ +export async function createJWS(payload: T, signer: JWSSigner): Promise { + const headerObj = { alg: signer.algorithm, kid: signer.kid }; + const headerB64 = jose.base64url.encode(new TextEncoder().encode(JSON.stringify(headerObj))); + const canonical = canonicalize(payload); + const payloadB64 = jose.base64url.encode(new TextEncoder().encode(canonical)); + const signingInput = new TextEncoder().encode(`${headerB64}.${payloadB64}`); + const signatureB64 = await signer.sign(signingInput); + return `${headerB64}.${payloadB64}.${signatureB64}`; +} + +/** + * Extract JWS header without verification + * + * @param jws - The JWS compact serialization string + * @returns The decoded header object + */ +export function extractJWSHeader(jws: string): { alg: string; kid?: string } { + const parts = jws.split("."); + if (parts.length !== 3) throw new Error("Invalid JWS format"); + const headerJson = jose.base64url.decode(parts[0]); + return JSON.parse(new TextDecoder().decode(headerJson)); +} + +/** + * Extract JWS payload + * + * Note: This extracts the payload without verifying the signature or + * checking signer authorization. Signature verification requires resolving + * key bindings (did:web documents, attestations, etc.) which is outside + * the scope of x402 client utilities. + * + * @param jws - The JWS compact serialization string + * @returns The decoded payload + */ +export function extractJWSPayload(jws: string): T { + const parts = jws.split("."); + if (parts.length !== 3) throw new Error("Invalid JWS format"); + const payloadJson = jose.base64url.decode(parts[1]); + return JSON.parse(new TextDecoder().decode(payloadJson)); +} + +// ============================================================================ +// EIP-712 Domain Configuration (§3.2) +// ============================================================================ + +/** + * Create EIP-712 domain for offer signing + * + * @returns The EIP-712 domain object + */ +export function createOfferDomain(): TypedDataDomain { + return { name: "x402 offer", version: "1", chainId: 1 }; +} + +/** + * Create EIP-712 domain for receipt signing + * + * @returns The EIP-712 domain object + */ +export function createReceiptDomain(): TypedDataDomain { + return { name: "x402 receipt", version: "1", chainId: 1 }; +} + +/** + * EIP-712 types for Offer (§4.3) + */ +export const OFFER_TYPES = { + Offer: [ + { name: "version", type: "uint256" }, + { name: "resourceUrl", type: "string" }, + { name: "scheme", type: "string" }, + { name: "network", type: "string" }, + { name: "asset", type: "string" }, + { name: "payTo", type: "string" }, + { name: "amount", type: "string" }, + { name: "validUntil", type: "uint256" }, + ], +}; + +/** + * EIP-712 types for Receipt (§5.3) + */ +export const RECEIPT_TYPES = { + Receipt: [ + { name: "version", type: "uint256" }, + { name: "network", type: "string" }, + { name: "resourceUrl", type: "string" }, + { name: "payer", type: "string" }, + { name: "issuedAt", type: "uint256" }, + { name: "transaction", type: "string" }, + ], +}; + +// ============================================================================ +// EIP-712 Payload Preparation +// ============================================================================ + +/** + * Prepare offer payload for EIP-712 signing + * + * @param payload - The offer payload + * @returns The prepared message object for EIP-712 + */ +export function prepareOfferForEIP712(payload: OfferPayload): { + version: bigint; + resourceUrl: string; + scheme: string; + network: string; + asset: string; + payTo: string; + amount: string; + validUntil: bigint; +} { + return { + version: BigInt(payload.version), + resourceUrl: payload.resourceUrl, + scheme: payload.scheme, + network: payload.network, + asset: payload.asset, + payTo: payload.payTo, + amount: payload.amount, + validUntil: BigInt(payload.validUntil), + }; +} + +/** + * Prepare receipt payload for EIP-712 signing + * + * @param payload - The receipt payload + * @returns The prepared message object for EIP-712 + */ +export function prepareReceiptForEIP712(payload: ReceiptPayload): { + version: bigint; + network: string; + resourceUrl: string; + payer: string; + issuedAt: bigint; + transaction: string; +} { + return { + version: BigInt(payload.version), + network: payload.network, + resourceUrl: payload.resourceUrl, + payer: payload.payer, + issuedAt: BigInt(payload.issuedAt), + transaction: payload.transaction, + }; +} + +// ============================================================================ +// EIP-712 Hashing +// ============================================================================ + +/** + * Hash offer typed data for EIP-712 + * + * @param payload - The offer payload + * @returns The EIP-712 hash + */ +export function hashOfferTypedData(payload: OfferPayload): Hex { + return hashTypedData({ + domain: createOfferDomain(), + types: OFFER_TYPES, + primaryType: "Offer", + message: prepareOfferForEIP712(payload), + }); +} + +/** + * Hash receipt typed data for EIP-712 + * + * @param payload - The receipt payload + * @returns The EIP-712 hash + */ +export function hashReceiptTypedData(payload: ReceiptPayload): Hex { + return hashTypedData({ + domain: createReceiptDomain(), + types: RECEIPT_TYPES, + primaryType: "Receipt", + message: prepareReceiptForEIP712(payload), + }); +} + +// ============================================================================ +// EIP-712 Signing +// ============================================================================ + +/** + * Function type for signing EIP-712 typed data + */ +export type SignTypedDataFn = (params: { + domain: TypedDataDomain; + types: Record>; + primaryType: string; + message: Record; +}) => Promise; + +/** + * Sign an offer using EIP-712 + * + * @param payload - The offer payload + * @param signTypedData - The signing function + * @returns The signature hex string + */ +export async function signOfferEIP712( + payload: OfferPayload, + signTypedData: SignTypedDataFn, +): Promise { + return signTypedData({ + domain: createOfferDomain(), + types: OFFER_TYPES, + primaryType: "Offer", + message: prepareOfferForEIP712(payload) as unknown as Record, + }); +} + +/** + * Sign a receipt using EIP-712 + * + * @param payload - The receipt payload + * @param signTypedData - The signing function + * @returns The signature hex string + */ +export async function signReceiptEIP712( + payload: ReceiptPayload, + signTypedData: SignTypedDataFn, +): Promise { + return signTypedData({ + domain: createReceiptDomain(), + types: RECEIPT_TYPES, + primaryType: "Receipt", + message: prepareReceiptForEIP712(payload) as unknown as Record, + }); +} + +// ============================================================================ +// Network Utilities +// ============================================================================ + +/** + * Extract chain ID from an EIP-155 network string (strict format) + * + * @param network - The network string in "eip155:" format + * @returns The chain ID number + * @throws Error if network is not in "eip155:" format + */ +export function extractEIP155ChainId(network: string): number { + const match = network.match(/^eip155:(\d+)$/); + if (!match) { + throw new Error(`Invalid network format: ${network}. Expected "eip155:"`); + } + return parseInt(match[1], 10); +} + +/** + * V1 EVM network name to chain ID mapping + * Based on x402 v1 protocol network identifiers + */ +const V1_EVM_NETWORK_CHAIN_IDS: Record = { + ethereum: 1, + sepolia: 11155111, + abstract: 2741, + "abstract-testnet": 11124, + "base-sepolia": 84532, + base: 8453, + "avalanche-fuji": 43113, + avalanche: 43114, + iotex: 4689, + sei: 1329, + "sei-testnet": 1328, + polygon: 137, + "polygon-amoy": 80002, + peaq: 3338, + story: 1514, + educhain: 41923, + "skale-base-sepolia": 324705682, +}; + +/** + * V1 Solana network name to CAIP-2 mapping + */ +const V1_SOLANA_NETWORKS: Record = { + solana: "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp", + "solana-devnet": "solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1", + "solana-testnet": "solana:4uhcVJyU9pJkvQyS88uRDiswHXSCkY3z", +}; + +/** + * Convert a network string to CAIP-2 format + * + * Handles both CAIP-2 format and legacy x402 v1 network strings: + * - CAIP-2: "eip155:8453" → "eip155:8453" (passed through) + * - V1 EVM: "base" → "eip155:8453", "base-sepolia" → "eip155:84532" + * - V1 Solana: "solana" → "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp" + * + * @param network - The network string to convert + * @returns The CAIP-2 formatted network string + * @throws Error if network is not a recognized v1 identifier or CAIP-2 format + */ +export function convertNetworkStringToCAIP2(network: string): string { + // Already CAIP-2 format + if (network.includes(":")) return network; + + // Check V1 EVM networks + const chainId = V1_EVM_NETWORK_CHAIN_IDS[network.toLowerCase()]; + if (chainId !== undefined) { + return `eip155:${chainId}`; + } + + // Check V1 Solana networks + const solanaNetwork = V1_SOLANA_NETWORKS[network.toLowerCase()]; + if (solanaNetwork) { + return solanaNetwork; + } + + throw new Error( + `Unknown network identifier: "${network}". Expected CAIP-2 format (e.g., "eip155:8453") or v1 name (e.g., "base", "solana").`, + ); +} + +/** + * Extract chain ID from a CAIP-2 network string (EVM only) + * + * @param network - The CAIP-2 network string + * @returns Chain ID number, or undefined for non-EVM networks + */ +export function extractChainIdFromCAIP2(network: string): number | undefined { + const [namespace, reference] = network.split(":"); + if (namespace === "eip155" && reference) { + const chainId = parseInt(reference, 10); + return isNaN(chainId) ? undefined : chainId; + } + return undefined; +} + +// ============================================================================ +// Offer Creation (§4) +// ============================================================================ + +/** Default offer validity in seconds (matches x402ResourceServer.ts) */ +const DEFAULT_MAX_TIMEOUT_SECONDS = 300; + +/** Current extension version */ +const EXTENSION_VERSION = 1; + +/** + * Create an offer payload from input + * + * @param resourceUrl - The resource URL being paid for + * @param input - The offer input parameters + * @returns The offer payload + */ +function createOfferPayload(resourceUrl: string, input: OfferInput): OfferPayload { + const now = Math.floor(Date.now() / 1000); + const offerValiditySeconds = input.offerValiditySeconds ?? DEFAULT_MAX_TIMEOUT_SECONDS; + + return { + version: EXTENSION_VERSION, + resourceUrl, + scheme: input.scheme, + network: input.network, + asset: input.asset, + payTo: input.payTo, + amount: input.amount, + validUntil: now + offerValiditySeconds, + }; +} + +/** + * Create a signed offer using JWS + * + * @param resourceUrl - The resource URL being paid for + * @param input - The offer input parameters + * @param signer - The JWS signer + * @returns The signed offer with JWS format + */ +export async function createOfferJWS( + resourceUrl: string, + input: OfferInput, + signer: JWSSigner, +): Promise { + const payload = createOfferPayload(resourceUrl, input); + const jws = await createJWS(payload, signer); + return { + format: "jws", + acceptIndex: input.acceptIndex, + signature: jws, + }; +} + +/** + * Create a signed offer using EIP-712 + * + * @param resourceUrl - The resource URL being paid for + * @param input - The offer input parameters + * @param signTypedData - The signing function + * @returns The signed offer with EIP-712 format + */ +export async function createOfferEIP712( + resourceUrl: string, + input: OfferInput, + signTypedData: SignTypedDataFn, +): Promise { + const payload = createOfferPayload(resourceUrl, input); + const signature = await signOfferEIP712(payload, signTypedData); + return { + format: "eip712", + acceptIndex: input.acceptIndex, + payload, + signature, + }; +} + +/** + * Extract offer payload + * + * Note: This extracts the payload without verifying the signature or + * checking signer authorization. Signer authorization requires resolving + * key bindings (did:web documents, attestations, etc.) which is outside + * the scope of x402 client utilities. See spec §4.5.1. + * + * @param offer - The signed offer + * @returns The offer payload + */ +export function extractOfferPayload(offer: SignedOffer): OfferPayload { + if (isJWSSignedOffer(offer)) { + return extractJWSPayload(offer.signature); + } else if (isEIP712SignedOffer(offer)) { + return offer.payload; + } + throw new Error(`Unknown offer format: ${(offer as SignedOffer).format}`); +} + +// ============================================================================ +// Receipt Creation (§5) +// ============================================================================ + +/** + * Create a receipt payload for EIP-712 (requires all fields per spec §5.3) + * + * Per spec: "implementations MUST set unused fields to empty string" + * for EIP-712 signing where fixed schemas require all fields. + * + * @param input - The receipt input parameters + * @returns The receipt payload with all fields + */ +function createReceiptPayloadForEIP712(input: ReceiptInput): ReceiptPayload { + return { + version: EXTENSION_VERSION, + network: input.network, + resourceUrl: input.resourceUrl, + payer: input.payer, + issuedAt: Math.floor(Date.now() / 1000), + transaction: input.transaction ?? "", + }; +} + +/** + * Create a receipt payload for JWS (omits optional fields when not provided) + * + * Per spec §5.2: transaction is optional and should be omitted in JWS + * when not provided (privacy-minimal by default). + * + * @param input - The receipt input parameters + * @returns The receipt payload with optional fields omitted if not provided + */ +function createReceiptPayloadForJWS( + input: ReceiptInput, +): Omit & { transaction?: string } { + const payload: Omit & { transaction?: string } = { + version: EXTENSION_VERSION, + network: input.network, + resourceUrl: input.resourceUrl, + payer: input.payer, + issuedAt: Math.floor(Date.now() / 1000), + }; + if (input.transaction) { + payload.transaction = input.transaction; + } + return payload; +} + +/** + * Create a signed receipt using JWS + * + * @param input - The receipt input parameters + * @param signer - The JWS signer + * @returns The signed receipt with JWS format + */ +export async function createReceiptJWS( + input: ReceiptInput, + signer: JWSSigner, +): Promise { + const payload = createReceiptPayloadForJWS(input); + const jws = await createJWS(payload, signer); + return { format: "jws", signature: jws }; +} + +/** + * Create a signed receipt using EIP-712 + * + * @param input - The receipt input parameters + * @param signTypedData - The signing function + * @returns The signed receipt with EIP-712 format + */ +export async function createReceiptEIP712( + input: ReceiptInput, + signTypedData: SignTypedDataFn, +): Promise { + const payload = createReceiptPayloadForEIP712(input); + const signature = await signReceiptEIP712(payload, signTypedData); + return { format: "eip712", payload, signature }; +} + +/** + * Extract receipt payload + * + * Note: This extracts the payload without verifying the signature or + * checking signer authorization. Signer authorization requires resolving + * key bindings (did:web documents, attestations, etc.) which is outside + * the scope of x402 client utilities. See spec §5.5. + * + * @param receipt - The signed receipt + * @returns The receipt payload + */ +export function extractReceiptPayload(receipt: SignedReceipt): ReceiptPayload { + if (isJWSSignedReceipt(receipt)) { + return extractJWSPayload(receipt.signature); + } else if (isEIP712SignedReceipt(receipt)) { + return receipt.payload; + } + throw new Error(`Unknown receipt format: ${(receipt as SignedReceipt).format}`); +} + +// ============================================================================ +// Signature Verification +// ============================================================================ + +/** + * Result of EIP-712 signature verification + */ +export interface EIP712VerificationResult { + signer: Hex; + payload: T; +} + +/** + * Verify an EIP-712 signed offer and recover the signer address. + * Does NOT verify signer authorization for the resourceUrl - see spec §4.5.1. + * + * @param offer - The EIP-712 signed offer + * @returns The recovered signer address and payload + */ +export async function verifyOfferSignatureEIP712( + offer: EIP712SignedOffer, +): Promise> { + if (offer.format !== "eip712") { + throw new Error(`Expected eip712 format, got ${offer.format}`); + } + if (!offer.payload || !("scheme" in offer.payload)) { + throw new Error("Invalid offer: missing or malformed payload"); + } + + const signer = await recoverTypedDataAddress({ + domain: createOfferDomain(), + types: OFFER_TYPES, + primaryType: "Offer", + message: prepareOfferForEIP712(offer.payload), + signature: offer.signature as Hex, + }); + + return { signer, payload: offer.payload }; +} + +/** + * Verify an EIP-712 signed receipt and recover the signer address. + * Does NOT verify signer authorization for the resourceUrl - see spec §4.5.1. + * + * @param receipt - The EIP-712 signed receipt + * @returns The recovered signer address and payload + */ +export async function verifyReceiptSignatureEIP712( + receipt: EIP712SignedReceipt, +): Promise> { + if (receipt.format !== "eip712") { + throw new Error(`Expected eip712 format, got ${receipt.format}`); + } + if (!receipt.payload || !("payer" in receipt.payload)) { + throw new Error("Invalid receipt: missing or malformed payload"); + } + + const signer = await recoverTypedDataAddress({ + domain: createReceiptDomain(), + types: RECEIPT_TYPES, + primaryType: "Receipt", + message: prepareReceiptForEIP712(receipt.payload), + signature: receipt.signature as Hex, + }); + + return { signer, payload: receipt.payload }; +} + +/** + * Verify a JWS signed offer. + * Does NOT verify signer authorization for the resourceUrl - see spec §4.5.1. + * If no publicKey provided, extracts from kid (supports did:key, did:jwk, did:web). + * + * @param offer - The JWS signed offer + * @param publicKey - Optional public key (JWK or KeyLike). If not provided, extracted from kid. + * @returns The verified payload + */ +export async function verifyOfferSignatureJWS( + offer: JWSSignedOffer, + publicKey?: jose.KeyLike | jose.JWK, +): Promise { + if (offer.format !== "jws") { + throw new Error(`Expected jws format, got ${offer.format}`); + } + const key = await resolveVerificationKey(offer.signature, publicKey); + const { payload } = await jose.compactVerify(offer.signature, key); + return JSON.parse(new TextDecoder().decode(payload)) as OfferPayload; +} + +/** + * Verify a JWS signed receipt. + * Does NOT verify signer authorization for the resourceUrl - see spec §4.5.1. + * If no publicKey provided, extracts from kid (supports did:key, did:jwk, did:web). + * + * @param receipt - The JWS signed receipt + * @param publicKey - Optional public key (JWK or KeyLike). If not provided, extracted from kid. + * @returns The verified payload + */ +export async function verifyReceiptSignatureJWS( + receipt: JWSSignedReceipt, + publicKey?: jose.KeyLike | jose.JWK, +): Promise { + if (receipt.format !== "jws") { + throw new Error(`Expected jws format, got ${receipt.format}`); + } + const key = await resolveVerificationKey(receipt.signature, publicKey); + const { payload } = await jose.compactVerify(receipt.signature, key); + return JSON.parse(new TextDecoder().decode(payload)) as ReceiptPayload; +} + +/** + * Resolve the verification key for JWS verification + * + * @param jws - The JWS compact serialization string + * @param providedKey - Optional explicit public key + * @returns The resolved public key + */ +async function resolveVerificationKey( + jws: string, + providedKey?: jose.KeyLike | jose.JWK, +): Promise { + if (providedKey) { + if ("kty" in providedKey) { + const key = await jose.importJWK(providedKey); + if (key instanceof Uint8Array) { + throw new Error("Symmetric keys are not supported for JWS verification"); + } + return key; + } + return providedKey; + } + + const header = extractJWSHeader(jws); + if (!header.kid) { + throw new Error("No public key provided and JWS header missing kid"); + } + + return extractPublicKeyFromKid(header.kid); +} diff --git a/typescript/packages/extensions/src/offer-receipt/types.ts b/typescript/packages/extensions/src/offer-receipt/types.ts new file mode 100644 index 0000000000..546e8c0dae --- /dev/null +++ b/typescript/packages/extensions/src/offer-receipt/types.ts @@ -0,0 +1,302 @@ +/** + * Type definitions for the x402 Offer/Receipt Extension + * + * Based on: x402/specs/extensions/extension-offer-and-receipt.md (v1.0) + * + * Offers prove payment requirements originated from a resource server. + * Receipts prove service was delivered after payment. + */ + +/** + * Extension identifier constant + */ +export const OFFER_RECEIPT = "offer-receipt"; + +/** + * Supported signature formats (§3.1) + */ +export type SignatureFormat = "jws" | "eip712"; + +// ============================================================================ +// Low-Level Signer Interfaces +// ============================================================================ + +/** + * Base signer interface for pluggable signing backends + */ +export interface Signer { + /** Key identifier DID (e.g., did:web:api.example.com#key-1) */ + kid: string; + /** Sign payload and return signature string */ + sign: (payload: Uint8Array) => Promise; + /** Signature format */ + format: SignatureFormat; +} + +/** + * JWS-specific signer with algorithm info + */ +export interface JWSSigner extends Signer { + format: "jws"; + /** JWS algorithm (e.g., ES256K, EdDSA) */ + algorithm: string; +} + +/** + * EIP-712 specific signer + */ +export interface EIP712Signer extends Signer { + format: "eip712"; + /** Chain ID for EIP-712 domain */ + chainId: number; +} + +// ============================================================================ +// Offer Types (§4) +// ============================================================================ + +/** + * Offer payload fields (§4.2) + * + * Required: version, resourceUrl, scheme, network, asset, payTo, amount + * Optional: validUntil + */ +export interface OfferPayload { + /** Offer payload schema version (currently 1) */ + version: number; + /** The paid resource URL */ + resourceUrl: string; + /** Payment scheme identifier (e.g., "exact") */ + scheme: string; + /** Blockchain network identifier (CAIP-2 format, e.g., "eip155:8453") */ + network: string; + /** Token contract address or "native" */ + asset: string; + /** Recipient wallet address */ + payTo: string; + /** Required payment amount */ + amount: string; + /** Unix timestamp (seconds) when the offer expires (optional) */ + validUntil: number; +} + +/** + * Signed offer in JWS format (§3.1.1) + * + * "When format = 'jws': payload MUST be omitted" + */ +export interface JWSSignedOffer { + format: "jws"; + /** Index into accepts[] array (unsigned envelope field, §4.1.1) */ + acceptIndex?: number; + /** JWS Compact Serialization string (header.payload.signature) */ + signature: string; +} + +/** + * Signed offer in EIP-712 format (§3.1.1) + * + * "When format = 'eip712': payload is REQUIRED" + */ +export interface EIP712SignedOffer { + format: "eip712"; + /** Index into accepts[] array (unsigned envelope field, §4.1.1) */ + acceptIndex?: number; + /** The canonical payload fields */ + payload: OfferPayload; + /** Hex-encoded ECDSA signature (0x-prefixed, 65 bytes: r+s+v) */ + signature: string; +} + +/** + * Union type for signed offers + */ +export type SignedOffer = JWSSignedOffer | EIP712SignedOffer; + +// ============================================================================ +// Receipt Types (§5) +// ============================================================================ + +/** + * Receipt payload fields (§5.2) + * + * Required: version, network, resourceUrl, payer, issuedAt + * Optional: transaction (for verifiability over privacy) + */ +export interface ReceiptPayload { + /** Receipt payload schema version (currently 1) */ + version: number; + /** Blockchain network identifier (CAIP-2 format, e.g., "eip155:8453") */ + network: string; + /** The paid resource URL */ + resourceUrl: string; + /** Payer identifier (commonly a wallet address) */ + payer: string; + /** Unix timestamp (seconds) when receipt was issued */ + issuedAt: number; + /** Blockchain transaction hash (optional - for verifiability over privacy) */ + transaction: string; +} + +/** + * Signed receipt in JWS format (§3.1.1) + */ +export interface JWSSignedReceipt { + format: "jws"; + /** JWS Compact Serialization string */ + signature: string; +} + +/** + * Signed receipt in EIP-712 format (§3.1.1) + */ +export interface EIP712SignedReceipt { + format: "eip712"; + /** The receipt payload */ + payload: ReceiptPayload; + /** Hex-encoded ECDSA signature */ + signature: string; +} + +/** + * Union type for signed receipts + */ +export type SignedReceipt = JWSSignedReceipt | EIP712SignedReceipt; + +// ============================================================================ +// Extension Configuration Types +// ============================================================================ + +/** + * Declaration for the offer-receipt extension in route config + * Used by servers to declare that a route uses offer-receipt + */ +export interface OfferReceiptDeclaration { + /** Include transaction hash in receipt (default: false for privacy). Set to true for verifiability. */ + includeTxHash?: boolean; + /** Offer validity duration in seconds. Default: 300 (see x402ResourceServer.ts) */ + offerValiditySeconds?: number; +} + +/** + * Input for creating an offer (derived from PaymentRequirements) + */ +export interface OfferInput { + /** Index into accepts[] array this offer corresponds to (0-based) */ + acceptIndex: number; + /** Payment scheme identifier */ + scheme: string; + /** Blockchain network identifier (CAIP-2 format) */ + network: string; + /** Token contract address or "native" */ + asset: string; + /** Recipient wallet address */ + payTo: string; + /** Required payment amount */ + amount: string; + /** Offer validity duration in seconds. Default: 300 (see x402ResourceServer.ts) */ + offerValiditySeconds?: number; +} + +/** + * High-level issuer interface for the offer-receipt extension. + * Creates and signs offers and receipts. + * Used by createOfferReceiptExtension() + */ +export interface OfferReceiptIssuer { + /** Key identifier DID */ + kid: string; + /** Signature format */ + format: SignatureFormat; + /** Create and sign an offer for a resource */ + issueOffer(resourceUrl: string, input: OfferInput): Promise; + /** Create and sign a receipt for a completed payment */ + issueReceipt( + resourceUrl: string, + payer: string, + network: string, + transaction?: string, + ): Promise; +} + +// ============================================================================ +// Type Guards +// ============================================================================ + +/** + * Check if an offer is JWS format + * + * @param offer - The signed offer to check + * @returns True if the offer uses JWS format + */ +export function isJWSSignedOffer(offer: SignedOffer): offer is JWSSignedOffer { + return offer.format === "jws"; +} + +/** + * Check if an offer is EIP-712 format + * + * @param offer - The signed offer to check + * @returns True if the offer uses EIP-712 format + */ +export function isEIP712SignedOffer(offer: SignedOffer): offer is EIP712SignedOffer { + return offer.format === "eip712"; +} + +/** + * Check if a receipt is JWS format + * + * @param receipt - The signed receipt to check + * @returns True if the receipt uses JWS format + */ +export function isJWSSignedReceipt(receipt: SignedReceipt): receipt is JWSSignedReceipt { + return receipt.format === "jws"; +} + +/** + * Check if a receipt is EIP-712 format + * + * @param receipt - The signed receipt to check + * @returns True if the receipt uses EIP-712 format + */ +export function isEIP712SignedReceipt(receipt: SignedReceipt): receipt is EIP712SignedReceipt { + return receipt.format === "eip712"; +} + +/** + * Check if a signer is JWS format + * + * @param signer - The signer to check + * @returns True if the signer uses JWS format + */ +export function isJWSSigner(signer: Signer): signer is JWSSigner { + return signer.format === "jws"; +} + +/** + * Check if a signer is EIP-712 format + * + * @param signer - The signer to check + * @returns True if the signer uses EIP-712 format + */ +export function isEIP712Signer(signer: Signer): signer is EIP712Signer { + return signer.format === "eip712"; +} + +// ============================================================================ +// Receipt Input Type +// ============================================================================ + +/** + * Input for creating a receipt + */ +export interface ReceiptInput { + /** The resource URL that was paid for */ + resourceUrl: string; + /** The payer identifier (wallet address) */ + payer: string; + /** The blockchain network (CAIP-2 format) */ + network: string; + /** The transaction hash (optional, for verifiability) */ + transaction?: string; +} diff --git a/typescript/packages/extensions/src/sign-in-with-x/hooks.ts b/typescript/packages/extensions/src/sign-in-with-x/hooks.ts index 741b30f226..c416d2b31b 100644 --- a/typescript/packages/extensions/src/sign-in-with-x/hooks.ts +++ b/typescript/packages/extensions/src/sign-in-with-x/hooks.ts @@ -75,7 +75,11 @@ export function createSIWxSettleHook(options: CreateSIWxHookOptions) { } /** - * Creates an onProtectedRequest hook that validates SIWX auth before payment. + * Creates an onProtectedRequest hook that validates SIWX auth. + * + * For paid routes: grants access when the SIWX signature is valid and the address has paid. + * For auth-only routes (accepts: []): grants access on valid SIWX signature alone. + * Auth-only detection uses the routeConfig passed by x402HTTPResourceServer. * * @param options - Hook configuration * @returns Hook function for x402HTTPResourceServer.onProtectedRequest() @@ -99,10 +103,13 @@ export function createSIWxRequestHook(options: CreateSIWxHookOptions) { ); } - return async (context: { - adapter: { getHeader(name: string): string | undefined; getUrl(): string }; - path: string; - }): Promise => { + return async ( + context: { + adapter: { getHeader(name: string): string | undefined; getUrl(): string }; + path: string; + }, + routeConfig?: { accepts?: unknown }, + ): Promise => { // Try both cases for header (HTTP headers are case-insensitive) const header = context.adapter.getHeader(SIGN_IN_WITH_X) || @@ -134,8 +141,11 @@ export function createSIWxRequestHook(options: CreateSIWxHookOptions) { } } - const hasPaid = await storage.hasPaid(context.path, verification.address); - if (hasPaid) { + // Auth-only routes (accepts: []) grant access on valid SIWX alone + const isAuthOnly = Array.isArray(routeConfig?.accepts) && routeConfig.accepts.length === 0; + + const shouldGrant = isAuthOnly || (await storage.hasPaid(context.path, verification.address)); + if (shouldGrant) { // Record nonce as used before granting access if (storage.recordNonce) { await storage.recordNonce(payload.nonce); diff --git a/typescript/packages/extensions/src/sign-in-with-x/index.ts b/typescript/packages/extensions/src/sign-in-with-x/index.ts index 75dde2f561..1fe8b83241 100644 --- a/typescript/packages/extensions/src/sign-in-with-x/index.ts +++ b/typescript/packages/extensions/src/sign-in-with-x/index.ts @@ -5,65 +5,8 @@ * Allows clients to prove control of a wallet that may have previously paid * for a resource, enabling servers to grant access without requiring repurchase. * - * ## Server Usage - * - * ```typescript - * import { - * declareSIWxExtension, - * parseSIWxHeader, - * validateSIWxMessage, - * verifySIWxSignature, - * SIGN_IN_WITH_X, - * } from '@x402/extensions/sign-in-with-x'; - * - * // 1. Declare auth requirement in PaymentRequired response - * const extensions = declareSIWxExtension({ - * domain: 'api.example.com', - * resourceUri: 'https://api.example.com/data', - * network: 'eip155:8453', - * statement: 'Sign in to access your purchased content', - * }); - * - * // 2. Verify incoming proof - * const header = request.headers.get('SIGN-IN-WITH-X'); - * if (header) { - * const payload = parseSIWxHeader(header); - * - * const validation = await validateSIWxMessage( - * payload, - * 'https://api.example.com/data' - * ); - * - * if (validation.valid) { - * const verification = await verifySIWxSignature(payload); - * if (verification.valid) { - * // Authentication successful! - * // verification.address is the verified wallet - * } - * } - * } - * ``` - * - * ## Client Usage - * - * ```typescript - * import { - * createSIWxPayload, - * encodeSIWxHeader, - * } from '@x402/extensions/sign-in-with-x'; - * - * // 1. Get extension info from 402 response - * const serverInfo = paymentRequired.extensions['sign-in-with-x'].info; - * - * // 2. Create signed payload - * const payload = await createSIWxPayload(serverInfo, wallet); - * - * // 3. Encode for header - * const header = encodeSIWxHeader(payload); - * - * // 4. Send authenticated request - * fetch(url, { headers: { 'SIGN-IN-WITH-X': header } }); - * ``` + * Auth-only routes (accepts: []) are supported — the SIWX request hook + * grants access on a valid signature alone, no payment required. * * @module sign-in-with-x */ diff --git a/typescript/packages/extensions/src/sign-in-with-x/server.ts b/typescript/packages/extensions/src/sign-in-with-x/server.ts index 429f12d1c3..5135000cb9 100644 --- a/typescript/packages/extensions/src/sign-in-with-x/server.ts +++ b/typescript/packages/extensions/src/sign-in-with-x/server.ts @@ -6,7 +6,6 @@ * - Refresh time-based fields per request (nonce, issuedAt, expirationTime) */ -import { randomBytes } from "crypto"; import type { ResourceServerExtension, PaymentRequiredContext } from "@x402/core/types"; import type { SIWxExtension, SIWxExtensionInfo, SupportedChain, DeclareSIWxOptions } from "./types"; import { SIGN_IN_WITH_X } from "./types"; @@ -62,7 +61,9 @@ export const siwxResourceServerExtension: ResourceServerExtension = { } // Generate fresh time-based fields - const nonce = randomBytes(16).toString("hex"); + const nonce = Array.from(globalThis.crypto.getRandomValues(new Uint8Array(16))) + .map(b => b.toString(16).padStart(2, "0")) + .join(""); const issuedAt = new Date().toISOString(); // Calculate expirationTime based on configured duration diff --git a/typescript/packages/extensions/test/bazaar.test.ts b/typescript/packages/extensions/test/bazaar.test.ts index c72e642107..69ca52ce80 100644 --- a/typescript/packages/extensions/test/bazaar.test.ts +++ b/typescript/packages/extensions/test/bazaar.test.ts @@ -7,6 +7,7 @@ import { BAZAAR, declareDiscoveryExtension, validateDiscoveryExtension, + isValidRouteTemplate, extractDiscoveryInfo, extractDiscoveryInfoFromExtension, extractDiscoveryInfoV1, @@ -177,6 +178,7 @@ describe("Bazaar Discovery Extension", () => { describe("validateDiscoveryExtension", () => { it("should validate a correct GET extension", () => { const declared = declareDiscoveryExtension({ + method: "GET", input: { query: "test" }, inputSchema: { properties: { @@ -193,6 +195,7 @@ describe("Bazaar Discovery Extension", () => { it("should validate a correct POST extension", () => { const declared = declareDiscoveryExtension({ + method: "POST", input: { name: "John" }, inputSchema: { properties: { @@ -207,6 +210,19 @@ describe("Bazaar Discovery Extension", () => { expect(result.valid).toBe(true); }); + it("should fail validation when method is absent", () => { + // Per spec, method is required. An extension without method (e.g. pre-enrichment) + // must be rejected. + const declared = declareDiscoveryExtension({ + input: { query: "test" }, + inputSchema: { properties: { query: { type: "string" } } }, + }); + + const result = validateDiscoveryExtension(declared.bazaar); + expect(result.valid).toBe(false); + expect(result.errors?.some(e => e.includes("method"))).toBe(true); + }); + it("should detect invalid extension structure", () => { const invalidExtension = { info: { @@ -242,6 +258,7 @@ describe("Bazaar Discovery Extension", () => { describe("extractDiscoveryInfoFromExtension", () => { it("should extract info from a valid extension", () => { const declared = declareDiscoveryExtension({ + method: "GET", input: { query: "test" }, inputSchema: { properties: { @@ -306,6 +323,7 @@ describe("Bazaar Discovery Extension", () => { describe("extractDiscoveryInfo (full flow)", () => { it("should extract info from v2 PaymentPayload with extensions", () => { const declared = declareDiscoveryExtension({ + method: "POST", input: { userId: "123" }, inputSchema: { properties: { @@ -338,6 +356,7 @@ describe("Bazaar Discovery Extension", () => { it("should strip query params from v2 resourceUrl", () => { const declared = declareDiscoveryExtension({ + method: "GET", input: { city: "NYC" }, inputSchema: { properties: { @@ -374,6 +393,7 @@ describe("Bazaar Discovery Extension", () => { it("should strip hash sections from v2 resourceUrl", () => { const declared = declareDiscoveryExtension({ + method: "GET", input: {}, inputSchema: { properties: {} }, }); @@ -404,6 +424,7 @@ describe("Bazaar Discovery Extension", () => { it("should strip both query params and hash sections from v2 resourceUrl", () => { const declared = declareDiscoveryExtension({ + method: "GET", input: {}, inputSchema: { properties: {} }, }); @@ -558,6 +579,7 @@ describe("Bazaar Discovery Extension", () => { describe("validateAndExtract", () => { it("should return valid result with info for correct extension", () => { const declared = declareDiscoveryExtension({ + method: "GET", input: { query: "test" }, inputSchema: { properties: { @@ -921,6 +943,7 @@ describe("Bazaar Discovery Extension", () => { describe("Integration - Full workflow", () => { it("should handle GET endpoint with output schema (e2e scenario)", () => { const declared = declareDiscoveryExtension({ + method: "GET", input: {}, inputSchema: { properties: {}, @@ -962,6 +985,7 @@ describe("Bazaar Discovery Extension", () => { it("should handle complete v2 server-to-facilitator workflow", () => { const declared = declareDiscoveryExtension({ + method: "POST", input: { userId: "123", action: "create" }, inputSchema: { properties: { @@ -1067,6 +1091,7 @@ describe("Bazaar Discovery Extension", () => { it("should handle unified extraction for both v1 and v2", () => { const declared = declareDiscoveryExtension({ + method: "GET", input: { limit: 10 }, inputSchema: { properties: { @@ -1267,6 +1292,57 @@ describe("Bazaar Discovery Extension", () => { expect(required).toContain("method"); }); + it("should produce a valid extension after enrichment (GET)", () => { + const declared = declareDiscoveryExtension({ + input: { query: "test" }, + inputSchema: { properties: { query: { type: "string" } } }, + }); + + // Pre-enrichment: method not set, validation should fail + const preResult = validateDiscoveryExtension(declared.bazaar); + expect(preResult.valid).toBe(false); + + const httpContext: HTTPRequestContext = { + method: "GET", + path: "/test", + adapter: createMockAdapter(), + }; + + const enriched = bazaarResourceServerExtension.enrichDeclaration!( + declared.bazaar, + httpContext, + ) as DiscoveryExtension; + + // Post-enrichment: validation should pass + const postResult = validateDiscoveryExtension(enriched); + expect(postResult.valid).toBe(true); + }); + + it("should produce a valid extension after enrichment (POST)", () => { + const declared = declareDiscoveryExtension({ + input: { data: "test" }, + inputSchema: { properties: { data: { type: "string" } } }, + bodyType: "json", + }); + + const preResult = validateDiscoveryExtension(declared.bazaar); + expect(preResult.valid).toBe(false); + + const httpContext: HTTPRequestContext = { + method: "POST", + path: "/test", + adapter: createMockAdapter(), + }; + + const enriched = bazaarResourceServerExtension.enrichDeclaration!( + declared.bazaar, + httpContext, + ) as DiscoveryExtension; + + const postResult = validateDiscoveryExtension(enriched); + expect(postResult.valid).toBe(true); + }); + it("should return unchanged declaration for non-HTTP context", () => { const declared = declareDiscoveryExtension({ input: { data: "test" }, @@ -1590,4 +1666,391 @@ describe("Bazaar Discovery Extension", () => { expect((enriched.info as McpDiscoveryInfo).input.toolName).toBe("my_tool"); }); }); + + describe("dynamic routes", () => { + const createMockAdapterWithPath = (path: string): HTTPAdapter => ({ + getHeader: () => undefined, + getMethod: () => "GET", + getPath: () => path, + getUrl: () => `http://example.com${path}`, + getAcceptHeader: () => "application/json", + getUserAgent: () => "test-agent", + }); + + it("should leave static routes unchanged", () => { + const declared = declareDiscoveryExtension({ + input: { query: "test" }, + inputSchema: { properties: { query: { type: "string" } } }, + }); + const extension = declared.bazaar; + + const httpContext: HTTPRequestContext = { + method: "GET", + path: "/users", + routePattern: "/users", + adapter: createMockAdapterWithPath("/users"), + }; + + const enriched = bazaarResourceServerExtension.enrichDeclaration!( + extension, + httpContext, + ) as Record; + + expect(enriched.routeTemplate).toBeUndefined(); + }); + + it("should produce routeTemplate for dynamic routes", () => { + const declared = declareDiscoveryExtension({ + input: {}, + inputSchema: { properties: {} }, + }); + const extension = declared.bazaar; + + const httpContext: HTTPRequestContext = { + method: "GET", + path: "/users/123", + routePattern: "/users/[userId]", + adapter: createMockAdapterWithPath("/users/123"), + }; + + const enriched = bazaarResourceServerExtension.enrichDeclaration!( + extension, + httpContext, + ) as Record; + + expect(enriched.routeTemplate).toBe("/users/:userId"); + }); + + it("should extract path params from concrete URL", () => { + const declared = declareDiscoveryExtension({ + input: {}, + inputSchema: { properties: {} }, + }); + const extension = declared.bazaar; + + const httpContext: HTTPRequestContext = { + method: "GET", + path: "/users/123", + routePattern: "/users/[userId]", + adapter: createMockAdapterWithPath("/users/123"), + }; + + const enriched = bazaarResourceServerExtension.enrichDeclaration!( + extension, + httpContext, + ) as Record; + + const info = enriched.info as Record; + const input = info.input as Record; + expect(input.pathParams).toEqual({ userId: "123" }); + }); + + it("should extract multiple path params", () => { + const declared = declareDiscoveryExtension({ + input: {}, + inputSchema: { properties: {} }, + }); + const extension = declared.bazaar; + + const httpContext: HTTPRequestContext = { + method: "GET", + path: "/users/42/posts/7", + routePattern: "/users/[userId]/posts/[postId]", + adapter: createMockAdapterWithPath("/users/42/posts/7"), + }; + + const enriched = bazaarResourceServerExtension.enrichDeclaration!( + extension, + httpContext, + ) as Record; + + expect(enriched.routeTemplate).toBe("/users/:userId/posts/:postId"); + const info = enriched.info as Record; + const input = info.input as Record; + expect(input.pathParams).toEqual({ userId: "42", postId: "7" }); + }); + + it("should use routeTemplate for canonical URL in facilitator", () => { + const declared = declareDiscoveryExtension({ + input: {}, + inputSchema: { properties: {} }, + }); + const extension = declared.bazaar; + // Simulate enriched extension with routeTemplate + const enrichedExtension = { + ...extension, + routeTemplate: "/users/:userId", + info: { + ...extension.info, + input: { ...extension.info.input, pathParams: { userId: "123" } }, + }, + }; + + const paymentPayload = { + x402Version: 2, + scheme: "exact", + network: "eip155:8453" as unknown, + payload: {}, + accepted: {} as unknown, + resource: { url: "http://example.com/users/123" }, + extensions: { + [BAZAAR.key]: enrichedExtension, + }, + }; + + const discovered = extractDiscoveryInfo(paymentPayload, {} as unknown, false); + + expect(discovered).not.toBeNull(); + expect(discovered!.resourceUrl).toBe("http://example.com/users/:userId"); + // Narrow to DiscoveredHTTPResource to access routeTemplate (HTTP-only field) + expect((discovered as import("./..").DiscoveredHTTPResource).routeTemplate).toBe( + "/users/:userId", + ); + }); + + it("should return empty pathParams when URL path does not match pattern structure", () => { + const declared = declareDiscoveryExtension({ + input: {}, + inputSchema: { properties: {} }, + }); + const extension = declared.bazaar; + + // Pattern expects /users/[userId] but path is /api/other — structurally mismatched. + // This can occur in production if middleware and extension patterns diverge. + const httpContext: HTTPRequestContext = { + method: "GET", + path: "/api/other", + routePattern: "/users/[userId]", + adapter: createMockAdapterWithPath("/api/other"), + }; + + const enriched = bazaarResourceServerExtension.enrichDeclaration!( + extension, + httpContext, + ) as Record; + + const info = enriched.info as Record; + const input = info.input as Record; + // extractPathParams returns {} gracefully when pattern and URL structure don't match + expect(input.pathParams).toEqual({}); + }); + + it("should produce routeTemplate for :param style routes", () => { + const declared = declareDiscoveryExtension({ + input: {}, + inputSchema: { properties: {} }, + }); + const extension = declared.bazaar; + + const httpContext: HTTPRequestContext = { + method: "GET", + path: "/users/123", + routePattern: "/users/:userId", + adapter: createMockAdapterWithPath("/users/123"), + }; + + const enriched = bazaarResourceServerExtension.enrichDeclaration!( + extension, + httpContext, + ) as Record; + + expect(enriched.routeTemplate).toBe("/users/:userId"); + }); + + it("should extract path params from :param style routes", () => { + const declared = declareDiscoveryExtension({ + input: {}, + inputSchema: { properties: {} }, + }); + const extension = declared.bazaar; + + const httpContext: HTTPRequestContext = { + method: "GET", + path: "/users/42/posts/7", + routePattern: "/users/:userId/posts/:postId", + adapter: createMockAdapterWithPath("/users/42/posts/7"), + }; + + const enriched = bazaarResourceServerExtension.enrichDeclaration!( + extension, + httpContext, + ) as Record; + + expect(enriched.routeTemplate).toBe("/users/:userId/posts/:postId"); + const info = enriched.info as Record; + const input = info.input as Record; + expect(input.pathParams).toEqual({ userId: "42", postId: "7" }); + }); + + it("should auto-convert wildcard * to :varN for discovery", () => { + const declared = declareDiscoveryExtension({ + input: {}, + inputSchema: { properties: {} }, + }); + const extension = declared.bazaar; + + const httpContext: HTTPRequestContext = { + method: "GET", + path: "/weather/san-francisco", + routePattern: "/weather/*", + adapter: createMockAdapterWithPath("/weather/san-francisco"), + }; + + const enriched = bazaarResourceServerExtension.enrichDeclaration!( + extension, + httpContext, + ) as Record; + + expect(enriched.routeTemplate).toBe("/weather/:var1"); + const info = enriched.info as Record; + const input = info.input as Record; + expect(input.pathParams).toEqual({ var1: "san-francisco" }); + }); + + it("should auto-convert multiple wildcards to :var1, :var2, etc.", () => { + const declared = declareDiscoveryExtension({ + input: {}, + inputSchema: { properties: {} }, + }); + const extension = declared.bazaar; + + const httpContext: HTTPRequestContext = { + method: "GET", + path: "/api/users/42/posts/7", + routePattern: "/api/*/*/posts/*", + adapter: createMockAdapterWithPath("/api/users/42/posts/7"), + }; + + const enriched = bazaarResourceServerExtension.enrichDeclaration!( + extension, + httpContext, + ) as Record; + + expect(enriched.routeTemplate).toBe("/api/:var1/:var2/posts/:var3"); + }); + + it("should handle mixed [param] and :param patterns", () => { + const declared = declareDiscoveryExtension({ + input: {}, + inputSchema: { properties: {} }, + }); + const extension = declared.bazaar; + + const httpContext: HTTPRequestContext = { + method: "GET", + path: "/users/42/posts/7", + routePattern: "/users/[userId]/posts/:postId", + adapter: createMockAdapterWithPath("/users/42/posts/7"), + }; + + const enriched = bazaarResourceServerExtension.enrichDeclaration!( + extension, + httpContext, + ) as Record; + + expect(enriched.routeTemplate).toBe("/users/:userId/posts/:postId"); + const info = enriched.info as Record; + const input = info.input as Record; + expect(input.pathParams).toEqual({ userId: "42", postId: "7" }); + }); + + it("should pass schema validation after enrichment with auto-injected pathParams", () => { + const declared = declareDiscoveryExtension({ + input: {}, + inputSchema: { properties: {} }, + }); + const extension = declared.bazaar; + + const httpContext: HTTPRequestContext = { + method: "GET", + path: "/users/123", + routePattern: "/users/:userId", + adapter: createMockAdapterWithPath("/users/123"), + }; + + const enriched = bazaarResourceServerExtension.enrichDeclaration!( + extension, + httpContext, + ) as import("../src/bazaar/http/types").QueryDiscoveryExtension; + + const result = validateDiscoveryExtension(enriched); + expect(result.valid).toBe(true); + }); + + it("should use concrete URL for static routes in facilitator", () => { + const declared = declareDiscoveryExtension({ + input: { query: "test" }, + inputSchema: { properties: { query: { type: "string" } } }, + }); + const extension = declared.bazaar; + + const paymentPayload = { + x402Version: 2, + scheme: "exact", + network: "eip155:8453" as unknown, + payload: {}, + accepted: {} as unknown, + resource: { url: "http://example.com/search?q=test" }, + extensions: { + [BAZAAR.key]: extension, + }, + }; + + const discovered = extractDiscoveryInfo(paymentPayload, {} as unknown, false); + + expect(discovered).not.toBeNull(); + expect(discovered!.resourceUrl).toBe("http://example.com/search"); + // Narrow to DiscoveredHTTPResource to access routeTemplate (HTTP-only field) + expect((discovered as import("./..").DiscoveredHTTPResource).routeTemplate).toBeUndefined(); + }); + }); + + describe("isValidRouteTemplate", () => { + it("returns false for empty string", () => { + expect(isValidRouteTemplate("")).toBe(false); + }); + + it("returns false for undefined input", () => { + expect(isValidRouteTemplate(undefined)).toBe(false); + }); + + it("returns false for paths not starting with /", () => { + expect(isValidRouteTemplate("users/123")).toBe(false); + expect(isValidRouteTemplate("relative/path")).toBe(false); + expect(isValidRouteTemplate("no-slash")).toBe(false); + }); + + it("returns false for paths containing ..", () => { + expect(isValidRouteTemplate("/users/../admin")).toBe(false); + expect(isValidRouteTemplate("/../etc/passwd")).toBe(false); + expect(isValidRouteTemplate("/users/..")).toBe(false); + }); + + it("returns false for paths containing ://", () => { + expect(isValidRouteTemplate("http://evil.com/path")).toBe(false); + expect(isValidRouteTemplate("/users/http://evil")).toBe(false); + expect(isValidRouteTemplate("javascript://foo")).toBe(false); + }); + + it("returns true for valid paths", () => { + expect(isValidRouteTemplate("/users/:userId")).toBe(true); + expect(isValidRouteTemplate("/api/v1/items")).toBe(true); + expect(isValidRouteTemplate("/products/:productId/reviews/:reviewId")).toBe(true); + expect(isValidRouteTemplate("/weather/:country/:city")).toBe(true); + }); + + it("rejects paths with spaces or invalid characters", () => { + expect(isValidRouteTemplate("/users/ bad")).toBe(false); + expect(isValidRouteTemplate("/path with spaces")).toBe(false); + }); + + it("rejects /users/..hidden because it contains '..' as a substring", () => { + expect(isValidRouteTemplate("/users/..hidden")).toBe(false); + }); + + it("rejects percent-encoded traversal sequences", () => { + expect(isValidRouteTemplate("/users/%2e%2e/admin")).toBe(false); + expect(isValidRouteTemplate("/users/%2E%2E/admin")).toBe(false); + }); + }); }); diff --git a/typescript/packages/extensions/test/offer-receipt-test-utils.ts b/typescript/packages/extensions/test/offer-receipt-test-utils.ts new file mode 100644 index 0000000000..b526c09726 --- /dev/null +++ b/typescript/packages/extensions/test/offer-receipt-test-utils.ts @@ -0,0 +1,135 @@ +/** + * Test utilities for x402 Offer/Receipt Extension + * + * These are convenience functions for testing only. + * Production implementations should use HSM, TPM, or secure key management. + */ + +import * as jose from "jose"; +import { secp256k1 } from "@noble/curves/secp256k1"; +import type { JWSSigner } from "../src/offer-receipt/types"; + +// ============================================================================ +// P-256 (ES256) Utilities - Clean Web Crypto implementation +// ============================================================================ + +/** + * Create an ES256 (P-256) JWS signer from a CryptoKey (FOR TESTING ONLY) + * + * The signer's sign() function returns ONLY the raw base64url-encoded signature. + * The library's createJWS function is responsible for assembling the + * full JWS compact serialization (header.payload.signature). + * + * @param privateKey - The CryptoKey private key (P-256) + * @param kid - The key identifier + * @returns A JWS signer + */ +export function createES256Signer(privateKey: CryptoKey, kid: string): JWSSigner { + return { + kid, + algorithm: "ES256", + format: "jws", + sign: async (signingInput: Uint8Array): Promise => { + const signature = await crypto.subtle.sign( + { name: "ECDSA", hash: "SHA-256" }, + privateKey, + signingInput, + ); + return jose.base64url.encode(new Uint8Array(signature)); + }, + }; +} + +/** + * Generate a P-256 (ES256) key pair for testing + * + * Returns both the CryptoKey (for signing) and JWK (for verification). + * + * @returns Promise resolving to privateKey CryptoKey and publicKey JWK + */ +export async function generateES256KeyPair(): Promise<{ + privateKey: CryptoKey; + publicKeyJWK: jose.JWK; +}> { + const keyPair = await crypto.subtle.generateKey({ name: "ECDSA", namedCurve: "P-256" }, true, [ + "sign", + "verify", + ]); + + const publicKeyJWK = await crypto.subtle.exportKey("jwk", keyPair.publicKey); + + return { + privateKey: keyPair.privateKey, + publicKeyJWK, + }; +} + +// ============================================================================ +// secp256k1 (ES256K) Utilities - For EVM-compatible testing +// ============================================================================ + +/** + * SHA-256 hash using Web Crypto API + * + * @param data - The data to hash + * @returns The SHA-256 hash as Uint8Array + */ +async function sha256(data: Uint8Array): Promise { + const hashBuffer = await crypto.subtle.digest("SHA-256", data); + return new Uint8Array(hashBuffer); +} + +/** + * Create an ES256K (secp256k1) JWS signer from a JWK (FOR TESTING ONLY) + * + * @param jwk - The JWK private key + * @param kid - The key identifier + * @returns A JWS signer + */ +export async function createES256KSigner(jwk: jose.JWK, kid: string): Promise { + if (jwk.crv !== "secp256k1") { + throw new Error(`Unsupported curve: ${jwk.crv}. Use createJWSSigner for P-256.`); + } + if (!jwk.d) { + throw new Error("JWK must contain private key (d parameter)"); + } + + const privateKeyBytes = jose.base64url.decode(jwk.d); + + return { + kid, + algorithm: "ES256K", + format: "jws", + sign: async (signingInput: Uint8Array): Promise => { + const hash = await sha256(signingInput); + const signature = secp256k1.sign(hash, privateKeyBytes); + + // JWS uses concatenated r || s format (not DER) + const r = signature.r.toString(16).padStart(64, "0"); + const s = signature.s.toString(16).padStart(64, "0"); + const sigBytes = new Uint8Array(64); + for (let i = 0; i < 32; i++) { + sigBytes[i] = parseInt(r.slice(i * 2, i * 2 + 2), 16); + sigBytes[i + 32] = parseInt(s.slice(i * 2, i * 2 + 2), 16); + } + + return jose.base64url.encode(sigBytes); + }, + }; +} + +/** + * Generate an ES256K (secp256k1) key pair (FOR TESTING ONLY) + * + * @returns Promise resolving to an object with privateKey and publicKey JWKs + */ +export async function generateES256KKeyPair(): Promise<{ + privateKey: jose.JWK; + publicKey: jose.JWK; +}> { + const { privateKey, publicKey } = await jose.generateKeyPair("ES256K"); + return { + privateKey: await jose.exportJWK(privateKey), + publicKey: await jose.exportJWK(publicKey), + }; +} diff --git a/typescript/packages/extensions/test/offer-receipt.test.ts b/typescript/packages/extensions/test/offer-receipt.test.ts new file mode 100644 index 0000000000..504d98cfd7 --- /dev/null +++ b/typescript/packages/extensions/test/offer-receipt.test.ts @@ -0,0 +1,2144 @@ +/** + * Specification-driven tests for x402 Offer/Receipt Extension + */ +/* eslint-disable @typescript-eslint/no-explicit-any */ + +import { describe, it, expect, beforeAll, beforeEach, afterEach, vi } from "vitest"; +import * as jose from "jose"; +import { privateKeyToAccount } from "viem/accounts"; +import { recoverTypedDataAddress } from "viem"; +import type { Hex } from "viem"; + +import { + canonicalize, + hashCanonical, + getCanonicalBytes, + createJWS, + extractJWSHeader, + extractJWSPayload, + createOfferJWS, + createOfferEIP712, + extractOfferPayload, + createReceiptJWS, + createReceiptEIP712, + extractReceiptPayload, + createOfferDomain, + createReceiptDomain, + OFFER_TYPES, + RECEIPT_TYPES, + prepareOfferForEIP712, + prepareReceiptForEIP712, + hashOfferTypedData, + hashReceiptTypedData, + convertNetworkStringToCAIP2, + extractChainIdFromCAIP2, + extractEIP155ChainId, + extractOffersFromPaymentRequired, + decodeSignedOffers, + findAcceptsObjectFromSignedOffer, + extractReceiptFromResponse, + declareOfferReceiptExtension, + createJWSOfferReceiptIssuer, + createEIP712OfferReceiptIssuer, + verifyReceiptMatchesOffer, + verifyOfferSignatureEIP712, + verifyReceiptSignatureEIP712, + verifyOfferSignatureJWS, + verifyReceiptSignatureJWS, + extractPublicKeyFromKid, + OFFER_RECEIPT, + type JWSSigner, + type OfferPayload, + type ReceiptPayload, + type EIP712SignedOffer, + type EIP712SignedReceipt, + type JWSSignedOffer, +} from "../src/offer-receipt"; + +import { + createES256Signer, + generateES256KeyPair, + createES256KSigner, + generateES256KKeyPair, +} from "./offer-receipt-test-utils"; + +const TEST_PRIVATE_KEY = + "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80" as Hex; + +// ============================================================================ +// Core JWS Assembly Tests (Layer 1) +// These tests verify createJWS produces valid JWS compact serialization. +// Higher-level tests (createOfferJWS, createReceiptJWS) depend on this. +// ============================================================================ + +describe("createJWS (Core JWS Assembly)", () => { + let signer: JWSSigner; + let publicKeyJWK: jose.JWK; + + beforeAll(async () => { + const keyPair = await generateES256KeyPair(); + publicKeyJWK = keyPair.publicKeyJWK; + signer = createES256Signer(keyPair.privateKey, "did:web:example.com#key-1"); + }); + + it("produces valid JWS compact serialization (header.payload.signature)", async () => { + const payload = { test: "data", number: 42 }; + const jws = await createJWS(payload, signer); + + // Must be three dot-separated parts + const parts = jws.split("."); + expect(parts).toHaveLength(3); + expect(parts[0].length).toBeGreaterThan(0); // header + expect(parts[1].length).toBeGreaterThan(0); // payload + expect(parts[2].length).toBeGreaterThan(0); // signature + }); + + it("includes alg and kid in JWS header", async () => { + const payload = { test: "data" }; + const jws = await createJWS(payload, signer); + + const header = extractJWSHeader(jws); + expect(header.alg).toBe("ES256"); + expect(header.kid).toBe("did:web:example.com#key-1"); + }); + + it("encodes payload as canonicalized JSON", async () => { + // Keys in different order should produce same canonical form + const payload = { z: 1, a: 2 }; + const jws = await createJWS(payload, signer); + + const decoded = extractJWSPayload(jws); + expect(decoded).toEqual({ a: 2, z: 1 }); // Canonicalized order + }); + + it("produces signature verifiable with jose.compactVerify", async () => { + const payload = { resourceUrl: "https://example.com", amount: "1000" }; + const jws = await createJWS(payload, signer); + + const key = await jose.importJWK(publicKeyJWK); + const { payload: verifiedPayload } = await jose.compactVerify(jws, key); + const decoded = JSON.parse(new TextDecoder().decode(verifiedPayload)); + + expect(decoded.resourceUrl).toBe("https://example.com"); + expect(decoded.amount).toBe("1000"); + }); + + it("round-trips through extractJWSHeader and extractJWSPayload", async () => { + const payload = { version: 1, data: "test" }; + const jws = await createJWS(payload, signer); + + // Should be able to extract header and payload + const header = extractJWSHeader(jws); + const extractedPayload = extractJWSPayload(jws); + + expect(header.alg).toBe("ES256"); + expect(header.kid).toBe("did:web:example.com#key-1"); + expect(extractedPayload.version).toBe(1); + expect(extractedPayload.data).toBe("test"); + }); +}); + +describe("x402 Offer/Receipt Extension", () => { + describe("§3.1 Common Object Shape", () => { + describe("JWS format", () => { + let signer: JWSSigner; + beforeAll(async () => { + const keyPair = await generateES256KKeyPair(); + signer = await createES256KSigner(keyPair.privateKey, "did:web:example.com"); + }); + + it("JWS offer has format='jws', signature field, no payload field", async () => { + const offer = await createOfferJWS( + "https://api.example.com/resource", + { + acceptIndex: 0, + scheme: "exact", + network: "eip155:8453", + asset: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", + payTo: "0x1234567890123456789012345678901234567890", + amount: "10000", + }, + signer, + ); + expect(offer.format).toBe("jws"); + expect(offer.signature).toMatch(/^[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$/); + expect(offer).not.toHaveProperty("payload"); + }); + }); + + describe("EIP-712 format", () => { + const account = privateKeyToAccount(TEST_PRIVATE_KEY); + it("EIP-712 offer has format='eip712', payload field, hex signature", async () => { + const offer = await createOfferEIP712( + "https://api.example.com/resource", + { + acceptIndex: 0, + scheme: "exact", + network: "eip155:8453", + asset: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", + payTo: "0x1234567890123456789012345678901234567890", + amount: "10000", + }, + p => account.signTypedData(p), + ); + expect(offer.format).toBe("eip712"); + expect(offer).toHaveProperty("payload"); + expect(offer.signature).toMatch(/^0x[a-fA-F0-9]{130}$/); + }); + }); + }); + + describe("§3.2 EIP-712 Domain", () => { + const account = privateKeyToAccount(TEST_PRIVATE_KEY); + it("Offer domain: name='x402 offer', version='1', chainId=1 (canonical)", async () => { + await createOfferEIP712( + "https://api.example.com/resource", + { + acceptIndex: 0, + scheme: "exact", + network: "eip155:8453", + asset: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", + payTo: "0x1234567890123456789012345678901234567890", + amount: "10000", + }, + p => { + expect(p.domain.name).toBe("x402 offer"); + expect(p.domain.version).toBe("1"); + expect(Number(p.domain.chainId)).toBe(1); + return account.signTypedData(p); + }, + ); + }); + + it("Receipt domain: name='x402 receipt', version='1', chainId=1 (canonical)", async () => { + await createReceiptEIP712( + { + resourceUrl: "https://api.example.com/resource", + payer: "0xabc123", + network: "eip155:8453", + }, + p => { + expect(p.domain.name).toBe("x402 receipt"); + expect(p.domain.version).toBe("1"); + expect(Number(p.domain.chainId)).toBe(1); + return account.signTypedData(p); + }, + ); + }); + + it("EIP-712 chainId is constant regardless of payment network", async () => { + // Even with different payment networks, chainId should always be 1 + await createOfferEIP712( + "https://api.example.com/resource", + { + acceptIndex: 0, + scheme: "exact", + network: "eip155:137", // Polygon + asset: "native", + payTo: "0x1234567890123456789012345678901234567890", + amount: "10000", + }, + p => { + expect(Number(p.domain.chainId)).toBe(1); // Still 1, not 137 + return account.signTypedData(p); + }, + ); + }); + }); + + describe("§3.3 JWS Header Requirements", () => { + it("JWS header MUST include alg and kid", async () => { + const keyPair = await generateES256KKeyPair(); + const expectedKid = "did:web:api.example.com#key-1"; + const signer = await createES256KSigner(keyPair.privateKey, expectedKid); + const offer = await createOfferJWS( + "https://api.example.com/resource", + { + acceptIndex: 0, + scheme: "exact", + network: "eip155:8453", + asset: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", + payTo: "0x1234567890123456789012345678901234567890", + amount: "10000", + }, + signer, + ); + const header = JSON.parse( + new TextDecoder().decode(jose.base64url.decode(offer.signature.split(".")[0])), + ); + expect(header.alg).toBe("ES256K"); + expect(header.kid).toBe(expectedKid); + }); + }); + + describe("§4.2 Offer Payload Fields", () => { + it("Offer payload includes all required fields per spec v1.0", async () => { + const keyPair = await generateES256KKeyPair(); + const signer = await createES256KSigner(keyPair.privateKey, "did:web:example.com"); + const beforeCreate = Math.floor(Date.now() / 1000); + const offer = await createOfferJWS( + "https://api.example.com/premium", + { + acceptIndex: 0, + scheme: "exact", + network: "eip155:8453", + asset: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", + payTo: "0x209693Bc6afc0C5328bA36FaF03C514EF312287C", + amount: "10000", + offerValiditySeconds: 60, + }, + signer, + ); + const payload = extractOfferPayload(offer); + // Required fields per spec §4.2 + expect(payload.version).toBe(1); + expect(payload.resourceUrl).toBe("https://api.example.com/premium"); + expect(payload.scheme).toBe("exact"); + expect(payload.network).toBe("eip155:8453"); + expect(payload.asset).toBe("0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"); + expect(payload.payTo).toBe("0x209693Bc6afc0C5328bA36FaF03C514EF312287C"); + expect(payload.amount).toBe("10000"); + // validUntil should be approximately now + offerValiditySeconds + expect(payload.validUntil).toBeGreaterThanOrEqual(beforeCreate + 60); + expect(payload.validUntil).toBeLessThanOrEqual(beforeCreate + 62); // Allow 2s tolerance + }); + }); + + describe("§5.2 Receipt Payload Fields (Privacy-Minimal)", () => { + it("JWS receipt omits transaction when not provided (privacy-minimal)", async () => { + const keyPair = await generateES256KKeyPair(); + const signer = await createES256KSigner(keyPair.privateKey, "did:web:example.com"); + const receipt = await createReceiptJWS( + { + resourceUrl: "https://api.example.com/resource", + payer: "0x857b06519E91e3A54538791bDbb0E22373e36b66", + network: "eip155:8453", + }, + signer, + ); + const payload = extractReceiptPayload(receipt); + // Required fields per spec §5.2 + expect(payload.version).toBe(1); + expect(payload.network).toBe("eip155:8453"); + expect(payload.resourceUrl).toBe("https://api.example.com/resource"); + expect(payload.payer).toBe("0x857b06519E91e3A54538791bDbb0E22373e36b66"); + expect(typeof payload.issuedAt).toBe("number"); + // Per spec: transaction is optional, should be omitted in JWS when not provided + expect(payload).not.toHaveProperty("transaction"); + }); + + it("JWS receipt includes transaction when provided", async () => { + const keyPair = await generateES256KKeyPair(); + const signer = await createES256KSigner(keyPair.privateKey, "did:web:example.com"); + const receipt = await createReceiptJWS( + { + resourceUrl: "https://api.example.com/resource", + payer: "0x857b06519E91e3A54538791bDbb0E22373e36b66", + network: "eip155:8453", + transaction: "0xabc123", + }, + signer, + ); + const payload = extractReceiptPayload(receipt); + expect(payload.transaction).toBe("0xabc123"); + }); + + it("EIP-712 receipt uses empty string for transaction when not provided", async () => { + const account = privateKeyToAccount(TEST_PRIVATE_KEY); + const receipt = await createReceiptEIP712( + { + resourceUrl: "https://api.example.com/resource", + payer: "0x857b06519E91e3A54538791bDbb0E22373e36b66", + network: "eip155:8453", + }, + p => account.signTypedData(p), + ); + const payload = extractReceiptPayload(receipt); + // Per spec §5.3: EIP-712 MUST set unused optional fields to empty string + expect(payload.transaction).toBe(""); + }); + }); + + describe("JCS Canonicalization (RFC 8785)", () => { + it("sorts object keys lexicographically", () => { + expect(canonicalize({ z: 1, a: 2 })).toBe('{"a":2,"z":1}'); + }); + it("handles nested objects", () => { + expect(canonicalize({ b: { d: 1, c: 2 }, a: 3 })).toBe('{"a":3,"b":{"c":2,"d":1}}'); + }); + it("handles arrays (preserves order)", () => { + expect(canonicalize({ arr: [3, 1, 2] })).toBe('{"arr":[3,1,2]}'); + }); + it("handles -0 as 0", () => { + expect(canonicalize({ n: -0 })).toBe('{"n":0}'); + }); + }); + + describe("Cryptographic Verification", () => { + it("JWS signature verifies with jose.compactVerify", async () => { + const keyPair = await generateES256KKeyPair(); + const signer = await createES256KSigner(keyPair.privateKey, "did:web:example.com"); + const publicKey = await jose.importJWK(keyPair.publicKey); + + const offer = await createOfferJWS( + "https://api.example.com/resource", + { + acceptIndex: 0, + scheme: "exact", + network: "eip155:8453", + asset: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", + payTo: "0x1234567890123456789012345678901234567890", + amount: "10000", + }, + signer, + ); + + const { payload } = await jose.compactVerify(offer.signature, publicKey); + const decoded = JSON.parse(new TextDecoder().decode(payload)); + expect(decoded.resourceUrl).toBe("https://api.example.com/resource"); + }); + + it("EIP-712 signature recovers correct signer", async () => { + const account = privateKeyToAccount(TEST_PRIVATE_KEY); + + const offer = await createOfferEIP712( + "https://api.example.com/resource", + { + acceptIndex: 0, + scheme: "exact", + network: "eip155:8453", + asset: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", + payTo: "0x1234567890123456789012345678901234567890", + amount: "10000", + }, + p => account.signTypedData(p), + ); + + const recovered = await recoverTypedDataAddress({ + domain: createOfferDomain(), + types: OFFER_TYPES, + primaryType: "Offer", + message: prepareOfferForEIP712(offer.payload), + signature: offer.signature as Hex, + }); + + expect(recovered.toLowerCase()).toBe(account.address.toLowerCase()); + }); + }); +}); + +describe("Attestation Helper", () => { + describe("convertNetworkStringToCAIP2", () => { + it("passes through CAIP-2 format unchanged", () => { + expect(convertNetworkStringToCAIP2("eip155:8453")).toBe("eip155:8453"); + expect(convertNetworkStringToCAIP2("eip155:1")).toBe("eip155:1"); + expect(convertNetworkStringToCAIP2("solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp")).toBe( + "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp", + ); + }); + + it("converts v1 Solana network names", () => { + expect(convertNetworkStringToCAIP2("solana")).toBe("solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp"); + expect(convertNetworkStringToCAIP2("Solana")).toBe("solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp"); + expect(convertNetworkStringToCAIP2("solana-devnet")).toBe( + "solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1", + ); + }); + + it("converts v1 EVM network names to CAIP-2", () => { + expect(convertNetworkStringToCAIP2("base")).toBe("eip155:8453"); + expect(convertNetworkStringToCAIP2("base-sepolia")).toBe("eip155:84532"); + expect(convertNetworkStringToCAIP2("ethereum")).toBe("eip155:1"); + expect(convertNetworkStringToCAIP2("polygon")).toBe("eip155:137"); + expect(convertNetworkStringToCAIP2("avalanche")).toBe("eip155:43114"); + }); + + it("throws for unknown network identifiers", () => { + expect(() => convertNetworkStringToCAIP2("unknown-network")).toThrow( + 'Unknown network identifier: "unknown-network"', + ); + expect(() => convertNetworkStringToCAIP2("foo")).toThrow('Unknown network identifier: "foo"'); + }); + }); + + describe("extractChainIdFromCAIP2", () => { + it("extracts chain ID from EVM networks", () => { + expect(extractChainIdFromCAIP2("eip155:8453")).toBe(8453); + expect(extractChainIdFromCAIP2("eip155:1")).toBe(1); + expect(extractChainIdFromCAIP2("eip155:137")).toBe(137); + }); + + it("returns undefined for non-EVM networks", () => { + expect(extractChainIdFromCAIP2("solana:mainnet")).toBeUndefined(); + expect(extractChainIdFromCAIP2("cosmos:cosmoshub-4")).toBeUndefined(); + }); + }); + + describe("extractReceiptPayload", () => { + it("extracts payload from JWS receipt", async () => { + const keyPair = await generateES256KKeyPair(); + const signer = await createES256KSigner(keyPair.privateKey, "did:web:example.com"); + const receipt = await createReceiptJWS( + { + resourceUrl: "https://api.example.com/resource", + payer: "0x857b06519E91e3A54538791bDbb0E22373e36b66", + network: "eip155:8453", + }, + signer, + ); + + const payload = extractReceiptPayload(receipt); + expect(payload.resourceUrl).toBe("https://api.example.com/resource"); + expect(payload.payer).toBe("0x857b06519E91e3A54538791bDbb0E22373e36b66"); + expect(typeof payload.issuedAt).toBe("number"); + }); + + it("extracts payload from EIP-712 receipt", async () => { + const account = privateKeyToAccount(TEST_PRIVATE_KEY); + const receipt = await createReceiptEIP712( + { + resourceUrl: "https://api.example.com/resource", + payer: "0x857b06519E91e3A54538791bDbb0E22373e36b66", + network: "eip155:8453", + }, + p => account.signTypedData(p), + ); + + const payload = extractReceiptPayload(receipt); + expect(payload.resourceUrl).toBe("https://api.example.com/resource"); + expect(payload.payer).toBe("0x857b06519E91e3A54538791bDbb0E22373e36b66"); + }); + }); +}); + +describe("Client Utilities", () => { + describe("extractOffersFromPaymentRequired", () => { + it("extracts offers from PaymentRequired extensions", async () => { + const keyPair = await generateES256KKeyPair(); + const signer = await createES256KSigner(keyPair.privateKey, "did:web:example.com"); + + const offer1 = await createOfferJWS( + "https://api.example.com/resource", + { + acceptIndex: 0, + scheme: "exact", + network: "eip155:8453", + asset: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", + payTo: "0x1234567890123456789012345678901234567890", + amount: "10000", + }, + signer, + ); + + const paymentRequired = { + accepts: [ + { + scheme: "exact", + network: "eip155:8453", + asset: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", + payTo: "0x1234567890123456789012345678901234567890", + amount: "10000", + }, + ], + extensions: { + [OFFER_RECEIPT]: { + info: { + offers: [offer1], + }, + }, + }, + }; + + const offers = extractOffersFromPaymentRequired(paymentRequired as any); + expect(offers).toHaveLength(1); + expect(offers[0].format).toBe("jws"); + }); + + it("returns empty array when no offers present", () => { + const paymentRequired = { + accepts: [], + extensions: {}, + }; + + const offers = extractOffersFromPaymentRequired(paymentRequired as any); + expect(offers).toEqual([]); + }); + + it("returns empty array when extensions is undefined", () => { + const paymentRequired = { + accepts: [], + }; + + const offers = extractOffersFromPaymentRequired(paymentRequired as any); + expect(offers).toEqual([]); + }); + }); + + describe("decodeSignedOffers", () => { + it("decodes JWS offers with payload fields at top level", async () => { + const keyPair = await generateES256KKeyPair(); + const signer = await createES256KSigner(keyPair.privateKey, "did:web:example.com"); + + const offer = await createOfferJWS( + "https://api.example.com/resource", + { + acceptIndex: 0, + scheme: "exact", + network: "eip155:8453", + asset: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", + payTo: "0x1234567890123456789012345678901234567890", + amount: "10000", + }, + signer, + ); + + const decoded = decodeSignedOffers([offer]); + expect(decoded).toHaveLength(1); + expect(decoded[0].network).toBe("eip155:8453"); + expect(decoded[0].amount).toBe("10000"); + expect(decoded[0].format).toBe("jws"); + expect(decoded[0].acceptIndex).toBe(0); + expect(decoded[0].signedOffer).toBe(offer); + }); + + it("decodes EIP-712 offers", async () => { + const account = privateKeyToAccount(TEST_PRIVATE_KEY); + const offer = await createOfferEIP712( + "https://api.example.com/resource", + { + acceptIndex: 1, + scheme: "exact", + network: "eip155:8453", + asset: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", + payTo: "0x1234567890123456789012345678901234567890", + amount: "5000", + }, + p => account.signTypedData(p), + ); + + const decoded = decodeSignedOffers([offer]); + expect(decoded).toHaveLength(1); + expect(decoded[0].network).toBe("eip155:8453"); + expect(decoded[0].amount).toBe("5000"); + expect(decoded[0].format).toBe("eip712"); + expect(decoded[0].acceptIndex).toBe(1); + }); + + it("returns empty array for empty input", () => { + const decoded = decodeSignedOffers([]); + expect(decoded).toEqual([]); + }); + }); + + describe("findAcceptsObjectFromSignedOffer", () => { + it("finds matching accepts entry using acceptIndex hint", async () => { + const keyPair = await generateES256KKeyPair(); + const signer = await createES256KSigner(keyPair.privateKey, "did:web:example.com"); + + const offer = await createOfferJWS( + "https://api.example.com/resource", + { + acceptIndex: 0, + scheme: "exact", + network: "eip155:8453", + asset: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", + payTo: "0x1234567890123456789012345678901234567890", + amount: "10000", + }, + signer, + ); + + const accepts = [ + { + scheme: "exact", + network: "eip155:8453", + asset: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", + payTo: "0x1234567890123456789012345678901234567890", + amount: "10000", + maxAmountRequired: "10000", + }, + ]; + + const found = findAcceptsObjectFromSignedOffer(offer, accepts as any); + expect(found).toBeDefined(); + expect(found?.network).toBe("eip155:8453"); + }); + + it("finds matching accepts entry with DecodedOffer", async () => { + const keyPair = await generateES256KKeyPair(); + const signer = await createES256KSigner(keyPair.privateKey, "did:web:example.com"); + + const offer = await createOfferJWS( + "https://api.example.com/resource", + { + acceptIndex: 0, + scheme: "exact", + network: "eip155:8453", + asset: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", + payTo: "0x1234567890123456789012345678901234567890", + amount: "10000", + }, + signer, + ); + + const decoded = decodeSignedOffers([offer])[0]; + const accepts = [ + { + scheme: "exact", + network: "eip155:8453", + asset: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", + payTo: "0x1234567890123456789012345678901234567890", + amount: "10000", + maxAmountRequired: "10000", + }, + ]; + + const found = findAcceptsObjectFromSignedOffer(decoded, accepts as any); + expect(found).toBeDefined(); + expect(found?.network).toBe("eip155:8453"); + }); + + it("falls back to searching all accepts when hint misses", async () => { + const keyPair = await generateES256KKeyPair(); + const signer = await createES256KSigner(keyPair.privateKey, "did:web:example.com"); + + const offer = await createOfferJWS( + "https://api.example.com/resource", + { + acceptIndex: 5, // Wrong index + scheme: "exact", + network: "eip155:8453", + asset: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", + payTo: "0x1234567890123456789012345678901234567890", + amount: "10000", + }, + signer, + ); + + const accepts = [ + { + scheme: "exact", + network: "eip155:8453", + asset: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", + payTo: "0x1234567890123456789012345678901234567890", + amount: "10000", + maxAmountRequired: "10000", + }, + ]; + + const found = findAcceptsObjectFromSignedOffer(offer, accepts as any); + expect(found).toBeDefined(); + expect(found?.network).toBe("eip155:8453"); + }); + + it("returns undefined when no match found", async () => { + const keyPair = await generateES256KKeyPair(); + const signer = await createES256KSigner(keyPair.privateKey, "did:web:example.com"); + + const offer = await createOfferJWS( + "https://api.example.com/resource", + { + acceptIndex: 0, + scheme: "exact", + network: "eip155:8453", + asset: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", + payTo: "0x1234567890123456789012345678901234567890", + amount: "10000", + }, + signer, + ); + + const accepts = [ + { + scheme: "exact", + network: "eip155:1", // Different network + asset: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", + payTo: "0x1234567890123456789012345678901234567890", + amount: "10000", + maxAmountRequired: "10000", + }, + ]; + + const found = findAcceptsObjectFromSignedOffer(offer, accepts as any); + expect(found).toBeUndefined(); + }); + }); + + describe("extractReceiptFromResponse", () => { + it("extracts receipt from PAYMENT-RESPONSE header", async () => { + const keyPair = await generateES256KKeyPair(); + const signer = await createES256KSigner(keyPair.privateKey, "did:web:example.com"); + + const receipt = await createReceiptJWS( + { + resourceUrl: "https://api.example.com/resource", + payer: "0x857b06519E91e3A54538791bDbb0E22373e36b66", + network: "eip155:8453", + }, + signer, + ); + + const settlementResponse = { + success: true, + extensions: { + [OFFER_RECEIPT]: { + info: { receipt }, + }, + }, + }; + + const headers = new Headers(); + headers.set("PAYMENT-RESPONSE", btoa(JSON.stringify(settlementResponse))); + + const response = new Response("OK", { headers }); + const extracted = extractReceiptFromResponse(response); + + expect(extracted).toBeDefined(); + expect(extracted?.format).toBe("jws"); + }); + + it("extracts receipt from X-PAYMENT-RESPONSE header", async () => { + const keyPair = await generateES256KKeyPair(); + const signer = await createES256KSigner(keyPair.privateKey, "did:web:example.com"); + + const receipt = await createReceiptJWS( + { + resourceUrl: "https://api.example.com/resource", + payer: "0x857b06519E91e3A54538791bDbb0E22373e36b66", + network: "eip155:8453", + }, + signer, + ); + + const settlementResponse = { + success: true, + extensions: { + [OFFER_RECEIPT]: { + info: { receipt }, + }, + }, + }; + + const headers = new Headers(); + headers.set("X-PAYMENT-RESPONSE", btoa(JSON.stringify(settlementResponse))); + + const response = new Response("OK", { headers }); + const extracted = extractReceiptFromResponse(response); + + expect(extracted).toBeDefined(); + expect(extracted?.format).toBe("jws"); + }); + + it("returns undefined when no header present", () => { + const response = new Response("OK"); + const extracted = extractReceiptFromResponse(response); + expect(extracted).toBeUndefined(); + }); + + it("returns undefined when header has no receipt", () => { + const settlementResponse = { + success: true, + extensions: {}, + }; + + const headers = new Headers(); + headers.set("PAYMENT-RESPONSE", btoa(JSON.stringify(settlementResponse))); + + const response = new Response("OK", { headers }); + const extracted = extractReceiptFromResponse(response); + expect(extracted).toBeUndefined(); + }); + }); + + describe("verifyReceiptMatchesOffer", () => { + it("returns true when receipt matches offer and payer", async () => { + const keyPair = await generateES256KKeyPair(); + const signer = await createES256KSigner(keyPair.privateKey, "did:web:example.com"); + const payerAddress = "0x857b06519E91e3A54538791bDbb0E22373e36b66"; + + const offer = await createOfferJWS( + "https://api.example.com/resource", + { + acceptIndex: 0, + scheme: "exact", + network: "eip155:8453", + asset: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", + payTo: "0x1234567890123456789012345678901234567890", + amount: "10000", + }, + signer, + ); + + const receipt = await createReceiptJWS( + { + resourceUrl: "https://api.example.com/resource", + payer: payerAddress, + network: "eip155:8453", + }, + signer, + ); + + const decoded = decodeSignedOffers([offer])[0]; + const result = verifyReceiptMatchesOffer(receipt, decoded, [payerAddress]); + expect(result).toBe(true); + }); + + it("returns true with case-insensitive payer address match", async () => { + const keyPair = await generateES256KKeyPair(); + const signer = await createES256KSigner(keyPair.privateKey, "did:web:example.com"); + + const offer = await createOfferJWS( + "https://api.example.com/resource", + { + acceptIndex: 0, + scheme: "exact", + network: "eip155:8453", + asset: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", + payTo: "0x1234567890123456789012345678901234567890", + amount: "10000", + }, + signer, + ); + + const receipt = await createReceiptJWS( + { + resourceUrl: "https://api.example.com/resource", + payer: "0x857b06519E91e3A54538791bDbb0E22373e36b66", + network: "eip155:8453", + }, + signer, + ); + + const decoded = decodeSignedOffers([offer])[0]; + // Uppercase payer address should still match + const result = verifyReceiptMatchesOffer(receipt, decoded, [ + "0x857B06519E91E3A54538791BDBB0E22373E36B66", + ]); + expect(result).toBe(true); + }); + + it("returns false when resourceUrl does not match", async () => { + const keyPair = await generateES256KKeyPair(); + const signer = await createES256KSigner(keyPair.privateKey, "did:web:example.com"); + const payerAddress = "0x857b06519E91e3A54538791bDbb0E22373e36b66"; + + const offer = await createOfferJWS( + "https://api.example.com/resource", + { + acceptIndex: 0, + scheme: "exact", + network: "eip155:8453", + asset: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", + payTo: "0x1234567890123456789012345678901234567890", + amount: "10000", + }, + signer, + ); + + const receipt = await createReceiptJWS( + { + resourceUrl: "https://api.example.com/different-resource", + payer: payerAddress, + network: "eip155:8453", + }, + signer, + ); + + const decoded = decodeSignedOffers([offer])[0]; + const result = verifyReceiptMatchesOffer(receipt, decoded, [payerAddress]); + expect(result).toBe(false); + }); + + it("returns false when network does not match", async () => { + const keyPair = await generateES256KKeyPair(); + const signer = await createES256KSigner(keyPair.privateKey, "did:web:example.com"); + const payerAddress = "0x857b06519E91e3A54538791bDbb0E22373e36b66"; + + const offer = await createOfferJWS( + "https://api.example.com/resource", + { + acceptIndex: 0, + scheme: "exact", + network: "eip155:8453", + asset: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", + payTo: "0x1234567890123456789012345678901234567890", + amount: "10000", + }, + signer, + ); + + const receipt = await createReceiptJWS( + { + resourceUrl: "https://api.example.com/resource", + payer: payerAddress, + network: "eip155:1", // Different network + }, + signer, + ); + + const decoded = decodeSignedOffers([offer])[0]; + const result = verifyReceiptMatchesOffer(receipt, decoded, [payerAddress]); + expect(result).toBe(false); + }); + + it("returns false when payer does not match any address", async () => { + const keyPair = await generateES256KKeyPair(); + const signer = await createES256KSigner(keyPair.privateKey, "did:web:example.com"); + + const offer = await createOfferJWS( + "https://api.example.com/resource", + { + acceptIndex: 0, + scheme: "exact", + network: "eip155:8453", + asset: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", + payTo: "0x1234567890123456789012345678901234567890", + amount: "10000", + }, + signer, + ); + + const receipt = await createReceiptJWS( + { + resourceUrl: "https://api.example.com/resource", + payer: "0x857b06519E91e3A54538791bDbb0E22373e36b66", + network: "eip155:8453", + }, + signer, + ); + + const decoded = decodeSignedOffers([offer])[0]; + // Different payer address + const result = verifyReceiptMatchesOffer(receipt, decoded, [ + "0xDifferentAddress1234567890123456789012345", + ]); + expect(result).toBe(false); + }); + + it("returns true when payer matches one of multiple addresses", async () => { + const keyPair = await generateES256KKeyPair(); + const signer = await createES256KSigner(keyPair.privateKey, "did:web:example.com"); + const payerAddress = "0x857b06519E91e3A54538791bDbb0E22373e36b66"; + + const offer = await createOfferJWS( + "https://api.example.com/resource", + { + acceptIndex: 0, + scheme: "exact", + network: "eip155:8453", + asset: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", + payTo: "0x1234567890123456789012345678901234567890", + amount: "10000", + }, + signer, + ); + + const receipt = await createReceiptJWS( + { + resourceUrl: "https://api.example.com/resource", + payer: payerAddress, + network: "eip155:8453", + }, + signer, + ); + + const decoded = decodeSignedOffers([offer])[0]; + // Multiple addresses, one matches + const result = verifyReceiptMatchesOffer(receipt, decoded, [ + "0xOtherAddress12345678901234567890123456789", + payerAddress, + "SolanaAddressHere", + ]); + expect(result).toBe(true); + }); + }); +}); + +describe("Utility Functions", () => { + describe("hashCanonical", () => { + it("returns SHA-256 hash of canonicalized object", async () => { + const hash = await hashCanonical({ b: 2, a: 1 }); + expect(hash).toBeInstanceOf(Uint8Array); + expect(hash.length).toBe(32); // SHA-256 produces 32 bytes + }); + + it("produces same hash for equivalent objects with different key order", async () => { + const hash1 = await hashCanonical({ z: 1, a: 2 }); + const hash2 = await hashCanonical({ a: 2, z: 1 }); + expect(hash1).toEqual(hash2); + }); + + it("produces different hashes for different objects", async () => { + const hash1 = await hashCanonical({ a: 1 }); + const hash2 = await hashCanonical({ a: 2 }); + expect(hash1).not.toEqual(hash2); + }); + }); + + describe("getCanonicalBytes", () => { + it("returns UTF-8 encoded canonical JSON", () => { + const bytes = getCanonicalBytes({ b: 2, a: 1 }); + expect(bytes).toBeInstanceOf(Uint8Array); + const decoded = new TextDecoder().decode(bytes); + expect(decoded).toBe('{"a":1,"b":2}'); + }); + + it("handles nested objects", () => { + const bytes = getCanonicalBytes({ outer: { z: 1, a: 2 } }); + const decoded = new TextDecoder().decode(bytes); + expect(decoded).toBe('{"outer":{"a":2,"z":1}}'); + }); + }); + + describe("hashOfferTypedData", () => { + it("returns EIP-712 hash for offer payload", () => { + const payload: OfferPayload = { + version: 1, + resourceUrl: "https://api.example.com/resource", + scheme: "exact", + network: "eip155:8453", + asset: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", + payTo: "0x1234567890123456789012345678901234567890", + amount: "10000", + validUntil: 1700000000, + }; + const hash = hashOfferTypedData(payload); + expect(hash).toMatch(/^0x[a-fA-F0-9]{64}$/); + }); + }); + + describe("hashReceiptTypedData", () => { + it("returns EIP-712 hash for receipt payload", () => { + const payload: ReceiptPayload = { + version: 1, + network: "eip155:8453", + resourceUrl: "https://api.example.com/resource", + payer: "0x857b06519E91e3A54538791bDbb0E22373e36b66", + issuedAt: 1700000000, + transaction: "", + }; + const hash = hashReceiptTypedData(payload); + expect(hash).toMatch(/^0x[a-fA-F0-9]{64}$/); + }); + + it("produces different hashes for different payloads", () => { + const payload1: ReceiptPayload = { + version: 1, + network: "eip155:8453", + resourceUrl: "https://api.example.com/resource", + payer: "0x857b06519E91e3A54538791bDbb0E22373e36b66", + issuedAt: 1700000000, + transaction: "", + }; + const payload2: ReceiptPayload = { + ...payload1, + payer: "0x1234567890123456789012345678901234567890", + }; + const hash1 = hashReceiptTypedData(payload1); + const hash2 = hashReceiptTypedData(payload2); + expect(hash1).not.toBe(hash2); + }); + }); + + describe("extractEIP155ChainId", () => { + it("extracts chain ID from valid eip155 network string", () => { + expect(extractEIP155ChainId("eip155:8453")).toBe(8453); + expect(extractEIP155ChainId("eip155:1")).toBe(1); + expect(extractEIP155ChainId("eip155:137")).toBe(137); + }); + + it("throws for non-eip155 networks", () => { + expect(() => extractEIP155ChainId("solana:mainnet")).toThrow( + 'Invalid network format: solana:mainnet. Expected "eip155:"', + ); + }); + + it("throws for malformed eip155 strings", () => { + expect(() => extractEIP155ChainId("eip155:")).toThrow( + 'Invalid network format: eip155:. Expected "eip155:"', + ); + expect(() => extractEIP155ChainId("eip155:abc")).toThrow( + 'Invalid network format: eip155:abc. Expected "eip155:"', + ); + }); + + it("throws for strings without colon", () => { + expect(() => extractEIP155ChainId("base")).toThrow( + 'Invalid network format: base. Expected "eip155:"', + ); + }); + }); + + describe("createReceiptDomain", () => { + it("creates receipt domain with correct name, version, and canonical chainId", () => { + const domain = createReceiptDomain(); + expect(domain.name).toBe("x402 receipt"); + expect(domain.version).toBe("1"); + expect(domain.chainId).toBe(1); + }); + }); + + describe("prepareReceiptForEIP712", () => { + it("converts receipt payload to EIP-712 message format", () => { + const payload: ReceiptPayload = { + version: 1, + network: "eip155:8453", + resourceUrl: "https://api.example.com/resource", + payer: "0x857b06519E91e3A54538791bDbb0E22373e36b66", + issuedAt: 1700000000, + transaction: "0xabc123", + }; + const prepared = prepareReceiptForEIP712(payload); + expect(prepared.version).toBe(BigInt(1)); + expect(prepared.network).toBe("eip155:8453"); + expect(prepared.resourceUrl).toBe("https://api.example.com/resource"); + expect(prepared.payer).toBe("0x857b06519E91e3A54538791bDbb0E22373e36b66"); + expect(prepared.issuedAt).toBe(BigInt(1700000000)); + expect(prepared.transaction).toBe("0xabc123"); + }); + }); + + describe("RECEIPT_TYPES", () => { + it("has correct EIP-712 type definition", () => { + expect(RECEIPT_TYPES.Receipt).toBeDefined(); + expect(RECEIPT_TYPES.Receipt).toHaveLength(6); + const fieldNames = RECEIPT_TYPES.Receipt.map(f => f.name); + expect(fieldNames).toContain("version"); + expect(fieldNames).toContain("network"); + expect(fieldNames).toContain("resourceUrl"); + expect(fieldNames).toContain("payer"); + expect(fieldNames).toContain("issuedAt"); + expect(fieldNames).toContain("transaction"); + }); + }); +}); + +describe("Server Extension Utilities", () => { + describe("declareOfferReceiptExtension", () => { + it("returns extension declaration with default values", () => { + const declaration = declareOfferReceiptExtension(); + expect(declaration).toHaveProperty(OFFER_RECEIPT); + expect(declaration[OFFER_RECEIPT].includeTxHash).toBeUndefined(); + expect(declaration[OFFER_RECEIPT].offerValiditySeconds).toBeUndefined(); + }); + + it("returns extension declaration with custom config", () => { + const declaration = declareOfferReceiptExtension({ + includeTxHash: true, + offerValiditySeconds: 120, + }); + expect(declaration[OFFER_RECEIPT].includeTxHash).toBe(true); + expect(declaration[OFFER_RECEIPT].offerValiditySeconds).toBe(120); + }); + }); + + describe("createJWSOfferReceiptIssuer", () => { + it("creates issuer with correct properties", async () => { + const keyPair = await generateES256KKeyPair(); + const jwsSigner = await createES256KSigner( + keyPair.privateKey, + "did:web:api.example.com#key-1", + ); + + const issuer = createJWSOfferReceiptIssuer("did:web:api.example.com#key-1", jwsSigner); + + expect(issuer.kid).toBe("did:web:api.example.com#key-1"); + expect(issuer.format).toBe("jws"); + expect(typeof issuer.issueOffer).toBe("function"); + expect(typeof issuer.issueReceipt).toBe("function"); + }); + + it("issueOffer creates valid JWS offer", async () => { + const keyPair = await generateES256KKeyPair(); + const jwsSigner = await createES256KSigner( + keyPair.privateKey, + "did:web:api.example.com#key-1", + ); + const issuer = createJWSOfferReceiptIssuer("did:web:api.example.com#key-1", jwsSigner); + + const offer = await issuer.issueOffer("https://api.example.com/resource", { + acceptIndex: 0, + scheme: "exact", + network: "eip155:8453", + asset: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", + payTo: "0x1234567890123456789012345678901234567890", + amount: "10000", + }); + + expect(offer.format).toBe("jws"); + expect(offer.signature).toMatch(/^[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$/); + }); + + it("issueReceipt creates valid JWS receipt", async () => { + const keyPair = await generateES256KKeyPair(); + const jwsSigner = await createES256KSigner( + keyPair.privateKey, + "did:web:api.example.com#key-1", + ); + const issuer = createJWSOfferReceiptIssuer("did:web:api.example.com#key-1", jwsSigner); + + const receipt = await issuer.issueReceipt( + "https://api.example.com/resource", + "0x857b06519E91e3A54538791bDbb0E22373e36b66", + "eip155:8453", + "0xabc123", + ); + + expect(receipt.format).toBe("jws"); + expect(receipt.signature).toMatch(/^[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$/); + }); + }); + + describe("createEIP712OfferReceiptIssuer", () => { + const account = privateKeyToAccount(TEST_PRIVATE_KEY); + + it("creates issuer with correct properties", () => { + const issuer = createEIP712OfferReceiptIssuer(`did:pkh:eip155:8453:${account.address}`, p => + account.signTypedData(p), + ); + + expect(issuer.kid).toBe(`did:pkh:eip155:8453:${account.address}`); + expect(issuer.format).toBe("eip712"); + expect(typeof issuer.issueOffer).toBe("function"); + expect(typeof issuer.issueReceipt).toBe("function"); + }); + + it("issueOffer creates valid EIP-712 offer", async () => { + const issuer = createEIP712OfferReceiptIssuer(`did:pkh:eip155:8453:${account.address}`, p => + account.signTypedData(p), + ); + + const offer = await issuer.issueOffer("https://api.example.com/resource", { + acceptIndex: 0, + scheme: "exact", + network: "eip155:8453", + asset: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", + payTo: "0x1234567890123456789012345678901234567890", + amount: "10000", + }); + + expect(offer.format).toBe("eip712"); + expect(offer).toHaveProperty("payload"); + expect(offer.signature).toMatch(/^0x[a-fA-F0-9]{130}$/); + }); + + it("issueReceipt creates valid EIP-712 receipt", async () => { + const issuer = createEIP712OfferReceiptIssuer(`did:pkh:eip155:8453:${account.address}`, p => + account.signTypedData(p), + ); + + const receipt = await issuer.issueReceipt( + "https://api.example.com/resource", + "0x857b06519E91e3A54538791bDbb0E22373e36b66", + "eip155:8453", + "0xabc123", + ); + + expect(receipt.format).toBe("eip712"); + expect(receipt).toHaveProperty("payload"); + expect(receipt.signature).toMatch(/^0x[a-fA-F0-9]{130}$/); + }); + }); + + /** + * NOTE: createOfferReceiptExtension is not tested here because it requires + * a mock ResourceServer with PaymentRequiredContext and SettleResultContext. + * The extension hooks (enrichPaymentRequiredResponse, enrichSettlementResponse) + * depend on the full server context which would require significant mocking. + * The signer factories above test the core signing functionality. + */ +}); + +// ============================================================================ +// Signature Verification Tests +// ============================================================================ + +describe("Signature Verification", () => { + describe("EIP-712 Verification", () => { + describe("verifyOfferSignatureEIP712", () => { + it("should verify a valid EIP-712 signed offer and recover signer", async () => { + const account = privateKeyToAccount(TEST_PRIVATE_KEY); + + const offer = await createOfferEIP712( + "https://api.example.com/resource", + { + acceptIndex: 0, + scheme: "exact", + network: "eip155:8453", + asset: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", + payTo: "0x209693Bc6afc0C5328bA36FaF03C514EF312287C", + amount: "10000", + }, + p => account.signTypedData(p), + ); + + const result = await verifyOfferSignatureEIP712(offer); + + expect(result.signer.toLowerCase()).toBe(account.address.toLowerCase()); + expect(result.payload.resourceUrl).toBe("https://api.example.com/resource"); + expect(result.payload.scheme).toBe("exact"); + expect(result.payload.network).toBe("eip155:8453"); + expect(result.payload.amount).toBe("10000"); + }); + + it("should throw for wrong format", async () => { + const invalidOffer = { + format: "jws", + signature: "test.jws.signature", + } as unknown as EIP712SignedOffer; + + await expect(verifyOfferSignatureEIP712(invalidOffer)).rejects.toThrow( + "Expected eip712 format", + ); + }); + + it("should throw for invalid offer payload", async () => { + const invalidOffer = { + format: "eip712", + payload: null, + signature: "0x1234", + } as unknown as EIP712SignedOffer; + + await expect(verifyOfferSignatureEIP712(invalidOffer)).rejects.toThrow( + "Invalid offer: missing or malformed payload", + ); + }); + + it("should recover different address for tampered signature", async () => { + const account = privateKeyToAccount(TEST_PRIVATE_KEY); + + const offer = await createOfferEIP712( + "https://api.example.com/resource", + { + acceptIndex: 0, + scheme: "exact", + network: "eip155:8453", + asset: "native", + payTo: "0x209693Bc6afc0C5328bA36FaF03C514EF312287C", + amount: "10000", + }, + p => account.signTypedData(p), + ); + + // Tamper with the signature + const tamperedOffer = { + ...offer, + signature: offer.signature.slice(0, -4) + "0000", + }; + + // Should recover a different address (not throw) + const result = await verifyOfferSignatureEIP712(tamperedOffer); + expect(result.signer).toBeDefined(); + // The recovered address will likely be different + }); + }); + + describe("verifyReceiptSignatureEIP712", () => { + it("should verify a valid EIP-712 signed receipt and recover signer", async () => { + const account = privateKeyToAccount(TEST_PRIVATE_KEY); + + const receipt = await createReceiptEIP712( + { + resourceUrl: "https://api.example.com/resource", + payer: "0x857b06519E91e3A54538791bDbb0E22373e36b66", + network: "eip155:8453", + transaction: "0x1234567890abcdef", + }, + p => account.signTypedData(p), + ); + + const result = await verifyReceiptSignatureEIP712(receipt); + + expect(result.signer.toLowerCase()).toBe(account.address.toLowerCase()); + expect(result.payload.resourceUrl).toBe("https://api.example.com/resource"); + expect(result.payload.payer).toBe("0x857b06519E91e3A54538791bDbb0E22373e36b66"); + expect(result.payload.network).toBe("eip155:8453"); + }); + + it("should throw for wrong format", async () => { + const invalidReceipt = { + format: "jws", + signature: "test.jws.signature", + } as unknown as EIP712SignedReceipt; + + await expect(verifyReceiptSignatureEIP712(invalidReceipt)).rejects.toThrow( + "Expected eip712 format", + ); + }); + + it("should throw for invalid receipt payload", async () => { + const invalidReceipt = { + format: "eip712", + payload: { version: 1 }, // missing payer + signature: "0x1234", + } as unknown as EIP712SignedReceipt; + + await expect(verifyReceiptSignatureEIP712(invalidReceipt)).rejects.toThrow( + "Invalid receipt: missing or malformed payload", + ); + }); + }); + }); + + describe("JWS Verification", () => { + describe("verifyOfferSignatureJWS", () => { + it("should verify a JWS signed offer with explicit public key", async () => { + const keyPair = await generateES256KKeyPair(); + const signer = await createES256KSigner(keyPair.privateKey, "did:web:example.com"); + + const offer = await createOfferJWS( + "https://api.example.com/resource", + { + acceptIndex: 0, + scheme: "exact", + network: "eip155:8453", + asset: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", + payTo: "0x209693Bc6afc0C5328bA36FaF03C514EF312287C", + amount: "10000", + }, + signer, + ); + + // Pass JWK directly - function accepts both KeyLike and JWK + const payload = await verifyOfferSignatureJWS(offer, keyPair.publicKey); + + expect(payload.resourceUrl).toBe("https://api.example.com/resource"); + expect(payload.scheme).toBe("exact"); + expect(payload.amount).toBe("10000"); + }); + + it("should verify a JWS signed offer with JWK public key", async () => { + const keyPair = await generateES256KKeyPair(); + const signer = await createES256KSigner(keyPair.privateKey, "did:web:example.com"); + + const offer = await createOfferJWS( + "https://api.example.com/resource", + { + acceptIndex: 0, + scheme: "exact", + network: "eip155:8453", + asset: "native", + payTo: "0x209693Bc6afc0C5328bA36FaF03C514EF312287C", + amount: "5000", + }, + signer, + ); + + const payload = await verifyOfferSignatureJWS(offer, keyPair.publicKey); + + expect(payload.resourceUrl).toBe("https://api.example.com/resource"); + expect(payload.amount).toBe("5000"); + }); + + it("should verify a JWS signed offer by extracting key from did:jwk kid", async () => { + const keyPair = await generateES256KKeyPair(); + // Create signer with did:jwk kid (self-contained key) + const kid = `did:jwk:${jose.base64url.encode(JSON.stringify(keyPair.publicKey))}#0`; + const signer = await createES256KSigner(keyPair.privateKey, kid); + + const offer = await createOfferJWS( + "https://api.example.com/resource", + { + acceptIndex: 0, + scheme: "exact", + network: "eip155:8453", + asset: "native", + payTo: "0x209693Bc6afc0C5328bA36FaF03C514EF312287C", + amount: "7500", + }, + signer, + ); + + // No public key provided - should extract from kid + const payload = await verifyOfferSignatureJWS(offer); + + expect(payload.resourceUrl).toBe("https://api.example.com/resource"); + expect(payload.amount).toBe("7500"); + }); + + it("should throw for wrong format", async () => { + const invalidOffer = { + format: "eip712", + payload: {}, + signature: "0x1234", + } as unknown as JWSSignedOffer; + + await expect(verifyOfferSignatureJWS(invalidOffer)).rejects.toThrow("Expected jws format"); + }); + + it("should throw for invalid JWS signature", async () => { + const keyPair = await generateES256KKeyPair(); + + const invalidOffer: JWSSignedOffer = { + format: "jws", + signature: "invalid.jws.signature", + }; + + // Pass JWK directly + await expect(verifyOfferSignatureJWS(invalidOffer, keyPair.publicKey)).rejects.toThrow(); + }); + + it("should throw when no key provided and kid missing", async () => { + const { privateKey } = await jose.generateKeyPair("ES256K"); + const payload = JSON.stringify({ version: 1, resourceUrl: "test" }); + const jws = await new jose.CompactSign(new TextEncoder().encode(payload)) + .setProtectedHeader({ alg: "ES256K" }) // No kid + .sign(privateKey); + + const offer: JWSSignedOffer = { format: "jws", signature: jws }; + + await expect(verifyOfferSignatureJWS(offer)).rejects.toThrow( + "No public key provided and JWS header missing kid", + ); + }); + }); + + describe("verifyReceiptSignatureJWS", () => { + it("should verify a JWS signed receipt", async () => { + const keyPair = await generateES256KKeyPair(); + const signer = await createES256KSigner(keyPair.privateKey, "did:web:example.com"); + + const receipt = await createReceiptJWS( + { + resourceUrl: "https://api.example.com/resource", + payer: "0x857b06519E91e3A54538791bDbb0E22373e36b66", + network: "eip155:8453", + }, + signer, + ); + + // Pass JWK directly + const payload = await verifyReceiptSignatureJWS(receipt, keyPair.publicKey); + + expect(payload.resourceUrl).toBe("https://api.example.com/resource"); + expect(payload.payer).toBe("0x857b06519E91e3A54538791bDbb0E22373e36b66"); + expect(payload.network).toBe("eip155:8453"); + }); + + it("should verify a JWS signed receipt by extracting key from did:jwk kid", async () => { + const keyPair = await generateES256KKeyPair(); + const kid = `did:jwk:${jose.base64url.encode(JSON.stringify(keyPair.publicKey))}#0`; + const signer = await createES256KSigner(keyPair.privateKey, kid); + + const receipt = await createReceiptJWS( + { + resourceUrl: "https://api.example.com/resource", + payer: "0x857b06519E91e3A54538791bDbb0E22373e36b66", + network: "eip155:8453", + transaction: "0xabcdef", + }, + signer, + ); + + // No public key provided - should extract from kid + const payload = await verifyReceiptSignatureJWS(receipt); + + expect(payload.resourceUrl).toBe("https://api.example.com/resource"); + expect(payload.transaction).toBe("0xabcdef"); + }); + }); + }); +}); + +// ============================================================================ +// DID Key Resolution Tests +// ============================================================================ + +describe("extractPublicKeyFromKid", () => { + describe("did:jwk", () => { + it("should extract key from did:jwk", async () => { + const { publicKey } = await jose.generateKeyPair("ES256K"); + const jwk = await jose.exportJWK(publicKey); + const kid = `did:jwk:${jose.base64url.encode(JSON.stringify(jwk))}`; + + const extractedKey = await extractPublicKeyFromKid(kid); + expect(extractedKey).toBeDefined(); + }); + + it("should handle did:jwk with fragment", async () => { + const { publicKey } = await jose.generateKeyPair("ES256"); + const jwk = await jose.exportJWK(publicKey); + const kid = `did:jwk:${jose.base64url.encode(JSON.stringify(jwk))}#key-1`; + + const extractedKey = await extractPublicKeyFromKid(kid); + expect(extractedKey).toBeDefined(); + }); + }); + + describe("did:key", () => { + it("should extract Ed25519 key from did:key", async () => { + // Known Ed25519 did:key (from did-key spec examples) + const kid = "did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK"; + + const extractedKey = await extractPublicKeyFromKid(kid); + expect(extractedKey).toBeDefined(); + }); + }); + + describe("error cases", () => { + it("should throw for invalid DID format", async () => { + await expect(extractPublicKeyFromKid("not-a-did")).rejects.toThrow("Invalid DID format"); + }); + + it("should throw for unsupported DID method", async () => { + await expect(extractPublicKeyFromKid("did:unsupported:123")).rejects.toThrow( + 'Unsupported DID method "unsupported"', + ); + }); + + it("should throw for did:key with unsupported multibase", async () => { + await expect(extractPublicKeyFromKid("did:key:f1234")).rejects.toThrow( + "Unsupported multibase encoding", + ); + }); + }); + + describe("did:web", () => { + let originalFetch: typeof global.fetch; + + beforeEach(() => { + originalFetch = global.fetch; + }); + + afterEach(() => { + global.fetch = originalFetch; + }); + + it("should resolve did:web by fetching DID document", async () => { + const { publicKey } = await jose.generateKeyPair("ES256K"); + const jwk = await jose.exportJWK(publicKey); + + const didDocument = { + id: "did:web:api.example.com", + verificationMethod: [ + { + id: "did:web:api.example.com#key-1", + type: "JsonWebKey2020", + controller: "did:web:api.example.com", + publicKeyJwk: jwk, + }, + ], + }; + + global.fetch = vi.fn().mockResolvedValue({ + ok: true, + json: () => Promise.resolve(didDocument), + }); + + const extractedKey = await extractPublicKeyFromKid("did:web:api.example.com#key-1"); + expect(extractedKey).toBeDefined(); + expect(global.fetch).toHaveBeenCalledWith( + "https://api.example.com/.well-known/did.json", + expect.any(Object), + ); + }); + + it("should resolve did:web with path", async () => { + const { publicKey } = await jose.generateKeyPair("ES256"); + const jwk = await jose.exportJWK(publicKey); + + const didDocument = { + id: "did:web:example.com:users:alice", + verificationMethod: [ + { + id: "did:web:example.com:users:alice#key-1", + type: "JsonWebKey2020", + controller: "did:web:example.com:users:alice", + publicKeyJwk: jwk, + }, + ], + }; + + global.fetch = vi.fn().mockResolvedValue({ + ok: true, + json: () => Promise.resolve(didDocument), + }); + + const extractedKey = await extractPublicKeyFromKid("did:web:example.com:users:alice#key-1"); + expect(extractedKey).toBeDefined(); + expect(global.fetch).toHaveBeenCalledWith( + "https://example.com/users/alice/did.json", + expect.any(Object), + ); + }); + + it("should use http:// for did:web:localhost", async () => { + const { publicKey } = await jose.generateKeyPair("ES256"); + const jwk = await jose.exportJWK(publicKey); + + const didDocument = { + id: "did:web:localhost%3A3000", + verificationMethod: [ + { + id: "did:web:localhost%3A3000#key-1", + type: "JsonWebKey2020", + controller: "did:web:localhost%3A3000", + publicKeyJwk: jwk, + }, + ], + }; + + global.fetch = vi.fn().mockResolvedValue({ + ok: true, + json: () => Promise.resolve(didDocument), + }); + + const extractedKey = await extractPublicKeyFromKid("did:web:localhost%3A3000#key-1"); + expect(extractedKey).toBeDefined(); + expect(global.fetch).toHaveBeenCalledWith( + "http://localhost:3000/.well-known/did.json", + expect.any(Object), + ); + }); + + it("should use http:// for did:web:127.0.0.1", async () => { + const { publicKey } = await jose.generateKeyPair("ES256"); + const jwk = await jose.exportJWK(publicKey); + + const didDocument = { + id: "did:web:127.0.0.1%3A8080", + verificationMethod: [ + { + id: "did:web:127.0.0.1%3A8080#key-1", + type: "JsonWebKey2020", + controller: "did:web:127.0.0.1%3A8080", + publicKeyJwk: jwk, + }, + ], + }; + + global.fetch = vi.fn().mockResolvedValue({ + ok: true, + json: () => Promise.resolve(didDocument), + }); + + const extractedKey = await extractPublicKeyFromKid("did:web:127.0.0.1%3A8080#key-1"); + expect(extractedKey).toBeDefined(); + expect(global.fetch).toHaveBeenCalledWith( + "http://127.0.0.1:8080/.well-known/did.json", + expect.any(Object), + ); + }); + + it("should still use https:// for non-localhost domains", async () => { + const { publicKey } = await jose.generateKeyPair("ES256"); + const jwk = await jose.exportJWK(publicKey); + + const didDocument = { + id: "did:web:example.com", + verificationMethod: [ + { + id: "did:web:example.com#key-1", + type: "JsonWebKey2020", + controller: "did:web:example.com", + publicKeyJwk: jwk, + }, + ], + }; + + global.fetch = vi.fn().mockResolvedValue({ + ok: true, + json: () => Promise.resolve(didDocument), + }); + + const extractedKey = await extractPublicKeyFromKid("did:web:example.com#key-1"); + expect(extractedKey).toBeDefined(); + expect(global.fetch).toHaveBeenCalledWith( + "https://example.com/.well-known/did.json", + expect.any(Object), + ); + }); + + it("should throw when did:web fetch fails", async () => { + global.fetch = vi.fn().mockResolvedValue({ + ok: false, + status: 404, + statusText: "Not Found", + }); + + await expect(extractPublicKeyFromKid("did:web:nonexistent.example.com")).rejects.toThrow( + "Failed to resolve did:web", + ); + }); + + it("should throw when verification method not found", async () => { + const didDocument = { + id: "did:web:api.example.com", + verificationMethod: [], + }; + + global.fetch = vi.fn().mockResolvedValue({ + ok: true, + json: () => Promise.resolve(didDocument), + }); + + await expect(extractPublicKeyFromKid("did:web:api.example.com#nonexistent")).rejects.toThrow( + "No verification method found", + ); + }); + + // Malformed DID Document Tests + + it("should throw when DID document has no verificationMethod array", async () => { + const didDocument = { id: "did:web:api.example.com" }; + + global.fetch = vi.fn().mockResolvedValue({ + ok: true, + json: () => Promise.resolve(didDocument), + }); + + await expect(extractPublicKeyFromKid("did:web:api.example.com#key-1")).rejects.toThrow( + "No verification method found", + ); + }); + + it("should throw when verification method has no key material", async () => { + const didDocument = { + id: "did:web:api.example.com", + verificationMethod: [ + { + id: "did:web:api.example.com#key-1", + type: "JsonWebKey2020", + controller: "did:web:api.example.com", + }, + ], + }; + + global.fetch = vi.fn().mockResolvedValue({ + ok: true, + json: () => Promise.resolve(didDocument), + }); + + await expect(extractPublicKeyFromKid("did:web:api.example.com#key-1")).rejects.toThrow( + "has no supported key format", + ); + }); + + it("should throw when fetch returns invalid JSON", async () => { + global.fetch = vi.fn().mockResolvedValue({ + ok: true, + json: () => Promise.reject(new Error("Invalid JSON")), + }); + + await expect(extractPublicKeyFromKid("did:web:api.example.com")).rejects.toThrow( + "Failed to resolve did:web", + ); + }); + + it("should throw when network error occurs", async () => { + global.fetch = vi.fn().mockRejectedValue(new Error("Network error")); + + await expect(extractPublicKeyFromKid("did:web:api.example.com")).rejects.toThrow( + "Failed to resolve did:web", + ); + }); + + // DID Document structure variations + + it("should resolve key from assertionMethod reference", async () => { + const { publicKey } = await jose.generateKeyPair("ES256K"); + const jwk = await jose.exportJWK(publicKey); + + const didDocument = { + id: "did:web:api.example.com", + verificationMethod: [ + { + id: "did:web:api.example.com#key-1", + type: "JsonWebKey2020", + controller: "did:web:api.example.com", + publicKeyJwk: jwk, + }, + ], + assertionMethod: ["did:web:api.example.com#key-1"], + }; + + global.fetch = vi.fn().mockResolvedValue({ + ok: true, + json: () => Promise.resolve(didDocument), + }); + + const extractedKey = await extractPublicKeyFromKid("did:web:api.example.com"); + expect(extractedKey).toBeDefined(); + }); + + it("should resolve key from authentication reference", async () => { + const { publicKey } = await jose.generateKeyPair("ES256K"); + const jwk = await jose.exportJWK(publicKey); + + const didDocument = { + id: "did:web:api.example.com", + verificationMethod: [ + { + id: "did:web:api.example.com#auth-key", + type: "JsonWebKey2020", + controller: "did:web:api.example.com", + publicKeyJwk: jwk, + }, + ], + authentication: ["did:web:api.example.com#auth-key"], + }; + + global.fetch = vi.fn().mockResolvedValue({ + ok: true, + json: () => Promise.resolve(didDocument), + }); + + const extractedKey = await extractPublicKeyFromKid("did:web:api.example.com"); + expect(extractedKey).toBeDefined(); + }); + + it("should resolve embedded verification method in assertionMethod", async () => { + const { publicKey } = await jose.generateKeyPair("ES256K"); + const jwk = await jose.exportJWK(publicKey); + + const didDocument = { + id: "did:web:api.example.com", + verificationMethod: [], + assertionMethod: [ + { + id: "did:web:api.example.com#embedded-key", + type: "JsonWebKey2020", + controller: "did:web:api.example.com", + publicKeyJwk: jwk, + }, + ], + }; + + global.fetch = vi.fn().mockResolvedValue({ + ok: true, + json: () => Promise.resolve(didDocument), + }); + + const extractedKey = await extractPublicKeyFromKid("did:web:api.example.com"); + expect(extractedKey).toBeDefined(); + }); + + it("should handle publicKeyMultibase format in did:web", async () => { + const didDocument = { + id: "did:web:api.example.com", + verificationMethod: [ + { + id: "did:web:api.example.com#key-1", + type: "Ed25519VerificationKey2020", + controller: "did:web:api.example.com", + publicKeyMultibase: "z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK", + }, + ], + }; + + global.fetch = vi.fn().mockResolvedValue({ + ok: true, + json: () => Promise.resolve(didDocument), + }); + + const extractedKey = await extractPublicKeyFromKid("did:web:api.example.com#key-1"); + expect(extractedKey).toBeDefined(); + }); + }); +}); + +// ============================================================================ +// Real DID Document Fixtures (captured from live endpoints) +// ============================================================================ + +describe("Real DID Document Fixtures", () => { + let originalFetch: typeof global.fetch; + + beforeEach(() => { + originalFetch = global.fetch; + }); + + afterEach(() => { + global.fetch = originalFetch; + }); + + // Captured from https://identity.foundation/.well-known/did.json (P-256 key) + const IDENTITY_FOUNDATION_DID_DOC = { + "@context": ["https://www.w3.org/ns/did/v1", "https://w3id.org/security/suites/jws-2020/v1"], + id: "did:web:identity.foundation", + verificationMethod: [ + { + id: "did:web:identity.foundation#XXS7zTsbIIAxgNlYEXJ4y810GFeLkYdqfK3ChhoQn7c", + type: "JsonWebKey2020", + controller: "did:web:identity.foundation", + publicKeyJwk: { + kty: "EC", + kid: "XXS7zTsbIIAxgNlYEXJ4y810GFeLkYdqfK3ChhoQn7c", + crv: "P-256", + alg: "ES256", + x: "TIIYSHfbBoXZi-B8Q5KBEmYpg6gXk0Getwt2nDPhxvI", + y: "zNbtUvyDHTdmtz3tyiw84UYgzma1X8r4ToP7PbCVHgI", + }, + }, + ], + authentication: ["did:web:identity.foundation#XXS7zTsbIIAxgNlYEXJ4y810GFeLkYdqfK3ChhoQn7c"], + assertionMethod: ["did:web:identity.foundation#XXS7zTsbIIAxgNlYEXJ4y810GFeLkYdqfK3ChhoQn7c"], + }; + + // Captured from https://demo.spruceid.com/.well-known/did.json (Ed25519 key) + const SPRUCE_DID_DOC = { + "@context": [ + "https://www.w3.org/ns/did/v1", + { "@id": "https://w3id.org/security#publicKeyJwk", "@type": "@json" }, + ], + id: "did:web:demo.spruceid.com", + verificationMethod: [ + { + id: "did:web:demo.spruceid.com#_t-v-Ep7AtkELhhvAzCCDzy1O5Bn_z1CVFv9yiRXdHY", + type: "Ed25519VerificationKey2018", + controller: "did:web:demo.spruceid.com", + publicKeyJwk: { + kty: "OKP", + crv: "Ed25519", + x: "2yv3J-Sf263OmwDLS9uFPTRD0PzbvfBGKLiSnPHtXIU", + }, + }, + ], + authentication: ["did:web:demo.spruceid.com#_t-v-Ep7AtkELhhvAzCCDzy1O5Bn_z1CVFv9yiRXdHY"], + assertionMethod: ["did:web:demo.spruceid.com#_t-v-Ep7AtkELhhvAzCCDzy1O5Bn_z1CVFv9yiRXdHY"], + }; + + it("should parse identity.foundation DID document (P-256)", async () => { + global.fetch = vi.fn().mockResolvedValue({ + ok: true, + json: () => Promise.resolve(IDENTITY_FOUNDATION_DID_DOC), + }); + + const key = await extractPublicKeyFromKid( + "did:web:identity.foundation#XXS7zTsbIIAxgNlYEXJ4y810GFeLkYdqfK3ChhoQn7c", + ); + expect(key).toBeDefined(); + }); + + it("should parse identity.foundation via assertionMethod (no fragment)", async () => { + global.fetch = vi.fn().mockResolvedValue({ + ok: true, + json: () => Promise.resolve(IDENTITY_FOUNDATION_DID_DOC), + }); + + const key = await extractPublicKeyFromKid("did:web:identity.foundation"); + expect(key).toBeDefined(); + }); + + it("should parse demo.spruceid.com DID document (Ed25519)", async () => { + global.fetch = vi.fn().mockResolvedValue({ + ok: true, + json: () => Promise.resolve(SPRUCE_DID_DOC), + }); + + const key = await extractPublicKeyFromKid( + "did:web:demo.spruceid.com#_t-v-Ep7AtkELhhvAzCCDzy1O5Bn_z1CVFv9yiRXdHY", + ); + expect(key).toBeDefined(); + }); + + it("should parse demo.spruceid.com via assertionMethod (no fragment)", async () => { + global.fetch = vi.fn().mockResolvedValue({ + ok: true, + json: () => Promise.resolve(SPRUCE_DID_DOC), + }); + + const key = await extractPublicKeyFromKid("did:web:demo.spruceid.com"); + expect(key).toBeDefined(); + }); +}); diff --git a/typescript/packages/extensions/test/sign-in-with-x.test.ts b/typescript/packages/extensions/test/sign-in-with-x.test.ts index 94284fe7a3..004b9c3a4c 100644 --- a/typescript/packages/extensions/test/sign-in-with-x.test.ts +++ b/typescript/packages/extensions/test/sign-in-with-x.test.ts @@ -31,27 +31,53 @@ import { type SolanaSigner, type EVMSigner, type EVMMessageVerifier, - type SIWxExtension, } from "../src/sign-in-with-x/index"; import { safeBase64Encode } from "@x402/core/utils"; import { privateKeyToAccount, generatePrivateKey } from "viem/accounts"; import nacl from "tweetnacl"; import { randomBytes } from "crypto"; +import type { SIWxExtension } from "../src/sign-in-with-x/index"; /** - * Helper to generate fresh time-based fields for tests. - * In production, these are generated by enrichPaymentRequiredResponse. + * Test-only helper: builds a complete SIWX extension with nonce/issuedAt. * - * @param expirationSeconds - Optional expiration duration in seconds - * @returns Time-based fields for SIWX extension + * @param opts - Challenge configuration + * @param opts.domain - Server domain + * @param opts.resourceUri - Full resource URI + * @param opts.network - CAIP-2 network identifier(s) + * @param opts.statement - Human-readable signing statement + * @param opts.expirationSeconds - Challenge TTL in seconds + * @returns Extension object keyed by "sign-in-with-x" */ -function generateTimeBasedFields(expirationSeconds?: number) { - const nonce = randomBytes(16).toString("hex"); - const issuedAt = new Date().toISOString(); - const expirationTime = expirationSeconds - ? new Date(Date.now() + expirationSeconds * 1000).toISOString() - : undefined; - return { nonce, issuedAt, expirationTime }; +function createTestChallenge(opts: { + domain: string; + resourceUri: string; + network: string | string[]; + statement?: string; + expirationSeconds?: number; +}): Record { + const networks = Array.isArray(opts.network) ? opts.network : [opts.network]; + return { + "sign-in-with-x": { + info: { + domain: opts.domain, + uri: opts.resourceUri, + version: "1", + nonce: randomBytes(16).toString("hex"), + issuedAt: new Date().toISOString(), + ...(opts.expirationSeconds !== undefined && { + expirationTime: new Date(Date.now() + opts.expirationSeconds * 1000).toISOString(), + }), + ...(opts.statement && { statement: opts.statement }), + resources: [opts.resourceUri], + }, + supportedChains: networks.map(n => ({ + chainId: n, + type: n.startsWith("solana:") ? ("ed25519" as const) : ("eip191" as const), + })), + schema: { header: "sign-in-with-x", type: "object" }, + }, + }; } const validPayload = { @@ -134,7 +160,7 @@ describe("Sign-In-With-X Extension", () => { }); describe("declareSIWxExtension", () => { - it("should create extension with supportedChains array (without time-based fields)", () => { + it("should create static declaration without time-based fields", () => { const result = declareSIWxExtension({ domain: "api.example.com", resourceUri: "https://api.example.com/data", @@ -149,11 +175,10 @@ describe("Sign-In-With-X Extension", () => { expect(extension.info.uri).toBe("https://api.example.com/data"); expect(extension.schema).toBeDefined(); - // Time-based fields are NOT generated by declareSIWxExtension - // They are generated per-request by enrichPaymentRequiredResponse + // Time-based fields are NOT generated by declareSIWxExtension; + // they are generated per-request by enrichPaymentRequiredResponse expect(extension.info.nonce).toBeUndefined(); expect(extension.info.issuedAt).toBeUndefined(); - expect(extension.info.expirationTime).toBeUndefined(); // Check supportedChains array expect(extension.supportedChains).toHaveLength(1); @@ -179,8 +204,9 @@ describe("Sign-In-With-X Extension", () => { expect(extension.supportedChains[1].chainId).toBe(SOLANA_DEVNET); expect(extension.supportedChains[1].type).toBe("ed25519"); - // Time-based fields are NOT generated - only _options are stored + // Static declaration — no time-based fields expect(extension.info.nonce).toBeUndefined(); + expect(extension.info.issuedAt).toBeUndefined(); expect(extension._options.expirationSeconds).toBe(300); }); @@ -255,7 +281,7 @@ describe("Sign-In-With-X Extension", () => { it("should sign and verify a message with a real wallet", async () => { const account = privateKeyToAccount(generatePrivateKey()); - const extension = declareSIWxExtension({ + const extension = createTestChallenge({ domain: "api.example.com", resourceUri: "https://api.example.com/resource", network: "eip155:8453", @@ -263,10 +289,8 @@ describe("Sign-In-With-X Extension", () => { }); const ext = extension["sign-in-with-x"]; - // Add time-based fields (in production, enrichPaymentRequiredResponse does this) const completeInfo = { ...ext.info, - ...generateTimeBasedFields(300), chainId: ext.supportedChains[0].chainId, type: ext.supportedChains[0].type, }; @@ -285,17 +309,15 @@ describe("Sign-In-With-X Extension", () => { it("should reject tampered signature", async () => { const account = privateKeyToAccount(generatePrivateKey()); - const extension = declareSIWxExtension({ + const extension = createTestChallenge({ domain: "api.example.com", resourceUri: "https://api.example.com/resource", network: "eip155:8453", }); const ext = extension["sign-in-with-x"]; - // Add time-based fields (in production, enrichPaymentRequiredResponse does this) const completeInfo = { ...ext.info, - ...generateTimeBasedFields(300), chainId: ext.supportedChains[0].chainId, type: ext.supportedChains[0].type, }; @@ -305,6 +327,36 @@ describe("Sign-In-With-X Extension", () => { const verification = await verifySIWxSignature(payload); expect(verification.valid).toBe(false); }); + + it("should work for auth-only endpoints (no enrichment)", async () => { + const account = privateKeyToAccount(generatePrivateKey()); + const resourceUri = "https://api.example.com/resource"; + + const extensions = createTestChallenge({ + domain: "api.example.com", + resourceUri, + network: "eip155:8453", + statement: "Sign in to access", + expirationSeconds: 300, + }); + + const ext = extensions["sign-in-with-x"]; + const completeInfo = { + ...ext.info, + chainId: ext.supportedChains[0].chainId, + type: ext.supportedChains[0].type, + }; + const payload = await createSIWxPayload(completeInfo, account); + const header = encodeSIWxHeader(payload); + + const parsed = parseSIWxHeader(header); + const validation = await validateSIWxMessage(parsed, resourceUri); + expect(validation.valid).toBe(true); + + const result = await verifySIWxSignature(parsed); + expect(result.valid).toBe(true); + expect(result.address?.toLowerCase()).toBe(account.address.toLowerCase()); + }); }); describe("Smart wallet verification (evmVerifier option)", () => { @@ -312,7 +364,7 @@ describe("Sign-In-With-X Extension", () => { const mockVerifier: EVMMessageVerifier = vi.fn().mockResolvedValue(true); const account = privateKeyToAccount(generatePrivateKey()); - const extension = declareSIWxExtension({ + const extension = createTestChallenge({ domain: "api.example.com", resourceUri: "https://api.example.com/resource", network: "eip155:8453", @@ -321,7 +373,6 @@ describe("Sign-In-With-X Extension", () => { const ext = extension["sign-in-with-x"]; const completeInfo = { ...ext.info, - ...generateTimeBasedFields(300), chainId: ext.supportedChains[0].chainId, type: ext.supportedChains[0].type, }; @@ -343,7 +394,7 @@ describe("Sign-In-With-X Extension", () => { it("should fallback to EOA verification when no verifier provided", async () => { const account = privateKeyToAccount(generatePrivateKey()); - const extension = declareSIWxExtension({ + const extension = createTestChallenge({ domain: "api.example.com", resourceUri: "https://api.example.com/resource", network: "eip155:8453", @@ -352,7 +403,6 @@ describe("Sign-In-With-X Extension", () => { const ext = extension["sign-in-with-x"]; const completeInfo = { ...ext.info, - ...generateTimeBasedFields(300), chainId: ext.supportedChains[0].chainId, type: ext.supportedChains[0].type, }; @@ -368,7 +418,7 @@ describe("Sign-In-With-X Extension", () => { const mockVerifier: EVMMessageVerifier = vi.fn().mockResolvedValue(false); const account = privateKeyToAccount(generatePrivateKey()); - const extension = declareSIWxExtension({ + const extension = createTestChallenge({ domain: "api.example.com", resourceUri: "https://api.example.com/resource", network: "eip155:8453", @@ -377,7 +427,6 @@ describe("Sign-In-With-X Extension", () => { const ext = extension["sign-in-with-x"]; const completeInfo = { ...ext.info, - ...generateTimeBasedFields(300), chainId: ext.supportedChains[0].chainId, type: ext.supportedChains[0].type, }; @@ -395,7 +444,7 @@ describe("Sign-In-With-X Extension", () => { const mockVerifier: EVMMessageVerifier = vi.fn().mockRejectedValue(new Error("RPC error")); const account = privateKeyToAccount(generatePrivateKey()); - const extension = declareSIWxExtension({ + const extension = createTestChallenge({ domain: "api.example.com", resourceUri: "https://api.example.com/resource", network: "eip155:8453", @@ -404,7 +453,6 @@ describe("Sign-In-With-X Extension", () => { const ext = extension["sign-in-with-x"]; const completeInfo = { ...ext.info, - ...generateTimeBasedFields(300), chainId: ext.supportedChains[0].chainId, type: ext.supportedChains[0].type, }; @@ -428,7 +476,7 @@ describe("Sign-In-With-X Extension", () => { publicKey: address, }; - const extension = declareSIWxExtension({ + const extension = createTestChallenge({ domain: "api.example.com", resourceUri: "https://api.example.com/resource", network: SOLANA_MAINNET, @@ -437,7 +485,6 @@ describe("Sign-In-With-X Extension", () => { const ext = extension["sign-in-with-x"]; const completeInfo = { ...ext.info, - ...generateTimeBasedFields(300), chainId: ext.supportedChains[0].chainId, type: ext.supportedChains[0].type, }; @@ -798,7 +845,7 @@ describe("Sign-In-With-X Extension", () => { publicKey: address, }; - const extension = declareSIWxExtension({ + const extension = createTestChallenge({ domain: "api.example.com", resourceUri: "https://api.example.com/resource", network: SOLANA_MAINNET, @@ -808,7 +855,6 @@ describe("Sign-In-With-X Extension", () => { const ext = extension["sign-in-with-x"]; const completeInfo = { ...ext.info, - ...generateTimeBasedFields(300), chainId: ext.supportedChains[0].chainId, type: ext.supportedChains[0].type, }; @@ -834,7 +880,7 @@ describe("Sign-In-With-X Extension", () => { publicKey: { toBase58: () => address }, }; - const extension = declareSIWxExtension({ + const extension = createTestChallenge({ domain: "api.example.com", resourceUri: "https://api.example.com/resource", network: SOLANA_DEVNET, @@ -843,7 +889,6 @@ describe("Sign-In-With-X Extension", () => { const ext = extension["sign-in-with-x"]; const completeInfo = { ...ext.info, - ...generateTimeBasedFields(300), chainId: ext.supportedChains[0].chainId, type: ext.supportedChains[0].type, }; @@ -1046,7 +1091,7 @@ describe("SIWX Hooks", () => { storage.recordPayment("/resource", account.address); // Create valid SIWX header - const extension = declareSIWxExtension({ + const extension = createTestChallenge({ domain: "example.com", resourceUri: "http://example.com/resource", network: "eip155:8453", @@ -1054,7 +1099,6 @@ describe("SIWX Hooks", () => { const ext = extension["sign-in-with-x"]; const completeInfo = { ...ext.info, - ...generateTimeBasedFields(300), chainId: ext.supportedChains[0].chainId, type: ext.supportedChains[0].type, }; @@ -1080,7 +1124,7 @@ describe("SIWX Hooks", () => { // Don't pre-record payment - const extension = declareSIWxExtension({ + const extension = createTestChallenge({ domain: "example.com", resourceUri: "http://example.com/resource", network: "eip155:8453", @@ -1088,7 +1132,6 @@ describe("SIWX Hooks", () => { const ext = extension["sign-in-with-x"]; const completeInfo = { ...ext.info, - ...generateTimeBasedFields(300), chainId: ext.supportedChains[0].chainId, type: ext.supportedChains[0].type, }; @@ -1201,7 +1244,7 @@ describe("SIWX Hooks", () => { storage.recordPayment("/resource", account.address); // Create valid SIWX header - const extension = declareSIWxExtension({ + const extension = createTestChallenge({ domain: "example.com", resourceUri: "http://example.com/resource", network: "eip155:8453", @@ -1209,7 +1252,6 @@ describe("SIWX Hooks", () => { const ext = extension["sign-in-with-x"]; const completeInfo = { ...ext.info, - ...generateTimeBasedFields(300), chainId: ext.supportedChains[0].chainId, type: ext.supportedChains[0].type, }; @@ -1242,7 +1284,7 @@ describe("SIWX Hooks", () => { storage.recordPayment("/resource", account.address); // Create valid SIWX header - const extension = declareSIWxExtension({ + const extension = createTestChallenge({ domain: "example.com", resourceUri: "http://example.com/resource", network: "eip155:8453", @@ -1250,7 +1292,6 @@ describe("SIWX Hooks", () => { const ext = extension["sign-in-with-x"]; const completeInfo = { ...ext.info, - ...generateTimeBasedFields(300), chainId: ext.supportedChains[0].chainId, type: ext.supportedChains[0].type, }; @@ -1282,7 +1323,7 @@ describe("SIWX Hooks", () => { storage.recordPayment("/resource", account.address); // Create valid SIWX header - const extension = declareSIWxExtension({ + const extension = createTestChallenge({ domain: "example.com", resourceUri: "http://example.com/resource", network: "eip155:8453", @@ -1290,7 +1331,6 @@ describe("SIWX Hooks", () => { const ext = extension["sign-in-with-x"]; const completeInfo = { ...ext.info, - ...generateTimeBasedFields(300), chainId: ext.supportedChains[0].chainId, type: ext.supportedChains[0].type, }; @@ -1322,6 +1362,127 @@ describe("SIWX Hooks", () => { expect(result2).toEqual({ grantAccess: true }); }); }); + + describe("auth-only routes (accepts: [])", () => { + it("should grant access with valid SIWX when accepts is empty array", async () => { + const storage = new InMemorySIWxStorage(); + const account = privateKeyToAccount(generatePrivateKey()); + + // Do NOT record any payment — auth-only should not require it + + const extension = createTestChallenge({ + domain: "example.com", + resourceUri: "http://example.com/profile", + network: "eip155:8453", + }); + const ext = extension["sign-in-with-x"]; + const completeInfo = { + ...ext.info, + chainId: ext.supportedChains[0].chainId, + type: ext.supportedChains[0].type, + }; + const payload = await createSIWxPayload(completeInfo, account); + const header = encodeSIWxHeader(payload); + + const hook = createSIWxRequestHook({ storage }); + const result = await hook( + { + adapter: { + getHeader: (name: string) => + name === "sign-in-with-x" || name === "SIGN-IN-WITH-X" ? header : undefined, + getUrl: () => "http://example.com/profile", + }, + path: "/profile", + }, + { accepts: [] }, + ); + + expect(result).toEqual({ grantAccess: true }); + }); + + it("should reject nonce replay on auth-only routes", async () => { + const base = new InMemorySIWxStorage(); + const usedNonces = new Set(); + const storage = { + ...base, + hasPaid: base.hasPaid.bind(base), + recordPayment: base.recordPayment.bind(base), + hasUsedNonce: (nonce: string) => usedNonces.has(nonce), + recordNonce: (nonce: string) => { + usedNonces.add(nonce); + }, + }; + + const account = privateKeyToAccount(generatePrivateKey()); + const events: SIWxHookEvent[] = []; + + const extension = createTestChallenge({ + domain: "example.com", + resourceUri: "http://example.com/profile", + network: "eip155:8453", + }); + const ext = extension["sign-in-with-x"]; + const completeInfo = { + ...ext.info, + chainId: ext.supportedChains[0].chainId, + type: ext.supportedChains[0].type, + }; + const payload = await createSIWxPayload(completeInfo, account); + const header = encodeSIWxHeader(payload); + + const hook = createSIWxRequestHook({ storage, onEvent: e => events.push(e) }); + const authOnlyRoute = { accepts: [] }; + const context = { + adapter: { + getHeader: (name: string) => + name === "sign-in-with-x" || name === "SIGN-IN-WITH-X" ? header : undefined, + getUrl: () => "http://example.com/profile", + }, + path: "/profile", + }; + + // First request should succeed + const result1 = await hook(context, authOnlyRoute); + expect(result1).toEqual({ grantAccess: true }); + + // Second request with same nonce should be rejected + const result2 = await hook(context, authOnlyRoute); + expect(result2).toBeUndefined(); + expect(events.some(e => e.type === "nonce_reused")).toBe(true); + }); + + it("should NOT grant access without routeConfig when address has not paid", async () => { + const storage = new InMemorySIWxStorage(); + const account = privateKeyToAccount(generatePrivateKey()); + + // No payment recorded, no routeConfig passed — should behave as before + const extension = createTestChallenge({ + domain: "example.com", + resourceUri: "http://example.com/resource", + network: "eip155:8453", + }); + const ext = extension["sign-in-with-x"]; + const completeInfo = { + ...ext.info, + chainId: ext.supportedChains[0].chainId, + type: ext.supportedChains[0].type, + }; + const payload = await createSIWxPayload(completeInfo, account); + const header = encodeSIWxHeader(payload); + + const hook = createSIWxRequestHook({ storage }); + const result = await hook({ + adapter: { + getHeader: (name: string) => + name === "sign-in-with-x" || name === "SIGN-IN-WITH-X" ? header : undefined, + getUrl: () => "http://example.com/resource", + }, + path: "/resource", + }); + + expect(result).toBeUndefined(); + }); + }); }); describe("createSIWxClientHook", () => { @@ -1340,29 +1501,16 @@ describe("SIWX Hooks", () => { const account = privateKeyToAccount(generatePrivateKey()); const hook = createSIWxClientHook(account); - const declaration = declareSIWxExtension({ + const challenge = createTestChallenge({ domain: "example.com", resourceUri: "http://example.com/resource", network: "eip155:1", }); - // Simulate what enrichPaymentRequiredResponse does: add time-based fields - const ext = declaration["sign-in-with-x"]; - const enrichedExtension = { - "sign-in-with-x": { - info: { - ...ext.info, - ...generateTimeBasedFields(300), - }, - supportedChains: ext.supportedChains, - schema: ext.schema, - }, - }; - const result = await hook({ paymentRequired: { accepts: [{ network: "eip155:1" }], - extensions: enrichedExtension, + extensions: challenge, }, }); @@ -1435,4 +1583,23 @@ describe("siwxResourceServerExtension", () => { expect(result.info.domain).toBe("api.example.com"); expect(result.info.uri).toBe("https://api.example.com/data"); }); + + it("should generate time-based fields from static declaration", async () => { + const declaration = declareSIWxExtension({ expirationSeconds: 300 }); + const ext = declaration["sign-in-with-x"]; + + // Static declaration has no nonce/issuedAt + expect(ext.info.nonce).toBeUndefined(); + expect(ext.info.issuedAt).toBeUndefined(); + + const result = (await siwxResourceServerExtension.enrichPaymentRequiredResponse!( + ext, + mockContext(["eip155:8453"]), + )) as SIWxExtension; + + // Enrichment generates fresh time-based fields + expect(result.info.nonce).toHaveLength(32); + expect(result.info.issuedAt).toBeDefined(); + expect(result.info.expirationTime).toBeDefined(); + }); }); diff --git a/typescript/packages/extensions/tsup.config.ts b/typescript/packages/extensions/tsup.config.ts index f082e07998..bd85a73925 100644 --- a/typescript/packages/extensions/tsup.config.ts +++ b/typescript/packages/extensions/tsup.config.ts @@ -5,6 +5,7 @@ const baseConfig = { index: "src/index.ts", "bazaar/index": "src/bazaar/index.ts", "sign-in-with-x/index": "src/sign-in-with-x/index.ts", + "offer-receipt/index": "src/offer-receipt/index.ts", "payment-identifier/index": "src/payment-identifier/index.ts", }, dts: { diff --git a/typescript/packages/http/axios/CHANGELOG.md b/typescript/packages/http/axios/CHANGELOG.md index da6673c6cd..db2ff2bd6b 100644 --- a/typescript/packages/http/axios/CHANGELOG.md +++ b/typescript/packages/http/axios/CHANGELOG.md @@ -1,5 +1,28 @@ # @x402/axios Changelog +## 2.8.0 + +### Minor Changes + +- Updated dependencies [067f297] +- Updated dependencies [4c1e44f] +- Updated dependencies [5135fab] + - @x402/core@2.8.0 + +## 2.7.0 + +### Minor Changes + +- Updated dependencies [8931cb3] + - @x402/core@2.7.0 + +## 2.6.0 + +### Minor Changes + +- Updated dependencies + - @x402/core@2.6.0 + ## 2.5.0 ### Minor Changes diff --git a/typescript/packages/http/axios/package.json b/typescript/packages/http/axios/package.json index 7576667a29..35431041b3 100644 --- a/typescript/packages/http/axios/package.json +++ b/typescript/packages/http/axios/package.json @@ -1,6 +1,6 @@ { "name": "@x402/axios", - "version": "2.5.0", + "version": "2.8.0", "main": "./dist/cjs/index.js", "module": "./dist/esm/index.js", "types": "./dist/index.d.ts", diff --git a/typescript/packages/http/express/CHANGELOG.md b/typescript/packages/http/express/CHANGELOG.md index e80dd62610..3b0f1a5f54 100644 --- a/typescript/packages/http/express/CHANGELOG.md +++ b/typescript/packages/http/express/CHANGELOG.md @@ -1,5 +1,47 @@ # @x402/express Changelog +## 2.8.0 + +### Minor Changes + +- 4c1e44f: Treat malformed facilitator success payloads as upstream facilitator errors and return 502 responses from framework middleware instead of flattening them into payment failures. +- Updated dependencies [4f2f4f3] +- Updated dependencies [067f297] +- Updated dependencies [067f297] +- Updated dependencies [4c1e44f] +- Updated dependencies [5135fab] + - @x402/extensions@2.8.0 + - @x402/core@2.8.0 + - @x402/paywall@2.8.0 + +## 2.7.0 + +### Minor Changes + +- Updated dependencies [34d2442] +- Updated dependencies [8b731cb] +- Updated dependencies [f2bbb5c] +- Updated dependencies [8931cb3] +- Updated dependencies [34d2442] + - @x402/extensions@2.7.0 + - @x402/core@2.7.0 + - @x402/paywall@2.7.0 + +## 2.6.0 + +### Minor Changes + +- aeef1bf: Added dynamic function for servers to generate custom response for settlement failures defaulting to empty +- 205257b: Cleaned up dependencies +- 2564781: Include PAYMENT-RESPONSE header on settlement failure responses +- Updated dependencies [f41baed] +- Updated dependencies [aeef1bf] +- Updated dependencies [2564781] +- Updated dependencies [b341973] +- Updated dependencies [29fe09a] + - @x402/core@2.6.0 + - @x402/paywall@2.6.0 + ## 2.5.0 ### Minor Changes diff --git a/typescript/packages/http/express/package.json b/typescript/packages/http/express/package.json index 4dd076bde3..b926af8c07 100644 --- a/typescript/packages/http/express/package.json +++ b/typescript/packages/http/express/package.json @@ -1,6 +1,6 @@ { "name": "@x402/express", - "version": "2.5.0", + "version": "2.8.0", "main": "./dist/cjs/index.js", "module": "./dist/esm/index.js", "types": "./dist/index.d.ts", @@ -40,15 +40,13 @@ "vitest": "^3.0.5" }, "dependencies": { - "@coinbase/cdp-sdk": "^1.22.0", - "@solana/kit": "^6.1.0", "@x402/core": "workspace:~", "@x402/extensions": "workspace:~", "viem": "^2.39.3", "zod": "^3.24.2" }, "peerDependencies": { - "@x402/paywall": "workspace:*", + "@x402/paywall": "workspace:^", "express": "^4.0.0 || ^5.0.0" }, "peerDependenciesMeta": { diff --git a/typescript/packages/http/express/src/index.test.ts b/typescript/packages/http/express/src/index.test.ts index 6085241d27..92947c9b04 100644 --- a/typescript/packages/http/express/src/index.test.ts +++ b/typescript/packages/http/express/src/index.test.ts @@ -7,6 +7,7 @@ import type { FacilitatorClient, } from "@x402/core/server"; import { + FacilitatorResponseError, x402ResourceServer, x402HTTPResourceServer as HTTPResourceServer, } from "@x402/core/server"; @@ -40,6 +41,28 @@ let mockRegisterPaywallProvider: ReturnType; let mockRequiresPayment: ReturnType; vi.mock("@x402/core/server", () => ({ + SETTLEMENT_OVERRIDES_HEADER: "Settlement-Overrides", + FacilitatorResponseError: class FacilitatorResponseError extends Error { + /** + * Creates a mock facilitator response error. + * + * @param message - Error message. + */ + constructor(message: string) { + super(message); + this.name = "FacilitatorResponseError"; + } + }, + getFacilitatorResponseError: (error: unknown) => { + let current = error; + while (current instanceof Error) { + if (current.name === "FacilitatorResponseError") { + return current; + } + current = (current as Error & { cause?: unknown }).cause; + } + return null; + }, x402ResourceServer: vi.fn().mockImplementation(() => ({ initialize: vi.fn().mockResolvedValue(undefined), registerExtension: vi.fn(), @@ -71,7 +94,12 @@ function setupMockHttpServer( processResult: HTTPProcessResult, settlementResult: | { success: true; headers: Record } - | { success: false; errorReason: string; headers: Record } = { + | { + success: false; + errorReason: string; + headers: Record; + response: { status: number; headers: Record; body?: unknown }; + } = { success: true, headers: {}, }, @@ -131,6 +159,16 @@ function createMockResponse(): Response & { this._headers[key] = value; return this; }), + getHeaders: vi.fn(function (this: typeof res) { + return this._headers; + }), + getHeader: vi.fn(function (this: typeof res, key: string) { + return this._headers[key] ?? undefined; + }), + removeHeader: vi.fn(function (this: typeof res, key: string) { + delete this._headers[key]; + return this; + }), json: vi.fn(function (this: typeof res, body: unknown) { this._body = body; this._ended = true; @@ -386,9 +424,145 @@ describe("paymentMiddleware", () => { await middleware(req, res, next); expect(res.status).toHaveBeenCalledWith(402); + expect(res.json).toHaveBeenCalledWith({}); + }); + + it("returns 502 when facilitator init fails during protected request", async () => { + const initialize = vi.fn().mockRejectedValue( + new Error("Failed to initialize: no supported payment kinds loaded from any facilitator.", { + cause: new FacilitatorResponseError( + "Facilitator supported returned invalid JSON: not-json", + ), + }), + ); + + vi.mocked(HTTPResourceServer).mockImplementation( + (server, routes) => + ({ + initialize, + processHTTPRequest: mockProcessHTTPRequest, + processSettlement: mockProcessSettlement, + registerPaywallProvider: mockRegisterPaywallProvider, + requiresPayment: mockRequiresPayment, + routes, + server: server || { + hasExtension: vi.fn().mockReturnValue(false), + registerExtension: vi.fn(), + }, + }) as unknown as x402HTTPResourceServer, + ); + + const middleware = paymentMiddleware(mockRoutes, {} as unknown as x402ResourceServer); + const req = createMockRequest(); + const res = createMockResponse(); + const next = vi.fn(); + + await middleware(req, res, next); + + expect(mockProcessHTTPRequest).not.toHaveBeenCalled(); + expect(res.status).toHaveBeenCalledWith(502); + expect(res.json).toHaveBeenCalledWith({ + error: "Facilitator supported returned invalid JSON: not-json", + }); + }); + + it("retries initialization after a facilitator init failure", async () => { + const initialize = vi + .fn() + .mockRejectedValueOnce( + new Error("Failed to initialize: no supported payment kinds loaded from any facilitator.", { + cause: new FacilitatorResponseError( + "Facilitator supported returned invalid JSON: not-json", + ), + }), + ) + .mockResolvedValueOnce(undefined); + + vi.mocked(HTTPResourceServer).mockImplementation( + (server, routes) => + ({ + initialize, + processHTTPRequest: mockProcessHTTPRequest, + processSettlement: mockProcessSettlement, + registerPaywallProvider: mockRegisterPaywallProvider, + requiresPayment: mockRequiresPayment, + routes, + server: server || { + hasExtension: vi.fn().mockReturnValue(false), + registerExtension: vi.fn(), + }, + }) as unknown as x402HTTPResourceServer, + ); + mockProcessHTTPRequest.mockResolvedValue({ type: "no-payment-required" }); + + const middleware = paymentMiddleware(mockRoutes, {} as unknown as x402ResourceServer); + const firstRes = createMockResponse(); + const secondRes = createMockResponse(); + const next = vi.fn(); + + await middleware(createMockRequest(), firstRes, next); + await middleware(createMockRequest(), secondRes, next); + + expect(firstRes.status).toHaveBeenCalledWith(502); + expect(initialize).toHaveBeenCalledTimes(2); + expect(mockProcessHTTPRequest).toHaveBeenCalledTimes(1); + expect(next).toHaveBeenCalledTimes(1); + }); + + it("returns 502 when processHTTPRequest surfaces FacilitatorResponseError", async () => { + mockProcessHTTPRequest.mockRejectedValue( + new FacilitatorResponseError("Facilitator verify returned invalid JSON: not-json"), + ); + + const middleware = paymentMiddleware( + mockRoutes, + {} as unknown as x402ResourceServer, + undefined, + undefined, + false, + ); + const req = createMockRequest(); + const res = createMockResponse(); + const next = vi.fn(); + + await middleware(req, res, next); + + expect(res.status).toHaveBeenCalledWith(502); expect(res.json).toHaveBeenCalledWith({ - error: "Settlement failed", - details: "Settlement rejected", + error: "Facilitator verify returned invalid JSON: not-json", + }); + expect(next).not.toHaveBeenCalled(); + }); + + it("returns 502 when settlement surfaces FacilitatorResponseError", async () => { + setupMockHttpServer({ + type: "payment-verified", + paymentPayload: mockPaymentPayload, + paymentRequirements: mockPaymentRequirements, + }); + mockProcessSettlement.mockRejectedValue( + new FacilitatorResponseError('Facilitator settle returned invalid data: {"success":true}'), + ); + + const middleware = paymentMiddleware( + mockRoutes, + {} as unknown as x402ResourceServer, + undefined, + undefined, + false, + ); + const req = createMockRequest(); + const res = createMockResponse(); + const next = vi.fn(() => { + res.statusCode = 200; + res.end(); + }); + + await middleware(req, res, next); + + expect(res.status).toHaveBeenCalledWith(502); + expect(res.json).toHaveBeenCalledWith({ + error: 'Facilitator settle returned invalid data: {"success":true}', }); }); @@ -403,6 +577,14 @@ describe("paymentMiddleware", () => { success: false, errorReason: "Insufficient funds", headers: { "PAYMENT-RESPONSE": "settlement-failed-encoded" }, + response: { + status: 402, + headers: { + "Content-Type": "application/json", + "PAYMENT-RESPONSE": "settlement-failed-encoded", + }, + body: {}, + }, }, ); @@ -424,10 +606,7 @@ describe("paymentMiddleware", () => { expect(res.setHeader).toHaveBeenCalledWith("PAYMENT-RESPONSE", "settlement-failed-encoded"); expect(res.status).toHaveBeenCalledWith(402); - expect(res.json).toHaveBeenCalledWith({ - error: "Settlement failed", - details: "Insufficient funds", - }); + expect(res.json).toHaveBeenCalledWith({}); }); it("passes paywallConfig to processHTTPRequest", async () => { diff --git a/typescript/packages/http/express/src/index.ts b/typescript/packages/http/express/src/index.ts index 2da120c83f..efd262fc6a 100644 --- a/typescript/packages/http/express/src/index.ts +++ b/typescript/packages/http/express/src/index.ts @@ -6,11 +6,26 @@ import { x402ResourceServer, RoutesConfig, FacilitatorClient, + FacilitatorResponseError, + getFacilitatorResponseError, + SETTLEMENT_OVERRIDES_HEADER, + SettlementOverrides, } from "@x402/core/server"; import { SchemeNetworkServer, Network } from "@x402/core/types"; import { NextFunction, Request, Response } from "express"; import { ExpressAdapter } from "./adapter"; +/** + * Set settlement overrides on the response for partial settlement. + * The middleware will extract these before settlement and strip the header from the client response. + * + * @param res - Express response object + * @param overrides - Settlement overrides (e.g., { amount: "500" } for partial settlement) + */ +export function setSettlementOverrides(res: Response, overrides: SettlementOverrides): void { + res.setHeader(SETTLEMENT_OVERRIDES_HEADER, JSON.stringify(overrides)); +} + /** * Check if any routes in the configuration declare bazaar extensions * @@ -44,6 +59,16 @@ export interface SchemeRegistration { server: SchemeNetworkServer; } +/** + * Sends a normalized 502 response for facilitator boundary failures. + * + * @param res - The Express response to write to + * @param error - The facilitator response error to surface + */ +function sendFacilitatorError(res: Response, error: FacilitatorResponseError): void { + res.status(502).json({ error: error.message }); +} + /** * Express payment middleware for x402 protocol (direct HTTP server instance). * @@ -82,6 +107,28 @@ export function paymentMiddlewareFromHTTPServer( // Store initialization promise (not the result) // httpServer.initialize() fetches facilitator support and validates routes let initPromise: Promise | null = syncFacilitatorOnStart ? httpServer.initialize() : null; + let isInitialized = false; + + /** + * Ensures facilitator initialization succeeds once, while allowing retries after failures. + */ + async function initializeHttpServer(): Promise { + if (!syncFacilitatorOnStart || isInitialized) { + return; + } + + if (!initPromise) { + initPromise = httpServer.initialize(); + } + + try { + await initPromise; + isInitialized = true; + } catch (error) { + initPromise = null; + throw error; + } + } // Dynamically register bazaar extension if routes declare it and not already registered // Skip if pre-registered (e.g., in serverless environments where static imports are used) @@ -112,9 +159,17 @@ export function paymentMiddlewareFromHTTPServer( } // Only initialize when processing a protected route - if (initPromise) { - await initPromise; - initPromise = null; // Clear after first await + if (syncFacilitatorOnStart && !isInitialized) { + try { + await initializeHttpServer(); + } catch (error) { + const facilitatorError = getFacilitatorResponseError(error); + if (facilitatorError) { + sendFacilitatorError(res, facilitatorError); + return; + } + return next(error); + } } // Await bazaar extension loading if needed @@ -124,7 +179,16 @@ export function paymentMiddlewareFromHTTPServer( } // Process payment requirement check - const result = await httpServer.processHTTPRequest(context, paywallConfig); + let result: Awaited>; + try { + result = await httpServer.processHTTPRequest(context, paywallConfig); + } catch (error) { + if (error instanceof FacilitatorResponseError) { + sendFacilitatorError(res, error); + return; + } + return next(error); + } // Handle the different result types switch (result.type) { @@ -238,23 +302,32 @@ export function paymentMiddlewareFromHTTPServer( ), ); + const responseHeaders: Record = {}; + for (const [key, value] of Object.entries(res.getHeaders())) { + if (value != null) { + responseHeaders[key] = String(value); + } + } + const settleResult = await httpServer.processSettlement( paymentPayload, paymentRequirements, declaredExtensions, - { request: context, responseBody }, + { request: context, responseBody, responseHeaders }, ); // If settlement fails, return an error and do not send the buffered response if (!settleResult.success) { bufferedCalls = []; - Object.entries(settleResult.headers).forEach(([key, value]) => { + const { response } = settleResult; + Object.entries(response.headers).forEach(([key, value]) => { res.setHeader(key, value); }); - res.status(402).json({ - error: "Settlement failed", - details: settleResult.errorReason, - }); + if (response.isHtml) { + res.status(response.status).send(response.body); + } else { + res.status(response.status).json(response.body ?? {}); + } return; } @@ -263,13 +336,15 @@ export function paymentMiddlewareFromHTTPServer( res.setHeader(key, value); }); } catch (error) { + if (error instanceof FacilitatorResponseError) { + bufferedCalls = []; + sendFacilitatorError(res, error); + return; + } console.error(error); // If settlement fails, don't send the buffered response bufferedCalls = []; - res.status(402).json({ - error: "Settlement failed", - details: error instanceof Error ? error.message : "Unknown error", - }); + res.status(402).json({}); return; } finally { settled = true; @@ -393,9 +468,9 @@ export type { SchemeNetworkServer, } from "@x402/core/types"; -export type { PaywallProvider, PaywallConfig } from "@x402/core/server"; +export type { PaywallProvider, PaywallConfig, SettlementOverrides } from "@x402/core/server"; -export { RouteConfigurationError } from "@x402/core/server"; +export { RouteConfigurationError, SETTLEMENT_OVERRIDES_HEADER } from "@x402/core/server"; export type { RouteValidationError } from "@x402/core/server"; diff --git a/typescript/packages/http/fastify/.prettierignore b/typescript/packages/http/fastify/.prettierignore new file mode 100644 index 0000000000..5bd240ba90 --- /dev/null +++ b/typescript/packages/http/fastify/.prettierignore @@ -0,0 +1,8 @@ +docs/ +dist/ +node_modules/ +coverage/ +.github/ +src/client +**/**/*.json +*.md \ No newline at end of file diff --git a/typescript/packages/http/fastify/.prettierrc b/typescript/packages/http/fastify/.prettierrc new file mode 100644 index 0000000000..ffb416b74b --- /dev/null +++ b/typescript/packages/http/fastify/.prettierrc @@ -0,0 +1,11 @@ +{ + "tabWidth": 2, + "useTabs": false, + "semi": true, + "singleQuote": false, + "trailingComma": "all", + "bracketSpacing": true, + "arrowParens": "avoid", + "printWidth": 100, + "proseWrap": "never" +} diff --git a/typescript/packages/http/fastify/README.md b/typescript/packages/http/fastify/README.md new file mode 100644 index 0000000000..c8b17bf6f3 --- /dev/null +++ b/typescript/packages/http/fastify/README.md @@ -0,0 +1,254 @@ +# @x402/fastify + +Fastify middleware integration for the x402 Payment Protocol. This package provides payment middleware for adding x402 payment requirements to your Fastify applications. + +## Installation + +```bash +pnpm install @x402/fastify +``` + +## Quick Start + +```typescript +import Fastify from "fastify"; +import { paymentMiddleware, x402ResourceServer } from "@x402/fastify"; +import { ExactEvmScheme } from "@x402/evm/exact/server"; +import { HTTPFacilitatorClient } from "@x402/core/server"; + +const app = Fastify(); + +const facilitatorClient = new HTTPFacilitatorClient({ url: "https://facilitator.x402.org" }); +const resourceServer = new x402ResourceServer(facilitatorClient) + .register("eip155:84532", new ExactEvmScheme()); + +// Apply the payment middleware with your configuration +paymentMiddleware( + app, + { + "GET /protected-route": { + accepts: { + scheme: "exact", + price: "$0.10", + network: "eip155:84532", + payTo: "0xYourAddress", + }, + description: "Access to premium content", + }, + }, + resourceServer, +); + +// Implement your protected route +app.get("/protected-route", async () => { + return { message: "This content is behind a paywall" }; +}); + +app.listen({ port: 3000 }); +``` + +## Configuration + +The `paymentMiddleware` function accepts the following parameters: + +```typescript +paymentMiddleware( + app: FastifyInstance, + routes: RoutesConfig, + server: x402ResourceServer, + paywallConfig?: PaywallConfig, + paywall?: PaywallProvider, + syncFacilitatorOnStart?: boolean +) +``` + +### Parameters + +1. **`app`** (required): The Fastify instance to register hooks on +2. **`routes`** (required): Route configurations for protected endpoints +3. **`server`** (required): Pre-configured x402ResourceServer instance +4. **`paywallConfig`** (optional): Configuration for the built-in paywall UI +5. **`paywall`** (optional): Custom paywall provider +6. **`syncFacilitatorOnStart`** (optional): Whether to sync with facilitator on startup (defaults to true) + +## API Reference + +### FastifyAdapter + +The `FastifyAdapter` class implements the `HTTPAdapter` interface from `@x402/core`, providing Fastify-specific request handling: + +```typescript +class FastifyAdapter implements HTTPAdapter { + getHeader(name: string): string | undefined; + getMethod(): string; + getPath(): string; + getUrl(): string; + getAcceptHeader(): string; + getUserAgent(): string; +} +``` + +### Middleware Function + +```typescript +function paymentMiddleware( + app: FastifyInstance, + routes: RoutesConfig, + server: x402ResourceServer, + paywallConfig?: PaywallConfig, + paywall?: PaywallProvider, + syncFacilitatorOnStart?: boolean, +): void; +``` + +Registers Fastify hooks (`onRequest` and `onSend`) that: + +1. Use the provided x402ResourceServer for payment processing +2. Check if the incoming request matches a protected route +3. Validate payment headers if required +4. Return payment instructions (402 status) if payment is missing or invalid +5. Process the request if payment is valid +6. Handle settlement after successful response + +### Route Configuration + +Routes are passed as the second parameter to `paymentMiddleware`: + +```typescript +const routes: RoutesConfig = { + "GET /api/protected": { + accepts: { + scheme: "exact", + price: "$0.10", + network: "eip155:84532", + payTo: "0xYourAddress", + maxTimeoutSeconds: 60, + }, + description: "Premium API access", + }, +}; + +paymentMiddleware(app, routes, resourceServer); +``` + +### Paywall Configuration + +The middleware automatically displays a paywall UI when browsers request protected endpoints. + +**Option 1: Full Paywall UI (Recommended)** + +Install the optional `@x402/paywall` package for a complete wallet connection and payment UI: + +```bash +pnpm add @x402/paywall +``` + +Then configure it: + +```typescript +const paywallConfig: PaywallConfig = { + appName: "Your App Name", + appLogo: "/path/to/logo.svg", + testnet: true, +}; + +paymentMiddleware(app, routes, resourceServer, paywallConfig); +``` + +**Option 2: Basic Paywall (No Installation)** + +Without `@x402/paywall` installed, the middleware returns a basic HTML page with payment instructions. + +**Option 3: Custom Paywall Provider** + +Provide your own paywall provider: + +```typescript +paymentMiddleware(app, routes, resourceServer, paywallConfig, customPaywallProvider); +``` + +## Advanced Usage + +### Multiple Protected Routes + +```typescript +paymentMiddleware( + app, + { + "GET /api/premium/*": { + accepts: { + scheme: "exact", + price: "$1.00", + network: "eip155:8453", + payTo: "0xYourAddress", + }, + description: "Premium API access", + }, + "GET /api/data": { + accepts: { + scheme: "exact", + price: "$0.50", + network: "eip155:84532", + payTo: "0xYourAddress", + maxTimeoutSeconds: 120, + }, + description: "Data endpoint access", + }, + }, + resourceServer, +); +``` + +### Multiple Payment Networks + +```typescript +paymentMiddleware( + app, + { + "GET /weather": { + accepts: [ + { + scheme: "exact", + price: "$0.001", + network: "eip155:84532", + payTo: evmAddress, + }, + { + scheme: "exact", + price: "$0.001", + network: "solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1", + payTo: svmAddress, + }, + ], + description: "Weather data", + mimeType: "application/json", + }, + }, + new x402ResourceServer(facilitatorClient) + .register("eip155:84532", new ExactEvmScheme()) + .register("solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1", new ExactSvmScheme()), +); +``` + +### Custom Facilitator Client + +If you need to use a custom facilitator server, configure it when creating the x402ResourceServer: + +```typescript +import { HTTPFacilitatorClient } from "@x402/core/server"; +import { x402ResourceServer } from "@x402/fastify"; +import { ExactEvmScheme } from "@x402/evm/exact/server"; + +const customFacilitator = new HTTPFacilitatorClient({ + url: "https://your-facilitator.com", + createAuthHeaders: async () => ({ + verify: { Authorization: "Bearer your-token" }, + settle: { Authorization: "Bearer your-token" }, + }), +}); + +const resourceServer = new x402ResourceServer(customFacilitator) + .register("eip155:84532", new ExactEvmScheme()); + +paymentMiddleware(app, routes, resourceServer, paywallConfig); +``` diff --git a/typescript/packages/http/fastify/eslint.config.js b/typescript/packages/http/fastify/eslint.config.js new file mode 100644 index 0000000000..28e564722c --- /dev/null +++ b/typescript/packages/http/fastify/eslint.config.js @@ -0,0 +1,73 @@ +import js from "@eslint/js"; +import ts from "@typescript-eslint/eslint-plugin"; +import tsParser from "@typescript-eslint/parser"; +import prettier from "eslint-plugin-prettier"; +import jsdoc from "eslint-plugin-jsdoc"; +import importPlugin from "eslint-plugin-import"; + +export default [ + { + ignores: ["dist/**", "node_modules/**"], + }, + { + files: ["**/*.ts"], + languageOptions: { + parser: tsParser, + sourceType: "module", + ecmaVersion: 2020, + globals: { + process: "readonly", + __dirname: "readonly", + module: "readonly", + require: "readonly", + Buffer: "readonly", + Headers: "readonly", + exports: "readonly", + setTimeout: "readonly", + clearTimeout: "readonly", + setInterval: "readonly", + clearInterval: "readonly", + }, + }, + plugins: { + "@typescript-eslint": ts, + prettier: prettier, + jsdoc: jsdoc, + import: importPlugin, + }, + rules: { + ...ts.configs.recommended.rules, + "import/first": "error", + "prettier/prettier": "error", + "@typescript-eslint/member-ordering": "error", + "@typescript-eslint/no-unused-vars": ["error", { argsIgnorePattern: "^_$" }], + "jsdoc/tag-lines": ["error", "any", { startLines: 1 }], + "jsdoc/check-alignment": "error", + "jsdoc/no-undefined-types": "off", + "jsdoc/check-param-names": "error", + "jsdoc/check-tag-names": "error", + "jsdoc/check-types": "error", + "jsdoc/implements-on-classes": "error", + "jsdoc/require-description": "error", + "jsdoc/require-jsdoc": [ + "error", + { + require: { + FunctionDeclaration: true, + MethodDefinition: true, + ClassDeclaration: true, + ArrowFunctionExpression: false, + FunctionExpression: false, + }, + }, + ], + "jsdoc/require-param": "error", + "jsdoc/require-param-description": "error", + "jsdoc/require-param-type": "off", + "jsdoc/require-returns": "error", + "jsdoc/require-returns-description": "error", + "jsdoc/require-returns-type": "off", + "jsdoc/require-hyphen-before-param-description": ["error", "always"], + }, + }, +]; diff --git a/typescript/packages/http/fastify/package.json b/typescript/packages/http/fastify/package.json new file mode 100644 index 0000000000..5956fdcd8a --- /dev/null +++ b/typescript/packages/http/fastify/package.json @@ -0,0 +1,69 @@ +{ + "name": "@x402/fastify", + "version": "0.1.0", + "main": "./dist/cjs/index.js", + "module": "./dist/esm/index.js", + "types": "./dist/index.d.ts", + "scripts": { + "start": "tsx --env-file=.env index.ts", + "test": "vitest run", + "test:watch": "vitest", + "build": "tsup", + "watch": "tsc --watch", + "format": "prettier -c .prettierrc --write \"**/*.{ts,js,cjs,json,md}\"", + "format:check": "prettier -c .prettierrc --check \"**/*.{ts,js,cjs,json,md}\"", + "lint": "eslint . --ext .ts --fix", + "lint:check": "eslint . --ext .ts" + }, + "keywords": [], + "license": "Apache-2.0", + "author": "Coinbase Inc.", + "repository": "https://github.com/coinbase/x402", + "description": "x402 Payment Protocol - Fastify middleware", + "devDependencies": { + "@eslint/js": "^9.24.0", + "@types/node": "^22.13.4", + "@typescript-eslint/eslint-plugin": "^8.29.1", + "@typescript-eslint/parser": "^8.29.1", + "eslint": "^9.24.0", + "eslint-plugin-import": "^2.31.0", + "eslint-plugin-jsdoc": "^50.6.9", + "eslint-plugin-prettier": "^5.2.6", + "fastify": "^5.0.0", + "prettier": "3.5.2", + "tsup": "^8.4.0", + "tsx": "^4.19.2", + "typescript": "^5.7.3", + "vite": "^6.2.6", + "vite-tsconfig-paths": "^5.1.4", + "vitest": "^3.0.5" + }, + "dependencies": { + "@x402/core": "workspace:~", + "@x402/extensions": "workspace:~" + }, + "peerDependencies": { + "fastify": "^5.0.0", + "@x402/paywall": "workspace:*" + }, + "peerDependenciesMeta": { + "@x402/paywall": { + "optional": true + } + }, + "exports": { + ".": { + "import": { + "types": "./dist/esm/index.d.mts", + "default": "./dist/esm/index.mjs" + }, + "require": { + "types": "./dist/cjs/index.d.ts", + "default": "./dist/cjs/index.js" + } + } + }, + "files": [ + "dist" + ] +} diff --git a/typescript/packages/http/fastify/src/adapter.test.ts b/typescript/packages/http/fastify/src/adapter.test.ts new file mode 100644 index 0000000000..9b0675fc20 --- /dev/null +++ b/typescript/packages/http/fastify/src/adapter.test.ts @@ -0,0 +1,169 @@ +import { describe, it, expect } from "vitest"; +import { FastifyRequest } from "fastify"; +import { FastifyAdapter } from "./adapter"; + +/** + * Factory for creating mock Fastify requests. + * + * @param options - Configuration options for the mock request. + * @param options.url - The request URL path with optional query string. + * @param options.method - The HTTP method. + * @param options.headers - Request headers. + * @param options.query - Query parameters. + * @param options.body - Request body. + * @param options.protocol - The request protocol. + * @param options.hostname - The request hostname. + * @param options.host - The request host header, including port if present. + * @returns A mock Fastify request. + */ +function createMockRequest( + options: { + url?: string; + method?: string; + headers?: Record; + query?: Record; + body?: unknown; + protocol?: string; + hostname?: string; + host?: string; + } = {}, +): FastifyRequest { + return { + url: options.url || "/api/test", + method: options.method || "GET", + headers: options.headers || {}, + query: options.query || {}, + body: options.body, + protocol: options.protocol || "https", + hostname: options.hostname || "example.com", + host: options.host || options.hostname || "example.com", + } as unknown as FastifyRequest; +} + +describe("FastifyAdapter", () => { + describe("getHeader", () => { + it("returns header value when present", () => { + const req = createMockRequest({ headers: { "x-payment": "test-payment" } }); + const adapter = new FastifyAdapter(req); + expect(adapter.getHeader("X-Payment")).toBe("test-payment"); + }); + + it("returns first value for array headers", () => { + const req = createMockRequest({ headers: { "x-payment": ["first", "second"] } }); + const adapter = new FastifyAdapter(req); + expect(adapter.getHeader("X-Payment")).toBe("first"); + }); + + it("returns undefined for missing headers", () => { + const req = createMockRequest(); + const adapter = new FastifyAdapter(req); + expect(adapter.getHeader("X-Missing")).toBeUndefined(); + }); + }); + + describe("getMethod", () => { + it("returns the HTTP method", () => { + const req = createMockRequest({ method: "POST" }); + const adapter = new FastifyAdapter(req); + expect(adapter.getMethod()).toBe("POST"); + }); + }); + + describe("getPath", () => { + it("returns the pathname without query string", () => { + const req = createMockRequest({ url: "/api/weather?city=NYC" }); + const adapter = new FastifyAdapter(req); + expect(adapter.getPath()).toBe("/api/weather"); + }); + + it("returns the pathname when no query string", () => { + const req = createMockRequest({ url: "/api/test" }); + const adapter = new FastifyAdapter(req); + expect(adapter.getPath()).toBe("/api/test"); + }); + }); + + describe("getUrl", () => { + it("returns the full URL", () => { + const req = createMockRequest({ + url: "/api/test?foo=bar", + protocol: "https", + hostname: "example.com", + host: "example.com:3000", + }); + const adapter = new FastifyAdapter(req); + expect(adapter.getUrl()).toBe("https://example.com:3000/api/test?foo=bar"); + }); + }); + + describe("getAcceptHeader", () => { + it("returns Accept header when present", () => { + const req = createMockRequest({ headers: { accept: "text/html" } }); + const adapter = new FastifyAdapter(req); + expect(adapter.getAcceptHeader()).toBe("text/html"); + }); + + it("returns empty string when missing", () => { + const req = createMockRequest(); + const adapter = new FastifyAdapter(req); + expect(adapter.getAcceptHeader()).toBe(""); + }); + }); + + describe("getUserAgent", () => { + it("returns User-Agent header when present", () => { + const req = createMockRequest({ headers: { "user-agent": "Mozilla/5.0" } }); + const adapter = new FastifyAdapter(req); + expect(adapter.getUserAgent()).toBe("Mozilla/5.0"); + }); + + it("returns empty string when missing", () => { + const req = createMockRequest(); + const adapter = new FastifyAdapter(req); + expect(adapter.getUserAgent()).toBe(""); + }); + }); + + describe("getQueryParams", () => { + it("returns all query parameters", () => { + const req = createMockRequest({ query: { foo: "bar", baz: "qux" } }); + const adapter = new FastifyAdapter(req); + expect(adapter.getQueryParams()).toEqual({ foo: "bar", baz: "qux" }); + }); + + it("returns empty object when no query params", () => { + const req = createMockRequest({ query: {} }); + const adapter = new FastifyAdapter(req); + expect(adapter.getQueryParams()).toEqual({}); + }); + }); + + describe("getQueryParam", () => { + it("returns single value for single param", () => { + const req = createMockRequest({ query: { city: "NYC" } }); + const adapter = new FastifyAdapter(req); + expect(adapter.getQueryParam("city")).toBe("NYC"); + }); + + it("returns undefined for missing param", () => { + const req = createMockRequest({ query: {} }); + const adapter = new FastifyAdapter(req); + expect(adapter.getQueryParam("missing")).toBeUndefined(); + }); + }); + + describe("getBody", () => { + it("returns parsed body", () => { + const body = { data: "test" }; + const req = createMockRequest({ body }); + const adapter = new FastifyAdapter(req); + expect(adapter.getBody()).toEqual(body); + }); + + it("returns undefined when no body", () => { + const req = createMockRequest(); + const adapter = new FastifyAdapter(req); + expect(adapter.getBody()).toBeUndefined(); + }); + }); +}); diff --git a/typescript/packages/http/fastify/src/adapter.ts b/typescript/packages/http/fastify/src/adapter.ts new file mode 100644 index 0000000000..706bc56a12 --- /dev/null +++ b/typescript/packages/http/fastify/src/adapter.ts @@ -0,0 +1,99 @@ +import { HTTPAdapter } from "@x402/core/server"; +import { FastifyRequest } from "fastify"; + +/** + * Fastify adapter implementation for the x402 HTTP protocol. + */ +export class FastifyAdapter implements HTTPAdapter { + /** + * Creates a new FastifyAdapter instance. + * + * @param request - The Fastify request object + */ + constructor(private request: FastifyRequest) {} + + /** + * Gets a header value from the request. + * + * @param name - The header name + * @returns The header value or undefined + */ + getHeader(name: string): string | undefined { + const value = this.request.headers[name.toLowerCase()]; + return Array.isArray(value) ? value[0] : value; + } + + /** + * Gets the HTTP method of the request. + * + * @returns The HTTP method + */ + getMethod(): string { + return this.request.method; + } + + /** + * Gets the path of the request. + * + * @returns The request path without query string + */ + getPath(): string { + return this.request.url.split("?")[0]; + } + + /** + * Gets the full URL of the request. + * + * @returns The full request URL + */ + getUrl(): string { + return `${this.request.protocol}://${this.request.host || this.request.hostname}${this.request.url}`; + } + + /** + * Gets the Accept header from the request. + * + * @returns The Accept header value or empty string + */ + getAcceptHeader(): string { + return this.getHeader("accept") || ""; + } + + /** + * Gets the User-Agent header from the request. + * + * @returns The User-Agent header value or empty string + */ + getUserAgent(): string { + return this.getHeader("user-agent") || ""; + } + + /** + * Gets all query parameters from the request URL. + * + * @returns Record of query parameter key-value pairs + */ + getQueryParams(): Record { + return (this.request.query as Record) || {}; + } + + /** + * Gets a specific query parameter by name. + * + * @param name - The query parameter name + * @returns The query parameter value(s) or undefined + */ + getQueryParam(name: string): string | string[] | undefined { + return this.getQueryParams()[name]; + } + + /** + * Gets the parsed request body. + * Fastify automatically parses JSON bodies. + * + * @returns The parsed request body + */ + getBody(): unknown { + return this.request.body; + } +} diff --git a/typescript/packages/http/fastify/src/index.test.ts b/typescript/packages/http/fastify/src/index.test.ts new file mode 100644 index 0000000000..ce0e2afbd5 --- /dev/null +++ b/typescript/packages/http/fastify/src/index.test.ts @@ -0,0 +1,890 @@ +import { describe, it, expect, vi, beforeEach } from "vitest"; +import type { FastifyInstance, FastifyRequest, FastifyReply } from "fastify"; +import type { + HTTPProcessResult, + x402HTTPResourceServer, + PaywallProvider, + FacilitatorClient, +} from "@x402/core/server"; +import { + x402ResourceServer, + x402HTTPResourceServer as HTTPResourceServer, +} from "@x402/core/server"; +import type { PaymentPayload, PaymentRequirements, SchemeNetworkServer } from "@x402/core/types"; +import { paymentMiddleware, paymentMiddlewareFromConfig, type SchemeRegistration } from "./index"; + +// --- Test Fixtures --- +const mockRoutes = { + "/api/*": { + accepts: { scheme: "exact", payTo: "0x123", price: "$0.01", network: "eip155:84532" }, + }, +} as const; + +const mockPaymentPayload = { + scheme: "exact", + network: "eip155:84532", + payload: { signature: "0xabc" }, +} as unknown as PaymentPayload; + +const mockPaymentRequirements = { + scheme: "exact", + network: "eip155:84532", + maxAmountRequired: "1000", + payTo: "0x123", +} as unknown as PaymentRequirements; + +// --- Mock setup --- +let mockProcessHTTPRequest: ReturnType; +let mockProcessSettlement: ReturnType; +let mockRegisterPaywallProvider: ReturnType; +let mockRequiresPayment: ReturnType; + +vi.mock("@x402/core/server", () => ({ + SETTLEMENT_OVERRIDES_HEADER: "Settlement-Overrides", + FacilitatorResponseError: class FacilitatorResponseError extends Error { + /** + * Mock error class matching @x402/core/server FacilitatorResponseError. + * + * @param message - Error message passed to the superclass. + */ + constructor(message: string) { + super(message); + this.name = "FacilitatorResponseError"; + } + }, + getFacilitatorResponseError: (error: unknown) => { + let current = error; + while (current instanceof Error) { + if (current.name === "FacilitatorResponseError") { + return current; + } + current = (current as Error & { cause?: unknown }).cause; + } + return null; + }, + x402ResourceServer: vi.fn().mockImplementation(() => ({ + initialize: vi.fn().mockResolvedValue(undefined), + registerExtension: vi.fn(), + register: vi.fn(), + hasExtension: vi.fn().mockReturnValue(false), + })), + x402HTTPResourceServer: vi.fn().mockImplementation((server, routes) => ({ + initialize: vi.fn().mockResolvedValue(undefined), + processHTTPRequest: mockProcessHTTPRequest, + processSettlement: mockProcessSettlement, + registerPaywallProvider: mockRegisterPaywallProvider, + requiresPayment: mockRequiresPayment, + routes: routes, + server: server || { + hasExtension: vi.fn().mockReturnValue(false), + registerExtension: vi.fn(), + }, + })), +})); + +// --- Hook Capture --- +type HookHandler = (...args: unknown[]) => Promise; + +/** + * Captured hooks from a mock Fastify instance. + */ +interface CapturedHooks { + onRequest: HookHandler[]; + onSend: HookHandler[]; +} + +/** + * Creates a mock Fastify instance that captures registered hooks. + * + * @returns Object containing the mock app and captured hooks. + */ +function createMockApp(): { app: FastifyInstance; hooks: CapturedHooks } { + const hooks: CapturedHooks = { onRequest: [], onSend: [] }; + + const app = { + addHook: vi.fn((name: string, handler: HookHandler) => { + if (name === "onRequest") hooks.onRequest.push(handler); + if (name === "onSend") hooks.onSend.push(handler); + }), + decorateRequest: vi.fn(), + } as unknown as FastifyInstance; + + return { app, hooks }; +} + +/** + * Sets up the mock HTTP server to return specified results. + * + * @param processResult - The result to return from processHTTPRequest. + * @param settlementResult - Result to return from processSettlement. + */ +function setupMockHttpServer( + processResult: HTTPProcessResult, + settlementResult: + | { success: true; headers: Record } + | { + success: false; + errorReason: string; + headers: Record; + response: { + status: number; + headers: Record; + body?: unknown; + isHtml?: boolean; + }; + } = { + success: true, + headers: {}, + }, +): void { + mockProcessHTTPRequest.mockResolvedValue(processResult); + mockProcessSettlement.mockResolvedValue(settlementResult); +} + +/** + * Creates a mock Fastify request for testing. + * + * @param options - Configuration options for the mock request. + * @param options.url - The request URL path. + * @param options.method - The HTTP method. + * @param options.headers - Request headers. + * @returns A mock Fastify request. + */ +function createMockRequest( + options: { + url?: string; + method?: string; + headers?: Record; + } = {}, +): FastifyRequest { + return { + url: options.url || "/api/test", + method: options.method || "GET", + headers: options.headers || {}, + query: {}, + body: undefined, + protocol: "https", + hostname: "example.com", + } as unknown as FastifyRequest; +} + +/** + * Creates a mock Fastify reply for testing. + * + * @returns A mock Fastify reply with tracking properties. + */ +function createMockReply(): FastifyReply & { + _status: number; + _headers: Record; + _body: unknown; + _type: string | undefined; +} { + const reply = { + _status: 200, + _headers: {} as Record, + _body: undefined as unknown, + _type: undefined as string | undefined, + statusCode: 200, + raw: { + write: vi.fn(), + end: vi.fn(), + writeHead: vi.fn(), + flushHeaders: vi.fn(), + }, + getHeaders: vi.fn(function (this: typeof reply) { + return this._headers; + }), + getHeader: vi.fn(function (this: typeof reply, key: string) { + return this._headers[key]; + }), + removeHeader: vi.fn(function (this: typeof reply, key: string) { + delete this._headers[key]; + return this; + }), + header: vi.fn(function (this: typeof reply, key: string, value: string) { + this._headers[key] = value; + return this; + }), + status: vi.fn(function (this: typeof reply, code: number) { + this._status = code; + this.statusCode = code; + return this; + }), + type: vi.fn(function (this: typeof reply, contentType: string) { + this._type = contentType; + return this; + }), + send: vi.fn(function (this: typeof reply, body: unknown) { + this._body = body; + return this; + }), + }; + + return reply as unknown as typeof reply; +} + +// --- Tests --- +describe("paymentMiddleware", () => { + beforeEach(() => { + vi.clearAllMocks(); + mockProcessHTTPRequest = vi.fn(); + mockProcessSettlement = vi.fn(); + mockRegisterPaywallProvider = vi.fn(); + mockRequiresPayment = vi.fn().mockReturnValue(true); + + vi.mocked(HTTPResourceServer).mockImplementation( + (server, routes) => + ({ + initialize: vi.fn().mockResolvedValue(undefined), + processHTTPRequest: mockProcessHTTPRequest, + processSettlement: mockProcessSettlement, + registerPaywallProvider: mockRegisterPaywallProvider, + requiresPayment: mockRequiresPayment, + routes: routes, + server: server || { + hasExtension: vi.fn().mockReturnValue(false), + registerExtension: vi.fn(), + }, + }) as unknown as x402HTTPResourceServer, + ); + }); + + it("registers onRequest and onSend hooks", () => { + const { app } = createMockApp(); + paymentMiddleware( + app, + mockRoutes, + {} as unknown as x402ResourceServer, + undefined, + undefined, + false, + ); + + expect(app.addHook).toHaveBeenCalledWith("onRequest", expect.any(Function)); + expect(app.addHook).toHaveBeenCalledWith("onSend", expect.any(Function)); + }); + + it("proceeds when no-payment-required", async () => { + setupMockHttpServer({ type: "no-payment-required" }); + + const { app, hooks } = createMockApp(); + paymentMiddleware( + app, + mockRoutes, + {} as unknown as x402ResourceServer, + undefined, + undefined, + false, + ); + + const request = createMockRequest(); + const reply = createMockReply(); + await hooks.onRequest[0](request, reply); + + expect(mockProcessHTTPRequest).toHaveBeenCalled(); + expect(reply.send).not.toHaveBeenCalled(); + }); + + it("skips payment check for non-protected routes", async () => { + mockRequiresPayment.mockReturnValue(false); + + const { app, hooks } = createMockApp(); + paymentMiddleware( + app, + mockRoutes, + {} as unknown as x402ResourceServer, + undefined, + undefined, + false, + ); + + const request = createMockRequest({ url: "/health" }); + const reply = createMockReply(); + await hooks.onRequest[0](request, reply); + + expect(mockProcessHTTPRequest).not.toHaveBeenCalled(); + expect(reply.send).not.toHaveBeenCalled(); + }); + + it("returns 402 HTML for payment-error with isHtml", async () => { + setupMockHttpServer({ + type: "payment-error", + response: { + status: 402, + body: "Paywall", + headers: { "PAYMENT-REQUIRED": "encoded-data" }, + isHtml: true, + }, + }); + + const { app, hooks } = createMockApp(); + paymentMiddleware( + app, + mockRoutes, + {} as unknown as x402ResourceServer, + undefined, + undefined, + false, + ); + + const request = createMockRequest(); + const reply = createMockReply(); + await hooks.onRequest[0](request, reply); + + expect(reply.status).toHaveBeenCalledWith(402); + expect(reply.type).toHaveBeenCalledWith("text/html"); + expect(reply.send).toHaveBeenCalledWith("Paywall"); + expect(reply.header).toHaveBeenCalledWith("PAYMENT-REQUIRED", "encoded-data"); + }); + + it("returns 402 JSON for payment-error", async () => { + setupMockHttpServer({ + type: "payment-error", + response: { + status: 402, + body: { error: "Payment required" }, + headers: {}, + isHtml: false, + }, + }); + + const { app, hooks } = createMockApp(); + paymentMiddleware( + app, + mockRoutes, + {} as unknown as x402ResourceServer, + undefined, + undefined, + false, + ); + + const request = createMockRequest(); + const reply = createMockReply(); + await hooks.onRequest[0](request, reply); + + expect(reply.status).toHaveBeenCalledWith(402); + expect(reply.send).toHaveBeenCalledWith({ error: "Payment required" }); + }); + + it("stashes payment context on request for payment-verified", async () => { + setupMockHttpServer({ + type: "payment-verified", + paymentPayload: mockPaymentPayload, + paymentRequirements: mockPaymentRequirements, + }); + + const { app, hooks } = createMockApp(); + paymentMiddleware( + app, + mockRoutes, + {} as unknown as x402ResourceServer, + undefined, + undefined, + false, + ); + + const request = createMockRequest(); + const reply = createMockReply(); + await hooks.onRequest[0](request, reply); + + expect(reply.send).not.toHaveBeenCalled(); + expect(request.x402Context).toBeDefined(); + expect(request.x402RawGuard).toBeDefined(); + }); + + it("settles payment and adds headers in onSend for verified payments", async () => { + setupMockHttpServer( + { + type: "payment-verified", + paymentPayload: mockPaymentPayload, + paymentRequirements: mockPaymentRequirements, + }, + { success: true, headers: { "PAYMENT-RESPONSE": "settled" } }, + ); + + const { app, hooks } = createMockApp(); + paymentMiddleware( + app, + mockRoutes, + {} as unknown as x402ResourceServer, + undefined, + undefined, + false, + ); + + const request = createMockRequest(); + const reply = createMockReply(); + + // Step 1: onRequest stashes payment context + await hooks.onRequest[0](request, reply); + + // Step 2: onSend settles payment + const payload = JSON.stringify({ data: "premium content" }); + const result = await hooks.onSend[0](request, reply, payload); + + expect(mockProcessSettlement).toHaveBeenCalledWith( + mockPaymentPayload, + mockPaymentRequirements, + undefined, + expect.objectContaining({ + request: expect.objectContaining({ + path: "/api/test", + method: "GET", + }), + responseBody: expect.any(Buffer), + }), + ); + expect(reply.header).toHaveBeenCalledWith("PAYMENT-RESPONSE", "settled"); + expect(result).toBe(payload); + }); + + it("passes Buffer payload bytes to settlement without JSON stringifying them", async () => { + setupMockHttpServer( + { + type: "payment-verified", + paymentPayload: mockPaymentPayload, + paymentRequirements: mockPaymentRequirements, + }, + { success: true, headers: { "PAYMENT-RESPONSE": "settled" } }, + ); + + const { app, hooks } = createMockApp(); + paymentMiddleware( + app, + mockRoutes, + {} as unknown as x402ResourceServer, + undefined, + undefined, + false, + ); + + const request = createMockRequest(); + const reply = createMockReply(); + const payload = Buffer.from([0, 1, 2, 255]); + + await hooks.onRequest[0](request, reply); + const result = await hooks.onSend[0](request, reply, payload); + + expect(result).toBe(payload); + expect(mockProcessSettlement).toHaveBeenCalledTimes(1); + expect( + (mockProcessSettlement.mock.calls[0]?.[3] as { responseBody?: Buffer }).responseBody, + ).toEqual(payload); + }); + + it("skips settlement for non-payment requests in onSend", async () => { + const { app, hooks } = createMockApp(); + paymentMiddleware( + app, + mockRoutes, + {} as unknown as x402ResourceServer, + undefined, + undefined, + false, + ); + + const request = createMockRequest(); + const reply = createMockReply(); + const payload = JSON.stringify({ data: "free content" }); + + const result = await hooks.onSend[0](request, reply, payload); + + expect(mockProcessSettlement).not.toHaveBeenCalled(); + expect(result).toBe(payload); + }); + + it("skips settlement when handler returns >= 400", async () => { + setupMockHttpServer( + { + type: "payment-verified", + paymentPayload: mockPaymentPayload, + paymentRequirements: mockPaymentRequirements, + }, + { success: true, headers: {} }, + ); + + const { app, hooks } = createMockApp(); + paymentMiddleware( + app, + mockRoutes, + {} as unknown as x402ResourceServer, + undefined, + undefined, + false, + ); + + const request = createMockRequest(); + const reply = createMockReply(); + + await hooks.onRequest[0](request, reply); + + reply.statusCode = 500; + const payload = JSON.stringify({ error: "Server error" }); + const result = await hooks.onSend[0](request, reply, payload); + + expect(mockProcessSettlement).not.toHaveBeenCalled(); + expect(result).toBe(payload); + }); + + it("returns 402 when settlement fails", async () => { + setupMockHttpServer( + { + type: "payment-verified", + paymentPayload: mockPaymentPayload, + paymentRequirements: mockPaymentRequirements, + }, + { + success: false, + errorReason: "Insufficient funds", + headers: {}, + response: { + status: 402, + headers: { + "PAYMENT-RESPONSE": "failed", + "Content-Type": "application/json", + }, + body: { error: "Settlement failed" }, + }, + }, + ); + + const { app, hooks } = createMockApp(); + paymentMiddleware( + app, + mockRoutes, + {} as unknown as x402ResourceServer, + undefined, + undefined, + false, + ); + + const request = createMockRequest(); + const reply = createMockReply(); + + await hooks.onRequest[0](request, reply); + + reply.type("application/octet-stream"); + const payload = JSON.stringify({ data: "premium content" }); + const result = await hooks.onSend[0](request, reply, payload); + + expect(reply.status).toHaveBeenCalledWith(402); + expect(reply.header).toHaveBeenCalledWith("PAYMENT-RESPONSE", "failed"); + expect(reply.type).toHaveBeenCalledWith("application/json"); + expect(result).toBe(JSON.stringify({ error: "Settlement failed" })); + }); + + it("returns 402 when settlement throws error", async () => { + setupMockHttpServer({ + type: "payment-verified", + paymentPayload: mockPaymentPayload, + paymentRequirements: mockPaymentRequirements, + }); + mockProcessSettlement.mockRejectedValue(new Error("Settlement rejected")); + + const { app, hooks } = createMockApp(); + paymentMiddleware( + app, + mockRoutes, + {} as unknown as x402ResourceServer, + undefined, + undefined, + false, + ); + + const request = createMockRequest(); + const reply = createMockReply(); + + await hooks.onRequest[0](request, reply); + + const payload = JSON.stringify({ data: "premium content" }); + const result = await hooks.onSend[0](request, reply, payload); + + expect(reply.status).toHaveBeenCalledWith(402); + expect(reply.type).toHaveBeenCalledWith("application/json"); + expect(result).toBe(JSON.stringify({})); + }); + + it("passes paywallConfig to processHTTPRequest", async () => { + setupMockHttpServer({ type: "no-payment-required" }); + const paywallConfig = { appName: "test-app" }; + + const { app, hooks } = createMockApp(); + paymentMiddleware( + app, + mockRoutes, + {} as unknown as x402ResourceServer, + paywallConfig, + undefined, + false, + ); + + const request = createMockRequest(); + const reply = createMockReply(); + await hooks.onRequest[0](request, reply); + + expect(mockProcessHTTPRequest).toHaveBeenCalledWith(expect.anything(), paywallConfig); + }); + + it("registers custom paywall provider", () => { + const { app } = createMockApp(); + const paywall: PaywallProvider = { generateHtml: vi.fn() }; + + paymentMiddleware( + app, + mockRoutes, + {} as unknown as x402ResourceServer, + undefined, + paywall, + false, + ); + + expect(mockRegisterPaywallProvider).toHaveBeenCalledWith(paywall); + }); +}); + +describe("paymentMiddlewareFromConfig", () => { + beforeEach(() => { + vi.clearAllMocks(); + mockProcessHTTPRequest = vi.fn(); + mockProcessSettlement = vi.fn(); + mockRegisterPaywallProvider = vi.fn(); + mockRequiresPayment = vi.fn().mockReturnValue(true); + + vi.mocked(HTTPResourceServer).mockImplementation( + (server, routes) => + ({ + initialize: vi.fn().mockResolvedValue(undefined), + processHTTPRequest: mockProcessHTTPRequest, + processSettlement: mockProcessSettlement, + registerPaywallProvider: mockRegisterPaywallProvider, + requiresPayment: mockRequiresPayment, + routes: routes, + server: server || { + hasExtension: vi.fn().mockReturnValue(false), + registerExtension: vi.fn(), + }, + }) as unknown as x402HTTPResourceServer, + ); + + vi.mocked(x402ResourceServer).mockImplementation( + () => + ({ + initialize: vi.fn().mockResolvedValue(undefined), + registerExtension: vi.fn(), + register: vi.fn(), + }) as unknown as x402ResourceServer, + ); + }); + + it("creates x402ResourceServer with facilitator clients", () => { + const { app } = createMockApp(); + const facilitator = { verify: vi.fn(), settle: vi.fn() } as unknown as FacilitatorClient; + + paymentMiddlewareFromConfig(app, mockRoutes, facilitator); + + expect(x402ResourceServer).toHaveBeenCalledWith(facilitator); + }); + + it("registers scheme servers for each network", () => { + const { app } = createMockApp(); + const schemeServer = { verify: vi.fn(), settle: vi.fn() } as unknown as SchemeNetworkServer; + const schemes: SchemeRegistration[] = [ + { network: "eip155:84532", server: schemeServer }, + { network: "eip155:8453", server: schemeServer }, + ]; + + paymentMiddlewareFromConfig(app, mockRoutes, undefined, schemes); + + const serverInstance = vi.mocked(x402ResourceServer).mock.results[0].value; + expect(serverInstance.register).toHaveBeenCalledTimes(2); + expect(serverInstance.register).toHaveBeenCalledWith("eip155:84532", schemeServer); + expect(serverInstance.register).toHaveBeenCalledWith("eip155:8453", schemeServer); + }); + + it("registers hooks on the Fastify instance", () => { + const { app } = createMockApp(); + paymentMiddlewareFromConfig(app, mockRoutes); + + expect(app.addHook).toHaveBeenCalledWith("onRequest", expect.any(Function)); + expect(app.addHook).toHaveBeenCalledWith("onSend", expect.any(Function)); + }); +}); + +describe("FastifyAdapter integration", () => { + beforeEach(() => { + vi.clearAllMocks(); + mockProcessHTTPRequest = vi.fn(); + mockProcessSettlement = vi.fn(); + mockRegisterPaywallProvider = vi.fn(); + mockRequiresPayment = vi.fn().mockReturnValue(true); + + vi.mocked(HTTPResourceServer).mockImplementation( + (server, routes) => + ({ + initialize: vi.fn().mockResolvedValue(undefined), + processHTTPRequest: mockProcessHTTPRequest, + processSettlement: mockProcessSettlement, + registerPaywallProvider: mockRegisterPaywallProvider, + requiresPayment: mockRequiresPayment, + routes: routes, + server: server || { + hasExtension: vi.fn().mockReturnValue(false), + registerExtension: vi.fn(), + }, + }) as unknown as x402HTTPResourceServer, + ); + }); + + it("extracts path and method from request", async () => { + setupMockHttpServer({ type: "no-payment-required" }); + + const { app, hooks } = createMockApp(); + paymentMiddleware( + app, + mockRoutes, + {} as unknown as x402ResourceServer, + undefined, + undefined, + false, + ); + + const request = createMockRequest({ url: "/api/weather", method: "POST" }); + const reply = createMockReply(); + await hooks.onRequest[0](request, reply); + + expect(mockProcessHTTPRequest).toHaveBeenCalledWith( + expect.objectContaining({ + path: "/api/weather", + method: "POST", + }), + undefined, + ); + }); + + it("strips query string from path", async () => { + setupMockHttpServer({ type: "no-payment-required" }); + + const { app, hooks } = createMockApp(); + paymentMiddleware( + app, + mockRoutes, + {} as unknown as x402ResourceServer, + undefined, + undefined, + false, + ); + + const request = createMockRequest({ url: "/api/weather?city=NYC" }); + const reply = createMockReply(); + await hooks.onRequest[0](request, reply); + + expect(mockProcessHTTPRequest).toHaveBeenCalledWith( + expect.objectContaining({ + path: "/api/weather", + }), + undefined, + ); + }); + + it("extracts payment-signature header", async () => { + setupMockHttpServer({ type: "no-payment-required" }); + + const { app, hooks } = createMockApp(); + paymentMiddleware( + app, + mockRoutes, + {} as unknown as x402ResourceServer, + undefined, + undefined, + false, + ); + + const request = createMockRequest({ headers: { "payment-signature": "sig-data" } }); + const reply = createMockReply(); + await hooks.onRequest[0](request, reply); + + expect(mockProcessHTTPRequest).toHaveBeenCalledWith( + expect.objectContaining({ + paymentHeader: "sig-data", + }), + undefined, + ); + }); + + it("extracts x-payment header", async () => { + setupMockHttpServer({ type: "no-payment-required" }); + + const { app, hooks } = createMockApp(); + paymentMiddleware( + app, + mockRoutes, + {} as unknown as x402ResourceServer, + undefined, + undefined, + false, + ); + + const request = createMockRequest({ headers: { "x-payment": "payment-data" } }); + const reply = createMockReply(); + await hooks.onRequest[0](request, reply); + + expect(mockProcessHTTPRequest).toHaveBeenCalledWith( + expect.objectContaining({ + paymentHeader: "payment-data", + }), + undefined, + ); + }); + + it("prefers payment-signature over x-payment", async () => { + setupMockHttpServer({ type: "no-payment-required" }); + + const { app, hooks } = createMockApp(); + paymentMiddleware( + app, + mockRoutes, + {} as unknown as x402ResourceServer, + undefined, + undefined, + false, + ); + + const request = createMockRequest({ + headers: { "payment-signature": "sig-data", "x-payment": "x-payment-data" }, + }); + const reply = createMockReply(); + await hooks.onRequest[0](request, reply); + + expect(mockProcessHTTPRequest).toHaveBeenCalledWith( + expect.objectContaining({ + paymentHeader: "sig-data", + }), + undefined, + ); + }); + + it("returns undefined paymentHeader when no payment headers present", async () => { + setupMockHttpServer({ type: "no-payment-required" }); + + const { app, hooks } = createMockApp(); + paymentMiddleware( + app, + mockRoutes, + {} as unknown as x402ResourceServer, + undefined, + undefined, + false, + ); + + const request = createMockRequest(); + const reply = createMockReply(); + await hooks.onRequest[0](request, reply); + + expect(mockProcessHTTPRequest).toHaveBeenCalledWith( + expect.objectContaining({ + paymentHeader: undefined, + }), + undefined, + ); + }); +}); diff --git a/typescript/packages/http/fastify/src/index.ts b/typescript/packages/http/fastify/src/index.ts new file mode 100644 index 0000000000..1e32952d7a --- /dev/null +++ b/typescript/packages/http/fastify/src/index.ts @@ -0,0 +1,580 @@ +import type { ServerResponse } from "http"; +import { + HTTPRequestContext, + PaywallConfig, + PaywallProvider, + x402HTTPResourceServer, + x402ResourceServer, + RoutesConfig, + FacilitatorClient, + FacilitatorResponseError, + getFacilitatorResponseError, + SETTLEMENT_OVERRIDES_HEADER, + SettlementOverrides, +} from "@x402/core/server"; +import { + SchemeNetworkServer, + Network, + PaymentPayload, + PaymentRequirements, +} from "@x402/core/types"; +import { FastifyInstance, FastifyRequest, FastifyReply } from "fastify"; +import { FastifyAdapter } from "./adapter"; + +/** + * Sets settlement overrides on a Fastify reply for partial settlement (upto scheme). + * The middleware extracts these before settlement and strips the header from the client response. + * + * @param reply - The Fastify reply object + * @param overrides - Settlement overrides (e.g., { amount: "500" } for partial settlement) + */ +export function setSettlementOverrides(reply: FastifyReply, overrides: SettlementOverrides): void { + reply.header(SETTLEMENT_OVERRIDES_HEADER, JSON.stringify(overrides)); +} + +interface X402PaymentContext { + paymentPayload: PaymentPayload; + paymentRequirements: PaymentRequirements; + declaredExtensions?: Record; + requestContext: HTTPRequestContext; +} + +interface BufferedWriteHead { + method: "writeHead"; + statusCode: number; + headers?: Record; +} + +interface BufferedWrite { + method: "write"; + data: string | Buffer; +} + +interface BufferedEnd { + method: "end"; + data?: string | Buffer; +} + +interface BufferedFlushHeaders { + method: "flushHeaders"; +} + +type BufferedRawCall = BufferedWriteHead | BufferedWrite | BufferedEnd | BufferedFlushHeaders; + +interface RawGuard { + triggered: boolean; + buffer: BufferedRawCall[]; + deactivate: () => void; +} + +declare module "fastify" { + interface FastifyRequest { + x402Context?: X402PaymentContext; + x402RawGuard?: RawGuard; + } +} + +/** + * Gets a header value from a plain header record using a case-insensitive lookup. + * + * @param headers - Headers to search + * @param headerName - Header name to find + * @returns Matching header value or undefined + */ +function getHeaderValue(headers: Record, headerName: string): string | undefined { + const target = headerName.toLowerCase(); + return Object.entries(headers).find(([key]) => key.toLowerCase() === target)?.[1]; +} + +/** + * Converts a Fastify onSend payload into the byte representation used for settlement. + * + * @param payload - Fastify payload + * @returns Buffer when the payload can be represented eagerly, otherwise undefined + */ +function getResponseBodyBuffer(payload: unknown): Buffer | undefined { + if (typeof payload === "string") { + return Buffer.from(payload); + } + + if (Buffer.isBuffer(payload)) { + return payload; + } + + if (payload instanceof Uint8Array) { + return Buffer.from(payload); + } + + if (payload instanceof ArrayBuffer) { + return Buffer.from(new Uint8Array(payload)); + } + + if (payload && typeof payload === "object" && "pipe" in payload) { + return undefined; + } + + return Buffer.from(JSON.stringify(payload ?? {})); +} + +/** + * Check if any routes in the configuration declare bazaar extensions. + * + * @param routes - Route configuration + * @returns True if any route has extensions.bazaar defined + */ +function checkIfBazaarNeeded(routes: RoutesConfig): boolean { + if ("accepts" in routes) { + return !!(routes.extensions && "bazaar" in routes.extensions); + } + + return Object.values(routes).some(routeConfig => { + return !!(routeConfig.extensions && "bazaar" in routeConfig.extensions); + }); +} + +/** + * Buffers reply.raw method calls on a protected route so that settlement + * can inspect the response body before anything reaches the client. + * + * Fastify's normal reply flow (return value / reply.send) goes through the + * onSend hook where settlement runs before data reaches the client. However, + * reply.raw gives direct access to the underlying Node.js ServerResponse, + * allowing data to be flushed without triggering onSend. + * + * This guard intercepts writeHead/write/end/flushHeaders, stores them in a + * buffer, and ensures Fastify's lifecycle still fires (via reply.send on end) + * so that onSend can reconstruct the response, settle, then replay the calls. + * + * The guard is deactivated at the start of onSend so that Fastify's own + * internal reply.raw usage (which happens after onSend) is unaffected. + * + * @param reply - Fastify reply whose raw ServerResponse is wrapped for buffering. + * @returns Guard state and buffer used to replay raw writes after settlement. + */ +function guardReplyRaw(reply: FastifyReply): RawGuard { + const raw = reply.raw; + const origWrite = raw.write; + const origEnd = raw.end; + const origWriteHead = raw.writeHead; + const origFlushHeaders = raw.flushHeaders; + + let active = true; + const guard: RawGuard = { + triggered: false, + buffer: [], + deactivate() { + if (!active) return; + active = false; + raw.write = origWrite; + raw.end = origEnd; + raw.writeHead = origWriteHead; + raw.flushHeaders = origFlushHeaders; + }, + }; + + raw.writeHead = function (this: ServerResponse, ...args: unknown[]) { + if (active) { + guard.triggered = true; + const statusCode = args[0] as number; + const headers = (typeof args[1] === "string" ? args[2] : args[1]) as + | Record + | undefined; + guard.buffer.push({ method: "writeHead", statusCode, headers }); + return this; + } + return Reflect.apply(origWriteHead, this, args) as ServerResponse; + } as ServerResponse["writeHead"]; + + raw.write = function (this: ServerResponse, ...args: unknown[]) { + if (active) { + guard.triggered = true; + guard.buffer.push({ method: "write", data: args[0] as string | Buffer }); + return true; + } + return Reflect.apply(origWrite, this, args) as boolean; + } as ServerResponse["write"]; + + raw.end = function (this: ServerResponse, ...args: unknown[]) { + if (active) { + guard.triggered = true; + const data = + typeof args[0] === "function" ? undefined : (args[0] as string | Buffer | undefined); + guard.buffer.push({ method: "end", data }); + return this; + } + return Reflect.apply(origEnd, this, args) as ServerResponse; + } as ServerResponse["end"]; + + raw.flushHeaders = function (this: ServerResponse) { + if (active) { + guard.triggered = true; + guard.buffer.push({ method: "flushHeaders" }); + } else { + origFlushHeaders.call(this); + } + }; + + return guard; +} + +/** + * Sends a normalized 502 response for facilitator boundary failures. + * + * @param reply - The Fastify reply to write to + * @param error - The facilitator response error to surface + */ +function sendFacilitatorError(reply: FastifyReply, error: FacilitatorResponseError): void { + reply.status(502).send({ error: error.message }); +} + +/** + * Configuration for registering a payment scheme with a specific network. + */ +export interface SchemeRegistration { + /** + * The network identifier (e.g., 'eip155:84532', 'solana:mainnet') + */ + network: Network; + + /** + * The scheme server implementation for this network + */ + server: SchemeNetworkServer; +} + +/** + * Registers x402 payment middleware on a Fastify instance using a pre-configured HTTP server. + * + * Use this when you need to configure HTTP-level hooks. + * + * @param app - The Fastify instance + * @param httpServer - Pre-configured x402HTTPResourceServer instance + * @param paywallConfig - Optional configuration for the built-in paywall UI + * @param paywall - Optional custom paywall provider (overrides default) + * @param syncFacilitatorOnStart - Whether to sync with the facilitator on startup (defaults to true) + * + * @example + * ```typescript + * import { paymentMiddlewareFromHTTPServer, x402ResourceServer, x402HTTPResourceServer } from "@x402/fastify"; + * + * const resourceServer = new x402ResourceServer(facilitatorClient) + * .register(NETWORK, new ExactEvmScheme()); + * + * const httpServer = new x402HTTPResourceServer(resourceServer, routes) + * .onProtectedRequest(requestHook); + * + * paymentMiddlewareFromHTTPServer(app, httpServer); + * ``` + */ +export function paymentMiddlewareFromHTTPServer( + app: FastifyInstance, + httpServer: x402HTTPResourceServer, + paywallConfig?: PaywallConfig, + paywall?: PaywallProvider, + syncFacilitatorOnStart: boolean = true, +): void { + if (paywall) { + httpServer.registerPaywallProvider(paywall); + } + + app.decorateRequest("x402Context", undefined); + app.decorateRequest("x402RawGuard", undefined); + + let initPromise: Promise | null = syncFacilitatorOnStart ? httpServer.initialize() : null; + let isInitialized = false; + + /** + * Ensures facilitator initialization succeeds once, while allowing retries after failures. + */ + async function initializeHttpServer(): Promise { + if (!syncFacilitatorOnStart || isInitialized) { + return; + } + + if (!initPromise) { + initPromise = httpServer.initialize(); + } + + try { + await initPromise; + isInitialized = true; + } catch (error) { + initPromise = null; + throw error; + } + } + + let bazaarPromise: Promise | null = null; + if (checkIfBazaarNeeded(httpServer.routes) && !httpServer.server.hasExtension("bazaar")) { + bazaarPromise = import("@x402/extensions/bazaar") + .then(({ bazaarResourceServerExtension }) => { + httpServer.server.registerExtension(bazaarResourceServerExtension); + }) + .catch(err => { + console.error("Failed to load bazaar extension:", err); + }); + } + + app.addHook("onRequest", async (request: FastifyRequest, reply: FastifyReply) => { + const path = request.url.split("?")[0]; + const adapter = new FastifyAdapter(request); + const context: HTTPRequestContext = { + adapter, + path, + method: request.method, + paymentHeader: + (request.headers["payment-signature"] as string | undefined) || + (request.headers["x-payment"] as string | undefined), + }; + + if (!httpServer.requiresPayment(context)) { + return; + } + + if (syncFacilitatorOnStart && !isInitialized) { + try { + await initializeHttpServer(); + } catch (error) { + const facilitatorError = getFacilitatorResponseError(error); + if (facilitatorError) { + return sendFacilitatorError(reply, facilitatorError); + } + throw error; + } + } + + if (bazaarPromise) { + await bazaarPromise; + bazaarPromise = null; + } + + let result: Awaited>; + try { + result = await httpServer.processHTTPRequest(context, paywallConfig); + } catch (error) { + if (error instanceof FacilitatorResponseError) { + return sendFacilitatorError(reply, error); + } + throw error; + } + + switch (result.type) { + case "no-payment-required": + return; + + case "payment-error": { + const { response } = result; + for (const [key, value] of Object.entries(response.headers)) { + reply.header(key, value); + } + if (response.isHtml) { + return reply.status(response.status).type("text/html").send(response.body); + } else { + return reply.status(response.status).send(response.body || {}); + } + } + + case "payment-verified": { + request.x402Context = { + paymentPayload: result.paymentPayload, + paymentRequirements: result.paymentRequirements, + declaredExtensions: result.declaredExtensions, + requestContext: context, + }; + request.x402RawGuard = guardReplyRaw(reply); + return; + } + } + }); + + app.addHook("onSend", async (request: FastifyRequest, reply: FastifyReply, payload: unknown) => { + const rawGuard = request.x402RawGuard; + if (rawGuard) { + rawGuard.deactivate(); + } + + const x402Context = request.x402Context; + if (!x402Context) { + return payload; + } + + let effectivePayload: unknown = payload; + if (rawGuard?.triggered && rawGuard.buffer.length > 0) { + const writeHeadCall = rawGuard.buffer.find( + (c): c is BufferedWriteHead => c.method === "writeHead", + ); + if (writeHeadCall) { + reply.status(writeHeadCall.statusCode); + if (writeHeadCall.headers) { + for (const [key, value] of Object.entries(writeHeadCall.headers)) { + if (value != null) reply.header(key, String(value)); + } + } + } + + const bodyChunks: Buffer[] = []; + for (const call of rawGuard.buffer) { + if (call.method === "write") { + bodyChunks.push(Buffer.from(call.data)); + } else if (call.method === "end" && call.data != null) { + bodyChunks.push(Buffer.from(call.data)); + } + } + if (bodyChunks.length > 0) { + effectivePayload = Buffer.concat(bodyChunks); + } + } + + if (reply.statusCode >= 400) { + return effectivePayload; + } + + try { + const responseBody = getResponseBodyBuffer(effectivePayload); + + const responseHeaders: Record = {}; + for (const [key, value] of Object.entries(reply.getHeaders())) { + if (value != null) { + responseHeaders[key] = String(value); + } + } + + const settleResult = await httpServer.processSettlement( + x402Context.paymentPayload, + x402Context.paymentRequirements, + x402Context.declaredExtensions, + { request: x402Context.requestContext, responseBody, responseHeaders }, + ); + + if (!settleResult.success) { + const { response } = settleResult; + for (const [key, value] of Object.entries(response.headers)) { + reply.header(key, value); + } + reply.status(response.status); + reply.type( + getHeaderValue(response.headers, "content-type") || + (response.isHtml ? "text/html" : "application/json"), + ); + return response.isHtml ? String(response.body ?? "") : JSON.stringify(response.body ?? {}); + } + + for (const [key, value] of Object.entries(settleResult.headers)) { + reply.header(key, value); + } + return effectivePayload; + } catch (error) { + if (error instanceof FacilitatorResponseError) { + reply.status(502); + reply.type("application/json"); + return JSON.stringify({ error: error.message }); + } + console.error(error); + reply.status(402); + reply.type("application/json"); + return JSON.stringify({}); + } + }); +} + +/** + * Registers x402 payment middleware on a Fastify instance using a pre-configured resource server. + * + * Use this when you want to pass a pre-configured x402ResourceServer instance. + * This provides more flexibility for testing, custom configuration, and reusing + * server instances across multiple middlewares. + * + * @param app - The Fastify instance + * @param routes - Route configurations for protected endpoints + * @param server - Pre-configured x402ResourceServer instance + * @param paywallConfig - Optional configuration for the built-in paywall UI + * @param paywall - Optional custom paywall provider (overrides default) + * @param syncFacilitatorOnStart - Whether to sync with the facilitator on startup (defaults to true) + * + * @example + * ```typescript + * import { paymentMiddleware } from "@x402/fastify"; + * + * const server = new x402ResourceServer(myFacilitatorClient) + * .register(NETWORK, new ExactEvmScheme()); + * + * paymentMiddleware(app, routes, server, paywallConfig); + * ``` + */ +export function paymentMiddleware( + app: FastifyInstance, + routes: RoutesConfig, + server: x402ResourceServer, + paywallConfig?: PaywallConfig, + paywall?: PaywallProvider, + syncFacilitatorOnStart: boolean = true, +): void { + const httpServer = new x402HTTPResourceServer(server, routes); + + paymentMiddlewareFromHTTPServer(app, httpServer, paywallConfig, paywall, syncFacilitatorOnStart); +} + +/** + * Registers x402 payment middleware on a Fastify instance using configuration. + * + * Use this when you want to quickly set up middleware with simple configuration. + * This function creates and configures the x402ResourceServer internally. + * + * @param app - The Fastify instance + * @param routes - Route configurations for protected endpoints + * @param facilitatorClients - Optional facilitator client(s) for payment processing + * @param schemes - Optional array of scheme registrations for server-side payment processing + * @param paywallConfig - Optional configuration for the built-in paywall UI + * @param paywall - Optional custom paywall provider (overrides default) + * @param syncFacilitatorOnStart - Whether to sync with the facilitator on startup (defaults to true) + * + * @example + * ```typescript + * import { paymentMiddlewareFromConfig } from "@x402/fastify"; + * + * paymentMiddlewareFromConfig( + * app, + * routes, + * myFacilitatorClient, + * [{ network: "eip155:8453", server: evmSchemeServer }], + * paywallConfig + * ); + * ``` + */ +export function paymentMiddlewareFromConfig( + app: FastifyInstance, + routes: RoutesConfig, + facilitatorClients?: FacilitatorClient | FacilitatorClient[], + schemes?: SchemeRegistration[], + paywallConfig?: PaywallConfig, + paywall?: PaywallProvider, + syncFacilitatorOnStart: boolean = true, +): void { + const ResourceServer = new x402ResourceServer(facilitatorClients); + + if (schemes) { + for (const { network, server: schemeServer } of schemes) { + ResourceServer.register(network, schemeServer); + } + } + + paymentMiddleware(app, routes, ResourceServer, paywallConfig, paywall, syncFacilitatorOnStart); +} + +export { x402ResourceServer, x402HTTPResourceServer } from "@x402/core/server"; + +export type { + PaymentRequired, + PaymentRequirements, + PaymentPayload, + Network, + SchemeNetworkServer, +} from "@x402/core/types"; + +export type { PaywallProvider, PaywallConfig } from "@x402/core/server"; + +export { RouteConfigurationError, SETTLEMENT_OVERRIDES_HEADER } from "@x402/core/server"; + +export type { RouteValidationError } from "@x402/core/server"; + +export { FastifyAdapter } from "./adapter"; diff --git a/typescript/packages/http/fastify/src/malformedPathBypass.test.ts b/typescript/packages/http/fastify/src/malformedPathBypass.test.ts new file mode 100644 index 0000000000..f56750b345 --- /dev/null +++ b/typescript/packages/http/fastify/src/malformedPathBypass.test.ts @@ -0,0 +1,157 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; +import type { FastifyInstance, FastifyRequest, FastifyReply } from "fastify"; +import { paymentMiddleware } from "./index"; +import { + x402HTTPResourceServer, + x402ResourceServer, + type HTTPRequestContext, +} from "@x402/core/server"; + +type HookHandler = (...args: unknown[]) => Promise; + +/** + * Captured hooks from a mock Fastify instance. + */ +interface CapturedHooks { + onRequest: HookHandler[]; + onSend: HookHandler[]; +} + +/** + * Creates a mock Fastify instance that captures registered hooks. + * + * @returns Object containing the mock app and captured hooks. + */ +function createMockApp(): { app: FastifyInstance; hooks: CapturedHooks } { + const hooks: CapturedHooks = { onRequest: [], onSend: [] }; + + const app = { + addHook: vi.fn((name: string, handler: HookHandler) => { + if (name === "onRequest") hooks.onRequest.push(handler); + if (name === "onSend") hooks.onSend.push(handler); + }), + decorateRequest: vi.fn(), + } as unknown as FastifyInstance; + + return { app, hooks }; +} + +/** + * Creates a mock Fastify request for testing. + * + * @param options - Configuration options for the mock request. + * @param options.url - The request URL path. + * @param options.method - The HTTP method. + * @param options.headers - Request headers. + * @returns A mock Fastify request. + */ +function createMockRequest( + options: { + url?: string; + method?: string; + headers?: Record; + } = {}, +): FastifyRequest { + return { + url: options.url || "/api/test", + method: options.method || "GET", + headers: options.headers || {}, + query: {}, + body: undefined, + protocol: "https", + hostname: "example.com", + } as unknown as FastifyRequest; +} + +/** + * Creates a mock Fastify reply for testing. + * + * @returns A mock Fastify reply with tracking properties. + */ +function createMockReply(): FastifyReply & { + _status: number; + _headers: Record; + _body: unknown; + _type: string | undefined; +} { + const reply = { + _status: 200, + _headers: {} as Record, + _body: undefined as unknown, + _type: undefined as string | undefined, + statusCode: 200, + header: vi.fn(function (this: typeof reply, key: string, value: string) { + this._headers[key] = value; + return this; + }), + status: vi.fn(function (this: typeof reply, code: number) { + this._status = code; + this.statusCode = code; + return this; + }), + type: vi.fn(function (this: typeof reply, contentType: string) { + this._type = contentType; + return this; + }), + send: vi.fn(function (this: typeof reply, body: unknown) { + this._body = body; + return this; + }), + }; + + return reply as unknown as typeof reply; +} + +describe("paymentMiddleware malformed path bypass", () => { + let processSpy: ReturnType; + + beforeEach(() => { + processSpy = vi + .spyOn(x402HTTPResourceServer.prototype, "processHTTPRequest") + .mockImplementation(async (context: HTTPRequestContext) => { + return { + type: "payment-error", + response: { + status: 402, + body: { error: "Payment required", path: context.path }, + headers: {}, + isHtml: false, + }, + }; + }); + }); + + afterEach(() => { + processSpy.mockRestore(); + }); + + it.each(["/paywall/some-param%", "/paywall/some-param%c0"])( + "does not skip payment check and returns 402 for %s", + async path => { + const routes = { + "/paywall/*": { + accepts: { + scheme: "exact", + payTo: "0xabc", + price: "$1.00", + network: "eip155:8453", + }, + }, + }; + + const server = new x402ResourceServer(); + + const { app, hooks } = createMockApp(); + paymentMiddleware(app, routes, server, undefined, undefined, false); + + const request = createMockRequest({ url: path }); + const reply = createMockReply(); + + await hooks.onRequest[0](request, reply); + + expect(processSpy).toHaveBeenCalled(); + expect(processSpy.mock.calls[0]?.[0]).toEqual(expect.objectContaining({ path })); + expect(reply._status).toBe(402); + }, + ); +}); diff --git a/typescript/packages/http/fastify/tsconfig.json b/typescript/packages/http/fastify/tsconfig.json new file mode 100644 index 0000000000..1b119d3868 --- /dev/null +++ b/typescript/packages/http/fastify/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "allowJs": false, + "checkJs": false + }, + "include": ["src"] +} diff --git a/typescript/packages/http/fastify/tsup.config.ts b/typescript/packages/http/fastify/tsup.config.ts new file mode 100644 index 0000000000..f8699f925c --- /dev/null +++ b/typescript/packages/http/fastify/tsup.config.ts @@ -0,0 +1,27 @@ +import { defineConfig } from "tsup"; + +const baseConfig = { + entry: { + index: "src/index.ts", + }, + dts: { + resolve: true, + }, + sourcemap: true, + target: "node16", +}; + +export default defineConfig([ + { + ...baseConfig, + format: "esm", + outDir: "dist/esm", + clean: true, + }, + { + ...baseConfig, + format: "cjs", + outDir: "dist/cjs", + clean: false, + }, +]); diff --git a/typescript/packages/http/fastify/vitest.config.ts b/typescript/packages/http/fastify/vitest.config.ts new file mode 100644 index 0000000000..156f8c924f --- /dev/null +++ b/typescript/packages/http/fastify/vitest.config.ts @@ -0,0 +1,10 @@ +import { loadEnv } from "vite"; +import { defineConfig } from "vitest/config"; +import tsconfigPaths from "vite-tsconfig-paths"; + +export default defineConfig(({ mode }) => ({ + test: { + env: loadEnv(mode, process.cwd(), ""), + }, + plugins: [tsconfigPaths({ projects: ["."] })], +})); diff --git a/typescript/packages/http/fetch/CHANGELOG.md b/typescript/packages/http/fetch/CHANGELOG.md index 3de3a9e62a..da8feca42c 100644 --- a/typescript/packages/http/fetch/CHANGELOG.md +++ b/typescript/packages/http/fetch/CHANGELOG.md @@ -1,5 +1,28 @@ # @x402/fetch Changelog +## 2.8.0 + +### Minor Changes + +- Updated dependencies [067f297] +- Updated dependencies [4c1e44f] +- Updated dependencies [5135fab] + - @x402/core@2.8.0 + +## 2.7.0 + +### Minor Changes + +- Updated dependencies [8931cb3] + - @x402/core@2.7.0 + +## 2.6.0 + +### Minor Changes + +- Updated dependencies + - @x402/core@2.6.0 + ## 2.5.0 ### Minor Changes diff --git a/typescript/packages/http/fetch/package.json b/typescript/packages/http/fetch/package.json index a2ba606676..e0c9520227 100644 --- a/typescript/packages/http/fetch/package.json +++ b/typescript/packages/http/fetch/package.json @@ -1,6 +1,6 @@ { "name": "@x402/fetch", - "version": "2.5.0", + "version": "2.8.0", "main": "./dist/cjs/index.js", "module": "./dist/esm/index.js", "types": "./dist/index.d.ts", diff --git a/typescript/packages/http/hono/CHANGELOG.md b/typescript/packages/http/hono/CHANGELOG.md index d53f8aa1c2..9a99c70de9 100644 --- a/typescript/packages/http/hono/CHANGELOG.md +++ b/typescript/packages/http/hono/CHANGELOG.md @@ -1,5 +1,46 @@ # @x402/hono Changelog +## 2.8.0 + +### Minor Changes + +- 4c1e44f: Treat malformed facilitator success payloads as upstream facilitator errors and return 502 responses from framework middleware instead of flattening them into payment failures. +- Updated dependencies [4f2f4f3] +- Updated dependencies [067f297] +- Updated dependencies [067f297] +- Updated dependencies [4c1e44f] +- Updated dependencies [5135fab] + - @x402/extensions@2.8.0 + - @x402/core@2.8.0 + - @x402/paywall@2.8.0 + +## 2.7.0 + +### Minor Changes + +- Updated dependencies [34d2442] +- Updated dependencies [8b731cb] +- Updated dependencies [f2bbb5c] +- Updated dependencies [8931cb3] +- Updated dependencies [34d2442] + - @x402/extensions@2.7.0 + - @x402/core@2.7.0 + - @x402/paywall@2.7.0 + +## 2.6.0 + +### Minor Changes + +- aeef1bf: Added dynamic function for servers to generate custom response for settlement failures defaulting to empty +- 2564781: Include PAYMENT-RESPONSE header on settlement failure responses +- Updated dependencies [f41baed] +- Updated dependencies [aeef1bf] +- Updated dependencies [2564781] +- Updated dependencies [b341973] +- Updated dependencies [29fe09a] + - @x402/core@2.6.0 + - @x402/paywall@2.6.0 + ## 2.5.0 ### Minor Changes diff --git a/typescript/packages/http/hono/package.json b/typescript/packages/http/hono/package.json index 3ad9dd4595..d3281029cc 100644 --- a/typescript/packages/http/hono/package.json +++ b/typescript/packages/http/hono/package.json @@ -1,6 +1,6 @@ { "name": "@x402/hono", - "version": "2.5.0", + "version": "2.8.0", "main": "./dist/cjs/index.js", "module": "./dist/esm/index.js", "types": "./dist/index.d.ts", @@ -45,7 +45,7 @@ }, "peerDependencies": { "hono": "^4.0.0", - "@x402/paywall": "workspace:*" + "@x402/paywall": "workspace:^" }, "peerDependenciesMeta": { "@x402/paywall": { diff --git a/typescript/packages/http/hono/src/index.test.ts b/typescript/packages/http/hono/src/index.test.ts index 8e34868fab..0f6a2926d1 100644 --- a/typescript/packages/http/hono/src/index.test.ts +++ b/typescript/packages/http/hono/src/index.test.ts @@ -7,6 +7,7 @@ import type { FacilitatorClient, } from "@x402/core/server"; import { + FacilitatorResponseError, x402ResourceServer, x402HTTPResourceServer as HTTPResourceServer, } from "@x402/core/server"; @@ -40,6 +41,28 @@ let mockRegisterPaywallProvider: ReturnType; let mockRequiresPayment: ReturnType; vi.mock("@x402/core/server", () => ({ + SETTLEMENT_OVERRIDES_HEADER: "Settlement-Overrides", + FacilitatorResponseError: class FacilitatorResponseError extends Error { + /** + * Creates a mock facilitator response error. + * + * @param message - Error message. + */ + constructor(message: string) { + super(message); + this.name = "FacilitatorResponseError"; + } + }, + getFacilitatorResponseError: (error: unknown) => { + let current = error; + while (current instanceof Error) { + if (current.name === "FacilitatorResponseError") { + return current; + } + current = (current as Error & { cause?: unknown }).cause; + } + return null; + }, x402ResourceServer: vi.fn().mockImplementation(() => ({ initialize: vi.fn().mockResolvedValue(undefined), registerExtension: vi.fn(), @@ -71,7 +94,12 @@ function setupMockHttpServer( processResult: HTTPProcessResult, settlementResult: | { success: true; headers: Record } - | { success: false; errorReason: string; headers: Record } = { + | { + success: false; + errorReason: string; + headers: Record; + response: { status: number; headers: Record; body?: unknown }; + } = { success: true, headers: {}, }, @@ -388,13 +416,47 @@ describe("paymentMiddleware", () => { await middleware(context, next); - expect(context.json).toHaveBeenCalledWith( - { - error: "Settlement failed", - details: "Settlement rejected", - }, - 402, + expect(context.json).toHaveBeenCalledWith({}, 402); + }); + + it("retries initialization after a facilitator init failure", async () => { + const initialize = vi + .fn() + .mockRejectedValueOnce( + new Error("Failed to initialize: no supported payment kinds loaded from any facilitator.", { + cause: new FacilitatorResponseError( + "Facilitator supported returned invalid JSON: not-json", + ), + }), + ) + .mockResolvedValueOnce(undefined); + + vi.mocked(HTTPResourceServer).mockImplementation( + (server, routes) => + ({ + initialize, + processHTTPRequest: mockProcessHTTPRequest, + processSettlement: mockProcessSettlement, + registerPaywallProvider: mockRegisterPaywallProvider, + requiresPayment: mockRequiresPayment, + routes, + server: server || { + hasExtension: vi.fn().mockReturnValue(false), + registerExtension: vi.fn(), + }, + }) as unknown as x402HTTPResourceServer, ); + mockProcessHTTPRequest.mockResolvedValue({ type: "no-payment-required" }); + + const middleware = paymentMiddleware(mockRoutes, {} as unknown as x402ResourceServer); + const next = vi.fn().mockResolvedValue(undefined); + + await middleware(createMockContext(), next); + await middleware(createMockContext(), next); + + expect(initialize).toHaveBeenCalledTimes(2); + expect(mockProcessHTTPRequest).toHaveBeenCalledTimes(1); + expect(next).toHaveBeenCalledTimes(1); }); it("returns 402 when settlement returns success: false", async () => { @@ -408,6 +470,14 @@ describe("paymentMiddleware", () => { success: false, errorReason: "Insufficient funds", headers: { "PAYMENT-RESPONSE": "settlement-failed-encoded" }, + response: { + status: 402, + headers: { + "Content-Type": "application/json", + "PAYMENT-RESPONSE": "settlement-failed-encoded", + }, + body: {}, + }, }, ); @@ -433,14 +503,10 @@ describe("paymentMiddleware", () => { await middleware(context, next); - expect(context.json).toHaveBeenCalledWith( - { - error: "Settlement failed", - details: "Insufficient funds", - }, - 402, - ); + expect(context.res?.status).toBe(402); expect(context.res?.headers.get("PAYMENT-RESPONSE")).toBe("settlement-failed-encoded"); + const body = await context.res?.json(); + expect(body).toEqual({}); }); it("passes paywallConfig to processHTTPRequest", async () => { diff --git a/typescript/packages/http/hono/src/index.ts b/typescript/packages/http/hono/src/index.ts index a62ad08f5d..049dd08db0 100644 --- a/typescript/packages/http/hono/src/index.ts +++ b/typescript/packages/http/hono/src/index.ts @@ -6,11 +6,26 @@ import { x402ResourceServer, RoutesConfig, FacilitatorClient, + FacilitatorResponseError, + getFacilitatorResponseError, + SETTLEMENT_OVERRIDES_HEADER, + SettlementOverrides, } from "@x402/core/server"; import { SchemeNetworkServer, Network } from "@x402/core/types"; import { Context, MiddlewareHandler } from "hono"; import { HonoAdapter } from "./adapter"; +/** + * Set settlement overrides on the response for partial settlement. + * The middleware will extract these before settlement and strip the header from the client response. + * + * @param c - Hono context + * @param overrides - Settlement overrides (e.g., { amount: "500" } for partial settlement) + */ +export function setSettlementOverrides(c: Context, overrides: SettlementOverrides): void { + c.header(SETTLEMENT_OVERRIDES_HEADER, JSON.stringify(overrides)); +} + /** * Check if any routes in the configuration declare bazaar extensions * @@ -44,6 +59,17 @@ export interface SchemeRegistration { server: SchemeNetworkServer; } +/** + * Builds a normalized 502 response for facilitator boundary failures. + * + * @param c - The current Hono context + * @param error - The facilitator response error to surface + * @returns A JSON 502 response + */ +function facilitatorErrorResponse(c: Context, error: FacilitatorResponseError): Response { + return c.json({ error: error.message }, 502); +} + /** * Hono payment middleware for x402 protocol (direct HTTP server instance). * @@ -82,6 +108,28 @@ export function paymentMiddlewareFromHTTPServer( // Store initialization promise (not the result) // httpServer.initialize() fetches facilitator support and validates routes let initPromise: Promise | null = syncFacilitatorOnStart ? httpServer.initialize() : null; + let isInitialized = false; + + /** + * Ensures facilitator initialization succeeds once, while allowing retries after failures. + */ + async function initializeHttpServer(): Promise { + if (!syncFacilitatorOnStart || isInitialized) { + return; + } + + if (!initPromise) { + initPromise = httpServer.initialize(); + } + + try { + await initPromise; + isInitialized = true; + } catch (error) { + initPromise = null; + throw error; + } + } // Dynamically register bazaar extension if routes declare it and not already registered // Skip if pre-registered (e.g., in serverless environments where static imports are used) @@ -112,9 +160,16 @@ export function paymentMiddlewareFromHTTPServer( } // Only initialize when processing a protected route - if (initPromise) { - await initPromise; - initPromise = null; // Clear after first await + if (syncFacilitatorOnStart && !isInitialized) { + try { + await initializeHttpServer(); + } catch (error) { + const facilitatorError = getFacilitatorResponseError(error); + if (facilitatorError) { + return facilitatorErrorResponse(c, facilitatorError); + } + throw error; + } } // Await bazaar extension loading if needed @@ -124,7 +179,15 @@ export function paymentMiddlewareFromHTTPServer( } // Process payment requirement check - const result = await httpServer.processHTTPRequest(context, paywallConfig); + let result: Awaited>; + try { + result = await httpServer.processHTTPRequest(context, paywallConfig); + } catch (error) { + if (error instanceof FacilitatorResponseError) { + return facilitatorErrorResponse(c, error); + } + throw error; + } // Handle the different result types switch (result.type) { @@ -162,6 +225,11 @@ export function paymentMiddlewareFromHTTPServer( // Get response body for extensions const responseBody = Buffer.from(await res.clone().arrayBuffer()); + const responseHeaders: Record = {}; + res.headers.forEach((value, key) => { + responseHeaders[key] = value; + }); + // Clear the response so we can modify headers c.res = undefined; @@ -170,20 +238,18 @@ export function paymentMiddlewareFromHTTPServer( paymentPayload, paymentRequirements, declaredExtensions, - { request: context, responseBody }, + { request: context, responseBody, responseHeaders }, ); if (!settleResult.success) { // Settlement failed - do not return the protected resource - res = c.json( - { - error: "Settlement failed", - details: settleResult.errorReason, - }, - 402, - ); - Object.entries(settleResult.headers).forEach(([key, value]) => { - res.headers.set(key, value); + const { response } = settleResult; + const body = response.isHtml + ? String(response.body ?? "") + : JSON.stringify(response.body ?? {}); + res = new Response(body, { + status: response.status, + headers: response.headers, }); } else { // Settlement succeeded - add headers to response @@ -192,15 +258,14 @@ export function paymentMiddlewareFromHTTPServer( }); } } catch (error) { + if (error instanceof FacilitatorResponseError) { + res = facilitatorErrorResponse(c, error); + c.res = res; + return; + } console.error(error); // If settlement fails, return an error response - res = c.json( - { - error: "Settlement failed", - details: error instanceof Error ? error.message : "Unknown error", - }, - 402, - ); + res = c.json({}, 402); } // Restore the response (potentially modified with settlement headers) @@ -309,9 +374,9 @@ export type { SchemeNetworkServer, } from "@x402/core/types"; -export type { PaywallProvider, PaywallConfig } from "@x402/core/server"; +export type { PaywallProvider, PaywallConfig, SettlementOverrides } from "@x402/core/server"; -export { RouteConfigurationError } from "@x402/core/server"; +export { RouteConfigurationError, SETTLEMENT_OVERRIDES_HEADER } from "@x402/core/server"; export type { RouteValidationError } from "@x402/core/server"; diff --git a/typescript/packages/http/next/CHANGELOG.md b/typescript/packages/http/next/CHANGELOG.md index 01c5856c68..161a472f01 100644 --- a/typescript/packages/http/next/CHANGELOG.md +++ b/typescript/packages/http/next/CHANGELOG.md @@ -1,5 +1,47 @@ # @x402/next Changelog +## 2.8.0 + +### Minor Changes + +- 4c1e44f: Treat malformed facilitator success payloads as upstream facilitator errors and return 502 responses from framework middleware instead of flattening them into payment failures. +- Updated dependencies [4f2f4f3] +- Updated dependencies [067f297] +- Updated dependencies [067f297] +- Updated dependencies [4c1e44f] +- Updated dependencies [5135fab] + - @x402/extensions@2.8.0 + - @x402/core@2.8.0 + - @x402/paywall@2.8.0 + +## 2.7.0 + +### Minor Changes + +- Updated dependencies [34d2442] +- Updated dependencies [8b731cb] +- Updated dependencies [f2bbb5c] +- Updated dependencies [8931cb3] +- Updated dependencies [34d2442] + - @x402/extensions@2.7.0 + - @x402/core@2.7.0 + - @x402/paywall@2.7.0 + +## 2.6.0 + +### Minor Changes + +- aeef1bf: Added dynamic function for servers to generate custom response for settlement failures defaulting to empty +- 205257b: Cleaned up dependencies +- 2564781: Include PAYMENT-RESPONSE header on settlement failure responses +- Updated dependencies [f41baed] +- Updated dependencies [aeef1bf] +- Updated dependencies [2564781] +- Updated dependencies [b341973] +- Updated dependencies [29fe09a] + - @x402/core@2.6.0 + - @x402/paywall@2.6.0 + ## 2.5.0 ### Minor Changes diff --git a/typescript/packages/http/next/package.json b/typescript/packages/http/next/package.json index 3c50e7f62f..56d8a2ea78 100644 --- a/typescript/packages/http/next/package.json +++ b/typescript/packages/http/next/package.json @@ -1,6 +1,6 @@ { "name": "@x402/next", - "version": "2.5.0", + "version": "2.8.0", "main": "./dist/cjs/index.js", "module": "./dist/esm/index.js", "types": "./dist/index.d.ts", @@ -38,14 +38,13 @@ "vitest": "^3.0.5" }, "dependencies": { - "@coinbase/cdp-sdk": "^1.22.0", "@x402/core": "workspace:~", "@x402/extensions": "workspace:~", "zod": "^3.24.2" }, "peerDependencies": { "next": "^16.0.10", - "@x402/paywall": "workspace:*" + "@x402/paywall": "workspace:^" }, "peerDependenciesMeta": { "@x402/paywall": { diff --git a/typescript/packages/http/next/src/index.test.ts b/typescript/packages/http/next/src/index.test.ts index 89aeea80c0..3d1ded9ed6 100644 --- a/typescript/packages/http/next/src/index.test.ts +++ b/typescript/packages/http/next/src/index.test.ts @@ -30,6 +30,27 @@ const mockFunctions = { // Mock @x402/core/server vi.mock("@x402/core/server", () => ({ + FacilitatorResponseError: class FacilitatorResponseError extends Error { + /** + * Creates a mock facilitator response error. + * + * @param message - Error message. + */ + constructor(message: string) { + super(message); + this.name = "FacilitatorResponseError"; + } + }, + getFacilitatorResponseError: (error: unknown) => { + let current = error; + while (current instanceof Error) { + if (current.name === "FacilitatorResponseError") { + return current; + } + current = (current as Error & { cause?: unknown }).cause; + } + return null; + }, x402ResourceServer: vi.fn().mockImplementation(() => ({ initialize: vi.fn().mockResolvedValue(undefined), registerExtension: vi.fn(), @@ -87,7 +108,12 @@ function createMockHttpServer( processResult: HTTPProcessResult, settlementResult: | { success: true; headers: Record } - | { success: false; errorReason: string; headers: Record } = { + | { + success: false; + errorReason: string; + headers: Record; + response: { status: number; headers: Record; body?: unknown }; + } = { success: true, headers: {}, }, @@ -274,7 +300,7 @@ describe("paymentProxy", () => { expect(response.status).toBe(402); const body = await response.json(); - expect(body.error).toBe("Settlement failed"); + expect(body).toEqual({}); }); it("returns 402 when settlement returns success: false, not the resource", async () => { @@ -288,6 +314,14 @@ describe("paymentProxy", () => { success: false, errorReason: "Insufficient funds", headers: { "PAYMENT-RESPONSE": "settlement-failed-encoded" }, + response: { + status: 402, + headers: { + "Content-Type": "application/json", + "PAYMENT-RESPONSE": "settlement-failed-encoded", + }, + body: {}, + }, }, ); setupMockCreateHttpServer(mockServer); @@ -297,8 +331,7 @@ describe("paymentProxy", () => { expect(response.status).toBe(402); const body = await response.json(); - expect(body.error).toBe("Settlement failed"); - expect(body.details).toBe("Insufficient funds"); + expect(body).toEqual({}); expect(response.headers.get("PAYMENT-RESPONSE")).toBe("settlement-failed-encoded"); }); }); @@ -397,7 +430,7 @@ describe("withX402", () => { expect(handler).toHaveBeenCalled(); expect(response.status).toBe(402); const body = await response.json(); - expect(body.error).toBe("Settlement failed"); + expect(body).toEqual({}); }); it("returns 402 when settlement returns success: false, not the handler response", async () => { @@ -411,6 +444,14 @@ describe("withX402", () => { success: false, errorReason: "Insufficient funds", headers: { "PAYMENT-RESPONSE": "settlement-failed-encoded" }, + response: { + status: 402, + headers: { + "Content-Type": "application/json", + "PAYMENT-RESPONSE": "settlement-failed-encoded", + }, + body: {}, + }, }, ); setupMockCreateHttpServer(mockServer); @@ -422,8 +463,7 @@ describe("withX402", () => { expect(handler).toHaveBeenCalled(); expect(response.status).toBe(402); const body = await response.json(); - expect(body.error).toBe("Settlement failed"); - expect(body.details).toBe("Insufficient funds"); + expect(body).toEqual({}); expect(response.headers.get("PAYMENT-RESPONSE")).toBe("settlement-failed-encoded"); }); }); diff --git a/typescript/packages/http/next/src/index.ts b/typescript/packages/http/next/src/index.ts index b29589e8d9..aab58e9d26 100644 --- a/typescript/packages/http/next/src/index.ts +++ b/typescript/packages/http/next/src/index.ts @@ -5,6 +5,7 @@ import { RoutesConfig, RouteConfig, FacilitatorClient, + FacilitatorResponseError, } from "@x402/core/server"; import { SchemeNetworkServer, Network } from "@x402/core/types"; import { NextRequest, NextResponse } from "next/server"; @@ -13,6 +14,8 @@ import { createRequestContext, handlePaymentError, handleSettlement, + createFacilitatorErrorResponse, + getFacilitatorResponseError, } from "./utils"; import { x402HTTPResourceServer } from "@x402/core/server"; @@ -85,7 +88,15 @@ export function paymentProxyFromHTTPServer( } // Only initialize when processing a protected route - await init(); + try { + await init(); + } catch (error) { + const facilitatorError = getFacilitatorResponseError(error); + if (facilitatorError) { + return createFacilitatorErrorResponse(facilitatorError); + } + throw error; + } // Await bazaar extension loading if needed if (bazaarPromise) { @@ -94,7 +105,15 @@ export function paymentProxyFromHTTPServer( } // Process payment requirement check - const result = await httpServer.processHTTPRequest(context, paywallConfig); + let result: Awaited>; + try { + result = await httpServer.processHTTPRequest(context, paywallConfig); + } catch (error) { + if (error instanceof FacilitatorResponseError) { + return createFacilitatorErrorResponse(error); + } + throw error; + } // Handle the different result types switch (result.type) { diff --git a/typescript/packages/http/next/src/utils.test.ts b/typescript/packages/http/next/src/utils.test.ts index aea7f2f2ea..2b0167b008 100644 --- a/typescript/packages/http/next/src/utils.test.ts +++ b/typescript/packages/http/next/src/utils.test.ts @@ -13,15 +13,38 @@ import { handleSettlement, } from "./utils"; +let mockInitialize: ReturnType; + // Mock @x402/core/server vi.mock("@x402/core/server", () => { const MockHTTPResourceServer = vi.fn().mockImplementation(() => ({ - initialize: vi.fn().mockResolvedValue(undefined), + initialize: mockInitialize, registerPaywallProvider: vi.fn(), processSettlement: vi.fn(), requiresPayment: vi.fn().mockReturnValue(true), })); return { + FacilitatorResponseError: class FacilitatorResponseError extends Error { + /** + * Creates a mock facilitator response error. + * + * @param message - Error message. + */ + constructor(message: string) { + super(message); + this.name = "FacilitatorResponseError"; + } + }, + getFacilitatorResponseError: (error: unknown) => { + let current = error; + while (current instanceof Error) { + if (current.name === "FacilitatorResponseError") { + return current; + } + current = (current as Error & { cause?: unknown }).cause; + } + return null; + }, x402HTTPResourceServer: MockHTTPResourceServer, x402ResourceServer: vi.fn(), }; @@ -62,6 +85,10 @@ function createMockResourceServer(): x402ResourceServer { } describe("createHttpServer", () => { + beforeEach(() => { + mockInitialize = vi.fn().mockResolvedValue(undefined); + }); + it("creates server and initializes on start by default", async () => { const routes = { "/api/*": { @@ -107,6 +134,25 @@ describe("createHttpServer", () => { await init(); expect(httpServer.registerPaywallProvider).toHaveBeenCalledWith(paywall); }); + + it("retries initialization after a facilitator init failure", async () => { + mockInitialize = vi + .fn() + .mockRejectedValueOnce(new Error("not-json")) + .mockResolvedValueOnce(undefined); + const routes = { + "/api/*": { + accepts: { scheme: "exact", payTo: "0x123", price: "$0.01", network: "eip155:84532" }, + }, + } as const; + const server = createMockResourceServer(); + + const { init } = createHttpServer(routes, server); + + await expect(init()).rejects.toThrow("not-json"); + await expect(init()).resolves.toBeUndefined(); + expect(mockInitialize).toHaveBeenCalledTimes(2); + }); }); describe("createRequestContext", () => { @@ -270,6 +316,14 @@ describe("handleSettlement", () => { transaction: "", network: "eip155:84532", headers: { "PAYMENT-RESPONSE": "settlement-failed-encoded" }, + response: { + status: 402, + headers: { + "Content-Type": "application/json", + "PAYMENT-RESPONSE": "settlement-failed-encoded", + }, + body: {}, + }, }); const response = new NextResponse("OK", { status: 200 }); @@ -281,9 +335,8 @@ describe("handleSettlement", () => { ); expect(result.status).toBe(402); - const body = (await result.json()) as { error: string; details: string }; - expect(body.error).toBe("Settlement failed"); - expect(body.details).toBe("Insufficient funds"); + const body = await result.json(); + expect(body).toEqual({}); expect(result.headers.get("PAYMENT-RESPONSE")).toBe("settlement-failed-encoded"); }); @@ -299,8 +352,7 @@ describe("handleSettlement", () => { ); expect(result.status).toBe(402); - const body = (await result.json()) as { error: string; details: string }; - expect(body.error).toBe("Settlement failed"); - expect(body.details).toBe("Settlement rejected"); + const body = await result.json(); + expect(body).toEqual({}); }); }); diff --git a/typescript/packages/http/next/src/utils.ts b/typescript/packages/http/next/src/utils.ts index 1b016b44ac..11b634a66c 100644 --- a/typescript/packages/http/next/src/utils.ts +++ b/typescript/packages/http/next/src/utils.ts @@ -6,6 +6,8 @@ import { x402HTTPResourceServer, x402ResourceServer, RoutesConfig, + FacilitatorResponseError, + getFacilitatorResponseError as getCoreFacilitatorResponseError, } from "@x402/core/server"; import { PaymentPayload, PaymentRequirements } from "@x402/core/types"; import { NextAdapter } from "./adapter"; @@ -18,6 +20,21 @@ export interface HttpServerInstance { init: () => Promise; } +export const getFacilitatorResponseError = getCoreFacilitatorResponseError; + +/** + * Builds a normalized 502 response for facilitator boundary failures. + * + * @param error - The facilitator response error to surface + * @returns A JSON 502 response + */ +export function createFacilitatorErrorResponse(error: FacilitatorResponseError): NextResponse { + return new NextResponse(JSON.stringify({ error: error.message }), { + status: 502, + headers: { "Content-Type": "application/json" }, + }); +} + /** * Prepares an existing x402HTTPResourceServer with initialization logic * @@ -39,14 +56,28 @@ export function prepareHttpServer( // Store initialization promise (not the result) // httpServer.initialize() fetches facilitator support and validates routes let initPromise: Promise | null = syncFacilitatorOnStart ? httpServer.initialize() : null; + let isInitialized = false; return { httpServer, + /** + * Ensures facilitator initialization succeeds once, while allowing retries after failures. + */ async init() { - // Ensure initialization completes before processing - if (initPromise) { + if (!syncFacilitatorOnStart || isInitialized) { + return; + } + + if (!initPromise) { + initPromise = httpServer.initialize(); + } + + try { await initPromise; - initPromise = null; // Clear after first await + isInitialized = true; + } catch (error) { + initPromise = null; + throw error; } }, }; @@ -150,16 +181,12 @@ export async function handleSettlement( if (!result.success) { // Settlement failed - do not return the protected resource - return new NextResponse( - JSON.stringify({ - error: "Settlement failed", - details: result.errorReason, - }), - { - status: 402, - headers: { "Content-Type": "application/json", ...result.headers }, - }, - ); + const { response } = result; + const body = response.isHtml ? response.body : JSON.stringify(response.body ?? {}); + return new NextResponse(body, { + status: response.status, + headers: response.headers, + }); } // Settlement succeeded - add headers and return original response @@ -169,17 +196,14 @@ export async function handleSettlement( return response; } catch (error) { + if (error instanceof FacilitatorResponseError) { + return createFacilitatorErrorResponse(error); + } console.error("Settlement failed:", error); // If settlement fails, return an error response - return new NextResponse( - JSON.stringify({ - error: "Settlement failed", - details: error instanceof Error ? error.message : "Unknown error", - }), - { - status: 402, - headers: { "Content-Type": "application/json" }, - }, - ); + return new NextResponse(JSON.stringify({}), { + status: 402, + headers: { "Content-Type": "application/json" }, + }); } } diff --git a/typescript/packages/http/paywall/CHANGELOG.md b/typescript/packages/http/paywall/CHANGELOG.md index b50f371dfb..ebf7028247 100644 --- a/typescript/packages/http/paywall/CHANGELOG.md +++ b/typescript/packages/http/paywall/CHANGELOG.md @@ -1,5 +1,34 @@ # @x402/paywall Changelog +## 2.8.0 + +### Minor Changes + +- Updated dependencies [067f297] +- Updated dependencies [4c1e44f] +- Updated dependencies [5135fab] + - @x402/core@2.8.0 + +## 2.7.0 + +### Minor Changes + +- 34d2442: Fixed encoding of characters outside of the Latin1 range +- Updated dependencies [8931cb3] + - @x402/core@2.7.0 + +## 2.6.0 + +### Minor Changes + +- 29fe09a: Make ResourceInfo.description, ResourceInfo.mimeType, and PaymentPayload.resource optional to match v2 spec +- Updated dependencies [f41baed] +- Updated dependencies [aeef1bf] +- Updated dependencies [2564781] +- Updated dependencies [b341973] +- Updated dependencies [29fe09a] + - @x402/core@2.6.0 + ## 2.5.0 ### Minor Changes diff --git a/typescript/packages/http/paywall/package.json b/typescript/packages/http/paywall/package.json index 179272c450..ec2dbaa470 100644 --- a/typescript/packages/http/paywall/package.json +++ b/typescript/packages/http/paywall/package.json @@ -1,6 +1,6 @@ { "name": "@x402/paywall", - "version": "2.5.0", + "version": "2.8.0", "main": "./dist/cjs/index.js", "module": "./dist/esm/index.js", "types": "./dist/index.d.ts", diff --git a/typescript/packages/http/paywall/src/evm/EvmPaywall.tsx b/typescript/packages/http/paywall/src/evm/EvmPaywall.tsx index f8663c5a15..b28c130093 100644 --- a/typescript/packages/http/paywall/src/evm/EvmPaywall.tsx +++ b/typescript/packages/http/paywall/src/evm/EvmPaywall.tsx @@ -5,6 +5,7 @@ import { useAccount, useSwitchChain, useWalletClient, useConnect, useDisconnect import { ExactEvmScheme } from "@x402/evm/exact/client"; import { x402Client } from "@x402/core/client"; +import { encodePaymentSignatureHeader } from "@x402/core/http"; import type { PaymentRequired } from "@x402/core/types"; import { getUSDCBalance } from "./utils"; @@ -154,7 +155,7 @@ export function EvmPaywall({ paymentRequired, onSuccessfulResponse }: EvmPaywall const paymentPayload = await client.createPaymentPayload(paymentRequired); // Encode as base64 JSON for v2 header - const paymentHeader = btoa(JSON.stringify(paymentPayload)); + const paymentHeader = encodePaymentSignatureHeader(paymentPayload); setStatus("Requesting content with payment..."); const response = await fetch(x402.currentUrl, { diff --git a/typescript/packages/http/paywall/src/evm/gen/template.ts b/typescript/packages/http/paywall/src/evm/gen/template.ts index 8d9a00e2ec..d31f495b40 100644 --- a/typescript/packages/http/paywall/src/evm/gen/template.ts +++ b/typescript/packages/http/paywall/src/evm/gen/template.ts @@ -3,4 +3,4 @@ * The pre-built EVM paywall template with inlined CSS and JS */ export const EVM_PAYWALL_TEMPLATE = - '\n \n \n Payment Required\n \n
\n \n \n '; + '\n \n \n Payment Required\n \n
\n \n \n '; diff --git a/typescript/packages/http/paywall/src/svm/SolanaPaywall.tsx b/typescript/packages/http/paywall/src/svm/SolanaPaywall.tsx index 4826ca06aa..74fa2f4409 100644 --- a/typescript/packages/http/paywall/src/svm/SolanaPaywall.tsx +++ b/typescript/packages/http/paywall/src/svm/SolanaPaywall.tsx @@ -4,6 +4,7 @@ import type { WalletWithSolanaFeatures } from "@solana/wallet-standard-features" import { ExactSvmScheme } from "@x402/svm/exact/client"; import { x402Client } from "@x402/core/client"; +import { encodePaymentSignatureHeader } from "@x402/core/http"; import type { PaymentRequired } from "@x402/core/types"; import { Spinner } from "./Spinner"; @@ -185,7 +186,7 @@ export function SolanaPaywall({ paymentRequired, onSuccessfulResponse }: SolanaP const paymentPayload = await client.createPaymentPayload(paymentRequired); - const paymentHeader = btoa(JSON.stringify(paymentPayload)); + const paymentHeader = encodePaymentSignatureHeader(paymentPayload); setStatus("Requesting content with payment..."); const response = await fetch(x402.currentUrl, { diff --git a/typescript/packages/http/paywall/src/svm/gen/template.ts b/typescript/packages/http/paywall/src/svm/gen/template.ts index 0ba985a098..954431627c 100644 --- a/typescript/packages/http/paywall/src/svm/gen/template.ts +++ b/typescript/packages/http/paywall/src/svm/gen/template.ts @@ -3,4 +3,4 @@ * The pre-built SVM paywall template with inlined CSS and JS */ export const SVM_PAYWALL_TEMPLATE = - '\n \n \n Payment Required\n \n
\n \n \n '; + '\n \n \n Payment Required\n \n
\n \n \n '; diff --git a/typescript/packages/legacy/x402-axios/package.json b/typescript/packages/legacy/x402-axios/package.json index a01b89e730..59d7c191e1 100644 --- a/typescript/packages/legacy/x402-axios/package.json +++ b/typescript/packages/legacy/x402-axios/package.json @@ -1,6 +1,6 @@ { "name": "x402-axios", - "version": "1.1.0", + "version": "1.2.0", "main": "./dist/cjs/index.js", "module": "./dist/esm/index.js", "types": "./dist/index.d.ts", diff --git a/typescript/packages/legacy/x402-express/package.json b/typescript/packages/legacy/x402-express/package.json index f4bec6691d..a27ee14af6 100644 --- a/typescript/packages/legacy/x402-express/package.json +++ b/typescript/packages/legacy/x402-express/package.json @@ -1,6 +1,6 @@ { "name": "x402-express", - "version": "1.1.0", + "version": "1.2.0", "main": "./dist/cjs/index.js", "module": "./dist/esm/index.js", "types": "./dist/index.d.ts", diff --git a/typescript/packages/legacy/x402-fetch/package.json b/typescript/packages/legacy/x402-fetch/package.json index c45173373e..9ee5ac84a9 100644 --- a/typescript/packages/legacy/x402-fetch/package.json +++ b/typescript/packages/legacy/x402-fetch/package.json @@ -1,6 +1,6 @@ { "name": "x402-fetch", - "version": "1.1.0", + "version": "1.2.0", "main": "./dist/cjs/index.js", "module": "./dist/esm/index.js", "types": "./dist/index.d.ts", diff --git a/typescript/packages/legacy/x402-hono/package.json b/typescript/packages/legacy/x402-hono/package.json index 760203999e..b69cd019b6 100644 --- a/typescript/packages/legacy/x402-hono/package.json +++ b/typescript/packages/legacy/x402-hono/package.json @@ -1,6 +1,6 @@ { "name": "x402-hono", - "version": "1.1.0", + "version": "1.2.0", "main": "./dist/cjs/index.js", "module": "./dist/esm/index.js", "types": "./dist/index.d.ts", diff --git a/typescript/packages/legacy/x402-next/package.json b/typescript/packages/legacy/x402-next/package.json index 7e502e52d1..c2ebebfdcd 100644 --- a/typescript/packages/legacy/x402-next/package.json +++ b/typescript/packages/legacy/x402-next/package.json @@ -1,6 +1,6 @@ { "name": "x402-next", - "version": "1.1.0", + "version": "1.2.0", "main": "./dist/cjs/index.js", "module": "./dist/esm/index.js", "types": "./dist/index.d.ts", diff --git a/typescript/packages/legacy/x402/package.json b/typescript/packages/legacy/x402/package.json index cd5a86d4f3..a180d01184 100644 --- a/typescript/packages/legacy/x402/package.json +++ b/typescript/packages/legacy/x402/package.json @@ -1,6 +1,6 @@ { "name": "x402", - "version": "1.1.0", + "version": "1.2.0", "main": "./dist/cjs/index.js", "module": "./dist/esm/index.js", "types": "./dist/cjs/index.d.ts", diff --git a/typescript/packages/legacy/x402/src/schemes/exact/svm/facilitator/index.ts b/typescript/packages/legacy/x402/src/schemes/exact/svm/facilitator/index.ts index 9fe06b13a7..41d7607ba7 100644 --- a/typescript/packages/legacy/x402/src/schemes/exact/svm/facilitator/index.ts +++ b/typescript/packages/legacy/x402/src/schemes/exact/svm/facilitator/index.ts @@ -1,2 +1,3 @@ export * from "./settle"; +export * from "./settlement-cache"; export * from "./verify"; diff --git a/typescript/packages/legacy/x402/src/schemes/exact/svm/facilitator/settle.test.ts b/typescript/packages/legacy/x402/src/schemes/exact/svm/facilitator/settle.test.ts index 27dd989d2f..ddc1b4bff1 100644 --- a/typescript/packages/legacy/x402/src/schemes/exact/svm/facilitator/settle.test.ts +++ b/typescript/packages/legacy/x402/src/schemes/exact/svm/facilitator/settle.test.ts @@ -12,6 +12,7 @@ import { import { getRpcClient, getRpcSubscriptions } from "../../../../shared/svm/rpc"; import { verify } from "./verify"; import * as settleModule from "./settle"; +import { SettlementCache } from "./settlement-cache"; // Mocking dependencies vi.mock("../../../../shared/svm"); @@ -105,6 +106,19 @@ describe("SVM Settle", () => { }; }); + let txCounter = 0; + /** + * Returns a unique payment payload for each call (increments tx counter). + * + * @returns A PaymentPayload with a unique transaction identifier + */ + function uniquePayload(): PaymentPayload { + return { + ...paymentPayload, + payload: { transaction: `tx_${txCounter++}` } as ExactSvmPayload, + }; + } + afterEach(() => { vi.clearAllMocks(); }); @@ -112,6 +126,7 @@ describe("SVM Settle", () => { describe("settle", () => { it("should successfully settle a payment when verification passes", async () => { // Arrange + const payload = uniquePayload(); const mockVerifyResponse = { isValid: true, invalidReason: undefined, @@ -142,11 +157,11 @@ describe("SVM Settle", () => { ); // Act - const result = await settleModule.settle(signer, paymentPayload, paymentRequirements); + const result = await settleModule.settle(signer, payload, paymentRequirements); // Assert - expect(verify).toHaveBeenCalledWith(signer, paymentPayload, paymentRequirements, undefined); - expect(decodeTransactionFromPayload).toHaveBeenCalledWith(paymentPayload.payload); + expect(verify).toHaveBeenCalledWith(signer, payload, paymentRequirements, undefined); + expect(decodeTransactionFromPayload).toHaveBeenCalledWith(payload.payload); expect(signTransactionWithSigner).toHaveBeenCalledWith(signer, mockSignedTransaction); expect(transactionConfirmation.waitForRecentTransactionConfirmation).toHaveBeenCalledOnce(); expect(mockRpcClient.sendTransaction).toHaveBeenCalled(); @@ -182,6 +197,7 @@ describe("SVM Settle", () => { it("should return unexpected errors during settlement", async () => { // Arrange + const payload = uniquePayload(); const mockVerifyResponse = { isValid: true, invalidReason: undefined, @@ -198,7 +214,7 @@ describe("SVM Settle", () => { }); // Act - const result = await settleModule.settle(signer, paymentPayload, paymentRequirements); + const result = await settleModule.settle(signer, payload, paymentRequirements); // Assert expect(result).toEqual({ @@ -425,6 +441,7 @@ describe("SVM Settle", () => { describe("Custom RPC Configuration", () => { it("should use custom RPC URL from config for both client and subscriptions", async () => { // Arrange + const payload = uniquePayload(); const customRpcUrl = "http://localhost:8899"; const config = { svmConfig: { rpcUrl: customRpcUrl } }; const mockVerifyResponse = { @@ -450,12 +467,14 @@ describe("SVM Settle", () => { instructions: [], version: 0, } as any); + vi.mocked(getTokenPayerFromTransaction).mockReturnValue(payerAddress); + vi.mocked(signTransactionWithSigner).mockResolvedValue(mockSignedTransaction); vi.mocked(transactionConfirmation.waitForRecentTransactionConfirmation).mockResolvedValue( undefined, ); // Act - await settleModule.settle(signer, paymentPayload, paymentRequirements, config); + await settleModule.settle(signer, payload, paymentRequirements, config); // Assert expect(getRpcClient).toHaveBeenCalledWith("solana-devnet", customRpcUrl); @@ -464,6 +483,7 @@ describe("SVM Settle", () => { it("should propagate config to verify() call", async () => { // Arrange + const payload = uniquePayload(); const customRpcUrl = "https://api.mainnet-beta.solana.com"; const config = { svmConfig: { rpcUrl: customRpcUrl } }; const mockVerifyResponse = { @@ -489,19 +509,22 @@ describe("SVM Settle", () => { instructions: [], version: 0, } as any); + vi.mocked(getTokenPayerFromTransaction).mockReturnValue(payerAddress); + vi.mocked(signTransactionWithSigner).mockResolvedValue(mockSignedTransaction); vi.mocked(transactionConfirmation.waitForRecentTransactionConfirmation).mockResolvedValue( undefined, ); // Act - await settleModule.settle(signer, paymentPayload, paymentRequirements, config); + await settleModule.settle(signer, payload, paymentRequirements, config); // Assert - expect(verify).toHaveBeenCalledWith(signer, paymentPayload, paymentRequirements, config); + expect(verify).toHaveBeenCalledWith(signer, payload, paymentRequirements, config); }); it("should work without config (backward compatibility)", async () => { // Arrange + const payload = uniquePayload(); const mockVerifyResponse = { isValid: true, invalidReason: undefined, @@ -525,15 +548,17 @@ describe("SVM Settle", () => { instructions: [], version: 0, } as any); + vi.mocked(getTokenPayerFromTransaction).mockReturnValue(payerAddress); + vi.mocked(signTransactionWithSigner).mockResolvedValue(mockSignedTransaction); vi.mocked(transactionConfirmation.waitForRecentTransactionConfirmation).mockResolvedValue( undefined, ); // Act - await settleModule.settle(signer, paymentPayload, paymentRequirements); + await settleModule.settle(signer, payload, paymentRequirements); // Assert - expect(verify).toHaveBeenCalledWith(signer, paymentPayload, paymentRequirements, undefined); + expect(verify).toHaveBeenCalledWith(signer, payload, paymentRequirements, undefined); expect(getRpcClient).toHaveBeenCalledWith("solana-devnet", undefined); expect(getRpcSubscriptions).toHaveBeenCalledWith("solana-devnet", undefined); }); @@ -666,4 +691,160 @@ describe("SVM Settle", () => { }); }); }); + + describe("duplicate settlement cache", () => { + /** + * Builds a payment payload for the given transaction signature. + * + * @param transaction - The transaction signature to embed in the payload + * @returns A PaymentPayload for Solana devnet with the given transaction + */ + function makePayload(transaction: string): PaymentPayload { + return { + scheme: "exact", + network: "solana-devnet", + x402Version: 1, + payload: { transaction } as ExactSvmPayload, + }; + } + + /** + * Configures mocks so that settle() succeeds (verify passes, RPC and decode mocks in place). + * + * @returns void + */ + function setupMocksForSettle() { + const mockVerifyResponse = { isValid: true, invalidReason: undefined }; + vi.mocked(verify).mockResolvedValue(mockVerifyResponse); + vi.mocked(decodeTransactionFromPayload).mockReturnValue(mockSignedTransaction); + vi.mocked(getRpcClient).mockReturnValue(mockRpcClient); + vi.mocked(getRpcSubscriptions).mockReturnValue(mockRpcSubscriptions); + vi.mocked(mockRpcClient.sendTransaction).mockReturnValue({ + send: vi.fn().mockResolvedValue("mock_signature_123"), + }); + vi.mocked(solanaKit.getCompiledTransactionMessageDecoder).mockReturnValue({ + decode: vi.fn().mockReturnValue({}), + read: vi.fn(), + } as any); + vi.mocked(solanaKit.decompileTransactionMessageFetchingLookupTables).mockResolvedValue({ + lifetimeConstraint: { + blockhash: "mock_blockhash" as any, + lastValidBlockHeight: BigInt(1234), + }, + instructions: [], + version: 0, + } as any); + vi.mocked(getTokenPayerFromTransaction).mockReturnValue(payerAddress); + vi.mocked(signTransactionWithSigner).mockResolvedValue(mockSignedTransaction); + vi.mocked(transactionConfirmation.waitForRecentTransactionConfirmation).mockResolvedValue( + undefined, + ); + } + + it("should reject duplicate settlement of the same transaction", async () => { + setupMocksForSettle(); + + const payload = makePayload("sameTransactionBase64=="); + + const result1 = await settleModule.settle(signer, payload, paymentRequirements); + expect(result1.success).toBe(true); + + const result2 = await settleModule.settle(signer, payload, paymentRequirements); + expect(result2.success).toBe(false); + expect(result2.errorReason).toBe("duplicate_settlement"); + }); + + it("should allow settlement of distinct transactions", async () => { + setupMocksForSettle(); + + const result1 = await settleModule.settle( + signer, + makePayload("transactionA=="), + paymentRequirements, + ); + expect(result1.success).toBe(true); + + const result2 = await settleModule.settle( + signer, + makePayload("transactionB=="), + paymentRequirements, + ); + expect(result2.success).toBe(true); + }); + + it("should evict cache entries after TTL", async () => { + vi.useFakeTimers(); + try { + setupMocksForSettle(); + + const payload = makePayload("expiringTransaction=="); + + const result1 = await settleModule.settle(signer, payload, paymentRequirements); + expect(result1.success).toBe(true); + + // Advance past the 120s TTL + vi.advanceTimersByTime(121_000); + + const result2 = await settleModule.settle(signer, payload, paymentRequirements); + expect(result2.success).toBe(true); + } finally { + vi.useRealTimers(); + } + }); + }); +}); + +describe("SettlementCache prune optimization", () => { + it("should prune only expired entries and preserve non-expired ones", () => { + vi.useFakeTimers(); + try { + const cache = new SettlementCache(); + + cache.isDuplicate("tx-a"); + vi.advanceTimersByTime(10_000); + cache.isDuplicate("tx-b"); + vi.advanceTimersByTime(10_000); + cache.isDuplicate("tx-c"); + + // Advance so tx-a is expired (> 120s old) but tx-b and tx-c are not + vi.advanceTimersByTime(101_000); // total: tx-a=121s, tx-b=111s, tx-c=101s + + expect(cache.isDuplicate("tx-a")).toBe(false); // expired, re-inserted as new + expect(cache.isDuplicate("tx-b")).toBe(true); // still cached + expect(cache.isDuplicate("tx-c")).toBe(true); // still cached + } finally { + vi.useRealTimers(); + } + }); + + it("should prune all entries when all are expired", () => { + vi.useFakeTimers(); + try { + const cache = new SettlementCache(); + + cache.isDuplicate("tx-1"); + cache.isDuplicate("tx-2"); + cache.isDuplicate("tx-3"); + + vi.advanceTimersByTime(121_000); + + expect(cache.isDuplicate("tx-1")).toBe(false); + expect(cache.isDuplicate("tx-2")).toBe(false); + expect(cache.isDuplicate("tx-3")).toBe(false); + } finally { + vi.useRealTimers(); + } + }); + + it("should not prune any entries when none are expired", () => { + const cache = new SettlementCache(); + + cache.isDuplicate("tx-x"); + cache.isDuplicate("tx-y"); + cache.isDuplicate("tx-z"); + + expect(cache.isDuplicate("tx-x")).toBe(true); + expect(cache.isDuplicate("tx-y")).toBe(true); + expect(cache.isDuplicate("tx-z")).toBe(true); + }); }); diff --git a/typescript/packages/legacy/x402/src/schemes/exact/svm/facilitator/settle.ts b/typescript/packages/legacy/x402/src/schemes/exact/svm/facilitator/settle.ts index a733effb76..8391816b61 100644 --- a/typescript/packages/legacy/x402/src/schemes/exact/svm/facilitator/settle.ts +++ b/typescript/packages/legacy/x402/src/schemes/exact/svm/facilitator/settle.ts @@ -35,6 +35,7 @@ import { createRecentSignatureConfirmationPromiseFactory, } from "@solana/transaction-confirmation"; import { verify } from "./verify"; +import { SettlementCache } from "./settlement-cache"; /** * Settle the payment payload against the payment requirements. @@ -46,6 +47,17 @@ import { verify } from "./verify"; * @param config - Optional configuration for X402 operations (e.g., custom RPC URLs) * @returns A SettleResponse indicating if the payment is settled and any error reason */ +const settlementCache = new SettlementCache(); + +/** + * Settles an exact SVM payment by verifying the payment and recording the settlement. + * + * @param signer - Transaction signer used to verify the payment + * @param payload - The payment payload to settle + * @param paymentRequirements - The payment requirements to settle against + * @param config - Optional configuration for X402 operations (e.g., custom RPC URLs) + * @returns A promise that resolves to a SettleResponse indicating success or failure + */ export async function settle( signer: TransactionSigner, payload: PaymentPayload, @@ -63,6 +75,18 @@ export async function settle( } const svmPayload = payload.payload as ExactSvmPayload; + + // Duplicate settlement check: reject if this transaction is already being settled. + // Must occur before any async work so concurrent calls for the same tx are caught. + const txKey = svmPayload.transaction; + if (settlementCache.isDuplicate(txKey)) { + return { + success: false, + errorReason: "duplicate_settlement", + network: payload.network, + transaction: "", + }; + } const decodedTransaction = decodeTransactionFromPayload(svmPayload); const signedTransaction = await signTransactionWithSigner(signer, decodedTransaction); assertTransactionFullySigned(signedTransaction); diff --git a/typescript/packages/legacy/x402/src/schemes/exact/svm/facilitator/settlement-cache.ts b/typescript/packages/legacy/x402/src/schemes/exact/svm/facilitator/settlement-cache.ts new file mode 100644 index 0000000000..696fdce4d1 --- /dev/null +++ b/typescript/packages/legacy/x402/src/schemes/exact/svm/facilitator/settlement-cache.ts @@ -0,0 +1,48 @@ +/** + * How long a transaction is held in the duplicate settlement cache (ms). + * Covers the Solana blockhash lifetime (~60-90s) with margin. + */ +export const SETTLEMENT_TTL_MS = 120_000; + +/** + * In-memory cache for deduplicating concurrent settlement requests. + * + * Because Node.js is single-threaded, no lock is required — the cache + * check + insert must simply occur before the first `await` in the settle path. + */ +export class SettlementCache { + private readonly entries = new Map(); + + /** + * Returns `true` if `key` is already pending settlement (duplicate), + * or `false` after recording it as newly pending. + * + * Callers should reject the settlement when this returns `true`. + * + * @param key - The unique identifier for the settlement (typically the base64 transaction). + * @returns `true` if the key was already present (duplicate); `false` otherwise. + */ + isDuplicate(key: string): boolean { + this.prune(); + if (this.entries.has(key)) { + return true; + } + this.entries.set(key, Date.now()); + return false; + } + + /** + * Remove entries older than the settlement TTL. + * Leverages Map insertion-order guarantee to break early. + */ + private prune(): void { + const cutoff = Date.now() - SETTLEMENT_TTL_MS; + for (const [key, timestamp] of this.entries) { + if (timestamp < cutoff) { + this.entries.delete(key); + } else { + break; + } + } + } +} diff --git a/typescript/packages/legacy/x402/src/types/verify/x402Specs.ts b/typescript/packages/legacy/x402/src/types/verify/x402Specs.ts index 3295f7beed..53ab4d7a96 100644 --- a/typescript/packages/legacy/x402/src/types/verify/x402Specs.ts +++ b/typescript/packages/legacy/x402/src/types/verify/x402Specs.ts @@ -54,6 +54,7 @@ export const ErrorReasons = [ "unsupported_scheme", "unexpected_settle_error", "unexpected_verify_error", + "duplicate_settlement", ] as const; // Refiners diff --git a/typescript/packages/mcp/CHANGELOG.md b/typescript/packages/mcp/CHANGELOG.md index 34c111e5ee..a88d1f484c 100644 --- a/typescript/packages/mcp/CHANGELOG.md +++ b/typescript/packages/mcp/CHANGELOG.md @@ -1,5 +1,28 @@ # @x402/mcp Changelog +## 2.8.0 + +### Minor Changes + +- Updated dependencies [067f297] +- Updated dependencies [4c1e44f] +- Updated dependencies [5135fab] + - @x402/core@2.8.0 + +## 2.7.0 + +### Minor Changes + +- Updated dependencies [8931cb3] + - @x402/core@2.7.0 + +## 2.6.0 + +### Minor Changes + +- Updated dependencies + - @x402/core@2.6.0 + ## 2.5.0 ### Minor Changes diff --git a/typescript/packages/mcp/package.json b/typescript/packages/mcp/package.json index 2d655759f0..b7838335bf 100644 --- a/typescript/packages/mcp/package.json +++ b/typescript/packages/mcp/package.json @@ -1,6 +1,6 @@ { "name": "@x402/mcp", - "version": "2.5.0", + "version": "2.8.0", "main": "./dist/cjs/index.js", "module": "./dist/esm/index.js", "types": "./dist/cjs/index.d.ts", diff --git a/typescript/packages/mcp/src/server/paymentWrapper.ts b/typescript/packages/mcp/src/server/paymentWrapper.ts index c67352339e..ffd72d6fff 100644 --- a/typescript/packages/mcp/src/server/paymentWrapper.ts +++ b/typescript/packages/mcp/src/server/paymentWrapper.ts @@ -267,13 +267,10 @@ export function createPaymentWrapper( await config.hooks.onAfterSettlement(settlementContext); } - // Return result with payment response in _meta + // Return full result (preserving structuredContent, etc.) with payment response in _meta return { - content: result.content, - isError: result.isError, - _meta: { - [MCP_PAYMENT_RESPONSE_META_KEY]: settleResult, - }, + ...result, + _meta: { [MCP_PAYMENT_RESPONSE_META_KEY]: settleResult }, }; } catch (settleError) { // Settlement failed after execution - return 402 error diff --git a/typescript/packages/mcp/test/unit/server.test.ts b/typescript/packages/mcp/test/unit/server.test.ts index 1370ea7b18..ae36faeec1 100644 --- a/typescript/packages/mcp/test/unit/server.test.ts +++ b/typescript/packages/mcp/test/unit/server.test.ts @@ -170,6 +170,31 @@ describe("createPaymentWrapper", () => { ); }); + it("should preserve structuredContent from handler result", async () => { + const paid = createPaymentWrapper( + mockResourceServer as unknown as Parameters[0], + { + accepts: [mockPaymentRequirements], + }, + ); + + const structuredData = { query: "test", results: [{ id: 1 }], count: 1 }; + const handler = vi.fn().mockResolvedValue({ + content: [{ type: "text", text: JSON.stringify(structuredData) }], + structuredContent: structuredData, + }); + + const wrappedHandler = paid(handler); + const result = await wrappedHandler( + { test: "arg" }, + { _meta: { "x402/payment": mockPaymentPayload } }, + ); + + expect(result.structuredContent).toEqual(structuredData); + expect(result.content).toEqual([{ type: "text", text: JSON.stringify(structuredData) }]); + expect(result._meta?.[MCP_PAYMENT_RESPONSE_META_KEY]).toEqual(mockSettleResponse); + }); + it("should not settle payment if tool returns error", async () => { const paid = createPaymentWrapper( mockResourceServer as unknown as Parameters[0], diff --git a/typescript/packages/mechanisms/aptos/.prettierignore b/typescript/packages/mechanisms/aptos/.prettierignore new file mode 100644 index 0000000000..0510cefc24 --- /dev/null +++ b/typescript/packages/mechanisms/aptos/.prettierignore @@ -0,0 +1,3 @@ +# build output +dist/ +node_modules/ diff --git a/typescript/packages/mechanisms/aptos/CHANGELOG.md b/typescript/packages/mechanisms/aptos/CHANGELOG.md index 16dd440bf8..49b766e9a2 100644 --- a/typescript/packages/mechanisms/aptos/CHANGELOG.md +++ b/typescript/packages/mechanisms/aptos/CHANGELOG.md @@ -1,5 +1,32 @@ # @x402/aptos +## 2.8.0 + +### Minor Changes + +- Updated dependencies [067f297] +- Updated dependencies [4c1e44f] +- Updated dependencies [5135fab] + - @x402/core@2.8.0 + +## 2.7.0 + +### Minor Changes + +- Updated dependencies [8931cb3] + - @x402/core@2.7.0 + +## 2.6.0 + +### Minor Changes + +- Updated dependencies [f41baed] +- Updated dependencies [aeef1bf] +- Updated dependencies [2564781] +- Updated dependencies [b341973] +- Updated dependencies [29fe09a] + - @x402/core@2.6.0 + ## 2.5.0 ### Patch Changes diff --git a/typescript/packages/mechanisms/aptos/package.json b/typescript/packages/mechanisms/aptos/package.json index 6efa05e8bb..7147b4d1ec 100644 --- a/typescript/packages/mechanisms/aptos/package.json +++ b/typescript/packages/mechanisms/aptos/package.json @@ -1,6 +1,6 @@ { "name": "@x402/aptos", - "version": "2.5.0", + "version": "2.8.0", "main": "./dist/cjs/index.js", "module": "./dist/esm/index.js", "types": "./dist/cjs/index.d.ts", diff --git a/typescript/packages/mechanisms/evm/CHANGELOG.md b/typescript/packages/mechanisms/evm/CHANGELOG.md index e44972f2c6..19e9a93d82 100644 --- a/typescript/packages/mechanisms/evm/CHANGELOG.md +++ b/typescript/packages/mechanisms/evm/CHANGELOG.md @@ -1,5 +1,39 @@ # @x402/evm Changelog +## 2.8.0 + +### Minor Changes + +- Updated dependencies [067f297] +- Updated dependencies [4c1e44f] +- Updated dependencies [5135fab] + - @x402/core@2.8.0 + +## 2.7.0 + +### Minor Changes + +- 8b731cb: Replaced `sendRawApprovalAndSettle` with a generic `sendTransactions` signer method that accepts an array of pre-signed serialized transactions or unsigned call intents. The signer owns execution strategy (sequential, batched, or atomic bundling). Closed fail-open verification paths, aligned Permit2 amount check to exact match, and added `signerForNetwork` to the extensions package. + +### Patch Changes + +- d8e9f3f: Added simulation to permit2 verify and (optional) settle +- 1a6e08b: Simulate transaction in verify and (optional) settle; Added multicall utility for efficient rpc calls; Fixed undeployed smart wallet handling to prevent facilitator grieving and account for implementation dependent verifyTypedData +- Updated dependencies [8931cb3] + - @x402/core@2.7.0 + +## 2.6.0 + +### Minor Changes + +- f431337: Added assetTransferMethod and supportsEip2612 flag to defaultAssets +- Updated dependencies [f41baed] +- Updated dependencies [aeef1bf] +- Updated dependencies [2564781] +- Updated dependencies [b341973] +- Updated dependencies [29fe09a] + - @x402/core@2.6.0 + ## 2.5.0 ### Minor Changes diff --git a/typescript/packages/mechanisms/evm/README.md b/typescript/packages/mechanisms/evm/README.md index 6c9fff9141..7fb2af2849 100644 --- a/typescript/packages/mechanisms/evm/README.md +++ b/typescript/packages/mechanisms/evm/README.md @@ -57,33 +57,6 @@ This package provides three main components for handling x402 payments on EVM-co ] ``` -### Client Builder (`@x402/evm/client`) - -**Convenience builder** for creating fully-configured EVM clients - -**Exports:** -- `createEvmClient(config)` - Creates x402Client with EVM support -- `EvmClientConfig` - Configuration interface - -**What it does:** -- Automatically registers V2 wildcard scheme (`eip155:*`) -- Automatically registers all V1 networks from `NETWORKS` -- Optionally applies payment policies -- Optionally uses custom payment selector - -**Example:** -```typescript -import { createEvmClient } from "@x402/evm/client"; -import { toClientEvmSigner } from "@x402/evm"; -import { privateKeyToAccount } from "viem/accounts"; - -const account = privateKeyToAccount("0x..."); -const signer = toClientEvmSigner(account); - -const client = createEvmClient({ signer }); -// Ready to use with both V1 and V2! -``` - ## Version Differences ### V2 (Main Package) @@ -102,17 +75,7 @@ const client = createEvmClient({ signer }); ## Usage Patterns -### 1. Using Pre-built Builder (Recommended) - -```typescript -import { createEvmClient } from "@x402/evm/client"; -import { wrapFetchWithPayment } from "@x402/fetch"; - -const client = createEvmClient({ signer: myEvmSigner }); -const paidFetch = wrapFetchWithPayment(fetch, client); -``` - -### 2. Direct Registration (Full Control) +### 1. Direct Registration (Full Control) ```typescript import { x402Client } from "@x402/core/client"; @@ -125,7 +88,31 @@ const client = new x402Client() .registerSchemeV1("base", new ExactEvmClientV1(signer)); ``` -### 3. Using Config (Flexible) +### Extension RPC Configuration (Optional) + +`ExactEvmClient` only requires signer support for `address` + `signTypedData`. +Permit2 extension enrichment (EIP-2612 / ERC-20 approval gas sponsoring) can +optionally use explicit RPC config when signer read/fee helpers are unavailable. + +No chain-default RPC fallback is applied by the SDK. + +```typescript +// Per-network explicit registration +const client = new x402Client() + .register("eip155:137", new ExactEvmClient(signer, { rpcUrl: polygonRpcUrl })) + .register("eip155:8453", new ExactEvmClient(signer, { rpcUrl: baseRpcUrl })); + +// Wildcard registration with chain-id keyed config map +const wildcardClient = new x402Client().register( + "eip155:*", + new ExactEvmClient(signer, { + 137: { rpcUrl: polygonRpcUrl }, + 8453: { rpcUrl: baseRpcUrl }, + }), +); +``` + +### 2. Using Config (Flexible) ```typescript import { x402Client } from "@x402/core/client"; @@ -154,10 +141,11 @@ See `NETWORKS` constant in `@x402/evm/v1` ## Asset Support -Supports any ERC-3009 compatible token: -- USDC (primary) -- EURC -- Any token implementing `transferWithAuthorization()` +Supports two asset transfer methods: +- **EIP-3009**: Tokens with native `transferWithAuthorization()` (e.g., USDC, EURC) — simplest, truly gasless +- **Permit2**: Any ERC-20 token — universal fallback, requires one-time approval + +See [DEFAULT_ASSET.md](src/exact/server/DEFAULT_ASSET.md) for the current list of configured chains and how to add new ones. ## Development @@ -181,3 +169,4 @@ npm run format - `@x402/core` - Core protocol types and client - `@x402/fetch` - HTTP wrapper with automatic payment handling - `@x402/svm` - Solana/SVM implementation +- `@x402/stellar` - Stellar implementation diff --git a/typescript/packages/mechanisms/evm/package.json b/typescript/packages/mechanisms/evm/package.json index 8d57cfe37c..ffceb6cfbf 100644 --- a/typescript/packages/mechanisms/evm/package.json +++ b/typescript/packages/mechanisms/evm/package.json @@ -1,6 +1,6 @@ { "name": "@x402/evm", - "version": "2.5.0", + "version": "2.8.0", "main": "./dist/cjs/index.js", "module": "./dist/esm/index.js", "types": "./dist/cjs/index.d.ts", @@ -46,7 +46,6 @@ }, "dependencies": { "@x402/core": "workspace:~", - "@x402/extensions": "workspace:~", "viem": "^2.39.3", "zod": "^3.24.2" }, @@ -120,6 +119,36 @@ "types": "./dist/cjs/exact/v1/facilitator/index.d.ts", "default": "./dist/cjs/exact/v1/facilitator/index.js" } + }, + "./upto/client": { + "import": { + "types": "./dist/esm/upto/client/index.d.mts", + "default": "./dist/esm/upto/client/index.mjs" + }, + "require": { + "types": "./dist/cjs/upto/client/index.d.ts", + "default": "./dist/cjs/upto/client/index.js" + } + }, + "./upto/server": { + "import": { + "types": "./dist/esm/upto/server/index.d.mts", + "default": "./dist/esm/upto/server/index.mjs" + }, + "require": { + "types": "./dist/cjs/upto/server/index.d.ts", + "default": "./dist/cjs/upto/server/index.js" + } + }, + "./upto/facilitator": { + "import": { + "types": "./dist/esm/upto/facilitator/index.d.mts", + "default": "./dist/esm/upto/facilitator/index.mjs" + }, + "require": { + "types": "./dist/cjs/upto/facilitator/index.d.ts", + "default": "./dist/cjs/upto/facilitator/index.js" + } } }, "files": [ diff --git a/typescript/packages/mechanisms/evm/src/constants.ts b/typescript/packages/mechanisms/evm/src/constants.ts index 55bd23f8d1..f81adff91e 100644 --- a/typescript/packages/mechanisms/evm/src/constants.ts +++ b/typescript/packages/mechanisms/evm/src/constants.ts @@ -11,7 +11,7 @@ export const authorizationTypes = { } as const; /** - * Permit2 EIP-712 types for signing PermitWitnessTransferFrom. + * Permit2 EIP-712 types for signing PermitWitnessTransferFrom (exact scheme). * Must match the exact format expected by the Permit2 contract. * Note: Types must be in ALPHABETICAL order after the primary type (TokenPermissions < Witness). */ @@ -33,6 +33,31 @@ export const permit2WitnessTypes = { ], } as const; +/** + * Permit2 EIP-712 types for signing PermitWitnessTransferFrom (upto scheme). + * The upto witness includes a `facilitator` field that the exact witness does not. + * This ensures only the authorized facilitator can settle the payment. + * Must match: Witness(address to,address facilitator,uint256 validAfter) + */ +export const uptoPermit2WitnessTypes = { + PermitWitnessTransferFrom: [ + { name: "permitted", type: "TokenPermissions" }, + { name: "spender", type: "address" }, + { name: "nonce", type: "uint256" }, + { name: "deadline", type: "uint256" }, + { name: "witness", type: "Witness" }, + ], + TokenPermissions: [ + { name: "token", type: "address" }, + { name: "amount", type: "uint256" }, + ], + Witness: [ + { name: "to", type: "address" }, + { name: "facilitator", type: "address" }, + { name: "validAfter", type: "uint256" }, + ], +} as const; + // EIP3009 ABI for transferWithAuthorization function export const eip3009ABI = [ { @@ -81,6 +106,23 @@ export const eip3009ABI = [ stateMutability: "view", type: "function", }, + { + inputs: [], + name: "name", + outputs: [{ name: "", type: "string" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { name: "authorizer", type: "address" }, + { name: "nonce", type: "bytes32" }, + ], + name: "authorizationState", + outputs: [{ name: "", type: "bool" }], + stateMutability: "view", + type: "function", + }, ] as const; /** @@ -172,12 +214,10 @@ export const x402ExactPermit2ProxyAddress = "0x402085c248EeA27D92E8b30b2C58ed07f * - Vanity-mined salt for prefix 0x4020 and suffix 0002 * - Contract bytecode + constructor args (PERMIT2_ADDRESS) */ -export const x402UptoPermit2ProxyAddress = "0x402039b3d6E6BEC5A02c2C9fd937ac17A6940002" as const; +export const x402UptoPermit2ProxyAddress = "0x4020A4f3b7b90ccA423B9fabCc0CE57C6C240002" as const; /** - * Shared ABI components for the Permit2 witness tuple. - * Used in both x402ExactPermit2ProxyABI and x402UptoPermit2ProxyABI to keep them in sync. - * The upto contract's witness struct is identical to exact (both remove 'extra' post-audit). + * ABI components for the exact Permit2 witness tuple: Witness(address to, uint256 validAfter). */ const permit2WitnessABIComponents = [ { name: "to", type: "address", internalType: "address" }, @@ -185,9 +225,19 @@ const permit2WitnessABIComponents = [ ] as const; /** - * x402UptoPermit2Proxy ABI - settle function for upto payment scheme (variable amounts). - * Updated post-audit: 'extra' removed from witness struct, 'initialize()' removed (now - * a constructor arg), and error names aligned with x402ExactPermit2Proxy. + * ABI components for the upto Permit2 witness tuple: + * Witness(address to, address facilitator, uint256 validAfter). + */ +const uptoPermit2WitnessABIComponents = [ + { name: "to", type: "address", internalType: "address" }, + { name: "facilitator", type: "address", internalType: "address" }, + { name: "validAfter", type: "uint256", internalType: "uint256" }, +] as const; + +/** + * x402UptoPermit2Proxy ABI — settle/settleWithPermit for the upto payment scheme. + * Key differences from exact: settle() takes a `uint256 amount` parameter, and the + * Witness struct includes an `address facilitator` field. */ export const x402UptoPermit2ProxyABI = [ { @@ -233,12 +283,13 @@ export const x402UptoPermit2ProxyABI = [ { name: "deadline", type: "uint256", internalType: "uint256" }, ], }, + { name: "amount", type: "uint256", internalType: "uint256" }, { name: "owner", type: "address", internalType: "address" }, { name: "witness", type: "tuple", internalType: "struct x402UptoPermit2Proxy.Witness", - components: permit2WitnessABIComponents, + components: uptoPermit2WitnessABIComponents, }, { name: "signature", type: "bytes", internalType: "bytes" }, ], @@ -279,12 +330,13 @@ export const x402UptoPermit2ProxyABI = [ { name: "deadline", type: "uint256", internalType: "uint256" }, ], }, + { name: "amount", type: "uint256", internalType: "uint256" }, { name: "owner", type: "address", internalType: "address" }, { name: "witness", type: "tuple", internalType: "struct x402UptoPermit2Proxy.Witness", - components: permit2WitnessABIComponents, + components: uptoPermit2WitnessABIComponents, }, { name: "signature", type: "bytes", internalType: "bytes" }, ], @@ -293,13 +345,14 @@ export const x402UptoPermit2ProxyABI = [ }, { type: "event", name: "Settled", inputs: [], anonymous: false }, { type: "event", name: "SettledWithPermit", inputs: [], anonymous: false }, - { type: "error", name: "InvalidAmount", inputs: [] }, + { type: "error", name: "AmountExceedsPermitted", inputs: [] }, { type: "error", name: "InvalidDestination", inputs: [] }, { type: "error", name: "InvalidOwner", inputs: [] }, { type: "error", name: "InvalidPermit2Address", inputs: [] }, { type: "error", name: "PaymentTooEarly", inputs: [] }, { type: "error", name: "Permit2612AmountMismatch", inputs: [] }, { type: "error", name: "ReentrancyGuardReentrantCall", inputs: [] }, + { type: "error", name: "UnauthorizedFacilitator", inputs: [] }, ] as const; /** diff --git a/typescript/packages/mechanisms/evm/src/exact/client/eip2612.ts b/typescript/packages/mechanisms/evm/src/exact/client/eip2612.ts index f5887cbc36..f2b86dec53 100644 --- a/typescript/packages/mechanisms/evm/src/exact/client/eip2612.ts +++ b/typescript/packages/mechanisms/evm/src/exact/client/eip2612.ts @@ -1,7 +1,11 @@ import { getAddress } from "viem"; -import type { Eip2612GasSponsoringInfo } from "@x402/extensions"; import { eip2612PermitTypes, eip2612NoncesAbi, PERMIT2_ADDRESS } from "../../constants"; import { ClientEvmSigner } from "../../signer"; +import type { Eip2612GasSponsoringInfo } from "../extensions"; + +export type Eip2612PermitSigner = Pick & { + readContract: NonNullable; +}; /** * Signs an EIP-2612 permit authorizing the Permit2 contract to spend tokens. @@ -22,7 +26,7 @@ import { ClientEvmSigner } from "../../signer"; * @returns The EIP-2612 gas sponsoring info object */ export async function signEip2612Permit( - signer: ClientEvmSigner, + signer: Eip2612PermitSigner, tokenAddress: `0x${string}`, tokenName: string, tokenVersion: string, diff --git a/typescript/packages/mechanisms/evm/src/exact/client/erc20approval.ts b/typescript/packages/mechanisms/evm/src/exact/client/erc20approval.ts index b3928902f2..78d24b52a8 100644 --- a/typescript/packages/mechanisms/evm/src/exact/client/erc20approval.ts +++ b/typescript/packages/mechanisms/evm/src/exact/client/erc20approval.ts @@ -1,8 +1,4 @@ import { encodeFunctionData, getAddress, maxUint256 } from "viem"; -import { - ERC20_APPROVAL_GAS_SPONSORING_VERSION, - type Erc20ApprovalGasSponsoringInfo, -} from "@x402/extensions"; import { PERMIT2_ADDRESS, erc20ApproveAbi, @@ -11,6 +7,16 @@ import { DEFAULT_MAX_PRIORITY_FEE_PER_GAS, } from "../../constants"; import { ClientEvmSigner } from "../../signer"; +import { + ERC20_APPROVAL_GAS_SPONSORING_VERSION, + type Erc20ApprovalGasSponsoringInfo, +} from "../extensions"; + +export type Erc20ApprovalTxSigner = Pick & { + signTransaction: NonNullable; + getTransactionCount: NonNullable; + estimateFeesPerGas?: NonNullable; +}; /** * Signs an EIP-1559 `approve(Permit2, MaxUint256)` transaction for the given token. @@ -27,7 +33,7 @@ import { ClientEvmSigner } from "../../signer"; * @returns The ERC-20 approval gas sponsoring info object */ export async function signErc20ApprovalTransaction( - signer: ClientEvmSigner, + signer: Erc20ApprovalTxSigner, tokenAddress: `0x${string}`, chainId: number, ): Promise { @@ -42,13 +48,16 @@ export async function signErc20ApprovalTransaction( }); // Get current nonce for the sender - const nonce = await signer.getTransactionCount!({ address: from }); + const nonce = await signer.getTransactionCount({ address: from }); // Get current fee estimates, with fallback values let maxFeePerGas: bigint; let maxPriorityFeePerGas: bigint; try { - const fees = await signer.estimateFeesPerGas!(); + const fees = await signer.estimateFeesPerGas?.(); + if (!fees) { + throw new Error("no fee estimates available"); + } maxFeePerGas = fees.maxFeePerGas; maxPriorityFeePerGas = fees.maxPriorityFeePerGas; } catch { @@ -57,7 +66,7 @@ export async function signErc20ApprovalTransaction( } // Sign the EIP-1559 transaction (not broadcast) - const signedTransaction = await signer.signTransaction!({ + const signedTransaction = await signer.signTransaction({ to: tokenAddress, data, nonce, diff --git a/typescript/packages/mechanisms/evm/src/exact/client/index.ts b/typescript/packages/mechanisms/evm/src/exact/client/index.ts index fb10b8dd17..6d0958b019 100644 --- a/typescript/packages/mechanisms/evm/src/exact/client/index.ts +++ b/typescript/packages/mechanisms/evm/src/exact/client/index.ts @@ -1,6 +1,11 @@ export { ExactEvmScheme } from "./scheme"; export { registerExactEvmScheme } from "./register"; export type { EvmClientConfig } from "./register"; +export type { + ExactEvmSchemeConfig, + ExactEvmSchemeConfigByChainId, + ExactEvmSchemeOptions, +} from "./rpc"; export { createPermit2ApprovalTx, getPermit2AllowanceReadParams, diff --git a/typescript/packages/mechanisms/evm/src/exact/client/permit2.ts b/typescript/packages/mechanisms/evm/src/exact/client/permit2.ts index 6f8534dabc..ba02333af3 100644 --- a/typescript/packages/mechanisms/evm/src/exact/client/permit2.ts +++ b/typescript/packages/mechanisms/evm/src/exact/client/permit2.ts @@ -1,15 +1,13 @@ import { PaymentRequirements, PaymentPayloadResult } from "@x402/core/types"; import { encodeFunctionData, getAddress } from "viem"; import { - permit2WitnessTypes, PERMIT2_ADDRESS, x402ExactPermit2ProxyAddress, erc20ApproveAbi, erc20AllowanceAbi, } from "../../constants"; import { ClientEvmSigner } from "../../signer"; -import { ExactPermit2Payload } from "../../types"; -import { createPermit2Nonce, getEvmChainId } from "../../utils"; +import { createPermit2PayloadForProxy } from "../../shared/permit2"; /** Maximum uint256 value for unlimited approval. */ const MAX_UINT256 = BigInt("0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"); @@ -29,88 +27,12 @@ export async function createPermit2Payload( x402Version: number, paymentRequirements: PaymentRequirements, ): Promise { - const now = Math.floor(Date.now() / 1000); - const nonce = createPermit2Nonce(); - - // Lower time bound - allow some clock skew - const validAfter = (now - 600).toString(); - // Upper time bound is enforced by Permit2's deadline field - const deadline = (now + paymentRequirements.maxTimeoutSeconds).toString(); - - const permit2Authorization: ExactPermit2Payload["permit2Authorization"] = { - from: signer.address, - permitted: { - token: getAddress(paymentRequirements.asset), - amount: paymentRequirements.amount, - }, - spender: x402ExactPermit2ProxyAddress, - nonce, - deadline, - witness: { - to: getAddress(paymentRequirements.payTo), - validAfter, - }, - }; - - const signature = await signPermit2Authorization( + return createPermit2PayloadForProxy( + x402ExactPermit2ProxyAddress, signer, - permit2Authorization, + x402Version, paymentRequirements, ); - - const payload: ExactPermit2Payload = { - signature, - permit2Authorization, - }; - - return { - x402Version, - payload, - }; -} - -/** - * Sign the Permit2 authorization using EIP-712 with witness data. - * The signature authorizes the x402Permit2Proxy to transfer tokens on behalf of the signer. - * - * @param signer - The EVM signer - * @param permit2Authorization - The Permit2 authorization parameters - * @param requirements - The payment requirements - * @returns Promise resolving to the signature - */ -async function signPermit2Authorization( - signer: ClientEvmSigner, - permit2Authorization: ExactPermit2Payload["permit2Authorization"], - requirements: PaymentRequirements, -): Promise<`0x${string}`> { - const chainId = getEvmChainId(requirements.network); - - const domain = { - name: "Permit2", - chainId, - verifyingContract: PERMIT2_ADDRESS, - }; - - const message = { - permitted: { - token: getAddress(permit2Authorization.permitted.token), - amount: BigInt(permit2Authorization.permitted.amount), - }, - spender: getAddress(permit2Authorization.spender), - nonce: BigInt(permit2Authorization.nonce), - deadline: BigInt(permit2Authorization.deadline), - witness: { - to: getAddress(permit2Authorization.witness.to), - validAfter: BigInt(permit2Authorization.witness.validAfter), - }, - }; - - return await signer.signTypedData({ - domain, - types: permit2WitnessTypes, - primaryType: "PermitWitnessTransferFrom", - message, - }); } /** diff --git a/typescript/packages/mechanisms/evm/src/exact/client/register.ts b/typescript/packages/mechanisms/evm/src/exact/client/register.ts index d28912074c..3e91e135af 100644 --- a/typescript/packages/mechanisms/evm/src/exact/client/register.ts +++ b/typescript/packages/mechanisms/evm/src/exact/client/register.ts @@ -2,6 +2,7 @@ import { x402Client, SelectPaymentRequirements, PaymentPolicy } from "@x402/core import { Network } from "@x402/core/types"; import { ClientEvmSigner } from "../../signer"; import { ExactEvmScheme } from "./scheme"; +import { ExactEvmSchemeOptions } from "./rpc"; import { ExactEvmSchemeV1 } from "../v1/client/scheme"; import { NETWORKS } from "../../v1"; @@ -26,8 +27,15 @@ export interface EvmClientConfig { policies?: PaymentPolicy[]; /** - * Optional specific networks to register - * If not provided, registers wildcard support (eip155:*) + * Optional Exact EVM client scheme options. + * Supports either a single config ({ rpcUrl }) or per-chain configs + * keyed by EVM chain ID ({ 8453: { rpcUrl: "..." } }). + */ + schemeOptions?: ExactEvmSchemeOptions; + + /** + * Optional specific networks to register. + * If not provided, registers wildcard support (eip155:*). */ networks?: Network[]; } @@ -55,7 +63,7 @@ export interface EvmClientConfig { * ``` */ export function registerExactEvmScheme(client: x402Client, config: EvmClientConfig): x402Client { - const evmScheme = new ExactEvmScheme(config.signer); + const evmScheme = new ExactEvmScheme(config.signer, config.schemeOptions); // Register V2 scheme // EIP-2612 gas sponsoring is handled internally by the scheme when the diff --git a/typescript/packages/mechanisms/evm/src/exact/client/rpc.ts b/typescript/packages/mechanisms/evm/src/exact/client/rpc.ts new file mode 100644 index 0000000000..7c4318766f --- /dev/null +++ b/typescript/packages/mechanisms/evm/src/exact/client/rpc.ts @@ -0,0 +1,11 @@ +// Re-export from shared for backward compatibility +export { + type EvmSchemeConfig, + type EvmSchemeConfigByChainId, + type EvmSchemeOptions, + type ExactEvmSchemeConfig, + type ExactEvmSchemeConfigByChainId, + type ExactEvmSchemeOptions, + resolveExtensionRpcCapabilities, + resolveRpcUrl, +} from "../../shared/rpc"; diff --git a/typescript/packages/mechanisms/evm/src/exact/client/scheme.ts b/typescript/packages/mechanisms/evm/src/exact/client/scheme.ts index d4e91904ee..50202bde29 100644 --- a/typescript/packages/mechanisms/evm/src/exact/client/scheme.ts +++ b/typescript/packages/mechanisms/evm/src/exact/client/scheme.ts @@ -1,19 +1,18 @@ import { - PaymentRequirements, SchemeNetworkClient, + PaymentRequirements, PaymentPayloadResult, PaymentPayloadContext, } from "@x402/core/types"; -import { EIP2612_GAS_SPONSORING, ERC20_APPROVAL_GAS_SPONSORING } from "@x402/extensions"; import { ClientEvmSigner } from "../../signer"; import { AssetTransferMethod } from "../../types"; -import { PERMIT2_ADDRESS, erc20AllowanceAbi } from "../../constants"; -import { getAddress } from "viem"; -import { getEvmChainId } from "../../utils"; import { createEIP3009Payload } from "./eip3009"; import { createPermit2Payload } from "./permit2"; -import { signEip2612Permit } from "./eip2612"; -import { signErc20ApprovalTransaction } from "./erc20approval"; +import { + trySignEip2612PermitExtension, + trySignErc20ApprovalExtension, +} from "../../shared/extensions"; +import { ExactEvmSchemeOptions } from "./rpc"; /** * EVM client implementation for the Exact payment scheme. @@ -34,10 +33,15 @@ export class ExactEvmScheme implements SchemeNetworkClient { * Creates a new ExactEvmClient instance. * * @param signer - The EVM signer for client operations. - * Must support `readContract` for EIP-2612 gas sponsoring. - * Use `createWalletClient(...).extend(publicActions)` or `toClientEvmSigner(account, publicClient)`. + * Base flow only requires `address` + `signTypedData`. + * Extension enrichment (EIP-2612 / ERC-20 approval sponsoring) additionally + * requires optional capabilities like `readContract` and tx signing helpers. + * @param options - Optional RPC configuration used to backfill extension capabilities. */ - constructor(private readonly signer: ClientEvmSigner) {} + constructor( + private readonly signer: ClientEvmSigner, + private readonly options?: ExactEvmSchemeOptions, + ) {} /** * Creates a payment payload for the Exact scheme. @@ -63,8 +67,9 @@ export class ExactEvmScheme implements SchemeNetworkClient { if (assetTransferMethod === "permit2") { const result = await createPermit2Payload(this.signer, x402Version, paymentRequirements); - // Check if EIP-2612 gas sponsoring is advertised and we can handle it - const eip2612Extensions = await this.trySignEip2612Permit( + const eip2612Extensions = await trySignEip2612PermitExtension( + this.signer, + this.options, paymentRequirements, result, context, @@ -77,8 +82,12 @@ export class ExactEvmScheme implements SchemeNetworkClient { }; } - // EIP-2612 not applicable — try ERC-20 approval gas sponsoring as fallback - const erc20Extensions = await this.trySignErc20Approval(paymentRequirements, result, context); + const erc20Extensions = await trySignErc20ApprovalExtension( + this.signer, + this.options, + paymentRequirements, + context, + ); if (erc20Extensions) { return { ...result, @@ -91,139 +100,4 @@ export class ExactEvmScheme implements SchemeNetworkClient { return createEIP3009Payload(this.signer, x402Version, paymentRequirements); } - - /** - * Attempts to sign an EIP-2612 permit for gasless Permit2 approval. - * - * Returns extension data if: - * 1. Server advertises eip2612GasSponsoring - * 2. Signer has readContract capability - * 3. Current Permit2 allowance is insufficient - * - * Returns undefined if the extension should not be used. - * - * @param requirements - The payment requirements from the server - * @param result - The payment payload result from the scheme - * @param context - Optional context containing server extensions and metadata - * @returns Extension data for EIP-2612 gas sponsoring, or undefined if not applicable - */ - private async trySignEip2612Permit( - requirements: PaymentRequirements, - result: PaymentPayloadResult, - context?: PaymentPayloadContext, - ): Promise | undefined> { - // Check if server advertises eip2612GasSponsoring - if (!context?.extensions?.[EIP2612_GAS_SPONSORING.key]) { - return undefined; - } - - // Check that required token metadata is available - const tokenName = requirements.extra?.name as string | undefined; - const tokenVersion = requirements.extra?.version as string | undefined; - if (!tokenName || !tokenVersion) { - return undefined; - } - - const chainId = getEvmChainId(requirements.network); - const tokenAddress = getAddress(requirements.asset) as `0x${string}`; - - // Check if user already has sufficient Permit2 allowance - try { - const allowance = (await this.signer.readContract({ - address: tokenAddress, - abi: erc20AllowanceAbi, - functionName: "allowance", - args: [this.signer.address, PERMIT2_ADDRESS], - })) as bigint; - - if (allowance >= BigInt(requirements.amount)) { - return undefined; // Already approved, no need for EIP-2612 - } - } catch { - // If we can't check allowance, proceed with EIP-2612 signing - } - - // Use the same deadline as the Permit2 authorization - const permit2Auth = result.payload?.permit2Authorization as Record | undefined; - const deadline = - (permit2Auth?.deadline as string) ?? - Math.floor(Date.now() / 1000 + requirements.maxTimeoutSeconds).toString(); - - // Sign the EIP-2612 permit with the exact Permit2 permitted amount - // (the contract enforces permit2612.value == permit.permitted.amount) - const info = await signEip2612Permit( - this.signer, - tokenAddress, - tokenName, - tokenVersion, - chainId, - deadline, - requirements.amount, - ); - - return { - [EIP2612_GAS_SPONSORING.key]: { info }, - }; - } - - /** - * Attempts to sign an ERC-20 approval transaction for gasless Permit2 approval. - * - * This is the fallback path when the token does not support EIP-2612. The client - * signs (but does not broadcast) a raw `approve(Permit2, MaxUint256)` transaction. - * The facilitator broadcasts it atomically before settling. - * - * Returns extension data if: - * 1. Server advertises erc20ApprovalGasSponsoring - * 2. Signer has signTransaction + getTransactionCount capabilities - * 3. Current Permit2 allowance is insufficient - * - * Returns undefined if the extension should not be used. - * - * @param requirements - The payment requirements from the server - * @param _result - The payment payload result from the scheme (unused) - * @param context - Optional context containing server extensions and metadata - * @returns Extension data for ERC-20 approval gas sponsoring, or undefined if not applicable - */ - private async trySignErc20Approval( - requirements: PaymentRequirements, - _result: PaymentPayloadResult, - context?: PaymentPayloadContext, - ): Promise | undefined> { - // Check if server advertises erc20ApprovalGasSponsoring - if (!context?.extensions?.[ERC20_APPROVAL_GAS_SPONSORING.key]) { - return undefined; - } - - // Check that signer has the required capabilities for signing raw transactions - if (!this.signer.signTransaction || !this.signer.getTransactionCount) { - return undefined; - } - - const chainId = getEvmChainId(requirements.network); - const tokenAddress = getAddress(requirements.asset) as `0x${string}`; - - // Check if user already has sufficient Permit2 allowance - try { - const allowance = (await this.signer.readContract({ - address: tokenAddress, - abi: erc20AllowanceAbi, - functionName: "allowance", - args: [this.signer.address, PERMIT2_ADDRESS], - })) as bigint; - - if (allowance >= BigInt(requirements.amount)) { - return undefined; // Already approved, no need for ERC-20 approval tx - } - } catch { - // If we can't check allowance, proceed with signing - } - - // Sign the approve(Permit2, MaxUint256) transaction - const info = await signErc20ApprovalTransaction(this.signer, tokenAddress, chainId); - - return { - [ERC20_APPROVAL_GAS_SPONSORING.key]: { info }, - }; - } } diff --git a/typescript/packages/mechanisms/evm/src/exact/extensions.ts b/typescript/packages/mechanisms/evm/src/exact/extensions.ts new file mode 100644 index 0000000000..d3e3fe458b --- /dev/null +++ b/typescript/packages/mechanisms/evm/src/exact/extensions.ts @@ -0,0 +1,177 @@ +import type { PaymentPayload } from "@x402/core/types"; +import type { FacilitatorEvmSigner } from "../signer"; + +export const EIP2612_GAS_SPONSORING_KEY = "eip2612GasSponsoring" as const; +export const ERC20_APPROVAL_GAS_SPONSORING_KEY = "erc20ApprovalGasSponsoring" as const; +export const ERC20_APPROVAL_GAS_SPONSORING_VERSION = "1" as const; + +export interface Eip2612GasSponsoringInfo { + [key: string]: unknown; + from: string; + asset: string; + spender: string; + amount: string; + nonce: string; + deadline: string; + signature: string; + version: string; +} + +export interface Erc20ApprovalGasSponsoringInfo { + [key: string]: unknown; + from: `0x${string}`; + asset: `0x${string}`; + spender: `0x${string}`; + amount: string; + signedTransaction: `0x${string}`; + version: string; +} + +/** + * A single transaction to be executed by the signer. + * - `0x${string}`: a pre-signed serialized transaction (broadcast as-is via sendRawTransaction) + * - `{ to, data, gas? }`: an unsigned call intent (signer signs and broadcasts) + */ +export type TransactionRequest = + | `0x${string}` + | { to: `0x${string}`; data: `0x${string}`; gas?: bigint }; + +export type Erc20ApprovalGasSponsoringSigner = FacilitatorEvmSigner & { + sendTransactions(transactions: TransactionRequest[]): Promise<`0x${string}`[]>; + simulateTransactions?(transactions: TransactionRequest[]): Promise; +}; + +export interface Erc20ApprovalGasSponsoringFacilitatorExtension { + key: typeof ERC20_APPROVAL_GAS_SPONSORING_KEY; + signer?: Erc20ApprovalGasSponsoringSigner; + signerForNetwork?: (network: string) => Erc20ApprovalGasSponsoringSigner | undefined; +} + +/** + * Extracts a typed `info` payload from an extension entry. + * + * @param payload - Payment payload containing optional extensions. + * @param extensionKey - Extension key to extract. + * @returns The extension `info` object when present; otherwise null. + */ +function _extractInfo( + payload: PaymentPayload, + extensionKey: string, +): Record | null { + const extensions = payload.extensions; + if (!extensions) return null; + const extension = extensions[extensionKey] as { info?: Record } | undefined; + if (!extension?.info) return null; + return extension.info; +} + +/** + * Extracts and validates required EIP-2612 gas sponsoring fields. + * + * @param payload - Payment payload returned by the client scheme. + * @returns Parsed EIP-2612 gas sponsoring info when available and complete. + */ +export function extractEip2612GasSponsoringInfo( + payload: PaymentPayload, +): Eip2612GasSponsoringInfo | null { + const info = _extractInfo(payload, EIP2612_GAS_SPONSORING_KEY); + if (!info) return null; + if ( + !info.from || + !info.asset || + !info.spender || + !info.amount || + !info.nonce || + !info.deadline || + !info.signature || + !info.version + ) { + return null; + } + return info as unknown as Eip2612GasSponsoringInfo; +} + +/** + * Validates the structure and formatting of EIP-2612 sponsoring info. + * + * @param info - EIP-2612 extension info to validate. + * @returns True when all required fields match expected patterns. + */ +export function validateEip2612GasSponsoringInfo(info: Eip2612GasSponsoringInfo): boolean { + const addressPattern = /^0x[a-fA-F0-9]{40}$/; + const numericPattern = /^[0-9]+$/; + const hexPattern = /^0x[a-fA-F0-9]+$/; + const versionPattern = /^[0-9]+(\.[0-9]+)*$/; + return ( + addressPattern.test(info.from) && + addressPattern.test(info.asset) && + addressPattern.test(info.spender) && + numericPattern.test(info.amount) && + numericPattern.test(info.nonce) && + numericPattern.test(info.deadline) && + hexPattern.test(info.signature) && + versionPattern.test(info.version) + ); +} + +/** + * Extracts and validates required ERC-20 approval sponsoring fields. + * + * @param payload - Payment payload returned by the client scheme. + * @returns Parsed ERC-20 approval sponsoring info when available and complete. + */ +export function extractErc20ApprovalGasSponsoringInfo( + payload: PaymentPayload, +): Erc20ApprovalGasSponsoringInfo | null { + const info = _extractInfo(payload, ERC20_APPROVAL_GAS_SPONSORING_KEY); + if (!info) return null; + if ( + !info.from || + !info.asset || + !info.spender || + !info.amount || + !info.signedTransaction || + !info.version + ) { + return null; + } + return info as unknown as Erc20ApprovalGasSponsoringInfo; +} + +/** + * Validates the structure and formatting of ERC-20 approval sponsoring info. + * + * @param info - ERC-20 approval extension info to validate. + * @returns True when all required fields match expected patterns. + */ +export function validateErc20ApprovalGasSponsoringInfo( + info: Erc20ApprovalGasSponsoringInfo, +): boolean { + const addressPattern = /^0x[a-fA-F0-9]{40}$/; + const numericPattern = /^[0-9]+$/; + const hexPattern = /^0x[a-fA-F0-9]+$/; + const versionPattern = /^[0-9]+(\.[0-9]+)*$/; + return ( + addressPattern.test(info.from) && + addressPattern.test(info.asset) && + addressPattern.test(info.spender) && + numericPattern.test(info.amount) && + hexPattern.test(info.signedTransaction) && + versionPattern.test(info.version) + ); +} + +/** + * Resolves the ERC-20 approval extension signer for a specific network. + * + * @param extension - Optional facilitator extension config. + * @param network - CAIP-2 network identifier. + * @returns A network-specific signer when available, else the default signer. + */ +export function resolveErc20ApprovalExtensionSigner( + extension: Erc20ApprovalGasSponsoringFacilitatorExtension | undefined, + network: string, +): Erc20ApprovalGasSponsoringSigner | undefined { + if (!extension) return undefined; + return extension.signerForNetwork?.(network) ?? extension.signer; +} diff --git a/typescript/packages/mechanisms/evm/src/exact/facilitator/eip3009-utils.ts b/typescript/packages/mechanisms/evm/src/exact/facilitator/eip3009-utils.ts new file mode 100644 index 0000000000..2d8f2c4a69 --- /dev/null +++ b/typescript/packages/mechanisms/evm/src/exact/facilitator/eip3009-utils.ts @@ -0,0 +1,240 @@ +import { PaymentRequirements, VerifyResponse } from "@x402/core/types"; +import { encodeFunctionData, getAddress, Hex, parseErc6492Signature, parseSignature } from "viem"; +import { eip3009ABI } from "../../constants"; +import { multicall, ContractCall, RawContractCall } from "../../multicall"; +import { FacilitatorEvmSigner } from "../../signer"; +import { ExactEIP3009Payload } from "../../types"; +import * as Errors from "./errors"; + +export interface Eip6492Deployment { + factoryAddress: `0x${string}`; + factoryCalldata: `0x${string}`; +} + +/** + * Simulates transferWithAuthorization via eth_call. + * Returns true if simulation succeeded, false if it failed. + * + * @param signer - EVM signer for contract reads + * @param erc20Address - ERC-20 token contract address + * @param payload - EIP-3009 transfer authorization payload + * @param eip6492Deployment - Optional EIP-6492 factory info for undeployed smart wallets + * + * @returns true if simulation succeeded, false if it failed + */ +export async function simulateEip3009Transfer( + signer: FacilitatorEvmSigner, + erc20Address: `0x${string}`, + payload: ExactEIP3009Payload, + eip6492Deployment?: Eip6492Deployment, +): Promise { + const auth = payload.authorization; + const transferArgs = [ + getAddress(auth.from), + getAddress(auth.to), + BigInt(auth.value), + BigInt(auth.validAfter), + BigInt(auth.validBefore), + auth.nonce, + ] as const; + + if (eip6492Deployment) { + const { signature: innerSignature } = parseErc6492Signature(payload.signature!); + const transferCalldata = encodeFunctionData({ + abi: eip3009ABI, + functionName: "transferWithAuthorization", + args: [...transferArgs, innerSignature], + }); + + try { + const results = await multicall(signer.readContract.bind(signer), [ + { + address: getAddress(eip6492Deployment.factoryAddress), + callData: eip6492Deployment.factoryCalldata, + } satisfies RawContractCall, + { + address: erc20Address, + callData: transferCalldata, + } satisfies RawContractCall, + ]); + + return results[1]?.status === "success"; + } catch { + return false; + } + } + + const sig = payload.signature!; + const sigLength = sig.startsWith("0x") ? sig.length - 2 : sig.length; + const isECDSA = sigLength === 130; + + try { + if (isECDSA) { + const parsedSig = parseSignature(sig); + await signer.readContract({ + address: erc20Address, + abi: eip3009ABI, + functionName: "transferWithAuthorization", + args: [ + ...transferArgs, + (parsedSig.v as number | undefined) ?? parsedSig.yParity, + parsedSig.r, + parsedSig.s, + ], + }); + } else { + await signer.readContract({ + address: erc20Address, + abi: eip3009ABI, + functionName: "transferWithAuthorization", + args: [...transferArgs, sig], + }); + } + return true; + } catch { + return false; + } +} + +/** + * After simulation fails, runs a single diagnostic multicall to determine the most specific error reason. + * Checks balanceOf, name, version and authorizationState in one RPC round-trip. + * + * @param signer - EVM signer used for the payment + * @param erc20Address - Address of the ERC-20 token contract + * @param payload - The EIP-3009 transfer authorization payload + * @param requirements - Payment requirements to validate against + * @param amountRequired - Required amount for the payment (balance check) + * + * @returns Promise resolving to the verification result with validity and optional invalid reason + */ +export async function diagnoseEip3009SimulationFailure( + signer: FacilitatorEvmSigner, + erc20Address: `0x${string}`, + payload: ExactEIP3009Payload, + requirements: PaymentRequirements, + amountRequired: string, +): Promise { + const payer = payload.authorization.from; + + const diagnosticCalls: ContractCall[] = [ + { + address: erc20Address, + abi: eip3009ABI, + functionName: "balanceOf", + args: [payload.authorization.from], + }, + { + address: erc20Address, + abi: eip3009ABI, + functionName: "name", + }, + { + address: erc20Address, + abi: eip3009ABI, + functionName: "version", + }, + { + address: erc20Address, + abi: eip3009ABI, + functionName: "authorizationState", + args: [payload.authorization.from, payload.authorization.nonce], + }, + ]; + + try { + const results = await multicall(signer.readContract.bind(signer), diagnosticCalls); + + const [balanceResult, nameResult, versionResult, authStateResult] = results; + + if (authStateResult.status === "failure") { + return { isValid: false, invalidReason: Errors.ErrEip3009NotSupported, payer }; + } + + if (authStateResult.status === "success" && authStateResult.result === true) { + return { isValid: false, invalidReason: Errors.ErrEip3009NonceAlreadyUsed, payer }; + } + + if ( + nameResult.status === "success" && + requirements.extra?.name && + nameResult.result !== requirements.extra.name + ) { + return { isValid: false, invalidReason: Errors.ErrEip3009TokenNameMismatch, payer }; + } + + if ( + versionResult.status === "success" && + requirements.extra?.version && + versionResult.result !== requirements.extra.version + ) { + return { isValid: false, invalidReason: Errors.ErrEip3009TokenVersionMismatch, payer }; + } + + if (balanceResult.status === "success") { + const balance = balanceResult.result as bigint; + if (balance < BigInt(amountRequired)) { + return { + isValid: false, + invalidReason: Errors.ErrEip3009InsufficientBalance, + payer, + }; + } + } + } catch { + // Diagnostic multicall failed — fall through to generic error + } + + return { isValid: false, invalidReason: Errors.ErrEip3009SimulationFailed, payer }; +} + +/** + * Executes transferWithAuthorization onchain. + * + * @param signer - EVM signer for contract writes + * @param erc20Address - ERC-20 token contract address + * @param payload - EIP-3009 transfer authorization payload + * + * @returns Transaction hash + */ +export async function executeTransferWithAuthorization( + signer: FacilitatorEvmSigner, + erc20Address: `0x${string}`, + payload: ExactEIP3009Payload, +): Promise { + const { signature } = parseErc6492Signature(payload.signature!); + const signatureLength = signature.startsWith("0x") ? signature.length - 2 : signature.length; + const isECDSA = signatureLength === 130; + + const auth = payload.authorization; + const baseArgs = [ + getAddress(auth.from), + getAddress(auth.to), + BigInt(auth.value), + BigInt(auth.validAfter), + BigInt(auth.validBefore), + auth.nonce, + ] as const; + + if (isECDSA) { + const parsedSig = parseSignature(signature); + return signer.writeContract({ + address: erc20Address, + abi: eip3009ABI, + functionName: "transferWithAuthorization", + args: [ + ...baseArgs, + (parsedSig.v as number | undefined) || parsedSig.yParity, + parsedSig.r, + parsedSig.s, + ], + }); + } + + return signer.writeContract({ + address: erc20Address, + abi: eip3009ABI, + functionName: "transferWithAuthorization", + args: [...baseArgs, signature], + }); +} diff --git a/typescript/packages/mechanisms/evm/src/exact/facilitator/eip3009.ts b/typescript/packages/mechanisms/evm/src/exact/facilitator/eip3009.ts index b353b56098..69056a938b 100644 --- a/typescript/packages/mechanisms/evm/src/exact/facilitator/eip3009.ts +++ b/typescript/packages/mechanisms/evm/src/exact/facilitator/eip3009.ts @@ -4,11 +4,22 @@ import { SettleResponse, VerifyResponse, } from "@x402/core/types"; -import { getAddress, Hex, isAddressEqual, parseErc6492Signature, parseSignature } from "viem"; -import { authorizationTypes, eip3009ABI } from "../../constants"; +import { getAddress, Hex, isAddressEqual, parseErc6492Signature } from "viem"; +import { authorizationTypes } from "../../constants"; import { FacilitatorEvmSigner } from "../../signer"; import { getEvmChainId } from "../../utils"; import { ExactEIP3009Payload } from "../../types"; +import * as Errors from "./errors"; +import { + diagnoseEip3009SimulationFailure, + executeTransferWithAuthorization, + simulateEip3009Transfer, +} from "./eip3009-utils"; + +export interface VerifyEIP3009Options { + /** Run onchain simulation. Defaults to true. */ + simulate?: boolean; +} export interface EIP3009FacilitatorConfig { /** @@ -18,6 +29,12 @@ export interface EIP3009FacilitatorConfig { * @default false */ deployERC4337WithEIP6492: boolean; + /** + * If enabled, simulates transaction before settling. Defaults to false, ie only simulate during verify. + * + * @default false + */ + simulateInSettle?: boolean; } /** @@ -27,6 +44,7 @@ export interface EIP3009FacilitatorConfig { * @param payload - The payment payload to verify * @param requirements - The payment requirements * @param eip3009Payload - The EIP-3009 specific payload + * @param options - Optional verification options * @returns Promise resolving to verification response */ export async function verifyEIP3009( @@ -34,14 +52,18 @@ export async function verifyEIP3009( payload: PaymentPayload, requirements: PaymentRequirements, eip3009Payload: ExactEIP3009Payload, + options?: VerifyEIP3009Options, ): Promise { const payer = eip3009Payload.authorization.from; + let eip6492Deployment: + | { factoryAddress: `0x${string}`; factoryCalldata: `0x${string}` } + | undefined; // Verify scheme matches if (payload.accepted.scheme !== "exact" || requirements.scheme !== "exact") { return { isValid: false, - invalidReason: "unsupported_scheme", + invalidReason: Errors.ErrInvalidScheme, payer, }; } @@ -50,7 +72,7 @@ export async function verifyEIP3009( if (!requirements.extra?.name || !requirements.extra?.version) { return { isValid: false, - invalidReason: "missing_eip712_domain", + invalidReason: Errors.ErrMissingEip712Domain, payer, }; } @@ -62,7 +84,7 @@ export async function verifyEIP3009( if (payload.accepted.network !== requirements.network) { return { isValid: false, - invalidReason: "network_mismatch", + invalidReason: Errors.ErrNetworkMismatch, payer, }; } @@ -88,71 +110,70 @@ export async function verifyEIP3009( }; // Verify signature + // Note: verifyTypedData is implementation-dependent and pluggable on FacilitatorEvmSigner + // Some implementations only do EOA-style ECDSA recovery (e.g. viem/utils verifyTypedData, ethers.verifyTypedData) + // Viem's publicClient.verifyTypedData supports EOA and Smart Contract Account (ERC-1271 / ERC-6492) signature verification + let isValid = false; try { - const recoveredAddress = await signer.verifyTypedData({ + isValid = await signer.verifyTypedData({ address: eip3009Payload.authorization.from, ...permitTypedData, signature: eip3009Payload.signature!, }); + } catch { + isValid = false; + } + const signature = eip3009Payload.signature!; + const sigLen = signature.startsWith("0x") ? signature.length - 2 : signature.length; - if (!recoveredAddress) { + // Extract EIP-6492 deployment info (factory address + calldata) if present + const erc6492Data = parseErc6492Signature(signature); + const hasDeploymentInfo = + erc6492Data.address && + erc6492Data.data && + !isAddressEqual(erc6492Data.address, "0x0000000000000000000000000000000000000000"); + + if (hasDeploymentInfo) { + eip6492Deployment = { + factoryAddress: erc6492Data.address!, + factoryCalldata: erc6492Data.data!, + }; + } + + if (!isValid) { + // Check if signature is from a smart wallet + const isSmartWallet = sigLen > 130; // 65 bytes = 130 hex chars for EOA + + // EOA signature that failed verification — definitely invalid + if (!isSmartWallet) { return { isValid: false, - invalidReason: "invalid_exact_evm_payload_signature", + invalidReason: Errors.ErrInvalidSignature, payer, }; } - } catch { - // Signature verification failed - could be an undeployed smart wallet - // Check if smart wallet is deployed - const signature = eip3009Payload.signature!; - const signatureLength = signature.startsWith("0x") ? signature.length - 2 : signature.length; - const isSmartWallet = signatureLength > 130; // 65 bytes = 130 hex chars for EOA - if (isSmartWallet) { - const payerAddress = eip3009Payload.authorization.from; - const bytecode = await signer.getCode({ address: payerAddress }); - - if (!bytecode || bytecode === "0x") { - // Wallet is not deployed. Check if it's EIP-6492 with deployment info. - const erc6492Data = parseErc6492Signature(signature); - const hasDeploymentInfo = - erc6492Data.address && - erc6492Data.data && - !isAddressEqual(erc6492Data.address, "0x0000000000000000000000000000000000000000"); + // Smart wallet signature: check if deployed or has ERC-6492 deployment info + const bytecode = await signer.getCode({ address: payer }); + const isDeployed = bytecode && bytecode !== "0x"; - if (!hasDeploymentInfo) { - // Non-EIP-6492 undeployed smart wallet - will always fail at settlement - return { - isValid: false, - invalidReason: "invalid_exact_evm_payload_undeployed_smart_wallet", - payer: payerAddress, - }; - } - // EIP-6492 signature with deployment info - allow through - } else { - // Wallet is deployed but signature still failed - invalid signature - return { - isValid: false, - invalidReason: "invalid_exact_evm_payload_signature", - payer, - }; - } - } else { - // EOA signature failed + if (!isDeployed && !hasDeploymentInfo) { + // Undeployed smart wallet with no factory info return { isValid: false, - invalidReason: "invalid_exact_evm_payload_signature", + invalidReason: Errors.ErrUndeployedSmartWallet, payer, }; } + // Deployed smart wallet or undeployed with ERC-6492 factory info + // fall through to remaining field checks and onchain simulation } // Verify payment recipient matches if (getAddress(eip3009Payload.authorization.to) !== getAddress(requirements.payTo)) { return { isValid: false, - invalidReason: "invalid_exact_evm_payload_recipient_mismatch", + invalidReason: Errors.ErrRecipientMismatch, payer, }; } @@ -162,7 +183,7 @@ export async function verifyEIP3009( if (BigInt(eip3009Payload.authorization.validBefore) < BigInt(now + 6)) { return { isValid: false, - invalidReason: "invalid_exact_evm_payload_authorization_valid_before", + invalidReason: Errors.ErrValidBeforeExpired, payer, }; } @@ -171,41 +192,39 @@ export async function verifyEIP3009( if (BigInt(eip3009Payload.authorization.validAfter) > BigInt(now)) { return { isValid: false, - invalidReason: "invalid_exact_evm_payload_authorization_valid_after", + invalidReason: Errors.ErrValidAfterInFuture, payer, }; } - // Check balance - try { - const balance = (await signer.readContract({ - address: erc20Address, - abi: eip3009ABI, - functionName: "balanceOf", - args: [eip3009Payload.authorization.from], - })) as bigint; - - if (BigInt(balance) < BigInt(requirements.amount)) { - return { - isValid: false, - invalidReason: "insufficient_funds", - invalidMessage: `Insufficient funds to complete the payment. Required: ${requirements.amount} ${requirements.asset}, Available: ${balance.toString()} ${requirements.asset}. Please add funds to your wallet and try again.`, - payer, - }; - } - } catch { - // If we can't check balance, continue with other validations - } - - // Verify amount is sufficient - if (BigInt(eip3009Payload.authorization.value) < BigInt(requirements.amount)) { + // Verify amount exactly matches requirements + if (BigInt(eip3009Payload.authorization.value) !== BigInt(requirements.amount)) { return { isValid: false, - invalidReason: "invalid_exact_evm_payload_authorization_value", + invalidReason: Errors.ErrInvalidAuthorizationValue, payer, }; } + // Transaction simulation + if (options?.simulate !== false) { + const simulationSucceeded = await simulateEip3009Transfer( + signer, + erc20Address, + eip3009Payload, + eip6492Deployment, + ); + if (!simulationSucceeded) { + return diagnoseEip3009SimulationFailure( + signer, + erc20Address, + eip3009Payload, + requirements, + requirements.amount, + ); + } + } + return { isValid: true, invalidReason: undefined, @@ -233,21 +252,24 @@ export async function settleEIP3009( const payer = eip3009Payload.authorization.from; // Re-verify before settling - const valid = await verifyEIP3009(signer, payload, requirements, eip3009Payload); + const valid = await verifyEIP3009(signer, payload, requirements, eip3009Payload, { + simulate: config.simulateInSettle ?? false, + }); if (!valid.isValid) { return { success: false, network: payload.accepted.network, transaction: "", - errorReason: valid.invalidReason ?? "invalid_scheme", + errorReason: valid.invalidReason ?? Errors.ErrInvalidScheme, payer, }; } try { - // Parse ERC-6492 signature if applicable - const parseResult = parseErc6492Signature(eip3009Payload.signature!); - const { signature, address: factoryAddress, data: factoryCalldata } = parseResult; + // Parse ERC-6492 signature if applicable (for optional deployment) + const { address: factoryAddress, data: factoryCalldata } = parseErc6492Signature( + eip3009Payload.signature!, + ); // Deploy ERC-4337 smart wallet via EIP-6492 if configured and needed if ( @@ -271,48 +293,11 @@ export async function settleEIP3009( } } - // Determine if this is an ECDSA signature (EOA) or smart wallet signature - const signatureLength = signature.startsWith("0x") ? signature.length - 2 : signature.length; - const isECDSA = signatureLength === 130; - - let tx: Hex; - if (isECDSA) { - // For EOA wallets, parse signature into v, r, s and use that overload - const parsedSig = parseSignature(signature); - - tx = await signer.writeContract({ - address: getAddress(requirements.asset), - abi: eip3009ABI, - functionName: "transferWithAuthorization", - args: [ - getAddress(eip3009Payload.authorization.from), - getAddress(eip3009Payload.authorization.to), - BigInt(eip3009Payload.authorization.value), - BigInt(eip3009Payload.authorization.validAfter), - BigInt(eip3009Payload.authorization.validBefore), - eip3009Payload.authorization.nonce, - (parsedSig.v as number | undefined) || parsedSig.yParity, - parsedSig.r, - parsedSig.s, - ], - }); - } else { - // For smart wallets, use the bytes signature overload - tx = await signer.writeContract({ - address: getAddress(requirements.asset), - abi: eip3009ABI, - functionName: "transferWithAuthorization", - args: [ - getAddress(eip3009Payload.authorization.from), - getAddress(eip3009Payload.authorization.to), - BigInt(eip3009Payload.authorization.value), - BigInt(eip3009Payload.authorization.validAfter), - BigInt(eip3009Payload.authorization.validBefore), - eip3009Payload.authorization.nonce, - signature, - ], - }); - } + const tx = await executeTransferWithAuthorization( + signer, + getAddress(requirements.asset), + eip3009Payload, + ); // Wait for transaction confirmation const receipt = await signer.waitForTransactionReceipt({ hash: tx }); @@ -320,7 +305,7 @@ export async function settleEIP3009( if (receipt.status !== "success") { return { success: false, - errorReason: "invalid_transaction_state", + errorReason: Errors.ErrTransactionFailed, transaction: tx, network: payload.accepted.network, payer, @@ -336,7 +321,7 @@ export async function settleEIP3009( } catch { return { success: false, - errorReason: "transaction_failed", + errorReason: Errors.ErrTransactionFailed, transaction: "", network: payload.accepted.network, payer, diff --git a/typescript/packages/mechanisms/evm/src/exact/facilitator/erc20approval.ts b/typescript/packages/mechanisms/evm/src/exact/facilitator/erc20approval.ts index e11d1a933b..eda6a047d7 100644 --- a/typescript/packages/mechanisms/evm/src/exact/facilitator/erc20approval.ts +++ b/typescript/packages/mechanisms/evm/src/exact/facilitator/erc20approval.ts @@ -1,154 +1,2 @@ -import { - getAddress, - parseTransaction, - decodeFunctionData, - recoverTransactionAddress, - type TransactionSerialized, -} from "viem"; -import type { VerifyResponse } from "@x402/core/types"; -import { - validateErc20ApprovalGasSponsoringInfo, - type Erc20ApprovalGasSponsoringInfo, -} from "@x402/extensions"; -import { PERMIT2_ADDRESS, erc20ApproveAbi } from "../../constants"; -import { - ErrErc20ApprovalInvalidFormat, - ErrErc20ApprovalFromMismatch, - ErrErc20ApprovalAssetMismatch, - ErrErc20ApprovalSpenderNotPermit2, - ErrErc20ApprovalTxWrongTarget, - ErrErc20ApprovalTxWrongSelector, - ErrErc20ApprovalTxWrongSpender, - ErrErc20ApprovalTxInvalidCalldata, - ErrErc20ApprovalTxSignerMismatch, - ErrErc20ApprovalTxInvalidSignature, - ErrErc20ApprovalTxParseFailed, -} from "./errors"; - -/** The approve(address,uint256) function selector */ -const APPROVE_SELECTOR = "0x095ea7b3"; - -/** - * Validates ERC-20 approval extension data for a Permit2 payment. - * - * Performs comprehensive validation: - * - Format validation via validateErc20ApprovalGasSponsoringInfo (JSON Schema) - * - `from` matches payer - * - `asset` matches token - * - `spender` is PERMIT2_ADDRESS - * - Transaction `to` matches token address - * - Transaction calldata is a valid approve(PERMIT2_ADDRESS, ...) call - * - Recovered transaction signer matches `from` - * - * @param info - The ERC-20 approval gas sponsoring info - * @param payer - The expected payer address - * @param tokenAddress - The expected token address - * @returns Validation result with invalidReason and invalidMessage on failure - */ -export async function validateErc20ApprovalForPayment( - info: Erc20ApprovalGasSponsoringInfo, - payer: `0x${string}`, - tokenAddress: `0x${string}`, -): Promise> { - if (!validateErc20ApprovalGasSponsoringInfo(info)) { - return { - isValid: false, - invalidReason: ErrErc20ApprovalInvalidFormat, - invalidMessage: "ERC-20 approval extension info failed schema validation", - }; - } - - if (getAddress(info.from) !== getAddress(payer)) { - return { - isValid: false, - invalidReason: ErrErc20ApprovalFromMismatch, - invalidMessage: `Expected from=${payer}, got ${info.from}`, - }; - } - - if (getAddress(info.asset) !== tokenAddress) { - return { - isValid: false, - invalidReason: ErrErc20ApprovalAssetMismatch, - invalidMessage: `Expected asset=${tokenAddress}, got ${info.asset}`, - }; - } - - if (getAddress(info.spender) !== getAddress(PERMIT2_ADDRESS)) { - return { - isValid: false, - invalidReason: ErrErc20ApprovalSpenderNotPermit2, - invalidMessage: `Expected spender=${PERMIT2_ADDRESS}, got ${info.spender}`, - }; - } - - try { - const serializedTx = info.signedTransaction as TransactionSerialized; - const tx = parseTransaction(serializedTx); - - if (!tx.to || getAddress(tx.to) !== tokenAddress) { - return { - isValid: false, - invalidReason: ErrErc20ApprovalTxWrongTarget, - invalidMessage: `Transaction targets ${tx.to ?? "null"}, expected ${tokenAddress}`, - }; - } - - const data = tx.data ?? "0x"; - if (!data.startsWith(APPROVE_SELECTOR)) { - return { - isValid: false, - invalidReason: ErrErc20ApprovalTxWrongSelector, - invalidMessage: `Transaction calldata does not start with approve() selector ${APPROVE_SELECTOR}`, - }; - } - - try { - const decoded = decodeFunctionData({ - abi: erc20ApproveAbi, - data: data as `0x${string}`, - }); - const calldataSpender = getAddress(decoded.args[0] as `0x${string}`); - if (calldataSpender !== getAddress(PERMIT2_ADDRESS)) { - return { - isValid: false, - invalidReason: ErrErc20ApprovalTxWrongSpender, - invalidMessage: `approve() spender is ${calldataSpender}, expected Permit2 ${PERMIT2_ADDRESS}`, - }; - } - } catch { - return { - isValid: false, - invalidReason: ErrErc20ApprovalTxInvalidCalldata, - invalidMessage: "Failed to decode approve() calldata from the signed transaction", - }; - } - - try { - const recoveredAddress = await recoverTransactionAddress({ - serializedTransaction: serializedTx, - }); - if (getAddress(recoveredAddress) !== getAddress(payer)) { - return { - isValid: false, - invalidReason: ErrErc20ApprovalTxSignerMismatch, - invalidMessage: `Transaction signed by ${recoveredAddress}, expected payer ${payer}`, - }; - } - } catch { - return { - isValid: false, - invalidReason: ErrErc20ApprovalTxInvalidSignature, - invalidMessage: "Failed to recover signer from the signed transaction", - }; - } - } catch { - return { - isValid: false, - invalidReason: ErrErc20ApprovalTxParseFailed, - invalidMessage: "Failed to parse the signed transaction", - }; - } - - return { isValid: true }; -} +// Re-export from shared for backward compatibility +export { validateErc20ApprovalForPayment } from "../../shared/erc20approval"; diff --git a/typescript/packages/mechanisms/evm/src/exact/facilitator/errors.ts b/typescript/packages/mechanisms/evm/src/exact/facilitator/errors.ts index 9b0aabf166..2573990747 100644 --- a/typescript/packages/mechanisms/evm/src/exact/facilitator/errors.ts +++ b/typescript/packages/mechanisms/evm/src/exact/facilitator/errors.ts @@ -5,19 +5,37 @@ * go/mechanisms/evm/exact/facilitator/errors.go to maintain cross-SDK parity. */ -// EIP-3009 verify errors export const ErrInvalidScheme = "invalid_exact_evm_scheme"; export const ErrNetworkMismatch = "invalid_exact_evm_network_mismatch"; +export const ErrMissingEip712Domain = "invalid_exact_evm_missing_eip712_domain"; +export const ErrRecipientMismatch = "invalid_exact_evm_recipient_mismatch"; +export const ErrInvalidSignature = "invalid_exact_evm_signature"; +export const ErrValidBeforeExpired = "invalid_exact_evm_payload_authorization_valid_before"; +export const ErrValidAfterInFuture = "invalid_exact_evm_payload_authorization_valid_after"; +export const ErrInvalidAuthorizationValue = "invalid_exact_evm_authorization_value"; +export const ErrUndeployedSmartWallet = "invalid_exact_evm_payload_undeployed_smart_wallet"; +export const ErrTransactionFailed = "invalid_exact_evm_transaction_failed"; + +// EIP-3009 verify errors +export const ErrEip3009TokenNameMismatch = "invalid_exact_evm_token_name_mismatch"; +export const ErrEip3009TokenVersionMismatch = "invalid_exact_evm_token_version_mismatch"; +export const ErrEip3009NotSupported = "invalid_exact_evm_eip3009_not_supported"; +export const ErrEip3009NonceAlreadyUsed = "invalid_exact_evm_nonce_already_used"; +export const ErrEip3009InsufficientBalance = "invalid_exact_evm_insufficient_balance"; +export const ErrEip3009SimulationFailed = "invalid_exact_evm_transaction_simulation_failed"; // Permit2 verify errors export const ErrPermit2InvalidSpender = "invalid_permit2_spender"; export const ErrPermit2RecipientMismatch = "invalid_permit2_recipient_mismatch"; export const ErrPermit2DeadlineExpired = "permit2_deadline_expired"; export const ErrPermit2NotYetValid = "permit2_not_yet_valid"; -export const ErrPermit2InsufficientAmount = "permit2_insufficient_amount"; +export const ErrPermit2AmountMismatch = "permit2_amount_mismatch"; export const ErrPermit2TokenMismatch = "permit2_token_mismatch"; export const ErrPermit2InvalidSignature = "invalid_permit2_signature"; export const ErrPermit2AllowanceRequired = "permit2_allowance_required"; +export const ErrPermit2SimulationFailed = "permit2_simulation_failed"; +export const ErrPermit2InsufficientBalance = "permit2_insufficient_balance"; +export const ErrPermit2ProxyNotDeployed = "permit2_proxy_not_deployed"; // Permit2 settle errors (from contract reverts) export const ErrPermit2InvalidAmount = "permit2_invalid_amount"; @@ -39,3 +57,15 @@ export const ErrErc20ApprovalTxInvalidCalldata = "erc20_approval_tx_invalid_call export const ErrErc20ApprovalTxSignerMismatch = "erc20_approval_tx_signer_mismatch"; export const ErrErc20ApprovalTxInvalidSignature = "erc20_approval_tx_invalid_signature"; export const ErrErc20ApprovalTxParseFailed = "erc20_approval_tx_parse_failed"; +export const ErrErc20ApprovalTxFailed = "erc20_approval_tx_failed"; + +// EIP-2612 gas sponsoring verify errors +export const ErrInvalidEip2612ExtensionFormat = "invalid_eip2612_extension_format"; +export const ErrEip2612FromMismatch = "eip2612_from_mismatch"; +export const ErrEip2612AssetMismatch = "eip2612_asset_mismatch"; +export const ErrEip2612SpenderNotPermit2 = "eip2612_spender_not_permit2"; +export const ErrEip2612DeadlineExpired = "eip2612_deadline_expired"; + +// Shared settle errors +export const ErrUnsupportedPayloadType = "unsupported_payload_type"; +export const ErrInvalidTransactionState = "invalid_transaction_state"; diff --git a/typescript/packages/mechanisms/evm/src/exact/facilitator/permit2.ts b/typescript/packages/mechanisms/evm/src/exact/facilitator/permit2.ts index 9a30840fd9..ad74c0b217 100644 --- a/typescript/packages/mechanisms/evm/src/exact/facilitator/permit2.ts +++ b/typescript/packages/mechanisms/evm/src/exact/facilitator/permit2.ts @@ -7,34 +7,58 @@ import { } from "@x402/core/types"; import { extractEip2612GasSponsoringInfo, - validateEip2612GasSponsoringInfo, extractErc20ApprovalGasSponsoringInfo, - ERC20_APPROVAL_GAS_SPONSORING, + ERC20_APPROVAL_GAS_SPONSORING_KEY, + resolveErc20ApprovalExtensionSigner, + type Eip2612GasSponsoringInfo, type Erc20ApprovalGasSponsoringFacilitatorExtension, -} from "@x402/extensions"; -import type { Eip2612GasSponsoringInfo } from "@x402/extensions"; -import { getAddress } from "viem"; + type Erc20ApprovalGasSponsoringSigner, +} from "../extensions"; +import { getAddress, encodeFunctionData } from "viem"; import { - eip3009ABI, PERMIT2_ADDRESS, permit2WitnessTypes, x402ExactPermit2ProxyABI, x402ExactPermit2ProxyAddress, - erc20AllowanceAbi, } from "../../constants"; -import { - ErrPermit2612AmountMismatch, - ErrPermit2InvalidAmount, - ErrPermit2InvalidDestination, - ErrPermit2InvalidNonce, - ErrPermit2InvalidOwner, - ErrPermit2InvalidSignature, - ErrPermit2PaymentTooEarly, -} from "./errors"; +import * as Errors from "./errors"; import { FacilitatorEvmSigner } from "../../signer"; import { ExactPermit2Payload } from "../../types"; import { getEvmChainId } from "../../utils"; import { validateErc20ApprovalForPayment } from "./erc20approval"; +import { + simulatePermit2Settle, + simulatePermit2SettleWithPermit, + simulatePermit2SettleWithErc20Approval, + diagnosePermit2SimulationFailure, + checkPermit2Prerequisites, + validateEip2612PermitForPayment, + buildExactPermit2SettleArgs, + splitEip2612Signature, + waitAndReturnSettleResponse, + mapSettleError, + type Permit2ProxyConfig, +} from "../../shared/permit2"; + +const exactProxyConfig: Permit2ProxyConfig = { + proxyAddress: x402ExactPermit2ProxyAddress, + proxyABI: x402ExactPermit2ProxyABI, +}; + +export interface VerifyPermit2Options { + /** Run onchain simulation. Defaults to true. */ + simulate?: boolean; +} + +export interface Permit2FacilitatorConfig { + /** + * If enabled, simulates transaction before settling. Defaults to false, + * i.e. only simulate during verify. + * + * @default false + */ + simulateInSettle?: boolean; +} /** * Verifies a Permit2 payment payload. @@ -49,6 +73,7 @@ import { validateErc20ApprovalForPayment } from "./erc20approval"; * @param requirements - The payment requirements * @param permit2Payload - The Permit2 specific payload * @param context - Optional facilitator context for extension-provided capabilities + * @param options - Optional verification options (e.g. simulate) * @returns Promise resolving to verification response */ export async function verifyPermit2( @@ -57,13 +82,14 @@ export async function verifyPermit2( requirements: PaymentRequirements, permit2Payload: ExactPermit2Payload, context?: FacilitatorContext, + options?: VerifyPermit2Options, ): Promise { const payer = permit2Payload.permit2Authorization.from; if (payload.accepted.scheme !== "exact" || requirements.scheme !== "exact") { return { isValid: false, - invalidReason: "unsupported_scheme", + invalidReason: Errors.ErrUnsupportedPayloadType, payer, }; } @@ -71,7 +97,7 @@ export async function verifyPermit2( if (payload.accepted.network !== requirements.network) { return { isValid: false, - invalidReason: "network_mismatch", + invalidReason: Errors.ErrNetworkMismatch, payer, }; } @@ -85,7 +111,7 @@ export async function verifyPermit2( ) { return { isValid: false, - invalidReason: "invalid_permit2_spender", + invalidReason: Errors.ErrPermit2InvalidSpender, payer, }; } @@ -95,7 +121,7 @@ export async function verifyPermit2( ) { return { isValid: false, - invalidReason: "invalid_permit2_recipient_mismatch", + invalidReason: Errors.ErrPermit2RecipientMismatch, payer, }; } @@ -104,7 +130,7 @@ export async function verifyPermit2( if (BigInt(permit2Payload.permit2Authorization.deadline) < BigInt(now + 6)) { return { isValid: false, - invalidReason: "permit2_deadline_expired", + invalidReason: Errors.ErrPermit2DeadlineExpired, payer, }; } @@ -112,15 +138,18 @@ export async function verifyPermit2( if (BigInt(permit2Payload.permit2Authorization.witness.validAfter) > BigInt(now)) { return { isValid: false, - invalidReason: "permit2_not_yet_valid", + invalidReason: Errors.ErrPermit2NotYetValid, payer, }; } - if (BigInt(permit2Payload.permit2Authorization.permitted.amount) < BigInt(requirements.amount)) { + // Verify amount exactly matches requirements + if ( + BigInt(permit2Payload.permit2Authorization.permitted.amount) !== BigInt(requirements.amount) + ) { return { isValid: false, - invalidReason: "permit2_insufficient_amount", + invalidReason: Errors.ErrPermit2AmountMismatch, payer, }; } @@ -128,7 +157,7 @@ export async function verifyPermit2( if (getAddress(permit2Payload.permit2Authorization.permitted.token) !== tokenAddress) { return { isValid: false, - invalidReason: "permit2_token_mismatch", + invalidReason: Errors.ErrPermit2TokenMismatch, payer, }; } @@ -156,137 +185,134 @@ export async function verifyPermit2( }, }; + // Verify signature + // Note: verifyTypedData is implementation-dependent and pluggable on FacilitatorEvmSigner + // Some implementations only do EOA-style ECDSA recovery (e.g. viem/utils verifyTypedData, ethers.verifyTypedData) + // Viem's publicClient.verifyTypedData supports EOA and Smart Contract Account (ERC-1271 / ERC-6492) signature verification + let signatureValid = false; try { - const isValid = await signer.verifyTypedData({ + signatureValid = await signer.verifyTypedData({ address: payer, ...permit2TypedData, signature: permit2Payload.signature, }); - - if (!isValid) { - return { - isValid: false, - invalidReason: "invalid_permit2_signature", - payer, - }; - } } catch { - return { - isValid: false, - invalidReason: "invalid_permit2_signature", - payer, - }; + signatureValid = false; } - // Check Permit2 allowance — if insufficient, try gas sponsoring extensions - const allowanceResult = await _verifyPermit2Allowance( - signer, - payload, - requirements, - payer, - tokenAddress, - context, - ); - if (allowanceResult) { - return allowanceResult; - } + if (!signatureValid) { + // Check if the payer is a deployed smart contract + const bytecode = await signer.getCode({ address: payer }); + const isDeployedContract = bytecode && bytecode !== "0x"; - try { - const balance = (await signer.readContract({ - address: tokenAddress, - abi: eip3009ABI, - functionName: "balanceOf", - args: [payer], - })) as bigint; - - if (balance < BigInt(requirements.amount)) { + if (!isDeployedContract) { return { isValid: false, - invalidReason: "insufficient_funds", - invalidMessage: `Insufficient funds to complete the payment. Required: ${requirements.amount} ${requirements.asset}, Available: ${balance.toString()} ${requirements.asset}. Please add funds to your wallet and try again.`, + invalidReason: Errors.ErrPermit2InvalidSignature, payer, }; } - } catch { - // If we can't check balance, continue + // Deployed smart contract: fall through to simulation } - return { - isValid: true, - invalidReason: undefined, - payer, - }; -} + // If simulation is disabled, return early + if (options?.simulate === false) { + return { isValid: true, invalidReason: undefined, payer }; + } -/** - * Checks Permit2 allowance and validates gas sponsoring extensions if allowance is insufficient. - * - * @param signer - The facilitator signer for on-chain reads - * @param payload - The payment payload - * @param requirements - The payment requirements - * @param payer - The payer address - * @param tokenAddress - The token contract address - * @param context - Optional facilitator context for extension lookup - * @returns A VerifyResponse if verification should stop (failure), or null to continue - */ -async function _verifyPermit2Allowance( - signer: FacilitatorEvmSigner, - payload: PaymentPayload, - requirements: PaymentRequirements, - payer: `0x${string}`, - tokenAddress: `0x${string}`, - context?: FacilitatorContext, -): Promise { - try { - const allowance = (await signer.readContract({ - address: tokenAddress, - abi: erc20AllowanceAbi, - functionName: "allowance", - args: [payer, PERMIT2_ADDRESS], - })) as bigint; - - if (allowance >= BigInt(requirements.amount)) { - return null; // Sufficient allowance, continue verification + // Branch: EIP-2612 gas sponsoring (atomic settleWithPermit via contract) + const eip2612Info = extractEip2612GasSponsoringInfo(payload); + if (eip2612Info) { + const fieldResult = validateEip2612PermitForPayment(eip2612Info, payer, tokenAddress); + if (!fieldResult.isValid) { + return { isValid: false, invalidReason: fieldResult.invalidReason!, payer }; } - // Allowance insufficient — try EIP-2612 gas sponsoring first - const eip2612Info = extractEip2612GasSponsoringInfo(payload); - if (eip2612Info) { - const result = validateEip2612PermitForPayment(eip2612Info, payer, tokenAddress); - if (!result.isValid) { - return { isValid: false, invalidReason: result.invalidReason!, payer }; - } - return null; // EIP-2612 is valid, allowance will be set atomically during settlement + const exactSettleArgs = buildExactPermit2SettleArgs(permit2Payload); + const simOk = await simulatePermit2SettleWithPermit( + exactProxyConfig, + signer, + exactSettleArgs, + eip2612Info, + ); + if (!simOk) { + return diagnosePermit2SimulationFailure( + exactProxyConfig, + signer, + tokenAddress, + permit2Payload, + requirements.amount, + ); } - // Try ERC-20 approval gas sponsoring as fallback - const erc20GasSponsorshipExtension = - context?.getExtension( - ERC20_APPROVAL_GAS_SPONSORING.key, + return { isValid: true, invalidReason: undefined, payer }; + } + + // Branch: ERC-20 approval gas sponsoring (broadcast approval + settle via extension signer) + const erc20GasSponsorshipExtension = + context?.getExtension( + ERC20_APPROVAL_GAS_SPONSORING_KEY, + ); + if (erc20GasSponsorshipExtension) { + const erc20Info = extractErc20ApprovalGasSponsoringInfo(payload); + if (erc20Info) { + const fieldResult = await validateErc20ApprovalForPayment(erc20Info, payer, tokenAddress); + if (!fieldResult.isValid) { + return { isValid: false, invalidReason: fieldResult.invalidReason!, payer }; + } + + const extensionSigner = resolveErc20ApprovalExtensionSigner( + erc20GasSponsorshipExtension, + requirements.network, ); - if (erc20GasSponsorshipExtension) { - const erc20Info = extractErc20ApprovalGasSponsoringInfo(payload); - if (erc20Info) { - const result = await validateErc20ApprovalForPayment(erc20Info, payer, tokenAddress); - if (!result.isValid) { - return { isValid: false, invalidReason: result.invalidReason!, payer }; + + if (extensionSigner?.simulateTransactions) { + const simOk = await simulatePermit2SettleWithErc20Approval( + exactProxyConfig, + extensionSigner, + buildExactPermit2SettleArgs(permit2Payload), + erc20Info, + ); + if (!simOk) { + return diagnosePermit2SimulationFailure( + exactProxyConfig, + signer, + tokenAddress, + permit2Payload, + requirements.amount, + ); } - return null; // ERC-20 approval is valid, will be broadcast before settlement + return { isValid: true, invalidReason: undefined, payer }; } - } - return { isValid: false, invalidReason: "permit2_allowance_required", payer }; - } catch { - // If allowance check fails, validate extensions if present; otherwise proceed optimistically - const eip2612Info = extractEip2612GasSponsoringInfo(payload); - if (eip2612Info) { - const result = validateEip2612PermitForPayment(eip2612Info, payer, tokenAddress); - if (!result.isValid) { - return { isValid: false, invalidReason: result.invalidReason!, payer }; - } + // Fallback to prerequisite-only check if simulateTransactions is not available + return checkPermit2Prerequisites( + exactProxyConfig, + signer, + tokenAddress, + payer, + requirements.amount, + ); } - return null; } + + // Branch: standard settle (allowance already on-chain) + const simOk = await simulatePermit2Settle( + exactProxyConfig, + signer, + buildExactPermit2SettleArgs(permit2Payload), + ); + if (!simOk) { + return diagnosePermit2SimulationFailure( + exactProxyConfig, + signer, + tokenAddress, + permit2Payload, + requirements.amount, + ); + } + + return { isValid: true, invalidReason: undefined, payer }; } /** @@ -301,6 +327,7 @@ async function _verifyPermit2Allowance( * @param requirements - The payment requirements * @param permit2Payload - The Permit2 specific payload * @param context - Optional facilitator context for extension-provided capabilities + * @param config - Optional facilitator config (simulateInSettle) * @returns Promise resolving to settlement response */ export async function settlePermit2( @@ -309,16 +336,19 @@ export async function settlePermit2( requirements: PaymentRequirements, permit2Payload: ExactPermit2Payload, context?: FacilitatorContext, + config?: Permit2FacilitatorConfig, ): Promise { const payer = permit2Payload.permit2Authorization.from; - const valid = await verifyPermit2(signer, payload, requirements, permit2Payload, context); + const valid = await verifyPermit2(signer, payload, requirements, permit2Payload, context, { + simulate: config?.simulateInSettle ?? false, + }); if (!valid.isValid) { return { success: false, network: payload.accepted.network, transaction: "", - errorReason: valid.invalidReason ?? "invalid_scheme", + errorReason: valid.invalidReason ?? Errors.ErrInvalidScheme, payer, }; } @@ -326,7 +356,7 @@ export async function settlePermit2( // Branch: EIP-2612 gas sponsoring (atomic settleWithPermit via contract) const eip2612Info = extractEip2612GasSponsoringInfo(payload); if (eip2612Info) { - return _settlePermit2WithEIP2612(signer, payload, permit2Payload, eip2612Info); + return settlePermit2WithEIP2612(exactProxyConfig, signer, payload, permit2Payload, eip2612Info); } // Branch: ERC-20 approval gas sponsoring (broadcast approval + settle via extension signer) @@ -334,11 +364,16 @@ export async function settlePermit2( if (erc20Info) { const erc20GasSponsorshipExtension = context?.getExtension( - ERC20_APPROVAL_GAS_SPONSORING.key, + ERC20_APPROVAL_GAS_SPONSORING_KEY, ); - if (erc20GasSponsorshipExtension?.signer) { - return _settlePermit2WithERC20Approval( - erc20GasSponsorshipExtension.signer, + const extensionSigner = resolveErc20ApprovalExtensionSigner( + erc20GasSponsorshipExtension, + payload.accepted.network, + ); + if (extensionSigner) { + return settlePermit2WithERC20Approval( + exactProxyConfig, + extensionSigner, payload, permit2Payload, erc20Info, @@ -347,19 +382,25 @@ export async function settlePermit2( } // Branch: standard settle (allowance already on-chain) - return _settlePermit2Direct(signer, payload, permit2Payload); + return settlePermit2Direct(exactProxyConfig, signer, payload, permit2Payload); } +// --------------------------------------------------------------------------- +// Exact-only settle helpers (not shared — upto has its own implementations) +// --------------------------------------------------------------------------- + /** - * Settles via settleWithPermit — includes the EIP-2612 permit atomically in one tx. + * Settles a Permit2 payment via settleWithPermit, including the EIP-2612 permit atomically. * - * @param signer - The base facilitator signer - * @param payload - The payment payload - * @param permit2Payload - The Permit2 specific payload + * @param config - The proxy contract configuration (address and ABI) + * @param signer - The facilitator signer for contract writes + * @param payload - The payment payload for network info + * @param permit2Payload - The Permit2 payload with authorization and signature * @param eip2612Info - The EIP-2612 gas sponsoring info from the payload extension - * @returns Promise resolving to settlement response + * @returns Promise resolving to a settlement response */ -async function _settlePermit2WithEIP2612( +async function settlePermit2WithEIP2612( + config: Permit2ProxyConfig, signer: FacilitatorEvmSigner, payload: PaymentPayload, permit2Payload: ExactPermit2Payload, @@ -370,8 +411,8 @@ async function _settlePermit2WithEIP2612( const { v, r, s } = splitEip2612Signature(eip2612Info.signature); const tx = await signer.writeContract({ - address: x402ExactPermit2ProxyAddress, - abi: x402ExactPermit2ProxyABI, + address: config.proxyAddress, + abi: config.proxyABI, functionName: "settleWithPermit", args: [ { @@ -381,42 +422,30 @@ async function _settlePermit2WithEIP2612( s, v, }, - { - permitted: { - token: getAddress(permit2Payload.permit2Authorization.permitted.token), - amount: BigInt(permit2Payload.permit2Authorization.permitted.amount), - }, - nonce: BigInt(permit2Payload.permit2Authorization.nonce), - deadline: BigInt(permit2Payload.permit2Authorization.deadline), - }, - getAddress(payer), - { - to: getAddress(permit2Payload.permit2Authorization.witness.to), - validAfter: BigInt(permit2Payload.permit2Authorization.witness.validAfter), - }, - permit2Payload.signature, + ...buildExactPermit2SettleArgs(permit2Payload), ], }); - return _waitAndReturn(signer, tx, payload, payer); + return waitAndReturnSettleResponse(signer, tx, payload, payer); } catch (error) { - return _mapSettleError(error, payload, payer); + return mapSettleError(error, payload, payer); } } /** - * Broadcasts the pre-signed ERC-20 approve tx then settles via the extension signer. - * Both operations use the extension signer, enabling atomic bundling by production implementations. + * Settles a Permit2 payment using an ERC-20 approval gas sponsoring extension. * - * @param extensionSigner - The extension signer with sendRawTransaction + writeContract - * @param payload - The payment payload - * @param permit2Payload - The Permit2 specific payload + * @param config - The proxy contract configuration (address and ABI) + * @param extensionSigner - The extension signer with sendTransactions capability + * @param payload - The payment payload for network info + * @param permit2Payload - The Permit2 payload with authorization and signature * @param erc20Info - Object containing the signed approval transaction - * @param erc20Info.signedTransaction - The RLP-encoded signed EIP-1559 approval tx - * @returns Promise resolving to settlement response + * @param erc20Info.signedTransaction - The RLP-encoded signed ERC-20 approve transaction + * @returns Promise resolving to a settlement response */ -async function _settlePermit2WithERC20Approval( - extensionSigner: Erc20ApprovalGasSponsoringFacilitatorExtension["signer"] & {}, +async function settlePermit2WithERC20Approval( + config: Permit2ProxyConfig, + extensionSigner: Erc20ApprovalGasSponsoringSigner, payload: PaymentPayload, permit2Payload: ExactPermit2Payload, erc20Info: { signedTransaction: string }, @@ -424,61 +453,35 @@ async function _settlePermit2WithERC20Approval( const payer = permit2Payload.permit2Authorization.from; try { - const approvalTxHash = await extensionSigner.sendRawTransaction({ - serializedTransaction: erc20Info.signedTransaction as `0x${string}`, - }); - - const approvalReceipt = await extensionSigner.waitForTransactionReceipt({ - hash: approvalTxHash, - }); - - if (approvalReceipt.status !== "success") { - return { - success: false, - errorReason: "erc20_approval_tx_failed", - transaction: approvalTxHash, - network: payload.accepted.network, - payer, - }; - } - - const tx = await extensionSigner.writeContract({ - address: x402ExactPermit2ProxyAddress, - abi: x402ExactPermit2ProxyABI, + const settleData = encodeFunctionData({ + abi: config.proxyABI, functionName: "settle", - args: [ - { - permitted: { - token: getAddress(permit2Payload.permit2Authorization.permitted.token), - amount: BigInt(permit2Payload.permit2Authorization.permitted.amount), - }, - nonce: BigInt(permit2Payload.permit2Authorization.nonce), - deadline: BigInt(permit2Payload.permit2Authorization.deadline), - }, - getAddress(payer), - { - to: getAddress(permit2Payload.permit2Authorization.witness.to), - validAfter: BigInt(permit2Payload.permit2Authorization.witness.validAfter), - }, - permit2Payload.signature, - ], + args: buildExactPermit2SettleArgs(permit2Payload), }); - return _waitAndReturn(extensionSigner, tx, payload, payer); + const txHashes = await extensionSigner.sendTransactions([ + erc20Info.signedTransaction as `0x${string}`, + { to: config.proxyAddress, data: settleData, gas: BigInt(300_000) }, + ]); + + const settleTxHash = txHashes[txHashes.length - 1]; + return waitAndReturnSettleResponse(extensionSigner, settleTxHash, payload, payer); } catch (error) { - return _mapSettleError(error, payload, payer); + return mapSettleError(error, payload, payer); } } /** - * Standard Permit2 settle — allowance is already on-chain. + * Settles a Permit2 payment directly when Permit2 allowance is already on-chain. * - * @param signer - The base facilitator signer - * @param payload - The payment payload - * @param permit2Payload - The Permit2 specific payload - * @returns Promise resolving to settlement response + * @param config - The proxy contract configuration (address and ABI) + * @param signer - The facilitator signer for contract writes + * @param payload - The payment payload for network info + * @param permit2Payload - The Permit2 payload with authorization and signature + * @returns Promise resolving to a settlement response */ -async function _settlePermit2Direct( +async function settlePermit2Direct( + config: Permit2ProxyConfig, signer: FacilitatorEvmSigner, payload: PaymentPayload, permit2Payload: ExactPermit2Payload, @@ -486,170 +489,14 @@ async function _settlePermit2Direct( const payer = permit2Payload.permit2Authorization.from; try { const tx = await signer.writeContract({ - address: x402ExactPermit2ProxyAddress, - abi: x402ExactPermit2ProxyABI, + address: config.proxyAddress, + abi: config.proxyABI, functionName: "settle", - args: [ - { - permitted: { - token: getAddress(permit2Payload.permit2Authorization.permitted.token), - amount: BigInt(permit2Payload.permit2Authorization.permitted.amount), - }, - nonce: BigInt(permit2Payload.permit2Authorization.nonce), - deadline: BigInt(permit2Payload.permit2Authorization.deadline), - }, - getAddress(payer), - { - to: getAddress(permit2Payload.permit2Authorization.witness.to), - validAfter: BigInt(permit2Payload.permit2Authorization.witness.validAfter), - }, - permit2Payload.signature, - ], + args: buildExactPermit2SettleArgs(permit2Payload), }); - return _waitAndReturn(signer, tx, payload, payer); + return waitAndReturnSettleResponse(signer, tx, payload, payer); } catch (error) { - return _mapSettleError(error, payload, payer); - } -} - -/** - * Waits for tx receipt and returns the appropriate SettleResponse. - * - * @param signer - Signer with waitForTransactionReceipt capability - * @param tx - The transaction hash to wait for - * @param payload - The payment payload (for network info) - * @param payer - The payer address - * @returns Promise resolving to settlement response - */ -async function _waitAndReturn( - signer: Pick, - tx: `0x${string}`, - payload: PaymentPayload, - payer: `0x${string}`, -): Promise { - const receipt = await signer.waitForTransactionReceipt({ hash: tx }); - - if (receipt.status !== "success") { - return { - success: false, - errorReason: "invalid_transaction_state", - transaction: tx, - network: payload.accepted.network, - payer, - }; + return mapSettleError(error, payload, payer); } - - return { - success: true, - transaction: tx, - network: payload.accepted.network, - payer, - }; -} - -/** - * Maps contract revert errors to structured SettleResponse error reasons. - * - * @param error - The caught error - * @param payload - The payment payload (for network info) - * @param payer - The payer address - * @returns A failed SettleResponse with mapped error reason - */ -function _mapSettleError( - error: unknown, - payload: PaymentPayload, - payer: `0x${string}`, -): SettleResponse { - let errorReason = "transaction_failed"; - if (error instanceof Error) { - const message = error.message; - if (message.includes("Permit2612AmountMismatch")) { - errorReason = ErrPermit2612AmountMismatch; - } else if (message.includes("InvalidAmount")) { - errorReason = ErrPermit2InvalidAmount; - } else if (message.includes("InvalidDestination")) { - errorReason = ErrPermit2InvalidDestination; - } else if (message.includes("InvalidOwner")) { - errorReason = ErrPermit2InvalidOwner; - } else if (message.includes("PaymentTooEarly")) { - errorReason = ErrPermit2PaymentTooEarly; - } else if (message.includes("InvalidSignature") || message.includes("SignatureExpired")) { - errorReason = ErrPermit2InvalidSignature; - } else if (message.includes("InvalidNonce")) { - errorReason = ErrPermit2InvalidNonce; - } else { - errorReason = `transaction_failed: ${message.slice(0, 500)}`; - } - } - return { - success: false, - errorReason, - transaction: "", - network: payload.accepted.network, - payer, - }; -} - -/** - * Validates EIP-2612 permit extension data for a Permit2 payment. - * - * @param info - The EIP-2612 gas sponsoring info - * @param payer - The expected payer address - * @param tokenAddress - The expected token address - * @returns Validation result with optional invalidReason - */ -function validateEip2612PermitForPayment( - info: Eip2612GasSponsoringInfo, - payer: `0x${string}`, - tokenAddress: `0x${string}`, -): { isValid: boolean; invalidReason?: string } { - if (!validateEip2612GasSponsoringInfo(info)) { - return { isValid: false, invalidReason: "invalid_eip2612_extension_format" }; - } - - if (getAddress(info.from as `0x${string}`) !== getAddress(payer)) { - return { isValid: false, invalidReason: "eip2612_from_mismatch" }; - } - - if (getAddress(info.asset as `0x${string}`) !== tokenAddress) { - return { isValid: false, invalidReason: "eip2612_asset_mismatch" }; - } - - if (getAddress(info.spender as `0x${string}`) !== getAddress(PERMIT2_ADDRESS)) { - return { isValid: false, invalidReason: "eip2612_spender_not_permit2" }; - } - - const now = Math.floor(Date.now() / 1000); - if (BigInt(info.deadline) < BigInt(now + 6)) { - return { isValid: false, invalidReason: "eip2612_deadline_expired" }; - } - - return { isValid: true }; -} - -/** - * Splits a 65-byte EIP-2612 signature into v, r, s components. - * - * @param signature - The hex-encoded 65-byte signature - * @returns Object with v (uint8), r (bytes32), s (bytes32) - */ -function splitEip2612Signature(signature: string): { - v: number; - r: `0x${string}`; - s: `0x${string}`; -} { - const sig = signature.startsWith("0x") ? signature.slice(2) : signature; - - if (sig.length !== 130) { - throw new Error( - `invalid EIP-2612 signature length: expected 65 bytes (130 hex chars), got ${sig.length / 2} bytes`, - ); - } - - const r = `0x${sig.slice(0, 64)}` as `0x${string}`; - const s = `0x${sig.slice(64, 128)}` as `0x${string}`; - const v = parseInt(sig.slice(128, 130), 16); - - return { v, r, s }; } diff --git a/typescript/packages/mechanisms/evm/src/exact/facilitator/register.ts b/typescript/packages/mechanisms/evm/src/exact/facilitator/register.ts index 61224d7a0d..9ab056175d 100644 --- a/typescript/packages/mechanisms/evm/src/exact/facilitator/register.ts +++ b/typescript/packages/mechanisms/evm/src/exact/facilitator/register.ts @@ -27,6 +27,13 @@ export interface EvmFacilitatorConfig { * @default false */ deployERC4337WithEIP6492?: boolean; + + /** + * If enabled, reruns on-chain simulation during settle's re-verify. + * + * @default false + */ + simulateInSettle?: boolean; } /** @@ -70,6 +77,7 @@ export function registerExactEvmScheme( config.networks, new ExactEvmScheme(config.signer, { deployERC4337WithEIP6492: config.deployERC4337WithEIP6492, + simulateInSettle: config.simulateInSettle, }), ); @@ -78,6 +86,7 @@ export function registerExactEvmScheme( NETWORKS as Network[], new ExactEvmSchemeV1(config.signer, { deployERC4337WithEIP6492: config.deployERC4337WithEIP6492, + simulateInSettle: config.simulateInSettle, }), ); diff --git a/typescript/packages/mechanisms/evm/src/exact/facilitator/scheme.ts b/typescript/packages/mechanisms/evm/src/exact/facilitator/scheme.ts index a4bf984e98..d5bcd5afc6 100644 --- a/typescript/packages/mechanisms/evm/src/exact/facilitator/scheme.ts +++ b/typescript/packages/mechanisms/evm/src/exact/facilitator/scheme.ts @@ -19,6 +19,12 @@ export interface ExactEvmSchemeConfig { * @default false */ deployERC4337WithEIP6492?: boolean; + /** + * If enabled, run on-chain simulation during settle's re-verify. + * + * @default false + */ + simulateInSettle?: boolean; } /** @@ -44,6 +50,7 @@ export class ExactEvmScheme implements SchemeNetworkFacilitator { ) { this.config = { deployERC4337WithEIP6492: config?.deployERC4337WithEIP6492 ?? false, + simulateInSettle: config?.simulateInSettle ?? false, }; } @@ -81,8 +88,9 @@ export class ExactEvmScheme implements SchemeNetworkFacilitator { context?: FacilitatorContext, ): Promise { const rawPayload = payload.payload as ExactEvmPayloadV2; + const isPermit2 = isPermit2Payload(rawPayload); - if (isPermit2Payload(rawPayload)) { + if (isPermit2) { return verifyPermit2(this.signer, payload, requirements, rawPayload, context); } @@ -104,9 +112,12 @@ export class ExactEvmScheme implements SchemeNetworkFacilitator { context?: FacilitatorContext, ): Promise { const rawPayload = payload.payload as ExactEvmPayloadV2; + const isPermit2 = isPermit2Payload(rawPayload); - if (isPermit2Payload(rawPayload)) { - return settlePermit2(this.signer, payload, requirements, rawPayload, context); + if (isPermit2) { + return settlePermit2(this.signer, payload, requirements, rawPayload, context, { + simulateInSettle: this.config.simulateInSettle, + }); } const eip3009Payload: ExactEIP3009Payload = rawPayload; diff --git a/typescript/packages/mechanisms/evm/src/exact/server/DEFAULT_ASSET.md b/typescript/packages/mechanisms/evm/src/exact/server/DEFAULT_ASSET.md index 2852c2f83e..7c03ba892d 100644 --- a/typescript/packages/mechanisms/evm/src/exact/server/DEFAULT_ASSET.md +++ b/typescript/packages/mechanisms/evm/src/exact/server/DEFAULT_ASSET.md @@ -10,24 +10,26 @@ When a server uses `price: "$0.10"` syntax (USD string pricing), x402 needs to k To add support for a new EVM chain, add an entry to the `stablecoins` map in `getDefaultAsset()`: ```typescript -const stablecoins: Record = { +const stablecoins: Record = { "eip155:8453": { address: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", name: "USD Coin", version: "2", - decimals: 6 - }, // Base mainnet USDC + decimals: 6, + }, // Base mainnet USDC (EIP-3009) // Add your chain here: "eip155:YOUR_CHAIN_ID": { address: "0xYOUR_STABLECOIN_ADDRESS", - name: "Token Name", // Must match EIP-712 domain name - version: "1", // Must match EIP-712 domain version - decimals: 6, // Token decimals (typically 6 for USDC) + name: "Token Name", // Must match EIP-712 domain name + version: "1", // Must match EIP-712 domain version + decimals: 6, // Token decimals (typically 6 for USDC) + // assetTransferMethod: "permit2", // Uncomment if token doesn't support EIP-3009 + // supportsEip2612: true, // Set if permit2 token implements EIP-2612 permit() }, }; ``` -### Required Fields +### Fields | Field | Description | |-------|-------------| @@ -35,6 +37,8 @@ const stablecoins: Record **Note**: Default assets should support EIP-3009 for the best user experience (no approval required). Tokens requiring Permit2 can be added via `registerMoneyParser` as shown above. +1. Obtain the correct EIP-712 domain `name` and `version` from the token contract +2. Check whether the token supports EIP-3009 (`transferWithAuthorization`): + - If yes: add the entry without `assetTransferMethod` (EIP-3009 is the default) + - If no: add `assetTransferMethod: "permit2"` to the entry so the client uses Permit2 automatically +3. For permit2 tokens, check whether the token supports EIP-2612 (`permit()`): + - If yes: add `supportsEip2612: true` so clients can use gasless EIP-2612 permits for Permit2 approval + - If no: omit `supportsEip2612` — clients will fall back to ERC-20 approval gas sponsoring +4. Add the entry to `getDefaultAsset()` in `scheme.ts` +5. Submit a PR with the chain name and rationale for the asset selection + +## Cross-SDK Checklist + +When adding a new chain's default asset, update all three SDKs to maintain parity: + +| SDK | File to edit | What to add | +|-----|-------------|-------------| +| **Go** | `go/mechanisms/evm/constants.go` | Entry in `NetworkConfigs` map | +| **TypeScript** | `typescript/packages/mechanisms/evm/src/exact/server/scheme.ts` | Entry in `stablecoins` map inside `getDefaultAsset()` | +| **Python** | `python/x402/mechanisms/evm/constants.py` | Entry in `NETWORK_CONFIGS` dict | + +All three must use: +- The same CAIP-2 network key (e.g., `eip155:YOUR_CHAIN_ID`) +- The same token contract address +- The same EIP-712 domain `name` and `version` +- The same `decimals` value +- The same asset transfer method (EIP-3009 default, or Permit2 if specified) diff --git a/typescript/packages/mechanisms/evm/src/exact/server/scheme.ts b/typescript/packages/mechanisms/evm/src/exact/server/scheme.ts index e81c1bf120..a8fbfbf536 100644 --- a/typescript/packages/mechanisms/evm/src/exact/server/scheme.ts +++ b/typescript/packages/mechanisms/evm/src/exact/server/scheme.ts @@ -6,6 +6,7 @@ import { SchemeNetworkServer, MoneyParser, } from "@x402/core/types"; +import { getDefaultAsset, type ExactDefaultAssetInfo } from "../../shared/defaultAssets"; /** * EVM server implementation for the Exact payment scheme. @@ -39,6 +40,22 @@ export class ExactEvmScheme implements SchemeNetworkServer { return this; } + /** + * Returns the decimal precision of the default stablecoin for the given network. + * Implements the optional AssetDecimalsProvider interface used by resolveSettlementOverrideAmount. + * + * @param _asset - The asset symbol (unused; defaults to the network's default stablecoin) + * @param network - The network to look up the default asset for + * @returns The number of decimal places for the asset + */ + getAssetDecimals(_asset: string, network: Network): number { + try { + return getDefaultAsset(network).decimals; + } catch { + return 6; + } + } + /** * Parses a price into an asset amount. * If price is already an AssetAmount, returns it directly. @@ -129,91 +146,52 @@ export class ExactEvmScheme implements SchemeNetworkServer { } /** - * Default money conversion implementation. - * Converts decimal amount to the default stablecoin on the specified network. + * Converts a numeric dollar amount to an AssetAmount using the default token for the network. * - * @param amount - The decimal amount (e.g., 1.50) - * @param network - The network to use - * @returns The parsed asset amount in the default stablecoin + * @param amount - The dollar amount as a number + * @param network - The target network + * @returns The converted asset amount with token metadata */ private defaultMoneyConversion(amount: number, network: Network): AssetAmount { - const assetInfo = this.getDefaultAsset(network); + const assetInfo: ExactDefaultAssetInfo = getDefaultAsset(network); const tokenAmount = this.convertToTokenAmount(amount.toString(), assetInfo.decimals); + // EIP-3009 tokens always need name/version for their transferWithAuthorization domain. + // Permit2 tokens only need them if the token supports EIP-2612 (for gasless permit signing). + // Omitting name/version for permit2 tokens signals the client to skip EIP-2612 and use + // ERC-20 approval gas sponsoring instead. + const includeEip712Domain = !assetInfo.assetTransferMethod || assetInfo.supportsEip2612; + return { amount: tokenAmount, asset: assetInfo.address, extra: { - name: assetInfo.name, - version: assetInfo.version, + ...(includeEip712Domain && { + name: assetInfo.name, + version: assetInfo.version, + }), + ...(assetInfo.assetTransferMethod && { + assetTransferMethod: assetInfo.assetTransferMethod, + }), }, }; } /** - * Convert decimal amount to token units (e.g., 0.10 -> 100000 for 6-decimal tokens) + * Converts a decimal string amount to an integer token amount using the given decimals. * - * @param decimalAmount - The decimal amount to convert - * @param decimals - The number of decimals for the token - * @returns The token amount as a string + * @param decimalAmount - The amount as a decimal string (e.g. "1.5") + * @param decimals - The number of decimal places for the token + * @returns The token amount as an integer string in smallest units */ private convertToTokenAmount(decimalAmount: string, decimals: number): string { const amount = parseFloat(decimalAmount); if (isNaN(amount)) { throw new Error(`Invalid amount: ${decimalAmount}`); } - // Convert to smallest unit (e.g., for USDC with 6 decimals: 0.10 * 10^6 = 100000) const [intPart, decPart = ""] = String(amount).split("."); const paddedDec = decPart.padEnd(decimals, "0").slice(0, decimals); const tokenAmount = (intPart + paddedDec).replace(/^0+/, "") || "0"; return tokenAmount; } - - /** - * Get the default asset info for a network (typically USDC) - * - * @param network - The network to get asset info for - * @returns The asset information including address, name, version, and decimals - */ - private getDefaultAsset(network: Network): { - address: string; - name: string; - version: string; - decimals: number; - } { - // Map of network to USDC info including EIP-712 domain parameters - // Each network has the right to determine its own default stablecoin that can be expressed as a USD string by calling servers - // NOTE: Currently only EIP-3009 supporting stablecoins can be used with this scheme - // Generic ERC20 support via EIP-2612/permit2 is planned, but not yet implemented. - const stablecoins: Record< - string, - { address: string; name: string; version: string; decimals: number } - > = { - "eip155:8453": { - address: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", - name: "USD Coin", - version: "2", - decimals: 6, - }, // Base mainnet USDC - "eip155:84532": { - address: "0x036CbD53842c5426634e7929541eC2318f3dCF7e", - name: "USDC", - version: "2", - decimals: 6, - }, // Base Sepolia USDC - "eip155:4326": { - address: "0xFAfDdbb3FC7688494971a79cc65DCa3EF82079E7", - name: "MegaUSD", - version: "1", - decimals: 18, - }, // MegaETH mainnet USDM - }; - - const assetInfo = stablecoins[network]; - if (!assetInfo) { - throw new Error(`No default asset configured for network ${network}`); - } - - return assetInfo; - } } diff --git a/typescript/packages/mechanisms/evm/src/exact/v1/facilitator/scheme.ts b/typescript/packages/mechanisms/evm/src/exact/v1/facilitator/scheme.ts index 2920dec252..381aa8ca2f 100644 --- a/typescript/packages/mechanisms/evm/src/exact/v1/facilitator/scheme.ts +++ b/typescript/packages/mechanisms/evm/src/exact/v1/facilitator/scheme.ts @@ -7,11 +7,22 @@ import { VerifyResponse, } from "@x402/core/types"; import { PaymentRequirementsV1 } from "@x402/core/types/v1"; -import { getAddress, Hex, isAddressEqual, parseErc6492Signature, parseSignature } from "viem"; -import { authorizationTypes, eip3009ABI } from "../../../constants"; +import { getAddress, Hex, isAddressEqual, parseErc6492Signature } from "viem"; +import { authorizationTypes } from "../../../constants"; import { FacilitatorEvmSigner } from "../../../signer"; import { ExactEvmPayloadV1 } from "../../../types"; import { EvmNetworkV1, getEvmChainIdV1 } from "../../../v1"; +import * as Errors from "../../facilitator/errors"; +import { + diagnoseEip3009SimulationFailure, + executeTransferWithAuthorization, + simulateEip3009Transfer, +} from "../../facilitator/eip3009-utils"; + +export interface VerifyV1Options { + /** Run onchain simulation. Defaults to true. */ + simulate?: boolean; +} export interface ExactEvmSchemeV1Config { /** @@ -21,6 +32,12 @@ export interface ExactEvmSchemeV1Config { * @default false */ deployERC4337WithEIP6492?: boolean; + /** + * If enabled, simulates transaction before settling. Defaults to false, ie only simulate during verify. + * + * @default false + */ + simulateInSettle?: boolean; } /** @@ -43,6 +60,7 @@ export class ExactEvmSchemeV1 implements SchemeNetworkFacilitator { ) { this.config = { deployERC4337WithEIP6492: config?.deployERC4337WithEIP6492 ?? false, + simulateInSettle: config?.simulateInSettle ?? false, }; } @@ -78,17 +96,131 @@ export class ExactEvmSchemeV1 implements SchemeNetworkFacilitator { async verify( payload: PaymentPayload, requirements: PaymentRequirements, + ): Promise { + return this._verify(payload, requirements); + } + + /** + * Settles a payment by executing the transfer (V1). + * + * @param payload - The payment payload to settle + * @param requirements - The payment requirements + * @returns Promise resolving to settlement response + */ + async settle( + payload: PaymentPayload, + requirements: PaymentRequirements, + ): Promise { + const payloadV1 = payload as unknown as PaymentPayloadV1; + const exactEvmPayload = payload.payload as ExactEvmPayloadV1; + + // Re-verify before settling + const valid = await this._verify(payload, requirements, { + simulate: this.config.simulateInSettle ?? false, + }); + if (!valid.isValid) { + return { + success: false, + network: payloadV1.network, + transaction: "", + errorReason: valid.invalidReason ?? Errors.ErrInvalidScheme, + payer: exactEvmPayload.authorization.from, + }; + } + + try { + // Parse ERC-6492 signature if applicable (for optional deployment) + const { address: factoryAddress, data: factoryCalldata } = parseErc6492Signature( + exactEvmPayload.signature!, + ); + + // Deploy ERC-4337 smart wallet via EIP-6492 if configured and needed + if ( + this.config.deployERC4337WithEIP6492 && + factoryAddress && + factoryCalldata && + !isAddressEqual(factoryAddress, "0x0000000000000000000000000000000000000000") + ) { + // Check if smart wallet is already deployed + const payerAddress = exactEvmPayload.authorization.from; + const bytecode = await this.signer.getCode({ address: payerAddress }); + + if (!bytecode || bytecode === "0x") { + // Send the factory calldata directly as a transaction + // The factoryCalldata already contains the complete encoded function call + const deployTx = await this.signer.sendTransaction({ + to: factoryAddress as Hex, + data: factoryCalldata as Hex, + }); + + // Wait for deployment transaction + await this.signer.waitForTransactionReceipt({ hash: deployTx }); + } + } + + const tx = await executeTransferWithAuthorization( + this.signer, + getAddress(requirements.asset), + exactEvmPayload, + ); + + // Wait for transaction confirmation + const receipt = await this.signer.waitForTransactionReceipt({ hash: tx }); + + if (receipt.status !== "success") { + return { + success: false, + errorReason: Errors.ErrTransactionFailed, + transaction: tx, + network: payloadV1.network, + payer: exactEvmPayload.authorization.from, + }; + } + + return { + success: true, + transaction: tx, + network: payloadV1.network, + payer: exactEvmPayload.authorization.from, + }; + } catch (error) { + return { + success: false, + errorReason: error instanceof Error ? error.message : Errors.ErrTransactionFailed, + transaction: "", + network: payloadV1.network, + payer: exactEvmPayload.authorization.from, + }; + } + } + + /** + * Internal verify with optional simulation control. + * + * @param payload - The payment payload to verify + * @param requirements - The payment requirements + * @param options - Verification options (e.g. simulate) + * @returns Promise resolving to verification response + */ + private async _verify( + payload: PaymentPayload, + requirements: PaymentRequirements, + options?: VerifyV1Options, ): Promise { const requirementsV1 = requirements as unknown as PaymentRequirementsV1; const payloadV1 = payload as unknown as PaymentPayloadV1; const exactEvmPayload = payload.payload as ExactEvmPayloadV1; + const payer = exactEvmPayload.authorization.from; + let eip6492Deployment: + | { factoryAddress: `0x${string}`; factoryCalldata: `0x${string}` } + | undefined; // Verify scheme matches if (payloadV1.scheme !== "exact" || requirements.scheme !== "exact") { return { isValid: false, - invalidReason: "unsupported_scheme", - payer: exactEvmPayload.authorization.from, + invalidReason: Errors.ErrInvalidScheme, + payer, }; } @@ -99,16 +231,16 @@ export class ExactEvmSchemeV1 implements SchemeNetworkFacilitator { } catch { return { isValid: false, - invalidReason: `invalid_network`, - payer: exactEvmPayload.authorization.from, + invalidReason: Errors.ErrNetworkMismatch, + payer, }; } if (!requirements.extra?.name || !requirements.extra?.version) { return { isValid: false, - invalidReason: "missing_eip712_domain", - payer: exactEvmPayload.authorization.from, + invalidReason: Errors.ErrMissingEip712Domain, + payer, }; } @@ -119,8 +251,8 @@ export class ExactEvmSchemeV1 implements SchemeNetworkFacilitator { if (payloadV1.network !== requirements.network) { return { isValid: false, - invalidReason: "network_mismatch", - payer: exactEvmPayload.authorization.from, + invalidReason: Errors.ErrNetworkMismatch, + payer, }; } @@ -144,67 +276,54 @@ export class ExactEvmSchemeV1 implements SchemeNetworkFacilitator { }, }; - // Verify signature + // Verify signature (flatten EIP-6492 handling out of catch block) + let isValid = false; try { - const recoveredAddress = await this.signer.verifyTypedData({ - address: exactEvmPayload.authorization.from, + isValid = await this.signer.verifyTypedData({ + address: payer, ...permitTypedData, signature: exactEvmPayload.signature!, }); + } catch { + isValid = false; + } + + const signature = exactEvmPayload.signature!; + const sigLen = signature.startsWith("0x") ? signature.length - 2 : signature.length; - if (!recoveredAddress) { + // Extract EIP-6492 deployment info (factory address + calldata) if present + const erc6492Data = parseErc6492Signature(signature); + const hasDeploymentInfo = + erc6492Data.address && + erc6492Data.data && + !isAddressEqual(erc6492Data.address, "0x0000000000000000000000000000000000000000"); + + if (hasDeploymentInfo) { + eip6492Deployment = { + factoryAddress: erc6492Data.address!, + factoryCalldata: erc6492Data.data!, + }; + } + + if (!isValid) { + const isSmartWallet = sigLen > 130; // 65 bytes = 130 hex chars for EOA + + if (!isSmartWallet) { return { isValid: false, - invalidReason: "invalid_exact_evm_payload_signature", - payer: exactEvmPayload.authorization.from, + invalidReason: Errors.ErrInvalidSignature, + payer, }; } - } catch { - // Signature verification failed - could be an undeployed smart wallet - // Check if smart wallet is deployed - const signature = exactEvmPayload.signature!; - const signatureLength = signature.startsWith("0x") ? signature.length - 2 : signature.length; - const isSmartWallet = signatureLength > 130; // 65 bytes = 130 hex chars for EOA - if (isSmartWallet) { - const payerAddress = exactEvmPayload.authorization.from; - const bytecode = await this.signer.getCode({ address: payerAddress }); + const bytecode = await this.signer.getCode({ address: payer }); + const isDeployed = bytecode && bytecode !== "0x"; - if (!bytecode || bytecode === "0x") { - // Wallet is not deployed. Check if it's EIP-6492 with deployment info. - // EIP-6492 signatures contain factory address and calldata needed for deployment. - // Non-EIP-6492 undeployed wallets cannot succeed (no way to deploy them). - const erc6492Data = parseErc6492Signature(signature); - const hasDeploymentInfo = - erc6492Data.address && - erc6492Data.data && - !isAddressEqual(erc6492Data.address, "0x0000000000000000000000000000000000000000"); - - if (!hasDeploymentInfo) { - // Non-EIP-6492 undeployed smart wallet - will always fail at settlement - // since EIP-3009 requires on-chain EIP-1271 validation - return { - isValid: false, - invalidReason: "invalid_exact_evm_payload_undeployed_smart_wallet", - payer: payerAddress, - }; - } - // EIP-6492 signature with deployment info - allow through - // Facilitators with sponsored deployment support can handle this in settle() - } else { - // Wallet is deployed but signature still failed - invalid signature - return { - isValid: false, - invalidReason: "invalid_exact_evm_payload_signature", - payer: exactEvmPayload.authorization.from, - }; - } - } else { - // EOA signature failed + if (!isDeployed && !hasDeploymentInfo) { return { isValid: false, - invalidReason: "invalid_exact_evm_payload_signature", - payer: exactEvmPayload.authorization.from, + invalidReason: Errors.ErrUndeployedSmartWallet, + payer, }; } } @@ -213,8 +332,8 @@ export class ExactEvmSchemeV1 implements SchemeNetworkFacilitator { if (getAddress(exactEvmPayload.authorization.to) !== getAddress(requirements.payTo)) { return { isValid: false, - invalidReason: "invalid_exact_evm_payload_recipient_mismatch", - payer: exactEvmPayload.authorization.from, + invalidReason: Errors.ErrRecipientMismatch, + payer, }; } @@ -223,8 +342,8 @@ export class ExactEvmSchemeV1 implements SchemeNetworkFacilitator { if (BigInt(exactEvmPayload.authorization.validBefore) < BigInt(now + 6)) { return { isValid: false, - invalidReason: "invalid_exact_evm_payload_authorization_valid_before", - payer: exactEvmPayload.authorization.from, + invalidReason: Errors.ErrValidBeforeExpired, + payer, }; } @@ -232,188 +351,43 @@ export class ExactEvmSchemeV1 implements SchemeNetworkFacilitator { if (BigInt(exactEvmPayload.authorization.validAfter) > BigInt(now)) { return { isValid: false, - invalidReason: "invalid_exact_evm_payload_authorization_valid_after", - payer: exactEvmPayload.authorization.from, + invalidReason: Errors.ErrValidAfterInFuture, + payer, }; } - // Check balance - try { - const balance = (await this.signer.readContract({ - address: erc20Address, - abi: eip3009ABI, - functionName: "balanceOf", - args: [exactEvmPayload.authorization.from], - })) as bigint; - - if (BigInt(balance) < BigInt(requirementsV1.maxAmountRequired)) { - return { - isValid: false, - invalidReason: "insufficient_funds", - invalidMessage: `Insufficient funds to complete the payment. Required: ${requirementsV1.maxAmountRequired} ${requirements.asset}, Available: ${balance.toString()} ${requirements.asset}. Please add funds to your wallet and try again.`, - payer: exactEvmPayload.authorization.from, - }; - } - } catch { - // If we can't check balance, continue with other validations - } - - // Verify amount is sufficient - if (BigInt(exactEvmPayload.authorization.value) < BigInt(requirementsV1.maxAmountRequired)) { + // Verify amount exactly matches requirements + if (BigInt(exactEvmPayload.authorization.value) !== BigInt(requirementsV1.maxAmountRequired)) { return { isValid: false, - invalidReason: "invalid_exact_evm_payload_authorization_value", - payer: exactEvmPayload.authorization.from, + invalidReason: Errors.ErrInvalidAuthorizationValue, + payer, }; } + // Transaction simulation + if (options?.simulate !== false) { + const simulationSucceeded = await simulateEip3009Transfer( + this.signer, + erc20Address, + exactEvmPayload, + eip6492Deployment, + ); + if (!simulationSucceeded) { + return diagnoseEip3009SimulationFailure( + this.signer, + erc20Address, + exactEvmPayload, + requirements, + requirementsV1.maxAmountRequired, + ); + } + } + return { isValid: true, invalidReason: undefined, - payer: exactEvmPayload.authorization.from, + payer, }; } - - /** - * Settles a payment by executing the transfer (V1). - * - * @param payload - The payment payload to settle - * @param requirements - The payment requirements - * @returns Promise resolving to settlement response - */ - async settle( - payload: PaymentPayload, - requirements: PaymentRequirements, - ): Promise { - const payloadV1 = payload as unknown as PaymentPayloadV1; - const exactEvmPayload = payload.payload as ExactEvmPayloadV1; - - // Re-verify before settling - const valid = await this.verify(payload, requirements); - if (!valid.isValid) { - return { - success: false, - network: payloadV1.network, - transaction: "", - errorReason: valid.invalidReason ?? "invalid_scheme", - payer: exactEvmPayload.authorization.from, - }; - } - - try { - // Parse ERC-6492 signature if applicable - const parseResult = parseErc6492Signature(exactEvmPayload.signature!); - const { signature, address: factoryAddress, data: factoryCalldata } = parseResult; - - // Deploy ERC-4337 smart wallet via EIP-6492 if configured and needed - if ( - this.config.deployERC4337WithEIP6492 && - factoryAddress && - factoryCalldata && - !isAddressEqual(factoryAddress, "0x0000000000000000000000000000000000000000") - ) { - // Check if smart wallet is already deployed - const payerAddress = exactEvmPayload.authorization.from; - const bytecode = await this.signer.getCode({ address: payerAddress }); - - if (!bytecode || bytecode === "0x") { - // Wallet not deployed - attempt deployment - try { - console.log(`Deploying ERC-4337 smart wallet for ${payerAddress} via EIP-6492`); - - // Send the factory calldata directly as a transaction - // The factoryCalldata already contains the complete encoded function call - const deployTx = await this.signer.sendTransaction({ - to: factoryAddress as Hex, - data: factoryCalldata as Hex, - }); - - // Wait for deployment transaction - await this.signer.waitForTransactionReceipt({ hash: deployTx }); - console.log(`Successfully deployed smart wallet for ${payerAddress}`); - } catch (deployError) { - console.error("Smart wallet deployment failed:", deployError); - // Deployment failed - cannot proceed - throw deployError; - } - } else { - console.log(`Smart wallet for ${payerAddress} already deployed, skipping deployment`); - } - } - - // Determine if this is an ECDSA signature (EOA) or smart wallet signature - // ECDSA signatures are exactly 65 bytes (130 hex chars without 0x) - const signatureLength = signature.startsWith("0x") ? signature.length - 2 : signature.length; - const isECDSA = signatureLength === 130; - - let tx: Hex; - if (isECDSA) { - // For EOA wallets, parse signature into v, r, s and use that overload - const parsedSig = parseSignature(signature); - - tx = await this.signer.writeContract({ - address: getAddress(requirements.asset), - abi: eip3009ABI, - functionName: "transferWithAuthorization", - args: [ - getAddress(exactEvmPayload.authorization.from), - getAddress(exactEvmPayload.authorization.to), - BigInt(exactEvmPayload.authorization.value), - BigInt(exactEvmPayload.authorization.validAfter), - BigInt(exactEvmPayload.authorization.validBefore), - exactEvmPayload.authorization.nonce, - (parsedSig.v as number | undefined) || parsedSig.yParity, - parsedSig.r, - parsedSig.s, - ], - }); - } else { - // For smart wallets, use the bytes signature overload - // The signature contains WebAuthn/P256 or other ERC-1271 compatible signature data - tx = await this.signer.writeContract({ - address: getAddress(requirements.asset), - abi: eip3009ABI, - functionName: "transferWithAuthorization", - args: [ - getAddress(exactEvmPayload.authorization.from), - getAddress(exactEvmPayload.authorization.to), - BigInt(exactEvmPayload.authorization.value), - BigInt(exactEvmPayload.authorization.validAfter), - BigInt(exactEvmPayload.authorization.validBefore), - exactEvmPayload.authorization.nonce, - signature, - ], - }); - } - - // Wait for transaction confirmation - const receipt = await this.signer.waitForTransactionReceipt({ hash: tx }); - - if (receipt.status !== "success") { - return { - success: false, - errorReason: "invalid_transaction_state", - transaction: tx, - network: payloadV1.network, - payer: exactEvmPayload.authorization.from, - }; - } - - return { - success: true, - transaction: tx, - network: payloadV1.network, - payer: exactEvmPayload.authorization.from, - }; - } catch (error) { - console.error("Failed to settle transaction:", error); - return { - success: false, - errorReason: "transaction_failed", - transaction: "", - network: payloadV1.network, - payer: exactEvmPayload.authorization.from, - }; - } - } } diff --git a/typescript/packages/mechanisms/evm/src/index.ts b/typescript/packages/mechanisms/evm/src/index.ts index 380e5300dd..38277afe0c 100644 --- a/typescript/packages/mechanisms/evm/src/index.ts +++ b/typescript/packages/mechanisms/evm/src/index.ts @@ -29,13 +29,22 @@ export type { } from "./types"; export { isPermit2Payload, isEIP3009Payload } from "./types"; +// Upto scheme client +export { UptoEvmScheme } from "./upto"; + +// Upto types +export type { UptoPermit2Payload, UptoPermit2Witness, UptoPermit2Authorization } from "./types"; +export { isUptoPermit2Payload } from "./types"; + // Constants export { PERMIT2_ADDRESS, x402ExactPermit2ProxyAddress, x402UptoPermit2ProxyAddress, permit2WitnessTypes, + uptoPermit2WitnessTypes, authorizationTypes, eip3009ABI, x402ExactPermit2ProxyABI, + x402UptoPermit2ProxyABI, } from "./constants"; diff --git a/typescript/packages/mechanisms/evm/src/multicall.ts b/typescript/packages/mechanisms/evm/src/multicall.ts new file mode 100644 index 0000000000..9e0534ab64 --- /dev/null +++ b/typescript/packages/mechanisms/evm/src/multicall.ts @@ -0,0 +1,140 @@ +import { encodeFunctionData, decodeFunctionResult } from "viem"; + +/** + * Multicall3 contract address. + * Same address on all EVM chains via CREATE2 deployment. + * + * @see https://github.com/mds1/multicall + */ +export const MULTICALL3_ADDRESS = "0xcA11bde05977b3631167028862bE2a173976CA11" as const; + +/** Multicall3 getEthBalance ABI for querying native token balance. */ +export const multicall3GetEthBalanceAbi = [ + { + name: "getEthBalance", + inputs: [{ name: "addr", type: "address" }], + outputs: [{ name: "balance", type: "uint256" }], + stateMutability: "view", + type: "function", + }, +] as const; + +/** Multicall3 tryAggregate ABI for batching calls. */ +const multicall3ABI = [ + { + inputs: [ + { name: "requireSuccess", type: "bool" }, + { + name: "calls", + type: "tuple[]", + components: [ + { name: "target", type: "address" }, + { name: "callData", type: "bytes" }, + ], + }, + ], + name: "tryAggregate", + outputs: [ + { + name: "returnData", + type: "tuple[]", + components: [ + { name: "success", type: "bool" }, + { name: "returnData", type: "bytes" }, + ], + }, + ], + stateMutability: "payable", + type: "function", + }, +] as const; + +export type ContractCall = { + address: `0x${string}`; + abi: readonly unknown[]; + functionName: string; + args?: readonly unknown[]; +}; + +export type RawContractCall = { + address: `0x${string}`; + callData: `0x${string}`; +}; + +export type MulticallSuccess = { status: "success"; result: unknown }; +export type MulticallFailure = { status: "failure"; error: Error }; +export type MulticallResult = MulticallSuccess | MulticallFailure; + +/** + * Batches contract calls via Multicall3 `tryAggregate(false, ...)`. + * + * Accepts a mix of typed ContractCall (ABI-encoded + decoded) and + * RawContractCall (pre-encoded calldata, no decoding) entries. + * Raw calls are useful for the EIP-6492 factory deployment case + * where calldata is pre-encoded with no ABI available. + */ +type ReadContractFn = (args: { + address: `0x${string}`; + abi: readonly unknown[]; + functionName: string; + args?: readonly unknown[]; +}) => Promise; + +/** + * Executes multiple contract read calls in a single RPC round-trip using Multicall3. + * + * @param readContract - Function that performs a single contract read (e.g. viem readContract) + * @param calls - Array of contract calls to batch (ContractCall or RawContractCall) + * @returns A promise that resolves to an array of decoded results, one per call + */ +export async function multicall( + readContract: ReadContractFn, + calls: ReadonlyArray, +): Promise { + const aggregateCalls = calls.map(call => { + if ("callData" in call) { + return { target: call.address, callData: call.callData }; + } + const callData = encodeFunctionData({ + abi: call.abi, + functionName: call.functionName, + args: call.args as unknown[], + }); + return { target: call.address, callData }; + }); + + const rawResults = (await readContract({ + address: MULTICALL3_ADDRESS, + abi: multicall3ABI, + functionName: "tryAggregate", + args: [false, aggregateCalls], + })) as { success: boolean; returnData: `0x${string}` }[]; + + return rawResults.map((raw, i) => { + if (!raw.success) { + return { + status: "failure" as const, + error: new Error(`multicall: call reverted (returnData: ${raw.returnData})`), + }; + } + + const call = calls[i]; + if ("callData" in call) { + return { status: "success" as const, result: undefined }; + } + + try { + const decoded = decodeFunctionResult({ + abi: call.abi, + functionName: call.functionName, + data: raw.returnData, + }); + return { status: "success" as const, result: decoded }; + } catch (err) { + return { + status: "failure" as const, + error: err instanceof Error ? err : new Error(String(err)), + }; + } + }); +} diff --git a/typescript/packages/mechanisms/evm/src/shared/defaultAssets.ts b/typescript/packages/mechanisms/evm/src/shared/defaultAssets.ts new file mode 100644 index 0000000000..f62ecbd804 --- /dev/null +++ b/typescript/packages/mechanisms/evm/src/shared/defaultAssets.ts @@ -0,0 +1,115 @@ +import type { Network } from "@x402/core/types"; + +/** + * Base stablecoin asset configuration shared across all EVM payment schemes. + * Contains the core fields needed to identify and convert tokens. + */ +export type DefaultAssetInfo = { + /** Token contract address */ + address: string; + /** EIP-712 domain name (must match the token's domain separator) */ + name: string; + /** EIP-712 domain version (must match the token's domain separator) */ + version: string; + /** Token decimal places (typically 6 for USDC) */ + decimals: number; +}; + +/** + * Extended asset configuration for the exact scheme. + * Includes transfer method hints that control client-side behaviour. + */ +export type ExactDefaultAssetInfo = DefaultAssetInfo & { + /** + * Transfer method override: `"permit2"` for tokens that don't support EIP-3009. + * Omit for EIP-3009 tokens (default behaviour). + */ + assetTransferMethod?: string; + /** + * Set to `true` for permit2 tokens that implement EIP-2612 `permit()`. + * Controls whether name/version are included in `extra` so the client can + * sign a gasless EIP-2612 permit for Permit2 approval. + */ + supportsEip2612?: boolean; +}; + +/** + * Default stablecoins indexed by CAIP-2 network identifier. + * + * Each network has the right to determine its own default stablecoin that can + * be expressed as a USD string by calling servers. See DEFAULT_ASSET.md in + * exact/server/ for how to add new chains. + */ +export const DEFAULT_STABLECOINS: Record = { + "eip155:8453": { + address: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", + name: "USD Coin", + version: "2", + decimals: 6, + }, // Base mainnet USDC + "eip155:84532": { + address: "0x036CbD53842c5426634e7929541eC2318f3dCF7e", + name: "USDC", + version: "2", + decimals: 6, + }, // Base Sepolia USDC + "eip155:4326": { + address: "0xFAfDdbb3FC7688494971a79cc65DCa3EF82079E7", + name: "MegaUSD", + version: "1", + decimals: 18, + assetTransferMethod: "permit2", + supportsEip2612: true, + }, // MegaETH mainnet MegaUSD (no EIP-3009, supports EIP-2612) + "eip155:143": { + address: "0x754704Bc059F8C67012fEd69BC8A327a5aafb603", + name: "USD Coin", + version: "2", + decimals: 6, + }, // Monad mainnet USDC + "eip155:988": { + address: "0x779Ded0c9e1022225f8E0630b35a9b54bE713736", + name: "USDT0", + version: "1", + decimals: 6, + }, // Stable mainnet USDT0 + "eip155:2201": { + address: "0x78Cf24370174180738C5B8E352B6D14c83a6c9A9", + name: "USDT0", + version: "1", + decimals: 6, + }, // Stable testnet USDT0 + "eip155:137": { + address: "0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359", + name: "USD Coin", + version: "2", + decimals: 6, + }, // Polygon mainnet USDC + "eip155:42161": { + address: "0xaf88d065e77c8cC2239327C5EDb3A432268e5831", + name: "USD Coin", + version: "2", + decimals: 6, + }, // Arbitrum One USDC + "eip155:421614": { + address: "0x75faf114eafb1BDbe2F0316DF893fd58CE46AA4d", + name: "USD Coin", + version: "2", + decimals: 6, + }, // Arbitrum Sepolia USDC +}; + +/** + * Look up the default stablecoin for a network. + * + * @param network - CAIP-2 network identifier (e.g. "eip155:8453") + * @returns The default asset info + * @throws If no default asset is configured for the network + */ +export function getDefaultAsset(network: Network): ExactDefaultAssetInfo { + const info = DEFAULT_STABLECOINS[network]; + if (!info) { + throw new Error(`No default asset configured for network ${network}`); + } + return info; +} diff --git a/typescript/packages/mechanisms/evm/src/shared/erc20approval.ts b/typescript/packages/mechanisms/evm/src/shared/erc20approval.ts new file mode 100644 index 0000000000..0a020d906c --- /dev/null +++ b/typescript/packages/mechanisms/evm/src/shared/erc20approval.ts @@ -0,0 +1,154 @@ +import { + getAddress, + parseTransaction, + decodeFunctionData, + recoverTransactionAddress, + type TransactionSerialized, +} from "viem"; +import type { VerifyResponse } from "@x402/core/types"; +import { + validateErc20ApprovalGasSponsoringInfo, + type Erc20ApprovalGasSponsoringInfo, +} from "../exact/extensions"; +import { PERMIT2_ADDRESS, erc20ApproveAbi } from "../constants"; +import { + ErrErc20ApprovalInvalidFormat, + ErrErc20ApprovalFromMismatch, + ErrErc20ApprovalAssetMismatch, + ErrErc20ApprovalSpenderNotPermit2, + ErrErc20ApprovalTxWrongTarget, + ErrErc20ApprovalTxWrongSelector, + ErrErc20ApprovalTxWrongSpender, + ErrErc20ApprovalTxInvalidCalldata, + ErrErc20ApprovalTxSignerMismatch, + ErrErc20ApprovalTxInvalidSignature, + ErrErc20ApprovalTxParseFailed, +} from "../exact/facilitator/errors"; + +/** The approve(address,uint256) function selector */ +const APPROVE_SELECTOR = "0x095ea7b3"; + +/** + * Validates ERC-20 approval extension data for a Permit2 payment. + * + * Performs comprehensive validation: + * - Format validation via validateErc20ApprovalGasSponsoringInfo (JSON Schema) + * - `from` matches payer + * - `asset` matches token + * - `spender` is PERMIT2_ADDRESS + * - Transaction `to` matches token address + * - Transaction calldata is a valid approve(PERMIT2_ADDRESS, ...) call + * - Recovered transaction signer matches `from` + * + * @param info - The ERC-20 approval gas sponsoring info + * @param payer - The expected payer address + * @param tokenAddress - The expected token address + * @returns Validation result with invalidReason and invalidMessage on failure + */ +export async function validateErc20ApprovalForPayment( + info: Erc20ApprovalGasSponsoringInfo, + payer: `0x${string}`, + tokenAddress: `0x${string}`, +): Promise> { + if (!validateErc20ApprovalGasSponsoringInfo(info)) { + return { + isValid: false, + invalidReason: ErrErc20ApprovalInvalidFormat, + invalidMessage: "ERC-20 approval extension info failed schema validation", + }; + } + + if (getAddress(info.from) !== getAddress(payer)) { + return { + isValid: false, + invalidReason: ErrErc20ApprovalFromMismatch, + invalidMessage: `Expected from=${payer}, got ${info.from}`, + }; + } + + if (getAddress(info.asset) !== tokenAddress) { + return { + isValid: false, + invalidReason: ErrErc20ApprovalAssetMismatch, + invalidMessage: `Expected asset=${tokenAddress}, got ${info.asset}`, + }; + } + + if (getAddress(info.spender) !== getAddress(PERMIT2_ADDRESS)) { + return { + isValid: false, + invalidReason: ErrErc20ApprovalSpenderNotPermit2, + invalidMessage: `Expected spender=${PERMIT2_ADDRESS}, got ${info.spender}`, + }; + } + + try { + const serializedTx = info.signedTransaction as TransactionSerialized; + const tx = parseTransaction(serializedTx); + + if (!tx.to || getAddress(tx.to) !== tokenAddress) { + return { + isValid: false, + invalidReason: ErrErc20ApprovalTxWrongTarget, + invalidMessage: `Transaction targets ${tx.to ?? "null"}, expected ${tokenAddress}`, + }; + } + + const data = tx.data ?? "0x"; + if (!data.startsWith(APPROVE_SELECTOR)) { + return { + isValid: false, + invalidReason: ErrErc20ApprovalTxWrongSelector, + invalidMessage: `Transaction calldata does not start with approve() selector ${APPROVE_SELECTOR}`, + }; + } + + try { + const decoded = decodeFunctionData({ + abi: erc20ApproveAbi, + data: data as `0x${string}`, + }); + const calldataSpender = getAddress(decoded.args[0] as `0x${string}`); + if (calldataSpender !== getAddress(PERMIT2_ADDRESS)) { + return { + isValid: false, + invalidReason: ErrErc20ApprovalTxWrongSpender, + invalidMessage: `approve() spender is ${calldataSpender}, expected Permit2 ${PERMIT2_ADDRESS}`, + }; + } + } catch { + return { + isValid: false, + invalidReason: ErrErc20ApprovalTxInvalidCalldata, + invalidMessage: "Failed to decode approve() calldata from the signed transaction", + }; + } + + try { + const recoveredAddress = await recoverTransactionAddress({ + serializedTransaction: serializedTx, + }); + if (getAddress(recoveredAddress) !== getAddress(payer)) { + return { + isValid: false, + invalidReason: ErrErc20ApprovalTxSignerMismatch, + invalidMessage: `Transaction signed by ${recoveredAddress}, expected payer ${payer}`, + }; + } + } catch { + return { + isValid: false, + invalidReason: ErrErc20ApprovalTxInvalidSignature, + invalidMessage: "Failed to recover signer from the signed transaction", + }; + } + } catch { + return { + isValid: false, + invalidReason: ErrErc20ApprovalTxParseFailed, + invalidMessage: "Failed to parse the signed transaction", + }; + } + + return { isValid: true }; +} diff --git a/typescript/packages/mechanisms/evm/src/shared/extensions.ts b/typescript/packages/mechanisms/evm/src/shared/extensions.ts new file mode 100644 index 0000000000..11baee6625 --- /dev/null +++ b/typescript/packages/mechanisms/evm/src/shared/extensions.ts @@ -0,0 +1,147 @@ +import { PaymentRequirements, PaymentPayloadResult, PaymentPayloadContext } from "@x402/core/types"; +import { EIP2612_GAS_SPONSORING_KEY, ERC20_APPROVAL_GAS_SPONSORING_KEY } from "../exact/extensions"; +import { getAddress } from "viem"; +import { PERMIT2_ADDRESS, erc20AllowanceAbi } from "../constants"; +import { getEvmChainId } from "../utils"; +import { ClientEvmSigner } from "../signer"; +import { signEip2612Permit } from "../exact/client/eip2612"; +import { signErc20ApprovalTransaction } from "../exact/client/erc20approval"; +import { resolveExtensionRpcCapabilities, type ExactEvmSchemeOptions } from "./rpc"; + +/** + * Attempts to sign an EIP-2612 permit for gasless Permit2 approval. + * + * @param signer - The EVM client signer + * @param options - Optional RPC configuration for backfilling capabilities + * @param requirements - The payment requirements from the server + * @param result - The payment payload result from the scheme + * @param context - Optional context containing server extensions and metadata + * @returns Extension data for EIP-2612 gas sponsoring, or undefined if not applicable + */ +export async function trySignEip2612PermitExtension( + signer: ClientEvmSigner, + options: ExactEvmSchemeOptions | undefined, + requirements: PaymentRequirements, + result: PaymentPayloadResult, + context?: PaymentPayloadContext, +): Promise | undefined> { + const capabilities = resolveExtensionRpcCapabilities(requirements.network, signer, options); + + if (!capabilities.readContract) { + return undefined; + } + + if (!context?.extensions?.[EIP2612_GAS_SPONSORING_KEY]) { + return undefined; + } + + const tokenName = requirements.extra?.name as string | undefined; + const tokenVersion = requirements.extra?.version as string | undefined; + if (!tokenName || !tokenVersion) { + return undefined; + } + + const chainId = getEvmChainId(requirements.network); + const tokenAddress = getAddress(requirements.asset) as `0x${string}`; + + try { + const allowance = (await capabilities.readContract({ + address: tokenAddress, + abi: erc20AllowanceAbi, + functionName: "allowance", + args: [signer.address, PERMIT2_ADDRESS], + })) as bigint; + + if (allowance >= BigInt(requirements.amount)) { + return undefined; + } + } catch { + // Allowance check failed, proceed with signing + } + + const permit2Auth = result.payload?.permit2Authorization as Record | undefined; + const deadline = + (permit2Auth?.deadline as string) ?? + Math.floor(Date.now() / 1000 + requirements.maxTimeoutSeconds).toString(); + + const info = await signEip2612Permit( + { + address: signer.address, + signTypedData: msg => signer.signTypedData(msg), + readContract: capabilities.readContract, + }, + tokenAddress, + tokenName, + tokenVersion, + chainId, + deadline, + requirements.amount, + ); + + return { + [EIP2612_GAS_SPONSORING_KEY]: { info }, + }; +} + +/** + * Attempts to sign an ERC-20 approval transaction for gasless Permit2 approval. + * + * @param signer - The EVM client signer + * @param options - Optional RPC configuration for backfilling capabilities + * @param requirements - The payment requirements from the server + * @param context - Optional context containing server extensions and metadata + * @returns Extension data for ERC-20 approval gas sponsoring, or undefined if not applicable + */ +export async function trySignErc20ApprovalExtension( + signer: ClientEvmSigner, + options: ExactEvmSchemeOptions | undefined, + requirements: PaymentRequirements, + context?: PaymentPayloadContext, +): Promise | undefined> { + const capabilities = resolveExtensionRpcCapabilities(requirements.network, signer, options); + + if (!capabilities.readContract) { + return undefined; + } + + if (!context?.extensions?.[ERC20_APPROVAL_GAS_SPONSORING_KEY]) { + return undefined; + } + + if (!capabilities.signTransaction || !capabilities.getTransactionCount) { + return undefined; + } + + const chainId = getEvmChainId(requirements.network); + const tokenAddress = getAddress(requirements.asset) as `0x${string}`; + + try { + const allowance = (await capabilities.readContract({ + address: tokenAddress, + abi: erc20AllowanceAbi, + functionName: "allowance", + args: [signer.address, PERMIT2_ADDRESS], + })) as bigint; + + if (allowance >= BigInt(requirements.amount)) { + return undefined; + } + } catch { + // Allowance check failed, proceed with signing + } + + const info = await signErc20ApprovalTransaction( + { + address: signer.address, + signTransaction: capabilities.signTransaction, + getTransactionCount: capabilities.getTransactionCount, + estimateFeesPerGas: capabilities.estimateFeesPerGas, + }, + tokenAddress, + chainId, + ); + + return { + [ERC20_APPROVAL_GAS_SPONSORING_KEY]: { info }, + }; +} diff --git a/typescript/packages/mechanisms/evm/src/shared/permit2.ts b/typescript/packages/mechanisms/evm/src/shared/permit2.ts new file mode 100644 index 0000000000..ed27a72cba --- /dev/null +++ b/typescript/packages/mechanisms/evm/src/shared/permit2.ts @@ -0,0 +1,696 @@ +import { + PaymentPayload, + PaymentPayloadResult, + PaymentRequirements, + FacilitatorContext, + SettleResponse, + VerifyResponse, +} from "@x402/core/types"; +import { + extractEip2612GasSponsoringInfo, + validateEip2612GasSponsoringInfo, + extractErc20ApprovalGasSponsoringInfo, + ERC20_APPROVAL_GAS_SPONSORING_KEY, + type Eip2612GasSponsoringInfo, + type Erc20ApprovalGasSponsoringFacilitatorExtension, + type Erc20ApprovalGasSponsoringSigner, +} from "../exact/extensions"; +import { getAddress, encodeFunctionData } from "viem"; +import { PERMIT2_ADDRESS, eip3009ABI, erc20AllowanceAbi, permit2WitnessTypes } from "../constants"; +import { multicall, ContractCall } from "../multicall"; +import { createPermit2Nonce, getEvmChainId } from "../utils"; +import { + ErrPermit2612AmountMismatch, + ErrPermit2InvalidAmount, + ErrPermit2InvalidDestination, + ErrPermit2InvalidNonce, + ErrPermit2InvalidOwner, + ErrPermit2InvalidSignature, + ErrPermit2PaymentTooEarly, + ErrPermit2AllowanceRequired, + ErrPermit2SimulationFailed, + ErrPermit2InsufficientBalance, + ErrPermit2ProxyNotDeployed, + ErrInvalidTransactionState, + ErrTransactionFailed, + ErrInvalidEip2612ExtensionFormat, + ErrEip2612FromMismatch, + ErrEip2612AssetMismatch, + ErrEip2612SpenderNotPermit2, + ErrEip2612DeadlineExpired, + ErrErc20ApprovalTxFailed, +} from "../exact/facilitator/errors"; +import { ClientEvmSigner, FacilitatorEvmSigner } from "../signer"; +import { ExactPermit2Payload, Permit2Authorization, UptoPermit2Payload } from "../types"; +import { validateErc20ApprovalForPayment } from "./erc20approval"; +import { + ErrUptoAmountExceedsPermitted, + ErrUptoUnauthorizedFacilitator, +} from "../upto/facilitator/errors"; + +/** + * Base type for Permit2 payloads shared between exact and upto schemes. + * Both {@link ExactPermit2Payload} and {@link UptoPermit2Payload} satisfy this type. + */ +export type Permit2PayloadBase = ExactPermit2Payload | UptoPermit2Payload; + +/** + * Configuration for the Permit2 proxy contract used during settlement. + * The exact and upto schemes use different proxy addresses and ABIs. + */ +export type Permit2ProxyConfig = { + /** The deployed proxy contract address. */ + proxyAddress: `0x${string}`; + /** The proxy contract ABI (must include settle and settleWithPermit functions). */ + proxyABI: readonly Record[]; +}; + +/** + * Checks Permit2 allowance and validates gas sponsoring extensions if allowance is insufficient. + * + * When the on-chain ERC-20 allowance to the Permit2 contract is below the required amount, + * this function falls back to validating EIP-2612 or ERC-20 approval gas sponsoring extensions + * attached to the payment payload. + * + * @param signer - The facilitator signer for on-chain reads + * @param payload - The payment payload + * @param requirements - The payment requirements + * @param payer - The payer address + * @param tokenAddress - The token contract address + * @param context - Optional facilitator context for extension lookup + * @returns A VerifyResponse if verification should stop (failure), or null to continue + */ +export async function verifyPermit2Allowance( + signer: FacilitatorEvmSigner, + payload: PaymentPayload, + requirements: PaymentRequirements, + payer: `0x${string}`, + tokenAddress: `0x${string}`, + context?: FacilitatorContext, +): Promise { + try { + const allowance = (await signer.readContract({ + address: tokenAddress, + abi: erc20AllowanceAbi, + functionName: "allowance", + args: [payer, PERMIT2_ADDRESS], + })) as bigint; + + if (allowance >= BigInt(requirements.amount)) { + return null; // Sufficient allowance, continue verification + } + + // Allowance insufficient — try EIP-2612 gas sponsoring first + const eip2612Info = extractEip2612GasSponsoringInfo(payload); + if (eip2612Info) { + const result = validateEip2612PermitForPayment(eip2612Info, payer, tokenAddress); + if (!result.isValid) { + return { isValid: false, invalidReason: result.invalidReason!, payer }; + } + return null; // EIP-2612 is valid, allowance will be set atomically during settlement + } + + // Try ERC-20 approval gas sponsoring as fallback + const erc20GasSponsorshipExtension = + context?.getExtension( + ERC20_APPROVAL_GAS_SPONSORING_KEY, + ); + if (erc20GasSponsorshipExtension) { + const erc20Info = extractErc20ApprovalGasSponsoringInfo(payload); + if (erc20Info) { + const result = await validateErc20ApprovalForPayment(erc20Info, payer, tokenAddress); + if (!result.isValid) { + return { isValid: false, invalidReason: result.invalidReason!, payer }; + } + return null; // ERC-20 approval is valid, will be broadcast before settlement + } + } + + return { isValid: false, invalidReason: "permit2_allowance_required", payer }; + } catch { + // Allowance check failed — validate extensions if present; fail closed if none valid + const eip2612Info = extractEip2612GasSponsoringInfo(payload); + if (eip2612Info) { + const result = validateEip2612PermitForPayment(eip2612Info, payer, tokenAddress); + if (!result.isValid) { + return { isValid: false, invalidReason: result.invalidReason!, payer }; + } + return null; + } + + const erc20GasSponsorshipExtension = + context?.getExtension( + ERC20_APPROVAL_GAS_SPONSORING_KEY, + ); + if (erc20GasSponsorshipExtension) { + const erc20Info = extractErc20ApprovalGasSponsoringInfo(payload); + if (erc20Info) { + const result = await validateErc20ApprovalForPayment(erc20Info, payer, tokenAddress); + if (!result.isValid) { + return { isValid: false, invalidReason: result.invalidReason!, payer }; + } + return null; + } + } + + return { isValid: false, invalidReason: "permit2_allowance_required", payer }; + } +} + +/** + * Waits for a transaction receipt and returns the appropriate SettleResponse. + * + * @param signer - Signer with waitForTransactionReceipt capability + * @param tx - The transaction hash to wait for + * @param payload - The payment payload (for network info) + * @param payer - The payer address + * @returns Promise resolving to a settlement response indicating success or failure + */ +export async function waitAndReturnSettleResponse( + signer: Pick, + tx: `0x${string}`, + payload: PaymentPayload, + payer: `0x${string}`, +): Promise { + const receipt = await signer.waitForTransactionReceipt({ hash: tx }); + + if (receipt.status !== "success") { + return { + success: false, + errorReason: ErrInvalidTransactionState, + transaction: tx, + network: payload.accepted.network, + payer, + }; + } + + return { + success: true, + transaction: tx, + network: payload.accepted.network, + payer, + }; +} + +/** + * Maps contract revert errors to structured SettleResponse error reasons. + * + * Inspects the error message for known contract revert strings and maps them + * to the corresponding error reason constants. Falls back to a generic + * "transaction_failed" reason with truncated message for unrecognized errors. + * + * @param error - The caught error (typically from a contract write) + * @param payload - The payment payload (for network info) + * @param payer - The payer address + * @returns A failed SettleResponse with the mapped error reason + */ +export function mapSettleError( + error: unknown, + payload: PaymentPayload, + payer: `0x${string}`, +): SettleResponse { + let errorReason: string = ErrTransactionFailed; + if (error instanceof Error) { + const message = error.message; + if (message.includes("Permit2612AmountMismatch")) { + errorReason = ErrPermit2612AmountMismatch; + } else if (message.includes("InvalidAmount")) { + errorReason = ErrPermit2InvalidAmount; + } else if (message.includes("InvalidDestination")) { + errorReason = ErrPermit2InvalidDestination; + } else if (message.includes("InvalidOwner")) { + errorReason = ErrPermit2InvalidOwner; + } else if (message.includes("PaymentTooEarly")) { + errorReason = ErrPermit2PaymentTooEarly; + } else if (message.includes("InvalidSignature") || message.includes("SignatureExpired")) { + errorReason = ErrPermit2InvalidSignature; + } else if (message.includes("InvalidNonce")) { + errorReason = ErrPermit2InvalidNonce; + } else if (message.includes("erc20_approval_tx_failed")) { + errorReason = ErrErc20ApprovalTxFailed; + } else if (message.includes("AmountExceedsPermitted")) { + errorReason = ErrUptoAmountExceedsPermitted; + } else if (message.includes("UnauthorizedFacilitator")) { + errorReason = ErrUptoUnauthorizedFacilitator; + } else { + errorReason = `${ErrTransactionFailed}: ${message.slice(0, 500)}`; + } + } + return { + success: false, + errorReason, + transaction: "", + network: payload.accepted.network, + payer, + }; +} + +/** + * Validates EIP-2612 permit extension data for a Permit2 payment. + * + * Checks that the permit extension has a valid format and that the from address, + * asset address, spender address, and deadline all match expectations. + * + * @param info - The EIP-2612 gas sponsoring info extracted from the payment payload + * @param payer - The expected payer address + * @param tokenAddress - The expected token address + * @returns Validation result with isValid flag and optional invalidReason string + */ +export function validateEip2612PermitForPayment( + info: Eip2612GasSponsoringInfo, + payer: `0x${string}`, + tokenAddress: `0x${string}`, +): { isValid: boolean; invalidReason?: string } { + if (!validateEip2612GasSponsoringInfo(info)) { + return { isValid: false, invalidReason: ErrInvalidEip2612ExtensionFormat }; + } + + if (getAddress(info.from as `0x${string}`) !== getAddress(payer)) { + return { isValid: false, invalidReason: ErrEip2612FromMismatch }; + } + + if (getAddress(info.asset as `0x${string}`) !== tokenAddress) { + return { isValid: false, invalidReason: ErrEip2612AssetMismatch }; + } + + if (getAddress(info.spender as `0x${string}`) !== getAddress(PERMIT2_ADDRESS)) { + return { isValid: false, invalidReason: ErrEip2612SpenderNotPermit2 }; + } + + const now = Math.floor(Date.now() / 1000); + if (BigInt(info.deadline) < BigInt(now + 6)) { + return { isValid: false, invalidReason: ErrEip2612DeadlineExpired }; + } + + return { isValid: true }; +} + +// --------------------------------------------------------------------------- +// Simulation helpers (shared across exact and upto) +// --------------------------------------------------------------------------- + +/** + * Simulates settle() via eth_call (readContract). + * Returns true if simulation succeeded, false if it failed. + * + * @param config - The proxy contract configuration (address and ABI) + * @param signer - EVM signer for contract reads + * @param settleArgs - Pre-built settle function arguments (scheme-specific) + * @returns true if simulation succeeded, false if it failed + */ +export async function simulatePermit2Settle( + config: Permit2ProxyConfig, + signer: FacilitatorEvmSigner, + settleArgs: readonly unknown[], +): Promise { + try { + await signer.readContract({ + address: config.proxyAddress, + abi: config.proxyABI, + functionName: "settle", + args: settleArgs, + }); + return true; + } catch { + return false; + } +} + +/** + * Simulates settleWithPermit() via eth_call (readContract). + * The contract atomically calls token.permit() then PERMIT2.permitTransferFrom(), + * so simulation covers allowance + balance + nonces. + * + * @param config - The proxy contract configuration (address and ABI) + * @param signer - EVM signer for contract reads + * @param settleArgs - Pre-built settle function arguments (scheme-specific) + * @param eip2612Info - EIP-2612 gas sponsoring info from the payload extension + * @returns true if simulation succeeded, false if it failed + */ +export async function simulatePermit2SettleWithPermit( + config: Permit2ProxyConfig, + signer: FacilitatorEvmSigner, + settleArgs: readonly unknown[], + eip2612Info: Eip2612GasSponsoringInfo, +): Promise { + try { + const { v, r, s } = splitEip2612Signature(eip2612Info.signature); + + await signer.readContract({ + address: config.proxyAddress, + abi: config.proxyABI, + functionName: "settleWithPermit", + args: [ + { + value: BigInt(eip2612Info.amount), + deadline: BigInt(eip2612Info.deadline), + r, + s, + v, + }, + ...settleArgs, + ], + }); + return true; + } catch { + return false; + } +} + +/** + * Delegates the full approve+settle simulation flow to the extension signer via simulateTransactions. + * The signer owns execution strategy. + * + * @param config - The proxy contract configuration (address and ABI) + * @param extensionSigner - The extension signer with simulateTransactions capability + * @param settleArgs - Pre-built settle function arguments (scheme-specific) + * @param erc20Info - Object containing the signed approval transaction + * @param erc20Info.signedTransaction - The RLP-encoded signed ERC-20 approve transaction hex string + * @returns true if the bundle simulation succeeded, false otherwise + */ +export async function simulatePermit2SettleWithErc20Approval( + config: Permit2ProxyConfig, + extensionSigner: Erc20ApprovalGasSponsoringSigner, + settleArgs: readonly unknown[], + erc20Info: { signedTransaction: string }, +): Promise { + if (!extensionSigner.simulateTransactions) { + return false; + } + + try { + const settleData = encodeFunctionData({ + abi: config.proxyABI, + functionName: "settle", + args: settleArgs, + }); + + return await extensionSigner.simulateTransactions([ + erc20Info.signedTransaction as `0x${string}`, + { to: config.proxyAddress, data: settleData, gas: BigInt(300_000) }, + ]); + } catch { + return false; + } +} + +/** + * Diagnoses a Permit2 simulation failure by performing a multicall to check the proxy deployment, balance and allowance. + * + * @param config - The proxy contract configuration (address and ABI) + * @param signer - EVM signer for contract reads + * @param tokenAddress - ERC-20 token contract address + * @param permit2Payload - The Permit2 authorization payload + * @param amountRequired - Required payment amount (as string) + * @returns VerifyResponse with the most specific failure reason + */ +export async function diagnosePermit2SimulationFailure( + config: Permit2ProxyConfig, + signer: FacilitatorEvmSigner, + tokenAddress: `0x${string}`, + permit2Payload: Permit2PayloadBase, + amountRequired: string, +): Promise { + const payer = permit2Payload.permit2Authorization.from; + + const diagnosticCalls: ContractCall[] = [ + { + address: config.proxyAddress, + abi: config.proxyABI, + functionName: "PERMIT2", + }, + { + address: tokenAddress, + abi: eip3009ABI, + functionName: "balanceOf", + args: [payer], + }, + { + address: tokenAddress, + abi: erc20AllowanceAbi, + functionName: "allowance", + args: [payer, PERMIT2_ADDRESS], + }, + ]; + + try { + const results = await multicall(signer.readContract.bind(signer), diagnosticCalls); + + const [proxyResult, balanceResult, allowanceResult] = results; + + if (proxyResult.status === "failure") { + return { isValid: false, invalidReason: ErrPermit2ProxyNotDeployed, payer }; + } + + if (balanceResult.status === "success") { + const balance = balanceResult.result as bigint; + if (balance < BigInt(amountRequired)) { + return { isValid: false, invalidReason: ErrPermit2InsufficientBalance, payer }; + } + } + + if (allowanceResult.status === "success") { + const allowance = allowanceResult.result as bigint; + if (allowance < BigInt(amountRequired)) { + return { isValid: false, invalidReason: ErrPermit2AllowanceRequired, payer }; + } + } + } catch { + // Diagnostic multicall itself failed — fall through to generic error + } + + return { isValid: false, invalidReason: ErrPermit2SimulationFailed, payer }; +} + +/** + * Targeted multicall for the ERC-20 approval path where simulation cannot be used + * (the approval hasn't been broadcast yet, so settle() would fail for expected reasons). + * Checks proxy deployment, payer token balance and payer ETH balance for gas. + * + * @param config - The proxy contract configuration (address and ABI) + * @param signer - EVM signer for contract reads + * @param tokenAddress - ERC-20 token contract address + * @param payer - The payer address + * @param amountRequired - Required payment amount (as string) + * @returns VerifyResponse — valid if checks pass, otherwise the most specific failure + */ +export async function checkPermit2Prerequisites( + config: Permit2ProxyConfig, + signer: FacilitatorEvmSigner, + tokenAddress: `0x${string}`, + payer: `0x${string}`, + amountRequired: string, +): Promise { + const diagnosticCalls: ContractCall[] = [ + { + address: config.proxyAddress, + abi: config.proxyABI, + functionName: "PERMIT2", + }, + { + address: tokenAddress, + abi: eip3009ABI, + functionName: "balanceOf", + args: [payer], + }, + ]; + + try { + const results = await multicall(signer.readContract.bind(signer), diagnosticCalls); + + const [proxyResult, balanceResult] = results; + + if (proxyResult.status === "failure") { + return { isValid: false, invalidReason: ErrPermit2ProxyNotDeployed, payer }; + } + + if (balanceResult.status === "success") { + const balance = balanceResult.result as bigint; + if (balance < BigInt(amountRequired)) { + return { isValid: false, invalidReason: ErrPermit2InsufficientBalance, payer }; + } + } + } catch { + // Multicall failed — fall through to valid (fail open for prerequisites-only check) + } + + return { isValid: true, invalidReason: undefined, payer }; +} + +/** + * Builds args for exact settle(permit, owner, witness, signature). + * + * @param permit2Payload - The Permit2 payload containing authorization and signature data + * @returns Tuple of contract call arguments for the exact settle function + */ +export function buildExactPermit2SettleArgs(permit2Payload: Permit2PayloadBase) { + return [ + { + permitted: { + token: getAddress(permit2Payload.permit2Authorization.permitted.token), + amount: BigInt(permit2Payload.permit2Authorization.permitted.amount), + }, + nonce: BigInt(permit2Payload.permit2Authorization.nonce), + deadline: BigInt(permit2Payload.permit2Authorization.deadline), + }, + getAddress(permit2Payload.permit2Authorization.from), + { + to: getAddress(permit2Payload.permit2Authorization.witness.to), + validAfter: BigInt(permit2Payload.permit2Authorization.witness.validAfter), + }, + permit2Payload.signature, + ] as const; +} + +/** + * Builds args for upto settle(permit, amount, owner, witness, signature). + * The upto contract's settle() takes an additional `amount` parameter and the witness + * includes a `facilitator` field. + * + * @param permit2Payload - The upto Permit2 payload containing authorization and signature data + * @param settlementAmount - The amount to settle on-chain + * @param facilitatorAddress - The facilitator address authorized in the witness + * @returns Tuple of contract call arguments for the upto settle function + */ +export function buildUptoPermit2SettleArgs( + permit2Payload: UptoPermit2Payload, + settlementAmount: bigint, + facilitatorAddress: `0x${string}`, +) { + return [ + { + permitted: { + token: getAddress(permit2Payload.permit2Authorization.permitted.token), + amount: BigInt(permit2Payload.permit2Authorization.permitted.amount), + }, + nonce: BigInt(permit2Payload.permit2Authorization.nonce), + deadline: BigInt(permit2Payload.permit2Authorization.deadline), + }, + settlementAmount, + getAddress(permit2Payload.permit2Authorization.from), + { + to: getAddress(permit2Payload.permit2Authorization.witness.to), + facilitator: getAddress(facilitatorAddress), + validAfter: BigInt(permit2Payload.permit2Authorization.witness.validAfter), + }, + permit2Payload.signature, + ] as const; +} + +/** + * Splits a 65-byte EIP-2612 signature into v, r, s components. + * + * @param signature - The hex-encoded 65-byte signature (with or without 0x prefix) + * @returns Object with v (uint8), r (bytes32 hex), s (bytes32 hex) + * @throws Error if the signature is not exactly 65 bytes (130 hex chars) + */ +export function splitEip2612Signature(signature: string): { + v: number; + r: `0x${string}`; + s: `0x${string}`; +} { + const sig = signature.startsWith("0x") ? signature.slice(2) : signature; + + if (sig.length !== 130) { + throw new Error( + `invalid EIP-2612 signature length: expected 65 bytes (130 hex chars), got ${sig.length / 2} bytes`, + ); + } + + const r = `0x${sig.slice(0, 64)}` as `0x${string}`; + const s = `0x${sig.slice(64, 128)}` as `0x${string}`; + const v = parseInt(sig.slice(128, 130), 16); + + return { v, r, s }; +} + +// --------------------------------------------------------------------------- +// Client-side helpers +// --------------------------------------------------------------------------- + +/** + * Creates a Permit2 payload for any scheme (exact or upto). + * The only scheme-specific input is the proxy address used as the spender. + * + * @param proxyAddress - The x402 proxy contract address to set as spender + * @param signer - The EVM client signer + * @param x402Version - The x402 protocol version + * @param paymentRequirements - The payment requirements + * @returns Promise resolving to a payment payload result + */ +export async function createPermit2PayloadForProxy( + proxyAddress: `0x${string}`, + signer: ClientEvmSigner, + x402Version: number, + paymentRequirements: PaymentRequirements, +): Promise { + const now = Math.floor(Date.now() / 1000); + const nonce = createPermit2Nonce(); + + // Lower time bound - allow some clock skew + const validAfter = (now - 600).toString(); + // Upper time bound is enforced by Permit2's deadline field + const deadline = (now + paymentRequirements.maxTimeoutSeconds).toString(); + + const permit2Authorization: Permit2Authorization & { from: `0x${string}` } = { + from: signer.address, + permitted: { + token: getAddress(paymentRequirements.asset), + amount: paymentRequirements.amount, + }, + spender: proxyAddress, + nonce, + deadline, + witness: { + to: getAddress(paymentRequirements.payTo), + validAfter, + }, + }; + + const signature = await signPermit2Authorization( + signer, + permit2Authorization, + paymentRequirements, + ); + + return { + x402Version, + payload: { signature, permit2Authorization }, + }; +} + +/** + * Signs a Permit2 authorization using EIP-712 with witness data. + * The signature authorizes the proxy contract to transfer tokens on behalf of the signer. + * + * @param signer - The EVM client signer + * @param permit2Authorization - The Permit2 authorization parameters + * @param requirements - The payment requirements + * @returns Promise resolving to the hex-encoded signature + */ +async function signPermit2Authorization( + signer: ClientEvmSigner, + permit2Authorization: Permit2Authorization & { from: `0x${string}` }, + requirements: PaymentRequirements, +): Promise<`0x${string}`> { + const chainId = getEvmChainId(requirements.network); + + return await signer.signTypedData({ + domain: { name: "Permit2", chainId, verifyingContract: PERMIT2_ADDRESS }, + types: permit2WitnessTypes, + primaryType: "PermitWitnessTransferFrom", + message: { + permitted: { + token: getAddress(permit2Authorization.permitted.token), + amount: BigInt(permit2Authorization.permitted.amount), + }, + spender: getAddress(permit2Authorization.spender), + nonce: BigInt(permit2Authorization.nonce), + deadline: BigInt(permit2Authorization.deadline), + witness: { + to: getAddress(permit2Authorization.witness.to), + validAfter: BigInt(permit2Authorization.witness.validAfter), + }, + }, + }); +} diff --git a/typescript/packages/mechanisms/evm/src/shared/rpc.ts b/typescript/packages/mechanisms/evm/src/shared/rpc.ts new file mode 100644 index 0000000000..1ad839e43d --- /dev/null +++ b/typescript/packages/mechanisms/evm/src/shared/rpc.ts @@ -0,0 +1,123 @@ +import { createPublicClient, http } from "viem"; +import type { ClientEvmSigner } from "../signer"; +import { getEvmChainId } from "../utils"; + +export type EvmSchemeConfig = { + rpcUrl?: string; +}; + +export type EvmSchemeConfigByChainId = Record; + +export type EvmSchemeOptions = EvmSchemeConfig | EvmSchemeConfigByChainId; + +/** @deprecated Use EvmSchemeConfig */ +export type ExactEvmSchemeConfig = EvmSchemeConfig; +/** @deprecated Use EvmSchemeConfigByChainId */ +export type ExactEvmSchemeConfigByChainId = EvmSchemeConfigByChainId; +/** @deprecated Use EvmSchemeOptions */ +export type ExactEvmSchemeOptions = EvmSchemeOptions; + +type ExtensionRpcCapabilities = Pick< + ClientEvmSigner, + "readContract" | "signTransaction" | "getTransactionCount" | "estimateFeesPerGas" +>; + +const rpcClientCache = new Map>(); + +/** + * Check if options is a per-chain-id configuration map. + * + * @param options - The EVM scheme options to check + * @returns True if the options are keyed by chain ID + */ +function isConfigByChainId(options: EvmSchemeOptions): options is EvmSchemeConfigByChainId { + const keys = Object.keys(options); + return keys.length > 0 && keys.every(key => /^\d+$/.test(key)); +} + +/** + * Get or create a cached viem public client for the given RPC URL. + * + * @param rpcUrl - The JSON-RPC endpoint URL + * @returns A viem PublicClient instance + */ +function getRpcClient(rpcUrl: string): ReturnType { + const existing = rpcClientCache.get(rpcUrl); + if (existing) { + return existing; + } + + const client = createPublicClient({ + transport: http(rpcUrl), + }); + rpcClientCache.set(rpcUrl, client); + return client; +} + +/** + * Resolve an RPC URL from scheme options for the given network. + * + * @param network - The CAIP-2 network identifier + * @param options - Optional EVM scheme options (flat or per-chain-id) + * @returns The resolved RPC URL, or undefined if not configured + */ +export function resolveRpcUrl(network: string, options?: EvmSchemeOptions): string | undefined { + if (!options) { + return undefined; + } + + if (isConfigByChainId(options)) { + const chainId = getEvmChainId(network); + const optionsByChainId = options as EvmSchemeConfigByChainId; + return optionsByChainId[chainId]?.rpcUrl; + } + + return (options as EvmSchemeConfig).rpcUrl; +} + +/** + * Resolve RPC capabilities for extensions, backfilling from a public RPC client when the signer lacks them. + * + * @param network - The CAIP-2 network identifier + * @param signer - The client EVM signer + * @param options - Optional EVM scheme options for RPC URL resolution + * @returns Extension RPC capabilities (readContract, signTransaction, etc.) + */ +export function resolveExtensionRpcCapabilities( + network: string, + signer: ClientEvmSigner, + options?: EvmSchemeOptions, +): ExtensionRpcCapabilities { + const capabilities: ExtensionRpcCapabilities = { + signTransaction: signer.signTransaction, + readContract: signer.readContract, + getTransactionCount: signer.getTransactionCount, + estimateFeesPerGas: signer.estimateFeesPerGas, + }; + + const needsRpcBackfill = + !capabilities.readContract || + !capabilities.getTransactionCount || + !capabilities.estimateFeesPerGas; + if (!needsRpcBackfill) { + return capabilities; + } + + const rpcUrl = resolveRpcUrl(network, options); + if (!rpcUrl) { + return capabilities; + } + const rpcClient = getRpcClient(rpcUrl); + if (!capabilities.readContract) { + capabilities.readContract = args => rpcClient.readContract(args as never) as Promise; + } + if (!capabilities.getTransactionCount) { + capabilities.getTransactionCount = async args => + rpcClient.getTransactionCount({ address: args.address }); + } + if (!capabilities.estimateFeesPerGas) { + capabilities.estimateFeesPerGas = async () => rpcClient.estimateFeesPerGas(); + } + + return capabilities; +} diff --git a/typescript/packages/mechanisms/evm/src/signer.ts b/typescript/packages/mechanisms/evm/src/signer.ts index 3e6f79bdfd..66ec3de62d 100644 --- a/typescript/packages/mechanisms/evm/src/signer.ts +++ b/typescript/packages/mechanisms/evm/src/signer.ts @@ -20,7 +20,11 @@ export type ClientEvmSigner = { primaryType: string; message: Record; }): Promise<`0x${string}`>; - readContract(args: { + /** + * Optional on-chain reads. + * Required only for extension enrichment (EIP-2612 / ERC-20 approval). + */ + readContract?(args: { address: `0x${string}`; abi: readonly unknown[]; functionName: string; @@ -84,6 +88,8 @@ export type FacilitatorEvmSigner = { abi: readonly unknown[]; functionName: string; args: readonly unknown[]; + /** Optional gas limit. When provided, skips eth_estimateGas simulation. */ + gas?: bigint; }): Promise<`0x${string}`>; sendTransaction(args: { to: `0x${string}`; data: `0x${string}` }): Promise<`0x${string}`>; waitForTransactionReceipt(args: { hash: `0x${string}` }): Promise<{ status: string }>; @@ -106,11 +112,11 @@ export type FacilitatorEvmSigner = { * ``` * * @param signer - A signer with `address` and `signTypedData` (and optionally `readContract`) - * @param publicClient - A client with `readContract` (required if signer lacks it) + * @param publicClient - A client with optional read/nonce/fee helpers * @param publicClient.readContract - The readContract method from the public client * @param publicClient.getTransactionCount - Optional getTransactionCount for ERC-20 approval * @param publicClient.estimateFeesPerGas - Optional estimateFeesPerGas for ERC-20 approval - * @returns A complete ClientEvmSigner + * @returns A ClientEvmSigner with any available optional capabilities * * @example * ```typescript @@ -136,19 +142,15 @@ export function toClientEvmSigner( ): ClientEvmSigner { const readContract = signer.readContract ?? publicClient?.readContract.bind(publicClient); - if (!readContract) { - throw new Error( - "toClientEvmSigner requires either a signer with readContract or a publicClient. " + - "Use createWalletClient(...).extend(publicActions) or pass a publicClient.", - ); - } - const result: ClientEvmSigner = { address: signer.address, signTypedData: msg => signer.signTypedData(msg), - readContract, }; + if (readContract) { + result.readContract = readContract; + } + // Forward optional capabilities from signer or publicClient const signTransaction = signer.signTransaction; if (signTransaction) { diff --git a/typescript/packages/mechanisms/evm/src/types.ts b/typescript/packages/mechanisms/evm/src/types.ts index ca243ca96f..04633277ae 100644 --- a/typescript/packages/mechanisms/evm/src/types.ts +++ b/typescript/packages/mechanisms/evm/src/types.ts @@ -80,3 +80,70 @@ export function isPermit2Payload(payload: ExactEvmPayloadV2): payload is ExactPe export function isEIP3009Payload(payload: ExactEvmPayloadV2): payload is ExactEIP3009Payload { return "authorization" in payload; } + +/** + * Upto Permit2 witness — includes `facilitator` field absent from exact witness. + * Only the address matching `witness.facilitator` can call settle() on-chain. + */ +export type UptoPermit2Witness = { + to: `0x${string}`; + facilitator: `0x${string}`; + validAfter: string; +}; + +export type UptoPermit2Authorization = { + permitted: { + token: `0x${string}`; + amount: string; + }; + spender: `0x${string}`; + nonce: string; + deadline: string; + witness: UptoPermit2Witness; +}; + +export type UptoPermit2Payload = { + signature: `0x${string}`; + permit2Authorization: UptoPermit2Authorization & { + from: `0x${string}`; + }; +}; + +/** + * Type guard to check if a payload is an upto Permit2 payload. + * Validates structural presence of all required fields: signature, permit2Authorization + * (with from, permitted, spender, nonce, deadline), and a witness containing facilitator. + * + * @param payload - The payload to check + * @returns True if the payload is an upto Permit2 payload, false otherwise + */ +export function isUptoPermit2Payload( + payload: Record, +): payload is UptoPermit2Payload { + if (typeof payload.signature !== "string") return false; + if (!("permit2Authorization" in payload)) return false; + + const auth = payload.permit2Authorization; + if (typeof auth !== "object" || auth === null) return false; + + const a = auth as Record; + if (typeof a.from !== "string") return false; + if (typeof a.spender !== "string") return false; + if (typeof a.nonce !== "string") return false; + if (typeof a.deadline !== "string") return false; + + const permitted = a.permitted; + if (typeof permitted !== "object" || permitted === null) return false; + const p = permitted as Record; + if (typeof p.token !== "string") return false; + if (typeof p.amount !== "string") return false; + + const witness = a.witness; + if (typeof witness !== "object" || witness === null) return false; + const w = witness as Record; + return ( + typeof w.facilitator === "string" && + typeof w.to === "string" && + typeof w.validAfter === "string" + ); +} diff --git a/typescript/packages/mechanisms/evm/src/upto/client/index.ts b/typescript/packages/mechanisms/evm/src/upto/client/index.ts new file mode 100644 index 0000000000..e378396a37 --- /dev/null +++ b/typescript/packages/mechanisms/evm/src/upto/client/index.ts @@ -0,0 +1,16 @@ +// Note: The upto scheme does not provide register.ts convenience helpers (unlike exact). +// The exact scheme's register helpers exist primarily for V1 backward compatibility, +// which is not needed for upto. Use direct class instantiation instead: +// client.register("eip155:*", new UptoEvmScheme(signer, options)) +export { UptoEvmScheme } from "./scheme"; +export type { + UptoEvmSchemeConfig, + UptoEvmSchemeConfigByChainId, + UptoEvmSchemeOptions, +} from "./rpc"; +export { + createPermit2ApprovalTx, + getPermit2AllowanceReadParams, + type Permit2AllowanceParams, +} from "./permit2"; +export { erc20AllowanceAbi } from "../../constants"; diff --git a/typescript/packages/mechanisms/evm/src/upto/client/permit2.ts b/typescript/packages/mechanisms/evm/src/upto/client/permit2.ts new file mode 100644 index 0000000000..41d2817c80 --- /dev/null +++ b/typescript/packages/mechanisms/evm/src/upto/client/permit2.ts @@ -0,0 +1,96 @@ +import { PaymentRequirements, PaymentPayloadResult } from "@x402/core/types"; +import { + PERMIT2_ADDRESS, + uptoPermit2WitnessTypes, + x402UptoPermit2ProxyAddress, +} from "../../constants"; +import { ClientEvmSigner } from "../../signer"; +import { UptoPermit2Authorization } from "../../types"; +import { createPermit2Nonce, getEvmChainId } from "../../utils"; +import { getAddress } from "viem"; + +// Re-export Permit2-generic approval helpers +export { createPermit2ApprovalTx, getPermit2AllowanceReadParams } from "../../exact/client/permit2"; +export type { Permit2AllowanceParams } from "../../exact/client/permit2"; + +/** + * Creates a signed upto Permit2 payment payload for the given requirements. + * + * Constructs a Permit2 authorization with an upto witness (including facilitator address) + * and signs it using EIP-712 typed data. + * + * @param signer - The EVM client signer for signing typed data + * @param x402Version - The x402 protocol version + * @param paymentRequirements - The payment requirements including asset, amount, and payTo + * @returns Promise resolving to a payment payload result with the signed authorization + */ +export async function createUptoPermit2Payload( + signer: ClientEvmSigner, + x402Version: number, + paymentRequirements: PaymentRequirements, +): Promise { + const facilitatorAddress = paymentRequirements.extra?.facilitatorAddress as + | `0x${string}` + | undefined; + if (!facilitatorAddress) { + throw new Error( + "upto scheme requires facilitatorAddress in paymentRequirements.extra. " + + "Ensure the server is configured with an upto facilitator that provides getExtra().", + ); + } + + const now = Math.floor(Date.now() / 1000); + const nonce = createPermit2Nonce(); + const validAfter = (now - 600).toString(); + const deadline = (now + paymentRequirements.maxTimeoutSeconds).toString(); + + if (BigInt(deadline) <= BigInt(validAfter)) { + throw new Error( + `Invalid time window: deadline (${deadline}) must be after validAfter (${validAfter}). ` + + `Check that maxTimeoutSeconds (${paymentRequirements.maxTimeoutSeconds}) is positive.`, + ); + } + + const permit2Authorization: UptoPermit2Authorization & { from: `0x${string}` } = { + from: signer.address, + permitted: { + token: getAddress(paymentRequirements.asset), + amount: paymentRequirements.amount, + }, + spender: x402UptoPermit2ProxyAddress, + nonce, + deadline, + witness: { + to: getAddress(paymentRequirements.payTo), + facilitator: getAddress(facilitatorAddress), + validAfter, + }, + }; + + const chainId = getEvmChainId(paymentRequirements.network); + + const signature = await signer.signTypedData({ + domain: { name: "Permit2", chainId, verifyingContract: PERMIT2_ADDRESS }, + types: uptoPermit2WitnessTypes, + primaryType: "PermitWitnessTransferFrom", + message: { + permitted: { + token: getAddress(permit2Authorization.permitted.token), + amount: BigInt(permit2Authorization.permitted.amount), + }, + spender: getAddress(permit2Authorization.spender), + nonce: BigInt(permit2Authorization.nonce), + deadline: BigInt(permit2Authorization.deadline), + witness: { + to: getAddress(permit2Authorization.witness.to), + facilitator: getAddress(permit2Authorization.witness.facilitator), + validAfter: BigInt(permit2Authorization.witness.validAfter), + }, + }, + }); + + return { + x402Version, + payload: { signature, permit2Authorization }, + }; +} diff --git a/typescript/packages/mechanisms/evm/src/upto/client/rpc.ts b/typescript/packages/mechanisms/evm/src/upto/client/rpc.ts new file mode 100644 index 0000000000..1bba7da512 --- /dev/null +++ b/typescript/packages/mechanisms/evm/src/upto/client/rpc.ts @@ -0,0 +1,4 @@ +export type { EvmSchemeConfig as UptoEvmSchemeConfig } from "../../shared/rpc"; +export type { EvmSchemeConfigByChainId as UptoEvmSchemeConfigByChainId } from "../../shared/rpc"; +export type { EvmSchemeOptions as UptoEvmSchemeOptions } from "../../shared/rpc"; +export { resolveExtensionRpcCapabilities } from "../../shared/rpc"; diff --git a/typescript/packages/mechanisms/evm/src/upto/client/scheme.ts b/typescript/packages/mechanisms/evm/src/upto/client/scheme.ts new file mode 100644 index 0000000000..d32e3c6d4d --- /dev/null +++ b/typescript/packages/mechanisms/evm/src/upto/client/scheme.ts @@ -0,0 +1,71 @@ +import { + SchemeNetworkClient, + PaymentRequirements, + PaymentPayloadResult, + PaymentPayloadContext, +} from "@x402/core/types"; +import { ClientEvmSigner } from "../../signer"; +import { createUptoPermit2Payload } from "./permit2"; +import { + trySignEip2612PermitExtension, + trySignErc20ApprovalExtension, +} from "../../shared/extensions"; +import { UptoEvmSchemeOptions } from "./rpc"; + +/** + * EVM client implementation for the Upto payment scheme. + * Handles Permit2-based payment payload creation and gas-sponsoring extensions. + */ +export class UptoEvmScheme implements SchemeNetworkClient { + readonly scheme = "upto"; + + /** + * Creates a new UptoEvmScheme instance. + * + * @param signer - The EVM signer for client operations + * @param options - Optional RPC configuration + */ + constructor( + private readonly signer: ClientEvmSigner, + private readonly options?: UptoEvmSchemeOptions, + ) {} + + /** + * Creates a payment payload for the Upto scheme using Permit2. + * + * @param x402Version - The x402 protocol version + * @param paymentRequirements - The payment requirements + * @param context - Optional context with server-declared extensions + * @returns Promise resolving to a payment payload result + */ + async createPaymentPayload( + x402Version: number, + paymentRequirements: PaymentRequirements, + context?: PaymentPayloadContext, + ): Promise { + const result = await createUptoPermit2Payload(this.signer, x402Version, paymentRequirements); + + const eip2612Extensions = await trySignEip2612PermitExtension( + this.signer, + this.options, + paymentRequirements, + result, + context, + ); + if (eip2612Extensions) { + return { ...result, extensions: eip2612Extensions }; + } + + const erc20Extensions = await trySignErc20ApprovalExtension( + this.signer, + this.options, + paymentRequirements, + context, + ); + if (erc20Extensions) { + return { ...result, extensions: erc20Extensions }; + } + + return result; + } +} diff --git a/typescript/packages/mechanisms/evm/src/upto/extensions.ts b/typescript/packages/mechanisms/evm/src/upto/extensions.ts new file mode 100644 index 0000000000..f02acd75e1 --- /dev/null +++ b/typescript/packages/mechanisms/evm/src/upto/extensions.ts @@ -0,0 +1 @@ +export { EIP2612_GAS_SPONSORING_KEY, ERC20_APPROVAL_GAS_SPONSORING_KEY } from "../exact/extensions"; diff --git a/typescript/packages/mechanisms/evm/src/upto/facilitator/errors.ts b/typescript/packages/mechanisms/evm/src/upto/facilitator/errors.ts new file mode 100644 index 0000000000..08034c0dac --- /dev/null +++ b/typescript/packages/mechanisms/evm/src/upto/facilitator/errors.ts @@ -0,0 +1,46 @@ +/** + * Named error reason constants for the upto EVM facilitator. + * + * Shared Permit2 errors are re-exported from exact/facilitator/errors.ts. + * Upto-specific errors are defined here. + * + * These strings must be character-for-character identical to the Go constants + * to maintain cross-SDK parity. + */ + +// Re-export shared Permit2 errors +export { + ErrPermit2InvalidSpender, + ErrPermit2RecipientMismatch, + ErrPermit2DeadlineExpired, + ErrPermit2NotYetValid, + ErrPermit2AmountMismatch, + ErrPermit2TokenMismatch, + ErrPermit2InvalidSignature, + ErrPermit2AllowanceRequired, + ErrPermit2InvalidAmount, + ErrPermit2InvalidDestination, + ErrPermit2InvalidOwner, + ErrPermit2PaymentTooEarly, + ErrPermit2InvalidNonce, + ErrPermit2612AmountMismatch, + ErrErc20ApprovalInvalidFormat, + ErrErc20ApprovalFromMismatch, + ErrErc20ApprovalAssetMismatch, + ErrErc20ApprovalSpenderNotPermit2, + ErrErc20ApprovalTxWrongTarget, + ErrErc20ApprovalTxWrongSelector, + ErrErc20ApprovalTxWrongSpender, + ErrErc20ApprovalTxInvalidCalldata, + ErrErc20ApprovalTxSignerMismatch, + ErrErc20ApprovalTxInvalidSignature, + ErrErc20ApprovalTxParseFailed, +} from "../../exact/facilitator/errors"; + +// Upto-specific errors +export const ErrUptoInvalidScheme = "invalid_upto_evm_scheme"; +export const ErrUptoNetworkMismatch = "invalid_upto_evm_network_mismatch"; +export const ErrUptoSettlementExceedsAmount = "invalid_upto_evm_payload_settlement_exceeds_amount"; +export const ErrUptoAmountExceedsPermitted = "upto_amount_exceeds_permitted"; +export const ErrUptoUnauthorizedFacilitator = "upto_unauthorized_facilitator"; +export const ErrUptoFacilitatorMismatch = "upto_facilitator_mismatch"; diff --git a/typescript/packages/mechanisms/evm/src/upto/facilitator/index.ts b/typescript/packages/mechanisms/evm/src/upto/facilitator/index.ts new file mode 100644 index 0000000000..c24e4afdf2 --- /dev/null +++ b/typescript/packages/mechanisms/evm/src/upto/facilitator/index.ts @@ -0,0 +1,3 @@ +// Note: No register.ts helper — V1 backward compatibility is not needed for upto. +// Use direct class instantiation: facilitator.register("eip155:*", new UptoEvmScheme(signer)) +export { UptoEvmScheme } from "./scheme"; diff --git a/typescript/packages/mechanisms/evm/src/upto/facilitator/permit2.ts b/typescript/packages/mechanisms/evm/src/upto/facilitator/permit2.ts new file mode 100644 index 0000000000..c21e724a3d --- /dev/null +++ b/typescript/packages/mechanisms/evm/src/upto/facilitator/permit2.ts @@ -0,0 +1,580 @@ +import { + PaymentPayload, + PaymentRequirements, + FacilitatorContext, + SettleResponse, + VerifyResponse, +} from "@x402/core/types"; +import { + extractEip2612GasSponsoringInfo, + extractErc20ApprovalGasSponsoringInfo, + ERC20_APPROVAL_GAS_SPONSORING_KEY, + resolveErc20ApprovalExtensionSigner, + type Erc20ApprovalGasSponsoringFacilitatorExtension, + type Erc20ApprovalGasSponsoringSigner, +} from "../../exact/extensions"; +import { getAddress, encodeFunctionData } from "viem"; +import { + PERMIT2_ADDRESS, + uptoPermit2WitnessTypes, + x402UptoPermit2ProxyABI, + x402UptoPermit2ProxyAddress, +} from "../../constants"; +import { + ErrPermit2AmountMismatch, + ErrUptoSettlementExceedsAmount, + ErrUptoFacilitatorMismatch, + ErrUptoInvalidScheme, + ErrUptoNetworkMismatch, +} from "./errors"; +import { FacilitatorEvmSigner } from "../../signer"; +import { UptoPermit2Payload } from "../../types"; +import { getEvmChainId } from "../../utils"; +import { validateErc20ApprovalForPayment } from "../../shared/erc20approval"; +import { + buildUptoPermit2SettleArgs, + waitAndReturnSettleResponse, + mapSettleError, + splitEip2612Signature, + simulatePermit2Settle, + simulatePermit2SettleWithPermit, + simulatePermit2SettleWithErc20Approval, + diagnosePermit2SimulationFailure, + checkPermit2Prerequisites, + validateEip2612PermitForPayment, + type Permit2ProxyConfig, +} from "../../shared/permit2"; +import type { Eip2612GasSponsoringInfo } from "../../exact/extensions"; + +const uptoProxyConfig: Permit2ProxyConfig = { + proxyAddress: x402UptoPermit2ProxyAddress, + proxyABI: x402UptoPermit2ProxyABI, +}; + +export interface VerifyUptoPermit2Options { + simulate?: boolean; +} + +export interface UptoPermit2FacilitatorConfig { + simulateInSettle?: boolean; +} + +/** + * Verifies an upto Permit2 payment payload against the given requirements. + * + * Validates scheme, network, spender, recipient, facilitator, deadline, amount, + * token, signature, Permit2 allowance, and payer balance. + * + * @param signer - The facilitator signer for contract reads and signature verification + * @param payload - The payment payload to verify + * @param requirements - The payment requirements to verify against + * @param permit2Payload - The upto Permit2 specific payload with witness data + * @param context - Optional facilitator context for extension-provided capabilities + * @param options - Optional verification options (e.g., skip simulation) + * @returns Promise resolving to a verification response indicating validity + */ +export async function verifyUptoPermit2( + signer: FacilitatorEvmSigner, + payload: PaymentPayload, + requirements: PaymentRequirements, + permit2Payload: UptoPermit2Payload, + context?: FacilitatorContext, + options?: VerifyUptoPermit2Options, +): Promise { + const payer = permit2Payload.permit2Authorization.from; + + if (payload.accepted.scheme !== "upto" || requirements.scheme !== "upto") { + return { + isValid: false, + invalidReason: ErrUptoInvalidScheme, + payer, + }; + } + + if (payload.accepted.network !== requirements.network) { + return { + isValid: false, + invalidReason: ErrUptoNetworkMismatch, + payer, + }; + } + + const chainId = getEvmChainId(requirements.network); + const tokenAddress = getAddress(requirements.asset); + + if ( + getAddress(permit2Payload.permit2Authorization.spender) !== + getAddress(x402UptoPermit2ProxyAddress) + ) { + return { + isValid: false, + invalidReason: "invalid_permit2_spender", + payer, + }; + } + + if ( + getAddress(permit2Payload.permit2Authorization.witness.to) !== getAddress(requirements.payTo) + ) { + return { + isValid: false, + invalidReason: "invalid_permit2_recipient_mismatch", + payer, + }; + } + + // Verify the facilitator address in the witness matches our own address + const facilitatorAddresses = signer.getAddresses(); + const witnessFacilitator = getAddress(permit2Payload.permit2Authorization.witness.facilitator); + const isFacilitatorMatch = facilitatorAddresses.some( + addr => getAddress(addr) === witnessFacilitator, + ); + if (!isFacilitatorMatch) { + return { + isValid: false, + invalidReason: ErrUptoFacilitatorMismatch, + payer, + }; + } + + const now = Math.floor(Date.now() / 1000); + if (BigInt(permit2Payload.permit2Authorization.deadline) < BigInt(now + 6)) { + return { + isValid: false, + invalidReason: "permit2_deadline_expired", + payer, + }; + } + + if (BigInt(permit2Payload.permit2Authorization.witness.validAfter) > BigInt(now)) { + return { + isValid: false, + invalidReason: "permit2_not_yet_valid", + payer, + }; + } + + if ( + BigInt(permit2Payload.permit2Authorization.permitted.amount) !== BigInt(requirements.amount) + ) { + return { + isValid: false, + invalidReason: ErrPermit2AmountMismatch, + payer, + }; + } + + if (getAddress(permit2Payload.permit2Authorization.permitted.token) !== tokenAddress) { + return { + isValid: false, + invalidReason: "permit2_token_mismatch", + payer, + }; + } + + // Verify signature using upto-specific witness types (includes facilitator) + const permit2TypedData = { + types: uptoPermit2WitnessTypes, + primaryType: "PermitWitnessTransferFrom" as const, + domain: { + name: "Permit2", + chainId, + verifyingContract: PERMIT2_ADDRESS, + }, + message: { + permitted: { + token: getAddress(permit2Payload.permit2Authorization.permitted.token), + amount: BigInt(permit2Payload.permit2Authorization.permitted.amount), + }, + spender: getAddress(permit2Payload.permit2Authorization.spender), + nonce: BigInt(permit2Payload.permit2Authorization.nonce), + deadline: BigInt(permit2Payload.permit2Authorization.deadline), + witness: { + to: getAddress(permit2Payload.permit2Authorization.witness.to), + facilitator: getAddress(permit2Payload.permit2Authorization.witness.facilitator), + validAfter: BigInt(permit2Payload.permit2Authorization.witness.validAfter), + }, + }, + }; + + // Verify signature + // Note: verifyTypedData is implementation-dependent and pluggable on FacilitatorEvmSigner + // Some implementations only do EOA-style ECDSA recovery (e.g. viem/utils verifyTypedData, ethers.verifyTypedData) + // Viem's publicClient.verifyTypedData supports EOA and Smart Contract Account (ERC-1271 / ERC-6492) signature verification + let signatureValid = false; + try { + signatureValid = await signer.verifyTypedData({ + address: payer, + ...permit2TypedData, + signature: permit2Payload.signature, + }); + } catch { + signatureValid = false; + } + + if (!signatureValid) { + // Check if the payer is a deployed smart contract (ERC-1271 / ERC-6492) + const bytecode = await signer.getCode({ address: payer }); + const isDeployedContract = bytecode && bytecode !== "0x"; + + if (!isDeployedContract) { + return { + isValid: false, + invalidReason: "invalid_permit2_signature", + payer, + }; + } + // Deployed smart contract: fall through to simulation + } + + // If simulation is disabled, return early + if (options?.simulate === false) { + return { isValid: true, invalidReason: undefined, payer }; + } + + const facilitatorAddress = getAddress(permit2Payload.permit2Authorization.witness.facilitator); + // Per spec §Phase 3 Step 7: simulate with requirements.amount (the worst-case charge). + // At verify time, requirements.amount = max authorized amount. + // At settle time, requirements.amount = actual settlement amount (≤ max). + const uptoSettleArgs = buildUptoPermit2SettleArgs( + permit2Payload, + BigInt(requirements.amount), + facilitatorAddress, + ); + + const eip2612InfoForSim = extractEip2612GasSponsoringInfo(payload); + if (eip2612InfoForSim) { + const fieldResult = validateEip2612PermitForPayment(eip2612InfoForSim, payer, tokenAddress); + if (!fieldResult.isValid) { + return { isValid: false, invalidReason: fieldResult.invalidReason!, payer }; + } + + const simOk = await simulatePermit2SettleWithPermit( + uptoProxyConfig, + signer, + uptoSettleArgs, + eip2612InfoForSim, + ); + if (!simOk) { + return diagnosePermit2SimulationFailure( + uptoProxyConfig, + signer, + tokenAddress, + permit2Payload, + requirements.amount, + ); + } + + return { isValid: true, invalidReason: undefined, payer }; + } + + const erc20GasSponsorshipExtension = + context?.getExtension( + ERC20_APPROVAL_GAS_SPONSORING_KEY, + ); + if (erc20GasSponsorshipExtension) { + const erc20Info = extractErc20ApprovalGasSponsoringInfo(payload); + if (erc20Info) { + const fieldResult = await validateErc20ApprovalForPayment(erc20Info, payer, tokenAddress); + if (!fieldResult.isValid) { + return { isValid: false, invalidReason: fieldResult.invalidReason!, payer }; + } + + const extensionSigner = resolveErc20ApprovalExtensionSigner( + erc20GasSponsorshipExtension, + requirements.network, + ); + + if (extensionSigner?.simulateTransactions) { + const simOk = await simulatePermit2SettleWithErc20Approval( + uptoProxyConfig, + extensionSigner, + uptoSettleArgs, + erc20Info, + ); + if (!simOk) { + return diagnosePermit2SimulationFailure( + uptoProxyConfig, + signer, + tokenAddress, + permit2Payload, + requirements.amount, + ); + } + return { isValid: true, invalidReason: undefined, payer }; + } + + return checkPermit2Prerequisites( + uptoProxyConfig, + signer, + tokenAddress, + payer, + requirements.amount, + ); + } + } + + const simOk = await simulatePermit2Settle(uptoProxyConfig, signer, uptoSettleArgs); + if (!simOk) { + return diagnosePermit2SimulationFailure( + uptoProxyConfig, + signer, + tokenAddress, + permit2Payload, + requirements.amount, + ); + } + + return { + isValid: true, + invalidReason: undefined, + payer, + }; +} + +/** + * Settles an upto Permit2 payment on-chain. + * + * Verifies the payment first, then selects the appropriate settlement path: + * EIP-2612 atomic permit, ERC-20 approval extension, or direct settlement. + * + * @param signer - The facilitator signer for contract writes + * @param payload - The payment payload to settle + * @param requirements - The payment requirements + * @param permit2Payload - The upto Permit2 specific payload with witness data + * @param context - Optional facilitator context for extension-provided capabilities + * @param config - Optional facilitator configuration (e.g., simulation settings for settle) + * @returns Promise resolving to a settlement response indicating success or failure + */ +export async function settleUptoPermit2( + signer: FacilitatorEvmSigner, + payload: PaymentPayload, + requirements: PaymentRequirements, + permit2Payload: UptoPermit2Payload, + context?: FacilitatorContext, + config?: UptoPermit2FacilitatorConfig, +): Promise { + const payer = permit2Payload.permit2Authorization.from; + const settlementAmount = BigInt(requirements.amount); + + // Re-verify the signature before settling. We override `requirements.amount` + // with the *authorized maximum* (`permitted.amount`) — NOT the actual + // settlement amount — because `verifyUptoPermit2` performs strict equality + // (`permitted.amount === requirements.amount`) to confirm the payload matches + // what the client signed. The actual settlement amount, which may be lower + // than the authorized maximum, is validated separately in the guard below + // (`settlementAmount > permitted.amount`). + const verifyRequirements: PaymentRequirements = { + ...requirements, + amount: permit2Payload.permit2Authorization.permitted.amount, + }; + + const valid = await verifyUptoPermit2( + signer, + payload, + verifyRequirements, + permit2Payload, + context, + { simulate: config?.simulateInSettle ?? true }, + ); + if (!valid.isValid) { + return { + success: false, + network: payload.accepted.network, + transaction: "", + errorReason: valid.invalidReason ?? "invalid_scheme", + payer, + }; + } + + // Zero settlement — no on-chain tx needed + if (settlementAmount === 0n) { + return { + success: true, + transaction: "", + network: payload.accepted.network, + payer, + amount: "0", + }; + } + + if (settlementAmount > BigInt(permit2Payload.permit2Authorization.permitted.amount)) { + return { + success: false, + network: payload.accepted.network, + transaction: "", + errorReason: ErrUptoSettlementExceedsAmount, + payer, + }; + } + + const facilitatorAddress = getAddress(permit2Payload.permit2Authorization.witness.facilitator); + + // Branch: EIP-2612 gas sponsoring (atomic settleWithPermit via contract) + const eip2612Info = extractEip2612GasSponsoringInfo(payload); + if (eip2612Info) { + return settleUptoWithEIP2612( + signer, + payload, + permit2Payload, + eip2612Info, + settlementAmount, + facilitatorAddress, + ); + } + + // Branch: ERC-20 approval gas sponsoring (broadcast approval + settle via extension signer) + const erc20Info = extractErc20ApprovalGasSponsoringInfo(payload); + if (erc20Info) { + const erc20GasSponsorshipExtension = + context?.getExtension( + ERC20_APPROVAL_GAS_SPONSORING_KEY, + ); + const extensionSigner = resolveErc20ApprovalExtensionSigner( + erc20GasSponsorshipExtension, + payload.accepted.network, + ); + if (extensionSigner) { + return settleUptoWithERC20Approval( + extensionSigner, + payload, + permit2Payload, + erc20Info, + settlementAmount, + facilitatorAddress, + ); + } + } + + // Branch: standard settle (allowance already on-chain) + return settleUptoDirect(signer, payload, permit2Payload, settlementAmount, facilitatorAddress); +} + +/** + * Settles an upto Permit2 payment via settleWithPermit, including the EIP-2612 permit atomically. + * + * @param signer - The facilitator signer for contract writes + * @param payload - The payment payload for network info + * @param permit2Payload - The upto Permit2 specific payload with authorization and signature + * @param eip2612Info - The EIP-2612 gas sponsoring info from the payload extension + * @param settlementAmount - The amount to settle on-chain + * @param facilitatorAddress - The facilitator address authorized in the witness + * @returns Promise resolving to a settlement response + */ +async function settleUptoWithEIP2612( + signer: FacilitatorEvmSigner, + payload: PaymentPayload, + permit2Payload: UptoPermit2Payload, + eip2612Info: Eip2612GasSponsoringInfo, + settlementAmount: bigint, + facilitatorAddress: `0x${string}`, +): Promise { + const payer = permit2Payload.permit2Authorization.from; + try { + const { v, r, s } = splitEip2612Signature(eip2612Info.signature); + + const tx = await signer.writeContract({ + address: uptoProxyConfig.proxyAddress, + abi: uptoProxyConfig.proxyABI, + functionName: "settleWithPermit", + args: [ + { + value: BigInt(eip2612Info.amount), + deadline: BigInt(eip2612Info.deadline), + r, + s, + v, + }, + ...buildUptoPermit2SettleArgs(permit2Payload, settlementAmount, facilitatorAddress), + ], + }); + + const response = await waitAndReturnSettleResponse(signer, tx, payload, payer); + return { ...response, amount: settlementAmount.toString() }; + } catch (error) { + return mapSettleError(error, payload, payer); + } +} + +/** + * Settles an upto Permit2 payment using an ERC-20 approval gas sponsoring extension. + * + * Broadcasts the pre-signed approval transaction followed by the settle transaction + * via the extension signer. + * + * @param extensionSigner - The extension signer with sendTransactions capability + * @param payload - The payment payload for network info + * @param permit2Payload - The upto Permit2 specific payload with authorization and signature + * @param erc20Info - Object containing the signed approval transaction + * @param erc20Info.signedTransaction - The RLP-encoded signed ERC-20 approve transaction hex string + * @param settlementAmount - The amount to settle on-chain + * @param facilitatorAddress - The facilitator address authorized in the witness + * @returns Promise resolving to a settlement response + */ +async function settleUptoWithERC20Approval( + extensionSigner: Erc20ApprovalGasSponsoringSigner, + payload: PaymentPayload, + permit2Payload: UptoPermit2Payload, + erc20Info: { signedTransaction: string }, + settlementAmount: bigint, + facilitatorAddress: `0x${string}`, +): Promise { + const payer = permit2Payload.permit2Authorization.from; + + try { + const settleData = encodeFunctionData({ + abi: uptoProxyConfig.proxyABI, + functionName: "settle", + args: buildUptoPermit2SettleArgs(permit2Payload, settlementAmount, facilitatorAddress), + }); + + const txHashes = await extensionSigner.sendTransactions([ + erc20Info.signedTransaction as `0x${string}`, + { to: uptoProxyConfig.proxyAddress, data: settleData, gas: BigInt(300_000) }, + ]); + + const settleTxHash = txHashes[txHashes.length - 1]; + const response = await waitAndReturnSettleResponse( + extensionSigner, + settleTxHash, + payload, + payer, + ); + return { ...response, amount: settlementAmount.toString() }; + } catch (error) { + return mapSettleError(error, payload, payer); + } +} + +/** + * Settles an upto Permit2 payment directly when Permit2 allowance is already on-chain. + * + * @param signer - The facilitator signer for contract writes + * @param payload - The payment payload for network info + * @param permit2Payload - The upto Permit2 specific payload with authorization and signature + * @param settlementAmount - The amount to settle on-chain + * @param facilitatorAddress - The facilitator address authorized in the witness + * @returns Promise resolving to a settlement response + */ +async function settleUptoDirect( + signer: FacilitatorEvmSigner, + payload: PaymentPayload, + permit2Payload: UptoPermit2Payload, + settlementAmount: bigint, + facilitatorAddress: `0x${string}`, +): Promise { + const payer = permit2Payload.permit2Authorization.from; + try { + const tx = await signer.writeContract({ + address: uptoProxyConfig.proxyAddress, + abi: uptoProxyConfig.proxyABI, + functionName: "settle", + args: buildUptoPermit2SettleArgs(permit2Payload, settlementAmount, facilitatorAddress), + }); + + const response = await waitAndReturnSettleResponse(signer, tx, payload, payer); + return { ...response, amount: settlementAmount.toString() }; + } catch (error) { + return mapSettleError(error, payload, payer); + } +} diff --git a/typescript/packages/mechanisms/evm/src/upto/facilitator/scheme.ts b/typescript/packages/mechanisms/evm/src/upto/facilitator/scheme.ts new file mode 100644 index 0000000000..b49eb9f9d5 --- /dev/null +++ b/typescript/packages/mechanisms/evm/src/upto/facilitator/scheme.ts @@ -0,0 +1,109 @@ +import { + PaymentPayload, + PaymentRequirements, + SchemeNetworkFacilitator, + FacilitatorContext, + SettleResponse, + VerifyResponse, +} from "@x402/core/types"; +import { FacilitatorEvmSigner } from "../../signer"; +import { UptoPermit2Payload, isUptoPermit2Payload } from "../../types"; +import { verifyUptoPermit2, settleUptoPermit2 } from "./permit2"; + +/** + * EVM facilitator implementation for the Upto payment scheme. + * Handles verification and settlement of Permit2-based payments. + */ +export class UptoEvmScheme implements SchemeNetworkFacilitator { + readonly scheme = "upto"; + readonly caipFamily = "eip155:*"; + + /** + * Creates a new UptoEvmScheme facilitator instance. + * + * @param signer - The EVM signer for facilitator operations + */ + constructor(private readonly signer: FacilitatorEvmSigner) {} + + /** + * Returns extra metadata required by the upto scheme, including the facilitator address. + * + * @param _ - The network identifier (unused) + * @returns Object with facilitatorAddress, or undefined if no signer addresses are available + */ + getExtra(_: string): Record | undefined { + const addresses = this.signer.getAddresses(); + if (addresses.length === 0) { + return undefined; + } + return { facilitatorAddress: addresses[Math.floor(Math.random() * addresses.length)] }; + } + + /** + * Returns the list of facilitator signer addresses for the upto scheme. + * + * @param _ - The network identifier (unused) + * @returns Array of facilitator signer addresses + */ + getSigners(_: string): string[] { + return [...this.signer.getAddresses()]; + } + + /** + * Verifies an upto Permit2 payment payload against the given requirements. + * + * @param payload - The payment payload to verify + * @param requirements - The payment requirements to verify against + * @param context - Optional facilitator context + * @returns Promise resolving to a verification response + */ + async verify( + payload: PaymentPayload, + requirements: PaymentRequirements, + context?: FacilitatorContext, + ): Promise { + const rawPayload = payload.payload as Record; + if (!isUptoPermit2Payload(rawPayload)) { + return { isValid: false, invalidReason: "unsupported_payload_type", payer: "" }; + } + return verifyUptoPermit2( + this.signer, + payload, + requirements, + rawPayload as UptoPermit2Payload, + context, + ); + } + + /** + * Settles an upto Permit2 payment on-chain. + * + * @param payload - The payment payload to settle + * @param requirements - The payment requirements + * @param context - Optional facilitator context + * @returns Promise resolving to a settlement response + */ + async settle( + payload: PaymentPayload, + requirements: PaymentRequirements, + context?: FacilitatorContext, + ): Promise { + const rawPayload = payload.payload as Record; + if (!isUptoPermit2Payload(rawPayload)) { + return { + success: false, + network: payload.accepted.network, + transaction: "", + errorReason: "unsupported_payload_type", + payer: "", + }; + } + return settleUptoPermit2( + this.signer, + payload, + requirements, + rawPayload as UptoPermit2Payload, + context, + ); + } +} diff --git a/typescript/packages/mechanisms/evm/src/upto/index.ts b/typescript/packages/mechanisms/evm/src/upto/index.ts new file mode 100644 index 0000000000..41d65cf2f2 --- /dev/null +++ b/typescript/packages/mechanisms/evm/src/upto/index.ts @@ -0,0 +1 @@ +export { UptoEvmScheme } from "./client/scheme"; diff --git a/typescript/packages/mechanisms/evm/src/upto/server/index.ts b/typescript/packages/mechanisms/evm/src/upto/server/index.ts new file mode 100644 index 0000000000..01843a1393 --- /dev/null +++ b/typescript/packages/mechanisms/evm/src/upto/server/index.ts @@ -0,0 +1,3 @@ +// Note: No register.ts helper — V1 backward compatibility is not needed for upto. +// Use direct class instantiation: server.register("eip155:*", new UptoEvmScheme()) +export { UptoEvmScheme } from "./scheme"; diff --git a/typescript/packages/mechanisms/evm/src/upto/server/scheme.ts b/typescript/packages/mechanisms/evm/src/upto/server/scheme.ts new file mode 100644 index 0000000000..7c3549c85e --- /dev/null +++ b/typescript/packages/mechanisms/evm/src/upto/server/scheme.ts @@ -0,0 +1,173 @@ +import { + AssetAmount, + Network, + PaymentRequirements, + Price, + SchemeNetworkServer, + MoneyParser, +} from "@x402/core/types"; +import { getAddress } from "viem"; +import { getDefaultAsset } from "../../shared/defaultAssets"; + +/** + * EVM server implementation for the Upto payment scheme. + * Handles price parsing, payment requirements enhancement, and default asset resolution. + */ +export class UptoEvmScheme implements SchemeNetworkServer { + readonly scheme = "upto"; + private moneyParsers: MoneyParser[] = []; + + /** + * Registers a custom money parser for converting prices to asset amounts. + * + * @param parser - The money parser function to register + * @returns This instance for chaining + */ + registerMoneyParser(parser: MoneyParser): UptoEvmScheme { + this.moneyParsers.push(parser); + return this; + } + + /** + * Returns the decimal precision of the default stablecoin for the given network. + * Implements the optional AssetDecimalsProvider interface used by resolveSettlementOverrideAmount. + * + * @param _asset - The asset symbol (unused; defaults to the network's default stablecoin) + * @param network - The network to look up the default asset for + * @returns The number of decimal places for the asset + */ + getAssetDecimals(_asset: string, network: Network): number { + try { + return getDefaultAsset(network).decimals; + } catch { + return 6; + } + } + + /** + * Parses a price into an asset amount for the given network. + * + * @param price - The price to parse (string, number, or AssetAmount) + * @param network - The target network + * @returns Promise resolving to an asset amount + */ + async parsePrice(price: Price, network: Network): Promise { + if (typeof price === "object" && price !== null && "amount" in price) { + if (!price.asset) { + throw new Error(`Asset address must be specified for AssetAmount on network ${network}`); + } + return { + amount: price.amount, + asset: price.asset, + extra: price.extra || {}, + }; + } + + const amount = this.parseMoneyToDecimal(price); + + for (const parser of this.moneyParsers) { + const result = await parser(amount, network); + if (result !== null) { + return result; + } + } + + return this.defaultMoneyConversion(amount, network); + } + + /** + * Enhances payment requirements with upto-specific metadata. + * + * @param paymentRequirements - The base payment requirements + * @param supportedKind - The supported scheme/network kind + * @param supportedKind.x402Version - The x402 protocol version + * @param supportedKind.scheme - The payment scheme name + * @param supportedKind.network - The target network + * @param supportedKind.extra - Optional extra metadata + * @param extensionKeys - Extension keys to include + * @returns Promise resolving to enhanced payment requirements + */ + enhancePaymentRequirements( + paymentRequirements: PaymentRequirements, + supportedKind: { + x402Version: number; + scheme: string; + network: Network; + extra?: Record; + }, + extensionKeys: string[], + ): Promise { + void extensionKeys; + return Promise.resolve({ + ...paymentRequirements, + extra: { + ...paymentRequirements.extra, + assetTransferMethod: "permit2", + ...(supportedKind.extra?.facilitatorAddress + ? { facilitatorAddress: getAddress(supportedKind.extra.facilitatorAddress as string) } + : {}), + }, + }); + } + + /** + * Parses a money string or number into a decimal value. + * + * @param money - The money value to parse + * @returns The parsed decimal amount + */ + private parseMoneyToDecimal(money: string | number): number { + if (typeof money === "number") { + return money; + } + + const cleanMoney = money.replace(/^\$/, "").trim(); + const amount = parseFloat(cleanMoney); + + if (isNaN(amount)) { + throw new Error(`Invalid money format: ${money}`); + } + + return amount; + } + + /** + * Converts a numeric dollar amount to an AssetAmount using the default token for the network. + * + * @param amount - The dollar amount as a number + * @param network - The target network + * @returns The converted asset amount with token metadata + */ + private defaultMoneyConversion(amount: number, network: Network): AssetAmount { + const assetInfo = getDefaultAsset(network); + const tokenAmount = this.convertToTokenAmount(amount.toString(), assetInfo.decimals); + + return { + amount: tokenAmount, + asset: assetInfo.address, + extra: { + name: assetInfo.name, + version: assetInfo.version, + assetTransferMethod: "permit2", + }, + }; + } + + /** + * Converts a decimal string amount to an integer token amount using the given decimals. + * + * @param decimalAmount - The amount as a decimal string (e.g. "1.5") + * @param decimals - The number of decimal places for the token + * @returns The token amount as an integer string in smallest units + */ + private convertToTokenAmount(decimalAmount: string, decimals: number): string { + const amount = parseFloat(decimalAmount); + if (isNaN(amount)) { + throw new Error(`Invalid amount: ${decimalAmount}`); + } + const [intPart, decPart = ""] = String(amount).split("."); + const paddedDec = decPart.padEnd(decimals, "0").slice(0, decimals); + const tokenAmount = (intPart + paddedDec).replace(/^0+/, "") || "0"; + return tokenAmount; + } +} diff --git a/typescript/packages/mechanisms/evm/src/v1/index.ts b/typescript/packages/mechanisms/evm/src/v1/index.ts index 30b2036bcc..1521a89d43 100644 --- a/typescript/packages/mechanisms/evm/src/v1/index.ts +++ b/typescript/packages/mechanisms/evm/src/v1/index.ts @@ -20,6 +20,8 @@ export const EVM_NETWORK_CHAIN_ID_MAP = { "skale-base-sepolia": 324705682, megaeth: 4326, monad: 143, + stable: 988, + "stable-testnet": 2201, } as const; export type EvmNetworkV1 = keyof typeof EVM_NETWORK_CHAIN_ID_MAP; diff --git a/typescript/packages/mechanisms/evm/test/unit/exact/client.rpc.test.ts b/typescript/packages/mechanisms/evm/test/unit/exact/client.rpc.test.ts new file mode 100644 index 0000000000..5143713e20 --- /dev/null +++ b/typescript/packages/mechanisms/evm/test/unit/exact/client.rpc.test.ts @@ -0,0 +1,123 @@ +import { describe, it, expect, vi, beforeEach } from "vitest"; +import type { ClientEvmSigner } from "../../../src/signer"; + +const { + mockReadContract, + mockGetTransactionCount, + mockEstimateFeesPerGas, + mockCreatePublicClient, + mockHttp, +} = vi.hoisted(() => { + const readContract = vi.fn(); + const getTransactionCount = vi.fn(); + const estimateFeesPerGas = vi.fn(); + return { + mockReadContract: readContract, + mockGetTransactionCount: getTransactionCount, + mockEstimateFeesPerGas: estimateFeesPerGas, + mockCreatePublicClient: vi.fn(() => ({ + readContract, + getTransactionCount, + estimateFeesPerGas, + })), + mockHttp: vi.fn((url: string) => ({ url })), + }; +}); + +vi.mock("viem", () => ({ + createPublicClient: mockCreatePublicClient, + http: mockHttp, +})); + +import { + resolveRpcUrl, + resolveExtensionRpcCapabilities, + type ExactEvmSchemeOptions, +} from "../../../src/exact/client/rpc"; + +describe("Exact EVM RPC resolver", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it("resolves rpc url from single config", () => { + const options: ExactEvmSchemeOptions = { rpcUrl: "https://base.example" }; + expect(resolveRpcUrl("eip155:8453", options)).toBe("https://base.example"); + }); + + it("resolves rpc url from chain map", () => { + const options: ExactEvmSchemeOptions = { + 137: { rpcUrl: "https://polygon.example" }, + 8453: { rpcUrl: "https://base.example" }, + }; + expect(resolveRpcUrl("eip155:8453", options)).toBe("https://base.example"); + expect(resolveRpcUrl("eip155:137", options)).toBe("https://polygon.example"); + }); + + it("keeps signer capabilities as highest precedence", async () => { + const signerRead = vi.fn().mockResolvedValue(1n); + const signerGetTx = vi.fn().mockResolvedValue(7); + const signerFees = vi.fn().mockResolvedValue({ + maxFeePerGas: 100n, + maxPriorityFeePerGas: 10n, + }); + + const signer: ClientEvmSigner = { + address: "0x1234567890123456789012345678901234567890", + signTypedData: vi.fn().mockResolvedValue("0xabc"), + readContract: signerRead, + getTransactionCount: signerGetTx, + estimateFeesPerGas: signerFees, + }; + + const capabilities = resolveExtensionRpcCapabilities("eip155:8453", signer, { + rpcUrl: "https://base.example", + }); + await capabilities.readContract?.({ + address: "0x1234567890123456789012345678901234567890", + abi: [], + functionName: "allowance", + args: [], + }); + + expect(capabilities.readContract).toBe(signerRead); + expect(capabilities.getTransactionCount).toBe(signerGetTx); + expect(capabilities.estimateFeesPerGas).toBe(signerFees); + expect(mockCreatePublicClient).not.toHaveBeenCalled(); + }); + + it("backfills missing read and fee capabilities from rpc", async () => { + mockReadContract.mockResolvedValue(0n); + mockGetTransactionCount.mockResolvedValue(3); + mockEstimateFeesPerGas.mockResolvedValue({ + maxFeePerGas: 111n, + maxPriorityFeePerGas: 22n, + }); + + const signer: ClientEvmSigner = { + address: "0x1234567890123456789012345678901234567890", + signTypedData: vi.fn().mockResolvedValue("0xabc"), + }; + + const capabilities = resolveExtensionRpcCapabilities("eip155:8453", signer, { + rpcUrl: "https://base.example", + }); + + const allowance = await capabilities.readContract?.({ + address: "0x1234567890123456789012345678901234567890", + abi: [], + functionName: "allowance", + args: [], + }); + const nonce = await capabilities.getTransactionCount?.({ + address: "0x1234567890123456789012345678901234567890", + }); + const fees = await capabilities.estimateFeesPerGas?.(); + + expect(mockCreatePublicClient).toHaveBeenCalledTimes(1); + expect(mockHttp).toHaveBeenCalledWith("https://base.example"); + expect(allowance).toBe(0n); + expect(nonce).toBe(3); + expect(fees).toEqual({ maxFeePerGas: 111n, maxPriorityFeePerGas: 22n }); + }); +}); diff --git a/typescript/packages/mechanisms/evm/test/unit/exact/facilitator.test.ts b/typescript/packages/mechanisms/evm/test/unit/exact/facilitator.test.ts index 39efa7fda6..fc824b22c8 100644 --- a/typescript/packages/mechanisms/evm/test/unit/exact/facilitator.test.ts +++ b/typescript/packages/mechanisms/evm/test/unit/exact/facilitator.test.ts @@ -4,7 +4,10 @@ import { ExactEvmScheme as ClientExactEvmScheme } from "../../../src/exact/clien import type { ClientEvmSigner, FacilitatorEvmSigner } from "../../../src/signer"; import { PaymentRequirements, PaymentPayload } from "@x402/core/types"; import { x402ExactPermit2ProxyAddress, PERMIT2_ADDRESS } from "../../../src/constants"; -import { ERC20_APPROVAL_GAS_SPONSORING } from "@x402/extensions"; +import { ERC20_APPROVAL_GAS_SPONSORING_KEY } from "../../../src/exact/extensions"; +import { MULTICALL3_ADDRESS } from "../../../src/multicall"; +import { concat, encodeAbiParameters } from "viem"; +import * as Errors from "../../../src/exact/facilitator/errors"; // Mock viem's transaction parsing utilities for ERC-20 approval tests // Uses importOriginal to preserve all other viem exports (getAddress, etc.) @@ -113,7 +116,7 @@ describe("ExactEvmScheme (Facilitator)", () => { const result = await facilitator.verify(payload, requirements); expect(result.isValid).toBe(false); - expect(result.invalidReason).toBe("unsupported_scheme"); + expect(result.invalidReason).toBe(Errors.ErrInvalidScheme); }); it("should reject if missing EIP-712 domain parameters", async () => { @@ -141,7 +144,7 @@ describe("ExactEvmScheme (Facilitator)", () => { const result = await facilitator.verify(fullPayload, requirements); expect(result.isValid).toBe(false); - expect(result.invalidReason).toBe("missing_eip712_domain"); + expect(result.invalidReason).toBe(Errors.ErrMissingEip712Domain); }); it("should reject if network doesn't match", async () => { @@ -159,7 +162,7 @@ describe("ExactEvmScheme (Facilitator)", () => { const fullPayload: PaymentPayload = { ...paymentPayload, - accepted: { ...requirements, network: "eip155:1" }, // Wrong network in accepted + accepted: requirements, resource: { url: "", description: "", mimeType: "" }, }; @@ -168,7 +171,7 @@ describe("ExactEvmScheme (Facilitator)", () => { const result = await facilitator.verify(fullPayload, wrongNetworkRequirements); expect(result.isValid).toBe(false); - // Verification should fail (network mismatch or other validation error) + expect(result.invalidReason).toBe(Errors.ErrNetworkMismatch); }); it("should reject if recipient doesn't match payTo", async () => { @@ -199,7 +202,7 @@ describe("ExactEvmScheme (Facilitator)", () => { const result = await facilitator.verify(fullPayload, modifiedRequirements); expect(result.isValid).toBe(false); - expect(result.invalidReason).toBe("invalid_exact_evm_payload_recipient_mismatch"); + expect(result.invalidReason).toBe(Errors.ErrRecipientMismatch); }); it("should reject if amount doesn't match", async () => { @@ -259,7 +262,7 @@ describe("ExactEvmScheme (Facilitator)", () => { }); describe("Permit2 payload verification", () => { - it("should verify Permit2 payloads with valid signature and allowance", async () => { + it("should verify Permit2 payloads with valid signature and simulation success", async () => { const requirements: PaymentRequirements = { scheme: "exact", network: "eip155:84532", @@ -270,8 +273,8 @@ describe("ExactEvmScheme (Facilitator)", () => { extra: { name: "USDC", version: "2", assetTransferMethod: "permit2" }, }; - // Mock readContract to return sufficient allowance and balance - mockFacilitatorSigner.readContract = vi.fn().mockResolvedValue(BigInt("10000000000")); + // Simulation of settle() on the proxy succeeds (readContract doesn't throw) + mockFacilitatorSigner.readContract = vi.fn().mockResolvedValue(undefined); const permit2Payload: PaymentPayload = { x402Version: 2, @@ -302,7 +305,7 @@ describe("ExactEvmScheme (Facilitator)", () => { expect(result.payer).toBe(mockClientSigner.address); }); - it("should reject Permit2 payloads with insufficient allowance", async () => { + it("should reject Permit2 payloads when simulation fails and allowance is insufficient", async () => { const requirements: PaymentRequirements = { scheme: "exact", network: "eip155:84532", @@ -313,8 +316,31 @@ describe("ExactEvmScheme (Facilitator)", () => { extra: { name: "USDC", version: "2", assetTransferMethod: "permit2" }, }; - // Mock readContract to return zero allowance - mockFacilitatorSigner.readContract = vi.fn().mockResolvedValue(BigInt(0)); + // Simulation fails (settle throws), diagnostic multicall returns proxy OK, balance OK, allowance 0 + mockFacilitatorSigner.readContract = vi + .fn() + .mockImplementation(({ address }: { address: string }) => { + if (address === x402ExactPermit2ProxyAddress) { + return Promise.reject(new Error("execution reverted")); + } + if (address === MULTICALL3_ADDRESS) { + return Promise.resolve([ + { + success: true, + returnData: "0x000000000000000000000000000000000022D473030F116dDEE9F6B43aC78BA3", + }, + { + success: true, + returnData: "0x00000000000000000000000000000000000000000000000000000000000f4240", + }, + { + success: true, + returnData: "0x0000000000000000000000000000000000000000000000000000000000000000", + }, + ]); + } + return Promise.resolve(BigInt(0)); + }); const permit2Payload: PaymentPayload = { x402Version: 2, @@ -482,8 +508,8 @@ describe("ExactEvmScheme (Facilitator)", () => { extra: { name: "USDC", version: "2", assetTransferMethod: "permit2" }, }; - // Mock readContract to return sufficient allowance and balance - mockFacilitatorSigner.readContract = vi.fn().mockResolvedValue(BigInt("10000000000")); + // settle's re-verify has simulate=false (default), so no simulation readContract needed + mockFacilitatorSigner.readContract = vi.fn().mockResolvedValue(undefined); const permit2Payload: PaymentPayload = { x402Version: 2, @@ -516,7 +542,7 @@ describe("ExactEvmScheme (Facilitator)", () => { expect(mockFacilitatorSigner.writeContract).toHaveBeenCalled(); }); - it("should fail Permit2 settlement when verification fails", async () => { + it("should fail Permit2 settlement when signature verification fails", async () => { const requirements: PaymentRequirements = { scheme: "exact", network: "eip155:84532", @@ -527,8 +553,8 @@ describe("ExactEvmScheme (Facilitator)", () => { extra: { name: "USDC", version: "2", assetTransferMethod: "permit2" }, }; - // Mock readContract to return zero allowance - mockFacilitatorSigner.readContract = vi.fn().mockResolvedValue(BigInt(0)); + // Signature verification fails + mockFacilitatorSigner.verifyTypedData = vi.fn().mockResolvedValue(false); const permit2Payload: PaymentPayload = { x402Version: 2, @@ -556,7 +582,7 @@ describe("ExactEvmScheme (Facilitator)", () => { const result = await facilitator.settle(permit2Payload, requirements); expect(result.success).toBe(false); - expect(result.errorReason).toBe("permit2_allowance_required"); + expect(result.errorReason).toBe("invalid_permit2_signature"); expect(result.payer).toBe(mockClientSigner.address); }); }); @@ -596,7 +622,7 @@ describe("ExactEvmScheme (Facilitator)", () => { const result = await facilitator.verify(payload, requirements); expect(result.isValid).toBe(false); - expect(result.invalidReason).toContain("invalid_exact_evm_payload_signature"); + expect(result.invalidReason).toBe(Errors.ErrInvalidSignature); }); it("should normalize addresses (case-insensitive)", async () => { @@ -627,12 +653,9 @@ describe("ExactEvmScheme (Facilitator)", () => { }); describe("EIP-2612 Gas Sponsoring - Verify", () => { - it("should accept valid EIP-2612 extension when Permit2 allowance is 0", async () => { - // Mock: allowance returns 0, then balance returns sufficient - mockFacilitatorSigner.readContract = vi - .fn() - .mockResolvedValueOnce(0n) // allowance check = 0 - .mockResolvedValueOnce(BigInt("10000000")); // balance check + it("should accept valid EIP-2612 extension when settleWithPermit simulation succeeds", async () => { + // Simulation of settleWithPermit on proxy succeeds + mockFacilitatorSigner.readContract = vi.fn().mockResolvedValue(undefined); const permit2Requirements: PaymentRequirements = { scheme: "exact", @@ -644,7 +667,6 @@ describe("ExactEvmScheme (Facilitator)", () => { extra: { assetTransferMethod: "permit2", name: "USDC", version: "2" }, }; - // Create a Permit2 payload const permit2ClientSigner: ClientEvmSigner = { address: "0x1234567890123456789012345678901234567890", signTypedData: vi.fn().mockResolvedValue("0x" + "ab".repeat(32) + "cd".repeat(32) + "1b"), @@ -653,7 +675,6 @@ describe("ExactEvmScheme (Facilitator)", () => { const permit2Client = new ClientExactEvmScheme(permit2ClientSigner); const paymentPayload = await permit2Client.createPaymentPayload(2, permit2Requirements); - // Add EIP-2612 extension data const now = Math.floor(Date.now() / 1000); const fullPayload: PaymentPayload = { ...paymentPayload, @@ -678,17 +699,38 @@ describe("ExactEvmScheme (Facilitator)", () => { }; const result = await facilitator.verify(fullPayload, permit2Requirements); - // Should pass verify (EIP-2612 extension provides the permit) expect(result).toBeDefined(); - // It may still fail on signature verification (mock), but should NOT fail with permit2_allowance_required if (!result.isValid) { expect(result.invalidReason).not.toBe("permit2_allowance_required"); } }); - it("should reject when allowance is 0 and no EIP-2612 extension", async () => { - // Mock: allowance returns 0 - mockFacilitatorSigner.readContract = vi.fn().mockResolvedValueOnce(0n); // allowance check = 0 + it("should reject when simulation fails and no extension present (allowance insufficient)", async () => { + // Simulation fails, diagnostic multicall returns low allowance + mockFacilitatorSigner.readContract = vi + .fn() + .mockImplementation(({ address }: { address: string }) => { + if (address === x402ExactPermit2ProxyAddress) { + return Promise.reject(new Error("execution reverted")); + } + if (address === MULTICALL3_ADDRESS) { + return Promise.resolve([ + { + success: true, + returnData: "0x000000000000000000000000000000000022D473030F116dDEE9F6B43aC78BA3", + }, + { + success: true, + returnData: "0x00000000000000000000000000000000000000000000000000000000000f4240", + }, + { + success: true, + returnData: "0x0000000000000000000000000000000000000000000000000000000000000000", + }, + ]); + } + return Promise.resolve(BigInt(0)); + }); const permit2Requirements: PaymentRequirements = { scheme: "exact", @@ -712,20 +754,15 @@ describe("ExactEvmScheme (Facilitator)", () => { ...paymentPayload, accepted: permit2Requirements, resource: { url: "https://test.com", description: "", mimeType: "" }, - // NO eip2612GasSponsoring extension }; const result = await facilitator.verify(fullPayload, permit2Requirements); - // Should fail with permit2_allowance_required since there's no EIP-2612 extension expect(result.isValid).toBe(false); expect(result.invalidReason).toBe("permit2_allowance_required"); }); it("should reject EIP-2612 extension with wrong spender", async () => { - mockFacilitatorSigner.readContract = vi - .fn() - .mockResolvedValueOnce(0n) // allowance = 0 - .mockResolvedValueOnce(BigInt("10000000")); // balance + mockFacilitatorSigner.readContract = vi.fn().mockResolvedValue(undefined); const permit2Requirements: PaymentRequirements = { scheme: "exact", @@ -774,6 +811,234 @@ describe("ExactEvmScheme (Facilitator)", () => { }); }); + describe("ERC-6492 counterfactual signature verification", () => { + const ERC6492_MAGIC = "0x6492649264926492649264926492649264926492649264926492649264926492"; + + function makeERC6492Sig( + factory: `0x${string}`, + calldata: `0x${string}`, + innerSig: `0x${string}`, + ): `0x${string}` { + const encoded = encodeAbiParameters( + [{ type: "address" }, { type: "bytes" }, { type: "bytes" }], + [factory, calldata, innerSig], + ); + return concat([encoded, ERC6492_MAGIC]) as `0x${string}`; + } + + const erc6492Requirements: PaymentRequirements = { + scheme: "exact", + network: "eip155:84532", + amount: "1000000", + asset: "0x036CbD53842c5426634e7929541eC2318f3dCF7e", + payTo: "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb0", + maxTimeoutSeconds: 300, + extra: { name: "USDC", version: "2" }, + }; + + const erc6492Payer = "0x1234567890123456789012345678901234567890"; + const factory = "0x1111111111111111111111111111111111111111" as `0x${string}`; + const factoryCalldata = "0xdeadbeef" as `0x${string}`; + const garbageInnerSig = ("0x" + "00".repeat(65)) as `0x${string}`; + const erc6492Sig = makeERC6492Sig(factory, factoryCalldata, garbageInnerSig); + + function makeERC6492Payload(sig: `0x${string}`): PaymentPayload { + return { + x402Version: 2, + payload: { + authorization: { + from: erc6492Payer, + to: erc6492Requirements.payTo, + value: erc6492Requirements.amount, + validAfter: "0", + validBefore: "999999999999", + nonce: "0x0000000000000000000000000000000000000000000000000000000000000001", + }, + signature: sig, + }, + accepted: erc6492Requirements, + resource: { url: "", description: "", mimeType: "" }, + }; + } + + it("should accept ERC-6492 when verifyTypedData returns true and simulation passes", async () => { + mockFacilitatorSigner.verifyTypedData = vi.fn().mockResolvedValue(true); + mockFacilitatorSigner.getCode = vi.fn().mockResolvedValue("0x"); + mockFacilitatorSigner.readContract = vi + .fn() + .mockImplementation(({ address }: { address: string }) => { + if (address === MULTICALL3_ADDRESS) { + return Promise.resolve([ + { success: true, returnData: "0x" }, + { success: true, returnData: "0x" }, + ]); + } + return Promise.resolve(BigInt("10000000")); + }); + + const result = await facilitator.verify(makeERC6492Payload(erc6492Sig), erc6492Requirements); + + expect(result.isValid).toBe(true); + expect(result.payer).toBe(erc6492Payer); + }); + + it("should accept ERC-6492 when verifyTypedData fails but simulation passes (EOA-only signer)", async () => { + mockFacilitatorSigner.verifyTypedData = vi.fn().mockResolvedValue(false); + mockFacilitatorSigner.getCode = vi.fn().mockResolvedValue("0x"); + mockFacilitatorSigner.readContract = vi + .fn() + .mockImplementation(({ address }: { address: string }) => { + if (address === MULTICALL3_ADDRESS) { + return Promise.resolve([ + { success: true, returnData: "0x" }, + { success: true, returnData: "0x" }, + ]); + } + return Promise.resolve(BigInt("10000000")); + }); + + const result = await facilitator.verify(makeERC6492Payload(erc6492Sig), erc6492Requirements); + + expect(result.isValid).toBe(true); + expect(result.payer).toBe(erc6492Payer); + }); + + it("should accept ERC-6492 when verifyTypedData throws but simulation passes", async () => { + mockFacilitatorSigner.verifyTypedData = vi + .fn() + .mockRejectedValue(new Error("invalid signature length")); + mockFacilitatorSigner.getCode = vi.fn().mockResolvedValue("0x"); + mockFacilitatorSigner.readContract = vi + .fn() + .mockImplementation(({ address }: { address: string }) => { + if (address === MULTICALL3_ADDRESS) { + return Promise.resolve([ + { success: true, returnData: "0x" }, + { success: true, returnData: "0x" }, + ]); + } + return Promise.resolve(BigInt("10000000")); + }); + + const result = await facilitator.verify(makeERC6492Payload(erc6492Sig), erc6492Requirements); + + expect(result.isValid).toBe(true); + expect(result.payer).toBe(erc6492Payer); + }); + + it("should reject ERC-6492 when simulation fails (multicall transfer reverts)", async () => { + mockFacilitatorSigner.verifyTypedData = vi.fn().mockResolvedValue(true); + mockFacilitatorSigner.getCode = vi.fn().mockResolvedValue("0x"); + mockFacilitatorSigner.readContract = vi + .fn() + .mockImplementation(({ address }: { address: string }) => { + if (address === MULTICALL3_ADDRESS) { + return Promise.resolve([ + { success: true, returnData: "0x" }, + { success: false, returnData: "0x" }, + ]); + } + return Promise.resolve([ + { + success: true, + returnData: "0x00000000000000000000000000000000000000000000000000000000000f4240", + }, + { success: true, returnData: "0x" }, + { success: true, returnData: "0x" }, + { + success: true, + returnData: "0x0000000000000000000000000000000000000000000000000000000000000000", + }, + ]); + }); + + const result = await facilitator.verify(makeERC6492Payload(erc6492Sig), erc6492Requirements); + + expect(result.isValid).toBe(false); + }); + + it("should reject forged ERC-6492 when verifyTypedData fails and simulation fails", async () => { + mockFacilitatorSigner.verifyTypedData = vi.fn().mockResolvedValue(false); + mockFacilitatorSigner.getCode = vi.fn().mockResolvedValue("0x"); + mockFacilitatorSigner.readContract = vi + .fn() + .mockImplementation(({ address }: { address: string }) => { + if (address === MULTICALL3_ADDRESS) { + return Promise.resolve([ + { success: true, returnData: "0x" }, + { success: false, returnData: "0x" }, + ]); + } + return Promise.resolve([ + { + success: true, + returnData: "0x00000000000000000000000000000000000000000000000000000000000f4240", + }, + { success: true, returnData: "0x" }, + { success: true, returnData: "0x" }, + { + success: true, + returnData: "0x0000000000000000000000000000000000000000000000000000000000000000", + }, + ]); + }); + + const result = await facilitator.verify(makeERC6492Payload(erc6492Sig), erc6492Requirements); + + expect(result.isValid).toBe(false); + expect(result.payer).toBe(erc6492Payer); + }); + + it("should reject undeployed smart wallet without ERC-6492 deployment info", async () => { + const longNonERC6492Sig = ("0x" + "ab".repeat(100)) as `0x${string}`; + mockFacilitatorSigner.verifyTypedData = vi.fn().mockResolvedValue(false); + mockFacilitatorSigner.getCode = vi.fn().mockResolvedValue("0x"); + + const result = await facilitator.verify( + makeERC6492Payload(longNonERC6492Sig), + erc6492Requirements, + ); + + expect(result.isValid).toBe(false); + expect(result.invalidReason).toBe("invalid_exact_evm_payload_undeployed_smart_wallet"); + expect(result.payer).toBe(erc6492Payer); + }); + + it("should accept deployed smart wallet when verifyTypedData fails but simulation passes (ERC-1271)", async () => { + mockFacilitatorSigner.verifyTypedData = vi.fn().mockResolvedValue(false); + mockFacilitatorSigner.getCode = vi.fn().mockResolvedValue("0x6080604052"); + mockFacilitatorSigner.readContract = vi + .fn() + .mockImplementation(({ address }: { address: string }) => { + if (address === MULTICALL3_ADDRESS) { + return Promise.resolve([ + { success: true, returnData: "0x" }, + { success: true, returnData: "0x" }, + ]); + } + return Promise.resolve(undefined); + }); + + const result = await facilitator.verify(makeERC6492Payload(erc6492Sig), erc6492Requirements); + + expect(result.isValid).toBe(true); + expect(result.payer).toBe(erc6492Payer); + }); + + it("should reject deployed smart wallet when both verifyTypedData and simulation fail", async () => { + mockFacilitatorSigner.verifyTypedData = vi.fn().mockResolvedValue(false); + mockFacilitatorSigner.getCode = vi.fn().mockResolvedValue("0x6080604052"); + mockFacilitatorSigner.readContract = vi + .fn() + .mockRejectedValue(new Error("execution reverted")); + + const result = await facilitator.verify(makeERC6492Payload(erc6492Sig), erc6492Requirements); + + expect(result.isValid).toBe(false); + expect(result.invalidReason).toBe(Errors.ErrEip3009SimulationFailed); + }); + }); + describe("EIP-2612 Gas Sponsoring - Settlement", () => { const permit2Requirements: PaymentRequirements = { scheme: "exact", @@ -833,14 +1098,8 @@ describe("ExactEvmScheme (Facilitator)", () => { } it("should call settleWithPermit when EIP-2612 extension is present", async () => { - // Mock: allowance=0 (verify), balance=sufficient, allowance=0 (settle re-verify), balance=sufficient - mockFacilitatorSigner.readContract = vi - .fn() - .mockImplementation(({ functionName }: { functionName: string }) => { - if (functionName === "allowance") return Promise.resolve(0n); - if (functionName === "balanceOf") return Promise.resolve(BigInt("10000000")); - return Promise.resolve(0n); - }); + // settle's re-verify has simulate=false, so readContract is not called for simulation + mockFacilitatorSigner.readContract = vi.fn().mockResolvedValue(undefined); const payload = makePermit2Payload(makeEip2612Extension()); const result = await facilitator.settle(payload, permit2Requirements); @@ -848,15 +1107,14 @@ describe("ExactEvmScheme (Facilitator)", () => { expect(result.success).toBe(true); expect(result.transaction).toBe("0xtxhash"); - // Verify writeContract was called with settleWithPermit const writeCall = (mockFacilitatorSigner.writeContract as ReturnType).mock .calls[0][0]; expect(writeCall.functionName).toBe("settleWithPermit"); }); it("should call settle (not settleWithPermit) when no EIP-2612 extension", async () => { - // Mock: allowance=sufficient, balance=sufficient - mockFacilitatorSigner.readContract = vi.fn().mockResolvedValue(BigInt("10000000000")); + // settle's re-verify has simulate=false + mockFacilitatorSigner.readContract = vi.fn().mockResolvedValue(undefined); const payload = makePermit2Payload(); const result = await facilitator.settle(payload, permit2Requirements); @@ -864,14 +1122,13 @@ describe("ExactEvmScheme (Facilitator)", () => { expect(result.success).toBe(true); expect(result.transaction).toBe("0xtxhash"); - // Verify writeContract was called with settle (not settleWithPermit) const writeCall = (mockFacilitatorSigner.writeContract as ReturnType).mock .calls[0][0]; expect(writeCall.functionName).toBe("settle"); }); it("should map Permit2612AmountMismatch contract revert to permit2_2612_amount_mismatch", async () => { - mockFacilitatorSigner.readContract = vi.fn().mockResolvedValue(BigInt("10000000000")); + mockFacilitatorSigner.readContract = vi.fn().mockResolvedValue(undefined); mockFacilitatorSigner.writeContract = vi .fn() .mockRejectedValue(new Error("execution reverted: Permit2612AmountMismatch()")); @@ -884,7 +1141,7 @@ describe("ExactEvmScheme (Facilitator)", () => { }); it("should map InvalidAmount contract revert to permit2_invalid_amount", async () => { - mockFacilitatorSigner.readContract = vi.fn().mockResolvedValue(BigInt("10000000000")); + mockFacilitatorSigner.readContract = vi.fn().mockResolvedValue(undefined); mockFacilitatorSigner.writeContract = vi .fn() .mockRejectedValue(new Error("execution reverted: InvalidAmount()")); @@ -897,7 +1154,7 @@ describe("ExactEvmScheme (Facilitator)", () => { }); it("should map InvalidNonce contract revert to permit2_invalid_nonce", async () => { - mockFacilitatorSigner.readContract = vi.fn().mockResolvedValue(BigInt("10000000000")); + mockFacilitatorSigner.readContract = vi.fn().mockResolvedValue(undefined); mockFacilitatorSigner.writeContract = vi .fn() .mockRejectedValue(new Error("execution reverted: InvalidNonce()")); @@ -910,13 +1167,8 @@ describe("ExactEvmScheme (Facilitator)", () => { }); it("should pass correct EIP-2612 permit struct to settleWithPermit", async () => { - mockFacilitatorSigner.readContract = vi - .fn() - .mockImplementation(({ functionName }: { functionName: string }) => { - if (functionName === "allowance") return Promise.resolve(0n); - if (functionName === "balanceOf") return Promise.resolve(BigInt("10000000")); - return Promise.resolve(0n); - }); + // settle's re-verify has simulate=false + mockFacilitatorSigner.readContract = vi.fn().mockResolvedValue(undefined); const extensions = makeEip2612Extension(); const payload = makePermit2Payload(extensions); @@ -926,14 +1178,12 @@ describe("ExactEvmScheme (Facilitator)", () => { .calls[0][0]; expect(writeCall.functionName).toBe("settleWithPermit"); - // First arg to settleWithPermit is the EIP-2612 permit struct (value, deadline, r, s, v) const permit2612Struct = writeCall.args[0]; expect(permit2612Struct.value).toBeDefined(); expect(permit2612Struct.deadline).toBeDefined(); expect(permit2612Struct.r).toBeDefined(); expect(permit2612Struct.s).toBeDefined(); expect(permit2612Struct.v).toBeDefined(); - // v should be a number (27 or 28) expect(typeof permit2612Struct.v).toBe("number"); }); }); @@ -1006,16 +1256,40 @@ describe("ExactEvmScheme (Facilitator)", () => { function makeErc20Context() { return { getExtension: vi.fn().mockImplementation((key: string) => { - if (key === ERC20_APPROVAL_GAS_SPONSORING.key) { - return { key: ERC20_APPROVAL_GAS_SPONSORING.key }; + if (key === ERC20_APPROVAL_GAS_SPONSORING_KEY) { + return { key: ERC20_APPROVAL_GAS_SPONSORING_KEY }; } return undefined; }), }; } - it("should reject when allowance is 0 and no ERC-20 extension (no context)", async () => { - mockFacilitatorSigner.readContract = vi.fn().mockResolvedValueOnce(0n); + it("should reject when simulation fails and no ERC-20 extension (no context)", async () => { + // Simulation of settle() fails, diagnostic multicall shows low allowance + mockFacilitatorSigner.readContract = vi + .fn() + .mockImplementation(({ address }: { address: string }) => { + if (address === x402ExactPermit2ProxyAddress) { + return Promise.reject(new Error("execution reverted")); + } + if (address === MULTICALL3_ADDRESS) { + return Promise.resolve([ + { + success: true, + returnData: "0x000000000000000000000000000000000022D473030F116dDEE9F6B43aC78BA3", + }, + { + success: true, + returnData: "0x00000000000000000000000000000000000000000000000000000000000f4240", + }, + { + success: true, + returnData: "0x0000000000000000000000000000000000000000000000000000000000000000", + }, + ]); + } + return Promise.resolve(BigInt(0)); + }); const payload = makeErc20Permit2Payload(); const result = await facilitator.verify(payload, erc20Requirements); @@ -1025,7 +1299,7 @@ describe("ExactEvmScheme (Facilitator)", () => { }); it("should reject when ERC-20 extension has invalid format (bad address)", async () => { - mockFacilitatorSigner.readContract = vi.fn().mockResolvedValueOnce(0n); + mockFacilitatorSigner.readContract = vi.fn().mockResolvedValue(undefined); const payload = makeErc20Permit2Payload({ erc20ApprovalGasSponsoring: { @@ -1048,7 +1322,7 @@ describe("ExactEvmScheme (Facilitator)", () => { }); it("should reject when ERC-20 extension `from` doesn't match payer", async () => { - mockFacilitatorSigner.readContract = vi.fn().mockResolvedValueOnce(0n); + mockFacilitatorSigner.readContract = vi.fn().mockResolvedValue(undefined); const payload = makeErc20Permit2Payload({ erc20ApprovalGasSponsoring: { @@ -1071,7 +1345,7 @@ describe("ExactEvmScheme (Facilitator)", () => { }); it("should reject when ERC-20 extension `asset` doesn't match token", async () => { - mockFacilitatorSigner.readContract = vi.fn().mockResolvedValueOnce(0n); + mockFacilitatorSigner.readContract = vi.fn().mockResolvedValue(undefined); const payload = makeErc20Permit2Payload({ erc20ApprovalGasSponsoring: { @@ -1094,7 +1368,7 @@ describe("ExactEvmScheme (Facilitator)", () => { }); it("should reject when ERC-20 extension spender is not PERMIT2_ADDRESS", async () => { - mockFacilitatorSigner.readContract = vi.fn().mockResolvedValueOnce(0n); + mockFacilitatorSigner.readContract = vi.fn().mockResolvedValue(undefined); const payload = makeErc20Permit2Payload({ erc20ApprovalGasSponsoring: { @@ -1116,11 +1390,26 @@ describe("ExactEvmScheme (Facilitator)", () => { expect(result.invalidReason).toBe("erc20_approval_spender_not_permit2"); }); - it("should accept when allowance insufficient but valid ERC-20 extension present", async () => { - // allowance=0 (verifyPermit2 returns permit2_allowance_required, scheme handles it) - mockFacilitatorSigner.readContract = vi.fn().mockResolvedValueOnce(0n); + it("should accept when valid ERC-20 extension present and prerequisites pass", async () => { + // checkPermit2Prerequisites multicall: proxy deployed + sufficient token balance + mockFacilitatorSigner.readContract = vi + .fn() + .mockImplementation(({ address }: { address: string }) => { + if (address === MULTICALL3_ADDRESS) { + return Promise.resolve([ + { + success: true, + returnData: "0x000000000000000000000000000000000022D473030F116dDEE9F6B43aC78BA3", + }, + { + success: true, + returnData: "0x00000000000000000000000000000000000000000000000000000000000f4240", + }, + ]); + } + return Promise.resolve(undefined); + }); - // Mock viem functions used in validateErc20ApprovalForPayment const { parseTransaction, recoverTransactionAddress } = await import("viem"); vi.mocked(parseTransaction).mockReturnValue({ to: TOKEN_ADDRESS, @@ -1131,14 +1420,13 @@ describe("ExactEvmScheme (Facilitator)", () => { const payload = makeErc20Permit2Payload(makeValidErc20Extension()); const result = await facilitator.verify(payload, erc20Requirements, makeErc20Context()); - // Should NOT fail with permit2_allowance_required if (!result.isValid) { expect(result.invalidReason).not.toBe("permit2_allowance_required"); } }); it("should reject when calldata targets wrong address (not PERMIT2_ADDRESS)", async () => { - mockFacilitatorSigner.readContract = vi.fn().mockResolvedValueOnce(0n); + mockFacilitatorSigner.readContract = vi.fn().mockResolvedValue(undefined); const wrongSpenderCalldata = "0x095ea7b3" + @@ -1158,13 +1446,156 @@ describe("ExactEvmScheme (Facilitator)", () => { expect(result.isValid).toBe(false); expect(result.invalidReason).toBe("erc20_approval_tx_wrong_spender"); }); + + it("Path 2 simulation: should accept when extension signer simulateTransactions returns true", async () => { + const { parseTransaction, recoverTransactionAddress } = await import("viem"); + vi.mocked(parseTransaction).mockReturnValue({ + to: TOKEN_ADDRESS, + data: APPROVE_CALLDATA as `0x${string}`, + } as any); + vi.mocked(recoverTransactionAddress).mockResolvedValue(PAYER); + + mockFacilitatorSigner.readContract = vi.fn().mockResolvedValue(undefined); + + const mockSimulateTransactions = vi.fn().mockResolvedValue(true); + + const mockContext = { + getExtension: vi.fn().mockImplementation((key: string) => { + if (key === ERC20_APPROVAL_GAS_SPONSORING_KEY) { + return { + key: ERC20_APPROVAL_GAS_SPONSORING_KEY, + signer: { + ...mockFacilitatorSigner, + sendTransactions: vi.fn(), + simulateTransactions: mockSimulateTransactions, + }, + }; + } + return undefined; + }), + }; + + const payload = makeErc20Permit2Payload(makeValidErc20Extension()); + const result = await facilitator.verify(payload, erc20Requirements, mockContext); + + expect(mockSimulateTransactions).toHaveBeenCalledOnce(); + const bundle = mockSimulateTransactions.mock.calls[0][0]; + expect(bundle[0]).toBe(MOCK_SIGNED_TX); + expect(bundle[1]).toHaveProperty("to"); + expect(bundle[1]).toHaveProperty("data"); + expect(result.isValid).toBe(true); + }); + + it("Path 2 simulation: should reject with diagnostic reason when simulateTransactions returns false", async () => { + const { parseTransaction, recoverTransactionAddress } = await import("viem"); + vi.mocked(parseTransaction).mockReturnValue({ + to: TOKEN_ADDRESS, + data: APPROVE_CALLDATA as `0x${string}`, + } as any); + vi.mocked(recoverTransactionAddress).mockResolvedValue(PAYER); + + mockFacilitatorSigner.readContract = vi + .fn() + .mockImplementation(({ address }: { address: string }) => { + if (address === MULTICALL3_ADDRESS) { + // diagnostic multicall: proxy deployed, balance insufficient + return Promise.resolve([ + { + success: true, + returnData: "0x000000000000000000000000000000000022D473030F116dDEE9F6B43aC78BA3", + }, + { + success: true, + returnData: "0x0000000000000000000000000000000000000000000000000000000000000001", + }, + { + success: true, + returnData: "0x0000000000000000000000000000000000000000000000000000000000000000", + }, + ]); + } + return Promise.resolve(undefined); + }); + + const mockSimulateTransactions = vi.fn().mockResolvedValue(false); + + const mockContext = { + getExtension: vi.fn().mockImplementation((key: string) => { + if (key === ERC20_APPROVAL_GAS_SPONSORING_KEY) { + return { + key: ERC20_APPROVAL_GAS_SPONSORING_KEY, + signer: { + ...mockFacilitatorSigner, + sendTransactions: vi.fn(), + simulateTransactions: mockSimulateTransactions, + }, + }; + } + return undefined; + }), + }; + + const payload = makeErc20Permit2Payload(makeValidErc20Extension()); + const result = await facilitator.verify(payload, erc20Requirements, mockContext); + + expect(result.isValid).toBe(false); + expect(result.invalidReason).toBe(Errors.ErrPermit2InsufficientBalance); + }); + + it("Path 2 simulation: should fall back to checkPermit2Prerequisites when simulateTransactions is absent", async () => { + const { parseTransaction, recoverTransactionAddress } = await import("viem"); + vi.mocked(parseTransaction).mockReturnValue({ + to: TOKEN_ADDRESS, + data: APPROVE_CALLDATA as `0x${string}`, + } as any); + vi.mocked(recoverTransactionAddress).mockResolvedValue(PAYER); + + // prerequisites pass: proxy deployed + sufficient token balance + mockFacilitatorSigner.readContract = vi + .fn() + .mockImplementation(({ address }: { address: string }) => { + if (address === MULTICALL3_ADDRESS) { + return Promise.resolve([ + { + success: true, + returnData: "0x000000000000000000000000000000000022D473030F116dDEE9F6B43aC78BA3", + }, + { + success: true, + returnData: "0x00000000000000000000000000000000000000000000000000000000000f4240", + }, + ]); + } + return Promise.resolve(undefined); + }); + + // signer has sendTransactions but no simulateTransactions (legacy) + const mockContext = { + getExtension: vi.fn().mockImplementation((key: string) => { + if (key === ERC20_APPROVAL_GAS_SPONSORING_KEY) { + return { + key: ERC20_APPROVAL_GAS_SPONSORING_KEY, + signer: { + ...mockFacilitatorSigner, + sendTransactions: vi.fn(), + }, + }; + } + return undefined; + }), + }; + + const payload = makeErc20Permit2Payload(makeValidErc20Extension()); + const result = await facilitator.verify(payload, erc20Requirements, mockContext); + + expect(result.isValid).toBe(true); + }); }); describe("ERC-20 Approval Gas Sponsoring - Settlement", () => { const PAYER = "0x1234567890123456789012345678901234567890" as `0x${string}`; const TOKEN_ADDRESS = "0xeED520980fC7C7B4eB379B96d61CEdea2423005a" as `0x${string}`; const MOCK_SIGNED_TX = "0x02f8ab0102030405060708" as `0x${string}`; - const APPROVAL_TX_HASH = "0xapproval_tx_hash_mock" as `0x${string}`; const APPROVE_CALLDATA = `0x095ea7b3000000000000000000000000000000000022d473030f116ddee9f6b43ac78ba3` + @@ -1232,35 +1663,28 @@ describe("ExactEvmScheme (Facilitator)", () => { } as any); vi.mocked(recoverTransactionAddress).mockResolvedValue(PAYER); - // Base signer: allowance=0 (re-verify sees ERC-20 extension in context and accepts it) - mockFacilitatorSigner.readContract = vi - .fn() - .mockImplementation(({ functionName }: { functionName: string }) => { - if (functionName === "allowance") return Promise.resolve(0n); - if (functionName === "balanceOf") return Promise.resolve(BigInt("10000000")); - return Promise.resolve(0n); - }); + // settle's re-verify has simulate=false, so no simulation calls + mockFacilitatorSigner.readContract = vi.fn().mockResolvedValue(undefined); const SETTLE_TX_HASH = "0xsettle_tx_hash_mock" as `0x${string}`; - const mockExtSendRawTx = vi.fn().mockResolvedValue(APPROVAL_TX_HASH); - const mockExtWriteContract = vi.fn().mockResolvedValue(SETTLE_TX_HASH); + const mockSendTransactions = vi.fn().mockResolvedValue([SETTLE_TX_HASH]); const mockExtWaitForReceipt = vi.fn().mockResolvedValue({ status: "success" }); - // Extension signer has all FacilitatorEvmSigner methods + sendRawTransaction + // Extension signer has all FacilitatorEvmSigner methods + sendTransactions const mockContext = { getExtension: vi.fn().mockImplementation((key: string) => { - if (key === ERC20_APPROVAL_GAS_SPONSORING.key) { + if (key === ERC20_APPROVAL_GAS_SPONSORING_KEY) { return { - key: ERC20_APPROVAL_GAS_SPONSORING.key, + key: ERC20_APPROVAL_GAS_SPONSORING_KEY, signer: { getAddresses: vi.fn().mockReturnValue([PAYER]), readContract: mockFacilitatorSigner.readContract, verifyTypedData: mockFacilitatorSigner.verifyTypedData, - writeContract: mockExtWriteContract, + writeContract: vi.fn(), sendTransaction: vi.fn(), waitForTransactionReceipt: mockExtWaitForReceipt, getCode: vi.fn().mockResolvedValue("0x"), - sendRawTransaction: mockExtSendRawTx, + sendTransactions: mockSendTransactions, }, }; } @@ -1271,18 +1695,73 @@ describe("ExactEvmScheme (Facilitator)", () => { const payload = makeErc20Permit2Payload(makeValidErc20Extension()); const result = await facilitator.settle(payload, erc20Requirements, mockContext); - // Extension signer broadcast the approval tx - expect(mockExtSendRawTx).toHaveBeenCalledWith({ serializedTransaction: MOCK_SIGNED_TX }); - - // Extension signer called settle (not the base signer) - expect(mockExtWriteContract).toHaveBeenCalled(); - const writeCall = mockExtWriteContract.mock.calls[0][0]; - expect(writeCall.functionName).toBe("settle"); + // Extension signer called sendTransactions with [approvalTx, settleCall] + expect(mockSendTransactions).toHaveBeenCalled(); + const transactions = mockSendTransactions.mock.calls[0][0]; + expect(transactions[0]).toBe(MOCK_SIGNED_TX); + expect(transactions[1]).toHaveProperty("to"); + expect(transactions[1]).toHaveProperty("data"); // Base signer's writeContract should NOT have been called expect(mockFacilitatorSigner.writeContract).not.toHaveBeenCalled(); expect(result.success).toBe(true); }); + + it("should resolve extension signer by network when signerForNetwork is present", async () => { + const { parseTransaction, recoverTransactionAddress } = await import("viem"); + vi.mocked(parseTransaction).mockReturnValue({ + to: TOKEN_ADDRESS, + data: APPROVE_CALLDATA as `0x${string}`, + } as any); + vi.mocked(recoverTransactionAddress).mockResolvedValue(PAYER); + + // settle's re-verify has simulate=false + mockFacilitatorSigner.readContract = vi.fn().mockResolvedValue(undefined); + + const selectedSignerSendTransactions = vi + .fn() + .mockResolvedValue(["0xsettle_hash" as `0x${string}`]); + const selectedSignerWait = vi.fn().mockResolvedValue({ status: "success" }); + const fallbackSignerSendTransactions = vi.fn(); + + const mockContext = { + getExtension: vi.fn().mockImplementation((key: string) => { + if (key !== ERC20_APPROVAL_GAS_SPONSORING_KEY) return undefined; + return { + key: ERC20_APPROVAL_GAS_SPONSORING_KEY, + signer: { + getAddresses: vi.fn().mockReturnValue([PAYER]), + readContract: mockFacilitatorSigner.readContract, + verifyTypedData: mockFacilitatorSigner.verifyTypedData, + writeContract: vi.fn(), + sendTransaction: vi.fn(), + waitForTransactionReceipt: selectedSignerWait, + getCode: vi.fn().mockResolvedValue("0x"), + sendTransactions: fallbackSignerSendTransactions, + }, + signerForNetwork: (network: string) => { + if (network !== "eip155:84532") return undefined; + return { + getAddresses: vi.fn().mockReturnValue([PAYER]), + readContract: mockFacilitatorSigner.readContract, + verifyTypedData: mockFacilitatorSigner.verifyTypedData, + writeContract: vi.fn(), + sendTransaction: vi.fn(), + waitForTransactionReceipt: selectedSignerWait, + getCode: vi.fn().mockResolvedValue("0x"), + sendTransactions: selectedSignerSendTransactions, + }; + }, + }; + }), + }; + + const payload = makeErc20Permit2Payload(makeValidErc20Extension()); + await facilitator.settle(payload, erc20Requirements, mockContext); + + expect(selectedSignerSendTransactions).toHaveBeenCalled(); + expect(fallbackSignerSendTransactions).not.toHaveBeenCalled(); + }); }); }); diff --git a/typescript/packages/mechanisms/evm/test/unit/server.test.ts b/typescript/packages/mechanisms/evm/test/unit/server.test.ts index 892abfd9e2..dec6b5f6b0 100644 --- a/typescript/packages/mechanisms/evm/test/unit/server.test.ts +++ b/typescript/packages/mechanisms/evm/test/unit/server.test.ts @@ -13,6 +13,7 @@ describe("ExactEvmScheme (Server)", () => { expect(result.amount).toBe("100000"); // 0.10 USDC = 100000 smallest units expect(result.asset).toBe("0x036CbD53842c5426634e7929541eC2318f3dCF7e"); expect(result.extra).toEqual({ name: "USDC", version: "2" }); + expect(result.extra).not.toHaveProperty("assetTransferMethod"); }); it("should parse simple number string prices", async () => { @@ -51,6 +52,28 @@ describe("ExactEvmScheme (Server)", () => { expect(result.asset).toBe("0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"); expect(result.amount).toBe("1000000"); expect(result.extra).toEqual({ name: "USD Coin", version: "2" }); + expect(result.extra).not.toHaveProperty("assetTransferMethod"); + }); + }); + + describe("MegaETH network", () => { + const network = "eip155:4326"; + + it("should parse dollar string and include assetTransferMethod permit2", async () => { + const result = await server.parsePrice("$0.10", network); + expect(result.asset).toBe("0xFAfDdbb3FC7688494971a79cc65DCa3EF82079E7"); + expect(result.amount).toBe("100000000000000000"); // 0.10 * 10^18 + expect(result.extra).toEqual({ + name: "MegaUSD", + version: "1", + assetTransferMethod: "permit2", + }); + }); + + it("should produce correct 18-decimal amount", async () => { + const result = await server.parsePrice("1.00", network); + expect(result.amount).toBe("1000000000000000000"); // 1.00 * 10^18 + expect(result.extra).toHaveProperty("assetTransferMethod", "permit2"); }); }); diff --git a/typescript/packages/mechanisms/evm/test/unit/signer.test.ts b/typescript/packages/mechanisms/evm/test/unit/signer.test.ts index ed2faf15fa..2eaf5737e5 100644 --- a/typescript/packages/mechanisms/evm/test/unit/signer.test.ts +++ b/typescript/packages/mechanisms/evm/test/unit/signer.test.ts @@ -31,15 +31,15 @@ describe("EVM Signer Converters", () => { expect(result.readContract).toBeDefined(); }); - it("should throw when neither signer nor publicClient has readContract", () => { + it("should return minimal signer when no readContract exists", () => { const mockAccount = { address: "0x1234567890123456789012345678901234567890" as `0x${string}`, signTypedData: async () => "0xsignature" as `0x${string}`, }; - expect(() => toClientEvmSigner(mockAccount)).toThrow( - "toClientEvmSigner requires either a signer with readContract or a publicClient", - ); + const result = toClientEvmSigner(mockAccount); + expect(result.address).toBe(mockAccount.address); + expect(result.readContract).toBeUndefined(); }); }); diff --git a/typescript/packages/mechanisms/evm/test/unit/upto/client.test.ts b/typescript/packages/mechanisms/evm/test/unit/upto/client.test.ts new file mode 100644 index 0000000000..e0e3099048 --- /dev/null +++ b/typescript/packages/mechanisms/evm/test/unit/upto/client.test.ts @@ -0,0 +1,305 @@ +import { describe, it, expect, beforeEach, vi } from "vitest"; +import { UptoEvmScheme } from "../../../src/upto/client/scheme"; +import { + createPermit2ApprovalTx, + getPermit2AllowanceReadParams, +} from "../../../src/upto/client/permit2"; +import { createUptoPermit2Payload } from "../../../src/upto/client/permit2"; +import type { ClientEvmSigner } from "../../../src/signer"; +import { PaymentRequirements } from "@x402/core/types"; +import { PERMIT2_ADDRESS, x402UptoPermit2ProxyAddress } from "../../../src/constants"; +import { isUptoPermit2Payload } from "../../../src/types"; + +const FACILITATOR_ADDRESS = "0xFAC11174700123456789012345678901234aBCDe" as `0x${string}`; + +describe("UptoEvmScheme (Client)", () => { + let client: UptoEvmScheme; + let mockSigner: ClientEvmSigner; + + beforeEach(() => { + mockSigner = { + address: "0x1234567890123456789012345678901234567890", + signTypedData: vi.fn().mockResolvedValue("0xmocksignature123456789"), + readContract: vi.fn().mockResolvedValue(BigInt(0)), + }; + client = new UptoEvmScheme(mockSigner); + }); + + function makeRequirements(overrides?: Partial): PaymentRequirements { + return { + scheme: "upto", + network: "eip155:8453", + amount: "1000000", + asset: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", + payTo: "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb0", + maxTimeoutSeconds: 300, + extra: { assetTransferMethod: "permit2", facilitatorAddress: FACILITATOR_ADDRESS }, + ...overrides, + }; + } + + describe("Construction", () => { + it("should create instance with signer", () => { + expect(client).toBeDefined(); + expect(client.scheme).toBe("upto"); + }); + }); + + describe("createPaymentPayload", () => { + it("should create Permit2 payload with correct structure", async () => { + const result = await client.createPaymentPayload(2, makeRequirements()); + const payload = result.payload; + + expect(result.x402Version).toBe(2); + expect(payload.signature).toBeDefined(); + expect(payload.permit2Authorization).toBeDefined(); + expect(isUptoPermit2Payload(payload)).toBe(true); + }); + + it("should set spender to x402UptoPermit2ProxyAddress", async () => { + const result = await client.createPaymentPayload(2, makeRequirements()); + + expect(result.payload.permit2Authorization.spender).toBe(x402UptoPermit2ProxyAddress); + }); + + it("should set witness.to to payTo address", async () => { + const payToAddress = "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb0"; + const result = await client.createPaymentPayload(2, makeRequirements()); + + expect(result.payload.permit2Authorization.witness.to.toLowerCase()).toBe( + payToAddress.toLowerCase(), + ); + }); + + it("should set witness.facilitator to facilitatorAddress from extra", async () => { + const result = await client.createPaymentPayload(2, makeRequirements()); + + expect(result.payload.permit2Authorization.witness.facilitator.toLowerCase()).toBe( + FACILITATOR_ADDRESS.toLowerCase(), + ); + }); + + it("should throw if facilitatorAddress is missing from extra", async () => { + const requirements = makeRequirements({ + extra: { assetTransferMethod: "permit2" }, + }); + + await expect(client.createPaymentPayload(2, requirements)).rejects.toThrow( + "upto scheme requires facilitatorAddress", + ); + }); + + it("should use requirements.amount as permitted amount", async () => { + const requirements = makeRequirements({ amount: "2500000" }); + + const result = await client.createPaymentPayload(2, requirements); + + expect(result.payload.permit2Authorization.permitted.amount).toBe("2500000"); + }); + + it("should use signer's address as from", async () => { + const result = await client.createPaymentPayload(2, makeRequirements()); + + expect(result.payload.permit2Authorization.from).toBe(mockSigner.address); + }); + + it("should use Permit2 EIP-712 domain for signing", async () => { + await client.createPaymentPayload(2, makeRequirements()); + + const callArgs = (mockSigner.signTypedData as ReturnType).mock.calls[0][0]; + expect(callArgs.domain.name).toBe("Permit2"); + expect(callArgs.domain.verifyingContract).toBe(PERMIT2_ADDRESS); + expect(callArgs.primaryType).toBe("PermitWitnessTransferFrom"); + }); + + it("should use uptoPermit2WitnessTypes with facilitator in Witness", async () => { + await client.createPaymentPayload(2, makeRequirements()); + + const callArgs = (mockSigner.signTypedData as ReturnType).mock.calls[0][0]; + const witnessType = callArgs.types.Witness; + expect(witnessType).toEqual([ + { name: "to", type: "address" }, + { name: "facilitator", type: "address" }, + { name: "validAfter", type: "uint256" }, + ]); + }); + + it("should set deadline in the future based on maxTimeoutSeconds", async () => { + const fakeNow = 1700000000000; + vi.useFakeTimers(); + vi.setSystemTime(fakeNow); + + try { + const requirements = makeRequirements({ maxTimeoutSeconds: 600 }); + const result = await client.createPaymentPayload(2, requirements); + const deadline = parseInt(result.payload.permit2Authorization.deadline); + const expectedDeadline = Math.floor(fakeNow / 1000) + 600; + + expect(deadline).toBe(expectedDeadline); + } finally { + vi.useRealTimers(); + } + }); + + it("should set validAfter to 10 minutes before current time", async () => { + const fakeNow = 1700000000000; + vi.useFakeTimers(); + vi.setSystemTime(fakeNow); + + try { + const result = await client.createPaymentPayload(2, makeRequirements()); + const validAfter = parseInt(result.payload.permit2Authorization.witness.validAfter); + const expectedValidAfter = Math.floor(fakeNow / 1000) - 600; + + expect(validAfter).toBe(expectedValidAfter); + } finally { + vi.useRealTimers(); + } + }); + + it("should generate unique nonces across calls", async () => { + const requirements = makeRequirements(); + + const result1 = await client.createPaymentPayload(2, requirements); + const result2 = await client.createPaymentPayload(2, requirements); + + expect(result1.payload.permit2Authorization.nonce).not.toBe( + result2.payload.permit2Authorization.nonce, + ); + }); + + it("should handle different networks", async () => { + const ethereumRequirements = makeRequirements({ + network: "eip155:1", + asset: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + }); + + const result = await client.createPaymentPayload(2, ethereumRequirements); + + expect(result.x402Version).toBe(2); + expect(result.payload.permit2Authorization).toBeDefined(); + }); + + it("should call signTypedData on signer", async () => { + const result = await client.createPaymentPayload(2, makeRequirements()); + + expect(mockSigner.signTypedData).toHaveBeenCalled(); + expect(result.payload.signature).toBeDefined(); + }); + }); +}); + +describe("Permit2 Approval Helpers", () => { + describe("createPermit2ApprovalTx", () => { + it("should create approval transaction data", () => { + const tokenAddress = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913" as `0x${string}`; + const tx = createPermit2ApprovalTx(tokenAddress); + + expect(tx.to.toLowerCase()).toBe(tokenAddress.toLowerCase()); + expect(tx.data).toBeDefined(); + expect(tx.data).toMatch(/^0x/); + }); + + it("should encode approve function call", () => { + const tokenAddress = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913" as `0x${string}`; + const tx = createPermit2ApprovalTx(tokenAddress); + + // approve(address,uint256) selector is 0x095ea7b3 + expect(tx.data.startsWith("0x095ea7b3")).toBe(true); + }); + }); + + describe("getPermit2AllowanceReadParams", () => { + it("should return correct read parameters", () => { + const params = getPermit2AllowanceReadParams({ + tokenAddress: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", + ownerAddress: "0x1234567890123456789012345678901234567890", + }); + + expect(params.address.toLowerCase()).toBe( + "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913".toLowerCase(), + ); + expect(params.functionName).toBe("allowance"); + expect(params.args[0].toLowerCase()).toBe( + "0x1234567890123456789012345678901234567890".toLowerCase(), + ); + expect(params.args[1]).toBe(PERMIT2_ADDRESS); + }); + + it("should include allowance ABI", () => { + const params = getPermit2AllowanceReadParams({ + tokenAddress: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", + ownerAddress: "0x1234567890123456789012345678901234567890", + }); + + expect(params.abi).toBeDefined(); + expect(params.abi[0].name).toBe("allowance"); + }); + }); +}); + +describe("createUptoPermit2Payload (direct)", () => { + let mockSigner: ClientEvmSigner; + + beforeEach(() => { + mockSigner = { + address: "0x1234567890123456789012345678901234567890", + signTypedData: vi.fn().mockResolvedValue("0xmocksignature123456789"), + readContract: vi.fn().mockResolvedValue(BigInt(0)), + }; + }); + + it("should throw when facilitatorAddress is missing from extra", async () => { + const requirements: PaymentRequirements = { + scheme: "upto", + network: "eip155:8453", + amount: "1000000", + asset: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", + payTo: "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb0", + maxTimeoutSeconds: 300, + extra: { assetTransferMethod: "permit2" }, + }; + + await expect(createUptoPermit2Payload(mockSigner, 2, requirements)).rejects.toThrow( + "upto scheme requires facilitatorAddress", + ); + }); + + it("should throw when extra is undefined", async () => { + const requirements: PaymentRequirements = { + scheme: "upto", + network: "eip155:8453", + amount: "1000000", + asset: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", + payTo: "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb0", + maxTimeoutSeconds: 300, + }; + + await expect(createUptoPermit2Payload(mockSigner, 2, requirements)).rejects.toThrow( + "upto scheme requires facilitatorAddress", + ); + }); + + it("should succeed when facilitatorAddress is provided", async () => { + const requirements: PaymentRequirements = { + scheme: "upto", + network: "eip155:8453", + amount: "1000000", + asset: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", + payTo: "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb0", + maxTimeoutSeconds: 300, + extra: { + assetTransferMethod: "permit2", + facilitatorAddress: "0xFAC11174700123456789012345678901234aBCDe", + }, + }; + + const result = await createUptoPermit2Payload(mockSigner, 2, requirements); + + expect(result.x402Version).toBe(2); + expect(result.payload.signature).toBeDefined(); + expect(result.payload.permit2Authorization.witness.facilitator.toLowerCase()).toBe( + "0xFAC11174700123456789012345678901234aBCDe".toLowerCase(), + ); + }); +}); diff --git a/typescript/packages/mechanisms/evm/test/unit/upto/facilitator.test.ts b/typescript/packages/mechanisms/evm/test/unit/upto/facilitator.test.ts new file mode 100644 index 0000000000..315fb8f29f --- /dev/null +++ b/typescript/packages/mechanisms/evm/test/unit/upto/facilitator.test.ts @@ -0,0 +1,908 @@ +import { describe, it, expect, beforeEach, vi } from "vitest"; +import { UptoEvmScheme } from "../../../src/upto/facilitator/scheme"; +import { verifyUptoPermit2, settleUptoPermit2 } from "../../../src/upto/facilitator/permit2"; +import type { FacilitatorEvmSigner } from "../../../src/signer"; +import { PaymentPayload, PaymentRequirements } from "@x402/core/types"; +import { x402UptoPermit2ProxyAddress } from "../../../src/constants"; +import { + ErrPermit2AmountMismatch, + ErrUptoAmountExceedsPermitted, + ErrUptoFacilitatorMismatch, + ErrUptoSettlementExceedsAmount, + ErrUptoUnauthorizedFacilitator, + ErrUptoInvalidScheme, + ErrUptoNetworkMismatch, +} from "../../../src/upto/facilitator/errors"; +import type { UptoPermit2Payload } from "../../../src/types"; +import { ERC20_APPROVAL_GAS_SPONSORING_KEY } from "../../../src/upto/extensions"; + +vi.mock("viem", async importOriginal => { + const actual = await importOriginal(); + return { + ...actual, + parseTransaction: vi.fn(), + recoverTransactionAddress: vi.fn(), + }; +}); + +const FACILITATOR_ADDRESS = "0xFAC11174700123456789012345678901234aBCDe" as `0x${string}`; + +const now = () => Math.floor(Date.now() / 1000); + +function makePermit2Payload(overrides?: Partial): UptoPermit2Payload { + const base: UptoPermit2Payload = { + signature: "0xmocksig" as `0x${string}`, + permit2Authorization: { + from: "0x1234567890123456789012345678901234567890", + permitted: { + token: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", + amount: "1000000", + }, + spender: x402UptoPermit2ProxyAddress, + nonce: "12345", + deadline: (now() + 3600).toString(), + witness: { + to: "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb0", + facilitator: FACILITATOR_ADDRESS, + validAfter: (now() - 600).toString(), + }, + }, + }; + return { ...base, ...overrides }; +} + +function makePayload( + permit2?: UptoPermit2Payload, + acceptedOverrides?: Record, +): PaymentPayload { + const p2 = permit2 ?? makePermit2Payload(); + return { + x402Version: 2, + accepted: { scheme: "upto", network: "eip155:8453", ...acceptedOverrides }, + payload: p2, + } as PaymentPayload; +} + +function makeRequirements(overrides?: Partial): PaymentRequirements { + return { + scheme: "upto", + network: "eip155:8453", + amount: "1000000", + asset: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", + payTo: "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb0", + maxTimeoutSeconds: 300, + extra: { assetTransferMethod: "permit2", facilitatorAddress: FACILITATOR_ADDRESS }, + ...overrides, + }; +} + +describe("UptoEvmScheme (Facilitator)", () => { + let mockSigner: FacilitatorEvmSigner; + let scheme: UptoEvmScheme; + + beforeEach(() => { + mockSigner = { + getAddresses: () => [FACILITATOR_ADDRESS], + readContract: vi.fn().mockResolvedValue(BigInt("999999999999999999")), + verifyTypedData: vi.fn().mockResolvedValue(true), + writeContract: vi.fn().mockResolvedValue("0xtxhash1234" as `0x${string}`), + sendTransaction: vi.fn(), + waitForTransactionReceipt: vi.fn().mockResolvedValue({ status: "success" }), + getCode: vi.fn(), + }; + scheme = new UptoEvmScheme(mockSigner); + }); + + describe("Construction", () => { + it("should create instance with scheme=upto", () => { + expect(scheme).toBeDefined(); + expect(scheme.scheme).toBe("upto"); + }); + }); + + describe("getExtra", () => { + it("should return facilitatorAddress from signer", () => { + const extra = scheme.getExtra("eip155:8453"); + expect(extra).toEqual({ facilitatorAddress: FACILITATOR_ADDRESS }); + }); + }); + + describe("verify", () => { + it("should return isValid=true for a valid payload", async () => { + const result = await scheme.verify(makePayload(), makeRequirements()); + + expect(result.isValid).toBe(true); + expect(result.payer).toBe("0x1234567890123456789012345678901234567890"); + expect(mockSigner.verifyTypedData).toHaveBeenCalled(); + }); + + it("should verify with uptoPermit2WitnessTypes containing facilitator", async () => { + await scheme.verify(makePayload(), makeRequirements()); + + const callArgs = (mockSigner.verifyTypedData as ReturnType).mock.calls[0][0]; + const witnessType = callArgs.types.Witness; + expect(witnessType).toEqual([ + { name: "to", type: "address" }, + { name: "facilitator", type: "address" }, + { name: "validAfter", type: "uint256" }, + ]); + }); + + it("should reject if scheme is not upto", async () => { + const payload = makePayload(undefined, { scheme: "exact" }); + const requirements = makeRequirements({ scheme: "exact" as any }); + + const result = await scheme.verify(payload, requirements); + + expect(result.isValid).toBe(false); + expect(result.invalidReason).toBe(ErrUptoInvalidScheme); + }); + + it("should reject if network mismatches", async () => { + const payload = makePayload(undefined, { network: "eip155:1" }); + const requirements = makeRequirements({ network: "eip155:8453" as any }); + + const result = await scheme.verify(payload, requirements); + + expect(result.isValid).toBe(false); + expect(result.invalidReason).toBe(ErrUptoNetworkMismatch); + }); + + it("should reject if spender is not x402UptoPermit2ProxyAddress", async () => { + const p2 = makePermit2Payload(); + p2.permit2Authorization.spender = "0x0000000000000000000000000000000000000001"; + const payload = makePayload(p2); + + const result = await scheme.verify(payload, makeRequirements()); + + expect(result.isValid).toBe(false); + expect(result.invalidReason).toBe("invalid_permit2_spender"); + }); + + it("should reject if facilitator in witness does not match signer", async () => { + const p2 = makePermit2Payload(); + p2.permit2Authorization.witness.facilitator = "0x0000000000000000000000000000000000000099"; + const payload = makePayload(p2); + + const result = await scheme.verify(payload, makeRequirements()); + + expect(result.isValid).toBe(false); + expect(result.invalidReason).toBe(ErrUptoFacilitatorMismatch); + }); + + it("should reject if deadline is expired", async () => { + const p2 = makePermit2Payload(); + p2.permit2Authorization.deadline = "1"; + const payload = makePayload(p2); + + const result = await scheme.verify(payload, makeRequirements()); + + expect(result.isValid).toBe(false); + expect(result.invalidReason).toBe("permit2_deadline_expired"); + }); + + it("should reject if validAfter is in the future", async () => { + const p2 = makePermit2Payload(); + p2.permit2Authorization.witness.validAfter = (now() + 3600).toString(); + const payload = makePayload(p2); + + const result = await scheme.verify(payload, makeRequirements()); + + expect(result.isValid).toBe(false); + expect(result.invalidReason).toBe("permit2_not_yet_valid"); + }); + + it("should reject if token mismatches", async () => { + const p2 = makePermit2Payload(); + p2.permit2Authorization.permitted.token = "0x0000000000000000000000000000000000000099"; + const payload = makePayload(p2); + + const result = await scheme.verify(payload, makeRequirements()); + + expect(result.isValid).toBe(false); + expect(result.invalidReason).toBe("permit2_token_mismatch"); + }); + + it("should reject if witness.to doesn't match payTo", async () => { + const p2 = makePermit2Payload(); + p2.permit2Authorization.witness.to = "0x0000000000000000000000000000000000000001"; + const payload = makePayload(p2); + + const result = await scheme.verify(payload, makeRequirements()); + + expect(result.isValid).toBe(false); + expect(result.invalidReason).toBe("invalid_permit2_recipient_mismatch"); + }); + + it("should PASS when permitted.amount equals requirements.amount", async () => { + const result = await scheme.verify(makePayload(), makeRequirements()); + + expect(result.isValid).toBe(true); + }); + + it("should FAIL when permitted.amount !== requirements.amount (too low)", async () => { + const requirements = makeRequirements({ amount: "5000000" }); + + const result = await scheme.verify(makePayload(), requirements); + + expect(result.isValid).toBe(false); + expect(result.invalidReason).toBe(ErrPermit2AmountMismatch); + }); + + it("should FAIL when permitted.amount !== requirements.amount (too high)", async () => { + const p2 = makePermit2Payload(); + p2.permit2Authorization.permitted.amount = "2000000"; + const result = await scheme.verify(makePayload(p2), makeRequirements()); + + expect(result.isValid).toBe(false); + expect(result.invalidReason).toBe(ErrPermit2AmountMismatch); + }); + + it("should reject if signature is invalid", async () => { + mockSigner.verifyTypedData = vi.fn().mockResolvedValue(false); + + const result = await scheme.verify(makePayload(), makeRequirements()); + + expect(result.isValid).toBe(false); + expect(result.invalidReason).toBe("invalid_permit2_signature"); + }); + + it("should reject non-Permit2 payload via scheme wrapper with unsupported_payload_type", async () => { + const payload: PaymentPayload = { + x402Version: 2, + accepted: { scheme: "upto", network: "eip155:8453" }, + payload: { + authorization: { + from: "0x1234567890123456789012345678901234567890", + to: "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb0", + value: "1000000", + validAfter: "0", + validBefore: "999999999999", + nonce: "0x00", + }, + signature: "0x", + }, + } as PaymentPayload; + + const result = await scheme.verify(payload, makeRequirements()); + + expect(result.isValid).toBe(false); + expect(result.invalidReason).toBe("unsupported_payload_type"); + }); + }); + + describe("settle", () => { + it("should settle successfully and return tx hash", async () => { + const result = await scheme.settle(makePayload(), makeRequirements()); + + expect(result.success).toBe(true); + expect(result.transaction).toBe("0xtxhash1234"); + expect(result.payer).toBe("0x1234567890123456789012345678901234567890"); + expect(mockSigner.writeContract).toHaveBeenCalled(); + }); + + it("should pass settlement amount to settle call", async () => { + await scheme.settle(makePayload(), makeRequirements({ amount: "500000" })); + + const writeCall = (mockSigner.writeContract as ReturnType).mock.calls[0][0]; + expect(writeCall.functionName).toBe("settle"); + // args: [permit, amount, owner, witness, signature] + expect(writeCall.args[1]).toBe(BigInt("500000")); + }); + + it("should include facilitator in witness for settle call", async () => { + await scheme.settle(makePayload(), makeRequirements()); + + const writeCall = (mockSigner.writeContract as ReturnType).mock.calls[0][0]; + // args[3] is the witness struct + expect(writeCall.args[3].facilitator.toLowerCase()).toBe(FACILITATOR_ADDRESS.toLowerCase()); + }); + + it("should return success with empty tx for zero settlement amount", async () => { + const requirements = makeRequirements({ amount: "0" }); + + const result = await scheme.settle(makePayload(), requirements); + + expect(result.success).toBe(true); + expect(result.transaction).toBe(""); + expect(mockSigner.writeContract).not.toHaveBeenCalled(); + }); + + it("should succeed when settlement amount < permitted amount (upto core feature)", async () => { + const result = await scheme.settle(makePayload(), makeRequirements({ amount: "500000" })); + + expect(result.success).toBe(true); + expect(result.transaction).toBe("0xtxhash1234"); + expect(mockSigner.writeContract).toHaveBeenCalled(); + + const writeCall = (mockSigner.writeContract as ReturnType).mock.calls[0][0]; + expect(writeCall.functionName).toBe("settle"); + expect(writeCall.args[1]).toBe(BigInt("500000")); + }); + + it("should fail when settlement exceeds permitted amount", async () => { + const p2 = makePermit2Payload(); + p2.permit2Authorization.permitted.amount = "1000000"; + const payload = makePayload(p2); + const requirements = makeRequirements({ amount: "2000000" }); + + const result = await scheme.settle(payload, requirements); + + expect(result.success).toBe(false); + expect(result.errorReason).toBe(ErrUptoSettlementExceedsAmount); + }); + + it("should reject non-Permit2 payload via scheme wrapper with unsupported_payload_type", async () => { + const payload: PaymentPayload = { + x402Version: 2, + accepted: { scheme: "upto", network: "eip155:8453" }, + payload: { + authorization: { + from: "0x1234567890123456789012345678901234567890", + to: "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb0", + value: "1000000", + validAfter: "0", + validBefore: "999999999999", + nonce: "0x00", + }, + signature: "0x", + }, + } as PaymentPayload; + + const result = await scheme.settle(payload, makeRequirements()); + + expect(result.success).toBe(false); + expect(result.errorReason).toBe("unsupported_payload_type"); + }); + }); + + describe("settle error mapping", () => { + it("should map Permit2612AmountMismatch revert", async () => { + mockSigner.writeContract = vi + .fn() + .mockRejectedValue(new Error("execution reverted: Permit2612AmountMismatch()")); + + const result = await scheme.settle(makePayload(), makeRequirements()); + + expect(result.success).toBe(false); + expect(result.errorReason).toBe("permit2_2612_amount_mismatch"); + }); + + it("should map InvalidNonce revert", async () => { + mockSigner.writeContract = vi + .fn() + .mockRejectedValue(new Error("execution reverted: InvalidNonce()")); + + const result = await scheme.settle(makePayload(), makeRequirements()); + + expect(result.success).toBe(false); + expect(result.errorReason).toBe("permit2_invalid_nonce"); + }); + + it("should map AmountExceedsPermitted revert", async () => { + mockSigner.writeContract = vi + .fn() + .mockRejectedValue(new Error("execution reverted: AmountExceedsPermitted()")); + + const result = await scheme.settle(makePayload(), makeRequirements()); + + expect(result.success).toBe(false); + expect(result.errorReason).toBe(ErrUptoAmountExceedsPermitted); + }); + + it("should map UnauthorizedFacilitator revert", async () => { + mockSigner.writeContract = vi + .fn() + .mockRejectedValue(new Error("execution reverted: UnauthorizedFacilitator()")); + + const result = await scheme.settle(makePayload(), makeRequirements()); + + expect(result.success).toBe(false); + expect(result.errorReason).toBe(ErrUptoUnauthorizedFacilitator); + }); + }); + + describe("direct function calls (verifyUptoPermit2 / settleUptoPermit2)", () => { + it("verifyUptoPermit2 returns isValid=true for valid input", async () => { + const p2 = makePermit2Payload(); + const result = await verifyUptoPermit2(mockSigner, makePayload(p2), makeRequirements(), p2); + + expect(result.isValid).toBe(true); + }); + + it("settleUptoPermit2 returns success for zero amount", async () => { + const p2 = makePermit2Payload(); + const result = await settleUptoPermit2( + mockSigner, + makePayload(p2), + makeRequirements({ amount: "0" }), + p2, + ); + + expect(result.success).toBe(true); + expect(result.transaction).toBe(""); + expect(result.amount).toBe("0"); + expect(mockSigner.writeContract).not.toHaveBeenCalled(); + }); + + it("settleUptoPermit2 rejects when settlement exceeds permitted", async () => { + const p2 = makePermit2Payload(); + p2.permit2Authorization.permitted.amount = "500000"; + const result = await settleUptoPermit2( + mockSigner, + makePayload(p2), + makeRequirements({ amount: "1000000" }), + p2, + ); + + expect(result.success).toBe(false); + expect(result.errorReason).toBe(ErrUptoSettlementExceedsAmount); + }); + }); + + describe("getSigners", () => { + it("should return facilitator addresses from signer", () => { + const signers = scheme.getSigners("eip155:8453"); + expect(signers).toEqual([FACILITATOR_ADDRESS]); + }); + }); + + describe("verify edge cases", () => { + it("should handle verifyTypedData throwing an exception", async () => { + mockSigner.verifyTypedData = vi.fn().mockRejectedValue(new Error("RPC unavailable")); + + const result = await scheme.verify(makePayload(), makeRequirements()); + + expect(result.isValid).toBe(false); + expect(result.invalidReason).toBe("invalid_permit2_signature"); + }); + }); + + describe("ERC-6492 / smart contract wallet signature fallback", () => { + it("should reject undeployed EOA with invalid signature", async () => { + mockSigner.verifyTypedData = vi.fn().mockResolvedValue(false); + mockSigner.getCode = vi.fn().mockResolvedValue("0x"); + + const result = await scheme.verify(makePayload(), makeRequirements()); + + expect(result.isValid).toBe(false); + expect(result.invalidReason).toBe("invalid_permit2_signature"); + }); + + it("should fall through to simulation for deployed smart contract when verifyTypedData returns false", async () => { + mockSigner.verifyTypedData = vi.fn().mockResolvedValue(false); + mockSigner.getCode = vi.fn().mockResolvedValue("0x608060405234"); + + const result = await scheme.verify(makePayload(), makeRequirements()); + + expect(result.isValid).toBe(true); + }); + + it("should fall through to simulation for deployed smart contract when verifyTypedData throws", async () => { + mockSigner.verifyTypedData = vi.fn().mockRejectedValue(new Error("unsupported")); + mockSigner.getCode = vi.fn().mockResolvedValue("0x608060405234"); + + const result = await scheme.verify(makePayload(), makeRequirements()); + + expect(result.isValid).toBe(true); + }); + + it("should reject undeployed contract when verifyTypedData throws", async () => { + mockSigner.verifyTypedData = vi.fn().mockRejectedValue(new Error("unsupported")); + mockSigner.getCode = vi.fn().mockResolvedValue("0x"); + + const result = await scheme.verify(makePayload(), makeRequirements()); + + expect(result.isValid).toBe(false); + expect(result.invalidReason).toBe("invalid_permit2_signature"); + }); + }); + + describe("settle receipt handling", () => { + it("should fail when transaction receipt returns reverted status", async () => { + mockSigner.waitForTransactionReceipt = vi.fn().mockResolvedValue({ status: "reverted" }); + + const result = await scheme.settle(makePayload(), makeRequirements()); + + expect(result.success).toBe(false); + }); + }); + + describe("EIP-2612 Gas Sponsoring - Settlement", () => { + const eip2612Requirements = makeRequirements(); + + function makeEip2612Extension() { + const ts = Math.floor(Date.now() / 1000); + return { + eip2612GasSponsoring: { + info: { + from: "0x1234567890123456789012345678901234567890", + asset: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", + spender: "0x000000000022D473030F116dDEE9F6B43aC78BA3", + amount: + "115792089237316195423570985008687907853269984665640564039457584007913129639935", + nonce: "0", + deadline: (ts + 300).toString(), + signature: "0x" + "ab".repeat(32) + "cd".repeat(32) + "1b", + version: "1", + }, + schema: {}, + }, + }; + } + + function makePayloadWithExtensions(extensions?: Record): PaymentPayload { + const p2 = makePermit2Payload(); + return { + x402Version: 2, + accepted: { scheme: "upto", network: "eip155:8453" }, + payload: p2, + resource: { url: "https://test.com", description: "", mimeType: "" }, + ...(extensions ? { extensions } : {}), + } as PaymentPayload; + } + + it("should call settleWithPermit when EIP-2612 extension is present", async () => { + mockSigner.readContract = vi.fn().mockResolvedValue(undefined); + + const payload = makePayloadWithExtensions(makeEip2612Extension()); + const result = await scheme.settle(payload, eip2612Requirements); + + expect(result.success).toBe(true); + expect(result.transaction).toBe("0xtxhash1234"); + + const writeCall = (mockSigner.writeContract as ReturnType).mock.calls[0][0]; + expect(writeCall.functionName).toBe("settleWithPermit"); + }); + + it("should call settle (not settleWithPermit) when no EIP-2612 extension", async () => { + mockSigner.readContract = vi.fn().mockResolvedValue(BigInt("999999999999999999")); + + const payload = makePayloadWithExtensions(); + const result = await scheme.settle(payload, eip2612Requirements); + + expect(result.success).toBe(true); + + const writeCall = (mockSigner.writeContract as ReturnType).mock.calls[0][0]; + expect(writeCall.functionName).toBe("settle"); + }); + + it("should pass correct EIP-2612 permit struct to settleWithPermit", async () => { + mockSigner.readContract = vi.fn().mockResolvedValue(undefined); + + const payload = makePayloadWithExtensions(makeEip2612Extension()); + await scheme.settle(payload, eip2612Requirements); + + const writeCall = (mockSigner.writeContract as ReturnType).mock.calls[0][0]; + expect(writeCall.functionName).toBe("settleWithPermit"); + + const permit2612Struct = writeCall.args[0]; + expect(permit2612Struct.value).toBeDefined(); + expect(permit2612Struct.deadline).toBeDefined(); + expect(permit2612Struct.r).toBeDefined(); + expect(permit2612Struct.s).toBeDefined(); + expect(permit2612Struct.v).toBeDefined(); + expect(typeof permit2612Struct.v).toBe("number"); + }); + + it("should include settlement amount in settleWithPermit args", async () => { + mockSigner.readContract = vi.fn().mockResolvedValue(undefined); + + const payload = makePayloadWithExtensions(makeEip2612Extension()); + await scheme.settle(payload, makeRequirements({ amount: "500000" })); + + const writeCall = (mockSigner.writeContract as ReturnType).mock.calls[0][0]; + expect(writeCall.functionName).toBe("settleWithPermit"); + // settleWithPermit args: [permit2612Struct, permit, amount, owner, witness, signature] + expect(writeCall.args[2]).toBe(BigInt("500000")); + }); + }); + + describe("ERC-20 Approval Gas Sponsoring - Verify", () => { + const PAYER = "0x1234567890123456789012345678901234567890" as `0x${string}`; + const TOKEN_ADDRESS = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913" as `0x${string}`; + const MOCK_SIGNED_TX = "0x02f8ab0102030405060708" as `0x${string}`; + + const APPROVE_CALLDATA = + `0x095ea7b3000000000000000000000000000000000022d473030f116ddee9f6b43ac78ba3` + + `ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff`; + + const erc20VerifyRequirements: PaymentRequirements = { + scheme: "upto", + network: "eip155:8453", + amount: "1000000", + asset: TOKEN_ADDRESS, + payTo: "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb0", + maxTimeoutSeconds: 300, + extra: { assetTransferMethod: "permit2", facilitatorAddress: FACILITATOR_ADDRESS }, + }; + + function makeErc20UptoPayload(extensions?: Record): PaymentPayload { + const ts = Math.floor(Date.now() / 1000); + return { + x402Version: 2, + payload: { + signature: "0x" + "ab".repeat(32) + "cd".repeat(32) + "1b", + permit2Authorization: { + from: PAYER, + permitted: { + token: TOKEN_ADDRESS, + amount: erc20VerifyRequirements.amount, + }, + spender: x402UptoPermit2ProxyAddress, + nonce: "99999", + deadline: (ts + 300).toString(), + witness: { + to: erc20VerifyRequirements.payTo, + facilitator: FACILITATOR_ADDRESS, + validAfter: (ts - 600).toString(), + }, + }, + } as UptoPermit2Payload, + accepted: { scheme: "upto", network: "eip155:8453" }, + resource: { url: "https://test.com", description: "", mimeType: "" }, + ...(extensions ? { extensions } : {}), + } as PaymentPayload; + } + + function makeValidErc20Extension() { + return { + erc20ApprovalGasSponsoring: { + info: { + from: PAYER, + asset: TOKEN_ADDRESS, + spender: "0x000000000022D473030F116dDEE9F6B43aC78BA3", + amount: + "115792089237316195423570985008687907853269984665640564039457584007913129639935", + signedTransaction: MOCK_SIGNED_TX, + version: "1", + }, + schema: {}, + }, + }; + } + + function makeErc20Context() { + return { + getExtension: vi.fn().mockImplementation((key: string) => { + if (key === ERC20_APPROVAL_GAS_SPONSORING_KEY) { + return { key: ERC20_APPROVAL_GAS_SPONSORING_KEY }; + } + return undefined; + }), + }; + } + + it("should reject when ERC-20 extension has invalid format (bad address)", async () => { + mockSigner.readContract = vi.fn().mockResolvedValue(undefined); + + const payload = makeErc20UptoPayload({ + erc20ApprovalGasSponsoring: { + info: { + from: "not-an-address", + asset: TOKEN_ADDRESS, + spender: "0x000000000022D473030F116dDEE9F6B43aC78BA3", + amount: "100", + signedTransaction: MOCK_SIGNED_TX, + version: "1", + }, + schema: {}, + }, + }); + + const result = await scheme.verify(payload, erc20VerifyRequirements, makeErc20Context()); + + expect(result.isValid).toBe(false); + expect(result.invalidReason).toBe("invalid_erc20_approval_extension_format"); + }); + + it("should reject when ERC-20 extension from doesn't match payer", async () => { + mockSigner.readContract = vi.fn().mockResolvedValue(undefined); + + const payload = makeErc20UptoPayload({ + erc20ApprovalGasSponsoring: { + info: { + from: "0x0000000000000000000000000000000000000001", + asset: TOKEN_ADDRESS, + spender: "0x000000000022D473030F116dDEE9F6B43aC78BA3", + amount: "100", + signedTransaction: MOCK_SIGNED_TX, + version: "1", + }, + schema: {}, + }, + }); + + const result = await scheme.verify(payload, erc20VerifyRequirements, makeErc20Context()); + + expect(result.isValid).toBe(false); + expect(result.invalidReason).toBe("erc20_approval_from_mismatch"); + }); + + it("should accept when valid ERC-20 extension present and simulation succeeds", async () => { + mockSigner.readContract = vi.fn().mockResolvedValue(undefined); + + const { parseTransaction, recoverTransactionAddress } = await import("viem"); + vi.mocked(parseTransaction).mockReturnValue({ + to: TOKEN_ADDRESS, + data: APPROVE_CALLDATA as `0x${string}`, + } as any); + vi.mocked(recoverTransactionAddress).mockResolvedValue(PAYER); + + const mockSimulateTransactions = vi.fn().mockResolvedValue(true); + + const mockContext = { + getExtension: vi.fn().mockImplementation((key: string) => { + if (key === ERC20_APPROVAL_GAS_SPONSORING_KEY) { + return { + key: ERC20_APPROVAL_GAS_SPONSORING_KEY, + signer: { + ...mockSigner, + sendTransactions: vi.fn(), + simulateTransactions: mockSimulateTransactions, + }, + }; + } + return undefined; + }), + }; + + const result = await scheme.verify( + makeErc20UptoPayload(makeValidErc20Extension()), + erc20VerifyRequirements, + mockContext, + ); + + expect(result.isValid).toBe(true); + }); + }); + + describe("ERC-20 Approval Gas Sponsoring - Settlement", () => { + const PAYER = "0x1234567890123456789012345678901234567890" as `0x${string}`; + const TOKEN_ADDRESS = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913" as `0x${string}`; + const MOCK_SIGNED_TX = "0x02f8ab0102030405060708" as `0x${string}`; + + const APPROVE_CALLDATA = + `0x095ea7b3000000000000000000000000000000000022d473030f116ddee9f6b43ac78ba3` + + `ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff`; + + const erc20SettleRequirements: PaymentRequirements = { + scheme: "upto", + network: "eip155:8453", + amount: "1000000", + asset: TOKEN_ADDRESS, + payTo: "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb0", + maxTimeoutSeconds: 300, + extra: { assetTransferMethod: "permit2", facilitatorAddress: FACILITATOR_ADDRESS }, + }; + + function makeErc20UptoPayload(extensions?: Record): PaymentPayload { + const ts = Math.floor(Date.now() / 1000); + return { + x402Version: 2, + payload: { + signature: "0x" + "ab".repeat(32) + "cd".repeat(32) + "1b", + permit2Authorization: { + from: PAYER, + permitted: { + token: TOKEN_ADDRESS, + amount: erc20SettleRequirements.amount, + }, + spender: x402UptoPermit2ProxyAddress, + nonce: "99999", + deadline: (ts + 300).toString(), + witness: { + to: erc20SettleRequirements.payTo, + facilitator: FACILITATOR_ADDRESS, + validAfter: (ts - 600).toString(), + }, + }, + } as UptoPermit2Payload, + accepted: { scheme: "upto", network: "eip155:8453" }, + resource: { url: "https://test.com", description: "", mimeType: "" }, + ...(extensions ? { extensions } : {}), + } as PaymentPayload; + } + + function makeValidErc20Extension() { + return { + erc20ApprovalGasSponsoring: { + info: { + from: PAYER, + asset: TOKEN_ADDRESS, + spender: "0x000000000022D473030F116dDEE9F6B43aC78BA3", + amount: + "115792089237316195423570985008687907853269984665640564039457584007913129639935", + signedTransaction: MOCK_SIGNED_TX, + version: "1", + }, + schema: {}, + }, + }; + } + + function makeErc20SettleContext() { + const SETTLE_TX_HASH = "0xsettle_tx_hash_mock" as `0x${string}`; + const mockSendTransactions = vi.fn().mockResolvedValue([SETTLE_TX_HASH]); + const mockExtWaitForReceipt = vi.fn().mockResolvedValue({ status: "success" }); + + const mockContext = { + getExtension: vi.fn().mockImplementation((key: string) => { + if (key === ERC20_APPROVAL_GAS_SPONSORING_KEY) { + return { + key: ERC20_APPROVAL_GAS_SPONSORING_KEY, + signer: { + getAddresses: () => [FACILITATOR_ADDRESS], + readContract: mockSigner.readContract, + verifyTypedData: mockSigner.verifyTypedData, + writeContract: vi.fn(), + sendTransaction: vi.fn(), + waitForTransactionReceipt: mockExtWaitForReceipt, + getCode: vi.fn().mockResolvedValue("0x"), + sendTransactions: mockSendTransactions, + }, + }; + } + return undefined; + }), + }; + + return { mockContext, mockSendTransactions }; + } + + it("should broadcast approval tx via extension signer then settle", async () => { + const { parseTransaction, recoverTransactionAddress } = await import("viem"); + vi.mocked(parseTransaction).mockReturnValue({ + to: TOKEN_ADDRESS, + data: APPROVE_CALLDATA as `0x${string}`, + } as any); + vi.mocked(recoverTransactionAddress).mockResolvedValue(PAYER); + + mockSigner.readContract = vi.fn().mockResolvedValue(undefined); + + const { mockContext, mockSendTransactions } = makeErc20SettleContext(); + + const result = await scheme.settle( + makeErc20UptoPayload(makeValidErc20Extension()), + erc20SettleRequirements, + mockContext, + ); + + expect(mockSendTransactions).toHaveBeenCalled(); + const transactions = mockSendTransactions.mock.calls[0][0]; + expect(transactions[0]).toBe(MOCK_SIGNED_TX); + expect(transactions[1]).toHaveProperty("to"); + expect(transactions[1]).toHaveProperty("data"); + + expect(mockSigner.writeContract).not.toHaveBeenCalled(); + + expect(result.success).toBe(true); + }); + + it("should include settlement amount in ERC-20 approval settle response", async () => { + const { parseTransaction, recoverTransactionAddress } = await import("viem"); + vi.mocked(parseTransaction).mockReturnValue({ + to: TOKEN_ADDRESS, + data: APPROVE_CALLDATA as `0x${string}`, + } as any); + vi.mocked(recoverTransactionAddress).mockResolvedValue(PAYER); + + mockSigner.readContract = vi.fn().mockResolvedValue(undefined); + + const { mockContext } = makeErc20SettleContext(); + + const result = await scheme.settle( + makeErc20UptoPayload(makeValidErc20Extension()), + makeRequirements({ + amount: "750000", + asset: TOKEN_ADDRESS, + extra: { assetTransferMethod: "permit2", facilitatorAddress: FACILITATOR_ADDRESS }, + }), + mockContext, + ); + + expect(result.success).toBe(true); + expect(result.amount).toBe("750000"); + }); + }); +}); diff --git a/typescript/packages/mechanisms/evm/test/unit/upto/server.test.ts b/typescript/packages/mechanisms/evm/test/unit/upto/server.test.ts new file mode 100644 index 0000000000..938a622631 --- /dev/null +++ b/typescript/packages/mechanisms/evm/test/unit/upto/server.test.ts @@ -0,0 +1,247 @@ +import { describe, it, expect } from "vitest"; +import { UptoEvmScheme } from "../../../src/upto/server/scheme"; +import type { PaymentRequirements } from "@x402/core/types"; + +const FACILITATOR_ADDRESS = "0xFAC11174700123456789012345678901234aBCDe"; + +describe("UptoEvmScheme (Server)", () => { + const server = new UptoEvmScheme(); + + describe("parsePrice", () => { + describe("Base Sepolia network", () => { + const network = "eip155:84532"; + + it("should parse dollar string prices", async () => { + const result = await server.parsePrice("$0.10", network); + expect(result.amount).toBe("100000"); + expect(result.asset).toBe("0x036CbD53842c5426634e7929541eC2318f3dCF7e"); + expect(result.extra).toEqual({ + name: "USDC", + version: "2", + assetTransferMethod: "permit2", + }); + }); + + it("should parse simple number string prices", async () => { + const result = await server.parsePrice("0.10", network); + expect(result.amount).toBe("100000"); + expect(result.asset).toBe("0x036CbD53842c5426634e7929541eC2318f3dCF7e"); + }); + + it("should parse number prices", async () => { + const result = await server.parsePrice(0.1, network); + expect(result.amount).toBe("100000"); + }); + + it("should handle larger amounts", async () => { + const result = await server.parsePrice("100.50", network); + expect(result.amount).toBe("100500000"); + }); + + it("should handle whole numbers", async () => { + const result = await server.parsePrice("1", network); + expect(result.amount).toBe("1000000"); + }); + + it("should avoid floating-point rounding error", async () => { + const result = await server.parsePrice("$4.02", network); + expect(result.amount).toBe("4020000"); + }); + + it("should always include assetTransferMethod=permit2 in extra", async () => { + const result = await server.parsePrice("$1.00", network); + expect(result.extra).toHaveProperty("assetTransferMethod", "permit2"); + }); + }); + + describe("Base mainnet network", () => { + const network = "eip155:8453"; + + it("should use Base mainnet USDC address with permit2", async () => { + const result = await server.parsePrice("1.00", network); + expect(result.asset).toBe("0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"); + expect(result.amount).toBe("1000000"); + expect(result.extra).toEqual({ + name: "USD Coin", + version: "2", + assetTransferMethod: "permit2", + }); + }); + }); + + describe("MegaETH network", () => { + const network = "eip155:4326"; + + it("should parse dollar string for 18-decimal token", async () => { + const result = await server.parsePrice("$0.10", network); + expect(result.asset).toBe("0xFAfDdbb3FC7688494971a79cc65DCa3EF82079E7"); + expect(result.amount).toBe("100000000000000000"); + expect(result.extra).toEqual({ + name: "MegaUSD", + version: "1", + assetTransferMethod: "permit2", + }); + }); + }); + + describe("pre-parsed price objects", () => { + it("should handle pre-parsed price objects with asset", async () => { + const result = await server.parsePrice( + { + amount: "123456", + asset: "0x1234567890123456789012345678901234567890", + extra: { foo: "bar" }, + }, + "eip155:84532", + ); + expect(result.amount).toBe("123456"); + expect(result.asset).toBe("0x1234567890123456789012345678901234567890"); + expect(result.extra).toEqual({ foo: "bar" }); + }); + + it("should throw for price objects without asset", async () => { + await expect( + async () => await server.parsePrice({ amount: "123456" } as never, "eip155:84532"), + ).rejects.toThrow("Asset address must be specified"); + }); + }); + + describe("custom money parser", () => { + it("should use custom parser when it returns a result", async () => { + const customServer = new UptoEvmScheme(); + + customServer.registerMoneyParser(async (amount, network) => { + if (network === "eip155:84532" && amount > 0) { + return { + amount: (amount * 1e18).toString(), + asset: "0xPermit2OnlyToken123456789012345678901234", + extra: { assetTransferMethod: "permit2" }, + }; + } + return null; + }); + + const result = await customServer.parsePrice("1.00", "eip155:84532"); + expect(result.amount).toBe("1000000000000000000"); + expect(result.asset).toBe("0xPermit2OnlyToken123456789012345678901234"); + }); + + it("should fall back to default when custom parser returns null", async () => { + const customServer = new UptoEvmScheme(); + + customServer.registerMoneyParser(async (_amount, network) => { + if (network === "eip155:42161") { + return { amount: "1", asset: "0xArb", extra: {} }; + } + return null; + }); + + const result = await customServer.parsePrice("1.00", "eip155:84532"); + expect(result.asset).toBe("0x036CbD53842c5426634e7929541eC2318f3dCF7e"); + }); + }); + + describe("error cases", () => { + it("should throw for unsupported networks", async () => { + await expect(async () => await server.parsePrice("1.00", "eip155:999999")).rejects.toThrow( + "No default asset configured", + ); + }); + + it("should throw for invalid money formats", async () => { + await expect( + async () => await server.parsePrice("not-a-price!", "eip155:84532"), + ).rejects.toThrow("Invalid money format"); + }); + }); + }); + + describe("enhancePaymentRequirements", () => { + const baseRequirements: PaymentRequirements = { + scheme: "upto", + network: "eip155:8453", + amount: "1000000", + asset: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", + payTo: "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb0", + maxTimeoutSeconds: 300, + extra: { name: "USD Coin", version: "2" }, + }; + + it("should always set assetTransferMethod=permit2 in extra", async () => { + const result = await server.enhancePaymentRequirements( + baseRequirements, + { x402Version: 2, scheme: "upto", network: "eip155:8453" }, + [], + ); + + expect(result.extra?.assetTransferMethod).toBe("permit2"); + }); + + it("should inject facilitatorAddress from supportedKind.extra", async () => { + const result = await server.enhancePaymentRequirements( + baseRequirements, + { + x402Version: 2, + scheme: "upto", + network: "eip155:8453", + extra: { facilitatorAddress: FACILITATOR_ADDRESS }, + }, + [], + ); + + expect(result.extra?.facilitatorAddress).toBe(FACILITATOR_ADDRESS); + }); + + it("should preserve existing extra fields", async () => { + const result = await server.enhancePaymentRequirements( + baseRequirements, + { x402Version: 2, scheme: "upto", network: "eip155:8453" }, + [], + ); + + expect(result.extra?.name).toBe("USD Coin"); + expect(result.extra?.version).toBe("2"); + }); + + it("should not include facilitatorAddress when not provided", async () => { + const result = await server.enhancePaymentRequirements( + baseRequirements, + { x402Version: 2, scheme: "upto", network: "eip155:8453" }, + [], + ); + + expect(result.extra?.facilitatorAddress).toBeUndefined(); + }); + + it("should checksum-validate facilitatorAddress via getAddress", async () => { + const lowercaseAddress = "0xfac11174700123456789012345678901234abcde"; + const result = await server.enhancePaymentRequirements( + baseRequirements, + { + x402Version: 2, + scheme: "upto", + network: "eip155:8453", + extra: { facilitatorAddress: lowercaseAddress }, + }, + [], + ); + + expect(result.extra?.facilitatorAddress).toBe("0xFAC11174700123456789012345678901234aBCDe"); + }); + + it("should throw for invalid facilitatorAddress", () => { + expect(() => + server.enhancePaymentRequirements( + baseRequirements, + { + x402Version: 2, + scheme: "upto", + network: "eip155:8453", + extra: { facilitatorAddress: "not-an-address" }, + }, + [], + ), + ).toThrow(); + }); + }); +}); diff --git a/typescript/packages/mechanisms/evm/test/unit/upto/types.test.ts b/typescript/packages/mechanisms/evm/test/unit/upto/types.test.ts new file mode 100644 index 0000000000..e27dea1f1a --- /dev/null +++ b/typescript/packages/mechanisms/evm/test/unit/upto/types.test.ts @@ -0,0 +1,237 @@ +import { describe, it, expect } from "vitest"; +import { isUptoPermit2Payload } from "../../../src/types"; +import { buildUptoPermit2SettleArgs } from "../../../src/shared/permit2"; +import type { UptoPermit2Payload } from "../../../src/types"; +import { getAddress } from "viem"; + +const VALID_PAYLOAD = { + signature: "0xmocksig" as `0x${string}`, + permit2Authorization: { + from: "0x1234567890123456789012345678901234567890", + permitted: { + token: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", + amount: "1000000", + }, + spender: "0x4020A4f3b7b90ccA423B9fabCc0CE57C6C240002", + nonce: "12345", + deadline: "1700000000", + witness: { + to: "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb0", + facilitator: "0xFAC11174700123456789012345678901234aBCDe", + validAfter: "1699999400", + }, + }, +}; + +describe("isUptoPermit2Payload", () => { + it("should return true for a valid payload", () => { + expect(isUptoPermit2Payload(VALID_PAYLOAD)).toBe(true); + }); + + it("should return false when signature is missing", () => { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { signature: _signature, ...rest } = VALID_PAYLOAD; + expect(isUptoPermit2Payload(rest as Record)).toBe(false); + }); + + it("should return false when signature is not a string", () => { + expect(isUptoPermit2Payload({ ...VALID_PAYLOAD, signature: 123 })).toBe(false); + }); + + it("should return false when permit2Authorization is missing", () => { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { permit2Authorization: _permit2Authorization, ...rest } = VALID_PAYLOAD; + expect(isUptoPermit2Payload(rest as Record)).toBe(false); + }); + + it("should return false when permit2Authorization is null", () => { + expect(isUptoPermit2Payload({ ...VALID_PAYLOAD, permit2Authorization: null })).toBe(false); + }); + + it("should return false when permit2Authorization is not an object", () => { + expect(isUptoPermit2Payload({ ...VALID_PAYLOAD, permit2Authorization: "bad" })).toBe(false); + }); + + it("should return false when from is missing", () => { + const payload = structuredClone(VALID_PAYLOAD); + delete (payload.permit2Authorization as Record).from; + expect(isUptoPermit2Payload(payload)).toBe(false); + }); + + it("should return false when from is not a string", () => { + const payload = structuredClone(VALID_PAYLOAD); + (payload.permit2Authorization as Record).from = 42; + expect(isUptoPermit2Payload(payload)).toBe(false); + }); + + it("should return false when spender is missing", () => { + const payload = structuredClone(VALID_PAYLOAD); + delete (payload.permit2Authorization as Record).spender; + expect(isUptoPermit2Payload(payload)).toBe(false); + }); + + it("should return false when nonce is not a string", () => { + const payload = structuredClone(VALID_PAYLOAD); + (payload.permit2Authorization as Record).nonce = 12345; + expect(isUptoPermit2Payload(payload)).toBe(false); + }); + + it("should return false when deadline is not a string", () => { + const payload = structuredClone(VALID_PAYLOAD); + (payload.permit2Authorization as Record).deadline = 1700000000; + expect(isUptoPermit2Payload(payload)).toBe(false); + }); + + it("should return false when permitted is missing", () => { + const payload = structuredClone(VALID_PAYLOAD); + delete (payload.permit2Authorization as Record).permitted; + expect(isUptoPermit2Payload(payload)).toBe(false); + }); + + it("should return false when permitted is null", () => { + const payload = structuredClone(VALID_PAYLOAD); + (payload.permit2Authorization as Record).permitted = null; + expect(isUptoPermit2Payload(payload)).toBe(false); + }); + + it("should return false when permitted.token is not a string", () => { + const payload = structuredClone(VALID_PAYLOAD); + (payload.permit2Authorization.permitted as Record).token = 0x833589; + expect(isUptoPermit2Payload(payload)).toBe(false); + }); + + it("should return false when permitted.amount is not a string", () => { + const payload = structuredClone(VALID_PAYLOAD); + (payload.permit2Authorization.permitted as Record).amount = 1000000; + expect(isUptoPermit2Payload(payload)).toBe(false); + }); + + it("should return false when witness is missing", () => { + const payload = structuredClone(VALID_PAYLOAD); + delete (payload.permit2Authorization as Record).witness; + expect(isUptoPermit2Payload(payload)).toBe(false); + }); + + it("should return false when witness is null", () => { + const payload = structuredClone(VALID_PAYLOAD); + (payload.permit2Authorization as Record).witness = null; + expect(isUptoPermit2Payload(payload)).toBe(false); + }); + + it("should return false when witness.facilitator is missing", () => { + const payload = structuredClone(VALID_PAYLOAD); + delete (payload.permit2Authorization.witness as Record).facilitator; + expect(isUptoPermit2Payload(payload)).toBe(false); + }); + + it("should return false when witness.facilitator is not a string", () => { + const payload = structuredClone(VALID_PAYLOAD); + (payload.permit2Authorization.witness as Record).facilitator = 123; + expect(isUptoPermit2Payload(payload)).toBe(false); + }); + + it("should return false when witness.to is missing", () => { + const payload = structuredClone(VALID_PAYLOAD); + delete (payload.permit2Authorization.witness as Record).to; + expect(isUptoPermit2Payload(payload)).toBe(false); + }); + + it("should return false when witness.to is not a string", () => { + const payload = structuredClone(VALID_PAYLOAD); + (payload.permit2Authorization.witness as Record).to = 42; + expect(isUptoPermit2Payload(payload)).toBe(false); + }); + + it("should return false when witness.validAfter is missing", () => { + const payload = structuredClone(VALID_PAYLOAD); + delete (payload.permit2Authorization.witness as Record).validAfter; + expect(isUptoPermit2Payload(payload)).toBe(false); + }); + + it("should return false when witness.validAfter is not a string", () => { + const payload = structuredClone(VALID_PAYLOAD); + (payload.permit2Authorization.witness as Record).validAfter = 1699999400; + expect(isUptoPermit2Payload(payload)).toBe(false); + }); + + it("should return false for an empty object", () => { + expect(isUptoPermit2Payload({})).toBe(false); + }); + + it("should return false for an exact scheme payload (no facilitator in witness)", () => { + const exactPayload = { + signature: "0xsig", + permit2Authorization: { + from: "0x1234567890123456789012345678901234567890", + permitted: { token: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", amount: "1000000" }, + spender: "0x402085c248EeA27D92E8b30b2C58ed07f9E20001", + nonce: "1", + deadline: "999999999", + witness: { to: "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb0", validAfter: "0" }, + }, + }; + expect(isUptoPermit2Payload(exactPayload as Record)).toBe(false); + }); +}); + +describe("buildUptoPermit2SettleArgs", () => { + const FACILITATOR = "0xFAC11174700123456789012345678901234aBCDe" as `0x${string}`; + const payload: UptoPermit2Payload = { + signature: "0xdeadbeef" as `0x${string}`, + permit2Authorization: { + from: "0x1234567890123456789012345678901234567890", + permitted: { + token: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", + amount: "5000000", + }, + spender: "0x4020A4f3b7b90ccA423B9fabCc0CE57C6C240002", + nonce: "99", + deadline: "1700000000", + witness: { + to: "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb0", + facilitator: FACILITATOR, + validAfter: "1699999400", + }, + }, + }; + + it("should return a 5-element tuple with correct types", () => { + const args = buildUptoPermit2SettleArgs(payload, 1000000n, FACILITATOR); + expect(args).toHaveLength(5); + }); + + it("should place the settlement amount as the second element", () => { + const args = buildUptoPermit2SettleArgs(payload, 750000n, FACILITATOR); + expect(args[1]).toBe(750000n); + }); + + it("should convert permitted.amount to BigInt", () => { + const args = buildUptoPermit2SettleArgs(payload, 1000000n, FACILITATOR); + expect(args[0].permitted.amount).toBe(5000000n); + }); + + it("should checksum all addresses", () => { + const args = buildUptoPermit2SettleArgs(payload, 1000000n, FACILITATOR); + const checksummedToken = getAddress(payload.permit2Authorization.permitted.token); + const checksummedTo = getAddress(payload.permit2Authorization.witness.to); + const checksummedFacilitator = getAddress(FACILITATOR); + const checksummedFrom = getAddress(payload.permit2Authorization.from); + + expect(args[0].permitted.token).toBe(checksummedToken); + expect(args[2]).toBe(checksummedFrom); + expect(args[3].to).toBe(checksummedTo); + expect(args[3].facilitator).toBe(checksummedFacilitator); + }); + + it("should convert nonce, deadline, and validAfter to BigInt", () => { + const args = buildUptoPermit2SettleArgs(payload, 1000000n, FACILITATOR); + expect(args[0].nonce).toBe(99n); + expect(args[0].deadline).toBe(1700000000n); + expect(args[3].validAfter).toBe(1699999400n); + }); + + it("should pass through the signature unchanged", () => { + const args = buildUptoPermit2SettleArgs(payload, 1000000n, FACILITATOR); + expect(args[4]).toBe("0xdeadbeef"); + }); +}); diff --git a/typescript/packages/mechanisms/evm/test/unit/v1/facilitator.test.ts b/typescript/packages/mechanisms/evm/test/unit/v1/facilitator.test.ts index b430491647..b6171afc0e 100644 --- a/typescript/packages/mechanisms/evm/test/unit/v1/facilitator.test.ts +++ b/typescript/packages/mechanisms/evm/test/unit/v1/facilitator.test.ts @@ -3,6 +3,7 @@ import { ExactEvmSchemeV1 } from "../../../src/exact/v1/facilitator/scheme"; import type { FacilitatorEvmSigner } from "../../../src/signer"; import type { PaymentRequirementsV1 } from "@x402/core/types/v1"; import type { PaymentPayloadV1 } from "@x402/core/types/v1"; +import * as Errors from "../../../src/exact/facilitator/errors"; describe("ExactEvmSchemeV1", () => { let mockSigner: FacilitatorEvmSigner; @@ -97,7 +98,7 @@ describe("ExactEvmSchemeV1", () => { const result = await facilitator.verify(payload as never, requirements as never); expect(result.isValid).toBe(false); - expect(result.invalidReason).toBe("unsupported_scheme"); + expect(result.invalidReason).toBe(Errors.ErrInvalidScheme); }); it("should reject if network does not match", async () => { @@ -133,7 +134,7 @@ describe("ExactEvmSchemeV1", () => { const result = await facilitator.verify(payload as never, requirements as never); expect(result.isValid).toBe(false); - expect(result.invalidReason).toBe("network_mismatch"); + expect(result.invalidReason).toBe(Errors.ErrNetworkMismatch); }); it("should reject if amount is insufficient (maxAmountRequired)", async () => { @@ -169,11 +170,12 @@ describe("ExactEvmSchemeV1", () => { const result = await facilitator.verify(payload as never, requirements as never); expect(result.isValid).toBe(false); - expect(result.invalidReason).toBe("invalid_exact_evm_payload_authorization_value"); + expect(result.invalidReason).toBe(Errors.ErrInvalidAuthorizationValue); }); it("should reject if balance is insufficient", async () => { - mockSigner.readContract = vi.fn().mockResolvedValue(BigInt("50000")); // Low balance + // Simulation fails (transfer would revert due to insufficient balance) + mockSigner.readContract = vi.fn().mockRejectedValue(new Error("simulation reverted")); const facilitator = new ExactEvmSchemeV1(mockSigner); @@ -207,7 +209,7 @@ describe("ExactEvmSchemeV1", () => { const result = await facilitator.verify(payload as never, requirements as never); expect(result.isValid).toBe(false); - expect(result.invalidReason).toBe("insufficient_funds"); + expect(result.invalidReason).toBe("invalid_exact_evm_transaction_simulation_failed"); }); it("should reject if recipient does not match", async () => { @@ -243,7 +245,7 @@ describe("ExactEvmSchemeV1", () => { const result = await facilitator.verify(payload as never, requirements as never); expect(result.isValid).toBe(false); - expect(result.invalidReason).toBe("invalid_exact_evm_payload_recipient_mismatch"); + expect(result.invalidReason).toBe(Errors.ErrRecipientMismatch); }); it("should reject if network not supported", async () => { @@ -278,7 +280,7 @@ describe("ExactEvmSchemeV1", () => { const result = await facilitator.verify(payload as never, requirements as never); expect(result.isValid).toBe(false); - expect(result.invalidReason).toBe("invalid_network"); + expect(result.invalidReason).toBe(Errors.ErrNetworkMismatch); }); }); @@ -359,7 +361,7 @@ describe("ExactEvmSchemeV1", () => { const result = await facilitator.settle(payload as never, requirements as never); expect(result.success).toBe(false); - expect(result.errorReason).toBe("invalid_exact_evm_payload_signature"); + expect(result.errorReason).toBe(Errors.ErrInvalidSignature); }); }); }); diff --git a/typescript/packages/mechanisms/evm/tsup.config.ts b/typescript/packages/mechanisms/evm/tsup.config.ts index 128ac9a97f..db132c689a 100644 --- a/typescript/packages/mechanisms/evm/tsup.config.ts +++ b/typescript/packages/mechanisms/evm/tsup.config.ts @@ -9,6 +9,9 @@ const baseConfig = { "exact/facilitator/index": "src/exact/facilitator/index.ts", "exact/v1/client/index": "src/exact/v1/client/index.ts", "exact/v1/facilitator/index": "src/exact/v1/facilitator/index.ts", + "upto/client/index": "src/upto/client/index.ts", + "upto/server/index": "src/upto/server/index.ts", + "upto/facilitator/index": "src/upto/facilitator/index.ts", }, dts: { resolve: true, diff --git a/typescript/packages/mechanisms/stellar/.prettierignore b/typescript/packages/mechanisms/stellar/.prettierignore new file mode 100644 index 0000000000..9fd1bade5d --- /dev/null +++ b/typescript/packages/mechanisms/stellar/.prettierignore @@ -0,0 +1,7 @@ +docs/ +dist/ +node_modules/ +coverage/ +.github/ +**/**/*.json +*.md diff --git a/typescript/packages/mechanisms/stellar/.prettierrc b/typescript/packages/mechanisms/stellar/.prettierrc new file mode 100644 index 0000000000..ffb416b74b --- /dev/null +++ b/typescript/packages/mechanisms/stellar/.prettierrc @@ -0,0 +1,11 @@ +{ + "tabWidth": 2, + "useTabs": false, + "semi": true, + "singleQuote": false, + "trailingComma": "all", + "bracketSpacing": true, + "arrowParens": "avoid", + "printWidth": 100, + "proseWrap": "never" +} diff --git a/typescript/packages/mechanisms/stellar/CHANGELOG.md b/typescript/packages/mechanisms/stellar/CHANGELOG.md new file mode 100644 index 0000000000..f92323e95a --- /dev/null +++ b/typescript/packages/mechanisms/stellar/CHANGELOG.md @@ -0,0 +1,22 @@ +# @x402/stellar Changelog + +## 2.8.0 + +### Minor Changes + +- Updated dependencies [067f297] +- Updated dependencies [4c1e44f] +- Updated dependencies [5135fab] + - @x402/core@2.8.0 + +## 2.7.0 + +### Minor Changes + +- c92c0d1: Bump "@stellar/stellar-sdk" dependency and refactor API call for better performance +- Updated dependencies [8931cb3] + - @x402/core@2.7.0 + +## 2.6.0 + +- Implement x402 v2 protocol support for the Stellar mechanism (exact scheme). diff --git a/typescript/packages/mechanisms/stellar/README.md b/typescript/packages/mechanisms/stellar/README.md new file mode 100644 index 0000000000..efffd237f6 --- /dev/null +++ b/typescript/packages/mechanisms/stellar/README.md @@ -0,0 +1,168 @@ +# @x402/stellar + +Stellar implementation of the x402 payment protocol using the **Exact** payment scheme with [Soroban token](https://stellar.org/protocol/sep-41) transfers. + +## Installation + +```bash +npm install @x402/stellar +``` + +## Overview + +This package provides three main components for handling x402 payments on Stellar: + +- **Client** - For applications that need to make payments (have wallets/signers) +- **Facilitator** - For payment processors that verify and execute on-chain transactions +- **Server** - For resource servers that accept payments and build payment requirements + +**Key Differences from EVM/SVM:** + +- **Ledger-based expiration** (not timestamps) - default ~12 ledgers ≈ 60 seconds +- **Auth entry signing** - client signs authorization entries only, facilitator rebuilds and submits transaction +- **Mainnet requires custom RPC URL** (see [Stellar RPC Providers](https://developers.stellar.org/docs/data/apis/rpc/providers)) + +## Package Exports + +### Main Package (`@x402/stellar`) + +**V2 Protocol Support** - x402 v2 protocol with CAIP-2 network identifiers + +**Client:** + +- `ExactStellarScheme` - Client implementation using Soroban token transfers +- `createEd25519Signer(privateKey, defaultNetwork)` - Creates a Stellar signer from private key that implements `SignAuthEntry` and `SignTransaction` according to [SEP-43](https://stellar.org/protocol/sep-43) +- `ClientStellarSigner` - TypeScript type for client signers + +**Facilitator:** + +- `ExactStellarScheme` - Facilitator for payment verification and settlement +- `FacilitatorStellarSigner` - TypeScript type for facilitator signers + +> [!NOTE] +> Facilitators currently always sponsor transaction fees (`areFeesSponsored: true`). A non-sponsored flow will be added later. See [spec](../../../specs/schemes/exact/scheme_exact_stellar.md#paymentrequirements-for-exact) for details. + +**Server:** + +- `ExactStellarScheme` - Server for building payment requirements + +**Utilities:** + +- `getRpcUrl(network, config?)` - Get RPC URL for a network +- `getRpcClient(network, config?)` - Create Soroban RPC client +- `getNetworkPassphrase(network)` - Get network passphrase +- `validateStellarDestinationAddress(address)` - Validate destination address +- `validateStellarAssetAddress(address)` - Validate asset/contract address +- `convertToTokenAmount(amount, decimals)` - Convert decimal to token units +- `getUsdcAddress(network)` - Get USDC contract address + +**Constants:** + +- `STELLAR_PUBNET_CAIP2` = `"stellar:pubnet"` +- `STELLAR_TESTNET_CAIP2` = `"stellar:testnet"` +- `USDC_PUBNET_ADDRESS` - USDC contract on mainnet +- `USDC_TESTNET_ADDRESS` - USDC contract on testnet +- `DEFAULT_TOKEN_DECIMALS` = `7` + +### Subpath Exports + +- `@x402/stellar/exact/client` - `ExactStellarScheme` (client) +- `@x402/stellar/exact/server` - `ExactStellarScheme` (server) +- `@x402/stellar/exact/facilitator` - `ExactStellarScheme` (facilitator) + +## Supported Networks + +**V2 Networks** (via [CAIP-28](https://namespaces.chainagnostic.org/stellar/caip2)): + +- `stellar:pubnet` - Mainnet (requires custom RPC URL) +- `stellar:testnet` - Testnet (default: [https://soroban-testnet.stellar.org](https://soroban-testnet.stellar.org)) +- `stellar:*` - Wildcard (matches all Stellar networks) + +## Asset Support + +Supports Soroban tokens implementing [SEP-41](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0041.md): + +- Any Soroban token contract with `transfer(from, to, amount)` function +- Default asset is USDC (primary, 7 decimals) + +> **For detailed protocol flow, transaction structure, and verification rules, see the [Exact Scheme Specification](../../../specs/schemes/exact/scheme_exact_stellar.md).** + +## Usage Patterns + +### 1. Direct Registration (Recommended) + +```typescript +import { x402Client } from "@x402/core/client"; +import { createEd25519Signer } from "@x402/stellar"; +import { ExactStellarScheme } from "@x402/stellar/exact/client"; + +const signer = createEd25519Signer(privateKey, "stellar:testnet"); +const client = new x402Client().register("stellar:*", new ExactStellarScheme(signer)); +``` + +### 2. Custom Configuration + +```typescript +// Client with custom RPC +const client = new x402Client().register( + "stellar:*", + new ExactStellarScheme(signer, { url: "https://custom-rpc.example.com" }), +); + +// Server with custom money parser +const scheme = new ExactStellarScheme().registerMoneyParser(async (amount, network) => ({ + amount: customConvert(amount), + asset: "TOKEN_ADDRESS", + extra: {}, +})); + +// Facilitator +const facilitator = new x402Facilitator().register( + "stellar:testnet", + new ExactStellarScheme([signer]), +); +``` + +## Development + +```bash +# Build +pnpm build + +# Test +pnpm test + +# Integration tests +pnpm test:integration + +# Lint & Format +pnpm lint +pnpm format +``` + +## Integration Tests + +Integration tests require four funded Stellar testnet accounts: + +```bash +CLIENT_PRIVATE_KEY=S... # Client's secret key +FACILITATOR_PRIVATE_KEY=S... # Facilitator's secret key +FACILITATOR_ADDRESS=G... # Facilitator's public address +RESOURCE_SERVER_ADDRESS=G... # Resource server's public address +``` + +### Stellar Testnet Account Setup + +1. Go to [Stellar Laboratory](https://lab.stellar.org/account/create) ➡️ Generate keypair ➡️ Fund account with Friendbot, then copy the `Secret` and `Public` keys so you can use them. +2. Add USDC trustline (required for client and resource server): go to [Fund Account](https://lab.stellar.org/account/fund) ➡️ Paste your `Public Key` ➡️ Add USDC Trustline ➡️ paste your `Secret key` ➡️ Sign transaction ➡️ Add Trustline. +3. Get testnet USDC from [Circle Faucet](https://faucet.circle.com/) (select Stellar network). + +> [!NOTE] +> The facilitator account only needs XLM (step 1). Client and resource server accounts need all three steps. + +## Related Packages + +- `@x402/core` - Core protocol types and client +- `@x402/fetch` - HTTP wrapper with automatic payment handling +- `@x402/evm` - EVM/Ethereum implementation +- `@x402/svm` - Solana/SVM implementation diff --git a/typescript/packages/mechanisms/stellar/eslint.config.js b/typescript/packages/mechanisms/stellar/eslint.config.js new file mode 100644 index 0000000000..8aecb38e3b --- /dev/null +++ b/typescript/packages/mechanisms/stellar/eslint.config.js @@ -0,0 +1,131 @@ +import js from "@eslint/js"; +import ts from "@typescript-eslint/eslint-plugin"; +import tsParser from "@typescript-eslint/parser"; +import prettier from "eslint-plugin-prettier"; +import jsdoc from "eslint-plugin-jsdoc"; +import importPlugin from "eslint-plugin-import"; + +export default [ + { + ignores: ["dist/**", "node_modules/**"], + }, + { + files: ["**/*.ts", "**/*.tsx"], + ignores: ["**/*.test.ts", "test/**/*"], + languageOptions: { + parser: tsParser, + sourceType: "module", + ecmaVersion: 2020, + globals: { + process: "readonly", + __dirname: "readonly", + module: "readonly", + require: "readonly", + Buffer: "readonly", + exports: "readonly", + setTimeout: "readonly", + clearTimeout: "readonly", + setInterval: "readonly", + clearInterval: "readonly", + }, + }, + plugins: { + "@typescript-eslint": ts, + prettier: prettier, + jsdoc: jsdoc, + import: importPlugin, + }, + rules: { + ...ts.configs.recommended.rules, + "import/first": "error", + "import/order": [ + "error", + { + groups: [ + "builtin", + "external", + "internal", + ["parent", "sibling"], + "index", + "object", + "type", + ], + "newlines-between": "never", + alphabetize: { + order: "asc", + caseInsensitive: true, + }, + }, + ], + "prettier/prettier": "error", + "@typescript-eslint/member-ordering": "error", + "@typescript-eslint/no-unused-vars": ["error", { argsIgnorePattern: "^_$" }], + "jsdoc/tag-lines": ["error", "any", { startLines: 1 }], + "jsdoc/check-alignment": "error", + "jsdoc/no-undefined-types": "off", + "jsdoc/check-param-names": "error", + "jsdoc/check-tag-names": "error", + "jsdoc/check-types": "error", + "jsdoc/implements-on-classes": "error", + "jsdoc/require-description": "error", + "jsdoc/require-jsdoc": [ + "error", + { + require: { + FunctionDeclaration: true, + MethodDefinition: true, + ClassDeclaration: true, + ArrowFunctionExpression: false, + FunctionExpression: false, + }, + }, + ], + "jsdoc/require-param": "error", + "jsdoc/require-param-description": "error", + "jsdoc/require-param-type": "off", + "jsdoc/require-returns": "error", + "jsdoc/require-returns-description": "error", + "jsdoc/require-returns-type": "off", + "jsdoc/require-hyphen-before-param-description": ["error", "always"], + }, + }, + { + files: ["**/*.test.ts", "test/**/*"], + languageOptions: { + parser: tsParser, + sourceType: "module", + ecmaVersion: 2020, + }, + plugins: { + "@typescript-eslint": ts, + prettier: prettier, + import: importPlugin, + }, + rules: { + "import/first": "error", + "import/order": [ + "error", + { + groups: [ + "builtin", + "external", + "internal", + ["parent", "sibling"], + "index", + "object", + "type", + ], + "newlines-between": "never", + alphabetize: { + order: "asc", + caseInsensitive: true, + }, + }, + ], + "prettier/prettier": "error", + "@typescript-eslint/no-unused-vars": ["error", { argsIgnorePattern: "^_" }], + "@typescript-eslint/no-explicit-any": "off", + "@typescript-eslint/member-ordering": "off", + }, + }, +]; diff --git a/typescript/packages/mechanisms/stellar/package.json b/typescript/packages/mechanisms/stellar/package.json new file mode 100644 index 0000000000..324a32b1c0 --- /dev/null +++ b/typescript/packages/mechanisms/stellar/package.json @@ -0,0 +1,97 @@ +{ + "name": "@x402/stellar", + "version": "2.8.0", + "main": "./dist/cjs/index.js", + "module": "./dist/esm/index.js", + "types": "./dist/cjs/index.d.ts", + "scripts": { + "start": "tsx --env-file=.env index.ts", + "build": "tsup", + "test": "vitest run", + "test:integration": "vitest run --config vitest.integration.config.ts", + "test:all": "vitest run --config vitest.config.ts && vitest run --config vitest.integration.config.ts", + "test:watch": "vitest", + "watch": "tsc --watch", + "format": "prettier -c .prettierrc --write \"**/*.{ts,js,cjs,json,md}\"", + "format:check": "prettier -c .prettierrc --check \"**/*.{ts,js,cjs,json,md}\"", + "lint": "eslint . --ext .ts --fix", + "lint:check": "eslint . --ext .ts" + }, + "keywords": [ + "x402", + "payment", + "protocol", + "stellar", + "soroban" + ], + "license": "Apache-2.0", + "author": "Coinbase Inc.", + "repository": "https://github.com/coinbase/x402", + "description": "x402 Payment Protocol Stellar Implementation", + "devDependencies": { + "@eslint/js": "^9.24.0", + "@types/node": "^22.13.4", + "@typescript-eslint/eslint-plugin": "^8.29.1", + "@typescript-eslint/parser": "^8.29.1", + "eslint": "^9.24.0", + "eslint-plugin-import": "^2.31.0", + "eslint-plugin-jsdoc": "^50.6.9", + "eslint-plugin-prettier": "^5.2.6", + "prettier": "3.5.2", + "tsup": "^8.4.0", + "tsx": "^4.19.2", + "typescript": "^5.7.3", + "vite": "^6.2.6", + "vite-tsconfig-paths": "^5.1.4", + "vitest": "^3.0.5" + }, + "dependencies": { + "@stellar/stellar-sdk": "^14.6.1", + "@x402/core": "workspace:*" + }, + "exports": { + ".": { + "import": { + "types": "./dist/esm/index.d.mts", + "default": "./dist/esm/index.mjs" + }, + "require": { + "types": "./dist/cjs/index.d.ts", + "default": "./dist/cjs/index.js" + } + }, + "./exact/client": { + "import": { + "types": "./dist/esm/exact/client/index.d.mts", + "default": "./dist/esm/exact/client/index.mjs" + }, + "require": { + "types": "./dist/cjs/exact/client/index.d.ts", + "default": "./dist/cjs/exact/client/index.js" + } + }, + "./exact/server": { + "import": { + "types": "./dist/esm/exact/server/index.d.mts", + "default": "./dist/esm/exact/server/index.mjs" + }, + "require": { + "types": "./dist/cjs/exact/server/index.d.ts", + "default": "./dist/cjs/exact/server/index.js" + } + }, + "./exact/facilitator": { + "import": { + "types": "./dist/esm/exact/facilitator/index.d.mts", + "default": "./dist/esm/exact/facilitator/index.mjs" + }, + "require": { + "types": "./dist/cjs/exact/facilitator/index.d.ts", + "default": "./dist/cjs/exact/facilitator/index.js" + } + } + }, + "files": [ + "dist" + ] +} diff --git a/typescript/packages/mechanisms/stellar/src/constants.ts b/typescript/packages/mechanisms/stellar/src/constants.ts new file mode 100644 index 0000000000..e3ef92198a --- /dev/null +++ b/typescript/packages/mechanisms/stellar/src/constants.ts @@ -0,0 +1,39 @@ +/** + * CAIP-2 network identifiers for Stellar (V2) + */ +export const STELLAR_PUBNET_CAIP2 = "stellar:pubnet"; +export const STELLAR_TESTNET_CAIP2 = "stellar:testnet"; +export const STELLAR_WILDCARD_CAIP2 = "stellar:*"; + +/** + * Default testnet RPC URL + */ +export const DEFAULT_TESTNET_RPC_URL = "https://soroban-testnet.stellar.org"; + +/** + * Default Horizon API URLs + */ +export const DEFAULT_TESTNET_HORIZON_URL = "https://horizon-testnet.stellar.org"; +export const DEFAULT_PUBNET_HORIZON_URL = "https://horizon.stellar.org"; + +/** + * Stellar validation regex for destination and asset addresses + */ +export const STELLAR_DESTINATION_ADDRESS_REGEX = /^(?:[GC][ABCD][A-Z2-7]{54}|M[ABCD][A-Z2-7]{67})$/; // Stellar address: G-account (56 chars), C-account (56 chars), or M-account (69 chars, muxed) +export const STELLAR_ASSET_ADDRESS_REGEX = /^(?:[C][ABCD][A-Z2-7]{54})$/; // Stellar token contract address: C-account (56 chars) + +/** + * USDC contract addresses (default stablecoin) + */ +export const USDC_PUBNET_ADDRESS = "CCW67TSZV3SSS2HXMBQ5JFGCKJNXKZM7UQUWUZPUTHXSTZLEO7SJMI75"; +export const USDC_TESTNET_ADDRESS = "CBIELTK6YBZJU5UP2WWQEUCYKLPU6AUNZ2BQ4WWFEIE3USCIHMXQDAMA"; + +export const STELLAR_NETWORK_TO_PASSPHRASE: ReadonlyMap = new Map([ + [STELLAR_PUBNET_CAIP2, "Public Global Stellar Network ; September 2015"], + [STELLAR_TESTNET_CAIP2, "Test SDF Network ; September 2015"], +]); + +/** + * Default token decimals + */ +export const DEFAULT_TOKEN_DECIMALS = 7; diff --git a/typescript/packages/mechanisms/stellar/src/exact/client/index.ts b/typescript/packages/mechanisms/stellar/src/exact/client/index.ts new file mode 100644 index 0000000000..9e91e067eb --- /dev/null +++ b/typescript/packages/mechanisms/stellar/src/exact/client/index.ts @@ -0,0 +1 @@ +export { ExactStellarScheme } from "./scheme"; diff --git a/typescript/packages/mechanisms/stellar/src/exact/client/scheme.ts b/typescript/packages/mechanisms/stellar/src/exact/client/scheme.ts new file mode 100644 index 0000000000..2724fcad67 --- /dev/null +++ b/typescript/packages/mechanisms/stellar/src/exact/client/scheme.ts @@ -0,0 +1,138 @@ +import { nativeToScVal, contract } from "@stellar/stellar-sdk"; +import { handleSimulationResult } from "../../shared"; +import { + getEstimatedLedgerCloseTimeSeconds, + getNetworkPassphrase, + getRpcClient, + getRpcUrl, + isStellarNetwork, + RpcConfig, + validateStellarAssetAddress, + validateStellarDestinationAddress, +} from "../../utils"; +import type { ClientStellarSigner } from "../../signer"; +import type { PaymentPayload, PaymentRequirements, SchemeNetworkClient } from "@x402/core/types"; + +/** + * Stellar client implementation for the Exact payment scheme. + */ +export class ExactStellarScheme implements SchemeNetworkClient { + readonly scheme = "exact"; + + /** + * Creates a new ExactStellarScheme instance. + * + * @param signer - The Stellar signer for client operations + * @param rpcConfig - Optional configuration with custom RPC URL + * @returns ExactStellarScheme instance + */ + constructor( + private readonly signer: ClientStellarSigner, + private readonly rpcConfig?: RpcConfig, + ) {} + + /** + * Creates a payment payload for the Exact scheme. + * + * @param x402Version - The x402 protocol version + * @param paymentRequirements - The payment requirements + * @returns Promise resolving to a payment payload + */ + async createPaymentPayload( + x402Version: number, + paymentRequirements: PaymentRequirements, + ): Promise> { + try { + this.validateCreateAndSignPaymentInput(paymentRequirements); + } catch (error) { + throw new Error(`Invalid input parameters for creating Stellar payment, cause: ${error}`); + } + + const sourcePublicKey = this.signer.address; + const { network, payTo, asset, amount, extra, maxTimeoutSeconds } = paymentRequirements; + const networkPassphrase = getNetworkPassphrase(network); + const rpcUrl = getRpcUrl(network, this.rpcConfig); + + if (!extra.areFeesSponsored) { + throw new Error(`Exact scheme requires areFeesSponsored to be true`); + } + + // Fetch current ledger and calculate maxLedger + const rpcServer = getRpcClient(network, this.rpcConfig); + const latestLedger = await rpcServer.getLatestLedger(); + const currentLedger = latestLedger.sequence; + const estimatedLedgerSeconds = await getEstimatedLedgerCloseTimeSeconds(network); + const maxLedger = currentLedger + Math.ceil(maxTimeoutSeconds / estimatedLedgerSeconds); + + const tx = await contract.AssembledTransaction.build({ + contractId: asset, + method: "transfer", + args: [ + // SEP-41 spec: https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0041.md#interface + nativeToScVal(sourcePublicKey, { type: "address" }), // from + nativeToScVal(payTo, { type: "address" }), // to + nativeToScVal(amount, { type: "i128" }), // amount + ], + networkPassphrase, + rpcUrl, + parseResultXdr: result => result, + }); + handleSimulationResult(tx.simulation); + + let missingSigners = tx.needsNonInvokerSigningBy(); + if (!missingSigners.includes(sourcePublicKey) || missingSigners.length > 1) { + throw new Error( + `Expected to sign with [${sourcePublicKey}], but got [${missingSigners.join(", ")}]`, + ); + } + await tx.signAuthEntries({ + address: sourcePublicKey, + signAuthEntry: this.signer.signAuthEntry, + expiration: maxLedger, + }); + + await tx.simulate(); + handleSimulationResult(tx.simulation); + + missingSigners = tx.needsNonInvokerSigningBy(); + if (missingSigners.length > 0) { + throw new Error(`unexpected signer(s) required: [${missingSigners.join(", ")}]`); + } + + return { + x402Version, + payload: { + transaction: tx.built!.toXDR(), + }, + }; + } + + /** + * Validates the input parameters for the createAndSignPayment function. + * + * @param paymentRequirements - Payment requirements + * @throws Error if validation fails + */ + private validateCreateAndSignPaymentInput(paymentRequirements: PaymentRequirements): void { + const { scheme, network, payTo, asset, amount } = paymentRequirements; + if (typeof amount !== "string" || !Number.isInteger(Number(amount)) || Number(amount) <= 0) { + throw new Error(`Invalid amount: ${amount}. Amount must be a positive integer.`); + } + + if (scheme !== "exact") { + throw new Error(`Unsupported scheme: ${scheme}`); + } + + if (!isStellarNetwork(network)) { + throw new Error(`Unsupported Stellar network: ${network}`); + } + + if (!validateStellarDestinationAddress(payTo)) { + throw new Error(`Invalid Stellar destination address: ${payTo}`); + } + + if (!validateStellarAssetAddress(asset)) { + throw new Error(`Invalid Stellar asset address: ${asset}`); + } + } +} diff --git a/typescript/packages/mechanisms/stellar/src/exact/facilitator/index.ts b/typescript/packages/mechanisms/stellar/src/exact/facilitator/index.ts new file mode 100644 index 0000000000..9e91e067eb --- /dev/null +++ b/typescript/packages/mechanisms/stellar/src/exact/facilitator/index.ts @@ -0,0 +1 @@ +export { ExactStellarScheme } from "./scheme"; diff --git a/typescript/packages/mechanisms/stellar/src/exact/facilitator/scheme.ts b/typescript/packages/mechanisms/stellar/src/exact/facilitator/scheme.ts new file mode 100644 index 0000000000..bac8c87dd2 --- /dev/null +++ b/typescript/packages/mechanisms/stellar/src/exact/facilitator/scheme.ts @@ -0,0 +1,765 @@ +import { + scValToNative, + Transaction, + FeeBumpTransaction, + Address, + Operation, + xdr, + rpc, + TransactionBuilder, +} from "@stellar/stellar-sdk"; +import { Api } from "@stellar/stellar-sdk/rpc"; +import { STELLAR_WILDCARD_CAIP2 } from "../../constants"; +import { gatherAuthEntrySignatureStatus } from "../../shared"; +import { ExactStellarPayloadV2 } from "../../types"; +import { + getEstimatedLedgerCloseTimeSeconds, + getRpcClient, + getNetworkPassphrase, + isStellarNetwork, + RpcConfig, +} from "../../utils"; +import type { FacilitatorStellarSigner } from "../../signer"; +import type { + Network, + PaymentPayload, + PaymentRequirements, + SchemeNetworkFacilitator, + SettleResponse, + VerifyResponse, +} from "@x402/core/types"; + +const DEFAULT_TIMEOUT_SECONDS = 60; +const SUPPORTED_X402_VERSION = 2; +const DEFAULT_MAX_TRANSACTION_FEE_STROOPS = 50_000; +const SIGNATURE_EXPIRATION_LEDGER_TOLERANCE = 2; + +/** + * Returns a round-robin selector for choosing which signer to use. + * Each invocation returns a new selector with its own counter. + * + * @returns A function that selects the next address from the given array on each call + */ +const roundRobinSelectSigner = (): ((addresses: readonly string[]) => string) => { + let index = 0; + return addrs => addrs[index++ % addrs.length]; +}; + +/** + * Helper to create a `VerifyResponse` with `isValid: false`. + * + * @param reason - The error reason code + * @param payer - Optional payer address + * @returns a `VerifyResponse` with `isValid: false` and the provided reason and (optional) payer + */ +export function invalidVerifyResponse(reason: string, payer?: string): VerifyResponse { + return { isValid: false, invalidReason: reason, payer }; +} + +/** + * Helper to create a `VerifyResponse` with `isValid: true`. + * + * @param payer - The payer address + * @returns a `VerifyResponse` with `isValid: true` and the provided payer + */ +export function validVerifyResponse(payer: string): VerifyResponse { + return { isValid: true, payer }; +} + +/** + * Stellar facilitator implementation for the Exact payment scheme. + */ +export class ExactStellarScheme implements SchemeNetworkFacilitator { + readonly scheme = "exact"; + readonly caipFamily = STELLAR_WILDCARD_CAIP2; + + public readonly signingAddresses: ReadonlySet; + public readonly areFeesSponsored: boolean; + public readonly rpcConfig?: RpcConfig; + public readonly maxTransactionFeeStroops: number; + public readonly feeBumpSigner?: FacilitatorStellarSigner; + private readonly signerMap: Map; + private readonly selectSigner: (addresses: readonly string[]) => string; + + /** + * Creates a new ExactStellarScheme instance. + * + * @param signers - One or more Stellar signers managed by the facilitator for settlement + * @param options - Configuration options + * @param options.rpcConfig - Optional RPC configuration with custom RPC URL + * @param options.areFeesSponsored - Indicates if fees are sponsored (default: true) + * @param options.maxTransactionFeeStroops - Maximum fee in stroops the facilitator will pay (default: 50_000) + * @param options.selectSigner - Callback to select which signer to use (default: round-robin) + * @param options.feeBumpSigner - Optional signer used as fee source in a fee bump transaction wrapper. + * When provided, settle() wraps the inner transaction (signed by the selected signer) in a + * FeeBumpTransaction where the feeBumpSigner pays the fees, decoupling fee payment from sequence number management. + * @returns ExactStellarScheme instance + */ + constructor( + signers: FacilitatorStellarSigner[], + { + rpcConfig, + areFeesSponsored = true, + maxTransactionFeeStroops = DEFAULT_MAX_TRANSACTION_FEE_STROOPS, + selectSigner = roundRobinSelectSigner(), + feeBumpSigner, + }: { + /** Optional RPC configuration with custom RPC URL */ + rpcConfig?: RpcConfig; + /** Indicates if fees are sponsored (default: true) */ + areFeesSponsored?: boolean; + /** Maximum fee in stroops the facilitator will pay (default: 50_000) */ + maxTransactionFeeStroops?: number; + /** Optional callback to select which signer to use. Receives addresses array, returns selected address. Defaults to round-robin. */ + selectSigner?: (addresses: readonly string[]) => string; + /** Optional signer used as fee source in a fee bump transaction wrapper. Decouples fee payment from sequence number management. */ + feeBumpSigner?: FacilitatorStellarSigner; + } = {}, + ) { + // Validate signers and store their data + if (!signers || signers.length === 0) { + throw new Error("At least one signer is required"); + } + this.signerMap = new Map(signers.map(s => [s.address, s])); + this.signingAddresses = new Set(this.signerMap.keys()); + + // Apply configuration options (with defaults) + this.rpcConfig = rpcConfig; + this.areFeesSponsored = areFeesSponsored ?? true; + this.maxTransactionFeeStroops = maxTransactionFeeStroops ?? DEFAULT_MAX_TRANSACTION_FEE_STROOPS; + this.selectSigner = selectSigner ?? roundRobinSelectSigner(); + this.feeBumpSigner = feeBumpSigner; + } + + /** + * Get mechanism-specific extra data for the supported kinds endpoint. + * For Stellar, returns `areFeesSponsored` indicating to clients if they can expect fees to be sponsored. + * As of now, the spec only supports `areFeesSponsored: true`. + * + * @param _ - The network identifier (unused, offset is network-agnostic) + * @returns Extra data with the `areFeesSponsored` flag + */ + getExtra(_: Network): Record | undefined { + return { + areFeesSponsored: this.areFeesSponsored, + }; + } + + /** + * Get signer addresses used by this facilitator. + * For Stellar, returns all facilitator addresses including the fee bump signer when configured. + * + * @param _ - The network identifier (unused for Stellar) + * @returns Array containing all facilitator addresses + */ + getSigners(_: string): string[] { + const signers = [...this.signingAddresses]; + if (this.feeBumpSigner && !this.signingAddresses.has(this.feeBumpSigner.address)) { + signers.push(this.feeBumpSigner.address); + } + return signers; + } + + /** + * Verifies a payment payload. + * + * @param payload - The payment payload to verify + * @param requirements - The payment requirements + * @returns Promise resolving to verification response + */ + async verify( + payload: PaymentPayload, + requirements: PaymentRequirements, + ): Promise { + let fromAddress: string | undefined; + try { + // Step 1: Validate protocol version, scheme, and network + if (payload.x402Version !== SUPPORTED_X402_VERSION) { + return invalidVerifyResponse("invalid_x402_version"); + } + + if (payload.accepted.scheme !== "exact" || requirements.scheme !== "exact") { + return invalidVerifyResponse("unsupported_scheme"); + } + + if (requirements.network !== payload.accepted.network) { + return invalidVerifyResponse("network_mismatch"); + } + if (!isStellarNetwork(requirements.network)) { + return invalidVerifyResponse("invalid_network"); + } + + const networkPassphrase = getNetworkPassphrase(requirements.network); + const server = getRpcClient(requirements.network, this.rpcConfig); + + // Step 2: Parse and decode transaction + const stellarPayload = payload.payload as ExactStellarPayloadV2; + if (!stellarPayload || typeof stellarPayload.transaction !== "string") { + return invalidVerifyResponse("invalid_exact_stellar_payload_malformed"); + } + + let transaction: Transaction; + try { + transaction = new Transaction(stellarPayload.transaction, networkPassphrase); + } catch (error) { + console.error("Error parsing transaction:", error); + return invalidVerifyResponse("invalid_exact_stellar_payload_malformed"); + } + + // Step 3: Validate transaction structure + if (transaction.operations.length !== 1) { + return invalidVerifyResponse("invalid_exact_stellar_payload_wrong_operation"); + } + + const operation = transaction.operations[0]; + if (operation.type !== "invokeHostFunction") { + return invalidVerifyResponse("invalid_exact_stellar_payload_wrong_operation"); + } + + if ( + this.signingAddresses.has(operation.source ?? "") || + this.signingAddresses.has(transaction.source) + ) { + return invalidVerifyResponse("invalid_exact_stellar_payload_unsafe_tx_or_op_source"); + } + + // Step 4: Extract and validate contract invocation details + const invokeOp = operation as Operation.InvokeHostFunction; + const func = invokeOp.func; + + if (!func || func.switch().name !== "hostFunctionTypeInvokeContract") { + return invalidVerifyResponse("invalid_exact_stellar_payload_wrong_operation"); + } + + // Step 5: Validate contract address and function name + const invokeContractArgs = func.invokeContract(); + const contractAddress = Address.fromScAddress( + invokeContractArgs.contractAddress(), + ).toString(); + const functionName = invokeContractArgs.functionName().toString(); + + const args = invokeContractArgs.args(); + if (contractAddress !== requirements.asset) { + return invalidVerifyResponse("invalid_exact_stellar_payload_wrong_asset"); + } + + if (functionName !== "transfer" || args.length !== 3) { + return invalidVerifyResponse("invalid_exact_stellar_payload_wrong_function_name"); + } + + // Step 6: Extract and validate transfer arguments + fromAddress = scValToNative(args[0]) as string; + const toAddress = scValToNative(args[1]) as string; + const amount = scValToNative(args[2]) as bigint; + + if (this.signingAddresses.has(fromAddress)) { + return invalidVerifyResponse("invalid_exact_stellar_payload_facilitator_is_payer"); + } + + if (toAddress !== requirements.payTo) { + return invalidVerifyResponse("invalid_exact_stellar_payload_wrong_recipient", fromAddress); + } + + const expectedAmount = BigInt(requirements.amount); + if (amount !== expectedAmount) { + return invalidVerifyResponse("invalid_exact_stellar_payload_wrong_amount", fromAddress); + } + + // Step 7: Re-simulate to ensure transaction will succeed + const simResponse = await server.simulateTransaction(transaction); + if (!Api.isSimulationSuccess(simResponse)) { + const errorMsg = simResponse.error ? `: ${simResponse.error}` : ""; + console.error("Simulation error:", errorMsg); + return invalidVerifyResponse( + "invalid_exact_stellar_payload_simulation_failed", + fromAddress, + ); + } + + // Step 8: Validate if the resource fees are within acceptable bounds + const clientFeeStroops = parseInt(transaction.fee, 10); + const minResourceFee = parseInt(simResponse.minResourceFee, 10); + + // Fee must be at least the minimum required by simulation + if (clientFeeStroops < minResourceFee) { + return invalidVerifyResponse( + "invalid_exact_stellar_payload_fee_below_minimum", + fromAddress, + ); + } + + // Fee must not exceed the facilitator's maximum + if (clientFeeStroops > this.maxTransactionFeeStroops) { + return invalidVerifyResponse("invalid_exact_stellar_payload_fee_exceeds_maximum"); + } + + // Step 9: Validate simulation events for expected transfer only. + const eventValidation = this.validateSimulationEvents( + simResponse.events, + fromAddress, + requirements.payTo, + expectedAmount, + requirements.asset, + ); + if (eventValidation) { + return eventValidation; + } + + const latestLedger = await server.getLatestLedger(); + const currentLedger = latestLedger.sequence; + const maxTimeoutSeconds = requirements.maxTimeoutSeconds ?? DEFAULT_TIMEOUT_SECONDS; + const estimatedLedgerSeconds = await getEstimatedLedgerCloseTimeSeconds(requirements.network); + const maxLedgerOffset = Math.ceil(maxTimeoutSeconds / estimatedLedgerSeconds); + const maxLedger = currentLedger + maxLedgerOffset; + + // Step 10: Validate auth entries (structure, credential type, expiration, facilitator safety, and signature status). + const authValidation = this.validateAuthEntries( + invokeOp, + this.signingAddresses, + fromAddress, + maxLedger, + transaction, + simResponse, + ); + if (authValidation) { + return authValidation; + } + + return validVerifyResponse(fromAddress); + } catch (error) { + console.error("Unexpected verification error:", error); + return invalidVerifyResponse("unexpected_verify_error", fromAddress); + } + } + + /** + * Settles a payment by submitting the transaction on-chain. + * + * @param payload - The payment payload to settle + * @param requirements - The payment requirements + * @returns Promise resolving to settlement response + */ + async settle( + payload: PaymentPayload, + requirements: PaymentRequirements, + ): Promise { + const server = getRpcClient(requirements.network, this.rpcConfig); + const networkPassphrase = getNetworkPassphrase(requirements.network); + let payer: string | undefined; + let txHash: string | undefined; + + try { + // Step 1: Verify payment before settlement + const verifyResult = await this.verify(payload, requirements); + + if (!verifyResult.isValid) { + return { + success: false, + network: payload.accepted.network, + transaction: "", + errorReason: verifyResult.invalidReason ?? "verification_failed", + payer: verifyResult.payer, + }; + } + + payer = verifyResult.payer!; + + // Step 2: Parse transaction envelope once to extract both transaction and Soroban data + const stellarPayload = payload.payload as ExactStellarPayloadV2; + const txEnvelope = xdr.TransactionEnvelope.fromXDR(stellarPayload.transaction, "base64"); + const transaction = new Transaction(stellarPayload.transaction, networkPassphrase); + const sorobanData = txEnvelope.v1()?.tx()?.ext()?.sorobanData() || undefined; + + // Validate Soroban data is present for Soroban transactions + if (!sorobanData) { + return { + success: false, + network: payload.accepted.network, + transaction: "", + errorReason: "invalid_exact_stellar_payload_malformed", + payer, + }; + } + + // Step 3: Extract operation + const invokeOp = transaction.operations[0] as Operation.InvokeHostFunction; + + // Step 4: Rebuild transaction with facilitator as source and facilitator-chosen fee + const signer = this.signerMap.get(this.selectSigner([...this.signingAddresses])); + if (!signer) { + return { + success: false, + network: payload.accepted.network, + transaction: "", + errorReason: "settle_exact_stellar_signer_selection_failed", + payer, + }; + } + const facilitatorAccount = await server.getAccount(signer.address); + + // Use the minimum of the client's fee and the maximum cap + const clientFeeStroops = parseInt(transaction.fee, 10); + const maxFeeStroops = Math.min(clientFeeStroops, this.maxTransactionFeeStroops); + + const rebuiltTx = new TransactionBuilder(facilitatorAccount, { + fee: maxFeeStroops.toString(), + networkPassphrase, + ledgerbounds: transaction.ledgerBounds, + memo: transaction.memo, + minAccountSequence: transaction.minAccountSequence, + minAccountSequenceAge: transaction.minAccountSequenceAge, + minAccountSequenceLedgerGap: transaction.minAccountSequenceLedgerGap, + extraSigners: transaction.extraSigners, + sorobanData, + }) + .setTimeout(requirements.maxTimeoutSeconds ?? DEFAULT_TIMEOUT_SECONDS) + .addOperation(Operation.invokeHostFunction(invokeOp)) + .build(); + + // Step 5: Sign inner transaction with the selected signer's key + const { signedTxXdr, error: signError } = await signer.signTransaction(rebuiltTx.toXDR(), { + networkPassphrase, + }); + + if (signError) { + return { + success: false, + network: payload.accepted.network, + transaction: "", + errorReason: "settle_exact_stellar_transaction_signing_failed", + payer, + }; + } + + // Step 6: Optionally wrap in a fee bump transaction + let txToSubmit: Transaction | FeeBumpTransaction; + + if (this.feeBumpSigner) { + const signedInnerTx = TransactionBuilder.fromXDR( + signedTxXdr, + networkPassphrase, + ) as Transaction; + + const feeBumpTx = TransactionBuilder.buildFeeBumpTransaction( + this.feeBumpSigner.address, + maxFeeStroops.toString(), // Same as the inner transaction fee + signedInnerTx, + networkPassphrase, + ); + + const { signedTxXdr: signedFeeBumpXdr, error: feeBumpSignError } = + await this.feeBumpSigner.signTransaction(feeBumpTx.toXDR(), { networkPassphrase }); + + if (feeBumpSignError) { + return { + success: false, + network: payload.accepted.network, + transaction: "", + errorReason: "settle_exact_stellar_fee_bump_signing_failed", + payer, + }; + } + + txToSubmit = TransactionBuilder.fromXDR( + signedFeeBumpXdr, + networkPassphrase, + ) as FeeBumpTransaction; + } else { + txToSubmit = TransactionBuilder.fromXDR(signedTxXdr, networkPassphrase) as Transaction; + } + + // Step 7: Submit transaction to network + const sendResult = await server.sendTransaction(txToSubmit); + + if (sendResult.status !== "PENDING") { + return { + success: false, + network: payload.accepted.network, + transaction: "", + errorReason: "settle_exact_stellar_transaction_submission_failed", + payer, + }; + } + + // Step 8: Poll for transaction confirmation + txHash = sendResult.hash; + const maxPollAttempts = requirements.maxTimeoutSeconds ?? DEFAULT_TIMEOUT_SECONDS; + const confirmResult = await this.pollForTransaction(server, txHash, maxPollAttempts); + + if (!confirmResult.success) { + return { + success: false, + network: payload.accepted.network, + transaction: txHash, + errorReason: "settle_exact_stellar_transaction_failed", + payer, + }; + } + + // Step 9: Return success + return { + success: true, + transaction: txHash, + network: payload.accepted.network, + payer: payer, + }; + } catch (error) { + console.error("Unexpected settlement error:", error); + return { + success: false, + network: payload.accepted.network, + transaction: txHash || "", + errorReason: "unexpected_settle_error", + payer, + }; + } + } + + /** + * Polls for transaction confirmation on Soroban. + * + * @param server - Soroban RPC server + * @param txHash - Transaction hash to poll for + * @param maxPollAttempts - Maximum number of polling attempts (default: 15) + * @param delayMs - Delay between attempts in milliseconds (default: 1000) + * @returns Result with success status + */ + private async pollForTransaction( + server: rpc.Server, + txHash: string, + maxPollAttempts = 15, + delayMs = 1000, + ): Promise<{ success: boolean }> { + for (let i = 0; i < maxPollAttempts; i++) { + try { + const txResult = await server.getTransaction(txHash); + + if (txResult.status === "SUCCESS") { + return { success: true }; + } else if (txResult.status === "FAILED") { + return { success: false }; + } + + // Transaction still pending, wait and retry + await new Promise(resolve => setTimeout(resolve, delayMs)); + } catch (error: unknown) { + if (error instanceof Error && !error.message.includes("NOT_FOUND")) { + console.warn(`Poll attempt ${i} failed:`, error); + } + await new Promise(resolve => setTimeout(resolve, delayMs)); + } + } + + // Timeout + return { success: false }; + } + + /** + * Validates simulation events for transfer correctness. + * Ensures there is exactly one token transfer event, the transfer matches the + * expected sender, recipient, amount, and asset (contract address), and the + * facilitator address is not involved in the transfer. + * + * @param events - The array of DiagnosticEvent objects from the simulation + * @param fromAddress - The payer's address + * @param toAddress - The recipient's address + * @param expectedAmount - The expected transfer amount + * @param expectedAsset - The expected token contract address + * @returns undefined if the validation succeeds, otherwise an invalid VerifyResponse + */ + private validateSimulationEvents( + events: xdr.DiagnosticEvent[], + fromAddress: string, + toAddress: string, + expectedAmount: bigint, + expectedAsset: string, + ): VerifyResponse | undefined { + // Soroban token transfer events follow the [CAP-46](https://github.com/stellar/stellar-protocol/blob/master/core/cap-0046-06.md) format: + // Topic: ["transfer", from, to], Data: amount + const transferEvents: Array<{ + from: string; + to: string; + amount: bigint; + }> = []; + + // Parse events into + for (const diagnosticEvent of events) { + try { + const event = diagnosticEvent.event(); + + // Skip non-contract events + if (event.type().name !== "contract") { + continue; + } + + const body = event.body().v0(); + const topics = body.topics(); + + // Check if this is a transfer event (first topic is "transfer" symbol) + if (topics.length < 3) { + return invalidVerifyResponse( + "invalid_exact_stellar_payload_event_not_transfer", + fromAddress, + ); + } + + const topicType = topics[0].switch().name; + if (topicType !== "scvSymbol") { + return invalidVerifyResponse( + "invalid_exact_stellar_payload_event_not_transfer", + fromAddress, + ); + } + + const symbol = topics[0].sym().toString(); + if (symbol !== "transfer") { + return invalidVerifyResponse( + "invalid_exact_stellar_payload_event_not_transfer", + fromAddress, + ); + } + + const contractIdHash = event.contractId(); + if (!contractIdHash) + return invalidVerifyResponse( + "invalid_exact_stellar_payload_event_missing_contract_id", + fromAddress, + ); + const eventContractAddress = Address.fromScAddress( + xdr.ScAddress.scAddressTypeContract(contractIdHash), + ).toString(); + if (eventContractAddress !== expectedAsset) { + return invalidVerifyResponse( + "invalid_exact_stellar_payload_event_wrong_asset", + fromAddress, + ); + } + + // Extract from, to, and amount + const from = scValToNative(topics[1]) as string; + const to = scValToNative(topics[2]) as string; + const amount = scValToNative(body.data()) as bigint; + + transferEvents.push({ from, to, amount }); + } catch (error: unknown) { + if (error instanceof Error) { + console.error("Error parsing diagnostic event:", error.message); + } else { + console.error("Error parsing diagnostic event:", String(error)); + } + return invalidVerifyResponse("unexpected_verify_error", fromAddress); + } + } + + // If no transfer events are present, reject. + if (transferEvents.length === 0) { + return invalidVerifyResponse("invalid_exact_stellar_payload_no_transfer_events", fromAddress); + } + + if (transferEvents.length > 1) { + return invalidVerifyResponse("invalid_exact_stellar_payload_multiple_transfers", fromAddress); + } + + const transferEvent = transferEvents[0]; + + // Validate the transfer matches the expected sender, recipient, and amount + if (transferEvent.from !== fromAddress) { + return invalidVerifyResponse("invalid_exact_stellar_payload_event_wrong_from", fromAddress); + } + if (transferEvent.to !== toAddress) { + return invalidVerifyResponse("invalid_exact_stellar_payload_event_wrong_to", fromAddress); + } + if (transferEvent.amount !== expectedAmount) { + return invalidVerifyResponse("invalid_exact_stellar_payload_event_wrong_amount", fromAddress); + } + + return undefined; + } + + /** + * Validates authorization entries: structure, credential type, expiration, + * facilitator safety, no sub-invocations, and that the payer has signed and + * no other signatures are pending (per simulation). + * + * @param invokeOp - The invoke host function operation + * @param facilitatorAddresses - Set of all facilitator addresses + * @param fromAddress - The payer's address (for error reporting) + * @param maxLedger - The maximum allowed expiration ledger + * @param transaction - The full transaction (for signature status) + * @param simResponse - The simulation result (used to interpret auth entry signatures) + * @returns Invalid VerifyResponse when validation fails + */ + private validateAuthEntries( + invokeOp: Operation.InvokeHostFunction, + facilitatorAddresses: ReadonlySet, + fromAddress: string, + maxLedger: number, + transaction: Transaction, + simResponse: Api.SimulateTransactionSuccessResponse, + ): VerifyResponse | undefined { + if (!invokeOp.auth || invokeOp.auth.length === 0) { + return invalidVerifyResponse("invalid_exact_stellar_payload_no_auth_entries", fromAddress); + } + + for (const auth of invokeOp.auth) { + const credentialsType = auth.credentials().switch(); + + // Only address-based credentials are allowed + if (credentialsType !== xdr.SorobanCredentialsType.sorobanCredentialsAddress()) { + return invalidVerifyResponse( + "invalid_exact_stellar_payload_unsupported_credential_type", + fromAddress, + ); + } + + // Extract address from credentials + const addressCredentials = auth.credentials().address(); + const authAddress = Address.fromScAddress(addressCredentials.address()).toString(); + + // Facilitator must not appear in auth entries + if (facilitatorAddresses.has(authAddress)) { + return invalidVerifyResponse( + "invalid_exact_stellar_payload_facilitator_in_auth", + fromAddress, + ); + } + + // Check signature expiration is within allowed window (with ledger tolerance for RPC skew) + const expirationLedger = addressCredentials.signatureExpirationLedger(); + if (expirationLedger > maxLedger + SIGNATURE_EXPIRATION_LEDGER_TOLERANCE) { + return invalidVerifyResponse( + "invalid_exact_stellar_signature_expiration_too_far", + fromAddress, + ); + } + + // No sub-invocations allowed + const rootInvocation = auth.rootInvocation(); + if (rootInvocation.subInvocations().length > 0) { + return invalidVerifyResponse( + "invalid_exact_stellar_payload_has_subinvocations", + fromAddress, + ); + } + } + + const authStatus = gatherAuthEntrySignatureStatus({ + transaction, + simulationResponse: simResponse, + }); + if (!authStatus.alreadySigned.includes(fromAddress)) { + return invalidVerifyResponse( + "invalid_exact_stellar_payload_missing_payer_signature", + fromAddress, + ); + } + if (authStatus.pendingSignature.length > 0) { + return invalidVerifyResponse( + "invalid_exact_stellar_payload_unexpected_pending_signatures", + fromAddress, + ); + } + + return undefined; + } +} diff --git a/typescript/packages/mechanisms/stellar/src/exact/index.ts b/typescript/packages/mechanisms/stellar/src/exact/index.ts new file mode 100644 index 0000000000..028d97c00a --- /dev/null +++ b/typescript/packages/mechanisms/stellar/src/exact/index.ts @@ -0,0 +1 @@ +export { ExactStellarScheme } from "./client/scheme"; diff --git a/typescript/packages/mechanisms/stellar/src/exact/server/index.ts b/typescript/packages/mechanisms/stellar/src/exact/server/index.ts new file mode 100644 index 0000000000..9e91e067eb --- /dev/null +++ b/typescript/packages/mechanisms/stellar/src/exact/server/index.ts @@ -0,0 +1 @@ +export { ExactStellarScheme } from "./scheme"; diff --git a/typescript/packages/mechanisms/stellar/src/exact/server/scheme.ts b/typescript/packages/mechanisms/stellar/src/exact/server/scheme.ts new file mode 100644 index 0000000000..df9e6df072 --- /dev/null +++ b/typescript/packages/mechanisms/stellar/src/exact/server/scheme.ts @@ -0,0 +1,150 @@ +import { DEFAULT_TOKEN_DECIMALS } from "../../constants"; +import { convertToTokenAmount, getUsdcAddress } from "../../utils"; +import type { + AssetAmount, + Network, + PaymentRequirements, + Price, + SchemeNetworkServer, + MoneyParser, +} from "@x402/core/types"; + +/** + * Stellar server implementation for the Exact payment scheme. + */ +export class ExactStellarScheme implements SchemeNetworkServer { + readonly scheme = "exact"; + private moneyParsers: MoneyParser[] = []; + + /** + * Register a custom money parser in the parser chain. + * Multiple parsers can be registered - they will be tried in registration order. + * Each parser receives a decimal amount (e.g., 1.50 for $1.50). + * If a parser returns null, the next parser in the chain will be tried. + * The default parser is always the final fallback. + * + * @param parser - Custom function to convert amount to AssetAmount (or null to skip) + * @returns The service instance for chaining + */ + registerMoneyParser(parser: MoneyParser): ExactStellarScheme { + this.moneyParsers.push(parser); + return this; + } + + /** + * Parses a price into `AssetAmount`. + * If price is already an `AssetAmount`, returns it directly. + * If price is `Money` (string | number), parses to decimal and tries custom parsers. + * If no custom parsers return a valid `AssetAmount`, falls back to default conversion, assuming USDC token contract. + * + * @param price - The `Price` to parse + * @param network - The `Network` to use + * @returns Promise that resolves to the parsed `AssetAmount` + */ + async parsePrice(price: Price, network: Network): Promise { + // Attempt 1: if already an AssetAmount, return it directly + if (typeof price === "object" && price !== null && "amount" in price) { + if (!price.asset) { + throw new Error(`Asset address must be specified for AssetAmount on network ${network}`); + } + return { + amount: price.amount, + asset: price.asset, + extra: price.extra || {}, + }; + } + + // Parse Money to decimal number + const amount = this.parseMoneyToDecimal(price); + + // Attempt 2: try each custom money parser in order + for (const parser of this.moneyParsers) { + const result = await parser(amount, network); + if (result !== null) { + return result; + } + } + + // Attempt 3: fallback to default conversion, assuming USDC token contract. + return this.defaultMoneyConversion(amount, network); + } + + /** + * Build payment requirements for this scheme/network combination + * + * @param paymentRequirements - The base payment requirements + * @param supportedKind - The supported kind configuration + * @param supportedKind.x402Version - The x402 protocol version + * @param supportedKind.scheme - The payment scheme + * @param supportedKind.network - The network identifier + * @param supportedKind.extra - Extra metadata including `areFeesSponsored` from facilitator + * @param extensionKeys - Extension keys supported by the facilitator + * @returns Enhanced payment requirements with `areFeesSponsored` in extra + */ + enhancePaymentRequirements( + paymentRequirements: PaymentRequirements, + supportedKind: { + x402Version: number; + scheme: string; + network: Network; + extra?: Record; + }, + extensionKeys: string[], + ): Promise { + // Mark unused parameters to satisfy linter + void extensionKeys; + + // Add `areFeesSponsored` from supportedKind.extra to payment requirements + // The facilitator provides `areFeesSponsored` which clients use to determine if fees are sponsored + const areFeesSponsored = supportedKind.extra?.areFeesSponsored; + return Promise.resolve({ + ...paymentRequirements, + extra: { + ...paymentRequirements.extra, + ...(typeof areFeesSponsored === "boolean" && { areFeesSponsored }), + }, + }); + } + + /** + * Parse Money (string | number) to a decimal number. + * Handles formats like "$1.50", "1.50", 1.50, etc. + * + * @param money - The money value to parse + * @returns Decimal number + */ + private parseMoneyToDecimal(money: string | number): number { + if (typeof money === "number") { + return money; + } + + // Remove $ sign and whitespace, then parse + const cleanMoney = money.replace(/^\$/, "").trim(); + const amount = parseFloat(cleanMoney); + + if (isNaN(amount)) { + throw new Error(`Invalid money format: ${money}`); + } + + return amount; + } + + /** + * Default money conversion implementation. + * Converts decimal amount to USDC on the specified network. + * + * @param amount - The decimal amount (e.g., 1.50) + * @param network - The network to use + * @returns The parsed asset amount in USDC + */ + private defaultMoneyConversion(amount: number, network: Network): AssetAmount { + // Convert decimal amount to token amount (USDC on Stellar has 7 decimals) + const tokenAmount = convertToTokenAmount(amount.toString(), DEFAULT_TOKEN_DECIMALS); + + return { + amount: tokenAmount, + asset: getUsdcAddress(network), + extra: {}, + }; + } +} diff --git a/typescript/packages/mechanisms/stellar/src/index.ts b/typescript/packages/mechanisms/stellar/src/index.ts new file mode 100644 index 0000000000..31c40bc6e5 --- /dev/null +++ b/typescript/packages/mechanisms/stellar/src/index.ts @@ -0,0 +1,24 @@ +/** + * Stellar blockchain support for x402 protocol. + * + * This package provides Stellar network support for the x402 payment protocol, + * including client signing, server validation, and facilitator settlement. + * + * @module + */ + +// Exact scheme client +export { ExactStellarScheme } from "./exact"; + +// Types +export * from "./types"; + +// Constants +export * from "./constants"; + +// Signers +export * from "./signer"; + +// Utilities +export * from "./utils"; +export * from "./shared"; diff --git a/typescript/packages/mechanisms/stellar/src/shared.ts b/typescript/packages/mechanisms/stellar/src/shared.ts new file mode 100644 index 0000000000..7cbb83bc7d --- /dev/null +++ b/typescript/packages/mechanisms/stellar/src/shared.ts @@ -0,0 +1,134 @@ +import { Transaction, Address, Operation, xdr } from "@stellar/stellar-sdk"; +import { Api, assembleTransaction } from "@stellar/stellar-sdk/rpc"; + +/** + * Handles the simulation result of a Stellar transaction. + * + * @param simulation - The simulation result to handle + * @throws An error if the simulation result is of type "RESTORE" or "ERROR" + */ +export function handleSimulationResult(simulation?: Api.SimulateTransactionResponse) { + if (!simulation) { + throw new Error("Simulation result is undefined"); + } + + if (Api.isSimulationRestore(simulation)) { + throw new Error( + `Stellar simulation result has type "RESTORE" with restorePreamble: ${simulation.restorePreamble}`, + ); + } + + if (Api.isSimulationError(simulation)) { + const msg = `Stellar simulation failed${simulation.error ? ` with error message: ${simulation.error}` : ""}`; + + throw new Error(msg); + } +} + +/** + * Analysis result of transaction signers + */ +export type ContractSigners = { + /** Accounts that have already signed auth entries */ + alreadySigned: string[]; + /** Accounts that still need to sign auth entries */ + pendingSignature: string[]; +}; + +/** + * Input parameters for gathering auth entry signature status + */ +export type GatherAuthEntrySignatureStatusInput = { + /** The transaction to analyze */ + transaction: Transaction; + /** Optional simulation response to assemble with transaction before analysis */ + simulationResponse?: Api.SimulateTransactionResponse; + /** Whether to simulate/assemble the transaction with simulation data (default: true if simulationResponse was not provided) */ + simulate?: boolean; +}; + +/** + * Gathers the signature status of auth entries in a Stellar transaction. + * + * This function inspects the auth entries in the transaction's InvokeHostFunction + * operation and categorizes them based on their signature status. + * + * @param input - Input containing transaction and optional simulation data + * @param input.transaction - The transaction to analyze + * @param input.simulationResponse - Optional simulation response to assemble with transaction before analysis + * @param input.simulate - Whether to simulate/assemble the transaction with simulation data (default: true if simulationResponse was not provided) + * @returns ContractSigners with arrays of signed and pending signer addresses + * @throws Error if transaction doesn't have exactly one InvokeHostFunction operation + * + * @example + * ```ts + * const status = gatherAuthEntrySignatureStatus({ + * transaction: tx, + * simulationResponse: simResult + * }); + * console.log('Already signed:', status.alreadySigned); + * console.log('Pending:', status.pendingSignature); + * ``` + */ +export function gatherAuthEntrySignatureStatus({ + transaction, + simulationResponse, + simulate, +}: GatherAuthEntrySignatureStatusInput): ContractSigners { + // Determine if we should assemble with simulation + const shouldAssemble = simulate ?? simulationResponse !== undefined; + let assembledTx = transaction; + + // Assemble transaction with simulation if requested + if (shouldAssemble && simulationResponse) { + const assembledTxBuilder = assembleTransaction(transaction, simulationResponse); + assembledTx = assembledTxBuilder.build(); + } + + // Validate transaction structure + if (assembledTx.operations.length !== 1) { + throw new Error( + `Expected transaction with exactly one operation, got ${assembledTx.operations.length}`, + ); + } + + const operation = assembledTx.operations[0]; + if (operation.type !== "invokeHostFunction") { + throw new Error(`Expected InvokeHostFunction operation, got ${operation.type}`); + } + + const invokeOp = operation as Operation.InvokeHostFunction; + + const alreadySigned: string[] = []; + const pendingSignature: string[] = []; + + for (const entry of invokeOp.auth ?? []) { + const credentialsType = entry.credentials().switch(); + + // Skip source account credentials - these use the transaction source + if (credentialsType === xdr.SorobanCredentialsType.sorobanCredentialsSourceAccount()) { + continue; + } + + // Handle address-based credentials + if (credentialsType === xdr.SorobanCredentialsType.sorobanCredentialsAddress()) { + const addressCredentials = entry.credentials().address(); + const address = Address.fromScAddress(addressCredentials.address()).toString(); + const signature = addressCredentials.signature(); + + // Check if already signed (signature is not scvVoid) + const isSigned = signature.switch().name !== "scvVoid"; + + if (isSigned) { + alreadySigned.push(address); + } else { + pendingSignature.push(address); + } + } + } + + return { + alreadySigned: [...new Set(alreadySigned)], // Remove duplicates + pendingSignature: [...new Set(pendingSignature)], + }; +} diff --git a/typescript/packages/mechanisms/stellar/src/signer.ts b/typescript/packages/mechanisms/stellar/src/signer.ts new file mode 100644 index 0000000000..9a6498b809 --- /dev/null +++ b/typescript/packages/mechanisms/stellar/src/signer.ts @@ -0,0 +1,102 @@ +import { Keypair } from "@stellar/stellar-sdk"; +import { basicNodeSigner, SignAuthEntry, SignTransaction } from "@stellar/stellar-sdk/contract"; +import { STELLAR_TESTNET_CAIP2 } from "./constants"; +import { getNetworkPassphrase } from "./utils"; +import type { Network } from "@x402/core/types"; + +/** + * Ed25519 signer for Stellar transactions and auth entries. + * + * Implements SEP-43 interface (except signMessage). + * + * @see https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0043.md + */ +export type Ed25519Signer = { + address: string; + signAuthEntry: SignAuthEntry; + signTransaction: SignTransaction; +}; + +/** + * Facilitator signer for Stellar transactions. + * + * Alias for Ed25519Signer. Used by x402 facilitators to verify and settle payments. + * + * @see https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0043.md + */ +export type FacilitatorStellarSigner = Ed25519Signer; + +/** + * Client signer for Stellar transactions. + * + * Used by x402 clients to sign auth entries. Supports both classic (G) and contract (C) accounts. + * signTransaction is optional for client signers. + * + * @see https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0043.md + */ +export type ClientStellarSigner = { + address: string; + signAuthEntry: SignAuthEntry; + signTransaction?: SignTransaction; +}; + +/** + * Creates an Ed25519 signer for the given Stellar network. + * + * @param privateKey - Stellar classic (G) account private key + * @param defaultNetwork - Is the network the signTransactiopn method will default to if no network is provided. Must use the CAIP-2 format identifier. + * @returns Ed25519 signer implementing SEP-43 interface (except signMessage) + * @see https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0043.md + */ +export function createEd25519Signer( + privateKey: string, + defaultNetwork: Network = STELLAR_TESTNET_CAIP2, +): Ed25519Signer { + const kp = Keypair.fromSecret(privateKey); + const networkPassphrase = getNetworkPassphrase(defaultNetwork); + + const address = kp.publicKey(); + const { signAuthEntry, signTransaction } = basicNodeSigner(kp, networkPassphrase); + + return { + address, + signAuthEntry, + signTransaction, + }; +} + +/** + * Type guard for FacilitatorStellarSigner. + * + * Checks for required methods: address, signAuthEntry, signTransaction. + * + * @param signer - Value to check + * @returns `true` if signer is a FacilitatorStellarSigner + */ +export function isFacilitatorStellarSigner(signer: unknown): signer is FacilitatorStellarSigner { + if (typeof signer !== "object" || signer === null) return false; + const s = signer as Record; + return ( + typeof s.address === "string" && + typeof s.signAuthEntry === "function" && + typeof s.signTransaction === "function" + ); +} + +/** + * Type guard for ClientStellarSigner. + * + * Checks for required methods: address, signAuthEntry. signTransaction is optional. + * + * @param signer - Value to check + * @returns `true` if signer is a ClientStellarSigner + */ +export function isClientStellarSigner(signer: unknown): signer is ClientStellarSigner { + if (typeof signer !== "object" || signer === null) return false; + const s = signer as Record; + return ( + typeof s.address === "string" && + typeof s.signAuthEntry === "function" && + (s.signTransaction === undefined || typeof s.signTransaction === "function") + ); +} diff --git a/typescript/packages/mechanisms/stellar/src/types.ts b/typescript/packages/mechanisms/stellar/src/types.ts new file mode 100644 index 0000000000..0349610895 --- /dev/null +++ b/typescript/packages/mechanisms/stellar/src/types.ts @@ -0,0 +1,9 @@ +/** + * Exact Stellar payload structure containing a base64 encoded Stellar transaction + */ +export type ExactStellarPayloadV2 = { + /** + * Base64 encoded Stellar transaction + */ + transaction: string; +}; diff --git a/typescript/packages/mechanisms/stellar/src/utils.ts b/typescript/packages/mechanisms/stellar/src/utils.ts new file mode 100644 index 0000000000..2005cc2969 --- /dev/null +++ b/typescript/packages/mechanisms/stellar/src/utils.ts @@ -0,0 +1,215 @@ +import { Horizon, rpc } from "@stellar/stellar-sdk"; +import { + DEFAULT_PUBNET_HORIZON_URL, + DEFAULT_TESTNET_HORIZON_URL, + DEFAULT_TESTNET_RPC_URL, + DEFAULT_TOKEN_DECIMALS, + STELLAR_ASSET_ADDRESS_REGEX, + STELLAR_DESTINATION_ADDRESS_REGEX, + STELLAR_NETWORK_TO_PASSPHRASE, + STELLAR_PUBNET_CAIP2, + STELLAR_TESTNET_CAIP2, + USDC_PUBNET_ADDRESS, + USDC_TESTNET_ADDRESS, +} from "./constants"; +import type { Network } from "@x402/core/types"; + +export const DEFAULT_ESTIMATED_LEDGER_SECONDS = 5; +const HORIZON_LEDGERS_SAMPLE_SIZE = 20; + +/** + * Configuration for RPC client connections + */ +export interface RpcConfig { + /** Custom RPC URL to use instead of defaults */ + url?: string; +} + +/** + * Checks if a network is a Stellar network + * + * @param network - The CAIP-2 network identifier + * @returns `true` if the network is a Stellar network, `false` otherwise + */ +export function isStellarNetwork(network: Network): boolean { + return STELLAR_NETWORK_TO_PASSPHRASE.has(network); +} + +/** + * Validates a Stellar destination address (G-account, C-account, or M-account) + * + * @param address - Stellar destination address to validate + * @returns `true` if the address is valid, `false` otherwise + */ +export function validateStellarDestinationAddress(address: string): boolean { + return STELLAR_DESTINATION_ADDRESS_REGEX.test(address); +} + +/** + * Validates a Stellar asset/contract address (C-account only) + * + * @param address - Stellar asset address to validate + * @returns `true` if the address is valid, `false` otherwise + */ +export function validateStellarAssetAddress(address: string): boolean { + return STELLAR_ASSET_ADDRESS_REGEX.test(address); +} + +/** + * Gets the network passphrase for a given Stellar network + * + * @param network - The CAIP-2 network identifier + * @returns The network passphrase string + * @throws {Error} If the network is not a known Stellar network + */ +export function getNetworkPassphrase(network: Network): string { + const networkPassphrase = STELLAR_NETWORK_TO_PASSPHRASE.get(network); + if (!networkPassphrase) { + throw new Error(`Unknown Stellar network: ${network}`); + } + return networkPassphrase; +} + +/** + * Gets the RPC URL for a given Stellar network + * + * @param network - The CAIP-2 network identifier + * @param rpcConfig - Optional RPC configuration with custom URL + * @returns The RPC URL string + * @throws {Error} If the network is unknown or mainnet RPC URL is not provided + */ +export function getRpcUrl(network: Network, rpcConfig?: RpcConfig): string { + const customRpcUrl = rpcConfig?.url; + switch (network) { + case STELLAR_TESTNET_CAIP2: + return customRpcUrl || DEFAULT_TESTNET_RPC_URL; + case STELLAR_PUBNET_CAIP2: + if (!customRpcUrl) { + throw new Error( + "Stellar mainnet requires a non-empty rpcUrl. For a list of RPC providers, see https://developers.stellar.org/docs/data/apis/rpc/providers#publicly-accessible-apis", + ); + } + return customRpcUrl; + default: + throw new Error(`Unknown Stellar network: ${network}`); + } +} + +/** + * Creates a Soroban RPC client for the given network + * + * @param network - The CAIP-2 network identifier + * @param rpcConfig - Optional RPC configuration with custom URL + * @returns A configured Soroban RPC Server instance + * @throws {Error} If the network is not a valid Stellar network + */ +export function getRpcClient(network: Network, rpcConfig?: RpcConfig): rpc.Server { + const rpcUrl = getRpcUrl(network, rpcConfig); + return new rpc.Server(rpcUrl, { + allowHttp: network === STELLAR_TESTNET_CAIP2, // Allow HTTP for testnet + }); +} + +/** + * Creates a Horizon SDK client for the given network. + * + * @param network - The CAIP-2 network identifier + * @returns A configured Horizon.Server instance + * @throws {Error} If the network is unknown + */ +export function getHorizonClient(network: Network): Horizon.Server { + switch (network) { + case STELLAR_TESTNET_CAIP2: + return new Horizon.Server(DEFAULT_TESTNET_HORIZON_URL); + case STELLAR_PUBNET_CAIP2: + return new Horizon.Server(DEFAULT_PUBNET_HORIZON_URL); + default: + throw new Error(`Unknown Stellar network: ${network}`); + } +} + +/** + * Estimates ledger close time by fetching the most recent ledgers from Horizon. + * + * Uses the Horizon SDK's ledger query builder which is significantly faster + * than the Soroban RPC `getLedgers` method for this purpose. + * + * @param network - The CAIP-2 network identifier + * @returns Estimated seconds per ledger, or DEFAULT_ESTIMATED_LEDGER_SECONDS (5) on error + */ +export async function getEstimatedLedgerCloseTimeSeconds(network: Network): Promise { + try { + const horizon = getHorizonClient(network); + const page = await horizon.ledgers().limit(HORIZON_LEDGERS_SAMPLE_SIZE).order("desc").call(); + const records = page.records; + if (!records || records.length < 2) return DEFAULT_ESTIMATED_LEDGER_SECONDS; + + const newestTs = new Date(records[0].closed_at).getTime() / 1000; + const oldestTs = new Date(records[records.length - 1].closed_at).getTime() / 1000; + const intervals = records.length - 1; + return Math.ceil((newestTs - oldestTs) / intervals); + } catch { + return DEFAULT_ESTIMATED_LEDGER_SECONDS; + } +} + +/** + * Gets the default USDC contract address for a network + * + * @param network - The CAIP-2 network identifier + * @returns The USDC contract address for the network + * @throws {Error} If the network doesn't have a configured USDC address + */ +export function getUsdcAddress(network: Network): string { + switch (network) { + case STELLAR_PUBNET_CAIP2: + return USDC_PUBNET_ADDRESS; + case STELLAR_TESTNET_CAIP2: + return USDC_TESTNET_ADDRESS; + default: + throw new Error(`No USDC address configured for network: ${network}`); + } +} + +/** + * Converts a decimal amount to token smallest units + * + * Handles both regular decimal strings (e.g., "0.10") and scientific notation (e.g., "1e-7"). + * The result is truncated (not rounded) to the specified number of decimal places. + * + * @param decimalAmount - The decimal amount as a string + * @param decimals - Number of decimal places for the token (default: 7 for USDC) + * @returns The amount in smallest units as a string with leading zeros removed + * @throws {Error} If the amount is invalid or decimals is out of range + * + * @example + * ```ts + * convertToTokenAmount("0.1", 7) // "1000000" + * convertToTokenAmount("1.5", 7) // "15000000" + * convertToTokenAmount("1e-7", 7) // "1" + * convertToTokenAmount("1.5", 0) // "1" (truncated) + * ``` + */ +export function convertToTokenAmount( + decimalAmount: string, + decimals: number = DEFAULT_TOKEN_DECIMALS, +): string { + const amount = parseFloat(decimalAmount); + if (isNaN(amount)) { + throw new Error(`Invalid amount: ${decimalAmount}`); + } + + if (decimals < 0 || decimals > 20) { + throw new Error(`Decimals must be between 0 and 20, got ${decimals}`); + } + + // Normalize scientific notation to fixed decimal string + const normalizedDecimal = /[eE]/.test(decimalAmount) + ? amount.toFixed(Math.max(decimals, 20)) + : decimalAmount; + + const [intPart, decPart = ""] = normalizedDecimal.split("."); + const paddedDec = decPart.padEnd(decimals, "0").slice(0, decimals); + + return (intPart + paddedDec).replace(/^0+/, "") || "0"; +} diff --git a/typescript/packages/mechanisms/stellar/test/integrations/exact-stellar.test.ts b/typescript/packages/mechanisms/stellar/test/integrations/exact-stellar.test.ts new file mode 100644 index 0000000000..5a65262e16 --- /dev/null +++ b/typescript/packages/mechanisms/stellar/test/integrations/exact-stellar.test.ts @@ -0,0 +1,596 @@ +import { x402Client, x402HTTPClient } from "@x402/core/client"; +import { x402Facilitator } from "@x402/core/facilitator"; +import { + HTTPAdapter, + HTTPResponseInstructions, + x402HTTPResourceServer, + x402ResourceServer, + FacilitatorClient, +} from "@x402/core/server"; +import { + AssetAmount, + Network, + PaymentPayload, + PaymentRequirements, + VerifyResponse, + SettleResponse, + SupportedResponse, +} from "@x402/core/types"; +import { beforeAll, beforeEach, describe, expect, it } from "vitest"; +import { createEd25519Signer, Ed25519Signer, STELLAR_TESTNET_CAIP2 } from "../../src"; +import { ExactStellarScheme as ExactStellarClient } from "../../src/exact/client"; +import { ExactStellarScheme as ExactStellarFacilitator } from "../../src/exact/facilitator"; +import { ExactStellarScheme as ExactStellarServer } from "../../src/exact/server"; +import type { ExactStellarPayloadV2 } from "../../src/types"; + +// Load private keys and addresses from environment +const CLIENT_PRIVATE_KEY = process.env.CLIENT_PRIVATE_KEY; +const FACILITATOR_PRIVATE_KEY = process.env.FACILITATOR_PRIVATE_KEY; +const FACILITATOR_ADDRESS = process.env.FACILITATOR_ADDRESS; +const RESOURCE_SERVER_ADDRESS = process.env.RESOURCE_SERVER_ADDRESS; +const XLM_TESTNET_ASSET = "CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC"; + +async function xlmFallbackParser(amount: number, network: string): Promise { + if (network === STELLAR_TESTNET_CAIP2) { + return { + amount: Math.round(amount * 1e7).toString(), + asset: XLM_TESTNET_ASSET, + extra: {}, + }; + } + return null; +} + +const missingEnvVars = + !CLIENT_PRIVATE_KEY || + !FACILITATOR_PRIVATE_KEY || + !FACILITATOR_ADDRESS || + !RESOURCE_SERVER_ADDRESS; + +const HORIZON_TESTNET = "https://horizon-testnet.stellar.org"; +const FRIENDBOT_URL = "https://friendbot.stellar.org"; +const STELLAR_EXPERT_TESTNET_TX = "https://stellar.expert/explorer/testnet/tx"; + +function logStellarExpertTxUrl(txHash: string): void { + console.log(`Stellar Expert (testnet): ${STELLAR_EXPERT_TESTNET_TX}/${txHash}`); +} + +async function fundOneAccount(address: string): Promise { + const res = await fetch(`${HORIZON_TESTNET}/accounts/${address}`); + if (res.status === 404) { + console.log(`Account ${address} not found, funding with Friendbot\n`); + const fb = await fetch(`${FRIENDBOT_URL}?addr=${encodeURIComponent(address)}`); + if (!fb.ok) { + const body = await fb.text(); + throw new Error(`Friendbot failed for ${address}: ${fb.status} ${body}`); + } + console.log(`Account ${address} funded with Friendbot\n`); + } else if (!res.ok) { + throw new Error(`Horizon account check failed for ${address}: ${res.status}`); + } +} + +async function ensureAccountsFunded(addresses: string[]): Promise { + await Promise.all(addresses.map(fundOneAccount)); +} + +/** + * Stellar Facilitator Client wrapper + * Wraps the x402Facilitator for use with x402ResourceServer + */ +class StellarFacilitatorClient implements FacilitatorClient { + readonly scheme = "exact"; + readonly network = STELLAR_TESTNET_CAIP2; + readonly x402Version = 2; + + /** + * Creates a new StellarFacilitatorClient instance + * + * @param facilitator - The x402 facilitator to wrap + */ + constructor(private readonly facilitator: x402Facilitator) {} + + /** + * Verifies a payment payload + * + * @param paymentPayload - The payment payload to verify + * @param paymentRequirements - The payment requirements + * @returns Promise resolving to verification response + */ + verify( + paymentPayload: PaymentPayload, + paymentRequirements: PaymentRequirements, + ): Promise { + return this.facilitator.verify(paymentPayload, paymentRequirements); + } + + /** + * Settles a payment + * + * @param paymentPayload - The payment payload to settle + * @param paymentRequirements - The payment requirements + * @returns Promise resolving to settlement response + */ + settle( + paymentPayload: PaymentPayload, + paymentRequirements: PaymentRequirements, + ): Promise { + return this.facilitator.settle(paymentPayload, paymentRequirements); + } + + /** + * Gets supported payment kinds + * + * @returns Promise resolving to supported response + */ + getSupported(): Promise { + // Delegate to actual facilitator to get real supported kinds + return Promise.resolve(this.facilitator.getSupported() as SupportedResponse); + } +} + +/** + * Build Stellar payment requirements for testing + * + * @param payTo - The recipient address + * @param amount - The payment amount in smallest units + * @param network - The network identifier (defaults to Stellar Testnet) + * @returns Payment requirements object + */ +function buildStellarPaymentRequirements( + payTo: string, + amount: string, + network: Network = STELLAR_TESTNET_CAIP2, +): PaymentRequirements { + return { + scheme: "exact", + network, + asset: XLM_TESTNET_ASSET, + amount, + payTo, + maxTimeoutSeconds: 3600, + extra: { areFeesSponsored: true }, + }; +} + +/** + * Helper to check if an error is due to insufficient balance + */ +function isInsufficientBalanceError(error: unknown): boolean { + if (error instanceof Error) { + return ( + error.message.includes("resulting balance is not within the allowed range") || + error.message.includes("insufficient balance") || + error.message.includes("Error(Contract, #10)") + ); + } + return false; +} + +describe.skipIf(missingEnvVars)("Stellar Integration Tests", () => { + let clientAddress: string; + let clientSigner: Ed25519Signer; + let facilitatorSigner: Ed25519Signer; + beforeAll(async () => { + clientSigner = createEd25519Signer(CLIENT_PRIVATE_KEY!, STELLAR_TESTNET_CAIP2); + clientAddress = clientSigner.address; + + facilitatorSigner = createEd25519Signer(FACILITATOR_PRIVATE_KEY, STELLAR_TESTNET_CAIP2); + + await ensureAccountsFunded([FACILITATOR_ADDRESS, RESOURCE_SERVER_ADDRESS, clientAddress]); + }); + + describe("x402Client / x402ResourceServer / x402Facilitator - Stellar Flow", () => { + let client: x402Client; + let server: x402ResourceServer; + let facilitatorClient: StellarFacilitatorClient; + + beforeEach(async () => { + const stellarClient = new ExactStellarClient(clientSigner); + client = new x402Client().register(STELLAR_TESTNET_CAIP2, stellarClient); + + const stellarFacilitator = new ExactStellarFacilitator([facilitatorSigner]); + const facilitator = new x402Facilitator().register(STELLAR_TESTNET_CAIP2, stellarFacilitator); + + facilitatorClient = new StellarFacilitatorClient(facilitator); + server = new x402ResourceServer(facilitatorClient); + server.register(STELLAR_TESTNET_CAIP2, new ExactStellarServer()); + await server.initialize(); + }); + + it("server should successfully verify and settle a Stellar payment from a client", async () => { + // Server - builds PaymentRequired response + const accepts = [buildStellarPaymentRequirements(RESOURCE_SERVER_ADDRESS, "1000")]; + const resource = { + url: "https://company.co", + description: "Company Co. resource", + mimeType: "application/json", + }; + const paymentRequired = await server.createPaymentRequiredResponse(accepts, resource); + + // Client - responds with PaymentPayload response + let paymentPayload: PaymentPayload; + try { + paymentPayload = await client.createPaymentPayload(paymentRequired); + } catch (error) { + if (isInsufficientBalanceError(error)) { + throw new Error( + `Insufficient balance on testnet account ${clientAddress}. ` + + `Asset: ${XLM_TESTNET_ASSET}. Ensure the account is funded (e.g. via Friendbot).`, + ); + } + throw error; + } + + expect(paymentPayload).toBeDefined(); + expect(paymentPayload.x402Version).toBe(2); + expect(paymentPayload.accepted.scheme).toBe("exact"); + + // Verify the payload structure + const stellarPayload = paymentPayload.payload as ExactStellarPayloadV2; + expect(stellarPayload.transaction).toBeDefined(); + expect(typeof stellarPayload.transaction).toBe("string"); + expect(stellarPayload.transaction.length).toBeGreaterThan(0); + + // Server - maps payment payload to payment requirements + const accepted = server.findMatchingRequirements(accepts, paymentPayload); + expect(accepted).toBeDefined(); + + const verifyResponse = await server.verifyPayment(paymentPayload, accepted!); + + expect(verifyResponse.isValid).toBe(true); + expect(verifyResponse.payer).toBe(clientAddress); + + // Server does work here + const settleResponse = await server.settlePayment(paymentPayload, accepted!); + expect(settleResponse.success).toBe(true); + expect(settleResponse.network).toBe(STELLAR_TESTNET_CAIP2); + expect(settleResponse.transaction).toBeDefined(); + expect(settleResponse.payer).toBe(clientAddress); + logStellarExpertTxUrl(settleResponse.transaction); + }); + }); + + describe("x402HTTPClient / x402HTTPResourceServer / x402Facilitator - Stellar Flow", () => { + let client: x402HTTPClient; + let httpServer: x402HTTPResourceServer; + + const routes = { + "/api/protected": { + accepts: { + scheme: "exact", + payTo: RESOURCE_SERVER_ADDRESS, + price: { amount: "1000", asset: XLM_TESTNET_ASSET }, + network: STELLAR_TESTNET_CAIP2 as Network, + }, + description: "Access to protected API", + mimeType: "application/json", + }, + }; + + const mockAdapter: HTTPAdapter = { + getHeader: () => { + return undefined; + }, + getMethod: () => "GET", + getPath: () => "/api/protected", + getUrl: () => "https://example.com/api/protected", + getAcceptHeader: () => "application/json", + getUserAgent: () => "TestClient/1.0", + }; + + beforeEach(async () => { + const stellarFacilitator = new ExactStellarFacilitator([facilitatorSigner]); + const facilitator = new x402Facilitator().register(STELLAR_TESTNET_CAIP2, stellarFacilitator); + + const facilitatorClient = new StellarFacilitatorClient(facilitator); + + const stellarClient = new ExactStellarClient(clientSigner); + const paymentClient = new x402Client().register(STELLAR_TESTNET_CAIP2, stellarClient); + client = new x402HTTPClient(paymentClient) as x402HTTPClient; + + // Create resource server and register schemes (composition pattern) + const ResourceServer = new x402ResourceServer(facilitatorClient); + ResourceServer.register(STELLAR_TESTNET_CAIP2, new ExactStellarServer()); + await ResourceServer.initialize(); // Initialize to fetch supported kinds + + httpServer = new x402HTTPResourceServer(ResourceServer, routes); + }); + + it("middleware should successfully verify and settle a Stellar payment from an http client", async () => { + // Middleware creates a PaymentRequired response + const context = { + adapter: mockAdapter, + path: "/api/protected", + method: "GET", + }; + + // No payment made, get PaymentRequired response & header + const httpProcessResult = (await httpServer.processHTTPRequest(context))!; + expect(httpProcessResult.type).toBe("payment-error"); + + const initial402Response = ( + httpProcessResult as { type: "payment-error"; response: HTTPResponseInstructions } + ).response; + + expect(initial402Response).toBeDefined(); + expect(initial402Response.status).toBe(402); + expect(initial402Response.headers).toBeDefined(); + expect(initial402Response.headers["PAYMENT-REQUIRED"]).toBeDefined(); + + // Client responds to PaymentRequired and submits a request with a PaymentPayload + const paymentRequired = client.getPaymentRequiredResponse( + name => initial402Response.headers[name], + initial402Response.body, + ); + let paymentPayload: PaymentPayload; + try { + paymentPayload = await client.createPaymentPayload(paymentRequired); + } catch (error) { + if (isInsufficientBalanceError(error)) { + throw new Error( + `Insufficient balance on testnet account ${clientAddress}. ` + + `Asset: ${XLM_TESTNET_ASSET}. Ensure the account is funded (e.g. via Friendbot).`, + ); + } + throw error; + } + + expect(paymentPayload).toBeDefined(); + expect(paymentPayload.accepted.scheme).toBe("exact"); + + const requestHeaders = await client.encodePaymentSignatureHeader(paymentPayload); + + // Middleware handles PAYMENT-SIGNATURE request + mockAdapter.getHeader = (name: string) => { + if (name === "PAYMENT-SIGNATURE") { + return requestHeaders["PAYMENT-SIGNATURE"]; + } + return undefined; + }; + + const httpProcessResult2 = await httpServer.processHTTPRequest(context); + + // No need to respond, can continue with request + expect(httpProcessResult2.type).toBe("payment-verified"); + const { + paymentPayload: verifiedPaymentPayload, + paymentRequirements: verifiedPaymentRequirements, + } = httpProcessResult2 as { + type: "payment-verified"; + paymentPayload: PaymentPayload; + paymentRequirements: PaymentRequirements; + }; + + expect(verifiedPaymentPayload).toBeDefined(); + expect(verifiedPaymentRequirements).toBeDefined(); + + const settlementResult = await httpServer.processSettlement( + verifiedPaymentPayload, + verifiedPaymentRequirements, + ); + + expect(settlementResult).toBeDefined(); + expect(settlementResult.success).toBe(true); + + if (settlementResult.success) { + expect(settlementResult.headers).toBeDefined(); + expect(settlementResult.headers["PAYMENT-RESPONSE"]).toBeDefined(); + logStellarExpertTxUrl(settlementResult.transaction); + } + }); + }); + + describe("Price Parsing Integration", () => { + let server: x402ResourceServer; + let stellarServer: ExactStellarServer; + + beforeEach(async () => { + const facilitator = new x402Facilitator().register( + STELLAR_TESTNET_CAIP2, + new ExactStellarFacilitator([facilitatorSigner]), + ); + + const facilitatorClient = new StellarFacilitatorClient(facilitator); + server = new x402ResourceServer(facilitatorClient); + + stellarServer = new ExactStellarServer(); + server.register(STELLAR_TESTNET_CAIP2, stellarServer); + await server.initialize(); + }); + + it("should parse Money formats and build payment requirements", async () => { + stellarServer.registerMoneyParser(xlmFallbackParser); + + // Test different Money formats + const testCases = [ + { input: "$1.00", expectedAmount: "10000000" }, + { input: "1.50", expectedAmount: "15000000" }, + { input: 2.5, expectedAmount: "25000000" }, + ]; + + for (const testCase of testCases) { + const requirements = await server.buildPaymentRequirements({ + scheme: "exact", + payTo: RESOURCE_SERVER_ADDRESS, + price: testCase.input, + network: STELLAR_TESTNET_CAIP2 as Network, + }); + + expect(requirements).toHaveLength(1); + expect(requirements[0].amount).toBe(testCase.expectedAmount); + expect(requirements[0].asset).toBe(XLM_TESTNET_ASSET); + } + }); + + it("should handle AssetAmount pass-through", async () => { + const customAsset = { + amount: "50000000", + asset: "CUSTOMTOKENMINT111111111111111111111111111111", + extra: { foo: "bar" }, + }; + + const requirements = await server.buildPaymentRequirements({ + scheme: "exact", + payTo: RESOURCE_SERVER_ADDRESS, + price: customAsset, + network: STELLAR_TESTNET_CAIP2 as Network, + }); + + expect(requirements).toHaveLength(1); + expect(requirements[0].amount).toBe("50000000"); + expect(requirements[0].asset).toBe("CUSTOMTOKENMINT111111111111111111111111111111"); + expect(requirements[0].extra?.foo).toBe("bar"); + }); + + it("should use registerMoneyParser for custom conversion", async () => { + stellarServer + .registerMoneyParser(async (amount, _network) => { + if (amount > 100) { + return { + amount: (amount * 1e7).toString(), + asset: "CUSTOMLARGETOKENMINT111111111111111111111", + extra: { token: "CUSTOM", tier: "large" }, + }; + } + return null; + }) + .registerMoneyParser(xlmFallbackParser); + + // Test large amount - should use custom parser + const largeRequirements = await server.buildPaymentRequirements({ + scheme: "exact", + payTo: RESOURCE_SERVER_ADDRESS, + price: 150, // Large amount + network: STELLAR_TESTNET_CAIP2 as Network, + }); + + expect(largeRequirements[0].amount).toBe((150 * 1e7).toString()); + expect(largeRequirements[0].asset).toBe("CUSTOMLARGETOKENMINT111111111111111111111"); + expect(largeRequirements[0].extra?.token).toBe("CUSTOM"); + expect(largeRequirements[0].extra?.tier).toBe("large"); + + // Test small amount - should use default (XLM) + const smallRequirements = await server.buildPaymentRequirements({ + scheme: "exact", + payTo: RESOURCE_SERVER_ADDRESS, + price: 50, // Small amount + network: STELLAR_TESTNET_CAIP2 as Network, + }); + + expect(smallRequirements[0].amount).toBe("500000000"); // 50 * 1e7 (7 decimals) + expect(smallRequirements[0].asset).toBe(XLM_TESTNET_ASSET); + }); + + it("should support multiple MoneyParser in chain", async () => { + stellarServer + .registerMoneyParser(async amount => { + if (amount > 1000) { + return { + amount: (amount * 1e7).toString(), + asset: "VIPTOKENMINT111111111111111111111111111111", + extra: { tier: "vip" }, + }; + } + return null; + }) + .registerMoneyParser(async amount => { + if (amount > 100) { + return { + amount: (amount * 1e7).toString(), + asset: "PREMIUMTOKENMINT1111111111111111111111111", + extra: { tier: "premium" }, + }; + } + return null; + }) + .registerMoneyParser(xlmFallbackParser); + // < 100 uses XLM fallback + + // VIP tier + const vipReq = await server.buildPaymentRequirements({ + scheme: "exact", + payTo: RESOURCE_SERVER_ADDRESS, + price: 2000, + network: STELLAR_TESTNET_CAIP2 as Network, + }); + expect(vipReq[0].extra?.tier).toBe("vip"); + expect(vipReq[0].asset).toBe("VIPTOKENMINT111111111111111111111111111111"); + + // Premium tier + const premiumReq = await server.buildPaymentRequirements({ + scheme: "exact", + payTo: RESOURCE_SERVER_ADDRESS, + price: 500, + network: STELLAR_TESTNET_CAIP2 as Network, + }); + expect(premiumReq[0].extra?.tier).toBe("premium"); + expect(premiumReq[0].asset).toBe("PREMIUMTOKENMINT1111111111111111111111111"); + + // Standard tier (default) + const standardReq = await server.buildPaymentRequirements({ + scheme: "exact", + payTo: RESOURCE_SERVER_ADDRESS, + price: 50, + network: STELLAR_TESTNET_CAIP2 as Network, + }); + expect(standardReq[0].asset).toBe(XLM_TESTNET_ASSET); + }); + + it("should work with async MoneyParser (e.g., exchange rate lookup)", async () => { + const mockExchangeRate = 0.98; + + stellarServer.registerMoneyParser(async (amount, _network) => { + await new Promise(resolve => setTimeout(resolve, 10)); + + const convertedAmount = amount * mockExchangeRate; + return { + amount: Math.floor(convertedAmount * 1e7).toString(), + asset: XLM_TESTNET_ASSET, + extra: { + exchangeRate: mockExchangeRate, + originalUSD: amount, + }, + }; + }); + + const requirements = await server.buildPaymentRequirements({ + scheme: "exact", + payTo: RESOURCE_SERVER_ADDRESS, + price: 100, + network: STELLAR_TESTNET_CAIP2 as Network, + }); + + // 100 * 0.98 = 98 (XLM, 7 decimals) + expect(requirements[0].amount).toBe("980000000"); + expect(requirements[0].extra?.exchangeRate).toBe(0.98); + expect(requirements[0].extra?.originalUSD).toBe(100); + }); + + it("should avoid floating-point rounding error", async () => { + stellarServer.registerMoneyParser(xlmFallbackParser); + + // Test different Money formats + const testCases = [ + { input: "$4.02", expectedAmount: "40200000" }, + { input: "4.02", expectedAmount: "40200000" }, + { input: "4.02 XLM", expectedAmount: "40200000" }, + { input: "4.02 USD", expectedAmount: "40200000" }, + { input: 4.02, expectedAmount: "40200000" }, + ]; + + for (const testCase of testCases) { + const requirements = await server.buildPaymentRequirements({ + scheme: "exact", + payTo: RESOURCE_SERVER_ADDRESS, + price: testCase.input, + network: STELLAR_TESTNET_CAIP2 as Network, + }); + + expect(requirements).toHaveLength(1); + expect(requirements[0].amount).toBe(testCase.expectedAmount); + expect(requirements[0].asset).toBe(XLM_TESTNET_ASSET); + } + }); + }); +}); diff --git a/typescript/packages/mechanisms/stellar/test/unit/client.test.ts b/typescript/packages/mechanisms/stellar/test/unit/client.test.ts new file mode 100644 index 0000000000..4467c67320 --- /dev/null +++ b/typescript/packages/mechanisms/stellar/test/unit/client.test.ts @@ -0,0 +1,262 @@ +import { nativeToScVal } from "@stellar/stellar-sdk"; +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { STELLAR_PUBNET_CAIP2, STELLAR_TESTNET_CAIP2 } from "../../src/constants"; +import { ExactStellarScheme } from "../../src/exact/client/scheme"; +import * as stellarUtils from "../../src/utils"; +import type { ClientStellarSigner } from "../../src/signer"; +import type { RpcConfig } from "../../src/utils"; +import type { PaymentRequirements } from "@x402/core/types"; + +const { mockAssembledTransactionBuild } = vi.hoisted(() => ({ + mockAssembledTransactionBuild: vi.fn(), +})); + +vi.mock("@stellar/stellar-sdk", async () => { + const actual = + await vi.importActual("@stellar/stellar-sdk"); + return { + ...actual, + contract: { + ...actual.contract, + AssembledTransaction: { + ...actual.contract.AssembledTransaction, + build: mockAssembledTransactionBuild, + }, + }, + }; +}); + +vi.mock("../../src/utils", async () => { + const actual = await vi.importActual("../../src/utils"); + return { + ...actual, + getEstimatedLedgerCloseTimeSeconds: vi.fn().mockResolvedValue(5), + getRpcUrl: vi.fn(), + getRpcClient: vi.fn(), + isStellarNetwork: vi.fn(), + validateStellarAssetAddress: vi.fn(), + validateStellarDestinationAddress: vi.fn(), + }; +}); + +describe("ExactStellarScheme", () => { + const mockSignerAddress = "GBBO4ZDDZTSM2IUKQYBAST3CFHNPFXECGEFTGWTA2WELR2BIWDK57UVE"; + const mockSigner: ClientStellarSigner = { + address: mockSignerAddress, + signAuthEntry: vi.fn().mockResolvedValue({ signedAuthEntry: "signed" }), + }; + + const validPaymentReq: PaymentRequirements = { + scheme: "exact", + network: STELLAR_TESTNET_CAIP2, + amount: "1000000", + payTo: "GCHEI4PQEFJOA27MNZRPQNLGURS6KASW76X5UZCUZIXCOJLKXYCXOR2W", + maxTimeoutSeconds: 60, + asset: "CBIELTK6YBZJU5UP2WWQEUCYKLPU6AUNZ2BQ4WWFEIE3USCIHMXQDAMA", + extra: { areFeesSponsored: true }, + }; + + const mockTransaction = { + simulation: {}, + needsNonInvokerSigningBy: vi.fn(), + signAuthEntries: vi.fn(), + simulate: vi.fn(), + built: { toXDR: vi.fn().mockReturnValue("mock-xdr") }, + }; + + const setupSuccessfulTransaction = () => { + mockTransaction.needsNonInvokerSigningBy.mockReturnValueOnce([mockSignerAddress]); + mockTransaction.needsNonInvokerSigningBy.mockReturnValueOnce([]); + }; + + const mockRpcServer = { + getLatestLedger: vi.fn().mockResolvedValue({ sequence: 100000 }), + }; + + beforeEach(() => { + vi.clearAllMocks(); + vi.mocked(stellarUtils.isStellarNetwork).mockReturnValue(true); + vi.mocked(stellarUtils.validateStellarAssetAddress).mockReturnValue(true); + vi.mocked(stellarUtils.validateStellarDestinationAddress).mockReturnValue(true); + vi.mocked(stellarUtils.getRpcUrl).mockReturnValue("https://soroban-testnet.stellar.org"); + vi.mocked(stellarUtils.getRpcClient).mockReturnValue(mockRpcServer as never); + mockAssembledTransactionBuild.mockResolvedValue(mockTransaction); + }); + + describe("constructor", () => { + it("should create instance with correct scheme and accept optional rpcConfig", () => { + expect(new ExactStellarScheme(mockSigner).scheme).toBe("exact"); + expect( + new ExactStellarScheme(mockSigner, { url: "https://custom-rpc.example.com" }).scheme, + ).toBe("exact"); + }); + }); + + describe("createPaymentPayload", () => { + it.each([ + ["unsupported scheme", { scheme: "invalid" }, "Unsupported scheme: invalid"], + ["unsupported network", { network: "base-sepolia" as never }, "Unsupported Stellar network"], + ["invalid payTo", { payTo: "invalid-address" }, "Invalid Stellar destination address"], + ["invalid asset", { asset: "invalid-asset" }, "Invalid Stellar asset address"], + ["invalid amount (negative)", { amount: "-100" }, "Invalid amount"], + ["invalid amount (zero)", { amount: "0" }, "Invalid amount"], + ["invalid amount (non-integer)", { amount: "100.5" }, "Invalid amount"], + ["invalid amount (empty string)", { amount: "" }, "Invalid amount"], + ["invalid amount (non-numeric)", { amount: "abc" }, "Invalid amount"], + ])("should throw for %s", async (_, overrides, expectedError) => { + const client = new ExactStellarScheme(mockSigner); + if ("network" in overrides && overrides.network) { + vi.mocked(stellarUtils.isStellarNetwork).mockReturnValue(false); + } + if ("payTo" in overrides && overrides.payTo) { + vi.mocked(stellarUtils.validateStellarDestinationAddress).mockReturnValue(false); + } + if ("asset" in overrides && overrides.asset) { + vi.mocked(stellarUtils.validateStellarAssetAddress).mockReturnValue(false); + } + + await expect( + client.createPaymentPayload(2, { ...validPaymentReq, ...overrides } as PaymentRequirements), + ).rejects.toThrow(expectedError); + }); + + it("should work with both TESTNET and PUBNET networks", async () => { + const client = new ExactStellarScheme(mockSigner); + setupSuccessfulTransaction(); + + await expect(client.createPaymentPayload(2, validPaymentReq)).resolves.toBeDefined(); + + const pubnetReq = { + ...validPaymentReq, + network: STELLAR_PUBNET_CAIP2, + } as PaymentRequirements; + vi.mocked(stellarUtils.getRpcUrl).mockReturnValueOnce("https://mainnet-rpc.example.com"); + setupSuccessfulTransaction(); + + await expect(client.createPaymentPayload(2, pubnetReq)).resolves.toBeDefined(); + }); + + it("should accept G, C, or M addresses for payTo", async () => { + const client = new ExactStellarScheme(mockSigner); + const addresses = [ + validPaymentReq.payTo, // G address + "CBIELTK6YBZJU5UP2WWQEUCYKLPU6AUNZ2BQ4WWFEIE3USCIHMXQDAMA", // C address + "MA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KAAAAAAAAAAAAFKBA", // M address + ]; + + for (const address of addresses) { + setupSuccessfulTransaction(); + await expect( + client.createPaymentPayload(2, { ...validPaymentReq, payTo: address }), + ).resolves.toBeDefined(); + } + }); + + it("should use custom RPC URL from rpcConfig", async () => { + const client = new ExactStellarScheme(mockSigner, { url: "https://custom-rpc.example.com" }); + setupSuccessfulTransaction(); + vi.mocked(stellarUtils.getRpcUrl).mockReturnValue("https://custom-rpc.example.com"); + + await client.createPaymentPayload(2, validPaymentReq); + + expect(stellarUtils.getRpcUrl).toHaveBeenCalledWith( + STELLAR_TESTNET_CAIP2, + expect.objectContaining({ url: "https://custom-rpc.example.com" }), + ); + }); + + it("should throw for PUBNET without custom RPC URL", async () => { + const client = new ExactStellarScheme(mockSigner); + const pubnetReq = { + ...validPaymentReq, + network: STELLAR_PUBNET_CAIP2, + } as PaymentRequirements; + vi.mocked(stellarUtils.getRpcUrl).mockImplementation( + (network: string, rpcConfig?: RpcConfig) => { + if (network === STELLAR_PUBNET_CAIP2 && !rpcConfig?.url) { + throw new Error( + "Stellar mainnet requires a non-empty rpcUrl. For a list of RPC providers, see https://developers.stellar.org/docs/data/apis/rpc/providers#publicly-accessible-apis", + ); + } + return "https://soroban-testnet.stellar.org"; + }, + ); + + await expect(client.createPaymentPayload(2, pubnetReq)).rejects.toThrow( + /Stellar mainnet requires a non-empty rpcUrl/, + ); + }); + + it.each([ + ["wrong signer", ["DIFFERENT_ADDRESS"]], + ["multiple signers", [mockSignerAddress, "ANOTHER_ADDRESS"]], + ])("should throw if %s is needed", async (_, signers) => { + const client = new ExactStellarScheme(mockSigner); + mockTransaction.needsNonInvokerSigningBy.mockReturnValueOnce(signers); + await expect(client.createPaymentPayload(2, validPaymentReq)).rejects.toThrow( + /Expected to sign with/, + ); + }); + + it("should throw if signers still missing after signing", async () => { + const client = new ExactStellarScheme(mockSigner); + mockTransaction.needsNonInvokerSigningBy.mockReturnValueOnce([mockSignerAddress]); + mockTransaction.needsNonInvokerSigningBy.mockReturnValueOnce(["STILL_MISSING"]); + + await expect(client.createPaymentPayload(2, validPaymentReq)).rejects.toThrow( + /unexpected signer\(s\) required/, + ); + }); + + it.each([ + [ + "TESTNET", + STELLAR_TESTNET_CAIP2, + "Test SDF Network ; September 2015", + "https://soroban-testnet.stellar.org", + undefined, + ], + [ + "PUBNET", + STELLAR_PUBNET_CAIP2, + "Public Global Stellar Network ; September 2015", + "https://mainnet-rpc.example.com", + { url: "https://mainnet-rpc.example.com" }, + ], + ])( + "should build, sign, and return correct payment for %s", + async (_, network, passphrase, rpcUrl, rpcConfig) => { + const client = new ExactStellarScheme(mockSigner, rpcConfig); + setupSuccessfulTransaction(); + const req = { ...validPaymentReq, network } as PaymentRequirements; + vi.mocked(stellarUtils.getRpcUrl).mockReturnValue(rpcUrl); + + const result = await client.createPaymentPayload(2, req); + + expect(mockAssembledTransactionBuild).toHaveBeenCalledWith({ + contractId: req.asset, + method: "transfer", + args: [ + nativeToScVal(mockSignerAddress, { type: "address" }), + nativeToScVal(req.payTo, { type: "address" }), + nativeToScVal(req.amount, { type: "i128" }), + ], + networkPassphrase: passphrase, + rpcUrl, + parseResultXdr: expect.any(Function), + }); + // Expiration is calculated as currentLedger (100000) + ceil(maxTimeoutSeconds / 5) = 100012 + expect(mockTransaction.signAuthEntries).toHaveBeenCalledWith({ + address: mockSignerAddress, + signAuthEntry: mockSigner.signAuthEntry, + expiration: 100012, + }); + expect(mockTransaction.simulate).toHaveBeenCalled(); + expect(result).toEqual({ + x402Version: 2, + payload: { transaction: "mock-xdr" }, + }); + }, + ); + }); +}); diff --git a/typescript/packages/mechanisms/stellar/test/unit/constants.test.ts b/typescript/packages/mechanisms/stellar/test/unit/constants.test.ts new file mode 100644 index 0000000000..27300f91ef --- /dev/null +++ b/typescript/packages/mechanisms/stellar/test/unit/constants.test.ts @@ -0,0 +1,98 @@ +import { describe, it, expect } from "vitest"; +import { + STELLAR_ASSET_ADDRESS_REGEX, + STELLAR_DESTINATION_ADDRESS_REGEX, +} from "../../src/constants"; + +describe("STELLAR_DESTINATION_ADDRESS_REGEX", () => { + it("should match valid G-accounts (56 characters)", () => { + const validGAccounts = [ + "GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN", + "G" + "A".repeat(27) + "2".repeat(28), + ]; + + validGAccounts.forEach(address => { + expect(STELLAR_DESTINATION_ADDRESS_REGEX.test(address)).toBe(true); + }); + }); + + it("should match valid C-accounts (56 characters)", () => { + const validCAccounts = [ + "CCW67TSZV3SSS2HXMBQ5JFGCKJNXKZM7UQUWUZPUTHXSTZLEO7SJMI75", + "C" + "B".repeat(27) + "2".repeat(28), + ]; + + validCAccounts.forEach(address => { + expect(STELLAR_DESTINATION_ADDRESS_REGEX.test(address)).toBe(true); + }); + }); + + it("should match valid M-accounts (69 characters)", () => { + const validMAccounts = [ + "MA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KAAAAAAAAAAAAFKBA", + "M" + "C".repeat(34) + "3".repeat(34), + ]; + + validMAccounts.forEach(address => { + expect(STELLAR_DESTINATION_ADDRESS_REGEX.test(address)).toBe(true); + }); + }); + + it("should reject invalid Stellar addresses", () => { + const invalidAddresses = [ + "", // Empty string + "G", // Just prefix (too short) + "C", // Just prefix (too short) + "M", // Just prefix (too short) + "G" + "A".repeat(56), // G-account too long (57 chars) + "GA" + "2".repeat(53), // G-account too short (55 chars) + "C" + "B".repeat(56), // C-account too long (57 chars) + "CA" + "2".repeat(53), // C-account too short (55 chars) + "M" + "C".repeat(69), // M-account too long (70 chars) + "MA" + "3".repeat(66), // M-account too short (68 chars) + "XA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN", // Invalid prefix 'X' + "GE5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN", // Invalid second character + "gA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN", // Lowercase prefix + "GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN ", // Space character + "GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN-", // Hyphen character + "0xGA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN", // EVM-style prefix + "ME5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KAAAAAAAAAAAAFKBA", // invalid second character in M-account + ]; + + invalidAddresses.forEach(address => { + expect(STELLAR_DESTINATION_ADDRESS_REGEX.test(address)).toBe(false); + }); + }); +}); + +describe("STELLAR_ASSET_ADDRESS_REGEX", () => { + it("should match valid C-accounts (56 characters)", () => { + const validCAccounts = [ + "CCW67TSZV3SSS2HXMBQ5JFGCKJNXKZM7UQUWUZPUTHXSTZLEO7SJMI75", + "C" + "B".repeat(27) + "2".repeat(28), + ]; + + validCAccounts.forEach(address => { + expect(STELLAR_ASSET_ADDRESS_REGEX.test(address)).toBe(true); + }); + }); + + it("should reject invalid Stellar addresses", () => { + const invalidAddresses = [ + "", // Empty string + "C", // Just prefix (too short) + "C" + "B".repeat(56), // C-account too long (57 chars) + "CA" + "2".repeat(53), // C-account too short (55 chars) + "XA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN", // Invalid prefix 'X' + "CE5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN", // Invalid second character + "cA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN", // Lowercase prefix + "GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN", // G-account + "MA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KAAAAAAAAAAAAFKBA", // M-account + "0xGA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN", // EVM-style prefix + ]; + + invalidAddresses.forEach(address => { + expect(STELLAR_ASSET_ADDRESS_REGEX.test(address)).toBe(false); + }); + }); +}); diff --git a/typescript/packages/mechanisms/stellar/test/unit/facilitator-accessors.test.ts b/typescript/packages/mechanisms/stellar/test/unit/facilitator-accessors.test.ts new file mode 100644 index 0000000000..dbe5356b86 --- /dev/null +++ b/typescript/packages/mechanisms/stellar/test/unit/facilitator-accessors.test.ts @@ -0,0 +1,197 @@ +import { describe, it, expect, vi, beforeEach } from "vitest"; +import { STELLAR_TESTNET_CAIP2 } from "../../src/constants"; +import { ExactStellarScheme } from "../../src/exact/facilitator/scheme"; +import { createEd25519Signer } from "../../src/signer"; +import * as stellarUtils from "../../src/utils"; + +vi.mock("../../src/utils", async () => { + const actual = await vi.importActual("../../src/utils"); + return { + ...actual, + getRpcClient: vi.fn(), + }; +}); + +describe("ExactStellarScheme - getExtra", () => { + const mockRpcClient = { + getLatestLedger: vi.fn(), + }; + let scheme: ExactStellarScheme; + + beforeEach(() => { + vi.clearAllMocks(); + vi.mocked(stellarUtils.getRpcClient).mockReturnValue(mockRpcClient as never); + }); + + it("should return areFeesSponsored", () => { + const signer = createEd25519Signer( + "SDV3OZOPGIO6GQAVI7T6ZJ7NSNFB26JX6QZYCI64TBC7BAZY6FQVAXXK", + STELLAR_TESTNET_CAIP2, + ); + + scheme = new ExactStellarScheme([signer]); + + const result = scheme.getExtra(STELLAR_TESTNET_CAIP2); + + expect(result).toEqual({ areFeesSponsored: true }); + expect(mockRpcClient.getLatestLedger).not.toHaveBeenCalled(); + }); + + it("should return consistent areFeesSponsored on each call", () => { + const signer = createEd25519Signer( + "SDV3OZOPGIO6GQAVI7T6ZJ7NSNFB26JX6QZYCI64TBC7BAZY6FQVAXXK", + STELLAR_TESTNET_CAIP2, + ); + + scheme = new ExactStellarScheme([signer]); + + const result1 = scheme.getExtra(STELLAR_TESTNET_CAIP2); + expect(result1).toEqual({ areFeesSponsored: true }); + + const result2 = scheme.getExtra(STELLAR_TESTNET_CAIP2); + expect(result2).toEqual({ areFeesSponsored: true }); + + expect(mockRpcClient.getLatestLedger).not.toHaveBeenCalled(); + }); + + it("should use custom areFeesSponsored", () => { + const signer = createEd25519Signer( + "SDV3OZOPGIO6GQAVI7T6ZJ7NSNFB26JX6QZYCI64TBC7BAZY6FQVAXXK", + STELLAR_TESTNET_CAIP2, + ); + + scheme = new ExactStellarScheme([signer], { areFeesSponsored: false }); + + const result = scheme.getExtra(STELLAR_TESTNET_CAIP2); + expect(result).toEqual({ areFeesSponsored: false }); + }); + + it("should return consistent areFeesSponsored with multiple signers", () => { + const signer1 = createEd25519Signer( + "SDV3OZOPGIO6GQAVI7T6ZJ7NSNFB26JX6QZYCI64TBC7BAZY6FQVAXXK", + STELLAR_TESTNET_CAIP2, + ); + const signer2 = createEd25519Signer( + "SA6LFVPCYMDQILBRXQ2B2HRPK6DV2TX4FTQQQHWFPSCSY4H2RTCD3XAK", + STELLAR_TESTNET_CAIP2, + ); + + scheme = new ExactStellarScheme([signer1, signer2]); + + // Call getExtra multiple times to ensure consistency + for (let i = 0; i < 10; i++) { + const result = scheme.getExtra(STELLAR_TESTNET_CAIP2); + expect(result).toEqual({ areFeesSponsored: true }); + } + + expect(mockRpcClient.getLatestLedger).not.toHaveBeenCalled(); + }); +}); + +describe("ExactStellarScheme - getSigners", () => { + const mockRpcClient = { + getLatestLedger: vi.fn(), + }; + + beforeEach(() => { + vi.clearAllMocks(); + vi.mocked(stellarUtils.getRpcClient).mockReturnValue(mockRpcClient as never); + }); + + it("should return all signer addresses with a single signer", () => { + const signer = createEd25519Signer( + "SDV3OZOPGIO6GQAVI7T6ZJ7NSNFB26JX6QZYCI64TBC7BAZY6FQVAXXK", + STELLAR_TESTNET_CAIP2, + ); + + const scheme = new ExactStellarScheme([signer]); + const signers = scheme.getSigners(STELLAR_TESTNET_CAIP2); + + expect(signers).toEqual([signer.address]); + }); + + it("should return all signer addresses with multiple signers", () => { + const signer1 = createEd25519Signer( + "SDV3OZOPGIO6GQAVI7T6ZJ7NSNFB26JX6QZYCI64TBC7BAZY6FQVAXXK", + STELLAR_TESTNET_CAIP2, + ); + const signer2 = createEd25519Signer( + "SA6LFVPCYMDQILBRXQ2B2HRPK6DV2TX4FTQQQHWFPSCSY4H2RTCD3XAK", + STELLAR_TESTNET_CAIP2, + ); + + const scheme = new ExactStellarScheme([signer1, signer2]); + const signers = scheme.getSigners(STELLAR_TESTNET_CAIP2); + + expect(signers).toHaveLength(2); + expect(signers).toContain(signer1.address); + expect(signers).toContain(signer2.address); + }); + + it("should include feeBumpSigner address when configured", () => { + const signer = createEd25519Signer( + "SDV3OZOPGIO6GQAVI7T6ZJ7NSNFB26JX6QZYCI64TBC7BAZY6FQVAXXK", + STELLAR_TESTNET_CAIP2, + ); + const feeBumpSigner = createEd25519Signer( + "SA6LFVPCYMDQILBRXQ2B2HRPK6DV2TX4FTQQQHWFPSCSY4H2RTCD3XAK", + STELLAR_TESTNET_CAIP2, + ); + + const scheme = new ExactStellarScheme([signer], { feeBumpSigner }); + const signers = scheme.getSigners(STELLAR_TESTNET_CAIP2); + + expect(signers).toHaveLength(2); + expect(signers).toContain(signer.address); + expect(signers).toContain(feeBumpSigner.address); + }); + + it("should not duplicate feeBumpSigner if it is also a regular signer", () => { + const signer = createEd25519Signer( + "SDV3OZOPGIO6GQAVI7T6ZJ7NSNFB26JX6QZYCI64TBC7BAZY6FQVAXXK", + STELLAR_TESTNET_CAIP2, + ); + + const scheme = new ExactStellarScheme([signer], { feeBumpSigner: signer }); + const signers = scheme.getSigners(STELLAR_TESTNET_CAIP2); + + expect(signers).toHaveLength(1); + expect(signers).toEqual([signer.address]); + }); + + it("should include feeBumpSigner with multiple regular signers", () => { + const signer1 = createEd25519Signer( + "SDV3OZOPGIO6GQAVI7T6ZJ7NSNFB26JX6QZYCI64TBC7BAZY6FQVAXXK", + STELLAR_TESTNET_CAIP2, + ); + const signer2 = createEd25519Signer( + "SA6LFVPCYMDQILBRXQ2B2HRPK6DV2TX4FTQQQHWFPSCSY4H2RTCD3XAK", + STELLAR_TESTNET_CAIP2, + ); + const feeBumpSigner = createEd25519Signer( + "SACGSSH2Y7Q6P6BK3BBKGH5Z2RDSQQGD2XHOCDYQN7N6BU37HE2OLKMD", + STELLAR_TESTNET_CAIP2, + ); + + const scheme = new ExactStellarScheme([signer1, signer2], { feeBumpSigner }); + const signers = scheme.getSigners(STELLAR_TESTNET_CAIP2); + + expect(signers).toHaveLength(3); + expect(signers).toContain(signer1.address); + expect(signers).toContain(signer2.address); + expect(signers).toContain(feeBumpSigner.address); + }); + + it("should not include feeBumpSigner when not configured", () => { + const signer = createEd25519Signer( + "SDV3OZOPGIO6GQAVI7T6ZJ7NSNFB26JX6QZYCI64TBC7BAZY6FQVAXXK", + STELLAR_TESTNET_CAIP2, + ); + + const scheme = new ExactStellarScheme([signer]); + const signers = scheme.getSigners(STELLAR_TESTNET_CAIP2); + + expect(signers).toHaveLength(1); + expect(signers).toEqual([signer.address]); + }); +}); diff --git a/typescript/packages/mechanisms/stellar/test/unit/facilitator-settle.test.ts b/typescript/packages/mechanisms/stellar/test/unit/facilitator-settle.test.ts new file mode 100644 index 0000000000..804d81d72c --- /dev/null +++ b/typescript/packages/mechanisms/stellar/test/unit/facilitator-settle.test.ts @@ -0,0 +1,546 @@ +import { Buffer } from "buffer"; +import { + Networks as StellarNetworks, + rpc, + Account, + SorobanDataBuilder, + TransactionBuilder, + FeeBumpTransaction, +} from "@stellar/stellar-sdk"; +import { Api } from "@stellar/stellar-sdk/rpc"; +import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import { STELLAR_TESTNET_CAIP2 } from "../../src/constants"; +import { ExactStellarScheme } from "../../src/exact/facilitator/scheme"; +import { createEd25519Signer } from "../../src/signer"; +import * as stellarUtils from "../../src/utils"; +import type { FacilitatorStellarSigner } from "../../src/signer"; +import type { PaymentPayload, PaymentRequirements } from "@x402/core/types"; + +vi.mock("../../src/utils", async () => { + const actual = await vi.importActual("../../src/utils"); + return { + ...actual, + getNetworkPassphrase: vi.fn(), + getRpcUrl: vi.fn(), + getRpcClient: vi.fn(), + }; +}); + +describe("ExactStellarScheme - Settle (randomly using 1-2 facilitator signers)", () => { + const CLIENT_PUBLIC = "GBBO4ZDDZTSM2IUKQYBAST3CFHNPFXECGEFTGWTA2WELR2BIWDK57UVE"; + const FACILITATOR_SECRET_1 = "SCKB3ECHCPVM4HJPNCQWTQWJJ5XRL6UNKLTTCIH4B7TB22NKJ5GUFMIV"; + const FACILITATOR_PUBLIC_1 = "GCQAXB2D77Y4C66CTGVH25H2RMUKMQJGOWUPK7UXGG5MAQBONUEKFQ4P"; + const FACILITATOR_SECRET_2 = "SA6LFVPCYMDQILBRXQ2B2HRPK6DV2TX4FTQQQHWFPSCSY4H2RTCD3XAK"; + const FACILITATOR_PUBLIC_2 = "GDEDUVINLPX4AN7HYK3MZGY6YDQSNVJT657CVWHEM3QMAH4QHSGLIHVI"; + // The transaction's recipient (different from facilitator signer address) + const TRANSACTION_RECIPIENT = "GCHEI4PQEFJOA27MNZRPQNLGURS6KASW76X5UZCUZIXCOJLKXYCXOR2W"; + const ASSET = "CBIELTK6YBZJU5UP2WWQEUCYKLPU6AUNZ2BQ4WWFEIE3USCIHMXQDAMA"; + + const validRequirements: PaymentRequirements = { + scheme: "exact", + network: STELLAR_TESTNET_CAIP2, + amount: "10000", // Extracted from transaction XDR + payTo: TRANSACTION_RECIPIENT, // Must match transaction's recipient + maxTimeoutSeconds: 60, + asset: ASSET, + extra: { + areFeesSponsored: true, + }, + }; + + const facilitatorSigner1 = createEd25519Signer(FACILITATOR_SECRET_1, STELLAR_TESTNET_CAIP2); + const facilitatorSigner2 = createEd25519Signer(FACILITATOR_SECRET_2, STELLAR_TESTNET_CAIP2); + + let facilitatorSigners: FacilitatorStellarSigner[]; + let validPayload: PaymentPayload; + let facilitator: ExactStellarScheme; + let mockSignedTxXdr: string; + let mockServer: rpc.Server; + + // Use a real transaction XDR from shared test (base64 encoded JSON with tx field) + const signedTxJson = + "eyJtZXRob2QiOiJ0cmFuc2ZlciIsInR4IjoiQUFBQUFnQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQURsM0lBQUFBQUFBQUFBUUFBQUFFQUFBQUFBQUFBQUFBQUFBQnBGcEdGQUFBQUFBQUFBQUVBQUFBQUFBQUFHQUFBQUFBQUFBQUJVRVhOWHNCeW1uYVAxYTBDVUZoUzMwOENqYzZERGxyRklnbTZTRWc3THdFQUFBQUlkSEpoYm5ObVpYSUFBQUFEQUFBQUVnQUFBQUFBQUFBQVF1NWtZOHprelNLS2hnSUpUMklwMnZMY2dqRUxNMXBnMVlpNDZDaXcxZDhBQUFBU0FBQUFBQUFBQUFDT1JISHdJVkxnYSt4dVl2ZzFacVJsNVFKVy82L2FaRlRLTGljbGFyNEZkd0FBQUFvQUFBQUFBQUFBQUFBQUFBQUFBQ2NRQUFBQUFRQUFBQUVBQUFBQUFBQUFBRUx1WkdQTTVNMGlpb1lDQ1U5aUtkcnkzSUl4Q3pOYVlOV0l1T2dvc05YZlh4amsrOHlZOGhnQUk4ck9BQUFBRUFBQUFBRUFBQUFCQUFBQUVRQUFBQUVBQUFBQ0FBQUFEd0FBQUFwd2RXSnNhV05mYTJWNUFBQUFBQUFOQUFBQUlFTHVaR1BNNU0waWlvWUNDVTlpS2RyeTNJSXhDek5hWU5XSXVPZ29zTlhmQUFBQUR3QUFBQWx6YVdkdVlYUjFjbVVBQUFBQUFBQU5BQUFBUUl2bjJjU3VLbFl5TU96T0pTWnkwc0VaN3dkN1QwYmdSQ0ZxZjg1M3VXQXFVcjE1ZUpycXNqVjROUVpTQW05WXNWbHZEcEUrSFRLc3pUQUVBaTJBRkFnQUFBQUFBQUFBQVZCRnpWN0FjcHAyajlXdEFsQllVdDlQQW8zT2d3NWF4U0lKdWtoSU95OEJBQUFBQ0hSeVlXNXpabVZ5QUFBQUF3QUFBQklBQUFBQUFBQUFBRUx1WkdQTTVNMGlpb1lDQ1U5aUtkcnkzSUl4Q3pOYVlOV0l1T2dvc05YZkFBQUFFZ0FBQUFBQUFBQUFqa1J4OENGUzRHdnNibUw0Tldha1plVUNWdit2Mm1SVXlpNG5KV3ErQlhjQUFBQUtBQUFBQUFBQUFBQUFBQUFBQUFBbkVBQUFBQUFBQUFBQkFBQUFBQUFBQUFJQUFBQUFBQUFBQUVMdVpHUE01TTBpaW9ZQ0NVOWlLZHJ5M0lJeEN6TmFZTldJdU9nb3NOWGZBQUFBQmdBQUFBRlFSYzFld0hLYWRvL1ZyUUpRV0ZMZlR3S056b01PV3NVaUNicElTRHN2QVFBQUFCUUFBQUFCQUFBQUF3QUFBQUVBQUFBQVF1NWtZOHprelNLS2hnSUpUMklwMnZMY2dqRUxNMXBnMVlpNDZDaXcxZDhBQUFBQlZWTkVRd0FBQUFCQ1BuMEY4dXl2dit3Wkt5RmFQeHZwYXUyNDJPY0NWS3ZqUVQ0Q0I5NVdzZ0FBQUFFQUFBQUFqa1J4OENGUzRHdnNibUw0Tldha1plVUNWdit2Mm1SVXlpNG5KV3ErQlhjQUFBQUJWVk5FUXdBQUFBQkNQbjBGOHV5dnYrd1pLeUZhUHh2cGF1MjQyT2NDVkt2alFUNENCOTVXc2dBQUFBWUFBQUFBQUFBQUFFTHVaR1BNNU0waWlvWUNDVTlpS2RyeTNJSXhDek5hWU5XSXVPZ29zTlhmQUFBQUZWOFk1UHZNbVBJWUFBQUFBQUFMNVRFQUFBRjRBQUFCTkFBQUFBQUFBNWNPQUFBQUFBPT0iLCJzaW11bGF0aW9uUmVzdWx0Ijp7ImF1dGgiOlsiQUFBQUFRQUFBQUFBQUFBQVF1NWtZOHprelNLS2hnSUpUMklwMnZMY2dqRUxNMXBnMVlpNDZDaXcxZDlmR09UN3pKanlHQUFBQUFBQUFBQUJBQUFBQUFBQUFBRlFSYzFld0hLYWRvL1ZyUUpRV0ZMZlR3S056b01PV3NVaUNicElTRHN2QVFBQUFBaDBjbUZ1YzJabGNnQUFBQU1BQUFBU0FBQUFBQUFBQUFCQzdtUmp6T1ROSW9xR0FnbFBZaW5hOHR5Q01Rc3pXbURWaUxqb0tMRFYzd0FBQUJJQUFBQUFBQUFBQUk1RWNmQWhVdUJyN0c1aStEVm1wR1hsQWxiL3I5cGtWTW91SnlWcXZnVjNBQUFBQ2dBQUFBQUFBQUFBQUFBQUFBQUFKeEFBQUFBQSJdLCJyZXR2YWwiOiJBQUFBQVE9PSJ9LCJzaW11bGF0aW9uVHJhbnNhY3Rpb25EYXRhIjoiQUFBQUFBQUFBQUlBQUFBQUFBQUFBRUx1WkdQTTVNMGlpb1lDQ1U5aUtkcnkzSUl4Q3pOYVlOV0l1T2dvc05YZkFBQUFCZ0FBQUFGUVJjMWV3SEthZG8vVnJRSlFXRkxmVHdLTnpvTU9Xc1VpQ2JwSVNEc3ZBUUFBQUJRQUFBQUJBQUFBQXdBQUFBRUFBQUFBUXU1a1k4emt6U0tLaGdJSlQySXAydkxjZ2pFTE0xcGcxWWk0NkNpdzFkOEFBQUFCVlZORVF3QUFBQUJDUG4wRjh1eXZ2K3daS3lGYVB4dnBhdTI0Mk9jQ1ZLdmpRVDRDQjk1V3NnQUFBQUVBQUFBQWprUng4Q0ZTNEd2c2JtTDROV2FrWmVVQ1Z2K3YybVJVeWk0bkpXcStCWGNBQUFBQlZWTkVRd0FBQUFCQ1BuMEY4dXl2dit3Wkt5RmFQeHZwYXUyNDJPY0NWS3ZqUVQ0Q0I5NVdzZ0FBQUFZQUFBQUFBQUFBQUVMdVpHUE01TTBpaW9ZQ0NVOWlLZHJ5M0lJeEN6TmFZTldJdU9nb3NOWGZBQUFBRlY4WTVQdk1tUElZQUFBQUFBQUw1VEVBQUFGNEFBQUJOQUFBQUFBQUE1Y08ifQ=="; + const { tx: mockTransactionXDR } = JSON.parse( + Buffer.from(signedTxJson, "base64").toString("utf8"), + ); + + beforeAll(async () => { + vi.mocked(stellarUtils.getNetworkPassphrase).mockReturnValue(StellarNetworks.TESTNET); + vi.mocked(stellarUtils.getRpcUrl).mockReturnValue("https://soroban-testnet.stellar.org"); + + // Build full V2 PaymentPayload with mocked transaction + validPayload = { + x402Version: 2, + resource: { + url: "https://example.com/resource", + description: "Test payment", + mimeType: "application/json", + }, + accepted: validRequirements, + payload: { + transaction: mockTransactionXDR, + }, + }; + + // Use the same XDR for signed transaction + mockSignedTxXdr = mockTransactionXDR; + }); + + beforeEach(() => { + // Random selection for 1-2 facilitators + const useTwoFacilitators = Math.random() > 0.5; + facilitatorSigners = useTwoFacilitators + ? [facilitatorSigner1, facilitatorSigner2] + : [facilitatorSigner1]; + facilitator = new ExactStellarScheme(facilitatorSigners); + + // Create a fresh mock server for each test + mockServer = { + getAccount: vi.fn().mockImplementation(async addr => new Account(addr, "100")), + sendTransaction: vi.fn().mockResolvedValue({ + status: "PENDING", + hash: "test-tx-hash-123", + } as Api.SendTransactionResponse), + getTransaction: vi + .fn() + .mockResolvedValue({ status: "SUCCESS" } as Api.GetTransactionResponse), + getLatestLedger: vi.fn().mockResolvedValue({ sequence: 100000 }), + simulateTransaction: vi.fn().mockResolvedValue({ + id: "test", + latestLedger: 123, + events: [], + _parsed: true, + transactionData: new SorobanDataBuilder(), + minResourceFee: "100", + cost: { cpuInsns: "0", memBytes: "0" }, + results: [], + } as Api.SimulateTransactionSuccessResponse), + } as unknown as rpc.Server; + + vi.clearAllMocks(); + + vi.mocked(stellarUtils.getRpcClient).mockReturnValue(mockServer); + vi.mocked(stellarUtils.getNetworkPassphrase).mockReturnValue(StellarNetworks.TESTNET); + vi.mocked(stellarUtils.getRpcUrl).mockReturnValue("https://soroban-testnet.stellar.org"); + + // Mock verify to pass for settle tests (verify is tested separately) + // The expiration check may reject the test transaction, so we mock verify for settle tests + // Note: This is reset in tests that need to test actual verify behavior + vi.spyOn(facilitator, "verify").mockImplementation(async () => ({ + isValid: true, + payer: CLIENT_PUBLIC, + })); + + // Mock signTransaction for all signers to return the mock signed XDR + vi.spyOn(facilitatorSigner1, "signTransaction").mockResolvedValue({ + signedTxXdr: mockSignedTxXdr, + error: undefined, + }); + vi.spyOn(facilitatorSigner2, "signTransaction").mockResolvedValue({ + signedTxXdr: mockSignedTxXdr, + error: undefined, + }); + }); + + describe("settlement failures", () => { + it("should return error when verify fails", async () => { + vi.spyOn(facilitator, "verify").mockRestore(); + // Use requirements with wrong amount to make verify fail + const invalidRequirements = { + ...validRequirements, + amount: "9999", // Wrong amount (transaction has 10000) + }; + + const result = await facilitator.settle(validPayload, invalidRequirements); + + expect(result.success).toBe(false); + expect(result.errorReason).toBe("invalid_exact_stellar_payload_wrong_amount"); + expect(result.payer).toBe(CLIENT_PUBLIC); + expect(result.network).toBe(STELLAR_TESTNET_CAIP2); + expect(result.transaction).toBe(""); + expect(mockServer.sendTransaction).not.toHaveBeenCalled(); + }); + + it("should return error when signing fails", async () => { + vi.spyOn(facilitatorSigner1, "signTransaction").mockResolvedValue({ + signedTxXdr: "", + error: { code: 1, message: "Signing failed" }, + }); + + const result = await facilitator.settle(validPayload, validRequirements); + + expect(result).toEqual({ + success: false, + errorReason: "settle_exact_stellar_transaction_signing_failed", + payer: CLIENT_PUBLIC, + network: STELLAR_TESTNET_CAIP2, + transaction: "", + }); + }); + + it("should return error when transaction submission returns non-PENDING status", async () => { + vi.mocked(mockServer.sendTransaction).mockResolvedValue({ + status: "TRY_AGAIN_LATER", + hash: "", + } as Api.SendTransactionResponse); + + const result = await facilitator.settle(validPayload, validRequirements); + + expect(result).toEqual({ + success: false, + errorReason: "settle_exact_stellar_transaction_submission_failed", + payer: CLIENT_PUBLIC, + network: STELLAR_TESTNET_CAIP2, + transaction: "", + }); + }); + + it("should return error when transaction confirmation fails", async () => { + vi.mocked(mockServer.sendTransaction).mockResolvedValue({ + status: "PENDING", + hash: "test-tx-hash-123", + } as Api.SendTransactionResponse); + vi.mocked(mockServer.getTransaction).mockResolvedValue({ + status: "FAILED", + } as Api.GetTransactionResponse); + + const result = await facilitator.settle(validPayload, validRequirements); + + expect(result).toEqual({ + success: false, + errorReason: "settle_exact_stellar_transaction_failed", + payer: CLIENT_PUBLIC, + network: STELLAR_TESTNET_CAIP2, + transaction: "test-tx-hash-123", + }); + }); + + it("should return error when transaction confirmation times out", async () => { + vi.mocked(mockServer.sendTransaction).mockResolvedValue({ + status: "PENDING", + hash: "test-tx-hash-123", + } as Api.SendTransactionResponse); + vi.mocked(mockServer.getTransaction).mockResolvedValue({ + status: "NOT_FOUND", + } as Api.GetTransactionResponse); + + const result = await facilitator.settle(validPayload, { + ...validRequirements, + maxTimeoutSeconds: 1, + }); + + expect(result).toEqual({ + success: false, + errorReason: "settle_exact_stellar_transaction_failed", + payer: CLIENT_PUBLIC, + network: STELLAR_TESTNET_CAIP2, + transaction: "test-tx-hash-123", + }); + }); + + it("should handle unexpected errors during account fetch", async () => { + vi.mocked(mockServer.getAccount).mockRejectedValue(new Error("Network error")); + + const result = await facilitator.settle(validPayload, validRequirements); + + expect(result).toEqual({ + success: false, + errorReason: "unexpected_settle_error", + payer: CLIENT_PUBLIC, + network: STELLAR_TESTNET_CAIP2, + transaction: "", + }); + }); + }); + + describe("successful settlement", () => { + it("should successfully settle valid payment", async () => { + vi.mocked(mockServer.sendTransaction).mockResolvedValue({ + status: "PENDING", + hash: "test-tx-hash-123", + } as Api.SendTransactionResponse); + vi.mocked(mockServer.getTransaction).mockResolvedValue({ + status: "SUCCESS", + } as Api.GetTransactionResponse); + + const result = await facilitator.settle(validPayload, validRequirements); + + expect(result).toEqual({ + success: true, + transaction: "test-tx-hash-123", + payer: CLIENT_PUBLIC, + network: STELLAR_TESTNET_CAIP2, + }); + + // Verify getAccount was called with one of the facilitator addresses + expect(mockServer.getAccount).toHaveBeenCalledTimes(1); + const calledWithAddress = vi.mocked(mockServer.getAccount).mock.calls[0][0]; + expect(facilitator.signingAddresses).toContain(calledWithAddress); + expect(mockServer.sendTransaction).toHaveBeenCalled(); + expect(mockServer.getTransaction).toHaveBeenCalledWith("test-tx-hash-123"); + }); + + it("should poll until transaction succeeds", async () => { + let callCount = 0; + vi.mocked(mockServer.getTransaction).mockImplementation(async () => { + callCount++; + if (callCount < 3) { + return { status: "NOT_FOUND" } as Api.GetTransactionResponse; + } + return { status: "SUCCESS" } as Api.GetTransactionResponse; + }); + + const result = await facilitator.settle(validPayload, validRequirements); + + expect(result.success).toBe(true); + expect(mockServer.getTransaction).toHaveBeenCalledTimes(3); + }); + + it("should continue polling on errors", async () => { + let callCount = 0; + vi.mocked(mockServer.getTransaction).mockImplementation(async () => { + callCount++; + if (callCount === 1) { + throw new Error("Temporary network error"); + } + return { status: "SUCCESS" } as Api.GetTransactionResponse; + }); + + const result = await facilitator.settle(validPayload, validRequirements); + + expect(result.success).toBe(true); + expect(mockServer.getTransaction).toHaveBeenCalledTimes(2); + }); + }); + + describe("multi-signer tests", () => { + const multiSigners: FacilitatorStellarSigner[] = [facilitatorSigner1, facilitatorSigner2]; + + it("should use custom selectSigner callback", async () => { + const customFacilitator = new ExactStellarScheme(multiSigners, { + selectSigner: addrs => addrs[1], + }); + + vi.spyOn(customFacilitator, "verify").mockResolvedValue({ + isValid: true, + payer: CLIENT_PUBLIC, + }); + + const result = await customFacilitator.settle(validPayload, validRequirements); + + expect(result.success).toBe(true); + expect(mockServer.getAccount).toHaveBeenCalledWith(FACILITATOR_PUBLIC_2); + expect(facilitatorSigner2.signTransaction).toHaveBeenCalled(); + expect(facilitatorSigner1.signTransaction).not.toHaveBeenCalled(); + }); + + it("should use round-robin by default with multiple signers", async () => { + const roundRobinFacilitator = new ExactStellarScheme(multiSigners); + + vi.spyOn(roundRobinFacilitator, "verify").mockResolvedValue({ + isValid: true, + payer: CLIENT_PUBLIC, + }); + + const calledAddresses: string[] = []; + + for (let i = 0; i < 4; i++) { + await roundRobinFacilitator.settle(validPayload, validRequirements); + const callArgs = vi.mocked(mockServer.getAccount).mock.calls[i]; + calledAddresses.push(callArgs[0]); + } + + // Verify round-robin pattern: [addr1, addr2, addr1, addr2] + expect(calledAddresses).toEqual([ + FACILITATOR_PUBLIC_1, + FACILITATOR_PUBLIC_2, + FACILITATOR_PUBLIC_1, + FACILITATOR_PUBLIC_2, + ]); + }); + }); +}); + +describe("ExactStellarScheme - Settle with feeBumpSigner", () => { + const CLIENT_PUBLIC = "GBBO4ZDDZTSM2IUKQYBAST3CFHNPFXECGEFTGWTA2WELR2BIWDK57UVE"; + const FACILITATOR_SECRET_1 = "SCKB3ECHCPVM4HJPNCQWTQWJJ5XRL6UNKLTTCIH4B7TB22NKJ5GUFMIV"; + const FACILITATOR_SECRET_2 = "SA6LFVPCYMDQILBRXQ2B2HRPK6DV2TX4FTQQQHWFPSCSY4H2RTCD3XAK"; + // A separate keypair for the fee bump signer + const FEE_BUMP_SECRET = "SACGSSH2Y7Q6P6BK3BBKGH5Z2RDSQQGD2XHOCDYQN7N6BU37HE2OLKMD"; + const FEE_BUMP_PUBLIC = "GDBNQWJ67SGPPEZ2GALX5W5YGT2NACBJQDK64T6WXDGJNAA4IPWIULMV"; + const TRANSACTION_RECIPIENT = "GCHEI4PQEFJOA27MNZRPQNLGURS6KASW76X5UZCUZIXCOJLKXYCXOR2W"; + const ASSET = "CBIELTK6YBZJU5UP2WWQEUCYKLPU6AUNZ2BQ4WWFEIE3USCIHMXQDAMA"; + + const validRequirements: PaymentRequirements = { + scheme: "exact", + network: STELLAR_TESTNET_CAIP2, + amount: "10000", + payTo: TRANSACTION_RECIPIENT, + maxTimeoutSeconds: 60, + asset: ASSET, + extra: { + areFeesSponsored: true, + }, + }; + + const facilitatorSigner1 = createEd25519Signer(FACILITATOR_SECRET_1, STELLAR_TESTNET_CAIP2); + const facilitatorSigner2 = createEd25519Signer(FACILITATOR_SECRET_2, STELLAR_TESTNET_CAIP2); + const feeBumpSigner = createEd25519Signer(FEE_BUMP_SECRET, STELLAR_TESTNET_CAIP2); + + let validPayload: PaymentPayload; + let mockSignedTxXdr: string; + let mockServer: rpc.Server; + + const signedTxJson = + "eyJtZXRob2QiOiJ0cmFuc2ZlciIsInR4IjoiQUFBQUFnQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQURsM0lBQUFBQUFBQUFBUUFBQUFFQUFBQUFBQUFBQUFBQUFBQnBGcEdGQUFBQUFBQUFBQUVBQUFBQUFBQUFHQUFBQUFBQUFBQUJVRVhOWHNCeW1uYVAxYTBDVUZoUzMwOENqYzZERGxyRklnbTZTRWc3THdFQUFBQUlkSEpoYm5ObVpYSUFBQUFEQUFBQUVnQUFBQUFBQUFBQVF1NWtZOHprelNLS2hnSUpUMklwMnZMY2dqRUxNMXBnMVlpNDZDaXcxZDhBQUFBU0FBQUFBQUFBQUFDT1JISHdJVkxnYSt4dVl2ZzFacVJsNVFKVy82L2FaRlRLTGljbGFyNEZkd0FBQUFvQUFBQUFBQUFBQUFBQUFBQUFBQ2NRQUFBQUFRQUFBQUVBQUFBQUFBQUFBRUx1WkdQTTVNMGlpb1lDQ1U5aUtkcnkzSUl4Q3pOYVlOV0l1T2dvc05YZlh4amsrOHlZOGhnQUk4ck9BQUFBRUFBQUFBRUFBQUFCQUFBQUVRQUFBQUVBQUFBQ0FBQUFEd0FBQUFwd2RXSnNhV05mYTJWNUFBQUFBQUFOQUFBQUlFTHVaR1BNNU0waWlvWUNDVTlpS2RyeTNJSXhDek5hWU5XSXVPZ29zTlhmQUFBQUR3QUFBQWx6YVdkdVlYUjFjbVVBQUFBQUFBQU5BQUFBUUl2bjJjU3VLbFl5TU96T0pTWnkwc0VaN3dkN1QwYmdSQ0ZxZjg1M3VXQXFVcjE1ZUpycXNqVjROUVpTQW05WXNWbHZEcEUrSFRLc3pUQUVBaTJBRkFnQUFBQUFBQUFBQVZCRnpWN0FjcHAyajlXdEFsQllVdDlQQW8zT2d3NWF4U0lKdWtoSU95OEJBQUFBQ0hSeVlXNXpabVZ5QUFBQUF3QUFBQklBQUFBQUFBQUFBRUx1WkdQTTVNMGlpb1lDQ1U5aUtkcnkzSUl4Q3pOYVlOV0l1T2dvc05YZkFBQUFFZ0FBQUFBQUFBQUFqa1J4OENGUzRHdnNibUw0Tldha1plVUNWdit2Mm1SVXlpNG5KV3ErQlhjQUFBQUtBQUFBQUFBQUFBQUFBQUFBQUFBbkVBQUFBQUFBQUFBQkFBQUFBQUFBQUFJQUFBQUFBQUFBQUVMdVpHUE01TTBpaW9ZQ0NVOWlLZHJ5M0lJeEN6TmFZTldJdU9nb3NOWGZBQUFBQmdBQUFBRlFSYzFld0hLYWRvL1ZyUUpRV0ZMZlR3S056b01PV3NVaUNicElTRHN2QVFBQUFCUUFBQUFCQUFBQUF3QUFBQUVBQUFBQVF1NWtZOHprelNLS2hnSUpUMklwMnZMY2dqRUxNMXBnMVlpNDZDaXcxZDhBQUFBQlZWTkVRd0FBQUFCQ1BuMEY4dXl2dit3Wkt5RmFQeHZwYXUyNDJPY0NWS3ZqUVQ0Q0I5NVdzZ0FBQUFFQUFBQUFqa1J4OENGUzRHdnNibUw0Tldha1plVUNWdit2Mm1SVXlpNG5KV3ErQlhjQUFBQUJWVk5FUXdBQUFBQkNQbjBGOHV5dnYrd1pLeUZhUHh2cGF1MjQyT2NDVkt2alFUNENCOTVXc2dBQUFBWUFBQUFBQUFBQUFFTHVaR1BNNU0waWlvWUNDVTlpS2RyeTNJSXhDek5hWU5XSXVPZ29zTlhmQUFBQUZWOFk1UHZNbVBJWUFBQUFBQUFMNVRFQUFBRjRBQUFCTkFBQUFBQUFBNWNPQUFBQUFBPT0iLCJzaW11bGF0aW9uUmVzdWx0Ijp7ImF1dGgiOlsiQUFBQUFRQUFBQUFBQUFBQVF1NWtZOHprelNLS2hnSUpUMklwMnZMY2dqRUxNMXBnMVlpNDZDaXcxZDlmR09UN3pKanlHQUFBQUFBQUFBQUJBQUFBQUFBQUFBRlFSYzFld0hLYWRvL1ZyUUpRV0ZMZlR3S056b01PV3NVaUNicElTRHN2QVFBQUFBaDBjbUZ1YzJabGNnQUFBQU1BQUFBU0FBQUFBQUFBQUFCQzdtUmp6T1ROSW9xR0FnbFBZaW5hOHR5Q01Rc3pXbURWaUxqb0tMRFYzd0FBQUJJQUFBQUFBQUFBQUk1RWNmQWhVdUJyN0c1aStEVm1wR1hsQWxiL3I5cGtWTW91SnlWcXZnVjNBQUFBQ2dBQUFBQUFBQUFBQUFBQUFBQUFKeEFBQUFBQSJdLCJyZXR2YWwiOiJBQUFBQVE9PSJ9LCJzaW11bGF0aW9uVHJhbnNhY3Rpb25EYXRhIjoiQUFBQUFBQUFBQUlBQUFBQUFBQUFBRUx1WkdQTTVNMGlpb1lDQ1U5aUtkcnkzSUl4Q3pOYVlOV0l1T2dvc05YZkFBQUFCZ0FBQUFGUVJjMWV3SEthZG8vVnJRSlFXRkxmVHdLTnpvTU9Xc1VpQ2JwSVNEc3ZBUUFBQUJRQUFBQUJBQUFBQXdBQUFBRUFBQUFBUXU1a1k4emt6U0tLaGdJSlQySXAydkxjZ2pFTE0xcGcxWWk0NkNpdzFkOEFBQUFCVlZORVF3QUFBQUJDUG4wRjh1eXZ2K3daS3lGYVB4dnBhdTI0Mk9jQ1ZLdmpRVDRDQjk1V3NnQUFBQUVBQUFBQWprUng4Q0ZTNEd2c2JtTDROV2FrWmVVQ1Z2K3YybVJVeWk0bkpXcStCWGNBQUFBQlZWTkVRd0FBQUFCQ1BuMEY4dXl2dit3Wkt5RmFQeHZwYXUyNDJPY0NWS3ZqUVQ0Q0I5NVdzZ0FBQUFZQUFBQUFBQUFBQUVMdVpHUE01TTBpaW9ZQ0NVOWlLZHJ5M0lJeEN6TmFZTldJdU9nb3NOWGZBQUFBRlY4WTVQdk1tUElZQUFBQUFBQUw1VEVBQUFGNEFBQUJOQUFBQUFBQUE1Y08ifQ=="; + const { tx: mockTransactionXDR } = JSON.parse( + Buffer.from(signedTxJson, "base64").toString("utf8"), + ); + + beforeAll(async () => { + vi.mocked(stellarUtils.getNetworkPassphrase).mockReturnValue(StellarNetworks.TESTNET); + vi.mocked(stellarUtils.getRpcUrl).mockReturnValue("https://soroban-testnet.stellar.org"); + + validPayload = { + x402Version: 2, + resource: { + url: "https://example.com/resource", + description: "Test payment", + mimeType: "application/json", + }, + accepted: validRequirements, + payload: { + transaction: mockTransactionXDR, + }, + }; + + mockSignedTxXdr = mockTransactionXDR; + }); + + beforeEach(() => { + mockServer = { + getAccount: vi.fn().mockImplementation(async addr => new Account(addr, "100")), + sendTransaction: vi.fn().mockResolvedValue({ + status: "PENDING", + hash: "test-tx-hash-fee-bump", + } as Api.SendTransactionResponse), + getTransaction: vi + .fn() + .mockResolvedValue({ status: "SUCCESS" } as Api.GetTransactionResponse), + getLatestLedger: vi.fn().mockResolvedValue({ sequence: 100000 }), + simulateTransaction: vi.fn().mockResolvedValue({ + id: "test", + latestLedger: 123, + events: [], + _parsed: true, + transactionData: new SorobanDataBuilder(), + minResourceFee: "100", + cost: { cpuInsns: "0", memBytes: "0" }, + results: [], + } as Api.SimulateTransactionSuccessResponse), + } as unknown as rpc.Server; + + vi.clearAllMocks(); + + vi.mocked(stellarUtils.getRpcClient).mockReturnValue(mockServer); + vi.mocked(stellarUtils.getNetworkPassphrase).mockReturnValue(StellarNetworks.TESTNET); + vi.mocked(stellarUtils.getRpcUrl).mockReturnValue("https://soroban-testnet.stellar.org"); + + // Mock signTransaction for all signers to return the mock signed XDR + vi.spyOn(facilitatorSigner1, "signTransaction").mockResolvedValue({ + signedTxXdr: mockSignedTxXdr, + error: undefined, + }); + vi.spyOn(facilitatorSigner2, "signTransaction").mockResolvedValue({ + signedTxXdr: mockSignedTxXdr, + error: undefined, + }); + }); + + it("should wrap inner tx in FeeBumpTransaction when feeBumpSigner is set", async () => { + // Mock the fee bump signer to return a properly signed fee bump XDR + const feeBumpSignSpy = vi + .spyOn(feeBumpSigner, "signTransaction") + .mockImplementation(async (txXdr, opts) => { + // Verify the input is a fee bump transaction + const parsed = TransactionBuilder.fromXDR( + txXdr, + opts?.networkPassphrase ?? StellarNetworks.TESTNET, + ); + expect(parsed).toBeInstanceOf(FeeBumpTransaction); + const fbTx = parsed as FeeBumpTransaction; + expect(fbTx.feeSource).toBe(FEE_BUMP_PUBLIC); + // Return the same XDR (mock — no real signature needed for send mock) + return { signedTxXdr: txXdr, error: undefined }; + }); + + const facilitator = new ExactStellarScheme([facilitatorSigner1], { feeBumpSigner }); + vi.spyOn(facilitator, "verify").mockResolvedValue({ isValid: true, payer: CLIENT_PUBLIC }); + + const result = await facilitator.settle(validPayload, validRequirements); + + expect(result.success).toBe(true); + expect(result.transaction).toBe("test-tx-hash-fee-bump"); + // Inner tx signer called first + expect(facilitatorSigner1.signTransaction).toHaveBeenCalledTimes(1); + // Fee bump signer called second + expect(feeBumpSignSpy).toHaveBeenCalledTimes(1); + // The submitted transaction should be a FeeBumpTransaction + const submitCall = vi.mocked(mockServer.sendTransaction).mock.calls[0][0]; + expect(submitCall).toBeInstanceOf(FeeBumpTransaction); + }); + + it("should return error when fee bump signing fails", async () => { + vi.spyOn(feeBumpSigner, "signTransaction").mockResolvedValue({ + signedTxXdr: "", + error: { code: 1, message: "Fee bump signing failed" }, + }); + + const facilitator = new ExactStellarScheme([facilitatorSigner1], { feeBumpSigner }); + vi.spyOn(facilitator, "verify").mockResolvedValue({ isValid: true, payer: CLIENT_PUBLIC }); + + const result = await facilitator.settle(validPayload, validRequirements); + + expect(result.success).toBe(false); + expect(result.errorReason).toBe("settle_exact_stellar_fee_bump_signing_failed"); + expect(result.payer).toBe(CLIENT_PUBLIC); + // Inner tx signing should have succeeded + expect(facilitatorSigner1.signTransaction).toHaveBeenCalledTimes(1); + // Transaction should not have been submitted + expect(mockServer.sendTransaction).not.toHaveBeenCalled(); + }); + + it("should not use fee bump when feeBumpSigner is not set", async () => { + const facilitator = new ExactStellarScheme([facilitatorSigner1]); + vi.spyOn(facilitator, "verify").mockResolvedValue({ isValid: true, payer: CLIENT_PUBLIC }); + + const result = await facilitator.settle(validPayload, validRequirements); + + expect(result.success).toBe(true); + // The submitted transaction should be a regular Transaction, not FeeBumpTransaction + const submitCall = vi.mocked(mockServer.sendTransaction).mock.calls[0][0]; + expect(submitCall).not.toBeInstanceOf(FeeBumpTransaction); + }); + + it("should use round-robin signer for inner tx while fee bump signer pays fees", async () => { + vi.spyOn(feeBumpSigner, "signTransaction").mockImplementation(async txXdr => { + return { signedTxXdr: txXdr, error: undefined }; + }); + + const facilitator = new ExactStellarScheme([facilitatorSigner1, facilitatorSigner2], { + feeBumpSigner, + }); + vi.spyOn(facilitator, "verify").mockResolvedValue({ isValid: true, payer: CLIENT_PUBLIC }); + + // First settle — should use signer1 for inner tx + await facilitator.settle(validPayload, validRequirements); + expect(mockServer.getAccount).toHaveBeenLastCalledWith(facilitatorSigner1.address); + + // Second settle — should use signer2 for inner tx (round robin) + await facilitator.settle(validPayload, validRequirements); + expect(mockServer.getAccount).toHaveBeenLastCalledWith(facilitatorSigner2.address); + + // Fee bump signer should have been called for both settlements + expect(feeBumpSigner.signTransaction).toHaveBeenCalledTimes(2); + }); +}); diff --git a/typescript/packages/mechanisms/stellar/test/unit/facilitator-verify.test.ts b/typescript/packages/mechanisms/stellar/test/unit/facilitator-verify.test.ts new file mode 100644 index 0000000000..9bd84a89f1 --- /dev/null +++ b/typescript/packages/mechanisms/stellar/test/unit/facilitator-verify.test.ts @@ -0,0 +1,1037 @@ +import { Buffer } from "buffer"; +import { + Address, + Networks as StellarNetworks, + SorobanDataBuilder, + rpc, + Transaction, + TransactionBuilder, + Operation, + Account, + xdr, + Keypair, + Asset, +} from "@stellar/stellar-sdk"; +import { Api } from "@stellar/stellar-sdk/rpc"; +import { beforeEach, beforeAll, describe, expect, it, vi } from "vitest"; +import { STELLAR_TESTNET_CAIP2 } from "../../src/constants"; +import { + ExactStellarScheme, + invalidVerifyResponse, + validVerifyResponse, +} from "../../src/exact/facilitator/scheme"; +import { createEd25519Signer } from "../../src/signer"; +import * as stellarUtils from "../../src/utils"; +import type { FacilitatorStellarSigner } from "../../src/signer"; +import type { PaymentPayload, PaymentRequirements } from "@x402/core/types"; + +/** + * Creates a mock transfer event for testing event validation. + * Follows CAP-46-06 format: Topic: ["transfer", from, to, ...], Data: amount + * @see https://github.com/stellar/stellar-protocol/blob/master/core/cap-0046-06.md + * + * @param params - The parameters object + * @param params.from - The sender address + * @param params.to - The recipient address + * @param params.amount - The transfer amount as bigint + * @param params.fnName - The function name (defaults to "transfer") + * @returns A DiagnosticEvent XDR object + */ +function createMockContractEvent({ + from, + to, + amount, + fnName = "transfer", + contractId, +}: { + from: string; + to: string; + amount: bigint; + fnName?: string; + contractId?: xdr.Hash | null; +}): xdr.DiagnosticEvent { + // symbol for the function name + const transferSymbol = xdr.ScVal.scvSymbol(fnName); + + const fromKeypair = Keypair.fromPublicKey(from); + const fromScAddress = xdr.ScVal.scvAddress( + xdr.ScAddress.scAddressTypeAccount( + xdr.PublicKey.publicKeyTypeEd25519(fromKeypair.rawPublicKey()), + ), + ); + + const toKeypair = Keypair.fromPublicKey(to); + const toScAddress = xdr.ScVal.scvAddress( + xdr.ScAddress.scAddressTypeAccount( + xdr.PublicKey.publicKeyTypeEd25519(toKeypair.rawPublicKey()), + ), + ); + + const amountScVal = xdr.ScVal.scvI128( + new xdr.Int128Parts({ + lo: xdr.Uint64.fromString(amount.toString()), + hi: xdr.Int64.fromString("0"), + }), + ); + + const contractEventV0 = new xdr.ContractEventV0({ + topics: [transferSymbol, fromScAddress, toScAddress], + data: amountScVal, + }); + return createMockDiagnosticEvent( + contractEventV0, + xdr.ContractEventType.contract(), + contractId ?? null, + ); +} + +/** + * Creates a mock system diagnostic event (should be ignored by event validator). + */ +function createMockSystemEvent(): xdr.DiagnosticEvent { + return createMockDiagnosticEvent( + new xdr.ContractEventV0({ topics: [], data: xdr.ScVal.scvVoid() }), + xdr.ContractEventType.system(), + ); +} + +/** + * Creates a mock diagnostic event from a ContractEventV0 and event type. + * This helper function constructs the proper XDR structure for testing + * contract event validation in the facilitator verification process. + * + * @param v0 - The ContractEventV0 containing the event data + * @param eventType - The contract event type (contract or system) + * @param contractId - Optional contract ID (null = event has no contract; omit for backward compat) + * @returns A properly formatted DiagnosticEvent for testing + */ +function createMockDiagnosticEvent( + v0: xdr.ContractEventV0, + eventType: ReturnType = xdr.ContractEventType.contract(), + contractId: xdr.Hash | null = null, +): xdr.DiagnosticEvent { + const eventBodyXdr = xdr.ContractEventBody.toXDR( + xdr.ContractEventBody.fromXDR( + Buffer.concat([Buffer.from([0, 0, 0, 0]), xdr.ContractEventV0.toXDR(v0)]), + ), + ); + const contractEvent = new xdr.ContractEvent({ + ext: xdr.ExtensionPoint.fromXDR(Buffer.from([0, 0, 0, 0])), + contractId, + type: eventType, + body: xdr.ContractEventBody.fromXDR(eventBodyXdr), + }); + return new xdr.DiagnosticEvent({ + inSuccessfulContractCall: true, + event: contractEvent, + }); +} + +vi.mock("../../src/utils", async () => { + const actual = await vi.importActual("../../src/utils"); + return { + ...actual, + getEstimatedLedgerCloseTimeSeconds: vi.fn().mockResolvedValue(5), + getNetworkPassphrase: vi.fn(), + getRpcClient: vi.fn(), + }; +}); + +describe("ExactStellarScheme#Verify (randomly using 1-2 facilitator signers)", () => { + const mockServer = { + simulateTransaction: vi.fn(), + getLatestLedger: vi.fn(), + } as unknown as rpc.Server; + + const CLIENT_PUBLIC = "GBBO4ZDDZTSM2IUKQYBAST3CFHNPFXECGEFTGWTA2WELR2BIWDK57UVE"; + const FACILITATOR_PUBLIC = "GCQAXB2D77Y4C66CTGVH25H2RMUKMQJGOWUPK7UXGG5MAQBONUEKFQ4P"; + const TRANSACTION_RECIPIENT = "GCHEI4PQEFJOA27MNZRPQNLGURS6KASW76X5UZCUZIXCOJLKXYCXOR2W"; + const ASSET = "CBIELTK6YBZJU5UP2WWQEUCYKLPU6AUNZ2BQ4WWFEIE3USCIHMXQDAMA"; + const networkPassphrase = StellarNetworks.TESTNET; + const account = new Account(CLIENT_PUBLIC, "100"); + + const facilitatorSigner1 = createEd25519Signer( + "SCKB3ECHCPVM4HJPNCQWTQWJJ5XRL6UNKLTTCIH4B7TB22NKJ5GUFMIV", + STELLAR_TESTNET_CAIP2, + ); + const facilitatorSigner2 = createEd25519Signer( + "SA6LFVPCYMDQILBRXQ2B2HRPK6DV2TX4FTQQQHWFPSCSY4H2RTCD3XAK", + STELLAR_TESTNET_CAIP2, + ); + + let stellarPayload: { transaction: string }; + let baseTransaction: Transaction; + let baseSorobanData: xdr.SorobanTransactionData | undefined; + let baseOperation: Operation.InvokeHostFunction; + let baseFunc: xdr.HostFunction; + let baseInvokeContractArgs: xdr.InvokeContractArgs; + let facilitatorSigners: FacilitatorStellarSigner[]; + let facilitator: ExactStellarScheme; + let validPayload: PaymentPayload; + let validRequirements: PaymentRequirements; + + // Use a real transaction XDR from shared test (base64 encoded JSON with tx field) + const signedTxJson = + "eyJtZXRob2QiOiJ0cmFuc2ZlciIsInR4IjoiQUFBQUFnQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQURsM0lBQUFBQUFBQUFBUUFBQUFFQUFBQUFBQUFBQUFBQUFBQnBGcEdGQUFBQUFBQUFBQUVBQUFBQUFBQUFHQUFBQUFBQUFBQUJVRVhOWHNCeW1uYVAxYTBDVUZoUzMwOENqYzZERGxyRklnbTZTRWc3THdFQUFBQUlkSEpoYm5ObVpYSUFBQUFEQUFBQUVnQUFBQUFBQUFBQVF1NWtZOHprelNLS2hnSUpUMklwMnZMY2dqRUxNMXBnMVlpNDZDaXcxZDhBQUFBU0FBQUFBQUFBQUFDT1JISHdJVkxnYSt4dVl2ZzFacVJsNVFKVy82L2FaRlRLTGljbGFyNEZkd0FBQUFvQUFBQUFBQUFBQUFBQUFBQUFBQ2NRQUFBQUFRQUFBQUVBQUFBQUFBQUFBRUx1WkdQTTVNMGlpb1lDQ1U5aUtkcnkzSUl4Q3pOYVlOV0l1T2dvc05YZlh4amsrOHlZOGhnQUk4ck9BQUFBRUFBQUFBRUFBQUFCQUFBQUVRQUFBQUVBQUFBQ0FBQUFEd0FBQUFwd2RXSnNhV05mYTJWNUFBQUFBQUFOQUFBQUlFTHVaR1BNNU0waWlvWUNDVTlpS2RyeTNJSXhDek5hWU5XSXVPZ29zTlhmQUFBQUR3QUFBQWx6YVdkdVlYUjFjbVVBQUFBQUFBQU5BQUFBUUl2bjJjU3VLbFl5TU96T0pTWnkwc0VaN3dkN1QwYmdSQ0ZxZjg1M3VXQXFVcjE1ZUpycXNqVjROUVpTQW05WXNWbHZEcEUrSFRLc3pUQUVBaTJBRkFnQUFBQUFBQUFBQVZCRnpWN0FjcHAyajlXdEFsQllVdDlQQW8zT2d3NWF4U0lKdWtoSU95OEJBQUFBQ0hSeVlXNXpabVZ5QUFBQUF3QUFBQklBQUFBQUFBQUFBRUx1WkdQTTVNMGlpb1lDQ1U5aUtkcnkzSUl4Q3pOYVlOV0l1T2dvc05YZkFBQUFFZ0FBQUFBQUFBQUFqa1J4OENGUzRHdnNibUw0Tldha1plVUNWdit2Mm1SVXlpNG5KV3ErQlhjQUFBQUtBQUFBQUFBQUFBQUFBQUFBQUFBbkVBQUFBQUFBQUFBQkFBQUFBQUFBQUFJQUFBQUFBQUFBQUVMdVpHUE01TTBpaW9ZQ0NVOWlLZHJ5M0lJeEN6TmFZTldJdU9nb3NOWGZBQUFBQmdBQUFBRlFSYzFld0hLYWRvL1ZyUUpRV0ZMZlR3S056b01PV3NVaUNicElTRHN2QVFBQUFCUUFBQUFCQUFBQUF3QUFBQUVBQUFBQVF1NWtZOHprelNLS2hnSUpUMklwMnZMY2dqRUxNMXBnMVlpNDZDaXcxZDhBQUFBQlZWTkVRd0FBQUFCQ1BuMEY4dXl2dit3Wkt5RmFQeHZwYXUyNDJPY0NWS3ZqUVQ0Q0I5NVdzZ0FBQUFFQUFBQUFqa1J4OENGUzRHdnNibUw0Tldha1plVUNWdit2Mm1SVXlpNG5KV3ErQlhjQUFBQUJWVk5FUXdBQUFBQkNQbjBGOHV5dnYrd1pLeUZhUHh2cGF1MjQyT2NDVkt2alFUNENCOTVXc2dBQUFBWUFBQUFBQUFBQUFFTHVaR1BNNU0waWlvWUNDVTlpS2RyeTNJSXhDek5hWU5XSXVPZ29zTlhmQUFBQUZWOFk1UHZNbVBJWUFBQUFBQUFMNVRFQUFBRjRBQUFCTkFBQUFBQUFBNWNPQUFBQUFBPT0iLCJzaW11bGF0aW9uUmVzdWx0Ijp7ImF1dGgiOlsiQUFBQUFRQUFBQUFBQUFBQVF1NWtZOHprelNLS2hnSUpUMklwMnZMY2dqRUxNMXBnMVlpNDZDaXcxZDlmR09UN3pKanlHQUFBQUFBQUFBQUJBQUFBQUFBQUFBRlFSYzFld0hLYWRvL1ZyUUpRV0ZMZlR3S056b01PV3NVaUNicElTRHN2QVFBQUFBaDBjbUZ1YzJabGNnQUFBQU1BQUFBU0FBQUFBQUFBQUFCQzdtUmp6T1ROSW9xR0FnbFBZaW5hOHR5Q01Rc3pXbURWaUxqb0tMRFYzd0FBQUJJQUFBQUFBQUFBQUk1RWNmQWhVdUJyN0c1aStEVm1wR1hsQWxiL3I5cGtWTW91SnlWcXZnVjNBQUFBQ2dBQUFBQUFBQUFBQUFBQUFBQUFKeEFBQUFBQSJdLCJyZXR2YWwiOiJBQUFBQVE9PSJ9LCJzaW11bGF0aW9uVHJhbnNhY3Rpb25EYXRhIjoiQUFBQUFBQUFBQUlBQUFBQUFBQUFBRUx1WkdQTTVNMGlpb1lDQ1U5aUtkcnkzSUl4Q3pOYVlOV0l1T2dvc05YZkFBQUFCZ0FBQUFGUVJjMWV3SEthZG8vVnJRSlFXRkxmVHdLTnpvTU9Xc1VpQ2JwSVNEc3ZBUUFBQUJRQUFBQUJBQUFBQXdBQUFBRUFBQUFBUXU1a1k4emt6U0tLaGdJSlQySXAydkxjZ2pFTE0xcGcxWWk0NkNpdzFkOEFBQUFCVlZORVF3QUFBQUJDUG4wRjh1eXZ2K3daS3lGYVB4dnBhdTI0Mk9jQ1ZLdmpRVDRDQjk1V3NnQUFBQUVBQUFBQWprUng4Q0ZTNEd2c2JtTDROV2FrWmVVQ1Z2K3YybVJVeWk0bkpXcStCWGNBQUFBQlZWTkVRd0FBQUFCQ1BuMEY4dXl2dit3Wkt5RmFQeHZwYXUyNDJPY0NWS3ZqUVQ0Q0I5NVdzZ0FBQUFZQUFBQUFBQUFBQUVMdVpHUE01TTBpaW9ZQ0NVOWlLZHJ5M0lJeEN6TmFZTldJdU9nb3NOWGZBQUFBRlY4WTVQdk1tUElZQUFBQUFBQUw1VEVBQUFGNEFBQUJOQUFBQUFBQUE1Y08ifQ=="; + const txSignatureExpiration = 2345678; + const { tx: baseTransactionXDR } = JSON.parse( + Buffer.from(signedTxJson, "base64").toString("utf8"), + ); + + beforeAll(async () => { + // Set up mocks + vi.mocked(stellarUtils.getNetworkPassphrase).mockReturnValue(StellarNetworks.TESTNET); + vi.mocked(stellarUtils.getRpcClient).mockReturnValue(mockServer); + vi.mocked(mockServer.getLatestLedger).mockResolvedValue({ + sequence: txSignatureExpiration - 10, + } as Api.GetLatestLedgerResponse); + + // Create valid requirements (V2 format) + // Note: Values must match the transaction XDR from shared test + validRequirements = { + scheme: "exact", + network: STELLAR_TESTNET_CAIP2, + amount: "10000", // Extracted from transaction XDR + payTo: TRANSACTION_RECIPIENT, // Must match transaction's recipient + maxTimeoutSeconds: 60, + asset: ASSET, + extra: { + maxLedgerOffset: 12, + }, + }; + + // Build full V2 PaymentPayload with mocked transaction + validPayload = { + x402Version: 2, + resource: { + url: "https://example.com/resource", + description: "Test payment", + mimeType: "application/json", + }, + accepted: validRequirements, + payload: { + transaction: baseTransactionXDR, + }, + }; + + stellarPayload = validPayload.payload as { transaction: string }; + const txEnvelope = xdr.TransactionEnvelope.fromXDR(stellarPayload.transaction, "base64"); + baseTransaction = new Transaction(stellarPayload.transaction, networkPassphrase); + baseSorobanData = txEnvelope.v1()?.tx()?.ext()?.sorobanData() || undefined; + baseOperation = baseTransaction.operations[0] as Operation.InvokeHostFunction; + baseFunc = baseOperation.func; + baseInvokeContractArgs = baseFunc.invokeContract(); + }); + + /** + * Builds a modified transaction and wraps it in a PaymentPayload. + */ + function buildStellarPayloadFromOp( + operation: xdr.Operation, + options?: { includeSorobanData?: boolean }, + ): PaymentPayload { + const modifiedTx = new TransactionBuilder(account, { + fee: baseTransaction.fee, + networkPassphrase, + ledgerbounds: baseTransaction.ledgerBounds, + ...(options?.includeSorobanData !== false && + baseSorobanData && { sorobanData: baseSorobanData }), + }) + .addOperation(operation) + .setTimeout(validRequirements.maxTimeoutSeconds) + .build(); + return { + ...validPayload, + payload: { transaction: modifiedTx.toXDR() }, + }; + } + + beforeEach(() => { + // Random selection for 1-2 facilitators + const useTwoFacilitators = Math.random() > 0.5; + facilitatorSigners = useTwoFacilitators + ? [facilitatorSigner1, facilitatorSigner2] + : [facilitatorSigner1]; + + // Use a high max fee for tests to avoid fee validation errors in tests that check other validations + facilitator = new ExactStellarScheme(facilitatorSigners, { + areFeesSponsored: true, + maxTransactionFeeStroops: 1_000_000, + }); + + const expectedAssetHash = new Address(ASSET).toScAddress().contractId(); + const defaultTransferEvent = createMockContractEvent({ + from: CLIENT_PUBLIC, + to: TRANSACTION_RECIPIENT, + amount: BigInt(validRequirements.amount), + contractId: expectedAssetHash, + }); + + vi.mocked(mockServer.simulateTransaction).mockResolvedValue({ + id: "test", + latestLedger: 123, + events: [defaultTransferEvent], + _parsed: true, + transactionData: new SorobanDataBuilder(), + minResourceFee: "100", + cost: { cpuInsns: "0", memBytes: "0" }, + results: [], + } as Api.SimulateTransactionSuccessResponse); + }); + + describe("validation errors", () => { + it("should reject invalid x402 version, scheme, and network mismatch", async () => { + let result = await facilitator.verify( + { ...validPayload, x402Version: 9 }, // ❌ unsupported x402 version + validRequirements, + ); + expect(result).toEqual(invalidVerifyResponse("invalid_x402_version")); + + result = await facilitator.verify( + { + ...validPayload, + accepted: { ...validPayload.accepted, scheme: "invalid" }, // ❌ wrong scheme + }, + validRequirements, + ); + expect(result).toEqual(invalidVerifyResponse("unsupported_scheme")); + + result = await facilitator.verify( + { + ...validPayload, + accepted: { ...validPayload.accepted, network: "foo:bar" }, // ❌ wrong network + }, + validRequirements, + ); + expect(result).toEqual(invalidVerifyResponse("network_mismatch")); + }); + + it("should reject transactions with fees exceeding the maximum", async () => { + const lowMaxFeeFacilitator = new ExactStellarScheme(facilitatorSigners, { + areFeesSponsored: true, + maxTransactionFeeStroops: 1000, // 1000 stroops max + }); + + vi.mocked(stellarUtils.getRpcClient).mockReturnValue(mockServer as rpc.Server); + vi.mocked(stellarUtils.getNetworkPassphrase).mockReturnValue(StellarNetworks.TESTNET); + + const result = await lowMaxFeeFacilitator.verify(validPayload, validRequirements); + expect(result).toEqual( + invalidVerifyResponse("invalid_exact_stellar_payload_fee_exceeds_maximum"), + ); + }); + + it("should reject transactions with fees below simulation minimum", async () => { + const expectedAssetHashForFeeTest = new Address(ASSET).toScAddress().contractId(); + const mockTransferEventForFeeTest = createMockContractEvent({ + from: CLIENT_PUBLIC, + to: TRANSACTION_RECIPIENT, + amount: BigInt("10000"), + contractId: expectedAssetHashForFeeTest, + }); + + const originalSimulate = vi.mocked(stellarUtils.getRpcClient).getMockImplementation(); + const mockServerWithHighMinFee = { + ...mockServer, + simulateTransaction: vi.fn().mockResolvedValue({ + id: "test", + latestLedger: 123, + events: [mockTransferEventForFeeTest], + _parsed: true, + transactionData: new SorobanDataBuilder(), + minResourceFee: "999999999", + cost: { cpuInsns: "0", memBytes: "0" }, + results: [], + } as Api.SimulateTransactionSuccessResponse), + }; + + vi.mocked(stellarUtils.getRpcClient).mockReturnValue( + mockServerWithHighMinFee as unknown as rpc.Server, + ); + vi.mocked(stellarUtils.getNetworkPassphrase).mockReturnValue(StellarNetworks.TESTNET); + + try { + const result = await facilitator.verify(validPayload, validRequirements); + expect(result).toEqual( + invalidVerifyResponse("invalid_exact_stellar_payload_fee_below_minimum", CLIENT_PUBLIC), + ); + } finally { + vi.mocked(stellarUtils.getRpcClient).mockImplementation(originalSimulate!); + } + }); + + describe("mismatching networks", () => { + it("should reject mismatching requirement<>payload networks", async () => { + const requirements: PaymentRequirements = { + ...validRequirements, + network: "eip155:84532" as never, // ❌ requirements network != payload accepted + }; + const result = await facilitator.verify(validPayload, requirements); + expect(result).toEqual(invalidVerifyResponse("network_mismatch")); + }); + + it("should reject when network is not a Stellar network", async () => { + const wrongNetwork = "eip155:84532" as never; // ❌ non-Stellar network + const requirements: PaymentRequirements = { + ...validRequirements, + network: wrongNetwork, + }; + const payload: PaymentPayload = { + ...validPayload, + accepted: { ...validPayload.accepted, network: wrongNetwork }, + }; + const result = await facilitator.verify(payload, requirements); + expect(result).toEqual(invalidVerifyResponse("invalid_network")); + }); + }); + + it("should reject malformed transaction XDR", async () => { + const payload = { + ...validPayload, + payload: { transaction: "AAAA" }, // ❌ Invalid XDR + }; + const result = await facilitator.verify(payload, validRequirements); + expect(result).toEqual(invalidVerifyResponse("invalid_exact_stellar_payload_malformed")); + }); + + it("should reject wrong operation count", async () => { + expect(baseSorobanData).toBeDefined(); + + const parsedOperation = Operation.invokeHostFunction(baseOperation); + const modifiedTx = new TransactionBuilder(account, { + fee: baseTransaction.fee, + networkPassphrase, + ledgerbounds: baseTransaction.ledgerBounds, + sorobanData: baseSorobanData, + }) + .addOperation(parsedOperation) + .addOperation(parsedOperation) // ❌ Multiple operations are forbidden + .setTimeout(validRequirements.maxTimeoutSeconds) + .build(); + + const modifiedStellarPayload: PaymentPayload = { + ...validPayload, + payload: { transaction: modifiedTx.toXDR() }, + }; + + const result = await facilitator.verify(modifiedStellarPayload, validRequirements); + expect(result.isValid).toBe(false); + expect(result.invalidReason).toBe("invalid_exact_stellar_payload_wrong_operation"); + }); + + it("should reject wrong operation type", async () => { + const paymentOp = Operation.payment({ + // ❌ operation of unsupported type (MUST be invokeHostFunction) + destination: CLIENT_PUBLIC, + asset: Asset.native(), + amount: "1", + }); + const modifiedStellarPayload = buildStellarPayloadFromOp(paymentOp, { + includeSorobanData: false, + }); + + const result = await facilitator.verify(modifiedStellarPayload, validRequirements); + expect(result.isValid).toBe(false); + expect(result.invalidReason).toBe("invalid_exact_stellar_payload_wrong_operation"); + }); + + it("should reject wrong contract function name", async () => { + const wrongFuncArgs = new xdr.InvokeContractArgs({ + contractAddress: baseInvokeContractArgs.contractAddress(), + functionName: "mint", // ❌ function name MUST be "transfer" + args: baseInvokeContractArgs.args(), + }); + const modifiedFunc = xdr.HostFunction.hostFunctionTypeInvokeContract(wrongFuncArgs); + const modifiedOperation = Operation.invokeHostFunction({ + ...baseOperation, + func: modifiedFunc, + }); + const modifiedStellarPayload = buildStellarPayloadFromOp(modifiedOperation); + + const result = await facilitator.verify(modifiedStellarPayload, validRequirements); + expect(result.isValid).toBe(false); + expect(result.invalidReason).toBe("invalid_exact_stellar_payload_wrong_function_name"); + }); + + it("should reject wrong asset, recipient, or amount", async () => { + let result = await facilitator.verify(validPayload, { + ...validRequirements, + asset: "CDNVQW44C3HALYNVQ4SOBXY5EWYTGVYXX6JPESOLQDABJI5FC5LTRRUE", // ❌ wrong asset + }); + expect(result.invalidReason).toBe("invalid_exact_stellar_payload_wrong_asset"); + + result = await facilitator.verify(validPayload, { + ...validRequirements, + payTo: "GAHPYWLK6YRN7CVYZOO4H3VDRZ7PVF5UJGLZCSPAEIKJE2XSWF5LAGER", // ❌ wrong recipient + }); + expect(result.invalidReason).toBe("invalid_exact_stellar_payload_wrong_recipient"); + expect(result.payer).toBe(CLIENT_PUBLIC); + + result = await facilitator.verify(validPayload, { + ...validRequirements, + amount: "10001", // ❌ wrong amount + }); + expect(result.invalidReason).toBe("invalid_exact_stellar_payload_wrong_amount"); + expect(result.payer).toBe(CLIENT_PUBLIC); + }); + + describe("Authorization entries and facilitator safety", () => { + it("should reject when facilitator is the payer (from address)", async () => { + const facilitatorAddress = facilitatorSigner1.address; + + if (!baseSorobanData || !baseOperation.auth?.length) { + throw new Error("Missing sorobanData or auth in test transaction"); + } + + const originalArgs = baseInvokeContractArgs.args(); + const facilitatorKeypair = Keypair.fromPublicKey(facilitatorAddress); + const facilitatorScAddress = xdr.ScVal.scvAddress( + xdr.ScAddress.scAddressTypeAccount( + xdr.PublicKey.publicKeyTypeEd25519(facilitatorKeypair.rawPublicKey()), + ), + ); + + const modifiedInvokeContractArgs = new xdr.InvokeContractArgs({ + contractAddress: baseInvokeContractArgs.contractAddress(), + functionName: baseInvokeContractArgs.functionName(), + args: [ + facilitatorScAddress, // ❌ facilitator CANNOT be the payer + originalArgs[1], + originalArgs[2], + ], + }); + const modifiedFunc = xdr.HostFunction.hostFunctionTypeInvokeContract( + modifiedInvokeContractArgs, + ); + const modifiedOperation = Operation.invokeHostFunction({ + ...baseOperation, + func: modifiedFunc, + }); + const modifiedStellarPayload = buildStellarPayloadFromOp(modifiedOperation); + + const result = await facilitator.verify(modifiedStellarPayload, validRequirements); + expect(result.isValid).toBe(false); + expect(result.invalidReason).toBe("invalid_exact_stellar_payload_facilitator_is_payer"); + }); + + it("should reject empty auth entries array", async () => { + const modifiedOperation = Operation.invokeHostFunction({ + ...baseOperation, + auth: [], // ❌ Empty auth array + }); + const modifiedStellarPayload = buildStellarPayloadFromOp(modifiedOperation); + + const result = await facilitator.verify(modifiedStellarPayload, validRequirements); + expect(result.isValid).toBe(false); + expect(result.invalidReason).toBe("invalid_exact_stellar_payload_no_auth_entries"); + }); + + it("should reject missing payer signature", async () => { + if (!baseSorobanData || !baseOperation.auth || baseOperation.auth.length === 0) { + throw new Error("Missing sorobanData or auth in test transaction"); + } + + const originalAuth = baseOperation.auth[0]; + const originalCreds = originalAuth.credentials().address(); + + const unsignedCreds = new xdr.SorobanAddressCredentials({ + address: originalCreds.address(), + nonce: originalCreds.nonce(), + signatureExpirationLedger: originalCreds.signatureExpirationLedger(), + signature: xdr.ScVal.scvVoid(), // ❌ payer signature is missing + }); + + const authWithoutSignature = new xdr.SorobanAuthorizationEntry({ + credentials: xdr.SorobanCredentials.sorobanCredentialsAddress(unsignedCreds), + rootInvocation: originalAuth.rootInvocation(), + }); + + const modifiedOperation = Operation.invokeHostFunction({ + ...baseOperation, + auth: [authWithoutSignature], + }); + const modifiedStellarPayload = buildStellarPayloadFromOp(modifiedOperation); + + const result = await facilitator.verify(modifiedStellarPayload, validRequirements); + expect(result.isValid).toBe(false); + expect(result.invalidReason).toBe("invalid_exact_stellar_payload_missing_payer_signature"); + expect(result.payer).toBe(CLIENT_PUBLIC); + }); + + it("should reject unexpected pending signatures", async () => { + if (!baseSorobanData || !baseOperation.auth || baseOperation.auth.length === 0) { + throw new Error("Missing sorobanData or auth in test transaction"); + } + + const originalAuth = baseOperation.auth[0]; + const originalCreds = originalAuth.credentials().address(); + + const otherKeypair = Keypair.random(); + const otherAddressScVal = xdr.ScAddress.scAddressTypeAccount( + xdr.PublicKey.publicKeyTypeEd25519(otherKeypair.rawPublicKey()), + ); + + const pendingCreds = new xdr.SorobanAddressCredentials({ + address: otherAddressScVal, + nonce: originalCreds.nonce(), + signatureExpirationLedger: originalCreds.signatureExpirationLedger(), + signature: xdr.ScVal.scvVoid(), + }); + + const pendingAuth = new xdr.SorobanAuthorizationEntry({ + credentials: xdr.SorobanCredentials.sorobanCredentialsAddress(pendingCreds), + rootInvocation: originalAuth.rootInvocation(), + }); + + const modifiedOperation = Operation.invokeHostFunction({ + ...baseOperation, + auth: [originalAuth, pendingAuth], // ❌ unexpected pending signature(s) + }); + const modifiedStellarPayload = buildStellarPayloadFromOp(modifiedOperation); + + const result = await facilitator.verify(modifiedStellarPayload, validRequirements); + expect(result.isValid).toBe(false); + expect(result.invalidReason).toBe( + "invalid_exact_stellar_payload_unexpected_pending_signatures", + ); + expect(result.payer).toBe(CLIENT_PUBLIC); + }); + + it("should reject expiration ledger too far in the future", async () => { + if (!baseSorobanData || !baseOperation.auth || baseOperation.auth.length === 0) { + throw new Error("Missing sorobanData or auth in test transaction"); + } + + const originalAuth = baseOperation.auth[0]; + const originalCreds = originalAuth.credentials().address(); + const farFuture = (txSignatureExpiration + 10_000).toString(); + + const farFutureCreds = new xdr.SorobanAddressCredentials({ + address: originalCreds.address(), + nonce: originalCreds.nonce(), + signatureExpirationLedger: Number(farFuture), // ❌ Signature expiration too far + signature: originalCreds.signature(), + }); + + const authWithFarExpiration = new xdr.SorobanAuthorizationEntry({ + credentials: xdr.SorobanCredentials.sorobanCredentialsAddress(farFutureCreds), + rootInvocation: originalAuth.rootInvocation(), + }); + + const modifiedOperation = Operation.invokeHostFunction({ + ...baseOperation, + auth: [authWithFarExpiration], + }); + const modifiedStellarPayload = buildStellarPayloadFromOp(modifiedOperation); + + const result = await facilitator.verify(modifiedStellarPayload, validRequirements); + expect(result.isValid).toBe(false); + expect(result.invalidReason).toBe("invalid_exact_stellar_signature_expiration_too_far"); + expect(result.payer).toBe(CLIENT_PUBLIC); + }); + + describe("credential type validation", () => { + it("should reject auth entries with non-sorobanCredentialsAddress credentials", async () => { + if (!baseSorobanData) { + throw new Error("Missing sorobanData in test transaction"); + } + + const sourceAccountAuth = xdr.SorobanAuthorizationEntry.fromXDR( + xdr.SorobanAuthorizationEntry.toXDR( + new xdr.SorobanAuthorizationEntry({ + credentials: xdr.SorobanCredentials.sorobanCredentialsSourceAccount(), // ❌ not sorobanCredentialsAddress + rootInvocation: baseOperation.auth![0].rootInvocation(), + }), + ), + ); + + const modifiedOperation = Operation.invokeHostFunction({ + ...baseOperation, + auth: [sourceAccountAuth], + }); + const modifiedStellarPayload = buildStellarPayloadFromOp(modifiedOperation); + + const result = await facilitator.verify(modifiedStellarPayload, validRequirements); + expect(result.isValid).toBe(false); + expect(result.invalidReason).toBe( + "invalid_exact_stellar_payload_unsupported_credential_type", + ); + }); + }); + + describe("sub-invocation validation", () => { + it("should reject auth entries with sub-invocations", async () => { + if (!baseSorobanData || !baseOperation.auth || baseOperation.auth.length === 0) { + throw new Error("Missing sorobanData or auth in test transaction"); + } + + const originalAuth = baseOperation.auth[0]; + const originalRootInvocation = originalAuth.rootInvocation(); + + const subInvocation = new xdr.SorobanAuthorizedInvocation({ + function: originalRootInvocation.function(), + subInvocations: [], + }); + + const rootWithSubInvocations = new xdr.SorobanAuthorizedInvocation({ + function: originalRootInvocation.function(), + subInvocations: [subInvocation], // ❌ sub-invocations not allowed + }); + + const authWithSubInvocations = new xdr.SorobanAuthorizationEntry({ + credentials: originalAuth.credentials(), + rootInvocation: rootWithSubInvocations, + }); + + const modifiedOperation = Operation.invokeHostFunction({ + ...baseOperation, + auth: [authWithSubInvocations], + }); + const modifiedStellarPayload = buildStellarPayloadFromOp(modifiedOperation); + + const result = await facilitator.verify(modifiedStellarPayload, validRequirements); + expect(result.isValid).toBe(false); + expect(result.invalidReason).toBe("invalid_exact_stellar_payload_has_subinvocations"); + }); + }); + + describe("facilitator in auth entries validation", () => { + it("should reject when facilitator address is in auth entries", async () => { + if (!baseSorobanData || !baseOperation.auth || baseOperation.auth.length === 0) { + throw new Error("Missing sorobanData or auth in test transaction"); + } + + const facilitatorAddress = facilitatorSigner1.address; + const originalAuth = baseOperation.auth[0]; + const originalAddressCredentials = originalAuth.credentials().address(); + + const facilitatorKeypair = Keypair.fromPublicKey(facilitatorAddress); + const facilitatorAddressScVal = xdr.ScAddress.scAddressTypeAccount( + xdr.PublicKey.publicKeyTypeEd25519(facilitatorKeypair.rawPublicKey()), + ); + + const facilitatorCredentials = new xdr.SorobanAddressCredentials({ + address: facilitatorAddressScVal, // ❌ facilitator address in auth entry + nonce: originalAddressCredentials.nonce(), + signatureExpirationLedger: originalAddressCredentials.signatureExpirationLedger(), + signature: originalAddressCredentials.signature(), + }); + + const authWithFacilitator = new xdr.SorobanAuthorizationEntry({ + credentials: xdr.SorobanCredentials.sorobanCredentialsAddress(facilitatorCredentials), + rootInvocation: originalAuth.rootInvocation(), + }); + + const modifiedOperation = Operation.invokeHostFunction({ + ...baseOperation, + auth: [authWithFacilitator], + }); + const modifiedStellarPayload = buildStellarPayloadFromOp(modifiedOperation); + + const result = await facilitator.verify(modifiedStellarPayload, validRequirements); + expect(result.isValid).toBe(false); + expect(result.invalidReason).toBe("invalid_exact_stellar_payload_facilitator_in_auth"); + }); + }); + + describe("should reject when source is unauthorized", () => { + it("should reject operation.source == facilitatorAccount", async () => { + const facilitatorAddress = facilitatorSigner1.address; + + if (!baseSorobanData) { + throw new Error("Missing sorobanData in test transaction"); + } + + const modifiedOperation = Operation.invokeHostFunction({ + ...baseOperation, + source: facilitatorAddress, // ❌ operation source is facilitator + }); + const modifiedStellarPayload = buildStellarPayloadFromOp(modifiedOperation); + + const result = await facilitator.verify(modifiedStellarPayload, validRequirements); + expect(result.isValid).toBe(false); + expect(result.invalidReason).toBe("invalid_exact_stellar_payload_unsafe_tx_or_op_source"); + }); + + it("should reject transaction.source == facilitatorAccount", async () => { + const facilitatorAddress = facilitatorSigner1.address; + + if (!baseSorobanData) { + throw new Error("Missing sorobanData in test transaction"); + } + + const modifiedOperation = Operation.invokeHostFunction({ + ...baseOperation, + }); + const facilitatorAccount = new Account(facilitatorAddress, "100"); // ❌ transaction source is facilitator + const modifiedTx = new TransactionBuilder(facilitatorAccount, { + fee: baseTransaction.fee, + networkPassphrase, + ledgerbounds: baseTransaction.ledgerBounds, + sorobanData: baseSorobanData, + }) + .addOperation(modifiedOperation) + .setTimeout(validRequirements.maxTimeoutSeconds) + .build(); + + const modifiedStellarPayload: PaymentPayload = { + ...validPayload, + payload: { + transaction: modifiedTx.toXDR(), + }, + }; + + const result = await facilitator.verify(modifiedStellarPayload, validRequirements); + expect(result.isValid).toBe(false); + expect(result.invalidReason).toBe("invalid_exact_stellar_payload_unsafe_tx_or_op_source"); + }); + }); + }); + + it("should reject simulation failure", async () => { + vi.mocked(mockServer.simulateTransaction).mockResolvedValueOnce({ + error: "Simulation failed", // ❌ Simulation error + events: [], + id: "test", + latestLedger: 123, + _parsed: true, + } as Api.SimulateTransactionErrorResponse); + + const result = await facilitator.verify(validPayload, validRequirements); + expect(result.invalidReason).toBe("invalid_exact_stellar_payload_simulation_failed"); + expect(result.payer).toBe(CLIENT_PUBLIC); + }); + + // Event-based balance change validation + describe("simulation event validation", () => { + const expectedAssetHash = () => new Address(ASSET).toScAddress().contractId(); + + it("should reject when simulation shows multiple transfer events", async () => { + const mockTransferEvent1 = createMockContractEvent({ + from: CLIENT_PUBLIC, + to: TRANSACTION_RECIPIENT, + amount: BigInt(10000), + contractId: expectedAssetHash(), + }); + const mockTransferEvent2 = createMockContractEvent({ + from: CLIENT_PUBLIC, + to: "GAHPYWLK6YRN7CVYZOO4H3VDRZ7PVF5UJGLZCSPAEIKJE2XSWF5LAGER", + amount: BigInt(5000), + contractId: expectedAssetHash(), + }); + + vi.mocked(mockServer.simulateTransaction).mockResolvedValueOnce({ + id: "test", + latestLedger: 123, + events: [mockTransferEvent1, mockTransferEvent2], // ❌ multiple transfer events + _parsed: true, + transactionData: new SorobanDataBuilder(), + minResourceFee: "100", + cost: { cpuInsns: "0", memBytes: "0" }, + results: [], + } as Api.SimulateTransactionSuccessResponse); + + const result = await facilitator.verify(validPayload, validRequirements); + expect(result.isValid).toBe(false); + expect(result.invalidReason).toBe("invalid_exact_stellar_payload_multiple_transfers"); + }); + + it("should reject when transfer event has wrong from address", async () => { + const mockTransferEvent = createMockContractEvent({ + from: "GAHPYWLK6YRN7CVYZOO4H3VDRZ7PVF5UJGLZCSPAEIKJE2XSWF5LAGER", // ❌ wrong `from` address + to: FACILITATOR_PUBLIC, + amount: BigInt(10000), + contractId: expectedAssetHash(), + }); + + vi.mocked(mockServer.simulateTransaction).mockResolvedValueOnce({ + id: "test", + latestLedger: 123, + events: [mockTransferEvent], + _parsed: true, + transactionData: new SorobanDataBuilder(), + minResourceFee: "100", + cost: { cpuInsns: "0", memBytes: "0" }, + results: [], + } as Api.SimulateTransactionSuccessResponse); + + const result = await facilitator.verify(validPayload, validRequirements); + expect(result.isValid).toBe(false); + expect(result.invalidReason).toBe("invalid_exact_stellar_payload_event_wrong_from"); + }); + + it("should reject when transfer event has wrong to address", async () => { + const mockTransferEvent = createMockContractEvent({ + from: CLIENT_PUBLIC, + to: "GAHPYWLK6YRN7CVYZOO4H3VDRZ7PVF5UJGLZCSPAEIKJE2XSWF5LAGER", // ❌ wrong `to` address + amount: BigInt(10000), + contractId: expectedAssetHash(), + }); + + vi.mocked(mockServer.simulateTransaction).mockResolvedValueOnce({ + id: "test", + latestLedger: 123, + events: [mockTransferEvent], + _parsed: true, + transactionData: new SorobanDataBuilder(), + minResourceFee: "100", + cost: { cpuInsns: "0", memBytes: "0" }, + results: [], + } as Api.SimulateTransactionSuccessResponse); + + const result = await facilitator.verify(validPayload, validRequirements); + expect(result.isValid).toBe(false); + expect(result.invalidReason).toBe("invalid_exact_stellar_payload_event_wrong_to"); + }); + + it("should reject when transfer event has wrong amount", async () => { + const mockTransferEvent = createMockContractEvent({ + from: CLIENT_PUBLIC, + to: TRANSACTION_RECIPIENT, + amount: BigInt(5000), // ❌ wrong `amount` (expected 10000) + contractId: expectedAssetHash(), + }); + + vi.mocked(mockServer.simulateTransaction).mockResolvedValueOnce({ + id: "test", + latestLedger: 123, + events: [mockTransferEvent], + _parsed: true, + transactionData: new SorobanDataBuilder(), + minResourceFee: "100", + cost: { cpuInsns: "0", memBytes: "0" }, + results: [], + } as Api.SimulateTransactionSuccessResponse); + + const result = await facilitator.verify(validPayload, validRequirements); + expect(result.isValid).toBe(false); + expect(result.invalidReason).toBe("invalid_exact_stellar_payload_event_wrong_amount"); + }); + + it("should reject when transfer event has null contractId", async () => { + const mockTransferEvent = createMockContractEvent({ + from: CLIENT_PUBLIC, + to: TRANSACTION_RECIPIENT, + amount: BigInt(10000), + contractId: null, + }); + + vi.mocked(mockServer.simulateTransaction).mockResolvedValueOnce({ + id: "test", + latestLedger: 123, + events: [mockTransferEvent], + _parsed: true, + transactionData: new SorobanDataBuilder(), + minResourceFee: "100", + cost: { cpuInsns: "0", memBytes: "0" }, + results: [], + } as Api.SimulateTransactionSuccessResponse); + + const result = await facilitator.verify(validPayload, validRequirements); + expect(result.isValid).toBe(false); + expect(result.invalidReason).toBe( + "invalid_exact_stellar_payload_event_missing_contract_id", + ); + }); + + it("should reject when transfer event has wrong asset (contract address)", async () => { + const wrongAsset = "CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC"; + const mockTransferEvent = createMockContractEvent({ + from: CLIENT_PUBLIC, + to: TRANSACTION_RECIPIENT, + amount: BigInt(10000), + contractId: new Address(wrongAsset).toScAddress().contractId(), + }); + + vi.mocked(mockServer.simulateTransaction).mockResolvedValueOnce({ + id: "test", + latestLedger: 123, + events: [mockTransferEvent], + _parsed: true, + transactionData: new SorobanDataBuilder(), + minResourceFee: "100", + cost: { cpuInsns: "0", memBytes: "0" }, + results: [], + } as Api.SimulateTransactionSuccessResponse); + + const result = await facilitator.verify(validPayload, validRequirements); + expect(result.isValid).toBe(false); + expect(result.invalidReason).toBe("invalid_exact_stellar_payload_event_wrong_asset"); + }); + + it("should reject when no transfer events are present", async () => { + vi.mocked(mockServer.simulateTransaction).mockResolvedValueOnce({ + id: "test", + latestLedger: 123, + events: [], // ❌ no transfer events + _parsed: true, + transactionData: new SorobanDataBuilder(), + minResourceFee: "100", + cost: { cpuInsns: "0", memBytes: "0" }, + results: [], + } as Api.SimulateTransactionSuccessResponse); + + const result = await facilitator.verify(validPayload, validRequirements); + expect(result.isValid).toBe(false); + expect(result.invalidReason).toBe("invalid_exact_stellar_payload_no_transfer_events"); + }); + + it("should reject when a contract event is not a transfer", async () => { + const mockNonTransferEvent = createMockContractEvent({ + from: CLIENT_PUBLIC, + to: FACILITATOR_PUBLIC, + amount: BigInt(10000), + fnName: "mint", // ❌ event is not "transfer" + }); + + vi.mocked(mockServer.simulateTransaction).mockResolvedValueOnce({ + id: "test", + latestLedger: 123, + events: [mockNonTransferEvent], + _parsed: true, + transactionData: new SorobanDataBuilder(), + minResourceFee: "100", + cost: { cpuInsns: "0", memBytes: "0" }, + results: [], + } as Api.SimulateTransactionSuccessResponse); + + const result = await facilitator.verify(validPayload, validRequirements); + expect(result.isValid).toBe(false); + expect(result.invalidReason).toBe("invalid_exact_stellar_payload_event_not_transfer"); + }); + + it("should ignore non-contract events and still accept valid transfer", async () => { + const mockTransferEvent = createMockContractEvent({ + from: CLIENT_PUBLIC, + to: TRANSACTION_RECIPIENT, + amount: BigInt(10000), + contractId: expectedAssetHash(), + }); + const nonContractEvent = createMockSystemEvent(); + + vi.mocked(mockServer.simulateTransaction).mockResolvedValueOnce({ + id: "test", + latestLedger: 123, + events: [nonContractEvent, mockTransferEvent], + _parsed: true, + transactionData: new SorobanDataBuilder(), + minResourceFee: "100", + cost: { cpuInsns: "0", memBytes: "0" }, + results: [], + } as Api.SimulateTransactionSuccessResponse); + + const result = await facilitator.verify(validPayload, validRequirements); + expect(result.isValid).toBe(true); + }); + }); + }); + + describe("🎉 Successful verification", () => { + it("should verify valid payment", async () => { + const result = await facilitator.verify(validPayload, validRequirements); + expect(result).toEqual(validVerifyResponse(CLIENT_PUBLIC)); + expect(stellarUtils.getRpcClient).toHaveBeenCalledWith(STELLAR_TESTNET_CAIP2, undefined); + expect(mockServer.simulateTransaction).toHaveBeenCalled(); + }); + }); +}); diff --git a/typescript/packages/mechanisms/stellar/test/unit/server.test.ts b/typescript/packages/mechanisms/stellar/test/unit/server.test.ts new file mode 100644 index 0000000000..3cc2814864 --- /dev/null +++ b/typescript/packages/mechanisms/stellar/test/unit/server.test.ts @@ -0,0 +1,156 @@ +import { describe, it, expect } from "vitest"; +import { + USDC_PUBNET_ADDRESS, + USDC_TESTNET_ADDRESS, + STELLAR_PUBNET_CAIP2, + STELLAR_TESTNET_CAIP2, +} from "../../src/constants"; +import { ExactStellarScheme } from "../../src/exact/server/scheme"; + +describe("ExactStellarScheme", () => { + const server = new ExactStellarScheme(); + + describe("parsePrice", () => { + describe("Stellar Pubnet network", () => { + const network = STELLAR_PUBNET_CAIP2; + + it("should parse dollar string prices", async () => { + const result = await server.parsePrice("$0.10", network); + expect(result.amount).toBe("1000000"); // 0.10 USDC = 1000000 smallest units (7 decimals) + expect(result.asset).toBe(USDC_PUBNET_ADDRESS); + expect(result.extra).toEqual({}); + }); + + it("should parse simple number string prices", async () => { + const result = await server.parsePrice("0.10", network); + expect(result.amount).toBe("1000000"); + expect(result.asset).toBe(USDC_PUBNET_ADDRESS); + }); + + it("should parse number prices", async () => { + const result = await server.parsePrice(0.1, network); + expect(result.amount).toBe("1000000"); + expect(result.asset).toBe(USDC_PUBNET_ADDRESS); + }); + + it("should handle larger amounts", async () => { + const result = await server.parsePrice("100.50", network); + expect(result.amount).toBe("1005000000"); // 100.50 USDC + }); + + it("should handle whole numbers", async () => { + const result = await server.parsePrice("1", network); + expect(result.amount).toBe("10000000"); // 1 USDC (7 decimals) + }); + + it("should avoid floating-point rounding error", async () => { + const result = await server.parsePrice("$4.02", network); + expect(result.amount).toBe("40200000"); // 4.02 USDC + }); + }); + + describe("Stellar Testnet network", () => { + const network = STELLAR_TESTNET_CAIP2; + + it("should use Testnet USDC address", async () => { + const result = await server.parsePrice("1.00", network); + expect(result.asset).toBe(USDC_TESTNET_ADDRESS); + expect(result.amount).toBe("10000000"); + }); + }); + + describe("pre-parsed price objects", () => { + it("should handle pre-parsed price objects with asset", async () => { + const result = await server.parsePrice( + { + amount: "123456", + asset: "CBIELTK6YBZJU5UP2WWQEUCYKLPU6AUNZ2BQ4WWFEIE3USCIHMXQDAMA", + extra: { foo: "bar" }, + }, + STELLAR_PUBNET_CAIP2, + ); + expect(result.amount).toBe("123456"); + expect(result.asset).toBe("CBIELTK6YBZJU5UP2WWQEUCYKLPU6AUNZ2BQ4WWFEIE3USCIHMXQDAMA"); + expect(result.extra).toEqual({ foo: "bar" }); + }); + + it("should throw for price objects without asset", async () => { + await expect( + async () => await server.parsePrice({ amount: "123456" } as never, STELLAR_PUBNET_CAIP2), + ).rejects.toThrow("Asset address must be specified"); + }); + }); + + describe("error cases", () => { + it("should throw for invalid money formats", async () => { + await expect( + async () => await server.parsePrice("not-a-price!", STELLAR_PUBNET_CAIP2), + ).rejects.toThrow("Invalid money format"); + }); + + it("should throw for invalid amounts", async () => { + await expect( + async () => await server.parsePrice("abc", STELLAR_PUBNET_CAIP2), + ).rejects.toThrow("Invalid money format"); + }); + }); + }); + + describe("enhancePaymentRequirements", () => { + it("should add areFeesSponsored from facilitator to payment requirements", async () => { + const requirements = { + scheme: "exact", + network: STELLAR_PUBNET_CAIP2, + asset: USDC_PUBNET_ADDRESS, + amount: "1000000", + payTo: "GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5", + maxTimeoutSeconds: 3600, + extra: {}, + }; + + const result = await server.enhancePaymentRequirements( + requirements as never, + { + x402Version: 2, + scheme: "exact", + network: STELLAR_PUBNET_CAIP2, + extra: { areFeesSponsored: true }, + }, + [], + ); + + expect(result).toEqual({ + ...requirements, + extra: { areFeesSponsored: true }, + }); + }); + + it("should preserve existing extra fields", async () => { + const requirements = { + scheme: "exact", + network: STELLAR_TESTNET_CAIP2, + asset: USDC_TESTNET_ADDRESS, + amount: "1000000", + payTo: "GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5", + maxTimeoutSeconds: 3600, + extra: { custom: "value" }, + }; + + const result = await server.enhancePaymentRequirements( + requirements as never, + { + x402Version: 2, + scheme: "exact", + network: STELLAR_TESTNET_CAIP2, + extra: { areFeesSponsored: true }, + }, + [], + ); + + expect(result.extra).toEqual({ + areFeesSponsored: true, + custom: "value", + }); + }); + }); +}); diff --git a/typescript/packages/mechanisms/stellar/test/unit/shared.test.ts b/typescript/packages/mechanisms/stellar/test/unit/shared.test.ts new file mode 100644 index 0000000000..c8beb44663 --- /dev/null +++ b/typescript/packages/mechanisms/stellar/test/unit/shared.test.ts @@ -0,0 +1,155 @@ +import { + SorobanDataBuilder, + xdr, + Networks as StellarNetworks, + Transaction, +} from "@stellar/stellar-sdk"; +import { AssembledTransaction } from "@stellar/stellar-sdk/contract"; +import { Api } from "@stellar/stellar-sdk/rpc"; +import { beforeEach, describe, it, expect, vi } from "vitest"; +import { STELLAR_TESTNET_CAIP2 } from "../../src/constants"; +import { ExactStellarScheme } from "../../src/exact/client/scheme"; +import { gatherAuthEntrySignatureStatus, handleSimulationResult } from "../../src/shared"; +import { createEd25519Signer } from "../../src/signer"; +import * as stellarUtils from "../../src/utils"; +import type { PaymentRequirements } from "@x402/core/types"; + +vi.mock("../../src/utils", async () => { + const actual = await vi.importActual("../../src/utils"); + return { + ...actual, + getEstimatedLedgerCloseTimeSeconds: vi.fn().mockResolvedValue(5), + getRpcClient: vi.fn(), + }; +}); + +describe("Stellar Shared Utilities", () => { + describe("handleSimulationResult", () => { + it("should throw error when simulation is undefined", () => { + expect(() => handleSimulationResult(undefined)).toThrow("Simulation result is undefined"); + }); + + it("should throw error when simulation has type RESTORE", () => { + const mockRestoreSimulation: Api.SimulateTransactionResponse = { + id: "test-id", + latestLedger: 12345, + events: [], + _parsed: true, + result: { + auth: [], + retval: xdr.ScVal.scvVoid(), + }, + restorePreamble: { + minResourceFee: "100", + transactionData: new SorobanDataBuilder(), + }, + transactionData: new SorobanDataBuilder(), + minResourceFee: "100", + } as Api.SimulateTransactionRestoreResponse; + + expect(() => handleSimulationResult(mockRestoreSimulation)).toThrow( + /Stellar simulation result has type "RESTORE"/, + ); + }); + + it("should throw error when simulation has type ERROR", () => { + const mockErrorSimulation: Api.SimulateTransactionResponse = { + id: "test-id", + latestLedger: 12345, + _parsed: true, + error: "Transaction simulation failed: insufficient balance", + } as Api.SimulateTransactionErrorResponse; + + expect(() => handleSimulationResult(mockErrorSimulation)).toThrow( + /Stellar simulation failed with error message: Transaction simulation failed: insufficient balance/, + ); + }); + + it("should handle simulation with empty error message", () => { + const mockErrorSimulation: Api.SimulateTransactionResponse = { + id: "test-id", + latestLedger: 12345, + _parsed: true, + error: "", + } as Api.SimulateTransactionErrorResponse; + + expect(() => handleSimulationResult(mockErrorSimulation)).toThrow( + /Stellar simulation failed/, + ); + }); + + it("should not throw error when simulation is successful", () => { + const mockSuccessSimulation: Api.SimulateTransactionResponse = { + id: "test-id", + latestLedger: 12345, + events: [], + _parsed: true, + transactionData: new SorobanDataBuilder(), + minResourceFee: "100", + } as Api.SimulateTransactionSuccessResponse; + + expect(() => handleSimulationResult(mockSuccessSimulation)).not.toThrow(); + }); + }); + + describe("gatherAuthEntrySignatureStatus", () => { + const CLIENT_SECRET = "SDV3OZOPGIO6GQAVI7T6ZJ7NSNFB26JX6QZYCI64TBC7BAZY6FQVAXXK"; + const CLIENT_PUBLIC = "GBBO4ZDDZTSM2IUKQYBAST3CFHNPFXECGEFTGWTA2WELR2BIWDK57UVE"; + + const mockRpcServer = { + getLatestLedger: vi.fn().mockResolvedValue({ sequence: 100000 }), + }; + + beforeEach(() => { + vi.mocked(stellarUtils.getRpcClient).mockReturnValue(mockRpcServer as never); + }); + + // paymenrRequirements is used to create a valid payload for the test + const paymentRequirements: PaymentRequirements = { + scheme: "exact", + network: STELLAR_TESTNET_CAIP2, + asset: "CBIELTK6YBZJU5UP2WWQEUCYKLPU6AUNZ2BQ4WWFEIE3USCIHMXQDAMA", + amount: "1000000", + payTo: "GCHEI4PQEFJOA27MNZRPQNLGURS6KASW76X5UZCUZIXCOJLKXYCXOR2W", + maxTimeoutSeconds: 60, + extra: { + areFeesSponsored: true, + }, + }; + + it("should identify signed accounts and no pending signatures", async () => { + const signer = createEd25519Signer(CLIENT_SECRET, STELLAR_TESTNET_CAIP2); + const signedTxJson = + "eyJtZXRob2QiOiJ0cmFuc2ZlciIsInR4IjoiQUFBQUFnQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQURsM0lBQUFBQUFBQUFBUUFBQUFFQUFBQUFBQUFBQUFBQUFBQnBGcEdGQUFBQUFBQUFBQUVBQUFBQUFBQUFHQUFBQUFBQUFBQUJVRVhOWHNCeW1uYVAxYTBDVUZoUzMwOENqYzZERGxyRklnbTZTRWc3THdFQUFBQUlkSEpoYm5ObVpYSUFBQUFEQUFBQUVnQUFBQUFBQUFBQVF1NWtZOHprelNLS2hnSUpUMklwMnZMY2dqRUxNMXBnMVlpNDZDaXcxZDhBQUFBU0FBQUFBQUFBQUFDT1JISHdJVkxnYSt4dVl2ZzFacVJsNVFKVy82L2FaRlRLTGljbGFyNEZkd0FBQUFvQUFBQUFBQUFBQUFBQUFBQUFBQ2NRQUFBQUFRQUFBQUVBQUFBQUFBQUFBRUx1WkdQTTVNMGlpb1lDQ1U5aUtkcnkzSUl4Q3pOYVlOV0l1T2dvc05YZlh4amsrOHlZOGhnQUk4ck9BQUFBRUFBQUFBRUFBQUFCQUFBQUVRQUFBQUVBQUFBQ0FBQUFEd0FBQUFwd2RXSnNhV05mYTJWNUFBQUFBQUFOQUFBQUlFTHVaR1BNNU0waWlvWUNDVTlpS2RyeTNJSXhDek5hWU5XSXVPZ29zTlhmQUFBQUR3QUFBQWx6YVdkdVlYUjFjbVVBQUFBQUFBQU5BQUFBUUl2bjJjU3VLbFl5TU96T0pTWnkwc0VaN3dkN1QwYmdSQ0ZxZjg1M3VXQXFVcjE1ZUpycXNqVjROUVpTQW05WXNWbHZEcEUrSFRLc3pUQUVBaTJBRkFnQUFBQUFBQUFBQVZCRnpWN0FjcHAyajlXdEFsQllVdDlQQW8zT2d3NWF4U0lKdWtoSU95OEJBQUFBQ0hSeVlXNXpabVZ5QUFBQUF3QUFBQklBQUFBQUFBQUFBRUx1WkdQTTVNMGlpb1lDQ1U5aUtkcnkzSUl4Q3pOYVlOV0l1T2dvc05YZkFBQUFFZ0FBQUFBQUFBQUFqa1J4OENGUzRHdnNibUw0Tldha1plVUNWdit2Mm1SVXlpNG5KV3ErQlhjQUFBQUtBQUFBQUFBQUFBQUFBQUFBQUFBbkVBQUFBQUFBQUFBQkFBQUFBQUFBQUFJQUFBQUFBQUFBQUVMdVpHUE01TTBpaW9ZQ0NVOWlLZHJ5M0lJeEN6TmFZTldJdU9nb3NOWGZBQUFBQmdBQUFBRlFSYzFld0hLYWRvL1ZyUUpRV0ZMZlR3S056b01PV3NVaUNicElTRHN2QVFBQUFCUUFBQUFCQUFBQUF3QUFBQUVBQUFBQVF1NWtZOHprelNLS2hnSUpUMklwMnZMY2dqRUxNMXBnMVlpNDZDaXcxZDhBQUFBQlZWTkVRd0FBQUFCQ1BuMEY4dXl2dit3Wkt5RmFQeHZwYXUyNDJPY0NWS3ZqUVQ0Q0I5NVdzZ0FBQUFFQUFBQUFqa1J4OENGUzRHdnNibUw0Tldha1plVUNWdit2Mm1SVXlpNG5KV3ErQlhjQUFBQUJWVk5FUXdBQUFBQkNQbjBGOHV5dnYrd1pLeUZhUHh2cGF1MjQyT2NDVkt2alFUNENCOTVXc2dBQUFBWUFBQUFBQUFBQUFFTHVaR1BNNU0waWlvWUNDVTlpS2RyeTNJSXhDek5hWU5XSXVPZ29zTlhmQUFBQUZWOFk1UHZNbVBJWUFBQUFBQUFMNVRFQUFBRjRBQUFCTkFBQUFBQUFBNWNPQUFBQUFBPT0iLCJzaW11bGF0aW9uUmVzdWx0Ijp7ImF1dGgiOlsiQUFBQUFRQUFBQUFBQUFBQVF1NWtZOHprelNLS2hnSUpUMklwMnZMY2dqRUxNMXBnMVlpNDZDaXcxZDlmR09UN3pKanlHQUFBQUFBQUFBQUJBQUFBQUFBQUFBRlFSYzFld0hLYWRvL1ZyUUpRV0ZMZlR3S056b01PV3NVaUNicElTRHN2QVFBQUFBaDBjbUZ1YzJabGNnQUFBQU1BQUFBU0FBQUFBQUFBQUFCQzdtUmp6T1ROSW9xR0FnbFBZaW5hOHR5Q01Rc3pXbURWaUxqb0tMRFYzd0FBQUJJQUFBQUFBQUFBQUk1RWNmQWhVdUJyN0c1aStEVm1wR1hsQWxiL3I5cGtWTW91SnlWcXZnVjNBQUFBQ2dBQUFBQUFBQUFBQUFBQUFBQUFKeEFBQUFBQSJdLCJyZXR2YWwiOiJBQUFBQVE9PSJ9LCJzaW11bGF0aW9uVHJhbnNhY3Rpb25EYXRhIjoiQUFBQUFBQUFBQUlBQUFBQUFBQUFBRUx1WkdQTTVNMGlpb1lDQ1U5aUtkcnkzSUl4Q3pOYVlOV0l1T2dvc05YZkFBQUFCZ0FBQUFGUVJjMWV3SEthZG8vVnJRSlFXRkxmVHdLTnpvTU9Xc1VpQ2JwSVNEc3ZBUUFBQUJRQUFBQUJBQUFBQXdBQUFBRUFBQUFBUXU1a1k4emt6U0tLaGdJSlQySXAydkxjZ2pFTE0xcGcxWWk0NkNpdzFkOEFBQUFCVlZORVF3QUFBQUJDUG4wRjh1eXZ2K3daS3lGYVB4dnBhdTI0Mk9jQ1ZLdmpRVDRDQjk1V3NnQUFBQUVBQUFBQWprUng4Q0ZTNEd2c2JtTDROV2FrWmVVQ1Z2K3YybVJVeWk0bkpXcStCWGNBQUFBQlZWTkVRd0FBQUFCQ1BuMEY4dXl2dit3Wkt5RmFQeHZwYXUyNDJPY0NWS3ZqUVQ0Q0I5NVdzZ0FBQUFZQUFBQUFBQUFBQUVMdVpHUE01TTBpaW9ZQ0NVOWlLZHJ5M0lJeEN6TmFZTldJdU9nb3NOWGZBQUFBRlY4WTVQdk1tUElZQUFBQUFBQUw1VEVBQUFGNEFBQUJOQUFBQUFBQUE1Y08ifQ=="; + const { tx: transactionXDR } = JSON.parse( + Buffer.from(signedTxJson, "base64").toString("utf8"), + ); + + let needsSigning: string[] = [CLIENT_PUBLIC]; + vi.spyOn(AssembledTransaction, "build").mockResolvedValue({ + simulation: {} as Api.SimulateTransactionSuccessResponse, + needsNonInvokerSigningBy: vi.fn(() => { + const result = needsSigning; + needsSigning = []; + return result; + }), + signAuthEntries: vi.fn().mockResolvedValue(undefined), + simulate: vi.fn().mockResolvedValue(undefined), + built: { toXDR: () => transactionXDR }, + } as unknown as AssembledTransaction); + + const scheme = new ExactStellarScheme(signer); + const payload = await scheme.createPaymentPayload(1, paymentRequirements); + + if (!("transaction" in payload.payload)) { + throw new Error("Expected Stellar payload with transaction property"); + } + + const tx = new Transaction(payload.payload.transaction as string, StellarNetworks.TESTNET); + const status = gatherAuthEntrySignatureStatus({ transaction: tx }); + + expect(status.alreadySigned).toContain(CLIENT_PUBLIC); + expect(status.pendingSignature).toHaveLength(0); + }); + }); +}); diff --git a/typescript/packages/mechanisms/stellar/test/unit/signer.test.ts b/typescript/packages/mechanisms/stellar/test/unit/signer.test.ts new file mode 100644 index 0000000000..3ebe6d6038 --- /dev/null +++ b/typescript/packages/mechanisms/stellar/test/unit/signer.test.ts @@ -0,0 +1,148 @@ +import { Keypair, Networks as StellarNetworks } from "@stellar/stellar-sdk"; +import { AssembledTransaction } from "@stellar/stellar-sdk/contract"; +import { describe, expect, it } from "vitest"; +import { DEFAULT_TESTNET_RPC_URL, STELLAR_TESTNET_CAIP2 } from "../../src/constants"; +import { + createEd25519Signer, + isClientStellarSigner, + isFacilitatorStellarSigner, +} from "../../src/signer"; + +describe("Stellar Ed25519 Signer", () => { + const validSecret = "SDV3OZOPGIO6GQAVI7T6ZJ7NSNFB26JX6QZYCI64TBC7BAZY6FQVAXXK"; + const validPublicKey = "GBBO4ZDDZTSM2IUKQYBAST3CFHNPFXECGEFTGWTA2WELR2BIWDK57UVE"; + + describe("createEd25519Signer", () => { + it("should create signer with all required methods", async () => { + const signer = createEd25519Signer(validSecret, STELLAR_TESTNET_CAIP2); + + expect(signer.address).toBe(validPublicKey); + expect(signer.signAuthEntry).toBeInstanceOf(Function); + expect(signer.signTransaction).toBeInstanceOf(Function); + }); + + it("should create different signers for different keys", async () => { + const secret1 = "SA6LFVPCYMDQILBRXQ2B2HRPK6DV2TX4FTQQQHWFPSCSY4H2RTCD3XAK"; + const secret2 = "SBFCBBFETW6U5HSOADUUXTQMUEXK7DIQLLATM6OVKKBQNG3I3EWIYJAW"; + + const signer1 = createEd25519Signer(secret1, STELLAR_TESTNET_CAIP2); + const signer2 = createEd25519Signer(secret2, STELLAR_TESTNET_CAIP2); + + expect(signer1.address).not.toBe(signer2.address); + }); + + it("should throw for invalid secret key format", () => { + expect(() => createEd25519Signer("INVALID_SECRET_KEY", STELLAR_TESTNET_CAIP2)).toThrow(); + }); + + it("should throw for empty string", () => { + expect(() => createEd25519Signer("", STELLAR_TESTNET_CAIP2)).toThrow(); + }); + + it("should throw for public key instead of secret", () => { + expect(() => createEd25519Signer(validPublicKey, STELLAR_TESTNET_CAIP2)).toThrow(); + }); + + it("should throw for invalid network", () => { + expect(() => createEd25519Signer(validSecret, "invalid:network")).toThrow( + "Unknown Stellar network: invalid:network", + ); + }); + + it("should create a signer that can sign auth entries", async () => { + const unsignedTxXDR = + "eyJtZXRob2QiOiJ0cmFuc2ZlciIsInR4IjoiQUFBQUFnQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQURsM0lBQUFBQUFBQUFBUUFBQUFFQUFBQUFBQUFBQUFBQUFBQnBGcEdGQUFBQUFBQUFBQUVBQUFBQUFBQUFHQUFBQUFBQUFBQUJVRVhOWHNCeW1uYVAxYTBDVUZoUzMwOENqYzZERGxyRklnbTZTRWc3THdFQUFBQUlkSEpoYm5ObVpYSUFBQUFEQUFBQUVnQUFBQUFBQUFBQVF1NWtZOHprelNLS2hnSUpUMklwMnZMY2dqRUxNMXBnMVlpNDZDaXcxZDhBQUFBU0FBQUFBQUFBQUFDT1JISHdJVkxnYSt4dVl2ZzFacVJsNVFKVy82L2FaRlRLTGljbGFyNEZkd0FBQUFvQUFBQUFBQUFBQUFBQUFBQUFBQ2NRQUFBQUFRQUFBQUVBQUFBQUFBQUFBRUx1WkdQTTVNMGlpb1lDQ1U5aUtkcnkzSUl4Q3pOYVlOV0l1T2dvc05YZlh4amsrOHlZOGhnQUFBQUFBQUFBQVFBQUFBQUFBQUFCVUVYTlhzQnltbmFQMWEwQ1VGaFMzMDhDamM2RERsckZJZ202U0VnN0x3RUFBQUFJZEhKaGJuTm1aWElBQUFBREFBQUFFZ0FBQUFBQUFBQUFRdTVrWTh6a3pTS0toZ0lKVDJJcDJ2TGNnakVMTTFwZzFZaTQ2Q2l3MWQ4QUFBQVNBQUFBQUFBQUFBQ09SSEh3SVZMZ2EreHVZdmcxWnFSbDVRSlcvNi9hWkZUS0xpY2xhcjRGZHdBQUFBb0FBQUFBQUFBQUFBQUFBQUFBQUNjUUFBQUFBQUFBQUFFQUFBQUFBQUFBQWdBQUFBQUFBQUFBUXU1a1k4emt6U0tLaGdJSlQySXAydkxjZ2pFTE0xcGcxWWk0NkNpdzFkOEFBQUFHQUFBQUFWQkZ6VjdBY3BwMmo5V3RBbEJZVXQ5UEFvM09ndzVheFNJSnVraElPeThCQUFBQUZBQUFBQUVBQUFBREFBQUFBUUFBQUFCQzdtUmp6T1ROSW9xR0FnbFBZaW5hOHR5Q01Rc3pXbURWaUxqb0tMRFYzd0FBQUFGVlUwUkRBQUFBQUVJK2ZRWHk3SysvN0JrcklWby9HK2xxN2JqWTV3SlVxK05CUGdJSDNsYXlBQUFBQVFBQUFBQ09SSEh3SVZMZ2EreHVZdmcxWnFSbDVRSlcvNi9hWkZUS0xpY2xhcjRGZHdBQUFBRlZVMFJEQUFBQUFFSStmUVh5N0srLzdCa3JJVm8vRytscTdialk1d0pVcStOQlBnSUgzbGF5QUFBQUJnQUFBQUFBQUFBQVF1NWtZOHprelNLS2hnSUpUMklwMnZMY2dqRUxNMXBnMVlpNDZDaXcxZDhBQUFBVlh4amsrOHlZOGhnQUFBQUFBQXZsTVFBQUFYZ0FBQUUwQUFBQUFBQURsdzRBQUFBQSIsInNpbXVsYXRpb25SZXN1bHQiOnsiYXV0aCI6WyJBQUFBQVFBQUFBQUFBQUFBUXU1a1k4emt6U0tLaGdJSlQySXAydkxjZ2pFTE0xcGcxWWk0NkNpdzFkOWZHT1Q3ekpqeUdBQUFBQUFBQUFBQkFBQUFBQUFBQUFGUVJjMWV3SEthZG8vVnJRSlFXRkxmVHdLTnpvTU9Xc1VpQ2JwSVNEc3ZBUUFBQUFoMGNtRnVjMlpsY2dBQUFBTUFBQUFTQUFBQUFBQUFBQUJDN21SanpPVE5Jb3FHQWdsUFlpbmE4dHlDTVFzeldtRFZpTGpvS0xEVjN3QUFBQklBQUFBQUFBQUFBSTVFY2ZBaFV1QnI3RzVpK0RWbXBHWGxBbGIvcjlwa1ZNb3VKeVZxdmdWM0FBQUFDZ0FBQUFBQUFBQUFBQUFBQUFBQUp4QUFBQUFBIl0sInJldHZhbCI6IkFBQUFBUT09In0sInNpbXVsYXRpb25UcmFuc2FjdGlvbkRhdGEiOiJBQUFBQUFBQUFBSUFBQUFBQUFBQUFFTHVaR1BNNU0waWlvWUNDVTlpS2RyeTNJSXhDek5hWU5XSXVPZ29zTlhmQUFBQUJnQUFBQUZRUmMxZXdIS2Fkby9WclFKUVdGTGZUd0tOem9NT1dzVWlDYnBJU0RzdkFRQUFBQlFBQUFBQkFBQUFBd0FBQUFFQUFBQUFRdTVrWTh6a3pTS0toZ0lKVDJJcDJ2TGNnakVMTTFwZzFZaTQ2Q2l3MWQ4QUFBQUJWVk5FUXdBQUFBQkNQbjBGOHV5dnYrd1pLeUZhUHh2cGF1MjQyT2NDVkt2alFUNENCOTVXc2dBQUFBRUFBQUFBamtSeDhDRlM0R3ZzYm1MNE5XYWtaZVVDVnYrdjJtUlV5aTRuSldxK0JYY0FBQUFCVlZORVF3QUFBQUJDUG4wRjh1eXZ2K3daS3lGYVB4dnBhdTI0Mk9jQ1ZLdmpRVDRDQjk1V3NnQUFBQVlBQUFBQUFBQUFBRUx1WkdQTTVNMGlpb1lDQ1U5aUtkcnkzSUl4Q3pOYVlOV0l1T2dvc05YZkFBQUFGVjhZNVB2TW1QSVlBQUFBQUFBTDVURUFBQUY0QUFBQk5BQUFBQUFBQTVjTyJ9"; + const expectedSignedTxXDR = + "eyJtZXRob2QiOiJ0cmFuc2ZlciIsInR4IjoiQUFBQUFnQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQURsM0lBQUFBQUFBQUFBUUFBQUFFQUFBQUFBQUFBQUFBQUFBQnBGcEdGQUFBQUFBQUFBQUVBQUFBQUFBQUFHQUFBQUFBQUFBQUJVRVhOWHNCeW1uYVAxYTBDVUZoUzMwOENqYzZERGxyRklnbTZTRWc3THdFQUFBQUlkSEpoYm5ObVpYSUFBQUFEQUFBQUVnQUFBQUFBQUFBQVF1NWtZOHprelNLS2hnSUpUMklwMnZMY2dqRUxNMXBnMVlpNDZDaXcxZDhBQUFBU0FBQUFBQUFBQUFDT1JISHdJVkxnYSt4dVl2ZzFacVJsNVFKVy82L2FaRlRLTGljbGFyNEZkd0FBQUFvQUFBQUFBQUFBQUFBQUFBQUFBQ2NRQUFBQUFRQUFBQUVBQUFBQUFBQUFBRUx1WkdQTTVNMGlpb1lDQ1U5aUtkcnkzSUl4Q3pOYVlOV0l1T2dvc05YZlh4amsrOHlZOGhnQUk4ck9BQUFBRUFBQUFBRUFBQUFCQUFBQUVRQUFBQUVBQUFBQ0FBQUFEd0FBQUFwd2RXSnNhV05mYTJWNUFBQUFBQUFOQUFBQUlFTHVaR1BNNU0waWlvWUNDVTlpS2RyeTNJSXhDek5hWU5XSXVPZ29zTlhmQUFBQUR3QUFBQWx6YVdkdVlYUjFjbVVBQUFBQUFBQU5BQUFBUUl2bjJjU3VLbFl5TU96T0pTWnkwc0VaN3dkN1QwYmdSQ0ZxZjg1M3VXQXFVcjE1ZUpycXNqVjROUVpTQW05WXNWbHZEcEUrSFRLc3pUQUVBaTJBRkFnQUFBQUFBQUFBQVZCRnpWN0FjcHAyajlXdEFsQllVdDlQQW8zT2d3NWF4U0lKdWtoSU95OEJBQUFBQ0hSeVlXNXpabVZ5QUFBQUF3QUFBQklBQUFBQUFBQUFBRUx1WkdQTTVNMGlpb1lDQ1U5aUtkcnkzSUl4Q3pOYVlOV0l1T2dvc05YZkFBQUFFZ0FBQUFBQUFBQUFqa1J4OENGUzRHdnNibUw0Tldha1plVUNWdit2Mm1SVXlpNG5KV3ErQlhjQUFBQUtBQUFBQUFBQUFBQUFBQUFBQUFBbkVBQUFBQUFBQUFBQkFBQUFBQUFBQUFJQUFBQUFBQUFBQUVMdVpHUE01TTBpaW9ZQ0NVOWlLZHJ5M0lJeEN6TmFZTldJdU9nb3NOWGZBQUFBQmdBQUFBRlFSYzFld0hLYWRvL1ZyUUpRV0ZMZlR3S056b01PV3NVaUNicElTRHN2QVFBQUFCUUFBQUFCQUFBQUF3QUFBQUVBQUFBQVF1NWtZOHprelNLS2hnSUpUMklwMnZMY2dqRUxNMXBnMVlpNDZDaXcxZDhBQUFBQlZWTkVRd0FBQUFCQ1BuMEY4dXl2dit3Wkt5RmFQeHZwYXUyNDJPY0NWS3ZqUVQ0Q0I5NVdzZ0FBQUFFQUFBQUFqa1J4OENGUzRHdnNibUw0Tldha1plVUNWdit2Mm1SVXlpNG5KV3ErQlhjQUFBQUJWVk5FUXdBQUFBQkNQbjBGOHV5dnYrd1pLeUZhUHh2cGF1MjQyT2NDVkt2alFUNENCOTVXc2dBQUFBWUFBQUFBQUFBQUFFTHVaR1BNNU0waWlvWUNDVTlpS2RyeTNJSXhDek5hWU5XSXVPZ29zTlhmQUFBQUZWOFk1UHZNbVBJWUFBQUFBQUFMNVRFQUFBRjRBQUFCTkFBQUFBQUFBNWNPQUFBQUFBPT0iLCJzaW11bGF0aW9uUmVzdWx0Ijp7ImF1dGgiOlsiQUFBQUFRQUFBQUFBQUFBQVF1NWtZOHprelNLS2hnSUpUMklwMnZMY2dqRUxNMXBnMVlpNDZDaXcxZDlmR09UN3pKanlHQUFBQUFBQUFBQUJBQUFBQUFBQUFBRlFSYzFld0hLYWRvL1ZyUUpRV0ZMZlR3S056b01PV3NVaUNicElTRHN2QVFBQUFBaDBjbUZ1YzJabGNnQUFBQU1BQUFBU0FBQUFBQUFBQUFCQzdtUmp6T1ROSW9xR0FnbFBZaW5hOHR5Q01Rc3pXbURWaUxqb0tMRFYzd0FBQUJJQUFBQUFBQUFBQUk1RWNmQWhVdUJyN0c1aStEVm1wR1hsQWxiL3I5cGtWTW91SnlWcXZnVjNBQUFBQ2dBQUFBQUFBQUFBQUFBQUFBQUFKeEFBQUFBQSJdLCJyZXR2YWwiOiJBQUFBQVE9PSJ9LCJzaW11bGF0aW9uVHJhbnNhY3Rpb25EYXRhIjoiQUFBQUFBQUFBQUlBQUFBQUFBQUFBRUx1WkdQTTVNMGlpb1lDQ1U5aUtkcnkzSUl4Q3pOYVlOV0l1T2dvc05YZkFBQUFCZ0FBQUFGUVJjMWV3SEthZG8vVnJRSlFXRkxmVHdLTnpvTU9Xc1VpQ2JwSVNEc3ZBUUFBQUJRQUFBQUJBQUFBQXdBQUFBRUFBQUFBUXU1a1k4emt6U0tLaGdJSlQySXAydkxjZ2pFTE0xcGcxWWk0NkNpdzFkOEFBQUFCVlZORVF3QUFBQUJDUG4wRjh1eXZ2K3daS3lGYVB4dnBhdTI0Mk9jQ1ZLdmpRVDRDQjk1V3NnQUFBQUVBQUFBQWprUng4Q0ZTNEd2c2JtTDROV2FrWmVVQ1Z2K3YybVJVeWk0bkpXcStCWGNBQUFBQlZWTkVRd0FBQUFCQ1BuMEY4dXl2dit3Wkt5RmFQeHZwYXUyNDJPY0NWS3ZqUVQ0Q0I5NVdzZ0FBQUFZQUFBQUFBQUFBQUVMdVpHUE01TTBpaW9ZQ0NVOWlLZHJ5M0lJeEN6TmFZTldJdU9nb3NOWGZBQUFBRlY4WTVQdk1tUElZQUFBQUFBQUw1VEVBQUFGNEFBQUJOQUFBQUFBQUE1Y08ifQ=="; + + const signer = createEd25519Signer(validSecret, STELLAR_TESTNET_CAIP2); + expect(signer.address).toBe(validPublicKey); + + // parse the unsigned tx XDR into AssembledTransaction + const { method, tx, simulationResult, simulationTransactionData } = JSON.parse( + Buffer.from(unsignedTxXDR, "base64").toString("utf8"), + ); + const recoveredTx = AssembledTransaction.fromJSON( + { + contractId: "CBIELTK6YBZJU5UP2WWQEUCYKLPU6AUNZ2BQ4WWFEIE3USCIHMXQDAMA", + networkPassphrase: StellarNetworks.TESTNET, + rpcUrl: DEFAULT_TESTNET_RPC_URL, + method, + parseResultXdr: result => result, + }, + { tx, simulationResult, simulationTransactionData }, + ); + + // ensure the tx is the same + const recoveredTxXDR = Buffer.from(recoveredTx.toJSON()).toString("base64"); + expect(recoveredTxXDR).toBe(unsignedTxXDR); + + // ensure the Tx is missing the signer we have + let missingSigners = recoveredTx.needsNonInvokerSigningBy(); + expect(missingSigners).toEqual([validPublicKey]); + + // ensure the tx is signed successfully + await recoveredTx.signAuthEntries({ + address: validPublicKey, + signAuthEntry: signer.signAuthEntry, + expiration: 2345678, + }); + missingSigners = recoveredTx.needsNonInvokerSigningBy(); + expect(missingSigners).toHaveLength(0); + + // ensure the result of the signed tx is the same as the expected signed tx + const signedTxXDR = Buffer.from(recoveredTx.toJSON()).toString("base64"); + expect(signedTxXDR).toBe(expectedSignedTxXDR); + }); + }); + + describe("is*StellarSigner", () => { + it("should return true for both when signer has all methods", () => { + const signer = createEd25519Signer(validSecret, STELLAR_TESTNET_CAIP2); + expect(isFacilitatorStellarSigner(signer)).toBe(true); + expect(isClientStellarSigner(signer)).toBe(true); + }); + + it("should return true for client but false for facilitator when signTransaction missing", () => { + const mockSigner = { + address: validPublicKey, + signAuthEntry: async () => ({ signedAuthEntry: "" }), + }; + expect(isFacilitatorStellarSigner(mockSigner)).toBe(false); + expect(isClientStellarSigner(mockSigner)).toBe(true); + }); + + it("should return false for invalid types", () => { + // false for null + expect(isFacilitatorStellarSigner(null)).toBe(false); + expect(isClientStellarSigner(null)).toBe(false); + // false for undefined + expect(isFacilitatorStellarSigner(undefined)).toBe(false); + expect(isClientStellarSigner(undefined)).toBe(false); + // false for string + expect(isFacilitatorStellarSigner("string")).toBe(false); + expect(isClientStellarSigner("string")).toBe(false); + // false for number + expect(isFacilitatorStellarSigner(123)).toBe(false); + expect(isClientStellarSigner(123)).toBe(false); + // false for empty object + expect(isFacilitatorStellarSigner({})).toBe(false); + expect(isClientStellarSigner({})).toBe(false); + // false for object incomplete object + expect(isFacilitatorStellarSigner({ address: "" })).toBe(false); + expect(isClientStellarSigner({ address: "" })).toBe(false); + // false for similar object with non-string address + const invalidSigner = { + address: 123, + signAuthEntry: () => {}, + signTransaction: () => {}, + }; + expect(isFacilitatorStellarSigner(invalidSigner)).toBe(false); + expect(isClientStellarSigner(invalidSigner)).toBe(false); + // false for Stellar keypair + const keypair = Keypair.fromSecret(validSecret); + expect(isFacilitatorStellarSigner(keypair)).toBe(false); + expect(isClientStellarSigner(keypair)).toBe(false); + }); + }); +}); diff --git a/typescript/packages/mechanisms/stellar/test/unit/utils.test.ts b/typescript/packages/mechanisms/stellar/test/unit/utils.test.ts new file mode 100644 index 0000000000..4adaf1a44f --- /dev/null +++ b/typescript/packages/mechanisms/stellar/test/unit/utils.test.ts @@ -0,0 +1,398 @@ +import { Horizon, rpc } from "@stellar/stellar-sdk"; +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { STELLAR_PUBNET_CAIP2, STELLAR_TESTNET_CAIP2 } from "../../src"; +import { + convertToTokenAmount, + DEFAULT_ESTIMATED_LEDGER_SECONDS, + getEstimatedLedgerCloseTimeSeconds, + getNetworkPassphrase, + getRpcClient, + getRpcUrl, + getUsdcAddress, + isStellarNetwork, + RpcConfig, + validateStellarAssetAddress, + validateStellarDestinationAddress, +} from "../../src/utils"; + +// Mock the Stellar SDK +vi.mock("@stellar/stellar-sdk", () => ({ + Horizon: { + Server: vi.fn(), + }, + rpc: { + Server: vi.fn(), + }, +})); + +describe("Stellar RPC Helper Functions", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + describe("validateStellarDestinationAddress", () => { + it("should return true for valid addresses", () => { + expect( + validateStellarDestinationAddress( + "GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN", + ), + ).toBe(true); + }); + + it("should return false for invalid addresses", () => { + expect(validateStellarDestinationAddress("")).toBe(false); + expect(validateStellarDestinationAddress("invalid")).toBe(false); + }); + }); + + describe("validateStellarAssetAddress", () => { + it("should return true for valid C-accounts", () => { + expect( + validateStellarAssetAddress("CCW67TSZV3SSS2HXMBQ5JFGCKJNXKZM7UQUWUZPUTHXSTZLEO7SJMI75"), + ).toBe(true); + }); + + it("should return false for invalid addresses", () => { + expect(validateStellarAssetAddress("")).toBe(false); + expect(validateStellarAssetAddress("invalid")).toBe(false); + }); + }); + + describe("isStellarNetwork", () => { + it("should return true for Stellar pubnet", () => { + expect(isStellarNetwork(STELLAR_PUBNET_CAIP2)).toBe(true); + }); + + it("should return true for Stellar testnet", () => { + expect(isStellarNetwork(STELLAR_TESTNET_CAIP2)).toBe(true); + }); + + it("should return false for invalid networks", () => { + expect(isStellarNetwork("invalid-network" as any)).toBe(false); + expect(isStellarNetwork("" as any)).toBe(false); + }); + + it("should return false for non-Stellar CAIP-2 networks", () => { + expect(isStellarNetwork("eip155:1" as any)).toBe(false); + expect(isStellarNetwork("eip155:8453" as any)).toBe(false); + expect(isStellarNetwork("solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp" as any)).toBe(false); + }); + }); + + describe("getNetworkPassphrase", () => { + it("should return the correct passphrase for stellar (mainnet)", () => { + const result = getNetworkPassphrase(STELLAR_PUBNET_CAIP2); + expect(result).toBe("Public Global Stellar Network ; September 2015"); + }); + + it("should return the correct passphrase for stellar testnet", () => { + const result = getNetworkPassphrase(STELLAR_TESTNET_CAIP2); + expect(result).toBe("Test SDF Network ; September 2015"); + }); + + it("should throw error for unknown network", () => { + expect(() => getNetworkPassphrase("invalid-network" as any)).toThrow( + "Unknown Stellar network: invalid-network", + ); + }); + }); + + describe("getRpcUrl", () => { + describe(STELLAR_TESTNET_CAIP2, () => { + it("should return default testnet URL when no config provided", () => { + const result = getRpcUrl(STELLAR_TESTNET_CAIP2); + expect(result).toBe("https://soroban-testnet.stellar.org"); + }); + + it("should return custom URL when provided in rpcConfig", () => { + const customUrl = "https://custom-stellar-testnet-rpc.example.com"; + const rpcConfig: RpcConfig = { url: customUrl }; + const result = getRpcUrl(STELLAR_TESTNET_CAIP2, rpcConfig); + expect(result).toBe(customUrl); + }); + }); + + describe("stellar mainnet", () => { + it("should throw error when no config provided for mainnet", () => { + expect(() => getRpcUrl(STELLAR_PUBNET_CAIP2)).toThrow( + "Stellar mainnet requires a non-empty rpcUrl. For a list of RPC providers, see https://developers.stellar.org/docs/data/apis/rpc/providers#publicly-accessible-apis", + ); + }); + + it("should throw error when rpcConfig provided without url for mainnet", () => { + const rpcConfig: RpcConfig = {}; + expect(() => getRpcUrl(STELLAR_PUBNET_CAIP2, rpcConfig)).toThrow( + "Stellar mainnet requires a non-empty rpcUrl. For a list of RPC providers, see https://developers.stellar.org/docs/data/apis/rpc/providers#publicly-accessible-apis", + ); + }); + + it("should return custom URL when provided in rpcConfig for mainnet", () => { + const customUrl = "https://custom-stellar-mainnet-rpc.example.com"; + const config: RpcConfig = { url: customUrl }; + const result = getRpcUrl(STELLAR_PUBNET_CAIP2, config); + expect(result).toBe(customUrl); + }); + }); + + describe("invalid networks", () => { + it("should throw error for unknown network", () => { + expect(() => getRpcUrl("invalid-network" as any)).toThrow( + "Unknown Stellar network: invalid-network", + ); + }); + }); + }); + + describe("getRpcClient", () => { + describe(STELLAR_TESTNET_CAIP2, () => { + it("should create RPC client with default testnet URL when no config provided", () => { + const mockServer = { mock: "testnet-server" }; + vi.mocked(rpc.Server).mockReturnValue(mockServer as any); + + const result = getRpcClient(STELLAR_TESTNET_CAIP2); + + expect(rpc.Server).toHaveBeenCalledWith("https://soroban-testnet.stellar.org", { + allowHttp: true, + }); + expect(result).toBe(mockServer); + }); + + it("should create RPC client with custom URL when provided in rpcConfig", () => { + const customUrl = "https://custom-testnet-rpc.com"; + const mockServer = { mock: "testnet-server-custom" }; + vi.mocked(rpc.Server).mockReturnValue(mockServer as any); + + const rpcConfig: RpcConfig = { url: customUrl }; + const result = getRpcClient(STELLAR_TESTNET_CAIP2, rpcConfig); + + expect(rpc.Server).toHaveBeenCalledWith(customUrl, { + allowHttp: true, + }); + expect(result).toBe(mockServer); + }); + + it("should allow HTTP for testnet", () => { + const mockServer = { mock: "testnet-server" }; + vi.mocked(rpc.Server).mockReturnValue(mockServer as any); + + getRpcClient(STELLAR_TESTNET_CAIP2); + + expect(rpc.Server).toHaveBeenCalledWith(expect.any(String), { + allowHttp: true, + }); + }); + }); + + describe("stellar mainnet", () => { + it("should throw error when no config provided for mainnet", () => { + expect(() => getRpcClient(STELLAR_PUBNET_CAIP2)).toThrow( + "Stellar mainnet requires a non-empty rpcUrl. For a list of RPC providers, see https://developers.stellar.org/docs/data/apis/rpc/providers#publicly-accessible-apis", + ); + }); + + it("should create RPC client with custom URL for mainnet", () => { + const customUrl = "https://custom-mainnet-rpc.com"; + const mockServer = { mock: "mainnet-server" }; + vi.mocked(rpc.Server).mockReturnValue(mockServer as any); + + const rpcConfig: RpcConfig = { url: customUrl }; + const result = getRpcClient(STELLAR_PUBNET_CAIP2, rpcConfig); + + expect(rpc.Server).toHaveBeenCalledWith(customUrl, { + allowHttp: false, + }); + expect(result).toBe(mockServer); + }); + + it("should not allow HTTP for mainnet", () => { + const customUrl = "https://custom-mainnet-rpc.com"; + const mockServer = { mock: "mainnet-server" }; + vi.mocked(rpc.Server).mockReturnValue(mockServer as any); + + const rpcConfig: RpcConfig = { url: customUrl }; + getRpcClient(STELLAR_PUBNET_CAIP2, rpcConfig); + expect(rpc.Server).toHaveBeenCalledWith(expect.any(String), { + allowHttp: false, + }); + }); + }); + + describe("invalid networks", () => { + it("should throw error for unknown network", () => { + expect(() => getRpcClient("invalid-network" as any)).toThrow( + "Unknown Stellar network: invalid-network", + ); + }); + + it("should throw error for non-Stellar network", () => { + expect(() => getRpcClient("base" as any)).toThrow("Unknown Stellar network: base"); + }); + }); + }); + + describe("getEstimatedLedgerCloseTimeSeconds", () => { + function mockHorizonServer(records: Array<{ closed_at: string; sequence: number }>) { + const mockCall = vi.fn().mockResolvedValue({ records }); + const mockOrder = vi.fn().mockReturnValue({ call: mockCall }); + const mockLimit = vi.fn().mockReturnValue({ order: mockOrder }); + const mockLedgers = vi.fn().mockReturnValue({ limit: mockLimit }); + vi.mocked(Horizon.Server).mockImplementation(() => ({ ledgers: mockLedgers }) as any); + return { mockCall, mockOrder, mockLimit, mockLedgers }; + } + + it("should compute seconds per ledger from Horizon SDK ledgers response", async () => { + const baseTs = 1734032457; + const records = [105, 104, 103, 102, 101, 100].map((seq, i) => ({ + sequence: seq, + closed_at: new Date((baseTs + (5 - i) * 3) * 1000).toISOString(), + })); + const { mockLedgers, mockLimit, mockOrder } = mockHorizonServer(records); + + const result = await getEstimatedLedgerCloseTimeSeconds(STELLAR_TESTNET_CAIP2); + + expect(result).toBe(3); + expect(Horizon.Server).toHaveBeenCalledWith("https://horizon-testnet.stellar.org"); + expect(mockLedgers).toHaveBeenCalled(); + expect(mockLimit).toHaveBeenCalledWith(20); + expect(mockOrder).toHaveBeenCalledWith("desc"); + }); + + it("should return DEFAULT_ESTIMATED_LEDGER_SECONDS when SDK call throws", async () => { + const mockCall = vi.fn().mockRejectedValue(new Error("Network error")); + const mockOrder = vi.fn().mockReturnValue({ call: mockCall }); + const mockLimit = vi.fn().mockReturnValue({ order: mockOrder }); + const mockLedgers = vi.fn().mockReturnValue({ limit: mockLimit }); + vi.mocked(Horizon.Server).mockImplementation(() => ({ ledgers: mockLedgers }) as any); + + const result = await getEstimatedLedgerCloseTimeSeconds(STELLAR_TESTNET_CAIP2); + + expect(result).toBe(DEFAULT_ESTIMATED_LEDGER_SECONDS); + }); + + it("should return DEFAULT_ESTIMATED_LEDGER_SECONDS when fewer than 2 records", async () => { + mockHorizonServer([{ sequence: 100, closed_at: "2024-12-13T00:00:57Z" }]); + + const result = await getEstimatedLedgerCloseTimeSeconds(STELLAR_TESTNET_CAIP2); + + expect(result).toBe(DEFAULT_ESTIMATED_LEDGER_SECONDS); + }); + + it("should use pubnet Horizon URL for pubnet network", async () => { + const baseTs = 1734032457; + const records = [102, 101, 100].map((seq, i) => ({ + sequence: seq, + closed_at: new Date((baseTs + (2 - i) * 6) * 1000).toISOString(), + })); + mockHorizonServer(records); + + const result = await getEstimatedLedgerCloseTimeSeconds(STELLAR_PUBNET_CAIP2); + + expect(result).toBe(6); + expect(Horizon.Server).toHaveBeenCalledWith("https://horizon.stellar.org"); + }); + }); + + describe("getUsdcAddress", () => { + it("should return USDC address for mainnet", () => { + const result = getUsdcAddress(STELLAR_PUBNET_CAIP2); + expect(result).toBe("CCW67TSZV3SSS2HXMBQ5JFGCKJNXKZM7UQUWUZPUTHXSTZLEO7SJMI75"); + }); + + it("should return USDC address for testnet", () => { + const result = getUsdcAddress(STELLAR_TESTNET_CAIP2); + expect(result).toBe("CBIELTK6YBZJU5UP2WWQEUCYKLPU6AUNZ2BQ4WWFEIE3USCIHMXQDAMA"); + }); + + it("should throw error for unknown network", () => { + expect(() => getUsdcAddress("invalid-network" as any)).toThrow( + "No USDC address configured for network: invalid-network", + ); + }); + }); + + describe("convertToTokenAmount", () => { + describe("with default 7 decimals", () => { + it("should convert decimal amount correctly", () => { + expect(convertToTokenAmount("0.1")).toBe("1000000"); + expect(convertToTokenAmount("1.5")).toBe("15000000"); + expect(convertToTokenAmount("0.1234567")).toBe("1234567"); + }); + + it("should convert integer amount correctly", () => { + expect(convertToTokenAmount("1")).toBe("10000000"); + expect(convertToTokenAmount("10")).toBe("100000000"); + expect(convertToTokenAmount("0")).toBe("0"); + }); + + it("should handle amounts with trailing zeros", () => { + expect(convertToTokenAmount("1.0")).toBe("10000000"); + expect(convertToTokenAmount("0.1000000")).toBe("1000000"); + }); + + it("should handle very small amounts", () => { + expect(convertToTokenAmount("0.0000001")).toBe("1"); + expect(convertToTokenAmount("0.00000001")).toBe("0"); + }); + + it("should truncate excess decimal places", () => { + expect(convertToTokenAmount("1.12345678")).toBe("11234567"); + }); + }); + + describe("with custom decimals", () => { + it("should convert with 6 decimals", () => { + expect(convertToTokenAmount("1.5", 6)).toBe("1500000"); + expect(convertToTokenAmount("0.1", 6)).toBe("100000"); + }); + + it("should convert with 18 decimals", () => { + expect(convertToTokenAmount("1.0", 18)).toBe("1000000000000000000"); + expect(convertToTokenAmount("0.5", 18)).toBe("500000000000000000"); + }); + + it("should convert with 0 decimals", () => { + expect(convertToTokenAmount("1.5", 0)).toBe("1"); + expect(convertToTokenAmount("2.9", 0)).toBe("2"); + }); + }); + + describe("special cases", () => { + it("should handle negative numbers", () => { + expect(convertToTokenAmount("-1.5")).toBe("-15000000"); + }); + + it("should handle scientific notation input", () => { + // Scientific notation should be correctly converted + // 1e-7 = 0.0000001, which with 7 decimals = 1 + expect(convertToTokenAmount("1e-7")).toBe("1"); + expect(convertToTokenAmount("1e-6")).toBe("10"); + expect(convertToTokenAmount("1.5e-6")).toBe("15"); + }); + + it("should handle very large numbers", () => { + expect(convertToTokenAmount("999999999.9999999")).toBe("9999999999999999"); + }); + }); + + describe("error cases", () => { + it("should throw error for invalid amount", () => { + expect(() => convertToTokenAmount("invalid")).toThrow("Invalid amount: invalid"); + expect(() => convertToTokenAmount("abc")).toThrow("Invalid amount: abc"); + expect(() => convertToTokenAmount("")).toThrow("Invalid amount: "); + }); + + it("should throw error for NaN", () => { + expect(() => convertToTokenAmount("NaN")).toThrow("Invalid amount: NaN"); + }); + + it("should throw error for invalid decimals", () => { + expect(() => convertToTokenAmount("1.5", -1)).toThrow( + "Decimals must be between 0 and 20, got -1", + ); + expect(() => convertToTokenAmount("1.5", 21)).toThrow( + "Decimals must be between 0 and 20, got 21", + ); + }); + }); + }); +}); diff --git a/typescript/packages/mechanisms/stellar/tsconfig.json b/typescript/packages/mechanisms/stellar/tsconfig.json new file mode 100644 index 0000000000..19ba0fba3a --- /dev/null +++ b/typescript/packages/mechanisms/stellar/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "lib": ["ES2020"], + "allowJs": false, + "checkJs": false + }, + "include": ["src", "test"] +} diff --git a/typescript/packages/mechanisms/stellar/tsup.config.ts b/typescript/packages/mechanisms/stellar/tsup.config.ts new file mode 100644 index 0000000000..1143bfeaf5 --- /dev/null +++ b/typescript/packages/mechanisms/stellar/tsup.config.ts @@ -0,0 +1,30 @@ +import { defineConfig } from "tsup"; + +const baseConfig = { + entry: { + index: "src/index.ts", + "exact/client/index": "src/exact/client/index.ts", + "exact/server/index": "src/exact/server/index.ts", + "exact/facilitator/index": "src/exact/facilitator/index.ts", + }, + dts: { + resolve: true, + }, + sourcemap: true, + target: "es2020", +}; + +export default defineConfig([ + { + ...baseConfig, + format: "esm", + outDir: "dist/esm", + clean: true, + }, + { + ...baseConfig, + format: "cjs", + outDir: "dist/cjs", + clean: false, + }, +]); diff --git a/typescript/packages/mechanisms/stellar/vitest.config.ts b/typescript/packages/mechanisms/stellar/vitest.config.ts new file mode 100644 index 0000000000..1d5c581d9c --- /dev/null +++ b/typescript/packages/mechanisms/stellar/vitest.config.ts @@ -0,0 +1,15 @@ +import { loadEnv } from "vite"; +import tsconfigPaths from "vite-tsconfig-paths"; +import { defineConfig } from "vitest/config"; + +export default defineConfig(({ mode }) => ({ + test: { + env: loadEnv(mode, process.cwd(), ""), + exclude: [ + "**/node_modules/**", + "**/dist/**", + "**/test/integrations/**", // Exclude integration tests from default run + ], + }, + plugins: [tsconfigPaths({ projects: ["."] })], +})); diff --git a/typescript/packages/mechanisms/stellar/vitest.integration.config.ts b/typescript/packages/mechanisms/stellar/vitest.integration.config.ts new file mode 100644 index 0000000000..a0e4a65874 --- /dev/null +++ b/typescript/packages/mechanisms/stellar/vitest.integration.config.ts @@ -0,0 +1,13 @@ +import { loadEnv } from "vite"; +import tsconfigPaths from "vite-tsconfig-paths"; +import { defineConfig } from "vitest/config"; + +export default defineConfig(({ mode }) => ({ + test: { + env: loadEnv(mode, process.cwd(), ""), + include: ["test/integrations/**/*.test.ts"], // Only include integration tests + testTimeout: 20000, + hookTimeout: 20000, + }, + plugins: [tsconfigPaths({ projects: ["."] })], +})); diff --git a/typescript/packages/mechanisms/svm/CHANGELOG.md b/typescript/packages/mechanisms/svm/CHANGELOG.md index f15d547fa2..f471bd210b 100644 --- a/typescript/packages/mechanisms/svm/CHANGELOG.md +++ b/typescript/packages/mechanisms/svm/CHANGELOG.md @@ -1,5 +1,33 @@ # @x402/svm Changelog +## 2.8.0 + +### Minor Changes + +- Updated dependencies [067f297] +- Updated dependencies [4c1e44f] +- Updated dependencies [5135fab] + - @x402/core@2.8.0 + +## 2.7.0 + +### Minor Changes + +- Updated dependencies [8931cb3] + - @x402/core@2.7.0 + +## 2.6.0 + +### Minor Changes + +- 7cd93d8: Add in-memory SettlementCache to prevent duplicate SVM transaction settlement during on-chain confirmation window +- Updated dependencies [f41baed] +- Updated dependencies [aeef1bf] +- Updated dependencies [2564781] +- Updated dependencies [b341973] +- Updated dependencies [29fe09a] + - @x402/core@2.6.0 + ## 2.5.0 ### Minor Changes diff --git a/typescript/packages/mechanisms/svm/README.md b/typescript/packages/mechanisms/svm/README.md index 5c9b975f0f..01601da1fa 100644 --- a/typescript/packages/mechanisms/svm/README.md +++ b/typescript/packages/mechanisms/svm/README.md @@ -174,6 +174,16 @@ The exact payment scheme uses SPL Token `TransferChecked` instruction with: - Source/destination ATAs (Associated Token Accounts) - Partial signing (client signs, facilitator completes and submits) +## Duplicate Settlement Protection + +This package includes a built-in `SettlementCache` that prevents a known race condition on Solana where the same payment transaction could be settled multiple times before on-chain confirmation. When the facilitator scheme is registered via `registerExactSvmScheme`, a single `SettlementCache` instance is automatically shared across both V1 and V2 scheme versions. + +The cache rejects concurrent `/settle` calls that carry the same transaction payload, returning a `duplicate_settlement` error for the second and subsequent attempts. Entries are automatically evicted after 120 seconds (approximately twice the Solana blockhash lifetime). + +**No additional configuration is required** — duplicate settlement protection is enabled by default when using the standard registration helpers. + +For full details on the race condition and mitigation strategy, see the [Exact SVM Scheme Specification](../../../../specs/schemes/exact/scheme_exact_svm.md#duplicate-settlement-mitigation-recommended). + ## Development ```bash @@ -196,5 +206,5 @@ pnpm format - `@x402/core` - Core protocol types and client - `@x402/fetch` - HTTP wrapper with automatic payment handling - `@x402/evm` - EVM/Ethereum implementation +- `@x402/stellar` - Stellar implementation - `@solana/web3.js` - Solana JavaScript SDK (peer dependency) - diff --git a/typescript/packages/mechanisms/svm/package.json b/typescript/packages/mechanisms/svm/package.json index 93067cc08e..c810376358 100644 --- a/typescript/packages/mechanisms/svm/package.json +++ b/typescript/packages/mechanisms/svm/package.json @@ -1,6 +1,6 @@ { "name": "@x402/svm", - "version": "2.5.0", + "version": "2.8.0", "main": "./dist/cjs/index.js", "module": "./dist/esm/index.js", "types": "./dist/cjs/index.d.ts", diff --git a/typescript/packages/mechanisms/svm/src/constants.ts b/typescript/packages/mechanisms/svm/src/constants.ts index 15a5463e8a..ea79bb3b86 100644 --- a/typescript/packages/mechanisms/svm/src/constants.ts +++ b/typescript/packages/mechanisms/svm/src/constants.ts @@ -42,6 +42,12 @@ export const DEFAULT_COMPUTE_UNIT_PRICE_MICROLAMPORTS = 1; export const MAX_COMPUTE_UNIT_PRICE_MICROLAMPORTS = 5_000_000; // 5 lamports export const DEFAULT_COMPUTE_UNIT_LIMIT = 20_000; +/** + * How long a transaction is held in the duplicate settlement cache (ms). + * Covers the Solana blockhash lifetime (~60-90s) with margin. + */ +export const SETTLEMENT_TTL_MS = 120_000; + /** * Solana address validation regex (base58, 32-44 characters) */ diff --git a/typescript/packages/mechanisms/svm/src/exact/facilitator/register.ts b/typescript/packages/mechanisms/svm/src/exact/facilitator/register.ts index 7055142191..25d18c6881 100644 --- a/typescript/packages/mechanisms/svm/src/exact/facilitator/register.ts +++ b/typescript/packages/mechanisms/svm/src/exact/facilitator/register.ts @@ -1,5 +1,6 @@ import { x402Facilitator } from "@x402/core/facilitator"; import { Network } from "@x402/core/types"; +import { SettlementCache } from "../../settlement-cache"; import { FacilitatorSvmSigner } from "../../signer"; import { ExactSvmScheme } from "./scheme"; import { ExactSvmSchemeV1 } from "../v1/facilitator/scheme"; @@ -47,11 +48,18 @@ export function registerExactSvmScheme( facilitator: x402Facilitator, config: SvmFacilitatorConfig, ): x402Facilitator { + // Share a single settlement cache across V1 and V2 so that a duplicate + // transaction submitted through one protocol version is also caught by the other. + const settlementCache = new SettlementCache(); + // Register V2 scheme with specified networks - facilitator.register(config.networks, new ExactSvmScheme(config.signer)); + facilitator.register(config.networks, new ExactSvmScheme(config.signer, settlementCache)); // Register all V1 networks - facilitator.registerV1(NETWORKS as Network[], new ExactSvmSchemeV1(config.signer)); + facilitator.registerV1( + NETWORKS as Network[], + new ExactSvmSchemeV1(config.signer, settlementCache), + ); return facilitator; } diff --git a/typescript/packages/mechanisms/svm/src/exact/facilitator/scheme.ts b/typescript/packages/mechanisms/svm/src/exact/facilitator/scheme.ts index 0aa54064db..9ea1acef26 100644 --- a/typescript/packages/mechanisms/svm/src/exact/facilitator/scheme.ts +++ b/typescript/packages/mechanisms/svm/src/exact/facilitator/scheme.ts @@ -29,6 +29,7 @@ import { MAX_COMPUTE_UNIT_PRICE_MICROLAMPORTS, MEMO_PROGRAM_ADDRESS, } from "../../constants"; +import { SettlementCache } from "../../settlement-cache"; import type { FacilitatorSvmSigner } from "../../signer"; import type { ExactSvmPayloadV2 } from "../../types"; import { decodeTransactionFromPayload, getTokenPayerFromTransaction } from "../../utils"; @@ -40,13 +41,21 @@ export class ExactSvmScheme implements SchemeNetworkFacilitator { readonly scheme = "exact"; readonly caipFamily = "solana:*"; + private readonly settlementCache: SettlementCache; + /** * Creates a new ExactSvmFacilitator instance. * * @param signer - The SVM RPC client for facilitator operations + * @param settlementCache - Optional shared settlement cache (one is created if omitted) * @returns ExactSvmFacilitator instance */ - constructor(private readonly signer: FacilitatorSvmSigner) {} + constructor( + private readonly signer: FacilitatorSvmSigner, + settlementCache?: SettlementCache, + ) { + this.settlementCache = settlementCache ?? new SettlementCache(); + } /** * Get mechanism-specific extra data for the supported kinds endpoint. @@ -258,10 +267,10 @@ export class ExactSvmScheme implements SchemeNetworkFacilitator { // Verify transfer amount meets requirements const amount = parsedTransfer.data.amount; - if (amount < BigInt(requirements.amount)) { + if (amount !== BigInt(requirements.amount)) { return { isValid: false, - invalidReason: "invalid_exact_svm_payload_amount_insufficient", + invalidReason: "invalid_exact_svm_payload_amount_mismatch", payer, }; } @@ -348,6 +357,19 @@ export class ExactSvmScheme implements SchemeNetworkFacilitator { }; } + // Duplicate settlement check: reject if this transaction is already being settled. + // Must occur before any async work so concurrent calls for the same tx are caught. + const txKey = exactSvmPayload.transaction; + if (this.settlementCache.isDuplicate(txKey)) { + return { + success: false, + network: payload.accepted.network, + transaction: "", + errorReason: "duplicate_settlement", + payer: valid.payer || "", + }; + } + try { // Extract feePayer from requirements (already validated in verify) const feePayer = requirements.extra.feePayer as Address; diff --git a/typescript/packages/mechanisms/svm/src/exact/v1/facilitator/scheme.ts b/typescript/packages/mechanisms/svm/src/exact/v1/facilitator/scheme.ts index 16d0f465f7..297eaf6ae8 100644 --- a/typescript/packages/mechanisms/svm/src/exact/v1/facilitator/scheme.ts +++ b/typescript/packages/mechanisms/svm/src/exact/v1/facilitator/scheme.ts @@ -30,6 +30,7 @@ import { MAX_COMPUTE_UNIT_PRICE_MICROLAMPORTS, MEMO_PROGRAM_ADDRESS, } from "../../../constants"; +import { SettlementCache } from "../../../settlement-cache"; import type { FacilitatorSvmSigner } from "../../../signer"; import type { ExactSvmPayloadV1 } from "../../../types"; import { decodeTransactionFromPayload, getTokenPayerFromTransaction } from "../../../utils"; @@ -41,13 +42,21 @@ export class ExactSvmSchemeV1 implements SchemeNetworkFacilitator { readonly scheme = "exact"; readonly caipFamily = "solana:*"; + private readonly settlementCache: SettlementCache; + /** * Creates a new ExactSvmFacilitatorV1 instance. * * @param signer - The SVM RPC client for facilitator operations + * @param settlementCache - Optional shared settlement cache (one is created if omitted) * @returns ExactSvmFacilitatorV1 instance */ - constructor(private readonly signer: FacilitatorSvmSigner) {} + constructor( + private readonly signer: FacilitatorSvmSigner, + settlementCache?: SettlementCache, + ) { + this.settlementCache = settlementCache ?? new SettlementCache(); + } /** * Get mechanism-specific extra data for the supported kinds endpoint. @@ -259,12 +268,12 @@ export class ExactSvmSchemeV1 implements SchemeNetworkFacilitator { }; } - // Verify transfer amount meets requirements + // Verify transfer amount exactly matches requirements const amount = parsedTransfer.data.amount; - if (amount < BigInt(requirementsV1.maxAmountRequired)) { + if (amount !== BigInt(requirementsV1.maxAmountRequired)) { return { isValid: false, - invalidReason: "invalid_exact_svm_payload_amount_insufficient", + invalidReason: "invalid_exact_svm_payload_amount_mismatch", payer, }; } @@ -352,6 +361,19 @@ export class ExactSvmSchemeV1 implements SchemeNetworkFacilitator { }; } + // Duplicate settlement check: reject if this transaction is already being settled. + // Must occur before any async work so concurrent calls for the same tx are caught. + const txKey = exactSvmPayload.transaction; + if (this.settlementCache.isDuplicate(txKey)) { + return { + success: false, + network: payloadV1.network, + transaction: "", + errorReason: "duplicate_settlement", + payer: valid.payer || "", + }; + } + try { // Extract feePayer from requirements (already validated in verify) const feePayer = requirements.extra.feePayer as Address; diff --git a/typescript/packages/mechanisms/svm/src/index.ts b/typescript/packages/mechanisms/svm/src/index.ts index 3f1e736fd9..ef5eca0f76 100644 --- a/typescript/packages/mechanisms/svm/src/index.ts +++ b/typescript/packages/mechanisms/svm/src/index.ts @@ -20,6 +20,9 @@ export type { // Export payload types export type { ExactSvmPayloadV1, ExactSvmPayloadV2 } from "./types"; +// Export settlement cache (shared across V1/V2 facilitator instances) +export { SettlementCache } from "./settlement-cache"; + // Export constants export * from "./constants"; diff --git a/typescript/packages/mechanisms/svm/src/settlement-cache.ts b/typescript/packages/mechanisms/svm/src/settlement-cache.ts new file mode 100644 index 0000000000..a8941b4510 --- /dev/null +++ b/typescript/packages/mechanisms/svm/src/settlement-cache.ts @@ -0,0 +1,46 @@ +import { SETTLEMENT_TTL_MS } from "./constants"; + +/** + * In-memory cache for deduplicating concurrent settlement requests. + * + * A single instance should be shared across V1 and V2 facilitator scheme + * instances so that a transaction submitted through one protocol version is + * also blocked on the other. Because Node.js is single-threaded, no lock + * is required — the cache check + insert must simply occur before the first + * `await` in the settle path. + */ +export class SettlementCache { + private readonly entries = new Map(); + + /** + * Returns `true` if `key` is already pending settlement (duplicate), + * or `false` after recording it as newly pending. + * + * Callers should reject the settlement when this returns `true`. + * + * @param key - The unique identifier for the settlement (typically the transaction signature). + * @returns `true` if the key was already present (duplicate); `false` otherwise. + */ + isDuplicate(key: string): boolean { + this.prune(); + if (this.entries.has(key)) { + return true; + } + this.entries.set(key, Date.now()); + return false; + } + + /** + * Remove entries older than the settlement TTL. + */ + private prune(): void { + const cutoff = Date.now() - SETTLEMENT_TTL_MS; + for (const [key, timestamp] of this.entries) { + if (timestamp < cutoff) { + this.entries.delete(key); + } else { + break; + } + } + } +} diff --git a/typescript/packages/mechanisms/svm/test/unit/facilitator.test.ts b/typescript/packages/mechanisms/svm/test/unit/facilitator.test.ts index 5d27bb523f..cb7fe1f535 100644 --- a/typescript/packages/mechanisms/svm/test/unit/facilitator.test.ts +++ b/typescript/packages/mechanisms/svm/test/unit/facilitator.test.ts @@ -1,7 +1,10 @@ import { describe, it, expect, vi, beforeEach } from "vitest"; import { ExactSvmScheme } from "../../src/exact/facilitator/scheme"; +import { ExactSvmSchemeV1 } from "../../src/exact/v1/facilitator/scheme"; +import { SettlementCache } from "../../src/settlement-cache"; import type { FacilitatorSvmSigner } from "../../src/signer"; import type { PaymentRequirements, PaymentPayload } from "@x402/core/types"; +import type { PaymentPayloadV1, PaymentRequirementsV1 } from "@x402/core/types/v1"; import { USDC_DEVNET_ADDRESS, SOLANA_DEVNET_CAIP2 } from "../../src/constants"; describe("ExactSvmScheme", () => { @@ -255,4 +258,211 @@ describe("ExactSvmScheme", () => { expect(result.network).toBe(SOLANA_DEVNET_CAIP2); }); }); + + describe("duplicate settlement cache", () => { + function makePayload(transaction: string): PaymentPayload { + return { + x402Version: 2, + resource: { + url: "http://example.com/protected", + description: "Test resource", + mimeType: "application/json", + }, + accepted: { + scheme: "exact", + network: SOLANA_DEVNET_CAIP2, + asset: USDC_DEVNET_ADDRESS, + amount: "100000", + payTo: "PayToAddress11111111111111111111111111", + maxTimeoutSeconds: 3600, + extra: { feePayer: "FeePayer1111111111111111111111111111" }, + }, + payload: { transaction }, + }; + } + + const requirements: PaymentRequirements = { + scheme: "exact", + network: SOLANA_DEVNET_CAIP2, + asset: USDC_DEVNET_ADDRESS, + amount: "100000", + payTo: "PayToAddress11111111111111111111111111", + maxTimeoutSeconds: 3600, + extra: { feePayer: "FeePayer1111111111111111111111111111" }, + }; + + function setupSettleMocks(facilitator: ExactSvmScheme) { + vi.spyOn(facilitator, "verify").mockResolvedValue({ + isValid: true, + payer: "PayerAddress", + }); + (mockSigner as Record).signTransaction = vi + .fn() + .mockResolvedValue("signedTx"); + (mockSigner as Record).sendTransaction = vi + .fn() + .mockResolvedValue("txSignature123"); + (mockSigner as Record).confirmTransaction = vi + .fn() + .mockResolvedValue(undefined); + } + + it("should reject duplicate settlement of the same transaction", async () => { + const facilitator = new ExactSvmScheme(mockSigner); + setupSettleMocks(facilitator); + + const payload = makePayload("sameTransactionBase64=="); + + const result1 = await facilitator.settle(payload, requirements); + expect(result1.success).toBe(true); + + const result2 = await facilitator.settle(payload, requirements); + expect(result2.success).toBe(false); + expect(result2.errorReason).toBe("duplicate_settlement"); + }); + + it("should allow settlement of distinct transactions", async () => { + const facilitator = new ExactSvmScheme(mockSigner); + setupSettleMocks(facilitator); + + const result1 = await facilitator.settle(makePayload("transactionA=="), requirements); + expect(result1.success).toBe(true); + + const result2 = await facilitator.settle(makePayload("transactionB=="), requirements); + expect(result2.success).toBe(true); + }); + + it("should evict cache entries after TTL", async () => { + vi.useFakeTimers(); + try { + const facilitator = new ExactSvmScheme(mockSigner); + setupSettleMocks(facilitator); + + const payload = makePayload("expiringTransaction=="); + + const result1 = await facilitator.settle(payload, requirements); + expect(result1.success).toBe(true); + + // Advance past the 120s TTL + vi.advanceTimersByTime(121_000); + + const result2 = await facilitator.settle(payload, requirements); + expect(result2.success).toBe(true); + } finally { + vi.useRealTimers(); + } + }); + + it("should block cross-version duplicate when sharing a cache", async () => { + const sharedCache = new SettlementCache(); + const v2 = new ExactSvmScheme(mockSigner, sharedCache); + const v1 = new ExactSvmSchemeV1(mockSigner, sharedCache); + + // Mock V2 settle flow + vi.spyOn(v2, "verify").mockResolvedValue({ + isValid: true, + payer: "PayerAddress", + }); + (mockSigner as Record).signTransaction = vi + .fn() + .mockResolvedValue("signedTx"); + (mockSigner as Record).sendTransaction = vi + .fn() + .mockResolvedValue("txSignature123"); + (mockSigner as Record).confirmTransaction = vi + .fn() + .mockResolvedValue(undefined); + + // Settle via V2 first + const v2Result = await v2.settle(makePayload("crossVersionTx=="), requirements); + expect(v2Result.success).toBe(true); + + // Mock V1 verify + vi.spyOn(v1, "verify").mockResolvedValue({ + isValid: true, + payer: "PayerAddress", + }); + + // Same tx via V1 should be rejected + const v1Payload: PaymentPayloadV1 = { + x402Version: 1, + scheme: "exact", + network: "solana-devnet", + payload: { transaction: "crossVersionTx==" }, + }; + const v1Requirements: PaymentRequirementsV1 = { + scheme: "exact", + network: "solana-devnet", + asset: USDC_DEVNET_ADDRESS, + maxAmountRequired: "100000", + payTo: "PayToAddress11111111111111111111111111", + maxTimeoutSeconds: 3600, + extra: { feePayer: "FeePayer1111111111111111111111111111" }, + }; + + const v1Result = await v1.settle(v1Payload as never, v1Requirements as never); + expect(v1Result.success).toBe(false); + expect(v1Result.errorReason).toBe("duplicate_settlement"); + }); + }); +}); + +describe("SettlementCache prune optimization", () => { + it("should prune only expired entries and preserve non-expired ones", () => { + vi.useFakeTimers(); + try { + const cache = new SettlementCache(); + + // Insert three entries 10s apart + cache.isDuplicate("tx-a"); + vi.advanceTimersByTime(10_000); + cache.isDuplicate("tx-b"); + vi.advanceTimersByTime(10_000); + cache.isDuplicate("tx-c"); + + // Advance so tx-a and tx-b are expired (> 120s old) but tx-c is not + vi.advanceTimersByTime(101_000); // total: tx-a=121s, tx-b=111s, tx-c=101s + + // tx-a should be expired, tx-b and tx-c should still be cached + // Trigger prune via a new isDuplicate call + expect(cache.isDuplicate("tx-a")).toBe(false); // expired, re-inserted as new + expect(cache.isDuplicate("tx-b")).toBe(true); // still cached + expect(cache.isDuplicate("tx-c")).toBe(true); // still cached + } finally { + vi.useRealTimers(); + } + }); + + it("should prune all entries when all are expired", () => { + vi.useFakeTimers(); + try { + const cache = new SettlementCache(); + + cache.isDuplicate("tx-1"); + cache.isDuplicate("tx-2"); + cache.isDuplicate("tx-3"); + + vi.advanceTimersByTime(121_000); + + // All expired — none should be detected as duplicates + expect(cache.isDuplicate("tx-1")).toBe(false); + expect(cache.isDuplicate("tx-2")).toBe(false); + expect(cache.isDuplicate("tx-3")).toBe(false); + } finally { + vi.useRealTimers(); + } + }); + + it("should not prune any entries when none are expired", () => { + const cache = new SettlementCache(); + + cache.isDuplicate("tx-x"); + cache.isDuplicate("tx-y"); + cache.isDuplicate("tx-z"); + + // All still fresh — all should be detected as duplicates + expect(cache.isDuplicate("tx-x")).toBe(true); + expect(cache.isDuplicate("tx-y")).toBe(true); + expect(cache.isDuplicate("tx-z")).toBe(true); + }); }); diff --git a/typescript/packages/mechanisms/svm/test/unit/v1/facilitator.test.ts b/typescript/packages/mechanisms/svm/test/unit/v1/facilitator.test.ts index ac5a1c3f76..0502996217 100644 --- a/typescript/packages/mechanisms/svm/test/unit/v1/facilitator.test.ts +++ b/typescript/packages/mechanisms/svm/test/unit/v1/facilitator.test.ts @@ -204,4 +204,93 @@ describe("ExactSvmSchemeV1", () => { expect(result.network).toBe("solana-devnet"); }); }); + + describe("duplicate settlement cache", () => { + function makePayload(transaction: string): PaymentPayloadV1 { + return { + x402Version: 1, + scheme: "exact", + network: "solana-devnet", + payload: { transaction }, + }; + } + + const requirements: PaymentRequirementsV1 = { + scheme: "exact", + network: "solana-devnet", + asset: USDC_DEVNET_ADDRESS, + maxAmountRequired: "100000", + payTo: "PayToAddress11111111111111111111111111", + maxTimeoutSeconds: 3600, + extra: { feePayer: "FeePayer1111111111111111111111111111" }, + }; + + function setupSettleMocks(facilitator: ExactSvmSchemeV1) { + vi.spyOn(facilitator, "verify").mockResolvedValue({ + isValid: true, + payer: "PayerAddress", + }); + (mockSigner as Record).signTransaction = vi + .fn() + .mockResolvedValue("signedTx"); + (mockSigner as Record).sendTransaction = vi + .fn() + .mockResolvedValue("txSignature123"); + (mockSigner as Record).confirmTransaction = vi + .fn() + .mockResolvedValue(undefined); + } + + it("should reject duplicate settlement of the same transaction", async () => { + const facilitator = new ExactSvmSchemeV1(mockSigner); + setupSettleMocks(facilitator); + + const payload = makePayload("sameTransactionBase64=="); + + const result1 = await facilitator.settle(payload as never, requirements as never); + expect(result1.success).toBe(true); + + const result2 = await facilitator.settle(payload as never, requirements as never); + expect(result2.success).toBe(false); + expect(result2.errorReason).toBe("duplicate_settlement"); + }); + + it("should allow settlement of distinct transactions", async () => { + const facilitator = new ExactSvmSchemeV1(mockSigner); + setupSettleMocks(facilitator); + + const result1 = await facilitator.settle( + makePayload("transactionA==") as never, + requirements as never, + ); + expect(result1.success).toBe(true); + + const result2 = await facilitator.settle( + makePayload("transactionB==") as never, + requirements as never, + ); + expect(result2.success).toBe(true); + }); + + it("should evict cache entries after TTL", async () => { + vi.useFakeTimers(); + try { + const facilitator = new ExactSvmSchemeV1(mockSigner); + setupSettleMocks(facilitator); + + const payload = makePayload("expiringTransaction=="); + + const result1 = await facilitator.settle(payload as never, requirements as never); + expect(result1.success).toBe(true); + + // Advance past the 120s TTL + vi.advanceTimersByTime(121_000); + + const result2 = await facilitator.settle(payload as never, requirements as never); + expect(result2.success).toBe(true); + } finally { + vi.useRealTimers(); + } + }); + }); }); diff --git a/typescript/pnpm-lock.yaml b/typescript/pnpm-lock.yaml index 0666c69bc8..c30c460f81 100644 --- a/typescript/pnpm-lock.yaml +++ b/typescript/pnpm-lock.yaml @@ -16,16 +16,16 @@ importers: version: 0.5.2 '@changesets/cli': specifier: ^2.28.1 - version: 2.29.8(@types/node@22.19.7) + version: 2.29.8(@types/node@22.18.0) tsup: specifier: ^8.4.0 - version: 8.5.1(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(typescript@5.9.3) + version: 8.5.0(jiti@2.6.1)(postcss@8.5.6)(tsx@4.20.5)(typescript@5.9.2)(yaml@2.8.1) turbo: specifier: ^2.5.0 - version: 2.8.1 + version: 2.5.6 typescript: specifier: ^5.8.3 - version: 5.9.3 + version: 5.9.2 packages/core: dependencies: @@ -35,52 +35,55 @@ importers: devDependencies: '@eslint/js': specifier: ^9.24.0 - version: 9.39.2 + version: 9.34.0 '@types/node': specifier: ^22.13.4 - version: 22.19.7 + version: 22.18.0 '@typescript-eslint/eslint-plugin': specifier: ^8.29.1 - version: 8.54.0(@typescript-eslint/parser@8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + version: 8.42.0(@typescript-eslint/parser@8.42.0(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.2))(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.2) '@typescript-eslint/parser': specifier: ^8.29.1 - version: 8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + version: 8.42.0(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.2) eslint: specifier: ^9.24.0 - version: 9.39.2(jiti@2.6.1) + version: 9.34.0(jiti@2.6.1) eslint-plugin-import: specifier: ^2.31.0 - version: 2.32.0(@typescript-eslint/parser@8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.2(jiti@2.6.1)) + version: 2.32.0(@typescript-eslint/parser@8.42.0(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.2))(eslint-import-resolver-typescript@3.10.1)(eslint@9.34.0(jiti@2.6.1)) eslint-plugin-jsdoc: specifier: ^50.6.9 - version: 50.8.0(eslint@9.39.2(jiti@2.6.1)) + version: 50.8.0(eslint@9.34.0(jiti@2.6.1)) eslint-plugin-prettier: specifier: ^5.2.6 - version: 5.5.5(eslint@9.39.2(jiti@2.6.1))(prettier@3.5.2) + version: 5.5.4(eslint@9.34.0(jiti@2.6.1))(prettier@3.5.2) prettier: specifier: 3.5.2 version: 3.5.2 tsup: specifier: ^8.4.0 - version: 8.5.1(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(typescript@5.9.3) + version: 8.5.0(jiti@2.6.1)(postcss@8.5.6)(tsx@4.20.5)(typescript@5.9.2)(yaml@2.8.1) tsx: specifier: ^4.19.2 - version: 4.21.0 + version: 4.20.5 typescript: specifier: ^5.7.3 - version: 5.9.3 + version: 5.9.2 vite: specifier: ^6.2.6 - version: 6.4.1(@types/node@22.19.7)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0) + version: 6.3.5(@types/node@22.18.0)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.5)(yaml@2.8.1) vite-tsconfig-paths: specifier: ^5.1.4 - version: 5.1.4(typescript@5.9.3)(vite@6.4.1(@types/node@22.19.7)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)) + version: 5.1.4(typescript@5.9.2)(vite@6.3.5(@types/node@22.18.0)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.5)(yaml@2.8.1)) vitest: specifier: ^3.0.5 - version: 3.2.4(@types/debug@4.1.12)(@types/node@22.19.7)(jiti@2.6.1)(jsdom@26.1.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))(lightningcss@1.30.2)(tsx@4.21.0) + version: 3.2.4(@types/debug@4.1.12)(@types/node@22.18.0)(jiti@2.6.1)(jsdom@27.4.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(lightningcss@1.30.2)(tsx@4.20.5)(yaml@2.8.1) packages/extensions: dependencies: + '@noble/curves': + specifier: ^1.9.0 + version: 1.9.7 '@scure/base': specifier: ^1.2.6 version: 1.2.6 @@ -90,64 +93,67 @@ importers: ajv: specifier: ^8.17.1 version: 8.17.1 + jose: + specifier: ^5.9.6 + version: 5.10.0 siwe: specifier: ^2.3.2 - version: 2.3.2(ethers@6.16.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)) + version: 2.3.2(ethers@6.16.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)) tweetnacl: specifier: ^1.0.3 version: 1.0.3 viem: specifier: ^2.43.5 - version: 2.45.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + version: 2.45.1(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) zod: specifier: ^3.24.2 version: 3.25.76 devDependencies: '@eslint/js': specifier: ^9.24.0 - version: 9.39.2 + version: 9.34.0 '@types/node': specifier: ^22.13.4 - version: 22.19.7 + version: 22.18.0 '@typescript-eslint/eslint-plugin': specifier: ^8.29.1 - version: 8.54.0(@typescript-eslint/parser@8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + version: 8.42.0(@typescript-eslint/parser@8.42.0(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.2))(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.2) '@typescript-eslint/parser': specifier: ^8.29.1 - version: 8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + version: 8.42.0(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.2) eslint: specifier: ^9.24.0 - version: 9.39.2(jiti@2.6.1) + version: 9.34.0(jiti@2.6.1) eslint-plugin-import: specifier: ^2.31.0 - version: 2.32.0(@typescript-eslint/parser@8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.2(jiti@2.6.1)) + version: 2.32.0(@typescript-eslint/parser@8.42.0(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.2))(eslint-import-resolver-typescript@3.10.1)(eslint@9.34.0(jiti@2.6.1)) eslint-plugin-jsdoc: specifier: ^50.6.9 - version: 50.8.0(eslint@9.39.2(jiti@2.6.1)) + version: 50.8.0(eslint@9.34.0(jiti@2.6.1)) eslint-plugin-prettier: specifier: ^5.2.6 - version: 5.5.5(eslint@9.39.2(jiti@2.6.1))(prettier@3.5.2) + version: 5.5.4(eslint@9.34.0(jiti@2.6.1))(prettier@3.5.2) prettier: specifier: 3.5.2 version: 3.5.2 tsup: specifier: ^8.4.0 - version: 8.5.1(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(typescript@5.9.3) + version: 8.5.0(jiti@2.6.1)(postcss@8.5.6)(tsx@4.20.5)(typescript@5.9.2)(yaml@2.8.1) tsx: specifier: ^4.19.2 - version: 4.21.0 + version: 4.20.5 typescript: specifier: ^5.7.3 - version: 5.9.3 + version: 5.9.2 vite: specifier: ^6.2.6 - version: 6.4.1(@types/node@22.19.7)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0) + version: 6.3.5(@types/node@22.18.0)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.5)(yaml@2.8.1) vite-tsconfig-paths: specifier: ^5.1.4 - version: 5.1.4(typescript@5.9.3)(vite@6.4.1(@types/node@22.19.7)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)) + version: 5.1.4(typescript@5.9.2)(vite@6.3.5(@types/node@22.18.0)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.5)(yaml@2.8.1)) vitest: specifier: ^3.0.5 - version: 3.2.4(@types/debug@4.1.12)(@types/node@22.19.7)(jiti@2.6.1)(jsdom@26.1.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))(lightningcss@1.30.2)(tsx@4.21.0) + version: 3.2.4(@types/debug@4.1.12)(@types/node@22.18.0)(jiti@2.6.1)(jsdom@27.4.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(lightningcss@1.30.2)(tsx@4.20.5)(yaml@2.8.1) packages/http/axios: dependencies: @@ -156,65 +162,59 @@ importers: version: link:../../core axios: specifier: ^1.7.9 - version: 1.13.4 + version: 1.11.0 zod: specifier: ^3.24.2 version: 3.25.76 devDependencies: '@eslint/js': specifier: ^9.24.0 - version: 9.39.2 + version: 9.34.0 '@types/node': specifier: ^22.13.4 - version: 22.19.7 + version: 22.18.0 '@typescript-eslint/eslint-plugin': specifier: ^8.29.1 - version: 8.54.0(@typescript-eslint/parser@8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + version: 8.42.0(@typescript-eslint/parser@8.42.0(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.2))(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.2) '@typescript-eslint/parser': specifier: ^8.29.1 - version: 8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + version: 8.42.0(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.2) eslint: specifier: ^9.24.0 - version: 9.39.2(jiti@2.6.1) + version: 9.34.0(jiti@2.6.1) eslint-plugin-import: specifier: ^2.31.0 - version: 2.32.0(@typescript-eslint/parser@8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.2(jiti@2.6.1)) + version: 2.32.0(@typescript-eslint/parser@8.42.0(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.2))(eslint-import-resolver-typescript@3.10.1)(eslint@9.34.0(jiti@2.6.1)) eslint-plugin-jsdoc: specifier: ^50.6.9 - version: 50.8.0(eslint@9.39.2(jiti@2.6.1)) + version: 50.8.0(eslint@9.34.0(jiti@2.6.1)) eslint-plugin-prettier: specifier: ^5.2.6 - version: 5.5.5(eslint@9.39.2(jiti@2.6.1))(prettier@3.5.2) + version: 5.5.4(eslint@9.34.0(jiti@2.6.1))(prettier@3.5.2) prettier: specifier: 3.5.2 version: 3.5.2 tsup: specifier: ^8.4.0 - version: 8.5.1(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(typescript@5.9.3) + version: 8.5.0(jiti@2.6.1)(postcss@8.5.6)(tsx@4.20.5)(typescript@5.9.2)(yaml@2.8.1) tsx: specifier: ^4.19.2 - version: 4.21.0 + version: 4.20.5 typescript: specifier: ^5.7.3 - version: 5.9.3 + version: 5.9.2 vite: specifier: ^6.2.6 - version: 6.4.1(@types/node@22.19.7)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0) + version: 6.3.5(@types/node@22.18.0)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.5)(yaml@2.8.1) vite-tsconfig-paths: specifier: ^5.1.4 - version: 5.1.4(typescript@5.9.3)(vite@6.4.1(@types/node@22.19.7)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)) + version: 5.1.4(typescript@5.9.2)(vite@6.3.5(@types/node@22.18.0)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.5)(yaml@2.8.1)) vitest: specifier: ^3.0.5 - version: 3.2.4(@types/debug@4.1.12)(@types/node@22.19.7)(jiti@2.6.1)(jsdom@26.1.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))(lightningcss@1.30.2)(tsx@4.21.0) + version: 3.2.4(@types/debug@4.1.12)(@types/node@22.18.0)(jiti@2.6.1)(jsdom@27.4.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(lightningcss@1.30.2)(tsx@4.20.5)(yaml@2.8.1) packages/http/express: dependencies: - '@coinbase/cdp-sdk': - specifier: ^1.22.0 - version: 1.44.0(bufferutil@4.1.0)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(utf-8-validate@5.0.10) - '@solana/kit': - specifier: ^6.1.0 - version: 6.1.0(bufferutil@4.1.0)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(utf-8-validate@5.0.10) '@x402/core': specifier: workspace:~ version: link:../../core @@ -222,66 +222,127 @@ importers: specifier: workspace:~ version: link:../../extensions '@x402/paywall': - specifier: workspace:* + specifier: workspace:^ version: link:../paywall viem: specifier: ^2.39.3 - version: 2.45.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + version: 2.40.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) zod: specifier: ^3.24.2 version: 3.25.76 devDependencies: '@eslint/js': specifier: ^9.24.0 - version: 9.39.2 + version: 9.34.0 '@types/express': specifier: ^5.0.1 - version: 5.0.6 + version: 5.0.3 '@types/node': specifier: ^22.13.4 - version: 22.19.7 + version: 22.18.0 '@typescript-eslint/eslint-plugin': specifier: ^8.29.1 - version: 8.54.0(@typescript-eslint/parser@8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + version: 8.42.0(@typescript-eslint/parser@8.42.0(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.2))(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.2) '@typescript-eslint/parser': specifier: ^8.29.1 - version: 8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + version: 8.42.0(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.2) eslint: specifier: ^9.24.0 - version: 9.39.2(jiti@2.6.1) + version: 9.34.0(jiti@2.6.1) eslint-plugin-import: specifier: ^2.31.0 - version: 2.32.0(@typescript-eslint/parser@8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.2(jiti@2.6.1)) + version: 2.32.0(@typescript-eslint/parser@8.42.0(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.2))(eslint-import-resolver-typescript@3.10.1)(eslint@9.34.0(jiti@2.6.1)) eslint-plugin-jsdoc: specifier: ^50.6.9 - version: 50.8.0(eslint@9.39.2(jiti@2.6.1)) + version: 50.8.0(eslint@9.34.0(jiti@2.6.1)) eslint-plugin-prettier: specifier: ^5.2.6 - version: 5.5.5(eslint@9.39.2(jiti@2.6.1))(prettier@3.5.2) + version: 5.5.4(eslint@9.34.0(jiti@2.6.1))(prettier@3.5.2) express: specifier: ^4.18.2 - version: 4.22.1 + version: 4.21.2 + prettier: + specifier: 3.5.2 + version: 3.5.2 + tsup: + specifier: ^8.4.0 + version: 8.5.0(jiti@2.6.1)(postcss@8.5.6)(tsx@4.20.5)(typescript@5.9.2)(yaml@2.8.1) + tsx: + specifier: ^4.19.2 + version: 4.20.5 + typescript: + specifier: ^5.7.3 + version: 5.9.2 + vite: + specifier: ^6.2.6 + version: 6.3.5(@types/node@22.18.0)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.5)(yaml@2.8.1) + vite-tsconfig-paths: + specifier: ^5.1.4 + version: 5.1.4(typescript@5.9.2)(vite@6.3.5(@types/node@22.18.0)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.5)(yaml@2.8.1)) + vitest: + specifier: ^3.0.5 + version: 3.2.4(@types/debug@4.1.12)(@types/node@22.18.0)(jiti@2.6.1)(jsdom@27.4.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(lightningcss@1.30.2)(tsx@4.20.5)(yaml@2.8.1) + + packages/http/fastify: + dependencies: + '@x402/core': + specifier: workspace:~ + version: link:../../core + '@x402/extensions': + specifier: workspace:~ + version: link:../../extensions + '@x402/paywall': + specifier: workspace:* + version: link:../paywall + devDependencies: + '@eslint/js': + specifier: ^9.24.0 + version: 9.34.0 + '@types/node': + specifier: ^22.13.4 + version: 22.18.0 + '@typescript-eslint/eslint-plugin': + specifier: ^8.29.1 + version: 8.49.0(@typescript-eslint/parser@8.49.0(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.2))(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.2) + '@typescript-eslint/parser': + specifier: ^8.29.1 + version: 8.49.0(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.2) + eslint: + specifier: ^9.24.0 + version: 9.34.0(jiti@2.6.1) + eslint-plugin-import: + specifier: ^2.31.0 + version: 2.32.0(@typescript-eslint/parser@8.49.0(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.2))(eslint@9.34.0(jiti@2.6.1)) + eslint-plugin-jsdoc: + specifier: ^50.6.9 + version: 50.8.0(eslint@9.34.0(jiti@2.6.1)) + eslint-plugin-prettier: + specifier: ^5.2.6 + version: 5.5.4(eslint@9.34.0(jiti@2.6.1))(prettier@3.5.2) + fastify: + specifier: ^5.0.0 + version: 5.8.2 prettier: specifier: 3.5.2 version: 3.5.2 tsup: specifier: ^8.4.0 - version: 8.5.1(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(typescript@5.9.3) + version: 8.5.0(jiti@2.6.1)(postcss@8.5.6)(tsx@4.20.5)(typescript@5.9.2)(yaml@2.8.1) tsx: specifier: ^4.19.2 - version: 4.21.0 + version: 4.20.5 typescript: specifier: ^5.7.3 - version: 5.9.3 + version: 5.9.2 vite: specifier: ^6.2.6 - version: 6.4.1(@types/node@22.19.7)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0) + version: 6.3.5(@types/node@22.18.0)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.5)(yaml@2.8.1) vite-tsconfig-paths: specifier: ^5.1.4 - version: 5.1.4(typescript@5.9.3)(vite@6.4.1(@types/node@22.19.7)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)) + version: 5.1.4(typescript@5.9.2)(vite@6.3.5(@types/node@22.18.0)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.5)(yaml@2.8.1)) vitest: specifier: ^3.0.5 - version: 3.2.4(@types/debug@4.1.12)(@types/node@22.19.7)(jiti@2.6.1)(jsdom@26.1.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))(lightningcss@1.30.2)(tsx@4.21.0) + version: 3.2.4(@types/debug@4.1.12)(@types/node@22.18.0)(jiti@2.6.1)(jsdom@27.4.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(lightningcss@1.30.2)(tsx@4.20.5)(yaml@2.8.1) packages/http/fetch: dependencies: @@ -290,56 +351,56 @@ importers: version: link:../../core viem: specifier: ^2.39.3 - version: 2.45.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + version: 2.40.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) zod: specifier: ^3.24.2 version: 3.25.76 devDependencies: '@eslint/js': specifier: ^9.24.0 - version: 9.39.2 + version: 9.34.0 '@types/node': specifier: ^22.13.4 - version: 22.19.7 + version: 22.18.0 '@typescript-eslint/eslint-plugin': specifier: ^8.29.1 - version: 8.54.0(@typescript-eslint/parser@8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + version: 8.42.0(@typescript-eslint/parser@8.42.0(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.2))(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.2) '@typescript-eslint/parser': specifier: ^8.29.1 - version: 8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + version: 8.42.0(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.2) eslint: specifier: ^9.24.0 - version: 9.39.2(jiti@2.6.1) + version: 9.34.0(jiti@2.6.1) eslint-plugin-import: specifier: ^2.31.0 - version: 2.32.0(@typescript-eslint/parser@8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.2(jiti@2.6.1)) + version: 2.32.0(@typescript-eslint/parser@8.42.0(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.2))(eslint-import-resolver-typescript@3.10.1)(eslint@9.34.0(jiti@2.6.1)) eslint-plugin-jsdoc: specifier: ^50.6.9 - version: 50.8.0(eslint@9.39.2(jiti@2.6.1)) + version: 50.8.0(eslint@9.34.0(jiti@2.6.1)) eslint-plugin-prettier: specifier: ^5.2.6 - version: 5.5.5(eslint@9.39.2(jiti@2.6.1))(prettier@3.5.2) + version: 5.5.4(eslint@9.34.0(jiti@2.6.1))(prettier@3.5.2) prettier: specifier: 3.5.2 version: 3.5.2 tsup: specifier: ^8.4.0 - version: 8.5.1(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(typescript@5.9.3) + version: 8.5.0(jiti@2.6.1)(postcss@8.5.6)(tsx@4.20.5)(typescript@5.9.2)(yaml@2.8.1) tsx: specifier: ^4.19.2 - version: 4.21.0 + version: 4.20.5 typescript: specifier: ^5.7.3 - version: 5.9.3 + version: 5.9.2 vite: specifier: ^6.2.6 - version: 6.4.1(@types/node@22.19.7)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0) + version: 6.3.5(@types/node@22.18.0)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.5)(yaml@2.8.1) vite-tsconfig-paths: specifier: ^5.1.4 - version: 5.1.4(typescript@5.9.3)(vite@6.4.1(@types/node@22.19.7)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)) + version: 5.1.4(typescript@5.9.2)(vite@6.3.5(@types/node@22.18.0)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.5)(yaml@2.8.1)) vitest: specifier: ^3.0.5 - version: 3.2.4(@types/debug@4.1.12)(@types/node@22.19.7)(jiti@2.6.1)(jsdom@26.1.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))(lightningcss@1.30.2)(tsx@4.21.0) + version: 3.2.4(@types/debug@4.1.12)(@types/node@22.18.0)(jiti@2.6.1)(jsdom@27.4.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(lightningcss@1.30.2)(tsx@4.20.5)(yaml@2.8.1) packages/http/hono: dependencies: @@ -350,7 +411,7 @@ importers: specifier: workspace:~ version: link:../../extensions '@x402/paywall': - specifier: workspace:* + specifier: workspace:^ version: link:../paywall zod: specifier: ^3.24.2 @@ -358,58 +419,55 @@ importers: devDependencies: '@eslint/js': specifier: ^9.24.0 - version: 9.39.2 + version: 9.34.0 '@types/node': specifier: ^22.13.4 - version: 22.19.7 + version: 22.18.0 '@typescript-eslint/eslint-plugin': specifier: ^8.29.1 - version: 8.54.0(@typescript-eslint/parser@8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + version: 8.42.0(@typescript-eslint/parser@8.42.0(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.2))(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.2) '@typescript-eslint/parser': specifier: ^8.29.1 - version: 8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + version: 8.42.0(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.2) eslint: specifier: ^9.24.0 - version: 9.39.2(jiti@2.6.1) + version: 9.34.0(jiti@2.6.1) eslint-plugin-import: specifier: ^2.31.0 - version: 2.32.0(@typescript-eslint/parser@8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.2(jiti@2.6.1)) + version: 2.32.0(@typescript-eslint/parser@8.42.0(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.2))(eslint-import-resolver-typescript@3.10.1)(eslint@9.34.0(jiti@2.6.1)) eslint-plugin-jsdoc: specifier: ^50.6.9 - version: 50.8.0(eslint@9.39.2(jiti@2.6.1)) + version: 50.8.0(eslint@9.34.0(jiti@2.6.1)) eslint-plugin-prettier: specifier: ^5.2.6 - version: 5.5.5(eslint@9.39.2(jiti@2.6.1))(prettier@3.5.2) + version: 5.5.4(eslint@9.34.0(jiti@2.6.1))(prettier@3.5.2) hono: specifier: ^4.7.1 - version: 4.11.7 + version: 4.10.7 prettier: specifier: 3.5.2 version: 3.5.2 tsup: specifier: ^8.4.0 - version: 8.5.1(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(typescript@5.9.3) + version: 8.5.0(jiti@2.6.1)(postcss@8.5.6)(tsx@4.20.5)(typescript@5.9.2)(yaml@2.8.1) tsx: specifier: ^4.19.2 - version: 4.21.0 + version: 4.20.5 typescript: specifier: ^5.7.3 - version: 5.9.3 + version: 5.9.2 vite: specifier: ^6.2.6 - version: 6.4.1(@types/node@22.19.7)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0) + version: 6.3.5(@types/node@22.18.0)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.5)(yaml@2.8.1) vite-tsconfig-paths: specifier: ^5.1.4 - version: 5.1.4(typescript@5.9.3)(vite@6.4.1(@types/node@22.19.7)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)) + version: 5.1.4(typescript@5.9.2)(vite@6.3.5(@types/node@22.18.0)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.5)(yaml@2.8.1)) vitest: specifier: ^3.0.5 - version: 3.2.4(@types/debug@4.1.12)(@types/node@22.19.7)(jiti@2.6.1)(jsdom@26.1.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))(lightningcss@1.30.2)(tsx@4.21.0) + version: 3.2.4(@types/debug@4.1.12)(@types/node@22.18.0)(jiti@2.6.1)(jsdom@27.4.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(lightningcss@1.30.2)(tsx@4.20.5)(yaml@2.8.1) packages/http/next: dependencies: - '@coinbase/cdp-sdk': - specifier: ^1.22.0 - version: 1.44.0(bufferutil@4.1.0)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(utf-8-validate@5.0.10) '@x402/core': specifier: workspace:~ version: link:../../core @@ -417,60 +475,60 @@ importers: specifier: workspace:~ version: link:../../extensions '@x402/paywall': - specifier: workspace:* + specifier: workspace:^ version: link:../paywall next: specifier: ^16.0.10 - version: 16.1.6(@babel/core@7.29.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + version: 16.0.10(@babel/core@7.28.3)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) zod: specifier: ^3.24.2 version: 3.25.76 devDependencies: '@eslint/js': specifier: ^9.24.0 - version: 9.39.2 + version: 9.34.0 '@types/node': specifier: ^22.13.4 - version: 22.19.7 + version: 22.18.0 '@typescript-eslint/eslint-plugin': specifier: ^8.29.1 - version: 8.54.0(@typescript-eslint/parser@8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + version: 8.42.0(@typescript-eslint/parser@8.42.0(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.2))(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.2) '@typescript-eslint/parser': specifier: ^8.29.1 - version: 8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + version: 8.42.0(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.2) eslint: specifier: ^9.24.0 - version: 9.39.2(jiti@2.6.1) + version: 9.34.0(jiti@2.6.1) eslint-plugin-import: specifier: ^2.31.0 - version: 2.32.0(@typescript-eslint/parser@8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.2(jiti@2.6.1)) + version: 2.32.0(@typescript-eslint/parser@8.42.0(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.2))(eslint-import-resolver-typescript@3.10.1)(eslint@9.34.0(jiti@2.6.1)) eslint-plugin-jsdoc: specifier: ^50.6.9 - version: 50.8.0(eslint@9.39.2(jiti@2.6.1)) + version: 50.8.0(eslint@9.34.0(jiti@2.6.1)) eslint-plugin-prettier: specifier: ^5.2.6 - version: 5.5.5(eslint@9.39.2(jiti@2.6.1))(prettier@3.5.2) + version: 5.5.4(eslint@9.34.0(jiti@2.6.1))(prettier@3.5.2) prettier: specifier: 3.5.2 version: 3.5.2 tsup: specifier: ^8.4.0 - version: 8.5.1(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(typescript@5.9.3) + version: 8.5.0(jiti@2.6.1)(postcss@8.5.6)(tsx@4.20.5)(typescript@5.9.2)(yaml@2.8.1) tsx: specifier: ^4.19.2 - version: 4.21.0 + version: 4.20.5 typescript: specifier: ^5.7.3 - version: 5.9.3 + version: 5.9.2 vite: specifier: ^6.2.6 - version: 6.4.1(@types/node@22.19.7)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0) + version: 6.3.5(@types/node@22.18.0)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.5)(yaml@2.8.1) vite-tsconfig-paths: specifier: ^5.1.4 - version: 5.1.4(typescript@5.9.3)(vite@6.4.1(@types/node@22.19.7)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)) + version: 5.1.4(typescript@5.9.2)(vite@6.3.5(@types/node@22.18.0)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.5)(yaml@2.8.1)) vitest: specifier: ^3.0.5 - version: 3.2.4(@types/debug@4.1.12)(@types/node@22.19.7)(jiti@2.6.1)(jsdom@26.1.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))(lightningcss@1.30.2)(tsx@4.21.0) + version: 3.2.4(@types/debug@4.1.12)(@types/node@22.18.0)(jiti@2.6.1)(jsdom@27.4.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(lightningcss@1.30.2)(tsx@4.20.5)(yaml@2.8.1) packages/http/paywall: dependencies: @@ -479,31 +537,31 @@ importers: version: 1.2.6 '@solana-program/compute-budget': specifier: ^0.8.0 - version: 0.8.0(@solana/kit@6.1.0(bufferutil@4.1.0)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(utf-8-validate@5.0.10)) + version: 0.8.0(@solana/kit@6.1.0(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(utf-8-validate@5.0.10)) '@solana-program/token': specifier: ^0.5.1 - version: 0.5.1(@solana/kit@6.1.0(bufferutil@4.1.0)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(utf-8-validate@5.0.10)) + version: 0.5.1(@solana/kit@6.1.0(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(utf-8-validate@5.0.10)) '@solana-program/token-2022': specifier: ^0.4.2 - version: 0.4.2(@solana/kit@6.1.0(bufferutil@4.1.0)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(utf-8-validate@5.0.10))(@solana/sysvars@6.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)) + version: 0.4.2(@solana/kit@6.1.0(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(utf-8-validate@5.0.10))(@solana/sysvars@6.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)) '@solana/kit': specifier: ^6.1.0 - version: 6.1.0(bufferutil@4.1.0)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(utf-8-validate@5.0.10) + version: 6.1.0(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(utf-8-validate@5.0.10) '@solana/transaction-confirmation': specifier: ^2.1.1 - version: 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@8.18.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)) + version: 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)) '@solana/wallet-standard-features': specifier: ^1.3.0 version: 1.3.0 '@tanstack/react-query': specifier: ^5.90.7 - version: 5.90.20(react@19.2.4) + version: 5.90.11(react@19.2.1) '@wagmi/connectors': specifier: ^5.8.1 - version: 5.11.2(@tanstack/react-query@5.90.20(react@19.2.4))(@types/react@19.2.10)(@vercel/functions@2.2.13)(@wagmi/core@2.22.1(@tanstack/query-core@5.90.20)(@types/react@19.2.10)(react@19.2.4)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.4))(viem@2.45.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)))(bufferutil@4.1.0)(react@19.2.4)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.4))(utf-8-validate@5.0.10)(viem@2.45.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(wagmi@2.19.5(@tanstack/query-core@5.90.20)(@tanstack/react-query@5.90.20(react@19.2.4))(@types/react@19.2.10)(@vercel/functions@2.2.13)(bufferutil@4.1.0)(fastestsmallesttextencoderdecoder@1.0.22)(react@19.2.4)(typescript@5.9.3)(utf-8-validate@5.0.10)(viem@2.45.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(zod@3.25.76))(zod@3.25.76) + version: 5.9.9(@types/react@19.1.12)(@vercel/functions@2.2.13)(@wagmi/core@2.20.3(@tanstack/query-core@5.90.11)(@types/react@19.1.12)(react@19.2.1)(typescript@5.9.2)(use-sync-external-store@1.4.0(react@19.2.1))(viem@2.40.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76)))(bufferutil@4.0.9)(react@19.2.1)(typescript@5.9.2)(use-sync-external-store@1.4.0(react@19.2.1))(utf-8-validate@5.0.10)(viem@2.40.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76))(zod@3.25.76) '@wagmi/core': specifier: ^2.17.1 - version: 2.22.1(@tanstack/query-core@5.90.20)(@types/react@19.2.10)(react@19.2.4)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.4))(viem@2.45.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)) + version: 2.20.3(@tanstack/query-core@5.90.11)(@types/react@19.1.12)(react@19.2.1)(typescript@5.9.2)(use-sync-external-store@1.4.0(react@19.2.1))(viem@2.40.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76)) '@wallet-standard/app': specifier: ^1.1.0 version: 1.1.0 @@ -518,35 +576,35 @@ importers: version: link:../../core viem: specifier: ^2.39.3 - version: 2.45.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + version: 2.40.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) wagmi: specifier: ^2.17.1 - version: 2.19.5(@tanstack/query-core@5.90.20)(@tanstack/react-query@5.90.20(react@19.2.4))(@types/react@19.2.10)(@vercel/functions@2.2.13)(bufferutil@4.1.0)(fastestsmallesttextencoderdecoder@1.0.22)(react@19.2.4)(typescript@5.9.3)(utf-8-validate@5.0.10)(viem@2.45.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(zod@3.25.76) + version: 2.19.5(@tanstack/query-core@5.90.11)(@tanstack/react-query@5.90.11(react@19.2.1))(@types/react@19.1.12)(@vercel/functions@2.2.13)(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(react@19.2.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(viem@2.40.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76))(zod@3.25.76) zod: specifier: ^3.24.2 version: 3.25.76 devDependencies: '@craftamap/esbuild-plugin-html': specifier: ^0.9.0 - version: 0.9.0(bufferutil@4.1.0)(esbuild@0.25.12)(utf-8-validate@5.0.10) + version: 0.9.0(bufferutil@4.0.9)(esbuild@0.25.9)(utf-8-validate@5.0.10) '@eslint/js': specifier: ^9.24.0 - version: 9.39.2 + version: 9.34.0 '@types/node': specifier: ^22.13.4 - version: 22.19.7 + version: 22.18.0 '@types/react': specifier: ^19 - version: 19.2.10 + version: 19.1.12 '@types/react-dom': specifier: ^19 - version: 19.2.3(@types/react@19.2.10) + version: 19.1.9(@types/react@19.1.12) '@typescript-eslint/eslint-plugin': specifier: ^8.29.1 - version: 8.54.0(@typescript-eslint/parser@8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + version: 8.42.0(@typescript-eslint/parser@8.42.0(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.2))(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.2) '@typescript-eslint/parser': specifier: ^8.29.1 - version: 8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + version: 8.42.0(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.2) '@x402/evm': specifier: workspace:~ version: link:../../mechanisms/evm @@ -558,46 +616,46 @@ importers: version: 6.0.3 esbuild: specifier: ^0.25.4 - version: 0.25.12 + version: 0.25.9 eslint: specifier: ^9.24.0 - version: 9.39.2(jiti@2.6.1) + version: 9.34.0(jiti@2.6.1) eslint-plugin-import: specifier: ^2.31.0 - version: 2.32.0(@typescript-eslint/parser@8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.2(jiti@2.6.1)) + version: 2.32.0(@typescript-eslint/parser@8.42.0(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.2))(eslint-import-resolver-typescript@3.10.1)(eslint@9.34.0(jiti@2.6.1)) eslint-plugin-jsdoc: specifier: ^50.6.9 - version: 50.8.0(eslint@9.39.2(jiti@2.6.1)) + version: 50.8.0(eslint@9.34.0(jiti@2.6.1)) eslint-plugin-prettier: specifier: ^5.2.6 - version: 5.5.5(eslint@9.39.2(jiti@2.6.1))(prettier@3.5.2) + version: 5.5.4(eslint@9.34.0(jiti@2.6.1))(prettier@3.5.2) prettier: specifier: 3.5.2 version: 3.5.2 react: specifier: ^19.0.0 - version: 19.2.4 + version: 19.2.1 react-dom: specifier: ^19.0.0 - version: 19.2.4(react@19.2.4) + version: 19.2.1(react@19.2.1) tsup: specifier: ^8.4.0 - version: 8.5.1(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(typescript@5.9.3) + version: 8.5.0(jiti@2.6.1)(postcss@8.5.6)(tsx@4.20.5)(typescript@5.9.2)(yaml@2.8.1) tsx: specifier: ^4.19.2 - version: 4.21.0 + version: 4.20.5 typescript: specifier: ^5.7.3 - version: 5.9.3 + version: 5.9.2 vite: specifier: ^6.2.6 - version: 6.4.1(@types/node@22.19.7)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0) + version: 6.3.5(@types/node@22.18.0)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.5)(yaml@2.8.1) vite-tsconfig-paths: specifier: ^5.1.4 - version: 5.1.4(typescript@5.9.3)(vite@6.4.1(@types/node@22.19.7)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)) + version: 5.1.4(typescript@5.9.2)(vite@6.3.5(@types/node@22.18.0)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.5)(yaml@2.8.1)) vitest: specifier: ^3.0.5 - version: 3.2.4(@types/debug@4.1.12)(@types/node@22.19.7)(jiti@2.6.1)(jsdom@26.1.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))(lightningcss@1.30.2)(tsx@4.21.0) + version: 3.2.4(@types/debug@4.1.12)(@types/node@22.18.0)(jiti@2.6.1)(jsdom@27.4.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(lightningcss@1.30.2)(tsx@4.20.5)(yaml@2.8.1) packages/legacy/x402: dependencies: @@ -606,19 +664,19 @@ importers: version: 1.2.6 '@solana-program/compute-budget': specifier: ^0.11.0 - version: 0.11.0(@solana/kit@5.5.1(bufferutil@4.1.0)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(utf-8-validate@5.0.10)) + version: 0.11.0(@solana/kit@5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.19.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))) '@solana-program/token': specifier: ^0.9.0 - version: 0.9.0(@solana/kit@5.5.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)) + version: 0.9.0(@solana/kit@5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.19.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))) '@solana-program/token-2022': specifier: ^0.6.1 - version: 0.6.1(@solana/kit@5.5.1(bufferutil@4.1.0)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(utf-8-validate@5.0.10))(@solana/sysvars@6.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)) + version: 0.6.1(@solana/kit@5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.19.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)))(@solana/sysvars@6.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)) '@solana/kit': specifier: ^5.0.0 - version: 5.5.1(bufferutil@4.1.0)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(utf-8-validate@5.0.10) + version: 5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.19.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)) '@solana/transaction-confirmation': specifier: ^5.0.0 - version: 5.5.1(bufferutil@4.1.0)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(utf-8-validate@5.0.10) + version: 5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.19.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)) '@solana/wallet-standard-features': specifier: ^1.3.0 version: 1.3.0 @@ -633,62 +691,62 @@ importers: version: 1.1.0 viem: specifier: ^2.21.26 - version: 2.45.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + version: 2.40.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) wagmi: specifier: ^2.15.6 - version: 2.19.5(@tanstack/query-core@5.90.20)(@tanstack/react-query@5.90.20(react@19.2.1))(@types/react@19.2.10)(@vercel/functions@2.2.13)(bufferutil@4.1.0)(fastestsmallesttextencoderdecoder@1.0.22)(react@19.2.1)(typescript@5.9.3)(utf-8-validate@5.0.10)(viem@2.45.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(zod@3.25.76) + version: 2.19.5(@tanstack/query-core@5.90.11)(@tanstack/react-query@5.90.11(react@19.2.1))(@types/react@19.1.12)(@vercel/functions@2.2.13)(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(react@19.2.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(viem@2.40.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76))(zod@3.25.76) zod: specifier: ^3.24.2 version: 3.25.76 devDependencies: '@coinbase/onchainkit': specifier: ^0.38.14 - version: 0.38.19(@farcaster/miniapp-sdk@0.2.2(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(@tanstack/query-core@5.90.20)(@types/react@19.2.10)(@vercel/functions@2.2.13)(bufferutil@4.1.0)(fastestsmallesttextencoderdecoder@1.0.22)(react-dom@19.2.1(react@19.2.1))(react@19.2.1)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.1))(utf-8-validate@5.0.10)(zod@3.25.76) + version: 0.38.19(@farcaster/miniapp-sdk@0.1.9(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76))(@tanstack/query-core@5.90.11)(@types/react@19.1.12)(@vercel/functions@2.2.13)(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(react-dom@19.2.1(react@19.2.1))(react@19.2.1)(typescript@5.9.2)(use-sync-external-store@1.4.0(react@19.2.1))(utf-8-validate@5.0.10)(zod@3.25.76) '@craftamap/esbuild-plugin-html': specifier: ^0.9.0 - version: 0.9.0(bufferutil@4.1.0)(esbuild@0.25.12)(utf-8-validate@5.0.10) + version: 0.9.0(bufferutil@4.0.9)(esbuild@0.25.9)(utf-8-validate@5.0.10) '@eslint/js': specifier: ^9.24.0 - version: 9.39.2 + version: 9.34.0 '@types/node': specifier: ^22.13.4 - version: 22.19.7 + version: 22.18.0 '@types/react': specifier: ^19 - version: 19.2.10 + version: 19.1.12 '@types/react-dom': specifier: ^19 - version: 19.2.3(@types/react@19.2.10) + version: 19.1.9(@types/react@19.1.12) '@typescript-eslint/eslint-plugin': specifier: ^8.29.1 - version: 8.54.0(@typescript-eslint/parser@8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + version: 8.42.0(@typescript-eslint/parser@8.42.0(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.2))(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.2) '@typescript-eslint/parser': specifier: ^8.29.1 - version: 8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + version: 8.42.0(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.2) '@wagmi/connectors': specifier: ^5.8.1 - version: 5.11.2(@tanstack/react-query@5.90.20(react@19.2.1))(@types/react@19.2.10)(@vercel/functions@2.2.13)(@wagmi/core@2.22.1(@tanstack/query-core@5.90.20)(@types/react@19.2.10)(react@19.2.1)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.1))(viem@2.45.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)))(bufferutil@4.1.0)(react@19.2.1)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.1))(utf-8-validate@5.0.10)(viem@2.45.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(wagmi@2.19.5(@tanstack/query-core@5.90.20)(@tanstack/react-query@5.90.20(react@19.2.1))(@types/react@19.2.10)(@vercel/functions@2.2.13)(bufferutil@4.1.0)(fastestsmallesttextencoderdecoder@1.0.22)(react@19.2.1)(typescript@5.9.3)(utf-8-validate@5.0.10)(viem@2.45.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(zod@3.25.76))(zod@3.25.76) + version: 5.9.9(@types/react@19.1.12)(@vercel/functions@2.2.13)(@wagmi/core@2.20.3(@tanstack/query-core@5.90.11)(@types/react@19.1.12)(react@19.2.1)(typescript@5.9.2)(use-sync-external-store@1.4.0(react@19.2.1))(viem@2.40.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76)))(bufferutil@4.0.9)(react@19.2.1)(typescript@5.9.2)(use-sync-external-store@1.4.0(react@19.2.1))(utf-8-validate@5.0.10)(viem@2.40.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76))(zod@3.25.76) '@wagmi/core': specifier: ^2.17.1 - version: 2.22.1(@tanstack/query-core@5.90.20)(@types/react@19.2.10)(react@19.2.1)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.1))(viem@2.45.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)) + version: 2.20.3(@tanstack/query-core@5.90.11)(@types/react@19.1.12)(react@19.2.1)(typescript@5.9.2)(use-sync-external-store@1.4.0(react@19.2.1))(viem@2.40.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76)) buffer: specifier: ^6.0.3 version: 6.0.3 esbuild: specifier: ^0.25.4 - version: 0.25.12 + version: 0.25.9 eslint: specifier: ^9.24.0 - version: 9.39.2(jiti@2.6.1) + version: 9.34.0(jiti@2.6.1) eslint-plugin-import: specifier: ^2.31.0 - version: 2.32.0(@typescript-eslint/parser@8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.2(jiti@2.6.1)) + version: 2.32.0(@typescript-eslint/parser@8.42.0(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.2))(eslint-import-resolver-typescript@3.10.1)(eslint@9.34.0(jiti@2.6.1)) eslint-plugin-jsdoc: specifier: ^50.6.9 - version: 50.8.0(eslint@9.39.2(jiti@2.6.1)) + version: 50.8.0(eslint@9.34.0(jiti@2.6.1)) eslint-plugin-prettier: specifier: ^5.2.6 - version: 5.5.5(eslint@9.39.2(jiti@2.6.1))(prettier@3.5.2) + version: 5.5.4(eslint@9.34.0(jiti@2.6.1))(prettier@3.5.2) prettier: specifier: 3.5.2 version: 3.5.2 @@ -700,31 +758,31 @@ importers: version: 19.2.1(react@19.2.1) tsup: specifier: ^8.4.0 - version: 8.5.1(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(typescript@5.9.3) + version: 8.5.0(jiti@2.6.1)(postcss@8.5.6)(tsx@4.20.5)(typescript@5.9.2)(yaml@2.8.1) tsx: specifier: ^4.19.2 - version: 4.21.0 + version: 4.20.5 typescript: specifier: ^5.7.3 - version: 5.9.3 + version: 5.9.2 vite: specifier: ^6.2.6 - version: 6.4.1(@types/node@22.19.7)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0) + version: 6.3.5(@types/node@22.18.0)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.5)(yaml@2.8.1) vite-tsconfig-paths: specifier: ^5.1.4 - version: 5.1.4(typescript@5.9.3)(vite@6.4.1(@types/node@22.19.7)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)) + version: 5.1.4(typescript@5.9.2)(vite@6.3.5(@types/node@22.18.0)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.5)(yaml@2.8.1)) vitest: specifier: ^3.0.5 - version: 3.2.4(@types/debug@4.1.12)(@types/node@22.19.7)(jiti@2.6.1)(jsdom@26.1.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))(lightningcss@1.30.2)(tsx@4.21.0) + version: 3.2.4(@types/debug@4.1.12)(@types/node@22.18.0)(jiti@2.6.1)(jsdom@27.4.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(lightningcss@1.30.2)(tsx@4.20.5)(yaml@2.8.1) packages/legacy/x402-axios: dependencies: axios: specifier: ^1.7.9 - version: 1.13.4 + version: 1.11.0 viem: specifier: ^2.21.26 - version: 2.45.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + version: 2.40.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) x402: specifier: workspace:^ version: link:../x402 @@ -734,64 +792,64 @@ importers: devDependencies: '@eslint/js': specifier: ^9.24.0 - version: 9.39.2 + version: 9.34.0 '@types/node': specifier: ^22.13.4 - version: 22.19.7 + version: 22.18.0 '@typescript-eslint/eslint-plugin': specifier: ^8.29.1 - version: 8.54.0(@typescript-eslint/parser@8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + version: 8.42.0(@typescript-eslint/parser@8.42.0(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.2))(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.2) '@typescript-eslint/parser': specifier: ^8.29.1 - version: 8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + version: 8.42.0(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.2) eslint: specifier: ^9.24.0 - version: 9.39.2(jiti@2.6.1) + version: 9.34.0(jiti@2.6.1) eslint-plugin-import: specifier: ^2.31.0 - version: 2.32.0(@typescript-eslint/parser@8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.2(jiti@2.6.1)) + version: 2.32.0(@typescript-eslint/parser@8.42.0(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.2))(eslint-import-resolver-typescript@3.10.1)(eslint@9.34.0(jiti@2.6.1)) eslint-plugin-jsdoc: specifier: ^50.6.9 - version: 50.8.0(eslint@9.39.2(jiti@2.6.1)) + version: 50.8.0(eslint@9.34.0(jiti@2.6.1)) eslint-plugin-prettier: specifier: ^5.2.6 - version: 5.5.5(eslint@9.39.2(jiti@2.6.1))(prettier@3.5.2) + version: 5.5.4(eslint@9.34.0(jiti@2.6.1))(prettier@3.5.2) prettier: specifier: 3.5.2 version: 3.5.2 tsup: specifier: ^8.4.0 - version: 8.5.1(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(typescript@5.9.3) + version: 8.5.0(jiti@2.6.1)(postcss@8.5.6)(tsx@4.20.5)(typescript@5.9.2)(yaml@2.8.1) tsx: specifier: ^4.19.2 - version: 4.21.0 + version: 4.20.5 typescript: specifier: ^5.7.3 - version: 5.9.3 + version: 5.9.2 vite: specifier: ^6.2.6 - version: 6.4.1(@types/node@22.19.7)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0) + version: 6.3.5(@types/node@22.18.0)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.5)(yaml@2.8.1) vite-tsconfig-paths: specifier: ^5.1.4 - version: 5.1.4(typescript@5.9.3)(vite@6.4.1(@types/node@22.19.7)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)) + version: 5.1.4(typescript@5.9.2)(vite@6.3.5(@types/node@22.18.0)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.5)(yaml@2.8.1)) vitest: specifier: ^3.0.5 - version: 3.2.4(@types/debug@4.1.12)(@types/node@22.19.7)(jiti@2.6.1)(jsdom@26.1.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))(lightningcss@1.30.2)(tsx@4.21.0) + version: 3.2.4(@types/debug@4.1.12)(@types/node@22.18.0)(jiti@2.6.1)(jsdom@27.4.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(lightningcss@1.30.2)(tsx@4.20.5)(yaml@2.8.1) packages/legacy/x402-express: dependencies: '@coinbase/cdp-sdk': specifier: ^1.22.0 - version: 1.44.0(bufferutil@4.1.0)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(utf-8-validate@5.0.10) + version: 1.36.1(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(utf-8-validate@5.0.10) '@solana/kit': specifier: ^5.0.0 - version: 5.5.1(bufferutil@4.1.0)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(utf-8-validate@5.0.10) + version: 5.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10)) express: specifier: ^4.18.2 - version: 4.22.1 + version: 4.21.2 viem: specifier: ^2.21.26 - version: 2.45.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + version: 2.40.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) x402: specifier: workspace:^ version: link:../x402 @@ -801,58 +859,58 @@ importers: devDependencies: '@eslint/js': specifier: ^9.24.0 - version: 9.39.2 + version: 9.34.0 '@types/express': specifier: ^5.0.1 - version: 5.0.6 + version: 5.0.3 '@types/node': specifier: ^22.13.4 - version: 22.19.7 + version: 22.18.0 '@typescript-eslint/eslint-plugin': specifier: ^8.29.1 - version: 8.54.0(@typescript-eslint/parser@8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + version: 8.42.0(@typescript-eslint/parser@8.42.0(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.2))(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.2) '@typescript-eslint/parser': specifier: ^8.29.1 - version: 8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + version: 8.42.0(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.2) eslint: specifier: ^9.24.0 - version: 9.39.2(jiti@2.6.1) + version: 9.34.0(jiti@2.6.1) eslint-plugin-import: specifier: ^2.31.0 - version: 2.32.0(@typescript-eslint/parser@8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.2(jiti@2.6.1)) + version: 2.32.0(@typescript-eslint/parser@8.42.0(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.2))(eslint-import-resolver-typescript@3.10.1)(eslint@9.34.0(jiti@2.6.1)) eslint-plugin-jsdoc: specifier: ^50.6.9 - version: 50.8.0(eslint@9.39.2(jiti@2.6.1)) + version: 50.8.0(eslint@9.34.0(jiti@2.6.1)) eslint-plugin-prettier: specifier: ^5.2.6 - version: 5.5.5(eslint@9.39.2(jiti@2.6.1))(prettier@3.5.2) + version: 5.5.4(eslint@9.34.0(jiti@2.6.1))(prettier@3.5.2) prettier: specifier: 3.5.2 version: 3.5.2 tsup: specifier: ^8.4.0 - version: 8.5.1(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(typescript@5.9.3) + version: 8.5.0(jiti@2.6.1)(postcss@8.5.6)(tsx@4.20.5)(typescript@5.9.2)(yaml@2.8.1) tsx: specifier: ^4.19.2 - version: 4.21.0 + version: 4.20.5 typescript: specifier: ^5.7.3 - version: 5.9.3 + version: 5.9.2 vite: specifier: ^6.2.6 - version: 6.4.1(@types/node@22.19.7)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0) + version: 6.3.5(@types/node@22.18.0)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.5)(yaml@2.8.1) vite-tsconfig-paths: specifier: ^5.1.4 - version: 5.1.4(typescript@5.9.3)(vite@6.4.1(@types/node@22.19.7)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)) + version: 5.1.4(typescript@5.9.2)(vite@6.3.5(@types/node@22.18.0)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.5)(yaml@2.8.1)) vitest: specifier: ^3.0.5 - version: 3.2.4(@types/debug@4.1.12)(@types/node@22.19.7)(jiti@2.6.1)(jsdom@26.1.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))(lightningcss@1.30.2)(tsx@4.21.0) + version: 3.2.4(@types/debug@4.1.12)(@types/node@22.18.0)(jiti@2.6.1)(jsdom@27.4.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(lightningcss@1.30.2)(tsx@4.20.5)(yaml@2.8.1) packages/legacy/x402-fetch: dependencies: viem: specifier: ^2.21.26 - version: 2.45.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + version: 2.40.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) x402: specifier: workspace:^ version: link:../x402 @@ -862,64 +920,64 @@ importers: devDependencies: '@eslint/js': specifier: ^9.24.0 - version: 9.39.2 + version: 9.34.0 '@types/node': specifier: ^22.13.4 - version: 22.19.7 + version: 22.18.0 '@typescript-eslint/eslint-plugin': specifier: ^8.29.1 - version: 8.54.0(@typescript-eslint/parser@8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + version: 8.42.0(@typescript-eslint/parser@8.42.0(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.2))(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.2) '@typescript-eslint/parser': specifier: ^8.29.1 - version: 8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + version: 8.42.0(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.2) eslint: specifier: ^9.24.0 - version: 9.39.2(jiti@2.6.1) + version: 9.34.0(jiti@2.6.1) eslint-plugin-import: specifier: ^2.31.0 - version: 2.32.0(@typescript-eslint/parser@8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.2(jiti@2.6.1)) + version: 2.32.0(@typescript-eslint/parser@8.42.0(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.2))(eslint-import-resolver-typescript@3.10.1)(eslint@9.34.0(jiti@2.6.1)) eslint-plugin-jsdoc: specifier: ^50.6.9 - version: 50.8.0(eslint@9.39.2(jiti@2.6.1)) + version: 50.8.0(eslint@9.34.0(jiti@2.6.1)) eslint-plugin-prettier: specifier: ^5.2.6 - version: 5.5.5(eslint@9.39.2(jiti@2.6.1))(prettier@3.5.2) + version: 5.5.4(eslint@9.34.0(jiti@2.6.1))(prettier@3.5.2) prettier: specifier: 3.5.2 version: 3.5.2 tsup: specifier: ^8.4.0 - version: 8.5.1(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(typescript@5.9.3) + version: 8.5.0(jiti@2.6.1)(postcss@8.5.6)(tsx@4.20.5)(typescript@5.9.2)(yaml@2.8.1) tsx: specifier: ^4.19.2 - version: 4.21.0 + version: 4.20.5 typescript: specifier: ^5.7.3 - version: 5.9.3 + version: 5.9.2 vite: specifier: ^6.2.6 - version: 6.4.1(@types/node@22.19.7)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0) + version: 6.3.5(@types/node@22.18.0)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.5)(yaml@2.8.1) vite-tsconfig-paths: specifier: ^5.1.4 - version: 5.1.4(typescript@5.9.3)(vite@6.4.1(@types/node@22.19.7)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)) + version: 5.1.4(typescript@5.9.2)(vite@6.3.5(@types/node@22.18.0)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.5)(yaml@2.8.1)) vitest: specifier: ^3.0.5 - version: 3.2.4(@types/debug@4.1.12)(@types/node@22.19.7)(jiti@2.6.1)(jsdom@26.1.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))(lightningcss@1.30.2)(tsx@4.21.0) + version: 3.2.4(@types/debug@4.1.12)(@types/node@22.18.0)(jiti@2.6.1)(jsdom@27.4.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(lightningcss@1.30.2)(tsx@4.20.5)(yaml@2.8.1) packages/legacy/x402-hono: dependencies: '@coinbase/cdp-sdk': specifier: ^1.22.0 - version: 1.44.0(bufferutil@4.1.0)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(utf-8-validate@5.0.10) + version: 1.36.1(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(utf-8-validate@5.0.10) '@solana/kit': specifier: ^5.0.0 - version: 5.5.1(bufferutil@4.1.0)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(utf-8-validate@5.0.10) + version: 5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.19.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)) hono: specifier: ^4.7.1 - version: 4.11.7 + version: 4.10.7 viem: specifier: ^2.21.26 - version: 2.45.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + version: 2.40.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) x402: specifier: workspace:^ version: link:../x402 @@ -929,64 +987,64 @@ importers: devDependencies: '@eslint/js': specifier: ^9.24.0 - version: 9.39.2 + version: 9.34.0 '@types/node': specifier: ^22.13.4 - version: 22.19.7 + version: 22.18.0 '@typescript-eslint/eslint-plugin': specifier: ^8.29.1 - version: 8.54.0(@typescript-eslint/parser@8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + version: 8.42.0(@typescript-eslint/parser@8.42.0(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.2))(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.2) '@typescript-eslint/parser': specifier: ^8.29.1 - version: 8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + version: 8.42.0(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.2) eslint: specifier: ^9.24.0 - version: 9.39.2(jiti@2.6.1) + version: 9.34.0(jiti@2.6.1) eslint-plugin-import: specifier: ^2.31.0 - version: 2.32.0(@typescript-eslint/parser@8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.2(jiti@2.6.1)) + version: 2.32.0(@typescript-eslint/parser@8.42.0(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.2))(eslint-import-resolver-typescript@3.10.1)(eslint@9.34.0(jiti@2.6.1)) eslint-plugin-jsdoc: specifier: ^50.6.9 - version: 50.8.0(eslint@9.39.2(jiti@2.6.1)) + version: 50.8.0(eslint@9.34.0(jiti@2.6.1)) eslint-plugin-prettier: specifier: ^5.2.6 - version: 5.5.5(eslint@9.39.2(jiti@2.6.1))(prettier@3.5.2) + version: 5.5.4(eslint@9.34.0(jiti@2.6.1))(prettier@3.5.2) prettier: specifier: 3.5.2 version: 3.5.2 tsup: specifier: ^8.4.0 - version: 8.5.1(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(typescript@5.9.3) + version: 8.5.0(jiti@2.6.1)(postcss@8.5.6)(tsx@4.20.5)(typescript@5.9.2)(yaml@2.8.1) tsx: specifier: ^4.19.2 - version: 4.21.0 + version: 4.20.5 typescript: specifier: ^5.7.3 - version: 5.9.3 + version: 5.9.2 vite: specifier: ^6.2.6 - version: 6.4.1(@types/node@22.19.7)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0) + version: 6.3.5(@types/node@22.18.0)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.5)(yaml@2.8.1) vite-tsconfig-paths: specifier: ^5.1.4 - version: 5.1.4(typescript@5.9.3)(vite@6.4.1(@types/node@22.19.7)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)) + version: 5.1.4(typescript@5.9.2)(vite@6.3.5(@types/node@22.18.0)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.5)(yaml@2.8.1)) vitest: specifier: ^3.0.5 - version: 3.2.4(@types/debug@4.1.12)(@types/node@22.19.7)(jiti@2.6.1)(jsdom@26.1.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))(lightningcss@1.30.2)(tsx@4.21.0) + version: 3.2.4(@types/debug@4.1.12)(@types/node@22.18.0)(jiti@2.6.1)(jsdom@27.4.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(lightningcss@1.30.2)(tsx@4.20.5)(yaml@2.8.1) packages/legacy/x402-next: dependencies: '@coinbase/cdp-sdk': specifier: ^1.22.0 - version: 1.44.0(bufferutil@4.1.0)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(utf-8-validate@5.0.10) + version: 1.36.1(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(utf-8-validate@5.0.10) '@solana/kit': specifier: ^5.0.0 - version: 5.5.1(bufferutil@4.1.0)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(utf-8-validate@5.0.10) + version: 5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.19.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)) next: specifier: '>=15.5.9 || >=16.0.10' - version: 16.1.6(@babel/core@7.29.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + version: 16.0.10(@babel/core@7.28.3)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) viem: specifier: ^2.21.26 - version: 2.45.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + version: 2.40.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) x402: specifier: workspace:^ version: link:../x402 @@ -996,55 +1054,55 @@ importers: devDependencies: '@eslint/js': specifier: ^9.24.0 - version: 9.39.2 + version: 9.34.0 '@types/node': specifier: ^22.13.4 - version: 22.19.7 + version: 22.18.0 '@typescript-eslint/eslint-plugin': specifier: ^8.29.1 - version: 8.54.0(@typescript-eslint/parser@8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + version: 8.42.0(@typescript-eslint/parser@8.42.0(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.2))(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.2) '@typescript-eslint/parser': specifier: ^8.29.1 - version: 8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + version: 8.42.0(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.2) eslint: specifier: ^9.24.0 - version: 9.39.2(jiti@2.6.1) + version: 9.34.0(jiti@2.6.1) eslint-plugin-import: specifier: ^2.31.0 - version: 2.32.0(@typescript-eslint/parser@8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.2(jiti@2.6.1)) + version: 2.32.0(@typescript-eslint/parser@8.42.0(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.2))(eslint-import-resolver-typescript@3.10.1)(eslint@9.34.0(jiti@2.6.1)) eslint-plugin-jsdoc: specifier: ^50.6.9 - version: 50.8.0(eslint@9.39.2(jiti@2.6.1)) + version: 50.8.0(eslint@9.34.0(jiti@2.6.1)) eslint-plugin-prettier: specifier: ^5.2.6 - version: 5.5.5(eslint@9.39.2(jiti@2.6.1))(prettier@3.5.2) + version: 5.5.4(eslint@9.34.0(jiti@2.6.1))(prettier@3.5.2) prettier: specifier: 3.5.2 version: 3.5.2 tsup: specifier: ^8.4.0 - version: 8.5.1(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(typescript@5.9.3) + version: 8.5.0(jiti@2.6.1)(postcss@8.5.6)(tsx@4.20.5)(typescript@5.9.2)(yaml@2.8.1) tsx: specifier: ^4.19.2 - version: 4.21.0 + version: 4.20.5 typescript: specifier: ^5.7.3 - version: 5.9.3 + version: 5.9.2 vite: specifier: ^6.2.6 - version: 6.4.1(@types/node@22.19.7)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0) + version: 6.3.5(@types/node@22.18.0)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.5)(yaml@2.8.1) vite-tsconfig-paths: specifier: ^5.1.4 - version: 5.1.4(typescript@5.9.3)(vite@6.4.1(@types/node@22.19.7)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)) + version: 5.1.4(typescript@5.9.2)(vite@6.3.5(@types/node@22.18.0)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.5)(yaml@2.8.1)) vitest: specifier: ^3.0.5 - version: 3.2.4(@types/debug@4.1.12)(@types/node@22.19.7)(jiti@2.6.1)(jsdom@26.1.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))(lightningcss@1.30.2)(tsx@4.21.0) + version: 3.2.4(@types/debug@4.1.12)(@types/node@22.18.0)(jiti@2.6.1)(jsdom@27.4.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(lightningcss@1.30.2)(tsx@4.20.5)(yaml@2.8.1) packages/mcp: dependencies: '@modelcontextprotocol/sdk': specifier: ^1.12.1 - version: 1.25.3(hono@4.11.7)(zod@3.25.76) + version: 1.26.0(zod@3.25.76) '@x402/core': specifier: workspace:~ version: link:../core @@ -1054,58 +1112,58 @@ importers: devDependencies: '@eslint/js': specifier: ^9.24.0 - version: 9.39.2 + version: 9.34.0 '@types/node': specifier: ^22.13.4 - version: 22.19.7 + version: 22.18.0 '@typescript-eslint/eslint-plugin': specifier: ^8.29.1 - version: 8.54.0(@typescript-eslint/parser@8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + version: 8.49.0(@typescript-eslint/parser@8.49.0(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.2))(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.2) '@typescript-eslint/parser': specifier: ^8.29.1 - version: 8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + version: 8.49.0(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.2) '@x402/evm': specifier: workspace:~ version: link:../mechanisms/evm eslint: specifier: ^9.24.0 - version: 9.39.2(jiti@2.6.1) + version: 9.34.0(jiti@2.6.1) eslint-plugin-import: specifier: ^2.31.0 - version: 2.32.0(@typescript-eslint/parser@8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.2(jiti@2.6.1)) + version: 2.32.0(@typescript-eslint/parser@8.49.0(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.2))(eslint@9.34.0(jiti@2.6.1)) eslint-plugin-jsdoc: specifier: ^50.6.9 - version: 50.8.0(eslint@9.39.2(jiti@2.6.1)) + version: 50.8.0(eslint@9.34.0(jiti@2.6.1)) eslint-plugin-prettier: specifier: ^5.2.6 - version: 5.5.5(eslint@9.39.2(jiti@2.6.1))(prettier@3.5.2) + version: 5.5.4(eslint@9.34.0(jiti@2.6.1))(prettier@3.5.2) express: specifier: ^4.21.2 - version: 4.22.1 + version: 4.21.2 prettier: specifier: 3.5.2 version: 3.5.2 tsup: specifier: ^8.4.0 - version: 8.5.1(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(typescript@5.9.3) + version: 8.5.0(jiti@2.6.1)(postcss@8.5.6)(tsx@4.20.5)(typescript@5.9.2)(yaml@2.8.1) tsx: specifier: ^4.19.2 - version: 4.21.0 + version: 4.20.5 typescript: specifier: ^5.7.3 - version: 5.9.3 + version: 5.9.2 viem: specifier: ^2.27.2 - version: 2.45.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + version: 2.45.1(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) vite: specifier: ^6.2.6 - version: 6.4.1(@types/node@22.19.7)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0) + version: 6.3.5(@types/node@22.18.0)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.5)(yaml@2.8.1) vite-tsconfig-paths: specifier: ^5.1.4 - version: 5.1.4(typescript@5.9.3)(vite@6.4.1(@types/node@22.19.7)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)) + version: 5.1.4(typescript@5.9.2)(vite@6.3.5(@types/node@22.18.0)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.5)(yaml@2.8.1)) vitest: specifier: ^3.0.5 - version: 3.2.4(@types/debug@4.1.12)(@types/node@22.19.7)(jiti@2.6.1)(jsdom@26.1.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))(lightningcss@1.30.2)(tsx@4.21.0) + version: 3.2.4(@types/debug@4.1.12)(@types/node@22.18.0)(jiti@2.6.1)(jsdom@27.4.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(lightningcss@1.30.2)(tsx@4.20.5)(yaml@2.8.1) packages/mechanisms/aptos: dependencies: @@ -1118,177 +1176,229 @@ importers: devDependencies: '@eslint/js': specifier: ^9.24.0 - version: 9.39.2 + version: 9.34.0 '@types/node': specifier: ^22.13.4 - version: 22.19.7 + version: 22.18.0 '@typescript-eslint/eslint-plugin': specifier: ^8.29.1 - version: 8.54.0(@typescript-eslint/parser@8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + version: 8.49.0(@typescript-eslint/parser@8.49.0(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.2))(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.2) '@typescript-eslint/parser': specifier: ^8.29.1 - version: 8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + version: 8.49.0(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.2) eslint: specifier: ^9.24.0 - version: 9.39.2(jiti@2.6.1) + version: 9.34.0(jiti@2.6.1) eslint-plugin-import: specifier: ^2.31.0 - version: 2.32.0(@typescript-eslint/parser@8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.2(jiti@2.6.1)) + version: 2.32.0(@typescript-eslint/parser@8.49.0(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.2))(eslint@9.34.0(jiti@2.6.1)) eslint-plugin-jsdoc: specifier: ^50.6.9 - version: 50.8.0(eslint@9.39.2(jiti@2.6.1)) + version: 50.8.0(eslint@9.34.0(jiti@2.6.1)) eslint-plugin-prettier: specifier: ^5.2.6 - version: 5.5.5(eslint@9.39.2(jiti@2.6.1))(prettier@3.5.2) + version: 5.5.4(eslint@9.34.0(jiti@2.6.1))(prettier@3.5.2) prettier: specifier: 3.5.2 version: 3.5.2 tsup: specifier: ^8.4.0 - version: 8.5.1(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(typescript@5.9.3) + version: 8.5.0(jiti@2.6.1)(postcss@8.5.6)(tsx@4.20.5)(typescript@5.9.2)(yaml@2.8.1) tsx: specifier: ^4.19.2 - version: 4.21.0 + version: 4.20.5 typescript: specifier: ^5.7.3 - version: 5.9.3 + version: 5.9.2 vite: specifier: ^6.2.6 - version: 6.4.1(@types/node@22.19.7)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0) + version: 6.3.5(@types/node@22.18.0)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.5)(yaml@2.8.1) vite-tsconfig-paths: specifier: ^5.1.4 - version: 5.1.4(typescript@5.9.3)(vite@6.4.1(@types/node@22.19.7)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)) + version: 5.1.4(typescript@5.9.2)(vite@6.3.5(@types/node@22.18.0)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.5)(yaml@2.8.1)) vitest: specifier: ^3.0.5 - version: 3.2.4(@types/debug@4.1.12)(@types/node@22.19.7)(jiti@2.6.1)(jsdom@26.1.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))(lightningcss@1.30.2)(tsx@4.21.0) + version: 3.2.4(@types/debug@4.1.12)(@types/node@22.18.0)(jiti@2.6.1)(jsdom@27.4.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(lightningcss@1.30.2)(tsx@4.20.5)(yaml@2.8.1) packages/mechanisms/evm: dependencies: '@x402/core': specifier: workspace:~ version: link:../../core - '@x402/extensions': - specifier: workspace:~ - version: link:../../extensions viem: specifier: ^2.39.3 - version: 2.45.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + version: 2.40.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) zod: specifier: ^3.24.2 version: 3.25.76 devDependencies: '@eslint/js': specifier: ^9.24.0 - version: 9.39.2 + version: 9.34.0 + '@types/node': + specifier: ^22.13.4 + version: 22.18.0 + '@typescript-eslint/eslint-plugin': + specifier: ^8.29.1 + version: 8.42.0(@typescript-eslint/parser@8.42.0(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.2))(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.2) + '@typescript-eslint/parser': + specifier: ^8.29.1 + version: 8.42.0(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.2) + eslint: + specifier: ^9.24.0 + version: 9.34.0(jiti@2.6.1) + eslint-plugin-import: + specifier: ^2.31.0 + version: 2.32.0(@typescript-eslint/parser@8.42.0(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.2))(eslint-import-resolver-typescript@3.10.1)(eslint@9.34.0(jiti@2.6.1)) + eslint-plugin-jsdoc: + specifier: ^50.6.9 + version: 50.8.0(eslint@9.34.0(jiti@2.6.1)) + eslint-plugin-prettier: + specifier: ^5.2.6 + version: 5.5.4(eslint@9.34.0(jiti@2.6.1))(prettier@3.5.2) + prettier: + specifier: 3.5.2 + version: 3.5.2 + tsup: + specifier: ^8.4.0 + version: 8.5.0(jiti@2.6.1)(postcss@8.5.6)(tsx@4.20.5)(typescript@5.9.2)(yaml@2.8.1) + tsx: + specifier: ^4.19.2 + version: 4.20.5 + typescript: + specifier: ^5.7.3 + version: 5.9.2 + vite: + specifier: ^6.2.6 + version: 6.3.5(@types/node@22.18.0)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.5)(yaml@2.8.1) + vite-tsconfig-paths: + specifier: ^5.1.4 + version: 5.1.4(typescript@5.9.2)(vite@6.3.5(@types/node@22.18.0)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.5)(yaml@2.8.1)) + vitest: + specifier: ^3.0.5 + version: 3.2.4(@types/debug@4.1.12)(@types/node@22.18.0)(jiti@2.6.1)(jsdom@27.4.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(lightningcss@1.30.2)(tsx@4.20.5)(yaml@2.8.1) + + packages/mechanisms/stellar: + dependencies: + '@stellar/stellar-sdk': + specifier: ^14.6.1 + version: 14.6.1 + '@x402/core': + specifier: workspace:* + version: link:../../core + devDependencies: + '@eslint/js': + specifier: ^9.24.0 + version: 9.34.0 '@types/node': specifier: ^22.13.4 - version: 22.19.7 + version: 22.18.0 '@typescript-eslint/eslint-plugin': specifier: ^8.29.1 - version: 8.54.0(@typescript-eslint/parser@8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + version: 8.49.0(@typescript-eslint/parser@8.49.0(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.2))(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.2) '@typescript-eslint/parser': specifier: ^8.29.1 - version: 8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + version: 8.49.0(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.2) eslint: specifier: ^9.24.0 - version: 9.39.2(jiti@2.6.1) + version: 9.34.0(jiti@2.6.1) eslint-plugin-import: specifier: ^2.31.0 - version: 2.32.0(@typescript-eslint/parser@8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.2(jiti@2.6.1)) + version: 2.32.0(@typescript-eslint/parser@8.49.0(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.2))(eslint@9.34.0(jiti@2.6.1)) eslint-plugin-jsdoc: specifier: ^50.6.9 - version: 50.8.0(eslint@9.39.2(jiti@2.6.1)) + version: 50.8.0(eslint@9.34.0(jiti@2.6.1)) eslint-plugin-prettier: specifier: ^5.2.6 - version: 5.5.5(eslint@9.39.2(jiti@2.6.1))(prettier@3.5.2) + version: 5.5.4(eslint@9.34.0(jiti@2.6.1))(prettier@3.5.2) prettier: specifier: 3.5.2 version: 3.5.2 tsup: specifier: ^8.4.0 - version: 8.5.1(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(typescript@5.9.3) + version: 8.5.0(jiti@2.6.1)(postcss@8.5.6)(tsx@4.20.5)(typescript@5.9.2)(yaml@2.8.1) tsx: specifier: ^4.19.2 - version: 4.21.0 + version: 4.20.5 typescript: specifier: ^5.7.3 - version: 5.9.3 + version: 5.9.2 vite: specifier: ^6.2.6 - version: 6.4.1(@types/node@22.19.7)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0) + version: 6.3.5(@types/node@22.18.0)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.5)(yaml@2.8.1) vite-tsconfig-paths: specifier: ^5.1.4 - version: 5.1.4(typescript@5.9.3)(vite@6.4.1(@types/node@22.19.7)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)) + version: 5.1.4(typescript@5.9.2)(vite@6.3.5(@types/node@22.18.0)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.5)(yaml@2.8.1)) vitest: specifier: ^3.0.5 - version: 3.2.4(@types/debug@4.1.12)(@types/node@22.19.7)(jiti@2.6.1)(jsdom@26.1.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))(lightningcss@1.30.2)(tsx@4.21.0) + version: 3.2.4(@types/debug@4.1.12)(@types/node@22.18.0)(jiti@2.6.1)(jsdom@27.4.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(lightningcss@1.30.2)(tsx@4.20.5)(yaml@2.8.1) packages/mechanisms/svm: dependencies: '@solana-program/compute-budget': specifier: ^0.11.0 - version: 0.11.0(@solana/kit@5.5.1(bufferutil@4.1.0)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(utf-8-validate@5.0.10)) + version: 0.11.0(@solana/kit@5.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.19.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))) '@solana-program/token': specifier: ^0.9.0 - version: 0.9.0(@solana/kit@5.5.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)) + version: 0.9.0(@solana/kit@5.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.19.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))) '@solana-program/token-2022': specifier: ^0.6.1 - version: 0.6.1(@solana/kit@5.5.1(bufferutil@4.1.0)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(utf-8-validate@5.0.10))(@solana/sysvars@6.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)) + version: 0.6.1(@solana/kit@5.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.19.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)))(@solana/sysvars@6.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)) '@solana/kit': specifier: '>=5.1.0' - version: 5.5.1(bufferutil@4.1.0)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(utf-8-validate@5.0.10) + version: 5.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.19.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)) '@x402/core': specifier: workspace:~ version: link:../../core devDependencies: '@eslint/js': specifier: ^9.24.0 - version: 9.39.2 + version: 9.34.0 '@scure/base': specifier: ^1.2.6 version: 1.2.6 '@types/node': specifier: ^22.13.4 - version: 22.19.7 + version: 22.18.0 '@typescript-eslint/eslint-plugin': specifier: ^8.29.1 - version: 8.54.0(@typescript-eslint/parser@8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + version: 8.42.0(@typescript-eslint/parser@8.42.0(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.2))(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.2) '@typescript-eslint/parser': specifier: ^8.29.1 - version: 8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + version: 8.42.0(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.2) eslint: specifier: ^9.24.0 - version: 9.39.2(jiti@2.6.1) + version: 9.34.0(jiti@2.6.1) eslint-plugin-import: specifier: ^2.31.0 - version: 2.32.0(@typescript-eslint/parser@8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.2(jiti@2.6.1)) + version: 2.32.0(@typescript-eslint/parser@8.42.0(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.2))(eslint-import-resolver-typescript@3.10.1)(eslint@9.34.0(jiti@2.6.1)) eslint-plugin-jsdoc: specifier: ^50.6.9 - version: 50.8.0(eslint@9.39.2(jiti@2.6.1)) + version: 50.8.0(eslint@9.34.0(jiti@2.6.1)) eslint-plugin-prettier: specifier: ^5.2.6 - version: 5.5.5(eslint@9.39.2(jiti@2.6.1))(prettier@3.5.2) + version: 5.5.4(eslint@9.34.0(jiti@2.6.1))(prettier@3.5.2) prettier: specifier: 3.5.2 version: 3.5.2 tsup: specifier: ^8.4.0 - version: 8.5.1(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(typescript@5.9.3) + version: 8.5.0(jiti@2.6.1)(postcss@8.5.6)(tsx@4.20.5)(typescript@5.9.2)(yaml@2.8.1) tsx: specifier: ^4.19.2 - version: 4.21.0 + version: 4.20.5 typescript: specifier: ^5.7.3 - version: 5.9.3 + version: 5.9.2 vite: specifier: ^6.2.6 - version: 6.4.1(@types/node@22.19.7)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0) + version: 6.3.5(@types/node@22.18.0)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.5)(yaml@2.8.1) vite-tsconfig-paths: specifier: ^5.1.4 - version: 5.1.4(typescript@5.9.3)(vite@6.4.1(@types/node@22.19.7)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)) + version: 5.1.4(typescript@5.9.2)(vite@6.3.5(@types/node@22.18.0)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.5)(yaml@2.8.1)) vitest: specifier: ^3.0.5 - version: 3.2.4(@types/debug@4.1.12)(@types/node@22.19.7)(jiti@2.6.1)(jsdom@26.1.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))(lightningcss@1.30.2)(tsx@4.21.0) + version: 3.2.4(@types/debug@4.1.12)(@types/node@22.18.0)(jiti@2.6.1)(jsdom@27.4.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(lightningcss@1.30.2)(tsx@4.20.5)(yaml@2.8.1) site: dependencies: @@ -1297,16 +1407,16 @@ importers: version: 5.2.1(got@11.8.6) '@coinbase/cdp-sdk': specifier: ^1.22.0 - version: 1.44.0(bufferutil@4.1.0)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(utf-8-validate@5.0.10) + version: 1.36.1(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(utf-8-validate@5.0.10) '@heroicons/react': specifier: ^2.2.0 - version: 2.2.0(react@19.2.4) + version: 2.2.0(react@19.2.3) '@scure/base': specifier: ^1.2.6 version: 1.2.6 '@solana/kit': specifier: ^6.1.0 - version: 6.1.0(bufferutil@4.1.0)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(utf-8-validate@5.0.10) + version: 6.1.0(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(utf-8-validate@5.0.10) '@vercel/functions': specifier: ^2.2.8 version: 2.2.13 @@ -1328,98 +1438,108 @@ importers: '@x402/paywall': specifier: workspace:* version: link:../packages/http/paywall + '@x402/stellar': + specifier: workspace:* + version: link:../packages/mechanisms/stellar '@x402/svm': specifier: workspace:* version: link:../packages/mechanisms/svm lottie-react: specifier: ^2.4.1 - version: 2.4.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + version: 2.4.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3) motion: specifier: ^11.18.0 - version: 11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + version: 11.18.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) next: specifier: ^16.0.10 - version: 16.1.6(@babel/core@7.29.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + version: 16.0.10(@babel/core@7.28.3)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) react: specifier: ^19.2.3 - version: 19.2.4 + version: 19.2.3 react-dom: specifier: ^19.2.3 - version: 19.2.4(react@19.2.4) + version: 19.2.3(react@19.2.3) viem: specifier: ^2.21.26 - version: 2.45.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + version: 2.37.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) wagmi: specifier: ^2.15.6 - version: 2.19.5(@tanstack/query-core@5.90.20)(@tanstack/react-query@5.90.20(react@19.2.4))(@types/react@19.2.10)(@vercel/functions@2.2.13)(bufferutil@4.1.0)(fastestsmallesttextencoderdecoder@1.0.22)(react@19.2.4)(typescript@5.9.3)(utf-8-validate@5.0.10)(viem@2.45.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(zod@3.25.76) + version: 2.16.9(@tanstack/query-core@5.90.11)(@tanstack/react-query@5.90.11(react@19.2.3))(@types/react@19.1.12)(@vercel/functions@2.2.13)(bufferutil@4.0.9)(react@19.2.3)(typescript@5.9.2)(utf-8-validate@5.0.10)(viem@2.37.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76))(zod@3.25.76) x402-legacy: specifier: npm:x402@0.1.2 - version: x402@0.1.2(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10) + version: x402@0.1.2(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10) devDependencies: '@eslint/eslintrc': specifier: ^3 - version: 3.3.3 + version: 3.3.1 '@eslint/js': specifier: ^9.24.0 - version: 9.39.2 + version: 9.34.0 '@svgr/webpack': specifier: ^8.1.0 - version: 8.1.0(typescript@5.9.3) + version: 8.1.0(typescript@5.9.2) '@tailwindcss/postcss': specifier: ^4.0.0 - version: 4.1.18 + version: 4.1.17 '@types/node': specifier: ^22.13.4 - version: 22.19.7 + version: 22.18.0 '@types/react': specifier: ^19 - version: 19.2.10 + version: 19.1.12 '@types/react-dom': specifier: ^19 - version: 19.2.3(@types/react@19.2.10) + version: 19.1.9(@types/react@19.1.12) '@typescript-eslint/eslint-plugin': specifier: ^8.29.1 - version: 8.54.0(@typescript-eslint/parser@8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + version: 8.42.0(@typescript-eslint/parser@8.42.0(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.2))(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.2) '@typescript-eslint/parser': specifier: ^8.29.1 - version: 8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + version: 8.42.0(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.2) eslint: specifier: ^9.24.0 - version: 9.39.2(jiti@2.6.1) + version: 9.34.0(jiti@2.6.1) eslint-config-next: specifier: 16.0.6 - version: 16.0.6(@typescript-eslint/parser@8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + version: 16.0.6(@typescript-eslint/parser@8.42.0(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.2))(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.2) eslint-plugin-import: specifier: ^2.31.0 - version: 2.32.0(@typescript-eslint/parser@8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.2(jiti@2.6.1)) + version: 2.32.0(@typescript-eslint/parser@8.42.0(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.2))(eslint-import-resolver-typescript@3.10.1)(eslint@9.34.0(jiti@2.6.1)) eslint-plugin-jsdoc: specifier: ^50.6.9 - version: 50.8.0(eslint@9.39.2(jiti@2.6.1)) + version: 50.8.0(eslint@9.34.0(jiti@2.6.1)) eslint-plugin-prettier: specifier: ^5.2.6 - version: 5.5.5(eslint@9.39.2(jiti@2.6.1))(prettier@3.5.2) + version: 5.5.4(eslint@9.34.0(jiti@2.6.1))(prettier@3.5.2) prettier: specifier: 3.5.2 version: 3.5.2 tailwindcss: specifier: ^4.0.0 - version: 4.1.18 + version: 4.1.17 typescript: specifier: ^5 - version: 5.9.3 + version: 5.9.2 packages: + '@acemir/cssom@0.9.30': + resolution: {integrity: sha512-9CnlMCI0LmCIq0olalQqdWrJHPzm0/tw3gzOA9zJSgvFX7Xau3D24mAGa4BtwxwY69nsuJW6kQqqCzf/mEcQgg==} + '@adraffy/ens-normalize@1.10.1': resolution: {integrity: sha512-96Z2IP3mYmF1Xg2cDm8f1gWGf/HUVedQ3FMifV4kG/PQ4yEP51xDtRAEfhVNt5f/uzpNkZHwWQuUcu6D6K+Ekw==} - '@adraffy/ens-normalize@1.11.1': - resolution: {integrity: sha512-nhCBV3quEgesuf7c7KYfperqSS14T8bYuvJ8PcLJp6znkZpFc0AuW4qBtr8eKVyPPe/8RSr7sglCWPU5eaxwKQ==} + '@adraffy/ens-normalize@1.11.0': + resolution: {integrity: sha512-/3DDPKHqqIqxUULp8yP4zODUY1i+2xvVWsv8A79xGWdCAG+8sb0hRh0Rk2QyOJUnnbyPUAZYcpBuRe3nS2OIUg==} '@alloc/quick-lru@5.2.0': resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==} engines: {node: '>=10'} + '@ampproject/remapping@2.3.0': + resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==} + engines: {node: '>=6.0.0'} + '@aptos-labs/aptos-cli@1.1.1': resolution: {integrity: sha512-sB7CokCM6s76SLJmccysbnFR+MDik6udKfj2+9ZsmTLV0/t73veIeCDKbvWJmbW267ibx4HiGbPI7L+1+yjEbQ==} hasBin: true @@ -1437,44 +1557,50 @@ packages: '@asamuzakjp/css-color@4.1.1': resolution: {integrity: sha512-B0Hv6G3gWGMn0xKJ0txEi/jM5iFpT3MfDxmhZFb4W047GvytCf1DHQ1D69W3zHI4yWe2aTZAA0JnbMZ7Xc8DuQ==} - '@babel/code-frame@7.29.0': - resolution: {integrity: sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==} + '@asamuzakjp/dom-selector@6.7.6': + resolution: {integrity: sha512-hBaJER6A9MpdG3WgdlOolHmbOYvSk46y7IQN/1+iqiCuUu6iWdQrs9DGKF8ocqsEqWujWf/V7b7vaDgiUmIvUg==} + + '@asamuzakjp/nwsapi@2.3.9': + resolution: {integrity: sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==} + + '@babel/code-frame@7.27.1': + resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==} engines: {node: '>=6.9.0'} - '@babel/compat-data@7.29.0': - resolution: {integrity: sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==} + '@babel/compat-data@7.28.0': + resolution: {integrity: sha512-60X7qkglvrap8mn1lh2ebxXdZYtUcpd7gsmy9kLaBJ4i/WdY8PqTSdxyA8qraikqKQK5C1KRBKXqznrVapyNaw==} engines: {node: '>=6.9.0'} - '@babel/core@7.29.0': - resolution: {integrity: sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==} + '@babel/core@7.28.3': + resolution: {integrity: sha512-yDBHV9kQNcr2/sUr9jghVyz9C3Y5G2zUM2H2lo+9mKv4sFgbA8s8Z9t8D1jiTkGoO/NoIfKMyKWr4s6CN23ZwQ==} engines: {node: '>=6.9.0'} - '@babel/generator@7.29.0': - resolution: {integrity: sha512-vSH118/wwM/pLR38g/Sgk05sNtro6TlTJKuiMXDaZqPUfjTFcudpCOt00IhOfj+1BFAX+UFAlzCU+6WXr3GLFQ==} + '@babel/generator@7.28.3': + resolution: {integrity: sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw==} engines: {node: '>=6.9.0'} '@babel/helper-annotate-as-pure@7.27.3': resolution: {integrity: sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg==} engines: {node: '>=6.9.0'} - '@babel/helper-compilation-targets@7.28.6': - resolution: {integrity: sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==} + '@babel/helper-compilation-targets@7.27.2': + resolution: {integrity: sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==} engines: {node: '>=6.9.0'} - '@babel/helper-create-class-features-plugin@7.28.6': - resolution: {integrity: sha512-dTOdvsjnG3xNT9Y0AUg1wAl38y+4Rl4sf9caSQZOXdNqVn+H+HbbJ4IyyHaIqNR6SW9oJpA/RuRjsjCw2IdIow==} + '@babel/helper-create-class-features-plugin@7.28.3': + resolution: {integrity: sha512-V9f6ZFIYSLNEbuGA/92uOvYsGCJNsuA8ESZ4ldc09bWk/j8H8TKiPw8Mk1eG6olpnO0ALHJmYfZvF4MEE4gajg==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0 - '@babel/helper-create-regexp-features-plugin@7.28.5': - resolution: {integrity: sha512-N1EhvLtHzOvj7QQOUCCS3NrPJP8c5W6ZXCHDn7Yialuy1iu4r5EmIYkXlKNqT99Ciw+W0mDqWoR6HWMZlFP3hw==} + '@babel/helper-create-regexp-features-plugin@7.27.1': + resolution: {integrity: sha512-uVDC72XVf8UbrH5qQTc18Agb8emwjTiZrQE11Nv3CuBEZmVvTwwE9CBUEvHku06gQCAyYf8Nv6ja1IN+6LMbxQ==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0 - '@babel/helper-define-polyfill-provider@0.6.6': - resolution: {integrity: sha512-mOAsxeeKkUKayvZR3HeTYD/fICpCPLJrU5ZjelT/PA6WHtNDBOE436YiaEUvHN454bRM3CebhDsIpieCc4texA==} + '@babel/helper-define-polyfill-provider@0.6.5': + resolution: {integrity: sha512-uJnGFcPsWQK8fvjgGP5LZUZZsYGIoPeRjSF5PGwrelYgq7Q15/Ft9NGFp1zglwgIv//W0uG4BevRuSJRyylZPg==} peerDependencies: '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 @@ -1482,16 +1608,16 @@ packages: resolution: {integrity: sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==} engines: {node: '>=6.9.0'} - '@babel/helper-member-expression-to-functions@7.28.5': - resolution: {integrity: sha512-cwM7SBRZcPCLgl8a7cY0soT1SptSzAlMH39vwiRpOQkJlh53r5hdHwLSCZpQdVLT39sZt+CRpNwYG4Y2v77atg==} + '@babel/helper-member-expression-to-functions@7.27.1': + resolution: {integrity: sha512-E5chM8eWjTp/aNoVpcbfM7mLxu9XGLWYise2eBKGQomAk/Mb4XoxyqXTZbuTohbsl8EKqdlMhnDI2CCLfcs9wA==} engines: {node: '>=6.9.0'} - '@babel/helper-module-imports@7.28.6': - resolution: {integrity: sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==} + '@babel/helper-module-imports@7.27.1': + resolution: {integrity: sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==} engines: {node: '>=6.9.0'} - '@babel/helper-module-transforms@7.28.6': - resolution: {integrity: sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==} + '@babel/helper-module-transforms@7.28.3': + resolution: {integrity: sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0 @@ -1500,8 +1626,8 @@ packages: resolution: {integrity: sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw==} engines: {node: '>=6.9.0'} - '@babel/helper-plugin-utils@7.28.6': - resolution: {integrity: sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==} + '@babel/helper-plugin-utils@7.27.1': + resolution: {integrity: sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==} engines: {node: '>=6.9.0'} '@babel/helper-remap-async-to-generator@7.27.1': @@ -1510,8 +1636,8 @@ packages: peerDependencies: '@babel/core': ^7.0.0 - '@babel/helper-replace-supers@7.28.6': - resolution: {integrity: sha512-mq8e+laIk94/yFec3DxSjCRD2Z0TAjhVbEJY3UQrlwVo15Lmt7C2wAUbK4bjnTs4APkwsYLTahXRraQXhb1WCg==} + '@babel/helper-replace-supers@7.27.1': + resolution: {integrity: sha512-7EHz6qDZc8RYS5ElPoShMheWvEgERonFCs7IAonWLLUTXW59DP14bCZt89/GKyreYn8g3S83m21FelHKbeDCKA==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0 @@ -1524,29 +1650,29 @@ packages: resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} engines: {node: '>=6.9.0'} - '@babel/helper-validator-identifier@7.28.5': - resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==} + '@babel/helper-validator-identifier@7.27.1': + resolution: {integrity: sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==} engines: {node: '>=6.9.0'} '@babel/helper-validator-option@7.27.1': resolution: {integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==} engines: {node: '>=6.9.0'} - '@babel/helper-wrap-function@7.28.6': - resolution: {integrity: sha512-z+PwLziMNBeSQJonizz2AGnndLsP2DeGHIxDAn+wdHOGuo4Fo1x1HBPPXeE9TAOPHNNWQKCSlA2VZyYyyibDnQ==} + '@babel/helper-wrap-function@7.28.3': + resolution: {integrity: sha512-zdf983tNfLZFletc0RRXYrHrucBEg95NIFMkn6K9dbeMYnsgHaSBGcQqdsCSStG2PYwRre0Qc2NNSCXbG+xc6g==} engines: {node: '>=6.9.0'} - '@babel/helpers@7.28.6': - resolution: {integrity: sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==} + '@babel/helpers@7.28.3': + resolution: {integrity: sha512-PTNtvUQihsAsDHMOP5pfobP8C6CM4JWXmP8DrEIt46c3r2bf87Ua1zoqevsMo9g+tWDwgWrFP5EIxuBx5RudAw==} engines: {node: '>=6.9.0'} - '@babel/parser@7.29.0': - resolution: {integrity: sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==} + '@babel/parser@7.28.3': + resolution: {integrity: sha512-7+Ey1mAgYqFAx2h0RuoxcQT5+MlG3GTV0TQrgr7/ZliKsm/MNDxVVutlWaziMq7wJNAz8MTqz55XLpWvva6StA==} engines: {node: '>=6.0.0'} hasBin: true - '@babel/plugin-bugfix-firefox-class-in-computed-class-key@7.28.5': - resolution: {integrity: sha512-87GDMS3tsmMSi/3bWOte1UblL+YUTFMV8SZPZ2eSEL17s74Cw/l63rR6NmGVKMYW2GYi85nE+/d6Hw5N0bEk2Q==} + '@babel/plugin-bugfix-firefox-class-in-computed-class-key@7.27.1': + resolution: {integrity: sha512-QPG3C9cCVRQLxAVwmefEmwdTanECuUBMQZ/ym5kiw3XKCGA7qkuQLcjWWHcrD/GKbn/WmJwaezfuuAOcyKlRPA==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0 @@ -1569,8 +1695,8 @@ packages: peerDependencies: '@babel/core': ^7.13.0 - '@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@7.28.6': - resolution: {integrity: sha512-a0aBScVTlNaiUe35UtfxAN7A/tehvvG4/ByO6+46VPKTRSlfnAFsgKy0FUh+qAkQrDTmhDkT+IBOKlOoMUxQ0g==} + '@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@7.28.3': + resolution: {integrity: sha512-b6YTX108evsvE4YgWyQ921ZAFFQm3Bn+CA3+ZXlNVnPhx+UfsVURoPjfGAPCjBgrqo30yX/C2nZGX96DxvR9Iw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0 @@ -1581,26 +1707,26 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-syntax-import-assertions@7.28.6': - resolution: {integrity: sha512-pSJUpFHdx9z5nqTSirOCMtYVP2wFgoWhP0p3g8ONK/4IHhLIBd0B9NYqAvIUAhq+OkhO4VM1tENCt0cjlsNShw==} + '@babel/plugin-syntax-import-assertions@7.27.1': + resolution: {integrity: sha512-UT/Jrhw57xg4ILHLFnzFpPDlMbcdEicaAtjPQpbj9wa8T4r5KVWCimHcL/460g8Ht0DMxDyjsLgiWSkVjnwPFg==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-syntax-import-attributes@7.28.6': - resolution: {integrity: sha512-jiLC0ma9XkQT3TKJ9uYvlakm66Pamywo+qwL+oL8HJOvc6TWdZXVfhqJr8CCzbSGUAbDOzlGHJC1U+vRfLQDvw==} + '@babel/plugin-syntax-import-attributes@7.27.1': + resolution: {integrity: sha512-oFT0FrKHgF53f4vOsZGi2Hh3I35PfSmVs4IBFLFj4dnafP+hIWDLg3VyKmUHfLoLHlyxY4C7DGtmHuJgn+IGww==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-syntax-jsx@7.28.6': - resolution: {integrity: sha512-wgEmr06G6sIpqr8YDwA2dSRTE3bJ+V0IfpzfSY3Lfgd7YWOaAdlykvJi13ZKBt8cZHfgH1IXN+CL656W3uUa4w==} + '@babel/plugin-syntax-jsx@7.27.1': + resolution: {integrity: sha512-y8YTNIeKoyhGd9O0Jiyzyyqk8gdjnumGTQPsz0xOZOQ2RmkVJeZ1vmmfIvFEKqucBG6axJGBZDE/7iI5suUI/w==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-syntax-typescript@7.28.6': - resolution: {integrity: sha512-+nDNmQye7nlnuuHDboPbGm00Vqg3oO8niRRL27/4LYHUsHYh0zJ1xWOz0uRwNFmM1Avzk8wZbc6rdiYhomzv/A==} + '@babel/plugin-syntax-typescript@7.27.1': + resolution: {integrity: sha512-xfYCBMxveHrRMnAWl1ZlPXOZjzkN82THFvLhQhFXFt81Z5HnN+EtUkZhv/zcKpmT3fzmWZB0ywiBrbC3vogbwQ==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 @@ -1617,14 +1743,14 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-async-generator-functions@7.29.0': - resolution: {integrity: sha512-va0VdWro4zlBr2JsXC+ofCPB2iG12wPtVGTWFx2WLDOM3nYQZZIGP82qku2eW/JR83sD+k2k+CsNtyEbUqhU6w==} + '@babel/plugin-transform-async-generator-functions@7.28.0': + resolution: {integrity: sha512-BEOdvX4+M765icNPZeidyADIvQ1m1gmunXufXxvRESy/jNNyfovIqUyE7MVgGBjWktCoJlzvFA1To2O4ymIO3Q==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-async-to-generator@7.28.6': - resolution: {integrity: sha512-ilTRcmbuXjsMmcZ3HASTe4caH5Tpo93PkTxF9oG2VZsSWsahydmcEHhix9Ik122RcTnZnUzPbmux4wh1swfv7g==} + '@babel/plugin-transform-async-to-generator@7.27.1': + resolution: {integrity: sha512-NREkZsZVJS4xmTr8qzE5y8AfIPqsdQfRuUiLRTEzb7Qii8iFWCyDKaUV2c0rCuh4ljDZ98ALHP/PetiBV2nddA==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 @@ -1635,44 +1761,44 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-block-scoping@7.28.6': - resolution: {integrity: sha512-tt/7wOtBmwHPNMPu7ax4pdPz6shjFrmHDghvNC+FG9Qvj7D6mJcoRQIF5dy4njmxR941l6rgtvfSB2zX3VlUIw==} + '@babel/plugin-transform-block-scoping@7.28.0': + resolution: {integrity: sha512-gKKnwjpdx5sER/wl0WN0efUBFzF/56YZO0RJrSYP4CljXnP31ByY7fol89AzomdlLNzI36AvOTmYHsnZTCkq8Q==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-class-properties@7.28.6': - resolution: {integrity: sha512-dY2wS3I2G7D697VHndN91TJr8/AAfXQNt5ynCTI/MpxMsSzHp+52uNivYT5wCPax3whc47DR8Ba7cmlQMg24bw==} + '@babel/plugin-transform-class-properties@7.27.1': + resolution: {integrity: sha512-D0VcalChDMtuRvJIu3U/fwWjf8ZMykz5iZsg77Nuj821vCKI3zCyRLwRdWbsuJ/uRwZhZ002QtCqIkwC/ZkvbA==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-class-static-block@7.28.6': - resolution: {integrity: sha512-rfQ++ghVwTWTqQ7w8qyDxL1XGihjBss4CmTgGRCTAC9RIbhVpyp4fOeZtta0Lbf+dTNIVJer6ych2ibHwkZqsQ==} + '@babel/plugin-transform-class-static-block@7.28.3': + resolution: {integrity: sha512-LtPXlBbRoc4Njl/oh1CeD/3jC+atytbnf/UqLoqTDcEYGUPj022+rvfkbDYieUrSj3CaV4yHDByPE+T2HwfsJg==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.12.0 - '@babel/plugin-transform-classes@7.28.6': - resolution: {integrity: sha512-EF5KONAqC5zAqT783iMGuM2ZtmEBy+mJMOKl2BCvPZ2lVrwvXnB6o+OBWCS+CoeCCpVRF2sA2RBKUxvT8tQT5Q==} + '@babel/plugin-transform-classes@7.28.3': + resolution: {integrity: sha512-DoEWC5SuxuARF2KdKmGUq3ghfPMO6ZzR12Dnp5gubwbeWJo4dbNWXJPVlwvh4Zlq6Z7YVvL8VFxeSOJgjsx4Sg==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-computed-properties@7.28.6': - resolution: {integrity: sha512-bcc3k0ijhHbc2lEfpFHgx7eYw9KNXqOerKWfzbxEHUGKnS3sz9C4CNL9OiFN1297bDNfUiSO7DaLzbvHQQQ1BQ==} + '@babel/plugin-transform-computed-properties@7.27.1': + resolution: {integrity: sha512-lj9PGWvMTVksbWiDT2tW68zGS/cyo4AkZ/QTp0sQT0mjPopCmrSkzxeXkznjqBxzDI6TclZhOJbBmbBLjuOZUw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-destructuring@7.28.5': - resolution: {integrity: sha512-Kl9Bc6D0zTUcFUvkNuQh4eGXPKKNDOJQXVyyM4ZAQPMveniJdxi8XMJwLo+xSoW3MIq81bD33lcUe9kZpl0MCw==} + '@babel/plugin-transform-destructuring@7.28.0': + resolution: {integrity: sha512-v1nrSMBiKcodhsyJ4Gf+Z0U/yawmJDBOTpEB3mcQY52r9RIyPneGyAS/yM6seP/8I+mWI3elOMtT5dB8GJVs+A==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-dotall-regex@7.28.6': - resolution: {integrity: sha512-SljjowuNKB7q5Oayv4FoPzeB74g3QgLt8IVJw9ADvWy3QnUb/01aw8I4AVv8wYnPvQz2GDDZ/g3GhcNyDBI4Bg==} + '@babel/plugin-transform-dotall-regex@7.27.1': + resolution: {integrity: sha512-gEbkDVGRvjj7+T1ivxrfgygpT7GUd4vmODtYpbs0gZATdkX8/iSnOtZSxiZnsgm1YjTgjI6VKBGSJJevkrclzw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 @@ -1683,8 +1809,8 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-duplicate-named-capturing-groups-regex@7.29.0': - resolution: {integrity: sha512-zBPcW2lFGxdiD8PUnPwJjag2J9otbcLQzvbiOzDxpYXyCuYX9agOwMPGn1prVH0a4qzhCKu24rlH4c1f7yA8rw==} + '@babel/plugin-transform-duplicate-named-capturing-groups-regex@7.27.1': + resolution: {integrity: sha512-hkGcueTEzuhB30B3eJCbCYeCaaEQOmQR0AdvzpD4LoN0GXMWzzGSuRrxR2xTnCrvNbVwK9N6/jQ92GSLfiZWoQ==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0 @@ -1695,14 +1821,14 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-explicit-resource-management@7.28.6': - resolution: {integrity: sha512-Iao5Konzx2b6g7EPqTy40UZbcdXE126tTxVFr/nAIj+WItNxjKSYTEw3RC+A2/ZetmdJsgueL1KhaMCQHkLPIg==} + '@babel/plugin-transform-explicit-resource-management@7.28.0': + resolution: {integrity: sha512-K8nhUcn3f6iB+P3gwCv/no7OdzOZQcKchW6N389V6PD8NUWKZHzndOd9sPDVbMoBsbmjMqlB4L9fm+fEFNVlwQ==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-exponentiation-operator@7.28.6': - resolution: {integrity: sha512-WitabqiGjV/vJ0aPOLSFfNY1u9U3R7W36B03r5I2KoNix+a3sOhJ3pKFB3R5It9/UiK78NiO0KE9P21cMhlPkw==} + '@babel/plugin-transform-exponentiation-operator@7.27.1': + resolution: {integrity: sha512-uspvXnhHvGKf2r4VVtBpeFnuDWsJLQ6MF6lGJLC89jBR1uoVeqM416AZtTuhTezOfgHicpJQmoD5YUakO/YmXQ==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 @@ -1725,8 +1851,8 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-json-strings@7.28.6': - resolution: {integrity: sha512-Nr+hEN+0geQkzhbdgQVPoqr47lZbm+5fCUmO70722xJZd0Mvb59+33QLImGj6F+DkK3xgDi1YVysP8whD6FQAw==} + '@babel/plugin-transform-json-strings@7.27.1': + resolution: {integrity: sha512-6WVLVJiTjqcQauBhn1LkICsR2H+zm62I3h9faTDKt1qP4jn2o72tSvqMwtGFKGTpojce0gJs+76eZ2uCHRZh0Q==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 @@ -1737,8 +1863,8 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-logical-assignment-operators@7.28.6': - resolution: {integrity: sha512-+anKKair6gpi8VsM/95kmomGNMD0eLz1NQ8+Pfw5sAwWH9fGYXT50E55ZpV0pHUHWf6IUTWPM+f/7AAff+wr9A==} + '@babel/plugin-transform-logical-assignment-operators@7.27.1': + resolution: {integrity: sha512-SJvDs5dXxiae4FbSL1aBJlG4wvl594N6YEVVn9e3JGulwioy6z3oPjx/sQBO3Y4NwUu5HNix6KJ3wBZoewcdbw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 @@ -1755,14 +1881,14 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-modules-commonjs@7.28.6': - resolution: {integrity: sha512-jppVbf8IV9iWWwWTQIxJMAJCWBuuKx71475wHwYytrRGQ2CWiDvYlADQno3tcYpS/T2UUWFQp3nVtYfK/YBQrA==} + '@babel/plugin-transform-modules-commonjs@7.27.1': + resolution: {integrity: sha512-OJguuwlTYlN0gBZFRPqwOGNWssZjfIUdS7HMYtN8c1KmwpwHFBwTeFZrg9XZa+DFTitWOW5iTAG7tyCUPsCCyw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-modules-systemjs@7.29.0': - resolution: {integrity: sha512-PrujnVFbOdUpw4UHiVwKvKRLMMic8+eC0CuNlxjsyZUiBjhFdPsewdXCkveh2KqBA9/waD0W1b4hXSOBQJezpQ==} + '@babel/plugin-transform-modules-systemjs@7.27.1': + resolution: {integrity: sha512-w5N1XzsRbc0PQStASMksmUeqECuzKuTJer7kFagK8AXgpCMkeDMO5S+aaFb7A51ZYDF7XI34qsTX+fkHiIm5yA==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 @@ -1773,8 +1899,8 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-named-capturing-groups-regex@7.29.0': - resolution: {integrity: sha512-1CZQA5KNAD6ZYQLPw7oi5ewtDNxH/2vuCh+6SmvgDfhumForvs8a1o9n0UrEoBD8HU4djO2yWngTQlXl1NDVEQ==} + '@babel/plugin-transform-named-capturing-groups-regex@7.27.1': + resolution: {integrity: sha512-SstR5JYy8ddZvD6MhV0tM/j16Qds4mIpJTOd1Yu9J9pJjH93bxHECF7pgtc28XvkzTD6Pxcm/0Z73Hvk7kb3Ng==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0 @@ -1785,20 +1911,20 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-nullish-coalescing-operator@7.28.6': - resolution: {integrity: sha512-3wKbRgmzYbw24mDJXT7N+ADXw8BC/imU9yo9c9X9NKaLF1fW+e5H1U5QjMUBe4Qo4Ox/o++IyUkl1sVCLgevKg==} + '@babel/plugin-transform-nullish-coalescing-operator@7.27.1': + resolution: {integrity: sha512-aGZh6xMo6q9vq1JGcw58lZ1Z0+i0xB2x0XaauNIUXd6O1xXc3RwoWEBlsTQrY4KQ9Jf0s5rgD6SiNkaUdJegTA==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-numeric-separator@7.28.6': - resolution: {integrity: sha512-SJR8hPynj8outz+SlStQSwvziMN4+Bq99it4tMIf5/Caq+3iOc0JtKyse8puvyXkk3eFRIA5ID/XfunGgO5i6w==} + '@babel/plugin-transform-numeric-separator@7.27.1': + resolution: {integrity: sha512-fdPKAcujuvEChxDBJ5c+0BTaS6revLV7CJL08e4m3de8qJfNIuCc2nc7XJYOjBoTMJeqSmwXJ0ypE14RCjLwaw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-object-rest-spread@7.28.6': - resolution: {integrity: sha512-5rh+JR4JBC4pGkXLAcYdLHZjXudVxWMXbB6u6+E9lRL5TrGVbHt1TjxGbZ8CkmYw9zjkB7jutzOROArsqtncEA==} + '@babel/plugin-transform-object-rest-spread@7.28.0': + resolution: {integrity: sha512-9VNGikXxzu5eCiQjdE4IZn8sb9q7Xsk5EXLDBKUYg1e/Tve8/05+KJEtcxGxAgCY5t/BpKQM+JEL/yT4tvgiUA==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 @@ -1809,14 +1935,14 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-optional-catch-binding@7.28.6': - resolution: {integrity: sha512-R8ja/Pyrv0OGAvAXQhSTmWyPJPml+0TMqXlO5w+AsMEiwb2fg3WkOvob7UxFSL3OIttFSGSRFKQsOhJ/X6HQdQ==} + '@babel/plugin-transform-optional-catch-binding@7.27.1': + resolution: {integrity: sha512-txEAEKzYrHEX4xSZN4kJ+OfKXFVSWKB2ZxM9dpcE3wT7smwkNmXo5ORRlVzMVdJbD+Q8ILTgSD7959uj+3Dm3Q==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-optional-chaining@7.28.6': - resolution: {integrity: sha512-A4zobikRGJTsX9uqVFdafzGkqD30t26ck2LmOzAuLL8b2x6k3TIqRiT2xVvA9fNmFeTX484VpsdgmKNA0bS23w==} + '@babel/plugin-transform-optional-chaining@7.27.1': + resolution: {integrity: sha512-BQmKPPIuc8EkZgNKsv0X4bPmOoayeu4F1YCwx2/CfmDSXDbp7GnzlUH+/ul5VGfRg1AoFPsrIThlEBj2xb4CAg==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 @@ -1827,14 +1953,14 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-private-methods@7.28.6': - resolution: {integrity: sha512-piiuapX9CRv7+0st8lmuUlRSmX6mBcVeNQ1b4AYzJxfCMuBfB0vBXDiGSmm03pKJw1v6cZ8KSeM+oUnM6yAExg==} + '@babel/plugin-transform-private-methods@7.27.1': + resolution: {integrity: sha512-10FVt+X55AjRAYI9BrdISN9/AQWHqldOeZDUoLyif1Kn05a56xVBXb8ZouL8pZ9jem8QpXaOt8TS7RHUIS+GPA==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-private-property-in-object@7.28.6': - resolution: {integrity: sha512-b97jvNSOb5+ehyQmBpmhOCiUC5oVK4PMnpRvO7+ymFBoqYjeDHIU9jnrNUuwHOiL9RpGDoKBpSViarV+BU+eVA==} + '@babel/plugin-transform-private-property-in-object@7.27.1': + resolution: {integrity: sha512-5J+IhqTi1XPa0DXF83jYOaARrX+41gOewWbkPyjMNRDqgOCqdffGh8L3f/Ek5utaEBZExjSAzcyjmV9SSAWObQ==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 @@ -1863,8 +1989,8 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-react-jsx@7.28.6': - resolution: {integrity: sha512-61bxqhiRfAACulXSLd/GxqmAedUSrRZIu/cbaT18T1CetkTmtDN15it7i80ru4DVqRK1WMxQhXs+Lf9kajm5Ow==} + '@babel/plugin-transform-react-jsx@7.27.1': + resolution: {integrity: sha512-2KH4LWGSrJIkVf5tSiBFYuXDAoWRq2MMwgivCf+93dd0GQi8RXLjKA/0EvRnVV5G0hrHczsquXuD01L8s6dmBw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 @@ -1875,14 +2001,14 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-regenerator@7.29.0': - resolution: {integrity: sha512-FijqlqMA7DmRdg/aINBSs04y8XNTYw/lr1gJ2WsmBnnaNw1iS43EPkJW+zK7z65auG3AWRFXWj+NcTQwYptUog==} + '@babel/plugin-transform-regenerator@7.28.3': + resolution: {integrity: sha512-K3/M/a4+ESb5LEldjQb+XSrpY0nF+ZBFlTCbSnKaYAMfD8v33O6PMs4uYnOk19HlcsI8WMu3McdFPTiQHF/1/A==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-regexp-modifiers@7.28.6': - resolution: {integrity: sha512-QGWAepm9qxpaIs7UM9FvUSnCGlb8Ua1RhyM4/veAxLwt3gMat/LSGrZixyuj4I6+Kn9iwvqCyPTtbdxanYoWYg==} + '@babel/plugin-transform-regexp-modifiers@7.27.1': + resolution: {integrity: sha512-TtEciroaiODtXvLZv4rmfMhkCv8jx3wgKpL68PuiPh2M4fvz5jhsA7697N1gMvkvr/JTF13DrFYyEbY9U7cVPA==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0 @@ -1899,8 +2025,8 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-spread@7.28.6': - resolution: {integrity: sha512-9U4QObUC0FtJl05AsUcodau/RWDytrU6uKgkxu09mLR9HLDAtUMoPuuskm5huQsoktmsYpI+bGmq+iapDcriKA==} + '@babel/plugin-transform-spread@7.27.1': + resolution: {integrity: sha512-kpb3HUqaILBJcRFVhFUs6Trdd4mkrzcGXss+6/mxUd273PfbWqSDHRzMT2234gIg2QYfAjvXLSquP1xECSg09Q==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 @@ -1923,8 +2049,8 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-typescript@7.28.6': - resolution: {integrity: sha512-0YWL2RFxOqEm9Efk5PvreamxPME8OyY0wM5wh5lHjF+VtVhdneCWGzZeSqzOfiobVqQaNCd2z0tQvnI9DaPWPw==} + '@babel/plugin-transform-typescript@7.28.0': + resolution: {integrity: sha512-4AEiDEBPIZvLQaWlc9liCavE0xRM0dNca41WtBeM3jgFptfUOSG9z0uteLhq6+3rq+WB6jIvUwKDTpXEHPJ2Vg==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 @@ -1935,8 +2061,8 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-unicode-property-regex@7.28.6': - resolution: {integrity: sha512-4Wlbdl/sIZjzi/8St0evF0gEZrgOswVO6aOzqxh1kDZOl9WmLrHq2HtGhnOJZmHZYKP8WZ1MDLCt5DAWwRo57A==} + '@babel/plugin-transform-unicode-property-regex@7.27.1': + resolution: {integrity: sha512-uW20S39PnaTImxp39O5qFlHLS9LJEmANjMG7SxIhap8rCHqu0Ik+tLEPX5DKmHn6CsWQ7j3lix2tFOa5YtL12Q==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 @@ -1947,14 +2073,14 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-unicode-sets-regex@7.28.6': - resolution: {integrity: sha512-/wHc/paTUmsDYN7SZkpWxogTOBNnlx7nBQYfy6JJlCT7G3mVhltk3e++N7zV0XfgGsrqBxd4rJQt9H16I21Y1Q==} + '@babel/plugin-transform-unicode-sets-regex@7.27.1': + resolution: {integrity: sha512-EtkOujbc4cgvb0mlpQefi4NTPBzhSIevblFevACNLUspmrALgmEBdL/XfnyyITfd8fKBZrZys92zOWcik7j9Tw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0 - '@babel/preset-env@7.29.0': - resolution: {integrity: sha512-fNEdfc0yi16lt6IZo2Qxk3knHVdfMYX33czNb4v8yWhemoBhibCpQK/uYHtSKIiO+p/zd3+8fYVXhQdOVV608w==} + '@babel/preset-env@7.28.3': + resolution: {integrity: sha512-ROiDcM+GbYVPYBOeCR6uBXKkQpBExLl8k9HO1ygXEyds39j+vCCsjmj7S8GOniZQlEs81QlkdJZe76IpLSiqpg==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 @@ -1964,32 +2090,32 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 || ^8.0.0-0 <8.0.0 - '@babel/preset-react@7.28.5': - resolution: {integrity: sha512-Z3J8vhRq7CeLjdC58jLv4lnZ5RKFUJWqH5emvxmv9Hv3BD1T9R/Im713R4MTKwvFaV74ejZ3sM01LyEKk4ugNQ==} + '@babel/preset-react@7.27.1': + resolution: {integrity: sha512-oJHWh2gLhU9dW9HHr42q0cI0/iHHXTLGe39qvpAZZzagHy0MzYLCnCVV0symeRvzmjHyVU7mw2K06E6u/JwbhA==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/preset-typescript@7.28.5': - resolution: {integrity: sha512-+bQy5WOI2V6LJZpPVxY+yp66XdZ2yifu0Mc1aP5CQKgjn4QM5IN2i5fAZ4xKop47pr8rpVhiAeu+nDQa12C8+g==} + '@babel/preset-typescript@7.27.1': + resolution: {integrity: sha512-l7WfQfX0WK4M0v2RudjuQK4u99BS6yLHYEmdtVPP7lKV013zr9DygFuWNlnbvQ9LR+LS0Egz/XAvGx5U9MX0fQ==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/runtime@7.28.6': - resolution: {integrity: sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==} + '@babel/runtime@7.28.3': + resolution: {integrity: sha512-9uIQ10o0WGdpP6GDhXcdOJPJuDgFtIDtN/9+ArJQ2NAfAmiuhTQdzkaTGR33v43GYS2UrSA0eX2pPPHoFVvpxA==} engines: {node: '>=6.9.0'} - '@babel/template@7.28.6': - resolution: {integrity: sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==} + '@babel/template@7.27.2': + resolution: {integrity: sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==} engines: {node: '>=6.9.0'} - '@babel/traverse@7.29.0': - resolution: {integrity: sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==} + '@babel/traverse@7.28.3': + resolution: {integrity: sha512-7w4kZYHneL3A6NP2nxzHvT3HCZ7puDZZjFMqDpBPECub79sTtSO5CGXDkKrTQq8ksAwfD/XI2MRFX23njdDaIQ==} engines: {node: '>=6.9.0'} - '@babel/types@7.29.0': - resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==} + '@babel/types@7.28.2': + resolution: {integrity: sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ==} engines: {node: '>=6.9.0'} '@base-org/account@1.1.1': @@ -2059,8 +2185,8 @@ packages: '@changesets/write@0.4.0': resolution: {integrity: sha512-CdTLvIOPiCNuH71pyDu3rA+Q0n65cmAbXnwWH84rKGiFumFzkmHNT8KHTMEchcxN+Kl8I54xGUhJ7l3E7X396Q==} - '@coinbase/cdp-sdk@1.44.0': - resolution: {integrity: sha512-0I5O1DzbchR91GAYQAU8lxx6q9DBvN0no9IBwrTKLHW8t5bABMg8dzQ/jrGRd6lr/QFJJW4L0ZSLGae5jsxGWw==} + '@coinbase/cdp-sdk@1.36.1': + resolution: {integrity: sha512-eL8PfuPMXdEKQ6v/AwlDbTpiLhmnwZPAxzJ2m90qL8oEQuYenALX8JqNKg0LBQ/sPM6c2vx109YvFx49g5vSYQ==} '@coinbase/onchainkit@0.38.19': resolution: {integrity: sha512-4uiujoTO5/8/dpWVZoTlBC7z0Y1N5fgBYDR6pKN/r6a8pX83ObUuOSGhSzJ8Xbu8NpPU6TXX+VuzLiwiLg/irg==} @@ -2104,24 +2230,28 @@ packages: peerDependencies: '@csstools/css-tokenizer': ^3.0.4 - '@csstools/css-syntax-patches-for-csstree@1.0.26': - resolution: {integrity: sha512-6boXK0KkzT5u5xOgF6TKB+CLq9SOpEGmkZw0g5n9/7yg85wab3UzSxB8TxhLJ31L4SGJ6BCFRw/iftTha1CJXA==} + '@csstools/css-syntax-patches-for-csstree@1.0.22': + resolution: {integrity: sha512-qBcx6zYlhleiFfdtzkRgwNC7VVoAwfK76Vmsw5t+PbvtdknO9StgRk7ROvq9so1iqbdW4uLIDAsXRsTfUrIoOw==} + engines: {node: '>=18'} '@csstools/css-tokenizer@3.0.4': resolution: {integrity: sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==} engines: {node: '>=18'} - '@ecies/ciphers@0.2.5': - resolution: {integrity: sha512-GalEZH4JgOMHYYcYmVqnFirFsjZHeoGMDt9IxEnM9F7GRUUyUksJ7Ou53L83WHJq3RWKD3AcBpo0iQh0oMpf8A==} + '@ecies/ciphers@0.2.4': + resolution: {integrity: sha512-t+iX+Wf5nRKyNzk8dviW3Ikb/280+aEJAnw9YXvCp2tYGPSkMki+NRY+8aNLmVFv3eNtMdvViPNOPxS8SZNP+w==} engines: {bun: '>=1', deno: '>=2', node: '>=16'} peerDependencies: '@noble/ciphers': ^1.0.0 - '@emnapi/core@1.8.1': - resolution: {integrity: sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg==} + '@emnapi/core@1.5.0': + resolution: {integrity: sha512-sbP8GzB1WDzacS8fgNPpHlp6C9VZe+SJP3F90W9rLemaQj2PzIuTEl1qDOYQf58YIpyjViI24y9aPWCjEzY2cg==} + + '@emnapi/runtime@1.5.0': + resolution: {integrity: sha512-97/BJ3iXHww3djw6hYIfErCZFee7qCtrneuLa20UXFCOTCfBM2cvQHjWJ2EG0s0MtdNwInarqCTz35i4wWXHsQ==} - '@emnapi/runtime@1.8.1': - resolution: {integrity: sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg==} + '@emnapi/runtime@1.7.1': + resolution: {integrity: sha512-PVtJr5CmLwYAU9PZDMITZoR5iAOShYREoR45EyyLrbntV50mdePTgUn4AmOw90Ifcj+x2kRjdzr1HP3RrNiHGA==} '@emnapi/wasi-threads@1.1.0': resolution: {integrity: sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==} @@ -2130,354 +2260,198 @@ packages: resolution: {integrity: sha512-YAdE/IJSpwbOTiaURNCKECdAwqrJuFiZhylmesBcIRawtYKnBR2wxPhoIewMg+Yu+QuYvHfJNReWpoxGBKOChA==} engines: {node: '>=18'} - '@esbuild/aix-ppc64@0.25.12': - resolution: {integrity: sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==} - engines: {node: '>=18'} - cpu: [ppc64] - os: [aix] - - '@esbuild/aix-ppc64@0.27.2': - resolution: {integrity: sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==} + '@esbuild/aix-ppc64@0.25.9': + resolution: {integrity: sha512-OaGtL73Jck6pBKjNIe24BnFE6agGl+6KxDtTfHhy1HmhthfKouEcOhqpSL64K4/0WCtbKFLOdzD/44cJ4k9opA==} engines: {node: '>=18'} cpu: [ppc64] os: [aix] - '@esbuild/android-arm64@0.25.12': - resolution: {integrity: sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==} - engines: {node: '>=18'} - cpu: [arm64] - os: [android] - - '@esbuild/android-arm64@0.27.2': - resolution: {integrity: sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA==} + '@esbuild/android-arm64@0.25.9': + resolution: {integrity: sha512-IDrddSmpSv51ftWslJMvl3Q2ZT98fUSL2/rlUXuVqRXHCs5EUF1/f+jbjF5+NG9UffUDMCiTyh8iec7u8RlTLg==} engines: {node: '>=18'} cpu: [arm64] os: [android] - '@esbuild/android-arm@0.25.12': - resolution: {integrity: sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==} - engines: {node: '>=18'} - cpu: [arm] - os: [android] - - '@esbuild/android-arm@0.27.2': - resolution: {integrity: sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA==} + '@esbuild/android-arm@0.25.9': + resolution: {integrity: sha512-5WNI1DaMtxQ7t7B6xa572XMXpHAaI/9Hnhk8lcxF4zVN4xstUgTlvuGDorBguKEnZO70qwEcLpfifMLoxiPqHQ==} engines: {node: '>=18'} cpu: [arm] os: [android] - '@esbuild/android-x64@0.25.12': - resolution: {integrity: sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==} - engines: {node: '>=18'} - cpu: [x64] - os: [android] - - '@esbuild/android-x64@0.27.2': - resolution: {integrity: sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A==} + '@esbuild/android-x64@0.25.9': + resolution: {integrity: sha512-I853iMZ1hWZdNllhVZKm34f4wErd4lMyeV7BLzEExGEIZYsOzqDWDf+y082izYUE8gtJnYHdeDpN/6tUdwvfiw==} engines: {node: '>=18'} cpu: [x64] os: [android] - '@esbuild/darwin-arm64@0.25.12': - resolution: {integrity: sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==} - engines: {node: '>=18'} - cpu: [arm64] - os: [darwin] - - '@esbuild/darwin-arm64@0.27.2': - resolution: {integrity: sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg==} + '@esbuild/darwin-arm64@0.25.9': + resolution: {integrity: sha512-XIpIDMAjOELi/9PB30vEbVMs3GV1v2zkkPnuyRRURbhqjyzIINwj+nbQATh4H9GxUgH1kFsEyQMxwiLFKUS6Rg==} engines: {node: '>=18'} cpu: [arm64] os: [darwin] - '@esbuild/darwin-x64@0.25.12': - resolution: {integrity: sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==} - engines: {node: '>=18'} - cpu: [x64] - os: [darwin] - - '@esbuild/darwin-x64@0.27.2': - resolution: {integrity: sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA==} + '@esbuild/darwin-x64@0.25.9': + resolution: {integrity: sha512-jhHfBzjYTA1IQu8VyrjCX4ApJDnH+ez+IYVEoJHeqJm9VhG9Dh2BYaJritkYK3vMaXrf7Ogr/0MQ8/MeIefsPQ==} engines: {node: '>=18'} cpu: [x64] os: [darwin] - '@esbuild/freebsd-arm64@0.25.12': - resolution: {integrity: sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==} - engines: {node: '>=18'} - cpu: [arm64] - os: [freebsd] - - '@esbuild/freebsd-arm64@0.27.2': - resolution: {integrity: sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g==} + '@esbuild/freebsd-arm64@0.25.9': + resolution: {integrity: sha512-z93DmbnY6fX9+KdD4Ue/H6sYs+bhFQJNCPZsi4XWJoYblUqT06MQUdBCpcSfuiN72AbqeBFu5LVQTjfXDE2A6Q==} engines: {node: '>=18'} cpu: [arm64] os: [freebsd] - '@esbuild/freebsd-x64@0.25.12': - resolution: {integrity: sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==} - engines: {node: '>=18'} - cpu: [x64] - os: [freebsd] - - '@esbuild/freebsd-x64@0.27.2': - resolution: {integrity: sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA==} + '@esbuild/freebsd-x64@0.25.9': + resolution: {integrity: sha512-mrKX6H/vOyo5v71YfXWJxLVxgy1kyt1MQaD8wZJgJfG4gq4DpQGpgTB74e5yBeQdyMTbgxp0YtNj7NuHN0PoZg==} engines: {node: '>=18'} cpu: [x64] os: [freebsd] - '@esbuild/linux-arm64@0.25.12': - resolution: {integrity: sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==} - engines: {node: '>=18'} - cpu: [arm64] - os: [linux] - - '@esbuild/linux-arm64@0.27.2': - resolution: {integrity: sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw==} + '@esbuild/linux-arm64@0.25.9': + resolution: {integrity: sha512-BlB7bIcLT3G26urh5Dmse7fiLmLXnRlopw4s8DalgZ8ef79Jj4aUcYbk90g8iCa2467HX8SAIidbL7gsqXHdRw==} engines: {node: '>=18'} cpu: [arm64] os: [linux] - '@esbuild/linux-arm@0.25.12': - resolution: {integrity: sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==} - engines: {node: '>=18'} - cpu: [arm] - os: [linux] - - '@esbuild/linux-arm@0.27.2': - resolution: {integrity: sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw==} + '@esbuild/linux-arm@0.25.9': + resolution: {integrity: sha512-HBU2Xv78SMgaydBmdor38lg8YDnFKSARg1Q6AT0/y2ezUAKiZvc211RDFHlEZRFNRVhcMamiToo7bDx3VEOYQw==} engines: {node: '>=18'} cpu: [arm] os: [linux] - '@esbuild/linux-ia32@0.25.12': - resolution: {integrity: sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==} - engines: {node: '>=18'} - cpu: [ia32] - os: [linux] - - '@esbuild/linux-ia32@0.27.2': - resolution: {integrity: sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w==} + '@esbuild/linux-ia32@0.25.9': + resolution: {integrity: sha512-e7S3MOJPZGp2QW6AK6+Ly81rC7oOSerQ+P8L0ta4FhVi+/j/v2yZzx5CqqDaWjtPFfYz21Vi1S0auHrap3Ma3A==} engines: {node: '>=18'} cpu: [ia32] os: [linux] - '@esbuild/linux-loong64@0.25.12': - resolution: {integrity: sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==} - engines: {node: '>=18'} - cpu: [loong64] - os: [linux] - - '@esbuild/linux-loong64@0.27.2': - resolution: {integrity: sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg==} + '@esbuild/linux-loong64@0.25.9': + resolution: {integrity: sha512-Sbe10Bnn0oUAB2AalYztvGcK+o6YFFA/9829PhOCUS9vkJElXGdphz0A3DbMdP8gmKkqPmPcMJmJOrI3VYB1JQ==} engines: {node: '>=18'} cpu: [loong64] os: [linux] - '@esbuild/linux-mips64el@0.25.12': - resolution: {integrity: sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==} - engines: {node: '>=18'} - cpu: [mips64el] - os: [linux] - - '@esbuild/linux-mips64el@0.27.2': - resolution: {integrity: sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw==} + '@esbuild/linux-mips64el@0.25.9': + resolution: {integrity: sha512-YcM5br0mVyZw2jcQeLIkhWtKPeVfAerES5PvOzaDxVtIyZ2NUBZKNLjC5z3/fUlDgT6w89VsxP2qzNipOaaDyA==} engines: {node: '>=18'} cpu: [mips64el] os: [linux] - '@esbuild/linux-ppc64@0.25.12': - resolution: {integrity: sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==} - engines: {node: '>=18'} - cpu: [ppc64] - os: [linux] - - '@esbuild/linux-ppc64@0.27.2': - resolution: {integrity: sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ==} + '@esbuild/linux-ppc64@0.25.9': + resolution: {integrity: sha512-++0HQvasdo20JytyDpFvQtNrEsAgNG2CY1CLMwGXfFTKGBGQT3bOeLSYE2l1fYdvML5KUuwn9Z8L1EWe2tzs1w==} engines: {node: '>=18'} cpu: [ppc64] os: [linux] - '@esbuild/linux-riscv64@0.25.12': - resolution: {integrity: sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==} - engines: {node: '>=18'} - cpu: [riscv64] - os: [linux] - - '@esbuild/linux-riscv64@0.27.2': - resolution: {integrity: sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA==} + '@esbuild/linux-riscv64@0.25.9': + resolution: {integrity: sha512-uNIBa279Y3fkjV+2cUjx36xkx7eSjb8IvnL01eXUKXez/CBHNRw5ekCGMPM0BcmqBxBcdgUWuUXmVWwm4CH9kg==} engines: {node: '>=18'} cpu: [riscv64] os: [linux] - '@esbuild/linux-s390x@0.25.12': - resolution: {integrity: sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==} - engines: {node: '>=18'} - cpu: [s390x] - os: [linux] - - '@esbuild/linux-s390x@0.27.2': - resolution: {integrity: sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w==} + '@esbuild/linux-s390x@0.25.9': + resolution: {integrity: sha512-Mfiphvp3MjC/lctb+7D287Xw1DGzqJPb/J2aHHcHxflUo+8tmN/6d4k6I2yFR7BVo5/g7x2Monq4+Yew0EHRIA==} engines: {node: '>=18'} cpu: [s390x] os: [linux] - '@esbuild/linux-x64@0.25.12': - resolution: {integrity: sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==} - engines: {node: '>=18'} - cpu: [x64] - os: [linux] - - '@esbuild/linux-x64@0.27.2': - resolution: {integrity: sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA==} + '@esbuild/linux-x64@0.25.9': + resolution: {integrity: sha512-iSwByxzRe48YVkmpbgoxVzn76BXjlYFXC7NvLYq+b+kDjyyk30J0JY47DIn8z1MO3K0oSl9fZoRmZPQI4Hklzg==} engines: {node: '>=18'} cpu: [x64] os: [linux] - '@esbuild/netbsd-arm64@0.25.12': - resolution: {integrity: sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==} - engines: {node: '>=18'} - cpu: [arm64] - os: [netbsd] - - '@esbuild/netbsd-arm64@0.27.2': - resolution: {integrity: sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw==} + '@esbuild/netbsd-arm64@0.25.9': + resolution: {integrity: sha512-9jNJl6FqaUG+COdQMjSCGW4QiMHH88xWbvZ+kRVblZsWrkXlABuGdFJ1E9L7HK+T0Yqd4akKNa/lO0+jDxQD4Q==} engines: {node: '>=18'} cpu: [arm64] os: [netbsd] - '@esbuild/netbsd-x64@0.25.12': - resolution: {integrity: sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==} - engines: {node: '>=18'} - cpu: [x64] - os: [netbsd] - - '@esbuild/netbsd-x64@0.27.2': - resolution: {integrity: sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA==} + '@esbuild/netbsd-x64@0.25.9': + resolution: {integrity: sha512-RLLdkflmqRG8KanPGOU7Rpg829ZHu8nFy5Pqdi9U01VYtG9Y0zOG6Vr2z4/S+/3zIyOxiK6cCeYNWOFR9QP87g==} engines: {node: '>=18'} cpu: [x64] os: [netbsd] - '@esbuild/openbsd-arm64@0.25.12': - resolution: {integrity: sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==} - engines: {node: '>=18'} - cpu: [arm64] - os: [openbsd] - - '@esbuild/openbsd-arm64@0.27.2': - resolution: {integrity: sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA==} + '@esbuild/openbsd-arm64@0.25.9': + resolution: {integrity: sha512-YaFBlPGeDasft5IIM+CQAhJAqS3St3nJzDEgsgFixcfZeyGPCd6eJBWzke5piZuZ7CtL656eOSYKk4Ls2C0FRQ==} engines: {node: '>=18'} cpu: [arm64] os: [openbsd] - '@esbuild/openbsd-x64@0.25.12': - resolution: {integrity: sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==} - engines: {node: '>=18'} - cpu: [x64] - os: [openbsd] - - '@esbuild/openbsd-x64@0.27.2': - resolution: {integrity: sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg==} + '@esbuild/openbsd-x64@0.25.9': + resolution: {integrity: sha512-1MkgTCuvMGWuqVtAvkpkXFmtL8XhWy+j4jaSO2wxfJtilVCi0ZE37b8uOdMItIHz4I6z1bWWtEX4CJwcKYLcuA==} engines: {node: '>=18'} cpu: [x64] os: [openbsd] - '@esbuild/openharmony-arm64@0.25.12': - resolution: {integrity: sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==} - engines: {node: '>=18'} - cpu: [arm64] - os: [openharmony] - - '@esbuild/openharmony-arm64@0.27.2': - resolution: {integrity: sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag==} + '@esbuild/openharmony-arm64@0.25.9': + resolution: {integrity: sha512-4Xd0xNiMVXKh6Fa7HEJQbrpP3m3DDn43jKxMjxLLRjWnRsfxjORYJlXPO4JNcXtOyfajXorRKY9NkOpTHptErg==} engines: {node: '>=18'} cpu: [arm64] os: [openharmony] - '@esbuild/sunos-x64@0.25.12': - resolution: {integrity: sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==} - engines: {node: '>=18'} - cpu: [x64] - os: [sunos] - - '@esbuild/sunos-x64@0.27.2': - resolution: {integrity: sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg==} + '@esbuild/sunos-x64@0.25.9': + resolution: {integrity: sha512-WjH4s6hzo00nNezhp3wFIAfmGZ8U7KtrJNlFMRKxiI9mxEK1scOMAaa9i4crUtu+tBr+0IN6JCuAcSBJZfnphw==} engines: {node: '>=18'} cpu: [x64] os: [sunos] - '@esbuild/win32-arm64@0.25.12': - resolution: {integrity: sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==} - engines: {node: '>=18'} - cpu: [arm64] - os: [win32] - - '@esbuild/win32-arm64@0.27.2': - resolution: {integrity: sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg==} + '@esbuild/win32-arm64@0.25.9': + resolution: {integrity: sha512-mGFrVJHmZiRqmP8xFOc6b84/7xa5y5YvR1x8djzXpJBSv/UsNK6aqec+6JDjConTgvvQefdGhFDAs2DLAds6gQ==} engines: {node: '>=18'} cpu: [arm64] os: [win32] - '@esbuild/win32-ia32@0.25.12': - resolution: {integrity: sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==} - engines: {node: '>=18'} - cpu: [ia32] - os: [win32] - - '@esbuild/win32-ia32@0.27.2': - resolution: {integrity: sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ==} + '@esbuild/win32-ia32@0.25.9': + resolution: {integrity: sha512-b33gLVU2k11nVx1OhX3C8QQP6UHQK4ZtN56oFWvVXvz2VkDoe6fbG8TOgHFxEvqeqohmRnIHe5A1+HADk4OQww==} engines: {node: '>=18'} cpu: [ia32] os: [win32] - '@esbuild/win32-x64@0.25.12': - resolution: {integrity: sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==} - engines: {node: '>=18'} - cpu: [x64] - os: [win32] - - '@esbuild/win32-x64@0.27.2': - resolution: {integrity: sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==} + '@esbuild/win32-x64@0.25.9': + resolution: {integrity: sha512-PPOl1mi6lpLNQxnGoyAfschAodRFYXJ+9fs6WHXz7CSWKbOqiMZsubC+BQsVKuul+3vKLuwTHsS2c2y9EoKwxQ==} engines: {node: '>=18'} cpu: [x64] os: [win32] - '@eslint-community/eslint-utils@4.9.1': - resolution: {integrity: sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==} + '@eslint-community/eslint-utils@4.8.0': + resolution: {integrity: sha512-MJQFqrZgcW0UNYLGOuQpey/oTN59vyWwplvCGZztn1cKz9agZPPYpJB7h2OMmuu7VLqkvEjN8feFZJmxNF9D+Q==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 - '@eslint-community/regexpp@4.12.2': - resolution: {integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==} + '@eslint-community/regexpp@4.12.1': + resolution: {integrity: sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==} engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} - '@eslint/config-array@0.21.1': - resolution: {integrity: sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==} + '@eslint/config-array@0.21.0': + resolution: {integrity: sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/config-helpers@0.4.2': - resolution: {integrity: sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==} + '@eslint/config-helpers@0.3.1': + resolution: {integrity: sha512-xR93k9WhrDYpXHORXpxVL5oHj3Era7wo6k/Wd8/IsQNnZUTzkGS29lyn3nAT05v6ltUuTFVCCYDEGfy2Or/sPA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/core@0.17.0': - resolution: {integrity: sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==} + '@eslint/core@0.15.2': + resolution: {integrity: sha512-78Md3/Rrxh83gCxoUc0EiciuOHsIITzLy53m3d9UyiW8y9Dj2D29FeETqyKA+BRK76tnTp6RXWb3pCay8Oyomg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/eslintrc@3.3.3': - resolution: {integrity: sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ==} + '@eslint/eslintrc@3.3.1': + resolution: {integrity: sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/js@9.39.2': - resolution: {integrity: sha512-q1mjIoW1VX4IvSocvM/vbTiveKC4k9eLrajNEuSsmjymSDEbpGddtpfOoN7YGAqBK3NG+uqo8ia4PDTt8buCYA==} + '@eslint/js@9.34.0': + resolution: {integrity: sha512-EoyvqQnBNsV1CWaEJ559rxXL4c8V92gxirbawSmVUOWXlsRxxQXl6LmCpdUblgxgSkDIqKnhzba2SjRTI/A5Rw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/object-schema@2.1.7': - resolution: {integrity: sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==} + '@eslint/object-schema@2.1.6': + resolution: {integrity: sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/plugin-kit@0.4.1': - resolution: {integrity: sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==} + '@eslint/plugin-kit@0.3.5': + resolution: {integrity: sha512-Z5kJ+wU3oA7MMIqVR9tyZRtjYPr4OC004Q4Rw7pgOKUOKkJfZ3O24nz3WYfGRpMDNmcOi3TwQOmgm7B7Tpii0w==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@ethereumjs/common@3.2.0': @@ -2496,20 +2470,29 @@ packages: resolution: {integrity: sha512-zQ0IqbdX8FZ9aw11vP+dZkKDkS+kgIvQPHnSAXzP9pLu+Rfu3D3XEeLbicvoXJTYnhZiPmsZUxgdzXwNKxRPbA==} engines: {node: '>=14'} - '@farcaster/frame-sdk@0.1.13': - resolution: {integrity: sha512-x4r6rz2vsReCaE0SThFx2rL9BTc9BW5UKcv2ednOFSSzejHSlxxd0SHH27Insrv4w56r6OAg+5YFGPwh3LZ/WA==} + '@exodus/bytes@1.8.0': + resolution: {integrity: sha512-8JPn18Bcp8Uo1T82gR8lh2guEOa5KKU/IEKvvdp0sgmi7coPBWf1Doi1EXsGZb2ehc8ym/StJCjffYV+ne7sXQ==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + peerDependencies: + '@exodus/crypto': ^1.0.0-rc.4 + peerDependenciesMeta: + '@exodus/crypto': + optional: true + + '@farcaster/frame-sdk@0.1.9': + resolution: {integrity: sha512-r5cAKgHn4w8Q1jaCi84uKqItfNRd6h8Lk0YyQaz5kMoEIeJ4C0gXPpyqKPYP2TVDFuvaexg2KvzCO2CQdygWyQ==} engines: {node: '>=22.11.0'} - '@farcaster/miniapp-core@0.5.0': - resolution: {integrity: sha512-3d4rMjKrSv6z2eUHczAP00p3+0zRaoSNUgQUyLM2BLpoi43cXmXmSP5E/tX0xE5//vkBKPLRzXE+nKoMyNL5kQ==} + '@farcaster/miniapp-core@0.3.8': + resolution: {integrity: sha512-LaRG1L3lxHqo5pP/E2CX9hNqusR0C8hX3QTV2+hzmQJz6IGvmSpH6Q9ivlLyDfbdqokiMFo5Y3Z1EX1zBHMEQQ==} - '@farcaster/miniapp-sdk@0.2.2': - resolution: {integrity: sha512-Z1POThw2yZL255BxZ+18hx+w6rzK47XzovSHpnKSmPFBqfIOPV5J3DuuhNe7vSQeQuMwZnYeUz8TvzxHqvvhPw==} + '@farcaster/miniapp-sdk@0.1.9': + resolution: {integrity: sha512-hn0dlIy0JP2Hx6PgKcn9bjYwyPS/SQgYJ/a0qjzG8ZsDfUdjsMPf3yI/jicBipTml/UUoKcbqXM68fsrsbNMKA==} - '@farcaster/miniapp-wagmi-connector@1.1.0': - resolution: {integrity: sha512-gf0nDx9nNJ6hJXbFBCgiTitb0eEqBvCU/njcyTXf7ebZhT0pzOrarOod2dkeisU5Py+WWjFyOVcqmeo4G3IvDA==} + '@farcaster/miniapp-wagmi-connector@1.0.0': + resolution: {integrity: sha512-vMRZbekUUctnAUvBFhNoEsJlujRRdxop94fDy5LrKiRR9ax0wtp8gCvLYO+LpaP2PtGs0HFpRwlHNDJWvBR8bg==} peerDependencies: - '@farcaster/miniapp-sdk': ^0.2.0 + '@farcaster/miniapp-sdk': ^0.1.0 '@wagmi/core': ^2.14.1 viem: ^2.21.55 @@ -2518,10 +2501,23 @@ packages: peerDependencies: typescript: 5.8.3 - '@farcaster/quick-auth@0.0.8': - resolution: {integrity: sha512-NRIq1BcbcQCC6xBF5owfckkY00xKQVpqhpLNl5rICVpl0xeDsiVbkenIrHaUuyjtCK2W28YVc2ZCFRyz9ERHKg==} - peerDependencies: - typescript: 5.8.3 + '@fastify/ajv-compiler@4.0.5': + resolution: {integrity: sha512-KoWKW+MhvfTRWL4qrhUwAAZoaChluo0m0vbiJlGMt2GXvL4LVPQEjt8kSpHI3IBq5Rez8fg+XeH3cneztq+C7A==} + + '@fastify/error@4.2.0': + resolution: {integrity: sha512-RSo3sVDXfHskiBZKBPRgnQTtIqpi/7zhJOEmAxCiBcM7d0uwdGdxLlsCaLzGs8v8NnxIRlfG0N51p5yFaOentQ==} + + '@fastify/fast-json-stringify-compiler@5.0.3': + resolution: {integrity: sha512-uik7yYHkLr6fxd8hJSZ8c+xF4WafPK+XzneQDPU+D10r5X19GW8lJcom2YijX2+qtFF1ENJlHXKFM9ouXNJYgQ==} + + '@fastify/forwarded@3.0.1': + resolution: {integrity: sha512-JqDochHFqXs3C3Ml3gOY58zM7OqO9ENqPo0UqAjAjH8L01fRZqwX9iLeX34//kiJubF7r2ZQHtBRU36vONbLlw==} + + '@fastify/merge-json-schemas@0.2.1': + resolution: {integrity: sha512-OA3KGBCy6KtIvLf8DINC5880o5iBlDX4SxzLQS8HorJAbqluzLRn80UXU0bxZn7UOFhFgpRJDasfwn9nG4FG4A==} + + '@fastify/proxy-addr@5.1.0': + resolution: {integrity: sha512-INS+6gh91cLUjB+PVHfu1UqcB76Sqtpyp7bnL+FYojhjygvOPA9ctiD/JDKsyD9Xgu4hUhCSJBPig/w7duNajw==} '@gemini-wallet/core@0.2.0': resolution: {integrity: sha512-vv9aozWnKrrPWQ3vIFcWk7yta4hQW1Ie0fsNNPeXnjAxkbXr2hqMagEptLuMxpEP2W3mnRu05VDNKzcvAuuZDw==} @@ -2543,6 +2539,12 @@ packages: peerDependencies: react: '>= 16 || ^19.0.0-rc' + '@hono/node-server@1.19.1': + resolution: {integrity: sha512-h44e5s+ByUriaRIbeS/C74O8v90m0A95luyYQGMF7KEn96KkYMXO7bZAwombzTpjQTU4e0TkU8U1WBIXlwuwtA==} + engines: {node: '>=18.14.1'} + peerDependencies: + hono: ^4 + '@hono/node-server@1.19.9': resolution: {integrity: sha512-vHL6w3ecZsky+8P5MD+eFfaGTyCeOHUIFYMGpQGbrBTSmNNoxv0if69rEZ5giu36weC5saFuznL411gRX7bJDw==} engines: {node: '>=18.14.1'} @@ -2711,6 +2713,10 @@ packages: '@types/node': optional: true + '@isaacs/cliui@8.0.2': + resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} + engines: {node: '>=12'} + '@jridgewell/gen-mapping@0.3.13': resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} @@ -2724,14 +2730,14 @@ packages: '@jridgewell/sourcemap-codec@1.5.5': resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} - '@jridgewell/trace-mapping@0.3.31': - resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + '@jridgewell/trace-mapping@0.3.30': + resolution: {integrity: sha512-GQ7Nw5G2lTu/BtHTKfXhKHok2WGetd4XYcVKGx00SjAk8GMwgJM3zr6zORiPGuOE+/vkc90KtTosSSvaCjKb2Q==} - '@lit-labs/ssr-dom-shim@1.5.1': - resolution: {integrity: sha512-Aou5UdlSpr5whQe8AA/bZG0jMj96CoJIWbGfZ91qieWu5AWUMKw8VR/pAkQkJYvBNhmCcWnZlyyk5oze8JIqYA==} + '@lit-labs/ssr-dom-shim@1.4.0': + resolution: {integrity: sha512-ficsEARKnmmW5njugNYKipTm4SFnbik7CXtoencDZzmzo/dQ+2Q0bgkzJuoJP20Aj0F+izzJjOqsnkd6F/o1bw==} - '@lit/reactive-element@2.1.2': - resolution: {integrity: sha512-pbCDiVMnne1lYUIaYNN5wrwQXDtHaYtg7YEFPeW+hws6U47WeFvISGUWekPGKWOP1ygrs0ef0o1VJMk1exos5A==} + '@lit/reactive-element@2.1.1': + resolution: {integrity: sha512-N+dm5PAYdQ8e6UlywyyrgI2t++wFGXfHx+dSJ1oBrg6FAxUj40jId++EaRm80MKX5JnlH1sBsyZ5h0bcZKemCg==} '@manypkg/find-root@1.1.0': resolution: {integrity: sha512-mki5uBvhHzO8kYYix/WRy2WX8S3B5wdVSc9D6KcU5lQNglP2yt58/VfLuAK49glRXChosY8ap2oJ1qgma3GUVA==} @@ -2784,6 +2790,15 @@ packages: '@metamask/sdk-analytics@0.0.5': resolution: {integrity: sha512-fDah+keS1RjSUlC8GmYXvx6Y26s3Ax1U9hGpWb6GSY5SAdmTSIqp2CvYy6yW0WgLhnYhW+6xERuD0eVqV63QIQ==} + '@metamask/sdk-communication-layer@0.32.0': + resolution: {integrity: sha512-dmj/KFjMi1fsdZGIOtbhxdg3amxhKL/A5BqSU4uh/SyDKPub/OT+x5pX8bGjpTL1WPWY/Q0OIlvFyX3VWnT06Q==} + peerDependencies: + cross-fetch: ^4.0.0 + eciesjs: '*' + eventemitter2: ^6.4.9 + readable-stream: ^3.6.2 + socket.io-client: ^4.5.1 + '@metamask/sdk-communication-layer@0.33.1': resolution: {integrity: sha512-0bI9hkysxcfbZ/lk0T2+aKVo1j0ynQVTuB3sJ5ssPWlz+Z3VwveCkP1O7EVu1tsVVCb0YV5WxK9zmURu2FIiaA==} peerDependencies: @@ -2793,9 +2808,15 @@ packages: readable-stream: ^3.6.2 socket.io-client: ^4.5.1 + '@metamask/sdk-install-modal-web@0.32.0': + resolution: {integrity: sha512-TFoktj0JgfWnQaL3yFkApqNwcaqJ+dw4xcnrJueMP3aXkSNev2Ido+WVNOg4IIMxnmOrfAC9t0UJ0u/dC9MjOQ==} + '@metamask/sdk-install-modal-web@0.32.1': resolution: {integrity: sha512-MGmAo6qSjf1tuYXhCu2EZLftq+DSt5Z7fsIKr2P+lDgdTPWgLfZB1tJKzNcwKKOdf6q9Qmmxn7lJuI/gq5LrKw==} + '@metamask/sdk@0.32.0': + resolution: {integrity: sha512-WmGAlP1oBuD9hk4CsdlG1WJFuPtYJY+dnTHJMeCyohTWD2GgkcLMUUuvu9lO1/NVzuOoSi1OrnjbuY1O/1NZ1g==} + '@metamask/sdk@0.33.1': resolution: {integrity: sha512-1mcOQVGr9rSrVcbKPNVzbZ8eCl1K0FATsYH3WJ/MH4WcZDWGECWrXJPNMZoEAkLxWiMe8jOQBumg2pmcDa9zpQ==} @@ -2803,8 +2824,8 @@ packages: resolution: {integrity: sha512-fLgJnDOXFmuVlB38rUN5SmU7hAFQcCjrg3Vrxz67KTY7YHFnSNEKvX4avmEBdOI0yTCxZjwMCFEqsC8k2+Wd3g==} engines: {node: '>=16.0.0'} - '@metamask/utils@11.9.0': - resolution: {integrity: sha512-wRnoSDD9jTWOge/+reFviJQANhS+uy8Y+OEwRanp5mQeGTjBFmK1r2cTOnei2UCZRV1crXHzeJVSFEoDDcgRbA==} + '@metamask/utils@11.7.0': + resolution: {integrity: sha512-IamqpZF8Lr4WeXJ84fD+Sy+v1Zo05SYuMPHHBrZWpzVbnHAmXQpL4ckn9s5dfA+zylp3WGypaBPb6SBZdOhuNQ==} engines: {node: ^18.18 || ^20.14 || >=22} '@metamask/utils@5.0.2': @@ -2819,8 +2840,8 @@ packages: resolution: {integrity: sha512-w8CVbdkDrVXFJbfBSlDfafDR6BAkpDmv1bC1UJVCoVny5tW2RKAdn9i68Xf7asYT4TnUhl/hN4zfUiKQq9II4g==} engines: {node: '>=16.0.0'} - '@modelcontextprotocol/sdk@1.25.3': - resolution: {integrity: sha512-vsAMBMERybvYgKbg/l4L1rhS7VXV1c0CtyJg72vwxONVX0l4ZfKVAnZEWTQixJGTzKnELjQ59e4NbdFDALRiAQ==} + '@modelcontextprotocol/sdk@1.26.0': + resolution: {integrity: sha512-Y5RmPncpiDtTXDbLKswIJzTqu2hyBKxTNsgKqKclDbhIgg1wgtf1fRuvxgTnRfcnxtvvgbIEcqUOzZrJ6iSReg==} engines: {node: '>=18'} peerDependencies: '@cfworker/json-schema': ^4.1.1 @@ -2832,56 +2853,56 @@ packages: '@napi-rs/wasm-runtime@0.2.12': resolution: {integrity: sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==} - '@next/env@16.1.6': - resolution: {integrity: sha512-N1ySLuZjnAtN3kFnwhAwPvZah8RJxKasD7x1f8shFqhncnWZn4JMfg37diLNuoHsLAlrDfM3g4mawVdtAG8XLQ==} + '@next/env@16.0.10': + resolution: {integrity: sha512-8tuaQkyDVgeONQ1MeT9Mkk8pQmZapMKFh5B+OrFUlG3rVmYTXcXlBetBgTurKXGaIZvkoqRT9JL5K3phXcgang==} '@next/eslint-plugin-next@16.0.6': resolution: {integrity: sha512-9INsBF3/4XL0/tON8AGsh0svnTtDMLwv3iREGWnWkewGdOnd790tguzq9rX8xwrVthPyvaBHhw1ww0GZz0jO5Q==} - '@next/swc-darwin-arm64@16.1.6': - resolution: {integrity: sha512-wTzYulosJr/6nFnqGW7FrG3jfUUlEf8UjGA0/pyypJl42ExdVgC6xJgcXQ+V8QFn6niSG2Pb8+MIG1mZr2vczw==} + '@next/swc-darwin-arm64@16.0.10': + resolution: {integrity: sha512-4XgdKtdVsaflErz+B5XeG0T5PeXKDdruDf3CRpnhN+8UebNa5N2H58+3GDgpn/9GBurrQ1uWW768FfscwYkJRg==} engines: {node: '>= 10'} cpu: [arm64] os: [darwin] - '@next/swc-darwin-x64@16.1.6': - resolution: {integrity: sha512-BLFPYPDO+MNJsiDWbeVzqvYd4NyuRrEYVB5k2N3JfWncuHAy2IVwMAOlVQDFjj+krkWzhY2apvmekMkfQR0CUQ==} + '@next/swc-darwin-x64@16.0.10': + resolution: {integrity: sha512-spbEObMvRKkQ3CkYVOME+ocPDFo5UqHb8EMTS78/0mQ+O1nqE8toHJVioZo4TvebATxgA8XMTHHrScPrn68OGw==} engines: {node: '>= 10'} cpu: [x64] os: [darwin] - '@next/swc-linux-arm64-gnu@16.1.6': - resolution: {integrity: sha512-OJYkCd5pj/QloBvoEcJ2XiMnlJkRv9idWA/j0ugSuA34gMT6f5b7vOiCQHVRpvStoZUknhl6/UxOXL4OwtdaBw==} + '@next/swc-linux-arm64-gnu@16.0.10': + resolution: {integrity: sha512-uQtWE3X0iGB8apTIskOMi2w/MKONrPOUCi5yLO+v3O8Mb5c7K4Q5KD1jvTpTF5gJKa3VH/ijKjKUq9O9UhwOYw==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - '@next/swc-linux-arm64-musl@16.1.6': - resolution: {integrity: sha512-S4J2v+8tT3NIO9u2q+S0G5KdvNDjXfAv06OhfOzNDaBn5rw84DGXWndOEB7d5/x852A20sW1M56vhC/tRVbccQ==} + '@next/swc-linux-arm64-musl@16.0.10': + resolution: {integrity: sha512-llA+hiDTrYvyWI21Z0L1GiXwjQaanPVQQwru5peOgtooeJ8qx3tlqRV2P7uH2pKQaUfHxI/WVarvI5oYgGxaTw==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - '@next/swc-linux-x64-gnu@16.1.6': - resolution: {integrity: sha512-2eEBDkFlMMNQnkTyPBhQOAyn2qMxyG2eE7GPH2WIDGEpEILcBPI/jdSv4t6xupSP+ot/jkfrCShLAa7+ZUPcJQ==} + '@next/swc-linux-x64-gnu@16.0.10': + resolution: {integrity: sha512-AK2q5H0+a9nsXbeZ3FZdMtbtu9jxW4R/NgzZ6+lrTm3d6Zb7jYrWcgjcpM1k8uuqlSy4xIyPR2YiuUr+wXsavA==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - '@next/swc-linux-x64-musl@16.1.6': - resolution: {integrity: sha512-oicJwRlyOoZXVlxmIMaTq7f8pN9QNbdes0q2FXfRsPhfCi8n8JmOZJm5oo1pwDaFbnnD421rVU409M3evFbIqg==} + '@next/swc-linux-x64-musl@16.0.10': + resolution: {integrity: sha512-1TDG9PDKivNw5550S111gsO4RGennLVl9cipPhtkXIFVwo31YZ73nEbLjNC8qG3SgTz/QZyYyaFYMeY4BKZR/g==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - '@next/swc-win32-arm64-msvc@16.1.6': - resolution: {integrity: sha512-gQmm8izDTPgs+DCWH22kcDmuUp7NyiJgEl18bcr8irXA5N2m2O+JQIr6f3ct42GOs9c0h8QF3L5SzIxcYAAXXw==} + '@next/swc-win32-arm64-msvc@16.0.10': + resolution: {integrity: sha512-aEZIS4Hh32xdJQbHz121pyuVZniSNoqDVx1yIr2hy+ZwJGipeqnMZBJHyMxv2tiuAXGx6/xpTcQJ6btIiBjgmg==} engines: {node: '>= 10'} cpu: [arm64] os: [win32] - '@next/swc-win32-x64-msvc@16.1.6': - resolution: {integrity: sha512-NRfO39AIrzBnixKbjuo2YiYhB6o9d8v/ymU9m/Xk8cyVk+k7XylniXkHwjs4s70wedVffc6bQNbufk5v0xEm0A==} + '@next/swc-win32-x64-msvc@16.0.10': + resolution: {integrity: sha512-E+njfCoFLb01RAFEnGZn6ERoOqhK1Gl3Lfz1Kjnj0Ulfu7oJbuMyvBKNj/bw8XZnenHDASlygTjZICQW+rYW1Q==} engines: {node: '>= 10'} cpu: [x64] os: [win32] @@ -2956,6 +2977,13 @@ packages: resolution: {integrity: sha512-IHnV6A+zxU7XwmKFinmYjUcwlyK9+xkG3/s9KcQhI9BjQKycrJ1JRO+FbNYPwZiPKW3je/DR0k7w8/gLa5eaxQ==} deprecated: 'The package is now available as "qr": npm install qr' + '@pinojs/redact@0.4.0': + resolution: {integrity: sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==} + + '@pkgjs/parseargs@0.11.0': + resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} + engines: {node: '>=14'} + '@pkgr/core@0.2.9': resolution: {integrity: sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==} engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} @@ -2989,128 +3017,108 @@ packages: '@reown/appkit@1.7.8': resolution: {integrity: sha512-51kTleozhA618T1UvMghkhKfaPcc9JlKwLJ5uV+riHyvSoWPKPRIa5A6M1Wano5puNyW0s3fwywhyqTHSilkaA==} - '@rollup/rollup-android-arm-eabi@4.57.1': - resolution: {integrity: sha512-A6ehUVSiSaaliTxai040ZpZ2zTevHYbvu/lDoeAteHI8QnaosIzm4qwtezfRg1jOYaUmnzLX1AOD6Z+UJjtifg==} + '@rollup/rollup-android-arm-eabi@4.50.0': + resolution: {integrity: sha512-lVgpeQyy4fWN5QYebtW4buT/4kn4p4IJ+kDNB4uYNT5b8c8DLJDg6titg20NIg7E8RWwdWZORW6vUFfrLyG3KQ==} cpu: [arm] os: [android] - '@rollup/rollup-android-arm64@4.57.1': - resolution: {integrity: sha512-dQaAddCY9YgkFHZcFNS/606Exo8vcLHwArFZ7vxXq4rigo2bb494/xKMMwRRQW6ug7Js6yXmBZhSBRuBvCCQ3w==} + '@rollup/rollup-android-arm64@4.50.0': + resolution: {integrity: sha512-2O73dR4Dc9bp+wSYhviP6sDziurB5/HCym7xILKifWdE9UsOe2FtNcM+I4xZjKrfLJnq5UR8k9riB87gauiQtw==} cpu: [arm64] os: [android] - '@rollup/rollup-darwin-arm64@4.57.1': - resolution: {integrity: sha512-crNPrwJOrRxagUYeMn/DZwqN88SDmwaJ8Cvi/TN1HnWBU7GwknckyosC2gd0IqYRsHDEnXf328o9/HC6OkPgOg==} + '@rollup/rollup-darwin-arm64@4.50.0': + resolution: {integrity: sha512-vwSXQN8T4sKf1RHr1F0s98Pf8UPz7pS6P3LG9NSmuw0TVh7EmaE+5Ny7hJOZ0M2yuTctEsHHRTMi2wuHkdS6Hg==} cpu: [arm64] os: [darwin] - '@rollup/rollup-darwin-x64@4.57.1': - resolution: {integrity: sha512-Ji8g8ChVbKrhFtig5QBV7iMaJrGtpHelkB3lsaKzadFBe58gmjfGXAOfI5FV0lYMH8wiqsxKQ1C9B0YTRXVy4w==} + '@rollup/rollup-darwin-x64@4.50.0': + resolution: {integrity: sha512-cQp/WG8HE7BCGyFVuzUg0FNmupxC+EPZEwWu2FCGGw5WDT1o2/YlENbm5e9SMvfDFR6FRhVCBePLqj0o8MN7Vw==} cpu: [x64] os: [darwin] - '@rollup/rollup-freebsd-arm64@4.57.1': - resolution: {integrity: sha512-R+/WwhsjmwodAcz65guCGFRkMb4gKWTcIeLy60JJQbXrJ97BOXHxnkPFrP+YwFlaS0m+uWJTstrUA9o+UchFug==} + '@rollup/rollup-freebsd-arm64@4.50.0': + resolution: {integrity: sha512-UR1uTJFU/p801DvvBbtDD7z9mQL8J80xB0bR7DqW7UGQHRm/OaKzp4is7sQSdbt2pjjSS72eAtRh43hNduTnnQ==} cpu: [arm64] os: [freebsd] - '@rollup/rollup-freebsd-x64@4.57.1': - resolution: {integrity: sha512-IEQTCHeiTOnAUC3IDQdzRAGj3jOAYNr9kBguI7MQAAZK3caezRrg0GxAb6Hchg4lxdZEI5Oq3iov/w/hnFWY9Q==} + '@rollup/rollup-freebsd-x64@4.50.0': + resolution: {integrity: sha512-G/DKyS6PK0dD0+VEzH/6n/hWDNPDZSMBmqsElWnCRGrYOb2jC0VSupp7UAHHQ4+QILwkxSMaYIbQ72dktp8pKA==} cpu: [x64] os: [freebsd] - '@rollup/rollup-linux-arm-gnueabihf@4.57.1': - resolution: {integrity: sha512-F8sWbhZ7tyuEfsmOxwc2giKDQzN3+kuBLPwwZGyVkLlKGdV1nvnNwYD0fKQ8+XS6hp9nY7B+ZeK01EBUE7aHaw==} + '@rollup/rollup-linux-arm-gnueabihf@4.50.0': + resolution: {integrity: sha512-u72Mzc6jyJwKjJbZZcIYmd9bumJu7KNmHYdue43vT1rXPm2rITwmPWF0mmPzLm9/vJWxIRbao/jrQmxTO0Sm9w==} cpu: [arm] os: [linux] - '@rollup/rollup-linux-arm-musleabihf@4.57.1': - resolution: {integrity: sha512-rGfNUfn0GIeXtBP1wL5MnzSj98+PZe/AXaGBCRmT0ts80lU5CATYGxXukeTX39XBKsxzFpEeK+Mrp9faXOlmrw==} + '@rollup/rollup-linux-arm-musleabihf@4.50.0': + resolution: {integrity: sha512-S4UefYdV0tnynDJV1mdkNawp0E5Qm2MtSs330IyHgaccOFrwqsvgigUD29uT+B/70PDY1eQ3t40+xf6wIvXJyg==} cpu: [arm] os: [linux] - '@rollup/rollup-linux-arm64-gnu@4.57.1': - resolution: {integrity: sha512-MMtej3YHWeg/0klK2Qodf3yrNzz6CGjo2UntLvk2RSPlhzgLvYEB3frRvbEF2wRKh1Z2fDIg9KRPe1fawv7C+g==} + '@rollup/rollup-linux-arm64-gnu@4.50.0': + resolution: {integrity: sha512-1EhkSvUQXJsIhk4msxP5nNAUWoB4MFDHhtc4gAYvnqoHlaL9V3F37pNHabndawsfy/Tp7BPiy/aSa6XBYbaD1g==} cpu: [arm64] os: [linux] - '@rollup/rollup-linux-arm64-musl@4.57.1': - resolution: {integrity: sha512-1a/qhaaOXhqXGpMFMET9VqwZakkljWHLmZOX48R0I/YLbhdxr1m4gtG1Hq7++VhVUmf+L3sTAf9op4JlhQ5u1Q==} + '@rollup/rollup-linux-arm64-musl@4.50.0': + resolution: {integrity: sha512-EtBDIZuDtVg75xIPIK1l5vCXNNCIRM0OBPUG+tbApDuJAy9mKago6QxX+tfMzbCI6tXEhMuZuN1+CU8iDW+0UQ==} cpu: [arm64] os: [linux] - '@rollup/rollup-linux-loong64-gnu@4.57.1': - resolution: {integrity: sha512-QWO6RQTZ/cqYtJMtxhkRkidoNGXc7ERPbZN7dVW5SdURuLeVU7lwKMpo18XdcmpWYd0qsP1bwKPf7DNSUinhvA==} - cpu: [loong64] - os: [linux] - - '@rollup/rollup-linux-loong64-musl@4.57.1': - resolution: {integrity: sha512-xpObYIf+8gprgWaPP32xiN5RVTi/s5FCR+XMXSKmhfoJjrpRAjCuuqQXyxUa/eJTdAE6eJ+KDKaoEqjZQxh3Gw==} + '@rollup/rollup-linux-loongarch64-gnu@4.50.0': + resolution: {integrity: sha512-BGYSwJdMP0hT5CCmljuSNx7+k+0upweM2M4YGfFBjnFSZMHOLYR0gEEj/dxyYJ6Zc6AiSeaBY8dWOa11GF/ppQ==} cpu: [loong64] os: [linux] - '@rollup/rollup-linux-ppc64-gnu@4.57.1': - resolution: {integrity: sha512-4BrCgrpZo4hvzMDKRqEaW1zeecScDCR+2nZ86ATLhAoJ5FQ+lbHVD3ttKe74/c7tNT9c6F2viwB3ufwp01Oh2w==} - cpu: [ppc64] - os: [linux] - - '@rollup/rollup-linux-ppc64-musl@4.57.1': - resolution: {integrity: sha512-NOlUuzesGauESAyEYFSe3QTUguL+lvrN1HtwEEsU2rOwdUDeTMJdO5dUYl/2hKf9jWydJrO9OL/XSSf65R5+Xw==} + '@rollup/rollup-linux-ppc64-gnu@4.50.0': + resolution: {integrity: sha512-I1gSMzkVe1KzAxKAroCJL30hA4DqSi+wGc5gviD0y3IL/VkvcnAqwBf4RHXHyvH66YVHxpKO8ojrgc4SrWAnLg==} cpu: [ppc64] os: [linux] - '@rollup/rollup-linux-riscv64-gnu@4.57.1': - resolution: {integrity: sha512-ptA88htVp0AwUUqhVghwDIKlvJMD/fmL/wrQj99PRHFRAG6Z5nbWoWG4o81Nt9FT+IuqUQi+L31ZKAFeJ5Is+A==} + '@rollup/rollup-linux-riscv64-gnu@4.50.0': + resolution: {integrity: sha512-bSbWlY3jZo7molh4tc5dKfeSxkqnf48UsLqYbUhnkdnfgZjgufLS/NTA8PcP/dnvct5CCdNkABJ56CbclMRYCA==} cpu: [riscv64] os: [linux] - '@rollup/rollup-linux-riscv64-musl@4.57.1': - resolution: {integrity: sha512-S51t7aMMTNdmAMPpBg7OOsTdn4tySRQvklmL3RpDRyknk87+Sp3xaumlatU+ppQ+5raY7sSTcC2beGgvhENfuw==} + '@rollup/rollup-linux-riscv64-musl@4.50.0': + resolution: {integrity: sha512-LSXSGumSURzEQLT2e4sFqFOv3LWZsEF8FK7AAv9zHZNDdMnUPYH3t8ZlaeYYZyTXnsob3htwTKeWtBIkPV27iQ==} cpu: [riscv64] os: [linux] - '@rollup/rollup-linux-s390x-gnu@4.57.1': - resolution: {integrity: sha512-Bl00OFnVFkL82FHbEqy3k5CUCKH6OEJL54KCyx2oqsmZnFTR8IoNqBF+mjQVcRCT5sB6yOvK8A37LNm/kPJiZg==} + '@rollup/rollup-linux-s390x-gnu@4.50.0': + resolution: {integrity: sha512-CxRKyakfDrsLXiCyucVfVWVoaPA4oFSpPpDwlMcDFQvrv3XY6KEzMtMZrA+e/goC8xxp2WSOxHQubP8fPmmjOQ==} cpu: [s390x] os: [linux] - '@rollup/rollup-linux-x64-gnu@4.57.1': - resolution: {integrity: sha512-ABca4ceT4N+Tv/GtotnWAeXZUZuM/9AQyCyKYyKnpk4yoA7QIAuBt6Hkgpw8kActYlew2mvckXkvx0FfoInnLg==} + '@rollup/rollup-linux-x64-gnu@4.50.0': + resolution: {integrity: sha512-8PrJJA7/VU8ToHVEPu14FzuSAqVKyo5gg/J8xUerMbyNkWkO9j2ExBho/68RnJsMGNJq4zH114iAttgm7BZVkA==} cpu: [x64] os: [linux] - '@rollup/rollup-linux-x64-musl@4.57.1': - resolution: {integrity: sha512-HFps0JeGtuOR2convgRRkHCekD7j+gdAuXM+/i6kGzQtFhlCtQkpwtNzkNj6QhCDp7DRJ7+qC/1Vg2jt5iSOFw==} + '@rollup/rollup-linux-x64-musl@4.50.0': + resolution: {integrity: sha512-SkE6YQp+CzpyOrbw7Oc4MgXFvTw2UIBElvAvLCo230pyxOLmYwRPwZ/L5lBe/VW/qT1ZgND9wJfOsdy0XptRvw==} cpu: [x64] os: [linux] - '@rollup/rollup-openbsd-x64@4.57.1': - resolution: {integrity: sha512-H+hXEv9gdVQuDTgnqD+SQffoWoc0Of59AStSzTEj/feWTBAnSfSD3+Dql1ZruJQxmykT/JVY0dE8Ka7z0DH1hw==} - cpu: [x64] - os: [openbsd] - - '@rollup/rollup-openharmony-arm64@4.57.1': - resolution: {integrity: sha512-4wYoDpNg6o/oPximyc/NG+mYUejZrCU2q+2w6YZqrAs2UcNUChIZXjtafAiiZSUc7On8v5NyNj34Kzj/Ltk6dQ==} + '@rollup/rollup-openharmony-arm64@4.50.0': + resolution: {integrity: sha512-PZkNLPfvXeIOgJWA804zjSFH7fARBBCpCXxgkGDRjjAhRLOR8o0IGS01ykh5GYfod4c2yiiREuDM8iZ+pVsT+Q==} cpu: [arm64] os: [openharmony] - '@rollup/rollup-win32-arm64-msvc@4.57.1': - resolution: {integrity: sha512-O54mtsV/6LW3P8qdTcamQmuC990HDfR71lo44oZMZlXU4tzLrbvTii87Ni9opq60ds0YzuAlEr/GNwuNluZyMQ==} + '@rollup/rollup-win32-arm64-msvc@4.50.0': + resolution: {integrity: sha512-q7cIIdFvWQoaCbLDUyUc8YfR3Jh2xx3unO8Dn6/TTogKjfwrax9SyfmGGK6cQhKtjePI7jRfd7iRYcxYs93esg==} cpu: [arm64] os: [win32] - '@rollup/rollup-win32-ia32-msvc@4.57.1': - resolution: {integrity: sha512-P3dLS+IerxCT/7D2q2FYcRdWRl22dNbrbBEtxdWhXrfIMPP9lQhb5h4Du04mdl5Woq05jVCDPCMF7Ub0NAjIew==} + '@rollup/rollup-win32-ia32-msvc@4.50.0': + resolution: {integrity: sha512-XzNOVg/YnDOmFdDKcxxK410PrcbcqZkBmz+0FicpW5jtjKQxcW1BZJEQOF0NJa6JO7CZhett8GEtRN/wYLYJuw==} cpu: [ia32] os: [win32] - '@rollup/rollup-win32-x64-gnu@4.57.1': - resolution: {integrity: sha512-VMBH2eOOaKGtIJYleXsi2B8CPVADrh+TyNxJ4mWPnKfLB/DBUmzW+5m1xUrcwWoMfSLagIRpjUFeW5CO5hyciQ==} - cpu: [x64] - os: [win32] - - '@rollup/rollup-win32-x64-msvc@4.57.1': - resolution: {integrity: sha512-mxRFDdHIWRxg3UfIIAwCm6NzvxG0jDX/wBN6KsQFTvKFqqg9vTrWUE68qEjHt19A5wwx5X5aUi2zuZT7YR0jrA==} + '@rollup/rollup-win32-x64-msvc@4.50.0': + resolution: {integrity: sha512-xMmiWRR8sp72Zqwjgtf3QbZfF1wdh8X2ABu3EaozvZcyHJeU0r+XAnXdKgs4cCAp6ORoYoCygipYP1mjmbjrsg==} cpu: [x64] os: [win32] @@ -3168,11 +3176,6 @@ packages: peerDependencies: '@solana/kit': ^2.1.0 - '@solana-program/system@0.10.0': - resolution: {integrity: sha512-Go+LOEZmqmNlfr+Gjy5ZWAdY5HbYzk2RBewD9QinEU/bBSzpFfzqDRT55JjFRBGJUvMgf3C2vfXEGT4i8DSI4g==} - peerDependencies: - '@solana/kit': ^5.0 - '@solana-program/token-2022@0.4.2': resolution: {integrity: sha512-zIpR5t4s9qEU3hZKupzIBxJ6nUV5/UVyIT400tu9vT1HMs5JHxaTTsb5GUhYjiiTvNwU0MQavbwc4Dl29L0Xvw==} peerDependencies: @@ -3195,14 +3198,17 @@ packages: peerDependencies: '@solana/kit': ^5.0 - '@solana/accounts@5.5.1': - resolution: {integrity: sha512-TfOY9xixg5rizABuLVuZ9XI2x2tmWUC/OoN556xwfDlhBHBjKfszicYYOyD6nbFmwTGYarCmyGIdteXxTXIdhQ==} + '@solana/accounts@5.0.0': + resolution: {integrity: sha512-0JzBdEobgp8NBdhhu+GgwNDh7e8KkHDsSTVZAnNQgvT3taOz0Mwv5E48MuEeDhW6DLFwWVAx/FO3pvibG/NGwA==} engines: {node: '>=20.18.0'} peerDependencies: - typescript: ^5.0.0 - peerDependenciesMeta: - typescript: - optional: true + typescript: '>=5.3.3' + + '@solana/accounts@5.1.0': + resolution: {integrity: sha512-Q1KzykCrl/YjLUH2RXF8vPq65U/ehAV2SHZicPbZ0jvgQUU6X1+Eca+0ilxA9xH8srYn3YTVDyEs/LYdfbY/2A==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5.3.3' '@solana/accounts@6.1.0': resolution: {integrity: sha512-0jhmhSSS71ClLtBQIDrLlhkiNER4M9RIXTl1eJ1yJoFlE608JaKHTjNWsdVKdke7uBD6exdjNZkIVmouQPHMcA==} @@ -3219,14 +3225,17 @@ packages: peerDependencies: typescript: '>=5.3.3' - '@solana/addresses@5.5.1': - resolution: {integrity: sha512-5xoah3Q9G30HQghu/9BiHLb5pzlPKRC3zydQDmE3O9H//WfayxTFppsUDCL6FjYUHqj/wzK6CWHySglc2RkpdA==} + '@solana/addresses@5.0.0': + resolution: {integrity: sha512-bVk+khc1ZZQHMri25csosM/ikuyPcB/CZidDM/ZMBX0CoJErpHJnmcID5mYOmv4/UHbqo2OANuEaGcFO0Q37sw==} engines: {node: '>=20.18.0'} peerDependencies: - typescript: ^5.0.0 - peerDependenciesMeta: - typescript: - optional: true + typescript: '>=5.3.3' + + '@solana/addresses@5.1.0': + resolution: {integrity: sha512-X84qSZLgve9YeYsyxGI49WnfEre53tdFu4x9/4oULBgoj8d0A+P9VGLYzmRJ0YFYKRcZG7U4u3MQpI5uLZ1AsQ==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5.3.3' '@solana/addresses@6.1.0': resolution: {integrity: sha512-QT04Vie4iICaalQQRJFMGj/P56IxXiwFtVuZHu1qjZUNmuGTOvX6G98b27RaGtLzpJ3NIku/6OtKxLUBqAKAyQ==} @@ -3243,14 +3252,17 @@ packages: peerDependencies: typescript: '>=5.3.3' - '@solana/assertions@5.5.1': - resolution: {integrity: sha512-YTCSWAlGwSlVPnWtWLm3ukz81wH4j2YaCveK+TjpvUU88hTy6fmUqxi0+hvAMAe4zKXpJyj3Az7BrLJRxbIm4Q==} + '@solana/assertions@5.0.0': + resolution: {integrity: sha512-2kIykk90kYciQW6bp+KaE6jRd1Y2CgHPeJxxlc5chQnjhoG6eiD8VXvocs6AvqPTht0p/SoEj9jH5tT4oG/bcg==} engines: {node: '>=20.18.0'} peerDependencies: - typescript: ^5.0.0 - peerDependenciesMeta: - typescript: - optional: true + typescript: '>=5.3.3' + + '@solana/assertions@5.1.0': + resolution: {integrity: sha512-5But2wyxuvGXMIOnD0jBMQ9yq1QQF2LSK3IbIRSkAkXbD3DS6O2tRvKUHNhogd+BpkPyCGOQHBycezgnxmStlg==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5.3.3' '@solana/assertions@6.1.0': resolution: {integrity: sha512-pLgxB2xxTk2QfTaWpnRpSMYgaPkKYDQgptRvbwmuDQnOW1Zopg+42MT2UrDGd3UFMML1uOFPxIwKM6m51H0uXw==} @@ -3261,24 +3273,36 @@ packages: typescript: optional: true + '@solana/buffer-layout-utils@0.2.0': + resolution: {integrity: sha512-szG4sxgJGktbuZYDg2FfNmkMi0DYQoVjN2h7ta1W1hPrwzarcFLBq9UpX1UjNXsNpT9dn+chgprtWGioUAr4/g==} + engines: {node: '>= 10'} + '@solana/buffer-layout@4.0.1': resolution: {integrity: sha512-E1ImOIAD1tBZFRdjeM4/pzTiTApC0AOBGwyAMS4fwIodCWArzJ3DWdoh8cKxeFM2fElkxBh2Aqts1BPC373rHA==} engines: {node: '>=5.10'} + '@solana/codecs-core@2.0.0-rc.1': + resolution: {integrity: sha512-bauxqMfSs8EHD0JKESaNmNuNvkvHSuN3bbWAF5RjOfDu2PugxHrvRebmYauvSumZ3cTfQ4HJJX6PG5rN852qyQ==} + peerDependencies: + typescript: '>=5' + '@solana/codecs-core@2.3.0': resolution: {integrity: sha512-oG+VZzN6YhBHIoSKgS5ESM9VIGzhWjEHEGNPSibiDTxFhsFWxNaz8LbMDPjBUE69r9wmdGLkrQ+wVPbnJcZPvw==} engines: {node: '>=20.18.0'} peerDependencies: typescript: '>=5.3.3' - '@solana/codecs-core@5.5.1': - resolution: {integrity: sha512-TgBt//bbKBct0t6/MpA8ElaOA3sa8eYVvR7LGslCZ84WiAwwjCY0lW/lOYsFHJQzwREMdUyuEyy5YWBKtdh8Rw==} + '@solana/codecs-core@5.0.0': + resolution: {integrity: sha512-rCG2d8OaamVF2/J//YyCgDqNJpUytVVltw9C8mJtEz5c6Se/LR6BFuG8g4xeJswq/ab4RFk5/HFdgbvNjKgQjA==} engines: {node: '>=20.18.0'} peerDependencies: - typescript: ^5.0.0 - peerDependenciesMeta: - typescript: - optional: true + typescript: '>=5.3.3' + + '@solana/codecs-core@5.1.0': + resolution: {integrity: sha512-vDwi03mxWeWCS5Il6BCdNdifYdOoHVz97YOmbWGIt45b77Ivu5NUYeSD2+ccl6fSw8eYQ6QaqqKXMjbSfsXv4g==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5.3.3' '@solana/codecs-core@6.1.0': resolution: {integrity: sha512-5rNnDOOm2GRFMJbd9imYCPNvGOrQ+TZ53NCkFFWbbB7f+L9KkLeuuAsDMFN1lCziJFlymvN785YtDnMeWj2W+g==} @@ -3289,20 +3313,28 @@ packages: typescript: optional: true + '@solana/codecs-data-structures@2.0.0-rc.1': + resolution: {integrity: sha512-rinCv0RrAVJ9rE/rmaibWJQxMwC5lSaORSZuwjopSUE6T0nb/MVg6Z1siNCXhh/HFTOg0l8bNvZHgBcN/yvXog==} + peerDependencies: + typescript: '>=5' + '@solana/codecs-data-structures@2.3.0': resolution: {integrity: sha512-qvU5LE5DqEdYMYgELRHv+HMOx73sSoV1ZZkwIrclwUmwTbTaH8QAJURBj0RhQ/zCne7VuLLOZFFGv6jGigWhSw==} engines: {node: '>=20.18.0'} peerDependencies: typescript: '>=5.3.3' - '@solana/codecs-data-structures@5.5.1': - resolution: {integrity: sha512-97bJWGyUY9WvBz3mX1UV3YPWGDTez6btCfD0ip3UVEXJbItVuUiOkzcO5iFDUtQT5riKT6xC+Mzl+0nO76gd0w==} + '@solana/codecs-data-structures@5.0.0': + resolution: {integrity: sha512-y503Pqmv0LHcfcf0vQJGaxDvydQJbyCo8nK3nxn56EhFj5lBQ1NWb3WvTd83epigwuZurW2MhJARrpikfhQglQ==} engines: {node: '>=20.18.0'} peerDependencies: - typescript: ^5.0.0 - peerDependenciesMeta: - typescript: - optional: true + typescript: '>=5.3.3' + + '@solana/codecs-data-structures@5.1.0': + resolution: {integrity: sha512-ftAwL/jsurFrk9kFVhkTLdQ8fGZ8I0PcbVH+V1a0dIP2aKDofGePvK0XbwZE/ohizC9gEIZxyBX5IgRKk5PXyg==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5.3.3' '@solana/codecs-data-structures@6.1.0': resolution: {integrity: sha512-1cb9g5hrrucTuGkGxqVVq7dCwSMnn4YqwTe365iKkK8HBpLBmUl8XATf1MUs5UtDun1g9eNWOL72Psr8mIUqTQ==} @@ -3313,20 +3345,28 @@ packages: typescript: optional: true + '@solana/codecs-numbers@2.0.0-rc.1': + resolution: {integrity: sha512-J5i5mOkvukXn8E3Z7sGIPxsThRCgSdgTWJDQeZvucQ9PT6Y3HiVXJ0pcWiOWAoQ3RX8e/f4I3IC+wE6pZiJzDQ==} + peerDependencies: + typescript: '>=5' + '@solana/codecs-numbers@2.3.0': resolution: {integrity: sha512-jFvvwKJKffvG7Iz9dmN51OGB7JBcy2CJ6Xf3NqD/VP90xak66m/Lg48T01u5IQ/hc15mChVHiBm+HHuOFDUrQg==} engines: {node: '>=20.18.0'} peerDependencies: typescript: '>=5.3.3' - '@solana/codecs-numbers@5.5.1': - resolution: {integrity: sha512-rllMIZAHqmtvC0HO/dc/21wDuWaD0B8Ryv8o+YtsICQBuiL/0U4AGwH7Pi5GNFySYk0/crSuwfIqQFtmxNSPFw==} + '@solana/codecs-numbers@5.0.0': + resolution: {integrity: sha512-a2+skRLuUK02f/XFe4L0e1+wHCyfK25PkyseFps1v1l4pvevukFwth/EhSyrs6w5CsTJRVoR7MuE3E00PM4egw==} engines: {node: '>=20.18.0'} peerDependencies: - typescript: ^5.0.0 - peerDependenciesMeta: - typescript: - optional: true + typescript: '>=5.3.3' + + '@solana/codecs-numbers@5.1.0': + resolution: {integrity: sha512-Ea5/9yjDNOrDZcI40UGzzi6Aq1JNsmzM4m5pOk6Xb3JRZ0YdKOv/MwuCqb6jRgzZ7SQjHhkfGL43kHLJA++bOw==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5.3.3' '@solana/codecs-numbers@6.1.0': resolution: {integrity: sha512-YPQwwl6LE3igH23ah+d8kgpyE5xFcPbuwhxCDsLWqY/ESrvO/0YQSbsgIXahbhZxN59ZC4uq1LnHhBNbpCSVQg==} @@ -3337,6 +3377,12 @@ packages: typescript: optional: true + '@solana/codecs-strings@2.0.0-rc.1': + resolution: {integrity: sha512-9/wPhw8TbGRTt6mHC4Zz1RqOnuPTqq1Nb4EyuvpZ39GW6O2t2Q7Q0XxiB3+BdoEjwA2XgPw6e2iRfvYgqty44g==} + peerDependencies: + fastestsmallesttextencoderdecoder: ^1.0.22 + typescript: '>=5' + '@solana/codecs-strings@2.3.0': resolution: {integrity: sha512-y5pSBYwzVziXu521hh+VxqUtp0hYGTl1eWGoc1W+8mdvBdC1kTqm/X7aYQw33J42hw03JjryvYOvmGgk3Qz/Ug==} engines: {node: '>=20.18.0'} @@ -3344,17 +3390,22 @@ packages: fastestsmallesttextencoderdecoder: ^1.0.22 typescript: '>=5.3.3' - '@solana/codecs-strings@5.5.1': - resolution: {integrity: sha512-7klX4AhfHYA+uKKC/nxRGP2MntbYQCR3N6+v7bk1W/rSxYuhNmt+FN8aoThSZtWIKwN6BEyR1167ka8Co1+E7A==} + '@solana/codecs-strings@5.0.0': + resolution: {integrity: sha512-ALkRwpV8bGR6qjAYw0YXZwp2YI4wzvKOJGmx04Ut8gMdbaUx7qOcJkhEQKI6ZVC3lAWSIS1N1wGccUZDwvfKxw==} engines: {node: '>=20.18.0'} peerDependencies: fastestsmallesttextencoderdecoder: ^1.0.22 - typescript: ^5.0.0 + typescript: '>=5.3.3' + + '@solana/codecs-strings@5.1.0': + resolution: {integrity: sha512-014xwl5T/3VnGW0gceizF47DUs5EURRtgGmbWIR5+Z32yxgQ6hT9Zl0atZbL268RHbUQ03/J8Ush1StQgy7sfQ==} + engines: {node: '>=20.18.0'} + peerDependencies: + fastestsmallesttextencoderdecoder: ^1.0.22 + typescript: '>=5.3.3' peerDependenciesMeta: fastestsmallesttextencoderdecoder: optional: true - typescript: - optional: true '@solana/codecs-strings@6.1.0': resolution: {integrity: sha512-pRH5uAn4VCFUs2rYiDITyWsRnpvs3Uh/nhSc6OSP/kusghcCcCJcUzHBIjT4x08MVacXmGUlSLe/9qPQO+QK3Q==} @@ -3368,14 +3419,22 @@ packages: typescript: optional: true - '@solana/codecs@5.5.1': - resolution: {integrity: sha512-Vea29nJub/bXjfzEV7ZZQ/PWr1pYLZo3z0qW0LQL37uKKVzVFRQlwetd7INk3YtTD3xm9WUYr7bCvYUk3uKy2g==} + '@solana/codecs@2.0.0-rc.1': + resolution: {integrity: sha512-qxoR7VybNJixV51L0G1RD2boZTcxmwUWnKCaJJExQ5qNKwbpSyDdWfFJfM5JhGyKe9DnPVOZB+JHWXnpbZBqrQ==} + peerDependencies: + typescript: '>=5' + + '@solana/codecs@5.0.0': + resolution: {integrity: sha512-KOw0gFUSBxIMDWLJ3AkVFkEci91dw0Rpx3C6y83Our7fSW+SEP8vRZklCElieYR85LHVB1QIEhoeHR7rc+Ifkw==} engines: {node: '>=20.18.0'} peerDependencies: - typescript: ^5.0.0 - peerDependenciesMeta: - typescript: - optional: true + typescript: '>=5.3.3' + + '@solana/codecs@5.1.0': + resolution: {integrity: sha512-krSuf/E2Sa/4oASZ/jb/5KGUG58m1/bQdLrKvBnoAFhYj7zZf+8V4UqHGTV5n2NCQfmMyORsg9n2saKjkUzo8w==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5.3.3' '@solana/codecs@6.1.0': resolution: {integrity: sha512-VHBS3t8fyVjE0Nqo6b4TUnzdwdRaVo+B5ufHhPLbbjkEXzz8HB4E/OBjgasn+zWGlfScfQAiBFOsfZjbVWu4XA==} @@ -3386,6 +3445,12 @@ packages: typescript: optional: true + '@solana/errors@2.0.0-rc.1': + resolution: {integrity: sha512-ejNvQ2oJ7+bcFAYWj225lyRkHnixuAeb7RQCixm+5mH4n1IA4Qya/9Bmfy5RAAHQzxK43clu3kZmL5eF9VGtYQ==} + hasBin: true + peerDependencies: + typescript: '>=5' + '@solana/errors@2.3.0': resolution: {integrity: sha512-66RI9MAbwYV0UtP7kGcTBVLxJgUxoZGm8Fbc0ah+lGiAw17Gugco6+9GrJCV83VyF2mDWyYnYM9qdI3yjgpnaQ==} engines: {node: '>=20.18.0'} @@ -3393,15 +3458,19 @@ packages: peerDependencies: typescript: '>=5.3.3' - '@solana/errors@5.5.1': - resolution: {integrity: sha512-vFO3p+S7HoyyrcAectnXbdsMfwUzY2zYFUc2DEe5BwpiE9J1IAxPBGjOWO6hL1bbYdBrlmjNx8DXCslqS+Kcmg==} + '@solana/errors@5.0.0': + resolution: {integrity: sha512-gTuhzO6E+ydfAAzqmqdPcvFyJwAzFKKIrqtnZPpgAuomcPYu+HSo0tuwSM/cTX0djmHt+GoOsf/julph+nvs2w==} engines: {node: '>=20.18.0'} hasBin: true peerDependencies: - typescript: ^5.0.0 - peerDependenciesMeta: - typescript: - optional: true + typescript: '>=5.3.3' + + '@solana/errors@5.1.0': + resolution: {integrity: sha512-JlTyekErWa6Fdcwu1Hrh+jZxjM4YxyorGCFDRVZlmHZFkp5N00DWKcYnSGZrTF8E6ZZEP9pfS2XwM8y7p7HPww==} + engines: {node: '>=20.18.0'} + hasBin: true + peerDependencies: + typescript: '>=5.3.3' '@solana/errors@6.1.0': resolution: {integrity: sha512-cqSwcw3Rmn85UR7PyF5nKPdlQsRYBkx7YGRvFaJ6Sal1PM+bfolhL5iT7STQoXxdhXGYwHMPg7kZYxmMdjwnJA==} @@ -3419,14 +3488,17 @@ packages: peerDependencies: typescript: '>=5.3.3' - '@solana/fast-stable-stringify@5.5.1': - resolution: {integrity: sha512-Ni7s2FN33zTzhTFgRjEbOVFO+UAmK8qi3Iu0/GRFYK4jN696OjKHnboSQH/EacQ+yGqS54bfxf409wU5dsLLCw==} + '@solana/fast-stable-stringify@5.0.0': + resolution: {integrity: sha512-sGTbu7a4/olL+8EIOOJ7IZjzqOOpCJcK1UaVJ6015sRgo9vwGf4jg9KtXEYv5LVhLCTYmAb50L4BaIUcBph/Ig==} engines: {node: '>=20.18.0'} peerDependencies: - typescript: ^5.0.0 - peerDependenciesMeta: - typescript: - optional: true + typescript: '>=5.3.3' + + '@solana/fast-stable-stringify@5.1.0': + resolution: {integrity: sha512-ACZo7cH/5EXsBmruw/0gU2/PXL2l4aET0YpL93H6QEaZwEAICFD8cLkj20nBcfLTf4srEiuKtwuSDeONTWIulw==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5.3.3' '@solana/fast-stable-stringify@6.1.0': resolution: {integrity: sha512-QXUfDFaJCFeARsxJgScWmJ153Tit7Cimk9y0UWWreNBr2Aphi67Nlcj/tr7UABTO0Qaw/0gwrK76zz3m1t3nIw==} @@ -3443,14 +3515,17 @@ packages: peerDependencies: typescript: '>=5.3.3' - '@solana/functional@5.5.1': - resolution: {integrity: sha512-tTHoJcEQq3gQx5qsdsDJ0LEJeFzwNpXD80xApW9o/PPoCNimI3SALkZl+zNW8VnxRrV3l3yYvfHWBKe/X3WG3w==} + '@solana/functional@5.0.0': + resolution: {integrity: sha512-UNBrpfzBL4dKD2iucjNnrkFbnjz5ZYDu2OvrIBAcCSQsxxgHMamUj1n3EDe6kl1us49YG1r05Ho8QLqNrbkVbw==} engines: {node: '>=20.18.0'} peerDependencies: - typescript: ^5.0.0 - peerDependenciesMeta: - typescript: - optional: true + typescript: '>=5.3.3' + + '@solana/functional@5.1.0': + resolution: {integrity: sha512-R6jacWU0Gr+j49lTDp+FSECBolqw2Gq7JlC22rI0JkcxJiiAlp3G80v6zAYq0FkHzxZbjyR6//JYUXSwliem5g==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5.3.3' '@solana/functional@6.1.0': resolution: {integrity: sha512-+Sm8ldVxSTHIKaZDvcBu81FPjknXx6OMPlakkKmXjKxPgVLl86ruqMo2yEwoDUHV7DysLrLLcRNn13rfulomRw==} @@ -3461,14 +3536,17 @@ packages: typescript: optional: true - '@solana/instruction-plans@5.5.1': - resolution: {integrity: sha512-7z3CB7YMcFKuVvgcnNY8bY6IsZ8LG61Iytbz7HpNVGX2u1RthOs1tRW8luTzSG1MPL0Ox7afyAVMYeFqSPHnaQ==} + '@solana/instruction-plans@5.0.0': + resolution: {integrity: sha512-n9oFOMFUPYKEhsXzrXT97QBQ2WvOTar+5SFEj/IOtRuCn4gl2kh0369cjXZpFwUdE3tmKr1zfYFNwbtiNx5pvg==} engines: {node: '>=20.18.0'} peerDependencies: - typescript: ^5.0.0 - peerDependenciesMeta: - typescript: - optional: true + typescript: '>=5.3.3' + + '@solana/instruction-plans@5.1.0': + resolution: {integrity: sha512-friMgHt0z5jQlCyyTDXfwAMYjCAagI7QYR+hLWB/BmvSuRpai0ddToWbWJoqrNRM312xZ+Oy/qjC3+Ftzi0DLA==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5.3.3' '@solana/instruction-plans@6.1.0': resolution: {integrity: sha512-zcsHg544t1zn7LLOVUxOWYlsKn9gvT7R+pL3cTiP2wFNoUN0h9En87H6nVqkZ8LWw23asgW0uM5uJGwfBx2h1Q==} @@ -3485,14 +3563,17 @@ packages: peerDependencies: typescript: '>=5.3.3' - '@solana/instructions@5.5.1': - resolution: {integrity: sha512-h0G1CG6S+gUUSt0eo6rOtsaXRBwCq1+Js2a+Ps9Bzk9q7YHNFA75/X0NWugWLgC92waRp66hrjMTiYYnLBoWOQ==} + '@solana/instructions@5.0.0': + resolution: {integrity: sha512-12dbrmwERT1o6NTr/Uvrjj/ZsiteSXoT5Gi+dnjIeRNHWg9H+gEFuFzJvTDVKlNg34CZ71xdvbVdbV0V8gKGvg==} engines: {node: '>=20.18.0'} peerDependencies: - typescript: ^5.0.0 - peerDependenciesMeta: - typescript: - optional: true + typescript: '>=5.3.3' + + '@solana/instructions@5.1.0': + resolution: {integrity: sha512-fkwpUwwqk5K14T/kZDnCrfeR0kww49HBx+BK8xdSeJx+bt4QTwAHa9YeOkGhGrHEFVEJEUf8FKoxxTzZzJZtKQ==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5.3.3' '@solana/instructions@6.1.0': resolution: {integrity: sha512-w1LdbJ3yanESckNTYC5KPckgN/25FyGCm07WWrs+dCnnpRNeLiVHIytXCPmArOVAXVkOYidXzhWmqCzqKUjYaA==} @@ -3509,14 +3590,17 @@ packages: peerDependencies: typescript: '>=5.3.3' - '@solana/keys@5.5.1': - resolution: {integrity: sha512-KRD61cL7CRL+b4r/eB9dEoVxIf/2EJ1Pm1DmRYhtSUAJD2dJ5Xw8QFuehobOGm9URqQ7gaQl+Fkc1qvDlsWqKg==} + '@solana/keys@5.0.0': + resolution: {integrity: sha512-kWkR7NslpTttk5i1BhBNCDtVQDkEtgkdsM3Jp9TGPk0GFjBjBwrQStw3vvwLe8itEIvRFGFZU6JHEk8HLS0WLQ==} engines: {node: '>=20.18.0'} peerDependencies: - typescript: ^5.0.0 - peerDependenciesMeta: - typescript: - optional: true + typescript: '>=5.3.3' + + '@solana/keys@5.1.0': + resolution: {integrity: sha512-ma4zTTuSOmtTCvATHMfUGNTw0Vqah/6XPe1VmLc66ohwXMI3yqatX1FQPXgDZozr15SvLAesfs7/bgl2TRoe9w==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5.3.3' '@solana/keys@6.1.0': resolution: {integrity: sha512-C/SGCl3VOgBQZ0mLrMxCcJYnMsGpgE8wbx29jqRY+R91m5YhS1f/GfXJPR1lN/h7QGrJ6YDm8eI0Y3AZ7goKHg==} @@ -3527,14 +3611,17 @@ packages: typescript: optional: true - '@solana/kit@5.5.1': - resolution: {integrity: sha512-irKUGiV2yRoyf+4eGQ/ZeCRxa43yjFEL1DUI5B0DkcfZw3cr0VJtVJnrG8OtVF01vT0OUfYOcUn6zJW5TROHvQ==} + '@solana/kit@5.0.0': + resolution: {integrity: sha512-3ahtzmmMgU+1l2YMhQJSKKm14IdvCycOE/m4XNMu/4icBIptmBgZxrmgRpPHqBilBa+Krp/hBuTg4HWl9IAgWw==} engines: {node: '>=20.18.0'} peerDependencies: - typescript: ^5.0.0 - peerDependenciesMeta: - typescript: - optional: true + typescript: '>=5.3.3' + + '@solana/kit@5.1.0': + resolution: {integrity: sha512-oNQRzI0+mGWmXy05psO0J7r9Boy8PF7LH5H0Y9Jxvs10AbG4oSOBtyj20EccsRrr+jkqLw42fqb/4rNuASfvsA==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5.3.3' '@solana/kit@6.1.0': resolution: {integrity: sha512-24exn11BPonquufyCkGgypVtmN4JOsdGMsbF3EZ4kFyk7ZNryCn/N8eELr1FCVrHWRXoc0xy/HFaESBULTMf6g==} @@ -3551,14 +3638,17 @@ packages: peerDependencies: typescript: '>=5.3.3' - '@solana/nominal-types@5.5.1': - resolution: {integrity: sha512-I1ImR+kfrLFxN5z22UDiTWLdRZeKtU0J/pkWkO8qm/8WxveiwdIv4hooi8pb6JnlR4mSrWhq0pCIOxDYrL9GIQ==} + '@solana/nominal-types@5.0.0': + resolution: {integrity: sha512-Qn7xH4UG2rDAv+wAyheP4jWvX3oQmbZ/woxFZwug7PaRLvyjUswGr38Hil+SjiQyFDo+un1UqWM9N9yusUeeZQ==} engines: {node: '>=20.18.0'} peerDependencies: - typescript: ^5.0.0 - peerDependenciesMeta: - typescript: - optional: true + typescript: '>=5.3.3' + + '@solana/nominal-types@5.1.0': + resolution: {integrity: sha512-+4Cm+SpK+D811i9giqv4Up93ZlmUcZfLDHkSH24F4in61+Y2TKA+XKuRtKhNytQMmqCfbvJZ9MHFaIeZw5g+Bg==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5.3.3' '@solana/nominal-types@6.1.0': resolution: {integrity: sha512-+skHjN0arNNB9TLsGqA94VCx7euyGURI+qG6wck6E4D7hH6i6DxGiVrtKRghx+smJkkLtTm9BvdVKGoeNQYr7Q==} @@ -3569,14 +3659,11 @@ packages: typescript: optional: true - '@solana/offchain-messages@5.5.1': - resolution: {integrity: sha512-g+xHH95prTU+KujtbOzj8wn+C7ZNoiLhf3hj6nYq3MTyxOXtBEysguc97jJveUZG0K97aIKG6xVUlMutg5yxhw==} + '@solana/offchain-messages@5.1.0': + resolution: {integrity: sha512-6FUXjiIJprjWa7y/T4E3rUb3HKi3P5zpBweBEwDflEEJ/QlieWUw7xlGAOvZ1eF3Wi+6LfcrdtZOwIkuv6o9Sg==} engines: {node: '>=20.18.0'} peerDependencies: - typescript: ^5.0.0 - peerDependenciesMeta: - typescript: - optional: true + typescript: '>=5.3.3' '@solana/offchain-messages@6.1.0': resolution: {integrity: sha512-jrUb7HGUnRA+k44upcqKeevtEdqMxYRSlFdE0JTctZunGlP3GCcTl12tFOpbnFHvBLt8RwS62+nyeES8zzNwXA==} @@ -3587,26 +3674,25 @@ packages: typescript: optional: true - '@solana/options@5.5.1': - resolution: {integrity: sha512-eo971c9iLNLmk+yOFyo7yKIJzJ/zou6uKpy6mBuyb/thKtS/haiKIc3VLhyTXty3OH2PW8yOlORJnv4DexJB8A==} + '@solana/options@2.0.0-rc.1': + resolution: {integrity: sha512-mLUcR9mZ3qfHlmMnREdIFPf9dpMc/Bl66tLSOOWxw4ml5xMT2ohFn7WGqoKcu/UHkT9CrC6+amEdqCNvUqI7AA==} + peerDependencies: + typescript: '>=5' + + '@solana/options@5.0.0': + resolution: {integrity: sha512-ezHVBFb9FXVSn8LUVRD2tLb6fejU0x8KtGEYyCYh0J0pQuXSITV0IQCjcEopvu/ZxWdXOJyzjvmymnhz90on5A==} engines: {node: '>=20.18.0'} peerDependencies: - typescript: ^5.0.0 - peerDependenciesMeta: - typescript: - optional: true + typescript: '>=5.3.3' - '@solana/options@6.1.0': - resolution: {integrity: sha512-/4FtVfR6nkHkMCumyh7/lJ6jMqyES6tKUbOJRa6gJxcIUWeRDu+XrHTHLf3gRNUqDAbFvW8FMIrQm7PdreZgRA==} + '@solana/options@5.1.0': + resolution: {integrity: sha512-PqgfALd0yhK+QFaYIbRFTV6hBpiy5xwdu07zSw1RLoNvt1sg+MRsRFDk9R8ZdEdiM69PY/cKiClVSjpNzLLcJg==} engines: {node: '>=20.18.0'} peerDependencies: - typescript: ^5.0.0 - peerDependenciesMeta: - typescript: - optional: true + typescript: '>=5.3.3' - '@solana/plugin-core@5.5.1': - resolution: {integrity: sha512-VUZl30lDQFJeiSyNfzU1EjYt2QZvoBFKEwjn1lilUJw7KgqD5z7mbV7diJhT+dLFs36i0OsjXvq5kSygn8YJ3A==} + '@solana/options@6.1.0': + resolution: {integrity: sha512-/4FtVfR6nkHkMCumyh7/lJ6jMqyES6tKUbOJRa6gJxcIUWeRDu+XrHTHLf3gRNUqDAbFvW8FMIrQm7PdreZgRA==} engines: {node: '>=20.18.0'} peerDependencies: typescript: ^5.0.0 @@ -3641,14 +3727,17 @@ packages: typescript: optional: true - '@solana/programs@5.5.1': - resolution: {integrity: sha512-7U9kn0Jsx1NuBLn5HRTFYh78MV4XN145Yc3WP/q5BlqAVNlMoU9coG5IUTJIG847TUqC1lRto3Dnpwm6T4YRpA==} + '@solana/programs@5.0.0': + resolution: {integrity: sha512-BKOfBDrSUCJGZ+qKk2aFLu0nU9/84o6z/VDCJkLjaNNuTv8nOlSYq5flNzo1eyJmnpyW372qNvqqRN3AS23+FQ==} engines: {node: '>=20.18.0'} peerDependencies: - typescript: ^5.0.0 - peerDependenciesMeta: - typescript: - optional: true + typescript: '>=5.3.3' + + '@solana/programs@5.1.0': + resolution: {integrity: sha512-zAghXyRGixWNcarShlrnpjMD2115BZTF9JMLIcgkCYDOwjDPFIB/Y0hwDCH87N5uSjzlgkDpxKEL4ILewoZTRQ==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5.3.3' '@solana/programs@6.1.0': resolution: {integrity: sha512-i4L4gSlIHDsdYRt3/YKVKMIN3UuYSKHRqK9B+AejcIc0y6Y/AXnHqzmpBRXEhvTXz18nt59MLXpVU4wu7ASjJA==} @@ -3665,14 +3754,17 @@ packages: peerDependencies: typescript: '>=5.3.3' - '@solana/promises@5.5.1': - resolution: {integrity: sha512-T9lfuUYkGykJmppEcssNiCf6yiYQxJkhiLPP+pyAc2z84/7r3UVIb2tNJk4A9sucS66pzJnVHZKcZVGUUp6wzA==} + '@solana/promises@5.0.0': + resolution: {integrity: sha512-Qmg3UfYfWINEUvBQL3DkPOq34tTg5cfrkPlDtJmi8RVifsPqb6hksbKZGu7ASLZohxIDGmnYQY6oELI7Me+5yw==} engines: {node: '>=20.18.0'} peerDependencies: - typescript: ^5.0.0 - peerDependenciesMeta: - typescript: - optional: true + typescript: '>=5.3.3' + + '@solana/promises@5.1.0': + resolution: {integrity: sha512-LU9wwS1PvGc/It610dclfq+JCuUEZSIWjvaF0+sqMP7QCk12Uz7MK2m9TtvLcjTvvKTIrucglRZP6qKroWRqGg==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5.3.3' '@solana/promises@6.1.0': resolution: {integrity: sha512-/mUW6peXQiEOaylLpGv4vtkvPzQvSbfhX9j5PNIK/ry4S3SHRQ3j3W/oGy4y3LR5alwo7NcVbubrkh4e4xwcww==} @@ -3689,14 +3781,17 @@ packages: peerDependencies: typescript: '>=5.3.3' - '@solana/rpc-api@5.5.1': - resolution: {integrity: sha512-XWOQQPhKl06Vj0xi3RYHAc6oEQd8B82okYJ04K7N0Vvy3J4PN2cxeK7klwkjgavdcN9EVkYCChm2ADAtnztKnA==} + '@solana/rpc-api@5.0.0': + resolution: {integrity: sha512-IJbZZnX2B1ldXPok1NhneXTYq9ZvdJbE5Pryr03pZTlPJaWGqDcZuQ14nwR4s6PoUUgdT+p87QlLZqLb8MusoQ==} engines: {node: '>=20.18.0'} peerDependencies: - typescript: ^5.0.0 - peerDependenciesMeta: - typescript: - optional: true + typescript: '>=5.3.3' + + '@solana/rpc-api@5.1.0': + resolution: {integrity: sha512-eI1tY0i3gmih1C65gFECYbfPRpHEYqFp+9IKjpknZtYpQIe9BqBKSpfYpGiCAbKdN/TMadBNPOzdK15ewhkkvQ==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5.3.3' '@solana/rpc-api@6.1.0': resolution: {integrity: sha512-+hO5+kZjJHuUNATUQxlJ1+ztXFkgn1j46zRwt3X7kF+VHkW3wsQ7up0JTS+Xsacmkrj1WKfymQweq8JTrsAG8A==} @@ -3713,14 +3808,17 @@ packages: peerDependencies: typescript: '>=5.3.3' - '@solana/rpc-parsed-types@5.5.1': - resolution: {integrity: sha512-HEi3G2nZqGEsa3vX6U0FrXLaqnUCg4SKIUrOe8CezD+cSFbRTOn3rCLrUmJrhVyXlHoQVaRO9mmeovk31jWxJg==} + '@solana/rpc-parsed-types@5.0.0': + resolution: {integrity: sha512-fU9uqlOYAaBqgk2qCl+ntenBm7wuSFBRbIO/rVjeBPd/qPCvNZU+qFET+ERLK6wbCTSz0MmdHqPn1V8KCMOvZQ==} engines: {node: '>=20.18.0'} peerDependencies: - typescript: ^5.0.0 - peerDependenciesMeta: - typescript: - optional: true + typescript: '>=5.3.3' + + '@solana/rpc-parsed-types@5.1.0': + resolution: {integrity: sha512-ZJoXHNItALMNa1zmGrNnIh96RBlc9GpIqoaZkdE14mAQ7gWe7Oc0ejYavUeSCmcL0wZcvIFh50AsfVxrHr4+2Q==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5.3.3' '@solana/rpc-parsed-types@6.1.0': resolution: {integrity: sha512-YKccynVgWt/gbs0tBYstNw6BSVuOeWdeAldTB2OgH95o2Q04DpO4v97X1MZDysA4SvSZM30Ek5Ni5ss3kskgdw==} @@ -3737,14 +3835,17 @@ packages: peerDependencies: typescript: '>=5.3.3' - '@solana/rpc-spec-types@5.5.1': - resolution: {integrity: sha512-6OFKtRpIEJQs8Jb2C4OO8KyP2h2Hy1MFhatMAoXA+0Ik8S3H+CicIuMZvGZ91mIu/tXicuOOsNNLu3HAkrakrw==} + '@solana/rpc-spec-types@5.0.0': + resolution: {integrity: sha512-B0P/ylXVaCG5oSIV+kB88s2qoW996D8iKhc7RyF0C/AyYvklF6kCwv0N9ZVrWp0ibjlQ8St290WbBHJyo7QZkA==} engines: {node: '>=20.18.0'} peerDependencies: - typescript: ^5.0.0 - peerDependenciesMeta: - typescript: - optional: true + typescript: '>=5.3.3' + + '@solana/rpc-spec-types@5.1.0': + resolution: {integrity: sha512-B8/WyjmHpC34vXtAmTpZyPwRCm7WwoSkmjBcBouaaY1uilJ9+Wp2nptbq2cJyWairOoMSoI7v5kvvnrJuquq4Q==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5.3.3' '@solana/rpc-spec-types@6.1.0': resolution: {integrity: sha512-tldMv1b6VGcvcRrY5MDWKlsyEKH6K96zE7gAIpKDX2G4T47ZOV+OMA3nh6xQpRgtyCUBsej0t80qmvTBDX/5IQ==} @@ -3761,14 +3862,17 @@ packages: peerDependencies: typescript: '>=5.3.3' - '@solana/rpc-spec@5.5.1': - resolution: {integrity: sha512-m3LX2bChm3E3by4mQrH4YwCAFY57QBzuUSWqlUw7ChuZ+oLLOq7b2czi4i6L4Vna67j3eCmB3e+4tqy1j5wy7Q==} + '@solana/rpc-spec@5.0.0': + resolution: {integrity: sha512-1LD2SYEQ5bYhiBumznAPzymtxSX4nYLZd6u+FA0bAxNBVzHDvUUQzVSXHAoWROhlGrCyvtALTs9u0DIDlgZHCA==} engines: {node: '>=20.18.0'} peerDependencies: - typescript: ^5.0.0 - peerDependenciesMeta: - typescript: - optional: true + typescript: '>=5.3.3' + + '@solana/rpc-spec@5.1.0': + resolution: {integrity: sha512-y8B6fUWA1EBKXUsNo6b9EiFcQPsaJREPLlcIDbo4b6TucQNwvl7FHfpf1VHJL64SkI/WE69i2WEkiOJYjmLO0A==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5.3.3' '@solana/rpc-spec@6.1.0': resolution: {integrity: sha512-RxpkIGizCYhXGUcap7npV2S/rAXZ7P/liozY/ExjMmCxYTDwGIW33kp/uH/JRxuzrL8+f8FqY76VsqqIe+2VZw==} @@ -3785,14 +3889,17 @@ packages: peerDependencies: typescript: '>=5.3.3' - '@solana/rpc-subscriptions-api@5.5.1': - resolution: {integrity: sha512-5Oi7k+GdeS8xR2ly1iuSFkAv6CZqwG0Z6b1QZKbEgxadE1XGSDrhM2cn59l+bqCozUWCqh4c/A2znU/qQjROlw==} + '@solana/rpc-subscriptions-api@5.0.0': + resolution: {integrity: sha512-DGUn3C12swV2FConOlLFN14npIrCtnxehtMLjszMC7g6p/P6WNIz5uAgF7YcIkLBDV8uTeWhM0azmK+V8Qqhvg==} engines: {node: '>=20.18.0'} peerDependencies: - typescript: ^5.0.0 - peerDependenciesMeta: - typescript: - optional: true + typescript: '>=5.3.3' + + '@solana/rpc-subscriptions-api@5.1.0': + resolution: {integrity: sha512-84e2AsgqAGiVloW3G4RzpHPkInknu3rEuFPut2/69eq3Ab97TiTz2s5kc9gJpprtGM+xbgnIfeuGqr5F+2bXQA==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5.3.3' '@solana/rpc-subscriptions-api@6.1.0': resolution: {integrity: sha512-I6J+3VU0dda6EySKbDyd+1urC7RGIRPRp0DcWRVcy68NOLbq0I5C40Dn9O2Zf8iCdK4PbQ7JKdCvZ/bDd45hdg==} @@ -3810,13 +3917,21 @@ packages: typescript: '>=5.3.3' ws: ^8.18.0 - '@solana/rpc-subscriptions-channel-websocket@5.5.1': - resolution: {integrity: sha512-7tGfBBrYY8TrngOyxSHoCU5shy86iA9SRMRrPSyBhEaZRAk6dnbdpmUTez7gtdVo0BCvh9nzQtUycKWSS7PnFQ==} + '@solana/rpc-subscriptions-channel-websocket@5.0.0': + resolution: {integrity: sha512-vsYXyjVX/kExfpr91zfMKTmWKKFCM+dkhXQDAz5aEE7kAF3KSZDiOGeYvN8Rc85lbIt9QK6BLAT+NBMv4/N9Qg==} engines: {node: '>=20.18.0'} peerDependencies: - typescript: ^5.0.0 + typescript: '>=5.3.3' + ws: ^8.18.0 + + '@solana/rpc-subscriptions-channel-websocket@5.1.0': + resolution: {integrity: sha512-FzAEmHzXtlckNn7T/1dzDS7r5HmekYPstrtZKjDcVxuGMVBUkZTnb69t7EJvKNuKw1wYZEUd0EEegtC2K/9dZA==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5.3.3' + ws: ^8.18.0 peerDependenciesMeta: - typescript: + ws: optional: true '@solana/rpc-subscriptions-channel-websocket@6.1.0': @@ -3834,14 +3949,17 @@ packages: peerDependencies: typescript: '>=5.3.3' - '@solana/rpc-subscriptions-spec@5.5.1': - resolution: {integrity: sha512-iq+rGq5fMKP3/mKHPNB6MC8IbVW41KGZg83Us/+LE3AWOTWV1WT20KT2iH1F1ik9roi42COv/TpoZZvhKj45XQ==} + '@solana/rpc-subscriptions-spec@5.0.0': + resolution: {integrity: sha512-erRLvZMncwnciJP6I1SlAk0CyRGIgt83PyHWOVCRXENP9Q5dZbZ9pm4lar2yIp8EjIMnodGHsQWIlKc1hlCQlQ==} engines: {node: '>=20.18.0'} peerDependencies: - typescript: ^5.0.0 - peerDependenciesMeta: - typescript: - optional: true + typescript: '>=5.3.3' + + '@solana/rpc-subscriptions-spec@5.1.0': + resolution: {integrity: sha512-ORfjKtainnYisql6z4YsXByVwY8/rWsedVWn5oe/V7Og9LyetTM7hwJ8FbUdRDZwyLlUrI0cEE1aG+3ma/8tPw==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5.3.3' '@solana/rpc-subscriptions-spec@6.1.0': resolution: {integrity: sha512-P06jhqzHpZGaLeJmIQkpDeMDD1xUp53ARpmXMsduMC+U5ZKQt29CLo+JrR18boNtls6WfttjVMEbzF25/4UPVA==} @@ -3858,14 +3976,17 @@ packages: peerDependencies: typescript: '>=5.3.3' - '@solana/rpc-subscriptions@5.5.1': - resolution: {integrity: sha512-CTMy5bt/6mDh4tc6vUJms9EcuZj3xvK0/xq8IQ90rhkpYvate91RjBP+egvjgSayUg9yucU9vNuUpEjz4spM7w==} + '@solana/rpc-subscriptions@5.0.0': + resolution: {integrity: sha512-cziOSzom/bwFZXViR9J+MxDsdLMcfvrXGw5Icng7dYODFKuVqfsDrQoG8uekJc4fREnbPEM2U+u9YnYSYbFbww==} engines: {node: '>=20.18.0'} peerDependencies: - typescript: ^5.0.0 - peerDependenciesMeta: - typescript: - optional: true + typescript: '>=5.3.3' + + '@solana/rpc-subscriptions@5.1.0': + resolution: {integrity: sha512-u/mafVzBbdqvYDD7x/98T5/5xk4Bl2C/90TaHiKx7FmutVC/H4QsritPTY0v9JG1dOVWbgIfUgfZ0C0DPkiYnA==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5.3.3' '@solana/rpc-subscriptions@6.1.0': resolution: {integrity: sha512-sqwj+cQinWcZ7M/9+cudKxMPTkTQyGP73980vPCWM7vCpPkp2qzgrEie4DdgDGo+NMwIjeFgu2kdUuLHI3GD/g==} @@ -3882,14 +4003,17 @@ packages: peerDependencies: typescript: '>=5.3.3' - '@solana/rpc-transformers@5.5.1': - resolution: {integrity: sha512-OsWqLCQdcrRJKvHiMmwFhp9noNZ4FARuMkHT5us3ustDLXaxOjF0gfqZLnMkulSLcKt7TGXqMhBV+HCo7z5M8Q==} + '@solana/rpc-transformers@5.0.0': + resolution: {integrity: sha512-EMHhSgfF6/T4FfHbLaBP08SIj1ZAjxJr6WPNZMHLV7Cup8UfiB9TNV+bPQkum7JbVQNhUKzkKEEmyYqPfQoV9w==} engines: {node: '>=20.18.0'} peerDependencies: - typescript: ^5.0.0 - peerDependenciesMeta: - typescript: - optional: true + typescript: '>=5.3.3' + + '@solana/rpc-transformers@5.1.0': + resolution: {integrity: sha512-6v93xi/ewGS/xEiSktNQ0bh0Uiv1/q9nR5oiFMn3BiAJRC+FdMRMxCjp6H+/Tua7wdhpClaPKrZYBQHoIp59tw==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5.3.3' '@solana/rpc-transformers@6.1.0': resolution: {integrity: sha512-OsSuuRPmsmS02eR9Zz+4iTsr+21hvEMEex5vwbwN6LAGPFlQ4ohqGkxgZCwmYd+Q5HWpnn9Uuf1MDTLLrKQkig==} @@ -3906,14 +4030,17 @@ packages: peerDependencies: typescript: '>=5.3.3' - '@solana/rpc-transport-http@5.5.1': - resolution: {integrity: sha512-yv8GoVSHqEV0kUJEIhkdOVkR2SvJ6yoWC51cJn2rSV7plr6huLGe0JgujCmB7uZhhaLbcbP3zxXxu9sOjsi7Fg==} + '@solana/rpc-transport-http@5.0.0': + resolution: {integrity: sha512-RoIEvWp7yc7rIRzNkOyjLs2UQF0odIEMWj87dbD4Ir4hwTCGo/TSTfQF/8KDV2etdke3Fa1K+W1NkpG2POqWFg==} engines: {node: '>=20.18.0'} peerDependencies: - typescript: ^5.0.0 - peerDependenciesMeta: - typescript: - optional: true + typescript: '>=5.3.3' + + '@solana/rpc-transport-http@5.1.0': + resolution: {integrity: sha512-XoGX+2n/iXzoGb3Xrltbx8avnzp15vCfCGXuZpQWFL+xUg3P4CGl217XyDGjS5VxuUml+f/30xzWl18RaAIEcw==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5.3.3' '@solana/rpc-transport-http@6.1.0': resolution: {integrity: sha512-3ebaTYuglLJagaXtjwDPVI7SQeeeFN2fpetpGKsuMAiti4fzYqEkNN8FIo+nXBzqqG/cVc2421xKjXl6sO1k/g==} @@ -3930,14 +4057,17 @@ packages: peerDependencies: typescript: '>=5.3.3' - '@solana/rpc-types@5.5.1': - resolution: {integrity: sha512-bibTFQ7PbHJJjGJPmfYC2I+/5CRFS4O2p9WwbFraX1Keeel+nRrt/NBXIy8veP5AEn2sVJIyJPpWBRpCx1oATA==} + '@solana/rpc-types@5.0.0': + resolution: {integrity: sha512-JMbhwnV6nX4ezJv/KmaElOR0r/MZTKzKpaz6cv7FopLNuPrYCBrRCZKuM2XQh6gUbt9Mey08/KBOmOGmzTbL/g==} engines: {node: '>=20.18.0'} peerDependencies: - typescript: ^5.0.0 - peerDependenciesMeta: - typescript: - optional: true + typescript: '>=5.3.3' + + '@solana/rpc-types@5.1.0': + resolution: {integrity: sha512-Rnpt5BuHQvnULPNXUC/yRqB+7iPbon95CSCeyRvPj5tJ4fx2JibvX3s/UEoud5vC+kRjPi/R0BGJ8XFvd3eDWg==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5.3.3' '@solana/rpc-types@6.1.0': resolution: {integrity: sha512-lR+Cb3v5Rpl49HsXWASy++TSE1AD86eRKabY+iuWnbBMYVGI4MamAvYwgBiygsCNc30nyO2TFNj9STMeSD/gAg==} @@ -3954,14 +4084,17 @@ packages: peerDependencies: typescript: '>=5.3.3' - '@solana/rpc@5.5.1': - resolution: {integrity: sha512-ku8zTUMrkCWci66PRIBC+1mXepEnZH/q1f3ck0kJZ95a06bOTl5KU7HeXWtskkyefzARJ5zvCs54AD5nxjQJ+A==} + '@solana/rpc@5.0.0': + resolution: {integrity: sha512-Myx/ZBmMHkgh9Di3tLzc+vd30f+6YC1JXr9+YmIHKEeqN/+iTHkDJU2E/hGRLy8vTOBOU7+2466A+dLnSVuGkg==} engines: {node: '>=20.18.0'} peerDependencies: - typescript: ^5.0.0 - peerDependenciesMeta: - typescript: - optional: true + typescript: '>=5.3.3' + + '@solana/rpc@5.1.0': + resolution: {integrity: sha512-j+ByLxFCoHWw9TnsGzkAVMFUfBDIUE53nIosJAYEsERpImD2mjwc33uDE6YXLKoaKRoYO4tc7IUzkKY1fQp/CA==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5.3.3' '@solana/rpc@6.1.0': resolution: {integrity: sha512-R3y5PklW9mPy5Y34hsXj40R28zN2N7AGLnHqYJVkXkllwVub/QCNpSdDxAnbbS5EGOYGoUOW8s5LFoXwMSr1LQ==} @@ -3972,14 +4105,17 @@ packages: typescript: optional: true - '@solana/signers@5.5.1': - resolution: {integrity: sha512-FY0IVaBT2kCAze55vEieR6hag4coqcuJ31Aw3hqRH7mv6sV8oqwuJmUrx+uFwOp1gwd5OEAzlv6N4hOOple4sQ==} + '@solana/signers@5.0.0': + resolution: {integrity: sha512-9Hw6HekSEzj5O7UBBFPrxk96W5e8tMI3n7KbW7/QiKBDpuvYw9WtnjOsWUE7LqQoc1P0JjGEsrmxE9raQBLvuQ==} engines: {node: '>=20.18.0'} peerDependencies: - typescript: ^5.0.0 - peerDependenciesMeta: - typescript: - optional: true + typescript: '>=5.3.3' + + '@solana/signers@5.1.0': + resolution: {integrity: sha512-B8xO0SGN1ZWYfJROL+da3id279qNbXbXoqud+AuT5gur51RrS4YhNkTQ6khVbGtAOpPMAhkoZN0jnfCC1r33jQ==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5.3.3' '@solana/signers@6.1.0': resolution: {integrity: sha512-WDPGZJr6jIe2dEChv/2KQBnaga8dqOjd6ceBj/HcDHxnCudo66t7GlyZ9+9jMO40AgOOb7EDE5FDqPMrHMg5Yw==} @@ -3990,20 +4126,41 @@ packages: typescript: optional: true + '@solana/spl-token-group@0.0.7': + resolution: {integrity: sha512-V1N/iX7Cr7H0uazWUT2uk27TMqlqedpXHRqqAbVO2gvmJyT0E0ummMEAVQeXZ05ZhQ/xF39DLSdBp90XebWEug==} + engines: {node: '>=16'} + peerDependencies: + '@solana/web3.js': ^1.95.3 + + '@solana/spl-token-metadata@0.1.6': + resolution: {integrity: sha512-7sMt1rsm/zQOQcUWllQX9mD2O6KhSAtY1hFR2hfFwgqfFWzSY9E9GDvFVNYUI1F0iQKcm6HmePU9QbKRXTEBiA==} + engines: {node: '>=16'} + peerDependencies: + '@solana/web3.js': ^1.95.3 + + '@solana/spl-token@0.4.14': + resolution: {integrity: sha512-u09zr96UBpX4U685MnvQsNzlvw9TiY005hk1vJmJr7gMJldoPG1eYU5/wNEyOA5lkMLiR/gOi9SFD4MefOYEsA==} + engines: {node: '>=16'} + peerDependencies: + '@solana/web3.js': ^1.95.5 + '@solana/subscribable@2.3.0': resolution: {integrity: sha512-DkgohEDbMkdTWiKAoatY02Njr56WXx9e/dKKfmne8/Ad6/2llUIrax78nCdlvZW9quXMaXPTxZvdQqo9N669Og==} engines: {node: '>=20.18.0'} peerDependencies: typescript: '>=5.3.3' - '@solana/subscribable@5.5.1': - resolution: {integrity: sha512-9K0PsynFq0CsmK1CDi5Y2vUIJpCqkgSS5yfDN0eKPgHqEptLEaia09Kaxc90cSZDZU5mKY/zv1NBmB6Aro9zQQ==} + '@solana/subscribable@5.0.0': + resolution: {integrity: sha512-C2TydIRRd5XUJ8asbARi67Sj/3DRLubWalnNoafBhDsrb88jsRVylntvwXgBw/+lwJdEPEsUnxvcdgdm+3lFlw==} engines: {node: '>=20.18.0'} peerDependencies: - typescript: ^5.0.0 - peerDependenciesMeta: - typescript: - optional: true + typescript: '>=5.3.3' + + '@solana/subscribable@5.1.0': + resolution: {integrity: sha512-OeW5AJwKzHh18+PIPtghuuPJTmEep2Mhb3Lsrq4alas4fibmMGkr39z1HXxVF6l6e2lu/YGhHIDtuhouWmY7ow==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5.3.3' '@solana/subscribable@6.1.0': resolution: {integrity: sha512-HiUfkxN7638uxPmY4t0gI4+yqnFLZYJKFaT9EpWIuGrOB1d9n+uOHNs3NU7cVMwWXgfZUbztTCKyCVTbcwesNg==} @@ -4014,14 +4171,17 @@ packages: typescript: optional: true - '@solana/sysvars@5.5.1': - resolution: {integrity: sha512-k3Quq87Mm+geGUu1GWv6knPk0ALsfY6EKSJGw9xUJDHzY/RkYSBnh0RiOrUhtFm2TDNjOailg8/m0VHmi3reFA==} + '@solana/sysvars@5.0.0': + resolution: {integrity: sha512-F/GEb2rS8mrgDd79lDPyu8za9jGE6cRlS4jHNeKCkvOCJxdKQbX34JIzx4kwzjtvk7O8/yrDHfGdpA8nBg/l4w==} engines: {node: '>=20.18.0'} peerDependencies: - typescript: ^5.0.0 - peerDependenciesMeta: - typescript: - optional: true + typescript: '>=5.3.3' + + '@solana/sysvars@5.1.0': + resolution: {integrity: sha512-FJ9YIsLTAaajnOrYEYn54znstXJsvKndRhyCrlyiAEN1IXHw5HtZHploLF3ZZ78b7YU3uv3tFJMziXFBwPOn4Q==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5.3.3' '@solana/sysvars@6.1.0': resolution: {integrity: sha512-KwJyBBrAOx0BgkiZqOKAaySDb/0JrUFSBQL9/O1kSKGy9TCRX55Ytr1HxNTcTPppWNpbM6JZVK+yW3Ruey0HRw==} @@ -4038,14 +4198,17 @@ packages: peerDependencies: typescript: '>=5.3.3' - '@solana/transaction-confirmation@5.5.1': - resolution: {integrity: sha512-j4mKlYPHEyu+OD7MBt3jRoX4ScFgkhZC6H65on4Fux6LMScgivPJlwnKoZMnsgxFgWds0pl+BYzSiALDsXlYtw==} + '@solana/transaction-confirmation@5.0.0': + resolution: {integrity: sha512-LpusTopYIuQC8hBCloExkTr4Z5/zdp5f4IIbzD5XFeW3xXPZytS3H1IDMGk4bmLdZi9zQNA4lnNHKra5IncRbw==} engines: {node: '>=20.18.0'} peerDependencies: - typescript: ^5.0.0 - peerDependenciesMeta: - typescript: - optional: true + typescript: '>=5.3.3' + + '@solana/transaction-confirmation@5.1.0': + resolution: {integrity: sha512-6HnL0uH8tWZXJVuaoeTbCQp/FS11Bsc4GSlq+k0N21GdhTbFuqBhsxlAYWbzPWs9+/kYRGHqqXvBPCReWxT7BA==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5.3.3' '@solana/transaction-confirmation@6.1.0': resolution: {integrity: sha512-akSjcqAMOGPFvKctFDSzhjcRc/45WbEVdVQ9mjgH6OYo7B11WZZZaeGPlzAw5KyuG34Px941xmICkBmNqEH47Q==} @@ -4062,14 +4225,17 @@ packages: peerDependencies: typescript: '>=5.3.3' - '@solana/transaction-messages@5.5.1': - resolution: {integrity: sha512-aXyhMCEaAp3M/4fP0akwBBQkFPr4pfwoC5CLDq999r/FUwDax2RE/h4Ic7h2Xk+JdcUwsb+rLq85Y52hq84XvQ==} + '@solana/transaction-messages@5.0.0': + resolution: {integrity: sha512-rJLe1wUGW5DovQFV0gjXHXnriPxTBgZ3TvGWnjCu2OIBU8mcQkQVJ7zzVZY2IAYlmJ6OSF9nvzhSt/ncPbkJPg==} engines: {node: '>=20.18.0'} peerDependencies: - typescript: ^5.0.0 - peerDependenciesMeta: - typescript: - optional: true + typescript: '>=5.3.3' + + '@solana/transaction-messages@5.1.0': + resolution: {integrity: sha512-9rNV2YJhd85WIMvnwa/vUY4xUw3ZTU17jP1KDo/fFZWk55a0ov0ATJJPyC5HAR1i6hT1cmJzGH/UHhnD9m/Q3w==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5.3.3' '@solana/transaction-messages@6.1.0': resolution: {integrity: sha512-Dpv54LRVcfFbFEa/uB53LaY/TRfKuPGMKR7Z4F290zBgkj9xkpZkI+WLiJBiSloI7Qo2KZqXj3514BIeZvJLcg==} @@ -4086,14 +4252,17 @@ packages: peerDependencies: typescript: '>=5.3.3' - '@solana/transactions@5.5.1': - resolution: {integrity: sha512-8hHtDxtqalZ157pnx6p8k10D7J/KY/biLzfgh9R09VNLLY3Fqi7kJvJCr7M2ik3oRll56pxhraAGCC9yIT6eOA==} + '@solana/transactions@5.0.0': + resolution: {integrity: sha512-4TcsqH7JtgRKGGBIRRGz0n+tXu4h5TPPC49kkV0ygIndQaHW7FOZUYTwQ0epq0A5h9KYi+ClNbzF9xiuDbAD5Q==} engines: {node: '>=20.18.0'} peerDependencies: - typescript: ^5.0.0 - peerDependenciesMeta: - typescript: - optional: true + typescript: '>=5.3.3' + + '@solana/transactions@5.1.0': + resolution: {integrity: sha512-06JwSPtz+38ozNgpysAXS2eTMPQCufIisXB6K88X8J4GF8ziqs4nkq0BpXAXn+MpZTkuMt+JeW2RxP3HKhXe5g==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5.3.3' '@solana/transactions@6.1.0': resolution: {integrity: sha512-1dkiNJcTtlHm4Fvs5VohNVpv7RbvbUYYKV7lYXMPIskoLF1eZp0tVlEqD/cRl91RNz7HEysfHqBAwlcJcRmrRg==} @@ -4126,6 +4295,18 @@ packages: '@stablelib/wipe@1.0.1': resolution: {integrity: sha512-WfqfX/eXGiAd3RJe4VU2snh/ZPwtSjLG4ynQ/vYzvghTh7dHFcI1wl+nrkWG6lGhukOxOsUHfv8dUXr58D0ayg==} + '@stellar/js-xdr@3.1.2': + resolution: {integrity: sha512-VVolPL5goVEIsvuGqDc5uiKxV03lzfWdvYg1KikvwheDmTBO68CKDji3bAZ/kppZrx5iTA8z3Ld5yuytcvhvOQ==} + + '@stellar/stellar-base@14.1.0': + resolution: {integrity: sha512-A8kFli6QGy22SRF45IjgPAJfUNGjnI+R7g4DF5NZYVsD1kGf7B4ITyc4OPclLV9tqNI4/lXxafGEw0JEUbHixw==} + engines: {node: '>=20.0.0'} + + '@stellar/stellar-sdk@14.6.1': + resolution: {integrity: sha512-A1rQWDLdUasXkMXnYSuhgep+3ZZzyuXJKdt5/KAIc0gkmSp906HTvUpbT4pu+bVr41tu0+J4Ugz9J4BQAGGytg==} + engines: {node: '>=20.0.0'} + hasBin: true + '@svgr/babel-plugin-add-jsx-attribute@8.0.0': resolution: {integrity: sha512-b9MIk7yhdS1pMCZM8VeNfUlSKVRhsHZNMl5O9SfaX0l0t5wjdgu4IDzGB8bpnGBBOjGST3rRFVsaaEtI4W6f7g==} engines: {node: '>=14'} @@ -4207,72 +4388,72 @@ packages: '@swc/helpers@0.5.15': resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==} - '@swc/helpers@0.5.18': - resolution: {integrity: sha512-TXTnIcNJQEKwThMMqBXsZ4VGAza6bvN4pa41Rkqoio6QBKMvo+5lexeTMScGCIxtzgQJzElcvIltani+adC5PQ==} + '@swc/helpers@0.5.17': + resolution: {integrity: sha512-5IKx/Y13RsYd+sauPb2x+U/xZikHjolzfuDgTAl/Tdf3Q8rslRvC19NKDLgAJQ6wsqADk10ntlv08nPFw/gO/A==} '@szmarczak/http-timer@4.0.6': resolution: {integrity: sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==} engines: {node: '>=10'} - '@tailwindcss/node@4.1.18': - resolution: {integrity: sha512-DoR7U1P7iYhw16qJ49fgXUlry1t4CpXeErJHnQ44JgTSKMaZUdf17cfn5mHchfJ4KRBZRFA/Coo+MUF5+gOaCQ==} + '@tailwindcss/node@4.1.17': + resolution: {integrity: sha512-csIkHIgLb3JisEFQ0vxr2Y57GUNYh447C8xzwj89U/8fdW8LhProdxvnVH6U8M2Y73QKiTIH+LWbK3V2BBZsAg==} - '@tailwindcss/oxide-android-arm64@4.1.18': - resolution: {integrity: sha512-dJHz7+Ugr9U/diKJA0W6N/6/cjI+ZTAoxPf9Iz9BFRF2GzEX8IvXxFIi/dZBloVJX/MZGvRuFA9rqwdiIEZQ0Q==} + '@tailwindcss/oxide-android-arm64@4.1.17': + resolution: {integrity: sha512-BMqpkJHgOZ5z78qqiGE6ZIRExyaHyuxjgrJ6eBO5+hfrfGkuya0lYfw8fRHG77gdTjWkNWEEm+qeG2cDMxArLQ==} engines: {node: '>= 10'} cpu: [arm64] os: [android] - '@tailwindcss/oxide-darwin-arm64@4.1.18': - resolution: {integrity: sha512-Gc2q4Qhs660bhjyBSKgq6BYvwDz4G+BuyJ5H1xfhmDR3D8HnHCmT/BSkvSL0vQLy/nkMLY20PQ2OoYMO15Jd0A==} + '@tailwindcss/oxide-darwin-arm64@4.1.17': + resolution: {integrity: sha512-EquyumkQweUBNk1zGEU/wfZo2qkp/nQKRZM8bUYO0J+Lums5+wl2CcG1f9BgAjn/u9pJzdYddHWBiFXJTcxmOg==} engines: {node: '>= 10'} cpu: [arm64] os: [darwin] - '@tailwindcss/oxide-darwin-x64@4.1.18': - resolution: {integrity: sha512-FL5oxr2xQsFrc3X9o1fjHKBYBMD1QZNyc1Xzw/h5Qu4XnEBi3dZn96HcHm41c/euGV+GRiXFfh2hUCyKi/e+yw==} + '@tailwindcss/oxide-darwin-x64@4.1.17': + resolution: {integrity: sha512-gdhEPLzke2Pog8s12oADwYu0IAw04Y2tlmgVzIN0+046ytcgx8uZmCzEg4VcQh+AHKiS7xaL8kGo/QTiNEGRog==} engines: {node: '>= 10'} cpu: [x64] os: [darwin] - '@tailwindcss/oxide-freebsd-x64@4.1.18': - resolution: {integrity: sha512-Fj+RHgu5bDodmV1dM9yAxlfJwkkWvLiRjbhuO2LEtwtlYlBgiAT4x/j5wQr1tC3SANAgD+0YcmWVrj8R9trVMA==} + '@tailwindcss/oxide-freebsd-x64@4.1.17': + resolution: {integrity: sha512-hxGS81KskMxML9DXsaXT1H0DyA+ZBIbyG/sSAjWNe2EDl7TkPOBI42GBV3u38itzGUOmFfCzk1iAjDXds8Oh0g==} engines: {node: '>= 10'} cpu: [x64] os: [freebsd] - '@tailwindcss/oxide-linux-arm-gnueabihf@4.1.18': - resolution: {integrity: sha512-Fp+Wzk/Ws4dZn+LV2Nqx3IilnhH51YZoRaYHQsVq3RQvEl+71VGKFpkfHrLM/Li+kt5c0DJe/bHXK1eHgDmdiA==} + '@tailwindcss/oxide-linux-arm-gnueabihf@4.1.17': + resolution: {integrity: sha512-k7jWk5E3ldAdw0cNglhjSgv501u7yrMf8oeZ0cElhxU6Y2o7f8yqelOp3fhf7evjIS6ujTI3U8pKUXV2I4iXHQ==} engines: {node: '>= 10'} cpu: [arm] os: [linux] - '@tailwindcss/oxide-linux-arm64-gnu@4.1.18': - resolution: {integrity: sha512-S0n3jboLysNbh55Vrt7pk9wgpyTTPD0fdQeh7wQfMqLPM/Hrxi+dVsLsPrycQjGKEQk85Kgbx+6+QnYNiHalnw==} + '@tailwindcss/oxide-linux-arm64-gnu@4.1.17': + resolution: {integrity: sha512-HVDOm/mxK6+TbARwdW17WrgDYEGzmoYayrCgmLEw7FxTPLcp/glBisuyWkFz/jb7ZfiAXAXUACfyItn+nTgsdQ==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - '@tailwindcss/oxide-linux-arm64-musl@4.1.18': - resolution: {integrity: sha512-1px92582HkPQlaaCkdRcio71p8bc8i/ap5807tPRDK/uw953cauQBT8c5tVGkOwrHMfc2Yh6UuxaH4vtTjGvHg==} + '@tailwindcss/oxide-linux-arm64-musl@4.1.17': + resolution: {integrity: sha512-HvZLfGr42i5anKtIeQzxdkw/wPqIbpeZqe7vd3V9vI3RQxe3xU1fLjss0TjyhxWcBaipk7NYwSrwTwK1hJARMg==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - '@tailwindcss/oxide-linux-x64-gnu@4.1.18': - resolution: {integrity: sha512-v3gyT0ivkfBLoZGF9LyHmts0Isc8jHZyVcbzio6Wpzifg/+5ZJpDiRiUhDLkcr7f/r38SWNe7ucxmGW3j3Kb/g==} + '@tailwindcss/oxide-linux-x64-gnu@4.1.17': + resolution: {integrity: sha512-M3XZuORCGB7VPOEDH+nzpJ21XPvK5PyjlkSFkFziNHGLc5d6g3di2McAAblmaSUNl8IOmzYwLx9NsE7bplNkwQ==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - '@tailwindcss/oxide-linux-x64-musl@4.1.18': - resolution: {integrity: sha512-bhJ2y2OQNlcRwwgOAGMY0xTFStt4/wyU6pvI6LSuZpRgKQwxTec0/3Scu91O8ir7qCR3AuepQKLU/kX99FouqQ==} + '@tailwindcss/oxide-linux-x64-musl@4.1.17': + resolution: {integrity: sha512-k7f+pf9eXLEey4pBlw+8dgfJHY4PZ5qOUFDyNf7SI6lHjQ9Zt7+NcscjpwdCEbYi6FI5c2KDTDWyf2iHcCSyyQ==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - '@tailwindcss/oxide-wasm32-wasi@4.1.18': - resolution: {integrity: sha512-LffYTvPjODiP6PT16oNeUQJzNVyJl1cjIebq/rWWBF+3eDst5JGEFSc5cWxyRCJ0Mxl+KyIkqRxk1XPEs9x8TA==} + '@tailwindcss/oxide-wasm32-wasi@4.1.17': + resolution: {integrity: sha512-cEytGqSSoy7zK4JRWiTCx43FsKP/zGr0CsuMawhH67ONlH+T79VteQeJQRO/X7L0juEUA8ZyuYikcRBf0vsxhg==} engines: {node: '>=14.0.0'} cpu: [wasm32] bundledDependencies: @@ -4283,30 +4464,30 @@ packages: - '@emnapi/wasi-threads' - tslib - '@tailwindcss/oxide-win32-arm64-msvc@4.1.18': - resolution: {integrity: sha512-HjSA7mr9HmC8fu6bdsZvZ+dhjyGCLdotjVOgLA2vEqxEBZaQo9YTX4kwgEvPCpRh8o4uWc4J/wEoFzhEmjvPbA==} + '@tailwindcss/oxide-win32-arm64-msvc@4.1.17': + resolution: {integrity: sha512-JU5AHr7gKbZlOGvMdb4722/0aYbU+tN6lv1kONx0JK2cGsh7g148zVWLM0IKR3NeKLv+L90chBVYcJ8uJWbC9A==} engines: {node: '>= 10'} cpu: [arm64] os: [win32] - '@tailwindcss/oxide-win32-x64-msvc@4.1.18': - resolution: {integrity: sha512-bJWbyYpUlqamC8dpR7pfjA0I7vdF6t5VpUGMWRkXVE3AXgIZjYUYAK7II1GNaxR8J1SSrSrppRar8G++JekE3Q==} + '@tailwindcss/oxide-win32-x64-msvc@4.1.17': + resolution: {integrity: sha512-SKWM4waLuqx0IH+FMDUw6R66Hu4OuTALFgnleKbqhgGU30DY20NORZMZUKgLRjQXNN2TLzKvh48QXTig4h4bGw==} engines: {node: '>= 10'} cpu: [x64] os: [win32] - '@tailwindcss/oxide@4.1.18': - resolution: {integrity: sha512-EgCR5tTS5bUSKQgzeMClT6iCY3ToqE1y+ZB0AKldj809QXk1Y+3jB0upOYZrn9aGIzPtUsP7sX4QQ4XtjBB95A==} + '@tailwindcss/oxide@4.1.17': + resolution: {integrity: sha512-F0F7d01fmkQhsTjXezGBLdrl1KresJTcI3DB8EkScCldyKp3Msz4hub4uyYaVnk88BAS1g5DQjjF6F5qczheLA==} engines: {node: '>= 10'} - '@tailwindcss/postcss@4.1.18': - resolution: {integrity: sha512-Ce0GFnzAOuPyfV5SxjXGn0CubwGcuDB0zcdaPuCSzAa/2vII24JTkH+I6jcbXLb1ctjZMZZI6OjDaLPJQL1S0g==} + '@tailwindcss/postcss@4.1.17': + resolution: {integrity: sha512-+nKl9N9mN5uJ+M7dBOOCzINw94MPstNR/GtIhz1fpZysxL/4a+No64jCBD6CPN+bIHWFx3KWuu8XJRrj/572Dw==} - '@tanstack/query-core@5.90.20': - resolution: {integrity: sha512-OMD2HLpNouXEfZJWcKeVKUgQ5n+n3A2JFmBaScpNDUqSrQSjiveC7dKMe53uJUg1nDG16ttFPz2xfilz6i2uVg==} + '@tanstack/query-core@5.90.11': + resolution: {integrity: sha512-f9z/nXhCgWDF4lHqgIE30jxLe4sYv15QodfdPDKYAk7nAEjNcndy4dHz3ezhdUaR23BpWa4I2EH4/DZ0//Uf8A==} - '@tanstack/react-query@5.90.20': - resolution: {integrity: sha512-vXBxa+qeyveVO7OA0jX1z+DeyCA4JKnThKv411jd5SORpBKgkcVnYKCiBgECvADvniBX7tobwBmg01qq9JmMJw==} + '@tanstack/react-query@5.90.11': + resolution: {integrity: sha512-3uyzz01D1fkTLXuxF3JfoJoHQMU2fxsfJwE+6N5hHy0dVNoZOvwKP8Z2k7k1KDeD54N20apcJnG75TBAStIrBA==} peerDependencies: react: ^18 || ^19 @@ -4314,8 +4495,8 @@ packages: resolution: {integrity: sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==} engines: {node: '>=10.13.0'} - '@tybys/wasm-util@0.10.1': - resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==} + '@tybys/wasm-util@0.10.0': + resolution: {integrity: sha512-VyyPYFlOMNylG45GoAe0xDoLwWuowvf92F9kySqzYh8vmYm7D2u4iUJKa1tOUpS70Ku13ASrOkS4ScXFsTaCNQ==} '@types/body-parser@1.19.6': resolution: {integrity: sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==} @@ -4323,8 +4504,8 @@ packages: '@types/cacheable-request@6.0.3': resolution: {integrity: sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==} - '@types/chai@5.2.3': - resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} + '@types/chai@5.2.2': + resolution: {integrity: sha512-8kB30R7Hwqf40JPiKhVzodJs2Qc1ZJ5zuT3uzw5Hq/dhNCl3G3l83jfpdI1e20BP348+fV7VIL/+FxaXkqBmWg==} '@types/connect@3.4.38': resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==} @@ -4338,14 +4519,14 @@ packages: '@types/estree@1.0.8': resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} - '@types/express-serve-static-core@5.1.1': - resolution: {integrity: sha512-v4zIMr/cX7/d2BpAEX3KNKL/JrT1s43s96lLvvdTmza1oEvDudCqK9aF/djc/SWgy8Yh0h30TZx5VpzqFCxk5A==} + '@types/express-serve-static-core@5.0.7': + resolution: {integrity: sha512-R+33OsgWw7rOhD1emjU7dzCDHucJrgJXMA5PYCzJxVil0dsyx5iBEPHqpPfiKNJQb7lZ1vxwoLR4Z87bBUpeGQ==} - '@types/express@5.0.6': - resolution: {integrity: sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA==} + '@types/express@5.0.3': + resolution: {integrity: sha512-wGA0NX93b19/dZC1J18tKWVIYWyyF2ZjT9vin/NRu0qzzvfVzWjs04iq2rQ3H65vCTQYlRqs3YHfY7zjdV+9Kw==} - '@types/http-cache-semantics@4.0.4': - resolution: {integrity: sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==} + '@types/http-cache-semantics@4.2.0': + resolution: {integrity: sha512-L3LgimLHXtGkWikKnsPg0/VFx9OGZaC+eN1u4r+OB1XRqH3meBIAVC2zr1WdMH+RHmnRkqliQAOHNJ/E0j/e0Q==} '@types/http-errors@2.0.5': resolution: {integrity: sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==} @@ -4359,8 +4540,11 @@ packages: '@types/keyv@3.1.4': resolution: {integrity: sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==} - '@types/lodash@4.17.23': - resolution: {integrity: sha512-RDvF6wTulMPjrNdCoYRC8gNR880JNGT8uB+REUpC2Ns4pRqQJhGz90wh7rgdXDPpCczF3VGktDuFGVnz8zP7HA==} + '@types/lodash@4.17.20': + resolution: {integrity: sha512-H3MHACvFUEiujabxhaI/ImO6gUrd8oOurg7LQtS7mbwIXA/cUqWrvBsaeJ23aZEPk1TAYkurjfMbSELfoCXlGA==} + + '@types/mime@1.3.5': + resolution: {integrity: sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==} '@types/ms@2.1.0': resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==} @@ -4368,8 +4552,8 @@ packages: '@types/node@12.20.55': resolution: {integrity: sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==} - '@types/node@22.19.7': - resolution: {integrity: sha512-MciR4AKGHWl7xwxkBa6xUGxQJ4VBOmPTF7sL+iGzuahOFaO0jHCsuEfS80pan1ef4gWId1oWOweIhrDEYLuaOw==} + '@types/node@22.18.0': + resolution: {integrity: sha512-m5ObIqwsUp6BZzyiy4RdZpzWGub9bqLJMvZDD0QMXhxjqMHMENlj+SqF5QxoUwaQNFe+8kz8XM8ZQhqkQPTgMQ==} '@types/node@22.7.5': resolution: {integrity: sha512-jML7s2NAzMWc//QSJ1a3prpk78cOPchGvXJsC3C6R6PSMoooztvRVQEz89gmBTBY1SPMaqo5teB4uNHPdetShQ==} @@ -4380,22 +4564,22 @@ packages: '@types/range-parser@1.2.7': resolution: {integrity: sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==} - '@types/react-dom@19.2.3': - resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==} + '@types/react-dom@19.1.9': + resolution: {integrity: sha512-qXRuZaOsAdXKFyOhRBg6Lqqc0yay13vN7KrIg4L7N4aaHN68ma9OK3NE1BoDFgFOTfM7zg+3/8+2n8rLUH3OKQ==} peerDependencies: - '@types/react': ^19.2.0 + '@types/react': ^19.0.0 - '@types/react@19.2.10': - resolution: {integrity: sha512-WPigyYuGhgZ/cTPRXB2EwUw+XvsRA3GqHlsP4qteqrnnjDrApbS7MxcGr/hke5iUoeB7E/gQtrs9I37zAJ0Vjw==} + '@types/react@19.1.12': + resolution: {integrity: sha512-cMoR+FoAf/Jyq6+Df2/Z41jISvGZZ2eTlnsaJRptmZ76Caldwy1odD4xTr/gNV9VLj0AWgg/nmkevIyUfIIq5w==} '@types/responselike@1.0.3': resolution: {integrity: sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==} - '@types/send@1.2.1': - resolution: {integrity: sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==} + '@types/send@0.17.5': + resolution: {integrity: sha512-z6F2D3cOStZvuk2SaP6YrwkNO65iTZcwA2ZkSABegdkAh/lf+Aa/YQndZVfmEXT5vgAp6zv06VQ3ejSVjAny4w==} - '@types/serve-static@2.2.0': - resolution: {integrity: sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ==} + '@types/serve-static@1.15.8': + resolution: {integrity: sha512-roei0UY3LhpOJvjbIP6ZZFngyLKl5dskOtDhxY5THRSpO+ZI+nzJ+m5yUMzGrp89YRa7lvknKkMYjqQFGwA7Sg==} '@types/trusted-types@2.0.7': resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} @@ -4409,63 +4593,122 @@ packages: '@types/ws@8.18.1': resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==} - '@typescript-eslint/eslint-plugin@8.54.0': - resolution: {integrity: sha512-hAAP5io/7csFStuOmR782YmTthKBJ9ND3WVL60hcOjvtGFb+HJxH4O5huAcmcZ9v9G8P+JETiZ/G1B8MALnWZQ==} + '@typescript-eslint/eslint-plugin@8.42.0': + resolution: {integrity: sha512-Aq2dPqsQkxHOLfb2OPv43RnIvfj05nw8v/6n3B2NABIPpHnjQnaLo9QGMTvml+tv4korl/Cjfrb/BYhoL8UUTQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + '@typescript-eslint/parser': ^8.42.0 + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/eslint-plugin@8.49.0': + resolution: {integrity: sha512-JXij0vzIaTtCwu6SxTh8qBc66kmf1xs7pI4UOiMDFVct6q86G0Zs7KRcEoJgY3Cav3x5Tq0MF5jwgpgLqgKG3A==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - '@typescript-eslint/parser': ^8.54.0 + '@typescript-eslint/parser': ^8.49.0 eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/parser@8.54.0': - resolution: {integrity: sha512-BtE0k6cjwjLZoZixN0t5AKP0kSzlGu7FctRXYuPAm//aaiZhmfq1JwdYpYr1brzEspYyFeF+8XF5j2VK6oalrA==} + '@typescript-eslint/parser@8.42.0': + resolution: {integrity: sha512-r1XG74QgShUgXph1BYseJ+KZd17bKQib/yF3SR+demvytiRXrwd12Blnz5eYGm8tXaeRdd4x88MlfwldHoudGg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/project-service@8.54.0': - resolution: {integrity: sha512-YPf+rvJ1s7MyiWM4uTRhE4DvBXrEV+d8oC3P9Y2eT7S+HBS0clybdMIPnhiATi9vZOYDc7OQ1L/i6ga6NFYK/g==} + '@typescript-eslint/parser@8.49.0': + resolution: {integrity: sha512-N9lBGA9o9aqb1hVMc9hzySbhKibHmB+N3IpoShyV6HyQYRGIhlrO5rQgttypi+yEeKsKI4idxC8Jw6gXKD4THA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/project-service@8.42.0': + resolution: {integrity: sha512-vfVpLHAhbPjilrabtOSNcUDmBboQNrJUiNAGoImkZKnMjs2TIcWG33s4Ds0wY3/50aZmTMqJa6PiwkwezaAklg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/project-service@8.49.0': + resolution: {integrity: sha512-/wJN0/DKkmRUMXjZUXYZpD1NEQzQAAn9QWfGwo+Ai8gnzqH7tvqS7oNVdTjKqOcPyVIdZdyCMoqN66Ia789e7g==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/scope-manager@8.42.0': + resolution: {integrity: sha512-51+x9o78NBAVgQzOPd17DkNTnIzJ8T/O2dmMBLoK9qbY0Gm52XJcdJcCl18ExBMiHo6jPMErUQWUv5RLE51zJw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/scope-manager@8.49.0': + resolution: {integrity: sha512-npgS3zi+/30KSOkXNs0LQXtsg9ekZ8OISAOLGWA/ZOEn0ZH74Ginfl7foziV8DT+D98WfQ5Kopwqb/PZOaIJGg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/tsconfig-utils@8.42.0': + resolution: {integrity: sha512-kHeFUOdwAJfUmYKjR3CLgZSglGHjbNTi1H8sTYRYV2xX6eNz4RyJ2LIgsDLKf8Yi0/GL1WZAC/DgZBeBft8QAQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/scope-manager@8.54.0': - resolution: {integrity: sha512-27rYVQku26j/PbHYcVfRPonmOlVI6gihHtXFbTdB5sb6qA0wdAQAbyXFVarQ5t4HRojIz64IV90YtsjQSSGlQg==} + '@typescript-eslint/tsconfig-utils@8.49.0': + resolution: {integrity: sha512-8prixNi1/6nawsRYxet4YOhnbW+W9FK/bQPxsGB1D3ZrDzbJ5FXw5XmzxZv82X3B+ZccuSxo/X8q9nQ+mFecWA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/tsconfig-utils@8.54.0': - resolution: {integrity: sha512-dRgOyT2hPk/JwxNMZDsIXDgyl9axdJI3ogZ2XWhBPsnZUv+hPesa5iuhdYt2gzwA9t8RE5ytOJ6xB0moV0Ujvw==} + '@typescript-eslint/type-utils@8.42.0': + resolution: {integrity: sha512-9KChw92sbPTYVFw3JLRH1ockhyR3zqqn9lQXol3/YbI6jVxzWoGcT3AsAW0mu1MY0gYtsXnUGV/AKpkAj5tVlQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: + eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/type-utils@8.54.0': - resolution: {integrity: sha512-hiLguxJWHjjwL6xMBwD903ciAwd7DmK30Y9Axs/etOkftC3ZNN9K44IuRD/EB08amu+Zw6W37x9RecLkOo3pMA==} + '@typescript-eslint/type-utils@8.49.0': + resolution: {integrity: sha512-KTExJfQ+svY8I10P4HdxKzWsvtVnsuCifU5MvXrRwoP2KOlNZ9ADNEWWsQTJgMxLzS5VLQKDjkCT/YzgsnqmZg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/types@8.54.0': - resolution: {integrity: sha512-PDUI9R1BVjqu7AUDsRBbKMtwmjWcn4J3le+5LpcFgWULN3LvHC5rkc9gCVxbrsrGmO1jfPybN5s6h4Jy+OnkAA==} + '@typescript-eslint/types@8.42.0': + resolution: {integrity: sha512-LdtAWMiFmbRLNP7JNeY0SqEtJvGMYSzfiWBSmx+VSZ1CH+1zyl8Mmw1TT39OrtsRvIYShjJWzTDMPWZJCpwBlw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/types@8.49.0': + resolution: {integrity: sha512-e9k/fneezorUo6WShlQpMxXh8/8wfyc+biu6tnAqA81oWrEic0k21RHzP9uqqpyBBeBKu4T+Bsjy9/b8u7obXQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/typescript-estree@8.42.0': + resolution: {integrity: sha512-ku/uYtT4QXY8sl9EDJETD27o3Ewdi72hcXg1ah/kkUgBvAYHLwj2ofswFFNXS+FL5G+AGkxBtvGt8pFBHKlHsQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/typescript-estree@8.49.0': + resolution: {integrity: sha512-jrLdRuAbPfPIdYNppHJ/D0wN+wwNfJ32YTAm10eJVsFmrVpXQnDWBn8niCSMlWjvml8jsce5E/O+86IQtTbJWA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/typescript-estree@8.54.0': - resolution: {integrity: sha512-BUwcskRaPvTk6fzVWgDPdUndLjB87KYDrN5EYGetnktoeAvPtO4ONHlAZDnj5VFnUANg0Sjm7j4usBlnoVMHwA==} + '@typescript-eslint/utils@8.42.0': + resolution: {integrity: sha512-JnIzu7H3RH5BrKC4NoZqRfmjqCIS1u3hGZltDYJgkVdqAezl4L9d1ZLw+36huCujtSBSAirGINF/S4UxOcR+/g==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: + eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/utils@8.54.0': - resolution: {integrity: sha512-9Cnda8GS57AQakvRyG0PTejJNlA2xhvyNtEVIMlDWOOeEyBkYWhGPnfrIAnqxLMTSTo6q8g12XVjjev5l1NvMA==} + '@typescript-eslint/utils@8.49.0': + resolution: {integrity: sha512-N3W7rJw7Rw+z1tRsHZbK395TWSYvufBXumYtEGzypgMUthlg0/hmCImeA8hgO2d2G4pd7ftpxxul2J8OdtdaFA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/visitor-keys@8.54.0': - resolution: {integrity: sha512-VFlhGSl4opC0bprJiItPQ1RfUhGDIBokcPwaFH4yiBCaNPeld/9VeXbiPO1cLyorQi1G1vL+ecBk1x8o1axORA==} + '@typescript-eslint/visitor-keys@8.42.0': + resolution: {integrity: sha512-3WbiuzoEowaEn8RSnhJBrxSwX8ULYE9CXaPepS2C2W3NSA5NNIvBaslpBSBElPq0UGr0xVJlXFWOAKIkyylydQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/visitor-keys@8.49.0': + resolution: {integrity: sha512-LlKaciDe3GmZFphXIc79THF/YYBugZ7FS1pO581E/edlVVNbZKDy93evqmrfQ9/Y4uN0vVhX4iuchq26mK/iiA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@unrs/resolver-binding-android-arm-eabi@1.11.1': @@ -4605,10 +4848,10 @@ packages: '@vitest/utils@3.2.4': resolution: {integrity: sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==} - '@wagmi/connectors@5.11.2': - resolution: {integrity: sha512-OkiElOI8xXGPDZE5UdG6NgDT3laSkEh9llX1DDapUnfnKecK3Tr/HUf5YzgwDhEoox8mdxp+8ZCjtnTKz56SdA==} + '@wagmi/connectors@5.9.9': + resolution: {integrity: sha512-6+eqU7P2OtxU2PkIw6kHojfYYUJykYG2K5rSkzVh29RDCAjhJqGEZW5f1b8kV5rUBORip1NpST8QTBNi96JHGQ==} peerDependencies: - '@wagmi/core': 2.21.2 + '@wagmi/core': 2.20.3 typescript: '>=5.0.4' viem: 2.x peerDependenciesMeta: @@ -4625,6 +4868,18 @@ packages: typescript: optional: true + '@wagmi/core@2.20.3': + resolution: {integrity: sha512-gsbuHnWxf0AYZISvR8LvF/vUCIq6/ZwT5f5/FKd6wLA7Wq05NihCvmQpIgrcVbpSJPL67wb6S8fXm3eJGJA1vQ==} + peerDependencies: + '@tanstack/query-core': '>=5.0.0' + typescript: '>=5.0.4' + viem: 2.x + peerDependenciesMeta: + '@tanstack/query-core': + optional: true + typescript: + optional: true + '@wagmi/core@2.22.1': resolution: {integrity: sha512-cG/xwQWsBEcKgRTkQVhH29cbpbs/TdcUJVFXCyri3ZknxhMyGv0YEjTcrNpRgt2SaswL1KrvslSNYKKo+5YEAg==} peerDependencies: @@ -4662,6 +4917,7 @@ packages: '@walletconnect/ethereum-provider@2.21.1': resolution: {integrity: sha512-SSlIG6QEVxClgl1s0LMk4xr2wg4eT3Zn/Hb81IocyqNSGfXpjtawWxKxiC5/9Z95f1INyBD6MctJbL/R1oBwIw==} + deprecated: 'Reliability and performance improvements. See: https://github.com/WalletConnect/walletconnect-monorepo/releases' '@walletconnect/events@1.0.1': resolution: {integrity: sha512-NPTqaoi0oPBVNuLv7qPaJazmGHs5JGyO8eEAk5VGKmJzDR7AHzD4k6ilox5kxk1iwiOnFopBOOMLs86Oa76HpQ==} @@ -4706,9 +4962,11 @@ packages: '@walletconnect/sign-client@2.21.0': resolution: {integrity: sha512-z7h+PeLa5Au2R591d/8ZlziE0stJvdzP9jNFzFolf2RG/OiXulgFKum8PrIyXy+Rg2q95U9nRVUF9fWcn78yBA==} + deprecated: 'Reliability and performance improvements. See: https://github.com/WalletConnect/walletconnect-monorepo/releases' '@walletconnect/sign-client@2.21.1': resolution: {integrity: sha512-QaXzmPsMnKGV6tc4UcdnQVNOz4zyXgarvdIQibJ4L3EmLat73r5ZVl4c0cCOcoaV7rgM9Wbphgu5E/7jNcd3Zg==} + deprecated: 'Reliability and performance improvements. See: https://github.com/WalletConnect/walletconnect-monorepo/releases' '@walletconnect/time@1.0.2': resolution: {integrity: sha512-uzdd9woDcJ1AaBZRhqy5rNC9laqWGErfc4dxA9a87mPdKOgWMD85mcFo9dIYIts/Jwocfwn07EC6EzclKubk/g==} @@ -4721,9 +4979,11 @@ packages: '@walletconnect/universal-provider@2.21.0': resolution: {integrity: sha512-mtUQvewt+X0VBQay/xOJBvxsB3Xsm1lTwFjZ6WUwSOTR1X+FNb71hSApnV5kbsdDIpYPXeQUbGt2se1n5E5UBg==} + deprecated: 'Reliability and performance improvements. See: https://github.com/WalletConnect/walletconnect-monorepo/releases' '@walletconnect/universal-provider@2.21.1': resolution: {integrity: sha512-Wjx9G8gUHVMnYfxtasC9poGm8QMiPCpXpbbLFT+iPoQskDDly8BwueWnqKs4Mx2SdIAWAwuXeZ5ojk5qQOxJJg==} + deprecated: 'Reliability and performance improvements. See: https://github.com/WalletConnect/walletconnect-monorepo/releases' '@walletconnect/utils@2.21.0': resolution: {integrity: sha512-zfHLiUoBrQ8rP57HTPXW7rQMnYxYI4gT9yTACxVW6LhIFROTF6/ytm5SKNoIvi4a5nX5dfXG4D9XwQUCu8Ilig==} @@ -4759,6 +5019,17 @@ packages: zod: optional: true + abitype@1.1.0: + resolution: {integrity: sha512-6Vh4HcRxNMLA0puzPjM5GBgT4aAcFGKZzSgAXvuZ27shJP6NEpielTuqbBmZILR5/xd0PizkBGy5hReKz9jl5A==} + peerDependencies: + typescript: '>=5.0.4' + zod: ^3.22.0 || ^4.0.0 + peerDependenciesMeta: + typescript: + optional: true + zod: + optional: true + abitype@1.2.3: resolution: {integrity: sha512-Ofer5QUnuUdTFsBRwARMoWKOH1ND5ehwYhJ3OJ/BQO+StkwQjHw0XyVh4vDttzHB7QOFhPHa/o413PJ82gU/Tg==} peerDependencies: @@ -4770,6 +5041,9 @@ packages: zod: optional: true + abstract-logging@2.0.1: + resolution: {integrity: sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA==} + accepts@1.3.8: resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==} engines: {node: '>= 0.6'} @@ -4821,10 +5095,18 @@ packages: resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} engines: {node: '>=8'} + ansi-regex@6.2.0: + resolution: {integrity: sha512-TKY5pyBkHyADOPYlRT9Lx6F544mPl0vS5Ew7BJ45hA08Q+t3GjbueLliBWN3sMICk6+y7HdyxSzC4bWS8baBdg==} + engines: {node: '>=12'} + ansi-styles@4.3.0: resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} engines: {node: '>=8'} + ansi-styles@6.2.1: + resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==} + engines: {node: '>=12'} + any-promise@1.3.0: resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==} @@ -4913,8 +5195,11 @@ packages: resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} engines: {node: '>= 0.4'} - axe-core@4.11.1: - resolution: {integrity: sha512-BASOg+YwO2C+346x3LZOeoovTIoTrRqEsqMa6fmfAV0P+U9mFr9NsyOEpiYvFjbc64NMrSswhV50WdXzdb/Z5A==} + avvio@9.2.0: + resolution: {integrity: sha512-2t/sy01ArdHHE0vRH5Hsay+RtCZt3dLPji7W7/MMOCEgze5b7SNDC4j5H6FnVgPkI1MTNFGzHdHrVXDDl7QSSQ==} + + axe-core@4.10.3: + resolution: {integrity: sha512-Xm7bpRXnDSX2YE2YFfBk2FnF0ep6tmG7xPh8iHee8MIcrgq762Nkce856dYtJYLkuIoYZvGfTs/PbZhideTcEg==} engines: {node: '>=4'} axios-retry@4.5.0: @@ -4922,6 +5207,9 @@ packages: peerDependencies: axios: 0.x || 1.x + axios@1.11.0: + resolution: {integrity: sha512-1Lx3WLFQWm3ooKDYZD1eXmoGO9fxYQjrycfHFC8P0sCfQVXyROp0p9PFWBehewBOdCwHc+f/b8I0fMto5eSfwA==} + axios@1.13.4: resolution: {integrity: sha512-1wVkUaAO6WyaYtCkcYCOx12ZgpGf9Zif+qXa4n+oYzK558YryKqiL6UWwd5DqiH3VRW0GYhTZQ/vlgJrCoNQlg==} @@ -4929,18 +5217,18 @@ packages: resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==} engines: {node: '>= 0.4'} - babel-plugin-polyfill-corejs2@0.4.15: - resolution: {integrity: sha512-hR3GwrRwHUfYwGfrisXPIDP3JcYfBrW7wKE7+Au6wDYl7fm/ka1NEII6kORzxNU556JjfidZeBsO10kYvtV1aw==} + babel-plugin-polyfill-corejs2@0.4.14: + resolution: {integrity: sha512-Co2Y9wX854ts6U8gAAPXfn0GmAyctHuK8n0Yhfjd6t30g7yvKjspvvOo9yG+z52PZRgFErt7Ka2pYnXCjLKEpg==} peerDependencies: '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 - babel-plugin-polyfill-corejs3@0.14.0: - resolution: {integrity: sha512-AvDcMxJ34W4Wgy4KBIIePQTAOP1Ie2WFwkQp3dB7FQ/f0lI5+nM96zUnYEOE1P9sEg0es5VCP0HxiWu5fUHZAQ==} + babel-plugin-polyfill-corejs3@0.13.0: + resolution: {integrity: sha512-U+GNwMdSFgzVmfhNm8GJUX88AadB3uo9KpJqS3FaqNIPKgySuvMb+bHPsOmmuWyIcuqZj/pzt1RUIUZns4y2+A==} peerDependencies: '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 - babel-plugin-polyfill-regenerator@0.6.6: - resolution: {integrity: sha512-hYm+XLYRMvupxiQzrvXUj7YyvFFVfv5gI0R71AJzudg1g2AI2vyCPPIFEBjk162/wFzti3inBHo7isWFuEVS/A==} + babel-plugin-polyfill-regenerator@0.6.5: + resolution: {integrity: sha512-ISqQ2frbiNU9vIJkzg7dlPpznPZ4jOiUQ1uSmB0fEHeowtN3COYRsXr/xexn64NpU13P06jc/L5TgiJXOgrbEg==} peerDependencies: '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 @@ -4953,25 +5241,38 @@ packages: base-x@5.0.1: resolution: {integrity: sha512-M7uio8Zt++eg3jPj+rHMfCC+IuygQHHCOU+IYsVtik6FWjuYpVt/+MRKcgsAMHh8mMFAwnB+Bs+mTrFiXjMzKg==} + base32.js@0.1.0: + resolution: {integrity: sha512-n3TkB02ixgBOhTvANakDb4xaMXnYUVkNoRFJjQflcqMQhyEKxEHdj3E6N8t8sUQ0mjH/3/JxzlXuz3ul/J90pQ==} + engines: {node: '>=0.12.0'} + base64-js@1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} - baseline-browser-mapping@2.9.19: - resolution: {integrity: sha512-ipDqC8FrAl/76p2SSWKSI+H9tFwm7vYqXQrItCuiVPt26Km0jS+NzSsBWAaBusvSbQcfJG+JitdMm+wZAgTYqg==} - hasBin: true - better-path-resolve@1.0.0: resolution: {integrity: sha512-pbnl5XzGBdrFU/wT4jqmJVPn2B6UHPBOhzMQkY/SPUPB6QtUXtmBHBIwCbXJol93mOpGMnQyP/+BB19q04xj7g==} engines: {node: '>=4'} + bidi-js@1.0.3: + resolution: {integrity: sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==} + big.js@6.2.2: resolution: {integrity: sha512-y/ie+Faknx7sZA5MfGA2xKlu0GDv8RWrXGsmlteyJQ2lvoKv9GBK/fpRMc2qlSoBAgNxrixICFCBefIq8WCQpQ==} + bigint-buffer@1.1.5: + resolution: {integrity: sha512-trfYco6AoZ+rKhKnxA0hgX0HAbVP/s808/EuDSe2JDzUnCp/xAsli35Orvk67UrTEcwuxZqYZDmfA2RXJgxVvA==} + engines: {node: '>= 10.0.0'} + + bignumber.js@9.3.1: + resolution: {integrity: sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==} + + bindings@1.5.0: + resolution: {integrity: sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==} + bn.js@5.2.2: resolution: {integrity: sha512-v2YAxEmKaBLahNwE1mjp4WON6huMNeuDvagFZW+ASCuA/ku0bXR9hSMw0XpiqMoA3+rmnyck/tPRSFQkoC9Cuw==} - body-parser@1.20.4: - resolution: {integrity: sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==} + body-parser@1.20.3: + resolution: {integrity: sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==} engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} body-parser@2.2.2: @@ -4984,8 +5285,8 @@ packages: borsh@0.7.0: resolution: {integrity: sha512-CLCsZGIBCFnPtkNnieW/a8wmreDmfUtjU2m9yHrzPXIlNbqVs0AQrSatSG6vdNYUqdc83tkQi2eHfF98ubzQLA==} - bowser@2.13.1: - resolution: {integrity: sha512-OHawaAbjwx6rqICCKgSG0SAnT05bzd7ppyKLVUITZpANBaaMFBAsaNkto3LoQ31tyFP5kNujE8Cdx85G9VzOkw==} + bowser@2.12.1: + resolution: {integrity: sha512-z4rE2Gxh7tvshQ4hluIT7XcFrgLIQaw9X3A+kTTRdovCz5PMukm/0QC/BKSYPj3omF5Qfypn9O/c5kgpmvYUCw==} brace-expansion@1.1.12: resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==} @@ -4997,8 +5298,8 @@ packages: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} engines: {node: '>=8'} - browserslist@4.28.1: - resolution: {integrity: sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==} + browserslist@4.25.4: + resolution: {integrity: sha512-4jYpcjabC606xJ3kw2QwGEZKX0Aw7sgQdZCvIK9dhVSPh76BKo+C+btT1RRofH7B+8iNpEbgGNVWiLki5q93yg==} engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true @@ -5011,8 +5312,8 @@ packages: buffer@6.0.3: resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==} - bufferutil@4.1.0: - resolution: {integrity: sha512-ZMANVnAixE6AWWnPzlW2KpUrxhm9woycYvPOo67jWHyFowASTEd9s+QN1EIMsSDtwhIxN4sWE1jotpuDUIgyIw==} + bufferutil@4.0.9: + resolution: {integrity: sha512-WDtdLmJvAuNNPzByAYpRo2rF1Mmradw6gvWsQKf63476DDXmomT9zUiGypLcG4ibIM67vhAj8jJRdbmEws2Aqw==} engines: {node: '>=6.14.2'} bundle-require@5.1.0: @@ -5061,8 +5362,8 @@ packages: resolution: {integrity: sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==} engines: {node: '>=10'} - caniuse-lite@1.0.30001767: - resolution: {integrity: sha512-34+zUAMhSH+r+9eKmYG+k2Rpt8XttfE4yXAjoZvkAPs15xcYQhyBYdalJ65BzivAvGRMViEjy6oKr/S91loekQ==} + caniuse-lite@1.0.30001739: + resolution: {integrity: sha512-y+j60d6ulelrNSwpPyrHdl+9mJnQzHBr08xm48Qno0nSk4h3Qojh+ziv2qE6rXf4k3tadF4o1J/1tAbVm1NtnA==} chai@5.3.3: resolution: {integrity: sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==} @@ -5082,18 +5383,14 @@ packages: charenc@0.0.2: resolution: {integrity: sha512-yrLQ/yVUFXkzg7EDQsPieE/53+0RlaWTs+wBrvW36cyilJ2SaDWfl4Yj7MtLTXleV9uEKefbAGUPv2/iWSooRA==} - check-error@2.1.3: - resolution: {integrity: sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==} + check-error@2.1.1: + resolution: {integrity: sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==} engines: {node: '>= 16'} chokidar@4.0.3: resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} engines: {node: '>= 14.16.0'} - chokidar@5.0.0: - resolution: {integrity: sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==} - engines: {node: '>= 20.19.0'} - ci-info@3.9.0: resolution: {integrity: sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==} engines: {node: '>=8'} @@ -5133,6 +5430,10 @@ packages: resolution: {integrity: sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==} engines: {node: '>=18'} + commander@14.0.1: + resolution: {integrity: sha512-2JkV3gUZUVrbNA+1sjBOYLsMZ5cEEl8GTFP2a4AVz5hvasAMCQ1D2l2le/cX+pV4N6ZU17zjUahLpIXRrnWL8A==} + engines: {node: '>=20'} + commander@14.0.2: resolution: {integrity: sha512-TywoWNNRbhoD0BXs1P3ZEScW8W5iKrnbithIl0YH+uCmBd0QpPOA8yc82DS3BIE5Ma6FnBVUsJ7wVUDz4dvOWQ==} engines: {node: '>=20'} @@ -5184,19 +5485,23 @@ packages: cookie-es@1.2.2: resolution: {integrity: sha512-+W7VmiVINB+ywl1HGXJXmrqkOhpKrIiVZV6tQuV54ZyQC7MMuBt81Vc336GMLoHBq5hV/F9eXgt5Mnx0Rha5Fg==} - cookie-signature@1.0.7: - resolution: {integrity: sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==} + cookie-signature@1.0.6: + resolution: {integrity: sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==} cookie-signature@1.2.2: resolution: {integrity: sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==} engines: {node: '>=6.6.0'} - cookie@0.7.2: - resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} + cookie@0.7.1: + resolution: {integrity: sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==} engines: {node: '>= 0.6'} - core-js-compat@3.48.0: - resolution: {integrity: sha512-OM4cAF3D6VtH/WkLtWvyNC56EZVXsZdU3iqaMG2B4WvYrlqU831pc4UtG5yp0sE9z8Y02wVN7PjW5Zf9Gt0f1Q==} + cookie@1.1.1: + resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==} + engines: {node: '>=18'} + + core-js-compat@3.45.1: + resolution: {integrity: sha512-tqTt5T4PzsMIZ430XGviK4vzYSoeNJ6CXODi6c/voxOT6IZqBht5/EKaSNnYiEjjRYxjVz7DQIsOsY0XNi8PIA==} core-util-is@1.0.3: resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} @@ -5262,8 +5567,8 @@ packages: resolution: {integrity: sha512-legscpSpgSAeGEe0TNcai97DKt9Vd9AsAdOL7Uoetb52Ar/8eJm3LIa39qpv8wWzLFlNG4vVvppQM+teaMPj3A==} engines: {node: '>=20'} - csstype@3.2.3: - resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + csstype@3.1.3: + resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} damerau-levenshtein@1.0.8: resolution: {integrity: sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==} @@ -5272,6 +5577,10 @@ packages: resolution: {integrity: sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==} engines: {node: '>=18'} + data-urls@6.0.0: + resolution: {integrity: sha512-BnBS08aLUM+DKamupXs3w2tJJoqU+AkaE/+6vQxi/G/DPmIZFJJp9Dkb1kM03AZx8ADehDUZgsNxju3mPXZYIA==} + engines: {node: '>=20'} + data-view-buffer@1.0.2: resolution: {integrity: sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==} engines: {node: '>= 0.4'} @@ -5319,6 +5628,15 @@ packages: supports-color: optional: true + debug@4.3.7: + resolution: {integrity: sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + debug@4.4.1: resolution: {integrity: sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==} engines: {node: '>=6.0'} @@ -5390,6 +5708,10 @@ packages: resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} engines: {node: '>= 0.8'} + dequal@2.0.3: + resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} + engines: {node: '>=6'} + derive-valtio@0.1.0: resolution: {integrity: sha512-OCg2UsLbXK7GmmpzMXhYkdO64vhJ1ROUUGaTFyHjVwEdMEcTTRj7W1TxLbSBxdY8QLBPCcp66MTyaSy0RpO17A==} peerDependencies: @@ -5451,15 +5773,18 @@ packages: duplexify@4.1.3: resolution: {integrity: sha512-M3BmBhwJRZsSx38lZyhE53Csddgzl5R7xGJNk7CVddZD6CcmwMCH8J+7AprIrQKH7TonKxaCjcv27Qmf+sQ+oA==} - eciesjs@0.4.17: - resolution: {integrity: sha512-TOOURki4G7sD1wDCjj7NfLaXZZ49dFOeEb5y39IXpb8p0hRzVvfvzZHOi5JcT+PpyAbi/Y+lxPb8eTag2WYH8w==} + eastasianwidth@0.2.0: + resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} + + eciesjs@0.4.15: + resolution: {integrity: sha512-r6kEJXDKecVOCj2nLMuXK/FCPeurW33+3JRpfXVbjLja3XUYFfD9I/JBreH6sUyzcm3G/YQboBjMla6poKeSdA==} engines: {bun: '>=1', deno: '>=2', node: '>=16'} ee-first@1.1.1: resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} - electron-to-chromium@1.5.283: - resolution: {integrity: sha512-3vifjt1HgrGW/h76UEeny+adYApveS9dH2h3p57JYzBSXJIKUJAvtmIytDKjcSCt9xHfrNCFJ7gts6vkhuq++w==} + electron-to-chromium@1.5.214: + resolution: {integrity: sha512-TpvUNdha+X3ybfU78NoQatKvQEm1oq3lf2QbnmCEdw+Bd9RuIAY+hJTvq1avzHM0f7EJfnH3vbCnbzKzisc/9Q==} emoji-regex@8.0.0: resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} @@ -5470,6 +5795,10 @@ packages: encode-utf8@1.0.3: resolution: {integrity: sha512-ucAnuBEhUK4boH2HjVYG5Q2mQyPorvv0u/ocS+zhdw0S8AlHYY+GOFhP1Gio5z4icpP2ivFSvhtFjQi8+T9ppw==} + encodeurl@1.0.2: + resolution: {integrity: sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==} + engines: {node: '>= 0.8'} + encodeurl@2.0.0: resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} engines: {node: '>= 0.8'} @@ -5477,15 +5806,15 @@ packages: end-of-stream@1.4.5: resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==} - engine.io-client@6.6.4: - resolution: {integrity: sha512-+kjUJnZGwzewFDw951CDWcwj35vMNf2fcj7xQWOctq1F2i1jkDdVvdFG9kM/BEChymCH36KgjnW0NsL58JYRxw==} + engine.io-client@6.6.3: + resolution: {integrity: sha512-T0iLjnyNWahNyv/lcjS2y4oE358tVS/SYQNxYXGAJ9/GLgH4VCvOQ/mhTjqU88mLZCQgiG8RIegFHYCdVC+j5w==} engine.io-parser@5.2.3: resolution: {integrity: sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==} engines: {node: '>=10.0.0'} - enhanced-resolve@5.18.4: - resolution: {integrity: sha512-LgQMM4WXU3QI+SYgEc2liRgznaD5ojbmY3sb8LxyguVkIg5FxdpTkvk72te2R38/TGKxH634oLxXRGY6d7AP+Q==} + enhanced-resolve@5.18.3: + resolution: {integrity: sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==} engines: {node: '>=10.13.0'} enquirer@2.4.1: @@ -5500,11 +5829,11 @@ packages: resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==} engines: {node: '>=0.12'} - error-ex@1.3.4: - resolution: {integrity: sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==} + error-ex@1.3.2: + resolution: {integrity: sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==} - es-abstract@1.24.1: - resolution: {integrity: sha512-zHXBLhP+QehSSbsS9Pt23Gg964240DPd6QCf8WpkqEXxQ7fhdZzYsocOr5u7apWonsS5EjZDmTF+/slGMyasvw==} + es-abstract@1.24.0: + resolution: {integrity: sha512-WSzPgsdLtTcQwm4CROfS5ju2Wa1QQcVeT37jFjYzdFz1r9ahadC8B8/a4qxJxM+09F18iumCdRmlr96ZYkQvEg==} engines: {node: '>= 0.4'} es-define-property@1.0.1: @@ -5515,8 +5844,8 @@ packages: resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} engines: {node: '>= 0.4'} - es-iterator-helpers@1.2.2: - resolution: {integrity: sha512-BrUQ0cPTB/IwXj23HtwHjS9n7O4h9FX94b4xc5zlTHxeLgTAdzYUDyy6KdExAl9lbN5rtfe44xpjpmj9grxs5w==} + es-iterator-helpers@1.2.1: + resolution: {integrity: sha512-uDn+FE1yrDzyC0pCo961B2IHbdM8y/ACZsKD4dG6WqrjV53BADjwa7D+1aom2rsNVfLyDgU/eigvlJGJ08OQ4w==} engines: {node: '>= 0.4'} es-module-lexer@1.7.0: @@ -5547,13 +5876,8 @@ packages: es6-promisify@5.0.0: resolution: {integrity: sha512-C+d6UdsYDk0lMebHNR4S2NybQMMngAOnOwYBQjTOiv0MkoJMP0Myw2mgpDLBcpfCmRLxyFqYhS/CfOENq4SJhQ==} - esbuild@0.25.12: - resolution: {integrity: sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==} - engines: {node: '>=18'} - hasBin: true - - esbuild@0.27.2: - resolution: {integrity: sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==} + esbuild@0.25.9: + resolution: {integrity: sha512-CRbODhYyQx3qp7ZEwzxOk4JBqmD/seJrzPa/cGjY1VtIn5E09Oi9/dB4JwctnfZ8Q8iT7rioVv5k/FNT/uf54g==} engines: {node: '>=18'} hasBin: true @@ -5636,8 +5960,8 @@ packages: peerDependencies: eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9 - eslint-plugin-prettier@5.5.5: - resolution: {integrity: sha512-hscXkbqUZ2sPithAuLm5MXL+Wph+U7wHngPBv9OMWwlP8iaflyxpjTYZkmdgB4/vPIhemRlBEoLrH7UC1n7aUw==} + eslint-plugin-prettier@5.5.4: + resolution: {integrity: sha512-swNtI95SToIz05YINMA6Ox5R057IMAmWZ26GqPxusAp1TZzj+IdY9tXNWWD3vkF/wEqydCONcwjTFpxybBqZsg==} engines: {node: ^14.18.0 || >=16.0.0} peerDependencies: '@types/eslint': '>=8.0.0' @@ -5674,8 +5998,8 @@ packages: resolution: {integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - eslint@9.39.2: - resolution: {integrity: sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==} + eslint@9.34.0: + resolution: {integrity: sha512-RNCHRX5EwdrESy3Jc9o8ie8Bog+PeYvvSR8sDGoZxNFTvZ4dlxUB3WzQ3bQMztFrSRODGrLLj8g6OFuGY/aiQg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} hasBin: true peerDependencies: @@ -5693,8 +6017,8 @@ packages: engines: {node: '>=4'} hasBin: true - esquery@1.7.0: - resolution: {integrity: sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==} + esquery@1.6.0: + resolution: {integrity: sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==} engines: {node: '>=0.10'} esrecurse@4.3.0: @@ -5743,9 +6067,6 @@ packages: eventemitter3@5.0.1: resolution: {integrity: sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==} - eventemitter3@5.0.4: - resolution: {integrity: sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==} - events@3.3.0: resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} engines: {node: '>=0.8.x'} @@ -5754,22 +6075,26 @@ packages: resolution: {integrity: sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==} engines: {node: '>=18.0.0'} + eventsource@2.0.2: + resolution: {integrity: sha512-IzUmBGPR3+oUG9dUeXynyNmf91/3zUSJg1lCktzKw47OXuhco54U3r9B7O4XX+Rb1Itm9OZ2b0RkTs10bICOxA==} + engines: {node: '>=12.0.0'} + eventsource@3.0.7: resolution: {integrity: sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==} engines: {node: '>=18.0.0'} - expect-type@1.3.0: - resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==} + expect-type@1.2.2: + resolution: {integrity: sha512-JhFGDVJ7tmDJItKhYgJCGLOWjuK9vPxiXoUFLwLDc99NlmklilbiQJwoctZtt13+xMw91MCk/REan6MWHqDjyA==} engines: {node: '>=12.0.0'} - express-rate-limit@7.5.1: - resolution: {integrity: sha512-7iN8iPMDzOMHPUYllBEsQdWVB6fPDMPqwjBaFrgr4Jgr/+okjvzAy+UHlYYL/Vs0OsOrMkwS6PJDkFlJwoxUnw==} + express-rate-limit@8.2.1: + resolution: {integrity: sha512-PCZEIEIxqwhzw4KF0n7QF4QqruVTcF73O5kFKUnGOyjbCCgizBBiFaYpd/fnBLUMPw/BWw9OsiN7GgrNYr7j6g==} engines: {node: '>= 16'} peerDependencies: express: '>= 4.11' - express@4.22.1: - resolution: {integrity: sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==} + express@4.21.2: + resolution: {integrity: sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==} engines: {node: '>= 0.10.0'} express@5.2.1: @@ -5787,6 +6112,9 @@ packages: resolution: {integrity: sha512-GipyPsXO1anza0AOZdy69Im7hGFCNB7Y/NGjDlZGJ3GJJLtwNSb2vrzYrTYJRrRloVx7pl+bhUaTB8yiccPvFQ==} engines: {node: '> 0.1.90'} + fast-decode-uri-component@1.0.1: + resolution: {integrity: sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg==} + fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} @@ -5804,9 +6132,15 @@ packages: fast-json-stable-stringify@2.1.0: resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + fast-json-stringify@6.3.0: + resolution: {integrity: sha512-oRCntNDY/329HJPlmdNLIdogNtt6Vyjb1WuT01Soss3slIdyUp8kAcDU3saQTOquEK8KFVfwIIF7FebxUAu+yA==} + fast-levenshtein@2.0.6: resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + fast-querystring@1.1.2: + resolution: {integrity: sha512-g6KuKWmFXc0fID8WWH0jit4g0AGBoJhCkJMb1RmbsSEUNvQ+ZC8D6CUZ+GtF8nMzSPXnhiePyyqqipzNNEnHjg==} + fast-redact@3.5.0: resolution: {integrity: sha512-dwsoQlS7h9hMeYUq1W++23NDcBLV4KqONnITDV9DjfS3q1SgDGVrBdvvTLUotWtPSD7asWDV9/CmsZPy8Hf70A==} engines: {node: '>=6'} @@ -5823,8 +6157,11 @@ packages: fastestsmallesttextencoderdecoder@1.0.22: resolution: {integrity: sha512-Pb8d48e+oIuY4MaM64Cd7OW1gt4nxCHs7/ddPPZ/Ic3sg8yVGM7O9wDvZ7us6ScaUupzM+pfBolwtYhN1IxBIw==} - fastq@1.20.1: - resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==} + fastify@5.8.2: + resolution: {integrity: sha512-lZmt3navvZG915IE+f7/TIVamxIwmBd+OMB+O9WBzcpIwOo6F0LTh0sluoMFk5VkrKTvvrwIaoJPkir4Z+jtAg==} + + fastq@1.19.1: + resolution: {integrity: sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==} fdir@6.5.0: resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} @@ -5835,10 +6172,16 @@ packages: picomatch: optional: true + feaxios@0.0.23: + resolution: {integrity: sha512-eghR0A21fvbkcQBgZuMfQhrXxJzC0GNUGC9fXhBge33D+mFDTwl0aJ35zoQQn575BhyjQitRc5N4f+L4cP708g==} + file-entry-cache@8.0.0: resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} engines: {node: '>=16.0.0'} + file-uri-to-path@1.0.0: + resolution: {integrity: sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==} + fill-range@7.1.1: resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} engines: {node: '>=8'} @@ -5847,14 +6190,18 @@ packages: resolution: {integrity: sha512-8rXg1ZnX7xzy2NGDVkBVaAy+lSlPNwad13BtgSlLuxfIslyt5Vg64U7tFcCt4WS1R0hvtnQybT/IyCkGZ3DpXQ==} engines: {node: '>=0.10.0'} - finalhandler@1.3.2: - resolution: {integrity: sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==} + finalhandler@1.3.1: + resolution: {integrity: sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==} engines: {node: '>= 0.8'} finalhandler@2.1.1: resolution: {integrity: sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==} engines: {node: '>= 18.0.0'} + find-my-way@9.5.0: + resolution: {integrity: sha512-VW2RfnmscZO5KgBY5XVyKREMW5nMZcxDy+buTOsL+zIPnBlbKm+00sgzoQzq1EVh4aALZLfKdwv6atBGcjvjrQ==} + engines: {node: '>=20'} + find-up@4.1.0: resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} engines: {node: '>=8'} @@ -5886,8 +6233,12 @@ packages: resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==} engines: {node: '>= 0.4'} - form-data@4.0.5: - resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==} + foreground-child@3.3.1: + resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} + engines: {node: '>=14'} + + form-data@4.0.4: + resolution: {integrity: sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==} engines: {node: '>= 6'} forwarded@0.2.0: @@ -5939,10 +6290,6 @@ packages: functions-have-names@1.2.3: resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==} - generator-function@2.0.1: - resolution: {integrity: sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==} - engines: {node: '>= 0.4'} - gensync@1.0.0-beta.2: resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} engines: {node: '>=6.9.0'} @@ -5967,8 +6314,8 @@ packages: resolution: {integrity: sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==} engines: {node: '>= 0.4'} - get-tsconfig@4.13.1: - resolution: {integrity: sha512-EoY1N2xCn44xU6750Sx7OjOIT59FkmstNc3X6y5xpz7D5cBtZRe/3pSlTkDJgqsOk3WwZPkWfonhhUJfttQo3w==} + get-tsconfig@4.10.1: + resolution: {integrity: sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ==} glob-parent@5.1.2: resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} @@ -5978,6 +6325,11 @@ packages: resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} engines: {node: '>=10.13.0'} + glob@10.4.5: + resolution: {integrity: sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==} + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me + hasBin: true + globals@14.0.0: resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} engines: {node: '>=18'} @@ -6008,17 +6360,20 @@ packages: graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + graphemer@1.4.0: + resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} + graphql-request@6.1.0: resolution: {integrity: sha512-p+XPfS4q7aIpKVcgmnZKhMNqhltk20hfXtkaIkTfjjmiKMJ5xrt5c743cL03y/K7y1rg3WrIC49xGiEQ4mxdNw==} peerDependencies: graphql: 14 - 16 - graphql@16.12.0: - resolution: {integrity: sha512-DKKrynuQRne0PNpEbzuEdHlYOMksHSUI8Zc9Unei5gTsMNA2/vMpoMz/yKba50pejK56qj98qM0SjYxAKi13gQ==} + graphql@16.11.0: + resolution: {integrity: sha512-mS1lbMsxgQj6hge1XZ6p7GPhbrtFwUFYi3wRzXAC/FmYnyXMTvvI3td3rjmQ2u8ewXueaSvRPWaEcgVVOT9Jnw==} engines: {node: ^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0} - h3@1.15.5: - resolution: {integrity: sha512-xEyq3rSl+dhGX2Lm0+eFQIAzlDN6Fs0EcC4f7BNUmzaRX/PTzeuM+Tr2lHB8FoXggsQIeXLj8EDVgs5ywxyxmg==} + h3@1.15.4: + resolution: {integrity: sha512-z5cFQWDffyOe4vQ9xIqNfCZdV4p//vy6fBnr8Q1AWnVZ0teurKMG66rLj++TKwKPUP3u7iMUvrvKaEUiQw2QWQ==} has-bigints@1.1.0: resolution: {integrity: sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==} @@ -6053,6 +6408,10 @@ packages: hermes-parser@0.25.1: resolution: {integrity: sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==} + hono@4.10.7: + resolution: {integrity: sha512-icXIITfw/07Q88nLSkB9aiUrd8rYzSweK681Kjo/TSggaGbOX4RRyxxm71v+3PC8C/j+4rlxGeoTRxQDkaJkUw==} + engines: {node: '>=16.9.0'} + hono@4.11.7: resolution: {integrity: sha512-l7qMiNee7t82bH3SeyUCt9UF15EVmaBvsppY2zQtrbIhl/yzBTny+YUxsVjSjQ6gaqaeVtZmGocom8TzBlA4Yw==} engines: {node: '>=16.9.0'} @@ -6061,9 +6420,17 @@ packages: resolution: {integrity: sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==} engines: {node: '>=18'} + html-encoding-sniffer@6.0.0: + resolution: {integrity: sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + http-cache-semantics@4.2.0: resolution: {integrity: sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==} + http-errors@2.0.0: + resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==} + engines: {node: '>= 0.8'} + http-errors@2.0.1: resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==} engines: {node: '>= 0.8'} @@ -6131,10 +6498,18 @@ packages: resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==} engines: {node: '>= 0.4'} + ip-address@10.0.1: + resolution: {integrity: sha512-NWv9YLW4PoW2B7xtzaS3NCot75m6nK7Icdv0o3lfMceJVRfSoQwqD4wEH5rLwoKJwUiZ/rfpiVBhnaF0FK4HoA==} + engines: {node: '>= 12'} + ipaddr.js@1.9.1: resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} engines: {node: '>= 0.10'} + ipaddr.js@2.3.0: + resolution: {integrity: sha512-Zv/pA+ciVFbCSBBjGfaKUya/CcGmUHzTydLMaTwrUUEM2DIEO3iZvueGxmacvmN50fGpGVKeTXpb2LcYQxeVdg==} + engines: {node: '>= 10'} + iron-webcrypto@1.2.1: resolution: {integrity: sha512-feOM6FaSr6rEABp/eDfVseKyTMDt+KGpeB35SkVn9Tyn0CqvVsY3EwI0v5i8nMHyJnzCIQf7nsy3p41TPkJZhg==} @@ -6195,8 +6570,8 @@ packages: resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} engines: {node: '>=8'} - is-generator-function@1.1.2: - resolution: {integrity: sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==} + is-generator-function@1.1.0: + resolution: {integrity: sha512-nPUB5km40q9e8UfN/Zc24eLlzdSf9OfKByBw9CIdw4H1giPMeA0OIJvbchsCu4npfI2QcMVBsGEBHKZ7wLTWmQ==} engines: {node: '>= 0.4'} is-glob@4.0.3: @@ -6233,6 +6608,10 @@ packages: resolution: {integrity: sha512-XVm7LOeLpTW4jV19QSH38vkswxoLud8sQ57YwJVTPWdiaI9I8keEhGFpBlslyVsgdQy4Opg8QOLb8YRgsyZiQg==} engines: {node: '>=10'} + is-retry-allowed@3.0.0: + resolution: {integrity: sha512-9xH0xvoggby+u0uGF7cZXdrutWiBiaFG8ZT4YFPXL8NzkyAwX3AKGLeFQLvzDpM430+nDFBZ1LHkie/8ocL06A==} + engines: {node: '>=12'} + is-set@2.0.3: resolution: {integrity: sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==} engines: {node: '>= 0.4'} @@ -6305,8 +6684,11 @@ packages: resolution: {integrity: sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==} engines: {node: '>= 0.4'} - jayson@4.3.0: - resolution: {integrity: sha512-AauzHcUcqs8OBnCHOkJY280VaTiCm57AbuO7lqzcw7JapGj50BisE3xhksye4zlTSR1+1tAz67wLTl8tEH1obQ==} + jackspeak@3.4.3: + resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==} + + jayson@4.2.0: + resolution: {integrity: sha512-VfJ9t1YLwacIubLhONk0KFeosUBwstRWQ0IRT1KDjEjnVnSOVHC3uwugyV7L0c7R9lpVyrUGT2XWiBA1UTtpyg==} engines: {node: '>=8'} hasBin: true @@ -6317,6 +6699,9 @@ packages: jose@5.10.0: resolution: {integrity: sha512-s+3Al/p9g32Iq+oqXxkW//7jk2Vig6FF1CFqzVXoTUXt2qz89YWbL+OwS17NFYEvxC35n0FKeGO2LGYSxeM2Gg==} + jose@6.1.0: + resolution: {integrity: sha512-TTQJyoEoKcC1lscpVDCSsVgYzUDg/0Bt3WE//WiTPK6uOCQC2KZS4MpugbMWt/zyjkopgZoXhZuCi00gLudfUA==} + jose@6.1.3: resolution: {integrity: sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ==} @@ -6337,6 +6722,10 @@ packages: resolution: {integrity: sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==} hasBin: true + js-yaml@4.1.0: + resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} + hasBin: true + js-yaml@4.1.1: resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} hasBin: true @@ -6354,6 +6743,20 @@ packages: canvas: optional: true + jsdom@27.4.0: + resolution: {integrity: sha512-mjzqwWRD9Y1J1KUi7W97Gja1bwOOM5Ug0EZ6UDK3xS7j7mndrkwozHtSblfomlzyB4NepioNt+B2sOSzczVgtQ==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + peerDependencies: + canvas: ^3.0.0 + peerDependenciesMeta: + canvas: + optional: true + + jsesc@3.0.2: + resolution: {integrity: sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==} + engines: {node: '>=6'} + hasBin: true + jsesc@3.1.0: resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} engines: {node: '>=6'} @@ -6372,6 +6775,9 @@ packages: json-rpc-random-id@1.0.1: resolution: {integrity: sha512-RJ9YYNCkhVDBuP4zN5BBtYAzEl03yq/jIIsyif0JY9qyJuQQZNeDK7anAPKKlyEtLSj2s8h6hNh2F8zO5q7ScA==} + json-schema-ref-resolver@3.0.0: + resolution: {integrity: sha512-hOrZIVL5jyYFjzk7+y7n5JDzGlU8rfWDuYyHwGa2WA8/pcmMHezp2xsVwxrebD/Q9t8Nc5DboieySDpCp4WG4A==} + json-schema-traverse@0.4.1: resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} @@ -6428,6 +6834,9 @@ packages: resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} engines: {node: '>= 0.8.0'} + light-my-request@6.6.0: + resolution: {integrity: sha512-CHYbu8RtboSIoVsHZ6Ye4cj4Aw/yg2oAFimlF7mNvfDV192LR7nDiKtSIfCuLT7KokPSTn/9kfVLm5OGN0A28A==} + lightningcss-android-arm64@1.30.2: resolution: {integrity: sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A==} engines: {node: '>= 12.0.0'} @@ -6505,11 +6914,11 @@ packages: lines-and-columns@1.2.4: resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} - lit-element@4.2.2: - resolution: {integrity: sha512-aFKhNToWxoyhkNDmWZwEva2SlQia+jfG0fjIWV//YeTaWrVnOxD89dPKfigCUspXFmjzOEUQpOkejH5Ly6sG0w==} + lit-element@4.2.1: + resolution: {integrity: sha512-WGAWRGzirAgyphK2urmYOV72tlvnxw7YfyLDgQ+OZnM9vQQBQnumQ7jUJe6unEzwGU3ahFOjuz1iz1jjrpCPuw==} - lit-html@3.3.2: - resolution: {integrity: sha512-Qy9hU88zcmaxBXcc10ZpdK7cOLXvXpRoBxERdtqV9QOrfpMZZ6pSYP91LhpPtap3sFMUiL7Tw2RImbe0Al2/kw==} + lit-html@3.3.1: + resolution: {integrity: sha512-S9hbyDu/vs1qNrithiNyeyv64c9yqiW9l+DBgI18fL+MTvOtWoFR0FWiyq1TxaYef5wNlpEmzlXoBlZEO+WjoA==} lit@3.3.0: resolution: {integrity: sha512-DGVsqsOIHBww2DqnuZzW7QsuCdahp50ojuDaBPC7jUDRpYoH0z7kHBBYZewRzer75FwtrkmkKk7iOAwSaWdBmw==} @@ -6532,11 +6941,14 @@ packages: lodash.merge@4.6.2: resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + lodash.sortby@4.7.0: + resolution: {integrity: sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==} + lodash.startcase@4.4.0: resolution: {integrity: sha512-+WKqsK294HMSc2jEbNgpHpd0JfIBhp7rEV4aqXWqFr6AlXov+SlcgB1Fv01y2kGe3Gc8nMW7VA0SrGuSkRfIEg==} - lodash@4.17.23: - resolution: {integrity: sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==} + lodash@4.17.21: + resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} loose-envify@1.4.0: resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} @@ -6561,13 +6973,19 @@ packages: resolution: {integrity: sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==} engines: {node: '>=8'} - lru-cache@11.2.5: - resolution: {integrity: sha512-vFrFJkWtJvJnD5hg+hJvVE8Lh/TcMzKnTgCWmtBipwI5yLX/iX+5UB2tfuyODF5E7k9xEzMdYgGqaSb1c0c5Yw==} + lru-cache@10.4.3: + resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} + + lru-cache@11.2.4: + resolution: {integrity: sha512-B5Y16Jr9LB9dHVkh6ZevG+vAbOsNOYCX+sXvFWFu7B3Iz5mijW3zdbMyhsh8ANd2mSWBYdJgnqi+mL7/LrOPYg==} engines: {node: 20 || >=22} lru-cache@5.1.1: resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + magic-string@0.30.18: + resolution: {integrity: sha512-yi8swmWbO17qHhwIBNeeZxTceJMeBvWJaId6dyvTSOwTipqeHhMhOrz6513r1sOKnpvQ7zkhlG8tPrpilwTxHQ==} + magic-string@0.30.21: resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} @@ -6656,6 +7074,10 @@ packages: minimist@1.2.8: resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + minipass@7.1.2: + resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} + engines: {node: '>=16 || 14 >=14.17'} + mipd@0.0.7: resolution: {integrity: sha512-aAPZPNDQ3uMTdKbuO2YmAw2TxLHO0moa4YKAyETM/DTj5FloZo+a+8tU+iv4GmW+sOxKLSRwcSFuczk+Cpt6fg==} peerDependencies: @@ -6711,8 +7133,8 @@ packages: engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true - napi-postinstall@0.3.4: - resolution: {integrity: sha512-PHI5f1O0EP5xJ9gQmFGMS6IZcrVvTjpXjz7Na41gTE7eE2hK11lg04CECCYEEjdc17EV4DO+fkGEtt7TpTaTiQ==} + napi-postinstall@0.3.3: + resolution: {integrity: sha512-uTp172LLXSxuSYHv/kou+f6KW3SMppU9ivthaVTXian9sOt3XM/zHYHpRZiLgQoxeWfYUnslNWQHF1+G71xcow==} engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} hasBin: true @@ -6727,8 +7149,8 @@ packages: resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==} engines: {node: '>= 0.6'} - next@16.1.6: - resolution: {integrity: sha512-hkyRkcu5x/41KoqnROkfTm2pZVbKxvbZRuNvKXLRXxs3VfyO0WhY50TQS40EuKO9SW3rBj/sF3WbVwDACeMZyw==} + next@16.0.10: + resolution: {integrity: sha512-RtWh5PUgI+vxlV3HdR+IfWA1UUHu0+Ram/JBO4vWB54cVPentCD0e+lxyAYEsDTqGGMg7qpjhKh6dc6aW7W/sA==} engines: {node: '>=20.9.0'} hasBin: true peerDependencies: @@ -6770,11 +7192,11 @@ packages: resolution: {integrity: sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==} hasBin: true - node-mock-http@1.0.4: - resolution: {integrity: sha512-8DY+kFsDkNXy1sJglUfuODx1/opAGJGyrTuFqEoN90oRc2Vk0ZbD4K2qmKXBBEhZQzdKHIVfEJpDU8Ak2NJEvQ==} + node-mock-http@1.0.2: + resolution: {integrity: sha512-zWaamgDUdo9SSLw47we78+zYw/bDr5gH8pH7oRRs8V3KmBtu8GLgGIbV2p/gRPd3LWpEOpjQj7X1FOU3VFMJ8g==} - node-releases@2.0.27: - resolution: {integrity: sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==} + node-releases@2.0.19: + resolution: {integrity: sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==} normalize-path@3.0.0: resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} @@ -6787,8 +7209,8 @@ packages: nth-check@2.1.1: resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==} - nwsapi@2.2.23: - resolution: {integrity: sha512-7wfH4sLbt4M0gCDzGE6vzQBo0bfTKjU7Sfpqy/7gs1qBfYz2vEJH6vXcBKpO3+6Yu1telwd0t9HpyOoLEQQbIQ==} + nwsapi@2.2.21: + resolution: {integrity: sha512-o6nIY3qwiSXl7/LuOU0Dmuctd34Yay0yeuZRLFmDPrrdHpXKFndPj3hM+YEPVHYC5fx2otBx4Ilc/gyYSAUaIA==} obj-multiplex@1.0.0: resolution: {integrity: sha512-0GNJAOsHoBHeNTvl5Vt6IWnpUEcc3uSRxzBri7EDyIcMgYvnY2JL2qdeV5zTMjWQX5OHcD5amcW2HFfDh0gjIA==} @@ -6825,12 +7247,16 @@ packages: resolution: {integrity: sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==} engines: {node: '>= 0.4'} - ofetch@1.5.1: - resolution: {integrity: sha512-2W4oUZlVaqAPAil6FUg/difl6YhqhUR7x2eZY4bQCko22UXg3hptq9KLQdqFClV+Wu85UX7hNtdGTngi/1BxcA==} + ofetch@1.4.1: + resolution: {integrity: sha512-QZj2DfGplQAr2oj9KzceK9Hwz6Whxazmn85yYeVuS3u9XTMOGMRx0kO95MQ+vLsj/S/NwBDMMLU5hpxvI6Tklw==} on-exit-leak-free@0.2.0: resolution: {integrity: sha512-dqaz3u44QbRXQooZLTUKU41ZrzYrcvLISVgbrzbyCMxpmSLJvZ3ZamIJIZ29P6OhZIkNIQKosdeM6t1LYbA9hg==} + on-exit-leak-free@2.1.2: + resolution: {integrity: sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==} + engines: {node: '>=14.0.0'} + on-finished@2.4.1: resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} engines: {node: '>= 0.8'} @@ -6895,6 +7321,22 @@ packages: typescript: optional: true + ox@0.9.3: + resolution: {integrity: sha512-KzyJP+fPV4uhuuqrTZyok4DC7vFzi7HLUFiUNEmpbyh59htKWkOC98IONC1zgXJPbHAhQgqs6B0Z6StCGhmQvg==} + peerDependencies: + typescript: '>=5.4.0' + peerDependenciesMeta: + typescript: + optional: true + + ox@0.9.6: + resolution: {integrity: sha512-8SuCbHPvv2eZLYXrNmC0EC12rdzXQLdhnOMlHDW2wiCPLxBrOOJwX5L5E61by+UjTPOryqQiRSnjIKCI+GykKg==} + peerDependencies: + typescript: '>=5.4.0' + peerDependenciesMeta: + typescript: + optional: true + p-cancelable@2.1.1: resolution: {integrity: sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==} engines: {node: '>=8'} @@ -6927,6 +7369,9 @@ packages: resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} engines: {node: '>=6'} + package-json-from-dist@1.0.1: + resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} + package-manager-detector@0.2.11: resolution: {integrity: sha512-BEnLolu+yuz22S56CU1SUKq3XC3PkwD5wv4ikR4MfGvnRVcmzXR9DwSlW2fEamyTPyXHomBJRzgapeuBvRNzJQ==} @@ -6947,6 +7392,9 @@ packages: parse5@7.3.0: resolution: {integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==} + parse5@8.0.0: + resolution: {integrity: sha512-9m4m5GSgXjL4AjumKzq1Fgfp3Z8rsvjRNbnkVwfu2ImRqE5D0LnY2QfDen18FSY9C573YU5XxSapdHZTZ2WolA==} + parseurl@1.3.3: resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} engines: {node: '>= 0.8'} @@ -6962,6 +7410,10 @@ packages: path-parse@1.0.7: resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + path-scurry@1.11.1: + resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} + engines: {node: '>=16 || 14 >=14.18'} + path-to-regexp@0.1.12: resolution: {integrity: sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==} @@ -7005,9 +7457,19 @@ packages: pino-abstract-transport@0.5.0: resolution: {integrity: sha512-+KAgmVeqXYbTtU2FScx1XS3kNyfZ5TrXY07V96QnUSFqo2gAqlvmaxH67Lj7SWazqsMabf+58ctdTcBgnOLUOQ==} + pino-abstract-transport@3.0.0: + resolution: {integrity: sha512-wlfUczU+n7Hy/Ha5j9a/gZNy7We5+cXp8YL+X+PG8S0KXxw7n/JXA3c46Y0zQznIJ83URJiwy7Lh56WLokNuxg==} + pino-std-serializers@4.0.0: resolution: {integrity: sha512-cK0pekc1Kjy5w9V2/n+8MkZwusa6EyyxfeQCB799CQRhRt/CqYKiWs5adeu8Shve2ZNffvfC/7J64A2PJo1W/Q==} + pino-std-serializers@7.1.0: + resolution: {integrity: sha512-BndPH67/JxGExRgiX1dX0w1FvZck5Wa4aal9198SrRhZjH3GxKQUKIBnYJTdj2HDN3UQAS06HlfcSbQj2OHmaw==} + + pino@10.3.1: + resolution: {integrity: sha512-r34yH/GlQpKZbU1BvFFqOjhISRo1MNx1tWYsYvmj6KIRHSPMT2+yHOEb1SG6NMvRoHRF0a07kCOox/9yakl1vg==} + hasBin: true + pino@7.11.0: resolution: {integrity: sha512-dMACeu63HtRLmCG8VKdy4cShCPKaYDR4youZqoSWLxl5Gu99HUw8bw75thbPv9Nip+H+QYX8o3ZJbTdVZZ2TVg==} hasBin: true @@ -7031,26 +7493,6 @@ packages: resolution: {integrity: sha512-M7LhCsdNbNgiLYiP4WjsfLUuFmCfnjdF6jKe2R9NKl4WFN+HZPGHJZ9lnLP7f9ZnKe3U9nuWD0szirmj+migUg==} engines: {node: '>=12.0.0'} - porto@0.2.19: - resolution: {integrity: sha512-q1vEJgdtlEOf6byWgD31GHiMwpfLuxFSfx9f7Sw4RGdvpQs2ANBGfnzzardADZegr87ZXsebSp+3vaaznEUzPQ==} - hasBin: true - peerDependencies: - '@tanstack/react-query': '>=5.59.0' - '@wagmi/core': '>=2.16.3' - react: '>=18' - typescript: '>=5.4.0' - viem: '>=2.37.0' - wagmi: '>=2.0.0' - peerDependenciesMeta: - '@tanstack/react-query': - optional: true - react: - optional: true - typescript: - optional: true - wagmi: - optional: true - porto@0.2.35: resolution: {integrity: sha512-gu9FfjjvvYBgQXUHWTp6n3wkTxVtEcqFotM7i3GEZeoQbvLGbssAicCz6hFZ8+xggrJWwi/RLmbwNra50SMmUQ==} hasBin: true @@ -7119,15 +7561,15 @@ packages: preact@10.24.2: resolution: {integrity: sha512-1cSoF0aCC8uaARATfrlz4VCBqE8LwZwRfLgkxJOQwAlQt6ayTmi0D9OF7nXid1POI5SZidFuG9CnlXbDfLqY/Q==} - preact@10.28.3: - resolution: {integrity: sha512-tCmoRkPQLpBeWzpmbhryairGnhW9tKV6c6gr/w+RhoRoKEJwsjzipwp//1oCpGPOchvSLaAPlpcJi9MwMmoPyA==} + preact@10.27.2: + resolution: {integrity: sha512-5SYSgFKSyhCbk6SrXyMpqjb5+MQBgfvEKE/OC+PujcY34sOpqtr+0AZQtPYx5IA6VxynQ7rUPCtKzyovpj9Bpg==} prelude-ls@1.2.1: resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} engines: {node: '>= 0.8.0'} - prettier-linter-helpers@1.0.1: - resolution: {integrity: sha512-SxToR7P8Y2lWmv/kTzVLC1t/GDI2WGjMwNhLLE9qtH8Q13C+aEmuRlzDst4Up4s0Wc8sF2M+J57iB3cMLqftfg==} + prettier-linter-helpers@1.0.0: + resolution: {integrity: sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==} engines: {node: '>=6.0.0'} prettier@2.8.8: @@ -7146,6 +7588,12 @@ packages: process-warning@1.0.0: resolution: {integrity: sha512-du4wfLyj4yCZq1VupnVSZmRsPJsNuxoDQFdCFHLaYiEbFBD7QE0a+I4D7hOxrVnh78QE/YipFAj9lXHiXocV+Q==} + process-warning@4.0.1: + resolution: {integrity: sha512-3c2LzQ3rY9d0hc1emcsHhfT9Jwz0cChib/QN89oME2R451w5fy3f0afAhERFZAwrbDU43wk12d0ORBpDVME50Q==} + + process-warning@5.0.0: + resolution: {integrity: sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA==} + prop-types@15.8.1: resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} @@ -7176,6 +7624,10 @@ packages: engines: {node: '>=10.13.0'} hasBin: true + qs@6.13.0: + resolution: {integrity: sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==} + engines: {node: '>=0.6'} + qs@6.14.1: resolution: {integrity: sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==} engines: {node: '>=0.6'} @@ -7200,12 +7652,15 @@ packages: radix3@1.1.2: resolution: {integrity: sha512-b484I/7b8rDEdSDKckSSBA8knMpcdsXudlE/LNL639wFoHKwLbEkQFZHWEYwDC0wa0FKUcCY+GAF73Z7wxNVFA==} + randombytes@2.1.0: + resolution: {integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==} + range-parser@1.2.1: resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} engines: {node: '>= 0.6'} - raw-body@2.5.3: - resolution: {integrity: sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==} + raw-body@2.5.2: + resolution: {integrity: sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==} engines: {node: '>= 0.8'} raw-body@3.0.2: @@ -7217,10 +7672,10 @@ packages: peerDependencies: react: ^19.2.1 - react-dom@19.2.4: - resolution: {integrity: sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==} + react-dom@19.2.3: + resolution: {integrity: sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==} peerDependencies: - react: ^19.2.4 + react: ^19.2.3 react-is@16.13.1: resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} @@ -7229,8 +7684,8 @@ packages: resolution: {integrity: sha512-DGrYcCWK7tvYMnWh79yrPHt+vdx9tY+1gPZa7nJQtO/p8bLTDaHp4dzwEhQB7pZ4Xe3ok4XKuEPrVuc+wlpkmw==} engines: {node: '>=0.10.0'} - react@19.2.4: - resolution: {integrity: sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==} + react@19.2.3: + resolution: {integrity: sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==} engines: {node: '>=0.10.0'} read-yaml-file@1.1.0: @@ -7248,20 +7703,20 @@ packages: resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} engines: {node: '>= 14.18.0'} - readdirp@5.0.0: - resolution: {integrity: sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==} - engines: {node: '>= 20.19.0'} - real-require@0.1.0: resolution: {integrity: sha512-r/H9MzAWtrv8aSVjPCMFpDMl5q66GqtmmRkRjpHTsp4zBAa+snZyiQNlMONiUmEJcsnaw0wCauJ2GWODr/aFkg==} engines: {node: '>= 12.13.0'} + real-require@0.2.0: + resolution: {integrity: sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==} + engines: {node: '>= 12.13.0'} + reflect.getprototypeof@1.0.10: resolution: {integrity: sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==} engines: {node: '>= 0.4'} - regenerate-unicode-properties@10.2.2: - resolution: {integrity: sha512-m03P+zhBeQd1RGnYxrGyDAPpWX/epKirLrp8e3qevZdVkKtnCrjjWczIbYc8+xd6vcTStVlqfycTx1KR4LOr0g==} + regenerate-unicode-properties@10.2.0: + resolution: {integrity: sha512-DqHn3DwbmmPVzeKj9woBadqmXxLvQoQIwu7nopMc72ztvxVmVk2SBhSnx67zuye5TP+lJsb/TBQsjLKhnDf3MA==} engines: {node: '>=4'} regenerate@1.4.2: @@ -7271,15 +7726,15 @@ packages: resolution: {integrity: sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==} engines: {node: '>= 0.4'} - regexpu-core@6.4.0: - resolution: {integrity: sha512-0ghuzq67LI9bLXpOX/ISfve/Mq33a4aFRzoQYhnnok1JOFpmE/A2TBGkNVenOGEeSBCjIiWcc6MVOG5HEQv0sA==} + regexpu-core@6.2.0: + resolution: {integrity: sha512-H66BPQMrv+V16t8xtmq+UC0CBpiTBA60V8ibS1QVReIp8T1z8hwFxqcGzm9K6lgsN7sB5edVH8a+ze6Fqm4weA==} engines: {node: '>=4'} regjsgen@0.8.0: resolution: {integrity: sha512-RvwtGe3d7LvWiDQXeQw8p5asZUmfU1G/l6WbUXeHta7Y2PEIvBTwH6E2EfmYUK8pxcxEdEmaomqyp0vZZ7C+3Q==} - regjsparser@0.13.0: - resolution: {integrity: sha512-NZQZdC5wOE/H3UT28fVGL+ikOZcEzfMGk/c3iN9UGxzWHMa1op7274oyiUVrAG4B2EuFhus8SvkaYnhvW92p9Q==} + regjsparser@0.12.0: + resolution: {integrity: sha512-cnE+y8bz4NhMjISKbgeVJtqNbtf5QpjZP+Bslo+UqkIt9QPnX9q095eiRRASJG1/tz6dlNr6Z5NsBiWYokp6EQ==} hasBin: true require-directory@2.1.1: @@ -7307,8 +7762,8 @@ packages: resolve-pkg-maps@1.0.0: resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} - resolve@1.22.11: - resolution: {integrity: sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==} + resolve@1.22.10: + resolution: {integrity: sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==} engines: {node: '>= 0.4'} hasBin: true @@ -7319,12 +7774,19 @@ packages: responselike@2.0.1: resolution: {integrity: sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw==} + ret@0.5.0: + resolution: {integrity: sha512-I1XxrZSQ+oErkRR4jYbAyEEu2I0avBvvMM5JN+6EBprOGRCs63ENqZ3vjavq8fBw2+62G5LF5XelKwuJpcvcxw==} + engines: {node: '>=10'} + reusify@1.1.0: resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} - rollup@4.57.1: - resolution: {integrity: sha512-oQL6lgK3e2QZeQ7gcgIkS2YZPg5slw37hYufJ3edKlfQSGGm8ICoxswK15ntSzF/a8+h7ekRy7k7oWc3BQ7y8A==} + rfdc@1.4.1: + resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==} + + rollup@4.50.0: + resolution: {integrity: sha512-/Zl4D8zPifNmyGzJS+3kVoyXeDeT/GrsJM94sACNg9RtUE0hrHa1bNPtRSrfHTMH5HjRzce6K7rlTh3Khiw+pw==} engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true @@ -7332,8 +7794,8 @@ packages: resolution: {integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==} engines: {node: '>= 18'} - rpc-websockets@9.3.3: - resolution: {integrity: sha512-OkCsBBzrwxX4DoSv4Zlf9DgXKRB0MzVfCFg5MC+fNnf9ktr4SMWjsri0VNZQlDbCnGcImT6KNEv4ZoxktQhdpA==} + rpc-websockets@9.1.3: + resolution: {integrity: sha512-I+kNjW0udB4Fetr3vvtRuYZJS0PcSPyyvBcH5sDdoV8DFs5E4W2pTr7aiMlKfPxANTClP9RlqCPolj9dd5MsEA==} rrweb-cssom@0.8.0: resolution: {integrity: sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==} @@ -7359,6 +7821,9 @@ packages: resolution: {integrity: sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==} engines: {node: '>= 0.4'} + safe-regex2@5.0.0: + resolution: {integrity: sha512-YwJwe5a51WlK7KbOJREPdjNrpViQBI3p4T50lfwPuDhZnE3XGVTlGvi+aolc5+RvxDD6bnUmjVsU9n1eboLUYw==} + safe-stable-stringify@2.5.0: resolution: {integrity: sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==} engines: {node: '>=10'} @@ -7373,25 +7838,33 @@ packages: scheduler@0.27.0: resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} + secure-json-parse@4.1.0: + resolution: {integrity: sha512-l4KnYfEyqYJxDwlNVyRfO2E4NTHfMKAWdUuA8J0yve2Dz/E/PdBepY03RvyJpssIpRFwJoCD55wA+mEDs6ByWA==} + semver@6.3.1: resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} hasBin: true + semver@7.7.2: + resolution: {integrity: sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==} + engines: {node: '>=10'} + hasBin: true + semver@7.7.3: resolution: {integrity: sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==} engines: {node: '>=10'} hasBin: true - send@0.19.2: - resolution: {integrity: sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==} + send@0.19.0: + resolution: {integrity: sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==} engines: {node: '>= 0.8.0'} send@1.2.1: resolution: {integrity: sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==} engines: {node: '>= 18'} - serve-static@1.16.3: - resolution: {integrity: sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==} + serve-static@1.16.2: + resolution: {integrity: sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==} engines: {node: '>= 0.8.0'} serve-static@2.2.1: @@ -7401,6 +7874,9 @@ packages: set-blocking@2.0.0: resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==} + set-cookie-parser@2.7.2: + resolution: {integrity: sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==} + set-function-length@1.2.2: resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} engines: {node: '>= 0.4'} @@ -7468,24 +7944,28 @@ packages: snake-case@3.0.4: resolution: {integrity: sha512-LAOh4z89bGQvl9pFfNF8V146i7o7/CqFPbqzYgP+yYzDIDeS9HaNFtXABamRW+AQzEVODcvE79ljJ+8a9YSdMg==} - socket.io-client@4.8.3: - resolution: {integrity: sha512-uP0bpjWrjQmUt5DTHq9RuoCBdFJF10cdX9X+a368j/Ft0wmaVgxlrjvK3kjvgCODOMMOz9lcaRzxmso0bTWZ/g==} + socket.io-client@4.8.1: + resolution: {integrity: sha512-hJVXfu3E28NmzGk8o1sHhN3om52tRvwYeidbj7xKy2eIIse5IoKX3USlS6Tqt3BHAtflLIkCQBkzVrEEfWUyYQ==} engines: {node: '>=10.0.0'} - socket.io-parser@4.2.5: - resolution: {integrity: sha512-bPMmpy/5WWKHea5Y/jYAP6k74A+hvmRCQaJuJB6I/ML5JZq/KfNieUVo/3Mh7SAqn7TyFdIo6wqYHInG1MU1bQ==} + socket.io-parser@4.2.4: + resolution: {integrity: sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==} engines: {node: '>=10.0.0'} sonic-boom@2.8.0: resolution: {integrity: sha512-kuonw1YOYYNOve5iHdSahXPOK49GqwA+LZhI6Wz/l0rP57iKyXXIHaRagOBHAPmGwJC6od2Z9zgvZ5loSgMlVg==} + sonic-boom@4.2.1: + resolution: {integrity: sha512-w6AxtubXa2wTXAUsZMMWERrsIRAdrK0Sc+FUytWvYAhBJLyuI4llrMIC1DtlNSdI99EI86KZum2MMq3EAZlF9Q==} + source-map-js@1.2.1: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} - source-map@0.7.6: - resolution: {integrity: sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==} - engines: {node: '>= 12'} + source-map@0.8.0-beta.0: + resolution: {integrity: sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA==} + engines: {node: '>= 8'} + deprecated: The work that was done in this beta branch won't be included in future versions spawndamnit@3.0.1: resolution: {integrity: sha512-MmnduQUuHCoFckZoWnXsTg7JaiLBJrKFj9UI2MbRPGaJeVpsLcVBu6P/IGZovziM/YBsellCmsprgNA+w0CzVg==} @@ -7516,12 +7996,16 @@ packages: stackback@0.0.2: resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + statuses@2.0.1: + resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==} + engines: {node: '>= 0.8'} + statuses@2.0.2: resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} engines: {node: '>= 0.8'} - std-env@3.10.0: - resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} + std-env@3.9.0: + resolution: {integrity: sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw==} stop-iteration-iterator@1.1.0: resolution: {integrity: sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==} @@ -7544,6 +8028,10 @@ packages: resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} engines: {node: '>=8'} + string-width@5.1.2: + resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} + engines: {node: '>=12'} + string.prototype.includes@2.0.1: resolution: {integrity: sha512-o7+c9bW6zpAdJHTtujeePODAhkuicdAryFsfVKwA+wGw89wJ4GTY484WTucM9hLtDEOpOvI+aHnzqnC5lHp4Rg==} engines: {node: '>= 0.4'} @@ -7577,6 +8065,10 @@ packages: resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} engines: {node: '>=8'} + strip-ansi@7.1.0: + resolution: {integrity: sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==} + engines: {node: '>=12'} + strip-bom@3.0.0: resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} engines: {node: '>=4'} @@ -7585,8 +8077,8 @@ packages: resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} engines: {node: '>=8'} - strip-literal@3.1.0: - resolution: {integrity: sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==} + strip-literal@3.0.0: + resolution: {integrity: sha512-TcccoMhJOM3OebGhSBEmp3UZ2SfDMZUEBdRA/9ynfLi8yYajyWX3JiXArcJt4Umh4vISpspkQIY8ZZoCqjbviA==} styled-jsx@5.1.6: resolution: {integrity: sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==} @@ -7601,8 +8093,8 @@ packages: babel-plugin-macros: optional: true - sucrase@3.35.1: - resolution: {integrity: sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==} + sucrase@3.35.0: + resolution: {integrity: sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==} engines: {node: '>=16 || 14 >=14.17'} hasBin: true @@ -7633,15 +8125,15 @@ packages: symbol-tree@3.2.4: resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} - synckit@0.11.12: - resolution: {integrity: sha512-Bh7QjT8/SuKUIfObSXNHNSK6WHo6J1tHCqJsuaFDP7gP0fkzSfTxI8y85JrppZ0h8l0maIgc2tfuZQ6/t3GtnQ==} + synckit@0.11.11: + resolution: {integrity: sha512-MeQTA1r0litLUf0Rp/iisCaL8761lKAZHaimlbGK4j0HysC4PLfqygQj9srcs0m2RdtDYnF8UuYyKpbjHYp7Jw==} engines: {node: ^14.18.0 || >=16.0.0} - tailwind-merge@2.6.1: - resolution: {integrity: sha512-Oo6tHdpZsGpkKG88HJ8RR1rg/RdnEkQEfMoEk2x1XRI3F1AxeU+ijRXpiVUF4UbLfcxxRGw6TbUINKYdWVsQTQ==} + tailwind-merge@2.6.0: + resolution: {integrity: sha512-P+Vu1qXfzediirmHOC3xKGAYeZtPcV9g76X+xg2FD4tYgR71ewMA35Y3sCz3zhiN/dwefRpJX0yBcgwi1fXNQA==} - tailwindcss@4.1.18: - resolution: {integrity: sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw==} + tailwindcss@4.1.17: + resolution: {integrity: sha512-j9Ee2YjuQqYT9bbRTfTZht9W/ytp5H+jJpZKiYdP/bpnXARAuELt9ofP0lPnmHjbga7SNQIxdTAXCmtKVYjN+Q==} tapable@2.3.0: resolution: {integrity: sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==} @@ -7664,12 +8156,20 @@ packages: thread-stream@0.15.2: resolution: {integrity: sha512-UkEhKIg2pD+fjkHQKyJO3yoIvAP3N6RlNFt2dUhcS1FGvCD1cQa1M/PGknCLFIyZdtJOWQjejp7bdNqmN7zwdA==} + thread-stream@4.0.0: + resolution: {integrity: sha512-4iMVL6HAINXWf1ZKZjIPcz5wYaOdPhtO8ATvZ+Xqp3BTdaqtAwQkNmKORqcIo5YkQqGXq5cwfswDwMqqQNrpJA==} + engines: {node: '>=20'} + tinybench@2.9.0: resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} tinyexec@0.3.2: resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} + tinyglobby@0.2.14: + resolution: {integrity: sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==} + engines: {node: '>=12.0.0'} + tinyglobby@0.2.15: resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} engines: {node: '>=12.0.0'} @@ -7682,46 +8182,71 @@ packages: resolution: {integrity: sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==} engines: {node: '>=14.0.0'} - tinyspy@4.0.4: - resolution: {integrity: sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==} + tinyspy@4.0.3: + resolution: {integrity: sha512-t2T/WLB2WRgZ9EpE4jgPJ9w+i66UZfDc8wHh0xrwiRNN+UwH98GIJkTeZqX9rg0i0ptwzqW+uYeIF0T4F8LR7A==} engines: {node: '>=14.0.0'} tldts-core@6.1.86: resolution: {integrity: sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==} + tldts-core@7.0.19: + resolution: {integrity: sha512-lJX2dEWx0SGH4O6p+7FPwYmJ/bu1JbcGJ8RLaG9b7liIgZ85itUVEPbMtWRVrde/0fnDPEPHW10ZsKW3kVsE9A==} + tldts@6.1.86: resolution: {integrity: sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==} hasBin: true - to-buffer@1.2.2: - resolution: {integrity: sha512-db0E3UJjcFhpDhAF4tLo03oli3pwl3dbnzXOUIlRKrp+ldk/VUxzpWYZENsw2SZiuBjHAk7DfB0VU7NKdpb6sw==} + tldts@7.0.19: + resolution: {integrity: sha512-8PWx8tvC4jDB39BQw1m4x8y5MH1BcQ5xHeL2n7UVFulMPH/3Q0uiamahFJ3lXA0zO2SUyRXuVVbWSDmstlt9YA==} + hasBin: true + + to-buffer@1.2.1: + resolution: {integrity: sha512-tB82LpAIWjhLYbqjx3X4zEeHN6M8CiuOEy2JY8SEQVdYRe3CCHOFaqrBW1doLDrfpWhplcW7BL+bO3/6S3pcDQ==} engines: {node: '>= 0.4'} to-regex-range@5.0.1: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} + toad-cache@3.7.0: + resolution: {integrity: sha512-/m8M+2BJUpoJdgAHoG+baCwBT+tf2VraSfkBgl0Y00qIWt41DJ8R5B8nsEw0I58YwF5IZH6z24/2TobDKnqSWw==} + engines: {node: '>=12'} + toidentifier@1.0.1: resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} engines: {node: '>=0.6'} + toml@3.0.0: + resolution: {integrity: sha512-y/mWCZinnvxjTKYhJ+pYxwD0mRLVvOtdS2Awbgxln6iEnt4rk0yBxeSBHkGJcPucRiG0e55mwWp+g/05rsrd6w==} + tough-cookie@5.1.2: resolution: {integrity: sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==} engines: {node: '>=16'} + tough-cookie@6.0.0: + resolution: {integrity: sha512-kXuRi1mtaKMrsLUxz3sQYvVl37B0Ns6MzfrtV5DvJceE9bPyspOqk9xxv7XbZWcfLWbFmm997vl83qUWVJA64w==} + engines: {node: '>=16'} + tr46@0.0.3: resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} + tr46@1.0.1: + resolution: {integrity: sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA==} + tr46@5.1.1: resolution: {integrity: sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==} engines: {node: '>=18'} + tr46@6.0.0: + resolution: {integrity: sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==} + engines: {node: '>=20'} + tree-kill@1.2.2: resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} hasBin: true - ts-api-utils@2.4.0: - resolution: {integrity: sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==} + ts-api-utils@2.1.0: + resolution: {integrity: sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==} engines: {node: '>=18.12'} peerDependencies: typescript: '>=4.8.4' @@ -7751,8 +8276,8 @@ packages: tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} - tsup@8.5.1: - resolution: {integrity: sha512-xtgkqwdhpKWr3tKPmCkvYmS9xnQK3m3XgxZHwSUjvfTjp7YfXe5tT3GgWi0F2N+ZSMsOeWeZFh7ZZFg5iPhing==} + tsup@8.5.0: + resolution: {integrity: sha512-VmBp77lWNQq6PfuMqCHD3xWl22vEoWsKajkF8t+yMBawlUS8JzEI+vOVMeuNZIuMML8qXRizFKi9oD5glKQVcQ==} engines: {node: '>=18'} hasBin: true peerDependencies: @@ -7770,43 +8295,43 @@ packages: typescript: optional: true - tsx@4.21.0: - resolution: {integrity: sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==} + tsx@4.20.5: + resolution: {integrity: sha512-+wKjMNU9w/EaQayHXb7WA7ZaHY6hN8WgfvHNQ3t1PnU91/7O8TcTnIhCDYTZwnt8JsO9IBqZ30Ln1r7pPF52Aw==} engines: {node: '>=18.0.0'} hasBin: true - turbo-darwin-64@2.8.1: - resolution: {integrity: sha512-FQ6Uqxty/H1Nvn1dpBe8KUlMRclTuiyNSc1PCeDL/ad7M9ykpWutB51YpMpf9ibTA32M6wLdIRf+D96W6hDAtQ==} + turbo-darwin-64@2.5.6: + resolution: {integrity: sha512-3C1xEdo4aFwMJAPvtlPqz1Sw/+cddWIOmsalHFMrsqqydcptwBfu26WW2cDm3u93bUzMbBJ8k3zNKFqxJ9ei2A==} cpu: [x64] os: [darwin] - turbo-darwin-arm64@2.8.1: - resolution: {integrity: sha512-4bCcEpGP2/aSXmeN2gl5SuAmS1q5ykjubnFvSoXjQoCKtDOV+vc4CTl/DduZzUUutCVUWXjl8OyfIQ+DGCaV4A==} + turbo-darwin-arm64@2.5.6: + resolution: {integrity: sha512-LyiG+rD7JhMfYwLqB6k3LZQtYn8CQQUePbpA8mF/hMLPAekXdJo1g0bUPw8RZLwQXUIU/3BU7tXENvhSGz5DPA==} cpu: [arm64] os: [darwin] - turbo-linux-64@2.8.1: - resolution: {integrity: sha512-m99JRlWlEgXPR7mkThAbKh6jbTmWSOXM/c6rt8yd4Uxh0+wjq7+DYcQbead6aoOqmCP9akswZ8EXIv1ogKBblg==} + turbo-linux-64@2.5.6: + resolution: {integrity: sha512-GOcUTT0xiT/pSnHL4YD6Yr3HreUhU8pUcGqcI2ksIF9b2/r/kRHwGFcsHgpG3+vtZF/kwsP0MV8FTlTObxsYIA==} cpu: [x64] os: [linux] - turbo-linux-arm64@2.8.1: - resolution: {integrity: sha512-AsPlza3AsavJdl2o7FE67qyv0aLfmT1XwFQGzvwpoAO6Bj7S4a03tpUchZKNuGjNAkKVProQRFnB7PgUAScFXA==} + turbo-linux-arm64@2.5.6: + resolution: {integrity: sha512-10Tm15bruJEA3m0V7iZcnQBpObGBcOgUcO+sY7/2vk1bweW34LMhkWi8svjV9iDF68+KJDThnYDlYE/bc7/zzQ==} cpu: [arm64] os: [linux] - turbo-windows-64@2.8.1: - resolution: {integrity: sha512-GdqNO6bYShRsr79B+2G/2ssjLEp9uBTvLBJSWRtRCiac/SEmv8T6RYv9hu+h5oGbFALtnKNp6BQBw78RJURsPw==} + turbo-windows-64@2.5.6: + resolution: {integrity: sha512-FyRsVpgaj76It0ludwZsNN40ytHN+17E4PFJyeliBEbxrGTc5BexlXVpufB7XlAaoaZVxbS6KT8RofLfDRyEPg==} cpu: [x64] os: [win32] - turbo-windows-arm64@2.8.1: - resolution: {integrity: sha512-n40E6IpkzrShRo3yMdRpgnn1/sAbGC6tZXwyNu8fe9RsufeD7KBiaoRSvw8xLyqV3pd2yoTL2rdCXq24MnTCWA==} + turbo-windows-arm64@2.5.6: + resolution: {integrity: sha512-j/tWu8cMeQ7HPpKri6jvKtyXg9K1gRyhdK4tKrrchH8GNHscPX/F71zax58yYtLRWTiK04zNzPcUJuoS0+v/+Q==} cpu: [arm64] os: [win32] - turbo@2.8.1: - resolution: {integrity: sha512-pbSMlRflA0RAuk/0jnAt8pzOYh1+sKaT8nVtcs75OFGVWD0evleQRmKtHJJV42QOhaC3Hx9mUUSOom/irasbjA==} + turbo@2.5.6: + resolution: {integrity: sha512-gxToHmi9oTBNB05UjUsrWf0OyN5ZXtD0apOarC1KIx232Vp3WimRNy3810QzeNSgyD5rsaIDXlxlbnOzlouo+w==} hasBin: true tweetnacl@1.0.3: @@ -7840,20 +8365,20 @@ packages: resolution: {integrity: sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==} engines: {node: '>= 0.4'} - typescript-eslint@8.54.0: - resolution: {integrity: sha512-CKsJ+g53QpsNPqbzUsfKVgd3Lny4yKZ1pP4qN3jdMOg/sisIDLGyDMezycquXLE5JsEU0wp3dGNdzig0/fmSVQ==} + typescript-eslint@8.49.0: + resolution: {integrity: sha512-zRSVH1WXD0uXczCXw+nsdjGPUdx4dfrs5VQoHnUWmv1U3oNlAKv4FUNdLDhVUg+gYn+a5hUESqch//Rv5wVhrg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <6.0.0' - typescript@5.9.3: - resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + typescript@5.9.2: + resolution: {integrity: sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==} engines: {node: '>=14.17'} hasBin: true - ufo@1.6.3: - resolution: {integrity: sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q==} + ufo@1.6.1: + resolution: {integrity: sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA==} uint8arrays@3.1.0: resolution: {integrity: sha512-ei5rfKtoRO8OyOIor2Rz5fhzjThwIHJZ3uyDPnDHTXbP0aMQ1RN/6AI5B5d9dBxJOU+BvOAk7ZQ1xphsX8Lrog==} @@ -7871,8 +8396,8 @@ packages: undici-types@6.21.0: resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} - undici-types@7.20.0: - resolution: {integrity: sha512-PZDAAlMkNw5ZzN/ebfyrwzrMWfIf7Jbn9iM/I6SF456OKrb2wnfqVowaxEY/cMAM8MjFu1zhdpJyA0L+rTYwNw==} + undici-types@7.16.0: + resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==} undici-types@7.22.0: resolution: {integrity: sha512-RKZvifiL60xdsIuC80UY0dq8Z7DbJUV8/l2hOVbyZAxBzEeQU4Z58+4ZzJ6WN2Lidi9KzT5EbiGX+PI/UGYuRw==} @@ -7885,12 +8410,12 @@ packages: resolution: {integrity: sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==} engines: {node: '>=4'} - unicode-match-property-value-ecmascript@2.2.1: - resolution: {integrity: sha512-JQ84qTuMg4nVkx8ga4A16a1epI9H6uTXAknqxkGF/aFfRLw1xC/Bp24HNLaZhHSkWd3+84t8iXnp1J0kYcZHhg==} + unicode-match-property-value-ecmascript@2.2.0: + resolution: {integrity: sha512-4IehN3V/+kkr5YeSSDDQG8QLqO26XpL2XP3GQtqwlT/QYSECAwFztxVHjlbh0+gjJ3XmNLS0zDsbgs9jWKExLg==} engines: {node: '>=4'} - unicode-property-aliases-ecmascript@2.2.0: - resolution: {integrity: sha512-hpbDzxUY9BFwX+UeBnxv3Sh1q7HFxj48DTmXchNgRa46lO8uj3/1iEn3MiNUYTg1g9ctIqXCCERn8gYZhHC5lQ==} + unicode-property-aliases-ecmascript@2.1.0: + resolution: {integrity: sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w==} engines: {node: '>=4'} universalify@0.1.2: @@ -7904,8 +8429,8 @@ packages: unrs-resolver@1.11.1: resolution: {integrity: sha512-bSjt9pjaEBnNiGgc9rUiHGKv5l4/TGzDmYw3RhnkJGtLhbnnA/5qJj7x3dNDCRx/PJxu774LlH8lCOlB4hEfKg==} - unstorage@1.17.4: - resolution: {integrity: sha512-fHK0yNg38tBiJKp/Vgsq4j0JEsCmgqH58HAn707S7zGkArbZsVr/CwINoi+nh3h98BRCwKvx1K3Xg9u3VV83sw==} + unstorage@1.17.0: + resolution: {integrity: sha512-l9Z7lBiwtNp8ZmcoZ/dmPkFXFdtEdZtTZafCSnEIj3YvtkXeGAtL2rN8MQFy/0cs4eOLpuRJMp9ivdug7TCvww==} peerDependencies: '@azure/app-configuration': ^1.8.0 '@azure/cosmos': ^4.2.0 @@ -7913,14 +8438,14 @@ packages: '@azure/identity': ^4.6.0 '@azure/keyvault-secrets': ^4.9.0 '@azure/storage-blob': ^12.26.0 - '@capacitor/preferences': ^6 || ^7 || ^8 + '@capacitor/preferences': ^6.0.3 || ^7.0.0 '@deno/kv': '>=0.9.0' '@netlify/blobs': ^6.5.0 || ^7.0.0 || ^8.1.0 || ^9.0.0 || ^10.0.0 '@planetscale/database': ^1.19.0 '@upstash/redis': ^1.34.3 '@vercel/blob': '>=0.27.1' - '@vercel/functions': ^2.2.12 || ^3.0.0 - '@vercel/kv': ^1 || ^2 || ^3 + '@vercel/functions': ^2.2.12 + '@vercel/kv': ^1.0.1 aws4fetch: ^1.0.20 db0: '>=0.2.1' idb-keyval: ^6.2.1 @@ -7966,8 +8491,8 @@ packages: uploadthing: optional: true - update-browserslist-db@1.2.3: - resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} + update-browserslist-db@1.1.3: + resolution: {integrity: sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==} hasBin: true peerDependencies: browserslist: '>= 4.21.0' @@ -7975,6 +8500,9 @@ packages: uri-js@4.4.1: resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + urijs@1.19.11: + resolution: {integrity: sha512-HXgFDgDommxn5/bIv0cnQZsPhHDA90NPHD6+c/v21U5+Sx5hoP8+dP9IZXBU1gIfvdRfhG8cel9QNPeionfcCQ==} + use-sync-external-store@1.2.0: resolution: {integrity: sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==} peerDependencies: @@ -8034,6 +8562,22 @@ packages: typescript: optional: true + viem@2.37.3: + resolution: {integrity: sha512-hwoZqkFSy13GCFzIftgfIH8hNENvdlcHIvtLt73w91tL6rKmZjQisXWTahi1Vn5of8/JQ1FBKfwUus3YkDXwbw==} + peerDependencies: + typescript: '>=5.0.4' + peerDependenciesMeta: + typescript: + optional: true + + viem@2.40.3: + resolution: {integrity: sha512-feYfEpbgjRkZYQpwcgxqkWzjxHI5LSDAjcGetHHwDRuX9BRQHUdV8ohrCosCYpdEhus/RknD3/bOd4qLYVPPuA==} + peerDependencies: + typescript: '>=5.0.4' + peerDependenciesMeta: + typescript: + optional: true + viem@2.45.1: resolution: {integrity: sha512-LN6Pp7vSfv50LgwhkfSbIXftAM5J89lP9x8TeDa8QM7o41IxlHrDh0F9X+FfnCWtsz11pEVV5sn+yBUoOHNqYA==} peerDependencies: @@ -8055,8 +8599,8 @@ packages: vite: optional: true - vite@6.4.1: - resolution: {integrity: sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==} + vite@6.3.5: + resolution: {integrity: sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ==} engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} hasBin: true peerDependencies: @@ -8127,6 +8671,17 @@ packages: resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==} engines: {node: '>=18'} + wagmi@2.16.9: + resolution: {integrity: sha512-5NbjvuNNhT0t0lQsDD5otQqZ5RZBM1UhInHoBq/Lpnr6xLLa8AWxYqHg5oZtGCdiUNltys11iBOS6z4mLepIqw==} + peerDependencies: + '@tanstack/react-query': '>=5.0.0' + react: '>=18' + typescript: '>=5.0.4' + viem: 2.x + peerDependenciesMeta: + typescript: + optional: true + wagmi@2.19.5: resolution: {integrity: sha512-RQUfKMv6U+EcSNNGiPbdkDtJwtuFxZWLmvDiQmjjBgkuPulUwDJsKhi7gjynzJdsx2yDqhHCXkKsbbfbIsHfcQ==} peerDependencies: @@ -8144,13 +8699,21 @@ packages: webidl-conversions@3.0.1: resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} + webidl-conversions@4.0.2: + resolution: {integrity: sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==} + webidl-conversions@7.0.0: resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==} engines: {node: '>=12'} + webidl-conversions@8.0.1: + resolution: {integrity: sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==} + engines: {node: '>=20'} + whatwg-encoding@3.1.1: resolution: {integrity: sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==} engines: {node: '>=18'} + deprecated: Use @exodus/bytes instead for a more spec-conformant and faster implementation whatwg-mimetype@4.0.0: resolution: {integrity: sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==} @@ -8160,9 +8723,16 @@ packages: resolution: {integrity: sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==} engines: {node: '>=18'} + whatwg-url@15.1.0: + resolution: {integrity: sha512-2ytDk0kiEj/yu90JOAp44PVPUkO9+jVhyf+SybKlRHSDlvOOZhdPIrr7xTH64l4WixO2cP+wQIcgujkGBPPz6g==} + engines: {node: '>=20'} + whatwg-url@5.0.0: resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} + whatwg-url@7.1.0: + resolution: {integrity: sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==} + which-boxed-primitive@1.1.1: resolution: {integrity: sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==} engines: {node: '>= 0.4'} @@ -8178,8 +8748,8 @@ packages: which-module@2.0.1: resolution: {integrity: sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==} - which-typed-array@1.1.20: - resolution: {integrity: sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg==} + which-typed-array@1.1.19: + resolution: {integrity: sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==} engines: {node: '>= 0.4'} which@2.0.2: @@ -8200,6 +8770,14 @@ packages: resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==} engines: {node: '>=8'} + wrap-ansi@7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} + + wrap-ansi@8.1.0: + resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} + engines: {node: '>=12'} + wrappy@1.0.2: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} @@ -8287,6 +8865,11 @@ packages: yallist@3.1.1: resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + yaml@2.8.1: + resolution: {integrity: sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==} + engines: {node: '>= 14.6'} + hasBin: true + yargs-parser@18.1.3: resolution: {integrity: sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==} engines: {node: '>=6'} @@ -8316,8 +8899,8 @@ packages: zod@3.25.76: resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==} - zod@4.3.6: - resolution: {integrity: sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==} + zod@4.1.13: + resolution: {integrity: sha512-AvvthqfqrAhNH9dnfmrfKzX5upOdjUVJYFqNSlkmGf64gRaTzlPwz99IHYnVs28qYAybvAlBV+H7pn0saFY4Ig==} zustand@5.0.0: resolution: {integrity: sha512-LE+VcmbartOPM+auOjCCLQOsQ05zUTp8RkgwRzefUk+2jISdMMFnxvyTjA4YNWr5ZGXYbVsEMZosttuxUBkojQ==} @@ -8337,24 +8920,6 @@ packages: use-sync-external-store: optional: true - zustand@5.0.11: - resolution: {integrity: sha512-fdZY+dk7zn/vbWNCYmzZULHRrss0jx5pPFiOuMZ/5HJN6Yv3u+1Wswy/4MpZEkEGhtNH+pwxZB8OKgUBPzYAGg==} - engines: {node: '>=12.20.0'} - peerDependencies: - '@types/react': '>=18.0.0' - immer: '>=9.0.6' - react: '>=18.0.0' - use-sync-external-store: '>=1.2.0' - peerDependenciesMeta: - '@types/react': - optional: true - immer: - optional: true - react: - optional: true - use-sync-external-store: - optional: true - zustand@5.0.3: resolution: {integrity: sha512-14fwWQtU3pH4dE0dOpdMiWjddcH+QzKIgk1cl8epwSE7yag43k/AD/m4L6+K7DytAOr9gGBe3/EXj9g7cdostg==} engines: {node: '>=12.20.0'} @@ -8375,12 +8940,20 @@ packages: snapshots: + '@acemir/cssom@0.9.30': + optional: true + '@adraffy/ens-normalize@1.10.1': {} - '@adraffy/ens-normalize@1.11.1': {} + '@adraffy/ens-normalize@1.11.0': {} '@alloc/quick-lru@5.2.0': {} + '@ampproject/remapping@2.3.0': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.30 + '@aptos-labs/aptos-cli@1.1.1': dependencies: commander: 12.1.0 @@ -8410,28 +8983,40 @@ snapshots: '@csstools/css-color-parser': 3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) '@csstools/css-tokenizer': 3.0.4 - lru-cache: 11.2.5 + lru-cache: 11.2.4 + + '@asamuzakjp/dom-selector@6.7.6': + dependencies: + '@asamuzakjp/nwsapi': 2.3.9 + bidi-js: 1.0.3 + css-tree: 3.1.0 + is-potential-custom-element-name: 1.0.1 + lru-cache: 11.2.4 + optional: true + + '@asamuzakjp/nwsapi@2.3.9': + optional: true - '@babel/code-frame@7.29.0': + '@babel/code-frame@7.27.1': dependencies: - '@babel/helper-validator-identifier': 7.28.5 + '@babel/helper-validator-identifier': 7.27.1 js-tokens: 4.0.0 picocolors: 1.1.1 - '@babel/compat-data@7.29.0': {} + '@babel/compat-data@7.28.0': {} - '@babel/core@7.29.0': + '@babel/core@7.28.3': dependencies: - '@babel/code-frame': 7.29.0 - '@babel/generator': 7.29.0 - '@babel/helper-compilation-targets': 7.28.6 - '@babel/helper-module-transforms': 7.28.6(@babel/core@7.29.0) - '@babel/helpers': 7.28.6 - '@babel/parser': 7.29.0 - '@babel/template': 7.28.6 - '@babel/traverse': 7.29.0 - '@babel/types': 7.29.0 - '@jridgewell/remapping': 2.3.5 + '@ampproject/remapping': 2.3.0 + '@babel/code-frame': 7.27.1 + '@babel/generator': 7.28.3 + '@babel/helper-compilation-targets': 7.27.2 + '@babel/helper-module-transforms': 7.28.3(@babel/core@7.28.3) + '@babel/helpers': 7.28.3 + '@babel/parser': 7.28.3 + '@babel/template': 7.27.2 + '@babel/traverse': 7.28.3 + '@babel/types': 7.28.2 convert-source-map: 2.0.0 debug: 4.4.3 gensync: 1.0.0-beta.2 @@ -8440,740 +9025,720 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/generator@7.29.0': + '@babel/generator@7.28.3': dependencies: - '@babel/parser': 7.29.0 - '@babel/types': 7.29.0 + '@babel/parser': 7.28.3 + '@babel/types': 7.28.2 '@jridgewell/gen-mapping': 0.3.13 - '@jridgewell/trace-mapping': 0.3.31 + '@jridgewell/trace-mapping': 0.3.30 jsesc: 3.1.0 '@babel/helper-annotate-as-pure@7.27.3': dependencies: - '@babel/types': 7.29.0 + '@babel/types': 7.28.2 - '@babel/helper-compilation-targets@7.28.6': + '@babel/helper-compilation-targets@7.27.2': dependencies: - '@babel/compat-data': 7.29.0 + '@babel/compat-data': 7.28.0 '@babel/helper-validator-option': 7.27.1 - browserslist: 4.28.1 + browserslist: 4.25.4 lru-cache: 5.1.1 semver: 6.3.1 - '@babel/helper-create-class-features-plugin@7.28.6(@babel/core@7.29.0)': + '@babel/helper-create-class-features-plugin@7.28.3(@babel/core@7.28.3)': dependencies: - '@babel/core': 7.29.0 + '@babel/core': 7.28.3 '@babel/helper-annotate-as-pure': 7.27.3 - '@babel/helper-member-expression-to-functions': 7.28.5 + '@babel/helper-member-expression-to-functions': 7.27.1 '@babel/helper-optimise-call-expression': 7.27.1 - '@babel/helper-replace-supers': 7.28.6(@babel/core@7.29.0) + '@babel/helper-replace-supers': 7.27.1(@babel/core@7.28.3) '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 - '@babel/traverse': 7.29.0 + '@babel/traverse': 7.28.3 semver: 6.3.1 transitivePeerDependencies: - supports-color - '@babel/helper-create-regexp-features-plugin@7.28.5(@babel/core@7.29.0)': + '@babel/helper-create-regexp-features-plugin@7.27.1(@babel/core@7.28.3)': dependencies: - '@babel/core': 7.29.0 + '@babel/core': 7.28.3 '@babel/helper-annotate-as-pure': 7.27.3 - regexpu-core: 6.4.0 + regexpu-core: 6.2.0 semver: 6.3.1 - '@babel/helper-define-polyfill-provider@0.6.6(@babel/core@7.29.0)': + '@babel/helper-define-polyfill-provider@0.6.5(@babel/core@7.28.3)': dependencies: - '@babel/core': 7.29.0 - '@babel/helper-compilation-targets': 7.28.6 - '@babel/helper-plugin-utils': 7.28.6 + '@babel/core': 7.28.3 + '@babel/helper-compilation-targets': 7.27.2 + '@babel/helper-plugin-utils': 7.27.1 debug: 4.4.3 lodash.debounce: 4.0.8 - resolve: 1.22.11 + resolve: 1.22.10 transitivePeerDependencies: - supports-color '@babel/helper-globals@7.28.0': {} - '@babel/helper-member-expression-to-functions@7.28.5': + '@babel/helper-member-expression-to-functions@7.27.1': dependencies: - '@babel/traverse': 7.29.0 - '@babel/types': 7.29.0 + '@babel/traverse': 7.28.3 + '@babel/types': 7.28.2 transitivePeerDependencies: - supports-color - '@babel/helper-module-imports@7.28.6': + '@babel/helper-module-imports@7.27.1': dependencies: - '@babel/traverse': 7.29.0 - '@babel/types': 7.29.0 + '@babel/traverse': 7.28.3 + '@babel/types': 7.28.2 transitivePeerDependencies: - supports-color - '@babel/helper-module-transforms@7.28.6(@babel/core@7.29.0)': + '@babel/helper-module-transforms@7.28.3(@babel/core@7.28.3)': dependencies: - '@babel/core': 7.29.0 - '@babel/helper-module-imports': 7.28.6 - '@babel/helper-validator-identifier': 7.28.5 - '@babel/traverse': 7.29.0 + '@babel/core': 7.28.3 + '@babel/helper-module-imports': 7.27.1 + '@babel/helper-validator-identifier': 7.27.1 + '@babel/traverse': 7.28.3 transitivePeerDependencies: - supports-color '@babel/helper-optimise-call-expression@7.27.1': dependencies: - '@babel/types': 7.29.0 + '@babel/types': 7.28.2 - '@babel/helper-plugin-utils@7.28.6': {} + '@babel/helper-plugin-utils@7.27.1': {} - '@babel/helper-remap-async-to-generator@7.27.1(@babel/core@7.29.0)': + '@babel/helper-remap-async-to-generator@7.27.1(@babel/core@7.28.3)': dependencies: - '@babel/core': 7.29.0 + '@babel/core': 7.28.3 '@babel/helper-annotate-as-pure': 7.27.3 - '@babel/helper-wrap-function': 7.28.6 - '@babel/traverse': 7.29.0 + '@babel/helper-wrap-function': 7.28.3 + '@babel/traverse': 7.28.3 transitivePeerDependencies: - supports-color - '@babel/helper-replace-supers@7.28.6(@babel/core@7.29.0)': + '@babel/helper-replace-supers@7.27.1(@babel/core@7.28.3)': dependencies: - '@babel/core': 7.29.0 - '@babel/helper-member-expression-to-functions': 7.28.5 + '@babel/core': 7.28.3 + '@babel/helper-member-expression-to-functions': 7.27.1 '@babel/helper-optimise-call-expression': 7.27.1 - '@babel/traverse': 7.29.0 + '@babel/traverse': 7.28.3 transitivePeerDependencies: - supports-color '@babel/helper-skip-transparent-expression-wrappers@7.27.1': dependencies: - '@babel/traverse': 7.29.0 - '@babel/types': 7.29.0 + '@babel/traverse': 7.28.3 + '@babel/types': 7.28.2 transitivePeerDependencies: - supports-color '@babel/helper-string-parser@7.27.1': {} - '@babel/helper-validator-identifier@7.28.5': {} + '@babel/helper-validator-identifier@7.27.1': {} '@babel/helper-validator-option@7.27.1': {} - '@babel/helper-wrap-function@7.28.6': + '@babel/helper-wrap-function@7.28.3': dependencies: - '@babel/template': 7.28.6 - '@babel/traverse': 7.29.0 - '@babel/types': 7.29.0 + '@babel/template': 7.27.2 + '@babel/traverse': 7.28.3 + '@babel/types': 7.28.2 transitivePeerDependencies: - supports-color - '@babel/helpers@7.28.6': + '@babel/helpers@7.28.3': dependencies: - '@babel/template': 7.28.6 - '@babel/types': 7.29.0 + '@babel/template': 7.27.2 + '@babel/types': 7.28.2 - '@babel/parser@7.29.0': + '@babel/parser@7.28.3': dependencies: - '@babel/types': 7.29.0 + '@babel/types': 7.28.2 - '@babel/plugin-bugfix-firefox-class-in-computed-class-key@7.28.5(@babel/core@7.29.0)': + '@babel/plugin-bugfix-firefox-class-in-computed-class-key@7.27.1(@babel/core@7.28.3)': dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 - '@babel/traverse': 7.29.0 + '@babel/core': 7.28.3 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/traverse': 7.28.3 transitivePeerDependencies: - supports-color - '@babel/plugin-bugfix-safari-class-field-initializer-scope@7.27.1(@babel/core@7.29.0)': + '@babel/plugin-bugfix-safari-class-field-initializer-scope@7.27.1(@babel/core@7.28.3)': dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 + '@babel/core': 7.28.3 + '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@7.27.1(@babel/core@7.29.0)': + '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@7.27.1(@babel/core@7.28.3)': dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 + '@babel/core': 7.28.3 + '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@7.27.1(@babel/core@7.29.0)': + '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@7.27.1(@babel/core@7.28.3)': dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 + '@babel/core': 7.28.3 + '@babel/helper-plugin-utils': 7.27.1 '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 - '@babel/plugin-transform-optional-chaining': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-optional-chaining': 7.27.1(@babel/core@7.28.3) transitivePeerDependencies: - supports-color - '@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@7.28.6(@babel/core@7.29.0)': + '@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@7.28.3(@babel/core@7.28.3)': dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 - '@babel/traverse': 7.29.0 + '@babel/core': 7.28.3 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/traverse': 7.28.3 transitivePeerDependencies: - supports-color - '@babel/plugin-proposal-private-property-in-object@7.21.0-placeholder-for-preset-env.2(@babel/core@7.29.0)': + '@babel/plugin-proposal-private-property-in-object@7.21.0-placeholder-for-preset-env.2(@babel/core@7.28.3)': dependencies: - '@babel/core': 7.29.0 + '@babel/core': 7.28.3 - '@babel/plugin-syntax-import-assertions@7.28.6(@babel/core@7.29.0)': + '@babel/plugin-syntax-import-assertions@7.27.1(@babel/core@7.28.3)': dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 + '@babel/core': 7.28.3 + '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-syntax-import-attributes@7.28.6(@babel/core@7.29.0)': + '@babel/plugin-syntax-import-attributes@7.27.1(@babel/core@7.28.3)': dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 + '@babel/core': 7.28.3 + '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-syntax-jsx@7.28.6(@babel/core@7.29.0)': + '@babel/plugin-syntax-jsx@7.27.1(@babel/core@7.28.3)': dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 + '@babel/core': 7.28.3 + '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-syntax-typescript@7.28.6(@babel/core@7.29.0)': + '@babel/plugin-syntax-typescript@7.27.1(@babel/core@7.28.3)': dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 + '@babel/core': 7.28.3 + '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-syntax-unicode-sets-regex@7.18.6(@babel/core@7.29.0)': + '@babel/plugin-syntax-unicode-sets-regex@7.18.6(@babel/core@7.28.3)': dependencies: - '@babel/core': 7.29.0 - '@babel/helper-create-regexp-features-plugin': 7.28.5(@babel/core@7.29.0) - '@babel/helper-plugin-utils': 7.28.6 + '@babel/core': 7.28.3 + '@babel/helper-create-regexp-features-plugin': 7.27.1(@babel/core@7.28.3) + '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-arrow-functions@7.27.1(@babel/core@7.29.0)': + '@babel/plugin-transform-arrow-functions@7.27.1(@babel/core@7.28.3)': dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 + '@babel/core': 7.28.3 + '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-async-generator-functions@7.29.0(@babel/core@7.29.0)': + '@babel/plugin-transform-async-generator-functions@7.28.0(@babel/core@7.28.3)': dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 - '@babel/helper-remap-async-to-generator': 7.27.1(@babel/core@7.29.0) - '@babel/traverse': 7.29.0 + '@babel/core': 7.28.3 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/helper-remap-async-to-generator': 7.27.1(@babel/core@7.28.3) + '@babel/traverse': 7.28.3 transitivePeerDependencies: - supports-color - '@babel/plugin-transform-async-to-generator@7.28.6(@babel/core@7.29.0)': + '@babel/plugin-transform-async-to-generator@7.27.1(@babel/core@7.28.3)': dependencies: - '@babel/core': 7.29.0 - '@babel/helper-module-imports': 7.28.6 - '@babel/helper-plugin-utils': 7.28.6 - '@babel/helper-remap-async-to-generator': 7.27.1(@babel/core@7.29.0) + '@babel/core': 7.28.3 + '@babel/helper-module-imports': 7.27.1 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/helper-remap-async-to-generator': 7.27.1(@babel/core@7.28.3) transitivePeerDependencies: - supports-color - '@babel/plugin-transform-block-scoped-functions@7.27.1(@babel/core@7.29.0)': + '@babel/plugin-transform-block-scoped-functions@7.27.1(@babel/core@7.28.3)': dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 + '@babel/core': 7.28.3 + '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-block-scoping@7.28.6(@babel/core@7.29.0)': + '@babel/plugin-transform-block-scoping@7.28.0(@babel/core@7.28.3)': dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 + '@babel/core': 7.28.3 + '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-class-properties@7.28.6(@babel/core@7.29.0)': + '@babel/plugin-transform-class-properties@7.27.1(@babel/core@7.28.3)': dependencies: - '@babel/core': 7.29.0 - '@babel/helper-create-class-features-plugin': 7.28.6(@babel/core@7.29.0) - '@babel/helper-plugin-utils': 7.28.6 + '@babel/core': 7.28.3 + '@babel/helper-create-class-features-plugin': 7.28.3(@babel/core@7.28.3) + '@babel/helper-plugin-utils': 7.27.1 transitivePeerDependencies: - supports-color - '@babel/plugin-transform-class-static-block@7.28.6(@babel/core@7.29.0)': + '@babel/plugin-transform-class-static-block@7.28.3(@babel/core@7.28.3)': dependencies: - '@babel/core': 7.29.0 - '@babel/helper-create-class-features-plugin': 7.28.6(@babel/core@7.29.0) - '@babel/helper-plugin-utils': 7.28.6 + '@babel/core': 7.28.3 + '@babel/helper-create-class-features-plugin': 7.28.3(@babel/core@7.28.3) + '@babel/helper-plugin-utils': 7.27.1 transitivePeerDependencies: - supports-color - '@babel/plugin-transform-classes@7.28.6(@babel/core@7.29.0)': + '@babel/plugin-transform-classes@7.28.3(@babel/core@7.28.3)': dependencies: - '@babel/core': 7.29.0 + '@babel/core': 7.28.3 '@babel/helper-annotate-as-pure': 7.27.3 - '@babel/helper-compilation-targets': 7.28.6 + '@babel/helper-compilation-targets': 7.27.2 '@babel/helper-globals': 7.28.0 - '@babel/helper-plugin-utils': 7.28.6 - '@babel/helper-replace-supers': 7.28.6(@babel/core@7.29.0) - '@babel/traverse': 7.29.0 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/helper-replace-supers': 7.27.1(@babel/core@7.28.3) + '@babel/traverse': 7.28.3 transitivePeerDependencies: - supports-color - '@babel/plugin-transform-computed-properties@7.28.6(@babel/core@7.29.0)': + '@babel/plugin-transform-computed-properties@7.27.1(@babel/core@7.28.3)': dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 - '@babel/template': 7.28.6 + '@babel/core': 7.28.3 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/template': 7.27.2 - '@babel/plugin-transform-destructuring@7.28.5(@babel/core@7.29.0)': + '@babel/plugin-transform-destructuring@7.28.0(@babel/core@7.28.3)': dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 - '@babel/traverse': 7.29.0 + '@babel/core': 7.28.3 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/traverse': 7.28.3 transitivePeerDependencies: - supports-color - '@babel/plugin-transform-dotall-regex@7.28.6(@babel/core@7.29.0)': + '@babel/plugin-transform-dotall-regex@7.27.1(@babel/core@7.28.3)': dependencies: - '@babel/core': 7.29.0 - '@babel/helper-create-regexp-features-plugin': 7.28.5(@babel/core@7.29.0) - '@babel/helper-plugin-utils': 7.28.6 + '@babel/core': 7.28.3 + '@babel/helper-create-regexp-features-plugin': 7.27.1(@babel/core@7.28.3) + '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-duplicate-keys@7.27.1(@babel/core@7.29.0)': + '@babel/plugin-transform-duplicate-keys@7.27.1(@babel/core@7.28.3)': dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 + '@babel/core': 7.28.3 + '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-duplicate-named-capturing-groups-regex@7.29.0(@babel/core@7.29.0)': + '@babel/plugin-transform-duplicate-named-capturing-groups-regex@7.27.1(@babel/core@7.28.3)': dependencies: - '@babel/core': 7.29.0 - '@babel/helper-create-regexp-features-plugin': 7.28.5(@babel/core@7.29.0) - '@babel/helper-plugin-utils': 7.28.6 + '@babel/core': 7.28.3 + '@babel/helper-create-regexp-features-plugin': 7.27.1(@babel/core@7.28.3) + '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-dynamic-import@7.27.1(@babel/core@7.29.0)': + '@babel/plugin-transform-dynamic-import@7.27.1(@babel/core@7.28.3)': dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 + '@babel/core': 7.28.3 + '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-explicit-resource-management@7.28.6(@babel/core@7.29.0)': + '@babel/plugin-transform-explicit-resource-management@7.28.0(@babel/core@7.28.3)': dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-transform-destructuring': 7.28.5(@babel/core@7.29.0) + '@babel/core': 7.28.3 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-transform-destructuring': 7.28.0(@babel/core@7.28.3) transitivePeerDependencies: - supports-color - '@babel/plugin-transform-exponentiation-operator@7.28.6(@babel/core@7.29.0)': + '@babel/plugin-transform-exponentiation-operator@7.27.1(@babel/core@7.28.3)': dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 + '@babel/core': 7.28.3 + '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-export-namespace-from@7.27.1(@babel/core@7.29.0)': + '@babel/plugin-transform-export-namespace-from@7.27.1(@babel/core@7.28.3)': dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 + '@babel/core': 7.28.3 + '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-for-of@7.27.1(@babel/core@7.29.0)': + '@babel/plugin-transform-for-of@7.27.1(@babel/core@7.28.3)': dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 + '@babel/core': 7.28.3 + '@babel/helper-plugin-utils': 7.27.1 '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 transitivePeerDependencies: - supports-color - '@babel/plugin-transform-function-name@7.27.1(@babel/core@7.29.0)': + '@babel/plugin-transform-function-name@7.27.1(@babel/core@7.28.3)': dependencies: - '@babel/core': 7.29.0 - '@babel/helper-compilation-targets': 7.28.6 - '@babel/helper-plugin-utils': 7.28.6 - '@babel/traverse': 7.29.0 + '@babel/core': 7.28.3 + '@babel/helper-compilation-targets': 7.27.2 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/traverse': 7.28.3 transitivePeerDependencies: - supports-color - '@babel/plugin-transform-json-strings@7.28.6(@babel/core@7.29.0)': + '@babel/plugin-transform-json-strings@7.27.1(@babel/core@7.28.3)': dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 + '@babel/core': 7.28.3 + '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-literals@7.27.1(@babel/core@7.29.0)': + '@babel/plugin-transform-literals@7.27.1(@babel/core@7.28.3)': dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 + '@babel/core': 7.28.3 + '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-logical-assignment-operators@7.28.6(@babel/core@7.29.0)': + '@babel/plugin-transform-logical-assignment-operators@7.27.1(@babel/core@7.28.3)': dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 + '@babel/core': 7.28.3 + '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-member-expression-literals@7.27.1(@babel/core@7.29.0)': + '@babel/plugin-transform-member-expression-literals@7.27.1(@babel/core@7.28.3)': dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 + '@babel/core': 7.28.3 + '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-modules-amd@7.27.1(@babel/core@7.29.0)': + '@babel/plugin-transform-modules-amd@7.27.1(@babel/core@7.28.3)': dependencies: - '@babel/core': 7.29.0 - '@babel/helper-module-transforms': 7.28.6(@babel/core@7.29.0) - '@babel/helper-plugin-utils': 7.28.6 + '@babel/core': 7.28.3 + '@babel/helper-module-transforms': 7.28.3(@babel/core@7.28.3) + '@babel/helper-plugin-utils': 7.27.1 transitivePeerDependencies: - supports-color - '@babel/plugin-transform-modules-commonjs@7.28.6(@babel/core@7.29.0)': + '@babel/plugin-transform-modules-commonjs@7.27.1(@babel/core@7.28.3)': dependencies: - '@babel/core': 7.29.0 - '@babel/helper-module-transforms': 7.28.6(@babel/core@7.29.0) - '@babel/helper-plugin-utils': 7.28.6 + '@babel/core': 7.28.3 + '@babel/helper-module-transforms': 7.28.3(@babel/core@7.28.3) + '@babel/helper-plugin-utils': 7.27.1 transitivePeerDependencies: - supports-color - '@babel/plugin-transform-modules-systemjs@7.29.0(@babel/core@7.29.0)': + '@babel/plugin-transform-modules-systemjs@7.27.1(@babel/core@7.28.3)': dependencies: - '@babel/core': 7.29.0 - '@babel/helper-module-transforms': 7.28.6(@babel/core@7.29.0) - '@babel/helper-plugin-utils': 7.28.6 - '@babel/helper-validator-identifier': 7.28.5 - '@babel/traverse': 7.29.0 + '@babel/core': 7.28.3 + '@babel/helper-module-transforms': 7.28.3(@babel/core@7.28.3) + '@babel/helper-plugin-utils': 7.27.1 + '@babel/helper-validator-identifier': 7.27.1 + '@babel/traverse': 7.28.3 transitivePeerDependencies: - supports-color - '@babel/plugin-transform-modules-umd@7.27.1(@babel/core@7.29.0)': + '@babel/plugin-transform-modules-umd@7.27.1(@babel/core@7.28.3)': dependencies: - '@babel/core': 7.29.0 - '@babel/helper-module-transforms': 7.28.6(@babel/core@7.29.0) - '@babel/helper-plugin-utils': 7.28.6 + '@babel/core': 7.28.3 + '@babel/helper-module-transforms': 7.28.3(@babel/core@7.28.3) + '@babel/helper-plugin-utils': 7.27.1 transitivePeerDependencies: - supports-color - '@babel/plugin-transform-named-capturing-groups-regex@7.29.0(@babel/core@7.29.0)': + '@babel/plugin-transform-named-capturing-groups-regex@7.27.1(@babel/core@7.28.3)': dependencies: - '@babel/core': 7.29.0 - '@babel/helper-create-regexp-features-plugin': 7.28.5(@babel/core@7.29.0) - '@babel/helper-plugin-utils': 7.28.6 + '@babel/core': 7.28.3 + '@babel/helper-create-regexp-features-plugin': 7.27.1(@babel/core@7.28.3) + '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-new-target@7.27.1(@babel/core@7.29.0)': + '@babel/plugin-transform-new-target@7.27.1(@babel/core@7.28.3)': dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 + '@babel/core': 7.28.3 + '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-nullish-coalescing-operator@7.28.6(@babel/core@7.29.0)': + '@babel/plugin-transform-nullish-coalescing-operator@7.27.1(@babel/core@7.28.3)': dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 + '@babel/core': 7.28.3 + '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-numeric-separator@7.28.6(@babel/core@7.29.0)': + '@babel/plugin-transform-numeric-separator@7.27.1(@babel/core@7.28.3)': dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 + '@babel/core': 7.28.3 + '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-object-rest-spread@7.28.6(@babel/core@7.29.0)': + '@babel/plugin-transform-object-rest-spread@7.28.0(@babel/core@7.28.3)': dependencies: - '@babel/core': 7.29.0 - '@babel/helper-compilation-targets': 7.28.6 - '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-transform-destructuring': 7.28.5(@babel/core@7.29.0) - '@babel/plugin-transform-parameters': 7.27.7(@babel/core@7.29.0) - '@babel/traverse': 7.29.0 + '@babel/core': 7.28.3 + '@babel/helper-compilation-targets': 7.27.2 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-transform-destructuring': 7.28.0(@babel/core@7.28.3) + '@babel/plugin-transform-parameters': 7.27.7(@babel/core@7.28.3) + '@babel/traverse': 7.28.3 transitivePeerDependencies: - supports-color - '@babel/plugin-transform-object-super@7.27.1(@babel/core@7.29.0)': + '@babel/plugin-transform-object-super@7.27.1(@babel/core@7.28.3)': dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 - '@babel/helper-replace-supers': 7.28.6(@babel/core@7.29.0) + '@babel/core': 7.28.3 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/helper-replace-supers': 7.27.1(@babel/core@7.28.3) transitivePeerDependencies: - supports-color - '@babel/plugin-transform-optional-catch-binding@7.28.6(@babel/core@7.29.0)': + '@babel/plugin-transform-optional-catch-binding@7.27.1(@babel/core@7.28.3)': dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 + '@babel/core': 7.28.3 + '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-optional-chaining@7.28.6(@babel/core@7.29.0)': + '@babel/plugin-transform-optional-chaining@7.27.1(@babel/core@7.28.3)': dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 + '@babel/core': 7.28.3 + '@babel/helper-plugin-utils': 7.27.1 '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 transitivePeerDependencies: - supports-color - '@babel/plugin-transform-parameters@7.27.7(@babel/core@7.29.0)': + '@babel/plugin-transform-parameters@7.27.7(@babel/core@7.28.3)': dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 + '@babel/core': 7.28.3 + '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-private-methods@7.28.6(@babel/core@7.29.0)': + '@babel/plugin-transform-private-methods@7.27.1(@babel/core@7.28.3)': dependencies: - '@babel/core': 7.29.0 - '@babel/helper-create-class-features-plugin': 7.28.6(@babel/core@7.29.0) - '@babel/helper-plugin-utils': 7.28.6 + '@babel/core': 7.28.3 + '@babel/helper-create-class-features-plugin': 7.28.3(@babel/core@7.28.3) + '@babel/helper-plugin-utils': 7.27.1 transitivePeerDependencies: - supports-color - '@babel/plugin-transform-private-property-in-object@7.28.6(@babel/core@7.29.0)': + '@babel/plugin-transform-private-property-in-object@7.27.1(@babel/core@7.28.3)': dependencies: - '@babel/core': 7.29.0 + '@babel/core': 7.28.3 '@babel/helper-annotate-as-pure': 7.27.3 - '@babel/helper-create-class-features-plugin': 7.28.6(@babel/core@7.29.0) - '@babel/helper-plugin-utils': 7.28.6 + '@babel/helper-create-class-features-plugin': 7.28.3(@babel/core@7.28.3) + '@babel/helper-plugin-utils': 7.27.1 transitivePeerDependencies: - supports-color - '@babel/plugin-transform-property-literals@7.27.1(@babel/core@7.29.0)': + '@babel/plugin-transform-property-literals@7.27.1(@babel/core@7.28.3)': dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 + '@babel/core': 7.28.3 + '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-react-constant-elements@7.27.1(@babel/core@7.29.0)': + '@babel/plugin-transform-react-constant-elements@7.27.1(@babel/core@7.28.3)': dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 + '@babel/core': 7.28.3 + '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-react-display-name@7.28.0(@babel/core@7.29.0)': + '@babel/plugin-transform-react-display-name@7.28.0(@babel/core@7.28.3)': dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 + '@babel/core': 7.28.3 + '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-react-jsx-development@7.27.1(@babel/core@7.29.0)': + '@babel/plugin-transform-react-jsx-development@7.27.1(@babel/core@7.28.3)': dependencies: - '@babel/core': 7.29.0 - '@babel/plugin-transform-react-jsx': 7.28.6(@babel/core@7.29.0) + '@babel/core': 7.28.3 + '@babel/plugin-transform-react-jsx': 7.27.1(@babel/core@7.28.3) transitivePeerDependencies: - supports-color - '@babel/plugin-transform-react-jsx@7.28.6(@babel/core@7.29.0)': + '@babel/plugin-transform-react-jsx@7.27.1(@babel/core@7.28.3)': dependencies: - '@babel/core': 7.29.0 + '@babel/core': 7.28.3 '@babel/helper-annotate-as-pure': 7.27.3 - '@babel/helper-module-imports': 7.28.6 - '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-syntax-jsx': 7.28.6(@babel/core@7.29.0) - '@babel/types': 7.29.0 + '@babel/helper-module-imports': 7.27.1 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-syntax-jsx': 7.27.1(@babel/core@7.28.3) + '@babel/types': 7.28.2 transitivePeerDependencies: - supports-color - '@babel/plugin-transform-react-pure-annotations@7.27.1(@babel/core@7.29.0)': + '@babel/plugin-transform-react-pure-annotations@7.27.1(@babel/core@7.28.3)': dependencies: - '@babel/core': 7.29.0 + '@babel/core': 7.28.3 '@babel/helper-annotate-as-pure': 7.27.3 - '@babel/helper-plugin-utils': 7.28.6 + '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-regenerator@7.29.0(@babel/core@7.29.0)': + '@babel/plugin-transform-regenerator@7.28.3(@babel/core@7.28.3)': dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 + '@babel/core': 7.28.3 + '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-regexp-modifiers@7.28.6(@babel/core@7.29.0)': + '@babel/plugin-transform-regexp-modifiers@7.27.1(@babel/core@7.28.3)': dependencies: - '@babel/core': 7.29.0 - '@babel/helper-create-regexp-features-plugin': 7.28.5(@babel/core@7.29.0) - '@babel/helper-plugin-utils': 7.28.6 + '@babel/core': 7.28.3 + '@babel/helper-create-regexp-features-plugin': 7.27.1(@babel/core@7.28.3) + '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-reserved-words@7.27.1(@babel/core@7.29.0)': + '@babel/plugin-transform-reserved-words@7.27.1(@babel/core@7.28.3)': dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 + '@babel/core': 7.28.3 + '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-shorthand-properties@7.27.1(@babel/core@7.29.0)': + '@babel/plugin-transform-shorthand-properties@7.27.1(@babel/core@7.28.3)': dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 + '@babel/core': 7.28.3 + '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-spread@7.28.6(@babel/core@7.29.0)': + '@babel/plugin-transform-spread@7.27.1(@babel/core@7.28.3)': dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 + '@babel/core': 7.28.3 + '@babel/helper-plugin-utils': 7.27.1 '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 transitivePeerDependencies: - supports-color - '@babel/plugin-transform-sticky-regex@7.27.1(@babel/core@7.29.0)': + '@babel/plugin-transform-sticky-regex@7.27.1(@babel/core@7.28.3)': dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 + '@babel/core': 7.28.3 + '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-template-literals@7.27.1(@babel/core@7.29.0)': + '@babel/plugin-transform-template-literals@7.27.1(@babel/core@7.28.3)': dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 + '@babel/core': 7.28.3 + '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-typeof-symbol@7.27.1(@babel/core@7.29.0)': + '@babel/plugin-transform-typeof-symbol@7.27.1(@babel/core@7.28.3)': dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 + '@babel/core': 7.28.3 + '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-typescript@7.28.6(@babel/core@7.29.0)': + '@babel/plugin-transform-typescript@7.28.0(@babel/core@7.28.3)': dependencies: - '@babel/core': 7.29.0 + '@babel/core': 7.28.3 '@babel/helper-annotate-as-pure': 7.27.3 - '@babel/helper-create-class-features-plugin': 7.28.6(@babel/core@7.29.0) - '@babel/helper-plugin-utils': 7.28.6 + '@babel/helper-create-class-features-plugin': 7.28.3(@babel/core@7.28.3) + '@babel/helper-plugin-utils': 7.27.1 '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 - '@babel/plugin-syntax-typescript': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-syntax-typescript': 7.27.1(@babel/core@7.28.3) transitivePeerDependencies: - supports-color - '@babel/plugin-transform-unicode-escapes@7.27.1(@babel/core@7.29.0)': + '@babel/plugin-transform-unicode-escapes@7.27.1(@babel/core@7.28.3)': dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 + '@babel/core': 7.28.3 + '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-unicode-property-regex@7.28.6(@babel/core@7.29.0)': + '@babel/plugin-transform-unicode-property-regex@7.27.1(@babel/core@7.28.3)': dependencies: - '@babel/core': 7.29.0 - '@babel/helper-create-regexp-features-plugin': 7.28.5(@babel/core@7.29.0) - '@babel/helper-plugin-utils': 7.28.6 + '@babel/core': 7.28.3 + '@babel/helper-create-regexp-features-plugin': 7.27.1(@babel/core@7.28.3) + '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-unicode-regex@7.27.1(@babel/core@7.29.0)': + '@babel/plugin-transform-unicode-regex@7.27.1(@babel/core@7.28.3)': dependencies: - '@babel/core': 7.29.0 - '@babel/helper-create-regexp-features-plugin': 7.28.5(@babel/core@7.29.0) - '@babel/helper-plugin-utils': 7.28.6 + '@babel/core': 7.28.3 + '@babel/helper-create-regexp-features-plugin': 7.27.1(@babel/core@7.28.3) + '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-unicode-sets-regex@7.28.6(@babel/core@7.29.0)': + '@babel/plugin-transform-unicode-sets-regex@7.27.1(@babel/core@7.28.3)': dependencies: - '@babel/core': 7.29.0 - '@babel/helper-create-regexp-features-plugin': 7.28.5(@babel/core@7.29.0) - '@babel/helper-plugin-utils': 7.28.6 + '@babel/core': 7.28.3 + '@babel/helper-create-regexp-features-plugin': 7.27.1(@babel/core@7.28.3) + '@babel/helper-plugin-utils': 7.27.1 - '@babel/preset-env@7.29.0(@babel/core@7.29.0)': + '@babel/preset-env@7.28.3(@babel/core@7.28.3)': dependencies: - '@babel/compat-data': 7.29.0 - '@babel/core': 7.29.0 - '@babel/helper-compilation-targets': 7.28.6 - '@babel/helper-plugin-utils': 7.28.6 + '@babel/compat-data': 7.28.0 + '@babel/core': 7.28.3 + '@babel/helper-compilation-targets': 7.27.2 + '@babel/helper-plugin-utils': 7.27.1 '@babel/helper-validator-option': 7.27.1 - '@babel/plugin-bugfix-firefox-class-in-computed-class-key': 7.28.5(@babel/core@7.29.0) - '@babel/plugin-bugfix-safari-class-field-initializer-scope': 7.27.1(@babel/core@7.29.0) - '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression': 7.27.1(@babel/core@7.29.0) - '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining': 7.27.1(@babel/core@7.29.0) - '@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly': 7.28.6(@babel/core@7.29.0) - '@babel/plugin-proposal-private-property-in-object': 7.21.0-placeholder-for-preset-env.2(@babel/core@7.29.0) - '@babel/plugin-syntax-import-assertions': 7.28.6(@babel/core@7.29.0) - '@babel/plugin-syntax-import-attributes': 7.28.6(@babel/core@7.29.0) - '@babel/plugin-syntax-unicode-sets-regex': 7.18.6(@babel/core@7.29.0) - '@babel/plugin-transform-arrow-functions': 7.27.1(@babel/core@7.29.0) - '@babel/plugin-transform-async-generator-functions': 7.29.0(@babel/core@7.29.0) - '@babel/plugin-transform-async-to-generator': 7.28.6(@babel/core@7.29.0) - '@babel/plugin-transform-block-scoped-functions': 7.27.1(@babel/core@7.29.0) - '@babel/plugin-transform-block-scoping': 7.28.6(@babel/core@7.29.0) - '@babel/plugin-transform-class-properties': 7.28.6(@babel/core@7.29.0) - '@babel/plugin-transform-class-static-block': 7.28.6(@babel/core@7.29.0) - '@babel/plugin-transform-classes': 7.28.6(@babel/core@7.29.0) - '@babel/plugin-transform-computed-properties': 7.28.6(@babel/core@7.29.0) - '@babel/plugin-transform-destructuring': 7.28.5(@babel/core@7.29.0) - '@babel/plugin-transform-dotall-regex': 7.28.6(@babel/core@7.29.0) - '@babel/plugin-transform-duplicate-keys': 7.27.1(@babel/core@7.29.0) - '@babel/plugin-transform-duplicate-named-capturing-groups-regex': 7.29.0(@babel/core@7.29.0) - '@babel/plugin-transform-dynamic-import': 7.27.1(@babel/core@7.29.0) - '@babel/plugin-transform-explicit-resource-management': 7.28.6(@babel/core@7.29.0) - '@babel/plugin-transform-exponentiation-operator': 7.28.6(@babel/core@7.29.0) - '@babel/plugin-transform-export-namespace-from': 7.27.1(@babel/core@7.29.0) - '@babel/plugin-transform-for-of': 7.27.1(@babel/core@7.29.0) - '@babel/plugin-transform-function-name': 7.27.1(@babel/core@7.29.0) - '@babel/plugin-transform-json-strings': 7.28.6(@babel/core@7.29.0) - '@babel/plugin-transform-literals': 7.27.1(@babel/core@7.29.0) - '@babel/plugin-transform-logical-assignment-operators': 7.28.6(@babel/core@7.29.0) - '@babel/plugin-transform-member-expression-literals': 7.27.1(@babel/core@7.29.0) - '@babel/plugin-transform-modules-amd': 7.27.1(@babel/core@7.29.0) - '@babel/plugin-transform-modules-commonjs': 7.28.6(@babel/core@7.29.0) - '@babel/plugin-transform-modules-systemjs': 7.29.0(@babel/core@7.29.0) - '@babel/plugin-transform-modules-umd': 7.27.1(@babel/core@7.29.0) - '@babel/plugin-transform-named-capturing-groups-regex': 7.29.0(@babel/core@7.29.0) - '@babel/plugin-transform-new-target': 7.27.1(@babel/core@7.29.0) - '@babel/plugin-transform-nullish-coalescing-operator': 7.28.6(@babel/core@7.29.0) - '@babel/plugin-transform-numeric-separator': 7.28.6(@babel/core@7.29.0) - '@babel/plugin-transform-object-rest-spread': 7.28.6(@babel/core@7.29.0) - '@babel/plugin-transform-object-super': 7.27.1(@babel/core@7.29.0) - '@babel/plugin-transform-optional-catch-binding': 7.28.6(@babel/core@7.29.0) - '@babel/plugin-transform-optional-chaining': 7.28.6(@babel/core@7.29.0) - '@babel/plugin-transform-parameters': 7.27.7(@babel/core@7.29.0) - '@babel/plugin-transform-private-methods': 7.28.6(@babel/core@7.29.0) - '@babel/plugin-transform-private-property-in-object': 7.28.6(@babel/core@7.29.0) - '@babel/plugin-transform-property-literals': 7.27.1(@babel/core@7.29.0) - '@babel/plugin-transform-regenerator': 7.29.0(@babel/core@7.29.0) - '@babel/plugin-transform-regexp-modifiers': 7.28.6(@babel/core@7.29.0) - '@babel/plugin-transform-reserved-words': 7.27.1(@babel/core@7.29.0) - '@babel/plugin-transform-shorthand-properties': 7.27.1(@babel/core@7.29.0) - '@babel/plugin-transform-spread': 7.28.6(@babel/core@7.29.0) - '@babel/plugin-transform-sticky-regex': 7.27.1(@babel/core@7.29.0) - '@babel/plugin-transform-template-literals': 7.27.1(@babel/core@7.29.0) - '@babel/plugin-transform-typeof-symbol': 7.27.1(@babel/core@7.29.0) - '@babel/plugin-transform-unicode-escapes': 7.27.1(@babel/core@7.29.0) - '@babel/plugin-transform-unicode-property-regex': 7.28.6(@babel/core@7.29.0) - '@babel/plugin-transform-unicode-regex': 7.27.1(@babel/core@7.29.0) - '@babel/plugin-transform-unicode-sets-regex': 7.28.6(@babel/core@7.29.0) - '@babel/preset-modules': 0.1.6-no-external-plugins(@babel/core@7.29.0) - babel-plugin-polyfill-corejs2: 0.4.15(@babel/core@7.29.0) - babel-plugin-polyfill-corejs3: 0.14.0(@babel/core@7.29.0) - babel-plugin-polyfill-regenerator: 0.6.6(@babel/core@7.29.0) - core-js-compat: 3.48.0 + '@babel/plugin-bugfix-firefox-class-in-computed-class-key': 7.27.1(@babel/core@7.28.3) + '@babel/plugin-bugfix-safari-class-field-initializer-scope': 7.27.1(@babel/core@7.28.3) + '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression': 7.27.1(@babel/core@7.28.3) + '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining': 7.27.1(@babel/core@7.28.3) + '@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly': 7.28.3(@babel/core@7.28.3) + '@babel/plugin-proposal-private-property-in-object': 7.21.0-placeholder-for-preset-env.2(@babel/core@7.28.3) + '@babel/plugin-syntax-import-assertions': 7.27.1(@babel/core@7.28.3) + '@babel/plugin-syntax-import-attributes': 7.27.1(@babel/core@7.28.3) + '@babel/plugin-syntax-unicode-sets-regex': 7.18.6(@babel/core@7.28.3) + '@babel/plugin-transform-arrow-functions': 7.27.1(@babel/core@7.28.3) + '@babel/plugin-transform-async-generator-functions': 7.28.0(@babel/core@7.28.3) + '@babel/plugin-transform-async-to-generator': 7.27.1(@babel/core@7.28.3) + '@babel/plugin-transform-block-scoped-functions': 7.27.1(@babel/core@7.28.3) + '@babel/plugin-transform-block-scoping': 7.28.0(@babel/core@7.28.3) + '@babel/plugin-transform-class-properties': 7.27.1(@babel/core@7.28.3) + '@babel/plugin-transform-class-static-block': 7.28.3(@babel/core@7.28.3) + '@babel/plugin-transform-classes': 7.28.3(@babel/core@7.28.3) + '@babel/plugin-transform-computed-properties': 7.27.1(@babel/core@7.28.3) + '@babel/plugin-transform-destructuring': 7.28.0(@babel/core@7.28.3) + '@babel/plugin-transform-dotall-regex': 7.27.1(@babel/core@7.28.3) + '@babel/plugin-transform-duplicate-keys': 7.27.1(@babel/core@7.28.3) + '@babel/plugin-transform-duplicate-named-capturing-groups-regex': 7.27.1(@babel/core@7.28.3) + '@babel/plugin-transform-dynamic-import': 7.27.1(@babel/core@7.28.3) + '@babel/plugin-transform-explicit-resource-management': 7.28.0(@babel/core@7.28.3) + '@babel/plugin-transform-exponentiation-operator': 7.27.1(@babel/core@7.28.3) + '@babel/plugin-transform-export-namespace-from': 7.27.1(@babel/core@7.28.3) + '@babel/plugin-transform-for-of': 7.27.1(@babel/core@7.28.3) + '@babel/plugin-transform-function-name': 7.27.1(@babel/core@7.28.3) + '@babel/plugin-transform-json-strings': 7.27.1(@babel/core@7.28.3) + '@babel/plugin-transform-literals': 7.27.1(@babel/core@7.28.3) + '@babel/plugin-transform-logical-assignment-operators': 7.27.1(@babel/core@7.28.3) + '@babel/plugin-transform-member-expression-literals': 7.27.1(@babel/core@7.28.3) + '@babel/plugin-transform-modules-amd': 7.27.1(@babel/core@7.28.3) + '@babel/plugin-transform-modules-commonjs': 7.27.1(@babel/core@7.28.3) + '@babel/plugin-transform-modules-systemjs': 7.27.1(@babel/core@7.28.3) + '@babel/plugin-transform-modules-umd': 7.27.1(@babel/core@7.28.3) + '@babel/plugin-transform-named-capturing-groups-regex': 7.27.1(@babel/core@7.28.3) + '@babel/plugin-transform-new-target': 7.27.1(@babel/core@7.28.3) + '@babel/plugin-transform-nullish-coalescing-operator': 7.27.1(@babel/core@7.28.3) + '@babel/plugin-transform-numeric-separator': 7.27.1(@babel/core@7.28.3) + '@babel/plugin-transform-object-rest-spread': 7.28.0(@babel/core@7.28.3) + '@babel/plugin-transform-object-super': 7.27.1(@babel/core@7.28.3) + '@babel/plugin-transform-optional-catch-binding': 7.27.1(@babel/core@7.28.3) + '@babel/plugin-transform-optional-chaining': 7.27.1(@babel/core@7.28.3) + '@babel/plugin-transform-parameters': 7.27.7(@babel/core@7.28.3) + '@babel/plugin-transform-private-methods': 7.27.1(@babel/core@7.28.3) + '@babel/plugin-transform-private-property-in-object': 7.27.1(@babel/core@7.28.3) + '@babel/plugin-transform-property-literals': 7.27.1(@babel/core@7.28.3) + '@babel/plugin-transform-regenerator': 7.28.3(@babel/core@7.28.3) + '@babel/plugin-transform-regexp-modifiers': 7.27.1(@babel/core@7.28.3) + '@babel/plugin-transform-reserved-words': 7.27.1(@babel/core@7.28.3) + '@babel/plugin-transform-shorthand-properties': 7.27.1(@babel/core@7.28.3) + '@babel/plugin-transform-spread': 7.27.1(@babel/core@7.28.3) + '@babel/plugin-transform-sticky-regex': 7.27.1(@babel/core@7.28.3) + '@babel/plugin-transform-template-literals': 7.27.1(@babel/core@7.28.3) + '@babel/plugin-transform-typeof-symbol': 7.27.1(@babel/core@7.28.3) + '@babel/plugin-transform-unicode-escapes': 7.27.1(@babel/core@7.28.3) + '@babel/plugin-transform-unicode-property-regex': 7.27.1(@babel/core@7.28.3) + '@babel/plugin-transform-unicode-regex': 7.27.1(@babel/core@7.28.3) + '@babel/plugin-transform-unicode-sets-regex': 7.27.1(@babel/core@7.28.3) + '@babel/preset-modules': 0.1.6-no-external-plugins(@babel/core@7.28.3) + babel-plugin-polyfill-corejs2: 0.4.14(@babel/core@7.28.3) + babel-plugin-polyfill-corejs3: 0.13.0(@babel/core@7.28.3) + babel-plugin-polyfill-regenerator: 0.6.5(@babel/core@7.28.3) + core-js-compat: 3.45.1 semver: 6.3.1 transitivePeerDependencies: - supports-color - '@babel/preset-modules@0.1.6-no-external-plugins(@babel/core@7.29.0)': + '@babel/preset-modules@0.1.6-no-external-plugins(@babel/core@7.28.3)': dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 - '@babel/types': 7.29.0 + '@babel/core': 7.28.3 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/types': 7.28.2 esutils: 2.0.3 - '@babel/preset-react@7.28.5(@babel/core@7.29.0)': + '@babel/preset-react@7.27.1(@babel/core@7.28.3)': dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 + '@babel/core': 7.28.3 + '@babel/helper-plugin-utils': 7.27.1 '@babel/helper-validator-option': 7.27.1 - '@babel/plugin-transform-react-display-name': 7.28.0(@babel/core@7.29.0) - '@babel/plugin-transform-react-jsx': 7.28.6(@babel/core@7.29.0) - '@babel/plugin-transform-react-jsx-development': 7.27.1(@babel/core@7.29.0) - '@babel/plugin-transform-react-pure-annotations': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-react-display-name': 7.28.0(@babel/core@7.28.3) + '@babel/plugin-transform-react-jsx': 7.27.1(@babel/core@7.28.3) + '@babel/plugin-transform-react-jsx-development': 7.27.1(@babel/core@7.28.3) + '@babel/plugin-transform-react-pure-annotations': 7.27.1(@babel/core@7.28.3) transitivePeerDependencies: - supports-color - '@babel/preset-typescript@7.28.5(@babel/core@7.29.0)': + '@babel/preset-typescript@7.27.1(@babel/core@7.28.3)': dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 + '@babel/core': 7.28.3 + '@babel/helper-plugin-utils': 7.27.1 '@babel/helper-validator-option': 7.27.1 - '@babel/plugin-syntax-jsx': 7.28.6(@babel/core@7.29.0) - '@babel/plugin-transform-modules-commonjs': 7.28.6(@babel/core@7.29.0) - '@babel/plugin-transform-typescript': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-syntax-jsx': 7.27.1(@babel/core@7.28.3) + '@babel/plugin-transform-modules-commonjs': 7.27.1(@babel/core@7.28.3) + '@babel/plugin-transform-typescript': 7.28.0(@babel/core@7.28.3) transitivePeerDependencies: - supports-color - '@babel/runtime@7.28.6': {} + '@babel/runtime@7.28.3': {} - '@babel/template@7.28.6': + '@babel/template@7.27.2': dependencies: - '@babel/code-frame': 7.29.0 - '@babel/parser': 7.29.0 - '@babel/types': 7.29.0 + '@babel/code-frame': 7.27.1 + '@babel/parser': 7.28.3 + '@babel/types': 7.28.2 - '@babel/traverse@7.29.0': + '@babel/traverse@7.28.3': dependencies: - '@babel/code-frame': 7.29.0 - '@babel/generator': 7.29.0 + '@babel/code-frame': 7.27.1 + '@babel/generator': 7.28.3 '@babel/helper-globals': 7.28.0 - '@babel/parser': 7.29.0 - '@babel/template': 7.28.6 - '@babel/types': 7.29.0 + '@babel/parser': 7.28.3 + '@babel/template': 7.27.2 + '@babel/types': 7.28.2 debug: 4.4.3 transitivePeerDependencies: - supports-color - '@babel/types@7.29.0': + '@babel/types@7.28.2': dependencies: '@babel/helper-string-parser': 7.27.1 - '@babel/helper-validator-identifier': 7.28.5 - - '@base-org/account@1.1.1(@types/react@19.2.10)(bufferutil@4.1.0)(react@19.2.1)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.1))(utf-8-validate@5.0.10)(zod@3.25.76)': - dependencies: - '@noble/hashes': 1.4.0 - clsx: 1.2.1 - eventemitter3: 5.0.1 - idb-keyval: 6.2.1 - ox: 0.6.9(typescript@5.9.3)(zod@3.25.76) - preact: 10.24.2 - viem: 2.45.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) - zustand: 5.0.3(@types/react@19.2.10)(react@19.2.1)(use-sync-external-store@1.4.0(react@19.2.1)) - transitivePeerDependencies: - - '@types/react' - - bufferutil - - immer - - react - - typescript - - use-sync-external-store - - utf-8-validate - - zod + '@babel/helper-validator-identifier': 7.27.1 - '@base-org/account@1.1.1(@types/react@19.2.10)(bufferutil@4.1.0)(react@19.2.4)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.4))(utf-8-validate@5.0.10)(zod@3.25.76)': + '@base-org/account@1.1.1(@types/react@19.1.12)(bufferutil@4.0.9)(react@19.2.1)(typescript@5.9.2)(use-sync-external-store@1.4.0(react@19.2.1))(utf-8-validate@5.0.10)(zod@3.25.76)': dependencies: '@noble/hashes': 1.4.0 clsx: 1.2.1 eventemitter3: 5.0.1 idb-keyval: 6.2.1 - ox: 0.6.9(typescript@5.9.3)(zod@3.25.76) + ox: 0.6.9(typescript@5.9.2)(zod@3.25.76) preact: 10.24.2 - viem: 2.45.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) - zustand: 5.0.3(@types/react@19.2.10)(react@19.2.4)(use-sync-external-store@1.4.0(react@19.2.4)) + viem: 2.45.1(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + zustand: 5.0.3(@types/react@19.1.12)(react@19.2.1)(use-sync-external-store@1.4.0(react@19.2.1)) transitivePeerDependencies: - '@types/react' - bufferutil @@ -9184,23 +9749,19 @@ snapshots: - utf-8-validate - zod - '@base-org/account@2.4.0(@types/react@19.2.10)(bufferutil@4.1.0)(fastestsmallesttextencoderdecoder@1.0.22)(react@19.2.1)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.1))(utf-8-validate@5.0.10)(zod@3.25.76)': + '@base-org/account@1.1.1(@types/react@19.1.12)(bufferutil@4.0.9)(react@19.2.3)(typescript@5.9.2)(use-sync-external-store@1.4.0(react@19.2.3))(utf-8-validate@5.0.10)(zod@3.25.76)': dependencies: - '@coinbase/cdp-sdk': 1.44.0(bufferutil@4.1.0)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(utf-8-validate@5.0.10) '@noble/hashes': 1.4.0 clsx: 1.2.1 eventemitter3: 5.0.1 idb-keyval: 6.2.1 - ox: 0.6.9(typescript@5.9.3)(zod@3.25.76) + ox: 0.6.9(typescript@5.9.2)(zod@3.25.76) preact: 10.24.2 - viem: 2.45.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) - zustand: 5.0.3(@types/react@19.2.10)(react@19.2.1)(use-sync-external-store@1.4.0(react@19.2.1)) + viem: 2.45.1(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + zustand: 5.0.3(@types/react@19.1.12)(react@19.2.3)(use-sync-external-store@1.4.0(react@19.2.3)) transitivePeerDependencies: - '@types/react' - bufferutil - - debug - - encoding - - fastestsmallesttextencoderdecoder - immer - react - typescript @@ -9208,17 +9769,17 @@ snapshots: - utf-8-validate - zod - '@base-org/account@2.4.0(@types/react@19.2.10)(bufferutil@4.1.0)(fastestsmallesttextencoderdecoder@1.0.22)(react@19.2.4)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.4))(utf-8-validate@5.0.10)(zod@3.25.76)': + '@base-org/account@2.4.0(@types/react@19.1.12)(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(react@19.2.1)(typescript@5.9.2)(use-sync-external-store@1.4.0(react@19.2.1))(utf-8-validate@5.0.10)(zod@3.25.76)': dependencies: - '@coinbase/cdp-sdk': 1.44.0(bufferutil@4.1.0)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(utf-8-validate@5.0.10) + '@coinbase/cdp-sdk': 1.36.1(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(utf-8-validate@5.0.10) '@noble/hashes': 1.4.0 clsx: 1.2.1 eventemitter3: 5.0.1 idb-keyval: 6.2.1 - ox: 0.6.9(typescript@5.9.3)(zod@3.25.76) + ox: 0.6.9(typescript@5.9.2)(zod@3.25.76) preact: 10.24.2 - viem: 2.45.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) - zustand: 5.0.3(@types/react@19.2.10)(react@19.2.4)(use-sync-external-store@1.4.0(react@19.2.4)) + viem: 2.45.1(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + zustand: 5.0.3(@types/react@19.1.12)(react@19.2.1)(use-sync-external-store@1.4.0(react@19.2.1)) transitivePeerDependencies: - '@types/react' - bufferutil @@ -9269,7 +9830,7 @@ snapshots: transitivePeerDependencies: - encoding - '@changesets/cli@2.29.8(@types/node@22.19.7)': + '@changesets/cli@2.29.8(@types/node@22.18.0)': dependencies: '@changesets/apply-release-plan': 7.0.14 '@changesets/assemble-release-plan': 6.0.9 @@ -9285,7 +9846,7 @@ snapshots: '@changesets/should-skip-package': 0.1.2 '@changesets/types': 6.1.0 '@changesets/write': 0.4.0 - '@inquirer/external-editor': 1.0.3(@types/node@22.19.7) + '@inquirer/external-editor': 1.0.3(@types/node@22.18.0) '@manypkg/get-packages': 1.1.3 ansi-colors: 4.1.3 ci-info: 3.9.0 @@ -9391,19 +9952,17 @@ snapshots: human-id: 4.1.3 prettier: 2.8.8 - '@coinbase/cdp-sdk@1.44.0(bufferutil@4.1.0)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(utf-8-validate@5.0.10)': + '@coinbase/cdp-sdk@1.36.1(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(utf-8-validate@5.0.10)': dependencies: - '@solana-program/system': 0.10.0(@solana/kit@5.5.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)) - '@solana-program/token': 0.9.0(@solana/kit@5.5.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)) - '@solana/kit': 5.5.1(bufferutil@4.1.0)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(utf-8-validate@5.0.10) - '@solana/web3.js': 1.98.4(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10) - abitype: 1.0.6(typescript@5.9.3)(zod@3.25.76) - axios: 1.13.4 - axios-retry: 4.5.0(axios@1.13.4) - jose: 6.1.3 + '@solana/spl-token': 0.4.14(@solana/web3.js@1.98.4(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10))(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(utf-8-validate@5.0.10) + '@solana/web3.js': 1.98.4(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10) + abitype: 1.0.6(typescript@5.9.2)(zod@3.25.76) + axios: 1.11.0 + axios-retry: 4.5.0(axios@1.11.0) + jose: 6.1.0 md5: 2.3.0 uncrypto: 0.1.3 - viem: 2.45.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + viem: 2.45.1(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) zod: 3.25.76 transitivePeerDependencies: - bufferutil @@ -9413,21 +9972,21 @@ snapshots: - typescript - utf-8-validate - '@coinbase/onchainkit@0.38.19(@farcaster/miniapp-sdk@0.2.2(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(@tanstack/query-core@5.90.20)(@types/react@19.2.10)(@vercel/functions@2.2.13)(bufferutil@4.1.0)(fastestsmallesttextencoderdecoder@1.0.22)(react-dom@19.2.1(react@19.2.1))(react@19.2.1)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.1))(utf-8-validate@5.0.10)(zod@3.25.76)': + '@coinbase/onchainkit@0.38.19(@farcaster/miniapp-sdk@0.1.9(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76))(@tanstack/query-core@5.90.11)(@types/react@19.1.12)(@vercel/functions@2.2.13)(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(react-dom@19.2.1(react@19.2.1))(react@19.2.1)(typescript@5.9.2)(use-sync-external-store@1.4.0(react@19.2.1))(utf-8-validate@5.0.10)(zod@3.25.76)': dependencies: - '@farcaster/frame-sdk': 0.1.13(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) - '@farcaster/miniapp-wagmi-connector': 1.1.0(@farcaster/miniapp-sdk@0.2.2(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(@wagmi/core@2.22.1(@tanstack/query-core@5.90.20)(@types/react@19.2.10)(react@19.2.1)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.1))(viem@2.45.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)))(viem@2.45.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)) - '@tanstack/react-query': 5.90.20(react@19.2.1) - '@wagmi/core': 2.22.1(@tanstack/query-core@5.90.20)(@types/react@19.2.10)(react@19.2.1)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.1))(viem@2.45.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)) + '@farcaster/frame-sdk': 0.1.9(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + '@farcaster/miniapp-wagmi-connector': 1.0.0(@farcaster/miniapp-sdk@0.1.9(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76))(@wagmi/core@2.20.3(@tanstack/query-core@5.90.11)(@types/react@19.1.12)(react@19.2.1)(typescript@5.9.2)(use-sync-external-store@1.4.0(react@19.2.1))(viem@2.40.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76)))(viem@2.45.1(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76)) + '@tanstack/react-query': 5.90.11(react@19.2.1) + '@wagmi/core': 2.20.3(@tanstack/query-core@5.90.11)(@types/react@19.1.12)(react@19.2.1)(typescript@5.9.2)(use-sync-external-store@1.4.0(react@19.2.1))(viem@2.45.1(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76)) clsx: 2.1.1 - graphql: 16.12.0 - graphql-request: 6.1.0(graphql@16.12.0) + graphql: 16.11.0 + graphql-request: 6.1.0(graphql@16.11.0) qrcode: 1.5.4 react: 19.2.1 react-dom: 19.2.1(react@19.2.1) - tailwind-merge: 2.6.1 - viem: 2.45.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) - wagmi: 2.19.5(@tanstack/query-core@5.90.20)(@tanstack/react-query@5.90.20(react@19.2.1))(@types/react@19.2.10)(@vercel/functions@2.2.13)(bufferutil@4.1.0)(fastestsmallesttextencoderdecoder@1.0.22)(react@19.2.1)(typescript@5.9.3)(utf-8-validate@5.0.10)(viem@2.45.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(zod@3.25.76) + tailwind-merge: 2.6.0 + viem: 2.45.1(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + wagmi: 2.19.5(@tanstack/query-core@5.90.11)(@tanstack/react-query@5.90.11(react@19.2.1))(@types/react@19.1.12)(@vercel/functions@2.2.13)(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(react@19.2.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(viem@2.45.1(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76))(zod@3.25.76) transitivePeerDependencies: - '@azure/app-configuration' - '@azure/cosmos' @@ -9473,23 +10032,23 @@ snapshots: clsx: 1.2.1 eth-block-tracker: 7.1.0 eth-json-rpc-filters: 6.0.1 - eventemitter3: 5.0.4 + eventemitter3: 5.0.1 keccak: 3.0.4 - preact: 10.28.3 + preact: 10.27.2 sha.js: 2.4.12 transitivePeerDependencies: - supports-color - '@coinbase/wallet-sdk@4.3.6(@types/react@19.2.10)(bufferutil@4.1.0)(react@19.2.1)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.1))(utf-8-validate@5.0.10)(zod@3.25.76)': + '@coinbase/wallet-sdk@4.3.6(@types/react@19.1.12)(bufferutil@4.0.9)(react@19.2.1)(typescript@5.9.2)(use-sync-external-store@1.4.0(react@19.2.1))(utf-8-validate@5.0.10)(zod@3.25.76)': dependencies: '@noble/hashes': 1.4.0 clsx: 1.2.1 eventemitter3: 5.0.1 idb-keyval: 6.2.1 - ox: 0.6.9(typescript@5.9.3)(zod@3.25.76) + ox: 0.6.9(typescript@5.9.2)(zod@3.25.76) preact: 10.24.2 - viem: 2.45.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) - zustand: 5.0.3(@types/react@19.2.10)(react@19.2.1)(use-sync-external-store@1.4.0(react@19.2.1)) + viem: 2.45.1(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + zustand: 5.0.3(@types/react@19.1.12)(react@19.2.1)(use-sync-external-store@1.4.0(react@19.2.1)) transitivePeerDependencies: - '@types/react' - bufferutil @@ -9500,16 +10059,16 @@ snapshots: - utf-8-validate - zod - '@coinbase/wallet-sdk@4.3.6(@types/react@19.2.10)(bufferutil@4.1.0)(react@19.2.4)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.4))(utf-8-validate@5.0.10)(zod@3.25.76)': + '@coinbase/wallet-sdk@4.3.6(@types/react@19.1.12)(bufferutil@4.0.9)(react@19.2.3)(typescript@5.9.2)(use-sync-external-store@1.4.0(react@19.2.3))(utf-8-validate@5.0.10)(zod@3.25.76)': dependencies: '@noble/hashes': 1.4.0 clsx: 1.2.1 eventemitter3: 5.0.1 idb-keyval: 6.2.1 - ox: 0.6.9(typescript@5.9.3)(zod@3.25.76) + ox: 0.6.9(typescript@5.9.2)(zod@3.25.76) preact: 10.24.2 - viem: 2.45.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) - zustand: 5.0.3(@types/react@19.2.10)(react@19.2.4)(use-sync-external-store@1.4.0(react@19.2.4)) + viem: 2.45.1(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + zustand: 5.0.3(@types/react@19.1.12)(react@19.2.3)(use-sync-external-store@1.4.0(react@19.2.3)) transitivePeerDependencies: - '@types/react' - bufferutil @@ -9520,11 +10079,11 @@ snapshots: - utf-8-validate - zod - '@craftamap/esbuild-plugin-html@0.9.0(bufferutil@4.1.0)(esbuild@0.25.12)(utf-8-validate@5.0.10)': + '@craftamap/esbuild-plugin-html@0.9.0(bufferutil@4.0.9)(esbuild@0.25.9)(utf-8-validate@5.0.10)': dependencies: - esbuild: 0.25.12 - jsdom: 26.1.0(bufferutil@4.1.0)(utf-8-validate@5.0.10) - lodash: 4.17.23 + esbuild: 0.25.9 + jsdom: 26.1.0(bufferutil@4.0.9)(utf-8-validate@5.0.10) + lodash: 4.17.21 transitivePeerDependencies: - bufferutil - canvas @@ -9549,21 +10108,26 @@ snapshots: dependencies: '@csstools/css-tokenizer': 3.0.4 - '@csstools/css-syntax-patches-for-csstree@1.0.26': {} + '@csstools/css-syntax-patches-for-csstree@1.0.22': {} '@csstools/css-tokenizer@3.0.4': {} - '@ecies/ciphers@0.2.5(@noble/ciphers@1.3.0)': + '@ecies/ciphers@0.2.4(@noble/ciphers@1.3.0)': dependencies: '@noble/ciphers': 1.3.0 - '@emnapi/core@1.8.1': + '@emnapi/core@1.5.0': dependencies: '@emnapi/wasi-threads': 1.1.0 tslib: 2.8.1 optional: true - '@emnapi/runtime@1.8.1': + '@emnapi/runtime@1.5.0': + dependencies: + tslib: 2.8.1 + optional: true + + '@emnapi/runtime@1.7.1': dependencies: tslib: 2.8.1 optional: true @@ -9576,211 +10140,131 @@ snapshots: '@es-joy/jsdoccomment@0.50.2': dependencies: '@types/estree': 1.0.8 - '@typescript-eslint/types': 8.54.0 + '@typescript-eslint/types': 8.49.0 comment-parser: 1.4.1 - esquery: 1.7.0 + esquery: 1.6.0 jsdoc-type-pratt-parser: 4.1.0 - '@esbuild/aix-ppc64@0.25.12': - optional: true - - '@esbuild/aix-ppc64@0.27.2': - optional: true - - '@esbuild/android-arm64@0.25.12': - optional: true - - '@esbuild/android-arm64@0.27.2': - optional: true - - '@esbuild/android-arm@0.25.12': - optional: true - - '@esbuild/android-arm@0.27.2': - optional: true - - '@esbuild/android-x64@0.25.12': + '@esbuild/aix-ppc64@0.25.9': optional: true - '@esbuild/android-x64@0.27.2': + '@esbuild/android-arm64@0.25.9': optional: true - '@esbuild/darwin-arm64@0.25.12': + '@esbuild/android-arm@0.25.9': optional: true - '@esbuild/darwin-arm64@0.27.2': + '@esbuild/android-x64@0.25.9': optional: true - '@esbuild/darwin-x64@0.25.12': + '@esbuild/darwin-arm64@0.25.9': optional: true - '@esbuild/darwin-x64@0.27.2': + '@esbuild/darwin-x64@0.25.9': optional: true - '@esbuild/freebsd-arm64@0.25.12': + '@esbuild/freebsd-arm64@0.25.9': optional: true - '@esbuild/freebsd-arm64@0.27.2': + '@esbuild/freebsd-x64@0.25.9': optional: true - '@esbuild/freebsd-x64@0.25.12': + '@esbuild/linux-arm64@0.25.9': optional: true - '@esbuild/freebsd-x64@0.27.2': + '@esbuild/linux-arm@0.25.9': optional: true - '@esbuild/linux-arm64@0.25.12': + '@esbuild/linux-ia32@0.25.9': optional: true - '@esbuild/linux-arm64@0.27.2': + '@esbuild/linux-loong64@0.25.9': optional: true - '@esbuild/linux-arm@0.25.12': + '@esbuild/linux-mips64el@0.25.9': optional: true - '@esbuild/linux-arm@0.27.2': + '@esbuild/linux-ppc64@0.25.9': optional: true - '@esbuild/linux-ia32@0.25.12': + '@esbuild/linux-riscv64@0.25.9': optional: true - '@esbuild/linux-ia32@0.27.2': + '@esbuild/linux-s390x@0.25.9': optional: true - '@esbuild/linux-loong64@0.25.12': + '@esbuild/linux-x64@0.25.9': optional: true - '@esbuild/linux-loong64@0.27.2': + '@esbuild/netbsd-arm64@0.25.9': optional: true - '@esbuild/linux-mips64el@0.25.12': + '@esbuild/netbsd-x64@0.25.9': optional: true - '@esbuild/linux-mips64el@0.27.2': + '@esbuild/openbsd-arm64@0.25.9': optional: true - '@esbuild/linux-ppc64@0.25.12': + '@esbuild/openbsd-x64@0.25.9': optional: true - '@esbuild/linux-ppc64@0.27.2': + '@esbuild/openharmony-arm64@0.25.9': optional: true - '@esbuild/linux-riscv64@0.25.12': + '@esbuild/sunos-x64@0.25.9': optional: true - '@esbuild/linux-riscv64@0.27.2': + '@esbuild/win32-arm64@0.25.9': optional: true - '@esbuild/linux-s390x@0.25.12': + '@esbuild/win32-ia32@0.25.9': optional: true - '@esbuild/linux-s390x@0.27.2': + '@esbuild/win32-x64@0.25.9': optional: true - '@esbuild/linux-x64@0.25.12': - optional: true - - '@esbuild/linux-x64@0.27.2': - optional: true - - '@esbuild/netbsd-arm64@0.25.12': - optional: true - - '@esbuild/netbsd-arm64@0.27.2': - optional: true - - '@esbuild/netbsd-x64@0.25.12': - optional: true - - '@esbuild/netbsd-x64@0.27.2': - optional: true - - '@esbuild/openbsd-arm64@0.25.12': - optional: true - - '@esbuild/openbsd-arm64@0.27.2': - optional: true - - '@esbuild/openbsd-x64@0.25.12': - optional: true - - '@esbuild/openbsd-x64@0.27.2': - optional: true - - '@esbuild/openharmony-arm64@0.25.12': - optional: true - - '@esbuild/openharmony-arm64@0.27.2': - optional: true - - '@esbuild/sunos-x64@0.25.12': - optional: true - - '@esbuild/sunos-x64@0.27.2': - optional: true - - '@esbuild/win32-arm64@0.25.12': - optional: true - - '@esbuild/win32-arm64@0.27.2': - optional: true - - '@esbuild/win32-ia32@0.25.12': - optional: true - - '@esbuild/win32-ia32@0.27.2': - optional: true - - '@esbuild/win32-x64@0.25.12': - optional: true - - '@esbuild/win32-x64@0.27.2': - optional: true - - '@eslint-community/eslint-utils@4.9.1(eslint@9.39.2(jiti@2.6.1))': + '@eslint-community/eslint-utils@4.8.0(eslint@9.34.0(jiti@2.6.1))': dependencies: - eslint: 9.39.2(jiti@2.6.1) + eslint: 9.34.0(jiti@2.6.1) eslint-visitor-keys: 3.4.3 - '@eslint-community/regexpp@4.12.2': {} + '@eslint-community/regexpp@4.12.1': {} - '@eslint/config-array@0.21.1': + '@eslint/config-array@0.21.0': dependencies: - '@eslint/object-schema': 2.1.7 + '@eslint/object-schema': 2.1.6 debug: 4.4.3 minimatch: 3.1.2 transitivePeerDependencies: - supports-color - '@eslint/config-helpers@0.4.2': - dependencies: - '@eslint/core': 0.17.0 + '@eslint/config-helpers@0.3.1': {} - '@eslint/core@0.17.0': + '@eslint/core@0.15.2': dependencies: '@types/json-schema': 7.0.15 - '@eslint/eslintrc@3.3.3': + '@eslint/eslintrc@3.3.1': dependencies: ajv: 6.12.6 - debug: 4.4.3 + debug: 4.4.1 espree: 10.4.0 globals: 14.0.0 ignore: 5.3.2 import-fresh: 3.3.1 - js-yaml: 4.1.1 + js-yaml: 4.1.0 minimatch: 3.1.2 strip-json-comments: 3.1.1 transitivePeerDependencies: - supports-color - '@eslint/js@9.39.2': {} + '@eslint/js@9.34.0': {} - '@eslint/object-schema@2.1.7': {} + '@eslint/object-schema@2.1.6': {} - '@eslint/plugin-kit@0.4.1': + '@eslint/plugin-kit@0.3.5': dependencies: - '@eslint/core': 0.17.0 + '@eslint/core': 0.15.2 levn: 0.4.1 '@ethereumjs/common@3.2.0': @@ -9803,13 +10287,16 @@ snapshots: ethereum-cryptography: 2.2.1 micro-ftch: 0.3.1 - '@farcaster/frame-sdk@0.1.13(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)': + '@exodus/bytes@1.8.0': + optional: true + + '@farcaster/frame-sdk@0.1.9(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76)': dependencies: - '@farcaster/miniapp-sdk': 0.2.2(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) - '@farcaster/quick-auth': 0.0.8(typescript@5.9.3) + '@farcaster/miniapp-sdk': 0.1.9(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + '@farcaster/quick-auth': 0.0.6(typescript@5.9.2) comlink: 4.4.2 - eventemitter3: 5.0.4 - ox: 0.4.4(typescript@5.9.3)(zod@3.25.76) + eventemitter3: 5.0.1 + ox: 0.4.4(typescript@5.9.2)(zod@3.25.76) transitivePeerDependencies: - bufferutil - encoding @@ -9817,10 +10304,10 @@ snapshots: - utf-8-validate - zod - '@farcaster/miniapp-core@0.5.0(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)': + '@farcaster/miniapp-core@0.3.8(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)': dependencies: - '@solana/web3.js': 1.98.4(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10) - ox: 0.4.4(typescript@5.9.3)(zod@3.25.76) + '@solana/web3.js': 1.98.4(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10) + ox: 0.4.4(typescript@5.9.2)(zod@3.25.76) zod: 3.25.76 transitivePeerDependencies: - bufferutil @@ -9828,13 +10315,13 @@ snapshots: - typescript - utf-8-validate - '@farcaster/miniapp-sdk@0.2.2(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)': + '@farcaster/miniapp-sdk@0.1.9(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76)': dependencies: - '@farcaster/miniapp-core': 0.5.0(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10) - '@farcaster/quick-auth': 0.0.6(typescript@5.9.3) + '@farcaster/miniapp-core': 0.3.8(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10) + '@farcaster/quick-auth': 0.0.6(typescript@5.9.2) comlink: 4.4.2 - eventemitter3: 5.0.4 - ox: 0.4.4(typescript@5.9.3)(zod@3.25.76) + eventemitter3: 5.0.1 + ox: 0.4.4(typescript@5.9.2)(zod@3.25.76) transitivePeerDependencies: - bufferutil - encoding @@ -9842,47 +10329,84 @@ snapshots: - utf-8-validate - zod - '@farcaster/miniapp-wagmi-connector@1.1.0(@farcaster/miniapp-sdk@0.2.2(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(@wagmi/core@2.22.1(@tanstack/query-core@5.90.20)(@types/react@19.2.10)(react@19.2.1)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.1))(viem@2.45.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)))(viem@2.45.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))': + '@farcaster/miniapp-wagmi-connector@1.0.0(@farcaster/miniapp-sdk@0.1.9(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76))(@wagmi/core@2.20.3(@tanstack/query-core@5.90.11)(@types/react@19.1.12)(react@19.2.1)(typescript@5.9.2)(use-sync-external-store@1.4.0(react@19.2.1))(viem@2.40.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76)))(viem@2.45.1(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76))': + dependencies: + '@farcaster/miniapp-sdk': 0.1.9(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + '@wagmi/core': 2.20.3(@tanstack/query-core@5.90.11)(@types/react@19.1.12)(react@19.2.1)(typescript@5.9.2)(use-sync-external-store@1.4.0(react@19.2.1))(viem@2.40.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76)) + viem: 2.45.1(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + + '@farcaster/quick-auth@0.0.6(typescript@5.9.2)': + dependencies: + jose: 5.10.0 + typescript: 5.9.2 + zod: 3.25.76 + + '@fastify/ajv-compiler@4.0.5': + dependencies: + ajv: 8.17.1 + ajv-formats: 3.0.1(ajv@8.17.1) + fast-uri: 3.1.0 + + '@fastify/error@4.2.0': {} + + '@fastify/fast-json-stringify-compiler@5.0.3': + dependencies: + fast-json-stringify: 6.3.0 + + '@fastify/forwarded@3.0.1': {} + + '@fastify/merge-json-schemas@0.2.1': + dependencies: + dequal: 2.0.3 + + '@fastify/proxy-addr@5.1.0': dependencies: - '@farcaster/miniapp-sdk': 0.2.2(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) - '@wagmi/core': 2.22.1(@tanstack/query-core@5.90.20)(@types/react@19.2.10)(react@19.2.1)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.1))(viem@2.45.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)) - viem: 2.45.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + '@fastify/forwarded': 3.0.1 + ipaddr.js: 2.3.0 - '@farcaster/quick-auth@0.0.6(typescript@5.9.3)': + '@gemini-wallet/core@0.2.0(viem@2.37.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76))': dependencies: - jose: 5.10.0 - typescript: 5.9.3 - zod: 3.25.76 + '@metamask/rpc-errors': 7.0.2 + eventemitter3: 5.0.1 + viem: 2.37.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + transitivePeerDependencies: + - supports-color - '@farcaster/quick-auth@0.0.8(typescript@5.9.3)': + '@gemini-wallet/core@0.2.0(viem@2.40.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76))': dependencies: - jose: 5.10.0 - typescript: 5.9.3 - zod: 3.25.76 + '@metamask/rpc-errors': 7.0.2 + eventemitter3: 5.0.1 + viem: 2.40.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + transitivePeerDependencies: + - supports-color - '@gemini-wallet/core@0.2.0(viem@2.45.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))': + '@gemini-wallet/core@0.3.2(viem@2.40.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76))': dependencies: '@metamask/rpc-errors': 7.0.2 eventemitter3: 5.0.1 - viem: 2.45.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + viem: 2.40.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) transitivePeerDependencies: - supports-color - '@gemini-wallet/core@0.3.2(viem@2.45.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))': + '@gemini-wallet/core@0.3.2(viem@2.45.1(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76))': dependencies: '@metamask/rpc-errors': 7.0.2 eventemitter3: 5.0.1 - viem: 2.45.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + viem: 2.45.1(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) transitivePeerDependencies: - supports-color - '@graphql-typed-document-node/core@3.2.0(graphql@16.12.0)': + '@graphql-typed-document-node/core@3.2.0(graphql@16.11.0)': + dependencies: + graphql: 16.11.0 + + '@heroicons/react@2.2.0(react@19.2.3)': dependencies: - graphql: 16.12.0 + react: 19.2.3 - '@heroicons/react@2.2.0(react@19.2.4)': + '@hono/node-server@1.19.1(hono@4.10.7)': dependencies: - react: 19.2.4 + hono: 4.10.7 '@hono/node-server@1.19.9(hono@4.11.7)': dependencies: @@ -9984,7 +10508,7 @@ snapshots: '@img/sharp-wasm32@0.34.5': dependencies: - '@emnapi/runtime': 1.8.1 + '@emnapi/runtime': 1.7.1 optional: true '@img/sharp-win32-arm64@0.34.5': @@ -9996,48 +10520,57 @@ snapshots: '@img/sharp-win32-x64@0.34.5': optional: true - '@inquirer/external-editor@1.0.3(@types/node@22.19.7)': + '@inquirer/external-editor@1.0.3(@types/node@22.18.0)': dependencies: chardet: 2.1.1 iconv-lite: 0.7.2 optionalDependencies: - '@types/node': 22.19.7 + '@types/node': 22.18.0 + + '@isaacs/cliui@8.0.2': + dependencies: + string-width: 5.1.2 + string-width-cjs: string-width@4.2.3 + strip-ansi: 7.1.0 + strip-ansi-cjs: strip-ansi@6.0.1 + wrap-ansi: 8.1.0 + wrap-ansi-cjs: wrap-ansi@7.0.0 '@jridgewell/gen-mapping@0.3.13': dependencies: '@jridgewell/sourcemap-codec': 1.5.5 - '@jridgewell/trace-mapping': 0.3.31 + '@jridgewell/trace-mapping': 0.3.30 '@jridgewell/remapping@2.3.5': dependencies: '@jridgewell/gen-mapping': 0.3.13 - '@jridgewell/trace-mapping': 0.3.31 + '@jridgewell/trace-mapping': 0.3.30 '@jridgewell/resolve-uri@3.1.2': {} '@jridgewell/sourcemap-codec@1.5.5': {} - '@jridgewell/trace-mapping@0.3.31': + '@jridgewell/trace-mapping@0.3.30': dependencies: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.5 - '@lit-labs/ssr-dom-shim@1.5.1': {} + '@lit-labs/ssr-dom-shim@1.4.0': {} - '@lit/reactive-element@2.1.2': + '@lit/reactive-element@2.1.1': dependencies: - '@lit-labs/ssr-dom-shim': 1.5.1 + '@lit-labs/ssr-dom-shim': 1.4.0 '@manypkg/find-root@1.1.0': dependencies: - '@babel/runtime': 7.28.6 + '@babel/runtime': 7.28.3 '@types/node': 12.20.55 find-up: 4.1.0 fs-extra: 8.1.0 '@manypkg/get-packages@1.1.3': dependencies: - '@babel/runtime': 7.28.6 + '@babel/runtime': 7.28.3 '@changesets/types': 4.1.0 '@manypkg/find-root': 1.1.0 fs-extra: 8.1.0 @@ -10084,7 +10617,7 @@ snapshots: '@metamask/onboarding@1.0.1': dependencies: - bowser: 2.13.1 + bowser: 2.12.1 '@metamask/providers@16.1.0': dependencies: @@ -10112,7 +10645,7 @@ snapshots: '@metamask/rpc-errors@7.0.2': dependencies: - '@metamask/utils': 11.9.0 + '@metamask/utils': 11.7.0 fast-safe-stringify: 2.1.1 transitivePeerDependencies: - supports-color @@ -10125,45 +10658,91 @@ snapshots: dependencies: openapi-fetch: 0.13.8 - '@metamask/sdk-communication-layer@0.33.1(cross-fetch@4.1.0)(eciesjs@0.4.17)(eventemitter2@6.4.9)(readable-stream@3.6.2)(socket.io-client@4.8.3(bufferutil@4.1.0)(utf-8-validate@5.0.10))': + '@metamask/sdk-communication-layer@0.32.0(cross-fetch@4.1.0)(eciesjs@0.4.15)(eventemitter2@6.4.9)(readable-stream@3.6.2)(socket.io-client@4.8.1(bufferutil@4.0.9)(utf-8-validate@5.0.10))': + dependencies: + bufferutil: 4.0.9 + cross-fetch: 4.1.0 + date-fns: 2.30.0 + debug: 4.4.3 + eciesjs: 0.4.15 + eventemitter2: 6.4.9 + readable-stream: 3.6.2 + socket.io-client: 4.8.1(bufferutil@4.0.9)(utf-8-validate@5.0.10) + utf-8-validate: 5.0.10 + uuid: 8.3.2 + transitivePeerDependencies: + - supports-color + + '@metamask/sdk-communication-layer@0.33.1(cross-fetch@4.1.0)(eciesjs@0.4.15)(eventemitter2@6.4.9)(readable-stream@3.6.2)(socket.io-client@4.8.1(bufferutil@4.0.9)(utf-8-validate@5.0.10))': dependencies: '@metamask/sdk-analytics': 0.0.5 - bufferutil: 4.1.0 + bufferutil: 4.0.9 cross-fetch: 4.1.0 date-fns: 2.30.0 debug: 4.3.4 - eciesjs: 0.4.17 + eciesjs: 0.4.15 eventemitter2: 6.4.9 readable-stream: 3.6.2 - socket.io-client: 4.8.3(bufferutil@4.1.0)(utf-8-validate@5.0.10) + socket.io-client: 4.8.1(bufferutil@4.0.9)(utf-8-validate@5.0.10) utf-8-validate: 5.0.10 uuid: 8.3.2 transitivePeerDependencies: - supports-color + '@metamask/sdk-install-modal-web@0.32.0': + dependencies: + '@paulmillr/qr': 0.2.1 + '@metamask/sdk-install-modal-web@0.32.1': dependencies: '@paulmillr/qr': 0.2.1 - '@metamask/sdk@0.33.1(bufferutil@4.1.0)(utf-8-validate@5.0.10)': + '@metamask/sdk@0.32.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)': + dependencies: + '@babel/runtime': 7.28.3 + '@metamask/onboarding': 1.0.1 + '@metamask/providers': 16.1.0 + '@metamask/sdk-communication-layer': 0.32.0(cross-fetch@4.1.0)(eciesjs@0.4.15)(eventemitter2@6.4.9)(readable-stream@3.6.2)(socket.io-client@4.8.1(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + '@metamask/sdk-install-modal-web': 0.32.0 + '@paulmillr/qr': 0.2.1 + bowser: 2.12.1 + cross-fetch: 4.1.0 + debug: 4.4.3 + eciesjs: 0.4.15 + eth-rpc-errors: 4.0.3 + eventemitter2: 6.4.9 + obj-multiplex: 1.0.0 + pump: 3.0.3 + readable-stream: 3.6.2 + socket.io-client: 4.8.1(bufferutil@4.0.9)(utf-8-validate@5.0.10) + tslib: 2.8.1 + util: 0.12.5 + uuid: 8.3.2 + transitivePeerDependencies: + - bufferutil + - encoding + - supports-color + - utf-8-validate + + '@metamask/sdk@0.33.1(bufferutil@4.0.9)(utf-8-validate@5.0.10)': dependencies: - '@babel/runtime': 7.28.6 + '@babel/runtime': 7.28.3 '@metamask/onboarding': 1.0.1 '@metamask/providers': 16.1.0 '@metamask/sdk-analytics': 0.0.5 - '@metamask/sdk-communication-layer': 0.33.1(cross-fetch@4.1.0)(eciesjs@0.4.17)(eventemitter2@6.4.9)(readable-stream@3.6.2)(socket.io-client@4.8.3(bufferutil@4.1.0)(utf-8-validate@5.0.10)) + '@metamask/sdk-communication-layer': 0.33.1(cross-fetch@4.1.0)(eciesjs@0.4.15)(eventemitter2@6.4.9)(readable-stream@3.6.2)(socket.io-client@4.8.1(bufferutil@4.0.9)(utf-8-validate@5.0.10)) '@metamask/sdk-install-modal-web': 0.32.1 '@paulmillr/qr': 0.2.1 - bowser: 2.13.1 + bowser: 2.12.1 cross-fetch: 4.1.0 debug: 4.3.4 - eciesjs: 0.4.17 + eciesjs: 0.4.15 eth-rpc-errors: 4.0.3 eventemitter2: 6.4.9 obj-multiplex: 1.0.0 pump: 3.0.3 readable-stream: 3.6.2 - socket.io-client: 4.8.3(bufferutil@4.1.0)(utf-8-validate@5.0.10) + socket.io-client: 4.8.1(bufferutil@4.0.9)(utf-8-validate@5.0.10) tslib: 2.8.1 util: 0.12.5 uuid: 8.3.2 @@ -10175,16 +10754,16 @@ snapshots: '@metamask/superstruct@3.2.1': {} - '@metamask/utils@11.9.0': + '@metamask/utils@11.7.0': dependencies: '@ethereumjs/tx': 4.2.0 '@metamask/superstruct': 3.2.1 '@noble/hashes': 1.8.0 '@scure/base': 1.2.6 '@types/debug': 4.1.12 - '@types/lodash': 4.17.23 + '@types/lodash': 4.17.20 debug: 4.4.3 - lodash: 4.17.23 + lodash: 4.17.21 pony-cause: 2.1.11 semver: 7.7.3 uuid: 9.0.1 @@ -10208,7 +10787,7 @@ snapshots: '@noble/hashes': 1.8.0 '@scure/base': 1.2.6 '@types/debug': 4.1.12 - debug: 4.3.4 + debug: 4.4.3 pony-cause: 2.1.11 semver: 7.7.3 uuid: 9.0.1 @@ -10222,14 +10801,14 @@ snapshots: '@noble/hashes': 1.8.0 '@scure/base': 1.2.6 '@types/debug': 4.1.12 - debug: 4.4.1 + debug: 4.4.3 pony-cause: 2.1.11 semver: 7.7.3 uuid: 9.0.1 transitivePeerDependencies: - supports-color - '@modelcontextprotocol/sdk@1.25.3(hono@4.11.7)(zod@3.25.76)': + '@modelcontextprotocol/sdk@1.26.0(zod@3.25.76)': dependencies: '@hono/node-server': 1.19.9(hono@4.11.7) ajv: 8.17.1 @@ -10240,7 +10819,8 @@ snapshots: eventsource: 3.0.7 eventsource-parser: 3.0.6 express: 5.2.1 - express-rate-limit: 7.5.1(express@5.2.1) + express-rate-limit: 8.2.1(express@5.2.1) + hono: 4.11.7 jose: 6.1.3 json-schema-typed: 8.0.2 pkce-challenge: 5.0.1 @@ -10248,44 +10828,43 @@ snapshots: zod: 3.25.76 zod-to-json-schema: 3.25.1(zod@3.25.76) transitivePeerDependencies: - - hono - supports-color '@napi-rs/wasm-runtime@0.2.12': dependencies: - '@emnapi/core': 1.8.1 - '@emnapi/runtime': 1.8.1 - '@tybys/wasm-util': 0.10.1 + '@emnapi/core': 1.5.0 + '@emnapi/runtime': 1.5.0 + '@tybys/wasm-util': 0.10.0 optional: true - '@next/env@16.1.6': {} + '@next/env@16.0.10': {} '@next/eslint-plugin-next@16.0.6': dependencies: fast-glob: 3.3.1 - '@next/swc-darwin-arm64@16.1.6': + '@next/swc-darwin-arm64@16.0.10': optional: true - '@next/swc-darwin-x64@16.1.6': + '@next/swc-darwin-x64@16.0.10': optional: true - '@next/swc-linux-arm64-gnu@16.1.6': + '@next/swc-linux-arm64-gnu@16.0.10': optional: true - '@next/swc-linux-arm64-musl@16.1.6': + '@next/swc-linux-arm64-musl@16.0.10': optional: true - '@next/swc-linux-x64-gnu@16.1.6': + '@next/swc-linux-x64-gnu@16.0.10': optional: true - '@next/swc-linux-x64-musl@16.1.6': + '@next/swc-linux-x64-musl@16.0.10': optional: true - '@next/swc-win32-arm64-msvc@16.1.6': + '@next/swc-win32-arm64-msvc@16.0.10': optional: true - '@next/swc-win32-x64-msvc@16.1.6': + '@next/swc-win32-x64-msvc@16.0.10': optional: true '@noble/ciphers@1.2.1': {} @@ -10336,43 +10915,48 @@ snapshots: '@nodelib/fs.walk@1.2.8': dependencies: '@nodelib/fs.scandir': 2.1.5 - fastq: 1.20.1 + fastq: 1.19.1 '@nolyfill/is-core-module@1.0.39': {} '@paulmillr/qr@0.2.1': {} + '@pinojs/redact@0.4.0': {} + + '@pkgjs/parseargs@0.11.0': + optional: true + '@pkgr/core@0.2.9': {} - '@reown/appkit-common@1.7.8(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.22.4)': + '@reown/appkit-common@1.7.8(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.22.4)': dependencies: big.js: 6.2.2 dayjs: 1.11.13 - viem: 2.45.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.22.4) + viem: 2.45.1(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.22.4) transitivePeerDependencies: - bufferutil - typescript - utf-8-validate - zod - '@reown/appkit-common@1.7.8(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)': + '@reown/appkit-common@1.7.8(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76)': dependencies: big.js: 6.2.2 dayjs: 1.11.13 - viem: 2.45.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + viem: 2.45.1(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) transitivePeerDependencies: - bufferutil - typescript - utf-8-validate - zod - '@reown/appkit-controllers@1.7.8(@types/react@19.2.10)(@vercel/functions@2.2.13)(bufferutil@4.1.0)(react@19.2.1)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)': + '@reown/appkit-controllers@1.7.8(@types/react@19.1.12)(@vercel/functions@2.2.13)(bufferutil@4.0.9)(react@19.2.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76)': dependencies: - '@reown/appkit-common': 1.7.8(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) - '@reown/appkit-wallet': 1.7.8(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10) - '@walletconnect/universal-provider': 2.21.0(@vercel/functions@2.2.13)(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) - valtio: 1.13.2(@types/react@19.2.10)(react@19.2.1) - viem: 2.45.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + '@reown/appkit-common': 1.7.8(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + '@reown/appkit-wallet': 1.7.8(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10) + '@walletconnect/universal-provider': 2.21.0(@vercel/functions@2.2.13)(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + valtio: 1.13.2(@types/react@19.1.12)(react@19.2.1) + viem: 2.45.1(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) transitivePeerDependencies: - '@azure/app-configuration' - '@azure/cosmos' @@ -10401,13 +10985,13 @@ snapshots: - utf-8-validate - zod - '@reown/appkit-controllers@1.7.8(@types/react@19.2.10)(@vercel/functions@2.2.13)(bufferutil@4.1.0)(react@19.2.4)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)': + '@reown/appkit-controllers@1.7.8(@types/react@19.1.12)(@vercel/functions@2.2.13)(bufferutil@4.0.9)(react@19.2.3)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76)': dependencies: - '@reown/appkit-common': 1.7.8(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) - '@reown/appkit-wallet': 1.7.8(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10) - '@walletconnect/universal-provider': 2.21.0(@vercel/functions@2.2.13)(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) - valtio: 1.13.2(@types/react@19.2.10)(react@19.2.4) - viem: 2.45.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + '@reown/appkit-common': 1.7.8(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + '@reown/appkit-wallet': 1.7.8(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10) + '@walletconnect/universal-provider': 2.21.0(@vercel/functions@2.2.13)(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + valtio: 1.13.2(@types/react@19.1.12)(react@19.2.3) + viem: 2.45.1(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) transitivePeerDependencies: - '@azure/app-configuration' - '@azure/cosmos' @@ -10436,14 +11020,14 @@ snapshots: - utf-8-validate - zod - '@reown/appkit-pay@1.7.8(@types/react@19.2.10)(@vercel/functions@2.2.13)(bufferutil@4.1.0)(react@19.2.1)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)': + '@reown/appkit-pay@1.7.8(@types/react@19.1.12)(@vercel/functions@2.2.13)(bufferutil@4.0.9)(react@19.2.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76)': dependencies: - '@reown/appkit-common': 1.7.8(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) - '@reown/appkit-controllers': 1.7.8(@types/react@19.2.10)(@vercel/functions@2.2.13)(bufferutil@4.1.0)(react@19.2.1)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) - '@reown/appkit-ui': 1.7.8(@types/react@19.2.10)(@vercel/functions@2.2.13)(bufferutil@4.1.0)(react@19.2.1)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) - '@reown/appkit-utils': 1.7.8(@types/react@19.2.10)(@vercel/functions@2.2.13)(bufferutil@4.1.0)(react@19.2.1)(typescript@5.9.3)(utf-8-validate@5.0.10)(valtio@1.13.2(@types/react@19.2.10)(react@19.2.1))(zod@3.25.76) + '@reown/appkit-common': 1.7.8(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + '@reown/appkit-controllers': 1.7.8(@types/react@19.1.12)(@vercel/functions@2.2.13)(bufferutil@4.0.9)(react@19.2.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + '@reown/appkit-ui': 1.7.8(@types/react@19.1.12)(@vercel/functions@2.2.13)(bufferutil@4.0.9)(react@19.2.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + '@reown/appkit-utils': 1.7.8(@types/react@19.1.12)(@vercel/functions@2.2.13)(bufferutil@4.0.9)(react@19.2.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(valtio@1.13.2(@types/react@19.1.12)(react@19.2.1))(zod@3.25.76) lit: 3.3.0 - valtio: 1.13.2(@types/react@19.2.10)(react@19.2.1) + valtio: 1.13.2(@types/react@19.1.12)(react@19.2.1) transitivePeerDependencies: - '@azure/app-configuration' - '@azure/cosmos' @@ -10472,14 +11056,14 @@ snapshots: - utf-8-validate - zod - '@reown/appkit-pay@1.7.8(@types/react@19.2.10)(@vercel/functions@2.2.13)(bufferutil@4.1.0)(react@19.2.4)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)': + '@reown/appkit-pay@1.7.8(@types/react@19.1.12)(@vercel/functions@2.2.13)(bufferutil@4.0.9)(react@19.2.3)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76)': dependencies: - '@reown/appkit-common': 1.7.8(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) - '@reown/appkit-controllers': 1.7.8(@types/react@19.2.10)(@vercel/functions@2.2.13)(bufferutil@4.1.0)(react@19.2.4)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) - '@reown/appkit-ui': 1.7.8(@types/react@19.2.10)(@vercel/functions@2.2.13)(bufferutil@4.1.0)(react@19.2.4)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) - '@reown/appkit-utils': 1.7.8(@types/react@19.2.10)(@vercel/functions@2.2.13)(bufferutil@4.1.0)(react@19.2.4)(typescript@5.9.3)(utf-8-validate@5.0.10)(valtio@1.13.2(@types/react@19.2.10)(react@19.2.4))(zod@3.25.76) + '@reown/appkit-common': 1.7.8(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + '@reown/appkit-controllers': 1.7.8(@types/react@19.1.12)(@vercel/functions@2.2.13)(bufferutil@4.0.9)(react@19.2.3)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + '@reown/appkit-ui': 1.7.8(@types/react@19.1.12)(@vercel/functions@2.2.13)(bufferutil@4.0.9)(react@19.2.3)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + '@reown/appkit-utils': 1.7.8(@types/react@19.1.12)(@vercel/functions@2.2.13)(bufferutil@4.0.9)(react@19.2.3)(typescript@5.9.2)(utf-8-validate@5.0.10)(valtio@1.13.2(@types/react@19.1.12)(react@19.2.3))(zod@3.25.76) lit: 3.3.0 - valtio: 1.13.2(@types/react@19.2.10)(react@19.2.4) + valtio: 1.13.2(@types/react@19.1.12)(react@19.2.3) transitivePeerDependencies: - '@azure/app-configuration' - '@azure/cosmos' @@ -10512,13 +11096,13 @@ snapshots: dependencies: buffer: 6.0.3 - '@reown/appkit-scaffold-ui@1.7.8(@types/react@19.2.10)(@vercel/functions@2.2.13)(bufferutil@4.1.0)(react@19.2.1)(typescript@5.9.3)(utf-8-validate@5.0.10)(valtio@1.13.2(@types/react@19.2.10)(react@19.2.1))(zod@3.25.76)': + '@reown/appkit-scaffold-ui@1.7.8(@types/react@19.1.12)(@vercel/functions@2.2.13)(bufferutil@4.0.9)(react@19.2.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(valtio@1.13.2(@types/react@19.1.12)(react@19.2.1))(zod@3.25.76)': dependencies: - '@reown/appkit-common': 1.7.8(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) - '@reown/appkit-controllers': 1.7.8(@types/react@19.2.10)(@vercel/functions@2.2.13)(bufferutil@4.1.0)(react@19.2.1)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) - '@reown/appkit-ui': 1.7.8(@types/react@19.2.10)(@vercel/functions@2.2.13)(bufferutil@4.1.0)(react@19.2.1)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) - '@reown/appkit-utils': 1.7.8(@types/react@19.2.10)(@vercel/functions@2.2.13)(bufferutil@4.1.0)(react@19.2.1)(typescript@5.9.3)(utf-8-validate@5.0.10)(valtio@1.13.2(@types/react@19.2.10)(react@19.2.1))(zod@3.25.76) - '@reown/appkit-wallet': 1.7.8(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10) + '@reown/appkit-common': 1.7.8(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + '@reown/appkit-controllers': 1.7.8(@types/react@19.1.12)(@vercel/functions@2.2.13)(bufferutil@4.0.9)(react@19.2.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + '@reown/appkit-ui': 1.7.8(@types/react@19.1.12)(@vercel/functions@2.2.13)(bufferutil@4.0.9)(react@19.2.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + '@reown/appkit-utils': 1.7.8(@types/react@19.1.12)(@vercel/functions@2.2.13)(bufferutil@4.0.9)(react@19.2.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(valtio@1.13.2(@types/react@19.1.12)(react@19.2.1))(zod@3.25.76) + '@reown/appkit-wallet': 1.7.8(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10) lit: 3.3.0 transitivePeerDependencies: - '@azure/app-configuration' @@ -10549,13 +11133,13 @@ snapshots: - valtio - zod - '@reown/appkit-scaffold-ui@1.7.8(@types/react@19.2.10)(@vercel/functions@2.2.13)(bufferutil@4.1.0)(react@19.2.4)(typescript@5.9.3)(utf-8-validate@5.0.10)(valtio@1.13.2(@types/react@19.2.10)(react@19.2.4))(zod@3.25.76)': + '@reown/appkit-scaffold-ui@1.7.8(@types/react@19.1.12)(@vercel/functions@2.2.13)(bufferutil@4.0.9)(react@19.2.3)(typescript@5.9.2)(utf-8-validate@5.0.10)(valtio@1.13.2(@types/react@19.1.12)(react@19.2.3))(zod@3.25.76)': dependencies: - '@reown/appkit-common': 1.7.8(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) - '@reown/appkit-controllers': 1.7.8(@types/react@19.2.10)(@vercel/functions@2.2.13)(bufferutil@4.1.0)(react@19.2.4)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) - '@reown/appkit-ui': 1.7.8(@types/react@19.2.10)(@vercel/functions@2.2.13)(bufferutil@4.1.0)(react@19.2.4)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) - '@reown/appkit-utils': 1.7.8(@types/react@19.2.10)(@vercel/functions@2.2.13)(bufferutil@4.1.0)(react@19.2.4)(typescript@5.9.3)(utf-8-validate@5.0.10)(valtio@1.13.2(@types/react@19.2.10)(react@19.2.4))(zod@3.25.76) - '@reown/appkit-wallet': 1.7.8(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10) + '@reown/appkit-common': 1.7.8(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + '@reown/appkit-controllers': 1.7.8(@types/react@19.1.12)(@vercel/functions@2.2.13)(bufferutil@4.0.9)(react@19.2.3)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + '@reown/appkit-ui': 1.7.8(@types/react@19.1.12)(@vercel/functions@2.2.13)(bufferutil@4.0.9)(react@19.2.3)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + '@reown/appkit-utils': 1.7.8(@types/react@19.1.12)(@vercel/functions@2.2.13)(bufferutil@4.0.9)(react@19.2.3)(typescript@5.9.2)(utf-8-validate@5.0.10)(valtio@1.13.2(@types/react@19.1.12)(react@19.2.3))(zod@3.25.76) + '@reown/appkit-wallet': 1.7.8(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10) lit: 3.3.0 transitivePeerDependencies: - '@azure/app-configuration' @@ -10586,11 +11170,11 @@ snapshots: - valtio - zod - '@reown/appkit-ui@1.7.8(@types/react@19.2.10)(@vercel/functions@2.2.13)(bufferutil@4.1.0)(react@19.2.1)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)': + '@reown/appkit-ui@1.7.8(@types/react@19.1.12)(@vercel/functions@2.2.13)(bufferutil@4.0.9)(react@19.2.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76)': dependencies: - '@reown/appkit-common': 1.7.8(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) - '@reown/appkit-controllers': 1.7.8(@types/react@19.2.10)(@vercel/functions@2.2.13)(bufferutil@4.1.0)(react@19.2.1)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) - '@reown/appkit-wallet': 1.7.8(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10) + '@reown/appkit-common': 1.7.8(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + '@reown/appkit-controllers': 1.7.8(@types/react@19.1.12)(@vercel/functions@2.2.13)(bufferutil@4.0.9)(react@19.2.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + '@reown/appkit-wallet': 1.7.8(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10) lit: 3.3.0 qrcode: 1.5.3 transitivePeerDependencies: @@ -10621,11 +11205,11 @@ snapshots: - utf-8-validate - zod - '@reown/appkit-ui@1.7.8(@types/react@19.2.10)(@vercel/functions@2.2.13)(bufferutil@4.1.0)(react@19.2.4)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)': + '@reown/appkit-ui@1.7.8(@types/react@19.1.12)(@vercel/functions@2.2.13)(bufferutil@4.0.9)(react@19.2.3)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76)': dependencies: - '@reown/appkit-common': 1.7.8(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) - '@reown/appkit-controllers': 1.7.8(@types/react@19.2.10)(@vercel/functions@2.2.13)(bufferutil@4.1.0)(react@19.2.4)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) - '@reown/appkit-wallet': 1.7.8(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10) + '@reown/appkit-common': 1.7.8(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + '@reown/appkit-controllers': 1.7.8(@types/react@19.1.12)(@vercel/functions@2.2.13)(bufferutil@4.0.9)(react@19.2.3)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + '@reown/appkit-wallet': 1.7.8(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10) lit: 3.3.0 qrcode: 1.5.3 transitivePeerDependencies: @@ -10656,16 +11240,16 @@ snapshots: - utf-8-validate - zod - '@reown/appkit-utils@1.7.8(@types/react@19.2.10)(@vercel/functions@2.2.13)(bufferutil@4.1.0)(react@19.2.1)(typescript@5.9.3)(utf-8-validate@5.0.10)(valtio@1.13.2(@types/react@19.2.10)(react@19.2.1))(zod@3.25.76)': + '@reown/appkit-utils@1.7.8(@types/react@19.1.12)(@vercel/functions@2.2.13)(bufferutil@4.0.9)(react@19.2.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(valtio@1.13.2(@types/react@19.1.12)(react@19.2.1))(zod@3.25.76)': dependencies: - '@reown/appkit-common': 1.7.8(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) - '@reown/appkit-controllers': 1.7.8(@types/react@19.2.10)(@vercel/functions@2.2.13)(bufferutil@4.1.0)(react@19.2.1)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + '@reown/appkit-common': 1.7.8(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + '@reown/appkit-controllers': 1.7.8(@types/react@19.1.12)(@vercel/functions@2.2.13)(bufferutil@4.0.9)(react@19.2.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) '@reown/appkit-polyfills': 1.7.8 - '@reown/appkit-wallet': 1.7.8(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10) + '@reown/appkit-wallet': 1.7.8(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10) '@walletconnect/logger': 2.1.2 - '@walletconnect/universal-provider': 2.21.0(@vercel/functions@2.2.13)(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) - valtio: 1.13.2(@types/react@19.2.10)(react@19.2.1) - viem: 2.45.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + '@walletconnect/universal-provider': 2.21.0(@vercel/functions@2.2.13)(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + valtio: 1.13.2(@types/react@19.1.12)(react@19.2.1) + viem: 2.45.1(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) transitivePeerDependencies: - '@azure/app-configuration' - '@azure/cosmos' @@ -10694,16 +11278,16 @@ snapshots: - utf-8-validate - zod - '@reown/appkit-utils@1.7.8(@types/react@19.2.10)(@vercel/functions@2.2.13)(bufferutil@4.1.0)(react@19.2.4)(typescript@5.9.3)(utf-8-validate@5.0.10)(valtio@1.13.2(@types/react@19.2.10)(react@19.2.4))(zod@3.25.76)': + '@reown/appkit-utils@1.7.8(@types/react@19.1.12)(@vercel/functions@2.2.13)(bufferutil@4.0.9)(react@19.2.3)(typescript@5.9.2)(utf-8-validate@5.0.10)(valtio@1.13.2(@types/react@19.1.12)(react@19.2.3))(zod@3.25.76)': dependencies: - '@reown/appkit-common': 1.7.8(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) - '@reown/appkit-controllers': 1.7.8(@types/react@19.2.10)(@vercel/functions@2.2.13)(bufferutil@4.1.0)(react@19.2.4)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + '@reown/appkit-common': 1.7.8(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + '@reown/appkit-controllers': 1.7.8(@types/react@19.1.12)(@vercel/functions@2.2.13)(bufferutil@4.0.9)(react@19.2.3)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) '@reown/appkit-polyfills': 1.7.8 - '@reown/appkit-wallet': 1.7.8(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10) + '@reown/appkit-wallet': 1.7.8(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10) '@walletconnect/logger': 2.1.2 - '@walletconnect/universal-provider': 2.21.0(@vercel/functions@2.2.13)(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) - valtio: 1.13.2(@types/react@19.2.10)(react@19.2.4) - viem: 2.45.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + '@walletconnect/universal-provider': 2.21.0(@vercel/functions@2.2.13)(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + valtio: 1.13.2(@types/react@19.1.12)(react@19.2.3) + viem: 2.45.1(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) transitivePeerDependencies: - '@azure/app-configuration' - '@azure/cosmos' @@ -10732,9 +11316,9 @@ snapshots: - utf-8-validate - zod - '@reown/appkit-wallet@1.7.8(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)': + '@reown/appkit-wallet@1.7.8(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)': dependencies: - '@reown/appkit-common': 1.7.8(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.22.4) + '@reown/appkit-common': 1.7.8(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.22.4) '@reown/appkit-polyfills': 1.7.8 '@walletconnect/logger': 2.1.2 zod: 3.22.4 @@ -10743,21 +11327,21 @@ snapshots: - typescript - utf-8-validate - '@reown/appkit@1.7.8(@types/react@19.2.10)(@vercel/functions@2.2.13)(bufferutil@4.1.0)(react@19.2.1)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)': + '@reown/appkit@1.7.8(@types/react@19.1.12)(@vercel/functions@2.2.13)(bufferutil@4.0.9)(react@19.2.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76)': dependencies: - '@reown/appkit-common': 1.7.8(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) - '@reown/appkit-controllers': 1.7.8(@types/react@19.2.10)(@vercel/functions@2.2.13)(bufferutil@4.1.0)(react@19.2.1)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) - '@reown/appkit-pay': 1.7.8(@types/react@19.2.10)(@vercel/functions@2.2.13)(bufferutil@4.1.0)(react@19.2.1)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + '@reown/appkit-common': 1.7.8(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + '@reown/appkit-controllers': 1.7.8(@types/react@19.1.12)(@vercel/functions@2.2.13)(bufferutil@4.0.9)(react@19.2.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + '@reown/appkit-pay': 1.7.8(@types/react@19.1.12)(@vercel/functions@2.2.13)(bufferutil@4.0.9)(react@19.2.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) '@reown/appkit-polyfills': 1.7.8 - '@reown/appkit-scaffold-ui': 1.7.8(@types/react@19.2.10)(@vercel/functions@2.2.13)(bufferutil@4.1.0)(react@19.2.1)(typescript@5.9.3)(utf-8-validate@5.0.10)(valtio@1.13.2(@types/react@19.2.10)(react@19.2.1))(zod@3.25.76) - '@reown/appkit-ui': 1.7.8(@types/react@19.2.10)(@vercel/functions@2.2.13)(bufferutil@4.1.0)(react@19.2.1)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) - '@reown/appkit-utils': 1.7.8(@types/react@19.2.10)(@vercel/functions@2.2.13)(bufferutil@4.1.0)(react@19.2.1)(typescript@5.9.3)(utf-8-validate@5.0.10)(valtio@1.13.2(@types/react@19.2.10)(react@19.2.1))(zod@3.25.76) - '@reown/appkit-wallet': 1.7.8(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10) + '@reown/appkit-scaffold-ui': 1.7.8(@types/react@19.1.12)(@vercel/functions@2.2.13)(bufferutil@4.0.9)(react@19.2.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(valtio@1.13.2(@types/react@19.1.12)(react@19.2.1))(zod@3.25.76) + '@reown/appkit-ui': 1.7.8(@types/react@19.1.12)(@vercel/functions@2.2.13)(bufferutil@4.0.9)(react@19.2.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + '@reown/appkit-utils': 1.7.8(@types/react@19.1.12)(@vercel/functions@2.2.13)(bufferutil@4.0.9)(react@19.2.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(valtio@1.13.2(@types/react@19.1.12)(react@19.2.1))(zod@3.25.76) + '@reown/appkit-wallet': 1.7.8(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10) '@walletconnect/types': 2.21.0(@vercel/functions@2.2.13) - '@walletconnect/universal-provider': 2.21.0(@vercel/functions@2.2.13)(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + '@walletconnect/universal-provider': 2.21.0(@vercel/functions@2.2.13)(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) bs58: 6.0.0 - valtio: 1.13.2(@types/react@19.2.10)(react@19.2.1) - viem: 2.45.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + valtio: 1.13.2(@types/react@19.1.12)(react@19.2.1) + viem: 2.45.1(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) transitivePeerDependencies: - '@azure/app-configuration' - '@azure/cosmos' @@ -10786,21 +11370,21 @@ snapshots: - utf-8-validate - zod - '@reown/appkit@1.7.8(@types/react@19.2.10)(@vercel/functions@2.2.13)(bufferutil@4.1.0)(react@19.2.4)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)': + '@reown/appkit@1.7.8(@types/react@19.1.12)(@vercel/functions@2.2.13)(bufferutil@4.0.9)(react@19.2.3)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76)': dependencies: - '@reown/appkit-common': 1.7.8(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) - '@reown/appkit-controllers': 1.7.8(@types/react@19.2.10)(@vercel/functions@2.2.13)(bufferutil@4.1.0)(react@19.2.4)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) - '@reown/appkit-pay': 1.7.8(@types/react@19.2.10)(@vercel/functions@2.2.13)(bufferutil@4.1.0)(react@19.2.4)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + '@reown/appkit-common': 1.7.8(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + '@reown/appkit-controllers': 1.7.8(@types/react@19.1.12)(@vercel/functions@2.2.13)(bufferutil@4.0.9)(react@19.2.3)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + '@reown/appkit-pay': 1.7.8(@types/react@19.1.12)(@vercel/functions@2.2.13)(bufferutil@4.0.9)(react@19.2.3)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) '@reown/appkit-polyfills': 1.7.8 - '@reown/appkit-scaffold-ui': 1.7.8(@types/react@19.2.10)(@vercel/functions@2.2.13)(bufferutil@4.1.0)(react@19.2.4)(typescript@5.9.3)(utf-8-validate@5.0.10)(valtio@1.13.2(@types/react@19.2.10)(react@19.2.4))(zod@3.25.76) - '@reown/appkit-ui': 1.7.8(@types/react@19.2.10)(@vercel/functions@2.2.13)(bufferutil@4.1.0)(react@19.2.4)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) - '@reown/appkit-utils': 1.7.8(@types/react@19.2.10)(@vercel/functions@2.2.13)(bufferutil@4.1.0)(react@19.2.4)(typescript@5.9.3)(utf-8-validate@5.0.10)(valtio@1.13.2(@types/react@19.2.10)(react@19.2.4))(zod@3.25.76) - '@reown/appkit-wallet': 1.7.8(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10) + '@reown/appkit-scaffold-ui': 1.7.8(@types/react@19.1.12)(@vercel/functions@2.2.13)(bufferutil@4.0.9)(react@19.2.3)(typescript@5.9.2)(utf-8-validate@5.0.10)(valtio@1.13.2(@types/react@19.1.12)(react@19.2.3))(zod@3.25.76) + '@reown/appkit-ui': 1.7.8(@types/react@19.1.12)(@vercel/functions@2.2.13)(bufferutil@4.0.9)(react@19.2.3)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + '@reown/appkit-utils': 1.7.8(@types/react@19.1.12)(@vercel/functions@2.2.13)(bufferutil@4.0.9)(react@19.2.3)(typescript@5.9.2)(utf-8-validate@5.0.10)(valtio@1.13.2(@types/react@19.1.12)(react@19.2.3))(zod@3.25.76) + '@reown/appkit-wallet': 1.7.8(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10) '@walletconnect/types': 2.21.0(@vercel/functions@2.2.13) - '@walletconnect/universal-provider': 2.21.0(@vercel/functions@2.2.13)(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + '@walletconnect/universal-provider': 2.21.0(@vercel/functions@2.2.13)(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) bs58: 6.0.0 - valtio: 1.13.2(@types/react@19.2.10)(react@19.2.4) - viem: 2.45.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + valtio: 1.13.2(@types/react@19.1.12)(react@19.2.3) + viem: 2.45.1(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) transitivePeerDependencies: - '@azure/app-configuration' - '@azure/cosmos' @@ -10829,86 +11413,74 @@ snapshots: - utf-8-validate - zod - '@rollup/rollup-android-arm-eabi@4.57.1': - optional: true - - '@rollup/rollup-android-arm64@4.57.1': - optional: true - - '@rollup/rollup-darwin-arm64@4.57.1': - optional: true - - '@rollup/rollup-darwin-x64@4.57.1': - optional: true - - '@rollup/rollup-freebsd-arm64@4.57.1': + '@rollup/rollup-android-arm-eabi@4.50.0': optional: true - '@rollup/rollup-freebsd-x64@4.57.1': + '@rollup/rollup-android-arm64@4.50.0': optional: true - '@rollup/rollup-linux-arm-gnueabihf@4.57.1': + '@rollup/rollup-darwin-arm64@4.50.0': optional: true - '@rollup/rollup-linux-arm-musleabihf@4.57.1': + '@rollup/rollup-darwin-x64@4.50.0': optional: true - '@rollup/rollup-linux-arm64-gnu@4.57.1': + '@rollup/rollup-freebsd-arm64@4.50.0': optional: true - '@rollup/rollup-linux-arm64-musl@4.57.1': + '@rollup/rollup-freebsd-x64@4.50.0': optional: true - '@rollup/rollup-linux-loong64-gnu@4.57.1': + '@rollup/rollup-linux-arm-gnueabihf@4.50.0': optional: true - '@rollup/rollup-linux-loong64-musl@4.57.1': + '@rollup/rollup-linux-arm-musleabihf@4.50.0': optional: true - '@rollup/rollup-linux-ppc64-gnu@4.57.1': + '@rollup/rollup-linux-arm64-gnu@4.50.0': optional: true - '@rollup/rollup-linux-ppc64-musl@4.57.1': + '@rollup/rollup-linux-arm64-musl@4.50.0': optional: true - '@rollup/rollup-linux-riscv64-gnu@4.57.1': + '@rollup/rollup-linux-loongarch64-gnu@4.50.0': optional: true - '@rollup/rollup-linux-riscv64-musl@4.57.1': + '@rollup/rollup-linux-ppc64-gnu@4.50.0': optional: true - '@rollup/rollup-linux-s390x-gnu@4.57.1': + '@rollup/rollup-linux-riscv64-gnu@4.50.0': optional: true - '@rollup/rollup-linux-x64-gnu@4.57.1': + '@rollup/rollup-linux-riscv64-musl@4.50.0': optional: true - '@rollup/rollup-linux-x64-musl@4.57.1': + '@rollup/rollup-linux-s390x-gnu@4.50.0': optional: true - '@rollup/rollup-openbsd-x64@4.57.1': + '@rollup/rollup-linux-x64-gnu@4.50.0': optional: true - '@rollup/rollup-openharmony-arm64@4.57.1': + '@rollup/rollup-linux-x64-musl@4.50.0': optional: true - '@rollup/rollup-win32-arm64-msvc@4.57.1': + '@rollup/rollup-openharmony-arm64@4.50.0': optional: true - '@rollup/rollup-win32-ia32-msvc@4.57.1': + '@rollup/rollup-win32-arm64-msvc@4.50.0': optional: true - '@rollup/rollup-win32-x64-gnu@4.57.1': + '@rollup/rollup-win32-ia32-msvc@4.50.0': optional: true - '@rollup/rollup-win32-x64-msvc@4.57.1': + '@rollup/rollup-win32-x64-msvc@4.50.0': optional: true '@rtsao/scc@1.1.0': {} - '@safe-global/safe-apps-provider@0.18.6(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)': + '@safe-global/safe-apps-provider@0.18.6(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76)': dependencies: - '@safe-global/safe-apps-sdk': 9.1.0(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + '@safe-global/safe-apps-sdk': 9.1.0(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) events: 3.3.0 transitivePeerDependencies: - bufferutil @@ -10916,10 +11488,10 @@ snapshots: - utf-8-validate - zod - '@safe-global/safe-apps-sdk@9.1.0(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)': + '@safe-global/safe-apps-sdk@9.1.0(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76)': dependencies: '@safe-global/safe-gateway-typescript-sdk': 3.23.1 - viem: 2.45.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + viem: 2.45.1(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) transitivePeerDependencies: - bufferutil - typescript @@ -10969,1180 +11541,1680 @@ snapshots: '@socket.io/component-emitter@3.1.2': {} - '@solana-program/compute-budget@0.11.0(@solana/kit@5.5.1(bufferutil@4.1.0)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(utf-8-validate@5.0.10))': + '@solana-program/compute-budget@0.11.0(@solana/kit@5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.19.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)))': dependencies: - '@solana/kit': 5.5.1(bufferutil@4.1.0)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(utf-8-validate@5.0.10) + '@solana/kit': 5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.19.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)) - '@solana-program/compute-budget@0.8.0(@solana/kit@6.1.0(bufferutil@4.1.0)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(utf-8-validate@5.0.10))': + '@solana-program/compute-budget@0.11.0(@solana/kit@5.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.19.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)))': dependencies: - '@solana/kit': 6.1.0(bufferutil@4.1.0)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(utf-8-validate@5.0.10) + '@solana/kit': 5.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.19.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)) - '@solana-program/system@0.10.0(@solana/kit@5.5.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10))': + '@solana-program/compute-budget@0.8.0(@solana/kit@6.1.0(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(utf-8-validate@5.0.10))': dependencies: - '@solana/kit': 5.5.1(bufferutil@4.1.0)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(utf-8-validate@5.0.10) + '@solana/kit': 6.1.0(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(utf-8-validate@5.0.10) - '@solana-program/token-2022@0.4.2(@solana/kit@6.1.0(bufferutil@4.1.0)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(utf-8-validate@5.0.10))(@solana/sysvars@6.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3))': + '@solana-program/token-2022@0.4.2(@solana/kit@6.1.0(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(utf-8-validate@5.0.10))(@solana/sysvars@6.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2))': dependencies: - '@solana/kit': 6.1.0(bufferutil@4.1.0)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(utf-8-validate@5.0.10) - '@solana/sysvars': 6.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/kit': 6.1.0(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(utf-8-validate@5.0.10) + '@solana/sysvars': 6.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) - '@solana-program/token-2022@0.6.1(@solana/kit@5.5.1(bufferutil@4.1.0)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(utf-8-validate@5.0.10))(@solana/sysvars@6.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3))': + '@solana-program/token-2022@0.6.1(@solana/kit@5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.19.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)))(@solana/sysvars@6.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2))': dependencies: - '@solana/kit': 5.5.1(bufferutil@4.1.0)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(utf-8-validate@5.0.10) - '@solana/sysvars': 6.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/kit': 5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.19.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + '@solana/sysvars': 6.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) - '@solana-program/token@0.5.1(@solana/kit@6.1.0(bufferutil@4.1.0)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(utf-8-validate@5.0.10))': + '@solana-program/token-2022@0.6.1(@solana/kit@5.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.19.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)))(@solana/sysvars@6.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2))': dependencies: - '@solana/kit': 6.1.0(bufferutil@4.1.0)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(utf-8-validate@5.0.10) + '@solana/kit': 5.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.19.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + '@solana/sysvars': 6.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) - '@solana-program/token@0.9.0(@solana/kit@5.5.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10))': + '@solana-program/token@0.5.1(@solana/kit@6.1.0(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(utf-8-validate@5.0.10))': dependencies: - '@solana/kit': 5.5.1(bufferutil@4.1.0)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(utf-8-validate@5.0.10) + '@solana/kit': 6.1.0(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(utf-8-validate@5.0.10) - '@solana/accounts@5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)': + '@solana-program/token@0.9.0(@solana/kit@5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.19.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)))': dependencies: - '@solana/addresses': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/codecs-core': 5.5.1(typescript@5.9.3) - '@solana/codecs-strings': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/errors': 5.5.1(typescript@5.9.3) - '@solana/rpc-spec': 5.5.1(typescript@5.9.3) - '@solana/rpc-types': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - optionalDependencies: - typescript: 5.9.3 + '@solana/kit': 5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.19.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + + '@solana-program/token@0.9.0(@solana/kit@5.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.19.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)))': + dependencies: + '@solana/kit': 5.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.19.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + + '@solana/accounts@5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)': + dependencies: + '@solana/addresses': 5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/codecs-core': 5.0.0(typescript@5.9.2) + '@solana/codecs-strings': 5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/errors': 5.0.0(typescript@5.9.2) + '@solana/rpc-spec': 5.0.0(typescript@5.9.2) + '@solana/rpc-types': 5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + typescript: 5.9.2 transitivePeerDependencies: - fastestsmallesttextencoderdecoder - '@solana/accounts@6.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)': + '@solana/accounts@5.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)': dependencies: - '@solana/addresses': 6.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/codecs-core': 6.1.0(typescript@5.9.3) - '@solana/codecs-strings': 6.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/errors': 6.1.0(typescript@5.9.3) - '@solana/rpc-spec': 6.1.0(typescript@5.9.3) - '@solana/rpc-types': 6.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/addresses': 5.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/codecs-core': 5.1.0(typescript@5.9.2) + '@solana/codecs-strings': 5.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/errors': 5.1.0(typescript@5.9.2) + '@solana/rpc-spec': 5.1.0(typescript@5.9.2) + '@solana/rpc-types': 5.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + typescript: 5.9.2 + transitivePeerDependencies: + - fastestsmallesttextencoderdecoder + + '@solana/accounts@6.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)': + dependencies: + '@solana/addresses': 6.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/codecs-core': 6.1.0(typescript@5.9.2) + '@solana/codecs-strings': 6.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/errors': 6.1.0(typescript@5.9.2) + '@solana/rpc-spec': 6.1.0(typescript@5.9.2) + '@solana/rpc-types': 6.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) optionalDependencies: - typescript: 5.9.3 + typescript: 5.9.2 transitivePeerDependencies: - fastestsmallesttextencoderdecoder - '@solana/addresses@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)': + '@solana/addresses@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)': dependencies: - '@solana/assertions': 2.3.0(typescript@5.9.3) - '@solana/codecs-core': 2.3.0(typescript@5.9.3) - '@solana/codecs-strings': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/errors': 2.3.0(typescript@5.9.3) - '@solana/nominal-types': 2.3.0(typescript@5.9.3) - typescript: 5.9.3 + '@solana/assertions': 2.3.0(typescript@5.9.2) + '@solana/codecs-core': 2.3.0(typescript@5.9.2) + '@solana/codecs-strings': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/errors': 2.3.0(typescript@5.9.2) + '@solana/nominal-types': 2.3.0(typescript@5.9.2) + typescript: 5.9.2 transitivePeerDependencies: - fastestsmallesttextencoderdecoder - '@solana/addresses@5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)': + '@solana/addresses@5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)': dependencies: - '@solana/assertions': 5.5.1(typescript@5.9.3) - '@solana/codecs-core': 5.5.1(typescript@5.9.3) - '@solana/codecs-strings': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/errors': 5.5.1(typescript@5.9.3) - '@solana/nominal-types': 5.5.1(typescript@5.9.3) - optionalDependencies: - typescript: 5.9.3 + '@solana/assertions': 5.0.0(typescript@5.9.2) + '@solana/codecs-core': 5.0.0(typescript@5.9.2) + '@solana/codecs-strings': 5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/errors': 5.0.0(typescript@5.9.2) + '@solana/nominal-types': 5.0.0(typescript@5.9.2) + typescript: 5.9.2 + transitivePeerDependencies: + - fastestsmallesttextencoderdecoder + + '@solana/addresses@5.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)': + dependencies: + '@solana/assertions': 5.1.0(typescript@5.9.2) + '@solana/codecs-core': 5.1.0(typescript@5.9.2) + '@solana/codecs-strings': 5.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/errors': 5.1.0(typescript@5.9.2) + '@solana/nominal-types': 5.1.0(typescript@5.9.2) + typescript: 5.9.2 transitivePeerDependencies: - fastestsmallesttextencoderdecoder - '@solana/addresses@6.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)': + '@solana/addresses@6.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)': dependencies: - '@solana/assertions': 6.1.0(typescript@5.9.3) - '@solana/codecs-core': 6.1.0(typescript@5.9.3) - '@solana/codecs-strings': 6.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/errors': 6.1.0(typescript@5.9.3) - '@solana/nominal-types': 6.1.0(typescript@5.9.3) + '@solana/assertions': 6.1.0(typescript@5.9.2) + '@solana/codecs-core': 6.1.0(typescript@5.9.2) + '@solana/codecs-strings': 6.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/errors': 6.1.0(typescript@5.9.2) + '@solana/nominal-types': 6.1.0(typescript@5.9.2) optionalDependencies: - typescript: 5.9.3 + typescript: 5.9.2 transitivePeerDependencies: - fastestsmallesttextencoderdecoder - '@solana/assertions@2.3.0(typescript@5.9.3)': + '@solana/assertions@2.3.0(typescript@5.9.2)': dependencies: - '@solana/errors': 2.3.0(typescript@5.9.3) - typescript: 5.9.3 + '@solana/errors': 2.3.0(typescript@5.9.2) + typescript: 5.9.2 - '@solana/assertions@5.5.1(typescript@5.9.3)': + '@solana/assertions@5.0.0(typescript@5.9.2)': dependencies: - '@solana/errors': 5.5.1(typescript@5.9.3) - optionalDependencies: - typescript: 5.9.3 + '@solana/errors': 5.0.0(typescript@5.9.2) + typescript: 5.9.2 + + '@solana/assertions@5.1.0(typescript@5.9.2)': + dependencies: + '@solana/errors': 5.1.0(typescript@5.9.2) + typescript: 5.9.2 - '@solana/assertions@6.1.0(typescript@5.9.3)': + '@solana/assertions@6.1.0(typescript@5.9.2)': dependencies: - '@solana/errors': 6.1.0(typescript@5.9.3) + '@solana/errors': 6.1.0(typescript@5.9.2) optionalDependencies: - typescript: 5.9.3 + typescript: 5.9.2 + + '@solana/buffer-layout-utils@0.2.0(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)': + dependencies: + '@solana/buffer-layout': 4.0.1 + '@solana/web3.js': 1.98.4(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10) + bigint-buffer: 1.1.5 + bignumber.js: 9.3.1 + transitivePeerDependencies: + - bufferutil + - encoding + - typescript + - utf-8-validate '@solana/buffer-layout@4.0.1': dependencies: buffer: 6.0.3 - '@solana/codecs-core@2.3.0(typescript@5.9.3)': + '@solana/codecs-core@2.0.0-rc.1(typescript@5.9.2)': dependencies: - '@solana/errors': 2.3.0(typescript@5.9.3) - typescript: 5.9.3 + '@solana/errors': 2.0.0-rc.1(typescript@5.9.2) + typescript: 5.9.2 - '@solana/codecs-core@5.5.1(typescript@5.9.3)': + '@solana/codecs-core@2.3.0(typescript@5.9.2)': dependencies: - '@solana/errors': 5.5.1(typescript@5.9.3) - optionalDependencies: - typescript: 5.9.3 + '@solana/errors': 2.3.0(typescript@5.9.2) + typescript: 5.9.2 - '@solana/codecs-core@6.1.0(typescript@5.9.3)': + '@solana/codecs-core@5.0.0(typescript@5.9.2)': dependencies: - '@solana/errors': 6.1.0(typescript@5.9.3) - optionalDependencies: - typescript: 5.9.3 + '@solana/errors': 5.0.0(typescript@5.9.2) + typescript: 5.9.2 - '@solana/codecs-data-structures@2.3.0(typescript@5.9.3)': + '@solana/codecs-core@5.1.0(typescript@5.9.2)': dependencies: - '@solana/codecs-core': 2.3.0(typescript@5.9.3) - '@solana/codecs-numbers': 2.3.0(typescript@5.9.3) - '@solana/errors': 2.3.0(typescript@5.9.3) - typescript: 5.9.3 + '@solana/errors': 5.1.0(typescript@5.9.2) + typescript: 5.9.2 - '@solana/codecs-data-structures@5.5.1(typescript@5.9.3)': + '@solana/codecs-core@6.1.0(typescript@5.9.2)': dependencies: - '@solana/codecs-core': 5.5.1(typescript@5.9.3) - '@solana/codecs-numbers': 5.5.1(typescript@5.9.3) - '@solana/errors': 5.5.1(typescript@5.9.3) + '@solana/errors': 6.1.0(typescript@5.9.2) optionalDependencies: - typescript: 5.9.3 + typescript: 5.9.2 - '@solana/codecs-data-structures@6.1.0(typescript@5.9.3)': + '@solana/codecs-data-structures@2.0.0-rc.1(typescript@5.9.2)': dependencies: - '@solana/codecs-core': 6.1.0(typescript@5.9.3) - '@solana/codecs-numbers': 6.1.0(typescript@5.9.3) - '@solana/errors': 6.1.0(typescript@5.9.3) - optionalDependencies: - typescript: 5.9.3 + '@solana/codecs-core': 2.0.0-rc.1(typescript@5.9.2) + '@solana/codecs-numbers': 2.0.0-rc.1(typescript@5.9.2) + '@solana/errors': 2.0.0-rc.1(typescript@5.9.2) + typescript: 5.9.2 + + '@solana/codecs-data-structures@2.3.0(typescript@5.9.2)': + dependencies: + '@solana/codecs-core': 2.3.0(typescript@5.9.2) + '@solana/codecs-numbers': 2.3.0(typescript@5.9.2) + '@solana/errors': 2.3.0(typescript@5.9.2) + typescript: 5.9.2 + + '@solana/codecs-data-structures@5.0.0(typescript@5.9.2)': + dependencies: + '@solana/codecs-core': 5.0.0(typescript@5.9.2) + '@solana/codecs-numbers': 5.0.0(typescript@5.9.2) + '@solana/errors': 5.0.0(typescript@5.9.2) + typescript: 5.9.2 - '@solana/codecs-numbers@2.3.0(typescript@5.9.3)': + '@solana/codecs-data-structures@5.1.0(typescript@5.9.2)': dependencies: - '@solana/codecs-core': 2.3.0(typescript@5.9.3) - '@solana/errors': 2.3.0(typescript@5.9.3) - typescript: 5.9.3 + '@solana/codecs-core': 5.1.0(typescript@5.9.2) + '@solana/codecs-numbers': 5.1.0(typescript@5.9.2) + '@solana/errors': 5.1.0(typescript@5.9.2) + typescript: 5.9.2 - '@solana/codecs-numbers@5.5.1(typescript@5.9.3)': + '@solana/codecs-data-structures@6.1.0(typescript@5.9.2)': dependencies: - '@solana/codecs-core': 5.5.1(typescript@5.9.3) - '@solana/errors': 5.5.1(typescript@5.9.3) + '@solana/codecs-core': 6.1.0(typescript@5.9.2) + '@solana/codecs-numbers': 6.1.0(typescript@5.9.2) + '@solana/errors': 6.1.0(typescript@5.9.2) optionalDependencies: - typescript: 5.9.3 + typescript: 5.9.2 + + '@solana/codecs-numbers@2.0.0-rc.1(typescript@5.9.2)': + dependencies: + '@solana/codecs-core': 2.0.0-rc.1(typescript@5.9.2) + '@solana/errors': 2.0.0-rc.1(typescript@5.9.2) + typescript: 5.9.2 + + '@solana/codecs-numbers@2.3.0(typescript@5.9.2)': + dependencies: + '@solana/codecs-core': 2.3.0(typescript@5.9.2) + '@solana/errors': 2.3.0(typescript@5.9.2) + typescript: 5.9.2 + + '@solana/codecs-numbers@5.0.0(typescript@5.9.2)': + dependencies: + '@solana/codecs-core': 5.0.0(typescript@5.9.2) + '@solana/errors': 5.0.0(typescript@5.9.2) + typescript: 5.9.2 + + '@solana/codecs-numbers@5.1.0(typescript@5.9.2)': + dependencies: + '@solana/codecs-core': 5.1.0(typescript@5.9.2) + '@solana/errors': 5.1.0(typescript@5.9.2) + typescript: 5.9.2 - '@solana/codecs-numbers@6.1.0(typescript@5.9.3)': + '@solana/codecs-numbers@6.1.0(typescript@5.9.2)': dependencies: - '@solana/codecs-core': 6.1.0(typescript@5.9.3) - '@solana/errors': 6.1.0(typescript@5.9.3) + '@solana/codecs-core': 6.1.0(typescript@5.9.2) + '@solana/errors': 6.1.0(typescript@5.9.2) optionalDependencies: - typescript: 5.9.3 + typescript: 5.9.2 - '@solana/codecs-strings@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)': + '@solana/codecs-strings@2.0.0-rc.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)': dependencies: - '@solana/codecs-core': 2.3.0(typescript@5.9.3) - '@solana/codecs-numbers': 2.3.0(typescript@5.9.3) - '@solana/errors': 2.3.0(typescript@5.9.3) + '@solana/codecs-core': 2.0.0-rc.1(typescript@5.9.2) + '@solana/codecs-numbers': 2.0.0-rc.1(typescript@5.9.2) + '@solana/errors': 2.0.0-rc.1(typescript@5.9.2) fastestsmallesttextencoderdecoder: 1.0.22 - typescript: 5.9.3 + typescript: 5.9.2 - '@solana/codecs-strings@5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)': + '@solana/codecs-strings@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)': dependencies: - '@solana/codecs-core': 5.5.1(typescript@5.9.3) - '@solana/codecs-numbers': 5.5.1(typescript@5.9.3) - '@solana/errors': 5.5.1(typescript@5.9.3) - optionalDependencies: + '@solana/codecs-core': 2.3.0(typescript@5.9.2) + '@solana/codecs-numbers': 2.3.0(typescript@5.9.2) + '@solana/errors': 2.3.0(typescript@5.9.2) fastestsmallesttextencoderdecoder: 1.0.22 - typescript: 5.9.3 + typescript: 5.9.2 - '@solana/codecs-strings@6.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)': + '@solana/codecs-strings@5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)': dependencies: - '@solana/codecs-core': 6.1.0(typescript@5.9.3) - '@solana/codecs-numbers': 6.1.0(typescript@5.9.3) - '@solana/errors': 6.1.0(typescript@5.9.3) + '@solana/codecs-core': 5.0.0(typescript@5.9.2) + '@solana/codecs-numbers': 5.0.0(typescript@5.9.2) + '@solana/errors': 5.0.0(typescript@5.9.2) + fastestsmallesttextencoderdecoder: 1.0.22 + typescript: 5.9.2 + + '@solana/codecs-strings@5.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)': + dependencies: + '@solana/codecs-core': 5.1.0(typescript@5.9.2) + '@solana/codecs-numbers': 5.1.0(typescript@5.9.2) + '@solana/errors': 5.1.0(typescript@5.9.2) + typescript: 5.9.2 optionalDependencies: fastestsmallesttextencoderdecoder: 1.0.22 - typescript: 5.9.3 - '@solana/codecs@5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)': + '@solana/codecs-strings@6.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)': dependencies: - '@solana/codecs-core': 5.5.1(typescript@5.9.3) - '@solana/codecs-data-structures': 5.5.1(typescript@5.9.3) - '@solana/codecs-numbers': 5.5.1(typescript@5.9.3) - '@solana/codecs-strings': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/options': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/codecs-core': 6.1.0(typescript@5.9.2) + '@solana/codecs-numbers': 6.1.0(typescript@5.9.2) + '@solana/errors': 6.1.0(typescript@5.9.2) optionalDependencies: - typescript: 5.9.3 + fastestsmallesttextencoderdecoder: 1.0.22 + typescript: 5.9.2 + + '@solana/codecs@2.0.0-rc.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)': + dependencies: + '@solana/codecs-core': 2.0.0-rc.1(typescript@5.9.2) + '@solana/codecs-data-structures': 2.0.0-rc.1(typescript@5.9.2) + '@solana/codecs-numbers': 2.0.0-rc.1(typescript@5.9.2) + '@solana/codecs-strings': 2.0.0-rc.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/options': 2.0.0-rc.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + typescript: 5.9.2 + transitivePeerDependencies: + - fastestsmallesttextencoderdecoder + + '@solana/codecs@5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)': + dependencies: + '@solana/codecs-core': 5.0.0(typescript@5.9.2) + '@solana/codecs-data-structures': 5.0.0(typescript@5.9.2) + '@solana/codecs-numbers': 5.0.0(typescript@5.9.2) + '@solana/codecs-strings': 5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/options': 5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + typescript: 5.9.2 + transitivePeerDependencies: + - fastestsmallesttextencoderdecoder + + '@solana/codecs@5.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)': + dependencies: + '@solana/codecs-core': 5.1.0(typescript@5.9.2) + '@solana/codecs-data-structures': 5.1.0(typescript@5.9.2) + '@solana/codecs-numbers': 5.1.0(typescript@5.9.2) + '@solana/codecs-strings': 5.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/options': 5.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + typescript: 5.9.2 transitivePeerDependencies: - fastestsmallesttextencoderdecoder - '@solana/codecs@6.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)': + '@solana/codecs@6.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)': dependencies: - '@solana/codecs-core': 6.1.0(typescript@5.9.3) - '@solana/codecs-data-structures': 6.1.0(typescript@5.9.3) - '@solana/codecs-numbers': 6.1.0(typescript@5.9.3) - '@solana/codecs-strings': 6.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/options': 6.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/codecs-core': 6.1.0(typescript@5.9.2) + '@solana/codecs-data-structures': 6.1.0(typescript@5.9.2) + '@solana/codecs-numbers': 6.1.0(typescript@5.9.2) + '@solana/codecs-strings': 6.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/options': 6.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) optionalDependencies: - typescript: 5.9.3 + typescript: 5.9.2 transitivePeerDependencies: - fastestsmallesttextencoderdecoder - '@solana/errors@2.3.0(typescript@5.9.3)': + '@solana/errors@2.0.0-rc.1(typescript@5.9.2)': dependencies: chalk: 5.6.2 - commander: 14.0.3 - typescript: 5.9.3 + commander: 12.1.0 + typescript: 5.9.2 - '@solana/errors@5.5.1(typescript@5.9.3)': + '@solana/errors@2.3.0(typescript@5.9.2)': dependencies: chalk: 5.6.2 commander: 14.0.2 - optionalDependencies: - typescript: 5.9.3 + typescript: 5.9.2 + + '@solana/errors@5.0.0(typescript@5.9.2)': + dependencies: + chalk: 5.6.2 + commander: 14.0.1 + typescript: 5.9.2 - '@solana/errors@6.1.0(typescript@5.9.3)': + '@solana/errors@5.1.0(typescript@5.9.2)': + dependencies: + chalk: 5.6.2 + commander: 14.0.2 + typescript: 5.9.2 + + '@solana/errors@6.1.0(typescript@5.9.2)': dependencies: chalk: 5.6.2 commander: 14.0.3 optionalDependencies: - typescript: 5.9.3 + typescript: 5.9.2 - '@solana/fast-stable-stringify@2.3.0(typescript@5.9.3)': + '@solana/fast-stable-stringify@2.3.0(typescript@5.9.2)': dependencies: - typescript: 5.9.3 + typescript: 5.9.2 - '@solana/fast-stable-stringify@5.5.1(typescript@5.9.3)': - optionalDependencies: - typescript: 5.9.3 + '@solana/fast-stable-stringify@5.0.0(typescript@5.9.2)': + dependencies: + typescript: 5.9.2 + + '@solana/fast-stable-stringify@5.1.0(typescript@5.9.2)': + dependencies: + typescript: 5.9.2 - '@solana/fast-stable-stringify@6.1.0(typescript@5.9.3)': + '@solana/fast-stable-stringify@6.1.0(typescript@5.9.2)': optionalDependencies: - typescript: 5.9.3 + typescript: 5.9.2 - '@solana/functional@2.3.0(typescript@5.9.3)': + '@solana/functional@2.3.0(typescript@5.9.2)': dependencies: - typescript: 5.9.3 + typescript: 5.9.2 - '@solana/functional@5.5.1(typescript@5.9.3)': - optionalDependencies: - typescript: 5.9.3 + '@solana/functional@5.0.0(typescript@5.9.2)': + dependencies: + typescript: 5.9.2 + + '@solana/functional@5.1.0(typescript@5.9.2)': + dependencies: + typescript: 5.9.2 - '@solana/functional@6.1.0(typescript@5.9.3)': + '@solana/functional@6.1.0(typescript@5.9.2)': optionalDependencies: - typescript: 5.9.3 + typescript: 5.9.2 - '@solana/instruction-plans@5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)': + '@solana/instruction-plans@5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)': dependencies: - '@solana/errors': 5.5.1(typescript@5.9.3) - '@solana/instructions': 5.5.1(typescript@5.9.3) - '@solana/keys': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/promises': 5.5.1(typescript@5.9.3) - '@solana/transaction-messages': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/transactions': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - optionalDependencies: - typescript: 5.9.3 + '@solana/errors': 5.0.0(typescript@5.9.2) + '@solana/instructions': 5.0.0(typescript@5.9.2) + '@solana/promises': 5.0.0(typescript@5.9.2) + '@solana/transaction-messages': 5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/transactions': 5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + typescript: 5.9.2 + transitivePeerDependencies: + - fastestsmallesttextencoderdecoder + + '@solana/instruction-plans@5.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)': + dependencies: + '@solana/errors': 5.1.0(typescript@5.9.2) + '@solana/instructions': 5.1.0(typescript@5.9.2) + '@solana/keys': 5.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/promises': 5.1.0(typescript@5.9.2) + '@solana/transaction-messages': 5.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/transactions': 5.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + typescript: 5.9.2 transitivePeerDependencies: - fastestsmallesttextencoderdecoder - '@solana/instruction-plans@6.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)': + '@solana/instruction-plans@6.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)': dependencies: - '@solana/errors': 6.1.0(typescript@5.9.3) - '@solana/instructions': 6.1.0(typescript@5.9.3) - '@solana/keys': 6.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/promises': 6.1.0(typescript@5.9.3) - '@solana/transaction-messages': 6.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/transactions': 6.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/errors': 6.1.0(typescript@5.9.2) + '@solana/instructions': 6.1.0(typescript@5.9.2) + '@solana/keys': 6.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/promises': 6.1.0(typescript@5.9.2) + '@solana/transaction-messages': 6.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/transactions': 6.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) optionalDependencies: - typescript: 5.9.3 + typescript: 5.9.2 transitivePeerDependencies: - fastestsmallesttextencoderdecoder - '@solana/instructions@2.3.0(typescript@5.9.3)': + '@solana/instructions@2.3.0(typescript@5.9.2)': dependencies: - '@solana/codecs-core': 2.3.0(typescript@5.9.3) - '@solana/errors': 2.3.0(typescript@5.9.3) - typescript: 5.9.3 + '@solana/codecs-core': 2.3.0(typescript@5.9.2) + '@solana/errors': 2.3.0(typescript@5.9.2) + typescript: 5.9.2 - '@solana/instructions@5.5.1(typescript@5.9.3)': + '@solana/instructions@5.0.0(typescript@5.9.2)': dependencies: - '@solana/codecs-core': 5.5.1(typescript@5.9.3) - '@solana/errors': 5.5.1(typescript@5.9.3) - optionalDependencies: - typescript: 5.9.3 + '@solana/codecs-core': 5.0.0(typescript@5.9.2) + '@solana/errors': 5.0.0(typescript@5.9.2) + typescript: 5.9.2 + + '@solana/instructions@5.1.0(typescript@5.9.2)': + dependencies: + '@solana/codecs-core': 5.1.0(typescript@5.9.2) + '@solana/errors': 5.1.0(typescript@5.9.2) + typescript: 5.9.2 - '@solana/instructions@6.1.0(typescript@5.9.3)': + '@solana/instructions@6.1.0(typescript@5.9.2)': dependencies: - '@solana/codecs-core': 6.1.0(typescript@5.9.3) - '@solana/errors': 6.1.0(typescript@5.9.3) + '@solana/codecs-core': 6.1.0(typescript@5.9.2) + '@solana/errors': 6.1.0(typescript@5.9.2) optionalDependencies: - typescript: 5.9.3 + typescript: 5.9.2 - '@solana/keys@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)': + '@solana/keys@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)': dependencies: - '@solana/assertions': 2.3.0(typescript@5.9.3) - '@solana/codecs-core': 2.3.0(typescript@5.9.3) - '@solana/codecs-strings': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/errors': 2.3.0(typescript@5.9.3) - '@solana/nominal-types': 2.3.0(typescript@5.9.3) - typescript: 5.9.3 + '@solana/assertions': 2.3.0(typescript@5.9.2) + '@solana/codecs-core': 2.3.0(typescript@5.9.2) + '@solana/codecs-strings': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/errors': 2.3.0(typescript@5.9.2) + '@solana/nominal-types': 2.3.0(typescript@5.9.2) + typescript: 5.9.2 transitivePeerDependencies: - fastestsmallesttextencoderdecoder - '@solana/keys@5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)': + '@solana/keys@5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)': dependencies: - '@solana/assertions': 5.5.1(typescript@5.9.3) - '@solana/codecs-core': 5.5.1(typescript@5.9.3) - '@solana/codecs-strings': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/errors': 5.5.1(typescript@5.9.3) - '@solana/nominal-types': 5.5.1(typescript@5.9.3) - optionalDependencies: - typescript: 5.9.3 + '@solana/assertions': 5.0.0(typescript@5.9.2) + '@solana/codecs-core': 5.0.0(typescript@5.9.2) + '@solana/codecs-strings': 5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/errors': 5.0.0(typescript@5.9.2) + '@solana/nominal-types': 5.0.0(typescript@5.9.2) + typescript: 5.9.2 transitivePeerDependencies: - fastestsmallesttextencoderdecoder - '@solana/keys@6.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)': + '@solana/keys@5.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)': dependencies: - '@solana/assertions': 6.1.0(typescript@5.9.3) - '@solana/codecs-core': 6.1.0(typescript@5.9.3) - '@solana/codecs-strings': 6.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/errors': 6.1.0(typescript@5.9.3) - '@solana/nominal-types': 6.1.0(typescript@5.9.3) - optionalDependencies: - typescript: 5.9.3 + '@solana/assertions': 5.1.0(typescript@5.9.2) + '@solana/codecs-core': 5.1.0(typescript@5.9.2) + '@solana/codecs-strings': 5.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/errors': 5.1.0(typescript@5.9.2) + '@solana/nominal-types': 5.1.0(typescript@5.9.2) + typescript: 5.9.2 transitivePeerDependencies: - fastestsmallesttextencoderdecoder - '@solana/kit@5.5.1(bufferutil@4.1.0)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(utf-8-validate@5.0.10)': - dependencies: - '@solana/accounts': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/addresses': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/codecs': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/errors': 5.5.1(typescript@5.9.3) - '@solana/functional': 5.5.1(typescript@5.9.3) - '@solana/instruction-plans': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/instructions': 5.5.1(typescript@5.9.3) - '@solana/keys': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/offchain-messages': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/plugin-core': 5.5.1(typescript@5.9.3) - '@solana/programs': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/rpc': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/rpc-api': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/rpc-parsed-types': 5.5.1(typescript@5.9.3) - '@solana/rpc-spec-types': 5.5.1(typescript@5.9.3) - '@solana/rpc-subscriptions': 5.5.1(bufferutil@4.1.0)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(utf-8-validate@5.0.10) - '@solana/rpc-types': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/signers': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/sysvars': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/transaction-confirmation': 5.5.1(bufferutil@4.1.0)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(utf-8-validate@5.0.10) - '@solana/transaction-messages': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/transactions': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/keys@6.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)': + dependencies: + '@solana/assertions': 6.1.0(typescript@5.9.2) + '@solana/codecs-core': 6.1.0(typescript@5.9.2) + '@solana/codecs-strings': 6.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/errors': 6.1.0(typescript@5.9.2) + '@solana/nominal-types': 6.1.0(typescript@5.9.2) optionalDependencies: - typescript: 5.9.3 + typescript: 5.9.2 transitivePeerDependencies: - - bufferutil - fastestsmallesttextencoderdecoder - - utf-8-validate - '@solana/kit@6.1.0(bufferutil@4.1.0)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(utf-8-validate@5.0.10)': - dependencies: - '@solana/accounts': 6.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/addresses': 6.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/codecs': 6.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/errors': 6.1.0(typescript@5.9.3) - '@solana/functional': 6.1.0(typescript@5.9.3) - '@solana/instruction-plans': 6.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/instructions': 6.1.0(typescript@5.9.3) - '@solana/keys': 6.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/offchain-messages': 6.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/plugin-core': 6.1.0(typescript@5.9.3) - '@solana/plugin-interfaces': 6.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/program-client-core': 6.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/programs': 6.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/rpc': 6.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/rpc-api': 6.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/rpc-parsed-types': 6.1.0(typescript@5.9.3) - '@solana/rpc-spec-types': 6.1.0(typescript@5.9.3) - '@solana/rpc-subscriptions': 6.1.0(bufferutil@4.1.0)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(utf-8-validate@5.0.10) - '@solana/rpc-types': 6.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/signers': 6.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/sysvars': 6.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/transaction-confirmation': 6.1.0(bufferutil@4.1.0)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(utf-8-validate@5.0.10) - '@solana/transaction-messages': 6.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/transactions': 6.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/kit@5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.19.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))': + dependencies: + '@solana/accounts': 5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/addresses': 5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/codecs': 5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/errors': 5.0.0(typescript@5.9.2) + '@solana/functional': 5.0.0(typescript@5.9.2) + '@solana/instruction-plans': 5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/instructions': 5.0.0(typescript@5.9.2) + '@solana/keys': 5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/programs': 5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/rpc': 5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/rpc-parsed-types': 5.0.0(typescript@5.9.2) + '@solana/rpc-spec-types': 5.0.0(typescript@5.9.2) + '@solana/rpc-subscriptions': 5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.19.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + '@solana/rpc-types': 5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/signers': 5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/sysvars': 5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/transaction-confirmation': 5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.19.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + '@solana/transaction-messages': 5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/transactions': 5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + typescript: 5.9.2 + transitivePeerDependencies: + - fastestsmallesttextencoderdecoder + - ws + + '@solana/kit@5.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10))': + dependencies: + '@solana/accounts': 5.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/addresses': 5.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/codecs': 5.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/errors': 5.1.0(typescript@5.9.2) + '@solana/functional': 5.1.0(typescript@5.9.2) + '@solana/instruction-plans': 5.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/instructions': 5.1.0(typescript@5.9.2) + '@solana/keys': 5.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/offchain-messages': 5.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/programs': 5.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/rpc': 5.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/rpc-parsed-types': 5.1.0(typescript@5.9.2) + '@solana/rpc-spec-types': 5.1.0(typescript@5.9.2) + '@solana/rpc-subscriptions': 5.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + '@solana/rpc-types': 5.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/signers': 5.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/sysvars': 5.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/transaction-confirmation': 5.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + '@solana/transaction-messages': 5.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/transactions': 5.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + typescript: 5.9.2 + transitivePeerDependencies: + - fastestsmallesttextencoderdecoder + - ws + + '@solana/kit@5.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.19.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))': + dependencies: + '@solana/accounts': 5.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/addresses': 5.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/codecs': 5.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/errors': 5.1.0(typescript@5.9.2) + '@solana/functional': 5.1.0(typescript@5.9.2) + '@solana/instruction-plans': 5.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/instructions': 5.1.0(typescript@5.9.2) + '@solana/keys': 5.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/offchain-messages': 5.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/programs': 5.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/rpc': 5.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/rpc-parsed-types': 5.1.0(typescript@5.9.2) + '@solana/rpc-spec-types': 5.1.0(typescript@5.9.2) + '@solana/rpc-subscriptions': 5.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.19.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + '@solana/rpc-types': 5.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/signers': 5.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/sysvars': 5.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/transaction-confirmation': 5.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.19.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + '@solana/transaction-messages': 5.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/transactions': 5.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + typescript: 5.9.2 + transitivePeerDependencies: + - fastestsmallesttextencoderdecoder + - ws + + '@solana/kit@6.1.0(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(utf-8-validate@5.0.10)': + dependencies: + '@solana/accounts': 6.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/addresses': 6.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/codecs': 6.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/errors': 6.1.0(typescript@5.9.2) + '@solana/functional': 6.1.0(typescript@5.9.2) + '@solana/instruction-plans': 6.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/instructions': 6.1.0(typescript@5.9.2) + '@solana/keys': 6.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/offchain-messages': 6.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/plugin-core': 6.1.0(typescript@5.9.2) + '@solana/plugin-interfaces': 6.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/program-client-core': 6.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/programs': 6.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/rpc': 6.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/rpc-api': 6.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/rpc-parsed-types': 6.1.0(typescript@5.9.2) + '@solana/rpc-spec-types': 6.1.0(typescript@5.9.2) + '@solana/rpc-subscriptions': 6.1.0(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(utf-8-validate@5.0.10) + '@solana/rpc-types': 6.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/signers': 6.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/sysvars': 6.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/transaction-confirmation': 6.1.0(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(utf-8-validate@5.0.10) + '@solana/transaction-messages': 6.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/transactions': 6.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) optionalDependencies: - typescript: 5.9.3 + typescript: 5.9.2 transitivePeerDependencies: - bufferutil - fastestsmallesttextencoderdecoder - utf-8-validate - '@solana/nominal-types@2.3.0(typescript@5.9.3)': + '@solana/nominal-types@2.3.0(typescript@5.9.2)': dependencies: - typescript: 5.9.3 + typescript: 5.9.2 - '@solana/nominal-types@5.5.1(typescript@5.9.3)': - optionalDependencies: - typescript: 5.9.3 + '@solana/nominal-types@5.0.0(typescript@5.9.2)': + dependencies: + typescript: 5.9.2 - '@solana/nominal-types@6.1.0(typescript@5.9.3)': - optionalDependencies: - typescript: 5.9.3 - - '@solana/offchain-messages@5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)': - dependencies: - '@solana/addresses': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/codecs-core': 5.5.1(typescript@5.9.3) - '@solana/codecs-data-structures': 5.5.1(typescript@5.9.3) - '@solana/codecs-numbers': 5.5.1(typescript@5.9.3) - '@solana/codecs-strings': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/errors': 5.5.1(typescript@5.9.3) - '@solana/keys': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/nominal-types': 5.5.1(typescript@5.9.3) + '@solana/nominal-types@5.1.0(typescript@5.9.2)': + dependencies: + typescript: 5.9.2 + + '@solana/nominal-types@6.1.0(typescript@5.9.2)': optionalDependencies: - typescript: 5.9.3 + typescript: 5.9.2 + + '@solana/offchain-messages@5.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)': + dependencies: + '@solana/addresses': 5.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/codecs-core': 5.1.0(typescript@5.9.2) + '@solana/codecs-data-structures': 5.1.0(typescript@5.9.2) + '@solana/codecs-numbers': 5.1.0(typescript@5.9.2) + '@solana/codecs-strings': 5.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/errors': 5.1.0(typescript@5.9.2) + '@solana/keys': 5.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/nominal-types': 5.1.0(typescript@5.9.2) + typescript: 5.9.2 transitivePeerDependencies: - fastestsmallesttextencoderdecoder - '@solana/offchain-messages@6.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)': + '@solana/offchain-messages@6.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)': dependencies: - '@solana/addresses': 6.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/codecs-core': 6.1.0(typescript@5.9.3) - '@solana/codecs-data-structures': 6.1.0(typescript@5.9.3) - '@solana/codecs-numbers': 6.1.0(typescript@5.9.3) - '@solana/codecs-strings': 6.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/errors': 6.1.0(typescript@5.9.3) - '@solana/keys': 6.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/nominal-types': 6.1.0(typescript@5.9.3) + '@solana/addresses': 6.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/codecs-core': 6.1.0(typescript@5.9.2) + '@solana/codecs-data-structures': 6.1.0(typescript@5.9.2) + '@solana/codecs-numbers': 6.1.0(typescript@5.9.2) + '@solana/codecs-strings': 6.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/errors': 6.1.0(typescript@5.9.2) + '@solana/keys': 6.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/nominal-types': 6.1.0(typescript@5.9.2) optionalDependencies: - typescript: 5.9.3 + typescript: 5.9.2 transitivePeerDependencies: - fastestsmallesttextencoderdecoder - '@solana/options@5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)': + '@solana/options@2.0.0-rc.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)': dependencies: - '@solana/codecs-core': 5.5.1(typescript@5.9.3) - '@solana/codecs-data-structures': 5.5.1(typescript@5.9.3) - '@solana/codecs-numbers': 5.5.1(typescript@5.9.3) - '@solana/codecs-strings': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/errors': 5.5.1(typescript@5.9.3) - optionalDependencies: - typescript: 5.9.3 + '@solana/codecs-core': 2.0.0-rc.1(typescript@5.9.2) + '@solana/codecs-data-structures': 2.0.0-rc.1(typescript@5.9.2) + '@solana/codecs-numbers': 2.0.0-rc.1(typescript@5.9.2) + '@solana/codecs-strings': 2.0.0-rc.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/errors': 2.0.0-rc.1(typescript@5.9.2) + typescript: 5.9.2 transitivePeerDependencies: - fastestsmallesttextencoderdecoder - '@solana/options@6.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)': + '@solana/options@5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)': dependencies: - '@solana/codecs-core': 6.1.0(typescript@5.9.3) - '@solana/codecs-data-structures': 6.1.0(typescript@5.9.3) - '@solana/codecs-numbers': 6.1.0(typescript@5.9.3) - '@solana/codecs-strings': 6.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/errors': 6.1.0(typescript@5.9.3) - optionalDependencies: - typescript: 5.9.3 + '@solana/codecs-core': 5.0.0(typescript@5.9.2) + '@solana/codecs-data-structures': 5.0.0(typescript@5.9.2) + '@solana/codecs-numbers': 5.0.0(typescript@5.9.2) + '@solana/codecs-strings': 5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/errors': 5.0.0(typescript@5.9.2) + typescript: 5.9.2 + transitivePeerDependencies: + - fastestsmallesttextencoderdecoder + + '@solana/options@5.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)': + dependencies: + '@solana/codecs-core': 5.1.0(typescript@5.9.2) + '@solana/codecs-data-structures': 5.1.0(typescript@5.9.2) + '@solana/codecs-numbers': 5.1.0(typescript@5.9.2) + '@solana/codecs-strings': 5.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/errors': 5.1.0(typescript@5.9.2) + typescript: 5.9.2 transitivePeerDependencies: - fastestsmallesttextencoderdecoder - '@solana/plugin-core@5.5.1(typescript@5.9.3)': + '@solana/options@6.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)': + dependencies: + '@solana/codecs-core': 6.1.0(typescript@5.9.2) + '@solana/codecs-data-structures': 6.1.0(typescript@5.9.2) + '@solana/codecs-numbers': 6.1.0(typescript@5.9.2) + '@solana/codecs-strings': 6.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/errors': 6.1.0(typescript@5.9.2) optionalDependencies: - typescript: 5.9.3 + typescript: 5.9.2 + transitivePeerDependencies: + - fastestsmallesttextencoderdecoder - '@solana/plugin-core@6.1.0(typescript@5.9.3)': + '@solana/plugin-core@6.1.0(typescript@5.9.2)': optionalDependencies: - typescript: 5.9.3 + typescript: 5.9.2 - '@solana/plugin-interfaces@6.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)': + '@solana/plugin-interfaces@6.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)': dependencies: - '@solana/addresses': 6.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/instruction-plans': 6.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/keys': 6.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/rpc-spec': 6.1.0(typescript@5.9.3) - '@solana/rpc-subscriptions-spec': 6.1.0(typescript@5.9.3) - '@solana/rpc-types': 6.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/signers': 6.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/addresses': 6.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/instruction-plans': 6.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/keys': 6.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/rpc-spec': 6.1.0(typescript@5.9.2) + '@solana/rpc-subscriptions-spec': 6.1.0(typescript@5.9.2) + '@solana/rpc-types': 6.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/signers': 6.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) optionalDependencies: - typescript: 5.9.3 + typescript: 5.9.2 transitivePeerDependencies: - fastestsmallesttextencoderdecoder - '@solana/program-client-core@6.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)': - dependencies: - '@solana/accounts': 6.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/addresses': 6.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/codecs-core': 6.1.0(typescript@5.9.3) - '@solana/errors': 6.1.0(typescript@5.9.3) - '@solana/instruction-plans': 6.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/instructions': 6.1.0(typescript@5.9.3) - '@solana/plugin-interfaces': 6.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/rpc-api': 6.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/signers': 6.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/program-client-core@6.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)': + dependencies: + '@solana/accounts': 6.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/addresses': 6.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/codecs-core': 6.1.0(typescript@5.9.2) + '@solana/errors': 6.1.0(typescript@5.9.2) + '@solana/instruction-plans': 6.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/instructions': 6.1.0(typescript@5.9.2) + '@solana/plugin-interfaces': 6.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/rpc-api': 6.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/signers': 6.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) optionalDependencies: - typescript: 5.9.3 + typescript: 5.9.2 transitivePeerDependencies: - fastestsmallesttextencoderdecoder - '@solana/programs@5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)': + '@solana/programs@5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)': dependencies: - '@solana/addresses': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/errors': 5.5.1(typescript@5.9.3) - optionalDependencies: - typescript: 5.9.3 + '@solana/addresses': 5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/errors': 5.0.0(typescript@5.9.2) + typescript: 5.9.2 + transitivePeerDependencies: + - fastestsmallesttextencoderdecoder + + '@solana/programs@5.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)': + dependencies: + '@solana/addresses': 5.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/errors': 5.1.0(typescript@5.9.2) + typescript: 5.9.2 transitivePeerDependencies: - fastestsmallesttextencoderdecoder - '@solana/programs@6.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)': + '@solana/programs@6.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)': dependencies: - '@solana/addresses': 6.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/errors': 6.1.0(typescript@5.9.3) + '@solana/addresses': 6.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/errors': 6.1.0(typescript@5.9.2) optionalDependencies: - typescript: 5.9.3 + typescript: 5.9.2 transitivePeerDependencies: - fastestsmallesttextencoderdecoder - '@solana/promises@2.3.0(typescript@5.9.3)': + '@solana/promises@2.3.0(typescript@5.9.2)': dependencies: - typescript: 5.9.3 + typescript: 5.9.2 - '@solana/promises@5.5.1(typescript@5.9.3)': - optionalDependencies: - typescript: 5.9.3 + '@solana/promises@5.0.0(typescript@5.9.2)': + dependencies: + typescript: 5.9.2 + + '@solana/promises@5.1.0(typescript@5.9.2)': + dependencies: + typescript: 5.9.2 - '@solana/promises@6.1.0(typescript@5.9.3)': + '@solana/promises@6.1.0(typescript@5.9.2)': optionalDependencies: - typescript: 5.9.3 - - '@solana/rpc-api@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)': - dependencies: - '@solana/addresses': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/codecs-core': 2.3.0(typescript@5.9.3) - '@solana/codecs-strings': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/errors': 2.3.0(typescript@5.9.3) - '@solana/keys': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/rpc-parsed-types': 2.3.0(typescript@5.9.3) - '@solana/rpc-spec': 2.3.0(typescript@5.9.3) - '@solana/rpc-transformers': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/rpc-types': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/transaction-messages': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/transactions': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - typescript: 5.9.3 + typescript: 5.9.2 + + '@solana/rpc-api@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)': + dependencies: + '@solana/addresses': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/codecs-core': 2.3.0(typescript@5.9.2) + '@solana/codecs-strings': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/errors': 2.3.0(typescript@5.9.2) + '@solana/keys': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/rpc-parsed-types': 2.3.0(typescript@5.9.2) + '@solana/rpc-spec': 2.3.0(typescript@5.9.2) + '@solana/rpc-transformers': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/rpc-types': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/transaction-messages': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/transactions': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + typescript: 5.9.2 transitivePeerDependencies: - fastestsmallesttextencoderdecoder - '@solana/rpc-api@5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)': - dependencies: - '@solana/addresses': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/codecs-core': 5.5.1(typescript@5.9.3) - '@solana/codecs-strings': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/errors': 5.5.1(typescript@5.9.3) - '@solana/keys': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/rpc-parsed-types': 5.5.1(typescript@5.9.3) - '@solana/rpc-spec': 5.5.1(typescript@5.9.3) - '@solana/rpc-transformers': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/rpc-types': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/transaction-messages': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/transactions': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - optionalDependencies: - typescript: 5.9.3 + '@solana/rpc-api@5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)': + dependencies: + '@solana/addresses': 5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/codecs-core': 5.0.0(typescript@5.9.2) + '@solana/codecs-strings': 5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/errors': 5.0.0(typescript@5.9.2) + '@solana/keys': 5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/rpc-parsed-types': 5.0.0(typescript@5.9.2) + '@solana/rpc-spec': 5.0.0(typescript@5.9.2) + '@solana/rpc-transformers': 5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/rpc-types': 5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/transaction-messages': 5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/transactions': 5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + typescript: 5.9.2 + transitivePeerDependencies: + - fastestsmallesttextencoderdecoder + + '@solana/rpc-api@5.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)': + dependencies: + '@solana/addresses': 5.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/codecs-core': 5.1.0(typescript@5.9.2) + '@solana/codecs-strings': 5.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/errors': 5.1.0(typescript@5.9.2) + '@solana/keys': 5.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/rpc-parsed-types': 5.1.0(typescript@5.9.2) + '@solana/rpc-spec': 5.1.0(typescript@5.9.2) + '@solana/rpc-transformers': 5.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/rpc-types': 5.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/transaction-messages': 5.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/transactions': 5.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + typescript: 5.9.2 transitivePeerDependencies: - fastestsmallesttextencoderdecoder - '@solana/rpc-api@6.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)': - dependencies: - '@solana/addresses': 6.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/codecs-core': 6.1.0(typescript@5.9.3) - '@solana/codecs-strings': 6.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/errors': 6.1.0(typescript@5.9.3) - '@solana/keys': 6.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/rpc-parsed-types': 6.1.0(typescript@5.9.3) - '@solana/rpc-spec': 6.1.0(typescript@5.9.3) - '@solana/rpc-transformers': 6.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/rpc-types': 6.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/transaction-messages': 6.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/transactions': 6.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/rpc-api@6.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)': + dependencies: + '@solana/addresses': 6.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/codecs-core': 6.1.0(typescript@5.9.2) + '@solana/codecs-strings': 6.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/errors': 6.1.0(typescript@5.9.2) + '@solana/keys': 6.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/rpc-parsed-types': 6.1.0(typescript@5.9.2) + '@solana/rpc-spec': 6.1.0(typescript@5.9.2) + '@solana/rpc-transformers': 6.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/rpc-types': 6.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/transaction-messages': 6.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/transactions': 6.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) optionalDependencies: - typescript: 5.9.3 + typescript: 5.9.2 transitivePeerDependencies: - fastestsmallesttextencoderdecoder - '@solana/rpc-parsed-types@2.3.0(typescript@5.9.3)': + '@solana/rpc-parsed-types@2.3.0(typescript@5.9.2)': dependencies: - typescript: 5.9.3 + typescript: 5.9.2 - '@solana/rpc-parsed-types@5.5.1(typescript@5.9.3)': - optionalDependencies: - typescript: 5.9.3 + '@solana/rpc-parsed-types@5.0.0(typescript@5.9.2)': + dependencies: + typescript: 5.9.2 + + '@solana/rpc-parsed-types@5.1.0(typescript@5.9.2)': + dependencies: + typescript: 5.9.2 - '@solana/rpc-parsed-types@6.1.0(typescript@5.9.3)': + '@solana/rpc-parsed-types@6.1.0(typescript@5.9.2)': optionalDependencies: - typescript: 5.9.3 + typescript: 5.9.2 - '@solana/rpc-spec-types@2.3.0(typescript@5.9.3)': + '@solana/rpc-spec-types@2.3.0(typescript@5.9.2)': dependencies: - typescript: 5.9.3 + typescript: 5.9.2 - '@solana/rpc-spec-types@5.5.1(typescript@5.9.3)': - optionalDependencies: - typescript: 5.9.3 + '@solana/rpc-spec-types@5.0.0(typescript@5.9.2)': + dependencies: + typescript: 5.9.2 + + '@solana/rpc-spec-types@5.1.0(typescript@5.9.2)': + dependencies: + typescript: 5.9.2 - '@solana/rpc-spec-types@6.1.0(typescript@5.9.3)': + '@solana/rpc-spec-types@6.1.0(typescript@5.9.2)': optionalDependencies: - typescript: 5.9.3 + typescript: 5.9.2 - '@solana/rpc-spec@2.3.0(typescript@5.9.3)': + '@solana/rpc-spec@2.3.0(typescript@5.9.2)': dependencies: - '@solana/errors': 2.3.0(typescript@5.9.3) - '@solana/rpc-spec-types': 2.3.0(typescript@5.9.3) - typescript: 5.9.3 + '@solana/errors': 2.3.0(typescript@5.9.2) + '@solana/rpc-spec-types': 2.3.0(typescript@5.9.2) + typescript: 5.9.2 - '@solana/rpc-spec@5.5.1(typescript@5.9.3)': + '@solana/rpc-spec@5.0.0(typescript@5.9.2)': dependencies: - '@solana/errors': 5.5.1(typescript@5.9.3) - '@solana/rpc-spec-types': 5.5.1(typescript@5.9.3) - optionalDependencies: - typescript: 5.9.3 + '@solana/errors': 5.0.0(typescript@5.9.2) + '@solana/rpc-spec-types': 5.0.0(typescript@5.9.2) + typescript: 5.9.2 + + '@solana/rpc-spec@5.1.0(typescript@5.9.2)': + dependencies: + '@solana/errors': 5.1.0(typescript@5.9.2) + '@solana/rpc-spec-types': 5.1.0(typescript@5.9.2) + typescript: 5.9.2 - '@solana/rpc-spec@6.1.0(typescript@5.9.3)': + '@solana/rpc-spec@6.1.0(typescript@5.9.2)': dependencies: - '@solana/errors': 6.1.0(typescript@5.9.3) - '@solana/rpc-spec-types': 6.1.0(typescript@5.9.3) + '@solana/errors': 6.1.0(typescript@5.9.2) + '@solana/rpc-spec-types': 6.1.0(typescript@5.9.2) optionalDependencies: - typescript: 5.9.3 + typescript: 5.9.2 + + '@solana/rpc-subscriptions-api@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)': + dependencies: + '@solana/addresses': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/keys': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/rpc-subscriptions-spec': 2.3.0(typescript@5.9.2) + '@solana/rpc-transformers': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/rpc-types': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/transaction-messages': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/transactions': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + typescript: 5.9.2 + transitivePeerDependencies: + - fastestsmallesttextencoderdecoder - '@solana/rpc-subscriptions-api@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)': + '@solana/rpc-subscriptions-api@5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)': dependencies: - '@solana/addresses': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/keys': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/rpc-subscriptions-spec': 2.3.0(typescript@5.9.3) - '@solana/rpc-transformers': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/rpc-types': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/transaction-messages': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/transactions': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - typescript: 5.9.3 + '@solana/addresses': 5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/keys': 5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/rpc-subscriptions-spec': 5.0.0(typescript@5.9.2) + '@solana/rpc-transformers': 5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/rpc-types': 5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/transaction-messages': 5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/transactions': 5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + typescript: 5.9.2 transitivePeerDependencies: - fastestsmallesttextencoderdecoder - '@solana/rpc-subscriptions-api@5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)': + '@solana/rpc-subscriptions-api@5.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)': dependencies: - '@solana/addresses': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/keys': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/rpc-subscriptions-spec': 5.5.1(typescript@5.9.3) - '@solana/rpc-transformers': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/rpc-types': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/transaction-messages': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/transactions': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - optionalDependencies: - typescript: 5.9.3 + '@solana/addresses': 5.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/keys': 5.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/rpc-subscriptions-spec': 5.1.0(typescript@5.9.2) + '@solana/rpc-transformers': 5.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/rpc-types': 5.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/transaction-messages': 5.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/transactions': 5.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + typescript: 5.9.2 transitivePeerDependencies: - fastestsmallesttextencoderdecoder - '@solana/rpc-subscriptions-api@6.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)': + '@solana/rpc-subscriptions-api@6.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)': dependencies: - '@solana/addresses': 6.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/keys': 6.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/rpc-subscriptions-spec': 6.1.0(typescript@5.9.3) - '@solana/rpc-transformers': 6.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/rpc-types': 6.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/transaction-messages': 6.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/transactions': 6.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/addresses': 6.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/keys': 6.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/rpc-subscriptions-spec': 6.1.0(typescript@5.9.2) + '@solana/rpc-transformers': 6.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/rpc-types': 6.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/transaction-messages': 6.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/transactions': 6.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) optionalDependencies: - typescript: 5.9.3 + typescript: 5.9.2 transitivePeerDependencies: - fastestsmallesttextencoderdecoder - '@solana/rpc-subscriptions-channel-websocket@2.3.0(typescript@5.9.3)(ws@8.18.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))': + '@solana/rpc-subscriptions-channel-websocket@2.3.0(typescript@5.9.2)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))': + dependencies: + '@solana/errors': 2.3.0(typescript@5.9.2) + '@solana/functional': 2.3.0(typescript@5.9.2) + '@solana/rpc-subscriptions-spec': 2.3.0(typescript@5.9.2) + '@solana/subscribable': 2.3.0(typescript@5.9.2) + typescript: 5.9.2 + ws: 8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10) + + '@solana/rpc-subscriptions-channel-websocket@5.0.0(typescript@5.9.2)(ws@8.19.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))': dependencies: - '@solana/errors': 2.3.0(typescript@5.9.3) - '@solana/functional': 2.3.0(typescript@5.9.3) - '@solana/rpc-subscriptions-spec': 2.3.0(typescript@5.9.3) - '@solana/subscribable': 2.3.0(typescript@5.9.3) - typescript: 5.9.3 - ws: 8.18.0(bufferutil@4.1.0)(utf-8-validate@5.0.10) + '@solana/errors': 5.0.0(typescript@5.9.2) + '@solana/functional': 5.0.0(typescript@5.9.2) + '@solana/rpc-subscriptions-spec': 5.0.0(typescript@5.9.2) + '@solana/subscribable': 5.0.0(typescript@5.9.2) + typescript: 5.9.2 + ws: 8.19.0(bufferutil@4.0.9)(utf-8-validate@5.0.10) - '@solana/rpc-subscriptions-channel-websocket@5.5.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)': + '@solana/rpc-subscriptions-channel-websocket@5.1.0(typescript@5.9.2)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10))': dependencies: - '@solana/errors': 5.5.1(typescript@5.9.3) - '@solana/functional': 5.5.1(typescript@5.9.3) - '@solana/rpc-subscriptions-spec': 5.5.1(typescript@5.9.3) - '@solana/subscribable': 5.5.1(typescript@5.9.3) - ws: 8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10) + '@solana/errors': 5.1.0(typescript@5.9.2) + '@solana/functional': 5.1.0(typescript@5.9.2) + '@solana/rpc-subscriptions-spec': 5.1.0(typescript@5.9.2) + '@solana/subscribable': 5.1.0(typescript@5.9.2) + typescript: 5.9.2 optionalDependencies: - typescript: 5.9.3 - transitivePeerDependencies: - - bufferutil - - utf-8-validate + ws: 7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10) + + '@solana/rpc-subscriptions-channel-websocket@5.1.0(typescript@5.9.2)(ws@8.19.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))': + dependencies: + '@solana/errors': 5.1.0(typescript@5.9.2) + '@solana/functional': 5.1.0(typescript@5.9.2) + '@solana/rpc-subscriptions-spec': 5.1.0(typescript@5.9.2) + '@solana/subscribable': 5.1.0(typescript@5.9.2) + typescript: 5.9.2 + optionalDependencies: + ws: 8.19.0(bufferutil@4.0.9)(utf-8-validate@5.0.10) - '@solana/rpc-subscriptions-channel-websocket@6.1.0(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)': + '@solana/rpc-subscriptions-channel-websocket@6.1.0(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)': dependencies: - '@solana/errors': 6.1.0(typescript@5.9.3) - '@solana/functional': 6.1.0(typescript@5.9.3) - '@solana/rpc-subscriptions-spec': 6.1.0(typescript@5.9.3) - '@solana/subscribable': 6.1.0(typescript@5.9.3) - ws: 8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10) + '@solana/errors': 6.1.0(typescript@5.9.2) + '@solana/functional': 6.1.0(typescript@5.9.2) + '@solana/rpc-subscriptions-spec': 6.1.0(typescript@5.9.2) + '@solana/subscribable': 6.1.0(typescript@5.9.2) + ws: 8.19.0(bufferutil@4.0.9)(utf-8-validate@5.0.10) optionalDependencies: - typescript: 5.9.3 + typescript: 5.9.2 transitivePeerDependencies: - bufferutil - utf-8-validate - '@solana/rpc-subscriptions-spec@2.3.0(typescript@5.9.3)': + '@solana/rpc-subscriptions-spec@2.3.0(typescript@5.9.2)': dependencies: - '@solana/errors': 2.3.0(typescript@5.9.3) - '@solana/promises': 2.3.0(typescript@5.9.3) - '@solana/rpc-spec-types': 2.3.0(typescript@5.9.3) - '@solana/subscribable': 2.3.0(typescript@5.9.3) - typescript: 5.9.3 + '@solana/errors': 2.3.0(typescript@5.9.2) + '@solana/promises': 2.3.0(typescript@5.9.2) + '@solana/rpc-spec-types': 2.3.0(typescript@5.9.2) + '@solana/subscribable': 2.3.0(typescript@5.9.2) + typescript: 5.9.2 - '@solana/rpc-subscriptions-spec@5.5.1(typescript@5.9.3)': + '@solana/rpc-subscriptions-spec@5.0.0(typescript@5.9.2)': dependencies: - '@solana/errors': 5.5.1(typescript@5.9.3) - '@solana/promises': 5.5.1(typescript@5.9.3) - '@solana/rpc-spec-types': 5.5.1(typescript@5.9.3) - '@solana/subscribable': 5.5.1(typescript@5.9.3) - optionalDependencies: - typescript: 5.9.3 + '@solana/errors': 5.0.0(typescript@5.9.2) + '@solana/promises': 5.0.0(typescript@5.9.2) + '@solana/rpc-spec-types': 5.0.0(typescript@5.9.2) + '@solana/subscribable': 5.0.0(typescript@5.9.2) + typescript: 5.9.2 + + '@solana/rpc-subscriptions-spec@5.1.0(typescript@5.9.2)': + dependencies: + '@solana/errors': 5.1.0(typescript@5.9.2) + '@solana/promises': 5.1.0(typescript@5.9.2) + '@solana/rpc-spec-types': 5.1.0(typescript@5.9.2) + '@solana/subscribable': 5.1.0(typescript@5.9.2) + typescript: 5.9.2 - '@solana/rpc-subscriptions-spec@6.1.0(typescript@5.9.3)': + '@solana/rpc-subscriptions-spec@6.1.0(typescript@5.9.2)': dependencies: - '@solana/errors': 6.1.0(typescript@5.9.3) - '@solana/promises': 6.1.0(typescript@5.9.3) - '@solana/rpc-spec-types': 6.1.0(typescript@5.9.3) - '@solana/subscribable': 6.1.0(typescript@5.9.3) + '@solana/errors': 6.1.0(typescript@5.9.2) + '@solana/promises': 6.1.0(typescript@5.9.2) + '@solana/rpc-spec-types': 6.1.0(typescript@5.9.2) + '@solana/subscribable': 6.1.0(typescript@5.9.2) optionalDependencies: - typescript: 5.9.3 - - '@solana/rpc-subscriptions@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@8.18.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))': - dependencies: - '@solana/errors': 2.3.0(typescript@5.9.3) - '@solana/fast-stable-stringify': 2.3.0(typescript@5.9.3) - '@solana/functional': 2.3.0(typescript@5.9.3) - '@solana/promises': 2.3.0(typescript@5.9.3) - '@solana/rpc-spec-types': 2.3.0(typescript@5.9.3) - '@solana/rpc-subscriptions-api': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/rpc-subscriptions-channel-websocket': 2.3.0(typescript@5.9.3)(ws@8.18.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)) - '@solana/rpc-subscriptions-spec': 2.3.0(typescript@5.9.3) - '@solana/rpc-transformers': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/rpc-types': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/subscribable': 2.3.0(typescript@5.9.3) - typescript: 5.9.3 + typescript: 5.9.2 + + '@solana/rpc-subscriptions@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))': + dependencies: + '@solana/errors': 2.3.0(typescript@5.9.2) + '@solana/fast-stable-stringify': 2.3.0(typescript@5.9.2) + '@solana/functional': 2.3.0(typescript@5.9.2) + '@solana/promises': 2.3.0(typescript@5.9.2) + '@solana/rpc-spec-types': 2.3.0(typescript@5.9.2) + '@solana/rpc-subscriptions-api': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/rpc-subscriptions-channel-websocket': 2.3.0(typescript@5.9.2)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + '@solana/rpc-subscriptions-spec': 2.3.0(typescript@5.9.2) + '@solana/rpc-transformers': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/rpc-types': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/subscribable': 2.3.0(typescript@5.9.2) + typescript: 5.9.2 transitivePeerDependencies: - fastestsmallesttextencoderdecoder - ws - '@solana/rpc-subscriptions@5.5.1(bufferutil@4.1.0)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(utf-8-validate@5.0.10)': - dependencies: - '@solana/errors': 5.5.1(typescript@5.9.3) - '@solana/fast-stable-stringify': 5.5.1(typescript@5.9.3) - '@solana/functional': 5.5.1(typescript@5.9.3) - '@solana/promises': 5.5.1(typescript@5.9.3) - '@solana/rpc-spec-types': 5.5.1(typescript@5.9.3) - '@solana/rpc-subscriptions-api': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/rpc-subscriptions-channel-websocket': 5.5.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10) - '@solana/rpc-subscriptions-spec': 5.5.1(typescript@5.9.3) - '@solana/rpc-transformers': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/rpc-types': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/subscribable': 5.5.1(typescript@5.9.3) - optionalDependencies: - typescript: 5.9.3 + '@solana/rpc-subscriptions@5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.19.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))': + dependencies: + '@solana/errors': 5.0.0(typescript@5.9.2) + '@solana/fast-stable-stringify': 5.0.0(typescript@5.9.2) + '@solana/functional': 5.0.0(typescript@5.9.2) + '@solana/promises': 5.0.0(typescript@5.9.2) + '@solana/rpc-spec-types': 5.0.0(typescript@5.9.2) + '@solana/rpc-subscriptions-api': 5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/rpc-subscriptions-channel-websocket': 5.0.0(typescript@5.9.2)(ws@8.19.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + '@solana/rpc-subscriptions-spec': 5.0.0(typescript@5.9.2) + '@solana/rpc-transformers': 5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/rpc-types': 5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/subscribable': 5.0.0(typescript@5.9.2) + typescript: 5.9.2 transitivePeerDependencies: - - bufferutil - fastestsmallesttextencoderdecoder - - utf-8-validate + - ws + + '@solana/rpc-subscriptions@5.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10))': + dependencies: + '@solana/errors': 5.1.0(typescript@5.9.2) + '@solana/fast-stable-stringify': 5.1.0(typescript@5.9.2) + '@solana/functional': 5.1.0(typescript@5.9.2) + '@solana/promises': 5.1.0(typescript@5.9.2) + '@solana/rpc-spec-types': 5.1.0(typescript@5.9.2) + '@solana/rpc-subscriptions-api': 5.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/rpc-subscriptions-channel-websocket': 5.1.0(typescript@5.9.2)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + '@solana/rpc-subscriptions-spec': 5.1.0(typescript@5.9.2) + '@solana/rpc-transformers': 5.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/rpc-types': 5.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/subscribable': 5.1.0(typescript@5.9.2) + typescript: 5.9.2 + transitivePeerDependencies: + - fastestsmallesttextencoderdecoder + - ws + + '@solana/rpc-subscriptions@5.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.19.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))': + dependencies: + '@solana/errors': 5.1.0(typescript@5.9.2) + '@solana/fast-stable-stringify': 5.1.0(typescript@5.9.2) + '@solana/functional': 5.1.0(typescript@5.9.2) + '@solana/promises': 5.1.0(typescript@5.9.2) + '@solana/rpc-spec-types': 5.1.0(typescript@5.9.2) + '@solana/rpc-subscriptions-api': 5.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/rpc-subscriptions-channel-websocket': 5.1.0(typescript@5.9.2)(ws@8.19.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + '@solana/rpc-subscriptions-spec': 5.1.0(typescript@5.9.2) + '@solana/rpc-transformers': 5.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/rpc-types': 5.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/subscribable': 5.1.0(typescript@5.9.2) + typescript: 5.9.2 + transitivePeerDependencies: + - fastestsmallesttextencoderdecoder + - ws - '@solana/rpc-subscriptions@6.1.0(bufferutil@4.1.0)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(utf-8-validate@5.0.10)': - dependencies: - '@solana/errors': 6.1.0(typescript@5.9.3) - '@solana/fast-stable-stringify': 6.1.0(typescript@5.9.3) - '@solana/functional': 6.1.0(typescript@5.9.3) - '@solana/promises': 6.1.0(typescript@5.9.3) - '@solana/rpc-spec-types': 6.1.0(typescript@5.9.3) - '@solana/rpc-subscriptions-api': 6.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/rpc-subscriptions-channel-websocket': 6.1.0(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10) - '@solana/rpc-subscriptions-spec': 6.1.0(typescript@5.9.3) - '@solana/rpc-transformers': 6.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/rpc-types': 6.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/subscribable': 6.1.0(typescript@5.9.3) + '@solana/rpc-subscriptions@6.1.0(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(utf-8-validate@5.0.10)': + dependencies: + '@solana/errors': 6.1.0(typescript@5.9.2) + '@solana/fast-stable-stringify': 6.1.0(typescript@5.9.2) + '@solana/functional': 6.1.0(typescript@5.9.2) + '@solana/promises': 6.1.0(typescript@5.9.2) + '@solana/rpc-spec-types': 6.1.0(typescript@5.9.2) + '@solana/rpc-subscriptions-api': 6.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/rpc-subscriptions-channel-websocket': 6.1.0(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10) + '@solana/rpc-subscriptions-spec': 6.1.0(typescript@5.9.2) + '@solana/rpc-transformers': 6.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/rpc-types': 6.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/subscribable': 6.1.0(typescript@5.9.2) optionalDependencies: - typescript: 5.9.3 + typescript: 5.9.2 transitivePeerDependencies: - bufferutil - fastestsmallesttextencoderdecoder - utf-8-validate - '@solana/rpc-transformers@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)': + '@solana/rpc-transformers@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)': + dependencies: + '@solana/errors': 2.3.0(typescript@5.9.2) + '@solana/functional': 2.3.0(typescript@5.9.2) + '@solana/nominal-types': 2.3.0(typescript@5.9.2) + '@solana/rpc-spec-types': 2.3.0(typescript@5.9.2) + '@solana/rpc-types': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + typescript: 5.9.2 + transitivePeerDependencies: + - fastestsmallesttextencoderdecoder + + '@solana/rpc-transformers@5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)': + dependencies: + '@solana/errors': 5.0.0(typescript@5.9.2) + '@solana/functional': 5.0.0(typescript@5.9.2) + '@solana/nominal-types': 5.0.0(typescript@5.9.2) + '@solana/rpc-spec-types': 5.0.0(typescript@5.9.2) + '@solana/rpc-types': 5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + typescript: 5.9.2 + transitivePeerDependencies: + - fastestsmallesttextencoderdecoder + + '@solana/rpc-transformers@5.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)': dependencies: - '@solana/errors': 2.3.0(typescript@5.9.3) - '@solana/functional': 2.3.0(typescript@5.9.3) - '@solana/nominal-types': 2.3.0(typescript@5.9.3) - '@solana/rpc-spec-types': 2.3.0(typescript@5.9.3) - '@solana/rpc-types': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - typescript: 5.9.3 + '@solana/errors': 5.1.0(typescript@5.9.2) + '@solana/functional': 5.1.0(typescript@5.9.2) + '@solana/nominal-types': 5.1.0(typescript@5.9.2) + '@solana/rpc-spec-types': 5.1.0(typescript@5.9.2) + '@solana/rpc-types': 5.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + typescript: 5.9.2 transitivePeerDependencies: - fastestsmallesttextencoderdecoder - '@solana/rpc-transformers@5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)': + '@solana/rpc-transformers@6.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)': dependencies: - '@solana/errors': 5.5.1(typescript@5.9.3) - '@solana/functional': 5.5.1(typescript@5.9.3) - '@solana/nominal-types': 5.5.1(typescript@5.9.3) - '@solana/rpc-spec-types': 5.5.1(typescript@5.9.3) - '@solana/rpc-types': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/errors': 6.1.0(typescript@5.9.2) + '@solana/functional': 6.1.0(typescript@5.9.2) + '@solana/nominal-types': 6.1.0(typescript@5.9.2) + '@solana/rpc-spec-types': 6.1.0(typescript@5.9.2) + '@solana/rpc-types': 6.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) optionalDependencies: - typescript: 5.9.3 + typescript: 5.9.2 transitivePeerDependencies: - fastestsmallesttextencoderdecoder - '@solana/rpc-transformers@6.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)': + '@solana/rpc-transport-http@2.3.0(typescript@5.9.2)': + dependencies: + '@solana/errors': 2.3.0(typescript@5.9.2) + '@solana/rpc-spec': 2.3.0(typescript@5.9.2) + '@solana/rpc-spec-types': 2.3.0(typescript@5.9.2) + typescript: 5.9.2 + undici-types: 7.16.0 + + '@solana/rpc-transport-http@5.0.0(typescript@5.9.2)': + dependencies: + '@solana/errors': 5.0.0(typescript@5.9.2) + '@solana/rpc-spec': 5.0.0(typescript@5.9.2) + '@solana/rpc-spec-types': 5.0.0(typescript@5.9.2) + typescript: 5.9.2 + undici-types: 7.16.0 + + '@solana/rpc-transport-http@5.1.0(typescript@5.9.2)': + dependencies: + '@solana/errors': 5.1.0(typescript@5.9.2) + '@solana/rpc-spec': 5.1.0(typescript@5.9.2) + '@solana/rpc-spec-types': 5.1.0(typescript@5.9.2) + typescript: 5.9.2 + undici-types: 7.16.0 + + '@solana/rpc-transport-http@6.1.0(typescript@5.9.2)': dependencies: - '@solana/errors': 6.1.0(typescript@5.9.3) - '@solana/functional': 6.1.0(typescript@5.9.3) - '@solana/nominal-types': 6.1.0(typescript@5.9.3) - '@solana/rpc-spec-types': 6.1.0(typescript@5.9.3) - '@solana/rpc-types': 6.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/errors': 6.1.0(typescript@5.9.2) + '@solana/rpc-spec': 6.1.0(typescript@5.9.2) + '@solana/rpc-spec-types': 6.1.0(typescript@5.9.2) + undici-types: 7.22.0 optionalDependencies: - typescript: 5.9.3 + typescript: 5.9.2 + + '@solana/rpc-types@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)': + dependencies: + '@solana/addresses': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/codecs-core': 2.3.0(typescript@5.9.2) + '@solana/codecs-numbers': 2.3.0(typescript@5.9.2) + '@solana/codecs-strings': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/errors': 2.3.0(typescript@5.9.2) + '@solana/nominal-types': 2.3.0(typescript@5.9.2) + typescript: 5.9.2 + transitivePeerDependencies: + - fastestsmallesttextencoderdecoder + + '@solana/rpc-types@5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)': + dependencies: + '@solana/addresses': 5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/codecs-core': 5.0.0(typescript@5.9.2) + '@solana/codecs-numbers': 5.0.0(typescript@5.9.2) + '@solana/codecs-strings': 5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/errors': 5.0.0(typescript@5.9.2) + '@solana/nominal-types': 5.0.0(typescript@5.9.2) + typescript: 5.9.2 transitivePeerDependencies: - fastestsmallesttextencoderdecoder - '@solana/rpc-transport-http@2.3.0(typescript@5.9.3)': + '@solana/rpc-types@5.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)': dependencies: - '@solana/errors': 2.3.0(typescript@5.9.3) - '@solana/rpc-spec': 2.3.0(typescript@5.9.3) - '@solana/rpc-spec-types': 2.3.0(typescript@5.9.3) - typescript: 5.9.3 - undici-types: 7.20.0 + '@solana/addresses': 5.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/codecs-core': 5.1.0(typescript@5.9.2) + '@solana/codecs-numbers': 5.1.0(typescript@5.9.2) + '@solana/codecs-strings': 5.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/errors': 5.1.0(typescript@5.9.2) + '@solana/nominal-types': 5.1.0(typescript@5.9.2) + typescript: 5.9.2 + transitivePeerDependencies: + - fastestsmallesttextencoderdecoder - '@solana/rpc-transport-http@5.5.1(typescript@5.9.3)': + '@solana/rpc-types@6.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)': dependencies: - '@solana/errors': 5.5.1(typescript@5.9.3) - '@solana/rpc-spec': 5.5.1(typescript@5.9.3) - '@solana/rpc-spec-types': 5.5.1(typescript@5.9.3) - undici-types: 7.20.0 + '@solana/addresses': 6.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/codecs-core': 6.1.0(typescript@5.9.2) + '@solana/codecs-numbers': 6.1.0(typescript@5.9.2) + '@solana/codecs-strings': 6.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/errors': 6.1.0(typescript@5.9.2) + '@solana/nominal-types': 6.1.0(typescript@5.9.2) optionalDependencies: - typescript: 5.9.3 + typescript: 5.9.2 + transitivePeerDependencies: + - fastestsmallesttextencoderdecoder + + '@solana/rpc@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)': + dependencies: + '@solana/errors': 2.3.0(typescript@5.9.2) + '@solana/fast-stable-stringify': 2.3.0(typescript@5.9.2) + '@solana/functional': 2.3.0(typescript@5.9.2) + '@solana/rpc-api': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/rpc-spec': 2.3.0(typescript@5.9.2) + '@solana/rpc-spec-types': 2.3.0(typescript@5.9.2) + '@solana/rpc-transformers': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/rpc-transport-http': 2.3.0(typescript@5.9.2) + '@solana/rpc-types': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + typescript: 5.9.2 + transitivePeerDependencies: + - fastestsmallesttextencoderdecoder - '@solana/rpc-transport-http@6.1.0(typescript@5.9.3)': - dependencies: - '@solana/errors': 6.1.0(typescript@5.9.3) - '@solana/rpc-spec': 6.1.0(typescript@5.9.3) - '@solana/rpc-spec-types': 6.1.0(typescript@5.9.3) - undici-types: 7.22.0 - optionalDependencies: - typescript: 5.9.3 + '@solana/rpc@5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)': + dependencies: + '@solana/errors': 5.0.0(typescript@5.9.2) + '@solana/fast-stable-stringify': 5.0.0(typescript@5.9.2) + '@solana/functional': 5.0.0(typescript@5.9.2) + '@solana/rpc-api': 5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/rpc-spec': 5.0.0(typescript@5.9.2) + '@solana/rpc-spec-types': 5.0.0(typescript@5.9.2) + '@solana/rpc-transformers': 5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/rpc-transport-http': 5.0.0(typescript@5.9.2) + '@solana/rpc-types': 5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + typescript: 5.9.2 + transitivePeerDependencies: + - fastestsmallesttextencoderdecoder - '@solana/rpc-types@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)': - dependencies: - '@solana/addresses': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/codecs-core': 2.3.0(typescript@5.9.3) - '@solana/codecs-numbers': 2.3.0(typescript@5.9.3) - '@solana/codecs-strings': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/errors': 2.3.0(typescript@5.9.3) - '@solana/nominal-types': 2.3.0(typescript@5.9.3) - typescript: 5.9.3 + '@solana/rpc@5.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)': + dependencies: + '@solana/errors': 5.1.0(typescript@5.9.2) + '@solana/fast-stable-stringify': 5.1.0(typescript@5.9.2) + '@solana/functional': 5.1.0(typescript@5.9.2) + '@solana/rpc-api': 5.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/rpc-spec': 5.1.0(typescript@5.9.2) + '@solana/rpc-spec-types': 5.1.0(typescript@5.9.2) + '@solana/rpc-transformers': 5.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/rpc-transport-http': 5.1.0(typescript@5.9.2) + '@solana/rpc-types': 5.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + typescript: 5.9.2 transitivePeerDependencies: - fastestsmallesttextencoderdecoder - '@solana/rpc-types@5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)': - dependencies: - '@solana/addresses': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/codecs-core': 5.5.1(typescript@5.9.3) - '@solana/codecs-numbers': 5.5.1(typescript@5.9.3) - '@solana/codecs-strings': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/errors': 5.5.1(typescript@5.9.3) - '@solana/nominal-types': 5.5.1(typescript@5.9.3) + '@solana/rpc@6.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)': + dependencies: + '@solana/errors': 6.1.0(typescript@5.9.2) + '@solana/fast-stable-stringify': 6.1.0(typescript@5.9.2) + '@solana/functional': 6.1.0(typescript@5.9.2) + '@solana/rpc-api': 6.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/rpc-spec': 6.1.0(typescript@5.9.2) + '@solana/rpc-spec-types': 6.1.0(typescript@5.9.2) + '@solana/rpc-transformers': 6.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/rpc-transport-http': 6.1.0(typescript@5.9.2) + '@solana/rpc-types': 6.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) optionalDependencies: - typescript: 5.9.3 + typescript: 5.9.2 transitivePeerDependencies: - fastestsmallesttextencoderdecoder - '@solana/rpc-types@6.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)': - dependencies: - '@solana/addresses': 6.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/codecs-core': 6.1.0(typescript@5.9.3) - '@solana/codecs-numbers': 6.1.0(typescript@5.9.3) - '@solana/codecs-strings': 6.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/errors': 6.1.0(typescript@5.9.3) - '@solana/nominal-types': 6.1.0(typescript@5.9.3) - optionalDependencies: - typescript: 5.9.3 + '@solana/signers@5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)': + dependencies: + '@solana/addresses': 5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/codecs-core': 5.0.0(typescript@5.9.2) + '@solana/errors': 5.0.0(typescript@5.9.2) + '@solana/instructions': 5.0.0(typescript@5.9.2) + '@solana/keys': 5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/nominal-types': 5.0.0(typescript@5.9.2) + '@solana/transaction-messages': 5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/transactions': 5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + typescript: 5.9.2 transitivePeerDependencies: - fastestsmallesttextencoderdecoder - '@solana/rpc@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)': - dependencies: - '@solana/errors': 2.3.0(typescript@5.9.3) - '@solana/fast-stable-stringify': 2.3.0(typescript@5.9.3) - '@solana/functional': 2.3.0(typescript@5.9.3) - '@solana/rpc-api': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/rpc-spec': 2.3.0(typescript@5.9.3) - '@solana/rpc-spec-types': 2.3.0(typescript@5.9.3) - '@solana/rpc-transformers': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/rpc-transport-http': 2.3.0(typescript@5.9.3) - '@solana/rpc-types': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - typescript: 5.9.3 + '@solana/signers@5.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)': + dependencies: + '@solana/addresses': 5.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/codecs-core': 5.1.0(typescript@5.9.2) + '@solana/errors': 5.1.0(typescript@5.9.2) + '@solana/instructions': 5.1.0(typescript@5.9.2) + '@solana/keys': 5.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/nominal-types': 5.1.0(typescript@5.9.2) + '@solana/offchain-messages': 5.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/transaction-messages': 5.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/transactions': 5.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + typescript: 5.9.2 transitivePeerDependencies: - fastestsmallesttextencoderdecoder - '@solana/rpc@5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)': - dependencies: - '@solana/errors': 5.5.1(typescript@5.9.3) - '@solana/fast-stable-stringify': 5.5.1(typescript@5.9.3) - '@solana/functional': 5.5.1(typescript@5.9.3) - '@solana/rpc-api': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/rpc-spec': 5.5.1(typescript@5.9.3) - '@solana/rpc-spec-types': 5.5.1(typescript@5.9.3) - '@solana/rpc-transformers': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/rpc-transport-http': 5.5.1(typescript@5.9.3) - '@solana/rpc-types': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/signers@6.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)': + dependencies: + '@solana/addresses': 6.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/codecs-core': 6.1.0(typescript@5.9.2) + '@solana/errors': 6.1.0(typescript@5.9.2) + '@solana/instructions': 6.1.0(typescript@5.9.2) + '@solana/keys': 6.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/nominal-types': 6.1.0(typescript@5.9.2) + '@solana/offchain-messages': 6.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/transaction-messages': 6.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/transactions': 6.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) optionalDependencies: - typescript: 5.9.3 + typescript: 5.9.2 transitivePeerDependencies: - fastestsmallesttextencoderdecoder - '@solana/rpc@6.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)': - dependencies: - '@solana/errors': 6.1.0(typescript@5.9.3) - '@solana/fast-stable-stringify': 6.1.0(typescript@5.9.3) - '@solana/functional': 6.1.0(typescript@5.9.3) - '@solana/rpc-api': 6.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/rpc-spec': 6.1.0(typescript@5.9.3) - '@solana/rpc-spec-types': 6.1.0(typescript@5.9.3) - '@solana/rpc-transformers': 6.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/rpc-transport-http': 6.1.0(typescript@5.9.3) - '@solana/rpc-types': 6.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - optionalDependencies: - typescript: 5.9.3 + '@solana/spl-token-group@0.0.7(@solana/web3.js@1.98.4(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10))(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)': + dependencies: + '@solana/codecs': 2.0.0-rc.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/web3.js': 1.98.4(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10) transitivePeerDependencies: - fastestsmallesttextencoderdecoder + - typescript - '@solana/signers@5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)': - dependencies: - '@solana/addresses': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/codecs-core': 5.5.1(typescript@5.9.3) - '@solana/errors': 5.5.1(typescript@5.9.3) - '@solana/instructions': 5.5.1(typescript@5.9.3) - '@solana/keys': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/nominal-types': 5.5.1(typescript@5.9.3) - '@solana/offchain-messages': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/transaction-messages': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/transactions': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - optionalDependencies: - typescript: 5.9.3 + '@solana/spl-token-metadata@0.1.6(@solana/web3.js@1.98.4(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10))(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)': + dependencies: + '@solana/codecs': 2.0.0-rc.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/web3.js': 1.98.4(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10) transitivePeerDependencies: - fastestsmallesttextencoderdecoder + - typescript - '@solana/signers@6.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)': - dependencies: - '@solana/addresses': 6.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/codecs-core': 6.1.0(typescript@5.9.3) - '@solana/errors': 6.1.0(typescript@5.9.3) - '@solana/instructions': 6.1.0(typescript@5.9.3) - '@solana/keys': 6.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/nominal-types': 6.1.0(typescript@5.9.3) - '@solana/offchain-messages': 6.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/transaction-messages': 6.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/transactions': 6.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - optionalDependencies: - typescript: 5.9.3 + '@solana/spl-token@0.4.14(@solana/web3.js@1.98.4(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10))(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(utf-8-validate@5.0.10)': + dependencies: + '@solana/buffer-layout': 4.0.1 + '@solana/buffer-layout-utils': 0.2.0(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10) + '@solana/spl-token-group': 0.0.7(@solana/web3.js@1.98.4(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10))(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/spl-token-metadata': 0.1.6(@solana/web3.js@1.98.4(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10))(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/web3.js': 1.98.4(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10) + buffer: 6.0.3 transitivePeerDependencies: + - bufferutil + - encoding - fastestsmallesttextencoderdecoder + - typescript + - utf-8-validate - '@solana/subscribable@2.3.0(typescript@5.9.3)': + '@solana/subscribable@2.3.0(typescript@5.9.2)': dependencies: - '@solana/errors': 2.3.0(typescript@5.9.3) - typescript: 5.9.3 + '@solana/errors': 2.3.0(typescript@5.9.2) + typescript: 5.9.2 - '@solana/subscribable@5.5.1(typescript@5.9.3)': + '@solana/subscribable@5.0.0(typescript@5.9.2)': dependencies: - '@solana/errors': 5.5.1(typescript@5.9.3) - optionalDependencies: - typescript: 5.9.3 + '@solana/errors': 5.0.0(typescript@5.9.2) + typescript: 5.9.2 - '@solana/subscribable@6.1.0(typescript@5.9.3)': + '@solana/subscribable@5.1.0(typescript@5.9.2)': dependencies: - '@solana/errors': 6.1.0(typescript@5.9.3) - optionalDependencies: - typescript: 5.9.3 + '@solana/errors': 5.1.0(typescript@5.9.2) + typescript: 5.9.2 - '@solana/sysvars@5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)': + '@solana/subscribable@6.1.0(typescript@5.9.2)': dependencies: - '@solana/accounts': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/codecs': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/errors': 5.5.1(typescript@5.9.3) - '@solana/rpc-types': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/errors': 6.1.0(typescript@5.9.2) optionalDependencies: - typescript: 5.9.3 + typescript: 5.9.2 + + '@solana/sysvars@5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)': + dependencies: + '@solana/accounts': 5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/codecs': 5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/errors': 5.0.0(typescript@5.9.2) + '@solana/rpc-types': 5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + typescript: 5.9.2 transitivePeerDependencies: - fastestsmallesttextencoderdecoder - '@solana/sysvars@6.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)': + '@solana/sysvars@5.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)': dependencies: - '@solana/accounts': 6.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/codecs': 6.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/errors': 6.1.0(typescript@5.9.3) - '@solana/rpc-types': 6.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - optionalDependencies: - typescript: 5.9.3 + '@solana/accounts': 5.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/codecs': 5.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/errors': 5.1.0(typescript@5.9.2) + '@solana/rpc-types': 5.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + typescript: 5.9.2 transitivePeerDependencies: - fastestsmallesttextencoderdecoder - '@solana/transaction-confirmation@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@8.18.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))': + '@solana/sysvars@6.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)': dependencies: - '@solana/addresses': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/codecs-strings': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/errors': 2.3.0(typescript@5.9.3) - '@solana/keys': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/promises': 2.3.0(typescript@5.9.3) - '@solana/rpc': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/rpc-subscriptions': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@8.18.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)) - '@solana/rpc-types': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/transaction-messages': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/transactions': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - typescript: 5.9.3 + '@solana/accounts': 6.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/codecs': 6.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/errors': 6.1.0(typescript@5.9.2) + '@solana/rpc-types': 6.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + optionalDependencies: + typescript: 5.9.2 + transitivePeerDependencies: + - fastestsmallesttextencoderdecoder + + '@solana/transaction-confirmation@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))': + dependencies: + '@solana/addresses': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/codecs-strings': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/errors': 2.3.0(typescript@5.9.2) + '@solana/keys': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/promises': 2.3.0(typescript@5.9.2) + '@solana/rpc': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/rpc-subscriptions': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + '@solana/rpc-types': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/transaction-messages': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/transactions': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + typescript: 5.9.2 transitivePeerDependencies: - fastestsmallesttextencoderdecoder - ws - '@solana/transaction-confirmation@5.5.1(bufferutil@4.1.0)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(utf-8-validate@5.0.10)': - dependencies: - '@solana/addresses': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/codecs-strings': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/errors': 5.5.1(typescript@5.9.3) - '@solana/keys': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/promises': 5.5.1(typescript@5.9.3) - '@solana/rpc': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/rpc-subscriptions': 5.5.1(bufferutil@4.1.0)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(utf-8-validate@5.0.10) - '@solana/rpc-types': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/transaction-messages': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/transactions': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - optionalDependencies: - typescript: 5.9.3 + '@solana/transaction-confirmation@5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.19.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))': + dependencies: + '@solana/addresses': 5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/codecs-strings': 5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/errors': 5.0.0(typescript@5.9.2) + '@solana/keys': 5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/promises': 5.0.0(typescript@5.9.2) + '@solana/rpc': 5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/rpc-subscriptions': 5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.19.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + '@solana/rpc-types': 5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/transaction-messages': 5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/transactions': 5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + typescript: 5.9.2 transitivePeerDependencies: - - bufferutil - fastestsmallesttextencoderdecoder - - utf-8-validate + - ws + + '@solana/transaction-confirmation@5.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10))': + dependencies: + '@solana/addresses': 5.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/codecs-strings': 5.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/errors': 5.1.0(typescript@5.9.2) + '@solana/keys': 5.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/promises': 5.1.0(typescript@5.9.2) + '@solana/rpc': 5.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/rpc-subscriptions': 5.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + '@solana/rpc-types': 5.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/transaction-messages': 5.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/transactions': 5.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + typescript: 5.9.2 + transitivePeerDependencies: + - fastestsmallesttextencoderdecoder + - ws + + '@solana/transaction-confirmation@5.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.19.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))': + dependencies: + '@solana/addresses': 5.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/codecs-strings': 5.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/errors': 5.1.0(typescript@5.9.2) + '@solana/keys': 5.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/promises': 5.1.0(typescript@5.9.2) + '@solana/rpc': 5.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/rpc-subscriptions': 5.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.19.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + '@solana/rpc-types': 5.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/transaction-messages': 5.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/transactions': 5.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + typescript: 5.9.2 + transitivePeerDependencies: + - fastestsmallesttextencoderdecoder + - ws - '@solana/transaction-confirmation@6.1.0(bufferutil@4.1.0)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(utf-8-validate@5.0.10)': - dependencies: - '@solana/addresses': 6.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/codecs-strings': 6.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/errors': 6.1.0(typescript@5.9.3) - '@solana/keys': 6.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/promises': 6.1.0(typescript@5.9.3) - '@solana/rpc': 6.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/rpc-subscriptions': 6.1.0(bufferutil@4.1.0)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(utf-8-validate@5.0.10) - '@solana/rpc-types': 6.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/transaction-messages': 6.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/transactions': 6.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/transaction-confirmation@6.1.0(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(utf-8-validate@5.0.10)': + dependencies: + '@solana/addresses': 6.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/codecs-strings': 6.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/errors': 6.1.0(typescript@5.9.2) + '@solana/keys': 6.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/promises': 6.1.0(typescript@5.9.2) + '@solana/rpc': 6.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/rpc-subscriptions': 6.1.0(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(utf-8-validate@5.0.10) + '@solana/rpc-types': 6.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/transaction-messages': 6.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/transactions': 6.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) optionalDependencies: - typescript: 5.9.3 + typescript: 5.9.2 transitivePeerDependencies: - bufferutil - fastestsmallesttextencoderdecoder - utf-8-validate - '@solana/transaction-messages@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)': - dependencies: - '@solana/addresses': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/codecs-core': 2.3.0(typescript@5.9.3) - '@solana/codecs-data-structures': 2.3.0(typescript@5.9.3) - '@solana/codecs-numbers': 2.3.0(typescript@5.9.3) - '@solana/errors': 2.3.0(typescript@5.9.3) - '@solana/functional': 2.3.0(typescript@5.9.3) - '@solana/instructions': 2.3.0(typescript@5.9.3) - '@solana/nominal-types': 2.3.0(typescript@5.9.3) - '@solana/rpc-types': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - typescript: 5.9.3 + '@solana/transaction-messages@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)': + dependencies: + '@solana/addresses': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/codecs-core': 2.3.0(typescript@5.9.2) + '@solana/codecs-data-structures': 2.3.0(typescript@5.9.2) + '@solana/codecs-numbers': 2.3.0(typescript@5.9.2) + '@solana/errors': 2.3.0(typescript@5.9.2) + '@solana/functional': 2.3.0(typescript@5.9.2) + '@solana/instructions': 2.3.0(typescript@5.9.2) + '@solana/nominal-types': 2.3.0(typescript@5.9.2) + '@solana/rpc-types': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + typescript: 5.9.2 transitivePeerDependencies: - fastestsmallesttextencoderdecoder - '@solana/transaction-messages@5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)': - dependencies: - '@solana/addresses': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/codecs-core': 5.5.1(typescript@5.9.3) - '@solana/codecs-data-structures': 5.5.1(typescript@5.9.3) - '@solana/codecs-numbers': 5.5.1(typescript@5.9.3) - '@solana/errors': 5.5.1(typescript@5.9.3) - '@solana/functional': 5.5.1(typescript@5.9.3) - '@solana/instructions': 5.5.1(typescript@5.9.3) - '@solana/nominal-types': 5.5.1(typescript@5.9.3) - '@solana/rpc-types': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - optionalDependencies: - typescript: 5.9.3 + '@solana/transaction-messages@5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)': + dependencies: + '@solana/addresses': 5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/codecs-core': 5.0.0(typescript@5.9.2) + '@solana/codecs-data-structures': 5.0.0(typescript@5.9.2) + '@solana/codecs-numbers': 5.0.0(typescript@5.9.2) + '@solana/errors': 5.0.0(typescript@5.9.2) + '@solana/functional': 5.0.0(typescript@5.9.2) + '@solana/instructions': 5.0.0(typescript@5.9.2) + '@solana/nominal-types': 5.0.0(typescript@5.9.2) + '@solana/rpc-types': 5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + typescript: 5.9.2 transitivePeerDependencies: - fastestsmallesttextencoderdecoder - '@solana/transaction-messages@6.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)': - dependencies: - '@solana/addresses': 6.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/codecs-core': 6.1.0(typescript@5.9.3) - '@solana/codecs-data-structures': 6.1.0(typescript@5.9.3) - '@solana/codecs-numbers': 6.1.0(typescript@5.9.3) - '@solana/errors': 6.1.0(typescript@5.9.3) - '@solana/functional': 6.1.0(typescript@5.9.3) - '@solana/instructions': 6.1.0(typescript@5.9.3) - '@solana/nominal-types': 6.1.0(typescript@5.9.3) - '@solana/rpc-types': 6.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/transaction-messages@5.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)': + dependencies: + '@solana/addresses': 5.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/codecs-core': 5.1.0(typescript@5.9.2) + '@solana/codecs-data-structures': 5.1.0(typescript@5.9.2) + '@solana/codecs-numbers': 5.1.0(typescript@5.9.2) + '@solana/errors': 5.1.0(typescript@5.9.2) + '@solana/functional': 5.1.0(typescript@5.9.2) + '@solana/instructions': 5.1.0(typescript@5.9.2) + '@solana/nominal-types': 5.1.0(typescript@5.9.2) + '@solana/rpc-types': 5.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + typescript: 5.9.2 + transitivePeerDependencies: + - fastestsmallesttextencoderdecoder + + '@solana/transaction-messages@6.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)': + dependencies: + '@solana/addresses': 6.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/codecs-core': 6.1.0(typescript@5.9.2) + '@solana/codecs-data-structures': 6.1.0(typescript@5.9.2) + '@solana/codecs-numbers': 6.1.0(typescript@5.9.2) + '@solana/errors': 6.1.0(typescript@5.9.2) + '@solana/functional': 6.1.0(typescript@5.9.2) + '@solana/instructions': 6.1.0(typescript@5.9.2) + '@solana/nominal-types': 6.1.0(typescript@5.9.2) + '@solana/rpc-types': 6.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) optionalDependencies: - typescript: 5.9.3 + typescript: 5.9.2 transitivePeerDependencies: - fastestsmallesttextencoderdecoder - '@solana/transactions@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)': - dependencies: - '@solana/addresses': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/codecs-core': 2.3.0(typescript@5.9.3) - '@solana/codecs-data-structures': 2.3.0(typescript@5.9.3) - '@solana/codecs-numbers': 2.3.0(typescript@5.9.3) - '@solana/codecs-strings': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/errors': 2.3.0(typescript@5.9.3) - '@solana/functional': 2.3.0(typescript@5.9.3) - '@solana/instructions': 2.3.0(typescript@5.9.3) - '@solana/keys': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/nominal-types': 2.3.0(typescript@5.9.3) - '@solana/rpc-types': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/transaction-messages': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - typescript: 5.9.3 + '@solana/transactions@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)': + dependencies: + '@solana/addresses': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/codecs-core': 2.3.0(typescript@5.9.2) + '@solana/codecs-data-structures': 2.3.0(typescript@5.9.2) + '@solana/codecs-numbers': 2.3.0(typescript@5.9.2) + '@solana/codecs-strings': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/errors': 2.3.0(typescript@5.9.2) + '@solana/functional': 2.3.0(typescript@5.9.2) + '@solana/instructions': 2.3.0(typescript@5.9.2) + '@solana/keys': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/nominal-types': 2.3.0(typescript@5.9.2) + '@solana/rpc-types': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/transaction-messages': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + typescript: 5.9.2 transitivePeerDependencies: - fastestsmallesttextencoderdecoder - '@solana/transactions@5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)': - dependencies: - '@solana/addresses': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/codecs-core': 5.5.1(typescript@5.9.3) - '@solana/codecs-data-structures': 5.5.1(typescript@5.9.3) - '@solana/codecs-numbers': 5.5.1(typescript@5.9.3) - '@solana/codecs-strings': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/errors': 5.5.1(typescript@5.9.3) - '@solana/functional': 5.5.1(typescript@5.9.3) - '@solana/instructions': 5.5.1(typescript@5.9.3) - '@solana/keys': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/nominal-types': 5.5.1(typescript@5.9.3) - '@solana/rpc-types': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/transaction-messages': 5.5.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - optionalDependencies: - typescript: 5.9.3 + '@solana/transactions@5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)': + dependencies: + '@solana/addresses': 5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/codecs-core': 5.0.0(typescript@5.9.2) + '@solana/codecs-data-structures': 5.0.0(typescript@5.9.2) + '@solana/codecs-numbers': 5.0.0(typescript@5.9.2) + '@solana/codecs-strings': 5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/errors': 5.0.0(typescript@5.9.2) + '@solana/functional': 5.0.0(typescript@5.9.2) + '@solana/instructions': 5.0.0(typescript@5.9.2) + '@solana/keys': 5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/nominal-types': 5.0.0(typescript@5.9.2) + '@solana/rpc-types': 5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/transaction-messages': 5.0.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + typescript: 5.9.2 transitivePeerDependencies: - fastestsmallesttextencoderdecoder - '@solana/transactions@6.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)': - dependencies: - '@solana/addresses': 6.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/codecs-core': 6.1.0(typescript@5.9.3) - '@solana/codecs-data-structures': 6.1.0(typescript@5.9.3) - '@solana/codecs-numbers': 6.1.0(typescript@5.9.3) - '@solana/codecs-strings': 6.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/errors': 6.1.0(typescript@5.9.3) - '@solana/functional': 6.1.0(typescript@5.9.3) - '@solana/instructions': 6.1.0(typescript@5.9.3) - '@solana/keys': 6.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/nominal-types': 6.1.0(typescript@5.9.3) - '@solana/rpc-types': 6.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) - '@solana/transaction-messages': 6.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/transactions@5.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)': + dependencies: + '@solana/addresses': 5.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/codecs-core': 5.1.0(typescript@5.9.2) + '@solana/codecs-data-structures': 5.1.0(typescript@5.9.2) + '@solana/codecs-numbers': 5.1.0(typescript@5.9.2) + '@solana/codecs-strings': 5.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/errors': 5.1.0(typescript@5.9.2) + '@solana/functional': 5.1.0(typescript@5.9.2) + '@solana/instructions': 5.1.0(typescript@5.9.2) + '@solana/keys': 5.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/nominal-types': 5.1.0(typescript@5.9.2) + '@solana/rpc-types': 5.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/transaction-messages': 5.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + typescript: 5.9.2 + transitivePeerDependencies: + - fastestsmallesttextencoderdecoder + + '@solana/transactions@6.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)': + dependencies: + '@solana/addresses': 6.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/codecs-core': 6.1.0(typescript@5.9.2) + '@solana/codecs-data-structures': 6.1.0(typescript@5.9.2) + '@solana/codecs-numbers': 6.1.0(typescript@5.9.2) + '@solana/codecs-strings': 6.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/errors': 6.1.0(typescript@5.9.2) + '@solana/functional': 6.1.0(typescript@5.9.2) + '@solana/instructions': 6.1.0(typescript@5.9.2) + '@solana/keys': 6.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/nominal-types': 6.1.0(typescript@5.9.2) + '@solana/rpc-types': 6.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/transaction-messages': 6.1.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) optionalDependencies: - typescript: 5.9.3 + typescript: 5.9.2 transitivePeerDependencies: - fastestsmallesttextencoderdecoder @@ -12151,22 +13223,22 @@ snapshots: '@wallet-standard/base': 1.1.0 '@wallet-standard/features': 1.1.0 - '@solana/web3.js@1.98.4(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)': + '@solana/web3.js@1.98.4(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)': dependencies: - '@babel/runtime': 7.28.6 + '@babel/runtime': 7.28.3 '@noble/curves': 1.9.7 '@noble/hashes': 1.8.0 '@solana/buffer-layout': 4.0.1 - '@solana/codecs-numbers': 2.3.0(typescript@5.9.3) + '@solana/codecs-numbers': 2.3.0(typescript@5.9.2) agentkeepalive: 4.6.0 bn.js: 5.2.2 borsh: 0.7.0 bs58: 4.0.1 buffer: 6.0.3 fast-stable-stringify: 1.0.0 - jayson: 4.3.0(bufferutil@4.1.0)(utf-8-validate@5.0.10) + jayson: 4.2.0(bufferutil@4.0.9)(utf-8-validate@5.0.10) node-fetch: 2.7.0 - rpc-websockets: 9.3.3 + rpc-websockets: 9.1.3 superstruct: 2.0.2 transitivePeerDependencies: - bufferutil @@ -12194,56 +13266,81 @@ snapshots: '@stablelib/wipe@1.0.1': {} - '@svgr/babel-plugin-add-jsx-attribute@8.0.0(@babel/core@7.29.0)': + '@stellar/js-xdr@3.1.2': {} + + '@stellar/stellar-base@14.1.0': + dependencies: + '@noble/curves': 1.9.7 + '@stellar/js-xdr': 3.1.2 + base32.js: 0.1.0 + bignumber.js: 9.3.1 + buffer: 6.0.3 + sha.js: 2.4.12 + + '@stellar/stellar-sdk@14.6.1': + dependencies: + '@stellar/stellar-base': 14.1.0 + axios: 1.13.4 + bignumber.js: 9.3.1 + commander: 14.0.3 + eventsource: 2.0.2 + feaxios: 0.0.23 + randombytes: 2.1.0 + toml: 3.0.0 + urijs: 1.19.11 + transitivePeerDependencies: + - debug + + '@svgr/babel-plugin-add-jsx-attribute@8.0.0(@babel/core@7.28.3)': dependencies: - '@babel/core': 7.29.0 + '@babel/core': 7.28.3 - '@svgr/babel-plugin-remove-jsx-attribute@8.0.0(@babel/core@7.29.0)': + '@svgr/babel-plugin-remove-jsx-attribute@8.0.0(@babel/core@7.28.3)': dependencies: - '@babel/core': 7.29.0 + '@babel/core': 7.28.3 - '@svgr/babel-plugin-remove-jsx-empty-expression@8.0.0(@babel/core@7.29.0)': + '@svgr/babel-plugin-remove-jsx-empty-expression@8.0.0(@babel/core@7.28.3)': dependencies: - '@babel/core': 7.29.0 + '@babel/core': 7.28.3 - '@svgr/babel-plugin-replace-jsx-attribute-value@8.0.0(@babel/core@7.29.0)': + '@svgr/babel-plugin-replace-jsx-attribute-value@8.0.0(@babel/core@7.28.3)': dependencies: - '@babel/core': 7.29.0 + '@babel/core': 7.28.3 - '@svgr/babel-plugin-svg-dynamic-title@8.0.0(@babel/core@7.29.0)': + '@svgr/babel-plugin-svg-dynamic-title@8.0.0(@babel/core@7.28.3)': dependencies: - '@babel/core': 7.29.0 + '@babel/core': 7.28.3 - '@svgr/babel-plugin-svg-em-dimensions@8.0.0(@babel/core@7.29.0)': + '@svgr/babel-plugin-svg-em-dimensions@8.0.0(@babel/core@7.28.3)': dependencies: - '@babel/core': 7.29.0 + '@babel/core': 7.28.3 - '@svgr/babel-plugin-transform-react-native-svg@8.1.0(@babel/core@7.29.0)': + '@svgr/babel-plugin-transform-react-native-svg@8.1.0(@babel/core@7.28.3)': dependencies: - '@babel/core': 7.29.0 + '@babel/core': 7.28.3 - '@svgr/babel-plugin-transform-svg-component@8.0.0(@babel/core@7.29.0)': + '@svgr/babel-plugin-transform-svg-component@8.0.0(@babel/core@7.28.3)': dependencies: - '@babel/core': 7.29.0 + '@babel/core': 7.28.3 - '@svgr/babel-preset@8.1.0(@babel/core@7.29.0)': + '@svgr/babel-preset@8.1.0(@babel/core@7.28.3)': dependencies: - '@babel/core': 7.29.0 - '@svgr/babel-plugin-add-jsx-attribute': 8.0.0(@babel/core@7.29.0) - '@svgr/babel-plugin-remove-jsx-attribute': 8.0.0(@babel/core@7.29.0) - '@svgr/babel-plugin-remove-jsx-empty-expression': 8.0.0(@babel/core@7.29.0) - '@svgr/babel-plugin-replace-jsx-attribute-value': 8.0.0(@babel/core@7.29.0) - '@svgr/babel-plugin-svg-dynamic-title': 8.0.0(@babel/core@7.29.0) - '@svgr/babel-plugin-svg-em-dimensions': 8.0.0(@babel/core@7.29.0) - '@svgr/babel-plugin-transform-react-native-svg': 8.1.0(@babel/core@7.29.0) - '@svgr/babel-plugin-transform-svg-component': 8.0.0(@babel/core@7.29.0) + '@babel/core': 7.28.3 + '@svgr/babel-plugin-add-jsx-attribute': 8.0.0(@babel/core@7.28.3) + '@svgr/babel-plugin-remove-jsx-attribute': 8.0.0(@babel/core@7.28.3) + '@svgr/babel-plugin-remove-jsx-empty-expression': 8.0.0(@babel/core@7.28.3) + '@svgr/babel-plugin-replace-jsx-attribute-value': 8.0.0(@babel/core@7.28.3) + '@svgr/babel-plugin-svg-dynamic-title': 8.0.0(@babel/core@7.28.3) + '@svgr/babel-plugin-svg-em-dimensions': 8.0.0(@babel/core@7.28.3) + '@svgr/babel-plugin-transform-react-native-svg': 8.1.0(@babel/core@7.28.3) + '@svgr/babel-plugin-transform-svg-component': 8.0.0(@babel/core@7.28.3) - '@svgr/core@8.1.0(typescript@5.9.3)': + '@svgr/core@8.1.0(typescript@5.9.2)': dependencies: - '@babel/core': 7.29.0 - '@svgr/babel-preset': 8.1.0(@babel/core@7.29.0) + '@babel/core': 7.28.3 + '@svgr/babel-preset': 8.1.0(@babel/core@7.28.3) camelcase: 6.3.0 - cosmiconfig: 8.3.6(typescript@5.9.3) + cosmiconfig: 8.3.6(typescript@5.9.2) snake-case: 3.0.4 transitivePeerDependencies: - supports-color @@ -12251,38 +13348,38 @@ snapshots: '@svgr/hast-util-to-babel-ast@8.0.0': dependencies: - '@babel/types': 7.29.0 + '@babel/types': 7.28.2 entities: 4.5.0 - '@svgr/plugin-jsx@8.1.0(@svgr/core@8.1.0(typescript@5.9.3))': + '@svgr/plugin-jsx@8.1.0(@svgr/core@8.1.0(typescript@5.9.2))': dependencies: - '@babel/core': 7.29.0 - '@svgr/babel-preset': 8.1.0(@babel/core@7.29.0) - '@svgr/core': 8.1.0(typescript@5.9.3) + '@babel/core': 7.28.3 + '@svgr/babel-preset': 8.1.0(@babel/core@7.28.3) + '@svgr/core': 8.1.0(typescript@5.9.2) '@svgr/hast-util-to-babel-ast': 8.0.0 svg-parser: 2.0.4 transitivePeerDependencies: - supports-color - '@svgr/plugin-svgo@8.1.0(@svgr/core@8.1.0(typescript@5.9.3))(typescript@5.9.3)': + '@svgr/plugin-svgo@8.1.0(@svgr/core@8.1.0(typescript@5.9.2))(typescript@5.9.2)': dependencies: - '@svgr/core': 8.1.0(typescript@5.9.3) - cosmiconfig: 8.3.6(typescript@5.9.3) + '@svgr/core': 8.1.0(typescript@5.9.2) + cosmiconfig: 8.3.6(typescript@5.9.2) deepmerge: 4.3.1 svgo: 3.3.2 transitivePeerDependencies: - typescript - '@svgr/webpack@8.1.0(typescript@5.9.3)': + '@svgr/webpack@8.1.0(typescript@5.9.2)': dependencies: - '@babel/core': 7.29.0 - '@babel/plugin-transform-react-constant-elements': 7.27.1(@babel/core@7.29.0) - '@babel/preset-env': 7.29.0(@babel/core@7.29.0) - '@babel/preset-react': 7.28.5(@babel/core@7.29.0) - '@babel/preset-typescript': 7.28.5(@babel/core@7.29.0) - '@svgr/core': 8.1.0(typescript@5.9.3) - '@svgr/plugin-jsx': 8.1.0(@svgr/core@8.1.0(typescript@5.9.3)) - '@svgr/plugin-svgo': 8.1.0(@svgr/core@8.1.0(typescript@5.9.3))(typescript@5.9.3) + '@babel/core': 7.28.3 + '@babel/plugin-transform-react-constant-elements': 7.27.1(@babel/core@7.28.3) + '@babel/preset-env': 7.28.3(@babel/core@7.28.3) + '@babel/preset-react': 7.27.1(@babel/core@7.28.3) + '@babel/preset-typescript': 7.27.1(@babel/core@7.28.3) + '@svgr/core': 8.1.0(typescript@5.9.2) + '@svgr/plugin-jsx': 8.1.0(@svgr/core@8.1.0(typescript@5.9.2)) + '@svgr/plugin-svgo': 8.1.0(@svgr/core@8.1.0(typescript@5.9.2))(typescript@5.9.2) transitivePeerDependencies: - supports-color - typescript @@ -12291,7 +13388,7 @@ snapshots: dependencies: tslib: 2.8.1 - '@swc/helpers@0.5.18': + '@swc/helpers@0.5.17': dependencies: tslib: 2.8.1 @@ -12299,90 +13396,90 @@ snapshots: dependencies: defer-to-connect: 2.0.1 - '@tailwindcss/node@4.1.18': + '@tailwindcss/node@4.1.17': dependencies: '@jridgewell/remapping': 2.3.5 - enhanced-resolve: 5.18.4 + enhanced-resolve: 5.18.3 jiti: 2.6.1 lightningcss: 1.30.2 magic-string: 0.30.21 source-map-js: 1.2.1 - tailwindcss: 4.1.18 + tailwindcss: 4.1.17 - '@tailwindcss/oxide-android-arm64@4.1.18': + '@tailwindcss/oxide-android-arm64@4.1.17': optional: true - '@tailwindcss/oxide-darwin-arm64@4.1.18': + '@tailwindcss/oxide-darwin-arm64@4.1.17': optional: true - '@tailwindcss/oxide-darwin-x64@4.1.18': + '@tailwindcss/oxide-darwin-x64@4.1.17': optional: true - '@tailwindcss/oxide-freebsd-x64@4.1.18': + '@tailwindcss/oxide-freebsd-x64@4.1.17': optional: true - '@tailwindcss/oxide-linux-arm-gnueabihf@4.1.18': + '@tailwindcss/oxide-linux-arm-gnueabihf@4.1.17': optional: true - '@tailwindcss/oxide-linux-arm64-gnu@4.1.18': + '@tailwindcss/oxide-linux-arm64-gnu@4.1.17': optional: true - '@tailwindcss/oxide-linux-arm64-musl@4.1.18': + '@tailwindcss/oxide-linux-arm64-musl@4.1.17': optional: true - '@tailwindcss/oxide-linux-x64-gnu@4.1.18': + '@tailwindcss/oxide-linux-x64-gnu@4.1.17': optional: true - '@tailwindcss/oxide-linux-x64-musl@4.1.18': + '@tailwindcss/oxide-linux-x64-musl@4.1.17': optional: true - '@tailwindcss/oxide-wasm32-wasi@4.1.18': + '@tailwindcss/oxide-wasm32-wasi@4.1.17': optional: true - '@tailwindcss/oxide-win32-arm64-msvc@4.1.18': + '@tailwindcss/oxide-win32-arm64-msvc@4.1.17': optional: true - '@tailwindcss/oxide-win32-x64-msvc@4.1.18': + '@tailwindcss/oxide-win32-x64-msvc@4.1.17': optional: true - '@tailwindcss/oxide@4.1.18': + '@tailwindcss/oxide@4.1.17': optionalDependencies: - '@tailwindcss/oxide-android-arm64': 4.1.18 - '@tailwindcss/oxide-darwin-arm64': 4.1.18 - '@tailwindcss/oxide-darwin-x64': 4.1.18 - '@tailwindcss/oxide-freebsd-x64': 4.1.18 - '@tailwindcss/oxide-linux-arm-gnueabihf': 4.1.18 - '@tailwindcss/oxide-linux-arm64-gnu': 4.1.18 - '@tailwindcss/oxide-linux-arm64-musl': 4.1.18 - '@tailwindcss/oxide-linux-x64-gnu': 4.1.18 - '@tailwindcss/oxide-linux-x64-musl': 4.1.18 - '@tailwindcss/oxide-wasm32-wasi': 4.1.18 - '@tailwindcss/oxide-win32-arm64-msvc': 4.1.18 - '@tailwindcss/oxide-win32-x64-msvc': 4.1.18 - - '@tailwindcss/postcss@4.1.18': + '@tailwindcss/oxide-android-arm64': 4.1.17 + '@tailwindcss/oxide-darwin-arm64': 4.1.17 + '@tailwindcss/oxide-darwin-x64': 4.1.17 + '@tailwindcss/oxide-freebsd-x64': 4.1.17 + '@tailwindcss/oxide-linux-arm-gnueabihf': 4.1.17 + '@tailwindcss/oxide-linux-arm64-gnu': 4.1.17 + '@tailwindcss/oxide-linux-arm64-musl': 4.1.17 + '@tailwindcss/oxide-linux-x64-gnu': 4.1.17 + '@tailwindcss/oxide-linux-x64-musl': 4.1.17 + '@tailwindcss/oxide-wasm32-wasi': 4.1.17 + '@tailwindcss/oxide-win32-arm64-msvc': 4.1.17 + '@tailwindcss/oxide-win32-x64-msvc': 4.1.17 + + '@tailwindcss/postcss@4.1.17': dependencies: '@alloc/quick-lru': 5.2.0 - '@tailwindcss/node': 4.1.18 - '@tailwindcss/oxide': 4.1.18 + '@tailwindcss/node': 4.1.17 + '@tailwindcss/oxide': 4.1.17 postcss: 8.5.6 - tailwindcss: 4.1.18 + tailwindcss: 4.1.17 - '@tanstack/query-core@5.90.20': {} + '@tanstack/query-core@5.90.11': {} - '@tanstack/react-query@5.90.20(react@19.2.1)': + '@tanstack/react-query@5.90.11(react@19.2.1)': dependencies: - '@tanstack/query-core': 5.90.20 + '@tanstack/query-core': 5.90.11 react: 19.2.1 - '@tanstack/react-query@5.90.20(react@19.2.4)': + '@tanstack/react-query@5.90.11(react@19.2.3)': dependencies: - '@tanstack/query-core': 5.90.20 - react: 19.2.4 + '@tanstack/query-core': 5.90.11 + react: 19.2.3 '@trysound/sax@0.2.0': {} - '@tybys/wasm-util@0.10.1': + '@tybys/wasm-util@0.10.0': dependencies: tslib: 2.8.1 optional: true @@ -12390,23 +13487,22 @@ snapshots: '@types/body-parser@1.19.6': dependencies: '@types/connect': 3.4.38 - '@types/node': 22.19.7 + '@types/node': 22.18.0 '@types/cacheable-request@6.0.3': dependencies: - '@types/http-cache-semantics': 4.0.4 + '@types/http-cache-semantics': 4.2.0 '@types/keyv': 3.1.4 - '@types/node': 22.19.7 + '@types/node': 22.18.0 '@types/responselike': 1.0.3 - '@types/chai@5.2.3': + '@types/chai@5.2.2': dependencies: '@types/deep-eql': 4.0.2 - assertion-error: 2.0.1 '@types/connect@3.4.38': dependencies: - '@types/node': 22.19.7 + '@types/node': 22.18.0 '@types/debug@4.1.12': dependencies: @@ -12416,20 +13512,20 @@ snapshots: '@types/estree@1.0.8': {} - '@types/express-serve-static-core@5.1.1': + '@types/express-serve-static-core@5.0.7': dependencies: - '@types/node': 22.19.7 + '@types/node': 22.18.0 '@types/qs': 6.14.0 '@types/range-parser': 1.2.7 - '@types/send': 1.2.1 + '@types/send': 0.17.5 - '@types/express@5.0.6': + '@types/express@5.0.3': dependencies: '@types/body-parser': 1.19.6 - '@types/express-serve-static-core': 5.1.1 - '@types/serve-static': 2.2.0 + '@types/express-serve-static-core': 5.0.7 + '@types/serve-static': 1.15.8 - '@types/http-cache-semantics@4.0.4': {} + '@types/http-cache-semantics@4.2.0': {} '@types/http-errors@2.0.5': {} @@ -12439,15 +13535,17 @@ snapshots: '@types/keyv@3.1.4': dependencies: - '@types/node': 22.19.7 + '@types/node': 22.18.0 - '@types/lodash@4.17.23': {} + '@types/lodash@4.17.20': {} + + '@types/mime@1.3.5': {} '@types/ms@2.1.0': {} '@types/node@12.20.55': {} - '@types/node@22.19.7': + '@types/node@22.18.0': dependencies: undici-types: 6.21.0 @@ -12459,26 +13557,28 @@ snapshots: '@types/range-parser@1.2.7': {} - '@types/react-dom@19.2.3(@types/react@19.2.10)': + '@types/react-dom@19.1.9(@types/react@19.1.12)': dependencies: - '@types/react': 19.2.10 + '@types/react': 19.1.12 - '@types/react@19.2.10': + '@types/react@19.1.12': dependencies: - csstype: 3.2.3 + csstype: 3.1.3 '@types/responselike@1.0.3': dependencies: - '@types/node': 22.19.7 + '@types/node': 22.18.0 - '@types/send@1.2.1': + '@types/send@0.17.5': dependencies: - '@types/node': 22.19.7 + '@types/mime': 1.3.5 + '@types/node': 22.18.0 - '@types/serve-static@2.2.0': + '@types/serve-static@1.15.8': dependencies: '@types/http-errors': 2.0.5 - '@types/node': 22.19.7 + '@types/node': 22.18.0 + '@types/send': 0.17.5 '@types/trusted-types@2.0.7': {} @@ -12486,101 +13586,194 @@ snapshots: '@types/ws@7.4.7': dependencies: - '@types/node': 22.19.7 + '@types/node': 22.18.0 '@types/ws@8.18.1': dependencies: - '@types/node': 22.19.7 + '@types/node': 22.18.0 + + '@typescript-eslint/eslint-plugin@8.42.0(@typescript-eslint/parser@8.42.0(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.2))(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.2)': + dependencies: + '@eslint-community/regexpp': 4.12.1 + '@typescript-eslint/parser': 8.42.0(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.2) + '@typescript-eslint/scope-manager': 8.42.0 + '@typescript-eslint/type-utils': 8.42.0(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.2) + '@typescript-eslint/utils': 8.42.0(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.2) + '@typescript-eslint/visitor-keys': 8.42.0 + eslint: 9.34.0(jiti@2.6.1) + graphemer: 1.4.0 + ignore: 7.0.5 + natural-compare: 1.4.0 + ts-api-utils: 2.1.0(typescript@5.9.2) + typescript: 5.9.2 + transitivePeerDependencies: + - supports-color - '@typescript-eslint/eslint-plugin@8.54.0(@typescript-eslint/parser@8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)': + '@typescript-eslint/eslint-plugin@8.49.0(@typescript-eslint/parser@8.49.0(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.2))(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.2)': dependencies: - '@eslint-community/regexpp': 4.12.2 - '@typescript-eslint/parser': 8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/scope-manager': 8.54.0 - '@typescript-eslint/type-utils': 8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/utils': 8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/visitor-keys': 8.54.0 - eslint: 9.39.2(jiti@2.6.1) + '@eslint-community/regexpp': 4.12.1 + '@typescript-eslint/parser': 8.49.0(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.2) + '@typescript-eslint/scope-manager': 8.49.0 + '@typescript-eslint/type-utils': 8.49.0(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.2) + '@typescript-eslint/utils': 8.49.0(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.2) + '@typescript-eslint/visitor-keys': 8.49.0 + eslint: 9.34.0(jiti@2.6.1) ignore: 7.0.5 natural-compare: 1.4.0 - ts-api-utils: 2.4.0(typescript@5.9.3) - typescript: 5.9.3 + ts-api-utils: 2.1.0(typescript@5.9.2) + typescript: 5.9.2 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/parser@8.42.0(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.2)': + dependencies: + '@typescript-eslint/scope-manager': 8.42.0 + '@typescript-eslint/types': 8.42.0 + '@typescript-eslint/typescript-estree': 8.42.0(typescript@5.9.2) + '@typescript-eslint/visitor-keys': 8.42.0 + debug: 4.4.1 + eslint: 9.34.0(jiti@2.6.1) + typescript: 5.9.2 transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)': + '@typescript-eslint/parser@8.49.0(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.2)': dependencies: - '@typescript-eslint/scope-manager': 8.54.0 - '@typescript-eslint/types': 8.54.0 - '@typescript-eslint/typescript-estree': 8.54.0(typescript@5.9.3) - '@typescript-eslint/visitor-keys': 8.54.0 + '@typescript-eslint/scope-manager': 8.49.0 + '@typescript-eslint/types': 8.49.0 + '@typescript-eslint/typescript-estree': 8.49.0(typescript@5.9.2) + '@typescript-eslint/visitor-keys': 8.49.0 debug: 4.4.3 - eslint: 9.39.2(jiti@2.6.1) - typescript: 5.9.3 + eslint: 9.34.0(jiti@2.6.1) + typescript: 5.9.2 transitivePeerDependencies: - supports-color - '@typescript-eslint/project-service@8.54.0(typescript@5.9.3)': + '@typescript-eslint/project-service@8.42.0(typescript@5.9.2)': dependencies: - '@typescript-eslint/tsconfig-utils': 8.54.0(typescript@5.9.3) - '@typescript-eslint/types': 8.54.0 + '@typescript-eslint/tsconfig-utils': 8.42.0(typescript@5.9.2) + '@typescript-eslint/types': 8.42.0 debug: 4.4.3 - typescript: 5.9.3 + typescript: 5.9.2 transitivePeerDependencies: - supports-color - '@typescript-eslint/scope-manager@8.54.0': + '@typescript-eslint/project-service@8.49.0(typescript@5.9.2)': dependencies: - '@typescript-eslint/types': 8.54.0 - '@typescript-eslint/visitor-keys': 8.54.0 + '@typescript-eslint/tsconfig-utils': 8.49.0(typescript@5.9.2) + '@typescript-eslint/types': 8.49.0 + debug: 4.4.3 + typescript: 5.9.2 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/scope-manager@8.42.0': + dependencies: + '@typescript-eslint/types': 8.42.0 + '@typescript-eslint/visitor-keys': 8.42.0 + + '@typescript-eslint/scope-manager@8.49.0': + dependencies: + '@typescript-eslint/types': 8.49.0 + '@typescript-eslint/visitor-keys': 8.49.0 + + '@typescript-eslint/tsconfig-utils@8.42.0(typescript@5.9.2)': + dependencies: + typescript: 5.9.2 + + '@typescript-eslint/tsconfig-utils@8.49.0(typescript@5.9.2)': + dependencies: + typescript: 5.9.2 - '@typescript-eslint/tsconfig-utils@8.54.0(typescript@5.9.3)': + '@typescript-eslint/type-utils@8.42.0(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.2)': dependencies: - typescript: 5.9.3 + '@typescript-eslint/types': 8.42.0 + '@typescript-eslint/typescript-estree': 8.42.0(typescript@5.9.2) + '@typescript-eslint/utils': 8.42.0(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.2) + debug: 4.4.3 + eslint: 9.34.0(jiti@2.6.1) + ts-api-utils: 2.1.0(typescript@5.9.2) + typescript: 5.9.2 + transitivePeerDependencies: + - supports-color - '@typescript-eslint/type-utils@8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)': + '@typescript-eslint/type-utils@8.49.0(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.2)': dependencies: - '@typescript-eslint/types': 8.54.0 - '@typescript-eslint/typescript-estree': 8.54.0(typescript@5.9.3) - '@typescript-eslint/utils': 8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/types': 8.49.0 + '@typescript-eslint/typescript-estree': 8.49.0(typescript@5.9.2) + '@typescript-eslint/utils': 8.49.0(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.2) debug: 4.4.3 - eslint: 9.39.2(jiti@2.6.1) - ts-api-utils: 2.4.0(typescript@5.9.3) - typescript: 5.9.3 + eslint: 9.34.0(jiti@2.6.1) + ts-api-utils: 2.1.0(typescript@5.9.2) + typescript: 5.9.2 transitivePeerDependencies: - supports-color - '@typescript-eslint/types@8.54.0': {} + '@typescript-eslint/types@8.42.0': {} + + '@typescript-eslint/types@8.49.0': {} + + '@typescript-eslint/typescript-estree@8.42.0(typescript@5.9.2)': + dependencies: + '@typescript-eslint/project-service': 8.42.0(typescript@5.9.2) + '@typescript-eslint/tsconfig-utils': 8.42.0(typescript@5.9.2) + '@typescript-eslint/types': 8.42.0 + '@typescript-eslint/visitor-keys': 8.42.0 + debug: 4.4.1 + fast-glob: 3.3.3 + is-glob: 4.0.3 + minimatch: 9.0.5 + semver: 7.7.2 + ts-api-utils: 2.1.0(typescript@5.9.2) + typescript: 5.9.2 + transitivePeerDependencies: + - supports-color - '@typescript-eslint/typescript-estree@8.54.0(typescript@5.9.3)': + '@typescript-eslint/typescript-estree@8.49.0(typescript@5.9.2)': dependencies: - '@typescript-eslint/project-service': 8.54.0(typescript@5.9.3) - '@typescript-eslint/tsconfig-utils': 8.54.0(typescript@5.9.3) - '@typescript-eslint/types': 8.54.0 - '@typescript-eslint/visitor-keys': 8.54.0 + '@typescript-eslint/project-service': 8.49.0(typescript@5.9.2) + '@typescript-eslint/tsconfig-utils': 8.49.0(typescript@5.9.2) + '@typescript-eslint/types': 8.49.0 + '@typescript-eslint/visitor-keys': 8.49.0 debug: 4.4.3 minimatch: 9.0.5 semver: 7.7.3 tinyglobby: 0.2.15 - ts-api-utils: 2.4.0(typescript@5.9.3) - typescript: 5.9.3 + ts-api-utils: 2.1.0(typescript@5.9.2) + typescript: 5.9.2 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/utils@8.42.0(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.2)': + dependencies: + '@eslint-community/eslint-utils': 4.8.0(eslint@9.34.0(jiti@2.6.1)) + '@typescript-eslint/scope-manager': 8.42.0 + '@typescript-eslint/types': 8.42.0 + '@typescript-eslint/typescript-estree': 8.42.0(typescript@5.9.2) + eslint: 9.34.0(jiti@2.6.1) + typescript: 5.9.2 transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)': + '@typescript-eslint/utils@8.49.0(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.2)': dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.2(jiti@2.6.1)) - '@typescript-eslint/scope-manager': 8.54.0 - '@typescript-eslint/types': 8.54.0 - '@typescript-eslint/typescript-estree': 8.54.0(typescript@5.9.3) - eslint: 9.39.2(jiti@2.6.1) - typescript: 5.9.3 + '@eslint-community/eslint-utils': 4.8.0(eslint@9.34.0(jiti@2.6.1)) + '@typescript-eslint/scope-manager': 8.49.0 + '@typescript-eslint/types': 8.49.0 + '@typescript-eslint/typescript-estree': 8.49.0(typescript@5.9.2) + eslint: 9.34.0(jiti@2.6.1) + typescript: 5.9.2 transitivePeerDependencies: - supports-color - '@typescript-eslint/visitor-keys@8.54.0': + '@typescript-eslint/visitor-keys@8.42.0': + dependencies: + '@typescript-eslint/types': 8.42.0 + eslint-visitor-keys: 4.2.1 + + '@typescript-eslint/visitor-keys@8.49.0': dependencies: - '@typescript-eslint/types': 8.54.0 + '@typescript-eslint/types': 8.49.0 eslint-visitor-keys: 4.2.1 '@unrs/resolver-binding-android-arm-eabi@1.11.1': @@ -12653,19 +13846,19 @@ snapshots: '@vitest/expect@3.2.4': dependencies: - '@types/chai': 5.2.3 + '@types/chai': 5.2.2 '@vitest/spy': 3.2.4 '@vitest/utils': 3.2.4 chai: 5.3.3 tinyrainbow: 2.0.0 - '@vitest/mocker@3.2.4(vite@6.4.1(@types/node@22.19.7)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0))': + '@vitest/mocker@3.2.4(vite@6.3.5(@types/node@22.18.0)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.5)(yaml@2.8.1))': dependencies: '@vitest/spy': 3.2.4 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - vite: 6.4.1(@types/node@22.19.7)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0) + vite: 6.3.5(@types/node@22.18.0)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.5)(yaml@2.8.1) '@vitest/pretty-format@3.2.4': dependencies: @@ -12675,7 +13868,7 @@ snapshots: dependencies: '@vitest/utils': 3.2.4 pathe: 2.0.3 - strip-literal: 3.1.0 + strip-literal: 3.0.0 '@vitest/snapshot@3.2.4': dependencies: @@ -12685,7 +13878,7 @@ snapshots: '@vitest/spy@3.2.4': dependencies: - tinyspy: 4.0.4 + tinyspy: 4.0.3 '@vitest/utils@3.2.4': dependencies: @@ -12693,21 +13886,20 @@ snapshots: loupe: 3.2.1 tinyrainbow: 2.0.0 - '@wagmi/connectors@5.11.2(@tanstack/react-query@5.90.20(react@19.2.1))(@types/react@19.2.10)(@vercel/functions@2.2.13)(@wagmi/core@2.22.1(@tanstack/query-core@5.90.20)(@types/react@19.2.10)(react@19.2.1)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.1))(viem@2.45.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)))(bufferutil@4.1.0)(react@19.2.1)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.1))(utf-8-validate@5.0.10)(viem@2.45.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(wagmi@2.19.5(@tanstack/query-core@5.90.20)(@tanstack/react-query@5.90.20(react@19.2.1))(@types/react@19.2.10)(@vercel/functions@2.2.13)(bufferutil@4.1.0)(fastestsmallesttextencoderdecoder@1.0.22)(react@19.2.1)(typescript@5.9.3)(utf-8-validate@5.0.10)(viem@2.45.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(zod@3.25.76))(zod@3.25.76)': + '@wagmi/connectors@5.9.9(@types/react@19.1.12)(@vercel/functions@2.2.13)(@wagmi/core@2.20.3(@tanstack/query-core@5.90.11)(@types/react@19.1.12)(react@19.2.1)(typescript@5.9.2)(use-sync-external-store@1.4.0(react@19.2.1))(viem@2.40.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76)))(bufferutil@4.0.9)(react@19.2.1)(typescript@5.9.2)(use-sync-external-store@1.4.0(react@19.2.1))(utf-8-validate@5.0.10)(viem@2.40.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76))(zod@3.25.76)': dependencies: - '@base-org/account': 1.1.1(@types/react@19.2.10)(bufferutil@4.1.0)(react@19.2.1)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.1))(utf-8-validate@5.0.10)(zod@3.25.76) - '@coinbase/wallet-sdk': 4.3.6(@types/react@19.2.10)(bufferutil@4.1.0)(react@19.2.1)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.1))(utf-8-validate@5.0.10)(zod@3.25.76) - '@gemini-wallet/core': 0.2.0(viem@2.45.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)) - '@metamask/sdk': 0.33.1(bufferutil@4.1.0)(utf-8-validate@5.0.10) - '@safe-global/safe-apps-provider': 0.18.6(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) - '@safe-global/safe-apps-sdk': 9.1.0(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) - '@wagmi/core': 2.22.1(@tanstack/query-core@5.90.20)(@types/react@19.2.10)(react@19.2.1)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.1))(viem@2.45.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)) - '@walletconnect/ethereum-provider': 2.21.1(@types/react@19.2.10)(@vercel/functions@2.2.13)(bufferutil@4.1.0)(react@19.2.1)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + '@base-org/account': 1.1.1(@types/react@19.1.12)(bufferutil@4.0.9)(react@19.2.1)(typescript@5.9.2)(use-sync-external-store@1.4.0(react@19.2.1))(utf-8-validate@5.0.10)(zod@3.25.76) + '@coinbase/wallet-sdk': 4.3.6(@types/react@19.1.12)(bufferutil@4.0.9)(react@19.2.1)(typescript@5.9.2)(use-sync-external-store@1.4.0(react@19.2.1))(utf-8-validate@5.0.10)(zod@3.25.76) + '@gemini-wallet/core': 0.2.0(viem@2.40.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76)) + '@metamask/sdk': 0.32.0(bufferutil@4.0.9)(utf-8-validate@5.0.10) + '@safe-global/safe-apps-provider': 0.18.6(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + '@safe-global/safe-apps-sdk': 9.1.0(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + '@wagmi/core': 2.20.3(@tanstack/query-core@5.90.11)(@types/react@19.1.12)(react@19.2.1)(typescript@5.9.2)(use-sync-external-store@1.4.0(react@19.2.1))(viem@2.40.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76)) + '@walletconnect/ethereum-provider': 2.21.1(@types/react@19.1.12)(@vercel/functions@2.2.13)(bufferutil@4.0.9)(react@19.2.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) cbw-sdk: '@coinbase/wallet-sdk@3.9.3' - porto: 0.2.19(@tanstack/react-query@5.90.20(react@19.2.1))(@types/react@19.2.10)(@wagmi/core@2.22.1(@tanstack/query-core@5.90.20)(@types/react@19.2.10)(react@19.2.1)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.1))(viem@2.45.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)))(react@19.2.1)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.1))(viem@2.45.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(wagmi@2.19.5(@tanstack/query-core@5.90.20)(@tanstack/react-query@5.90.20(react@19.2.1))(@types/react@19.2.10)(@vercel/functions@2.2.13)(bufferutil@4.1.0)(fastestsmallesttextencoderdecoder@1.0.22)(react@19.2.1)(typescript@5.9.3)(utf-8-validate@5.0.10)(viem@2.45.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(zod@3.25.76)) - viem: 2.45.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + viem: 2.40.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) optionalDependencies: - typescript: 5.9.3 + typescript: 5.9.2 transitivePeerDependencies: - '@azure/app-configuration' - '@azure/cosmos' @@ -12720,7 +13912,6 @@ snapshots: - '@netlify/blobs' - '@planetscale/database' - '@react-native-async-storage/async-storage' - - '@tanstack/react-query' - '@types/react' - '@upstash/redis' - '@vercel/blob' @@ -12737,24 +13928,22 @@ snapshots: - uploadthing - use-sync-external-store - utf-8-validate - - wagmi - zod - '@wagmi/connectors@5.11.2(@tanstack/react-query@5.90.20(react@19.2.4))(@types/react@19.2.10)(@vercel/functions@2.2.13)(@wagmi/core@2.22.1(@tanstack/query-core@5.90.20)(@types/react@19.2.10)(react@19.2.4)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.4))(viem@2.45.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)))(bufferutil@4.1.0)(react@19.2.4)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.4))(utf-8-validate@5.0.10)(viem@2.45.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(wagmi@2.19.5(@tanstack/query-core@5.90.20)(@tanstack/react-query@5.90.20(react@19.2.4))(@types/react@19.2.10)(@vercel/functions@2.2.13)(bufferutil@4.1.0)(fastestsmallesttextencoderdecoder@1.0.22)(react@19.2.4)(typescript@5.9.3)(utf-8-validate@5.0.10)(viem@2.45.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(zod@3.25.76))(zod@3.25.76)': + '@wagmi/connectors@5.9.9(@types/react@19.1.12)(@vercel/functions@2.2.13)(@wagmi/core@2.20.3(@tanstack/query-core@5.90.11)(@types/react@19.1.12)(react@19.2.3)(typescript@5.9.2)(use-sync-external-store@1.4.0(react@19.2.3))(viem@2.37.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76)))(bufferutil@4.0.9)(react@19.2.3)(typescript@5.9.2)(use-sync-external-store@1.4.0(react@19.2.3))(utf-8-validate@5.0.10)(viem@2.37.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76))(zod@3.25.76)': dependencies: - '@base-org/account': 1.1.1(@types/react@19.2.10)(bufferutil@4.1.0)(react@19.2.4)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.4))(utf-8-validate@5.0.10)(zod@3.25.76) - '@coinbase/wallet-sdk': 4.3.6(@types/react@19.2.10)(bufferutil@4.1.0)(react@19.2.4)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.4))(utf-8-validate@5.0.10)(zod@3.25.76) - '@gemini-wallet/core': 0.2.0(viem@2.45.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)) - '@metamask/sdk': 0.33.1(bufferutil@4.1.0)(utf-8-validate@5.0.10) - '@safe-global/safe-apps-provider': 0.18.6(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) - '@safe-global/safe-apps-sdk': 9.1.0(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) - '@wagmi/core': 2.22.1(@tanstack/query-core@5.90.20)(@types/react@19.2.10)(react@19.2.4)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.4))(viem@2.45.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)) - '@walletconnect/ethereum-provider': 2.21.1(@types/react@19.2.10)(@vercel/functions@2.2.13)(bufferutil@4.1.0)(react@19.2.4)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + '@base-org/account': 1.1.1(@types/react@19.1.12)(bufferutil@4.0.9)(react@19.2.3)(typescript@5.9.2)(use-sync-external-store@1.4.0(react@19.2.3))(utf-8-validate@5.0.10)(zod@3.25.76) + '@coinbase/wallet-sdk': 4.3.6(@types/react@19.1.12)(bufferutil@4.0.9)(react@19.2.3)(typescript@5.9.2)(use-sync-external-store@1.4.0(react@19.2.3))(utf-8-validate@5.0.10)(zod@3.25.76) + '@gemini-wallet/core': 0.2.0(viem@2.37.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76)) + '@metamask/sdk': 0.32.0(bufferutil@4.0.9)(utf-8-validate@5.0.10) + '@safe-global/safe-apps-provider': 0.18.6(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + '@safe-global/safe-apps-sdk': 9.1.0(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + '@wagmi/core': 2.20.3(@tanstack/query-core@5.90.11)(@types/react@19.1.12)(react@19.2.3)(typescript@5.9.2)(use-sync-external-store@1.4.0(react@19.2.3))(viem@2.37.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76)) + '@walletconnect/ethereum-provider': 2.21.1(@types/react@19.1.12)(@vercel/functions@2.2.13)(bufferutil@4.0.9)(react@19.2.3)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) cbw-sdk: '@coinbase/wallet-sdk@3.9.3' - porto: 0.2.19(@tanstack/react-query@5.90.20(react@19.2.4))(@types/react@19.2.10)(@wagmi/core@2.22.1(@tanstack/query-core@5.90.20)(@types/react@19.2.10)(react@19.2.4)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.4))(viem@2.45.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)))(react@19.2.4)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.4))(viem@2.45.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(wagmi@2.19.5(@tanstack/query-core@5.90.20)(@tanstack/react-query@5.90.20(react@19.2.4))(@types/react@19.2.10)(@vercel/functions@2.2.13)(bufferutil@4.1.0)(fastestsmallesttextencoderdecoder@1.0.22)(react@19.2.4)(typescript@5.9.3)(utf-8-validate@5.0.10)(viem@2.45.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(zod@3.25.76)) - viem: 2.45.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + viem: 2.37.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) optionalDependencies: - typescript: 5.9.3 + typescript: 5.9.2 transitivePeerDependencies: - '@azure/app-configuration' - '@azure/cosmos' @@ -12767,7 +13956,6 @@ snapshots: - '@netlify/blobs' - '@planetscale/database' - '@react-native-async-storage/async-storage' - - '@tanstack/react-query' - '@types/react' - '@upstash/redis' - '@vercel/blob' @@ -12784,24 +13972,23 @@ snapshots: - uploadthing - use-sync-external-store - utf-8-validate - - wagmi - zod - '@wagmi/connectors@6.2.0(@tanstack/react-query@5.90.20(react@19.2.1))(@types/react@19.2.10)(@vercel/functions@2.2.13)(@wagmi/core@2.22.1(@tanstack/query-core@5.90.20)(@types/react@19.2.10)(react@19.2.1)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.1))(viem@2.45.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)))(bufferutil@4.1.0)(fastestsmallesttextencoderdecoder@1.0.22)(react@19.2.1)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.1))(utf-8-validate@5.0.10)(viem@2.45.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(wagmi@2.19.5(@tanstack/query-core@5.90.20)(@tanstack/react-query@5.90.20(react@19.2.1))(@types/react@19.2.10)(@vercel/functions@2.2.13)(bufferutil@4.1.0)(fastestsmallesttextencoderdecoder@1.0.22)(react@19.2.1)(typescript@5.9.3)(utf-8-validate@5.0.10)(viem@2.45.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(zod@3.25.76))(zod@3.25.76)': + '@wagmi/connectors@6.2.0(@tanstack/react-query@5.90.11(react@19.2.1))(@types/react@19.1.12)(@vercel/functions@2.2.13)(@wagmi/core@2.22.1(@tanstack/query-core@5.90.11)(@types/react@19.1.12)(react@19.2.1)(typescript@5.9.2)(use-sync-external-store@1.4.0(react@19.2.1))(viem@2.40.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76)))(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(react@19.2.1)(typescript@5.9.2)(use-sync-external-store@1.4.0(react@19.2.1))(utf-8-validate@5.0.10)(viem@2.40.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76))(wagmi@2.19.5(@tanstack/query-core@5.90.11)(@tanstack/react-query@5.90.11(react@19.2.1))(@types/react@19.1.12)(@vercel/functions@2.2.13)(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(react@19.2.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(viem@2.40.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76))(zod@3.25.76))(zod@3.25.76)': dependencies: - '@base-org/account': 2.4.0(@types/react@19.2.10)(bufferutil@4.1.0)(fastestsmallesttextencoderdecoder@1.0.22)(react@19.2.1)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.1))(utf-8-validate@5.0.10)(zod@3.25.76) - '@coinbase/wallet-sdk': 4.3.6(@types/react@19.2.10)(bufferutil@4.1.0)(react@19.2.1)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.1))(utf-8-validate@5.0.10)(zod@3.25.76) - '@gemini-wallet/core': 0.3.2(viem@2.45.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)) - '@metamask/sdk': 0.33.1(bufferutil@4.1.0)(utf-8-validate@5.0.10) - '@safe-global/safe-apps-provider': 0.18.6(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) - '@safe-global/safe-apps-sdk': 9.1.0(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) - '@wagmi/core': 2.22.1(@tanstack/query-core@5.90.20)(@types/react@19.2.10)(react@19.2.1)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.1))(viem@2.45.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)) - '@walletconnect/ethereum-provider': 2.21.1(@types/react@19.2.10)(@vercel/functions@2.2.13)(bufferutil@4.1.0)(react@19.2.1)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + '@base-org/account': 2.4.0(@types/react@19.1.12)(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(react@19.2.1)(typescript@5.9.2)(use-sync-external-store@1.4.0(react@19.2.1))(utf-8-validate@5.0.10)(zod@3.25.76) + '@coinbase/wallet-sdk': 4.3.6(@types/react@19.1.12)(bufferutil@4.0.9)(react@19.2.1)(typescript@5.9.2)(use-sync-external-store@1.4.0(react@19.2.1))(utf-8-validate@5.0.10)(zod@3.25.76) + '@gemini-wallet/core': 0.3.2(viem@2.40.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76)) + '@metamask/sdk': 0.33.1(bufferutil@4.0.9)(utf-8-validate@5.0.10) + '@safe-global/safe-apps-provider': 0.18.6(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + '@safe-global/safe-apps-sdk': 9.1.0(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + '@wagmi/core': 2.22.1(@tanstack/query-core@5.90.11)(@types/react@19.1.12)(react@19.2.1)(typescript@5.9.2)(use-sync-external-store@1.4.0(react@19.2.1))(viem@2.40.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76)) + '@walletconnect/ethereum-provider': 2.21.1(@types/react@19.1.12)(@vercel/functions@2.2.13)(bufferutil@4.0.9)(react@19.2.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) cbw-sdk: '@coinbase/wallet-sdk@3.9.3' - porto: 0.2.35(@tanstack/react-query@5.90.20(react@19.2.1))(@types/react@19.2.10)(@wagmi/core@2.22.1(@tanstack/query-core@5.90.20)(@types/react@19.2.10)(react@19.2.1)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.1))(viem@2.45.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)))(react@19.2.1)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.1))(viem@2.45.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(wagmi@2.19.5(@tanstack/query-core@5.90.20)(@tanstack/react-query@5.90.20(react@19.2.1))(@types/react@19.2.10)(@vercel/functions@2.2.13)(bufferutil@4.1.0)(fastestsmallesttextencoderdecoder@1.0.22)(react@19.2.1)(typescript@5.9.3)(utf-8-validate@5.0.10)(viem@2.45.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(zod@3.25.76)) - viem: 2.45.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + porto: 0.2.35(@tanstack/react-query@5.90.11(react@19.2.1))(@types/react@19.1.12)(@wagmi/core@2.22.1(@tanstack/query-core@5.90.11)(@types/react@19.1.12)(react@19.2.1)(typescript@5.9.2)(use-sync-external-store@1.4.0(react@19.2.1))(viem@2.40.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76)))(react@19.2.1)(typescript@5.9.2)(use-sync-external-store@1.4.0(react@19.2.1))(viem@2.40.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76))(wagmi@2.19.5(@tanstack/query-core@5.90.11)(@tanstack/react-query@5.90.11(react@19.2.1))(@types/react@19.1.12)(@vercel/functions@2.2.13)(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(react@19.2.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(viem@2.40.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76))(zod@3.25.76)) + viem: 2.40.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) optionalDependencies: - typescript: 5.9.3 + typescript: 5.9.2 transitivePeerDependencies: - '@azure/app-configuration' - '@azure/cosmos' @@ -12840,21 +14027,21 @@ snapshots: - wagmi - zod - '@wagmi/connectors@6.2.0(@tanstack/react-query@5.90.20(react@19.2.4))(@types/react@19.2.10)(@vercel/functions@2.2.13)(@wagmi/core@2.22.1(@tanstack/query-core@5.90.20)(@types/react@19.2.10)(react@19.2.4)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.4))(viem@2.45.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)))(bufferutil@4.1.0)(fastestsmallesttextencoderdecoder@1.0.22)(react@19.2.4)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.4))(utf-8-validate@5.0.10)(viem@2.45.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(wagmi@2.19.5(@tanstack/query-core@5.90.20)(@tanstack/react-query@5.90.20(react@19.2.4))(@types/react@19.2.10)(@vercel/functions@2.2.13)(bufferutil@4.1.0)(fastestsmallesttextencoderdecoder@1.0.22)(react@19.2.4)(typescript@5.9.3)(utf-8-validate@5.0.10)(viem@2.45.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(zod@3.25.76))(zod@3.25.76)': + '@wagmi/connectors@6.2.0(@tanstack/react-query@5.90.11(react@19.2.1))(@types/react@19.1.12)(@vercel/functions@2.2.13)(@wagmi/core@2.22.1(@tanstack/query-core@5.90.11)(@types/react@19.1.12)(react@19.2.1)(typescript@5.9.2)(use-sync-external-store@1.4.0(react@19.2.1))(viem@2.45.1(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76)))(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(react@19.2.1)(typescript@5.9.2)(use-sync-external-store@1.4.0(react@19.2.1))(utf-8-validate@5.0.10)(viem@2.45.1(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76))(wagmi@2.19.5(@tanstack/query-core@5.90.11)(@tanstack/react-query@5.90.11(react@19.2.1))(@types/react@19.1.12)(@vercel/functions@2.2.13)(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(react@19.2.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(viem@2.40.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76))(zod@3.25.76))(zod@3.25.76)': dependencies: - '@base-org/account': 2.4.0(@types/react@19.2.10)(bufferutil@4.1.0)(fastestsmallesttextencoderdecoder@1.0.22)(react@19.2.4)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.4))(utf-8-validate@5.0.10)(zod@3.25.76) - '@coinbase/wallet-sdk': 4.3.6(@types/react@19.2.10)(bufferutil@4.1.0)(react@19.2.4)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.4))(utf-8-validate@5.0.10)(zod@3.25.76) - '@gemini-wallet/core': 0.3.2(viem@2.45.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)) - '@metamask/sdk': 0.33.1(bufferutil@4.1.0)(utf-8-validate@5.0.10) - '@safe-global/safe-apps-provider': 0.18.6(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) - '@safe-global/safe-apps-sdk': 9.1.0(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) - '@wagmi/core': 2.22.1(@tanstack/query-core@5.90.20)(@types/react@19.2.10)(react@19.2.4)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.4))(viem@2.45.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)) - '@walletconnect/ethereum-provider': 2.21.1(@types/react@19.2.10)(@vercel/functions@2.2.13)(bufferutil@4.1.0)(react@19.2.4)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + '@base-org/account': 2.4.0(@types/react@19.1.12)(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(react@19.2.1)(typescript@5.9.2)(use-sync-external-store@1.4.0(react@19.2.1))(utf-8-validate@5.0.10)(zod@3.25.76) + '@coinbase/wallet-sdk': 4.3.6(@types/react@19.1.12)(bufferutil@4.0.9)(react@19.2.1)(typescript@5.9.2)(use-sync-external-store@1.4.0(react@19.2.1))(utf-8-validate@5.0.10)(zod@3.25.76) + '@gemini-wallet/core': 0.3.2(viem@2.45.1(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76)) + '@metamask/sdk': 0.33.1(bufferutil@4.0.9)(utf-8-validate@5.0.10) + '@safe-global/safe-apps-provider': 0.18.6(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + '@safe-global/safe-apps-sdk': 9.1.0(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + '@wagmi/core': 2.22.1(@tanstack/query-core@5.90.11)(@types/react@19.1.12)(react@19.2.1)(typescript@5.9.2)(use-sync-external-store@1.4.0(react@19.2.1))(viem@2.45.1(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76)) + '@walletconnect/ethereum-provider': 2.21.1(@types/react@19.1.12)(@vercel/functions@2.2.13)(bufferutil@4.0.9)(react@19.2.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) cbw-sdk: '@coinbase/wallet-sdk@3.9.3' - porto: 0.2.35(@tanstack/react-query@5.90.20(react@19.2.4))(@types/react@19.2.10)(@wagmi/core@2.22.1(@tanstack/query-core@5.90.20)(@types/react@19.2.10)(react@19.2.4)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.4))(viem@2.45.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)))(react@19.2.4)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.4))(viem@2.45.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(wagmi@2.19.5(@tanstack/query-core@5.90.20)(@tanstack/react-query@5.90.20(react@19.2.4))(@types/react@19.2.10)(@vercel/functions@2.2.13)(bufferutil@4.1.0)(fastestsmallesttextencoderdecoder@1.0.22)(react@19.2.4)(typescript@5.9.3)(utf-8-validate@5.0.10)(viem@2.45.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(zod@3.25.76)) - viem: 2.45.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + porto: 0.2.35(@tanstack/react-query@5.90.11(react@19.2.1))(@types/react@19.1.12)(@wagmi/core@2.22.1(@tanstack/query-core@5.90.11)(@types/react@19.1.12)(react@19.2.1)(typescript@5.9.2)(use-sync-external-store@1.4.0(react@19.2.1))(viem@2.45.1(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76)))(react@19.2.1)(typescript@5.9.2)(use-sync-external-store@1.4.0(react@19.2.1))(viem@2.45.1(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76))(wagmi@2.19.5(@tanstack/query-core@5.90.11)(@tanstack/react-query@5.90.11(react@19.2.1))(@types/react@19.1.12)(@vercel/functions@2.2.13)(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(react@19.2.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(viem@2.40.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76))(zod@3.25.76)) + viem: 2.45.1(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) optionalDependencies: - typescript: 5.9.3 + typescript: 5.9.2 transitivePeerDependencies: - '@azure/app-configuration' - '@azure/cosmos' @@ -12893,30 +14080,75 @@ snapshots: - wagmi - zod - '@wagmi/core@2.22.1(@tanstack/query-core@5.90.20)(@types/react@19.2.10)(react@19.2.1)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.1))(viem@2.45.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))': + '@wagmi/core@2.20.3(@tanstack/query-core@5.90.11)(@types/react@19.1.12)(react@19.2.1)(typescript@5.9.2)(use-sync-external-store@1.4.0(react@19.2.1))(viem@2.40.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76))': + dependencies: + eventemitter3: 5.0.1 + mipd: 0.0.7(typescript@5.9.2) + viem: 2.40.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + zustand: 5.0.0(@types/react@19.1.12)(react@19.2.1)(use-sync-external-store@1.4.0(react@19.2.1)) + optionalDependencies: + '@tanstack/query-core': 5.90.11 + typescript: 5.9.2 + transitivePeerDependencies: + - '@types/react' + - immer + - react + - use-sync-external-store + + '@wagmi/core@2.20.3(@tanstack/query-core@5.90.11)(@types/react@19.1.12)(react@19.2.1)(typescript@5.9.2)(use-sync-external-store@1.4.0(react@19.2.1))(viem@2.45.1(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76))': + dependencies: + eventemitter3: 5.0.1 + mipd: 0.0.7(typescript@5.9.2) + viem: 2.45.1(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + zustand: 5.0.0(@types/react@19.1.12)(react@19.2.1)(use-sync-external-store@1.4.0(react@19.2.1)) + optionalDependencies: + '@tanstack/query-core': 5.90.11 + typescript: 5.9.2 + transitivePeerDependencies: + - '@types/react' + - immer + - react + - use-sync-external-store + + '@wagmi/core@2.20.3(@tanstack/query-core@5.90.11)(@types/react@19.1.12)(react@19.2.3)(typescript@5.9.2)(use-sync-external-store@1.4.0(react@19.2.3))(viem@2.37.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76))': + dependencies: + eventemitter3: 5.0.1 + mipd: 0.0.7(typescript@5.9.2) + viem: 2.37.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + zustand: 5.0.0(@types/react@19.1.12)(react@19.2.3)(use-sync-external-store@1.4.0(react@19.2.3)) + optionalDependencies: + '@tanstack/query-core': 5.90.11 + typescript: 5.9.2 + transitivePeerDependencies: + - '@types/react' + - immer + - react + - use-sync-external-store + + '@wagmi/core@2.22.1(@tanstack/query-core@5.90.11)(@types/react@19.1.12)(react@19.2.1)(typescript@5.9.2)(use-sync-external-store@1.4.0(react@19.2.1))(viem@2.40.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76))': dependencies: eventemitter3: 5.0.1 - mipd: 0.0.7(typescript@5.9.3) - viem: 2.45.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) - zustand: 5.0.0(@types/react@19.2.10)(react@19.2.1)(use-sync-external-store@1.4.0(react@19.2.1)) + mipd: 0.0.7(typescript@5.9.2) + viem: 2.40.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + zustand: 5.0.0(@types/react@19.1.12)(react@19.2.1)(use-sync-external-store@1.4.0(react@19.2.1)) optionalDependencies: - '@tanstack/query-core': 5.90.20 - typescript: 5.9.3 + '@tanstack/query-core': 5.90.11 + typescript: 5.9.2 transitivePeerDependencies: - '@types/react' - immer - react - use-sync-external-store - '@wagmi/core@2.22.1(@tanstack/query-core@5.90.20)(@types/react@19.2.10)(react@19.2.4)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.4))(viem@2.45.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))': + '@wagmi/core@2.22.1(@tanstack/query-core@5.90.11)(@types/react@19.1.12)(react@19.2.1)(typescript@5.9.2)(use-sync-external-store@1.4.0(react@19.2.1))(viem@2.45.1(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76))': dependencies: eventemitter3: 5.0.1 - mipd: 0.0.7(typescript@5.9.3) - viem: 2.45.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) - zustand: 5.0.0(@types/react@19.2.10)(react@19.2.4)(use-sync-external-store@1.4.0(react@19.2.4)) + mipd: 0.0.7(typescript@5.9.2) + viem: 2.45.1(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + zustand: 5.0.0(@types/react@19.1.12)(react@19.2.1)(use-sync-external-store@1.4.0(react@19.2.1)) optionalDependencies: - '@tanstack/query-core': 5.90.20 - typescript: 5.9.3 + '@tanstack/query-core': 5.90.11 + typescript: 5.9.2 transitivePeerDependencies: - '@types/react' - immer @@ -12933,13 +14165,13 @@ snapshots: dependencies: '@wallet-standard/base': 1.1.0 - '@walletconnect/core@2.21.0(@vercel/functions@2.2.13)(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)': + '@walletconnect/core@2.21.0(@vercel/functions@2.2.13)(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76)': dependencies: '@walletconnect/heartbeat': 1.2.2 '@walletconnect/jsonrpc-provider': 1.0.14 '@walletconnect/jsonrpc-types': 1.0.4 '@walletconnect/jsonrpc-utils': 1.0.8 - '@walletconnect/jsonrpc-ws-connection': 1.0.16(bufferutil@4.1.0)(utf-8-validate@5.0.10) + '@walletconnect/jsonrpc-ws-connection': 1.0.16(bufferutil@4.0.9)(utf-8-validate@5.0.10) '@walletconnect/keyvaluestorage': 1.1.1(@vercel/functions@2.2.13) '@walletconnect/logger': 2.1.2 '@walletconnect/relay-api': 1.0.11 @@ -12947,7 +14179,7 @@ snapshots: '@walletconnect/safe-json': 1.0.2 '@walletconnect/time': 1.0.2 '@walletconnect/types': 2.21.0(@vercel/functions@2.2.13) - '@walletconnect/utils': 2.21.0(@vercel/functions@2.2.13)(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + '@walletconnect/utils': 2.21.0(@vercel/functions@2.2.13)(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) '@walletconnect/window-getters': 1.0.1 es-toolkit: 1.33.0 events: 3.3.0 @@ -12977,13 +14209,13 @@ snapshots: - utf-8-validate - zod - '@walletconnect/core@2.21.1(@vercel/functions@2.2.13)(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)': + '@walletconnect/core@2.21.1(@vercel/functions@2.2.13)(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76)': dependencies: '@walletconnect/heartbeat': 1.2.2 '@walletconnect/jsonrpc-provider': 1.0.14 '@walletconnect/jsonrpc-types': 1.0.4 '@walletconnect/jsonrpc-utils': 1.0.8 - '@walletconnect/jsonrpc-ws-connection': 1.0.16(bufferutil@4.1.0)(utf-8-validate@5.0.10) + '@walletconnect/jsonrpc-ws-connection': 1.0.16(bufferutil@4.0.9)(utf-8-validate@5.0.10) '@walletconnect/keyvaluestorage': 1.1.1(@vercel/functions@2.2.13) '@walletconnect/logger': 2.1.2 '@walletconnect/relay-api': 1.0.11 @@ -12991,7 +14223,7 @@ snapshots: '@walletconnect/safe-json': 1.0.2 '@walletconnect/time': 1.0.2 '@walletconnect/types': 2.21.1(@vercel/functions@2.2.13) - '@walletconnect/utils': 2.21.1(@vercel/functions@2.2.13)(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + '@walletconnect/utils': 2.21.1(@vercel/functions@2.2.13)(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) '@walletconnect/window-getters': 1.0.1 es-toolkit: 1.33.0 events: 3.3.0 @@ -13025,18 +14257,18 @@ snapshots: dependencies: tslib: 1.14.1 - '@walletconnect/ethereum-provider@2.21.1(@types/react@19.2.10)(@vercel/functions@2.2.13)(bufferutil@4.1.0)(react@19.2.1)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)': + '@walletconnect/ethereum-provider@2.21.1(@types/react@19.1.12)(@vercel/functions@2.2.13)(bufferutil@4.0.9)(react@19.2.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76)': dependencies: - '@reown/appkit': 1.7.8(@types/react@19.2.10)(@vercel/functions@2.2.13)(bufferutil@4.1.0)(react@19.2.1)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + '@reown/appkit': 1.7.8(@types/react@19.1.12)(@vercel/functions@2.2.13)(bufferutil@4.0.9)(react@19.2.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) '@walletconnect/jsonrpc-http-connection': 1.0.8 '@walletconnect/jsonrpc-provider': 1.0.14 '@walletconnect/jsonrpc-types': 1.0.4 '@walletconnect/jsonrpc-utils': 1.0.8 '@walletconnect/keyvaluestorage': 1.1.1(@vercel/functions@2.2.13) - '@walletconnect/sign-client': 2.21.1(@vercel/functions@2.2.13)(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + '@walletconnect/sign-client': 2.21.1(@vercel/functions@2.2.13)(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) '@walletconnect/types': 2.21.1(@vercel/functions@2.2.13) - '@walletconnect/universal-provider': 2.21.1(@vercel/functions@2.2.13)(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) - '@walletconnect/utils': 2.21.1(@vercel/functions@2.2.13)(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + '@walletconnect/universal-provider': 2.21.1(@vercel/functions@2.2.13)(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + '@walletconnect/utils': 2.21.1(@vercel/functions@2.2.13)(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) events: 3.3.0 transitivePeerDependencies: - '@azure/app-configuration' @@ -13066,18 +14298,18 @@ snapshots: - utf-8-validate - zod - '@walletconnect/ethereum-provider@2.21.1(@types/react@19.2.10)(@vercel/functions@2.2.13)(bufferutil@4.1.0)(react@19.2.4)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)': + '@walletconnect/ethereum-provider@2.21.1(@types/react@19.1.12)(@vercel/functions@2.2.13)(bufferutil@4.0.9)(react@19.2.3)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76)': dependencies: - '@reown/appkit': 1.7.8(@types/react@19.2.10)(@vercel/functions@2.2.13)(bufferutil@4.1.0)(react@19.2.4)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + '@reown/appkit': 1.7.8(@types/react@19.1.12)(@vercel/functions@2.2.13)(bufferutil@4.0.9)(react@19.2.3)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) '@walletconnect/jsonrpc-http-connection': 1.0.8 '@walletconnect/jsonrpc-provider': 1.0.14 '@walletconnect/jsonrpc-types': 1.0.4 '@walletconnect/jsonrpc-utils': 1.0.8 '@walletconnect/keyvaluestorage': 1.1.1(@vercel/functions@2.2.13) - '@walletconnect/sign-client': 2.21.1(@vercel/functions@2.2.13)(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + '@walletconnect/sign-client': 2.21.1(@vercel/functions@2.2.13)(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) '@walletconnect/types': 2.21.1(@vercel/functions@2.2.13) - '@walletconnect/universal-provider': 2.21.1(@vercel/functions@2.2.13)(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) - '@walletconnect/utils': 2.21.1(@vercel/functions@2.2.13)(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + '@walletconnect/universal-provider': 2.21.1(@vercel/functions@2.2.13)(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + '@walletconnect/utils': 2.21.1(@vercel/functions@2.2.13)(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) events: 3.3.0 transitivePeerDependencies: - '@azure/app-configuration' @@ -13144,12 +14376,12 @@ snapshots: '@walletconnect/jsonrpc-types': 1.0.4 tslib: 1.14.1 - '@walletconnect/jsonrpc-ws-connection@1.0.16(bufferutil@4.1.0)(utf-8-validate@5.0.10)': + '@walletconnect/jsonrpc-ws-connection@1.0.16(bufferutil@4.0.9)(utf-8-validate@5.0.10)': dependencies: '@walletconnect/jsonrpc-utils': 1.0.8 '@walletconnect/safe-json': 1.0.2 events: 3.3.0 - ws: 7.5.10(bufferutil@4.1.0)(utf-8-validate@5.0.10) + ws: 7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10) transitivePeerDependencies: - bufferutil - utf-8-validate @@ -13158,7 +14390,7 @@ snapshots: dependencies: '@walletconnect/safe-json': 1.0.2 idb-keyval: 6.2.2 - unstorage: 1.17.4(@vercel/functions@2.2.13)(idb-keyval@6.2.2) + unstorage: 1.17.0(@vercel/functions@2.2.13)(idb-keyval@6.2.2) transitivePeerDependencies: - '@azure/app-configuration' - '@azure/cosmos' @@ -13200,16 +14432,16 @@ snapshots: dependencies: tslib: 1.14.1 - '@walletconnect/sign-client@2.21.0(@vercel/functions@2.2.13)(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)': + '@walletconnect/sign-client@2.21.0(@vercel/functions@2.2.13)(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76)': dependencies: - '@walletconnect/core': 2.21.0(@vercel/functions@2.2.13)(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + '@walletconnect/core': 2.21.0(@vercel/functions@2.2.13)(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) '@walletconnect/events': 1.0.1 '@walletconnect/heartbeat': 1.2.2 '@walletconnect/jsonrpc-utils': 1.0.8 '@walletconnect/logger': 2.1.2 '@walletconnect/time': 1.0.2 '@walletconnect/types': 2.21.0(@vercel/functions@2.2.13) - '@walletconnect/utils': 2.21.0(@vercel/functions@2.2.13)(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + '@walletconnect/utils': 2.21.0(@vercel/functions@2.2.13)(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) events: 3.3.0 transitivePeerDependencies: - '@azure/app-configuration' @@ -13236,16 +14468,16 @@ snapshots: - utf-8-validate - zod - '@walletconnect/sign-client@2.21.1(@vercel/functions@2.2.13)(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)': + '@walletconnect/sign-client@2.21.1(@vercel/functions@2.2.13)(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76)': dependencies: - '@walletconnect/core': 2.21.1(@vercel/functions@2.2.13)(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + '@walletconnect/core': 2.21.1(@vercel/functions@2.2.13)(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) '@walletconnect/events': 1.0.1 '@walletconnect/heartbeat': 1.2.2 '@walletconnect/jsonrpc-utils': 1.0.8 '@walletconnect/logger': 2.1.2 '@walletconnect/time': 1.0.2 '@walletconnect/types': 2.21.1(@vercel/functions@2.2.13) - '@walletconnect/utils': 2.21.1(@vercel/functions@2.2.13)(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + '@walletconnect/utils': 2.21.1(@vercel/functions@2.2.13)(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) events: 3.3.0 transitivePeerDependencies: - '@azure/app-configuration' @@ -13334,7 +14566,7 @@ snapshots: - ioredis - uploadthing - '@walletconnect/universal-provider@2.21.0(@vercel/functions@2.2.13)(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)': + '@walletconnect/universal-provider@2.21.0(@vercel/functions@2.2.13)(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76)': dependencies: '@walletconnect/events': 1.0.1 '@walletconnect/jsonrpc-http-connection': 1.0.8 @@ -13343,9 +14575,9 @@ snapshots: '@walletconnect/jsonrpc-utils': 1.0.8 '@walletconnect/keyvaluestorage': 1.1.1(@vercel/functions@2.2.13) '@walletconnect/logger': 2.1.2 - '@walletconnect/sign-client': 2.21.0(@vercel/functions@2.2.13)(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + '@walletconnect/sign-client': 2.21.0(@vercel/functions@2.2.13)(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) '@walletconnect/types': 2.21.0(@vercel/functions@2.2.13) - '@walletconnect/utils': 2.21.0(@vercel/functions@2.2.13)(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + '@walletconnect/utils': 2.21.0(@vercel/functions@2.2.13)(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) es-toolkit: 1.33.0 events: 3.3.0 transitivePeerDependencies: @@ -13374,7 +14606,7 @@ snapshots: - utf-8-validate - zod - '@walletconnect/universal-provider@2.21.1(@vercel/functions@2.2.13)(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)': + '@walletconnect/universal-provider@2.21.1(@vercel/functions@2.2.13)(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76)': dependencies: '@walletconnect/events': 1.0.1 '@walletconnect/jsonrpc-http-connection': 1.0.8 @@ -13383,9 +14615,9 @@ snapshots: '@walletconnect/jsonrpc-utils': 1.0.8 '@walletconnect/keyvaluestorage': 1.1.1(@vercel/functions@2.2.13) '@walletconnect/logger': 2.1.2 - '@walletconnect/sign-client': 2.21.1(@vercel/functions@2.2.13)(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + '@walletconnect/sign-client': 2.21.1(@vercel/functions@2.2.13)(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) '@walletconnect/types': 2.21.1(@vercel/functions@2.2.13) - '@walletconnect/utils': 2.21.1(@vercel/functions@2.2.13)(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + '@walletconnect/utils': 2.21.1(@vercel/functions@2.2.13)(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) es-toolkit: 1.33.0 events: 3.3.0 transitivePeerDependencies: @@ -13414,7 +14646,7 @@ snapshots: - utf-8-validate - zod - '@walletconnect/utils@2.21.0(@vercel/functions@2.2.13)(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)': + '@walletconnect/utils@2.21.0(@vercel/functions@2.2.13)(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76)': dependencies: '@noble/ciphers': 1.2.1 '@noble/curves': 1.8.1 @@ -13432,7 +14664,7 @@ snapshots: detect-browser: 5.3.0 query-string: 7.1.3 uint8arrays: 3.1.0 - viem: 2.23.2(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + viem: 2.23.2(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) transitivePeerDependencies: - '@azure/app-configuration' - '@azure/cosmos' @@ -13458,7 +14690,7 @@ snapshots: - utf-8-validate - zod - '@walletconnect/utils@2.21.1(@vercel/functions@2.2.13)(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)': + '@walletconnect/utils@2.21.1(@vercel/functions@2.2.13)(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76)': dependencies: '@noble/ciphers': 1.2.1 '@noble/curves': 1.8.1 @@ -13476,7 +14708,7 @@ snapshots: detect-browser: 5.3.0 query-string: 7.1.3 uint8arrays: 3.1.0 - viem: 2.23.2(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + viem: 2.23.2(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) transitivePeerDependencies: - '@azure/app-configuration' - '@azure/cosmos' @@ -13511,30 +14743,37 @@ snapshots: '@walletconnect/window-getters': 1.0.1 tslib: 1.14.1 - abitype@1.0.6(typescript@5.9.3)(zod@3.25.76): + abitype@1.0.6(typescript@5.9.2)(zod@3.25.76): + optionalDependencies: + typescript: 5.9.2 + zod: 3.25.76 + + abitype@1.0.8(typescript@5.9.2)(zod@3.25.76): optionalDependencies: - typescript: 5.9.3 + typescript: 5.9.2 zod: 3.25.76 - abitype@1.0.8(typescript@5.9.3)(zod@3.25.76): + abitype@1.1.0(typescript@5.9.2)(zod@3.25.76): optionalDependencies: - typescript: 5.9.3 + typescript: 5.9.2 zod: 3.25.76 - abitype@1.2.3(typescript@5.9.3)(zod@3.22.4): + abitype@1.1.0(typescript@5.9.2)(zod@4.1.13): optionalDependencies: - typescript: 5.9.3 + typescript: 5.9.2 + zod: 4.1.13 + + abitype@1.2.3(typescript@5.9.2)(zod@3.22.4): + optionalDependencies: + typescript: 5.9.2 zod: 3.22.4 - abitype@1.2.3(typescript@5.9.3)(zod@3.25.76): + abitype@1.2.3(typescript@5.9.2)(zod@3.25.76): optionalDependencies: - typescript: 5.9.3 + typescript: 5.9.2 zod: 3.25.76 - abitype@1.2.3(typescript@5.9.3)(zod@4.3.6): - optionalDependencies: - typescript: 5.9.3 - zod: 4.3.6 + abstract-logging@2.0.1: {} accepts@1.3.8: dependencies: @@ -13582,10 +14821,14 @@ snapshots: ansi-regex@5.0.1: {} + ansi-regex@6.2.0: {} + ansi-styles@4.3.0: dependencies: color-convert: 2.0.1 + ansi-styles@6.2.1: {} + any-promise@1.3.0: {} anymatch@3.1.3: @@ -13617,7 +14860,7 @@ snapshots: call-bind: 1.0.8 call-bound: 1.0.4 define-properties: 1.2.1 - es-abstract: 1.24.1 + es-abstract: 1.24.0 es-object-atoms: 1.1.1 get-intrinsic: 1.3.0 is-string: 1.1.1 @@ -13629,7 +14872,7 @@ snapshots: dependencies: call-bind: 1.0.8 define-properties: 1.2.1 - es-abstract: 1.24.1 + es-abstract: 1.24.0 es-errors: 1.3.0 es-object-atoms: 1.1.1 es-shim-unscopables: 1.1.0 @@ -13639,7 +14882,7 @@ snapshots: call-bind: 1.0.8 call-bound: 1.0.4 define-properties: 1.2.1 - es-abstract: 1.24.1 + es-abstract: 1.24.0 es-errors: 1.3.0 es-object-atoms: 1.1.1 es-shim-unscopables: 1.1.0 @@ -13648,21 +14891,21 @@ snapshots: dependencies: call-bind: 1.0.8 define-properties: 1.2.1 - es-abstract: 1.24.1 + es-abstract: 1.24.0 es-shim-unscopables: 1.1.0 array.prototype.flatmap@1.3.3: dependencies: call-bind: 1.0.8 define-properties: 1.2.1 - es-abstract: 1.24.1 + es-abstract: 1.24.0 es-shim-unscopables: 1.1.0 array.prototype.tosorted@1.1.4: dependencies: call-bind: 1.0.8 define-properties: 1.2.1 - es-abstract: 1.24.1 + es-abstract: 1.24.0 es-errors: 1.3.0 es-shim-unscopables: 1.1.0 @@ -13671,7 +14914,7 @@ snapshots: array-buffer-byte-length: 1.0.2 call-bind: 1.0.8 define-properties: 1.2.1 - es-abstract: 1.24.1 + es-abstract: 1.24.0 es-errors: 1.3.0 get-intrinsic: 1.3.0 is-array-buffer: 3.0.5 @@ -13694,44 +14937,57 @@ snapshots: dependencies: possible-typed-array-names: 1.1.0 - axe-core@4.11.1: {} + avvio@9.2.0: + dependencies: + '@fastify/error': 4.2.0 + fastq: 1.19.1 + + axe-core@4.10.3: {} - axios-retry@4.5.0(axios@1.13.4): + axios-retry@4.5.0(axios@1.11.0): dependencies: - axios: 1.13.4 + axios: 1.11.0 is-retry-allowed: 2.2.0 + axios@1.11.0: + dependencies: + follow-redirects: 1.15.11 + form-data: 4.0.4 + proxy-from-env: 1.1.0 + transitivePeerDependencies: + - debug + axios@1.13.4: dependencies: follow-redirects: 1.15.11 - form-data: 4.0.5 + form-data: 4.0.4 proxy-from-env: 1.1.0 transitivePeerDependencies: - debug axobject-query@4.1.0: {} - babel-plugin-polyfill-corejs2@0.4.15(@babel/core@7.29.0): + babel-plugin-polyfill-corejs2@0.4.14(@babel/core@7.28.3): dependencies: - '@babel/compat-data': 7.29.0 - '@babel/core': 7.29.0 - '@babel/helper-define-polyfill-provider': 0.6.6(@babel/core@7.29.0) + '@babel/compat-data': 7.28.0 + '@babel/core': 7.28.3 + '@babel/helper-define-polyfill-provider': 0.6.5(@babel/core@7.28.3) semver: 6.3.1 transitivePeerDependencies: - supports-color - babel-plugin-polyfill-corejs3@0.14.0(@babel/core@7.29.0): + babel-plugin-polyfill-corejs3@0.13.0(@babel/core@7.28.3): dependencies: - '@babel/core': 7.29.0 - '@babel/helper-define-polyfill-provider': 0.6.6(@babel/core@7.29.0) - core-js-compat: 3.48.0 + '@babel/core': 7.28.3 + '@babel/helper-define-polyfill-provider': 0.6.5(@babel/core@7.28.3) + core-js-compat: 3.45.1 transitivePeerDependencies: - supports-color - babel-plugin-polyfill-regenerator@0.6.6(@babel/core@7.29.0): + babel-plugin-polyfill-regenerator@0.6.5(@babel/core@7.28.3): dependencies: - '@babel/core': 7.29.0 - '@babel/helper-define-polyfill-provider': 0.6.6(@babel/core@7.29.0) + '@babel/core': 7.28.3 + '@babel/helper-define-polyfill-provider': 0.6.5(@babel/core@7.28.3) transitivePeerDependencies: - supports-color @@ -13743,30 +14999,45 @@ snapshots: base-x@5.0.1: {} - base64-js@1.5.1: {} + base32.js@0.1.0: {} - baseline-browser-mapping@2.9.19: {} + base64-js@1.5.1: {} better-path-resolve@1.0.0: dependencies: is-windows: 1.0.2 + bidi-js@1.0.3: + dependencies: + require-from-string: 2.0.2 + optional: true + big.js@6.2.2: {} + bigint-buffer@1.1.5: + dependencies: + bindings: 1.5.0 + + bignumber.js@9.3.1: {} + + bindings@1.5.0: + dependencies: + file-uri-to-path: 1.0.0 + bn.js@5.2.2: {} - body-parser@1.20.4: + body-parser@1.20.3: dependencies: bytes: 3.1.2 content-type: 1.0.5 debug: 2.6.9 depd: 2.0.0 destroy: 1.2.0 - http-errors: 2.0.1 + http-errors: 2.0.0 iconv-lite: 0.4.24 on-finished: 2.4.1 - qs: 6.14.1 - raw-body: 2.5.3 + qs: 6.13.0 + raw-body: 2.5.2 type-is: 1.6.18 unpipe: 1.0.0 transitivePeerDependencies: @@ -13777,7 +15048,7 @@ snapshots: bytes: 3.1.2 content-type: 1.0.5 debug: 4.4.3 - http-errors: 2.0.1 + http-errors: 2.0.0 iconv-lite: 0.7.2 on-finished: 2.4.1 qs: 6.14.1 @@ -13794,7 +15065,7 @@ snapshots: bs58: 4.0.1 text-encoding-utf-8: 1.0.2 - bowser@2.13.1: {} + bowser@2.12.1: {} brace-expansion@1.1.12: dependencies: @@ -13809,13 +15080,12 @@ snapshots: dependencies: fill-range: 7.1.1 - browserslist@4.28.1: + browserslist@4.25.4: dependencies: - baseline-browser-mapping: 2.9.19 - caniuse-lite: 1.0.30001767 - electron-to-chromium: 1.5.283 - node-releases: 2.0.27 - update-browserslist-db: 1.2.3(browserslist@4.28.1) + caniuse-lite: 1.0.30001739 + electron-to-chromium: 1.5.214 + node-releases: 2.0.19 + update-browserslist-db: 1.1.3(browserslist@4.25.4) bs58@4.0.1: dependencies: @@ -13830,13 +15100,13 @@ snapshots: base64-js: 1.5.1 ieee754: 1.2.1 - bufferutil@4.1.0: + bufferutil@4.0.9: dependencies: node-gyp-build: 4.8.4 - bundle-require@5.1.0(esbuild@0.27.2): + bundle-require@5.1.0(esbuild@0.25.9): dependencies: - esbuild: 0.27.2 + esbuild: 0.25.9 load-tsconfig: 0.2.5 bytes@3.1.2: {} @@ -13878,12 +15148,12 @@ snapshots: camelcase@6.3.0: {} - caniuse-lite@1.0.30001767: {} + caniuse-lite@1.0.30001739: {} chai@5.3.3: dependencies: assertion-error: 2.0.1 - check-error: 2.1.3 + check-error: 2.1.1 deep-eql: 5.0.2 loupe: 3.2.1 pathval: 2.0.1 @@ -13899,16 +15169,12 @@ snapshots: charenc@0.0.2: {} - check-error@2.1.3: {} + check-error@2.1.1: {} chokidar@4.0.3: dependencies: readdirp: 4.1.2 - chokidar@5.0.0: - dependencies: - readdirp: 5.0.0 - ci-info@3.9.0: {} client-only@0.0.1: {} @@ -13941,6 +15207,8 @@ snapshots: commander@12.1.0: {} + commander@14.0.1: {} + commander@14.0.2: {} commander@14.0.3: {} @@ -13971,15 +15239,17 @@ snapshots: cookie-es@1.2.2: {} - cookie-signature@1.0.7: {} + cookie-signature@1.0.6: {} cookie-signature@1.2.2: {} - cookie@0.7.2: {} + cookie@0.7.1: {} + + cookie@1.1.1: {} - core-js-compat@3.48.0: + core-js-compat@3.45.1: dependencies: - browserslist: 4.28.1 + browserslist: 4.25.4 core-util-is@1.0.3: {} @@ -13988,14 +15258,14 @@ snapshots: object-assign: 4.1.1 vary: 1.1.2 - cosmiconfig@8.3.6(typescript@5.9.3): + cosmiconfig@8.3.6(typescript@5.9.2): dependencies: import-fresh: 3.3.1 - js-yaml: 4.1.1 + js-yaml: 4.1.0 parse-json: 5.2.0 path-type: 4.0.0 optionalDependencies: - typescript: 5.9.3 + typescript: 5.9.2 crc-32@1.2.2: {} @@ -14055,11 +15325,11 @@ snapshots: cssstyle@5.3.6: dependencies: '@asamuzakjp/css-color': 4.1.1 - '@csstools/css-syntax-patches-for-csstree': 1.0.26 + '@csstools/css-syntax-patches-for-csstree': 1.0.22 css-tree: 3.1.0 - lru-cache: 11.2.5 + lru-cache: 11.2.4 - csstype@3.2.3: {} + csstype@3.1.3: {} damerau-levenshtein@1.0.8: {} @@ -14068,6 +15338,12 @@ snapshots: whatwg-mimetype: 4.0.0 whatwg-url: 14.2.0 + data-urls@6.0.0: + dependencies: + whatwg-mimetype: 4.0.0 + whatwg-url: 15.1.0 + optional: true + data-view-buffer@1.0.2: dependencies: call-bound: 1.0.4 @@ -14090,7 +15366,7 @@ snapshots: date-fns@2.30.0: dependencies: - '@babel/runtime': 7.28.6 + '@babel/runtime': 7.28.3 dayjs@1.11.13: {} @@ -14106,6 +15382,10 @@ snapshots: dependencies: ms: 2.1.2 + debug@4.3.7: + dependencies: + ms: 2.1.3 + debug@4.4.1: dependencies: ms: 2.1.3 @@ -14152,13 +15432,15 @@ snapshots: depd@2.0.0: {} - derive-valtio@0.1.0(valtio@1.13.2(@types/react@19.2.10)(react@19.2.1)): + dequal@2.0.3: {} + + derive-valtio@0.1.0(valtio@1.13.2(@types/react@19.1.12)(react@19.2.1)): dependencies: - valtio: 1.13.2(@types/react@19.2.10)(react@19.2.1) + valtio: 1.13.2(@types/react@19.1.12)(react@19.2.1) - derive-valtio@0.1.0(valtio@1.13.2(@types/react@19.2.10)(react@19.2.4)): + derive-valtio@0.1.0(valtio@1.13.2(@types/react@19.1.12)(react@19.2.3)): dependencies: - valtio: 1.13.2(@types/react@19.2.10)(react@19.2.4) + valtio: 1.13.2(@types/react@19.1.12)(react@19.2.3) destr@2.0.5: {} @@ -14218,16 +15500,18 @@ snapshots: readable-stream: 3.6.2 stream-shift: 1.0.3 - eciesjs@0.4.17: + eastasianwidth@0.2.0: {} + + eciesjs@0.4.15: dependencies: - '@ecies/ciphers': 0.2.5(@noble/ciphers@1.3.0) + '@ecies/ciphers': 0.2.4(@noble/ciphers@1.3.0) '@noble/ciphers': 1.3.0 '@noble/curves': 1.9.7 '@noble/hashes': 1.8.0 ee-first@1.1.1: {} - electron-to-chromium@1.5.283: {} + electron-to-chromium@1.5.214: {} emoji-regex@8.0.0: {} @@ -14235,18 +15519,20 @@ snapshots: encode-utf8@1.0.3: {} + encodeurl@1.0.2: {} + encodeurl@2.0.0: {} end-of-stream@1.4.5: dependencies: once: 1.4.0 - engine.io-client@6.6.4(bufferutil@4.1.0)(utf-8-validate@5.0.10): + engine.io-client@6.6.3(bufferutil@4.0.9)(utf-8-validate@5.0.10): dependencies: '@socket.io/component-emitter': 3.1.2 - debug: 4.4.3 + debug: 4.3.7 engine.io-parser: 5.2.3 - ws: 8.18.3(bufferutil@4.1.0)(utf-8-validate@5.0.10) + ws: 8.17.1(bufferutil@4.0.9)(utf-8-validate@5.0.10) xmlhttprequest-ssl: 2.1.2 transitivePeerDependencies: - bufferutil @@ -14255,7 +15541,7 @@ snapshots: engine.io-parser@5.2.3: {} - enhanced-resolve@5.18.4: + enhanced-resolve@5.18.3: dependencies: graceful-fs: 4.2.11 tapable: 2.3.0 @@ -14269,11 +15555,11 @@ snapshots: entities@6.0.1: {} - error-ex@1.3.4: + error-ex@1.3.2: dependencies: is-arrayish: 0.2.1 - es-abstract@1.24.1: + es-abstract@1.24.0: dependencies: array-buffer-byte-length: 1.0.2 arraybuffer.prototype.slice: 1.0.4 @@ -14328,18 +15614,18 @@ snapshots: typed-array-byte-offset: 1.0.4 typed-array-length: 1.0.7 unbox-primitive: 1.1.0 - which-typed-array: 1.1.20 + which-typed-array: 1.1.19 es-define-property@1.0.1: {} es-errors@1.3.0: {} - es-iterator-helpers@1.2.2: + es-iterator-helpers@1.2.1: dependencies: call-bind: 1.0.8 call-bound: 1.0.4 define-properties: 1.2.1 - es-abstract: 1.24.1 + es-abstract: 1.24.0 es-errors: 1.3.0 es-set-tostringtag: 2.1.0 function-bind: 1.1.2 @@ -14384,63 +15670,34 @@ snapshots: dependencies: es6-promise: 4.2.8 - esbuild@0.25.12: + esbuild@0.25.9: optionalDependencies: - '@esbuild/aix-ppc64': 0.25.12 - '@esbuild/android-arm': 0.25.12 - '@esbuild/android-arm64': 0.25.12 - '@esbuild/android-x64': 0.25.12 - '@esbuild/darwin-arm64': 0.25.12 - '@esbuild/darwin-x64': 0.25.12 - '@esbuild/freebsd-arm64': 0.25.12 - '@esbuild/freebsd-x64': 0.25.12 - '@esbuild/linux-arm': 0.25.12 - '@esbuild/linux-arm64': 0.25.12 - '@esbuild/linux-ia32': 0.25.12 - '@esbuild/linux-loong64': 0.25.12 - '@esbuild/linux-mips64el': 0.25.12 - '@esbuild/linux-ppc64': 0.25.12 - '@esbuild/linux-riscv64': 0.25.12 - '@esbuild/linux-s390x': 0.25.12 - '@esbuild/linux-x64': 0.25.12 - '@esbuild/netbsd-arm64': 0.25.12 - '@esbuild/netbsd-x64': 0.25.12 - '@esbuild/openbsd-arm64': 0.25.12 - '@esbuild/openbsd-x64': 0.25.12 - '@esbuild/openharmony-arm64': 0.25.12 - '@esbuild/sunos-x64': 0.25.12 - '@esbuild/win32-arm64': 0.25.12 - '@esbuild/win32-ia32': 0.25.12 - '@esbuild/win32-x64': 0.25.12 - - esbuild@0.27.2: - optionalDependencies: - '@esbuild/aix-ppc64': 0.27.2 - '@esbuild/android-arm': 0.27.2 - '@esbuild/android-arm64': 0.27.2 - '@esbuild/android-x64': 0.27.2 - '@esbuild/darwin-arm64': 0.27.2 - '@esbuild/darwin-x64': 0.27.2 - '@esbuild/freebsd-arm64': 0.27.2 - '@esbuild/freebsd-x64': 0.27.2 - '@esbuild/linux-arm': 0.27.2 - '@esbuild/linux-arm64': 0.27.2 - '@esbuild/linux-ia32': 0.27.2 - '@esbuild/linux-loong64': 0.27.2 - '@esbuild/linux-mips64el': 0.27.2 - '@esbuild/linux-ppc64': 0.27.2 - '@esbuild/linux-riscv64': 0.27.2 - '@esbuild/linux-s390x': 0.27.2 - '@esbuild/linux-x64': 0.27.2 - '@esbuild/netbsd-arm64': 0.27.2 - '@esbuild/netbsd-x64': 0.27.2 - '@esbuild/openbsd-arm64': 0.27.2 - '@esbuild/openbsd-x64': 0.27.2 - '@esbuild/openharmony-arm64': 0.27.2 - '@esbuild/sunos-x64': 0.27.2 - '@esbuild/win32-arm64': 0.27.2 - '@esbuild/win32-ia32': 0.27.2 - '@esbuild/win32-x64': 0.27.2 + '@esbuild/aix-ppc64': 0.25.9 + '@esbuild/android-arm': 0.25.9 + '@esbuild/android-arm64': 0.25.9 + '@esbuild/android-x64': 0.25.9 + '@esbuild/darwin-arm64': 0.25.9 + '@esbuild/darwin-x64': 0.25.9 + '@esbuild/freebsd-arm64': 0.25.9 + '@esbuild/freebsd-x64': 0.25.9 + '@esbuild/linux-arm': 0.25.9 + '@esbuild/linux-arm64': 0.25.9 + '@esbuild/linux-ia32': 0.25.9 + '@esbuild/linux-loong64': 0.25.9 + '@esbuild/linux-mips64el': 0.25.9 + '@esbuild/linux-ppc64': 0.25.9 + '@esbuild/linux-riscv64': 0.25.9 + '@esbuild/linux-s390x': 0.25.9 + '@esbuild/linux-x64': 0.25.9 + '@esbuild/netbsd-arm64': 0.25.9 + '@esbuild/netbsd-x64': 0.25.9 + '@esbuild/openbsd-arm64': 0.25.9 + '@esbuild/openbsd-x64': 0.25.9 + '@esbuild/openharmony-arm64': 0.25.9 + '@esbuild/sunos-x64': 0.25.9 + '@esbuild/win32-arm64': 0.25.9 + '@esbuild/win32-ia32': 0.25.9 + '@esbuild/win32-x64': 0.25.9 escalade@3.2.0: {} @@ -14448,20 +15705,20 @@ snapshots: escape-string-regexp@4.0.0: {} - eslint-config-next@16.0.6(@typescript-eslint/parser@8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3): + eslint-config-next@16.0.6(@typescript-eslint/parser@8.42.0(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.2))(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.2): dependencies: '@next/eslint-plugin-next': 16.0.6 - eslint: 9.39.2(jiti@2.6.1) + eslint: 9.34.0(jiti@2.6.1) eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.2(jiti@2.6.1)) - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.2(jiti@2.6.1)) - eslint-plugin-jsx-a11y: 6.10.2(eslint@9.39.2(jiti@2.6.1)) - eslint-plugin-react: 7.37.5(eslint@9.39.2(jiti@2.6.1)) - eslint-plugin-react-hooks: 7.0.1(eslint@9.39.2(jiti@2.6.1)) + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.34.0(jiti@2.6.1)) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.42.0(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.2))(eslint-import-resolver-typescript@3.10.1)(eslint@9.34.0(jiti@2.6.1)) + eslint-plugin-jsx-a11y: 6.10.2(eslint@9.34.0(jiti@2.6.1)) + eslint-plugin-react: 7.37.5(eslint@9.34.0(jiti@2.6.1)) + eslint-plugin-react-hooks: 7.0.1(eslint@9.34.0(jiti@2.6.1)) globals: 16.4.0 - typescript-eslint: 8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + typescript-eslint: 8.49.0(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.2) optionalDependencies: - typescript: 5.9.3 + typescript: 5.9.2 transitivePeerDependencies: - '@typescript-eslint/parser' - eslint-import-resolver-webpack @@ -14472,37 +15729,76 @@ snapshots: dependencies: debug: 3.2.7 is-core-module: 2.16.1 - resolve: 1.22.11 + resolve: 1.22.10 transitivePeerDependencies: - supports-color - eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.2(jiti@2.6.1)): + eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(eslint@9.34.0(jiti@2.6.1)): dependencies: '@nolyfill/is-core-module': 1.0.39 debug: 4.4.3 - eslint: 9.39.2(jiti@2.6.1) - get-tsconfig: 4.13.1 + eslint: 9.34.0(jiti@2.6.1) + get-tsconfig: 4.10.1 is-bun-module: 2.0.0 stable-hash: 0.0.5 tinyglobby: 0.2.15 unrs-resolver: 1.11.1 optionalDependencies: - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.2(jiti@2.6.1)) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.42.0(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.2))(eslint-import-resolver-typescript@3.10.1)(eslint@9.34.0(jiti@2.6.1)) + transitivePeerDependencies: + - supports-color + + eslint-module-utils@2.12.1(@typescript-eslint/parser@8.42.0(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.34.0(jiti@2.6.1)): + dependencies: + debug: 3.2.7 + optionalDependencies: + '@typescript-eslint/parser': 8.42.0(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.2) + eslint: 9.34.0(jiti@2.6.1) + eslint-import-resolver-node: 0.3.9 + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.34.0(jiti@2.6.1)) transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.1(@typescript-eslint/parser@8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.2(jiti@2.6.1)): + eslint-module-utils@2.12.1(@typescript-eslint/parser@8.49.0(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.2))(eslint-import-resolver-node@0.3.9)(eslint@9.34.0(jiti@2.6.1)): dependencies: debug: 3.2.7 optionalDependencies: - '@typescript-eslint/parser': 8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) - eslint: 9.39.2(jiti@2.6.1) + '@typescript-eslint/parser': 8.49.0(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.2) + eslint: 9.34.0(jiti@2.6.1) + eslint-import-resolver-node: 0.3.9 + transitivePeerDependencies: + - supports-color + + eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.42.0(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.2))(eslint-import-resolver-typescript@3.10.1)(eslint@9.34.0(jiti@2.6.1)): + dependencies: + '@rtsao/scc': 1.1.0 + array-includes: 3.1.9 + array.prototype.findlastindex: 1.2.6 + array.prototype.flat: 1.3.3 + array.prototype.flatmap: 1.3.3 + debug: 3.2.7 + doctrine: 2.1.0 + eslint: 9.34.0(jiti@2.6.1) eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.2(jiti@2.6.1)) + eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.42.0(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.34.0(jiti@2.6.1)) + hasown: 2.0.2 + is-core-module: 2.16.1 + is-glob: 4.0.3 + minimatch: 3.1.2 + object.fromentries: 2.0.8 + object.groupby: 1.0.3 + object.values: 1.2.1 + semver: 6.3.1 + string.prototype.trimend: 1.0.9 + tsconfig-paths: 3.15.0 + optionalDependencies: + '@typescript-eslint/parser': 8.42.0(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.2) transitivePeerDependencies: + - eslint-import-resolver-typescript + - eslint-import-resolver-webpack - supports-color - eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.2(jiti@2.6.1)): + eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.49.0(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.2))(eslint@9.34.0(jiti@2.6.1)): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.9 @@ -14511,9 +15807,9 @@ snapshots: array.prototype.flatmap: 1.3.3 debug: 3.2.7 doctrine: 2.1.0 - eslint: 9.39.2(jiti@2.6.1) + eslint: 9.34.0(jiti@2.6.1) eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.2(jiti@2.6.1)) + eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.49.0(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.2))(eslint-import-resolver-node@0.3.9)(eslint@9.34.0(jiti@2.6.1)) hasown: 2.0.2 is-core-module: 2.16.1 is-glob: 4.0.3 @@ -14525,39 +15821,39 @@ snapshots: string.prototype.trimend: 1.0.9 tsconfig-paths: 3.15.0 optionalDependencies: - '@typescript-eslint/parser': 8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/parser': 8.49.0(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.2) transitivePeerDependencies: - eslint-import-resolver-typescript - eslint-import-resolver-webpack - supports-color - eslint-plugin-jsdoc@50.8.0(eslint@9.39.2(jiti@2.6.1)): + eslint-plugin-jsdoc@50.8.0(eslint@9.34.0(jiti@2.6.1)): dependencies: '@es-joy/jsdoccomment': 0.50.2 are-docs-informative: 0.0.2 comment-parser: 1.4.1 debug: 4.4.3 escape-string-regexp: 4.0.0 - eslint: 9.39.2(jiti@2.6.1) + eslint: 9.34.0(jiti@2.6.1) espree: 10.4.0 - esquery: 1.7.0 + esquery: 1.6.0 parse-imports-exports: 0.2.4 semver: 7.7.3 spdx-expression-parse: 4.0.0 transitivePeerDependencies: - supports-color - eslint-plugin-jsx-a11y@6.10.2(eslint@9.39.2(jiti@2.6.1)): + eslint-plugin-jsx-a11y@6.10.2(eslint@9.34.0(jiti@2.6.1)): dependencies: aria-query: 5.3.2 array-includes: 3.1.9 array.prototype.flatmap: 1.3.3 ast-types-flow: 0.0.8 - axe-core: 4.11.1 + axe-core: 4.10.3 axobject-query: 4.1.0 damerau-levenshtein: 1.0.8 emoji-regex: 9.2.2 - eslint: 9.39.2(jiti@2.6.1) + eslint: 9.34.0(jiti@2.6.1) hasown: 2.0.2 jsx-ast-utils: 3.3.5 language-tags: 1.0.9 @@ -14566,33 +15862,33 @@ snapshots: safe-regex-test: 1.1.0 string.prototype.includes: 2.0.1 - eslint-plugin-prettier@5.5.5(eslint@9.39.2(jiti@2.6.1))(prettier@3.5.2): + eslint-plugin-prettier@5.5.4(eslint@9.34.0(jiti@2.6.1))(prettier@3.5.2): dependencies: - eslint: 9.39.2(jiti@2.6.1) + eslint: 9.34.0(jiti@2.6.1) prettier: 3.5.2 - prettier-linter-helpers: 1.0.1 - synckit: 0.11.12 + prettier-linter-helpers: 1.0.0 + synckit: 0.11.11 - eslint-plugin-react-hooks@7.0.1(eslint@9.39.2(jiti@2.6.1)): + eslint-plugin-react-hooks@7.0.1(eslint@9.34.0(jiti@2.6.1)): dependencies: - '@babel/core': 7.29.0 - '@babel/parser': 7.29.0 - eslint: 9.39.2(jiti@2.6.1) + '@babel/core': 7.28.3 + '@babel/parser': 7.28.3 + eslint: 9.34.0(jiti@2.6.1) hermes-parser: 0.25.1 zod: 3.25.76 zod-validation-error: 4.0.2(zod@3.25.76) transitivePeerDependencies: - supports-color - eslint-plugin-react@7.37.5(eslint@9.39.2(jiti@2.6.1)): + eslint-plugin-react@7.37.5(eslint@9.34.0(jiti@2.6.1)): dependencies: array-includes: 3.1.9 array.prototype.findlast: 1.2.5 array.prototype.flatmap: 1.3.3 array.prototype.tosorted: 1.1.4 doctrine: 2.1.0 - es-iterator-helpers: 1.2.2 - eslint: 9.39.2(jiti@2.6.1) + es-iterator-helpers: 1.2.1 + eslint: 9.34.0(jiti@2.6.1) estraverse: 5.3.0 hasown: 2.0.2 jsx-ast-utils: 3.3.5 @@ -14615,20 +15911,21 @@ snapshots: eslint-visitor-keys@4.2.1: {} - eslint@9.39.2(jiti@2.6.1): + eslint@9.34.0(jiti@2.6.1): dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.2(jiti@2.6.1)) - '@eslint-community/regexpp': 4.12.2 - '@eslint/config-array': 0.21.1 - '@eslint/config-helpers': 0.4.2 - '@eslint/core': 0.17.0 - '@eslint/eslintrc': 3.3.3 - '@eslint/js': 9.39.2 - '@eslint/plugin-kit': 0.4.1 + '@eslint-community/eslint-utils': 4.8.0(eslint@9.34.0(jiti@2.6.1)) + '@eslint-community/regexpp': 4.12.1 + '@eslint/config-array': 0.21.0 + '@eslint/config-helpers': 0.3.1 + '@eslint/core': 0.15.2 + '@eslint/eslintrc': 3.3.1 + '@eslint/js': 9.34.0 + '@eslint/plugin-kit': 0.3.5 '@humanfs/node': 0.16.7 '@humanwhocodes/module-importer': 1.0.1 '@humanwhocodes/retry': 0.4.3 '@types/estree': 1.0.8 + '@types/json-schema': 7.0.15 ajv: 6.12.6 chalk: 4.1.2 cross-spawn: 7.0.6 @@ -14637,7 +15934,7 @@ snapshots: eslint-scope: 8.4.0 eslint-visitor-keys: 4.2.1 espree: 10.4.0 - esquery: 1.7.0 + esquery: 1.6.0 esutils: 2.0.3 fast-deep-equal: 3.1.3 file-entry-cache: 8.0.0 @@ -14664,7 +15961,7 @@ snapshots: esprima@4.0.1: {} - esquery@1.7.0: + esquery@1.6.0: dependencies: estraverse: 5.3.0 @@ -14716,7 +16013,7 @@ snapshots: '@scure/bip32': 1.4.0 '@scure/bip39': 1.3.0 - ethers@6.16.0(bufferutil@4.1.0)(utf-8-validate@5.0.10): + ethers@6.16.0(bufferutil@4.0.9)(utf-8-validate@5.0.10): dependencies: '@adraffy/ens-normalize': 1.10.1 '@noble/curves': 1.2.0 @@ -14724,7 +16021,7 @@ snapshots: '@types/node': 22.7.5 aes-js: 4.0.0-beta.5 tslib: 2.7.0 - ws: 8.17.1(bufferutil@4.1.0)(utf-8-validate@5.0.10) + ws: 8.17.1(bufferutil@4.0.9)(utf-8-validate@5.0.10) transitivePeerDependencies: - bufferutil - utf-8-validate @@ -14733,52 +16030,53 @@ snapshots: eventemitter3@5.0.1: {} - eventemitter3@5.0.4: {} - events@3.3.0: {} eventsource-parser@3.0.6: {} + eventsource@2.0.2: {} + eventsource@3.0.7: dependencies: eventsource-parser: 3.0.6 - expect-type@1.3.0: {} + expect-type@1.2.2: {} - express-rate-limit@7.5.1(express@5.2.1): + express-rate-limit@8.2.1(express@5.2.1): dependencies: express: 5.2.1 + ip-address: 10.0.1 - express@4.22.1: + express@4.21.2: dependencies: accepts: 1.3.8 array-flatten: 1.1.1 - body-parser: 1.20.4 + body-parser: 1.20.3 content-disposition: 0.5.4 content-type: 1.0.5 - cookie: 0.7.2 - cookie-signature: 1.0.7 + cookie: 0.7.1 + cookie-signature: 1.0.6 debug: 2.6.9 depd: 2.0.0 encodeurl: 2.0.0 escape-html: 1.0.3 etag: 1.8.1 - finalhandler: 1.3.2 + finalhandler: 1.3.1 fresh: 0.5.2 - http-errors: 2.0.1 + http-errors: 2.0.0 merge-descriptors: 1.0.3 methods: 1.1.2 on-finished: 2.4.1 parseurl: 1.3.3 path-to-regexp: 0.1.12 proxy-addr: 2.0.7 - qs: 6.14.1 + qs: 6.13.0 range-parser: 1.2.1 safe-buffer: 5.2.1 - send: 0.19.2 - serve-static: 1.16.3 + send: 0.19.0 + serve-static: 1.16.2 setprototypeof: 1.2.0 - statuses: 2.0.2 + statuses: 2.0.1 type-is: 1.6.18 utils-merge: 1.0.1 vary: 1.1.2 @@ -14791,7 +16089,7 @@ snapshots: body-parser: 2.2.2 content-disposition: 1.0.1 content-type: 1.0.5 - cookie: 0.7.2 + cookie: 0.7.1 cookie-signature: 1.2.2 debug: 4.4.3 depd: 2.0.0 @@ -14800,7 +16098,7 @@ snapshots: etag: 1.8.1 finalhandler: 2.1.1 fresh: 2.0.0 - http-errors: 2.0.1 + http-errors: 2.0.0 merge-descriptors: 2.0.0 mime-types: 3.0.2 on-finished: 2.4.1 @@ -14812,7 +16110,7 @@ snapshots: router: 2.2.0 send: 1.2.1 serve-static: 2.2.1 - statuses: 2.0.2 + statuses: 2.0.1 type-is: 2.0.1 vary: 1.1.2 transitivePeerDependencies: @@ -14827,6 +16125,8 @@ snapshots: eyes@0.1.8: {} + fast-decode-uri-component@1.0.1: {} + fast-deep-equal@3.1.3: {} fast-diff@1.3.0: {} @@ -14849,8 +16149,21 @@ snapshots: fast-json-stable-stringify@2.1.0: {} + fast-json-stringify@6.3.0: + dependencies: + '@fastify/merge-json-schemas': 0.2.1 + ajv: 8.17.1 + ajv-formats: 3.0.1(ajv@8.17.1) + fast-uri: 3.1.0 + json-schema-ref-resolver: 3.0.0 + rfdc: 1.4.1 + fast-levenshtein@2.0.6: {} + fast-querystring@1.1.2: + dependencies: + fast-decode-uri-component: 1.0.1 + fast-redact@3.5.0: {} fast-safe-stringify@2.1.1: {} @@ -14861,7 +16174,25 @@ snapshots: fastestsmallesttextencoderdecoder@1.0.22: {} - fastq@1.20.1: + fastify@5.8.2: + dependencies: + '@fastify/ajv-compiler': 4.0.5 + '@fastify/error': 4.2.0 + '@fastify/fast-json-stringify-compiler': 5.0.3 + '@fastify/proxy-addr': 5.1.0 + abstract-logging: 2.0.1 + avvio: 9.2.0 + fast-json-stringify: 6.3.0 + find-my-way: 9.5.0 + light-my-request: 6.6.0 + pino: 10.3.1 + process-warning: 5.0.0 + rfdc: 1.4.1 + secure-json-parse: 4.1.0 + semver: 7.7.3 + toad-cache: 3.7.0 + + fastq@1.19.1: dependencies: reusify: 1.1.0 @@ -14869,24 +16200,30 @@ snapshots: optionalDependencies: picomatch: 4.0.3 + feaxios@0.0.23: + dependencies: + is-retry-allowed: 3.0.0 + file-entry-cache@8.0.0: dependencies: flat-cache: 4.0.1 + file-uri-to-path@1.0.0: {} + fill-range@7.1.1: dependencies: to-regex-range: 5.0.1 filter-obj@1.1.0: {} - finalhandler@1.3.2: + finalhandler@1.3.1: dependencies: debug: 2.6.9 encodeurl: 2.0.0 escape-html: 1.0.3 on-finished: 2.4.1 parseurl: 1.3.3 - statuses: 2.0.2 + statuses: 2.0.1 unpipe: 1.0.0 transitivePeerDependencies: - supports-color @@ -14898,10 +16235,16 @@ snapshots: escape-html: 1.0.3 on-finished: 2.4.1 parseurl: 1.3.3 - statuses: 2.0.2 + statuses: 2.0.1 transitivePeerDependencies: - supports-color + find-my-way@9.5.0: + dependencies: + fast-deep-equal: 3.1.3 + fast-querystring: 1.1.2 + safe-regex2: 5.0.0 + find-up@4.1.0: dependencies: locate-path: 5.0.0 @@ -14914,9 +16257,9 @@ snapshots: fix-dts-default-cjs-exports@1.0.1: dependencies: - magic-string: 0.30.21 + magic-string: 0.30.18 mlly: 1.8.0 - rollup: 4.57.1 + rollup: 4.50.0 flat-cache@4.0.1: dependencies: @@ -14931,7 +16274,12 @@ snapshots: dependencies: is-callable: 1.2.7 - form-data@4.0.5: + foreground-child@3.3.1: + dependencies: + cross-spawn: 7.0.6 + signal-exit: 4.1.0 + + form-data@4.0.4: dependencies: asynckit: 0.4.0 combined-stream: 1.0.8 @@ -14941,14 +16289,14 @@ snapshots: forwarded@0.2.0: {} - framer-motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4): + framer-motion@11.18.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3): dependencies: motion-dom: 11.18.1 motion-utils: 11.18.1 tslib: 2.8.1 optionalDependencies: - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) fresh@0.5.2: {} @@ -14982,8 +16330,6 @@ snapshots: functions-have-names@1.2.3: {} - generator-function@2.0.1: {} - gensync@1.0.0-beta.2: {} get-caller-file@2.0.5: {} @@ -15016,7 +16362,7 @@ snapshots: es-errors: 1.3.0 get-intrinsic: 1.3.0 - get-tsconfig@4.13.1: + get-tsconfig@4.10.1: dependencies: resolve-pkg-maps: 1.0.0 @@ -15028,6 +16374,15 @@ snapshots: dependencies: is-glob: 4.0.3 + glob@10.4.5: + dependencies: + foreground-child: 3.3.1 + jackspeak: 3.4.3 + minimatch: 9.0.5 + minipass: 7.1.2 + package-json-from-dist: 1.0.1 + path-scurry: 1.11.1 + globals@14.0.0: {} globals@16.4.0: {} @@ -15066,26 +16421,28 @@ snapshots: graceful-fs@4.2.11: {} - graphql-request@6.1.0(graphql@16.12.0): + graphemer@1.4.0: {} + + graphql-request@6.1.0(graphql@16.11.0): dependencies: - '@graphql-typed-document-node/core': 3.2.0(graphql@16.12.0) + '@graphql-typed-document-node/core': 3.2.0(graphql@16.11.0) cross-fetch: 3.2.0 - graphql: 16.12.0 + graphql: 16.11.0 transitivePeerDependencies: - encoding - graphql@16.12.0: {} + graphql@16.11.0: {} - h3@1.15.5: + h3@1.15.4: dependencies: cookie-es: 1.2.2 crossws: 0.3.5 defu: 6.1.4 destr: 2.0.5 iron-webcrypto: 1.2.1 - node-mock-http: 1.0.4 + node-mock-http: 1.0.2 radix3: 1.1.2 - ufo: 1.6.3 + ufo: 1.6.1 uncrypto: 0.1.3 has-bigints@1.1.0: {} @@ -15116,14 +16473,31 @@ snapshots: dependencies: hermes-estree: 0.25.1 + hono@4.10.7: {} + hono@4.11.7: {} html-encoding-sniffer@4.0.0: dependencies: whatwg-encoding: 3.1.1 + html-encoding-sniffer@6.0.0: + dependencies: + '@exodus/bytes': 1.8.0 + transitivePeerDependencies: + - '@exodus/crypto' + optional: true + http-cache-semantics@4.2.0: {} + http-errors@2.0.0: + dependencies: + depd: 2.0.0 + inherits: 2.0.4 + setprototypeof: 1.2.0 + statuses: 2.0.1 + toidentifier: 1.0.1 + http-errors@2.0.1: dependencies: depd: 2.0.0 @@ -15194,8 +16568,12 @@ snapshots: hasown: 2.0.2 side-channel: 1.1.0 + ip-address@10.0.1: {} + ipaddr.js@1.9.1: {} + ipaddr.js@2.3.0: {} + iron-webcrypto@1.2.1: {} is-arguments@1.2.0: @@ -15259,10 +16637,9 @@ snapshots: is-fullwidth-code-point@3.0.0: {} - is-generator-function@1.1.2: + is-generator-function@1.1.0: dependencies: call-bound: 1.0.4 - generator-function: 2.0.1 get-proto: 1.0.1 has-tostringtag: 1.0.2 safe-regex-test: 1.1.0 @@ -15295,6 +16672,8 @@ snapshots: is-retry-allowed@2.2.0: {} + is-retry-allowed@3.0.0: {} + is-set@2.0.3: {} is-shared-array-buffer@1.0.4: @@ -15320,7 +16699,7 @@ snapshots: is-typed-array@1.1.15: dependencies: - which-typed-array: 1.1.20 + which-typed-array: 1.1.19 is-weakmap@2.0.2: {} @@ -15341,17 +16720,17 @@ snapshots: isexe@2.0.0: {} - isomorphic-ws@4.0.1(ws@7.5.10(bufferutil@4.1.0)(utf-8-validate@5.0.10)): + isomorphic-ws@4.0.1(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10)): dependencies: - ws: 7.5.10(bufferutil@4.1.0)(utf-8-validate@5.0.10) + ws: 7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10) - isows@1.0.6(ws@8.18.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)): + isows@1.0.6(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)): dependencies: - ws: 8.18.0(bufferutil@4.1.0)(utf-8-validate@5.0.10) + ws: 8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10) - isows@1.0.7(ws@8.18.3(bufferutil@4.1.0)(utf-8-validate@5.0.10)): + isows@1.0.7(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10)): dependencies: - ws: 8.18.3(bufferutil@4.1.0)(utf-8-validate@5.0.10) + ws: 8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10) iterator.prototype@1.1.5: dependencies: @@ -15362,7 +16741,13 @@ snapshots: has-symbols: 1.1.0 set-function-name: 2.0.2 - jayson@4.3.0(bufferutil@4.1.0)(utf-8-validate@5.0.10): + jackspeak@3.4.3: + dependencies: + '@isaacs/cliui': 8.0.2 + optionalDependencies: + '@pkgjs/parseargs': 0.11.0 + + jayson@4.2.0(bufferutil@4.0.9)(utf-8-validate@5.0.10): dependencies: '@types/connect': 3.4.38 '@types/node': 12.20.55 @@ -15371,11 +16756,11 @@ snapshots: delay: 5.0.0 es6-promisify: 5.0.0 eyes: 0.1.8 - isomorphic-ws: 4.0.1(ws@7.5.10(bufferutil@4.1.0)(utf-8-validate@5.0.10)) + isomorphic-ws: 4.0.1(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10)) json-stringify-safe: 5.0.1 stream-json: 1.9.1 uuid: 8.3.2 - ws: 7.5.10(bufferutil@4.1.0)(utf-8-validate@5.0.10) + ws: 7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10) transitivePeerDependencies: - bufferutil - utf-8-validate @@ -15384,6 +16769,8 @@ snapshots: jose@5.10.0: {} + jose@6.1.0: {} + jose@6.1.3: {} joycon@3.1.1: {} @@ -15399,13 +16786,17 @@ snapshots: argparse: 1.0.10 esprima: 4.0.1 + js-yaml@4.1.0: + dependencies: + argparse: 2.0.1 + js-yaml@4.1.1: dependencies: argparse: 2.0.1 jsdoc-type-pratt-parser@4.1.0: {} - jsdom@26.1.0(bufferutil@4.1.0)(utf-8-validate@5.0.10): + jsdom@26.1.0(bufferutil@4.0.9)(utf-8-validate@5.0.10): dependencies: cssstyle: 5.3.6 data-urls: 5.0.0 @@ -15414,7 +16805,7 @@ snapshots: http-proxy-agent: 7.0.2 https-proxy-agent: 7.0.6 is-potential-custom-element-name: 1.0.1 - nwsapi: 2.2.23 + nwsapi: 2.2.21 parse5: 7.3.0 rrweb-cssom: 0.8.0 saxes: 6.0.0 @@ -15425,12 +16816,43 @@ snapshots: whatwg-encoding: 3.1.1 whatwg-mimetype: 4.0.0 whatwg-url: 14.2.0 - ws: 8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10) + ws: 8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10) + xml-name-validator: 5.0.0 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + jsdom@27.4.0(bufferutil@4.0.9)(utf-8-validate@5.0.10): + dependencies: + '@acemir/cssom': 0.9.30 + '@asamuzakjp/dom-selector': 6.7.6 + '@exodus/bytes': 1.8.0 + cssstyle: 5.3.6 + data-urls: 6.0.0 + decimal.js: 10.6.0 + html-encoding-sniffer: 6.0.0 + http-proxy-agent: 7.0.2 + https-proxy-agent: 7.0.6 + is-potential-custom-element-name: 1.0.1 + parse5: 8.0.0 + saxes: 6.0.0 + symbol-tree: 3.2.4 + tough-cookie: 6.0.0 + w3c-xmlserializer: 5.0.0 + webidl-conversions: 8.0.1 + whatwg-mimetype: 4.0.0 + whatwg-url: 15.1.0 + ws: 8.19.0(bufferutil@4.0.9)(utf-8-validate@5.0.10) xml-name-validator: 5.0.0 transitivePeerDependencies: + - '@exodus/crypto' - bufferutil - supports-color - utf-8-validate + optional: true + + jsesc@3.0.2: {} jsesc@3.1.0: {} @@ -15445,6 +16867,10 @@ snapshots: json-rpc-random-id@1.0.1: {} + json-schema-ref-resolver@3.0.0: + dependencies: + dequal: 2.0.3 + json-schema-traverse@0.4.1: {} json-schema-traverse@1.0.0: {} @@ -15497,6 +16923,12 @@ snapshots: prelude-ls: 1.2.1 type-check: 0.4.0 + light-my-request@6.6.0: + dependencies: + cookie: 1.1.1 + process-warning: 4.0.1 + set-cookie-parser: 2.7.2 + lightningcss-android-arm64@1.30.2: optional: true @@ -15550,21 +16982,21 @@ snapshots: lines-and-columns@1.2.4: {} - lit-element@4.2.2: + lit-element@4.2.1: dependencies: - '@lit-labs/ssr-dom-shim': 1.5.1 - '@lit/reactive-element': 2.1.2 - lit-html: 3.3.2 + '@lit-labs/ssr-dom-shim': 1.4.0 + '@lit/reactive-element': 2.1.1 + lit-html: 3.3.1 - lit-html@3.3.2: + lit-html@3.3.1: dependencies: '@types/trusted-types': 2.0.7 lit@3.3.0: dependencies: - '@lit/reactive-element': 2.1.2 - lit-element: 4.2.2 - lit-html: 3.3.2 + '@lit/reactive-element': 2.1.1 + lit-element: 4.2.1 + lit-html: 3.3.1 load-tsconfig@0.2.5: {} @@ -15580,19 +17012,21 @@ snapshots: lodash.merge@4.6.2: {} + lodash.sortby@4.7.0: {} + lodash.startcase@4.4.0: {} - lodash@4.17.23: {} + lodash@4.17.21: {} loose-envify@1.4.0: dependencies: js-tokens: 4.0.0 - lottie-react@2.4.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4): + lottie-react@2.4.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3): dependencies: lottie-web: 5.13.0 - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) lottie-web@5.13.0: {} @@ -15604,12 +17038,18 @@ snapshots: lowercase-keys@2.0.0: {} - lru-cache@11.2.5: {} + lru-cache@10.4.3: {} + + lru-cache@11.2.4: {} lru-cache@5.1.1: dependencies: yallist: 3.1.1 + magic-string@0.30.18: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + magic-string@0.30.21: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 @@ -15675,16 +17115,18 @@ snapshots: minimist@1.2.8: {} - mipd@0.0.7(typescript@5.9.3): + minipass@7.1.2: {} + + mipd@0.0.7(typescript@5.9.2): optionalDependencies: - typescript: 5.9.3 + typescript: 5.9.2 mlly@1.8.0: dependencies: acorn: 8.15.0 pathe: 2.0.3 pkg-types: 1.3.1 - ufo: 1.6.3 + ufo: 1.6.1 motion-dom@11.18.1: dependencies: @@ -15692,13 +17134,13 @@ snapshots: motion-utils@11.18.1: {} - motion@11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4): + motion@11.18.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3): dependencies: - framer-motion: 11.18.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + framer-motion: 11.18.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) tslib: 2.8.1 optionalDependencies: - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) mri@1.2.0: {} @@ -15718,7 +17160,7 @@ snapshots: nanoid@3.3.11: {} - napi-postinstall@0.3.4: {} + napi-postinstall@0.3.3: {} natural-compare@1.4.0: {} @@ -15726,25 +17168,24 @@ snapshots: negotiator@1.0.0: {} - next@16.1.6(@babel/core@7.29.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4): + next@16.0.10(@babel/core@7.28.3)(react-dom@19.2.3(react@19.2.3))(react@19.2.3): dependencies: - '@next/env': 16.1.6 + '@next/env': 16.0.10 '@swc/helpers': 0.5.15 - baseline-browser-mapping: 2.9.19 - caniuse-lite: 1.0.30001767 + caniuse-lite: 1.0.30001739 postcss: 8.4.31 - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) - styled-jsx: 5.1.6(@babel/core@7.29.0)(react@19.2.4) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + styled-jsx: 5.1.6(@babel/core@7.28.3)(react@19.2.3) optionalDependencies: - '@next/swc-darwin-arm64': 16.1.6 - '@next/swc-darwin-x64': 16.1.6 - '@next/swc-linux-arm64-gnu': 16.1.6 - '@next/swc-linux-arm64-musl': 16.1.6 - '@next/swc-linux-x64-gnu': 16.1.6 - '@next/swc-linux-x64-musl': 16.1.6 - '@next/swc-win32-arm64-msvc': 16.1.6 - '@next/swc-win32-x64-msvc': 16.1.6 + '@next/swc-darwin-arm64': 16.0.10 + '@next/swc-darwin-x64': 16.0.10 + '@next/swc-linux-arm64-gnu': 16.0.10 + '@next/swc-linux-arm64-musl': 16.0.10 + '@next/swc-linux-x64-gnu': 16.0.10 + '@next/swc-linux-x64-musl': 16.0.10 + '@next/swc-win32-arm64-msvc': 16.0.10 + '@next/swc-win32-x64-msvc': 16.0.10 sharp: 0.34.5 transitivePeerDependencies: - '@babel/core' @@ -15765,9 +17206,9 @@ snapshots: node-gyp-build@4.8.4: {} - node-mock-http@1.0.4: {} + node-mock-http@1.0.2: {} - node-releases@2.0.27: {} + node-releases@2.0.19: {} normalize-path@3.0.0: {} @@ -15777,7 +17218,7 @@ snapshots: dependencies: boolbase: 1.0.0 - nwsapi@2.2.23: {} + nwsapi@2.2.21: {} obj-multiplex@1.0.0: dependencies: @@ -15811,14 +17252,14 @@ snapshots: dependencies: call-bind: 1.0.8 define-properties: 1.2.1 - es-abstract: 1.24.1 + es-abstract: 1.24.0 es-object-atoms: 1.1.1 object.groupby@1.0.3: dependencies: call-bind: 1.0.8 define-properties: 1.2.1 - es-abstract: 1.24.1 + es-abstract: 1.24.0 object.values@1.2.1: dependencies: @@ -15827,14 +17268,16 @@ snapshots: define-properties: 1.2.1 es-object-atoms: 1.1.1 - ofetch@1.5.1: + ofetch@1.4.1: dependencies: destr: 2.0.5 node-fetch-native: 1.6.7 - ufo: 1.6.3 + ufo: 1.6.1 on-exit-leak-free@0.2.0: {} + on-exit-leak-free@2.1.2: {} + on-finished@2.4.1: dependencies: ee-first: 1.1.1 @@ -15866,90 +17309,120 @@ snapshots: object-keys: 1.1.1 safe-push-apply: 1.0.0 - ox@0.11.3(typescript@5.9.3)(zod@3.22.4): + ox@0.11.3(typescript@5.9.2)(zod@3.22.4): dependencies: - '@adraffy/ens-normalize': 1.11.1 + '@adraffy/ens-normalize': 1.11.0 '@noble/ciphers': 1.3.0 '@noble/curves': 1.9.1 '@noble/hashes': 1.8.0 '@scure/bip32': 1.7.0 '@scure/bip39': 1.6.0 - abitype: 1.2.3(typescript@5.9.3)(zod@3.22.4) + abitype: 1.2.3(typescript@5.9.2)(zod@3.22.4) eventemitter3: 5.0.1 optionalDependencies: - typescript: 5.9.3 + typescript: 5.9.2 transitivePeerDependencies: - zod - ox@0.11.3(typescript@5.9.3)(zod@3.25.76): + ox@0.11.3(typescript@5.9.2)(zod@3.25.76): dependencies: - '@adraffy/ens-normalize': 1.11.1 + '@adraffy/ens-normalize': 1.11.0 '@noble/ciphers': 1.3.0 '@noble/curves': 1.9.1 '@noble/hashes': 1.8.0 '@scure/bip32': 1.7.0 '@scure/bip39': 1.6.0 - abitype: 1.2.3(typescript@5.9.3)(zod@3.25.76) + abitype: 1.2.3(typescript@5.9.2)(zod@3.25.76) eventemitter3: 5.0.1 optionalDependencies: - typescript: 5.9.3 + typescript: 5.9.2 transitivePeerDependencies: - zod - ox@0.4.4(typescript@5.9.3)(zod@3.25.76): + ox@0.4.4(typescript@5.9.2)(zod@3.25.76): dependencies: - '@adraffy/ens-normalize': 1.11.1 + '@adraffy/ens-normalize': 1.11.0 '@noble/curves': 1.9.7 '@noble/hashes': 1.8.0 '@scure/bip32': 1.7.0 '@scure/bip39': 1.6.0 - abitype: 1.2.3(typescript@5.9.3)(zod@3.25.76) + abitype: 1.1.0(typescript@5.9.2)(zod@3.25.76) eventemitter3: 5.0.1 optionalDependencies: - typescript: 5.9.3 + typescript: 5.9.2 transitivePeerDependencies: - zod - ox@0.6.7(typescript@5.9.3)(zod@3.25.76): + ox@0.6.7(typescript@5.9.2)(zod@3.25.76): dependencies: - '@adraffy/ens-normalize': 1.11.1 + '@adraffy/ens-normalize': 1.11.0 '@noble/curves': 1.9.7 '@noble/hashes': 1.8.0 '@scure/bip32': 1.7.0 '@scure/bip39': 1.6.0 - abitype: 1.2.3(typescript@5.9.3)(zod@3.25.76) + abitype: 1.1.0(typescript@5.9.2)(zod@3.25.76) eventemitter3: 5.0.1 optionalDependencies: - typescript: 5.9.3 + typescript: 5.9.2 transitivePeerDependencies: - zod - ox@0.6.9(typescript@5.9.3)(zod@3.25.76): + ox@0.6.9(typescript@5.9.2)(zod@3.25.76): dependencies: - '@adraffy/ens-normalize': 1.11.1 + '@adraffy/ens-normalize': 1.11.0 '@noble/curves': 1.9.7 '@noble/hashes': 1.8.0 '@scure/bip32': 1.7.0 '@scure/bip39': 1.6.0 - abitype: 1.2.3(typescript@5.9.3)(zod@3.25.76) + abitype: 1.1.0(typescript@5.9.2)(zod@3.25.76) + eventemitter3: 5.0.1 + optionalDependencies: + typescript: 5.9.2 + transitivePeerDependencies: + - zod + + ox@0.9.17(typescript@5.9.2)(zod@4.1.13): + dependencies: + '@adraffy/ens-normalize': 1.11.0 + '@noble/ciphers': 1.3.0 + '@noble/curves': 1.9.1 + '@noble/hashes': 1.8.0 + '@scure/bip32': 1.7.0 + '@scure/bip39': 1.6.0 + abitype: 1.1.0(typescript@5.9.2)(zod@4.1.13) + eventemitter3: 5.0.1 + optionalDependencies: + typescript: 5.9.2 + transitivePeerDependencies: + - zod + + ox@0.9.3(typescript@5.9.2)(zod@3.25.76): + dependencies: + '@adraffy/ens-normalize': 1.11.0 + '@noble/ciphers': 1.3.0 + '@noble/curves': 1.9.1 + '@noble/hashes': 1.8.0 + '@scure/bip32': 1.7.0 + '@scure/bip39': 1.6.0 + abitype: 1.1.0(typescript@5.9.2)(zod@3.25.76) eventemitter3: 5.0.1 optionalDependencies: - typescript: 5.9.3 + typescript: 5.9.2 transitivePeerDependencies: - zod - ox@0.9.17(typescript@5.9.3)(zod@4.3.6): + ox@0.9.6(typescript@5.9.2)(zod@3.25.76): dependencies: - '@adraffy/ens-normalize': 1.11.1 + '@adraffy/ens-normalize': 1.11.0 '@noble/ciphers': 1.3.0 '@noble/curves': 1.9.1 '@noble/hashes': 1.8.0 '@scure/bip32': 1.7.0 '@scure/bip39': 1.6.0 - abitype: 1.2.3(typescript@5.9.3)(zod@4.3.6) + abitype: 1.1.0(typescript@5.9.2)(zod@3.25.76) eventemitter3: 5.0.1 optionalDependencies: - typescript: 5.9.3 + typescript: 5.9.2 transitivePeerDependencies: - zod @@ -15979,6 +17452,8 @@ snapshots: p-try@2.2.0: {} + package-json-from-dist@1.0.1: {} + package-manager-detector@0.2.11: dependencies: quansync: 0.2.11 @@ -15993,8 +17468,8 @@ snapshots: parse-json@5.2.0: dependencies: - '@babel/code-frame': 7.29.0 - error-ex: 1.3.4 + '@babel/code-frame': 7.27.1 + error-ex: 1.3.2 json-parse-even-better-errors: 2.3.1 lines-and-columns: 1.2.4 @@ -16004,6 +17479,11 @@ snapshots: dependencies: entities: 6.0.1 + parse5@8.0.0: + dependencies: + entities: 6.0.1 + optional: true + parseurl@1.3.3: {} path-exists@4.0.0: {} @@ -16012,6 +17492,11 @@ snapshots: path-parse@1.0.7: {} + path-scurry@1.11.1: + dependencies: + lru-cache: 10.4.3 + minipass: 7.1.2 + path-to-regexp@0.1.12: {} path-to-regexp@8.3.0: {} @@ -16039,8 +17524,28 @@ snapshots: duplexify: 4.1.3 split2: 4.2.0 + pino-abstract-transport@3.0.0: + dependencies: + split2: 4.2.0 + pino-std-serializers@4.0.0: {} + pino-std-serializers@7.1.0: {} + + pino@10.3.1: + dependencies: + '@pinojs/redact': 0.4.0 + atomic-sleep: 1.0.0 + on-exit-leak-free: 2.1.2 + pino-abstract-transport: 3.0.0 + pino-std-serializers: 7.1.0 + process-warning: 5.0.0 + quick-format-unescaped: 4.0.4 + real-require: 0.2.0 + safe-stable-stringify: 2.5.0 + sonic-boom: 4.2.1 + thread-stream: 4.0.0 + pino@7.11.0: dependencies: atomic-sleep: 1.0.0 @@ -16069,81 +17574,41 @@ snapshots: pony-cause@2.1.11: {} - porto@0.2.19(@tanstack/react-query@5.90.20(react@19.2.1))(@types/react@19.2.10)(@wagmi/core@2.22.1(@tanstack/query-core@5.90.20)(@types/react@19.2.10)(react@19.2.1)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.1))(viem@2.45.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)))(react@19.2.1)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.1))(viem@2.45.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(wagmi@2.19.5(@tanstack/query-core@5.90.20)(@tanstack/react-query@5.90.20(react@19.2.1))(@types/react@19.2.10)(@vercel/functions@2.2.13)(bufferutil@4.1.0)(fastestsmallesttextencoderdecoder@1.0.22)(react@19.2.1)(typescript@5.9.3)(utf-8-validate@5.0.10)(viem@2.45.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(zod@3.25.76)): + porto@0.2.35(@tanstack/react-query@5.90.11(react@19.2.1))(@types/react@19.1.12)(@wagmi/core@2.22.1(@tanstack/query-core@5.90.11)(@types/react@19.1.12)(react@19.2.1)(typescript@5.9.2)(use-sync-external-store@1.4.0(react@19.2.1))(viem@2.40.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76)))(react@19.2.1)(typescript@5.9.2)(use-sync-external-store@1.4.0(react@19.2.1))(viem@2.40.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76))(wagmi@2.19.5(@tanstack/query-core@5.90.11)(@tanstack/react-query@5.90.11(react@19.2.1))(@types/react@19.1.12)(@vercel/functions@2.2.13)(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(react@19.2.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(viem@2.40.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76))(zod@3.25.76)): dependencies: - '@wagmi/core': 2.22.1(@tanstack/query-core@5.90.20)(@types/react@19.2.10)(react@19.2.1)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.1))(viem@2.45.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)) - hono: 4.11.7 + '@wagmi/core': 2.22.1(@tanstack/query-core@5.90.11)(@types/react@19.1.12)(react@19.2.1)(typescript@5.9.2)(use-sync-external-store@1.4.0(react@19.2.1))(viem@2.40.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76)) + hono: 4.10.7 idb-keyval: 6.2.2 - mipd: 0.0.7(typescript@5.9.3) - ox: 0.9.17(typescript@5.9.3)(zod@4.3.6) - viem: 2.45.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) - zod: 4.3.6 - zustand: 5.0.11(@types/react@19.2.10)(react@19.2.1)(use-sync-external-store@1.4.0(react@19.2.1)) + mipd: 0.0.7(typescript@5.9.2) + ox: 0.9.17(typescript@5.9.2)(zod@4.1.13) + viem: 2.40.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + zod: 4.1.13 + zustand: 5.0.3(@types/react@19.1.12)(react@19.2.1)(use-sync-external-store@1.4.0(react@19.2.1)) optionalDependencies: - '@tanstack/react-query': 5.90.20(react@19.2.1) + '@tanstack/react-query': 5.90.11(react@19.2.1) react: 19.2.1 - typescript: 5.9.3 - wagmi: 2.19.5(@tanstack/query-core@5.90.20)(@tanstack/react-query@5.90.20(react@19.2.1))(@types/react@19.2.10)(@vercel/functions@2.2.13)(bufferutil@4.1.0)(fastestsmallesttextencoderdecoder@1.0.22)(react@19.2.1)(typescript@5.9.3)(utf-8-validate@5.0.10)(viem@2.45.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(zod@3.25.76) - transitivePeerDependencies: - - '@types/react' - - immer - - use-sync-external-store - - porto@0.2.19(@tanstack/react-query@5.90.20(react@19.2.4))(@types/react@19.2.10)(@wagmi/core@2.22.1(@tanstack/query-core@5.90.20)(@types/react@19.2.10)(react@19.2.4)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.4))(viem@2.45.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)))(react@19.2.4)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.4))(viem@2.45.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(wagmi@2.19.5(@tanstack/query-core@5.90.20)(@tanstack/react-query@5.90.20(react@19.2.4))(@types/react@19.2.10)(@vercel/functions@2.2.13)(bufferutil@4.1.0)(fastestsmallesttextencoderdecoder@1.0.22)(react@19.2.4)(typescript@5.9.3)(utf-8-validate@5.0.10)(viem@2.45.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(zod@3.25.76)): - dependencies: - '@wagmi/core': 2.22.1(@tanstack/query-core@5.90.20)(@types/react@19.2.10)(react@19.2.4)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.4))(viem@2.45.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)) - hono: 4.11.7 - idb-keyval: 6.2.2 - mipd: 0.0.7(typescript@5.9.3) - ox: 0.9.17(typescript@5.9.3)(zod@4.3.6) - viem: 2.45.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) - zod: 4.3.6 - zustand: 5.0.11(@types/react@19.2.10)(react@19.2.4)(use-sync-external-store@1.4.0(react@19.2.4)) - optionalDependencies: - '@tanstack/react-query': 5.90.20(react@19.2.4) - react: 19.2.4 - typescript: 5.9.3 - wagmi: 2.19.5(@tanstack/query-core@5.90.20)(@tanstack/react-query@5.90.20(react@19.2.4))(@types/react@19.2.10)(@vercel/functions@2.2.13)(bufferutil@4.1.0)(fastestsmallesttextencoderdecoder@1.0.22)(react@19.2.4)(typescript@5.9.3)(utf-8-validate@5.0.10)(viem@2.45.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(zod@3.25.76) + typescript: 5.9.2 + wagmi: 2.19.5(@tanstack/query-core@5.90.11)(@tanstack/react-query@5.90.11(react@19.2.1))(@types/react@19.1.12)(@vercel/functions@2.2.13)(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(react@19.2.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(viem@2.40.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76))(zod@3.25.76) transitivePeerDependencies: - '@types/react' - immer - use-sync-external-store - porto@0.2.35(@tanstack/react-query@5.90.20(react@19.2.1))(@types/react@19.2.10)(@wagmi/core@2.22.1(@tanstack/query-core@5.90.20)(@types/react@19.2.10)(react@19.2.1)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.1))(viem@2.45.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)))(react@19.2.1)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.1))(viem@2.45.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(wagmi@2.19.5(@tanstack/query-core@5.90.20)(@tanstack/react-query@5.90.20(react@19.2.1))(@types/react@19.2.10)(@vercel/functions@2.2.13)(bufferutil@4.1.0)(fastestsmallesttextencoderdecoder@1.0.22)(react@19.2.1)(typescript@5.9.3)(utf-8-validate@5.0.10)(viem@2.45.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(zod@3.25.76)): + porto@0.2.35(@tanstack/react-query@5.90.11(react@19.2.1))(@types/react@19.1.12)(@wagmi/core@2.22.1(@tanstack/query-core@5.90.11)(@types/react@19.1.12)(react@19.2.1)(typescript@5.9.2)(use-sync-external-store@1.4.0(react@19.2.1))(viem@2.45.1(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76)))(react@19.2.1)(typescript@5.9.2)(use-sync-external-store@1.4.0(react@19.2.1))(viem@2.45.1(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76))(wagmi@2.19.5(@tanstack/query-core@5.90.11)(@tanstack/react-query@5.90.11(react@19.2.1))(@types/react@19.1.12)(@vercel/functions@2.2.13)(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(react@19.2.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(viem@2.40.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76))(zod@3.25.76)): dependencies: - '@wagmi/core': 2.22.1(@tanstack/query-core@5.90.20)(@types/react@19.2.10)(react@19.2.1)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.1))(viem@2.45.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)) - hono: 4.11.7 + '@wagmi/core': 2.22.1(@tanstack/query-core@5.90.11)(@types/react@19.1.12)(react@19.2.1)(typescript@5.9.2)(use-sync-external-store@1.4.0(react@19.2.1))(viem@2.45.1(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76)) + hono: 4.10.7 idb-keyval: 6.2.2 - mipd: 0.0.7(typescript@5.9.3) - ox: 0.9.17(typescript@5.9.3)(zod@4.3.6) - viem: 2.45.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) - zod: 4.3.6 - zustand: 5.0.11(@types/react@19.2.10)(react@19.2.1)(use-sync-external-store@1.4.0(react@19.2.1)) + mipd: 0.0.7(typescript@5.9.2) + ox: 0.9.17(typescript@5.9.2)(zod@4.1.13) + viem: 2.45.1(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + zod: 4.1.13 + zustand: 5.0.3(@types/react@19.1.12)(react@19.2.1)(use-sync-external-store@1.4.0(react@19.2.1)) optionalDependencies: - '@tanstack/react-query': 5.90.20(react@19.2.1) + '@tanstack/react-query': 5.90.11(react@19.2.1) react: 19.2.1 - typescript: 5.9.3 - wagmi: 2.19.5(@tanstack/query-core@5.90.20)(@tanstack/react-query@5.90.20(react@19.2.1))(@types/react@19.2.10)(@vercel/functions@2.2.13)(bufferutil@4.1.0)(fastestsmallesttextencoderdecoder@1.0.22)(react@19.2.1)(typescript@5.9.3)(utf-8-validate@5.0.10)(viem@2.45.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(zod@3.25.76) - transitivePeerDependencies: - - '@types/react' - - immer - - use-sync-external-store - - porto@0.2.35(@tanstack/react-query@5.90.20(react@19.2.4))(@types/react@19.2.10)(@wagmi/core@2.22.1(@tanstack/query-core@5.90.20)(@types/react@19.2.10)(react@19.2.4)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.4))(viem@2.45.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)))(react@19.2.4)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.4))(viem@2.45.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(wagmi@2.19.5(@tanstack/query-core@5.90.20)(@tanstack/react-query@5.90.20(react@19.2.4))(@types/react@19.2.10)(@vercel/functions@2.2.13)(bufferutil@4.1.0)(fastestsmallesttextencoderdecoder@1.0.22)(react@19.2.4)(typescript@5.9.3)(utf-8-validate@5.0.10)(viem@2.45.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(zod@3.25.76)): - dependencies: - '@wagmi/core': 2.22.1(@tanstack/query-core@5.90.20)(@types/react@19.2.10)(react@19.2.4)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.4))(viem@2.45.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)) - hono: 4.11.7 - idb-keyval: 6.2.2 - mipd: 0.0.7(typescript@5.9.3) - ox: 0.9.17(typescript@5.9.3)(zod@4.3.6) - viem: 2.45.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) - zod: 4.3.6 - zustand: 5.0.11(@types/react@19.2.10)(react@19.2.4)(use-sync-external-store@1.4.0(react@19.2.4)) - optionalDependencies: - '@tanstack/react-query': 5.90.20(react@19.2.4) - react: 19.2.4 - typescript: 5.9.3 - wagmi: 2.19.5(@tanstack/query-core@5.90.20)(@tanstack/react-query@5.90.20(react@19.2.4))(@types/react@19.2.10)(@vercel/functions@2.2.13)(bufferutil@4.1.0)(fastestsmallesttextencoderdecoder@1.0.22)(react@19.2.4)(typescript@5.9.3)(utf-8-validate@5.0.10)(viem@2.45.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(zod@3.25.76) + typescript: 5.9.2 + wagmi: 2.19.5(@tanstack/query-core@5.90.11)(@tanstack/react-query@5.90.11(react@19.2.1))(@types/react@19.1.12)(@vercel/functions@2.2.13)(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(react@19.2.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(viem@2.40.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76))(zod@3.25.76) transitivePeerDependencies: - '@types/react' - immer @@ -16153,13 +17618,14 @@ snapshots: possible-typed-array-names@1.1.0: {} - postcss-load-config@6.0.1(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0): + postcss-load-config@6.0.1(jiti@2.6.1)(postcss@8.5.6)(tsx@4.20.5)(yaml@2.8.1): dependencies: lilconfig: 3.1.3 optionalDependencies: jiti: 2.6.1 postcss: 8.5.6 - tsx: 4.21.0 + tsx: 4.20.5 + yaml: 2.8.1 postcss@8.4.31: dependencies: @@ -16175,11 +17641,11 @@ snapshots: preact@10.24.2: {} - preact@10.28.3: {} + preact@10.27.2: {} prelude-ls@1.2.1: {} - prettier-linter-helpers@1.0.1: + prettier-linter-helpers@1.0.0: dependencies: fast-diff: 1.3.0 @@ -16191,6 +17657,10 @@ snapshots: process-warning@1.0.0: {} + process-warning@4.0.1: {} + + process-warning@5.0.0: {} + prop-types@15.8.1: dependencies: loose-envify: 1.4.0 @@ -16226,6 +17696,10 @@ snapshots: pngjs: 5.0.0 yargs: 15.4.1 + qs@6.13.0: + dependencies: + side-channel: 1.1.0 + qs@6.14.1: dependencies: side-channel: 1.1.0 @@ -16247,12 +17721,16 @@ snapshots: radix3@1.1.2: {} + randombytes@2.1.0: + dependencies: + safe-buffer: 5.2.1 + range-parser@1.2.1: {} - raw-body@2.5.3: + raw-body@2.5.2: dependencies: bytes: 3.1.2 - http-errors: 2.0.1 + http-errors: 2.0.0 iconv-lite: 0.4.24 unpipe: 1.0.0 @@ -16268,16 +17746,16 @@ snapshots: react: 19.2.1 scheduler: 0.27.0 - react-dom@19.2.4(react@19.2.4): + react-dom@19.2.3(react@19.2.3): dependencies: - react: 19.2.4 + react: 19.2.3 scheduler: 0.27.0 react-is@16.13.1: {} react@19.2.1: {} - react@19.2.4: {} + react@19.2.3: {} read-yaml-file@1.1.0: dependencies: @@ -16304,22 +17782,22 @@ snapshots: readdirp@4.1.2: {} - readdirp@5.0.0: {} - real-require@0.1.0: {} + real-require@0.2.0: {} + reflect.getprototypeof@1.0.10: dependencies: call-bind: 1.0.8 define-properties: 1.2.1 - es-abstract: 1.24.1 + es-abstract: 1.24.0 es-errors: 1.3.0 es-object-atoms: 1.1.1 get-intrinsic: 1.3.0 get-proto: 1.0.1 which-builtin-type: 1.2.1 - regenerate-unicode-properties@10.2.2: + regenerate-unicode-properties@10.2.0: dependencies: regenerate: 1.4.2 @@ -16334,20 +17812,20 @@ snapshots: gopd: 1.2.0 set-function-name: 2.0.2 - regexpu-core@6.4.0: + regexpu-core@6.2.0: dependencies: regenerate: 1.4.2 - regenerate-unicode-properties: 10.2.2 + regenerate-unicode-properties: 10.2.0 regjsgen: 0.8.0 - regjsparser: 0.13.0 + regjsparser: 0.12.0 unicode-match-property-ecmascript: 2.0.0 - unicode-match-property-value-ecmascript: 2.2.1 + unicode-match-property-value-ecmascript: 2.2.0 regjsgen@0.8.0: {} - regjsparser@0.13.0: + regjsparser@0.12.0: dependencies: - jsesc: 3.1.0 + jsesc: 3.0.2 require-directory@2.1.1: {} @@ -16363,7 +17841,7 @@ snapshots: resolve-pkg-maps@1.0.0: {} - resolve@1.22.11: + resolve@1.22.10: dependencies: is-core-module: 2.16.1 path-parse: 1.0.7 @@ -16379,37 +17857,37 @@ snapshots: dependencies: lowercase-keys: 2.0.0 + ret@0.5.0: {} + reusify@1.1.0: {} - rollup@4.57.1: + rfdc@1.4.1: {} + + rollup@4.50.0: dependencies: '@types/estree': 1.0.8 optionalDependencies: - '@rollup/rollup-android-arm-eabi': 4.57.1 - '@rollup/rollup-android-arm64': 4.57.1 - '@rollup/rollup-darwin-arm64': 4.57.1 - '@rollup/rollup-darwin-x64': 4.57.1 - '@rollup/rollup-freebsd-arm64': 4.57.1 - '@rollup/rollup-freebsd-x64': 4.57.1 - '@rollup/rollup-linux-arm-gnueabihf': 4.57.1 - '@rollup/rollup-linux-arm-musleabihf': 4.57.1 - '@rollup/rollup-linux-arm64-gnu': 4.57.1 - '@rollup/rollup-linux-arm64-musl': 4.57.1 - '@rollup/rollup-linux-loong64-gnu': 4.57.1 - '@rollup/rollup-linux-loong64-musl': 4.57.1 - '@rollup/rollup-linux-ppc64-gnu': 4.57.1 - '@rollup/rollup-linux-ppc64-musl': 4.57.1 - '@rollup/rollup-linux-riscv64-gnu': 4.57.1 - '@rollup/rollup-linux-riscv64-musl': 4.57.1 - '@rollup/rollup-linux-s390x-gnu': 4.57.1 - '@rollup/rollup-linux-x64-gnu': 4.57.1 - '@rollup/rollup-linux-x64-musl': 4.57.1 - '@rollup/rollup-openbsd-x64': 4.57.1 - '@rollup/rollup-openharmony-arm64': 4.57.1 - '@rollup/rollup-win32-arm64-msvc': 4.57.1 - '@rollup/rollup-win32-ia32-msvc': 4.57.1 - '@rollup/rollup-win32-x64-gnu': 4.57.1 - '@rollup/rollup-win32-x64-msvc': 4.57.1 + '@rollup/rollup-android-arm-eabi': 4.50.0 + '@rollup/rollup-android-arm64': 4.50.0 + '@rollup/rollup-darwin-arm64': 4.50.0 + '@rollup/rollup-darwin-x64': 4.50.0 + '@rollup/rollup-freebsd-arm64': 4.50.0 + '@rollup/rollup-freebsd-x64': 4.50.0 + '@rollup/rollup-linux-arm-gnueabihf': 4.50.0 + '@rollup/rollup-linux-arm-musleabihf': 4.50.0 + '@rollup/rollup-linux-arm64-gnu': 4.50.0 + '@rollup/rollup-linux-arm64-musl': 4.50.0 + '@rollup/rollup-linux-loongarch64-gnu': 4.50.0 + '@rollup/rollup-linux-ppc64-gnu': 4.50.0 + '@rollup/rollup-linux-riscv64-gnu': 4.50.0 + '@rollup/rollup-linux-riscv64-musl': 4.50.0 + '@rollup/rollup-linux-s390x-gnu': 4.50.0 + '@rollup/rollup-linux-x64-gnu': 4.50.0 + '@rollup/rollup-linux-x64-musl': 4.50.0 + '@rollup/rollup-openharmony-arm64': 4.50.0 + '@rollup/rollup-win32-arm64-msvc': 4.50.0 + '@rollup/rollup-win32-ia32-msvc': 4.50.0 + '@rollup/rollup-win32-x64-msvc': 4.50.0 fsevents: 2.3.3 router@2.2.0: @@ -16422,17 +17900,17 @@ snapshots: transitivePeerDependencies: - supports-color - rpc-websockets@9.3.3: + rpc-websockets@9.1.3: dependencies: - '@swc/helpers': 0.5.18 + '@swc/helpers': 0.5.17 '@types/uuid': 8.3.4 '@types/ws': 8.18.1 buffer: 6.0.3 - eventemitter3: 5.0.4 + eventemitter3: 5.0.1 uuid: 8.3.2 - ws: 8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10) + ws: 8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10) optionalDependencies: - bufferutil: 4.1.0 + bufferutil: 4.0.9 utf-8-validate: 5.0.10 rrweb-cssom@0.8.0: {} @@ -16464,6 +17942,10 @@ snapshots: es-errors: 1.3.0 is-regex: 1.2.1 + safe-regex2@5.0.0: + dependencies: + ret: 0.5.0 + safe-stable-stringify@2.5.0: {} safer-buffer@2.1.2: {} @@ -16474,25 +17956,29 @@ snapshots: scheduler@0.27.0: {} + secure-json-parse@4.1.0: {} + semver@6.3.1: {} + semver@7.7.2: {} + semver@7.7.3: {} - send@0.19.2: + send@0.19.0: dependencies: debug: 2.6.9 depd: 2.0.0 destroy: 1.2.0 - encodeurl: 2.0.0 + encodeurl: 1.0.2 escape-html: 1.0.3 etag: 1.8.1 fresh: 0.5.2 - http-errors: 2.0.1 + http-errors: 2.0.0 mime: 1.6.0 ms: 2.1.3 on-finished: 2.4.1 range-parser: 1.2.1 - statuses: 2.0.2 + statuses: 2.0.1 transitivePeerDependencies: - supports-color @@ -16512,12 +17998,12 @@ snapshots: transitivePeerDependencies: - supports-color - serve-static@1.16.3: + serve-static@1.16.2: dependencies: encodeurl: 2.0.0 escape-html: 1.0.3 parseurl: 1.3.3 - send: 0.19.2 + send: 0.19.0 transitivePeerDependencies: - supports-color @@ -16532,6 +18018,8 @@ snapshots: set-blocking@2.0.0: {} + set-cookie-parser@2.7.2: {} + set-function-length@1.2.2: dependencies: define-data-property: 1.1.4 @@ -16560,7 +18048,7 @@ snapshots: dependencies: inherits: 2.0.4 safe-buffer: 5.2.1 - to-buffer: 1.2.2 + to-buffer: 1.2.1 sharp@0.34.5: dependencies: @@ -16632,11 +18120,11 @@ snapshots: signal-exit@4.1.0: {} - siwe@2.3.2(ethers@6.16.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)): + siwe@2.3.2(ethers@6.16.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)): dependencies: '@spruceid/siwe-parser': 2.1.2 '@stablelib/random': 1.0.2 - ethers: 6.16.0(bufferutil@4.1.0)(utf-8-validate@5.0.10) + ethers: 6.16.0(bufferutil@4.0.9)(utf-8-validate@5.0.10) uri-js: 4.4.1 valid-url: 1.0.9 @@ -16647,21 +18135,21 @@ snapshots: dot-case: 3.0.4 tslib: 2.8.1 - socket.io-client@4.8.3(bufferutil@4.1.0)(utf-8-validate@5.0.10): + socket.io-client@4.8.1(bufferutil@4.0.9)(utf-8-validate@5.0.10): dependencies: '@socket.io/component-emitter': 3.1.2 - debug: 4.4.3 - engine.io-client: 6.6.4(bufferutil@4.1.0)(utf-8-validate@5.0.10) - socket.io-parser: 4.2.5 + debug: 4.3.7 + engine.io-client: 6.6.3(bufferutil@4.0.9)(utf-8-validate@5.0.10) + socket.io-parser: 4.2.4 transitivePeerDependencies: - bufferutil - supports-color - utf-8-validate - socket.io-parser@4.2.5: + socket.io-parser@4.2.4: dependencies: '@socket.io/component-emitter': 3.1.2 - debug: 4.4.3 + debug: 4.3.7 transitivePeerDependencies: - supports-color @@ -16669,9 +18157,15 @@ snapshots: dependencies: atomic-sleep: 1.0.0 + sonic-boom@4.2.1: + dependencies: + atomic-sleep: 1.0.0 + source-map-js@1.2.1: {} - source-map@0.7.6: {} + source-map@0.8.0-beta.0: + dependencies: + whatwg-url: 7.1.0 spawndamnit@3.0.1: dependencies: @@ -16697,9 +18191,11 @@ snapshots: stackback@0.0.2: {} + statuses@2.0.1: {} + statuses@2.0.2: {} - std-env@3.10.0: {} + std-env@3.9.0: {} stop-iteration-iterator@1.1.0: dependencies: @@ -16722,18 +18218,24 @@ snapshots: is-fullwidth-code-point: 3.0.0 strip-ansi: 6.0.1 + string-width@5.1.2: + dependencies: + eastasianwidth: 0.2.0 + emoji-regex: 9.2.2 + strip-ansi: 7.1.0 + string.prototype.includes@2.0.1: dependencies: call-bind: 1.0.8 define-properties: 1.2.1 - es-abstract: 1.24.1 + es-abstract: 1.24.0 string.prototype.matchall@4.0.12: dependencies: call-bind: 1.0.8 call-bound: 1.0.4 define-properties: 1.2.1 - es-abstract: 1.24.1 + es-abstract: 1.24.0 es-errors: 1.3.0 es-object-atoms: 1.1.1 get-intrinsic: 1.3.0 @@ -16747,7 +18249,7 @@ snapshots: string.prototype.repeat@1.0.0: dependencies: define-properties: 1.2.1 - es-abstract: 1.24.1 + es-abstract: 1.24.0 string.prototype.trim@1.2.10: dependencies: @@ -16755,7 +18257,7 @@ snapshots: call-bound: 1.0.4 define-data-property: 1.1.4 define-properties: 1.2.1 - es-abstract: 1.24.1 + es-abstract: 1.24.0 es-object-atoms: 1.1.1 has-property-descriptors: 1.0.2 @@ -16784,29 +18286,33 @@ snapshots: dependencies: ansi-regex: 5.0.1 + strip-ansi@7.1.0: + dependencies: + ansi-regex: 6.2.0 + strip-bom@3.0.0: {} strip-json-comments@3.1.1: {} - strip-literal@3.1.0: + strip-literal@3.0.0: dependencies: js-tokens: 9.0.1 - styled-jsx@5.1.6(@babel/core@7.29.0)(react@19.2.4): + styled-jsx@5.1.6(@babel/core@7.28.3)(react@19.2.3): dependencies: client-only: 0.0.1 - react: 19.2.4 + react: 19.2.3 optionalDependencies: - '@babel/core': 7.29.0 + '@babel/core': 7.28.3 - sucrase@3.35.1: + sucrase@3.35.0: dependencies: '@jridgewell/gen-mapping': 0.3.13 commander: 4.1.1 + glob: 10.4.5 lines-and-columns: 1.2.4 mz: 2.7.0 pirates: 4.0.7 - tinyglobby: 0.2.15 ts-interface-checker: 0.1.13 superstruct@1.0.4: {} @@ -16833,13 +18339,13 @@ snapshots: symbol-tree@3.2.4: {} - synckit@0.11.12: + synckit@0.11.11: dependencies: '@pkgr/core': 0.2.9 - tailwind-merge@2.6.1: {} + tailwind-merge@2.6.0: {} - tailwindcss@4.1.18: {} + tailwindcss@4.1.17: {} tapable@2.3.0: {} @@ -16859,10 +18365,19 @@ snapshots: dependencies: real-require: 0.1.0 + thread-stream@4.0.0: + dependencies: + real-require: 0.2.0 + tinybench@2.9.0: {} tinyexec@0.3.2: {} + tinyglobby@0.2.14: + dependencies: + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 + tinyglobby@0.2.15: dependencies: fdir: 6.5.0(picomatch@4.0.3) @@ -16872,15 +18387,23 @@ snapshots: tinyrainbow@2.0.0: {} - tinyspy@4.0.4: {} + tinyspy@4.0.3: {} tldts-core@6.1.86: {} + tldts-core@7.0.19: + optional: true + tldts@6.1.86: dependencies: tldts-core: 6.1.86 - to-buffer@1.2.2: + tldts@7.0.19: + dependencies: + tldts-core: 7.0.19 + optional: true + + to-buffer@1.2.1: dependencies: isarray: 2.0.5 safe-buffer: 5.2.1 @@ -16890,29 +18413,47 @@ snapshots: dependencies: is-number: 7.0.0 + toad-cache@3.7.0: {} + toidentifier@1.0.1: {} + toml@3.0.0: {} + tough-cookie@5.1.2: dependencies: tldts: 6.1.86 + tough-cookie@6.0.0: + dependencies: + tldts: 7.0.19 + optional: true + tr46@0.0.3: {} + tr46@1.0.1: + dependencies: + punycode: 2.3.1 + tr46@5.1.1: dependencies: punycode: 2.3.1 + tr46@6.0.0: + dependencies: + punycode: 2.3.1 + optional: true + tree-kill@1.2.2: {} - ts-api-utils@2.4.0(typescript@5.9.3): + ts-api-utils@2.1.0(typescript@5.9.2): dependencies: - typescript: 5.9.3 + typescript: 5.9.2 ts-interface-checker@0.1.13: {} - tsconfck@3.1.6(typescript@5.9.3): + tsconfck@3.1.6(typescript@5.9.2): optionalDependencies: - typescript: 5.9.3 + typescript: 5.9.2 tsconfig-paths@3.15.0: dependencies: @@ -16927,67 +18468,67 @@ snapshots: tslib@2.8.1: {} - tsup@8.5.1(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(typescript@5.9.3): + tsup@8.5.0(jiti@2.6.1)(postcss@8.5.6)(tsx@4.20.5)(typescript@5.9.2)(yaml@2.8.1): dependencies: - bundle-require: 5.1.0(esbuild@0.27.2) + bundle-require: 5.1.0(esbuild@0.25.9) cac: 6.7.14 chokidar: 4.0.3 consola: 3.4.2 - debug: 4.4.3 - esbuild: 0.27.2 + debug: 4.4.1 + esbuild: 0.25.9 fix-dts-default-cjs-exports: 1.0.1 joycon: 3.1.1 picocolors: 1.1.1 - postcss-load-config: 6.0.1(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0) + postcss-load-config: 6.0.1(jiti@2.6.1)(postcss@8.5.6)(tsx@4.20.5)(yaml@2.8.1) resolve-from: 5.0.0 - rollup: 4.57.1 - source-map: 0.7.6 - sucrase: 3.35.1 + rollup: 4.50.0 + source-map: 0.8.0-beta.0 + sucrase: 3.35.0 tinyexec: 0.3.2 - tinyglobby: 0.2.15 + tinyglobby: 0.2.14 tree-kill: 1.2.2 optionalDependencies: postcss: 8.5.6 - typescript: 5.9.3 + typescript: 5.9.2 transitivePeerDependencies: - jiti - supports-color - tsx - yaml - tsx@4.21.0: + tsx@4.20.5: dependencies: - esbuild: 0.27.2 - get-tsconfig: 4.13.1 + esbuild: 0.25.9 + get-tsconfig: 4.10.1 optionalDependencies: fsevents: 2.3.3 - turbo-darwin-64@2.8.1: + turbo-darwin-64@2.5.6: optional: true - turbo-darwin-arm64@2.8.1: + turbo-darwin-arm64@2.5.6: optional: true - turbo-linux-64@2.8.1: + turbo-linux-64@2.5.6: optional: true - turbo-linux-arm64@2.8.1: + turbo-linux-arm64@2.5.6: optional: true - turbo-windows-64@2.8.1: + turbo-windows-64@2.5.6: optional: true - turbo-windows-arm64@2.8.1: + turbo-windows-arm64@2.5.6: optional: true - turbo@2.8.1: + turbo@2.5.6: optionalDependencies: - turbo-darwin-64: 2.8.1 - turbo-darwin-arm64: 2.8.1 - turbo-linux-64: 2.8.1 - turbo-linux-arm64: 2.8.1 - turbo-windows-64: 2.8.1 - turbo-windows-arm64: 2.8.1 + turbo-darwin-64: 2.5.6 + turbo-darwin-arm64: 2.5.6 + turbo-linux-64: 2.5.6 + turbo-linux-arm64: 2.5.6 + turbo-windows-64: 2.5.6 + turbo-windows-arm64: 2.5.6 tweetnacl@1.0.3: {} @@ -17039,20 +18580,20 @@ snapshots: possible-typed-array-names: 1.1.0 reflect.getprototypeof: 1.0.10 - typescript-eslint@8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3): + typescript-eslint@8.49.0(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.2): dependencies: - '@typescript-eslint/eslint-plugin': 8.54.0(@typescript-eslint/parser@8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/parser': 8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/typescript-estree': 8.54.0(typescript@5.9.3) - '@typescript-eslint/utils': 8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3) - eslint: 9.39.2(jiti@2.6.1) - typescript: 5.9.3 + '@typescript-eslint/eslint-plugin': 8.49.0(@typescript-eslint/parser@8.49.0(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.2))(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.2) + '@typescript-eslint/parser': 8.49.0(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.2) + '@typescript-eslint/typescript-estree': 8.49.0(typescript@5.9.2) + '@typescript-eslint/utils': 8.49.0(eslint@9.34.0(jiti@2.6.1))(typescript@5.9.2) + eslint: 9.34.0(jiti@2.6.1) + typescript: 5.9.2 transitivePeerDependencies: - supports-color - typescript@5.9.3: {} + typescript@5.9.2: {} - ufo@1.6.3: {} + ufo@1.6.1: {} uint8arrays@3.1.0: dependencies: @@ -17071,7 +18612,7 @@ snapshots: undici-types@6.21.0: {} - undici-types@7.20.0: {} + undici-types@7.16.0: {} undici-types@7.22.0: {} @@ -17080,11 +18621,11 @@ snapshots: unicode-match-property-ecmascript@2.0.0: dependencies: unicode-canonical-property-names-ecmascript: 2.0.1 - unicode-property-aliases-ecmascript: 2.2.0 + unicode-property-aliases-ecmascript: 2.1.0 - unicode-match-property-value-ecmascript@2.2.1: {} + unicode-match-property-value-ecmascript@2.2.0: {} - unicode-property-aliases-ecmascript@2.2.0: {} + unicode-property-aliases-ecmascript@2.1.0: {} universalify@0.1.2: {} @@ -17092,7 +18633,7 @@ snapshots: unrs-resolver@1.11.1: dependencies: - napi-postinstall: 0.3.4 + napi-postinstall: 0.3.3 optionalDependencies: '@unrs/resolver-binding-android-arm-eabi': 1.11.1 '@unrs/resolver-binding-android-arm64': 1.11.1 @@ -17114,23 +18655,23 @@ snapshots: '@unrs/resolver-binding-win32-ia32-msvc': 1.11.1 '@unrs/resolver-binding-win32-x64-msvc': 1.11.1 - unstorage@1.17.4(@vercel/functions@2.2.13)(idb-keyval@6.2.2): + unstorage@1.17.0(@vercel/functions@2.2.13)(idb-keyval@6.2.2): dependencies: anymatch: 3.1.3 - chokidar: 5.0.0 + chokidar: 4.0.3 destr: 2.0.5 - h3: 1.15.5 - lru-cache: 11.2.5 + h3: 1.15.4 + lru-cache: 10.4.3 node-fetch-native: 1.6.7 - ofetch: 1.5.1 - ufo: 1.6.3 + ofetch: 1.4.1 + ufo: 1.6.1 optionalDependencies: '@vercel/functions': 2.2.13 idb-keyval: 6.2.2 - update-browserslist-db@1.2.3(browserslist@4.28.1): + update-browserslist-db@1.1.3(browserslist@4.25.4): dependencies: - browserslist: 4.28.1 + browserslist: 4.25.4 escalade: 3.2.0 picocolors: 1.1.1 @@ -17138,21 +18679,23 @@ snapshots: dependencies: punycode: 2.3.1 + urijs@1.19.11: {} + use-sync-external-store@1.2.0(react@19.2.1): dependencies: react: 19.2.1 - use-sync-external-store@1.2.0(react@19.2.4): + use-sync-external-store@1.2.0(react@19.2.3): dependencies: - react: 19.2.4 + react: 19.2.3 use-sync-external-store@1.4.0(react@19.2.1): dependencies: react: 19.2.1 - use-sync-external-store@1.4.0(react@19.2.4): + use-sync-external-store@1.4.0(react@19.2.3): dependencies: - react: 19.2.4 + react: 19.2.3 utf-8-validate@5.0.10: dependencies: @@ -17164,9 +18707,9 @@ snapshots: dependencies: inherits: 2.0.4 is-arguments: 1.2.0 - is-generator-function: 1.1.2 + is-generator-function: 1.1.0 is-typed-array: 1.1.15 - which-typed-array: 1.1.20 + which-typed-array: 1.1.19 utils-merge@1.0.1: {} @@ -17176,84 +18719,118 @@ snapshots: valid-url@1.0.9: {} - valtio@1.13.2(@types/react@19.2.10)(react@19.2.1): + valtio@1.13.2(@types/react@19.1.12)(react@19.2.1): dependencies: - derive-valtio: 0.1.0(valtio@1.13.2(@types/react@19.2.10)(react@19.2.1)) + derive-valtio: 0.1.0(valtio@1.13.2(@types/react@19.1.12)(react@19.2.1)) proxy-compare: 2.6.0 use-sync-external-store: 1.2.0(react@19.2.1) optionalDependencies: - '@types/react': 19.2.10 + '@types/react': 19.1.12 react: 19.2.1 - valtio@1.13.2(@types/react@19.2.10)(react@19.2.4): + valtio@1.13.2(@types/react@19.1.12)(react@19.2.3): dependencies: - derive-valtio: 0.1.0(valtio@1.13.2(@types/react@19.2.10)(react@19.2.4)) + derive-valtio: 0.1.0(valtio@1.13.2(@types/react@19.1.12)(react@19.2.3)) proxy-compare: 2.6.0 - use-sync-external-store: 1.2.0(react@19.2.4) + use-sync-external-store: 1.2.0(react@19.2.3) optionalDependencies: - '@types/react': 19.2.10 - react: 19.2.4 + '@types/react': 19.1.12 + react: 19.2.3 vary@1.1.2: {} - viem@2.23.2(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76): + viem@2.23.2(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76): dependencies: '@noble/curves': 1.8.1 '@noble/hashes': 1.7.1 '@scure/bip32': 1.6.2 '@scure/bip39': 1.5.4 - abitype: 1.0.8(typescript@5.9.3)(zod@3.25.76) - isows: 1.0.6(ws@8.18.0(bufferutil@4.1.0)(utf-8-validate@5.0.10)) - ox: 0.6.7(typescript@5.9.3)(zod@3.25.76) - ws: 8.18.0(bufferutil@4.1.0)(utf-8-validate@5.0.10) + abitype: 1.0.8(typescript@5.9.2)(zod@3.25.76) + isows: 1.0.6(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + ox: 0.6.7(typescript@5.9.2)(zod@3.25.76) + ws: 8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10) + optionalDependencies: + typescript: 5.9.2 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + - zod + + viem@2.37.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76): + dependencies: + '@noble/curves': 1.9.1 + '@noble/hashes': 1.8.0 + '@scure/bip32': 1.7.0 + '@scure/bip39': 1.6.0 + abitype: 1.1.0(typescript@5.9.2)(zod@3.25.76) + isows: 1.0.7(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + ox: 0.9.3(typescript@5.9.2)(zod@3.25.76) + ws: 8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10) + optionalDependencies: + typescript: 5.9.2 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + - zod + + viem@2.40.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76): + dependencies: + '@noble/curves': 1.9.1 + '@noble/hashes': 1.8.0 + '@scure/bip32': 1.7.0 + '@scure/bip39': 1.6.0 + abitype: 1.1.0(typescript@5.9.2)(zod@3.25.76) + isows: 1.0.7(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + ox: 0.9.6(typescript@5.9.2)(zod@3.25.76) + ws: 8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10) optionalDependencies: - typescript: 5.9.3 + typescript: 5.9.2 transitivePeerDependencies: - bufferutil - utf-8-validate - zod - viem@2.45.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.22.4): + viem@2.45.1(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.22.4): dependencies: '@noble/curves': 1.9.1 '@noble/hashes': 1.8.0 '@scure/bip32': 1.7.0 '@scure/bip39': 1.6.0 - abitype: 1.2.3(typescript@5.9.3)(zod@3.22.4) - isows: 1.0.7(ws@8.18.3(bufferutil@4.1.0)(utf-8-validate@5.0.10)) - ox: 0.11.3(typescript@5.9.3)(zod@3.22.4) - ws: 8.18.3(bufferutil@4.1.0)(utf-8-validate@5.0.10) + abitype: 1.2.3(typescript@5.9.2)(zod@3.22.4) + isows: 1.0.7(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + ox: 0.11.3(typescript@5.9.2)(zod@3.22.4) + ws: 8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10) optionalDependencies: - typescript: 5.9.3 + typescript: 5.9.2 transitivePeerDependencies: - bufferutil - utf-8-validate - zod - viem@2.45.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76): + viem@2.45.1(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76): dependencies: '@noble/curves': 1.9.1 '@noble/hashes': 1.8.0 '@scure/bip32': 1.7.0 '@scure/bip39': 1.6.0 - abitype: 1.2.3(typescript@5.9.3)(zod@3.25.76) - isows: 1.0.7(ws@8.18.3(bufferutil@4.1.0)(utf-8-validate@5.0.10)) - ox: 0.11.3(typescript@5.9.3)(zod@3.25.76) - ws: 8.18.3(bufferutil@4.1.0)(utf-8-validate@5.0.10) + abitype: 1.2.3(typescript@5.9.2)(zod@3.25.76) + isows: 1.0.7(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + ox: 0.11.3(typescript@5.9.2)(zod@3.25.76) + ws: 8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10) optionalDependencies: - typescript: 5.9.3 + typescript: 5.9.2 transitivePeerDependencies: - bufferutil - utf-8-validate - zod - vite-node@3.2.4(@types/node@22.19.7)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0): + vite-node@3.2.4(@types/node@22.18.0)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.5)(yaml@2.8.1): dependencies: cac: 6.7.14 debug: 4.4.3 es-module-lexer: 1.7.0 pathe: 2.0.3 - vite: 6.4.1(@types/node@22.19.7)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0) + vite: 6.3.5(@types/node@22.18.0)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.5)(yaml@2.8.1) transitivePeerDependencies: - '@types/node' - jiti @@ -17268,37 +18845,38 @@ snapshots: - tsx - yaml - vite-tsconfig-paths@5.1.4(typescript@5.9.3)(vite@6.4.1(@types/node@22.19.7)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)): + vite-tsconfig-paths@5.1.4(typescript@5.9.2)(vite@6.3.5(@types/node@22.18.0)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.5)(yaml@2.8.1)): dependencies: - debug: 4.4.3 + debug: 4.4.1 globrex: 0.1.2 - tsconfck: 3.1.6(typescript@5.9.3) + tsconfck: 3.1.6(typescript@5.9.2) optionalDependencies: - vite: 6.4.1(@types/node@22.19.7)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0) + vite: 6.3.5(@types/node@22.18.0)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.5)(yaml@2.8.1) transitivePeerDependencies: - supports-color - typescript - vite@6.4.1(@types/node@22.19.7)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0): + vite@6.3.5(@types/node@22.18.0)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.5)(yaml@2.8.1): dependencies: - esbuild: 0.25.12 + esbuild: 0.25.9 fdir: 6.5.0(picomatch@4.0.3) picomatch: 4.0.3 postcss: 8.5.6 - rollup: 4.57.1 + rollup: 4.50.0 tinyglobby: 0.2.15 optionalDependencies: - '@types/node': 22.19.7 + '@types/node': 22.18.0 fsevents: 2.3.3 jiti: 2.6.1 lightningcss: 1.30.2 - tsx: 4.21.0 + tsx: 4.20.5 + yaml: 2.8.1 - vitest@3.2.4(@types/debug@4.1.12)(@types/node@22.19.7)(jiti@2.6.1)(jsdom@26.1.0(bufferutil@4.1.0)(utf-8-validate@5.0.10))(lightningcss@1.30.2)(tsx@4.21.0): + vitest@3.2.4(@types/debug@4.1.12)(@types/node@22.18.0)(jiti@2.6.1)(jsdom@27.4.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(lightningcss@1.30.2)(tsx@4.20.5)(yaml@2.8.1): dependencies: - '@types/chai': 5.2.3 + '@types/chai': 5.2.2 '@vitest/expect': 3.2.4 - '@vitest/mocker': 3.2.4(vite@6.4.1(@types/node@22.19.7)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)) + '@vitest/mocker': 3.2.4(vite@6.3.5(@types/node@22.18.0)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.5)(yaml@2.8.1)) '@vitest/pretty-format': 3.2.4 '@vitest/runner': 3.2.4 '@vitest/snapshot': 3.2.4 @@ -17306,23 +18884,23 @@ snapshots: '@vitest/utils': 3.2.4 chai: 5.3.3 debug: 4.4.3 - expect-type: 1.3.0 + expect-type: 1.2.2 magic-string: 0.30.21 pathe: 2.0.3 picomatch: 4.0.3 - std-env: 3.10.0 + std-env: 3.9.0 tinybench: 2.9.0 tinyexec: 0.3.2 tinyglobby: 0.2.15 tinypool: 1.1.1 tinyrainbow: 2.0.0 - vite: 6.4.1(@types/node@22.19.7)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0) - vite-node: 3.2.4(@types/node@22.19.7)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0) + vite: 6.3.5(@types/node@22.18.0)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.5)(yaml@2.8.1) + vite-node: 3.2.4(@types/node@22.18.0)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.5)(yaml@2.8.1) why-is-node-running: 2.3.0 optionalDependencies: '@types/debug': 4.1.12 - '@types/node': 22.19.7 - jsdom: 26.1.0(bufferutil@4.1.0)(utf-8-validate@5.0.10) + '@types/node': 22.18.0 + jsdom: 27.4.0(bufferutil@4.0.9)(utf-8-validate@5.0.10) transitivePeerDependencies: - jiti - less @@ -17341,16 +18919,55 @@ snapshots: dependencies: xml-name-validator: 5.0.0 - wagmi@2.19.5(@tanstack/query-core@5.90.20)(@tanstack/react-query@5.90.20(react@19.2.1))(@types/react@19.2.10)(@vercel/functions@2.2.13)(bufferutil@4.1.0)(fastestsmallesttextencoderdecoder@1.0.22)(react@19.2.1)(typescript@5.9.3)(utf-8-validate@5.0.10)(viem@2.45.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(zod@3.25.76): + wagmi@2.16.9(@tanstack/query-core@5.90.11)(@tanstack/react-query@5.90.11(react@19.2.3))(@types/react@19.1.12)(@vercel/functions@2.2.13)(bufferutil@4.0.9)(react@19.2.3)(typescript@5.9.2)(utf-8-validate@5.0.10)(viem@2.37.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76))(zod@3.25.76): + dependencies: + '@tanstack/react-query': 5.90.11(react@19.2.3) + '@wagmi/connectors': 5.9.9(@types/react@19.1.12)(@vercel/functions@2.2.13)(@wagmi/core@2.20.3(@tanstack/query-core@5.90.11)(@types/react@19.1.12)(react@19.2.3)(typescript@5.9.2)(use-sync-external-store@1.4.0(react@19.2.3))(viem@2.37.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76)))(bufferutil@4.0.9)(react@19.2.3)(typescript@5.9.2)(use-sync-external-store@1.4.0(react@19.2.3))(utf-8-validate@5.0.10)(viem@2.37.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76))(zod@3.25.76) + '@wagmi/core': 2.20.3(@tanstack/query-core@5.90.11)(@types/react@19.1.12)(react@19.2.3)(typescript@5.9.2)(use-sync-external-store@1.4.0(react@19.2.3))(viem@2.37.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76)) + react: 19.2.3 + use-sync-external-store: 1.4.0(react@19.2.3) + viem: 2.37.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) + optionalDependencies: + typescript: 5.9.2 + transitivePeerDependencies: + - '@azure/app-configuration' + - '@azure/cosmos' + - '@azure/data-tables' + - '@azure/identity' + - '@azure/keyvault-secrets' + - '@azure/storage-blob' + - '@capacitor/preferences' + - '@deno/kv' + - '@netlify/blobs' + - '@planetscale/database' + - '@react-native-async-storage/async-storage' + - '@tanstack/query-core' + - '@types/react' + - '@upstash/redis' + - '@vercel/blob' + - '@vercel/functions' + - '@vercel/kv' + - aws4fetch + - bufferutil + - db0 + - encoding + - immer + - ioredis + - supports-color + - uploadthing + - utf-8-validate + - zod + + wagmi@2.19.5(@tanstack/query-core@5.90.11)(@tanstack/react-query@5.90.11(react@19.2.1))(@types/react@19.1.12)(@vercel/functions@2.2.13)(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(react@19.2.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(viem@2.40.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76))(zod@3.25.76): dependencies: - '@tanstack/react-query': 5.90.20(react@19.2.1) - '@wagmi/connectors': 6.2.0(@tanstack/react-query@5.90.20(react@19.2.1))(@types/react@19.2.10)(@vercel/functions@2.2.13)(@wagmi/core@2.22.1(@tanstack/query-core@5.90.20)(@types/react@19.2.10)(react@19.2.1)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.1))(viem@2.45.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)))(bufferutil@4.1.0)(fastestsmallesttextencoderdecoder@1.0.22)(react@19.2.1)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.1))(utf-8-validate@5.0.10)(viem@2.45.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(wagmi@2.19.5(@tanstack/query-core@5.90.20)(@tanstack/react-query@5.90.20(react@19.2.1))(@types/react@19.2.10)(@vercel/functions@2.2.13)(bufferutil@4.1.0)(fastestsmallesttextencoderdecoder@1.0.22)(react@19.2.1)(typescript@5.9.3)(utf-8-validate@5.0.10)(viem@2.45.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(zod@3.25.76))(zod@3.25.76) - '@wagmi/core': 2.22.1(@tanstack/query-core@5.90.20)(@types/react@19.2.10)(react@19.2.1)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.1))(viem@2.45.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)) + '@tanstack/react-query': 5.90.11(react@19.2.1) + '@wagmi/connectors': 6.2.0(@tanstack/react-query@5.90.11(react@19.2.1))(@types/react@19.1.12)(@vercel/functions@2.2.13)(@wagmi/core@2.22.1(@tanstack/query-core@5.90.11)(@types/react@19.1.12)(react@19.2.1)(typescript@5.9.2)(use-sync-external-store@1.4.0(react@19.2.1))(viem@2.40.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76)))(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(react@19.2.1)(typescript@5.9.2)(use-sync-external-store@1.4.0(react@19.2.1))(utf-8-validate@5.0.10)(viem@2.40.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76))(wagmi@2.19.5(@tanstack/query-core@5.90.11)(@tanstack/react-query@5.90.11(react@19.2.1))(@types/react@19.1.12)(@vercel/functions@2.2.13)(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(react@19.2.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(viem@2.40.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76))(zod@3.25.76))(zod@3.25.76) + '@wagmi/core': 2.22.1(@tanstack/query-core@5.90.11)(@types/react@19.1.12)(react@19.2.1)(typescript@5.9.2)(use-sync-external-store@1.4.0(react@19.2.1))(viem@2.40.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76)) react: 19.2.1 use-sync-external-store: 1.4.0(react@19.2.1) - viem: 2.45.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + viem: 2.40.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) optionalDependencies: - typescript: 5.9.3 + typescript: 5.9.2 transitivePeerDependencies: - '@azure/app-configuration' - '@azure/cosmos' @@ -17386,16 +19003,16 @@ snapshots: - utf-8-validate - zod - wagmi@2.19.5(@tanstack/query-core@5.90.20)(@tanstack/react-query@5.90.20(react@19.2.4))(@types/react@19.2.10)(@vercel/functions@2.2.13)(bufferutil@4.1.0)(fastestsmallesttextencoderdecoder@1.0.22)(react@19.2.4)(typescript@5.9.3)(utf-8-validate@5.0.10)(viem@2.45.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(zod@3.25.76): + wagmi@2.19.5(@tanstack/query-core@5.90.11)(@tanstack/react-query@5.90.11(react@19.2.1))(@types/react@19.1.12)(@vercel/functions@2.2.13)(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(react@19.2.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(viem@2.45.1(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76))(zod@3.25.76): dependencies: - '@tanstack/react-query': 5.90.20(react@19.2.4) - '@wagmi/connectors': 6.2.0(@tanstack/react-query@5.90.20(react@19.2.4))(@types/react@19.2.10)(@vercel/functions@2.2.13)(@wagmi/core@2.22.1(@tanstack/query-core@5.90.20)(@types/react@19.2.10)(react@19.2.4)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.4))(viem@2.45.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)))(bufferutil@4.1.0)(fastestsmallesttextencoderdecoder@1.0.22)(react@19.2.4)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.4))(utf-8-validate@5.0.10)(viem@2.45.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(wagmi@2.19.5(@tanstack/query-core@5.90.20)(@tanstack/react-query@5.90.20(react@19.2.4))(@types/react@19.2.10)(@vercel/functions@2.2.13)(bufferutil@4.1.0)(fastestsmallesttextencoderdecoder@1.0.22)(react@19.2.4)(typescript@5.9.3)(utf-8-validate@5.0.10)(viem@2.45.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))(zod@3.25.76))(zod@3.25.76) - '@wagmi/core': 2.22.1(@tanstack/query-core@5.90.20)(@types/react@19.2.10)(react@19.2.4)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.4))(viem@2.45.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76)) - react: 19.2.4 - use-sync-external-store: 1.4.0(react@19.2.4) - viem: 2.45.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + '@tanstack/react-query': 5.90.11(react@19.2.1) + '@wagmi/connectors': 6.2.0(@tanstack/react-query@5.90.11(react@19.2.1))(@types/react@19.1.12)(@vercel/functions@2.2.13)(@wagmi/core@2.22.1(@tanstack/query-core@5.90.11)(@types/react@19.1.12)(react@19.2.1)(typescript@5.9.2)(use-sync-external-store@1.4.0(react@19.2.1))(viem@2.45.1(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76)))(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(react@19.2.1)(typescript@5.9.2)(use-sync-external-store@1.4.0(react@19.2.1))(utf-8-validate@5.0.10)(viem@2.45.1(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76))(wagmi@2.19.5(@tanstack/query-core@5.90.11)(@tanstack/react-query@5.90.11(react@19.2.1))(@types/react@19.1.12)(@vercel/functions@2.2.13)(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(react@19.2.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(viem@2.40.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76))(zod@3.25.76))(zod@3.25.76) + '@wagmi/core': 2.22.1(@tanstack/query-core@5.90.11)(@types/react@19.1.12)(react@19.2.1)(typescript@5.9.2)(use-sync-external-store@1.4.0(react@19.2.1))(viem@2.45.1(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76)) + react: 19.2.1 + use-sync-external-store: 1.4.0(react@19.2.1) + viem: 2.45.1(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) optionalDependencies: - typescript: 5.9.3 + typescript: 5.9.2 transitivePeerDependencies: - '@azure/app-configuration' - '@azure/cosmos' @@ -17435,8 +19052,13 @@ snapshots: webidl-conversions@3.0.1: {} + webidl-conversions@4.0.2: {} + webidl-conversions@7.0.0: {} + webidl-conversions@8.0.1: + optional: true + whatwg-encoding@3.1.1: dependencies: iconv-lite: 0.6.3 @@ -17448,11 +19070,23 @@ snapshots: tr46: 5.1.1 webidl-conversions: 7.0.0 + whatwg-url@15.1.0: + dependencies: + tr46: 6.0.0 + webidl-conversions: 8.0.1 + optional: true + whatwg-url@5.0.0: dependencies: tr46: 0.0.3 webidl-conversions: 3.0.1 + whatwg-url@7.1.0: + dependencies: + lodash.sortby: 4.7.0 + tr46: 1.0.1 + webidl-conversions: 4.0.2 + which-boxed-primitive@1.1.1: dependencies: is-bigint: 1.1.0 @@ -17469,13 +19103,13 @@ snapshots: is-async-function: 2.1.1 is-date-object: 1.1.0 is-finalizationregistry: 1.1.1 - is-generator-function: 1.1.2 + is-generator-function: 1.1.0 is-regex: 1.2.1 is-weakref: 1.1.1 isarray: 2.0.5 which-boxed-primitive: 1.1.1 which-collection: 1.0.2 - which-typed-array: 1.1.20 + which-typed-array: 1.1.19 which-collection@1.0.2: dependencies: @@ -17486,7 +19120,7 @@ snapshots: which-module@2.0.1: {} - which-typed-array@1.1.20: + which-typed-array@1.1.19: dependencies: available-typed-arrays: 1.0.7 call-bind: 1.0.8 @@ -17513,40 +19147,52 @@ snapshots: string-width: 4.2.3 strip-ansi: 6.0.1 + wrap-ansi@7.0.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + wrap-ansi@8.1.0: + dependencies: + ansi-styles: 6.2.1 + string-width: 5.1.2 + strip-ansi: 7.1.0 + wrappy@1.0.2: {} - ws@7.5.10(bufferutil@4.1.0)(utf-8-validate@5.0.10): + ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10): optionalDependencies: - bufferutil: 4.1.0 + bufferutil: 4.0.9 utf-8-validate: 5.0.10 - ws@8.17.1(bufferutil@4.1.0)(utf-8-validate@5.0.10): + ws@8.17.1(bufferutil@4.0.9)(utf-8-validate@5.0.10): optionalDependencies: - bufferutil: 4.1.0 + bufferutil: 4.0.9 utf-8-validate: 5.0.10 - ws@8.18.0(bufferutil@4.1.0)(utf-8-validate@5.0.10): + ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10): optionalDependencies: - bufferutil: 4.1.0 + bufferutil: 4.0.9 utf-8-validate: 5.0.10 - ws@8.18.3(bufferutil@4.1.0)(utf-8-validate@5.0.10): + ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10): optionalDependencies: - bufferutil: 4.1.0 + bufferutil: 4.0.9 utf-8-validate: 5.0.10 - ws@8.19.0(bufferutil@4.1.0)(utf-8-validate@5.0.10): + ws@8.19.0(bufferutil@4.0.9)(utf-8-validate@5.0.10): optionalDependencies: - bufferutil: 4.1.0 + bufferutil: 4.0.9 utf-8-validate: 5.0.10 - x402@0.1.2(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10): + x402@0.1.2(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10): dependencies: - '@hono/node-server': 1.19.9(hono@4.11.7) - axios: 1.13.4 - express: 4.22.1 - hono: 4.11.7 - viem: 2.45.1(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76) + '@hono/node-server': 1.19.1(hono@4.10.7) + axios: 1.11.0 + express: 4.21.2 + hono: 4.10.7 + viem: 2.45.1(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) zod: 3.25.76 transitivePeerDependencies: - bufferutil @@ -17567,6 +19213,9 @@ snapshots: yallist@3.1.1: {} + yaml@2.8.1: + optional: true + yargs-parser@18.1.3: dependencies: camelcase: 5.3.1 @@ -17600,40 +19249,28 @@ snapshots: zod@3.25.76: {} - zod@4.3.6: {} - - zustand@5.0.0(@types/react@19.2.10)(react@19.2.1)(use-sync-external-store@1.4.0(react@19.2.1)): - optionalDependencies: - '@types/react': 19.2.10 - react: 19.2.1 - use-sync-external-store: 1.4.0(react@19.2.1) - - zustand@5.0.0(@types/react@19.2.10)(react@19.2.4)(use-sync-external-store@1.4.0(react@19.2.4)): - optionalDependencies: - '@types/react': 19.2.10 - react: 19.2.4 - use-sync-external-store: 1.4.0(react@19.2.4) + zod@4.1.13: {} - zustand@5.0.11(@types/react@19.2.10)(react@19.2.1)(use-sync-external-store@1.4.0(react@19.2.1)): + zustand@5.0.0(@types/react@19.1.12)(react@19.2.1)(use-sync-external-store@1.4.0(react@19.2.1)): optionalDependencies: - '@types/react': 19.2.10 + '@types/react': 19.1.12 react: 19.2.1 use-sync-external-store: 1.4.0(react@19.2.1) - zustand@5.0.11(@types/react@19.2.10)(react@19.2.4)(use-sync-external-store@1.4.0(react@19.2.4)): + zustand@5.0.0(@types/react@19.1.12)(react@19.2.3)(use-sync-external-store@1.4.0(react@19.2.3)): optionalDependencies: - '@types/react': 19.2.10 - react: 19.2.4 - use-sync-external-store: 1.4.0(react@19.2.4) + '@types/react': 19.1.12 + react: 19.2.3 + use-sync-external-store: 1.4.0(react@19.2.3) - zustand@5.0.3(@types/react@19.2.10)(react@19.2.1)(use-sync-external-store@1.4.0(react@19.2.1)): + zustand@5.0.3(@types/react@19.1.12)(react@19.2.1)(use-sync-external-store@1.4.0(react@19.2.1)): optionalDependencies: - '@types/react': 19.2.10 + '@types/react': 19.1.12 react: 19.2.1 use-sync-external-store: 1.4.0(react@19.2.1) - zustand@5.0.3(@types/react@19.2.10)(react@19.2.4)(use-sync-external-store@1.4.0(react@19.2.4)): + zustand@5.0.3(@types/react@19.1.12)(react@19.2.3)(use-sync-external-store@1.4.0(react@19.2.3)): optionalDependencies: - '@types/react': 19.2.10 - react: 19.2.4 - use-sync-external-store: 1.4.0(react@19.2.4) + '@types/react': 19.1.12 + react: 19.2.3 + use-sync-external-store: 1.4.0(react@19.2.3) diff --git a/typescript/site/app/components/BrandScroller.tsx b/typescript/site/app/components/BrandScroller.tsx new file mode 100644 index 0000000000..d75f553cff --- /dev/null +++ b/typescript/site/app/components/BrandScroller.tsx @@ -0,0 +1,53 @@ +"use client"; + +import Link from "next/link"; +import Image from "next/image"; + +const brands = [ + { name: "Stripe", logo: "/logos/stripe-mono.svg" }, + { name: "AWS", logo: "/logos/aws.-mono.svg", className: "h-8" }, + { name: "Messari", logo: "/logos/messari-mono.svg" }, + { name: "Alchemy", logo: "/logos/alchemy-mono.svg" }, + { name: "Nansen", logo: "/logos/nansen-mono.svg" }, + { name: "Vercel", logo: "/logos/vercel-mono.svg" }, + { name: "Cloudflare", logo: "/logos/cloudflare-mono.svg", className: "h-7" }, + { name: "World", logo: "/logos/world-mono.svg" }, +]; + +function BrandSet() { + return ( +
+ {brands.map((brand) => ( + {brand.name} + ))} +
+ ); +} + +export function BrandScroller() { + return ( + +

+ Adopted by +

+
+
+ {Array.from({ length: 4 }).map((_, i) => ( + + ))} +
+
+ + ); +} diff --git a/typescript/site/app/components/EcosystemCard.tsx b/typescript/site/app/components/EcosystemCard.tsx index 8c5f375442..e6f3aac7b0 100644 --- a/typescript/site/app/components/EcosystemCard.tsx +++ b/typescript/site/app/components/EcosystemCard.tsx @@ -6,17 +6,17 @@ import type { Partner } from "../ecosystem/data"; interface EcosystemCardProps { partner: Partner; - variant?: "featured" | "standard"; + variant?: "top_section" | "standard"; } export function EcosystemCard({ partner, variant = "standard" }: EcosystemCardProps) { const isExternal = partner.websiteUrl.startsWith("http"); - const isFeatured = variant === "featured"; + const isFeatured = variant === "top_section"; const tagLabel = partner.typeLabel ?? partner.category; return (
@@ -37,7 +37,7 @@ export function EcosystemCard({ partner, variant = "standard" }: EcosystemCardPr > {partner.logoUrl ? (
@@ -51,7 +51,7 @@ export function EcosystemCard({ partner, variant = "standard" }: EcosystemCardPr
) : ( -
+

+ {brands.map((brand) => ( + {brand.name} + ))} +

+ ); +} + export function StatsSection() { return (
+

Last 30 days

- - - - -
-
0
-
Last 30 days
-
+ + + + +
+ +
+ +

Adopted by

+
+
+ {Array.from({ length: 4 }).map((_, i) => ( + + ))} +
+
+
); diff --git a/typescript/site/app/components/WhatsX402Section.tsx b/typescript/site/app/components/WhatsX402Section.tsx index b566a1f3e5..7a179bf305 100644 --- a/typescript/site/app/components/WhatsX402Section.tsx +++ b/typescript/site/app/components/WhatsX402Section.tsx @@ -24,14 +24,11 @@ export function WhatsX402Section() { variants={fadeInUp} className="text-sm sm:text-base font-medium text-gray-70 max-w-[691px] leading-relaxed" > - Payments on the internet are fundamentally flawed. Credit cards are - high friction, hard to accept, have minimum payments that are far too - high, and don't fit into the programmatic nature of the internet. It's - time for an open, internet-native form of payments. A payment rail - that doesn't have high minimums plus a percentage fee. Payments that - are amazing for humans and AI agents. + Payments on the internet are fundamentally flawed. Filling out a form is a human behavor + that doesn't match the programmatic nature of the internet. It's time for an open, + internet-native form of payments. Payments that are amazing for humans and AI agents. ); -} \ No newline at end of file +} diff --git a/typescript/site/app/ecosystem/EcosystemClient.tsx b/typescript/site/app/ecosystem/EcosystemClient.tsx index 5879e33f04..a40eed318a 100644 --- a/typescript/site/app/ecosystem/EcosystemClient.tsx +++ b/typescript/site/app/ecosystem/EcosystemClient.tsx @@ -1,6 +1,6 @@ "use client"; -import { useMemo, useState } from "react"; +import { useMemo, useRef, useState, useEffect } from "react"; import Image from "next/image"; import { useRouter, useSearchParams } from "next/navigation"; import { AnimatePresence, motion } from "motion/react"; @@ -10,6 +10,93 @@ import { EcosystemCard } from "../components/EcosystemCard"; import FacilitatorCard from "./facilitator-card"; import type { Partner, CategoryInfo } from "./data"; +function SearchIcon({ className }: { className?: string }) { + return ( + + + + ); +} + +function EcosystemSearch({ partners, onQueryChange, onSelect }: { partners: Partner[]; onQueryChange: (q: string) => void; onSelect: (name: string) => void }) { + const [query, setQuery] = useState(""); + const [isOpen, setIsOpen] = useState(false); + const ref = useRef(null); + + useEffect(() => { + function handleClickOutside(e: MouseEvent) { + if (ref.current && !ref.current.contains(e.target as Node)) { + setIsOpen(false); + } + } + document.addEventListener("mousedown", handleClickOutside); + return () => document.removeEventListener("mousedown", handleClickOutside); + }, []); + + const results = useMemo(() => { + if (!query.trim()) return []; + const q = query.toLowerCase(); + return partners.filter((p) => p.name.toLowerCase().includes(q)).slice(0, 8); + }, [query, partners]); + + return ( +
+
+ + { + setQuery(e.target.value); + onQueryChange(e.target.value); + onSelect(""); + setIsOpen(true); + }} + onFocus={() => query.trim() && setIsOpen(true)} + placeholder="Search ecosystem..." + className="w-full border border-foreground bg-background pl-10 pr-4 py-2.5 text-sm font-mono placeholder:text-gray-40 focus:outline-none focus:border-accent-orange transition-colors" + /> +
+ {isOpen && results.length > 0 && ( +
+ {results.map((partner) => ( + + ))} +
+ )} + {isOpen && query.trim() && results.length === 0 && ( +
+

No results found

+
+ )} +
+ ); +} + interface EcosystemClientProps { initialPartners: Partner[]; categories: CategoryInfo[]; @@ -17,7 +104,7 @@ interface EcosystemClientProps { } type PartitionResult = { - featured: Partner[]; + topSection: Partner[]; byCategory: Record; }; @@ -65,9 +152,9 @@ function partitionPartners(partners: Partner[], categories: CategoryInfo[]): Par } } - const featured = partners.filter((partner) => partner.featured); + const topSection = partners.filter((partner) => partner.top_section); - return { featured, byCategory }; + return { topSection, byCategory }; } export default function EcosystemClient({ @@ -79,11 +166,14 @@ export default function EcosystemClient({ const router = useRouter(); const [isExpanded, setIsExpanded] = useState(true); + const [isSearching, setIsSearching] = useState(false); + const [searchQuery, setSearchQuery] = useState(""); + const [selectedPartner, setSelectedPartner] = useState(""); const activeFilter = (searchParams.get("filter") ?? initialSelectedCategory ?? "everything") || "everything"; - const { featured, byCategory } = useMemo( + const { topSection, byCategory } = useMemo( () => partitionPartners(initialPartners, categories), [initialPartners, categories], ); @@ -100,10 +190,21 @@ export default function EcosystemClient({ }); }; - const filteredPartners = + const basePartners = activeFilter === "everything" - ? initialPartners.filter((partner) => !partner.featured) - : (byCategory[activeFilter] ?? []).filter((partner) => !partner.featured); + ? initialPartners.filter((partner) => !partner.top_section) + : (byCategory[activeFilter] ?? []).filter((partner) => !partner.top_section); + + const filteredPartners = useMemo(() => { + if (selectedPartner) { + return initialPartners.filter((p) => p.name === selectedPartner); + } + if (searchQuery.trim()) { + const q = searchQuery.toLowerCase(); + return initialPartners.filter((p) => p.name.toLowerCase().includes(q)); + } + return basePartners; + }, [selectedPartner, searchQuery, basePartners, initialPartners]); return (
@@ -130,27 +231,39 @@ export default function EcosystemClient({ of partners and developers leveraging x402 technology.

+
+ +
+ { + setSearchQuery(q); + setIsSearching(q.trim().length > 0); + if (!q.trim()) setSelectedPartner(""); + }} + onSelect={(name) => setSelectedPartner(name)} + /> +
- {featured.length > 0 && ( -
-

Featured projects

+ {!isSearching && topSection.length > 0 && ( +
- {featured.slice(0, 4).map((partner) => ( + {topSection.map((partner) => ( {partner.facilitator ? ( - + ) : ( - + )} ))}
)} -
{/* Sidebar + main content */} @@ -201,6 +314,32 @@ export default function EcosystemClient({
+ {isSearching ? ( +
+

+ {selectedPartner ? selectedPartner : `Results for "${searchQuery}"`} +

+ {filteredPartners.length > 0 ? ( + + {filteredPartners.map((partner) => ( + + {partner.facilitator ? ( + + ) : ( + + )} + + ))} + + ) : ( +

No results found.

+ )} +
+ ) : ( {activeFilter === "everything" ? ( {categories.map((category) => { const partners = (byCategory[category.id] ?? []).filter( - (partner) => !partner.featured, + (partner) => !partner.top_section, ); if (!partners.length) return null; @@ -233,6 +372,7 @@ export default function EcosystemClient({ {partner.facilitator ? ( @@ -265,6 +405,7 @@ export default function EcosystemClient({ {partner.facilitator ? ( @@ -281,7 +422,7 @@ export default function EcosystemClient({ )} - + )}
diff --git a/typescript/site/app/ecosystem/data.ts b/typescript/site/app/ecosystem/data.ts index f3c3ede1ff..e37b40061f 100644 --- a/typescript/site/app/ecosystem/data.ts +++ b/typescript/site/app/ecosystem/data.ts @@ -21,7 +21,7 @@ export interface Partner { websiteUrl: string; category: string; // Main category name as defined in categories array typeLabel?: string; - featured?: boolean; + top_section?: boolean; // Additional fields like a slug for directory name can be added if needed for linking or lookup slug?: string; // Facilitator-specific data (only present for facilitators) diff --git a/typescript/site/app/ecosystem/facilitator-card.tsx b/typescript/site/app/ecosystem/facilitator-card.tsx index 4017b27700..f816c9fbcc 100644 --- a/typescript/site/app/ecosystem/facilitator-card.tsx +++ b/typescript/site/app/ecosystem/facilitator-card.tsx @@ -7,7 +7,7 @@ import type { Partner } from './data'; interface FacilitatorCardProps { partner: Partner; - variant?: 'standard' | 'featured'; + variant?: 'standard' | 'top_section'; } export default function FacilitatorCard({ partner, variant = 'standard' }: FacilitatorCardProps) { @@ -18,7 +18,7 @@ export default function FacilitatorCard({ partner, variant = 'standard' }: Facil } const { facilitator } = partner; - const isFeatured = variant === 'featured'; + const isFeatured = variant === 'top_section'; const tagLabel = partner.typeLabel ?? partner.category; const handleOpen = () => setIsModalOpen(true); const handleKeyDown = (event: KeyboardEvent) => { @@ -38,7 +38,7 @@ export default function FacilitatorCard({ partner, variant = 'standard' }: Facil tabIndex={0} onClick={handleOpen} onKeyDown={handleKeyDown} - className={`group relative w-full flex flex-col border border-foreground bg-background cursor-pointer outline-none transition-all duration-200 hover:bg-gray-10 hover:border-accent-orange hover:shadow-lg focus-visible:ring-2 focus-visible:ring-foreground focus-visible:ring-offset-2 focus-visible:ring-offset-background ${ + className={`group relative w-full h-full flex flex-col border border-foreground bg-background cursor-pointer outline-none transition-all duration-200 hover:bg-gray-10 hover:border-accent-orange hover:shadow-lg focus-visible:ring-2 focus-visible:ring-foreground focus-visible:ring-offset-2 focus-visible:ring-offset-background ${ isFeatured ? 'px-3 pt-4 pb-5' : 'px-4 pt-5 pb-6' }`} > @@ -51,7 +51,7 @@ export default function FacilitatorCard({ partner, variant = 'standard' }: Facil > {partner.logoUrl ? (
@@ -65,7 +65,7 @@ export default function FacilitatorCard({ partner, variant = 'standard' }: Facil
) : ( -
+

- {/* Backdrop */}
setIsModalOpen(false)} /> - {/* Modal Content */} -
+
+