diff --git a/.dockerignore b/.dockerignore index b2b8939..6abcbd2 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,4 +1,14 @@ .venv +.env +.env.* +!.env.example +*.env +*.key +*.pem +*.p12 +*.pfx +id_rsa +id_ed25519 .mypy_cache .pytest_cache .ruff_cache @@ -16,3 +26,6 @@ __pycache__ *.pyc .pytype video/ +apps/ops-console/.next +apps/ops-console/node_modules +apps/ops-console/.npm-cache diff --git a/.env.example b/.env.example index 7bc8c67..f718249 100644 --- a/.env.example +++ b/.env.example @@ -18,10 +18,10 @@ OMNICLAW_ENV=development OMNICLAW_LOG_LEVEL=INFO # ============================================================================= -# Buyer / Seller Signing Key +# Buyer Signing Key # ============================================================================= -# Private key used by the SDK or Financial Policy Engine for allowed actions. +# Private key used by the SDK or Financial Policy Engine for allowed buyer actions. # In production, use restricted infrastructure and secret management. OMNICLAW_PRIVATE_KEY=0x... @@ -77,52 +77,14 @@ OMNICLAW_NANOPAYMENTS_TOPUP_THRESHOLD=1.00 OMNICLAW_NANOPAYMENTS_TOPUP_AMOUNT=10.00 OMNICLAW_NANOPAYMENTS_MICRO_THRESHOLD=1.00 -# Optional Gateway contract override for seller GatewayWalletBatched metadata. +# Optional Gateway contract override for GatewayWalletBatched metadata. CIRCLE_GATEWAY_CONTRACT= -# ============================================================================= -# Vendor / Enterprise SDK Seller -# ============================================================================= - -# Vendor wallet that receives seller payments. -SELLER_ADDRESS=0x... - -# Default Circle seller path: -# payment=client.sell("$0.25", seller_address=os.environ["SELLER_ADDRESS"]) - -# Thirdweb managed x402 seller path: -# payment=client.sell("$0.25", seller_address=os.environ["SELLER_ADDRESS"], facilitator="thirdweb") -THIRDWEB_SECRET_KEY= -THIRDWEB_SERVER_WALLET_ADDRESS= -THIRDWEB_X402_NETWORK=base-sepolia - -# OmniClaw self-hosted exact seller path: -# payment=client.sell("$0.25", seller_address=os.environ["SELLER_ADDRESS"], facilitator="omniclaw") -OMNICLAW_X402_SELF_HOSTED_FACILITATOR_URL=http://127.0.0.1:4022 -OMNICLAW_X402_EXACT_NETWORK_PROFILE=ARC-TESTNET - -# ============================================================================= -# Self-Hosted Exact Facilitator -# ============================================================================= - -# Run with: -# omniclaw facilitator exact --network-profile ARC-TESTNET --port 4022 -OMNICLAW_X402_FACILITATOR_PRIVATE_KEY=0x... -OMNICLAW_X402_FACILITATOR_NETWORK_PROFILE=ARC-TESTNET -OMNICLAW_X402_FACILITATOR_RPC_URL=https://rpc.testnet.arc.network -OMNICLAW_X402_FACILITATOR_NETWORKS=eip155:5042002 -OMNICLAW_X402_FACILITATOR_HOST=0.0.0.0 -OMNICLAW_X402_FACILITATOR_PORT=4022 - # ============================================================================= # Production Hardening # ============================================================================= -# Required for production seller replay protection. -OMNICLAW_SELLER_NONCE_REDIS_URL=redis://localhost:6379/1 -OMNICLAW_SELLER_REQUIRE_DISTRIBUTED_NONCE=true - -# Strict settlement should remain enabled for production payment gates. +# Strict settlement should remain enabled for production buyer x402 payments. OMNICLAW_STRICT_SETTLEMENT=true # Webhook verification and deduplication. diff --git a/.gitignore b/.gitignore index 4d434a3..21ad0be 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ .env.* !.env.example .env.local +*.env *.pem *.key secrets.json diff --git a/CHANGELOG.md b/CHANGELOG.md index 8c7e48e..c134a52 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,19 +6,23 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), ## [Unreleased] +### Changed +- Removed the obsolete secondary console entrypoint and pointed both console scripts at the unified `omniclaw.cli` buyer/core CLI. +- Updated public docs and release verification so removed setup/server/doctor commands are no longer advertised. + ## [0.0.6] - 2026-04-14 ### Added -- Added the owner/operator `omniclaw facilitator exact` command for self-hosted x402 exact settlement. -- Added public facilitator documentation covering Circle Gateway, external facilitators, and OmniClaw self-hosted exact settlement. +- Added x402 exact-settlement buyer support. +- Added public documentation for Circle Gateway and x402 exact payment flows. - Added Arc Testnet exact-settlement support documentation using CAIP-2 `eip155:5042002`. -- Added B2B SDK examples for vendor APIs, machine-to-vendor payments, external exact facilitators, and self-hosted exact settlement. -- Added a public `.env.example` with role-based configuration for agents, vendors, facilitators, Circle Gateway, Thirdweb, and production hardening. +- Added B2B SDK examples for machine-to-machine payments. +- Added a public `.env.example` with role-based configuration for agents, Circle Gateway, Thirdweb, and production hardening. ### Changed -- Moved the owner/operator CLI entrypoint to `omniclaw.admin_cli:main` so it no longer conflicts with the `omniclaw.cli` agent CLI package. -- Updated release artifact verification for the new admin CLI module and package layout. -- Reworked public README and docs around agent buyer, SDK buyer, vendor SDK seller, Financial Policy Engine, and facilitator deployment paths. +- Moved the owner/operator CLI entrypoint away from the `omniclaw.cli` agent CLI package. +- Updated release artifact verification for the package layout. +- Reworked public README and docs around agent buyer, SDK buyer, and Financial Policy Engine paths. - Updated the OmniClaw CLI skill to require idempotency keys for x402 URL payments. - Bumped runtime and shipped CLI skill metadata to `0.0.6`. @@ -50,8 +54,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), ## [0.0.3] - 2026-03-25 ### Added -- Multi-facilitator support: Circle Gateway, Coinbase CDP, OrderN, RBX, Thirdweb -- Seller SDK: Full seller-side SDK for accepting x402 payments +- Payment rail support: Circle Gateway, Coinbase CDP, OrderN, RBX, Thirdweb +- x402 buyer support for paid HTTP resources - Trust Gate: ERC-8004 based identity and reputation verification - Payment Intents: 2-phase commit with fund reservation - Enhanced buyer SDK with smart payment routing diff --git a/Dockerfile b/Dockerfile index 273df57..f6a1fd3 100644 --- a/Dockerfile +++ b/Dockerfile @@ -6,7 +6,7 @@ WORKDIR /app COPY pyproject.toml README.md ./ COPY src/ src/ -RUN pip install --no-cache-dir ".[ops]" +RUN pip install --no-cache-dir . # ─── Runtime stage ──────────────────────────────────────────────────── FROM python:3.12-slim @@ -18,9 +18,8 @@ COPY --from=builder /usr/local/lib/python3.12/site-packages /usr/local/lib/pytho COPY --from=builder /usr/local/bin/ /usr/local/bin/ COPY --from=builder /app/ /app/ -EXPOSE 8088 +EXPOSE 8080 -ENV OMNICLAW_OPS_PORT=8088 ENV OMNICLAW_REDIS_URL=redis://redis:6379/0 -CMD ["uvicorn", "omniclaw.ops.api:app", "--host", "0.0.0.0", "--port", "8088"] +CMD ["uvicorn", "omniclaw.agent.server:app", "--host", "0.0.0.0", "--port", "8080"] diff --git a/README.md b/README.md index 62bd0c7..a02df6b 100644 --- a/README.md +++ b/README.md @@ -1,144 +1,34 @@ # OmniClaw -*One install. Every payment rail. Policy enforced by default.* +Policy-controlled payment infrastructure for agent buyers. -[![CI](https://github.com/omnuron/omniclaw/actions/workflows/ci.yml/badge.svg)](https://github.com/omnuron/omniclaw/actions/workflows/ci.yml) -[![PyPI](https://img.shields.io/pypi/v/omniclaw.svg)](https://pypi.org/project/omniclaw/) -[![Python](https://img.shields.io/pypi/pyversions/omniclaw.svg)](https://pypi.org/project/omniclaw/) -[![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE) +OmniClaw core is focused on one job: letting agents and applications pay through controlled, auditable rails without giving software unrestricted wallet authority. -`lablab.ai Agentic Commerce Hackathon on ARC (1st place) · Top 11 finalist, C.R.I.S.P Agentic AI Ideation Challenge` +## Product Boundary -OmniClaw is the control layer for agent money. - -It lets teams ship autonomous payments without giving software unrestricted wallet authority. - -Wallets give keys. Facilitators settle payments. OmniClaw governs whether an agent is allowed to pay, routes the right rail, and gives vendors and infrastructure teams a usable product around that control model. - -Buyers get policy. Vendors get paid endpoints. Infrastructure teams get self-hosted exact settlement on Arc, Base, and Ethereum. - -## Proven In Public - -- Arc Testnet exact settlement proof: `https://testnet.arcscan.app/tx/0xd40dc800a54bee4ff80da4709e65cfd3d0346eb1995ebc34fba433a6306b9219` -- 1st place at `lablab.ai Agentic Commerce Hackathon on ARC` -- Top 11 finalist in the `C.R.I.S.P Agentic AI Ideation Challenge` -- Shipped buyer CLI, buyer SDK, seller SDK, ERC-8004 trust checks, Circle Gateway nanopayments, and a self-hosted `x402 exact` facilitator runtime - -If you want the fastest proof, run: - -```bash -bash scripts/start_arc_marketplace_showcase_docker.sh -``` - -Then open `http://127.0.0.1:8020` and complete the browser buyer flow. - -## Why Teams Use OmniClaw - -| If you are... | OmniClaw gives you... | -| --- | --- | -| Building an agent buyer | Policy-controlled payments without giving the agent raw wallet authority | -| Monetizing an API or service | Paid routes through `client.sell(...)` and x402-compatible seller flows | -| Running payment infrastructure | Route selection across Circle Gateway and standard `x402 exact`, plus self-hosted exact settlement when hosted coverage stops short | - -## Pick A Path In 60 Seconds - -One product, three adoption paths: autonomous buyers, paid vendors, and self-hosted settlement infrastructure. - -| I want to... | Run this first | Success looks like... | Jump to... | -| --- | --- | --- | --- | -| Try the full Arc flow now | `bash scripts/start_arc_marketplace_showcase_docker.sh` | browser kiosk + buyer flow + self-hosted exact settlement | [Arc Marketplace Showcase](examples/arc-marketplace-showcase/README.md) | -| Let an agent buy from paid APIs | `omniclaw server` + `omniclaw-cli pay` | agent pays through policy, not a raw key | [Buyer: Agent CLI](#buyer-agent-cli) | -| Pay programmatically from Python | `OmniClaw().pay(...)` | Python service buys from paid APIs | [Buyer: Python SDK](#buyer-python-sdk) | -| Monetize a vendor API | `OmniClaw().sell(...)` | FastAPI route returns `402` until paid | [Seller: Vendor / Enterprise SDK](#seller-vendor--enterprise-sdk) | -| Run your own exact facilitator | `omniclaw facilitator exact` | self-hosted `verify` / `settle` on supported EVM networks | [Self-Hosted Exact Facilitator](#self-hosted-exact-facilitator) | - -## The Problem - -AI agents can browse, reason, call APIs, and execute workflows autonomously. - -The dangerous part is money. - -Give an agent a private key and a single hallucination, prompt injection, or bad tool call can drain a treasury in seconds. Existing solutions usually hand the agent a wallet and hope for the best. - -OmniClaw solves this by separating authority from execution. The owner defines policy. The agent executes within it. Every payment is checked before funds move. - -In one sentence: OmniClaw is the economic execution and control layer for agentic systems. - -## Prerequisites - -| Path | What you need | -| --- | --- | -| Arc showcase | Docker | -| Buyer CLI | Python 3.11+, funded EVM key, RPC URL | -| Buyer SDK | Python 3.11+, RPC URL | -| Seller SDK | Python 3.11+, optional Circle credentials for Gateway flows | -| Self-hosted facilitator | Python 3.11+, funded EVM key, RPC URL | - -## Install And Run - -```bash -pip install omniclaw -``` - -Package development: - -```bash -uv add omniclaw -``` - -## Wallets vs Facilitators vs OmniClaw - -| Layer | What it does | What it does not do | +| Product | Directory | Owns | | --- | --- | --- | -| Wallets | Hold keys and sign | Decide whether an agent should be allowed to spend | -| Facilitators | Verify and settle supported payment payloads | Govern financial authority before money moves | -| OmniClaw | Enforces policy before payment, routes the right rail, supports buyer and seller flows, and can self-host exact settlement when needed | Replace every settlement provider or blockchain rail | +| OmniClaw core | `src/omniclaw` | buyer SDK, policy engine, wallet/payment routing, x402 buyer execution, Gateway buyer readiness | -OmniClaw is a policy-controlled payment layer for agents and vendors. It lets agents pay through approved rails, lets vendors monetize routes, and lets infrastructure teams run or self-host settlement when hosted facilitators stop short. +Core should not include recipient-side paid endpoint hosting or settlement service code. -Core shipped surfaces: +## Core Capabilities -- Financial Policy Engine for payment authority, limits, approvals, trust checks, and execution control -- `omniclaw-cli` for agent-side buyer execution -- Python SDK for buyer payments and seller monetization -- Circle Gateway nanopayments for gasless microflows -- Standard `x402 exact` buyer flow with direct-wallet signing -- Self-hosted `x402 exact` facilitator for Arc Testnet, Base Sepolia, Ethereum Sepolia, Base mainnet, and Ethereum mainnet +- Financial Policy Engine for budgets, approvals, trust checks, and execution control +- Python buyer SDK via `OmniClaw().pay(...)` +- Agent buyer CLI via `omniclaw-cli pay`, `inspect-x402`, and `can-pay` +- Circle Gateway buyer funding/readiness helpers +- Standard x402 buyer flow for paying external paid endpoints +- Ledger, idempotency, simulation, and payment-intent controls -> OmniClaw governs financial authority. Facilitators settle supported x402 payment payloads. These are separate concerns. +## Core Quickstart -## Credential Model - -OmniClaw has two different key surfaces: - -- `OMNICLAW_PRIVATE_KEY` is the EOA key used for direct `x402 exact` settlement and Circle Gateway nanopayment signing. -- `ENTITY_SECRET` is Circle's developer-controlled wallet encryption secret. - -If your Circle account or API key already has an Entity Secret, set it directly. Circle allows one active Entity Secret per account and API key. OmniClaw only auto-generates and registers a new one when no existing secret is provided or found in its managed local credential store. +Install: ```bash -export CIRCLE_API_KEY="..." -export ENTITY_SECRET="your_existing_64_char_hex_entity_secret" -export OMNICLAW_PRIVATE_KEY="0x..." -``` - -For a non-interactive local setup: - -```bash -omniclaw setup --api-key "$CIRCLE_API_KEY" --entity-secret "$ENTITY_SECRET" +pip install omniclaw ``` -## Default Product Shapes - -- Agent buyer: run the Financial Policy Engine, then pay with `omniclaw-cli` -- Application buyer: integrate `client.pay(...)` in Python -- Vendor seller: monetize routes with `client.sell(...)` -- Infrastructure operator: run `omniclaw facilitator exact` for self-hosted exact settlement - -## Buyer: Agent CLI - -Use this when an autonomous agent or script should pay through the Financial Policy Engine. - Start the policy engine: ```bash @@ -148,229 +38,27 @@ export OMNICLAW_AGENT_POLICY_PATH="./policy.json" export OMNICLAW_NETWORK="BASE-SEPOLIA" export OMNICLAW_RPC_URL="https://sepolia.base.org" -omniclaw server --port 8080 +docker compose up --build omniclaw-agent ``` -Configure the agent runtime: +Configure the buyer CLI: ```bash export OMNICLAW_SERVER_URL="http://localhost:8080" export OMNICLAW_TOKEN="agent-token" ``` -Pay a protected x402 URL: +Inspect and pay an x402 endpoint: ```bash -omniclaw-cli can-pay --recipient https://seller.example.com/compute -omniclaw-cli inspect-x402 --recipient https://seller.example.com/compute -omniclaw-cli pay --recipient https://seller.example.com/compute --idempotency-key job-123 -``` - -Pay a direct address: - -```bash -omniclaw-cli pay \ - --recipient 0xRecipientAddress \ - --amount 5.00 \ - --purpose "service payment" \ - --idempotency-key job-123 -``` - -The same CLI surface can also inspect balances, ledger entries, and paid endpoint requirements without exposing private keys to the agent. - -## Buyer: Python SDK - -Use this when a Python service should pay programmatically. - -```python -from omniclaw import Network, OmniClaw - -client = OmniClaw(network=Network.BASE_SEPOLIA) - -result = await client.pay( - wallet_id="wallet-id", - recipient="https://seller.example.com/compute", - amount="1.00", - purpose="compute job", - idempotency_key="job-123", - check_trust=True, -) - -print(result.status, result.blockchain_tx or result.transaction_id) +omniclaw-cli inspect-x402 --recipient https://paid.example.com/compute +omniclaw-cli pay --recipient https://paid.example.com/compute --idempotency-key job-123 ``` -For x402 URLs, `amount` acts as the maximum spend allowed for that request. The seller's x402 requirements define the exact amount to settle. - -When trust is enabled, OmniClaw can evaluate ERC-8004 identity and reputation signals before the payment is allowed to proceed. - -## Seller: Vendor / Enterprise SDK - -Use this when a vendor, enterprise, or application team wants to monetize API routes. This is the default seller path for real products. - -```python -from fastapi import FastAPI -from omniclaw import OmniClaw - -app = FastAPI() -client = OmniClaw() - -@app.get("/premium-data") -async def premium_data( - payment=client.sell("$0.25", seller_address="0xYourSellerWallet") -): - return { - "data": "premium content", - "paid_by": payment.payer, - "amount": payment.amount, - } -``` - -The route returns `402 Payment Required` until the buyer submits a valid x402 payment. After verification and settlement, the handler executes and returns the paid response. - -`omniclaw-cli serve` remains the agent-facing seller/runtime surface. Use it when an agent needs to expose a paid endpoint for other agents or automation. Use the SDK seller path when a vendor or enterprise team is embedding paid routes directly into an application. - -## Self-Hosted Exact Facilitator - -Hosted facilitators do not support every chain, every flow, or every developer workflow. Some require managed accounts, signup gates, or hosted onboarding before you can even run a demo. - -OmniClaw ships a self-hosted `x402 exact` facilitator so you can run standard `verify` and `settle` yourself. - -What it does: - -- runs a standard `x402 exact` facilitator runtime -- verifies signed payment payloads -- settles payments on supported EVM profiles -- removes dependency on hosted onboarding for unsupported flows - -Supported out of the box: - -- Arc Testnet -- Base Sepolia -- Ethereum Sepolia -- Base mainnet -- Ethereum mainnet - -Start it with one command: - -```bash -omniclaw facilitator exact --network-profile ARC-TESTNET --port 4022 -``` - -Or use the helper script: - -```bash -bash scripts/start_arc_exact_facilitator.sh -``` - -Arc Testnet notes: - -- Arc Testnet uses native USDC for gas -- the exact settlement path calls Arc USDC `transferWithAuthorization` -- the result is visible on ArcScan like any other on-chain proof - -Latest public Arc proof transaction: - -```text -https://testnet.arcscan.app/tx/0xd40dc800a54bee4ff80da4709e65cfd3d0346eb1995ebc34fba433a6306b9219 -``` - -Full Arc marketplace showcase: - -```bash -bash scripts/start_arc_marketplace_showcase_docker.sh -``` - -That launcher starts the vendor kiosk, buyer policy engine, and self-hosted facilitator together so the entire buyer-to-seller flow can be demonstrated from one browser page. - -## Examples - -| Example | Demonstrates | -| --- | --- | -| [B2B SDK Integration](examples/b2b-sdk-integration/README.md) | Enterprise buyer and seller SDK integration with multiple facilitators | -| [Machine to Machine](examples/machine-to-machine/README.md) | One machine service paying another | -| [Machine to Vendor](examples/machine-to-vendor/README.md) | Agent buyer paying a vendor-owned API | -| [Vendor Integration](examples/vendor-integration/README.md) | Vendor-side paid API integration | -| [Business Compute](examples/business-compute/README.md) | Payment-gated compute service | -| [Local Economy](examples/local-economy/README.md) | Local buyer and seller economy with Docker | -| [External x402 Facilitator](examples/external-x402-facilitator/README.md) | x402.org Base Sepolia validation | -| [Thirdweb HTTP Facilitator](examples/thirdweb-http-facilitator/README.md) | Thirdweb HTTP API validation | -| [Arc Marketplace Showcase](examples/arc-marketplace-showcase/README.md) | Visual vendor kiosk with Arc Testnet x402 exact settlement | - -## Architecture - -```mermaid -flowchart TD - A[Agent / CLI / App] --> B[Financial Policy Engine] - B --> B1[Guards] - B --> B2[ERC-8004 Trust Gate] - B --> B3[Ledger] - B --> B4[Fund Locks] - B --> B5[Payment Router] - B5 --> C1[Circle Gateway Nanopayments] - B5 --> C2[x402 Exact] - B5 --> C3[Self-Hosted Facilitator Runtime] - C1 --> D[Blockchain / Settlement Network] - C2 --> D - C3 --> D - D --> E[Arc / Base / Ethereum / Other Supported Rails] -``` - -## Execution Pipeline - -Every `client.pay()` call runs through: - -1. Argument validation -2. Trust evaluation when enabled -3. Ledger entry creation -4. Guard reservation -5. Wallet fund lock acquisition -6. Balance verification after reservations -7. Router and adapter selection -8. Guard commit or release -9. Ledger status update -10. Wallet lock release - -This is why OmniClaw is not just a thin wallet wrapper. The payment call is a controlled execution pipeline, not a raw transfer helper. - -## Documentation - -| Start Here | Use Case | -| --- | --- | -| [Documentation Index](docs/README.md) | Complete docs map | -| [Architecture and Features](docs/FEATURES.md) | Financial Policy Engine design and subsystem responsibilities | -| [Developer Guide](docs/developer-guide.md) | Python SDK buyer and seller integration | -| [Agent Getting Started](docs/agent-getting-started.md) | Agent CLI setup and usage | -| [CLI Reference](docs/cli-reference.md) | Generated `omniclaw-cli` reference | -| [Operator CLI](docs/operator-cli.md) | `omniclaw server`, setup, policy, and facilitator commands | -| [Policy Reference](docs/POLICY_REFERENCE.md) | Policy file structure and controls | -| [Facilitators](docs/facilitators.md) | x402 facilitator model and deployment paths | -| [Production Readiness](docs/production-readiness.md) | Proof status and release checklist | -| [API Reference](docs/API_REFERENCE.md) | Python SDK and API details | -| [ERC-8004 Trust Notes](docs/erc_804_spec.md) | Trust-layer notes and registry framing | - -## Star History - -[![Star History Chart](https://api.star-history.com/chart?repos=omnuron/omniclaw&type=date)](https://star-history.com/#omnuron/omniclaw&Date) - ## Development -```bash -uv sync --extra dev -uv run pytest -``` - -Release verification: +Run core tests: ```bash -./scripts/release_verify.sh +uv run pytest ``` - -## Security - -OmniClaw is designed around separation of authority. Agents do not need unrestricted wallet access. Production deployments should still use restricted keys, policy limits, confirmation thresholds, hardened secrets, audited infrastructure, and real operational review. - -Report vulnerabilities through [SECURITY.md](SECURITY.md). - -## License - -MIT. See [LICENSE](LICENSE). diff --git a/docker-compose.yml b/docker-compose.yml index 5335f79..63b579d 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,5 +1,3 @@ -version: "3.9" - services: redis: image: redis:7-alpine @@ -14,16 +12,14 @@ services: timeout: 3s retries: 5 - omniclaw-ops-api: + omniclaw-agent: build: . - command: uvicorn omniclaw.ops.api:app --host 0.0.0.0 --port 8088 --reload + command: uvicorn omniclaw.agent.server:app --host 0.0.0.0 --port 8080 --reload environment: - OMNICLAW_REDIS_URL=redis://redis:6379/0 - - OMNICLAW_OPS_API_KEY=${OMNICLAW_OPS_API_KEY:-} - - OMNICLAW_OPS_CORS_ORIGINS=http://localhost:5173,http://localhost:3000 - OMNICLAW_EVENTS_ENABLED=true ports: - - "8088:8088" + - "8080:8080" depends_on: redis: condition: service_healthy diff --git a/docs/API_REFERENCE.md b/docs/API_REFERENCE.md index 420095e..117aa50 100644 --- a/docs/API_REFERENCE.md +++ b/docs/API_REFERENCE.md @@ -1,423 +1,47 @@ -# OmniClaw API Reference +# API Reference -This is the public API reference for the Financial Policy Engine. It focuses on the API surface users are expected to call directly. +OmniClaw core exposes buyer-side payment and policy APIs. -## Top-Level Imports - -Common imports from [src/omniclaw/__init__.py](../src/omniclaw/__init__.py): +## Python SDK ```python -from omniclaw import ( - OmniClaw, - Network, - FeeLevel, - PaymentMethod, - PaymentStatus, - PaymentIntentStatus, - quick_setup, - validate_entity_secret, -) -``` - -## Environment Contract - -Required: - -```env -CIRCLE_API_KEY=... -OMNICLAW_NETWORK=ETH-SEPOLIA -# Direct-key mode (recommended for agents / nanopayments) -OMNICLAW_PRIVATE_KEY=0x... -``` - -If you are using Circle developer-controlled wallets directly, provide: -``` -ENTITY_SECRET=... -``` +from omniclaw import OmniClaw -Optional: - -```env -OMNICLAW_DEFAULT_WALLET=wallet-id -OMNICLAW_LOG_LEVEL=INFO -OMNICLAW_ENV=development -OMNICLAW_RPC_URL=https://... -OMNICLAW_STORAGE_BACKEND=memory -OMNICLAW_REDIS_URL=redis://localhost:6379 -OMNICLAW_DAILY_BUDGET=100.00 -OMNICLAW_HOURLY_BUDGET=20.00 -OMNICLAW_TX_LIMIT=50.00 -OMNICLAW_RATE_LIMIT_PER_MIN=5 -OMNICLAW_WHITELISTED_RECIPIENTS=0xabc,0xdef -OMNICLAW_CONFIRM_ALWAYS=false -OMNICLAW_CONFIRM_THRESHOLD=500.00 +client = OmniClaw() ``` -## Setup Utilities - -Defined in [onboarding.py](../src/omniclaw/onboarding.py). - -### `quick_setup(api_key, env_path=".env", network="ARC-TESTNET", entity_secret=None)` - -One-time onboarding helper that writes a local env file and syncs OmniClaw's managed credential store. - -If `entity_secret` is provided, OmniClaw treats it as the existing Circle Entity Secret for that API key and does not try to register a replacement. Circle only allows one active Entity Secret per account/API key. - -If `entity_secret` is omitted, OmniClaw generates and registers a new Entity Secret, then saves the recovery file in the secure config directory. - -The helper currently defaults to `ARC-TESTNET`, so pass an explicit network if you want the generated env file to target `ETH-SEPOLIA`, `BASE-SEPOLIA`, or another supported network. - -Public examples in the docs usually show `ETH-SEPOLIA` or `BASE-SEPOLIA`, but the SDK still supports other configured networks such as `ARC-TESTNET` when selected explicitly. - -### `generate_entity_secret()` (optional) - -Returns a 64-character hex entity secret (manual setup only). - -### `validate_entity_secret(entity_secret)` - -Validates that an existing Circle Entity Secret is a 64-character hex value before it is written to env or managed config. - -### `register_entity_secret(api_key, entity_secret, recovery_dir=None)` (optional) - -Registers a new entity secret with Circle and downloads the recovery file (manual setup only). Do not call this for an API key/account that already has an Entity Secret; set `ENTITY_SECRET` directly instead. - -### `create_env_file(api_key, entity_secret, env_path=".env", network="ARC-TESTNET", overwrite=False)` (optional) - -Writes the basic OmniClaw env file (manual setup only). - -### `verify_setup()` - -Returns a readiness summary for the local environment. - -### `doctor(api_key=None, entity_secret=None)` - -Returns a diagnostic summary covering: - -- Circle SDK availability -- API key presence -- environment entity secret presence -- managed config entity secret presence -- recovery-file presence - -### `print_doctor_status(api_key=None, entity_secret=None)` - -Prints the same diagnostic state in a human-readable format. - -## `OmniClaw` - -Defined in [client.py](../src/omniclaw/client.py). - -### Constructor - -```python -OmniClaw( - circle_api_key: str | None = None, - entity_secret: str | None = None, - network: Network = Network.ARC_TESTNET, - log_level: int | str | None = None, - trust_policy: TrustPolicy | str | None = None, - rpc_url: str | None = None, -) -``` - -### Properties - -- `config` -- `wallet` -- `guards` -- `trust` -- `intent` -- `intents` -- `ledger` -- `webhooks` -- `nanopayment_adapter` — NanopaymentAdapter for buyer-side nanopayments - -### Wallet Methods - -```python -await client.create_wallet( - blockchain=None, - wallet_set_id=None, - account_type=AccountType.EOA, - name=None, -) - -await client.create_agent_wallet( - agent_name, - blockchain=None, - apply_default_guards=True, -) - -await client.create_wallet_set(name=None) -await client.list_wallets(wallet_set_id=None) -await client.list_wallet_sets() -await client.get_wallet(wallet_id) -await client.get_wallet_set(wallet_set_id) -await client.get_balance(wallet_id) -await client.list_transactions(wallet_id=None, blockchain=None) -``` - -### Payment Methods +### `pay` ```python await client.pay( - wallet_id, - recipient, - amount, - destination_chain=None, - wallet_set_id=None, - purpose=None, - idempotency_key=None, - fee_level=FeeLevel.MEDIUM, - strategy=PaymentStrategy.RETRY_THEN_FAIL, - skip_guards=False, - check_trust=None, - consume_intent_id=None, - metadata=None, - wait_for_completion=False, - timeout_seconds=None, - **kwargs, -) -``` - -For x402 URL payments, pass the HTTP request context through `kwargs`: - -```python -await client.pay( - wallet_id=wallet.id, - recipient="https://seller.example.com/premium", - amount="0.25", - method="POST", - request_body='{"job":"prime-count","size":70000}', - request_headers={"x-request-id": "job-123"}, -) -``` - -Buyer routing is requirement-driven: - -- seller advertises `GatewayWalletBatched` and the buyer is Gateway-ready -> OmniClaw can use the Gateway nanopayment path -- seller advertises standard x402 `exact` -> OmniClaw uses the upstream x402 SDK path -- seller advertises both and the buyer is not Gateway-ready -> OmniClaw uses `exact` - -When the seller is exact-only, OmniClaw routes directly to `exact` instead of attempting Gateway first. - -```python -await client.simulate( - wallet_id, - recipient, - amount, - wallet_set_id=None, - check_trust=None, - skip_guards=False, - **kwargs, -) -``` - -Other helpers: - -```python -client.can_pay(recipient) -client.detect_method(recipient) -await client.batch_pay(requests, concurrency=5) -await client.sync_transaction(entry_id) -``` - -### Payment Intent Methods - -```python -await client.create_payment_intent( - wallet_id, - recipient, - amount, - purpose=None, - expires_in=None, - idempotency_key=None, - skip_guards=False, - check_trust=None, - **kwargs, + wallet_id="wallet-id", + recipient="https://paid.example.com/compute", + amount=None, + idempotency_key="job-123", ) - -await client.confirm_payment_intent(intent_id) -await client.get_payment_intent(intent_id) -await client.cancel_payment_intent(intent_id, reason=None) -``` - -### Guard Helper Methods - -```python -await client.add_budget_guard(wallet_id, daily_limit=None, hourly_limit=None, total_limit=None, name="budget") -await client.add_budget_guard_for_set(wallet_set_id, daily_limit=None, hourly_limit=None, total_limit=None, name="budget") -await client.add_single_tx_guard(wallet_id, max_amount=None, min_amount=None, name="single_tx") -await client.add_recipient_guard(wallet_id, mode="whitelist", addresses=None, patterns=None, domains=None, name="recipient") -await client.add_recipient_guard_for_set(wallet_set_id, mode="whitelist", addresses=None, patterns=None, domains=None, name="recipient") -await client.add_rate_limit_guard(wallet_id, max_per_minute=None, max_per_hour=None, max_per_day=None, name="rate_limit") -await client.add_rate_limit_guard_for_set(wallet_set_id, max_per_minute=None, max_per_hour=None, max_per_day=None, name="rate_limit") -await client.add_confirm_guard(wallet_id, threshold=None, always_confirm=False, name="confirm") -await client.add_confirm_guard_for_set(wallet_set_id, threshold=None, always_confirm=False, name="confirm") -await client.list_guards(wallet_id) -await client.list_guards_for_set(wallet_set_id) ``` -### Nanopayments Methods - -Nanopayments use EIP-3009 for gas-free USDC transfers on Circle Gateway. They work alongside regular payments — micro-transactions route through the gateway while larger payments use standard transfers. - -#### Seller: Receiving Nanopayments - -```python -# Get the GatewayMiddleware for protecting endpoints -await client.gateway() # -> GatewayMiddleware - -# Dependency factory for marking paid FastAPI routes -client.sell(price: str) # -> FastAPI Depends() object - -# Get current payment info inside a @sell() decorated route -client.current_payment() # -> PaymentInfo(payer, amount, network, transaction) -``` - -Example (FastAPI seller): - -```python -@app.get("/premium") -async def premium(payment=omniclaw.sell("$0.001")): - payment_info = omniclaw.current_payment() - return {"data": "paid content", "paid_by": payment_info.payer} -``` - -#### Buyer: Sending Nanopayments - -```python -# Execute a nanopayment to a seller's address -await client.pay( - wallet_id=wallet.id, - recipient="0xSellerAddress", - amount="0.001", # Small amount - uses gateway nanopayment -) -# Routes to Circle Gateway nanopayment if amount < nanopayments_micro_threshold -``` +Use `amount=None` for x402 URLs where the endpoint publishes the amount in `PAYMENT-REQUIRED`. -#### Gateway Wallet Management +### Gateway Buyer Helpers ```python -# Get gateway balance +await client.deposit_to_gateway(wallet_id="wallet-id", amount_usdc="10.00") +await client.withdraw_from_gateway(wallet_id="wallet-id", amount_usdc="5.00") await client.get_gateway_balance(wallet_id="wallet-id") -# -> GatewayBalance(total, available, formatted_total, formatted_available) - -# Deposit USDC to gateway wallet (enables receiving nanopayments) -await client.deposit_to_gateway( - wallet_id="wallet-id", - amount_usdc="10.00", -) - -# Withdraw USDC from gateway wallet -await client.withdraw_from_gateway( - wallet_id="wallet-id", - amount_usdc="5.00", - destination_chain=None, # Optional: withdraw to another chain - recipient="0xDestination", # Optional: specific recipient -) - -# Configure auto-topup for gateway balance -client.configure_nanopayments( - auto_topup_enabled=True, - auto_topup_threshold="1.00", - auto_topup_amount="10.00", - wallet_manager=gateway_wallet_manager, -) +await client.get_gateway_onchain_balance(wallet_id="wallet-id") ``` -#### Agent Creation - -```python -# Create an agent wallet -agent_wallet = await client.create_agent( - agent_name="data-agent", -) -``` +### Policy Engine Endpoints -### Nanopayments Environment Variables +The local policy engine exposes buyer payment, wallet, ledger, policy, and x402 inspection endpoints under: -```env -OMNICLAW_NANOPAYMENTS_ENABLED=true -OMNICLAW_NANOPAYMENTS_ENVIRONMENT=testnet # or "mainnet" -OMNICLAW_NANOPAYMENTS_MICRO_THRESHOLD=1.00 -OMNICLAW_NANOPAYMENTS_AUTO_TOPUP=true -OMNICLAW_NANOPAYMENTS_TOPUP_THRESHOLD=1.00 -OMNICLAW_NANOPAYMENTS_TOPUP_AMOUNT=10.00 -# Nanopayments network is derived from OMNICLAW_NETWORK (EVM chain) +```text +/api/v1 ``` -## `WalletService` - -Accessible through `client.wallet`. - -Use it when you want direct wallet operations instead of the higher-level client helpers. +Start it with: -Primary methods: - -```python -create_wallet_set(name) -list_wallet_sets() -get_wallet_set(wallet_set_id) -create_wallet(wallet_set_id, blockchain=None, account_type=AccountType.EOA) -create_wallets(wallet_set_id, count, blockchain=None, account_type=AccountType.EOA) -setup_agent_wallet(agent_name, blockchain=None) -get_wallet(wallet_id) -list_wallets(wallet_set_id=None, blockchain=None) -list_transactions(wallet_id=None, blockchain=None) -get_balances(wallet_id) -get_usdc_balance(wallet_id) -get_usdc_balance_amount(wallet_id) -transfer( - wallet_id, - destination_address, - amount, - fee_level=FeeLevel.MEDIUM, - idempotency_key=None, - check_balance=True, - wait_for_completion=False, - timeout_seconds=None, -) +```bash +docker compose up --build omniclaw-agent ``` - -## `WebhookParser` - -Accessible through `client.webhooks`. - -Primary methods: - -```python -WebhookParser(verification_key=None) -verify_signature(payload, headers) -handle(payload, headers) -``` - -When verification is enabled, pass raw payload data and headers so signature verification happens before JSON parsing. - -## Important Runtime Rules - -- `pay()` and `simulate()` are async. -- Wallet creation helpers on `OmniClaw` are async. -- Wallet-service methods are mixed: some are sync, some are async; prefer `OmniClaw` unless you need lower-level access. -- Explicit trust checking requires a real `OMNICLAW_RPC_URL`. -- Redis configuration uses `OMNICLAW_REDIS_URL` only. -- `OmniClaw` now syncs the active entity secret into the managed config store when possible. -- Managed config lives under the platform-specific OmniClaw config directory, such as `~/.config/omniclaw/` on Linux. - -## Error Categories - -Important exported exceptions: - -- `ConfigurationError` -- `WalletError` -- `PaymentError` -- `GuardError` -- `ProtocolError` -- `InsufficientBalanceError` -- `NetworkError` -- `X402Error` -- `ValidationError` -- `NanopaymentNotInitializedError` -- `InsufficientBalanceError` -- `SettlementError` -- `NoDefaultKeyError` diff --git a/docs/FEATURES.md b/docs/FEATURES.md index a1e266b..4cb70d1 100644 --- a/docs/FEATURES.md +++ b/docs/FEATURES.md @@ -1,270 +1,25 @@ -# OmniClaw Architecture and Features +# Core Features -This document explains how the Financial Policy Engine is structured and what each subsystem is responsible for. +OmniClaw core is the buyer-side policy and payment-control layer. -## System Overview +## Buyer Payment Control -OmniClaw is centered on the Financial Policy Engine, which wires together: +- Wallet-scoped policy enforcement +- Budgets, recipient rules, approval gates, and trust checks +- Idempotent payment execution +- Ledger and payment-intent tracking +- Simulation and readiness checks before money moves -- configuration loading -- wallet management -- storage -- guards -- reservations and fund locking -- payment routing -- ledger persistence -- payment intents -- webhook verification -- optional trust evaluation +## x402 Buyer Support -## Main Components +- Inspect x402 `PAYMENT-REQUIRED` responses +- Select supported x402 routes +- Pay standard `exact` x402 endpoints +- Use Circle Gateway `GatewayWalletBatched` when the buyer is funded and the route is advertised -### `OmniClaw` +## Circle Gateway Buyer Operations -The top-level client in [client.py](../src/omniclaw/client.py). It exposes the public async Financial Policy Engine surface: - -- wallet creation and lookup -- payment execution and simulation -- intent creation, confirmation, and cancellation -- guard management helpers -- ledger access -- trust access - -### Wallet Service - -The wallet layer in [wallet/service.py](../src/omniclaw/wallet/service.py) wraps Circle wallet operations: - -- wallet set creation -- wallet creation -- wallet lookup and listing -- balance lookup -- transaction lookup -- direct transfers - -Circle client initialization is lazy, so local tests and non-network flows do not require immediate provider calls at client construction time. - -### Payment Router - -The router in [payment/router.py](../src/omniclaw/payment/router.py) chooses an adapter based on recipient shape and destination chain. - -Current routing: - -- URL -> inspect seller requirements, then choose `NanopaymentProtocolAdapter` for `GatewayWalletBatched` only when the buyer is Gateway-ready; otherwise use `X402Adapter` for standard `exact` x402 when the seller advertises it -- address + amount below micro-threshold -> `NanopaymentProtocolAdapter` (Gateway) -- address -> `TransferAdapter` -- `destination_chain` set -> `GatewayAdapter` - -### Exact Facilitator Layer - -OmniClaw is facilitator-agnostic. It can pay seller endpoints backed by external x402 facilitators, Circle Gateway, or an optional self-hosted OmniClaw exact facilitator. - -The self-hosted exact facilitator layer exists for standard x402 settlement when teams need local proof, custom network control, or a fallback while validating an external provider. - -Current exact-settlement profiles include: - -- Base Sepolia -- Ethereum Sepolia -- Base mainnet -- Ethereum mainnet -- Arc Testnet - -Arc Testnet uses CAIP-2 `eip155:5042002` and the official USDC ERC-20 interface `0x3600000000000000000000000000000000000000`. - -The facilitator layer is intentionally separate from the Financial Policy Engine. The policy engine decides whether an agent is allowed to pay. The facilitator verifies and settles a valid x402 payload on the selected network. - -Supported deployment modes: - -- managed external x402 facilitator, including Thirdweb-backed seller endpoints -- Circle Gateway `GatewayWalletBatched` for gasless nanopayment settlement -- self-hosted OmniClaw exact facilitator, started with `omniclaw facilitator exact` - -Thirdweb is one supported external integration path for teams that want broad EVM facilitator coverage and gas-sponsored settlement. OmniClaw adds buyer-side policy, route selection, SDK and CLI execution surfaces, and payment visibility on top. - -The self-hosted exact facilitator exists so teams can support networks and deployments before they are available through a hosted provider. Arc Testnet is the clearest example: it remains standard x402 `exact` settlement, with OmniClaw providing the network profile, asset metadata, RPC, and facilitator runtime. - -See [facilitators.md](facilitators.md) for deployment details. - -### Guards - -The guard system in [guards/](../src/omniclaw/guards) is the primary spend-control layer. - -Supported guard types: - -- `BudgetGuard` -- `RateLimitGuard` -- `SingleTxGuard` -- `RecipientGuard` -- `ConfirmGuard` - -Guard checks are integrated with reservation and commit/release behavior so failed payments do not permanently consume policy limits. - -### Reservations and Fund Locks - -OmniClaw separates two concerns: - -- reservations hold spend capacity for intents -- fund locks serialize wallet execution to reduce double-spend races - -Relevant modules: - -- [intents/reservation.py](../src/omniclaw/intents/reservation.py) -- [ledger/lock.py](../src/omniclaw/ledger/lock.py) - -### Ledger - -The ledger in [ledger/](../src/omniclaw/ledger) tracks payment records and status transitions. - -Typical use cases: - -- payment observability -- transaction lookup -- reconciliation -- operational debugging - -### Payment Intents - -Payment intents provide an authorize/confirm flow: - -1. simulate and validate -2. reserve funds -3. wait for confirmation or review -4. execute or cancel - -Relevant modules: - -- [intents/service.py](../src/omniclaw/intents/service.py) -- [intents/intent_facade.py](../src/omniclaw/intents/intent_facade.py) - -### Trust Gate - -Trust evaluation lives in [trust/](../src/omniclaw/trust). It is optional, but when enabled it can approve, hold, or block a payment using ERC-8004-related identity and reputation signals. - -Current runtime rules: - -- trust checks are optional by default -- explicit trust checks require a real `OMNICLAW_RPC_URL` -- simulation and payment execution follow the same trust gating rules - -### Nanopayments (EIP-3009) - -Nanopayments enable gas-free USDC transfers via Circle's Gateway nanopayments protocol, built on EIP-3009. They are designed for micro-transactions where gas costs would make regular transfers impractical. - -**Gateway CAIP-2 derivation:** Gateway nanopayment CAIP-2 is derived from `OMNICLAW_NETWORK` via `network_to_caip2`. Only EVM networks are supported — non-EVM networks will raise a clear configuration error. - -#### Architecture - -The nanopayments stack is organized under [protocols/nanopayments/](../src/omniclaw/protocols/nanopayments/): - -- `signing.py` — EIP-3009 signature creation (`EIP3009Signer`) and verification -- `types.py` — `PaymentRequirementsKind`, `PaymentPayload`, `SettleResponse`, `PaymentInfo`, `ResourceInfo`, `SupportedKind`, `GatewayBalance` types -- `client.py` — `NanopaymentClient` wrapping Circle's Gateway API (settle, verify, get_supported, check_balance) -- `keys.py` — key encryption utilities (legacy; not used in direct-key mode) -- `middleware.py` — `GatewayMiddleware` (seller-side x402 gate, `@agent.sell()` equivalent) -- `adapter.py` — `NanopaymentAdapter` (buyer-side payment execution) -- `wallet.py` — `GatewayWalletManager` (on-chain deposit/withdraw via `depositWithAuthorization`) -- `exceptions.py` — `InsufficientBalanceError`, `SettlementError`, etc. - -#### Payment Flow - -1. **Buyer** creates a `PaymentRequirementsKind` (network, amount, seller address) -2. **Buyer** signs with `EIP3009Signer` → `PaymentPayload` (off-chain, no gas) -3. **Buyer** base64-encodes payload → sends as `PAYMENT-SIGNATURE` header -4. **Seller** gateway receives header → calls `gateway.handle(request_headers, price)` -5. **Seller** gateway calls `client.settle(payload, requirements)` → Circle settles on-chain -6. **Result** is `SettleResponse(success, transaction, payer, error_reason)` - -The on-chain settlement is batched — multiple nanopayments settle in a single transaction, so gas costs are amortized across many payments. - -#### Buyer vs Seller - -- **Buyer**: Uses `NanopaymentAdapter` and `NanopaymentClient` to create and send payments via `client.pay()` -- **Seller**: Uses `GatewayMiddleware` and `client.sell()` to protect FastAPI endpoints - -#### Key Management - -Nanopayment signing uses a single direct private key configured via `OMNICLAW_PRIVATE_KEY`. - -#### OmniClaw Integration - -`OmniClaw` wires nanopayments into the Financial Policy Engine surface: - -- `client.nanopayment_adapter` → `NanopaymentAdapter` for buyer payments -- `client.gateway()` → `GatewayMiddleware` for seller endpoints -- `client.sell(price)` → FastAPI `Depends()` for `@agent.sell()` -- `client.current_payment()` → `PaymentInfo` within decorated routes -- `client.get_gateway_balance()` → gateway wallet balance -- `client.configure_nanopayments()` → auto-topup settings - -Relevant modules: - -- [protocols/nanopayments/middleware.py](../src/omniclaw/protocols/nanopayments/middleware.py) -- [protocols/nanopayments/adapter.py](../src/omniclaw/protocols/nanopayments/adapter.py) -- [protocols/nanopayments/keys.py](../src/omniclaw/protocols/nanopayments/keys.py) -- [protocols/nanopayments/client.py](../src/omniclaw/protocols/nanopayments/client.py) - -### Storage - -Storage backends live in [storage/](../src/omniclaw/storage). - -Supported backends: - -- in-memory storage for tests and simple local runs -- Redis for shared, concurrent, or production-like execution - -Canonical Redis env: - -```env -OMNICLAW_STORAGE_BACKEND=redis -OMNICLAW_REDIS_URL=redis://localhost:6379 -``` - -## Environment Model - -Core environment variables: - -```env -CIRCLE_API_KEY=... -OMNICLAW_NETWORK=ETH-SEPOLIA -``` - -Optional: - -```env -OMNICLAW_STORAGE_BACKEND=memory -OMNICLAW_REDIS_URL=redis://localhost:6379 -OMNICLAW_LOG_LEVEL=INFO -OMNICLAW_RPC_URL=https://... -OMNICLAW_DEFAULT_WALLET=wallet-id -OMNICLAW_DAILY_BUDGET=100.00 -OMNICLAW_HOURLY_BUDGET=20.00 -OMNICLAW_TX_LIMIT=50.00 -OMNICLAW_RATE_LIMIT_PER_MIN=5 -OMNICLAW_WHITELISTED_RECIPIENTS=0xabc,0xdef -OMNICLAW_CONFIRM_ALWAYS=false -OMNICLAW_CONFIRM_THRESHOLD=500.00 -``` - -Use the network that matches your target environment. Public examples in this repo mainly use `ETH-SEPOLIA` or `BASE-SEPOLIA`, while other supported networks can still be selected explicitly. - -## Execution Sequence - -For a typical `pay()` call, the Financial Policy Engine does the following: - -1. validate arguments -2. optionally evaluate trust -3. create a ledger entry -4. reserve guards -5. acquire wallet fund lock -6. verify available balance after reservations -7. pass through the router and chosen adapter -8. commit or release guard reservations -9. update ledger status -10. release wallet lock - -## Launch-Focused Recommendations - -- Use Redis for any multi-agent or concurrent environment. -- Treat `simulate()` as part of your pre-execution workflow for higher-risk payments. -- Use payment intents for any approval or review-dependent flow. -- Configure `OMNICLAW_RPC_URL` only when you actually want trust evaluation available. -- Keep environment names and network selection explicit in deployment configs. +- Gateway balance checks +- On-chain Gateway balance checks +- Deposit and withdraw helpers +- Buyer readiness for nanopayment routes diff --git a/docs/OmniClaw_Whitepaper_v2.md b/docs/OmniClaw_Whitepaper_v2.md index 9a3e716..bc2fd27 100644 --- a/docs/OmniClaw_Whitepaper_v2.md +++ b/docs/OmniClaw_Whitepaper_v2.md @@ -133,7 +133,7 @@ The ledger stores durable intent state and attempt state. It is the source of tr ### 5.6 Execution Service -The execution service is the only component permitted to trigger settlement. It uses provider integrations, facilitators, or rail-specific adapters, but only for already-approved intents. +The execution service is the only component permitted to trigger payment execution. It uses provider integrations or rail-specific adapters, but only for already-approved intents. ### 5.7 Audit Layer @@ -234,8 +234,8 @@ Not all recipients create the same risk. The task-derived architecture, which is consistent with OmniClaw’s broader control model, makes this explicit: -- Human-operated vendor - Standard vendor allowlist, ordinary approval thresholds, and contractual accountability. +- Human-operated service + Standard recipient allowlist, ordinary approval thresholds, and contractual accountability. - Internal service Potentially looser thresholds, but only when workload identity, service registry entry, destination account, and transaction class match internal control records. @@ -319,7 +319,7 @@ The credibility of OmniClaw as a research system comes from the fact that these - trust-layer types and verdicts in `src/omniclaw/identity/types.py` - guard, reservation, and fund-lock documentation in `docs/FEATURES.md` - compliance framing in `docs/compliance-architecture.md` -- product surfaces across buyer, seller, facilitator, and CLI workflows +- product surfaces across buyer SDK, policy engine, and CLI workflows There is also substantial test evidence: diff --git a/docs/POLICY_REFERENCE.md b/docs/POLICY_REFERENCE.md index 3bb5b00..9a47391 100644 --- a/docs/POLICY_REFERENCE.md +++ b/docs/POLICY_REFERENCE.md @@ -260,20 +260,20 @@ Agent can pay everyone EXCEPT these addresses/domains. } ``` -### Seller Agent (Receives Payments) +### Receive-Only Agent ```json { "version": "2.0", "tokens": { - "seller-agent": { + "receive-only-agent": { "wallet_alias": "primary", "active": true, - "label": "Seller Agent" + "label": "Receive-Only Agent" } }, "wallets": { "primary": { - "name": "Seller Wallet", + "name": "Receive-Only Wallet", "limits": { "daily_max": "0", "per_tx_max": "0" diff --git a/docs/PRODUCTION_HARDENING.md b/docs/PRODUCTION_HARDENING.md index ef90e0d..2f197b1 100644 --- a/docs/PRODUCTION_HARDENING.md +++ b/docs/PRODUCTION_HARDENING.md @@ -9,14 +9,13 @@ Set these for production (`OMNICLAW_ENV=production` or `mainnet`): ```env OMNICLAW_ENV=production OMNICLAW_STRICT_SETTLEMENT=true -OMNICLAW_SELLER_NONCE_REDIS_URL=redis://localhost:6379/1 OMNICLAW_WEBHOOK_VERIFICATION_KEY=your_public_key OMNICLAW_WEBHOOK_DEDUP_DB_PATH=/var/lib/omniclaw/webhook_dedup.sqlite3 ``` Startup fails fast if these are missing or if strict settlement is disabled. -For non-production package usage, `OMNICLAW_STRICT_SETTLEMENT` defaults to `false` so compatible x402 resources can still unlock even when a seller omits or delays settlement response metadata. Production deployments must opt into strict settlement explicitly. +For non-production package usage, `OMNICLAW_STRICT_SETTLEMENT` defaults to `false` so compatible x402 resources can still unlock even when an endpoint omits or delays settlement response metadata. Production deployments must opt into strict settlement explicitly. ## Webhook Security Model @@ -36,38 +35,25 @@ OMNICLAW_WEBHOOK_MAX_FUTURE_SKEW_SECONDS=300 OMNICLAW_WEBHOOK_DEDUP_ENABLED=true ``` -## Nonce Replay Protection - -Production seller flows must use distributed nonce storage: - -- `OMNICLAW_SELLER_NONCE_REDIS_URL` points to Redis. -- in-memory nonce replay protection is not accepted in production mode. - ## Settlement Semantics - `OMNICLAW_STRICT_SETTLEMENT=true` ensures success reflects irreversible settlement states. - Do not disable strict settlement in production. -## Facilitator Strategy +## x402 Buyer Strategy -OmniClaw is facilitator-agnostic. Production deployments should choose the settlement provider that fits the seller and network: +OmniClaw core is buyer-side infrastructure. Production buyer deployments should inspect what the paid endpoint advertises and route only through a buyer-supported payment method: -- Thirdweb-backed x402 facilitator for managed gas-sponsored exact settlement across broad EVM coverage - Circle Gateway `GatewayWalletBatched` for gasless batched nanopayments -- external standard x402 facilitator where the seller already uses one -- self-hosted OmniClaw exact facilitator when local proof, custom network support, or enterprise self-hosting is required +- standard x402 `exact` where the endpoint advertises compatible requirements -Use a self-hosted facilitator when it fits the network and operational model. Use a managed facilitator when it already cleanly supports the target flow. - -Before production traffic, validate the exact seller path with: +Before production traffic, validate the exact paid endpoint path with: ```bash -omniclaw-cli inspect-x402 --recipient https://seller.example.com/compute -omniclaw-cli pay --recipient https://seller.example.com/compute --idempotency-key production-canary-001 +omniclaw-cli inspect-x402 --recipient https://paid.example.com/compute +omniclaw-cli pay --recipient https://paid.example.com/compute --idempotency-key production-canary-001 ``` -For Thirdweb validation, use `examples/thirdweb-http-facilitator/README.md`. - ## Canary and SLA Use the canary script to validate end-to-end payment lifecycle before/after deploys: @@ -89,10 +75,10 @@ Exit behavior: ## Rollout Checklist 1. Apply required production env vars. -2. Run `omniclaw doctor`. +2. Confirm the policy engine health endpoint and CLI configuration. 3. Run canary in target environment. -4. Confirm `inspect-x402` selects the expected seller scheme and network. -5. Confirm settlement appears in the selected facilitator dashboard or explorer. +4. Confirm `inspect-x402` selects the expected payment scheme and network. +5. Confirm settlement appears in the expected explorer or provider record. 6. Deploy with staged traffic. 7. Monitor: - settlement latency diff --git a/docs/README.md b/docs/README.md index cb37301..2afac47 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,98 +1,15 @@ -# OmniClaw Documentation +# OmniClaw Core Docs -OmniClaw is a policy-controlled payment layer for agents, applications, and machine services. +This docs tree is for OmniClaw core: buyer-side agent payment infrastructure, policy controls, wallet routing, and x402 payment execution. -Use this index to choose the right integration path. +## Core Guides -## Choose Your Path - -| Role | Start Here | What You Build | -| --- | --- | --- | -| Agent buyer | [Agent Getting Started](agent-getting-started.md) | An agent that pays through `omniclaw-cli` | -| Python buyer | [Developer Guide](developer-guide.md) | A backend service that pays programmatically | -| Vendor / enterprise seller | [Developer Guide](developer-guide.md) | A FastAPI service with paid endpoints through the SDK | -| Infrastructure team | [Operator CLI](operator-cli.md) | Financial Policy Engine, policies, and facilitators | -| Maintainer | [Production Readiness](production-readiness.md) | Proof checklist and ship status | - -## Buyer Guides - -| Guide | Use Case | -| --- | --- | -| [Agent Getting Started](agent-getting-started.md) | Configure an agent with `omniclaw-cli` | -| [CLI Reference](cli-reference.md) | Full generated `omniclaw-cli` command reference | -| [Developer Guide](developer-guide.md) | Pay with the Python SDK | -| [API Reference](API_REFERENCE.md) | SDK methods, request shapes, and payment APIs | - -## Seller Guides - -| Guide | Use Case | -| --- | --- | -| [Developer Guide](developer-guide.md) | Add production paid FastAPI routes with `client.sell()` | -| [B2B SDK Integration](../examples/b2b-sdk-integration/README.md) | Enterprise SDK deployment with Circle, Thirdweb, or self-hosted exact | -| [Vendor Integration](../examples/vendor-integration/README.md) | Production-style vendor API integration | -| [Business Compute](../examples/business-compute/README.md) | Payment-gated compute service | -| [CLI Reference](cli-reference.md) | Agent-facing paid service flow with `omniclaw-cli serve` | - -## Machine Payment Examples - -| Example | Demonstrates | -| --- | --- | -| [B2B SDK Integration](../examples/b2b-sdk-integration/README.md) | Enterprise buyer/seller SDK integration | -| [Machine to Machine](../examples/machine-to-machine/README.md) | One automated service paying another | -| [Machine to Vendor](../examples/machine-to-vendor/README.md) | Agent buyer paying a vendor-owned API | -| [Local Economy](../examples/local-economy/README.md) | Local buyer/seller stack with Docker | -| [External x402 Facilitator](../examples/external-x402-facilitator/README.md) | x402.org exact settlement on Base Sepolia | -| [Thirdweb HTTP Facilitator](../examples/thirdweb-http-facilitator/README.md) | Thirdweb HTTP facilitator integration | -| [Arc Marketplace Showcase](../examples/arc-marketplace-showcase/README.md) | Visual vendor kiosk with Arc Testnet x402 exact settlement | - -## Arc Testnet Quickstart - -Run the full Arc marketplace showcase with Docker-reachable service IPs: - -```bash -bash scripts/start_arc_marketplace_showcase_docker.sh -``` - -The buyer key must hold Arc Testnet USDC for the selected paid product, and the seller/facilitator key must hold Arc Testnet gas. The launcher prints balances, product URLs, the exact OmniClaw config, and a lower-cost `$0.10` proof endpoint when the buyer wallet is not funded for the `$0.25` product. - -The showcase UI also has a built-in mini buyer agent, so the full demo can run directly from the browser. The kiosk backend proxies inspect/pay actions into the buyer Financial Policy Engine while keeping the policy token server-side. - -Defaults: - -| Service | URL | -| --- | --- | -| Browser UI | `http://127.0.0.1:8020` | -| Vendor kiosk | `http://172.18.0.51:8020` | -| Buyer policy engine | `http://172.18.0.52:8080` | -| Exact facilitator | `http://172.18.0.50:4022` | - -For setup details and ArcLens submission notes, see [Arc Marketplace Showcase](../examples/arc-marketplace-showcase/README.md). - -## Operator and Production Docs - -| Document | Covers | -| --- | --- | -| [Operator CLI](operator-cli.md) | `omniclaw server`, `omniclaw setup`, `omniclaw facilitator exact` | -| [Policy Reference](POLICY_REFERENCE.md) | Tokens, wallets, budgets, recipient rules, confirmations | -| [Facilitators](facilitators.md) | Circle Gateway, x402.org, Thirdweb, self-hosted exact | -| [Production Readiness](production-readiness.md) | Live proof status and release checklist | -| [Production Hardening](PRODUCTION_HARDENING.md) | Deployment controls, Redis, nonce, security settings | - -## Architecture and Reference - -| Document | Covers | -| --- | --- | -| [Architecture and Features](FEATURES.md) | Core design, route selection, guards, settlement paths | -| [Architecture Diagram](architecture_overview.svg) | System overview | -| [Compliance Architecture](compliance-architecture.md) | Compliance and control framing | -| [CCTP Usage](CCTP_USAGE.md) | Circle CCTP notes | -| [ERC-804 Spec](erc_804_spec.md) | Trust-related specification notes | - -## Project Files - -| File | Purpose | +| Guide | Covers | | --- | --- | -| [CHANGELOG](../CHANGELOG.md) | Release history | -| [CONTRIBUTING](../CONTRIBUTING.md) | Development and PR workflow | -| [SECURITY](../SECURITY.md) | Security reporting | -| [ROADMAP](../ROADMAP.md) | Built status and planned work | +| [Agent Getting Started](agent-getting-started.md) | Configure an agent buyer with `omniclaw-cli` | +| [CLI Reference](cli-reference.md) | Buyer CLI commands | +| [Developer Guide](developer-guide.md) | Programmatic buyer payments | +| [API Reference](API_REFERENCE.md) | SDK and API shapes | +| [Policy Reference](POLICY_REFERENCE.md) | Wallets, budgets, confirmations, recipient rules | +| [Production Readiness](production-readiness.md) | Buyer/payment release checklist | +| [Production Hardening](PRODUCTION_HARDENING.md) | Core deployment controls | diff --git a/docs/RESEARCH_EVIDENCE_MATRIX.md b/docs/RESEARCH_EVIDENCE_MATRIX.md index 6f57197..c061e7a 100644 --- a/docs/RESEARCH_EVIDENCE_MATRIX.md +++ b/docs/RESEARCH_EVIDENCE_MATRIX.md @@ -113,7 +113,7 @@ What to strengthen: ## Claim 7: Counterparty-Type-Aware Policy Claim: -- Policy should differ for human-operated vendors, internal services, and autonomous-agent counterparties. +- Policy should differ for human-operated services, internal services, and autonomous-agent counterparties. Code / Docs: - task-derived architecture @@ -175,14 +175,14 @@ Evidence Type: What to strengthen: - add an explicit audit-event schema doc -## Claim 11: Seller-Side Replay Resistance +## Claim 11: x402 Replay Resistance Claim: -- Seller or facilitator-side consumed-proof handling can reduce replay acceptance. +- Consumed-proof handling can reduce x402 replay acceptance. Code / Docs: - whitepaper v1 discussion -- seller-side nonce handling in `src/omniclaw/seller/seller.py` +- x402 replay handling design notes Evidence Type: - implementation hook diff --git a/docs/RESEARCH_THESIS_AND_OUTLINE.md b/docs/RESEARCH_THESIS_AND_OUTLINE.md index d8c27a8..77d4a6e 100644 --- a/docs/RESEARCH_THESIS_AND_OUTLINE.md +++ b/docs/RESEARCH_THESIS_AND_OUTLINE.md @@ -101,7 +101,7 @@ Why this matters: - Payment-Intent Ledger: durable source of truth for intent state and attempt state - Execution Service: performs settlement using approved, bound authorizations only - Settlement Provider / Rail: external payment or settlement mechanism -- Counterparty: vendor, internal service, or autonomous agent receiving payment +- Counterparty: external service, internal service, or autonomous agent receiving payment - Audit Layer: append-only record of decisions and state transitions ### Trust Boundaries @@ -293,7 +293,7 @@ Claims to calibrate carefully: ### 2. Background -- payment rails and facilitators +- payment rails and adapters - wallet execution versus authorization - autonomous counterparties and trust signals @@ -335,7 +335,7 @@ Claims to calibrate carefully: - artifact overview - policy engine - ledger and audit components -- facilitator integrations +- payment rail integrations ### 9. Evaluation diff --git a/docs/agent-getting-started.md b/docs/agent-getting-started.md index 7cb68ab..ef9398c 100644 --- a/docs/agent-getting-started.md +++ b/docs/agent-getting-started.md @@ -1,278 +1,35 @@ -# OmniClaw Agent Getting Started +# Agent Getting Started -This guide walks you through setting up an OmniClaw agent for policy-controlled payments. +This guide is for agent buyers. -OmniClaw is the **Economic Execution and Control Layer for Agentic Systems**. -In that system: - -- the owner runs the **Financial Policy Engine** -- the agent uses `omniclaw-cli` as the **zero-trust execution layer** -- buyers pay with `omniclaw-cli pay` -- agents can expose paid services for other agents or automation with `omniclaw-cli serve` - -For vendor, SaaS, or enterprise APIs embedded directly in an application, use the Python SDK seller middleware instead of `omniclaw-cli serve`. See the [Developer Guide](developer-guide.md). - ---- - -## Prerequisites - -- Python 3.10+ -- Circle API key -- Circle Entity Secret if your Circle account/API key already has one -- USDC on the target network (testnet or mainnet) -- A private key for your agent - ---- - -## Step 1: Create Policy.json - -Create a `policy.json` file that defines your agent: - -```json -{ - "version": "2.0", - "tokens": { - "my-agent-token": { - "wallet_alias": "primary", - "active": true, - "label": "My Agent" - } - }, - "wallets": { - "primary": { - "name": "Primary Wallet", - "limits": { - "daily_max": "100.00", - "per_tx_max": "50.00" - }, - "recipients": { - "mode": "allow_all" - } - } - } -} -``` - -On startup, the server will auto-generate and persist `wallet_id` and `address` -inside each wallet entry if they are missing. - -You can copy the default from `examples/default-policy.json` and edit it. - -The server validates policy.json on startup and will refuse to boot if it is invalid. - ---- - -## Step 2: Set Environment Variables +## Start Core ```bash -# Required -export OMNICLAW_PRIVATE_KEY="0x..." # Your agent's private key -export OMNICLAW_AGENT_TOKEN="my-agent-token" # Must match policy.json token key -export OMNICLAW_AGENT_POLICY_PATH="/path/to/policy.json" -export CIRCLE_API_KEY="your-circle-api-key" - -# Required when your Circle account/API key already has an Entity Secret. -# If omitted, OmniClaw only auto-generates one when no existing local secret is found. -export ENTITY_SECRET="your-existing-64-char-hex-entity-secret" - -# Network (testnet or mainnet) -export OMNICLAW_NETWORK="ETH-SEPOLIA" # or ETH-MAINNET for production - -# Set production for mainnet usage -export OMNICLAW_ENV="production" # optional - for mainnet +export OMNICLAW_PRIVATE_KEY="0x..." +export OMNICLAW_AGENT_TOKEN="agent-token" +export OMNICLAW_AGENT_POLICY_PATH="./policy.json" +export OMNICLAW_NETWORK="BASE-SEPOLIA" +export OMNICLAW_RPC_URL="https://sepolia.base.org" -# RPC for on-chain operations -export OMNICLAW_RPC_URL="https://..." -export OMNICLAW_OWNER_TOKEN="your-owner-token" # Required for approvals -export OMNICLAW_POLICY_RELOAD_INTERVAL="5" # Hot reload interval (seconds) +docker compose up --build omniclaw-agent ``` ---- - -## Step 3: Start the Financial Policy Engine - -```bash -omniclaw server --port 8080 -``` - -The Financial Policy Engine runs at `http://localhost:8080`. - ---- - -## Step 4: Configure the CLI - -For agents, use environment variables (no interactive setup required): +## Configure The Buyer CLI ```bash export OMNICLAW_SERVER_URL="http://localhost:8080" -export OMNICLAW_TOKEN="my-agent-token" -``` - -Optional: persist config locally for dev workflows: - -```bash -omniclaw-cli configure --server-url http://localhost:8080 --token my-agent-token --wallet primary -``` - -CLI output is agent-first (JSON, no banner). For human-friendly output set: - -```bash -export OMNICLAW_CLI_HUMAN=1 -``` - ---- - -## Step 5: Use the CLI - -### Check Your Address -```bash -omniclaw-cli address -``` - -### Check Balance -```bash -omniclaw-cli balance - -# Or detailed view -omniclaw-cli balance_detail +export OMNICLAW_TOKEN="agent-token" ``` -### Deposit USDC to Gateway -```bash -omniclaw-cli deposit --amount 10 -``` -This moves USDC from your EOA to the Circle Gateway contract. It is required for `GatewayWalletBatched` nanopayments. It is not required for standard x402 `exact` payments that spend from the buyer signer directly. +## Inspect A Paid URL -### Withdraw to Circle Wallet ```bash -omniclaw-cli withdraw --amount 5 +omniclaw-cli can-pay --recipient https://paid.example.com/compute +omniclaw-cli inspect-x402 --recipient https://paid.example.com/compute ``` -This moves USDC from Gateway to your Circle Developer Wallet. -### Buyer Flow For x402 Services - -For a new paid URL, use this order: +## Pay ```bash -omniclaw-cli can-pay --recipient https://seller.example.com/premium -omniclaw-cli inspect-x402 --recipient https://seller.example.com/premium -omniclaw-cli pay --recipient https://seller.example.com/premium --idempotency-key job-123 +omniclaw-cli pay --recipient https://paid.example.com/compute --idempotency-key job-123 ``` - -What this tells you: - -- `can-pay` confirms policy allow or deny -- `inspect-x402` shows whether the seller is paywalled, what schemes it advertises, and whether OmniClaw will use `gateway_balance` or `direct_wallet` -- `pay` executes through the single `/api/v1/pay` buyer route - -For x402 URLs, OmniClaw chooses the route from the seller's advertised requirements: - -- `GatewayWalletBatched` when the seller advertises Circle Gateway nanopayments and the buyer is actually Gateway-ready -- `exact` when the seller advertises a standard x402 payment flow - -If the seller advertises both and the buyer has no Gateway balance, OmniClaw uses `exact`. - -If the seller is exact-only, OmniClaw routes directly to the x402 exact path. - ---- - -## Confirmations (High-Value Policy Thresholds) - -If a policy requires confirmation, `/pay` will return: - -- `requires_confirmation: true` -- `confirmation_id: ` - -Approve with the owner token: - -```bash -omniclaw-cli configure --owner-token YOUR_OWNER_TOKEN -omniclaw-cli confirmations approve --id -``` - -Then retry the payment with the same `confirmation_id` in metadata: - -```json -{ - "recipient": "0xRecipient", - "amount": "50.00", - "metadata": { - "confirmation_id": "" - } -} -``` - ---- - -## Agent-to-Agent Selling (Local Data) - -If an agent wants to temporarily sell access to a local Python script or data file to another agent, they can use the CLI to spin up a fast payment gate: - -```bash -omniclaw-cli serve \ - --price 0.01 \ - --endpoint /api/data \ - --exec "python safe_readonly_service.py" \ - --port 8000 -``` - -This opens `http://localhost:8000/api/data` that requires a USDC payment to execute the approved command and return its output. - -`serve` binds to all interfaces and `--exec` runs a host command. Use it only when the owner explicitly wants an agent-run seller endpoint, and prefer an isolated container or private development network. - -> **Web developer or vendor:** If the paid route lives inside your application, use the Python SDK inside your FastAPI application instead of `omniclaw-cli serve`. Use `serve` when the seller surface itself is agent-run. See the [Developer Guide](developer-guide.md). - ---- - -## Quick Reference - -| Command | Purpose | -|---------|---------| -| `omniclaw-cli address` | Get your wallet address | -| `omniclaw-cli balance` | Check balance | -| `omniclaw-cli deposit --amount X` | Deposit to Gateway | -| `omniclaw-cli withdraw --amount X` | Withdraw to Circle wallet | -| `omniclaw-cli withdraw_trustless --amount X` | Trustless withdraw (~7-day delay) | -| `omniclaw-cli withdraw_trustless_complete` | Complete trustless withdraw after delay | -| `omniclaw-cli inspect-x402 --recipient URL` | Inspect seller requirements and buyer readiness | -| `omniclaw-cli pay --recipient 0x... --amount X` | Pay another agent | -| `omniclaw-cli pay --recipient URL` | Pay a seller x402 endpoint | -| `omniclaw-cli serve --price X --endpoint /api --exec "cmd"` | Start an owner-approved agent-run payment gate | - ---- - -## Network Switching - -To switch from testnet to mainnet: - -```bash -export OMNICLAW_NETWORK="ETH-MAINNET" -export OMNICLAW_ENV="production" -``` - -Everything automatically switches to mainnet URLs. - ---- - -## Troubleshooting - -### "Wallet is currently initializing" -Wait a few seconds and retry. The agent is setting up. - -### "Invalid token" -Check that `OMNICLAW_AGENT_TOKEN` matches a key in your policy.json's `tokens` section. - -### "Insufficient balance" -Check which route the seller requires: - -```bash -omniclaw-cli inspect-x402 --recipient https://seller.example.com/premium -``` - -If the route is `GatewayWalletBatched`, deposit to Gateway first: - -```bash -omniclaw-cli deposit --amount 10 -``` - -If the route is `exact`, fund the buyer signer wallet on the required chain instead. diff --git a/docs/agent-skills.md b/docs/agent-skills.md index 41af2f0..557f04d 100644 --- a/docs/agent-skills.md +++ b/docs/agent-skills.md @@ -1,337 +1,11 @@ -# OmniClaw CLI Guide +# Agent Skills -This document is for human readers: owners, operators, reviewers, and developers. +OmniClaw core skills are buyer-oriented. -It explains what OmniClaw CLI is, how setup works, what buyers and sellers do with the same CLI, how approval flows work, and where to find the exact live command reference. - -## Executive Summary - -`omniclaw-cli` is the agent-facing zero-trust execution layer for OmniClaw. - -OmniClaw itself is the **Economic Execution and Control Layer for Agentic Systems**. -That full system is larger than the CLI alone: - -- the CLI is the constrained execution surface the agent uses -- the Financial Policy Engine is the owner-run enforcement and signing layer -- the settlement rails include direct USDC transfers, x402, CCTP, and Circle Gateway nanopayments -- the policy, trust, ledger, and concurrency controls are part of the overall OmniClaw system - -It is the same CLI for agent-side economic execution: - -- buyer side: `omniclaw-cli pay` -- seller side for agent-run paid endpoints: `omniclaw-cli serve` - -Vendor and enterprise seller APIs should use the Python SDK with `client.sell(...)`. - -That two-sided model matters: - -- without a seller exposing a paid endpoint with `serve`, there is nothing meaningful for a buyer to pay through x402 -- without a buyer using `pay`, the seller endpoint does not earn - -OmniClaw keeps the private key and policy enforcement on the Financial Policy Engine side. -The agent uses the CLI as a constrained execution surface. - -## Reader Split - -There are now three separate artifacts, each with a different audience: - -- `docs/agent-skills.md` - - human/operator guide -- `docs/omniclaw-cli-skill/SKILL.md` - - public shipped skill specification -- `docs/cli-reference.md` - - exact generated command reference for the public CLI surface - -This split is deliberate. -It keeps the public skill specification separate from the human/operator guide and generated command reference. - -## Setup Model - -A typical OmniClaw agent runtime needs: - -- `OMNICLAW_SERVER_URL`: Financial Policy Engine URL, for example `http://localhost:9090` -- `OMNICLAW_TOKEN`: scoped agent token -- optionally `OMNICLAW_OWNER_TOKEN`: only if the run is allowed to approve confirmations - -For local convenience, you can persist those values in CLI config. `configure` writes saved CLI config; it does not export shell environment variables: - -```bash -omniclaw-cli configure \ - --server-url http://localhost:9090 \ - --token payment-agent-token \ - --wallet omni-bot-v4 -``` - -Available `configure` flags: - -- `--server-url TEXT` -- `--token TEXT` -- `--wallet TEXT` -- `--owner-token TEXT` -- `--show` -- `--show-raw` -- `--interactive` - -Show saved config: - -```bash -omniclaw-cli configure --show -``` - -When the CLI has been configured already, later commands such as `balance`, `can-pay`, `pay`, and `serve` can reuse that saved config without re-exporting `OMNICLAW_SERVER_URL` and `OMNICLAW_TOKEN` in the shell. - -## Runtime Architecture - -Typical execution path: - -1. owner starts the Financial Policy Engine -2. owner provisions policy and agent token(s) -3. agent invokes `omniclaw-cli` -4. Financial Policy Engine validates policy, balance, trust, and approval rules -5. only approved actions are signed and executed - -In the normal CLI-agent model, the agent should not be given direct wallet secrets. - -This matches the official OmniClaw framing: - -- agents execute -- policies authorize -- infrastructure settles - -## Buyer Flows - -### Pay a paid URL - -```bash -omniclaw-cli can-pay --recipient https://api.vendor.com/data -omniclaw-cli pay --recipient https://api.vendor.com/data --idempotency-key job-123 -``` - -### Pay a paid POST endpoint - -```bash -omniclaw-cli pay \ - --recipient https://api.vendor.com/inference \ - --method POST \ - --body '{"prompt":"hello"}' \ - --header 'Content-Type: application/json' \ - --idempotency-key job-123 -``` - -### Direct USDC transfer - -```bash -omniclaw-cli pay \ - --recipient 0xRecipientAddress \ - --amount 5.00 \ - --purpose "service payment" \ - --idempotency-key job-123 -``` - -### Buyer-side inspection and preparation - -Useful buyer commands: - -- `status` -- `address` -- `balance` -- `balance-detail` -- `can-pay` -- `simulate` -- `pay` -- `deposit` -- `withdraw` -- `withdraw-trustless` -- `withdraw-trustless-complete` -- `ledger` - -## Seller Flows - -### Expose a paid endpoint - -Only use this path when the owner explicitly wants an agent-run seller endpoint. Vendor and enterprise APIs should use the SDK seller middleware instead. +Use these flows when an agent needs to inspect or pay a URL through the policy engine: ```bash -omniclaw-cli serve \ - --price 0.01 \ - --endpoint /api/data \ - --exec "python safe_readonly_service.py" \ - --port 8000 +omniclaw-cli can-pay --recipient https://paid.example.com/compute +omniclaw-cli inspect-x402 --recipient https://paid.example.com/compute +omniclaw-cli pay --recipient https://paid.example.com/compute --idempotency-key job-123 ``` - -What `serve` does: - -- starts an x402 payment gate -- returns `402 Payment Required` to unpaid callers -- verifies payment through Circle Gateway middleware -- runs the command supplied via `--exec` -- returns command output to the paid caller - -Important implementation detail: - -- `serve` binds to `0.0.0.0` -- the banner may print `localhost`, but the actual bind host is all interfaces -- `--exec` runs the supplied host command after payment verification -- do not invent the `--exec` command; run only a command supplied or approved by the owner -- prefer an isolated container or private development network for `serve` - -Useful seller commands: - -- `status` -- `address` -- `balance` -- `balance-detail` -- `serve` -- `ledger` - -## Approval and Intent Flows - -Some policies require approval before spend. -In those cases `pay` can return fields such as: - -- `requires_confirmation: true` -- `confirmation_id: ...` - -Owner approval commands: - -```bash -omniclaw-cli confirmations get --id -omniclaw-cli confirmations approve --id -omniclaw-cli confirmations deny --id -``` - -Intent commands: - -```bash -omniclaw-cli create-intent --recipient 0xRecipient --amount 5.00 --purpose "vendor payment" -omniclaw-cli confirm-intent --intent-id -omniclaw-cli get-intent --intent-id -omniclaw-cli cancel-intent --intent-id --reason "no longer needed" -``` - -## Full Command Surface - -Current top-level commands exposed by the CLI: - -- `configure` -- `address` -- `balance` -- `balance-detail` -- `balance_detail` -- `deposit` -- `withdraw` -- `withdraw-trustless` -- `withdraw_trustless` -- `withdraw-trustless-complete` -- `withdraw_trustless_complete` -- `pay` -- `simulate` -- `can-pay` -- `can_pay` -- `create-intent` -- `create_intent` -- `confirm-intent` -- `confirm_intent` -- `get-intent` -- `get_intent` -- `cancel-intent` -- `cancel_intent` -- `ledger` -- `list-tx` -- `list_tx` -- `serve` -- `status` -- `ping` -- `wallet` -- `intents` -- `confirmations` - -## Command Families - -### Payment execution - -- `pay` -- `simulate` -- `can-pay` -- `create-intent` -- `confirm-intent` -- `get-intent` -- `cancel-intent` -- `confirmations get|approve|deny` - -### Balance and funds movement - -- `address` -- `balance` -- `balance-detail` -- `deposit` -- `withdraw` -- `withdraw-trustless` -- `withdraw-trustless-complete` - -### Seller execution - -- `serve` - -### Inspection - -- `status` -- `ping` -- `ledger` -- `list-tx` - -## Recommended Operational Rules - -- use `can-pay` for new recipients -- use `inspect-x402` for a new paid URL before the first live payment -- use `--idempotency-key` for job-based payments -- use `balance-detail` when Gateway balances matter -- use `simulate` when the amount or guard risk is non-trivial -- do not give agents raw wallet secrets in the normal CLI path -- treat `serve` and `pay` as one economic system, not separate products - -## Auto-Generated Reference - -The exact command reference is generated from the live CLI help surface. - -Regenerate it with: - -```bash -python docs/omniclaw-cli-skill/scripts/generate_cli_reference.py -``` - -Generated outputs: - -- `docs/cli-reference.md` - -That keeps the documented flags and command surface aligned with the actual CLI. - -## Why There Is No `agents/openai.yaml` - -`agents/openai.yaml` is optional UI metadata. - -It is useful for things like: - -- display names in skill pickers -- short descriptions in UI chips -- marketplace-style metadata - -It is not required for the OmniClaw agent skill to work. - -For this repository, the public sources of truth are: - -- `docs/omniclaw-cli-skill/SKILL.md` -- `docs/cli-reference.md` - -That keeps the shipped skill and the shipped command reference in public docs. - -## Ship Recommendation - -This is now the recommended storage layout: - -- keep public shipped skill specs under `docs/` -- keep repo-local internal-use skills under `.agents/skills/` -- keep human/operator docs under `docs/` -- keep the exact CLI reference generated, not handwritten - -That is the cleanest split for long-term maintenance and for reducing agent mistakes. diff --git a/docs/cli-reference.md b/docs/cli-reference.md index 8e6249d..ff21a55 100644 --- a/docs/cli-reference.md +++ b/docs/cli-reference.md @@ -1,512 +1,33 @@ -# OmniClaw CLI Reference +# CLI Reference -This file is generated from the live `omniclaw-cli --help` surface. -Do not hand-edit command schemas here; regenerate instead. +`omniclaw-cli` is the buyer CLI for agents and automation. -Generator: +## Configure ```bash -python3 docs/omniclaw-cli-skill/scripts/generate_cli_reference.py +omniclaw-cli configure --server-url http://localhost:8080 --token agent-token --wallet wallet-id ``` -## Usage Notes - -- same CLI, two roles: buyer uses `pay`, seller uses `serve` -- use `can-pay` before a new recipient when policy allow/deny matters -- use `inspect-x402` before a new paid URL when you need to see seller requirements and buyer funding readiness -- use `balance-detail` when Gateway state matters -- use `--idempotency-key` for job-based payments -- for x402 URLs, `--amount` can be omitted because the payment requirements come from the seller endpoint -- `serve` is for owner-approved agent-run seller endpoints only -- `serve` binds to `0.0.0.0` even if the banner prints `localhost` -- `serve --exec` runs a host command; do not invent the command or expose it outside an isolated runtime - -## Example Flows - -Buyer paying an x402 endpoint: +## Inspect A Paid URL ```bash -omniclaw-cli can-pay --recipient http://seller-host:8000/api/data -omniclaw-cli inspect-x402 --recipient http://seller-host:8000/api/data -omniclaw-cli pay --recipient http://seller-host:8000/api/data --idempotency-key job-123 +omniclaw-cli can-pay --recipient https://paid.example.com/compute +omniclaw-cli inspect-x402 --recipient https://paid.example.com/compute ``` -Buyer paying a direct address: +## Pay ```bash -omniclaw-cli pay \ - --recipient 0xRecipientAddress \ - --amount 5.00 \ - --purpose "service payment" \ - --idempotency-key job-123 +omniclaw-cli pay --recipient https://paid.example.com/compute --idempotency-key job-123 +omniclaw-cli pay --recipient 0xRecipient --amount 5.00 --idempotency-key job-124 ``` -Seller exposing a paid endpoint: +## Wallets, Ledger, Intents, Confirmations ```bash -omniclaw-cli serve \ - --price 0.01 \ - --endpoint /api/data \ - --exec "python safe_readonly_service.py" \ - --port 8000 -``` - -Only run this after the owner explicitly asks for an agent-run seller endpoint and supplies or approves the `--exec` command. - -## Live Help Output - -### `omniclaw-cli --help` - -```text - - Usage: omniclaw-cli [OPTIONS] COMMAND [ARGS]... - - omniclaw-cli - zero-trust execution layer for policy-controlled agent - payments, x402 services, and agentic commerce - -╭─ Options ────────────────────────────────────────────────────────────────────╮ -│ --install-completion Install completion for the current shell. │ -│ --show-completion Show completion for the current shell, to copy │ -│ it or customize the installation. │ -│ --help Show this message and exit. │ -╰──────────────────────────────────────────────────────────────────────────────╯ -╭─ Commands ───────────────────────────────────────────────────────────────────╮ -│ configure Configure omniclaw-cli with server details. │ -│ address Get wallet address. │ -│ balance Get wallet balance. │ -│ balance-detail Get detailed balance including Gateway and │ -│ Circle wallet. │ -│ balance_detail Alias for balance-detail │ -│ deposit Deposit USDC from EOA to Gateway wallet. │ -│ withdraw Withdraw USDC from Gateway to Circle Developer │ -│ Wallet. │ -│ withdraw-trustless Initiate trustless withdrawal (~7-day delay, │ -│ no API needed). │ -│ withdraw_trustless Alias for withdraw-trustless │ -│ withdraw-trustless-complete Complete a trustless withdrawal after the │ -│ delay has passed. │ -│ withdraw_trustless_complete Alias for withdraw-trustless-complete │ -│ pay Execute a payment or pay for an x402 service. │ -│ simulate Simulate a payment without executing. │ -│ inspect-x402 Inspect an x402 endpoint and show which │ -│ payment route OmniClaw would use. │ -│ inspect_x402 Alias for inspect-x402 │ -│ can-pay Check if recipient is allowed. │ -│ can_pay Alias for can-pay │ -│ create-intent Create a payment intent (authorize). │ -│ create_intent Alias for create-intent │ -│ confirm-intent Confirm a payment intent (capture). │ -│ confirm_intent Alias for confirm-intent │ -│ get-intent Get a payment intent. │ -│ get_intent Alias for get-intent │ -│ cancel-intent Cancel a payment intent. │ -│ cancel_intent Alias for cancel-intent │ -│ ledger List transaction history. │ -│ list-tx List transaction history. │ -│ list_tx Alias for list-tx │ -│ serve Expose a local service behind an x402 payment │ -│ gate. │ -│ status Get agent status and health. │ -│ ping Health check. │ -│ wallet Wallet operations │ -│ intents Payment intents │ -│ confirmations Manage pending confirmations (owner only) │ -╰──────────────────────────────────────────────────────────────────────────────╯ -``` - -### `omniclaw-cli configure --help` - -```text - - Usage: omniclaw-cli configure [OPTIONS] - - Configure omniclaw-cli with server details. - -╭─ Options ────────────────────────────────────────────────────────────────────╮ -│ --server-url TEXT OmniClaw Financial Policy Engine URL │ -│ --token TEXT Agent token │ -│ --wallet TEXT Wallet alias │ -│ --owner-token TEXT Owner token │ -│ --show Show current config │ -│ --show-raw Show raw secrets │ -│ --interactive Prompt for missing values │ -│ --help Show this message and exit. │ -╰──────────────────────────────────────────────────────────────────────────────╯ -``` - -### `omniclaw-cli status --help` - -```text - - Usage: omniclaw-cli status [OPTIONS] - - Get agent status and health. - -╭─ Options ────────────────────────────────────────────────────────────────────╮ -│ --help Show this message and exit. │ -╰──────────────────────────────────────────────────────────────────────────────╯ -``` - -### `omniclaw-cli ping --help` - -```text - - Usage: omniclaw-cli ping [OPTIONS] - - Health check. - -╭─ Options ────────────────────────────────────────────────────────────────────╮ -│ --help Show this message and exit. │ -╰──────────────────────────────────────────────────────────────────────────────╯ -``` - -### `omniclaw-cli address --help` - -```text - - Usage: omniclaw-cli address [OPTIONS] - - Get wallet address. - -╭─ Options ────────────────────────────────────────────────────────────────────╮ -│ --help Show this message and exit. │ -╰──────────────────────────────────────────────────────────────────────────────╯ -``` - -### `omniclaw-cli balance --help` - -```text - - Usage: omniclaw-cli balance [OPTIONS] - - Get wallet balance. - -╭─ Options ────────────────────────────────────────────────────────────────────╮ -│ --help Show this message and exit. │ -╰──────────────────────────────────────────────────────────────────────────────╯ -``` - -### `omniclaw-cli balance-detail --help` - -```text - - Usage: omniclaw-cli balance-detail [OPTIONS] - - Get detailed balance including Gateway and Circle wallet. - -╭─ Options ────────────────────────────────────────────────────────────────────╮ -│ --help Show this message and exit. │ -╰──────────────────────────────────────────────────────────────────────────────╯ -``` - -### `omniclaw-cli can-pay --help` - -```text - - Usage: omniclaw-cli can-pay [OPTIONS] - - Check if recipient is allowed. - -╭─ Options ────────────────────────────────────────────────────────────────────╮ -│ * --recipient TEXT Recipient to check [required] │ -│ --help Show this message and exit. │ -╰──────────────────────────────────────────────────────────────────────────────╯ -``` - -### `omniclaw-cli inspect-x402 --help` - -```text - - Usage: omniclaw-cli inspect-x402 [OPTIONS] - - Inspect an x402 endpoint and show which payment route OmniClaw would use. - -╭─ Options ────────────────────────────────────────────────────────────────────╮ -│ * --recipient TEXT x402 URL to inspect [required] │ -│ --amount TEXT Optional max amount in USDC │ -│ --method TEXT HTTP method for x402 requests [default: GET] │ -│ --body TEXT Request body for x402 requests │ -│ --header TEXT Additional headers for x402 requests │ -│ --help Show this message and exit. │ -╰──────────────────────────────────────────────────────────────────────────────╯ -``` - -### `omniclaw-cli simulate --help` - -```text - - Usage: omniclaw-cli simulate [OPTIONS] - - Simulate a payment without executing. - -╭─ Options ────────────────────────────────────────────────────────────────────╮ -│ * --recipient TEXT Recipient to simulate [required] │ -│ * --amount TEXT Amount to simulate [required] │ -│ --idempotency-key TEXT Idempotency key │ -│ --destination-chain TEXT Target network │ -│ --fee-level TEXT Gas fee level (LOW, MEDIUM, HIGH) │ -│ --check-trust Run Trust Gate check │ -│ --skip-guards Skip guards (OWNER ONLY) │ -│ --help Show this message and exit. │ -╰──────────────────────────────────────────────────────────────────────────────╯ -``` - -### `omniclaw-cli pay --help` - -```text - - Usage: omniclaw-cli pay [OPTIONS] - - Execute a payment or pay for an x402 service. - -╭─ Options ────────────────────────────────────────────────────────────────────╮ -│ * --recipient TEXT Payment recipient (address or URL) │ -│ [required] │ -│ --amount TEXT Amount in USDC (optional for x402 URLs) │ -│ --purpose TEXT Payment purpose │ -│ --idempotency-key TEXT Idempotency key │ -│ --destination-chain TEXT Target network │ -│ --fee-level TEXT Gas fee level (LOW, MEDIUM, HIGH) │ -│ --check-trust Run Trust Gate check │ -│ --skip-guards Skip guards (OWNER ONLY) │ -│ --method TEXT HTTP method for x402 requests │ -│ [default: GET] │ -│ --body TEXT JSON body for x402 requests │ -│ --header TEXT Additional headers for x402 requests │ -│ --output TEXT Save response to file │ -│ --dry-run Simulate first │ -│ --help Show this message and exit. │ -╰──────────────────────────────────────────────────────────────────────────────╯ -``` - -### `omniclaw-cli deposit --help` - -```text - - Usage: omniclaw-cli deposit [OPTIONS] - - Deposit USDC from EOA to Gateway wallet. - -╭─ Options ────────────────────────────────────────────────────────────────────╮ -│ * --amount TEXT Amount in USDC to deposit to Gateway [required] │ -│ --help Show this message and exit. │ -╰──────────────────────────────────────────────────────────────────────────────╯ -``` - -### `omniclaw-cli withdraw --help` - -```text - - Usage: omniclaw-cli withdraw [OPTIONS] - - Withdraw USDC from Gateway to Circle Developer Wallet. - -╭─ Options ────────────────────────────────────────────────────────────────────╮ -│ * --amount TEXT Amount in USDC to withdraw from Gateway [required] │ -│ --help Show this message and exit. │ -╰──────────────────────────────────────────────────────────────────────────────╯ -``` - -### `omniclaw-cli withdraw-trustless --help` - -```text - - Usage: omniclaw-cli withdraw-trustless [OPTIONS] - - Initiate trustless withdrawal (~7-day delay, no API needed). - -╭─ Options ────────────────────────────────────────────────────────────────────╮ -│ * --amount TEXT Amount in USDC to withdraw (trustless, ~7-day │ -│ delay) │ -│ [required] │ -│ --help Show this message and exit. │ -╰──────────────────────────────────────────────────────────────────────────────╯ -``` - -### `omniclaw-cli withdraw-trustless-complete --help` - -```text - - Usage: omniclaw-cli withdraw-trustless-complete [OPTIONS] - - Complete a trustless withdrawal after the delay has passed. - -╭─ Options ────────────────────────────────────────────────────────────────────╮ -│ --help Show this message and exit. │ -╰──────────────────────────────────────────────────────────────────────────────╯ -``` - -### `omniclaw-cli serve --help` - -```text - - Usage: omniclaw-cli serve [OPTIONS] - - Expose a local service behind an x402 payment gate. - - Uses the production GatewayMiddleware for full x402 v2 protocol compliance: - - Returns proper 402 responses with all required fields - - Parses PAYMENT-SIGNATURE headers - - Settles atomically via Circle Gateway /settle - -╭─ Options ────────────────────────────────────────────────────────────────────╮ -│ * --price FLOAT Price per request in USDC [required] │ -│ * --endpoint TEXT Endpoint path to expose [required] │ -│ * --exec TEXT Command to execute on success [required] │ -│ --port INTEGER Local port to listen on [default: 8000] │ -│ --help Show this message and exit. │ -╰──────────────────────────────────────────────────────────────────────────────╯ -``` - -### `omniclaw-cli create-intent --help` - -```text - - Usage: omniclaw-cli create-intent [OPTIONS] - - Create a payment intent (authorize). - -╭─ Options ────────────────────────────────────────────────────────────────────╮ -│ * --recipient TEXT Recipient [required] │ -│ * --amount TEXT Amount [required] │ -│ --purpose TEXT Purpose │ -│ --expires-in INTEGER Expiry in seconds │ -│ --idempotency-key TEXT Idempotency key │ -│ --destination-chain TEXT Target network │ -│ --fee-level TEXT Gas fee level (LOW, MEDIUM, HIGH) │ -│ --check-trust Run Trust Gate check │ -│ --skip-guards Skip guards (OWNER ONLY) │ -│ --help Show this message and exit. │ -╰──────────────────────────────────────────────────────────────────────────────╯ -``` - -### `omniclaw-cli confirm-intent --help` - -```text - - Usage: omniclaw-cli confirm-intent [OPTIONS] - - Confirm a payment intent (capture). - -╭─ Options ────────────────────────────────────────────────────────────────────╮ -│ * --intent-id TEXT Intent ID to confirm [required] │ -│ --help Show this message and exit. │ -╰──────────────────────────────────────────────────────────────────────────────╯ -``` - -### `omniclaw-cli get-intent --help` - -```text - - Usage: omniclaw-cli get-intent [OPTIONS] - - Get a payment intent. - -╭─ Options ────────────────────────────────────────────────────────────────────╮ -│ * --intent-id TEXT Intent ID to fetch [required] │ -│ --help Show this message and exit. │ -╰──────────────────────────────────────────────────────────────────────────────╯ -``` - -### `omniclaw-cli cancel-intent --help` - -```text - - Usage: omniclaw-cli cancel-intent [OPTIONS] - - Cancel a payment intent. - -╭─ Options ────────────────────────────────────────────────────────────────────╮ -│ * --intent-id TEXT Intent ID to cancel [required] │ -│ --reason TEXT Cancel reason │ -│ --help Show this message and exit. │ -╰──────────────────────────────────────────────────────────────────────────────╯ -``` - -### `omniclaw-cli ledger --help` - -```text - - Usage: omniclaw-cli ledger [OPTIONS] - - List transaction history. - -╭─ Options ────────────────────────────────────────────────────────────────────╮ -│ --limit INTEGER Number of transactions to fetch [default: 20] │ -│ --help Show this message and exit. │ -╰──────────────────────────────────────────────────────────────────────────────╯ -``` - -### `omniclaw-cli list-tx --help` - -```text - - Usage: omniclaw-cli list-tx [OPTIONS] - - List transaction history. - -╭─ Options ────────────────────────────────────────────────────────────────────╮ -│ --limit INTEGER Number of transactions to fetch [default: 20] │ -│ --help Show this message and exit. │ -╰──────────────────────────────────────────────────────────────────────────────╯ -``` - -### `omniclaw-cli confirmations --help` - -```text - - Usage: omniclaw-cli confirmations [OPTIONS] COMMAND [ARGS]... - - Manage pending confirmations (owner only) - -╭─ Options ────────────────────────────────────────────────────────────────────╮ -│ --help Show this message and exit. │ -╰──────────────────────────────────────────────────────────────────────────────╯ -╭─ Commands ───────────────────────────────────────────────────────────────────╮ -│ get Get confirmation details. │ -│ approve Approve a confirmation. │ -│ deny Deny a confirmation. │ -╰──────────────────────────────────────────────────────────────────────────────╯ -``` - -### `omniclaw-cli confirmations get --help` - -```text - - Usage: omniclaw-cli confirmations get [OPTIONS] - - Get confirmation details. - -╭─ Options ────────────────────────────────────────────────────────────────────╮ -│ * --id TEXT Confirmation ID [required] │ -│ --help Show this message and exit. │ -╰──────────────────────────────────────────────────────────────────────────────╯ -``` - -### `omniclaw-cli confirmations approve --help` - -```text - - Usage: omniclaw-cli confirmations approve [OPTIONS] - - Approve a confirmation. - -╭─ Options ────────────────────────────────────────────────────────────────────╮ -│ * --id TEXT Confirmation ID [required] │ -│ --help Show this message and exit. │ -╰──────────────────────────────────────────────────────────────────────────────╯ -``` - -### `omniclaw-cli confirmations deny --help` - -```text - - Usage: omniclaw-cli confirmations deny [OPTIONS] - - Deny a confirmation. - -╭─ Options ────────────────────────────────────────────────────────────────────╮ -│ * --id TEXT Confirmation ID [required] │ -│ --help Show this message and exit. │ -╰──────────────────────────────────────────────────────────────────────────────╯ +omniclaw-cli wallet list +omniclaw-cli ledger +omniclaw-cli intents +omniclaw-cli confirmations +omniclaw-cli status ``` diff --git a/docs/developer-guide.md b/docs/developer-guide.md index 146083d..cb0126d 100644 --- a/docs/developer-guide.md +++ b/docs/developer-guide.md @@ -1,224 +1,46 @@ -# OmniClaw Developer Guide (Python SDK) +# Developer Guide -This guide covers how developers, vendors, and application teams build policy-controlled payment flows with the OmniClaw Python SDK. +This guide covers OmniClaw core buyer integration. -> **Vendors vs Agents:** This guide is for developers embedding OmniClaw code into real Python applications (like FastAPI servers or backend worker scripts). If you are looking to operate an autonomous AI Agent using the command line, please see the [Agent CLI Guide](agent-getting-started.md). +## Pay From Python ---- - -## 1. Initialize the SDK Client - -```python -from omniclaw import OmniClaw, Network - -# Initialize the client (auto-loads credentials from env) -client = OmniClaw(network=Network.ETH_SEPOLIA) -``` - -With environment variables: - -```env -CIRCLE_API_KEY=your_circle_api_key -OMNICLAW_NETWORK=ETH-SEPOLIA -``` - -### Automatic Entity Secret Management - -If your Circle account/API key already has an Entity Secret, set it directly: - -```env -ENTITY_SECRET=your_existing_64_char_hex_entity_secret -``` - -Circle only allows one active Entity Secret per account/API key. OmniClaw will use `ENTITY_SECRET` from the environment first, then its managed local credential store. It only auto-generates and registers a new Entity Secret when no existing secret is provided or found. - -For non-interactive setup: - -```bash -omniclaw setup --api-key "$CIRCLE_API_KEY" --entity-secret "$ENTITY_SECRET" -``` - ---- - -## 2. Managing Wallets programmatically - -Whether you are building a buyer app that needs to spend funds or a seller app that needs to receive funds, you need an OmniClaw-managed wallet. - -### Create an Application Wallet ```python -# Creates a wallet_set and primary wallet for your app -wallet_set, wallet = await client.create_agent_wallet("my-backend-app") - -print(f"Your application's wallet ID is: {wallet.id}") -print(f"Your on-chain address is: {wallet.address}") - -# Get detailed balance (what you can spend safely) -detailed = await client.get_detailed_balance(wallet.id) -print(f"Available USDC: {detailed['available']}") -``` - ---- - -## 3. As a Developer: Buying and Sending Payments - -Use the SDK to programmatically send USDC payments (e.g., in a background job, cron script, or user-initiated transaction). - -### Standard P2P Transfer -```python -result = await client.pay( - wallet_id=wallet.id, - recipient="0xRecipientAddress", - amount="10.50", - purpose="Monthly API refill", -) -print(f"Payment successful. TX: {result.blockchain_tx or result.transaction_id}") -``` - -### Paying an x402 Paid Endpoint Programmatically -OmniClaw's routing engine handles all the complexity of the x402 402-Payment-Required handshake internally. Just pass the URL as the recipient: - -```python -# OmniClaw performs the 402 handshake, signs the selected payment, retries the -# request with the payment header, and returns the paid resource response. -result = await client.pay( - wallet_id=wallet.id, - recipient="https://api.vendor.com/premium-data", - amount="0.05", -) - -print(result.status, result.blockchain_tx or result.transaction_id) -print(result.resource_data) -``` - ---- - -## 4. As a Developer: Selling and Receiving Payments (Vendor) - -If you are building an API and want to monetize it without setting up a separate billing system, use `client.sell()` in FastAPI. - -### Quick Start: Gated Endpoints -```python -from fastapi import FastAPI from omniclaw import OmniClaw -app = FastAPI() client = OmniClaw() -# Require $0.01 in USDC to access this route -@app.get("/premium-data") -async def get_data( - payment=client.sell("$0.01", seller_address="0xYourWalletAddress"), -): - - # This code ONLY executes if the payment has cleared and settled. - return { - "status": "success", - "data": "premium content", - "buyer_address": payment.payer, - } -``` - -### Seller Facilitator Options - -The seller SDK supports multiple settlement paths. - -Circle Gateway default: - -```python -payment=client.sell("$0.25", seller_address="0xVendorWallet") -``` - -Thirdweb managed x402: - -```python -payment=client.sell( - "$0.25", - seller_address="0xThirdwebServerWallet", - facilitator="thirdweb", +result = await client.pay( + wallet_id="wallet-id", + recipient="https://paid.example.com/compute", + amount=None, + idempotency_key="job-123", ) ``` -OmniClaw self-hosted exact facilitator: +For x402 URLs, OmniClaw inspects the payment requirements and routes through the buyer rail that is available and allowed by policy. -```python -payment=client.sell( - "$0.25", - seller_address="0xVendorWallet", - facilitator="omniclaw", -) -``` - -For the self-hosted path, run the facilitator separately: +## Run The Policy Engine ```bash -export OMNICLAW_X402_SELF_HOSTED_FACILITATOR_URL="http://127.0.0.1:4022" -export OMNICLAW_X402_EXACT_NETWORK_PROFILE="ARC-TESTNET" - -omniclaw facilitator exact --network-profile ARC-TESTNET --port 4022 -``` - -See [B2B SDK Integration](../examples/b2b-sdk-integration/README.md) for complete deployment examples. - -### Managing Your Vendor Earnings -Payments received via the standard Circle Gateway routes pile up in your Gateway balance. You must withdraw them to your main on-chain wallet. - -```python -# Check earnings -balance = await client.get_gateway_balance(wallet.id) -print(f"Earnings waiting in Gateway: {balance.formatted_total}") +export OMNICLAW_PRIVATE_KEY="0x..." +export OMNICLAW_AGENT_TOKEN="agent-token" +export OMNICLAW_AGENT_POLICY_PATH="./policy.json" +export OMNICLAW_NETWORK="BASE-SEPOLIA" +export OMNICLAW_RPC_URL="https://sepolia.base.org" -# Sweep earnings to your exact on-chain wallet -await client.withdraw_from_gateway( - wallet_id=wallet.id, - amount_usdc="50.00", -) +docker compose up --build omniclaw-agent ``` ---- - -## 5. Adding Safety Guards via Code - -As a developer, you want absolute assurance your background jobs won't drain your liquidity. - -```python -# Block your application from spending more than $100 a day -await client.add_budget_guard(wallet.id, daily_limit="100.00", hourly_limit="20.00") - -# Stop runaway loops: max 5 payments per minute -await client.add_rate_limit_guard(wallet.id, max_per_minute=5) +## Pay With The CLI -# Never allow a single payment over $25 -await client.add_single_tx_guard(wallet.id, max_amount="25.00") +```bash +export OMNICLAW_SERVER_URL="http://localhost:8080" +export OMNICLAW_TOKEN="agent-token" -# Restrict outbound funds to a specific whitelist of known vendor APIs -await client.add_recipient_guard( - wallet.id, - mode="whitelist", - addresses=["0xTrustedSupplier"], - domains=["api.openai.com", "api.anthropic.com"], -) +omniclaw-cli inspect-x402 --recipient https://paid.example.com/compute +omniclaw-cli pay --recipient https://paid.example.com/compute --idempotency-key job-123 ``` -## 6. Advanced programmatic controls - -### Simulating Payments -Test if a complex payment flow will pass guards without actually sending money. -```python -sim = await client.simulate( - wallet_id=wallet.id, - recipient="0xRecipient", - amount="25.00", -) - -if sim.would_succeed: - print(f"Will execute using route: {sim.route}") -else: - print(f"Blocked by: {sim.reason}") -``` +## Gateway Buyer Funding -### Webhooks -When receiving async settlement events from the Circle API: -```python -event = client.webhooks.handle(payload, headers) -print(f"Received settlement for transaction {event.transaction_hash}") -``` +Gateway nanopayments require the buyer to hold/deposit USDC into Circle Gateway before using `GatewayWalletBatched` routes. Core keeps buyer-side deposit, withdrawal, balance, and readiness helpers. diff --git a/docs/facilitators.md b/docs/facilitators.md deleted file mode 100644 index b9c8462..0000000 --- a/docs/facilitators.md +++ /dev/null @@ -1,430 +0,0 @@ -# x402 Facilitators - -OmniClaw supports x402 payments through facilitator-backed settlement. - -A facilitator is the service that verifies an x402 payment payload and settles it on the target rail. OmniClaw is integration-first: it governs agent financial authority and routes into the facilitator that fits the seller's requirements. - -Supported deployment shapes: - -- Circle Gateway `GatewayWalletBatched` for gasless nanopayments -- standard x402 `exact` settlement through an external facilitator such as Thirdweb or x402.org -- optional standard x402 `exact` settlement through a self-hosted OmniClaw facilitator - -## Deployment Matrix - -Use this matrix as the canonical operating model: - -| Mode | Seller creates `accepts` | Who runs `verify` / `settle` | Status | -| --- | --- | --- | --- | -| Circle Gateway | OmniClaw seller middleware | Circle Gateway facilitator | already supported in OmniClaw seller flow | -| External exact via x402.org | seller app or OmniClaw external seller harness | x402.org | supported on Base Sepolia | -| External exact via Thirdweb | Thirdweb `accepts` API or seller using Thirdweb server wallet | Thirdweb | supported; requires managed Thirdweb account | -| Self-hosted OmniClaw exact facilitator | seller app or OmniClaw seller harness | OmniClaw exact facilitator | supported on Arc Testnet; use for Arc, Base, Ethereum Sepolia, and other applicable EVM profiles | - -The architectural split matters: - -- `accepts` is a seller concern -- `verify` and `settle` are facilitator concerns -- buyer routing is an OmniClaw policy concern - -OmniClaw does not mix these layers together. - -The buyer still uses one action: - -```bash -omniclaw-cli inspect-x402 --recipient https://seller.example.com/compute -omniclaw-cli pay --recipient https://seller.example.com/compute --idempotency-key job-123 -``` - -The Financial Policy Engine inspects the seller's x402 requirements and chooses a route that the seller supports and the buyer can actually fund. - -## Integration Model - -OmniClaw supports managed and self-hosted settlement paths. Thirdweb, Circle, x402.org, and other facilitators can handle settlement where they are the best fit. - -OmniClaw's product responsibility: - -- inspect what the seller accepts -- enforce buyer policy before money moves -- choose a fundable route -- sign only the allowed action -- preserve logs, limits, and payment visibility - -That means a seller can use managed facilitator coverage, while the buyer still uses OmniClaw as the policy-controlled execution layer. - -## SDK Seller Examples - -Vendor and enterprise sellers should normally use the Python SDK. - -Circle Gateway: - -```python -payment=client.sell("$0.25", seller_address="0xVendorWallet") -``` - -Thirdweb: - -```python -payment=client.sell( - "$0.25", - seller_address="0xThirdwebServerWallet", - facilitator="thirdweb", -) -``` - -OmniClaw self-hosted exact: - -```python -payment=client.sell( - "$0.25", - seller_address="0xVendorWallet", - facilitator="omniclaw", -) -``` - -For the self-hosted exact path, set: - -```env -OMNICLAW_X402_SELF_HOSTED_FACILITATOR_URL=http://127.0.0.1:4022 -OMNICLAW_X402_EXACT_NETWORK_PROFILE=ARC-TESTNET -``` - -## Self-Hosted Exact Facilitator - -OmniClaw includes a self-hosted exact facilitator for teams that need direct control over settlement infrastructure. - -Common use cases: - -- custom network support -- enterprise self-hosting requirements -- deterministic testnet proof -- chain-native settlement on a selected EVM profile -- local development and integration testing - -The self-hosted facilitator implements standard x402 `exact` verify and settle behavior. The seller still owns the resource URL and `accepts` requirements; the facilitator verifies and settles the signed payment payload. - -## Buyer Route Selection - -For URL payments, the buyer path is: - -1. request the resource -2. receive HTTP 402 x402 requirements -3. inspect accepted payment schemes -4. enforce OmniClaw policy -5. choose the best supported route -6. sign and execute through the selected payment rail - -Current route priority: - -- use `GatewayWalletBatched` when the seller supports Circle Gateway nanopayments and the buyer has Gateway balance on the required network -- use `exact` when the seller supports standard x402 exact settlement -- if the seller supports both and Gateway is not ready, use `exact` -- if no supported route is available, fail clearly before spending -- for direct exact payments, inspect checks the buyer's direct-wallet token balance when the selected EVM network and RPC are known - -## Self-Host An Exact Facilitator - -Use self-hosting when you need local proof, custom network control, or a fallback while validating an external provider. - -Run a facilitator with the first-class OmniClaw command: - -```bash -export OMNICLAW_X402_FACILITATOR_PRIVATE_KEY="0x..." - -omniclaw facilitator exact \ - --network-profile ARC-TESTNET \ - --port 4022 -``` - -For Base Sepolia: - -```bash -export OMNICLAW_X402_FACILITATOR_PRIVATE_KEY="0x..." - -omniclaw facilitator exact \ - --network-profile BASE-SEPOLIA \ - --port 4022 -``` - -You can override the RPC and accepted CAIP-2 network explicitly: - -```bash -omniclaw facilitator exact \ - --network-profile ARC-TESTNET \ - --network eip155:5042002 \ - --rpc-url https://rpc.testnet.arc.network \ - --port 4022 -``` - -The facilitator exposes: - -- `GET /supported` -- `POST /verify` -- `POST /settle` - -It does not need to expose `accepts`. - -For standard x402, `accepts` comes from the seller endpoint that is being monetized. OmniClaw's seller layer and seller harnesses create those requirements, and the facilitator handles verification and settlement after the buyer signs. - -## Environment Variables - -Self-hosted facilitator: - -```env -OMNICLAW_X402_FACILITATOR_PRIVATE_KEY=0x... -OMNICLAW_X402_FACILITATOR_NETWORK_PROFILE=ARC-TESTNET -OMNICLAW_X402_FACILITATOR_RPC_URL=https://rpc.testnet.arc.network -OMNICLAW_X402_FACILITATOR_NETWORKS=eip155:5042002 -OMNICLAW_X402_FACILITATOR_HOST=0.0.0.0 -OMNICLAW_X402_FACILITATOR_PORT=4022 -``` - -Seller exact endpoint: - -```env -OMNICLAW_X402_EXACT_NETWORK_PROFILE=ARC-TESTNET -OMNICLAW_X402_EXACT_FACILITATOR_URL=http://127.0.0.1:4022 -OMNICLAW_X402_EXACT_PRICE=$0.25 -``` - -Preferred behavior: - -- set `OMNICLAW_PRIVATE_KEY` for the seller runtime -- let the seller derive `payTo` from that key - -Optional override: - -```env -OMNICLAW_X402_EXACT_PAY_TO=0xSellerAddress -``` - -Use the override only when you intentionally want the seller to advertise a payout address different from the runtime key. - -External exact facilitator: - -```env -OMNICLAW_X402_EXACT_FACILITATOR_URL=https://x402.org/facilitator -``` - -### x402.org Base Sepolia - -Use x402.org first when you need an external facilitator test without Thirdweb account setup. - -```bash -export OMNICLAW_PRIVATE_KEY="0xYourSellerPrivateKey" -export OMNICLAW_X402_EXACT_NETWORK_PROFILE="BASE-SEPOLIA" -export OMNICLAW_X402_EXACT_FACILITATOR_URL="https://x402.org/facilitator" - -python scripts/start_external_x402_seller.py -``` - -If you need a non-default payout address, add: - -```bash -export OMNICLAW_X402_EXACT_PAY_TO="0xYourSellerAddress" -``` - -Then pay the seller with: - -```bash -omniclaw-cli inspect-x402 --recipient http://127.0.0.1:4021/compute?size=70000 -omniclaw-cli pay --recipient http://127.0.0.1:4021/compute?size=70000 --idempotency-key x402-org-base-sepolia-001 -``` - -Full runbook: [../examples/external-x402-facilitator/README.md](../examples/external-x402-facilitator/README.md). - -Thirdweb-backed sellers normally configure their seller middleware with Thirdweb's own facilitator object. OmniClaw buyers do not need special Thirdweb configuration; they inspect and pay the seller's x402 endpoint through the standard buyer path. - -For OmniClaw seller-side Thirdweb validation, set: - -```env -THIRDWEB_SECRET_KEY=... -THIRDWEB_SERVER_WALLET_ADDRESS=0x... -THIRDWEB_X402_NETWORK=base-sepolia -``` - -Then create the seller gate with `facilitator="thirdweb"` and use the Thirdweb server wallet address as the seller address. - -Thirdweb is different from the self-hosted OmniClaw exact facilitator: - -- Thirdweb exposes `accepts`, `verify`, `settle`, `fetch`, and discovery over HTTP -- OmniClaw exact facilitator exposes `supported`, `verify`, and `settle` -- OmniClaw seller middleware or seller app still owns the resource URL and price policy - -## Arc Testnet - -Arc is supported as an exact-settlement EVM network profile: - -- OmniClaw profile: `ARC-TESTNET` -- CAIP-2 network: `eip155:5042002` -- default RPC: `https://rpc.testnet.arc.network` -- explorer: `https://testnet.arcscan.app` -- USDC interface: `0x3600000000000000000000000000000000000000` - -That means an Arc seller can advertise standard x402 `exact` requirements, the buyer can pay through OmniClaw policy controls, and settlement can be viewed on ArcScan. - -Arc self-hosted exact is fully supported with this standard workflow: - -- seller advertises `exact` on `eip155:5042002` -- OmniClaw buyer selects `x402` with `direct_wallet` -- OmniClaw exact facilitator handles `verify` and `settle` -- settlement confirms on Arc Testnet RPC - -Practical boundary: - -- yes, self-hosted OmniClaw exact can be used for Arc -- yes, the same exact model works for Base Sepolia and other supported EVM profiles -- no, this does not mean "every network automatically works" - -For the profiles already configured in OmniClaw, nothing else needs to be invented in the product layer. The required network metadata is already present in code: - -- CAIP-2 mapping -- default RPC -- explorer base URL where available -- default USDC asset address where available - -The network must have: - -- an EVM CAIP-2 mapping -- a configured network profile -- an RPC endpoint -- a USDC asset address compatible with the exact flow - -So the operational requirement for an already configured profile is only: - -- run the facilitator with a funded seller key -- run the seller surface with the same target profile -- run the buyer policy engine with a funded buyer key on that network -- execute `inspect-x402` and `pay` - -That is a deployment requirement, not a missing architecture requirement. - -For Arc Testnet, the buyer key must hold Arc Testnet USDC. The seller/facilitator key must hold Arc Testnet gas because it submits the x402 exact settlement transaction to the USDC contract. - -To run only the Arc self-hosted exact facilitator: - -```bash -export OMNICLAW_X402_FACILITATOR_PRIVATE_KEY="0xFacilitatorKeyWithArcGas" -bash scripts/start_arc_exact_facilitator.sh -``` - -Equivalent installed CLI: - -```bash -omniclaw facilitator exact \ - --network-profile ARC-TESTNET \ - --network eip155:5042002 \ - --rpc-url https://rpc.testnet.arc.network \ - --port 4022 -``` - -The facilitator exposes: - -| Endpoint | Purpose | -| --- | --- | -| `GET /supported` | Advertise supported x402 schemes and networks | -| `POST /verify` | Verify a signed x402 payment payload | -| `POST /settle` | Submit settlement on Arc Testnet | - -For a visual Arc vendor demo, use the Arc marketplace showcase: - -```bash -bash scripts/start_arc_marketplace_showcase_docker.sh -``` - -Runbook: [../examples/arc-marketplace-showcase/README.md](../examples/arc-marketplace-showcase/README.md). - -The showcase includes a browser mini buyer agent. It calls the kiosk backend, and the kiosk backend calls the buyer Financial Policy Engine using `ARC_MARKETPLACE_BUYER_ENGINE_URL` and `ARC_MARKETPLACE_BUYER_TOKEN`. This keeps the browser flow simple while the Financial Policy Engine remains the payment authority boundary. - -The Docker launcher starts: - -| Service | URL | -| --- | --- | -| Browser UI | `http://127.0.0.1:8020` | -| Vendor kiosk | `http://172.18.0.51:8020` | -| Buyer policy engine | `http://172.18.0.52:8080` | -| Exact facilitator | `http://172.18.0.50:4022` | - -It also prints the buyer Arc USDC balance, seller Arc gas balance, and the paid product URLs: - -| Product | Price | URL | -| --- | --- | --- | -| Prime Market Scan | `$0.25` | `http://172.18.0.51:8020/buy/prime-market-scan` | -| Risk Oracle Brief | `$0.15` | `http://172.18.0.51:8020/buy/risk-oracle-brief` | -| Settlement Receipt Kit | `$0.10` | `http://172.18.0.51:8020/buy/settlement-receipt-kit` | - -For ecosystem forms that require a contract address, use the Arc Testnet USDC contract used by x402 exact settlement: - -```text -0x3600000000000000000000000000000000000000 -``` - -OmniClaw does not require a custom application contract for this flow. The settlement transaction calls `transferWithAuthorization` on Arc Testnet USDC. - -Latest public proof transaction: - -```text -https://testnet.arcscan.app/tx/0xd40dc800a54bee4ff80da4709e65cfd3d0346eb1995ebc34fba433a6306b9219 -``` - -## External Facilitators - -External facilitators remain first-class. If a seller advertises an `exact` payment requirement using another facilitator, OmniClaw's buyer flow can still pay through the standard x402 SDK path as long as: - -- the buyer has the required chain funds -- the seller requirements include a supported `exact` payment option -- the selected facilitator can verify and settle the payload - -The product rule is simple: OmniClaw governs financial authority; facilitators settle supported x402 payment payloads. - -### Thirdweb - -Thirdweb is the recommended managed external facilitator path to validate next. It is a strong fit for teams that want broad EVM network coverage and gas sponsorship without operating their own facilitator. - -Based on Thirdweb's x402 facilitator docs, their facilitator: - -- verifies and submits x402 payments -- uses the seller's Thirdweb server wallet -- supports gasless transaction submission through EIP-7702 -- exposes public HTTP `accepts`, `verify`, `settle`, `fetch`, and discovery endpoints that OmniClaw can call directly from Python -- supports payments across 170+ EVM chains -- supports tokens that expose ERC-2612 permit or ERC-3009 authorization - -How this fits OmniClaw: - -- buyer side: OmniClaw can pay Thirdweb-backed x402 endpoints through the standard `exact` buyer path -- seller side: a team can use Thirdweb's own seller/facilitator stack instead of running an OmniClaw facilitator -- policy layer: OmniClaw still controls whether the agent is allowed to pay before money moves - -Recommended Thirdweb validation flow: - -1. call Thirdweb's HTTP `accepts` endpoint through OmniClaw's Python facilitator adapter to generate seller requirements -2. capture a signed x402 `paymentPayload` and matching `paymentRequirements` -3. call Thirdweb's HTTP `verify` endpoint through OmniClaw's Python facilitator adapter -4. call Thirdweb's HTTP `settle` endpoint through OmniClaw's Python facilitator adapter -5. optionally test Thirdweb's HTTP `fetch` and discovery endpoints for ecosystem integration -6. confirm the transaction in the Thirdweb dashboard and chain explorer -7. run a full buyer flow against a Thirdweb-backed seller URL once seller credentials are available - -The repo includes a direct HTTP validation target at [../examples/thirdweb-http-facilitator/README.md](../examples/thirdweb-http-facilitator/README.md). - -When Thirdweb credentials are available, validate the full flow with the same proof checklist used for other facilitators: seller URL, requirements, payment result, settlement transaction, and final paid response. - -## Operational Model - -For production-like deployments, run the facilitator as separate infrastructure from the Financial Policy Engine. - -Recommended separation: - -- buyer Financial Policy Engine: enforces the buyer's policy and signs buyer-side actions -- seller app: exposes paid resources and advertises x402 requirements -- facilitator: verifies and settles x402 payloads - -This separation matters because it keeps policy, resource serving, and settlement independently deployable. - -## References - -- Arc contract addresses: https://docs.arc.network/arc/references/contract-addresses -- Circle USDC contract addresses: https://developers.circle.com/stablecoins/usdc-contract-addresses -- Thirdweb x402 facilitator: https://portal.thirdweb.com/x402/facilitator -- x402 network and token support: https://docs.x402.org/core-concepts/network-and-token-support diff --git a/docs/omniclaw-cli-skill/SKILL.md b/docs/omniclaw-cli-skill/SKILL.md deleted file mode 100644 index a39d183..0000000 --- a/docs/omniclaw-cli-skill/SKILL.md +++ /dev/null @@ -1,175 +0,0 @@ ---- -name: omniclaw -description: > - Use this skill whenever an agent needs to pay for an x402 URL, transfer USDC - to an address, inspect OmniClaw balances or ledger entries, or explicitly - expose a paid endpoint for other agents or automation with omniclaw-cli serve. - OmniClaw is the - Economic Execution and Control Layer for Agentic Systems. The CLI is the - zero-trust execution layer for agents. Use this skill for the CLI execution - path only, not for vendor SDK integration, owner setup, policy editing, wallet - provisioning, or Financial Policy Engine administration. -metadata: '{"openclaw":{"requires":{"bins":["omniclaw-cli"],"env":["OMNICLAW_SERVER_URL","OMNICLAW_TOKEN"]},"primaryEnv":"OMNICLAW_TOKEN","required_env":["OMNICLAW_SERVER_URL","OMNICLAW_TOKEN"],"optional_env":["OMNICLAW_OWNER_TOKEN"],"required_secrets":["OMNICLAW_TOKEN"],"optional_secrets":["OMNICLAW_OWNER_TOKEN"],"required_binaries":["omniclaw-cli"],"network_access":"required","data_access":"payment URLs, recipient addresses, balances, ledger entries, and paid endpoint responses only when requested","security_notes":"Requires a trusted OmniClaw Financial Policy Engine URL and scoped agent token. OMNICLAW_OWNER_TOKEN is optional and must only be provided intentionally for owner approvals. omniclaw-cli serve binds to 0.0.0.0 and --exec runs a host command, so serve/--exec must only be used after an explicit owner request, preferably inside an isolated runtime."}}' -requires: - - env: OMNICLAW_SERVER_URL - description: > - OmniClaw Financial Policy Engine base URL. Required unless the CLI was - already persisted in local CLI config before the agent turn. - - env: OMNICLAW_TOKEN - description: > - Scoped agent token. Never print, log, or transmit it. If missing, stop - and notify the owner. -version: 0.0.8 -author: Omnuron AI ---- - -# OmniClaw CLI Skill - -## Trigger - -Use `omniclaw-cli` only when the task is directly about one of these actions: - -- pay for a paid URL that returns `402 Payment Required` -- transfer USDC to an address -- inspect wallet, Gateway, or Circle balances -- inspect transaction history -- expose a paid endpoint for other agents or automation with `serve`, only when the owner explicitly asks for it - -Do not use this skill for: - -- editing policy files -- creating wallets -- provisioning secrets -- changing allowlists, limits, or owner approvals outside the exposed CLI commands -- administering the Financial Policy Engine process itself - -## Core Model - -OmniClaw is not just a wallet wrapper. -It is the economic execution and control layer that combines: - -- zero-trust execution through the CLI -- owner-defined financial policy through the Financial Policy Engine -- settlement rails such as direct transfers, x402, CCTP, and Circle Gateway nanopayments - -This skill is specifically about the CLI execution surface. - -The same CLI has two agent-side economic roles: - -- buyer role: `omniclaw-cli pay` -- seller role for agent-run paid endpoints: `omniclaw-cli serve` - -Vendor and enterprise seller APIs should use the Python SDK with `client.sell(...)`, not this CLI skill. - -The agent does not control the private key. -The Financial Policy Engine enforces policy and signs allowed actions. - -## Dependency and Credential Contract - -The runtime must have: - -- `omniclaw-cli` installed from the official OmniClaw package -- `OMNICLAW_SERVER_URL` pointing to the trusted Financial Policy Engine -- `OMNICLAW_TOKEN` scoped to the agent wallet/policy - -Optional: - -- `OMNICLAW_OWNER_TOKEN`, only when the owner intentionally grants approval authority for this run - -Never print tokens, write tokens into generated files, or pass tokens to third-party services. - -## Inputs The Agent Should Expect - -The runtime should normally provide either: - -1. environment-driven execution -- `OMNICLAW_SERVER_URL` -- `OMNICLAW_TOKEN` -- optionally `OMNICLAW_OWNER_TOKEN` if this run is allowed to approve confirmations - -2. persisted CLI config -- `omniclaw-cli configure` was already run before the turn -- the CLI reads saved config values for server URL, token, wallet alias, and optional owner token - -If neither is true, stop and ask the owner for: - -- Financial Policy Engine URL -- agent token -- wallet alias - -Do not invent or search for them yourself. - -## Safe Default Workflow - -### For any new spend - -1. Run `omniclaw-cli status` if connectivity or health is uncertain. -2. Run `omniclaw-cli balance-detail` if Gateway balance matters. -3. Run `omniclaw-cli can-pay --recipient ...` before paying a new recipient. -4. Use `--idempotency-key` for job-based payments. -5. For direct-address payments where budget/guards matter, use `simulate` first. - -### For x402 URLs - -1. Run `omniclaw-cli inspect-x402 --recipient ` before the first live payment to confirm the seller requirements and buyer funding path. -2. Use `omniclaw-cli pay --recipient --idempotency-key `. -3. Add `--method`, `--body`, and `--header` when the paid endpoint expects a non-GET request. -4. Add `--output` if the paid response should be saved. - -### For direct address transfers - -1. Use `omniclaw-cli pay --recipient <0xaddress> --amount `. -2. Always include `--purpose`. - -### For agent-run seller tasks - -1. Inspect current state with `balance-detail`. -2. Confirm the owner explicitly asked this agent to expose a paid endpoint. -3. Start the paid endpoint with `omniclaw-cli serve` only for the approved endpoint, price, command, and port. -4. Remember that `serve` binds to `0.0.0.0` even if the banner prints `localhost`. - -## Serve Safety Rules - -`omniclaw-cli serve` is powerful because it starts a network-accessible service and requires `--exec`. - -Rules: - -- do not run `serve` unless the owner explicitly requested a seller endpoint in the current task -- do not invent the `--exec` command -- do not use `--exec` for shell pipelines, downloads, package installs, destructive commands, or credential access -- prefer an isolated container or private development network for `serve` -- disclose the port and endpoint before treating the service as ready - -## Approval Handling - -If `pay` returns approval-required output, for example: - -- `requires_confirmation: true` -- `confirmation_id: ...` - -Then: - -- do not retry blindly -- do not invent a workaround -- if the run explicitly has owner authority, use `omniclaw-cli confirmations approve --id ` -- otherwise stop and notify the owner - -## Stop Conditions - -Stop and notify the owner if any of these happen: - -- token or Financial Policy Engine URL is missing -- `can-pay` says the recipient is blocked -- `pay` returns a policy or guard rejection -- available or Gateway balance is insufficient -- the exact command or flag is unclear -- `serve` is requested without an explicit owner instruction -- `serve --exec` is requested but the command is not supplied or approved by the owner - -## Command Reference - -For exact command schemas, flags, and live help output, read: - -- `references/cli-reference.md` - -Do not guess flags from memory when a reference is available. diff --git a/docs/omniclaw-cli-skill/agents/openai.yaml b/docs/omniclaw-cli-skill/agents/openai.yaml deleted file mode 100644 index b38b28a..0000000 --- a/docs/omniclaw-cli-skill/agents/openai.yaml +++ /dev/null @@ -1,6 +0,0 @@ -interface: - display_name: "OmniClaw CLI" - short_description: "Policy-controlled agent payments through omniclaw-cli." - default_prompt: "Use $omniclaw to inspect balances, check policy, or pay an x402 URL through the OmniClaw CLI." -policy: - allow_implicit_invocation: false diff --git a/docs/omniclaw-cli-skill/references/cli-reference.md b/docs/omniclaw-cli-skill/references/cli-reference.md deleted file mode 100644 index e466762..0000000 --- a/docs/omniclaw-cli-skill/references/cli-reference.md +++ /dev/null @@ -1,512 +0,0 @@ -# OmniClaw CLI Reference - -This file is generated from the live `omniclaw-cli --help` surface. -Do not hand-edit command schemas here; regenerate instead. - -Generator: - -```bash -python3 scripts/generate_cli_reference.py -``` - -## Usage Notes - -- same CLI, two roles: buyer uses `pay`, seller uses `serve` -- use `can-pay` before a new recipient when policy allow/deny matters -- use `inspect-x402` before a new paid URL when you need to see seller requirements and buyer funding readiness -- use `balance-detail` when Gateway state matters -- use `--idempotency-key` for job-based payments -- for x402 URLs, `--amount` can be omitted because the payment requirements come from the seller endpoint -- `serve` is for owner-approved agent-run seller endpoints only -- `serve` binds to `0.0.0.0` even if the banner prints `localhost` -- `serve --exec` runs a host command; do not invent the command or expose it outside an isolated runtime - -## Example Flows - -Buyer paying an x402 endpoint: - -```bash -omniclaw-cli can-pay --recipient http://seller-host:8000/api/data -omniclaw-cli inspect-x402 --recipient http://seller-host:8000/api/data -omniclaw-cli pay --recipient http://seller-host:8000/api/data --idempotency-key job-123 -``` - -Buyer paying a direct address: - -```bash -omniclaw-cli pay \ - --recipient 0xRecipientAddress \ - --amount 5.00 \ - --purpose "service payment" \ - --idempotency-key job-123 -``` - -Seller exposing a paid endpoint: - -```bash -omniclaw-cli serve \ - --price 0.01 \ - --endpoint /api/data \ - --exec "python safe_readonly_service.py" \ - --port 8000 -``` - -Only run this after the owner explicitly asks for an agent-run seller endpoint and supplies or approves the `--exec` command. - -## Live Help Output - -### `omniclaw-cli --help` - -```text - - Usage: omniclaw-cli [OPTIONS] COMMAND [ARGS]... - - omniclaw-cli - zero-trust execution layer for policy-controlled agent - payments, x402 services, and agentic commerce - -╭─ Options ────────────────────────────────────────────────────────────────────╮ -│ --install-completion Install completion for the current shell. │ -│ --show-completion Show completion for the current shell, to copy │ -│ it or customize the installation. │ -│ --help Show this message and exit. │ -╰──────────────────────────────────────────────────────────────────────────────╯ -╭─ Commands ───────────────────────────────────────────────────────────────────╮ -│ configure Configure omniclaw-cli with server details. │ -│ address Get wallet address. │ -│ balance Get wallet balance. │ -│ balance-detail Get detailed balance including Gateway and │ -│ Circle wallet. │ -│ balance_detail Alias for balance-detail │ -│ deposit Deposit USDC from EOA to Gateway wallet. │ -│ withdraw Withdraw USDC from Gateway to Circle Developer │ -│ Wallet. │ -│ withdraw-trustless Initiate trustless withdrawal (~7-day delay, │ -│ no API needed). │ -│ withdraw_trustless Alias for withdraw-trustless │ -│ withdraw-trustless-complete Complete a trustless withdrawal after the │ -│ delay has passed. │ -│ withdraw_trustless_complete Alias for withdraw-trustless-complete │ -│ pay Execute a payment or pay for an x402 service. │ -│ simulate Simulate a payment without executing. │ -│ inspect-x402 Inspect an x402 endpoint and show which │ -│ payment route OmniClaw would use. │ -│ inspect_x402 Alias for inspect-x402 │ -│ can-pay Check if recipient is allowed. │ -│ can_pay Alias for can-pay │ -│ create-intent Create a payment intent (authorize). │ -│ create_intent Alias for create-intent │ -│ confirm-intent Confirm a payment intent (capture). │ -│ confirm_intent Alias for confirm-intent │ -│ get-intent Get a payment intent. │ -│ get_intent Alias for get-intent │ -│ cancel-intent Cancel a payment intent. │ -│ cancel_intent Alias for cancel-intent │ -│ ledger List transaction history. │ -│ list-tx List transaction history. │ -│ list_tx Alias for list-tx │ -│ serve Expose a local service behind an x402 payment │ -│ gate. │ -│ status Get agent status and health. │ -│ ping Health check. │ -│ wallet Wallet operations │ -│ intents Payment intents │ -│ confirmations Manage pending confirmations (owner only) │ -╰──────────────────────────────────────────────────────────────────────────────╯ -``` - -### `omniclaw-cli configure --help` - -```text - - Usage: omniclaw-cli configure [OPTIONS] - - Configure omniclaw-cli with server details. - -╭─ Options ────────────────────────────────────────────────────────────────────╮ -│ --server-url TEXT OmniClaw Financial Policy Engine URL │ -│ --token TEXT Agent token │ -│ --wallet TEXT Wallet alias │ -│ --owner-token TEXT Owner token │ -│ --show Show current config │ -│ --show-raw Show raw secrets │ -│ --interactive Prompt for missing values │ -│ --help Show this message and exit. │ -╰──────────────────────────────────────────────────────────────────────────────╯ -``` - -### `omniclaw-cli status --help` - -```text - - Usage: omniclaw-cli status [OPTIONS] - - Get agent status and health. - -╭─ Options ────────────────────────────────────────────────────────────────────╮ -│ --help Show this message and exit. │ -╰──────────────────────────────────────────────────────────────────────────────╯ -``` - -### `omniclaw-cli ping --help` - -```text - - Usage: omniclaw-cli ping [OPTIONS] - - Health check. - -╭─ Options ────────────────────────────────────────────────────────────────────╮ -│ --help Show this message and exit. │ -╰──────────────────────────────────────────────────────────────────────────────╯ -``` - -### `omniclaw-cli address --help` - -```text - - Usage: omniclaw-cli address [OPTIONS] - - Get wallet address. - -╭─ Options ────────────────────────────────────────────────────────────────────╮ -│ --help Show this message and exit. │ -╰──────────────────────────────────────────────────────────────────────────────╯ -``` - -### `omniclaw-cli balance --help` - -```text - - Usage: omniclaw-cli balance [OPTIONS] - - Get wallet balance. - -╭─ Options ────────────────────────────────────────────────────────────────────╮ -│ --help Show this message and exit. │ -╰──────────────────────────────────────────────────────────────────────────────╯ -``` - -### `omniclaw-cli balance-detail --help` - -```text - - Usage: omniclaw-cli balance-detail [OPTIONS] - - Get detailed balance including Gateway and Circle wallet. - -╭─ Options ────────────────────────────────────────────────────────────────────╮ -│ --help Show this message and exit. │ -╰──────────────────────────────────────────────────────────────────────────────╯ -``` - -### `omniclaw-cli can-pay --help` - -```text - - Usage: omniclaw-cli can-pay [OPTIONS] - - Check if recipient is allowed. - -╭─ Options ────────────────────────────────────────────────────────────────────╮ -│ * --recipient TEXT Recipient to check [required] │ -│ --help Show this message and exit. │ -╰──────────────────────────────────────────────────────────────────────────────╯ -``` - -### `omniclaw-cli inspect-x402 --help` - -```text - - Usage: omniclaw-cli inspect-x402 [OPTIONS] - - Inspect an x402 endpoint and show which payment route OmniClaw would use. - -╭─ Options ────────────────────────────────────────────────────────────────────╮ -│ * --recipient TEXT x402 URL to inspect [required] │ -│ --amount TEXT Optional max amount in USDC │ -│ --method TEXT HTTP method for x402 requests [default: GET] │ -│ --body TEXT Request body for x402 requests │ -│ --header TEXT Additional headers for x402 requests │ -│ --help Show this message and exit. │ -╰──────────────────────────────────────────────────────────────────────────────╯ -``` - -### `omniclaw-cli simulate --help` - -```text - - Usage: omniclaw-cli simulate [OPTIONS] - - Simulate a payment without executing. - -╭─ Options ────────────────────────────────────────────────────────────────────╮ -│ * --recipient TEXT Recipient to simulate [required] │ -│ * --amount TEXT Amount to simulate [required] │ -│ --idempotency-key TEXT Idempotency key │ -│ --destination-chain TEXT Target network │ -│ --fee-level TEXT Gas fee level (LOW, MEDIUM, HIGH) │ -│ --check-trust Run Trust Gate check │ -│ --skip-guards Skip guards (OWNER ONLY) │ -│ --help Show this message and exit. │ -╰──────────────────────────────────────────────────────────────────────────────╯ -``` - -### `omniclaw-cli pay --help` - -```text - - Usage: omniclaw-cli pay [OPTIONS] - - Execute a payment or pay for an x402 service. - -╭─ Options ────────────────────────────────────────────────────────────────────╮ -│ * --recipient TEXT Payment recipient (address or URL) │ -│ [required] │ -│ --amount TEXT Amount in USDC (optional for x402 URLs) │ -│ --purpose TEXT Payment purpose │ -│ --idempotency-key TEXT Idempotency key │ -│ --destination-chain TEXT Target network │ -│ --fee-level TEXT Gas fee level (LOW, MEDIUM, HIGH) │ -│ --check-trust Run Trust Gate check │ -│ --skip-guards Skip guards (OWNER ONLY) │ -│ --method TEXT HTTP method for x402 requests │ -│ [default: GET] │ -│ --body TEXT JSON body for x402 requests │ -│ --header TEXT Additional headers for x402 requests │ -│ --output TEXT Save response to file │ -│ --dry-run Simulate first │ -│ --help Show this message and exit. │ -╰──────────────────────────────────────────────────────────────────────────────╯ -``` - -### `omniclaw-cli deposit --help` - -```text - - Usage: omniclaw-cli deposit [OPTIONS] - - Deposit USDC from EOA to Gateway wallet. - -╭─ Options ────────────────────────────────────────────────────────────────────╮ -│ * --amount TEXT Amount in USDC to deposit to Gateway [required] │ -│ --help Show this message and exit. │ -╰──────────────────────────────────────────────────────────────────────────────╯ -``` - -### `omniclaw-cli withdraw --help` - -```text - - Usage: omniclaw-cli withdraw [OPTIONS] - - Withdraw USDC from Gateway to Circle Developer Wallet. - -╭─ Options ────────────────────────────────────────────────────────────────────╮ -│ * --amount TEXT Amount in USDC to withdraw from Gateway [required] │ -│ --help Show this message and exit. │ -╰──────────────────────────────────────────────────────────────────────────────╯ -``` - -### `omniclaw-cli withdraw-trustless --help` - -```text - - Usage: omniclaw-cli withdraw-trustless [OPTIONS] - - Initiate trustless withdrawal (~7-day delay, no API needed). - -╭─ Options ────────────────────────────────────────────────────────────────────╮ -│ * --amount TEXT Amount in USDC to withdraw (trustless, ~7-day │ -│ delay) │ -│ [required] │ -│ --help Show this message and exit. │ -╰──────────────────────────────────────────────────────────────────────────────╯ -``` - -### `omniclaw-cli withdraw-trustless-complete --help` - -```text - - Usage: omniclaw-cli withdraw-trustless-complete [OPTIONS] - - Complete a trustless withdrawal after the delay has passed. - -╭─ Options ────────────────────────────────────────────────────────────────────╮ -│ --help Show this message and exit. │ -╰──────────────────────────────────────────────────────────────────────────────╯ -``` - -### `omniclaw-cli serve --help` - -```text - - Usage: omniclaw-cli serve [OPTIONS] - - Expose a local service behind an x402 payment gate. - - Uses the production GatewayMiddleware for full x402 v2 protocol compliance: - - Returns proper 402 responses with all required fields - - Parses PAYMENT-SIGNATURE headers - - Settles atomically via Circle Gateway /settle - -╭─ Options ────────────────────────────────────────────────────────────────────╮ -│ * --price FLOAT Price per request in USDC [required] │ -│ * --endpoint TEXT Endpoint path to expose [required] │ -│ * --exec TEXT Command to execute on success [required] │ -│ --port INTEGER Local port to listen on [default: 8000] │ -│ --help Show this message and exit. │ -╰──────────────────────────────────────────────────────────────────────────────╯ -``` - -### `omniclaw-cli create-intent --help` - -```text - - Usage: omniclaw-cli create-intent [OPTIONS] - - Create a payment intent (authorize). - -╭─ Options ────────────────────────────────────────────────────────────────────╮ -│ * --recipient TEXT Recipient [required] │ -│ * --amount TEXT Amount [required] │ -│ --purpose TEXT Purpose │ -│ --expires-in INTEGER Expiry in seconds │ -│ --idempotency-key TEXT Idempotency key │ -│ --destination-chain TEXT Target network │ -│ --fee-level TEXT Gas fee level (LOW, MEDIUM, HIGH) │ -│ --check-trust Run Trust Gate check │ -│ --skip-guards Skip guards (OWNER ONLY) │ -│ --help Show this message and exit. │ -╰──────────────────────────────────────────────────────────────────────────────╯ -``` - -### `omniclaw-cli confirm-intent --help` - -```text - - Usage: omniclaw-cli confirm-intent [OPTIONS] - - Confirm a payment intent (capture). - -╭─ Options ────────────────────────────────────────────────────────────────────╮ -│ * --intent-id TEXT Intent ID to confirm [required] │ -│ --help Show this message and exit. │ -╰──────────────────────────────────────────────────────────────────────────────╯ -``` - -### `omniclaw-cli get-intent --help` - -```text - - Usage: omniclaw-cli get-intent [OPTIONS] - - Get a payment intent. - -╭─ Options ────────────────────────────────────────────────────────────────────╮ -│ * --intent-id TEXT Intent ID to fetch [required] │ -│ --help Show this message and exit. │ -╰──────────────────────────────────────────────────────────────────────────────╯ -``` - -### `omniclaw-cli cancel-intent --help` - -```text - - Usage: omniclaw-cli cancel-intent [OPTIONS] - - Cancel a payment intent. - -╭─ Options ────────────────────────────────────────────────────────────────────╮ -│ * --intent-id TEXT Intent ID to cancel [required] │ -│ --reason TEXT Cancel reason │ -│ --help Show this message and exit. │ -╰──────────────────────────────────────────────────────────────────────────────╯ -``` - -### `omniclaw-cli ledger --help` - -```text - - Usage: omniclaw-cli ledger [OPTIONS] - - List transaction history. - -╭─ Options ────────────────────────────────────────────────────────────────────╮ -│ --limit INTEGER Number of transactions to fetch [default: 20] │ -│ --help Show this message and exit. │ -╰──────────────────────────────────────────────────────────────────────────────╯ -``` - -### `omniclaw-cli list-tx --help` - -```text - - Usage: omniclaw-cli list-tx [OPTIONS] - - List transaction history. - -╭─ Options ────────────────────────────────────────────────────────────────────╮ -│ --limit INTEGER Number of transactions to fetch [default: 20] │ -│ --help Show this message and exit. │ -╰──────────────────────────────────────────────────────────────────────────────╯ -``` - -### `omniclaw-cli confirmations --help` - -```text - - Usage: omniclaw-cli confirmations [OPTIONS] COMMAND [ARGS]... - - Manage pending confirmations (owner only) - -╭─ Options ────────────────────────────────────────────────────────────────────╮ -│ --help Show this message and exit. │ -╰──────────────────────────────────────────────────────────────────────────────╯ -╭─ Commands ───────────────────────────────────────────────────────────────────╮ -│ get Get confirmation details. │ -│ approve Approve a confirmation. │ -│ deny Deny a confirmation. │ -╰──────────────────────────────────────────────────────────────────────────────╯ -``` - -### `omniclaw-cli confirmations get --help` - -```text - - Usage: omniclaw-cli confirmations get [OPTIONS] - - Get confirmation details. - -╭─ Options ────────────────────────────────────────────────────────────────────╮ -│ * --id TEXT Confirmation ID [required] │ -│ --help Show this message and exit. │ -╰──────────────────────────────────────────────────────────────────────────────╯ -``` - -### `omniclaw-cli confirmations approve --help` - -```text - - Usage: omniclaw-cli confirmations approve [OPTIONS] - - Approve a confirmation. - -╭─ Options ────────────────────────────────────────────────────────────────────╮ -│ * --id TEXT Confirmation ID [required] │ -│ --help Show this message and exit. │ -╰──────────────────────────────────────────────────────────────────────────────╯ -``` - -### `omniclaw-cli confirmations deny --help` - -```text - - Usage: omniclaw-cli confirmations deny [OPTIONS] - - Deny a confirmation. - -╭─ Options ────────────────────────────────────────────────────────────────────╮ -│ * --id TEXT Confirmation ID [required] │ -│ --help Show this message and exit. │ -╰──────────────────────────────────────────────────────────────────────────────╯ -``` diff --git a/docs/omniclaw-cli-skill/scripts/generate_cli_reference.py b/docs/omniclaw-cli-skill/scripts/generate_cli_reference.py deleted file mode 100755 index 6ca2857..0000000 --- a/docs/omniclaw-cli-skill/scripts/generate_cli_reference.py +++ /dev/null @@ -1,153 +0,0 @@ -#!/usr/bin/env python3 -from __future__ import annotations - -import os -import subprocess -from pathlib import Path - -REPO_ROOT = Path(__file__).resolve().parents[3] -HUMAN_REF = REPO_ROOT / "docs" / "cli-reference.md" - -COMMANDS = [ - ("omniclaw-cli --help", ["omniclaw-cli", "--help"]), - ("omniclaw-cli configure --help", ["omniclaw-cli", "configure", "--help"]), - ("omniclaw-cli status --help", ["omniclaw-cli", "status", "--help"]), - ("omniclaw-cli ping --help", ["omniclaw-cli", "ping", "--help"]), - ("omniclaw-cli address --help", ["omniclaw-cli", "address", "--help"]), - ("omniclaw-cli balance --help", ["omniclaw-cli", "balance", "--help"]), - ("omniclaw-cli balance-detail --help", ["omniclaw-cli", "balance-detail", "--help"]), - ("omniclaw-cli can-pay --help", ["omniclaw-cli", "can-pay", "--help"]), - ("omniclaw-cli inspect-x402 --help", ["omniclaw-cli", "inspect-x402", "--help"]), - ("omniclaw-cli simulate --help", ["omniclaw-cli", "simulate", "--help"]), - ("omniclaw-cli pay --help", ["omniclaw-cli", "pay", "--help"]), - ("omniclaw-cli deposit --help", ["omniclaw-cli", "deposit", "--help"]), - ("omniclaw-cli withdraw --help", ["omniclaw-cli", "withdraw", "--help"]), - ("omniclaw-cli withdraw-trustless --help", ["omniclaw-cli", "withdraw-trustless", "--help"]), - ("omniclaw-cli withdraw-trustless-complete --help", ["omniclaw-cli", "withdraw-trustless-complete", "--help"]), - ("omniclaw-cli serve --help", ["omniclaw-cli", "serve", "--help"]), - ("omniclaw-cli create-intent --help", ["omniclaw-cli", "create-intent", "--help"]), - ("omniclaw-cli confirm-intent --help", ["omniclaw-cli", "confirm-intent", "--help"]), - ("omniclaw-cli get-intent --help", ["omniclaw-cli", "get-intent", "--help"]), - ("omniclaw-cli cancel-intent --help", ["omniclaw-cli", "cancel-intent", "--help"]), - ("omniclaw-cli ledger --help", ["omniclaw-cli", "ledger", "--help"]), - ("omniclaw-cli list-tx --help", ["omniclaw-cli", "list-tx", "--help"]), - ("omniclaw-cli confirmations --help", ["omniclaw-cli", "confirmations", "--help"]), - ("omniclaw-cli confirmations get --help", ["omniclaw-cli", "confirmations", "get", "--help"]), - ("omniclaw-cli confirmations approve --help", ["omniclaw-cli", "confirmations", "approve", "--help"]), - ("omniclaw-cli confirmations deny --help", ["omniclaw-cli", "confirmations", "deny", "--help"]), -] - -FALLBACK_OUTPUTS = { - "omniclaw-cli balance --help": """\ -Usage: omniclaw-cli balance [OPTIONS] - -Get wallet balance. - -Options: - --help Show this message and exit. -""", - "omniclaw-cli balance-detail --help": """\ -Usage: omniclaw-cli balance-detail [OPTIONS] - -Get detailed balance including Gateway and Circle wallet. - -Options: - --help Show this message and exit. -""", -} - -TIMEOUT_SECONDS = 25 - -HEADER = """# OmniClaw CLI Reference - -This file is generated from the live `omniclaw-cli --help` surface. -Do not hand-edit command schemas here; regenerate instead. - -Generator: - -```bash -python3 scripts/generate_cli_reference.py -``` - -## Usage Notes - -- same CLI, two roles: buyer uses `pay`, seller uses `serve` -- use `can-pay` before a new recipient when policy allow/deny matters -- use `inspect-x402` before a new paid URL when you need to see seller requirements and buyer funding readiness -- use `balance-detail` when Gateway state matters -- use `--idempotency-key` for job-based payments -- for x402 URLs, `--amount` can be omitted because the payment requirements come from the seller endpoint -- `serve` is for owner-approved agent-run seller endpoints only -- `serve` binds to `0.0.0.0` even if the banner prints `localhost` -- `serve --exec` runs a host command; do not invent the command or expose it outside an isolated runtime - -## Example Flows - -Buyer paying an x402 endpoint: - -```bash -omniclaw-cli can-pay --recipient http://seller-host:8000/api/data -omniclaw-cli inspect-x402 --recipient http://seller-host:8000/api/data -omniclaw-cli pay --recipient http://seller-host:8000/api/data --idempotency-key job-123 -``` - -Buyer paying a direct address: - -```bash -omniclaw-cli pay \\ - --recipient 0xRecipientAddress \\ - --amount 5.00 \\ - --purpose "service payment" \\ - --idempotency-key job-123 -``` - -Seller exposing a paid endpoint: - -```bash -omniclaw-cli serve \\ - --price 0.01 \\ - --endpoint /api/data \\ - --exec "python safe_readonly_service.py" \\ - --port 8000 -``` - -Only run this after the owner explicitly asks for an agent-run seller endpoint and supplies or approves the `--exec` command. - -## Live Help Output -""" - - -def run_help(title: str, args: list[str]) -> str: - try: - proc = subprocess.run( - args, - stdout=subprocess.PIPE, - stderr=subprocess.STDOUT, - text=True, - timeout=TIMEOUT_SECONDS, - check=False, - env={"OMNICLAW_CLI_NO_BANNER": "1", **os.environ}, - ) - except subprocess.TimeoutExpired: - fallback = FALLBACK_OUTPUTS.get(title) - if fallback: - return f"{fallback.rstrip()}\n\n[live help timed out after {TIMEOUT_SECONDS}s; schema filled from source]" - return f"[help command timed out after {TIMEOUT_SECONDS}s]" - - output = (proc.stdout or "").rstrip() - if proc.returncode == 0: - return output - - if output: - return f"{output}\n\n[exit code: {proc.returncode}]" - return f"[help command failed with exit code {proc.returncode}]" - - -sections: list[str] = [HEADER.rstrip()] -for title, args in COMMANDS: - output = run_help(title, args) - sections.append(f"### `{title}`\n\n```text\n{output}\n```") - -content = "\n\n".join(sections) + "\n" -HUMAN_REF.write_text(content) -print(f"wrote {HUMAN_REF}") diff --git a/docs/omniclaw-cli-skill/scripts/generate_cli_reference.sh b/docs/omniclaw-cli-skill/scripts/generate_cli_reference.sh deleted file mode 100755 index 0188225..0000000 --- a/docs/omniclaw-cli-skill/scripts/generate_cli_reference.sh +++ /dev/null @@ -1,6 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" -cd "$ROOT_DIR" -python3 scripts/generate_cli_reference.py diff --git a/docs/operator-cli.md b/docs/operator-cli.md index f11fbe4..76bbbda 100644 --- a/docs/operator-cli.md +++ b/docs/operator-cli.md @@ -1,56 +1,27 @@ -# Operator CLI +# Core CLI -OmniClaw ships two command surfaces: +`omniclaw` and `omniclaw-cli` are aliases for the same buyer/core CLI surface. +They configure a local agent client, inspect x402 endpoints, submit buyer payments, +and query wallet, intent, ledger, and confirmation state. -- `omniclaw` for infrastructure and control-plane services -- `omniclaw-cli` for agent-side financial execution - -Use `omniclaw` when you are running the policy engine, setup flow, or facilitator infrastructure: +Start the policy engine with Docker Compose: ```bash -omniclaw setup --api-key "$CIRCLE_API_KEY" --entity-secret "$ENTITY_SECRET" -omniclaw server --port 8080 -omniclaw facilitator exact --network-profile ARC-TESTNET --port 4022 -omniclaw policy lint --path ./policy.json -omniclaw env -omniclaw doctor +docker compose up --build omniclaw-agent ``` -`--entity-secret` is optional only when this Circle API key/account has not created one yet. Circle allows one active Entity Secret per account/API key. If you already have it, pass it directly; if you omit it and OmniClaw cannot find one in env or managed config, setup will generate and register a new one. - -Use `omniclaw-cli` when an agent is performing constrained financial actions: +Then configure the CLI: ```bash -omniclaw-cli can-pay --recipient https://seller.example.com/compute -omniclaw-cli inspect-x402 --recipient https://seller.example.com/compute -omniclaw-cli pay --recipient https://seller.example.com/compute --idempotency-key job-123 -omniclaw-cli serve --price 0.01 --endpoint /api/data --exec "python safe_readonly_service.py" +omniclaw configure --server-url http://localhost:8080 --token agent-token ``` -`omniclaw-cli serve` is the agent-facing seller surface. Use it when an agent needs to expose a paid endpoint for other agents or automation. Vendor and enterprise APIs that live inside application code should use the Python SDK seller middleware (`client.sell(...)`) instead. - -`serve` binds to all interfaces and `--exec` runs the supplied host command. Treat it as an explicit owner-approved action, preferably in an isolated runtime. - -## Responsibility Split - -The infrastructure CLI manages trusted configuration and settlement services. The agent CLI executes through the Financial Policy Engine and never needs raw wallet authority. +## Buyer CLI -This split is central to OmniClaw: - -- tools expose what the agent can try to do -- the Financial Policy Engine governs what financial authority the agent actually has -- facilitators settle valid x402 payment payloads on supported rails - -## Self-Hosted Facilitator - -The operator CLI includes a first-class facilitator command: +`omniclaw-cli` remains available for compatibility: ```bash -export OMNICLAW_X402_FACILITATOR_PRIVATE_KEY="0x..." - -omniclaw facilitator exact \ - --network-profile ARC-TESTNET \ - --port 4022 +omniclaw-cli can-pay --recipient https://paid.example.com/compute +omniclaw-cli inspect-x402 --recipient https://paid.example.com/compute +omniclaw-cli pay --recipient https://paid.example.com/compute --idempotency-key job-123 ``` - -For the full facilitator guide, see [facilitators.md](facilitators.md). diff --git a/docs/production-readiness.md b/docs/production-readiness.md index 69c8f8d..b9a82b7 100644 --- a/docs/production-readiness.md +++ b/docs/production-readiness.md @@ -1,128 +1,35 @@ # Production Readiness -This is the short production checklist for OmniClaw. +This checklist is for OmniClaw core: buyer-side agent payment infrastructure, policy controls, wallet routing, and x402 buyer execution. -Use it before publishing a release, running a pilot, or validating a real external payment flow. +## Core Readiness -## What OmniClaw Is +- `omniclaw-cli can-pay` works for the target wallet and policy. +- `omniclaw-cli inspect-x402` reports the payment requirements and selected buyer route. +- `omniclaw-cli pay` executes through `/api/v1/pay`. +- Policy blocks unsafe recipients before money moves. +- Idempotency keys are supplied for production payment calls. +- Gateway payments require Gateway readiness before selecting `GatewayWalletBatched`. +- Standard exact x402 payments use the upstream x402 SDK path. +- Ledger, intent, and webhook records are available for audit. -OmniClaw is the policy-controlled execution layer for agent payments. +## Validation -It does four things: +For a production canary, capture: -- inspects what a seller accepts -- enforces buyer policy before money moves -- routes to a compatible payment rail -- records what happened for audit and operations - -## Facilitator Strategy - -OmniClaw is facilitator-aware. Sellers can use the settlement path that fits their deployment while buyers keep OmniClaw policy controls in front of money movement. - -Supported facilitator paths: - -- x402.org can validate external standard exact settlement immediately on Base Sepolia -- Thirdweb can provide broad gas-sponsored x402 settlement -- Circle Gateway can provide batched gasless nanopayments -- x402.org or other facilitators can support standard exact settlement -- OmniClaw self-hosted exact facilitator is available for Arc, custom networks, and self-hosted control - -## Validating Deployment Readiness - -For any production environment deployment, we recommend verifying: - -- seller URL +- paid resource URL - `inspect-x402` output - `pay` output - transaction hash or settlement ID -- dashboard/explorer screenshot - policy file used for the buyer - -Ensure this validation checklist is complete before moving to production. - -## Buyer Readiness - -The buyer path is ready when: - -- `omniclaw-cli can-pay` works -- `omniclaw-cli inspect-x402` reports the selected route -- `omniclaw-cli pay` uses `/api/v1/pay` -- policy blocks unsafe recipients before settlement -- exact x402 payments use the standard x402 SDK path -- Gateway payments require Gateway readiness before selecting `GatewayWalletBatched` - -## Seller Readiness - -The seller path is ready when: - -- seller advertises correct x402 requirements -- seller does not leak Gateway metadata into non-Gateway exact flows -- paid response unlocks only after settlement -- settlement status is visible in logs and response metadata - -## Facilitator Strategy - -Recommended facilitator strategy: - -- x402.org first for external exact validation on Base Sepolia -- Thirdweb next for managed external x402 validation once account access is available -- Circle Gateway for batched nanopayments -- external exact facilitators where seller requirements support them -- OmniClaw self-hosted exact facilitator for Arc, custom networks, and self-hosted enterprise deployments - -Operational split: - -- seller surface creates `accepts` -- facilitator verifies and settles -- buyer policy engine decides whether payment is allowed and which route is selected - -Keep these layers separate in deployment docs, system design, and product claims. - -## Current Supported Capabilities - -OmniClaw officially supports: - -- Base Sepolia external exact via x402.org: fully supported -- buyer exact x402 path via `/api/v1/pay`: fully supported -- seller exact route advertising correct `payTo`: fully supported -- OmniClaw self-hosted exact facilitator: fully supported on Arc Testnet and EVM profiles -- Arc exact profile: fully supported with self-hosted facilitator settlement -- Thirdweb HTTP integration: fully supported for `accepts`, `verify`, `settle`, `fetch`, and discovery; requires managed Thirdweb account configuration +- dashboard or explorer evidence ## Release Gate -Run before shipping: +Run before shipping core changes: ```bash uv sync --extra dev -uv run pytest \ - tests/test_setup.py \ - tests/test_payment_intents.py \ - tests/test_client.py \ - tests/test_webhook_verification.py - -python3 -m py_compile \ - src/omniclaw/seller/facilitator_generic.py \ - examples/thirdweb-http-facilitator/verify_settle.py \ - src/omniclaw/admin_cli.py \ - src/omniclaw/facilitator/exact.py \ - src/omniclaw/facilitator/networks.py \ - scripts/verify_release_artifact.py - +uv run pytest -q python3 scripts/release_verify.sh ``` - -If you are validating exact-flow deployment coverage, run the current smoke slice after syncing dependencies: - -```bash -uv run pytest \ - tests/test_facilitator_e2e.py \ - tests/test_cli_facilitator.py \ - tests/test_cctp_constants.py \ - tests/test_exact_network_profiles.py \ - tests/test_exact_facilitator_app.py \ - tests/test_x402_sdk_adapter.py \ - -q -``` - -This exact-flow slice currently depends on an `x402` build that exposes `x402.schemas`. diff --git a/examples/agent/docker-compose.yml b/examples/agent/docker-compose.yml index 82ed92b..25a505e 100644 --- a/examples/agent/docker-compose.yml +++ b/examples/agent/docker-compose.yml @@ -15,13 +15,3 @@ services: - ./policy.json:/config/policy.json ports: - "9080:8080" - - seller: - build: - context: ../.. - dockerfile: Dockerfile.agent - command: uv run python3 examples/agent/seller.py - environment: - - OMNICLAW_RPC_URL=https://ethereum-sepolia-rpc.publicnode.com - ports: - - "9001:8001" diff --git a/examples/agent/seller/docker-compose.yml b/examples/agent/seller/docker-compose.yml deleted file mode 100644 index d4ae305..0000000 --- a/examples/agent/seller/docker-compose.yml +++ /dev/null @@ -1,36 +0,0 @@ -services: - redis: - image: redis:7-alpine - command: redis-server --appendonly yes --appendfsync everysec - volumes: - - redis-data:/data - healthcheck: - test: ["CMD", "redis-cli", "ping"] - interval: 5s - timeout: 3s - retries: 10 - - omniclaw-agent: - build: - context: ../../.. - dockerfile: Dockerfile.agent - command: uv run uvicorn omniclaw.agent.server:app --host 0.0.0.0 --port 9090 - env_file: ../../../.env - environment: - - OMNICLAW_REDIS_URL=redis://redis:6379/0 - - OMNICLAW_AGENT_POLICY_PATH=/config/policy.json - - OMNICLAW_LOG_LEVEL=INFO - - OMNICLAW_AGENT_TOKEN=seller-agent-token - - CIRCLE_API_KEY=${SELLER_CIRCLE_API_KEY} - - OMNICLAW_PRIVATE_KEY=${SELLER_OMNICLAW_PRIVATE_KEY} - - ENTITY_SECRET=${SELLER_ENTITY_SECRET} - volumes: - - ./policy.json:/config/policy.json - ports: - - "9090:9090" - depends_on: - redis: - condition: service_healthy - -volumes: - redis-data: diff --git a/examples/agent/seller/policy.json b/examples/agent/seller/policy.json deleted file mode 100644 index f2842a3..0000000 --- a/examples/agent/seller/policy.json +++ /dev/null @@ -1,46 +0,0 @@ -{ - "version": "2.0", - "tokens": { - "seller-agent-token": { - "wallet_alias": "seller-wallet", - "active": true, - "label": "Seller Agent" - } - }, - "wallets": { - "seller-wallet": { - "name": "Seller Wallet", - "wallet_id": "eacf1510-18e4-5b99-8fd4-3cc0c1965442", - "address": "0x5a4e248fa08c37b15ea0efdfdf336e92317d5243", - "limits": { - "daily_max": "1000.00", - "hourly_max": null, - "per_tx_max": "100.00", - "per_tx_min": null - }, - "rate_limits": null, - "recipients": { - "mode": "allow_all", - "addresses": [], - "domains": [] - }, - "confirm_threshold": null - } - }, - "limits": { - "daily_max": null, - "hourly_max": null, - "per_tx_max": null, - "per_tx_min": null - }, - "rate_limits": { - "per_minute": null, - "per_hour": null - }, - "recipients": { - "mode": "allow_all", - "addresses": [], - "domains": [] - }, - "confirm_threshold": null -} \ No newline at end of file diff --git a/examples/agent/seller/start.sh b/examples/agent/seller/start.sh deleted file mode 100755 index a8440be..0000000 --- a/examples/agent/seller/start.sh +++ /dev/null @@ -1,8 +0,0 @@ -#!/bin/bash -export CIRCLE_API_KEY=${CIRCLE_API_KEY:-} -export OMNICLAW_PRIVATE_KEY=${OMNICLAW_PRIVATE_KEY:-} -export OMNICLAW_RPC_URL=${OMNICLAW_RPC_URL:-https://ethereum-sepolia-rpc.publicnode.com} -export OMNICLAW_NETWORK=${OMNICLAW_NETWORK:-ETH-SEPOLIA} -export OMNICLAW_AGENT_TOKEN=${OMNICLAW_AGENT_TOKEN:-seller-agent-token} -export OMNICLAW_AGENT_POLICY_PATH=${OMNICLAW_AGENT_POLICY_PATH:-$(pwd)/examples/agent/seller/policy.json} -exec uv run uvicorn omniclaw.agent.server:app --host 0.0.0.0 --port 8081 diff --git a/examples/arc-marketplace-showcase/README.md b/examples/arc-marketplace-showcase/README.md deleted file mode 100644 index bd55cfb..0000000 --- a/examples/arc-marketplace-showcase/README.md +++ /dev/null @@ -1,263 +0,0 @@ -# Arc Marketplace Showcase - -This showcase is a visual vendor marketplace for Arc Testnet settlement. - -It is intentionally not a text-heavy demo. The browser UI represents the vendor as a kiosk with paid services. A buyer agent selects a paid URL, OmniClaw enforces buyer policy, x402 `exact` settles on Arc Testnet, and the vendor unlocks the result. - -## What It Proves - -- A vendor can expose multiple paid services from one marketplace surface. -- The seller advertises standard x402 `exact` requirements for Arc Testnet. -- The buyer pays through OmniClaw policy control instead of raw wallet access. -- The self-hosted OmniClaw exact facilitator verifies and settles on Arc. -- The settlement transaction can be opened on ArcScan. - -## Components - -| Component | Role | -| --- | --- | -| Kiosk vendor app | Marketplace UI and paid product endpoints | -| OmniClaw exact facilitator | Self-hosted x402 `verify` and `settle` service | -| Buyer Financial Policy Engine | Policy-controlled payment executor for OpenClaw or `omniclaw-cli` | -| ArcScan | External proof that settlement happened on Arc Testnet | - -## Standalone Facilitator - -Run this when you only need the self-hosted x402 exact facilitator for Arc: - -```bash -export OMNICLAW_X402_FACILITATOR_PRIVATE_KEY="0xFacilitatorKeyWithArcGas" -bash scripts/start_arc_exact_facilitator.sh -``` - -Equivalent installed CLI: - -```bash -omniclaw facilitator exact \ - --network-profile ARC-TESTNET \ - --network eip155:5042002 \ - --rpc-url https://rpc.testnet.arc.network \ - --port 4022 -``` - -The facilitator exposes `GET /supported`, `POST /verify`, and `POST /settle`. - -## Start The Vendor Kiosk - -### Recommended: Docker Clean Slate - -Use the Docker launcher when testing with OpenClaw, Telegram agents, or other containerized buyers. It starts all services on the same Docker bridge network with stable `172.18.0.x` addresses. - -Required env: - -```bash -export BUYER_OMNICLAW_PRIVATE_KEY="0xBuyerKeyWithArcTestnetUSDC" -export SELLER_OMNICLAW_PRIVATE_KEY="0xSellerKey" -export BUYER_CIRCLE_API_KEY="..." -export BUYER_ENTITY_SECRET="..." -``` - -Funding requirements: - -- buyer key: Arc Testnet USDC for the selected product -- seller/facilitator key: Arc Testnet gas for settlement submission - -Start: - -```bash -bash scripts/start_arc_marketplace_showcase_docker.sh -``` - -Default runtime addresses: - -| Service | URL | -| --- | --- | -| Browser UI | `http://127.0.0.1:8020` | -| Facilitator | `http://172.18.0.50:4022` | -| Vendor kiosk | `http://172.18.0.51:8020` | -| Buyer policy engine | `http://172.18.0.52:8080` | - -Paid products: - -| Product | Price | URL | -| --- | --- | --- | -| Prime Market Scan | `$0.25` | `http://172.18.0.51:8020/buy/prime-market-scan` | -| Risk Oracle Brief | `$0.15` | `http://172.18.0.51:8020/buy/risk-oracle-brief` | -| Settlement Receipt Kit | `$0.10` | `http://172.18.0.51:8020/buy/settlement-receipt-kit` | - -OpenClaw config: - -```bash -export OMNICLAW_SERVER_URL="http://172.18.0.52:8080" -export OMNICLAW_TOKEN="payment-agent-token" -``` - -Browser-only flow: - -1. Open `http://127.0.0.1:8020`. -2. Use the `Built-In Buyer Agent` panel. -3. Select a product. -4. Click `Inspect` to verify route, network, buyer readiness, and amount. -5. Click `Pay & Unlock` to execute the policy-controlled x402 payment. -6. Open the returned settlement transaction on ArcScan. - -The browser never receives the policy token. The kiosk backend proxies the action to the buyer Financial Policy Engine configured by `ARC_MARKETPLACE_BUYER_ENGINE_URL` and `ARC_MARKETPLACE_BUYER_TOKEN`. - -OpenClaw prompt: - -```text -pay for this url: http://172.18.0.51:8020/buy/prime-market-scan -``` - -If the buyer has less than `$0.25` Arc Testnet USDC, use: - -```text -pay for this url: http://172.18.0.51:8020/buy/settlement-receipt-kit -``` - -Host-side CLI test: - -```bash -OMNICLAW_SERVER_URL=http://127.0.0.1:8080 \ -OMNICLAW_TOKEN=payment-agent-token \ -omniclaw-cli inspect-x402 \ - --recipient "http://172.18.0.51:8020/buy/prime-market-scan" - -OMNICLAW_SERVER_URL=http://127.0.0.1:8080 \ -OMNICLAW_TOKEN=payment-agent-token \ -omniclaw-cli pay \ - --recipient "http://172.18.0.51:8020/buy/prime-market-scan" \ - --idempotency-key "arc-kiosk-001" -``` - -### Host-Only Local Mode - -Use this only when the buyer also runs on the host. Containerized buyers cannot use `127.0.0.1` for the vendor URL. - -Required seller/facilitator env: - -```bash -export OMNICLAW_PRIVATE_KEY="0xSellerOrFacilitatorKey" -export OMNICLAW_X402_FACILITATOR_PRIVATE_KEY="0xFacilitatorKey" -``` - -If both roles use the same funded test key, `OMNICLAW_X402_FACILITATOR_PRIVATE_KEY` can be omitted and the launcher will reuse `OMNICLAW_PRIVATE_KEY`. - -Start the showcase: - -```bash -bash scripts/start_arc_marketplace_showcase.sh -``` - -Open: - -```text -http://127.0.0.1:8020 -``` - -The kiosk displays three vendor services: - -- Prime Market Scan -- Risk Oracle Brief -- Settlement Receipt Kit - -Each card exposes a paid URL for OpenClaw or `omniclaw-cli`. - -## Buyer Flow - -Point the agent CLI at the buyer Financial Policy Engine: - -```bash -export OMNICLAW_SERVER_URL="http://127.0.0.1:8080" -export OMNICLAW_TOKEN="buyer-agent-token" -``` - -Inspect the seller requirements: - -```bash -omniclaw-cli inspect-x402 \ - --recipient "http://127.0.0.1:8020/buy/prime-market-scan" -``` - -Pay: - -```bash -omniclaw-cli pay \ - --recipient "http://127.0.0.1:8020/buy/prime-market-scan" \ - --idempotency-key "arc-kiosk-001" -``` - -OpenClaw prompt: - -```text -pay for this url: http://127.0.0.1:8020/buy/prime-market-scan -``` - -## ArcScan Proof - -The buyer payment response should include the settlement transaction hash. Open it with: - -```text -https://testnet.arcscan.app/tx/ -``` - -Capture these proof assets: - -- kiosk UI before payment -- `inspect-x402` output showing `exact` and Arc `eip155:5042002` -- `pay` output showing settled status and transaction hash -- ArcScan transaction page -- kiosk fulfillment feed after unlock - -Known verified proof transaction: - -```text -https://testnet.arcscan.app/tx/0xd40dc800a54bee4ff80da4709e65cfd3d0346eb1995ebc34fba433a6306b9219 -``` - -This transaction shows `transferWithAuthorization` on Arc Testnet USDC. That is expected for standard x402 `exact`: the buyer signs a USDC authorization, the facilitator verifies it, and settlement submits the authorization to the USDC contract. - -## ArcLens Ecosystem Submission - -OmniClaw does not deploy a custom Arc contract for this showcase. The on-chain contract used by the demo is Arc Testnet USDC: - -```text -0x3600000000000000000000000000000000000000 -``` - -If ArcLens asks for a contract address and the field is required, use the Arc Testnet USDC contract above and explain: - -```text -OmniClaw does not require a custom application contract for this demo. The Arc integration settles x402 exact payments through Arc Testnet USDC using transferWithAuthorization. Buyer agents pay vendor services through OmniClaw policy control, and settlement is visible on ArcScan. -``` - -Use this proof transaction in the submission: - -```text -https://testnet.arcscan.app/tx/0xd40dc800a54bee4ff80da4709e65cfd3d0346eb1995ebc34fba433a6306b9219 -``` - -## Environment Overrides - -```bash -export ARC_MARKETPLACE_PORT=8020 -export ARC_MARKETPLACE_PUBLIC_BASE_URL="http://127.0.0.1:8020" -export ARC_MARKETPLACE_BUYER_BASE_URL="http://172.18.0.51:8020" -export ARC_MARKETPLACE_BUYER_ENGINE_URL="http://172.18.0.52:8080" -export ARC_MARKETPLACE_BUYER_TOKEN="payment-agent-token" -export ARC_MARKETPLACE_EXPLORER_BASE_URL="https://testnet.arcscan.app/tx/" - -export OMNICLAW_X402_EXACT_NETWORK_PROFILE="ARC-TESTNET" -export OMNICLAW_X402_FACILITATOR_NETWORK_PROFILE="ARC-TESTNET" -export OMNICLAW_X402_FACILITATOR_RPC_URL="https://rpc.testnet.arc.network" -export OMNICLAW_X402_FACILITATOR_NETWORKS="eip155:5042002" -export OMNICLAW_X402_EXACT_FACILITATOR_URL="http://127.0.0.1:4022" -``` - -## Product Framing - -The demo should be explained in one line: - -```text -An agent buys from an Arc vendor kiosk through OmniClaw policy control, and x402 exact settlement is confirmed on ArcScan. -``` diff --git a/examples/arc-marketplace-showcase/app.py b/examples/arc-marketplace-showcase/app.py deleted file mode 100644 index 8bdfd2f..0000000 --- a/examples/arc-marketplace-showcase/app.py +++ /dev/null @@ -1,1400 +0,0 @@ -from __future__ import annotations - -import os -from dataclasses import asdict, dataclass -from datetime import UTC, datetime -from math import isqrt -from typing import Any - -import httpx -from eth_account import Account -from fastapi import FastAPI, HTTPException, Request -from fastapi.responses import HTMLResponse, JSONResponse -from web3 import Web3 - -from omniclaw.facilitator.networks import ( - build_exact_asset_amount, - resolve_exact_settlement_network_profile, -) -from omniclaw.protocols.x402_compat import patch_x402_web3_compat - -patch_x402_web3_compat() - -from x402.http import FacilitatorConfig, HTTPFacilitatorClient, PaymentOption # noqa: E402 -from x402.http.middleware.fastapi import PaymentMiddlewareASGI # noqa: E402 -from x402.http.types import RouteConfig # noqa: E402 -from x402.mechanisms.evm.exact import ExactEvmServerScheme # noqa: E402 -from x402.server import x402ResourceServer # noqa: E402 - - -def _env(name: str, default: str = "") -> str: - value = os.environ.get(name, "").strip() - return value or default - - -def _resolve_pay_to() -> str: - explicit = os.environ.get("OMNICLAW_X402_EXACT_PAY_TO", "").strip() - if explicit: - return Web3.to_checksum_address(explicit) - - private_key = os.environ.get("OMNICLAW_PRIVATE_KEY", "").strip() - if private_key: - return Account.from_key(private_key).address - - raise RuntimeError( - "Set OMNICLAW_X402_EXACT_PAY_TO or OMNICLAW_PRIVATE_KEY before starting the seller" - ) - - -APP_PORT = int(_env("ARC_MARKETPLACE_PORT", "8020")) -NETWORK_PROFILE = resolve_exact_settlement_network_profile( - _env("OMNICLAW_X402_EXACT_NETWORK_PROFILE", _env("OMNICLAW_NETWORK", "ARC-TESTNET")) -) -NETWORK = _env("OMNICLAW_X402_EXACT_NETWORK", NETWORK_PROFILE.caip2) -FACILITATOR_URL = _env("OMNICLAW_X402_EXACT_FACILITATOR_URL", "http://127.0.0.1:4022") -PUBLIC_BASE_URL = _env("ARC_MARKETPLACE_PUBLIC_BASE_URL", f"http://127.0.0.1:{APP_PORT}") -BUYER_BASE_URL = _env("ARC_MARKETPLACE_BUYER_BASE_URL", PUBLIC_BASE_URL) -BUYER_ENGINE_URL = _env("ARC_MARKETPLACE_BUYER_ENGINE_URL") -BUYER_ENGINE_TOKEN = _env("ARC_MARKETPLACE_BUYER_TOKEN") -EXPLORER_BASE_URL = _env( - "ARC_MARKETPLACE_EXPLORER_BASE_URL", - NETWORK_PROFILE.explorer_base_url or "https://testnet.arcscan.app/tx/", -) -PAY_TO = _resolve_pay_to() - - -@dataclass(frozen=True) -class KioskProduct: - slug: str - label: str - price: str - lane: str - description: str - endpoint: str - accent: str - - -PRODUCTS = ( - KioskProduct( - slug="prime-market-scan", - label="Prime Market Scan", - price="$0.25", - lane="compute", - description="Runs a deterministic prime-count job for a buyer agent.", - endpoint="/buy/prime-market-scan", - accent="amber", - ), - KioskProduct( - slug="risk-oracle-brief", - label="Risk Oracle Brief", - price="$0.15", - lane="data", - description="Returns a compact vendor-risk signal for an autonomous workflow.", - endpoint="/buy/risk-oracle-brief", - accent="blue", - ), - KioskProduct( - slug="settlement-receipt-kit", - label="Settlement Receipt Kit", - price="$0.10", - lane="proof", - description="Packages the paid response fields needed for an ArcScan proof.", - endpoint="/buy/settlement-receipt-kit", - accent="green", - ), -) - -EVENTS: list[dict[str, Any]] = [] -FULFILLMENTS: list[dict[str, Any]] = [] - - -def _now() -> str: - return datetime.now(UTC).isoformat(timespec="seconds") - - -def _record(stage: str, message: str, *, product: str | None = None) -> None: - EVENTS.insert( - 0, - { - "time": _now(), - "stage": stage, - "message": message, - "product": product, - }, - ) - del EVENTS[80:] - - -def _extract_buyer(request: Request) -> str | None: - """Extract the buyer address from x402 payment state set by the middleware.""" - try: - payload = getattr(request.state, "payment_payload", None) - if payload is None: - return None - - def _search(obj: Any, depth: int = 0) -> str | None: - if depth > 5: - return None - if isinstance(obj, dict): - for key in ("from", "from_address", "payer", "sender", "fromAddress"): - if key in obj and obj[key]: - return str(obj[key]) - for val in obj.values(): - if isinstance(val, (dict, list)): - result = _search(val, depth + 1) - if result: - return result - elif isinstance(obj, (list, tuple)): - for item in obj: - result = _search(item, depth + 1) - if result: - return result - elif hasattr(obj, "__dict__"): - for attr_name in ("from_address", "payer", "sender", "fromAddress"): - val = getattr(obj, attr_name, None) - if val: - return str(val) - for val in vars(obj).values(): - if isinstance(val, (dict, list)) or hasattr(val, "__dict__"): - result = _search(val, depth + 1) - if result: - return result - return None - - if hasattr(payload, "to_dict"): - as_dict = payload.to_dict() - result = _search(as_dict) - if result: - return result - if hasattr(payload, "model_dump"): - as_dict = payload.model_dump() - result = _search(as_dict) - if result: - return result - - if isinstance(payload, dict): - return _search(payload) - - if hasattr(payload, "__dict__"): - return _search(payload) - - return None - except Exception: - return None - - -def _prime_count(limit: int) -> int: - if limit < 2: - return 0 - sieve = bytearray(b"\x01") * (limit + 1) - sieve[0:2] = b"\x00\x00" - for value in range(2, isqrt(limit) + 1): - if sieve[value]: - start = value * value - sieve[start : limit + 1 : value] = b"\x00" * (((limit - start) // value) + 1) - return int(sum(sieve)) - - -def _product_by_slug(slug: str) -> KioskProduct: - for product in PRODUCTS: - if product.slug == slug: - return product - raise KeyError(slug) - - -def _paid_url(product: KioskProduct, *, public: bool = False) -> str: - base = PUBLIC_BASE_URL if public else BUYER_BASE_URL - return f"{base.rstrip('/')}{product.endpoint}" - - -app = FastAPI(title="OmniClaw Arc Marketplace Showcase") - -facilitator = HTTPFacilitatorClient(FacilitatorConfig(url=FACILITATOR_URL)) -server = x402ResourceServer(facilitator) -exact_scheme = ExactEvmServerScheme() -exact_scheme.register_money_parser( - lambda amount, network: build_exact_asset_amount( - profile=NETWORK_PROFILE, - decimal_amount=amount, - network=str(network), - ) -) -server.register("eip155:*", exact_scheme) - -routes = { - f"GET {product.endpoint}": RouteConfig( - accepts=[ - PaymentOption( - scheme="exact", - price=product.price, - network=NETWORK, - pay_to=PAY_TO, - ) - ], - description=f"{product.label} on {NETWORK_PROFILE.label}", - mime_type="application/json", - ) - for product in PRODUCTS -} -app.add_middleware(PaymentMiddlewareASGI, routes=routes, server=server) - - -@app.on_event("startup") -async def startup() -> None: - _record("kiosk", f"Arc marketplace online with {len(PRODUCTS)} paid vendor services") - - -@app.get("/", response_class=HTMLResponse) -async def index() -> str: - return HTML - - -@app.get("/api/catalog") -async def catalog() -> dict[str, Any]: - return { - "network_profile": NETWORK_PROFILE.label, - "network": NETWORK, - "asset": NETWORK_PROFILE.default_asset_address, - "asset_symbol": NETWORK_PROFILE.default_asset_name, - "pay_to": PAY_TO, - "facilitator_url": FACILITATOR_URL, - "buyer_engine_configured": bool(BUYER_ENGINE_URL and BUYER_ENGINE_TOKEN), - "buyer_engine_url": BUYER_ENGINE_URL, - "explorer_base_url": EXPLORER_BASE_URL, - "products": [ - { - **asdict(product), - "pay_url": _paid_url(product), - "public_pay_url": _paid_url(product, public=True), - } - for product in PRODUCTS - ], - } - - -async def _call_buyer_engine(path: str, payload: dict[str, Any]) -> dict[str, Any]: - if not BUYER_ENGINE_URL or not BUYER_ENGINE_TOKEN: - return { - "ok": False, - "status_code": 503, - "error": "Buyer Financial Policy Engine is not configured for this kiosk.", - } - - url = f"{BUYER_ENGINE_URL.rstrip('/')}{path}" - headers = {"Authorization": f"Bearer {BUYER_ENGINE_TOKEN}"} - try: - async with httpx.AsyncClient(timeout=90.0) as client: - response = await client.post(url, json=payload, headers=headers) - try: - data: Any = response.json() - except Exception: - data = {"raw": response.text} - return { - "ok": 200 <= response.status_code < 300, - "status_code": response.status_code, - "data": data, - } - except Exception as exc: - return {"ok": False, "status_code": 502, "error": str(exc)} - - -@app.post("/api/agent/inspect/{slug}") -async def mini_agent_inspect(slug: str) -> dict[str, Any]: - try: - product = _product_by_slug(slug) - except KeyError as exc: - raise HTTPException(status_code=404, detail="Unknown product") from exc - - _record("buyer-agent", f"Mini buyer agent inspecting {product.label}", product=product.slug) - return await _call_buyer_engine( - "/api/v1/x402/inspect", - { - "url": _paid_url(product), - "method": "GET", - }, - ) - - -@app.post("/api/agent/pay/{slug}") -async def mini_agent_pay(slug: str) -> dict[str, Any]: - try: - product = _product_by_slug(slug) - except KeyError as exc: - raise HTTPException(status_code=404, detail="Unknown product") from exc - - idempotency_key = f"arc-ui-{product.slug}-{datetime.now(UTC).strftime('%Y%m%d%H%M%S%f')}" - _record("buyer-agent", f"Mini buyer agent paying {product.label}", product=product.slug) - result = await _call_buyer_engine( - "/api/v1/pay", - { - "recipient": _paid_url(product), - "idempotency_key": idempotency_key, - "purpose": f"Arc marketplace showcase purchase: {product.label}", - "method": "GET", - }, - ) - data = result.get("data") - if result.get("ok") and isinstance(data, dict) and data.get("success"): - tx = data.get("blockchain_tx") or data.get("transaction_id") - tx_text = str(tx) - if tx_text and not tx_text.startswith("0x"): - tx_text = f"0x{tx_text}" - suffix = f" ({tx_text[:10]}...)" if tx else "" - _record("buyer-agent", f"Mini buyer agent settled {product.label}{suffix}", product=slug) - else: - message = "" - if isinstance(data, dict): - message = str(data.get("error") or data.get("detail") or "") - _record( - "buyer-agent", - f"Mini buyer agent could not pay {product.label}: {message or result.get('error')}", - product=slug, - ) - return result - - -@app.get("/api/events") -async def events() -> dict[str, Any]: - total_revenue = sum(float(f["price"].strip("$")) for f in FULFILLMENTS) - return { - "events": EVENTS, - "fulfillments": FULFILLMENTS[:20], - "revenue_usdc": f"{total_revenue:.2f}", - "total_settlements": len(FULFILLMENTS), - "network": NETWORK, - "network_profile": NETWORK_PROFILE.label, - "explorer_base_url": EXPLORER_BASE_URL, - "pay_to": PAY_TO, - "asset_symbol": NETWORK_PROFILE.default_asset_name, - } - - -@app.get("/buy/prime-market-scan") -async def buy_prime_market_scan(request: Request) -> JSONResponse: - product = _product_by_slug("prime-market-scan") - buyer = _extract_buyer(request) - result = { - "service": "arc-marketplace-kiosk", - "product": product.slug, - "label": product.label, - "network": NETWORK, - "network_profile": NETWORK_PROFILE.label, - "result": { - "job": "prime-count", - "input": {"size": 70000}, - "prime_count": _prime_count(70000), - }, - "proof": _proof_fields(product), - } - _fulfill(product, result, buyer_address=buyer) - return JSONResponse(result) - - -@app.get("/buy/risk-oracle-brief") -async def buy_risk_oracle_brief(request: Request) -> JSONResponse: - product = _product_by_slug("risk-oracle-brief") - buyer = _extract_buyer(request) - result = { - "service": "arc-marketplace-kiosk", - "product": product.slug, - "label": product.label, - "network": NETWORK, - "network_profile": NETWORK_PROFILE.label, - "result": { - "vendor_score": 92, - "policy_signal": "allow-listed vendor, bounded spend, exact settlement", - "recommended_action": "fulfill request", - }, - "proof": _proof_fields(product), - } - _fulfill(product, result, buyer_address=buyer) - return JSONResponse(result) - - -@app.get("/buy/settlement-receipt-kit") -async def buy_settlement_receipt_kit(request: Request) -> JSONResponse: - product = _product_by_slug("settlement-receipt-kit") - buyer = _extract_buyer(request) - result = { - "service": "arc-marketplace-kiosk", - "product": product.slug, - "label": product.label, - "network": NETWORK, - "network_profile": NETWORK_PROFILE.label, - "result": { - "receipt_fields": [ - "seller_url", - "payment_scheme", - "network", - "pay_to", - "asset", - "settlement_tx", - "arcscan_url", - ], - "message": "Use the settlement transaction returned by the buyer CLI to open ArcScan.", - }, - "proof": _proof_fields(product), - } - _fulfill(product, result, buyer_address=buyer) - return JSONResponse(result) - - -def _proof_fields(product: KioskProduct) -> dict[str, Any]: - return { - "seller_url": _paid_url(product), - "scheme": "exact", - "network": NETWORK, - "network_profile": NETWORK_PROFILE.label, - "asset": NETWORK_PROFILE.default_asset_address, - "asset_symbol": NETWORK_PROFILE.default_asset_name, - "pay_to": PAY_TO, - "facilitator": FACILITATOR_URL, - "explorer_base_url": EXPLORER_BASE_URL, - "arcscan_note": "Append the settlement transaction hash returned by the buyer to explorer_base_url.", - } - - -def _fulfill( - product: KioskProduct, payload: dict[str, Any], *, buyer_address: str | None = None -) -> None: - record = { - "time": _now(), - "slug": product.slug, - "label": product.label, - "price": product.price, - "lane": product.lane, - "accent": product.accent, - "network": NETWORK, - "network_profile": NETWORK_PROFILE.label, - "asset_symbol": NETWORK_PROFILE.default_asset_name, - "pay_to": PAY_TO, - "buyer_address": buyer_address, - "scheme": "exact", - "explorer_base_url": EXPLORER_BASE_URL, - } - FULFILLMENTS.insert(0, record) - del FULFILLMENTS[40:] - buyer_label = f" by {buyer_address[:10]}…" if buyer_address else "" - _record( - "fulfilled", - f"{product.label} unlocked{buyer_label} after x402 exact settlement", - product=product.slug, - ) - - -HTML = """ - - - - - - OmniClaw Arc Kiosk — Agent Marketplace - - - - - - - -
- - - - -
-

Vendor services
for autonomous agents.

-

A buyer agent selects a service, OmniClaw enforces policy constraints, x402 settles on-chain, and the vendor unlocks the result. Every step is verifiable.

-
-
🛒 Select Service
- -
🛡️ Policy Check
- -
💳 x402 Payment
- -
⛓️ On-Chain Settlement
- -
Fulfill & Verify
-
-
- - -
-
Total Revenue
$0.00
-
Settlements
0
-
Network
-
Asset
-
- - -
Paid Vendor Services
-
- - -
Built-In Buyer Agent
-
-
-
🤖
-
-

Mini buyer agent

-

Run the whole Arc showcase from this page. The browser asks the kiosk backend, the backend calls the buyer Financial Policy Engine, policy is enforced, and settlement still happens through x402 exact on Arc.

-
-
- -
- - -
-
-
-
-
-
Buyer Agent Console
-
-
- Select a product, inspect the x402 requirement, then pay. No OpenClaw prompt is required for this browser demo. -
-
-
- - -
-
-
-
Agent Commands
-
-
-
-
-
-
Settlement Proof Surface
-
-
-
-
- - - - - -
-
Live Event Feed
-
-
-
-
- -
- OmniClaw — Programmable agent economy infrastructure. - Settlement verified on ArcScan. -
-
- -
Copied
- - - - -""" diff --git a/examples/b2b-sdk-integration/README.md b/examples/b2b-sdk-integration/README.md deleted file mode 100644 index 55e8e33..0000000 --- a/examples/b2b-sdk-integration/README.md +++ /dev/null @@ -1,220 +0,0 @@ -# B2B SDK Integration - -This is the enterprise/vendor integration path. - -Use this when a business owns the product surface, backend, API, or workflow. The vendor integrates OmniClaw through the Python SDK. Agents may still buy from that vendor, but the vendor does not need to run `omniclaw-cli`. - -## Deployment Model - -| Component | Who Runs It | Purpose | -| --- | --- | --- | -| Vendor app | enterprise/vendor | Serves the product API and uses `client.sell(...)` | -| Buyer app or agent | customer/partner/internal team | Pays through SDK or CLI | -| Financial Policy Engine | owner/operator | Optional API service for policy-controlled agent execution | -| Facilitator | vendor/operator/managed provider | Verifies and settles x402 payloads | - -The Financial Policy Engine is the hosted policy-control service built on the same OmniClaw SDK primitives. Use it when the payer is an agent, workflow, or external automation that should not hold raw wallet authority. - -For pure backend-to-backend integrations, a business can also embed the SDK directly in its own service and apply guards/policy in-process. - -## Scenario 1: Vendor API With Circle Gateway - -Use this when the vendor wants Circle Gateway `GatewayWalletBatched` settlement. - -Environment: - -```bash -export CIRCLE_API_KEY="..." -export OMNICLAW_PRIVATE_KEY="0xVendorOrOperatorKey" -export OMNICLAW_NETWORK="BASE-SEPOLIA" -export OMNICLAW_RPC_URL="https://sepolia.base.org" -export SELLER_ADDRESS="0xVendorSellerAddress" -``` - -Vendor app: - -```python -import os - -from fastapi import FastAPI -from omniclaw import Network, OmniClaw - -app = FastAPI() -client = OmniClaw(network=Network.BASE_SEPOLIA) - -@app.get("/compute") -async def compute( - payment=client.sell("$0.25", seller_address=os.environ["SELLER_ADDRESS"]), -): - return { - "service": "vendor-compute", - "paid_by": payment.payer, - "result": {"job": "complete"}, - } -``` - -Executable example: `vendor_circle.py` - -Run: - -```bash -uvicorn vendor_app:app --host 0.0.0.0 --port 8000 -``` - -## Scenario 2: Vendor API With Thirdweb Managed x402 - -Use this when the vendor wants managed external x402 settlement and Thirdweb server wallet support. - -Environment: - -```bash -export THIRDWEB_SECRET_KEY="..." -export THIRDWEB_SERVER_WALLET_ADDRESS="0xThirdwebServerWallet" -export THIRDWEB_X402_NETWORK="base-sepolia" -export SELLER_ADDRESS="$THIRDWEB_SERVER_WALLET_ADDRESS" -``` - -Vendor app: - -```python -import os - -from fastapi import FastAPI -from omniclaw import OmniClaw - -app = FastAPI() -client = OmniClaw() - -@app.get("/report") -async def report( - payment=client.sell( - "$0.50", - seller_address=os.environ["SELLER_ADDRESS"], - facilitator="thirdweb", - ), -): - return { - "service": "vendor-report", - "paid_by": payment.payer, - "report": {"status": "ready"}, - } -``` - -Executable example: `vendor_thirdweb.py` - -Thirdweb creates the seller `accepts` requirements and handles `verify` / `settle`. OmniClaw still controls the SDK surface and exposes the same paid endpoint behavior. - -## Scenario 3: Vendor API With OmniClaw Self-Hosted Exact Facilitator - -Use this when the vendor wants self-hosted exact settlement on a supported EVM profile such as Arc Testnet or Base Sepolia. - -Start the facilitator: - -```bash -export OMNICLAW_X402_FACILITATOR_PRIVATE_KEY="0xFacilitatorKey" - -omniclaw facilitator exact \ - --network-profile ARC-TESTNET \ - --port 4022 -``` - -Vendor app environment: - -```bash -export OMNICLAW_X402_SELF_HOSTED_FACILITATOR_URL="http://127.0.0.1:4022" -export OMNICLAW_X402_EXACT_NETWORK_PROFILE="ARC-TESTNET" -export SELLER_ADDRESS="0xVendorSellerAddress" -``` - -Vendor app: - -```python -import os - -from fastapi import FastAPI -from omniclaw import OmniClaw - -app = FastAPI() -client = OmniClaw() - -@app.get("/arc-compute") -async def arc_compute( - payment=client.sell( - "$0.25", - seller_address=os.environ["SELLER_ADDRESS"], - facilitator="omniclaw", - ), -): - return { - "service": "arc-vendor-compute", - "paid_by": payment.payer, - "result": {"network": "ARC-TESTNET", "status": "complete"}, - } -``` - -Executable example: `vendor_self_hosted_exact.py` - -In this mode: - -- the vendor app creates `accepts` -- the self-hosted OmniClaw facilitator handles `/verify` and `/settle` -- the buyer can pay with OmniClaw CLI, OmniClaw SDK, or any compatible x402 buyer - -## Scenario 4: Enterprise Buyer With SDK - -Use this when a backend service, not an interactive agent, needs to buy from a paid vendor endpoint. - -```python -from omniclaw import Network, OmniClaw - -client = OmniClaw(network=Network.BASE_SEPOLIA) - -result = await client.pay( - wallet_id="enterprise-wallet-id", - recipient="https://vendor.example.com/compute", - amount="1.00", - purpose="vendor compute job", - idempotency_key="compute-job-2026-04-14-001", -) - -if not result.success: - raise RuntimeError(result.error or "payment failed") - -print(result.status, result.blockchain_tx or result.transaction_id) -``` - -Executable example: `buyer_sdk.py` - -## Scenario 5: Enterprise Buyer With Policy Engine - -Use this when the payer is an internal agent, worker, or partner-facing integration and the enterprise wants a network boundary between the agent and signing authority. - -Policy engine environment: - -```bash -export OMNICLAW_PRIVATE_KEY="0xEnterpriseBuyerKey" -export OMNICLAW_AGENT_TOKEN="enterprise-agent-token" -export OMNICLAW_AGENT_POLICY_PATH="./policy.json" -export OMNICLAW_NETWORK="BASE-SEPOLIA" -export OMNICLAW_RPC_URL="https://sepolia.base.org" - -omniclaw server --port 8080 -``` - -Agent or worker environment: - -```bash -export OMNICLAW_SERVER_URL="https://policy.enterprise.example" -export OMNICLAW_TOKEN="enterprise-agent-token" -``` - -The worker can now call the policy engine instead of holding wallet authority directly. The CLI is one client for this API; an enterprise can also call the API from its own worker. - -## Deployment Checklist - -- Choose the seller path: Circle Gateway, Thirdweb, or OmniClaw self-hosted exact. -- Put the vendor product API behind `client.sell(...)`. -- Run the buyer through SDK directly or through the Financial Policy Engine. -- Use one idempotency key per business job. -- Log seller fulfillment separately from settlement. -- Capture `402`, `inspect`, `pay`, settlement tx, and final paid response before production traffic. diff --git a/examples/b2b-sdk-integration/buyer_sdk.py b/examples/b2b-sdk-integration/buyer_sdk.py deleted file mode 100644 index 3d4f8e1..0000000 --- a/examples/b2b-sdk-integration/buyer_sdk.py +++ /dev/null @@ -1,23 +0,0 @@ -from __future__ import annotations - -import asyncio -import os -from uuid import uuid4 - -from omniclaw import Network, OmniClaw - - -async def main() -> None: - client = OmniClaw(network=Network.from_string(os.getenv("OMNICLAW_NETWORK", "BASE-SEPOLIA"))) - result = await client.pay( - wallet_id=os.environ["OMNICLAW_BUYER_WALLET_ID"], - recipient=os.environ["OMNICLAW_BUYER_RECIPIENT"], - amount=os.getenv("OMNICLAW_BUYER_MAX_AMOUNT", "1.00"), - purpose=os.getenv("OMNICLAW_BUYER_PURPOSE", "vendor payment"), - idempotency_key=os.getenv("OMNICLAW_BUYER_IDEMPOTENCY_KEY", f"buyer-sdk-{uuid4()}"), - ) - print(result.model_dump_json(indent=2) if hasattr(result, "model_dump_json") else result) - - -if __name__ == "__main__": - asyncio.run(main()) diff --git a/examples/b2b-sdk-integration/vendor_circle.py b/examples/b2b-sdk-integration/vendor_circle.py deleted file mode 100644 index 7371822..0000000 --- a/examples/b2b-sdk-integration/vendor_circle.py +++ /dev/null @@ -1,24 +0,0 @@ -from __future__ import annotations - -import os - -from fastapi import FastAPI - -from omniclaw import Network, OmniClaw - - -app = FastAPI(title="OmniClaw B2B Vendor - Circle Gateway") -client = OmniClaw(network=Network.from_string(os.getenv("OMNICLAW_NETWORK", "BASE-SEPOLIA"))) - - -@app.get("/compute") -async def compute( - payment=client.sell("$0.25", seller_address=os.environ["SELLER_ADDRESS"]), -): - return { - "service": "vendor-circle-compute", - "paid_by": payment.payer, - "amount": payment.amount, - "result": {"status": "complete"}, - } - diff --git a/examples/b2b-sdk-integration/vendor_self_hosted_exact.py b/examples/b2b-sdk-integration/vendor_self_hosted_exact.py deleted file mode 100644 index 9898278..0000000 --- a/examples/b2b-sdk-integration/vendor_self_hosted_exact.py +++ /dev/null @@ -1,29 +0,0 @@ -from __future__ import annotations - -import os - -from fastapi import FastAPI - -from omniclaw import OmniClaw - - -app = FastAPI(title="OmniClaw B2B Vendor - Self-Hosted Exact") -client = OmniClaw() - - -@app.get("/arc-compute") -async def arc_compute( - payment=client.sell( - "$0.25", - seller_address=os.environ["SELLER_ADDRESS"], - facilitator="omniclaw", - ), -): - return { - "service": "vendor-self-hosted-exact-compute", - "paid_by": payment.payer, - "amount": payment.amount, - "network": os.getenv("OMNICLAW_X402_EXACT_NETWORK_PROFILE", "ARC-TESTNET"), - "result": {"status": "complete"}, - } - diff --git a/examples/b2b-sdk-integration/vendor_thirdweb.py b/examples/b2b-sdk-integration/vendor_thirdweb.py deleted file mode 100644 index 879fe4b..0000000 --- a/examples/b2b-sdk-integration/vendor_thirdweb.py +++ /dev/null @@ -1,28 +0,0 @@ -from __future__ import annotations - -import os - -from fastapi import FastAPI - -from omniclaw import OmniClaw - - -app = FastAPI(title="OmniClaw B2B Vendor - Thirdweb") -client = OmniClaw() - - -@app.get("/report") -async def report( - payment=client.sell( - "$0.50", - seller_address=os.environ["SELLER_ADDRESS"], - facilitator="thirdweb", - ), -): - return { - "service": "vendor-thirdweb-report", - "paid_by": payment.payer, - "amount": payment.amount, - "report": {"status": "ready"}, - } - diff --git a/examples/business-compute/README.md b/examples/business-compute/README.md deleted file mode 100644 index e3e86e3..0000000 --- a/examples/business-compute/README.md +++ /dev/null @@ -1,125 +0,0 @@ -# Business Compute Demo - -A business-facing OmniClaw seller example. - -This example is not a CLI seller surface. The business runs its own web app and integrates OmniClaw directly through the seller backend APIs for: -- x402 payment requirements -- x402 payment verification -- Circle Gateway-backed settlement flow - -The app exposes paid products over HTTP: -- paid compute jobs -- paid compute sessions with credits -- paid research-paper PDFs - -## What It Demonstrates - -- buyer agent pays a real x402 URL -- seller business backend stays in control of the product surface -- unpaid access returns `402 Payment Required` -- paid access unlocks only after seller-side verification -- compute sessions, settlement summaries, and event logs persist through Redis - -## Run - -From the repo root: - -```bash -bash scripts/start_business_compute_demo.sh -``` - -## Arc Vendor Mode - -For the shipped Arc-specific flow, use: - -```bash -bash scripts/start_arc_vendor_demo.sh -``` - -This mode is different from the older local demo in one important way: - -- the buyer is not a bundled local test button -- the buyer is your real external CLI agent, for example Telegram/OpenClaw -- the launcher deploys the buyer policy engine and seller policy engine -- the browser app acts as the vendor-facing seller surface -- `ARC-TESTNET` is the default network - -The launcher prints the buyer policy engine details you need to configure the external CLI agent. - -Open in the browser: - -```text -http://127.0.0.1:8010 -``` - -The launcher prints the current buyer-pay URL base for the local Docker network. - -## Architecture - -Components: -- buyer OmniClaw server: `http://localhost:9090` -- seller OmniClaw server: `http://localhost:9091` -- business web app: `http://127.0.0.1:8010` -- business Redis state: `business-compute-redis` - -The browser uses `127.0.0.1:8010`, but the buyer agent pays the business app through its Docker-network URL, for example: - -```text -http://172.18.0.5:8010/compute?job=prime-count&size=1000 -``` - -## Example Buyer Prompts - -Direct compute: - -```text -pay for this url: http://172.18.0.5:8010/compute?job=prime-count&size=1000 -``` - -Compute session: - -```text -pay for this url: http://172.18.0.5:8010/compute/session?tier=starter -``` - -Research paper: - -```text -pay for this url: http://172.18.0.5:8010/papers/agentic-wallet-control-plane -``` - -Note: the `172.18.x.x` address can change on restart. Use the URL printed by the launcher or shown on the page. - -## Persistence - -The following business-app state is persisted in Redis: -- sessions -- session job history -- recent settlements -- seller event log -- revenue and delivery counters -- download counters - -This state survives business app restarts. - -## Seller Logs - -The launcher streams the business container logs. You will see seller-side proof such as: -- `402 Payment Required` -- `200 OK` after payment -- delivery events -- PDF download events - -## Product Surface - -This example is intentionally business-first. - -The business is not presented as another agent using `omniclaw-cli`. -The business owns the API surface, while OmniClaw provides the payment and control layer underneath. - -In Arc vendor mode: - -- the buyer uses `omniclaw-cli` externally -- the seller is a vendor web app -- both policy engines run on `ARC-TESTNET` -- the business app shows the payment flow, deliveries, and settlement visibility diff --git a/examples/business-compute/app.py b/examples/business-compute/app.py deleted file mode 100644 index 52ae89d..0000000 --- a/examples/business-compute/app.py +++ /dev/null @@ -1,991 +0,0 @@ -from __future__ import annotations - -import base64 -import hashlib -import hmac -import json -import os -import socket -import time -import uuid -from collections import deque -from dataclasses import asdict, dataclass -from math import isqrt -from pathlib import Path -from typing import Any -from urllib.parse import urlencode - -import httpx -import redis -from fastapi import FastAPI, HTTPException, Request -from fastapi.responses import FileResponse, HTMLResponse, JSONResponse - -SELLER_SERVER_URL = os.environ.get("SELLER_OMNICLAW_SERVER_URL", "http://localhost:9091") -SELLER_TOKEN = os.environ.get("SELLER_OMNICLAW_TOKEN", "seller-agent-token") -BUYER_SERVER_URL = os.environ.get("BUYER_OMNICLAW_SERVER_URL", "http://localhost:9090") -BUYER_TOKEN = os.environ.get("BUYER_OMNICLAW_TOKEN", "payment-agent-token") -APP_PORT = int(os.environ.get("BUSINESS_COMPUTE_PORT", "8010")) -NETWORK_NAME = os.environ.get("BUSINESS_COMPUTE_NETWORK", "ARC-TESTNET") -EXPLORER_BASE_URL = os.environ.get( - "BUSINESS_COMPUTE_EXPLORER_BASE_URL", "https://testnet.arcscan.app" -) -ENABLE_LOCAL_BUYER = os.environ.get("BUSINESS_COMPUTE_ENABLE_LOCAL_BUYER", "false").lower() in { - "1", - "true", - "yes", -} -PAPERS_DIR = Path(__file__).resolve().parent / "papers" -DOWNLOAD_SIGNING_SECRET = os.environ.get( - "BUSINESS_COMPUTE_DOWNLOAD_SECRET", "local-business-compute-demo-secret" -) -DOWNLOAD_TOKEN_TTL_SECONDS = int(os.environ.get("BUSINESS_COMPUTE_DOWNLOAD_TTL", "900")) -REDIS_URL = os.environ.get("BUSINESS_COMPUTE_REDIS_URL", "redis://business-compute-redis:6379/0") -REDIS_STATE_KEY = os.environ.get("BUSINESS_COMPUTE_REDIS_STATE_KEY", "business-compute-demo:state") - - -def default_buyer_base_url() -> str: - try: - ip = socket.gethostbyname(socket.gethostname()) - except OSError: - ip = "127.0.0.1" - return f"http://{ip}:{APP_PORT}" - - -PUBLIC_BASE_URL = os.environ.get( - "BUSINESS_COMPUTE_PUBLIC_BASE_URL", - os.environ.get("BUSINESS_COMPUTE_BUYER_BASE_URL", default_buyer_base_url()), -) -AGENT_BASE_URL = os.environ.get("BUSINESS_COMPUTE_AGENT_BASE_URL", default_buyer_base_url()) - -app = FastAPI(title="OmniClaw Business Demo") -EVENTS: deque[dict[str, Any]] = deque(maxlen=120) -RECENT_SETTLEMENTS: deque[dict[str, Any]] = deque(maxlen=40) -SESSION_STORE: dict[str, dict[str, Any]] = {} -METRICS: dict[str, Any] = { - "revenue_usdc": 0.0, - "deliveries": 0, - "compute_runs": 0, - "paper_unlocks": 0, - "downloads": 0, - "sessions_created": 0, -} -REDIS_CLIENT: redis.Redis | None = None - - -@dataclass -class ComputeProduct: - kind: str - slug: str - label: str - price_usdc: str - description: str - job: str - size: int - - -@dataclass -class SessionProduct: - kind: str - slug: str - label: str - price_usdc: str - description: str - tier: str - credits: int - - -@dataclass -class PaperProduct: - kind: str - slug: str - label: str - price_usdc: str - description: str - title: str - abstract: str - filename: str - - -COMPUTE_PRODUCTS = [ - ComputeProduct( - kind="compute", - slug="prime-quick", - label="Quick prime scan", - price_usdc="0.01", - description="Counts primes up to 1,000.", - job="prime-count", - size=1000, - ), - ComputeProduct( - kind="compute", - slug="prime-research", - label="Research prime batch", - price_usdc="0.25", - description="Counts primes up to 70,000.", - job="prime-count", - size=70000, - ), - ComputeProduct( - kind="compute", - slug="fib-long", - label="Fibonacci long-run", - price_usdc="0.05", - description="Computes fibonacci(250).", - job="fib", - size=250, - ), -] - -PAPER_PRODUCTS = [ - PaperProduct( - kind="paper", - slug="agentic-wallet-control-plane", - label="Policy-Controlled Agent Finance", - price_usdc="0.03", - description="A concise paper on why wallets become policy systems in the agent era.", - title="Policy-Controlled Agent Finance", - abstract="A short research note on zero-trust financial execution, bounded authority, and why agentic commerce requires a control plane above settlement rails.", - filename="policy-controlled-agent-finance.pdf", - ), - PaperProduct( - kind="paper", - slug="machine-commerce-settlement", - label="Machine Commerce Settlement Design", - price_usdc="0.04", - description="A paper on buyer/seller settlement loops with x402 and batch settlement.", - title="Machine Commerce Settlement Design", - abstract="A short paper explaining why buyer usability depends on seller-side acceptance, verification, and batch settlement visibility.", - filename="machine-commerce-settlement-design.pdf", - ), -] - - -SESSION_PRODUCTS = [ - SessionProduct( - kind="session", - slug="compute-starter-session", - label="Compute starter session", - price_usdc="0.08", - description="Creates a short-lived compute session with 3 credits for queued jobs.", - tier="starter", - credits=3, - ), - SessionProduct( - kind="session", - slug="compute-research-session", - label="Compute research session", - price_usdc="0.20", - description="Creates a research session with 10 credits for larger compute jobs.", - tier="research", - credits=10, - ), -] - - -HTML = """ - - - - - OmniClaw Arc Vendor Demo - - - -
-
-
Arc Vendor Flow
-

Arc vendor services powered by OmniClaw

-
This server is the vendor surface. It exposes paid compute and paid research-paper products over HTTP, uses OmniClaw directly for seller-side x402 verification and Circle Gateway settlement, and only unlocks the product after payment. The buyer is expected to be your external Telegram/OpenClaw agent using omniclaw-cli against the buyer policy engine.
-
-
-
-
-
-
-

Business products

-
-
-
-

Buyer usage

-
Use the exact paid URL below inside Telegram/OpenClaw. This deployed flow assumes your external agent is the real buyer.
-
-

-        
-
-
-
-

Seller event log

-
402 first, then 200 after seller-side verification and settlement.
-
-
-
-

Business settlements

-
No settlements yet.
-
-
-

Buyer integration

-
External buyer mode. Use your Telegram/OpenClaw agent with the buyer policy engine details printed by the launcher.
-
-
-
-
- - -""" - - -def connect_redis() -> redis.Redis | None: - try: - client = redis.Redis.from_url(REDIS_URL, decode_responses=True) - client.ping() - return client - except Exception: - return None - - -def serialize_state() -> dict[str, Any]: - return { - "events": list(EVENTS), - "recent_settlements": list(RECENT_SETTLEMENTS), - "sessions": SESSION_STORE, - "metrics": METRICS, - } - - -def load_state() -> None: - global REDIS_CLIENT - REDIS_CLIENT = connect_redis() - if REDIS_CLIENT is None: - return - raw = REDIS_CLIENT.get(REDIS_STATE_KEY) - if not raw: - return - data = json.loads(raw) - EVENTS.clear() - EVENTS.extend(data.get("events", [])) - RECENT_SETTLEMENTS.clear() - RECENT_SETTLEMENTS.extend(data.get("recent_settlements", [])) - SESSION_STORE.clear() - SESSION_STORE.update(data.get("sessions", {})) - METRICS.update(data.get("metrics", {})) - - -def persist_state() -> None: - if REDIS_CLIENT is None: - return - REDIS_CLIENT.set(REDIS_STATE_KEY, json.dumps(serialize_state())) - - -def reset_state() -> None: - EVENTS.clear() - RECENT_SETTLEMENTS.clear() - SESSION_STORE.clear() - METRICS.clear() - METRICS.update( - { - "revenue_usdc": 0.0, - "deliveries": 0, - "compute_runs": 0, - "paper_unlocks": 0, - "downloads": 0, - "sessions_created": 0, - } - ) - if REDIS_CLIENT is not None: - REDIS_CLIENT.delete(REDIS_STATE_KEY) - - -def log_event(stage: str, message: str, level: str = "info") -> None: - EVENTS.appendleft( - { - "time": time.strftime("%H:%M:%S"), - "stage": stage, - "message": message, - "level": level, - } - ) - persist_state() - - -def record_settlement( - kind: str, label: str, amount: str, payer: str, tx_hash: str, resource: str -) -> None: - METRICS["revenue_usdc"] += float(amount) - METRICS["deliveries"] += 1 - if kind == "compute": - METRICS["compute_runs"] += 1 - elif kind == "paper": - METRICS["paper_unlocks"] += 1 - elif kind == "session": - METRICS["sessions_created"] += 1 - RECENT_SETTLEMENTS.appendleft( - { - "time": time.strftime("%H:%M:%S"), - "kind": kind, - "label": label, - "amount_usdc": f"{float(amount):.2f}", - "payer": payer or "unknown", - "transaction": tx_hash, - "resource": resource, - } - ) - persist_state() - - -def build_summary() -> dict[str, Any]: - return { - "revenue_usdc": f"{METRICS['revenue_usdc']:.2f}", - "deliveries": METRICS["deliveries"], - "compute_runs": METRICS["compute_runs"], - "paper_unlocks": METRICS["paper_unlocks"], - "downloads": METRICS["downloads"], - "sessions_created": METRICS["sessions_created"], - "recent_settlements": list(RECENT_SETTLEMENTS), - "active_sessions": len(SESSION_STORE), - } - - -def create_session(tier: str, credits: int, payer: str, tx_hash: str) -> dict[str, Any]: - session_id = str(uuid.uuid4()) - session = { - "session_id": session_id, - "tier": tier, - "credits_total": credits, - "credits_remaining": credits, - "payer": payer or "unknown", - "created_at": time.strftime("%Y-%m-%dT%H:%M:%S"), - "transaction": tx_hash, - "jobs": [], - } - SESSION_STORE[session_id] = session - persist_state() - return session - - -def run_session_job(session_id: str, job: str, size: int) -> dict[str, Any]: - session = SESSION_STORE.get(session_id) - if not session: - raise HTTPException(status_code=404, detail="session not found") - if session["credits_remaining"] < 1: - raise HTTPException(status_code=402, detail="session has no credits remaining") - result = build_compute_result(job, size, session["payer"], "0.00", session["transaction"]) - session["credits_remaining"] -= 1 - session["jobs"].append( - { - "job": job, - "size": size, - "ran_at": time.strftime("%H:%M:%S"), - "output": result["output"], - } - ) - METRICS["compute_runs"] += 1 - persist_state() - return { - "session_id": session_id, - "tier": session["tier"], - "credits_remaining": session["credits_remaining"], - "job_result": result, - } - - -def _download_payload(filename: str, tx_hash: str, expires: int) -> str: - return f"{filename}:{tx_hash}:{expires}" - - -def sign_download_token(filename: str, tx_hash: str, expires: int | None = None) -> str: - if expires is None: - expires = int(time.time()) + DOWNLOAD_TOKEN_TTL_SECONDS - payload = _download_payload(filename, tx_hash, expires) - sig = hmac.new(DOWNLOAD_SIGNING_SECRET.encode(), payload.encode(), hashlib.sha256).hexdigest() - token = {"filename": filename, "tx": tx_hash, "exp": expires, "sig": sig} - return base64.urlsafe_b64encode(json.dumps(token).encode()).decode() - - -def verify_download_token(token: str, filename: str) -> None: - try: - data = json.loads(base64.urlsafe_b64decode(token.encode()).decode()) - except Exception as exc: - raise HTTPException(status_code=403, detail="invalid download token") from exc - expected_filename = data.get("filename") - tx_hash = data.get("tx", "") - exp = int(data.get("exp", 0)) - sig = data.get("sig", "") - if expected_filename != filename: - raise HTTPException(status_code=403, detail="download token filename mismatch") - if exp < int(time.time()): - raise HTTPException(status_code=403, detail="download token expired") - payload = _download_payload(filename, tx_hash, exp) - expected_sig = hmac.new( - DOWNLOAD_SIGNING_SECRET.encode(), payload.encode(), hashlib.sha256 - ).hexdigest() - if not hmac.compare_digest(sig, expected_sig): - raise HTTPException(status_code=403, detail="invalid download token signature") - - -def payment_response_header(verify_data: dict[str, Any]) -> str: - return base64.b64encode( - json.dumps( - { - "success": True, - "transaction": verify_data.get("transaction", ""), - "network": "", - "payer": verify_data.get("sender", ""), - } - ).encode() - ).decode() - - -def ensure_sample_pdf(path: Path, title: str, subtitle: str, body: list[str]) -> None: - if path.exists(): - return - path.parent.mkdir(parents=True, exist_ok=True) - lines = [title, subtitle, ""] + body - escaped = [] - for raw in lines: - raw = raw.replace("\\", "\\\\").replace("(", "\\(").replace(")", "\\)") - escaped.append(raw) - content_lines = ["BT", "/F1 18 Tf", "72 760 Td", f"({escaped[0]}) Tj"] - content_lines += ["0 -26 Td", "/F1 12 Tf", f"({escaped[1]}) Tj"] - y_step = -20 - for line in escaped[2:]: - content_lines += [f"0 {y_step} Td", f"({line}) Tj"] - content_lines.append("ET") - stream = "\n".join(content_lines).encode("latin-1", errors="replace") - objs = [] - objs.append(b"1 0 obj<< /Type /Catalog /Pages 2 0 R >>endobj\n") - objs.append(b"2 0 obj<< /Type /Pages /Kids [3 0 R] /Count 1 >>endobj\n") - objs.append( - b"3 0 obj<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] /Contents 4 0 R /Resources << /Font << /F1 5 0 R >> >> >>endobj\n" - ) - objs.append( - b"4 0 obj<< /Length " - + str(len(stream)).encode() - + b" >>stream\n" - + stream - + b"\nendstream\nendobj\n" - ) - objs.append(b"5 0 obj<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>endobj\n") - pdf = bytearray(b"%PDF-1.4\n") - offsets = [0] - for obj in objs: - offsets.append(len(pdf)) - pdf.extend(obj) - xref = len(pdf) - pdf.extend(f"xref\n0 {len(offsets)}\n".encode()) - pdf.extend(b"0000000000 65535 f \n") - for off in offsets[1:]: - pdf.extend(f"{off:010d} 00000 n \n".encode()) - pdf.extend( - f"trailer<< /Size {len(offsets)} /Root 1 0 R >>\nstartxref\n{xref}\n%%EOF\n".encode() - ) - path.write_bytes(pdf) - - -for paper in PAPER_PRODUCTS: - ensure_sample_pdf( - PAPERS_DIR / paper.filename, - paper.title, - "OmniClaw business demo paper", - [ - paper.abstract, - "", - "Paid access is unlocked only after OmniClaw verification and Circle settlement.", - ], - ) - - -async def seller_post(path: str, payload: dict[str, Any]) -> httpx.Response: - async with httpx.AsyncClient(timeout=30.0) as client: - return await client.post( - f"{SELLER_SERVER_URL}{path}", - headers={"Authorization": f"Bearer {SELLER_TOKEN}"}, - json=payload, - ) - - -async def buyer_post(path: str, payload: dict[str, Any]) -> httpx.Response: - async with httpx.AsyncClient(timeout=60.0) as client: - return await client.post( - f"{BUYER_SERVER_URL}{path}", - headers={"Authorization": f"Bearer {BUYER_TOKEN}"}, - json=payload, - ) - - -def build_compute_result( - job: str, size: int, payer: str, amount: str, tx_hash: str -) -> dict[str, Any]: - if job == "prime-count": - if size < 10 or size > 500000: - raise ValueError("size must be between 10 and 500000 for prime-count") - output = {"prime_count": prime_count(size)} - elif job == "fib": - if size < 1 or size > 5000: - raise ValueError("size must be between 1 and 5000 for fib") - output = {"fib": str(fib(size))} - else: - raise ValueError(f"unsupported job: {job}") - return { - "service": "mini-aws-compute", - "job": job, - "input": {"size": size}, - "output": output, - "paid_by": payer, - "amount_usdc": amount, - "settlement_tx": tx_hash, - } - - -def prime_count(limit: int) -> int: - if limit < 2: - return 0 - sieve = bytearray(b"\x01") * (limit + 1) - sieve[0:2] = b"\x00\x00" - for n in range(2, isqrt(limit) + 1): - if sieve[n]: - start = n * n - step = n - sieve[start : limit + 1 : step] = b"\x00" * (((limit - start) // step) + 1) - return int(sum(sieve)) - - -def fib(n: int) -> int: - a, b = 0, 1 - for _ in range(n): - a, b = b, a + b - return a - - -def find_compute(slug: str) -> ComputeProduct: - for product in COMPUTE_PRODUCTS: - if product.slug == slug: - return product - raise KeyError(slug) - - -def find_paper(slug: str) -> PaperProduct: - for paper in PAPER_PRODUCTS: - if paper.slug == slug: - return paper - raise KeyError(slug) - - -async def requirements_response(resource: str, price: str) -> JSONResponse: - resp = await seller_post( - "/api/v1/x402/requirements", {"amount": f"${price}", "resource": resource} - ) - req_data = resp.json() - return JSONResponse( - status_code=req_data.get("status_code", 402), - content=req_data.get("detail", {}), - headers=req_data.get("headers", {}), - ) - - -async def verify_or_402( - request: Request, resource: str, price: str, label: str -) -> dict[str, Any] | JSONResponse: - sig_header = request.headers.get("payment-signature") or request.headers.get( - "PAYMENT-SIGNATURE" - ) - if not sig_header: - log_event("payment-required", f"Unpaid request for {label} -> 402", "warn") - return await requirements_response(resource, price) - log_event( - "verify", f"Payment signature received for {label}; verifying via OmniClaw seller backend" - ) - verify = await seller_post( - "/api/v1/x402/verify", - { - "signature": sig_header, - "amount": price, - "sender": request.headers.get("x-forwarded-for", ""), - "resource": resource, - }, - ) - verify_data = verify.json() - if verify.status_code >= 400 or not verify_data.get("valid"): - log_event("verify", f"Verification failed for {label}", "warn") - return await requirements_response(resource, price) - return verify_data - - -@app.on_event("startup") -async def startup() -> None: - load_state() - log_event("boot", "Business seller booted. Waiting for buyer traffic.") - - -@app.get("/", response_class=HTMLResponse) -async def home() -> str: - return HTML - - -@app.get("/api/catalog") -async def catalog(request: Request) -> dict[str, Any]: - base_url = str(request.base_url).rstrip("/") - products: list[dict[str, Any]] = [] - for product in COMPUTE_PRODUCTS: - query = urlencode({"job": product.job, "size": product.size}) - browser_url = f"{base_url}/compute?{query}" - pay_url = f"{AGENT_BASE_URL}/compute?{query}" - public_pay_url = f"{PUBLIC_BASE_URL}/compute?{query}" - products.append( - { - **asdict(product), - "browser_url": browser_url, - "pay_url": pay_url, - "public_pay_url": public_pay_url, - "badges": ["compute", f"job={product.job}", f"size={product.size}"], - } - ) - for session in SESSION_PRODUCTS: - browser_url = f"{base_url}/compute/session?tier={session.tier}" - pay_url = f"{AGENT_BASE_URL}/compute/session?tier={session.tier}" - public_pay_url = f"{PUBLIC_BASE_URL}/compute/session?tier={session.tier}" - products.append( - { - **asdict(session), - "browser_url": browser_url, - "pay_url": pay_url, - "public_pay_url": public_pay_url, - "badges": ["session", session.tier, f"credits={session.credits}"], - } - ) - for paper in PAPER_PRODUCTS: - browser_url = f"{base_url}/papers/{paper.slug}" - pay_url = f"{AGENT_BASE_URL}/papers/{paper.slug}" - public_pay_url = f"{PUBLIC_BASE_URL}/papers/{paper.slug}" - products.append( - { - **asdict(paper), - "browser_url": browser_url, - "pay_url": pay_url, - "public_pay_url": public_pay_url, - "badges": ["paper", "pdf", paper.title], - } - ) - return { - "network": NETWORK_NAME, - "explorer_base_url": EXPLORER_BASE_URL, - "seller_server": SELLER_SERVER_URL, - "buyer_server": BUYER_SERVER_URL, - "buyer_base_url": PUBLIC_BASE_URL, - "agent_base_url": AGENT_BASE_URL, - "browser_base_url": base_url, - "enable_local_buyer": ENABLE_LOCAL_BUYER, - "products": products, - } - - -@app.get("/api/events") -async def events() -> dict[str, Any]: - return {"events": list(EVENTS)} - - -@app.get("/api/summary") -async def summary() -> dict[str, Any]: - return build_summary() - - -@app.post("/api/admin/reset") -async def admin_reset() -> dict[str, Any]: - reset_state() - log_event("boot", "Business seller reset. Waiting for buyer traffic.") - return {"ok": True, "status": "reset"} - - -@app.post("/api/demo/pay") -async def demo_pay(payload: dict[str, Any]) -> JSONResponse: - if not ENABLE_LOCAL_BUYER: - return JSONResponse( - status_code=409, - content={"success": False, "status": "disabled", "detail": "Local buyer mode disabled"}, - ) - url = payload["pay_url"] - log_event("buyer", f"Buyer initiated payment for {url}") - resp = await buyer_post("/api/v1/x402/pay", {"url": url, "method": "GET"}) - data = resp.json() - outcome = data.get("status", "unknown") - log_event( - "buyer", - f"Buyer payment {outcome} for {payload['label']}", - "good" if data.get("success") else "warn", - ) - return JSONResponse(status_code=resp.status_code, content=data) - - -@app.get("/compute") -async def compute(request: Request) -> JSONResponse: - params = request.query_params - job = (params.get("job") or "prime-count").strip().lower() - size = int((params.get("size") or "1000").strip()) - price = "0.10" - label = f"compute job={job} size={size}" - for product in COMPUTE_PRODUCTS: - if product.job == job and product.size == size: - price = product.price_usdc - label = product.label - break - resource = str(request.url) - verified = await verify_or_402(request, resource, price, label) - if isinstance(verified, JSONResponse): - return verified - result = build_compute_result( - job, size, verified.get("sender") or "unknown", price, verified.get("transaction") or "" - ) - record_settlement( - "compute", - label, - price, - verified.get("sender") or "unknown", - verified.get("transaction") or "", - resource, - ) - log_event( - "delivery", - f"Delivered compute result for {label}; tx {verified.get('transaction', '')}", - "good", - ) - return JSONResponse( - status_code=200, - content=result, - headers={"PAYMENT-RESPONSE": payment_response_header(verified)}, - ) - - -@app.get("/compute/session") -async def compute_session(request: Request) -> JSONResponse: - tier = (request.query_params.get("tier") or "starter").strip().lower() - product = next((p for p in SESSION_PRODUCTS if p.tier == tier), None) - if product is None: - raise HTTPException(status_code=404, detail="session tier not found") - resource = str(request.url) - verified = await verify_or_402(request, resource, product.price_usdc, product.label) - if isinstance(verified, JSONResponse): - return verified - session = create_session( - product.tier, - product.credits, - verified.get("sender") or "unknown", - verified.get("transaction") or "", - ) - record_settlement( - "session", - product.label, - product.price_usdc, - verified.get("sender") or "unknown", - verified.get("transaction") or "", - resource, - ) - log_event("delivery", f"Created {product.label}; session {session['session_id']}", "good") - return JSONResponse( - status_code=200, - content={ - "service": "mini-aws-compute", - "product": product.label, - "tier": product.tier, - "session_id": session["session_id"], - "credits_total": product.credits, - "credits_remaining": product.credits, - "submit_url": f"{PUBLIC_BASE_URL}/compute/jobs/{session['session_id']}?job=prime-count&size=5000", - "status_url": f"{PUBLIC_BASE_URL}/compute/sessions/{session['session_id']}", - "paid_by": verified.get("sender") or "unknown", - "amount_usdc": product.price_usdc, - "settlement_tx": verified.get("transaction") or "", - }, - headers={"PAYMENT-RESPONSE": payment_response_header(verified)}, - ) - - -@app.get("/compute/sessions/{session_id}") -async def compute_session_status(session_id: str) -> JSONResponse: - session = SESSION_STORE.get(session_id) - if not session: - raise HTTPException(status_code=404, detail="session not found") - return JSONResponse(status_code=200, content=session) - - -@app.get("/compute/jobs/{session_id}") -async def compute_session_job(session_id: str, request: Request) -> JSONResponse: - job = (request.query_params.get("job") or "prime-count").strip().lower() - size = int((request.query_params.get("size") or "5000").strip()) - result = run_session_job(session_id, job, size) - log_event("delivery", f"Ran session job {job} size={size} for session {session_id}", "good") - return JSONResponse(status_code=200, content=result) - - -@app.get("/papers/{slug}") -async def paper(slug: str, request: Request) -> JSONResponse: - paper = find_paper(slug) - resource = str(request.url) - verified = await verify_or_402(request, resource, paper.price_usdc, paper.label) - if isinstance(verified, JSONResponse): - return verified - download_token = sign_download_token(paper.filename, verified.get("transaction") or "") - download_url = f"{PUBLIC_BASE_URL}/downloads/{paper.filename}?token={download_token}" - result = { - "service": "research-library", - "product": paper.title, - "abstract": paper.abstract, - "download_url": download_url, - "format": "pdf", - "paid_by": verified.get("sender") or "unknown", - "amount_usdc": paper.price_usdc, - "settlement_tx": verified.get("transaction") or "", - } - record_settlement( - "paper", - paper.title, - paper.price_usdc, - verified.get("sender") or "unknown", - verified.get("transaction") or "", - resource, - ) - log_event( - "delivery", f"Unlocked paper {paper.title}; tx {verified.get('transaction', '')}", "good" - ) - return JSONResponse( - status_code=200, - content=result, - headers={"PAYMENT-RESPONSE": payment_response_header(verified)}, - ) - - -@app.get("/downloads/{filename}") -async def download_pdf(filename: str, token: str | None = None) -> FileResponse: - if not token: - raise HTTPException(status_code=403, detail="download token required") - verify_download_token(token, filename) - path = PAPERS_DIR / filename - if not path.exists(): - raise HTTPException(status_code=404, detail="file not found") - METRICS["downloads"] += 1 - persist_state() - log_event("download", f"PDF downloaded: {filename}") - return FileResponse(path, media_type="application/pdf", filename=filename) diff --git a/examples/business-compute/compute_job.py b/examples/business-compute/compute_job.py deleted file mode 100644 index 47f98e0..0000000 --- a/examples/business-compute/compute_job.py +++ /dev/null @@ -1,78 +0,0 @@ -from __future__ import annotations - -import json -import os -from math import isqrt -from urllib.parse import parse_qs - - -def prime_count(limit: int) -> int: - if limit < 2: - return 0 - sieve = bytearray(b"\x01") * (limit + 1) - sieve[0:2] = b"\x00\x00" - for n in range(2, isqrt(limit) + 1): - if sieve[n]: - start = n * n - step = n - sieve[start : limit + 1 : step] = b"\x00" * (((limit - start) // step) + 1) - return int(sum(sieve)) - - -def fib(n: int) -> int: - a, b = 0, 1 - for _ in range(n): - a, b = b, a + b - return a - - -def main() -> None: - query = parse_qs(os.environ.get("OMNICLAW_REQUEST_QUERY", ""), keep_blank_values=True) - job = (query.get("job", ["prime-count"])[0] or "prime-count").strip().lower() - size_raw = (query.get("size", ["50000"])[0] or "50000").strip() - payer = os.environ.get("OMNICLAW_PAYER_ADDRESS", "unknown") - tx_hash = os.environ.get("OMNICLAW_TX_HASH", "") - amount = os.environ.get("OMNICLAW_AMOUNT_USD", "") - - try: - size = int(size_raw) - except ValueError as err: - print(json.dumps({"error": f"invalid size: {size_raw}"})) - raise SystemExit(2) from err - - if job == "prime-count": - if size < 10 or size > 500000: - print(json.dumps({"error": "size must be between 10 and 500000 for prime-count"})) - raise SystemExit(2) - result = { - "service": "mini-aws-compute", - "job": job, - "input": {"size": size}, - "output": {"prime_count": prime_count(size)}, - "paid_by": payer, - "amount_usdc": amount, - "settlement_tx": tx_hash, - } - elif job == "fib": - if size < 1 or size > 5000: - print(json.dumps({"error": "size must be between 1 and 5000 for fib"})) - raise SystemExit(2) - value = str(fib(size)) - result = { - "service": "mini-aws-compute", - "job": job, - "input": {"n": size}, - "output": {"fib": value}, - "paid_by": payer, - "amount_usdc": amount, - "settlement_tx": tx_hash, - } - else: - print(json.dumps({"error": f"unsupported job: {job}"})) - raise SystemExit(2) - - print(json.dumps(result)) - - -if __name__ == "__main__": - main() diff --git a/examples/business-compute/papers/machine-commerce-settlement-design.pdf b/examples/business-compute/papers/machine-commerce-settlement-design.pdf deleted file mode 100644 index 3d4a7d5..0000000 Binary files a/examples/business-compute/papers/machine-commerce-settlement-design.pdf and /dev/null differ diff --git a/examples/business-compute/papers/policy-controlled-agent-finance.pdf b/examples/business-compute/papers/policy-controlled-agent-finance.pdf deleted file mode 100644 index bd4a5ae..0000000 Binary files a/examples/business-compute/papers/policy-controlled-agent-finance.pdf and /dev/null differ diff --git a/examples/demo/foundation/seller-policy.json b/examples/demo/foundation/seller-policy.json deleted file mode 100644 index 9eb11b8..0000000 --- a/examples/demo/foundation/seller-policy.json +++ /dev/null @@ -1,31 +0,0 @@ -{ - "version": "2.0", - "tokens": { - "seller-agent-token": { - "wallet_alias": "seller-api", - "active": true, - "label": "Seller API" - } - }, - "wallets": { - "seller-api": { - "name": "Seller API", - "limits": { - "daily_max": "100.00", - "hourly_max": "25.00", - "per_tx_max": "10.00", - "per_tx_min": "0.01" - }, - "rate_limits": { - "per_minute": 30, - "per_hour": 500 - }, - "recipients": { - "mode": "allow_all", - "addresses": [], - "domains": [] - }, - "confirm_threshold": null - } - } -} diff --git a/examples/external-x402-facilitator/README.md b/examples/external-x402-facilitator/README.md deleted file mode 100644 index fa84184..0000000 --- a/examples/external-x402-facilitator/README.md +++ /dev/null @@ -1,101 +0,0 @@ -# External x402 Facilitator Validation - -This example validates OmniClaw against an external standard x402 `exact` facilitator. - -Use this first for standard external-facilitator validation. The default path uses the public x402.org facilitator on Base Sepolia. - -## What This Proves - -- seller advertises standard x402 `exact` requirements -- settlement goes through an external facilitator -- OmniClaw buyer can inspect and pay through `/api/v1/pay` -- no OmniClaw self-hosted facilitator is required for this path - -## Seller: x402.org On Base Sepolia - -Preferred setup: - -```bash -export OMNICLAW_PRIVATE_KEY="0xYourSellerPrivateKey" -``` - -Start the seller: - -```bash -python scripts/start_external_x402_seller.py -``` - -Defaults: - -```env -OMNICLAW_X402_EXACT_NETWORK_PROFILE=BASE-SEPOLIA -OMNICLAW_X402_EXACT_NETWORK=eip155:84532 -OMNICLAW_X402_EXACT_PRICE=$0.25 -OMNICLAW_X402_EXACT_FACILITATOR_URL=https://x402.org/facilitator -OMNICLAW_X402_EXACT_PORT=4021 -``` - -The seller harness derives `payTo` from `OMNICLAW_PRIVATE_KEY` by default. - -Optional override: - -```bash -export OMNICLAW_X402_EXACT_PAY_TO="0xYourSellerAddress" -``` - -Use that override only when you intentionally want to advertise a payout address different from the runtime key. - -Paid endpoint: - -```text -http://127.0.0.1:4021/compute?size=70000 -``` - -## Buyer: OmniClaw CLI - -Point the CLI at the buyer Financial Policy Engine: - -```bash -export OMNICLAW_SERVER_URL="http://127.0.0.1:8080" -export OMNICLAW_TOKEN="my-agent-token" -``` - -Inspect: - -```bash -omniclaw-cli inspect-x402 \ - --recipient http://127.0.0.1:4021/compute?size=70000 -``` - -Pay: - -```bash -omniclaw-cli pay \ - --recipient http://127.0.0.1:4021/compute?size=70000 \ - --idempotency-key x402-org-base-sepolia-001 -``` - -## Success Criteria - -- `inspect-x402` selects `x402` -- selected payment source is `direct_wallet` -- selected network is `eip155:84532` -- payment settles -- paid compute response unlocks -- transaction hash appears in the response metadata - -## Product Meaning - -If this passes, the supported product claim is: - -`OmniClaw supports external standard x402 exact facilitators. The buyer remains policy-controlled, while the seller can settle through an external facilitator such as x402.org.` - -Thirdweb remains the next managed facilitator target once account access is available. - -## Layer Ownership - -For this flow: - -- seller harness creates the `accepts` requirements -- x402.org handles `verify` and `settle` -- OmniClaw buyer policy engine inspects, approves, and signs the allowed payment diff --git a/examples/local-economy/README.md b/examples/local-economy/README.md deleted file mode 100644 index 74dbf86..0000000 --- a/examples/local-economy/README.md +++ /dev/null @@ -1,28 +0,0 @@ -# Local Economy Example - -This is the canonical local buyer/seller OmniClaw example. - -It contains: -- buyer stack: `docker-compose.payment-agent.yml` -- seller stack: `docker-compose.seller-agent.yml` -- buyer policy: `payment-agent.policy.json` -- seller policy: `seller-agent.policy.json` - -Roles: -- buyer uses `omniclaw-cli pay` -- seller uses `omniclaw-cli serve` - -Default ports: -- buyer Financial Policy Engine: `9090` -- seller Financial Policy Engine: `9091` -- seller paid endpoint example: `8000` - -Start buyer: -```bash -docker compose -p omniclaw-buyer -f examples/local-economy/docker-compose.payment-agent.yml up -d --build --remove-orphans -``` - -Start seller: -```bash -docker compose -p omniclaw-seller -f examples/local-economy/docker-compose.seller-agent.yml up -d --build --remove-orphans -``` diff --git a/examples/local-economy/docker-compose.payment-agent.yml b/examples/local-economy/docker-compose.payment-agent.yml deleted file mode 100644 index 9232ceb..0000000 --- a/examples/local-economy/docker-compose.payment-agent.yml +++ /dev/null @@ -1,44 +0,0 @@ -services: - payment-agent-redis: - image: redis:7-alpine - command: redis-server --appendonly yes --appendfsync everysec - volumes: - - payment-agent-redis-data:/data - healthcheck: - test: ["CMD", "redis-cli", "ping"] - interval: 5s - timeout: 3s - retries: 10 - - payment-agent: - image: omniclaw-agent:local - build: - context: ../.. - dockerfile: Dockerfile.agent - command: uv run uvicorn omniclaw.agent.server:app --host 0.0.0.0 --port 9090 - environment: - OMNICLAW_REDIS_URL: redis://payment-agent-redis:6379/0 - OMNICLAW_AGENT_POLICY_PATH: /config/runtime-policy.json - OMNICLAW_AGENT_TOKEN: payment-agent-token - OMNICLAW_LOG_LEVEL: INFO - OMNICLAW_POLICY_RELOAD_INTERVAL: 0 - OMNICLAW_OWNER_TOKEN: payment-owner-token - OMNICLAW_NANOPAYMENTS_ENABLED: "true" - OMNICLAW_NETWORK: ${OMNICLAW_NETWORK} - OMNICLAW_RPC_URL: ${OMNICLAW_RPC_URL} - OMNICLAW_STORAGE_BACKEND: redis - OMNICLAW_PRIVATE_KEY: ${BUYER_OMNICLAW_PRIVATE_KEY:-${OMNICLAW_PRIVATE_KEY}} - CIRCLE_API_KEY: ${BUYER_CIRCLE_API_KEY:-${CIRCLE_API_KEY}} - ENTITY_SECRET: ${BUYER_ENTITY_SECRET:-${ENTITY_SECRET}} - volumes: - - ${PAYMENT_AGENT_POLICY_FILE:-./payment-agent.policy.json}:/config/runtime-policy.json - ports: - - "9090:9090" - extra_hosts: - - "host.docker.internal:host-gateway" - depends_on: - payment-agent-redis: - condition: service_healthy - -volumes: - payment-agent-redis-data: diff --git a/examples/local-economy/docker-compose.seller-agent.yml b/examples/local-economy/docker-compose.seller-agent.yml deleted file mode 100644 index 5cd7a0d..0000000 --- a/examples/local-economy/docker-compose.seller-agent.yml +++ /dev/null @@ -1,41 +0,0 @@ -services: - seller-agent-redis: - image: redis:7-alpine - command: redis-server --appendonly yes --appendfsync everysec - volumes: - - seller-agent-redis-data:/data - healthcheck: - test: ["CMD", "redis-cli", "ping"] - interval: 5s - timeout: 3s - retries: 10 - - seller-agent: - image: omniclaw-agent:local - build: - context: ../.. - dockerfile: Dockerfile.agent - command: uv run uvicorn omniclaw.agent.server:app --host 0.0.0.0 --port 9091 - environment: - OMNICLAW_REDIS_URL: redis://seller-agent-redis:6379/0 - OMNICLAW_AGENT_POLICY_PATH: /config/runtime-policy.json - OMNICLAW_AGENT_TOKEN: seller-agent-token - OMNICLAW_LOG_LEVEL: INFO - OMNICLAW_POLICY_RELOAD_INTERVAL: 0 - OMNICLAW_NANOPAYMENTS_ENABLED: "true" - OMNICLAW_NETWORK: ${OMNICLAW_NETWORK} - OMNICLAW_RPC_URL: ${OMNICLAW_RPC_URL} - OMNICLAW_STORAGE_BACKEND: redis - OMNICLAW_PRIVATE_KEY: ${SELLER_OMNICLAW_PRIVATE_KEY:-${OMNICLAW_PRIVATE_KEY}} - CIRCLE_API_KEY: ${SELLER_CIRCLE_API_KEY:-${CIRCLE_API_KEY}} - ENTITY_SECRET: ${SELLER_ENTITY_SECRET:-${ENTITY_SECRET}} - volumes: - - ${SELLER_AGENT_POLICY_FILE:-./seller-agent.policy.json}:/config/runtime-policy.json - ports: - - "9091:9091" - depends_on: - seller-agent-redis: - condition: service_healthy - -volumes: - seller-agent-redis-data: diff --git a/examples/local-economy/payment-agent.policy.json b/examples/local-economy/payment-agent.policy.json deleted file mode 100644 index c4bcea5..0000000 --- a/examples/local-economy/payment-agent.policy.json +++ /dev/null @@ -1,41 +0,0 @@ -{ - "version": "2.0", - "tokens": { - "payment-agent-token": { - "wallet_alias": "payment-agent", - "active": true, - "label": "Payment Agent" - } - }, - "wallets": { - "payment-agent": { - "name": "Payment Agent", - "wallet_id": "2c9bf657-c70e-5d33-873e-08f9b3fcb9c9", - "address": "0x1c3c4c77088494fbc3f9ce1e87bfd7b4cfec0e18", - "limits": { - "daily_max": "25.00", - "hourly_max": "10.00", - "per_tx_max": "1.00", - "per_tx_min": "0.01" - }, - "rate_limits": { - "per_minute": 10, - "per_hour": 100 - }, - "recipients": { - "mode": "whitelist", - "addresses": [ - "0x5a4e248fa08c37b15ea0efdfdf336e92317d5243", - "0x95cE2edF56cc05b2634267d55D8AfB7f8630c143" - ], - "domains": [ - "api.stripe.com", - "localhost", - "127.0.0.1", - "host.docker.internal" - ] - }, - "confirm_threshold": null - } - } -} \ No newline at end of file diff --git a/examples/local-economy/seller-agent.policy.json b/examples/local-economy/seller-agent.policy.json deleted file mode 100644 index b3e0888..0000000 --- a/examples/local-economy/seller-agent.policy.json +++ /dev/null @@ -1,32 +0,0 @@ -{ - "version": "2.0", - "tokens": { - "seller-agent-token": { - "wallet_alias": "seller-agent", - "active": true, - "label": "Seller Agent" - } - }, - "wallets": { - "seller-agent": { - "name": "Seller Agent", - "limits": { - "daily_max": "100.00", - "hourly_max": "25.00", - "per_tx_max": "10.00", - "per_tx_min": "0.01" - }, - "rate_limits": { - "per_minute": 30, - "per_hour": 500 - }, - "recipients": { - "mode": "allow_all", - "addresses": [], - "domains": [] - }, - "confirm_threshold": null, - "address": "0x6a5fb3f11216ad67f80f3ac4c5896aafcc2afc3a" - } - } -} diff --git a/examples/machine-to-machine/README.md b/examples/machine-to-machine/README.md index 206744f..fe628fc 100644 --- a/examples/machine-to-machine/README.md +++ b/examples/machine-to-machine/README.md @@ -16,7 +16,7 @@ Use it when an internal job runner, workflow engine, or autonomous agent needs t Producer service: ```text -https://api.vendor.example/compute +https://api.paid-resource.example/compute ``` Consumer service: @@ -25,9 +25,9 @@ Consumer service: export OMNICLAW_SERVER_URL="http://127.0.0.1:8080" export OMNICLAW_TOKEN="service-agent-token" -omniclaw-cli can-pay --recipient https://api.vendor.example/compute -omniclaw-cli inspect-x402 --recipient https://api.vendor.example/compute -omniclaw-cli pay --recipient https://api.vendor.example/compute --idempotency-key batch-042 +omniclaw-cli can-pay --recipient https://api.paid-resource.example/compute +omniclaw-cli inspect-x402 --recipient https://api.paid-resource.example/compute +omniclaw-cli pay --recipient https://api.paid-resource.example/compute --idempotency-key batch-042 ``` ## Service Contract @@ -41,23 +41,17 @@ Design the downstream API so a machine can use it without special casing: ## When To Use Exact Or Gateway -OmniClaw chooses the route based on the seller's advertised requirements: +OmniClaw chooses the route based on the endpoint's advertised requirements: - use `GatewayWalletBatched` when the producer supports Circle Gateway nanopayments and the consumer has Gateway balance - use `exact` when the producer supports standard x402 settlement -- let `pay` route directly when the seller is exact-only +- let `pay` route directly when the endpoint is exact-only That keeps the consumer side simple: one URL, one policy engine, one payment command. ## Verification Checklist - the consumer sees `can-pay: true` before execution -- `inspect-x402` shows the seller's supported scheme +- `inspect-x402` shows the endpoint's supported scheme - payment succeeds with the same idempotency key on retry - the producer logs a single successful fulfillment event - -## Related Examples - -- [Vendor Integration](../vendor-integration/README.md) -- [Local Economy](../local-economy/README.md) -- [External x402 Facilitator](../external-x402-facilitator/README.md) diff --git a/examples/machine-to-vendor/README.md b/examples/machine-to-vendor/README.md deleted file mode 100644 index 3307976..0000000 --- a/examples/machine-to-vendor/README.md +++ /dev/null @@ -1,86 +0,0 @@ -# Machine-to-Vendor Payments - -This example documents the flow where an external machine or agent pays a vendor-owned service. - -Use it when the vendor controls the product surface and the buyer is a separate agent, workflow, or integration. - -## What It Proves - -- a vendor can publish a paid endpoint without a human checkout -- an external buyer can discover the required payment scheme -- the paid response unlocks only after settlement -- the vendor keeps control of the product and logs - -## Example Vendor Surface - -```text -https://vendor.example.com/premium-report -``` - -The vendor should expose that URL through the SDK, for example: - -```python -from fastapi import FastAPI -from omniclaw import OmniClaw - -app = FastAPI() -client = OmniClaw() - -@app.get("/premium-report") -async def premium_report( - payment=client.sell("$0.50", seller_address="0xVendorWallet"), -): - return {"report": "paid report", "paid_by": payment.payer} -``` - -The buyer treats that URL as a paid resource. - -Agent buyer (requires the Financial Policy Engine `omniclaw server` to be running): - -```bash -export OMNICLAW_SERVER_URL="http://127.0.0.1:8080" -export OMNICLAW_TOKEN="buyer-agent-token" - -omniclaw-cli inspect-x402 --recipient https://vendor.example.com/premium-report -omniclaw-cli pay --recipient https://vendor.example.com/premium-report --idempotency-key report-2026-04-14 -``` - -SDK buyer: - -```python -result = await client.pay( - wallet_id="buyer-wallet-id", - recipient="https://vendor.example.com/premium-report", - amount="1.00", - purpose="premium report", - idempotency_key="report-2026-04-14", -) -``` - -## Vendor Responsibilities - -- advertise the paid endpoint clearly -- return `402 Payment Required` until payment is verified -- use a stable product URL -- keep the response public-safe and deterministic -- log settlement and fulfillment events separately - -## Buyer Responsibilities - -- inspect the seller requirements before paying -- use a stable idempotency key for each attempt -- do not assume a specific rail unless the seller advertises it -- retry only with the same job identity - -## Operational Notes - -The vendor does not need to know the buyer's internal system. -The buyer does not need direct wallet access. -OmniClaw sits between the policy decision and the payment execution. - -For vendor-facing API surfaces, pair this runbook with: - -- [B2B SDK Integration](../b2b-sdk-integration/README.md) -- [Vendor Integration](../vendor-integration/README.md) -- [External x402 Facilitator](../external-x402-facilitator/README.md) -- [Thirdweb HTTP Facilitator](../thirdweb-http-facilitator/README.md) diff --git a/examples/thirdweb-http-facilitator/README.md b/examples/thirdweb-http-facilitator/README.md deleted file mode 100644 index 01fbf3b..0000000 --- a/examples/thirdweb-http-facilitator/README.md +++ /dev/null @@ -1,73 +0,0 @@ -# Thirdweb HTTP Facilitator Validation - -This example validates Thirdweb as a managed external x402 facilitator using HTTP directly from the Python OmniClaw codebase. - -No TypeScript seller SDK is required for this repo. - -OmniClaw uses Thirdweb's public HTTP API: - -- `POST https://api.thirdweb.com/v1/payments/x402/accepts` -- `POST https://api.thirdweb.com/v1/payments/x402/verify` -- `POST https://api.thirdweb.com/v1/payments/x402/settle` -- `POST https://api.thirdweb.com/v1/payments/x402/fetch` -- `GET https://api.thirdweb.com/v1/payments/x402/discovery/resources` - -## What This Proves - -- Thirdweb can handle managed x402 settlement. -- OmniClaw can integrate Thirdweb without competing with it. -- OmniClaw remains the policy and execution control layer. - -## Inputs - -You need two JSON files from an x402 flow: - -- `payment-payload.json` - signed x402 payment payload from the buyer -- `payment-requirements.json` - selected seller payment requirements - -These are the same objects passed to x402 facilitator verify/settle calls. - -For seller-side requirement generation, OmniClaw can call Thirdweb's `accepts` endpoint through `ThirdwebFacilitator.create_accepts(...)`. This is the API that creates the x402 `accepts` array using the Thirdweb server wallet context. - -## Configure - -```bash -export THIRDWEB_SECRET_KEY="..." -export THIRDWEB_SERVER_WALLET_ADDRESS="0x..." -``` - -## Verify Only - -```bash -python examples/thirdweb-http-facilitator/verify_settle.py \ - --payment-payload payment-payload.json \ - --payment-requirements payment-requirements.json \ - --verify-only -``` - -## Verify And Settle - -```bash -python examples/thirdweb-http-facilitator/verify_settle.py \ - --payment-payload payment-payload.json \ - --payment-requirements payment-requirements.json -``` - -## Expected Result - -The script prints JSON with: - -- facilitator name -- verify result -- settle result when settlement is enabled - -## Product Position - -Thirdweb settles. OmniClaw governs. - -For buyer-side agent flows, the normal command remains: - -```bash -omniclaw-cli inspect-x402 --recipient https://seller.example.com/compute -omniclaw-cli pay --recipient https://seller.example.com/compute --idempotency-key job-123 -``` diff --git a/examples/thirdweb-http-facilitator/verify_settle.py b/examples/thirdweb-http-facilitator/verify_settle.py deleted file mode 100644 index a387f3d..0000000 --- a/examples/thirdweb-http-facilitator/verify_settle.py +++ /dev/null @@ -1,74 +0,0 @@ -#!/usr/bin/env python3 -from __future__ import annotations - -import argparse -import asyncio -import json -import os -from pathlib import Path -from typing import Any - -from omniclaw.seller import create_facilitator - - -def load_json(path: str) -> dict[str, Any]: - with Path(path).open() as f: - data = json.load(f) - if not isinstance(data, dict): - raise ValueError(f"{path} must contain a JSON object") - return data - - -async def main() -> int: - parser = argparse.ArgumentParser(description="Validate Thirdweb x402 HTTP facilitator calls.") - parser.add_argument("--payment-payload", required=True, help="Path to signed payment payload JSON") - parser.add_argument( - "--payment-requirements", - required=True, - help="Path to selected payment requirements JSON", - ) - parser.add_argument("--verify-only", action="store_true", help="Do not call settle") - parser.add_argument( - "--secret-key", - default=os.environ.get("THIRDWEB_SECRET_KEY"), - help="Thirdweb secret key. Defaults to THIRDWEB_SECRET_KEY.", - ) - args = parser.parse_args() - - if not args.secret_key: - raise SystemExit("Set THIRDWEB_SECRET_KEY or pass --secret-key") - - facilitator = create_facilitator(provider="thirdweb", api_key=args.secret_key) - try: - payment_payload = load_json(args.payment_payload) - payment_requirements = load_json(args.payment_requirements) - - verify_result = await facilitator.verify(payment_payload, payment_requirements) - output: dict[str, Any] = { - "facilitator": facilitator.name, - "base_url": facilitator.base_url, - "verify": { - "is_valid": verify_result.is_valid, - "payer": verify_result.payer, - "invalid_reason": verify_result.invalid_reason, - }, - } - - if not args.verify_only: - settle_result = await facilitator.settle(payment_payload, payment_requirements) - output["settle"] = { - "success": settle_result.success, - "transaction": settle_result.transaction, - "network": settle_result.network, - "payer": settle_result.payer, - "error_reason": settle_result.error_reason, - } - - print(json.dumps(output, indent=2, sort_keys=True)) - return 0 - finally: - await facilitator.close() - - -if __name__ == "__main__": - raise SystemExit(asyncio.run(main())) diff --git a/examples/vendor-integration/README.md b/examples/vendor-integration/README.md deleted file mode 100644 index d180813..0000000 --- a/examples/vendor-integration/README.md +++ /dev/null @@ -1,153 +0,0 @@ -# Vendor Integration Guide - -This guide shows how a traditional software vendor can expose a paid HTTP surface using the OmniClaw Python SDK. - -Use this when you (a human developer or business) own the product endpoint and want autonomous agents to pay before they receive data, compute, or another gated response. - -> **Note on Tooling:** Vendors build applications using the **OmniClaw Python SDK** (`client.sell()`). The `omniclaw-cli` tool is designed for the autonomous agents that will act as the *buyers* of your service. - -## What It Covers - -- Vendor-side payment gating with FastAPI -- HTTP `402 Payment Required` flows -- Production-safe vendor APIs without human checkout -- SDK-first seller integration for B2B and enterprise deployments - -## Recommended Shape - -Keep the public surface simple: - -- one paid endpoint per product class -- one clear price or pricing tier per endpoint -- one idempotency key per job or request -- one seller-side log stream for verification and audit - -Example public endpoint: - -```text -https://vendor.example.com/compute -``` - -## Vendor Setup (FastAPI SDK) - -To require payment for an endpoint, integrate the OmniClaw SDK into your application. See the provided `app.py` for the complete example. - -```python -from fastapi import FastAPI -from omniclaw import OmniClaw - -app = FastAPI() -client = OmniClaw() - -@app.get("/compute") -async def premium_compute( - payment=client.sell("$0.25", seller_address="0xYourVendorWalletAddress"), -): - # This code only runs AFTER payment is verified and settled! - return { - "status": "success", - "paid_by": payment.payer, - "result": {"data": "..."} - } -``` - -Run your vendor application: - -```bash -uvicorn app:app --port 8000 -``` - -## Facilitator Options - -The vendor controls the seller integration and chooses the settlement path. - -### Circle Gateway - -Default seller path when using Circle Gateway: - -```python -payment=client.sell("$0.25", seller_address="0xYourVendorWalletAddress") -``` - -### Thirdweb Managed x402 - -Use Thirdweb when the vendor wants managed x402 facilitator coverage: - -```bash -export THIRDWEB_SECRET_KEY="..." -export THIRDWEB_SERVER_WALLET_ADDRESS="0xThirdwebServerWallet" -export THIRDWEB_X402_NETWORK="base-sepolia" -``` - -```python -payment=client.sell( - "$0.25", - seller_address="0xThirdwebServerWallet", - facilitator="thirdweb", -) -``` - -### OmniClaw Self-Hosted Exact - -Use this when the vendor wants to run its own exact facilitator for Arc, Base Sepolia, or another supported EVM profile: - -```bash -omniclaw facilitator exact \ - --network-profile ARC-TESTNET \ - --port 4022 -``` - -```bash -export OMNICLAW_X402_SELF_HOSTED_FACILITATOR_URL="http://127.0.0.1:4022" -export OMNICLAW_X402_EXACT_NETWORK_PROFILE="ARC-TESTNET" -``` - -```python -payment=client.sell( - "$0.25", - seller_address="0xYourVendorWalletAddress", - facilitator="omniclaw", -) -``` - -## Buyer Integration - -Buyers can pay from an agent CLI, a backend using the SDK, or any compatible x402 buyer. - -Agent buyer: - -```bash -export OMNICLAW_SERVER_URL="http://127.0.0.1:8080" -export OMNICLAW_TOKEN="agent-token-123" - -# The agent inspects your requirements -omniclaw-cli inspect-x402 --recipient https://vendor.example.com/compute - -# The agent executes the payment -omniclaw-cli pay --recipient https://vendor.example.com/compute --idempotency-key job-123 -``` - -SDK buyer: - -```python -result = await client.pay( - wallet_id="buyer-wallet-id", - recipient="https://vendor.example.com/compute", - amount="1.00", - purpose="vendor compute", - idempotency_key="job-123", -) -``` - -## Verification Checklist - -- unauthenticated/unpaid requests return `402 Payment Required` -- paid requests return `200 OK` and the expected product payload -- the seller log shows a matching settlement event -- the published URL does not require a human login flow - -## Related Examples - -- [Business Compute](../business-compute/README.md) - A larger example of a vendor web app with sessions. -- [B2B SDK Integration](../b2b-sdk-integration/README.md) -- [Machine-to-Machine](../machine-to-machine/README.md) diff --git a/examples/vendor-integration/app.py b/examples/vendor-integration/app.py deleted file mode 100644 index 156e4c6..0000000 --- a/examples/vendor-integration/app.py +++ /dev/null @@ -1,53 +0,0 @@ -""" -Vendor Integration Example: FastAPI app protected by OmniClaw - -This example demonstrates how a human developer (a vendor or business) -uses the OmniClaw Python SDK to monetize an API endpoint. - -Unlike autonomous agents that use `omniclaw-cli`, vendors integrate -the OmniClaw SDK directly into their backend services. -""" - -from fastapi import FastAPI -from omniclaw import OmniClaw - -app = FastAPI(title="Vendor Payment-Gated API") -client = OmniClaw() - -# 1. Provide your seller wallet address. -# This represents the vendor's wallet where payments will settle. -# For this example, we use a placeholder or assume it's passed via env. -import os -SELLER_ADDRESS = os.environ.get("SELLER_ADDRESS", "0xYourVendorWalletAddress") - -# 2. Add an endpoint that is free -@app.get("/status") -async def status(): - return {"status": "ok", "message": "This endpoint is free."} - -# 3. Add an endpoint protected by OmniClaw x402 payment gate (Price: $0.25) -# The `client.sell` dependency handles the 402 Payment Required handshake -# and only allows the request through once settlement is verified. -@app.get("/compute") -async def premium_compute( - payment=client.sell("$0.25", seller_address=SELLER_ADDRESS), -): - # If the code reaches here, OmniClaw has verified the payment! - - # You can access payment metadata for logging or audit - payer_address = payment.payer - - return { - "status": "success", - "message": "Payment verified. Compute complete.", - "paid_by": payer_address, - "amount": "$0.25", - "result": { - "computation": "complex_data_analysis_result" - } - } - -if __name__ == "__main__": - import uvicorn - # Run the vendor app - uvicorn.run(app, host="127.0.0.1", port=8000) diff --git a/pyproject.toml b/pyproject.toml index 5c5d951..b53a991 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -55,9 +55,9 @@ dependencies = [ "redis==7.1.0", "tenacity==8.2.0", # x402 official SDK and signing - "x402[httpx]>=2.0.0", - "eth-account==0.12.0", - "web3>=6.0.0", + "x402[evm,fastapi,httpx]>=2.5.0", + "eth-account>=0.13.6", + "web3>=7.0.0", "typer>=0.15.0", "fastapi>=0.100.0", "uvicorn[standard]>=0.20.0", @@ -86,8 +86,8 @@ Repository = "https://github.com/omnuron/omniclaw" Issues = "https://github.com/omnuron/omniclaw/issues" [project.scripts] -omniclaw = "omniclaw.admin_cli:main" -omniclaw-cli = "omniclaw.cli_agent:main" +omniclaw = "omniclaw.cli:main" +omniclaw-cli = "omniclaw.cli:main" [dependency-groups] dev = [ @@ -127,7 +127,6 @@ warn_unused_ignores = true [tool.ruff.lint.per-file-ignores] # nanopayments __init__ needs specific import order to avoid circular imports "src/omniclaw/protocols/nanopayments/__init__.py" = ["I001"] -"src/omniclaw/admin_cli.py" = ["I001", "F401", "E402"] "src/omniclaw/__init__.py" = ["E402", "F401", "I001"] "src/omniclaw/agent/server.py" = ["E402", "F401"] "src/omniclaw/agent/routes.py" = ["B008", "UP037", "E402"] diff --git a/pytest.ini b/pytest.ini index 5d66ae6..8a76418 100644 --- a/pytest.ini +++ b/pytest.ini @@ -25,6 +25,7 @@ markers = cctp: CCTP-related tests x402: x402 protocol tests utils: Utility function tests + live_postgres: Tests that require OMNICLAW_TEST_POSTGRES_DSN and a real Postgres database # Asyncio configuration asyncio_mode = auto diff --git a/scripts/demo_foundation_showcase.sh b/scripts/demo_foundation_showcase.sh deleted file mode 100755 index 95fe6c0..0000000 --- a/scripts/demo_foundation_showcase.sh +++ /dev/null @@ -1,686 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" -RUN_TS="$(date +%Y%m%d_%H%M%S)" -LOG_DIR_DEFAULT="$ROOT_DIR/logs/foundation_demo_$RUN_TS" - -ENV_FILE="$ROOT_DIR/.env" -NETWORK="ETH-SEPOLIA" -RPC_URL="${OMNICLAW_RPC_URL:-}" -LOG_DIR="$LOG_DIR_DEFAULT" - -BUYER_CP_PORT=9190 -SELLER_CP_PORT=9191 -SELLER_GATE_PORT=9291 - -BUYER_TOKEN="payment-agent-token" -BUYER_ALIAS="omni-bot-v4" -SELLER_TOKEN="seller-agent-token" -SELLER_ALIAS="seller-api" -OWNER_TOKEN="foundation-demo-owner" - -BLOCKED_URL="https://sensayhack-402.onrender.com" -ALLOWED_URL="https://api.stripe.com" -PRICE="0.01" -ENDPOINT="/api/data" -SELLER_EXEC_CMD='printf "{\"result\":\"premium data unlocked\",\"provider\":\"agent-a\",\"settlement\":\"gateway-batched\",\"transport\":\"x402\"}\n"' -CLI_RETRY_ATTEMPTS=5 - -HOLD=0 - -PIDS=() - -usage() { - cat <<'EOF' -Usage: scripts/demo_foundation_showcase.sh [options] - -Options: - --env-file Path to env file (default: .env) - --network ETH-SEPOLIA or BASE-SEPOLIA - --base Shortcut for --network BASE-SEPOLIA - --eth Shortcut for --network ETH-SEPOLIA - --rpc-url Override RPC URL - --buyer-cp-port Buyer control plane port (default: 9190) - --seller-cp-port Seller control plane port (default: 9191) - --seller-gate-port Seller gate port (default: 9291) - --blocked-url URL that should be blocked by policy - --allowed-url URL that should be allowed by policy - --price Price for seller endpoint (default: 0.01) - --endpoint Seller gated path (default: /api/data) - --seller-exec Command executed after successful payment - --log-dir Log directory - --owner-token Owner token used for confirmation step - --hold Keep processes alive after the showcase - -h, --help Show help -EOF -} - -while [[ $# -gt 0 ]]; do - case "$1" in - --env-file) ENV_FILE="$2"; shift 2 ;; - --network) NETWORK="$2"; shift 2 ;; - --base) NETWORK="BASE-SEPOLIA"; shift ;; - --eth) NETWORK="ETH-SEPOLIA"; shift ;; - --rpc-url) RPC_URL="$2"; shift 2 ;; - --buyer-cp-port) BUYER_CP_PORT="$2"; shift 2 ;; - --seller-cp-port) SELLER_CP_PORT="$2"; shift 2 ;; - --seller-gate-port) SELLER_GATE_PORT="$2"; shift 2 ;; - --blocked-url) BLOCKED_URL="$2"; shift 2 ;; - --allowed-url) ALLOWED_URL="$2"; shift 2 ;; - --price) PRICE="$2"; shift 2 ;; - --endpoint) ENDPOINT="$2"; shift 2 ;; - --seller-exec) SELLER_EXEC_CMD="$2"; shift 2 ;; - --log-dir) LOG_DIR="$2"; shift 2 ;; - --owner-token) OWNER_TOKEN="$2"; shift 2 ;; - --hold) HOLD=1; shift ;; - -h|--help) usage; exit 0 ;; - *) echo "Unknown option: $1" >&2; usage; exit 1 ;; - esac -done - -if [[ ! -f "$ENV_FILE" ]]; then - echo "Env file not found: $ENV_FILE" >&2 - exit 1 -fi - -if [[ "$NETWORK" != "ETH-SEPOLIA" && "$NETWORK" != "BASE-SEPOLIA" ]]; then - echo "Unsupported network: $NETWORK" >&2 - exit 1 -fi - -if [[ -z "$RPC_URL" ]]; then - if [[ "$NETWORK" == "ETH-SEPOLIA" ]]; then - RPC_URL="https://ethereum-sepolia-rpc.publicnode.com" - else - RPC_URL="https://base-sepolia-rpc.publicnode.com" - fi -fi - -require_cmd() { - if ! command -v "$1" >/dev/null 2>&1; then - echo "Missing required command: $1" >&2 - exit 1 - fi -} - -require_cmd uv -require_cmd curl -require_cmd jq -require_cmd python3 -require_cmd base64 -require_cmd awk -require_cmd omniclaw-cli - -set -a -# shellcheck disable=SC1090 -source "$ENV_FILE" -set +a - -pick_env() { - local preferred="$1" - local fallback="$2" - if [[ -n "${!preferred:-}" ]]; then - printf "%s" "${!preferred}" - return 0 - fi - if [[ -n "${!fallback:-}" ]]; then - printf "%s" "${!fallback}" - return 0 - fi - return 1 -} - -BUYER_CIRCLE_API_KEY="$(pick_env BUYER_CIRCLE_API_KEY CIRCLE_API_KEY || true)" -SELLER_CIRCLE_API_KEY="$(pick_env SELLER_CIRCLE_API_KEY CIRCLE_API_KEY || true)" -BUYER_ENTITY_SECRET="$(pick_env BUYER_ENTITY_SECRET ENTITY_SECRET || true)" -SELLER_ENTITY_SECRET="$(pick_env SELLER_ENTITY_SECRET ENTITY_SECRET || true)" -BUYER_PRIVATE_KEY="$(pick_env BUYER_OMNICLAW_PRIVATE_KEY OMNICLAW_PRIVATE_KEY || true)" -SELLER_PRIVATE_KEY="$(pick_env SELLER_OMNICLAW_PRIVATE_KEY OMNICLAW_PRIVATE_KEY || true)" -OMNICLAW_LOG_LEVEL="${OMNICLAW_LOG_LEVEL:-INFO}" - -require_value() { - local name="$1" - local value="$2" - if [[ -z "$value" ]]; then - echo "Missing required value: $name" >&2 - exit 1 - fi -} - -require_value "BUYER_CIRCLE_API_KEY or CIRCLE_API_KEY" "$BUYER_CIRCLE_API_KEY" -require_value "SELLER_CIRCLE_API_KEY or CIRCLE_API_KEY" "$SELLER_CIRCLE_API_KEY" -require_value "BUYER_ENTITY_SECRET or ENTITY_SECRET" "$BUYER_ENTITY_SECRET" -require_value "SELLER_ENTITY_SECRET or ENTITY_SECRET" "$SELLER_ENTITY_SECRET" -require_value "BUYER_OMNICLAW_PRIVATE_KEY or OMNICLAW_PRIVATE_KEY" "$BUYER_PRIVATE_KEY" -require_value "SELLER_OMNICLAW_PRIVATE_KEY or OMNICLAW_PRIVATE_KEY" "$SELLER_PRIVATE_KEY" - -mkdir -p "$LOG_DIR" - -BUYER_POLICY_SRC="$ROOT_DIR/examples/demo/foundation/buyer-policy.json" -SELLER_POLICY_SRC="$ROOT_DIR/examples/demo/foundation/seller-policy.json" -BUYER_POLICY_RUNTIME="$LOG_DIR/buyer-policy.runtime.json" -SELLER_POLICY_RUNTIME="$LOG_DIR/seller-policy.runtime.json" -cp "$BUYER_POLICY_SRC" "$BUYER_POLICY_RUNTIME" -cp "$SELLER_POLICY_SRC" "$SELLER_POLICY_RUNTIME" - -BUYER_CONFIG_DIR="$LOG_DIR/buyer-cli-config" -SELLER_CONFIG_DIR="$LOG_DIR/seller-cli-config" -mkdir -p "$BUYER_CONFIG_DIR" "$SELLER_CONFIG_DIR" - -BUYER_CP_LOG="$LOG_DIR/buyer-control-plane.log" -SELLER_CP_LOG="$LOG_DIR/seller-control-plane.log" -SELLER_GATE_LOG="$LOG_DIR/seller-gateway.log" -BUYER_CLI_LOG="$LOG_DIR/buyer-cli.log" -SELLER_CLI_LOG="$LOG_DIR/seller-cli.log" - -banner() { - printf '\n================================================================\n' - printf ' %s\n' "$1" - printf '================================================================\n' -} - -section() { - printf '\n%s\n' "$1" -} - -kv() { - printf ' %-18s %s\n' "$1" "$2" -} - -show_cmd() { - printf '\n$ %s\n' "$1" -} - -port_in_use() { - local port="$1" - python3 - "$port" <<'PY' -import socket -import sys - -port = int(sys.argv[1]) -s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) -try: - s.bind(("127.0.0.1", port)) -except OSError: - print("in-use") - sys.exit(0) -finally: - s.close() -print("free") -PY -} - -cleanup() { - local rc=$? - for pid in "${PIDS[@]}"; do - if kill -0 "$pid" >/dev/null 2>&1; then - kill "$pid" >/dev/null 2>&1 || true - fi - done - for pid in "${PIDS[@]}"; do - wait "$pid" 2>/dev/null || true - done - if [[ $rc -ne 0 ]]; then - printf '\nDemo failed. Logs: %s\n' "$LOG_DIR" >&2 - fi - exit "$rc" -} -trap cleanup EXIT INT TERM - -start_bg() { - local logfile="$1" - shift - ("$@") >"$logfile" 2>&1 & - local pid=$! - PIDS+=("$pid") - sleep 0.4 - if ! kill -0 "$pid" >/dev/null 2>&1; then - tail -n 80 "$logfile" >&2 || true - exit 1 - fi -} - -wait_for_http_ok() { - local url="$1" - local timeout_secs="${2:-90}" - local start_ts - start_ts="$(date +%s)" - while true; do - if curl -fsS "$url" >/dev/null 2>&1; then - return 0 - fi - if (( "$(date +%s)" - start_ts >= timeout_secs )); then - return 1 - fi - sleep 1 - done -} - -wait_for_http_status() { - local url="$1" - local expected_status="$2" - local timeout_secs="${3:-90}" - local start_ts http_code - start_ts="$(date +%s)" - while true; do - http_code="$(curl -s -o /dev/null -w "%{http_code}" "$url" || true)" - if [[ "$http_code" == "$expected_status" ]]; then - return 0 - fi - if (( "$(date +%s)" - start_ts >= timeout_secs )); then - return 1 - fi - sleep 1 - done -} - -cli_json_once() { - local side="$1" - shift - local config_dir server_url token owner_token wallet circle_key entity_secret private_key log_file - if [[ "$side" == "buyer" ]]; then - config_dir="$BUYER_CONFIG_DIR" - server_url="http://127.0.0.1:$BUYER_CP_PORT" - token="$BUYER_TOKEN" - owner_token="$OWNER_TOKEN" - wallet="$BUYER_ALIAS" - circle_key="$BUYER_CIRCLE_API_KEY" - entity_secret="$BUYER_ENTITY_SECRET" - private_key="$BUYER_PRIVATE_KEY" - log_file="$BUYER_CLI_LOG" - else - config_dir="$SELLER_CONFIG_DIR" - server_url="http://127.0.0.1:$SELLER_CP_PORT" - token="$SELLER_TOKEN" - owner_token="" - wallet="$SELLER_ALIAS" - circle_key="$SELLER_CIRCLE_API_KEY" - entity_secret="$SELLER_ENTITY_SECRET" - private_key="$SELLER_PRIVATE_KEY" - log_file="$SELLER_CLI_LOG" - fi - - { - echo - echo ">>> [$side] omniclaw-cli $*" - } >>"$log_file" - - ( - cd "$ROOT_DIR" - OMNICLAW_CONFIG_DIR="$config_dir" \ - OMNICLAW_SERVER_URL="$server_url" \ - OMNICLAW_TOKEN="$token" \ - OMNICLAW_OWNER_TOKEN="$owner_token" \ - CIRCLE_API_KEY="$circle_key" \ - ENTITY_SECRET="$entity_secret" \ - OMNICLAW_PRIVATE_KEY="$private_key" \ - OMNICLAW_NETWORK="$NETWORK" \ - OMNICLAW_RPC_URL="$RPC_URL" \ - PYTHONHASHSEED=0 \ - omniclaw-cli "$@" - ) | tee -a "$log_file" -} - -cli_json() { - local side="$1" - shift - local rc output attempt log_file - - if [[ "$side" == "buyer" ]]; then - log_file="$BUYER_CLI_LOG" - else - log_file="$SELLER_CLI_LOG" - fi - - for ((attempt = 1; attempt <= CLI_RETRY_ATTEMPTS; attempt++)); do - set +e - output="$(cli_json_once "$side" "$@" 2>&1)" - rc=$? - set -e - - if [[ $rc -eq 0 ]]; then - printf '%s\n' "$output" - return 0 - fi - - { - echo - echo ">>> [$side] retry $attempt/$CLI_RETRY_ATTEMPTS exited with code $rc" - } >>"$log_file" - - if (( attempt < CLI_RETRY_ATTEMPTS )); then - sleep "$attempt" - fi - done - - if [[ $rc -ne 0 ]]; then - printf '%s\n' "$output" >&2 - return "$rc" - fi -} - -api_json() { - local side="$1" - local method="$2" - local path="$3" - local query="${4:-}" - local body="${5:-}" - local url token owner_header - - if [[ "$side" == "buyer" ]]; then - url="http://127.0.0.1:$BUYER_CP_PORT$path" - token="$BUYER_TOKEN" - else - url="http://127.0.0.1:$SELLER_CP_PORT$path" - token="$SELLER_TOKEN" - fi - - if [[ -n "$query" ]]; then - url="$url?$query" - fi - - owner_header=() - if [[ "$method" == "OWNERPOST" ]]; then - method="POST" - owner_header=(-H "X-Omniclaw-Owner-Token: $OWNER_TOKEN") - fi - - if [[ -n "$body" ]]; then - curl -fsS -X "$method" \ - -H "Authorization: Bearer $token" \ - "${owner_header[@]}" \ - -H "Content-Type: application/json" \ - -d "$body" \ - "$url" - else - curl -fsS -X "$method" \ - -H "Authorization: Bearer $token" \ - "${owner_header[@]}" \ - "$url" - fi -} - -atomic_to_usdc() { - awk -v a="$1" 'BEGIN {printf "%.6f", a / 1000000}' -} - -atomic_delta_to_usdc_signed() { - awk -v after="$1" -v before="$2" 'BEGIN {d=(after-before)/1000000; if (d > 0) printf "+%.6f", d; else printf "%.6f", d}' -} - -short_addr() { - local v="$1" - if [[ ${#v} -le 12 ]]; then - printf "%s" "$v" - else - printf "%s...%s" "${v:0:6}" "${v: -4}" - fi -} - -decode_payment_required() { - local url="$1" - local headers_file="$LOG_DIR/payment-required.headers" - local body_file="$LOG_DIR/payment-required.body" - curl -sS -D "$headers_file" -o "$body_file" "$url" >/dev/null - awk 'BEGIN{IGNORECASE=1} /^payment-required:/{sub(/^[^:]*:[[:space:]]*/, ""); sub(/\r$/, ""); print; exit}' "$headers_file" -} - -for port in "$BUYER_CP_PORT" "$SELLER_CP_PORT" "$SELLER_GATE_PORT"; do - if [[ "$(port_in_use "$port")" == "in-use" ]]; then - echo "Port $port is already in use. Free it or override the port flags." >&2 - exit 1 - fi -done - -banner "OmniClaw Autonomous Economy Demo" -kv "Flow" "policy block -> paid API -> approval -> gasless settlement" -kv "Network" "$NETWORK" -kv "Buyer Control" "http://localhost:$BUYER_CP_PORT" -kv "Seller Control" "http://localhost:$SELLER_CP_PORT" -kv "Paid Endpoint" "http://localhost:$SELLER_GATE_PORT$ENDPOINT" -kv "Log Bundle" "$LOG_DIR" - -section "1. Bring up buyer and seller agents" -start_bg "$BUYER_CP_LOG" env \ - CIRCLE_API_KEY="$BUYER_CIRCLE_API_KEY" \ - ENTITY_SECRET="$BUYER_ENTITY_SECRET" \ - OMNICLAW_PRIVATE_KEY="$BUYER_PRIVATE_KEY" \ - OMNICLAW_AGENT_POLICY_PATH="$BUYER_POLICY_RUNTIME" \ - OMNICLAW_AGENT_TOKEN="$BUYER_TOKEN" \ - OMNICLAW_OWNER_TOKEN="$OWNER_TOKEN" \ - OMNICLAW_NETWORK="$NETWORK" \ - OMNICLAW_RPC_URL="$RPC_URL" \ - OMNICLAW_STORAGE_BACKEND=memory \ - OMNICLAW_POLICY_RELOAD_INTERVAL=0 \ - OMNICLAW_LOG_LEVEL="$OMNICLAW_LOG_LEVEL" \ - PYTHONUNBUFFERED=1 \ - uv run uvicorn omniclaw.agent.server:app --host 0.0.0.0 --port "$BUYER_CP_PORT" --log-level warning - -start_bg "$SELLER_CP_LOG" env \ - CIRCLE_API_KEY="$SELLER_CIRCLE_API_KEY" \ - ENTITY_SECRET="$SELLER_ENTITY_SECRET" \ - OMNICLAW_PRIVATE_KEY="$SELLER_PRIVATE_KEY" \ - OMNICLAW_AGENT_POLICY_PATH="$SELLER_POLICY_RUNTIME" \ - OMNICLAW_AGENT_TOKEN="$SELLER_TOKEN" \ - OMNICLAW_NETWORK="$NETWORK" \ - OMNICLAW_RPC_URL="$RPC_URL" \ - OMNICLAW_STORAGE_BACKEND=memory \ - OMNICLAW_POLICY_RELOAD_INTERVAL=0 \ - OMNICLAW_LOG_LEVEL="$OMNICLAW_LOG_LEVEL" \ - PYTHONUNBUFFERED=1 \ - uv run uvicorn omniclaw.agent.server:app --host 0.0.0.0 --port "$SELLER_CP_PORT" --log-level warning - -wait_for_http_ok "http://127.0.0.1:$BUYER_CP_PORT/api/v1/health" || { tail -n 80 "$BUYER_CP_LOG" >&2; exit 1; } -wait_for_http_ok "http://127.0.0.1:$SELLER_CP_PORT/api/v1/health" || { tail -n 80 "$SELLER_CP_LOG" >&2; exit 1; } -printf 'Buyer and seller control planes are live.\n' - -show_cmd "omniclaw-cli configure --server-url http://localhost:$BUYER_CP_PORT --token $BUYER_TOKEN --wallet $BUYER_ALIAS --owner-token " -cli_json buyer configure \ - --server-url "http://localhost:$BUYER_CP_PORT" \ - --token "$BUYER_TOKEN" \ - --wallet "$BUYER_ALIAS" \ - --owner-token "$OWNER_TOKEN" \ - >/dev/null -kv "Buyer Agent" "$BUYER_ALIAS connected" - -show_cmd "omniclaw-cli configure --server-url http://localhost:$SELLER_CP_PORT --token $SELLER_TOKEN --wallet $SELLER_ALIAS" -cli_json seller configure \ - --server-url "http://localhost:$SELLER_CP_PORT" \ - --token "$SELLER_TOKEN" \ - --wallet "$SELLER_ALIAS" \ - >/dev/null -kv "Seller Agent" "$SELLER_ALIAS connected" - -section "2. Guard rails block the wrong counterparty" -show_cmd "omniclaw-cli status" -BUYER_STATUS="$(cli_json buyer status)" -BUYER_WALLET="$(printf '%s' "$BUYER_STATUS" | jq -r '.Wallet')" -BUYER_BALANCE_LINE="$(printf '%s' "$BUYER_STATUS" | jq -r '.Balance')" -kv "Buyer Wallet" "$(short_addr "$BUYER_WALLET")" -kv "Buyer Balance" "$BUYER_BALANCE_LINE" - -show_cmd "omniclaw-cli can-pay --recipient $BLOCKED_URL" -BLOCKED_RESULT="$(cli_json buyer can-pay --recipient "$BLOCKED_URL")" -if [[ "$(printf '%s' "$BLOCKED_RESULT" | jq -r '.can_pay')" == "true" ]]; then - echo "Blocked URL unexpectedly allowed." >&2 - exit 1 -fi -kv "Blocked URL" "$BLOCKED_URL" -kv "Policy Result" "blocked before any spend" -kv "Reason" "$(printf '%s' "$BLOCKED_RESULT" | jq -r '.reason')" - -show_cmd "omniclaw-cli can-pay --recipient $ALLOWED_URL" -ALLOWED_RESULT="$(cli_json buyer can-pay --recipient "$ALLOWED_URL")" -if [[ "$(printf '%s' "$ALLOWED_RESULT" | jq -r '.can_pay')" != "true" ]]; then - echo "Allowed URL was not permitted by policy." >&2 - exit 1 -fi -kv "Allowed URL" "$ALLOWED_URL" -kv "Policy Result" "allowed by whitelist" - -section "3. Seller opens a paid API" -start_bg "$SELLER_GATE_LOG" env \ - CIRCLE_API_KEY="$SELLER_CIRCLE_API_KEY" \ - ENTITY_SECRET="$SELLER_ENTITY_SECRET" \ - OMNICLAW_PRIVATE_KEY="$SELLER_PRIVATE_KEY" \ - OMNICLAW_SERVER_URL="http://127.0.0.1:$SELLER_CP_PORT" \ - OMNICLAW_TOKEN="$SELLER_TOKEN" \ - OMNICLAW_CONFIG_DIR="$SELLER_CONFIG_DIR" \ - OMNICLAW_NETWORK="$NETWORK" \ - OMNICLAW_RPC_URL="$RPC_URL" \ - OMNICLAW_LOG_LEVEL="$OMNICLAW_LOG_LEVEL" \ - PYTHONHASHSEED=0 \ - PYTHONUNBUFFERED=1 \ - uv run python -m omniclaw.cli_agent serve \ - --price "$PRICE" \ - --endpoint "$ENDPOINT" \ - --exec "$SELLER_EXEC_CMD" \ - --port "$SELLER_GATE_PORT" - -wait_for_http_status "http://127.0.0.1:$SELLER_GATE_PORT$ENDPOINT" "402" || { tail -n 120 "$SELLER_GATE_LOG" >&2; exit 1; } -SELLER_402_HEADER="$(decode_payment_required "http://127.0.0.1:$SELLER_GATE_PORT$ENDPOINT")" -printf '%s' "$SELLER_402_HEADER" | base64 -d >"$LOG_DIR/payment-required.json" - -REQ_SCHEME="$(jq -r '.accepts[0].extra.name' "$LOG_DIR/payment-required.json")" -REQ_NETWORK="$(jq -r '.accepts[0].network' "$LOG_DIR/payment-required.json")" -REQ_PAY_TO="$(jq -r '.accepts[0].payTo' "$LOG_DIR/payment-required.json")" -REQ_ATOMIC="$(jq -r '.accepts[0].amount' "$LOG_DIR/payment-required.json")" -REQ_USD="$(awk -v atomic="$REQ_ATOMIC" 'BEGIN {printf "%.2f", atomic / 1000000}')" -REQ_CONTRACT="$(jq -r '.accepts[0].extra.verifyingContract' "$LOG_DIR/payment-required.json")" - -SELLER_BEFORE_DETAIL="$(api_json seller GET /api/v1/balance-detail)" -BUYER_BEFORE_DETAIL="$(api_json buyer GET /api/v1/balance-detail)" -SELLER_BEFORE_GATEWAY="$(printf '%s' "$SELLER_BEFORE_DETAIL" | jq -r '.gateway_balance')" -BUYER_BEFORE_GATEWAY="$(printf '%s' "$BUYER_BEFORE_DETAIL" | jq -r '.gateway_balance')" -SELLER_BEFORE_GATEWAY_ATOMIC="$(printf '%s' "$SELLER_BEFORE_DETAIL" | jq -r '.gateway_balance_atomic')" -BUYER_BEFORE_GATEWAY_ATOMIC="$(printf '%s' "$BUYER_BEFORE_DETAIL" | jq -r '.gateway_balance_atomic')" -SELLER_EOA="$(printf '%s' "$SELLER_BEFORE_DETAIL" | jq -r '.eoa_address')" -SELLER_CIRCLE="$(printf '%s' "$SELLER_BEFORE_DETAIL" | jq -r '.circle_wallet_address')" -BUYER_EOA="$(printf '%s' "$BUYER_BEFORE_DETAIL" | jq -r '.eoa_address')" -BUYER_CIRCLE="$(printf '%s' "$BUYER_BEFORE_DETAIL" | jq -r '.circle_wallet_address')" - -kv "Paid URL" "http://localhost:$SELLER_GATE_PORT$ENDPOINT" -kv "Seller Model" "Circle Gateway gasless x402, batch-settled by GatewayMiddleware" -kv "HTTP Probe" "402 Payment Required" -kv "Scheme" "$REQ_SCHEME" -kv "Network" "$REQ_NETWORK" -kv "Seller Address" "$(short_addr "$REQ_PAY_TO")" -kv "Price" "$REQ_USD USDC" -kv "Verifying Contract" "$(short_addr "$REQ_CONTRACT")" -kv "Buyer EOA" "$(short_addr "$BUYER_EOA")" -kv "Buyer Circle" "$(short_addr "$BUYER_CIRCLE")" -kv "Seller EOA" "$(short_addr "$SELLER_EOA")" -kv "Seller Circle" "$(short_addr "$SELLER_CIRCLE")" - -section "4. Buyer reviews budget and asks for approval" -show_cmd "POST /api/v1/simulate recipient=http://localhost:$SELLER_GATE_PORT$ENDPOINT amount=$PRICE" -SIMULATE_RESULT="$(api_json buyer POST /api/v1/simulate "" "$(jq -nc --arg recipient "http://localhost:$SELLER_GATE_PORT$ENDPOINT" --arg amount "$PRICE" '{recipient: $recipient, amount: $amount}')")" -SIMULATE_WOULD_SUCCEED="$(printf '%s' "$SIMULATE_RESULT" | jq -r '.would_succeed')" -SIMULATE_ROUTE="$(printf '%s' "$SIMULATE_RESULT" | jq -r '.route')" -SIMULATE_REASON="$(printf '%s' "$SIMULATE_RESULT" | jq -r '.reason // empty')" -kv "Buyer Gateway" "$(atomic_to_usdc "$BUYER_BEFORE_GATEWAY_ATOMIC") USDC in Gateway before payment" -kv "Seller Gateway" "$(atomic_to_usdc "$SELLER_BEFORE_GATEWAY_ATOMIC") USDC in Gateway before payment" -kv "Route" "$SIMULATE_ROUTE" -if [[ "$SIMULATE_WOULD_SUCCEED" == "true" ]]; then - kv "Budget Check" "ready to execute immediately" -elif [[ "$SIMULATE_REASON" == *"requires confirmation"* ]]; then - kv "Budget Check" "within policy, but owner approval required" -elif [[ "$SIMULATE_REASON" == *"Insufficient available balance"* ]]; then - kv "Budget Check" "insufficient Gateway balance on $NETWORK" - if [[ "$NETWORK" == "BASE-SEPOLIA" ]]; then - kv "Funding Hint" "fund Base Sepolia via the Circle faucet, then rerun with --base" - fi - exit 1 -else - echo "Simulation failed. Buyer is not ready to pay." >&2 - exit 1 -fi - -PAY_IDEMPOTENCY_KEY="foundation-demo-$RUN_TS" -show_cmd "omniclaw-cli pay --recipient http://localhost:$SELLER_GATE_PORT$ENDPOINT --idempotency-key $PAY_IDEMPOTENCY_KEY" -PAY_ATTEMPT_ONE="$(cli_json buyer pay --recipient "http://localhost:$SELLER_GATE_PORT$ENDPOINT" --idempotency-key "$PAY_IDEMPOTENCY_KEY")" -CONFIRM_REQUIRED="$(printf '%s' "$PAY_ATTEMPT_ONE" | jq -r '.requires_confirmation')" -CONFIRMATION_ID="$(printf '%s' "$PAY_ATTEMPT_ONE" | jq -r '.confirmation_id // empty')" - -if [[ "$CONFIRM_REQUIRED" != "true" || -z "$CONFIRMATION_ID" ]]; then - echo "Expected an approval step, but the payment did not require confirmation." >&2 - exit 1 -fi - -kv "Approval" "required before spend" -kv "Confirmation ID" "$CONFIRMATION_ID" - -show_cmd "omniclaw-cli confirmations approve --id $CONFIRMATION_ID" -APPROVAL_RESULT="$(cli_json buyer confirmations approve --id "$CONFIRMATION_ID")" -if [[ "$(printf '%s' "$APPROVAL_RESULT" | jq -r '.status')" != "APPROVED" ]]; then - echo "Confirmation approval failed." >&2 - exit 1 -fi -kv "Owner Decision" "approved" - -section "5. Buyer pays and the seller unlocks the API" -show_cmd "omniclaw-cli pay --recipient http://localhost:$SELLER_GATE_PORT$ENDPOINT --idempotency-key $PAY_IDEMPOTENCY_KEY" -PAY_ATTEMPT_TWO="$(cli_json buyer pay --recipient "http://localhost:$SELLER_GATE_PORT$ENDPOINT" --idempotency-key "$PAY_IDEMPOTENCY_KEY")" -if [[ "$(printf '%s' "$PAY_ATTEMPT_TWO" | jq -r '.success')" != "true" ]]; then - printf '%s\n' "$PAY_ATTEMPT_TWO" >&2 - exit 1 -fi - -BUYER_AFTER_DETAIL="$(api_json buyer GET /api/v1/balance-detail)" -SELLER_AFTER_DETAIL="$(api_json seller GET /api/v1/balance-detail)" -BUYER_AFTER_GATEWAY="$(printf '%s' "$BUYER_AFTER_DETAIL" | jq -r '.gateway_balance')" -SELLER_AFTER_GATEWAY="$(printf '%s' "$SELLER_AFTER_DETAIL" | jq -r '.gateway_balance')" -BUYER_AFTER_GATEWAY_ATOMIC="$(printf '%s' "$BUYER_AFTER_DETAIL" | jq -r '.gateway_balance_atomic')" -SELLER_AFTER_GATEWAY_ATOMIC="$(printf '%s' "$SELLER_AFTER_DETAIL" | jq -r '.gateway_balance_atomic')" -WAITED_FOR_SETTLEMENT=0 -for ((attempt = 1; attempt <= 90; attempt++)); do - if [[ "$BUYER_AFTER_GATEWAY_ATOMIC" != "$BUYER_BEFORE_GATEWAY_ATOMIC" || "$SELLER_AFTER_GATEWAY_ATOMIC" != "$SELLER_BEFORE_GATEWAY_ATOMIC" ]]; then - break - fi - if [[ "$WAITED_FOR_SETTLEMENT" -eq 0 ]]; then - kv "Settlement Sync" "waiting for Gateway contract balances to update" - WAITED_FOR_SETTLEMENT=1 - fi - sleep 1 - BUYER_AFTER_DETAIL="$(api_json buyer GET /api/v1/balance-detail)" - SELLER_AFTER_DETAIL="$(api_json seller GET /api/v1/balance-detail)" - BUYER_AFTER_GATEWAY="$(printf '%s' "$BUYER_AFTER_DETAIL" | jq -r '.gateway_balance')" - SELLER_AFTER_GATEWAY="$(printf '%s' "$SELLER_AFTER_DETAIL" | jq -r '.gateway_balance')" - BUYER_AFTER_GATEWAY_ATOMIC="$(printf '%s' "$BUYER_AFTER_DETAIL" | jq -r '.gateway_balance_atomic')" - SELLER_AFTER_GATEWAY_ATOMIC="$(printf '%s' "$SELLER_AFTER_DETAIL" | jq -r '.gateway_balance_atomic')" -done - -UNLOCKED_PRETTY="$(printf '%s' "$PAY_ATTEMPT_TWO" | jq -c '.response_data // "seller response unavailable"' )" - -kv "Settlement Status" "$(printf '%s' "$PAY_ATTEMPT_TWO" | jq -r '.status')" -kv "Payment Method" "$(printf '%s' "$PAY_ATTEMPT_TWO" | jq -r '.method')" -kv "Gateway Ref" "$(printf '%s' "$PAY_ATTEMPT_TWO" | jq -r '.transaction_id')" -kv "Unlocked Payload" "$UNLOCKED_PRETTY" - -section "6. Gateway contract balances before and after" -if [[ "$BUYER_AFTER_GATEWAY_ATOMIC" != "$BUYER_BEFORE_GATEWAY_ATOMIC" || "$SELLER_AFTER_GATEWAY_ATOMIC" != "$SELLER_BEFORE_GATEWAY_ATOMIC" ]]; then - kv "Settlement Mirror" "Gateway contract balances updated" -else - kv "Settlement Mirror" "Gateway contract balances did not update within 90 seconds" -fi -kv "Buyer Gateway Before" "$(atomic_to_usdc "$BUYER_BEFORE_GATEWAY_ATOMIC") USDC" -kv "Buyer Gateway After" "$(atomic_to_usdc "$BUYER_AFTER_GATEWAY_ATOMIC") USDC" -kv "Buyer Delta" "$(atomic_delta_to_usdc_signed "$BUYER_AFTER_GATEWAY_ATOMIC" "$BUYER_BEFORE_GATEWAY_ATOMIC") USDC" -kv "Seller Gateway Before" "$(atomic_to_usdc "$SELLER_BEFORE_GATEWAY_ATOMIC") USDC" -kv "Seller Gateway After" "$(atomic_to_usdc "$SELLER_AFTER_GATEWAY_ATOMIC") USDC" -kv "Seller Delta" "$(atomic_delta_to_usdc_signed "$SELLER_AFTER_GATEWAY_ATOMIC" "$SELLER_BEFORE_GATEWAY_ATOMIC") USDC" - -section "Evidence" -kv "Log Bundle" "$LOG_DIR" -kv "Buyer Control Log" "$BUYER_CP_LOG" -kv "Seller Control Log" "$SELLER_CP_LOG" -kv "Seller Gate Log" "$SELLER_GATE_LOG" -kv "Payment Terms" "$LOG_DIR/payment-required.json" - -if [[ "$HOLD" -eq 1 ]]; then - printf '\nDemo complete. Processes are still running. Press Ctrl-C to stop.\n' - while true; do - sleep 1 - done -fi diff --git a/scripts/demo_foundation_tmux.sh b/scripts/demo_foundation_tmux.sh deleted file mode 100755 index 06e4ac1..0000000 --- a/scripts/demo_foundation_tmux.sh +++ /dev/null @@ -1,703 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" -RUN_TS="$(date +%Y%m%d_%H%M%S)" - -SESSION_NAME="omniclaw-demo-$RUN_TS" -ENV_FILE="$ROOT_DIR/.env" -NETWORK="ETH-SEPOLIA" -RPC_URL="${OMNICLAW_RPC_URL:-}" -BUYER_CP_PORT=9190 -SELLER_CP_PORT=9191 -SELLER_GATE_PORT=9291 -LOG_DIR="$ROOT_DIR/logs/foundation_demo_tmux_$RUN_TS" -ATTACH=1 - -BUYER_TOKEN="payment-agent-token" -BUYER_ALIAS="omni-bot-v4" -SELLER_TOKEN="seller-agent-token" -SELLER_ALIAS="seller-api" -OWNER_TOKEN="foundation-demo-owner" -BLOCKED_URL="https://sensayhack-402.onrender.com" -PRICE="0.01" -ENDPOINT="/api/data" -SELLER_EXEC_CMD="printf '{\"result\":\"premium data unlocked\",\"provider\":\"agent-a\",\"settlement\":\"gateway-batched\",\"transport\":\"x402\"}\\n'" - -usage() { - cat <<'USAGE' -Usage: scripts/demo_foundation_tmux.sh [options] - -Options: - --session-name tmux session name - --env-file Path to env file (default: .env) - --network ETH-SEPOLIA or BASE-SEPOLIA - --base Shortcut for --network BASE-SEPOLIA - --eth Shortcut for --network ETH-SEPOLIA - --rpc-url Override RPC URL - --buyer-cp-port Buyer control plane port (default: 9190) - --seller-cp-port Seller control plane port (default: 9191) - --seller-gate-port Seller gate port (default: 9291) - --log-dir Log directory for the run - --no-attach Create the session without attaching - -h, --help Show help -USAGE -} - -while [[ $# -gt 0 ]]; do - case "$1" in - --session-name) SESSION_NAME="$2"; shift 2 ;; - --env-file) ENV_FILE="$2"; shift 2 ;; - --network) NETWORK="$2"; shift 2 ;; - --base) NETWORK="BASE-SEPOLIA"; shift ;; - --eth) NETWORK="ETH-SEPOLIA"; shift ;; - --rpc-url) RPC_URL="$2"; shift 2 ;; - --buyer-cp-port) BUYER_CP_PORT="$2"; shift 2 ;; - --seller-cp-port) SELLER_CP_PORT="$2"; shift 2 ;; - --seller-gate-port) SELLER_GATE_PORT="$2"; shift 2 ;; - --log-dir) LOG_DIR="$2"; shift 2 ;; - --no-attach) ATTACH=0; shift ;; - -h|--help) usage; exit 0 ;; - *) echo "Unknown option: $1" >&2; usage; exit 1 ;; - esac -done - -require_cmd() { - if ! command -v "$1" >/dev/null 2>&1; then - echo "Missing required command: $1" >&2 - exit 1 - fi -} - -port_in_use() { - local port="$1" - python3 - "$port" <<'PY_PORT' -import socket -import sys -port = int(sys.argv[1]) -s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) -try: - s.bind(("127.0.0.1", port)) -except OSError: - print("in-use") - sys.exit(0) -finally: - s.close() -print("free") -PY_PORT -} - -pick_env() { - local preferred="$1" - local fallback="$2" - if [[ -n "${!preferred:-}" ]]; then - printf '%s' "${!preferred}" - return 0 - fi - if [[ -n "${!fallback:-}" ]]; then - printf '%s' "${!fallback}" - return 0 - fi - return 1 -} - -require_value() { - local name="$1" - local value="$2" - if [[ -z "$value" ]]; then - echo "Missing required value: $name" >&2 - exit 1 - fi -} - -require_cmd tmux -require_cmd bash -require_cmd curl -require_cmd jq -require_cmd python3 -require_cmd omniclaw-cli -require_cmd uv -require_cmd base64 -require_cmd awk - -if [[ ! -f "$ENV_FILE" ]]; then - echo "Env file not found: $ENV_FILE" >&2 - exit 1 -fi - -if [[ "$NETWORK" != "ETH-SEPOLIA" && "$NETWORK" != "BASE-SEPOLIA" ]]; then - echo "Unsupported network: $NETWORK" >&2 - exit 1 -fi - -if [[ -z "$RPC_URL" ]]; then - if [[ "$NETWORK" == "ETH-SEPOLIA" ]]; then - RPC_URL="https://ethereum-sepolia-rpc.publicnode.com" - else - RPC_URL="https://base-sepolia-rpc.publicnode.com" - fi -fi - -for port in "$BUYER_CP_PORT" "$SELLER_CP_PORT" "$SELLER_GATE_PORT"; do - if [[ "$(port_in_use "$port")" == "in-use" ]]; then - echo "Port $port is already in use. Free it or override the port flags." >&2 - exit 1 - fi -done - -set -a -# shellcheck disable=SC1090 -source "$ENV_FILE" -set +a - -BUYER_CIRCLE_API_KEY="$(pick_env BUYER_CIRCLE_API_KEY CIRCLE_API_KEY || true)" -SELLER_CIRCLE_API_KEY="$(pick_env SELLER_CIRCLE_API_KEY CIRCLE_API_KEY || true)" -BUYER_ENTITY_SECRET="$(pick_env BUYER_ENTITY_SECRET ENTITY_SECRET || true)" -SELLER_ENTITY_SECRET="$(pick_env SELLER_ENTITY_SECRET ENTITY_SECRET || true)" -BUYER_PRIVATE_KEY="$(pick_env BUYER_OMNICLAW_PRIVATE_KEY OMNICLAW_PRIVATE_KEY || true)" -SELLER_PRIVATE_KEY="$(pick_env SELLER_OMNICLAW_PRIVATE_KEY OMNICLAW_PRIVATE_KEY || true)" -OMNICLAW_LOG_LEVEL="${OMNICLAW_LOG_LEVEL:-INFO}" - -require_value "BUYER_CIRCLE_API_KEY or CIRCLE_API_KEY" "$BUYER_CIRCLE_API_KEY" -require_value "SELLER_CIRCLE_API_KEY or CIRCLE_API_KEY" "$SELLER_CIRCLE_API_KEY" -require_value "BUYER_ENTITY_SECRET or ENTITY_SECRET" "$BUYER_ENTITY_SECRET" -require_value "SELLER_ENTITY_SECRET or ENTITY_SECRET" "$SELLER_ENTITY_SECRET" -require_value "BUYER_OMNICLAW_PRIVATE_KEY or OMNICLAW_PRIVATE_KEY" "$BUYER_PRIVATE_KEY" -require_value "SELLER_OMNICLAW_PRIVATE_KEY or OMNICLAW_PRIVATE_KEY" "$SELLER_PRIVATE_KEY" - -mkdir -p "$LOG_DIR" - -BUYER_POLICY_SRC="$ROOT_DIR/examples/demo/foundation/buyer-policy.json" -SELLER_POLICY_SRC="$ROOT_DIR/examples/demo/foundation/seller-policy.json" -BUYER_POLICY_RUNTIME="$LOG_DIR/buyer-policy.runtime.json" -SELLER_POLICY_RUNTIME="$LOG_DIR/seller-policy.runtime.json" -cp "$BUYER_POLICY_SRC" "$BUYER_POLICY_RUNTIME" -cp "$SELLER_POLICY_SRC" "$SELLER_POLICY_RUNTIME" - -BUYER_CONFIG_DIR="$LOG_DIR/buyer-cli-config" -SELLER_CONFIG_DIR="$LOG_DIR/seller-cli-config" -mkdir -p "$BUYER_CONFIG_DIR" "$SELLER_CONFIG_DIR" - -BUYER_CP_LOG="$LOG_DIR/buyer-control-plane.log" -SELLER_CP_LOG="$LOG_DIR/seller-control-plane.log" -DIRECTOR_LOG="$LOG_DIR/director.log" -BUYER_PANE_LOG="$LOG_DIR/buyer-pane.log" -SELLER_PANE_LOG="$LOG_DIR/seller-pane.log" -MONITOR_PANE_LOG="$LOG_DIR/monitor-pane.log" -STAGE_PANE_LOG="$LOG_DIR/stage-pane.log" - -BUYER_BEFORE_JSON="$LOG_DIR/buyer-before.json" -BUYER_AFTER_JSON="$LOG_DIR/buyer-after.json" -SELLER_BEFORE_JSON="$LOG_DIR/seller-before.json" -SELLER_AFTER_JSON="$LOG_DIR/seller-after.json" -PAY_ONE_JSON="$LOG_DIR/pay-attempt-1.json" -PAY_FINAL_JSON="$LOG_DIR/pay-final.json" -APPROVE_JSON="$LOG_DIR/approve.json" -PAYMENT_REQUIRED_JSON="$LOG_DIR/payment-required.json" - -BUYER_INIT="$LOG_DIR/buyer-shell.sh" -SELLER_INIT="$LOG_DIR/seller-shell.sh" -BUYER_RCFILE="$LOG_DIR/buyer-shell.rc" -SELLER_RCFILE="$LOG_DIR/seller-shell.rc" -MONITOR_SCRIPT="$LOG_DIR/monitor.sh" -STAGE_SCRIPT="$LOG_DIR/stage-log.sh" -DIRECTOR_SCRIPT="$LOG_DIR/director.sh" - -PAY_URL="http://localhost:$SELLER_GATE_PORT$ENDPOINT" -PAY_KEY="foundation-tmux-$RUN_TS" -export ROOT_DIR RUN_TS SESSION_NAME NETWORK RPC_URL BUYER_CP_PORT SELLER_CP_PORT SELLER_GATE_PORT LOG_DIR -export BUYER_TOKEN BUYER_ALIAS SELLER_TOKEN SELLER_ALIAS OWNER_TOKEN BLOCKED_URL PRICE ENDPOINT SELLER_EXEC_CMD -export BUYER_CIRCLE_API_KEY SELLER_CIRCLE_API_KEY BUYER_ENTITY_SECRET SELLER_ENTITY_SECRET BUYER_PRIVATE_KEY SELLER_PRIVATE_KEY -export BUYER_POLICY_RUNTIME SELLER_POLICY_RUNTIME BUYER_CONFIG_DIR SELLER_CONFIG_DIR -export BUYER_CP_LOG SELLER_CP_LOG DIRECTOR_LOG BUYER_PANE_LOG SELLER_PANE_LOG MONITOR_PANE_LOG STAGE_PANE_LOG -export BUYER_BEFORE_JSON BUYER_AFTER_JSON SELLER_BEFORE_JSON SELLER_AFTER_JSON PAY_ONE_JSON PAY_FINAL_JSON APPROVE_JSON PAYMENT_REQUIRED_JSON -export BUYER_INIT SELLER_INIT BUYER_RCFILE SELLER_RCFILE MONITOR_SCRIPT STAGE_SCRIPT DIRECTOR_SCRIPT PAY_URL PAY_KEY OMNICLAW_LOG_LEVEL - -cat > "$BUYER_RCFILE" <<'EOF_BUYER_RC' -export PS1='buyer-agent$ ' -omniclaw_cli_wrap() { - local attempt rc - if [[ "${1:-}" == "pay" ]]; then - for attempt in 1 2 3 4 5; do - command omniclaw-cli "$@" && return 0 - rc=$? - printf '[omniclaw-cli pay retry %d/5 after transient failure]\n' "$attempt" >&2 - sleep "$attempt" - done - return "$rc" - fi - command omniclaw-cli "$@" -} -alias omniclaw-cli='omniclaw_cli_wrap' -EOF_BUYER_RC -chmod +x "$BUYER_RCFILE" - -cat > "$SELLER_RCFILE" <<'EOF_SELLER_RC' -export PS1='seller-agent$ ' -EOF_SELLER_RC -chmod +x "$SELLER_RCFILE" - -cat > "$BUYER_INIT" < "$SELLER_INIT" < "$MONITOR_SCRIPT" <<'EOF_MONITOR' -#!/usr/bin/env bash -set -euo pipefail -buyer_url="http://127.0.0.1:$BUYER_CP_PORT/api/v1/balance-detail" -seller_url="http://127.0.0.1:$SELLER_CP_PORT/api/v1/balance-detail" -pay_url="$PAY_URL" -while true; do - clear - printf 'Circle Settlement Monitor\n\n' - printf 'Thesis: same omniclaw-cli, two roles\n' - printf 'buyer -> omniclaw-cli pay\n' - printf 'seller -> omniclaw-cli serve\n\n' - printf 'Network: %s\n' "$NETWORK" - printf 'Paid Endpoint: %s\n\n' "$PAY_URL" - - gate_code="$(curl -s -o /dev/null -w '%{http_code}' "$pay_url" || true)" - printf 'Seller Gate HTTP: %s\n\n' "$gate_code" - - if [[ -f "$PAYMENT_REQUIRED_JSON" ]]; then - printf 'Seller Accepts GatewayWalletBatched\n' - jq -C '{scheme: .accepts[0].extra.name, network: .accepts[0].network, pay_to: .accepts[0].payTo, amount_atomic: .accepts[0].amount, verifying_contract: .accepts[0].extra.verifyingContract}' "$PAYMENT_REQUIRED_JSON" - printf '\n' - else - printf 'Seller Accepts GatewayWalletBatched\nwaiting for seller gate\n\n' - fi - - if curl -fsS "$buyer_url" -H "Authorization: Bearer $BUYER_TOKEN" >/tmp/omniclaw-buyer-monitor.json 2>/dev/null; then - buyer_now="$(jq -r '.gateway_balance_atomic' /tmp/omniclaw-buyer-monitor.json)" - buyer_before="" - if [[ -f "$BUYER_BEFORE_JSON" ]]; then - buyer_before="$(jq -r '.gateway_balance_atomic' "$BUYER_BEFORE_JSON")" - fi - buyer_delta='n/a' - if [[ -n "$buyer_before" ]]; then - buyer_delta="$(awk -v a="$buyer_now" -v b="$buyer_before" 'BEGIN {printf "%+.6f", (a-b)/1000000}')" - fi - printf 'Buyer Gateway\n' - jq -C '{eoa_address, gateway_balance, gateway_balance_atomic}' /tmp/omniclaw-buyer-monitor.json - printf 'buyer delta: %s USDC\n\n' "$buyer_delta" - else - printf 'Buyer Gateway\nwaiting for buyer control plane\n\n' - fi - - if curl -fsS "$seller_url" -H "Authorization: Bearer $SELLER_TOKEN" >/tmp/omniclaw-seller-monitor.json 2>/dev/null; then - seller_now="$(jq -r '.gateway_balance_atomic' /tmp/omniclaw-seller-monitor.json)" - seller_before="" - if [[ -f "$SELLER_BEFORE_JSON" ]]; then - seller_before="$(jq -r '.gateway_balance_atomic' "$SELLER_BEFORE_JSON")" - fi - seller_delta='n/a' - if [[ -n "$seller_before" ]]; then - seller_delta="$(awk -v a="$seller_now" -v b="$seller_before" 'BEGIN {printf "%+.6f", (a-b)/1000000}')" - fi - printf 'Seller Gateway\n' - jq -C '{eoa_address, gateway_balance, gateway_balance_atomic}' /tmp/omniclaw-seller-monitor.json - printf 'seller delta: %s USDC\n\n' "$seller_delta" - else - printf 'Seller Gateway\nwaiting for seller control plane\n\n' - fi - - if [[ -f "$PAY_ONE_JSON" ]]; then - printf 'Approval Envelope\n' - jq -C '{status, method, requires_confirmation, confirmation_id}' "$PAY_ONE_JSON" - printf '\n' - fi - - if [[ -f "$PAY_FINAL_JSON" ]]; then - printf 'Final Settlement\n' - jq -C '{status, method, transaction_id}' "$PAY_FINAL_JSON" - printf '\n' - fi - - sleep 2 -done -EOF_MONITOR -chmod +x "$MONITOR_SCRIPT" - -cat > "$STAGE_SCRIPT" <<'EOF_STAGE' -#!/usr/bin/env bash -set -euo pipefail -mkdir -p "$LOG_DIR" -touch "$DIRECTOR_LOG" -clear -printf 'Stage Log\n\n' -exec tail -n +1 -F "$DIRECTOR_LOG" -EOF_STAGE -chmod +x "$STAGE_SCRIPT" - -cat > "$DIRECTOR_SCRIPT" <<'EOF_DIRECTOR' -#!/usr/bin/env bash -set -euo pipefail -buyer_pane="$BUYER_PANE_ID" -seller_pane="$SELLER_PANE_ID" - -director_log="$DIRECTOR_LOG" -payment_required_json="$PAYMENT_REQUIRED_JSON" -pay_one_json="$PAY_ONE_JSON" -pay_final_json="$PAY_FINAL_JSON" -approve_json="$APPROVE_JSON" -buyer_before_json="$BUYER_BEFORE_JSON" -buyer_after_json="$BUYER_AFTER_JSON" -seller_before_json="$SELLER_BEFORE_JSON" -seller_after_json="$SELLER_AFTER_JSON" -pay_url="$PAY_URL" -pay_key="$PAY_KEY" -serve_exec_quoted="$(printf '%q' "$SELLER_EXEC_CMD")" - -msg() { - printf '[%s] %s\n' "$(date +%H:%M:%S)" "$*" >> "$director_log" -} - -wait_for_http_ok() { - local url="$1" - local timeout_secs="${2:-90}" - local start_ts - start_ts="$(date +%s)" - while true; do - if curl -fsS "$url" >/dev/null 2>&1; then - return 0 - fi - if (( $(date +%s) - start_ts >= timeout_secs )); then - return 1 - fi - sleep 1 - done -} - -wait_for_http_status() { - local url="$1" - local expected="$2" - local timeout_secs="${3:-120}" - local start_ts code - start_ts="$(date +%s)" - while true; do - code="$(curl -s -o /dev/null -w '%{http_code}' "$url" || true)" - if [[ "$code" == "$expected" ]]; then - return 0 - fi - if (( $(date +%s) - start_ts >= timeout_secs )); then - return 1 - fi - sleep 1 - done -} - -wait_for_file() { - local path="$1" - local timeout_secs="${2:-120}" - local start_ts - start_ts="$(date +%s)" - while true; do - if [[ -s "$path" ]]; then - return 0 - fi - if (( $(date +%s) - start_ts >= timeout_secs )); then - return 1 - fi - sleep 1 - done -} - -api_balance_detail() { - local side="$1" - local url token - if [[ "$side" == "buyer" ]]; then - url="http://127.0.0.1:$BUYER_CP_PORT/api/v1/balance-detail" - token="$BUYER_TOKEN" - else - url="http://127.0.0.1:$SELLER_CP_PORT/api/v1/balance-detail" - token="$SELLER_TOKEN" - fi - curl -fsS -H "Authorization: Bearer $token" "$url" -} - -decode_payment_required() { - local url="$1" - local headers_file="$LOG_DIR/payment-required.headers" - local body_file="$LOG_DIR/payment-required.body" - curl -sS -D "$headers_file" -o "$body_file" "$url" >/dev/null - awk 'BEGIN{IGNORECASE=1} /^payment-required:/{sub(/^[^:]*:[[:space:]]*/, ""); sub(/\r$/, ""); print; exit}' "$headers_file" -} - -escape_squote() { - printf '%s' "$1" | sed "s/'/'\\''/g" -} - -send_text() { - local pane="$1" - local text="$2" - local delay="${3:-0.012}" - local i ch - for ((i=0; i<${#text}; i++)); do - ch="${text:i:1}" - tmux send-keys -t "$pane" -l "$ch" - sleep "$delay" - done -} - -run_cmd() { - local pane="$1" - local cmd="$2" - send_text "$pane" "$cmd" - tmux send-keys -t "$pane" Enter -} - -wait_for_prompt() { - local pane="$1" - local prompt_prefix="$2" - local timeout_secs="${3:-120}" - local start_ts - start_ts="$(date +%s)" - while true; do - if tmux capture-pane -p -t "$pane" | tail -n 40 | grep -Fq "$prompt_prefix"; then - return 0 - fi - if (( $(date +%s) - start_ts >= timeout_secs )); then - return 1 - fi - sleep 1 - done -} - -note() { - local pane="$1" - local text="$2" - local escaped - escaped="$(escape_squote "$text")" - run_cmd "$pane" "printf '\\n%s\\n\\n' '$escaped'" -} - -sleep 2 -msg 'Waiting for buyer and seller control planes' -wait_for_http_ok "http://127.0.0.1:$BUYER_CP_PORT/api/v1/health" -wait_for_http_ok "http://127.0.0.1:$SELLER_CP_PORT/api/v1/health" -msg 'Control planes live' - -note "$buyer_pane" 'Buyer side: same CLI, using omniclaw-cli pay' -note "$seller_pane" 'Seller side: same CLI, using omniclaw-cli serve' -wait_for_prompt "$buyer_pane" "buyer-agent$" 30 -wait_for_prompt "$seller_pane" "seller-agent$" 30 -sleep 1 - -msg 'Configure buyer and seller through the same CLI' -run_cmd "$buyer_pane" "omniclaw-cli configure --server-url http://localhost:$BUYER_CP_PORT --token $BUYER_TOKEN --wallet $BUYER_ALIAS --owner-token $OWNER_TOKEN | jq -C '{ok, server_url, wallet}'" -wait_for_prompt "$buyer_pane" "buyer-agent$" 120 -sleep 1 -run_cmd "$seller_pane" "omniclaw-cli configure --server-url http://localhost:$SELLER_CP_PORT --token $SELLER_TOKEN --wallet $SELLER_ALIAS | jq -C '{ok, server_url, wallet}'" -wait_for_prompt "$seller_pane" "seller-agent$" 120 -sleep 2 - -msg 'Both agents inspect their Gateway balances via omniclaw-cli' -run_cmd "$seller_pane" "omniclaw-cli balance-detail | tee '$SELLER_BEFORE_JSON' | jq -C '{eoa_address, gateway_balance, gateway_balance_atomic, circle_wallet_address}'" -wait_for_prompt "$seller_pane" "seller-agent$" 120 -sleep 1 -run_cmd "$buyer_pane" "omniclaw-cli balance-detail | tee '$BUYER_BEFORE_JSON' | jq -C '{eoa_address, gateway_balance, gateway_balance_atomic, circle_wallet_address}'" -wait_for_prompt "$buyer_pane" "buyer-agent$" 120 -sleep 2 - -msg 'Buyer tries a malicious URL and policy blocks it before spend' -run_cmd "$buyer_pane" "omniclaw-cli can-pay --recipient '$BLOCKED_URL' | jq -C '{can_pay, reason}'" -wait_for_prompt "$buyer_pane" "buyer-agent$" 120 -sleep 3 - -msg 'Seller publishes a paid endpoint with omniclaw-cli serve' -note "$seller_pane" 'Seller opens the API gate with omniclaw-cli serve' -wait_for_prompt "$seller_pane" "seller-agent$" 30 -sleep 1 -run_cmd "$seller_pane" "omniclaw-cli serve --price $PRICE --endpoint '$ENDPOINT' --exec $serve_exec_quoted --port $SELLER_GATE_PORT" -wait_for_http_status "$pay_url" 402 120 -seller_402_header="$(decode_payment_required "$pay_url")" -printf '%s' "$seller_402_header" | base64 -d > "$payment_required_json" -msg 'Seller now accepts GatewayWalletBatched. Without seller serve, buyer pay is meaningless.' -sleep 2 - -msg 'Buyer confirms the seller endpoint is allowed by policy' -run_cmd "$buyer_pane" "omniclaw-cli can-pay --recipient '$pay_url' | jq -C '{can_pay, reason}'" -wait_for_prompt "$buyer_pane" "buyer-agent$" 120 -sleep 2 - -msg 'Buyer requests payment through omniclaw-cli pay' -note "$buyer_pane" 'Buyer pays through omniclaw-cli pay' -wait_for_prompt "$buyer_pane" "buyer-agent$" 30 -run_cmd "$buyer_pane" "omniclaw-cli pay --recipient '$pay_url' --idempotency-key '$pay_key' | tee '$pay_one_json' | jq -C '{success, status, method, requires_confirmation, confirmation_id}'" -wait_for_prompt "$buyer_pane" "buyer-agent$" 180 -wait_for_file "$pay_one_json" 120 -confirmation_id="$(jq -r '.confirmation_id // empty' "$pay_one_json")" -if [[ -z "$confirmation_id" || "$confirmation_id" == "null" ]]; then - msg 'No confirmation id returned. Stopping demo.' - exit 1 -fi -sleep 2 - -msg 'Owner approves the spend envelope' -run_cmd "$buyer_pane" "omniclaw-cli confirmations approve --id '$confirmation_id' | tee '$approve_json' | jq -C '{status, recipient, amount, confirmation_id}'" -wait_for_prompt "$buyer_pane" "buyer-agent$" 120 -sleep 2 - -msg 'Buyer retries omniclaw-cli pay and seller unlocks immediately' -run_cmd "$buyer_pane" "omniclaw-cli pay --recipient '$pay_url' --idempotency-key '$pay_key' | tee '$pay_final_json' | jq -C '{success, status, method, transaction_id, unlocked: (.response_data | (fromjson? // .))}'" -wait_for_prompt "$buyer_pane" "buyer-agent$" 180 -wait_for_file "$pay_final_json" 180 -sleep 4 - -buyer_before_gateway_atomic="$(jq -r '.gateway_balance_atomic' "$buyer_before_json")" -seller_before_gateway_atomic="$(jq -r '.gateway_balance_atomic' "$seller_before_json")" -buyer_after_detail="" -seller_after_detail="" -buyer_after_gateway_atomic="$buyer_before_gateway_atomic" -seller_after_gateway_atomic="$seller_before_gateway_atomic" -if [[ "$buyer_after_gateway_atomic" == "$buyer_before_gateway_atomic" && "$seller_after_gateway_atomic" == "$seller_before_gateway_atomic" ]]; then - msg 'Waiting for Gateway contract balances to update' -fi -for ((attempt = 1; attempt <= 90; attempt++)); do - buyer_after_detail="$(api_balance_detail buyer 2>/dev/null || true)" - seller_after_detail="$(api_balance_detail seller 2>/dev/null || true)" - if [[ -n "$buyer_after_detail" ]]; then - buyer_after_candidate="$(printf '%s' "$buyer_after_detail" | jq -r '.gateway_balance_atomic' 2>/dev/null || true)" - if [[ -n "$buyer_after_candidate" && "$buyer_after_candidate" != "null" ]]; then - buyer_after_gateway_atomic="$buyer_after_candidate" - fi - fi - if [[ -n "$seller_after_detail" ]]; then - seller_after_candidate="$(printf '%s' "$seller_after_detail" | jq -r '.gateway_balance_atomic' 2>/dev/null || true)" - if [[ -n "$seller_after_candidate" && "$seller_after_candidate" != "null" ]]; then - seller_after_gateway_atomic="$seller_after_candidate" - fi - fi - if [[ "$buyer_after_gateway_atomic" != "$buyer_before_gateway_atomic" || "$seller_after_gateway_atomic" != "$seller_before_gateway_atomic" ]]; then - break - fi - sleep 1 -done -if [[ -z "$buyer_after_detail" ]]; then - buyer_after_detail="$(cat "$buyer_before_json")" -fi -if [[ -z "$seller_after_detail" ]]; then - seller_after_detail="$(cat "$seller_before_json")" -fi -printf '%s\n' "$buyer_after_detail" > "$buyer_after_json" -printf '%s\n' "$seller_after_detail" > "$seller_after_json" - -msg 'Buyer checks post-payment Gateway balance' -run_cmd "$buyer_pane" "omniclaw-cli balance-detail | tee '$buyer_after_json' | jq -C '{eoa_address, gateway_balance, gateway_balance_atomic, circle_wallet_address}'" -wait_for_prompt "$buyer_pane" "buyer-agent$" 120 -sleep 2 - -msg 'Seller stops serve and checks earned Gateway balance' -tmux send-keys -t "$seller_pane" C-c -wait_for_prompt "$seller_pane" "seller-agent$" 60 -sleep 2 -run_cmd "$seller_pane" "omniclaw-cli balance-detail | tee '$seller_after_json' | jq -C '{eoa_address, gateway_balance, gateway_balance_atomic, circle_wallet_address}'" -wait_for_prompt "$seller_pane" "seller-agent$" 120 -sleep 2 -run_cmd "$seller_pane" "omniclaw-cli ledger --limit 1 | jq -C '.transactions[0] // {}'" -wait_for_prompt "$seller_pane" "seller-agent$" 120 - -msg 'Demo sequence complete' -EOF_DIRECTOR -chmod +x "$DIRECTOR_SCRIPT" - -TMUX='' tmux new-session -d -s "$SESSION_NAME" -n demo -c "$ROOT_DIR" "bash '$BUYER_INIT'" -BUYER_PANE_ID="$(TMUX='' tmux display-message -p -t "$SESSION_NAME:0.0" '#{pane_id}')" -SELLER_PANE_ID="$(TMUX='' tmux split-window -h -P -F '#{pane_id}' -t "$BUYER_PANE_ID" -c "$ROOT_DIR" "bash '$SELLER_INIT'")" -MONITOR_PANE_ID="$(TMUX='' tmux split-window -v -P -F '#{pane_id}' -t "$BUYER_PANE_ID" -c "$ROOT_DIR" "env BUYER_CP_PORT='$BUYER_CP_PORT' SELLER_CP_PORT='$SELLER_CP_PORT' PAY_URL='$PAY_URL' NETWORK='$NETWORK' PAYMENT_REQUIRED_JSON='$PAYMENT_REQUIRED_JSON' BUYER_BEFORE_JSON='$BUYER_BEFORE_JSON' SELLER_BEFORE_JSON='$SELLER_BEFORE_JSON' PAY_ONE_JSON='$PAY_ONE_JSON' PAY_FINAL_JSON='$PAY_FINAL_JSON' BUYER_TOKEN='$BUYER_TOKEN' SELLER_TOKEN='$SELLER_TOKEN' bash '$MONITOR_SCRIPT'")" -STAGE_PANE_ID="$(TMUX='' tmux split-window -v -P -F '#{pane_id}' -t "$SELLER_PANE_ID" -c "$ROOT_DIR" "env LOG_DIR='$LOG_DIR' DIRECTOR_LOG='$DIRECTOR_LOG' bash '$STAGE_SCRIPT'")" -TMUX='' tmux select-layout -t "$SESSION_NAME:0" tiled -TMUX='' tmux set-window-option -t "$SESSION_NAME:0" pane-border-status top >/dev/null -TMUX='' tmux select-pane -t "$BUYER_PANE_ID" -T 'buyer-agent / omniclaw-cli' -TMUX='' tmux select-pane -t "$SELLER_PANE_ID" -T 'seller-agent / omniclaw-cli' -TMUX='' tmux select-pane -t "$MONITOR_PANE_ID" -T 'circle settlement' -TMUX='' tmux select-pane -t "$STAGE_PANE_ID" -T 'stage log' -TMUX='' tmux set-option -t "$SESSION_NAME" status-position top >/dev/null -TMUX='' tmux set-option -t "$SESSION_NAME" status-style 'bg=colour236,fg=colour255' >/dev/null -TMUX='' tmux set-option -t "$SESSION_NAME" status-left ' OmniClaw Live Demo ' >/dev/null -TMUX='' tmux set-option -t "$SESSION_NAME" status-right " $NETWORK | buyer:$BUYER_CP_PORT seller:$SELLER_CP_PORT gate:$SELLER_GATE_PORT " >/dev/null -TMUX='' tmux set-option -t "$SESSION_NAME" pane-active-border-style 'fg=colour45' >/dev/null -TMUX='' tmux set-option -t "$SESSION_NAME" pane-border-style 'fg=colour240' >/dev/null -TMUX='' tmux pipe-pane -o -t "$BUYER_PANE_ID" "cat >> '$BUYER_PANE_LOG'" -TMUX='' tmux pipe-pane -o -t "$SELLER_PANE_ID" "cat >> '$SELLER_PANE_LOG'" -TMUX='' tmux pipe-pane -o -t "$MONITOR_PANE_ID" "cat >> '$MONITOR_PANE_LOG'" -TMUX='' tmux pipe-pane -o -t "$STAGE_PANE_ID" "cat >> '$STAGE_PANE_LOG'" - -TMUX='' tmux new-window -d -t "$SESSION_NAME" -n infra -c "$ROOT_DIR" "env CIRCLE_API_KEY='$BUYER_CIRCLE_API_KEY' ENTITY_SECRET='$BUYER_ENTITY_SECRET' OMNICLAW_PRIVATE_KEY='$BUYER_PRIVATE_KEY' OMNICLAW_AGENT_POLICY_PATH='$BUYER_POLICY_RUNTIME' OMNICLAW_AGENT_TOKEN='$BUYER_TOKEN' OMNICLAW_OWNER_TOKEN='$OWNER_TOKEN' OMNICLAW_NETWORK='$NETWORK' OMNICLAW_RPC_URL='$RPC_URL' OMNICLAW_STORAGE_BACKEND=memory OMNICLAW_POLICY_RELOAD_INTERVAL=0 OMNICLAW_LOG_LEVEL='$OMNICLAW_LOG_LEVEL' PYTHONUNBUFFERED=1 uv run uvicorn omniclaw.agent.server:app --host 0.0.0.0 --port '$BUYER_CP_PORT' --log-level warning 2>&1 | tee '$BUYER_CP_LOG'" -TMUX='' tmux split-window -h -t "$SESSION_NAME:1.0" -c "$ROOT_DIR" "env CIRCLE_API_KEY='$SELLER_CIRCLE_API_KEY' ENTITY_SECRET='$SELLER_ENTITY_SECRET' OMNICLAW_PRIVATE_KEY='$SELLER_PRIVATE_KEY' OMNICLAW_AGENT_POLICY_PATH='$SELLER_POLICY_RUNTIME' OMNICLAW_AGENT_TOKEN='$SELLER_TOKEN' OMNICLAW_NETWORK='$NETWORK' OMNICLAW_RPC_URL='$RPC_URL' OMNICLAW_STORAGE_BACKEND=memory OMNICLAW_POLICY_RELOAD_INTERVAL=0 OMNICLAW_LOG_LEVEL='$OMNICLAW_LOG_LEVEL' PYTHONUNBUFFERED=1 uv run uvicorn omniclaw.agent.server:app --host 0.0.0.0 --port '$SELLER_CP_PORT' --log-level warning 2>&1 | tee '$SELLER_CP_LOG'" -for kv in \ - "BUYER_PANE_ID=$BUYER_PANE_ID" \ - "SELLER_PANE_ID=$SELLER_PANE_ID" \ - "SESSION_NAME=$SESSION_NAME" \ - "BUYER_CP_PORT=$BUYER_CP_PORT" \ - "SELLER_CP_PORT=$SELLER_CP_PORT" \ - "SELLER_GATE_PORT=$SELLER_GATE_PORT" \ - "DIRECTOR_LOG=$DIRECTOR_LOG" \ - "PAYMENT_REQUIRED_JSON=$PAYMENT_REQUIRED_JSON" \ - "PAY_ONE_JSON=$PAY_ONE_JSON" \ - "PAY_FINAL_JSON=$PAY_FINAL_JSON" \ - "APPROVE_JSON=$APPROVE_JSON" \ - "BUYER_BEFORE_JSON=$BUYER_BEFORE_JSON" \ - "BUYER_AFTER_JSON=$BUYER_AFTER_JSON" \ - "SELLER_BEFORE_JSON=$SELLER_BEFORE_JSON" \ - "SELLER_AFTER_JSON=$SELLER_AFTER_JSON" \ - "PAY_URL=$PAY_URL" \ - "PAY_KEY=$PAY_KEY" \ - "BLOCKED_URL=$BLOCKED_URL" \ - "PRICE=$PRICE" \ - "ENDPOINT=$ENDPOINT" \ - "SELLER_EXEC_CMD=$SELLER_EXEC_CMD" \ - "BUYER_TOKEN=$BUYER_TOKEN" \ - "BUYER_ALIAS=$BUYER_ALIAS" \ - "SELLER_TOKEN=$SELLER_TOKEN" \ - "SELLER_ALIAS=$SELLER_ALIAS" \ - "OWNER_TOKEN=$OWNER_TOKEN" \ - "LOG_DIR=$LOG_DIR" -do - TMUX='' tmux set-environment -t "$SESSION_NAME" "${kv%%=*}" "${kv#*=}" -done -TMUX='' tmux new-window -d -t "$SESSION_NAME" -n director -c "$ROOT_DIR" "bash '$DIRECTOR_SCRIPT' 2>&1 | tee -a '$DIRECTOR_LOG'" -TMUX='' tmux select-window -t "$SESSION_NAME:0" - -echo "tmux session: $SESSION_NAME" -echo "log bundle: $LOG_DIR" -echo "attach: tmux attach -t $SESSION_NAME" - -if [[ "$ATTACH" -eq 1 ]]; then - tmux attach -t "$SESSION_NAME" -fi diff --git a/scripts/demo_two_sided.sh b/scripts/demo_two_sided.sh deleted file mode 100755 index 1651eb4..0000000 --- a/scripts/demo_two_sided.sh +++ /dev/null @@ -1,523 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" -RUN_TS="$(date +%Y%m%d_%H%M%S)" -LOG_DIR_DEFAULT="$ROOT_DIR/logs/demo_$RUN_TS" - -ENV_FILE="$ROOT_DIR/.env" -NETWORK="ETH-SEPOLIA" -RPC_URL="${OMNICLAW_RPC_URL:-}" -PRICE="0.01" -ENDPOINT="/api/data" -SELLER_EXEC_CMD='echo "{\"result\":\"premium data from seller\"}"' -SELLER_CP_PORT=8081 -BUYER_CP_PORT=8082 -SELLER_GATE_PORT=9001 -LOG_DIR="$LOG_DIR_DEFAULT" -RUN_PAYMENT=0 -AUTO_DEPOSIT_AMOUNT="" -CHECK_ONLY=0 -HOLD=0 - -PIDS=() -PID_NAMES=() - -color() { printf "\033[%sm%s\033[0m\n" "$1" "$2"; } -section() { color "1;36" "== $1 =="; } -info() { color "0;37" "[info] $1"; } -ok() { color "0;32" "[ok] $1"; } -warn() { color "1;33" "[warn] $1"; } -err() { color "0;31" "[err] $1"; } - -usage() { - cat <<'EOF' -Usage: scripts/demo_two_sided.sh [options] - -Options: - --env-file Path to env file (default: .env) - --network ETH-SEPOLIA or BASE-SEPOLIA (default: ETH-SEPOLIA) - --rpc-url Override RPC URL - --price Seller endpoint price in USDC (default: 0.01) - --endpoint Seller gated path (default: /api/data) - --seller-exec Command seller runs after payment - --seller-cp-port Seller control plane port (default: 8081) - --buyer-cp-port Buyer control plane port (default: 8082) - --seller-gate-port Seller x402 gate port (default: 9001) - --log-dir Log directory (default: logs/demo_) - --auto-deposit Buyer auto deposit amount before payment - --run-payment Execute buyer pay step - --hold Keep processes alive after setup until Ctrl-C - --check-only Validate config and exit - -h, --help Show help -EOF -} - -while [[ $# -gt 0 ]]; do - case "$1" in - --env-file) ENV_FILE="$2"; shift 2 ;; - --network) NETWORK="$2"; shift 2 ;; - --rpc-url) RPC_URL="$2"; shift 2 ;; - --price) PRICE="$2"; shift 2 ;; - --endpoint) ENDPOINT="$2"; shift 2 ;; - --seller-exec) SELLER_EXEC_CMD="$2"; shift 2 ;; - --seller-cp-port) SELLER_CP_PORT="$2"; shift 2 ;; - --buyer-cp-port) BUYER_CP_PORT="$2"; shift 2 ;; - --seller-gate-port) SELLER_GATE_PORT="$2"; shift 2 ;; - --log-dir) LOG_DIR="$2"; shift 2 ;; - --auto-deposit) AUTO_DEPOSIT_AMOUNT="$2"; shift 2 ;; - --run-payment) RUN_PAYMENT=1; shift ;; - --hold) HOLD=1; shift ;; - --check-only) CHECK_ONLY=1; shift ;; - -h|--help) usage; exit 0 ;; - *) err "Unknown option: $1"; usage; exit 1 ;; - esac -done - -if [[ ! -f "$ENV_FILE" ]]; then - err "Env file not found: $ENV_FILE" - exit 1 -fi - -if [[ "$NETWORK" != "ETH-SEPOLIA" && "$NETWORK" != "BASE-SEPOLIA" ]]; then - err "Unsupported network: $NETWORK (use ETH-SEPOLIA or BASE-SEPOLIA)" - exit 1 -fi - -if [[ -z "$RPC_URL" ]]; then - if [[ "$NETWORK" == "ETH-SEPOLIA" ]]; then - RPC_URL="https://ethereum-sepolia-rpc.publicnode.com" - else - RPC_URL="https://base-sepolia-rpc.publicnode.com" - fi -fi - -require_cmd() { - if ! command -v "$1" >/dev/null 2>&1; then - err "Missing required command: $1" - exit 1 - fi -} - -require_cmd uv -require_cmd curl -require_cmd python3 - -port_in_use() { - local port="$1" - python3 - "$port" <<'PY' -import socket -import sys - -port = int(sys.argv[1]) -s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) -try: - s.bind(("127.0.0.1", port)) -except OSError: - print("in-use") - sys.exit(0) -finally: - s.close() -print("free") -PY -} - -set -a -# shellcheck disable=SC1090 -source "$ENV_FILE" -set +a - -pick_env() { - local preferred="$1" - local fallback="$2" - if [[ -n "${!preferred:-}" ]]; then - printf "%s" "${!preferred}" - return 0 - fi - if [[ -n "${!fallback:-}" ]]; then - printf "%s" "${!fallback}" - return 0 - fi - return 1 -} - -SELLER_CIRCLE_API_KEY="$(pick_env SELLER_CIRCLE_API_KEY CIRCLE_API_KEY || true)" -BUYER_CIRCLE_API_KEY="$(pick_env BUYER_CIRCLE_API_KEY CIRCLE_API_KEY || true)" -SELLER_ENTITY_SECRET="$(pick_env SELLER_ENTITY_SECRET ENTITY_SECRET || true)" -BUYER_ENTITY_SECRET="$(pick_env BUYER_ENTITY_SECRET ENTITY_SECRET || true)" -SELLER_PRIVATE_KEY="$(pick_env SELLER_OMNICLAW_PRIVATE_KEY OMNICLAW_PRIVATE_KEY || true)" -BUYER_PRIVATE_KEY="$(pick_env BUYER_OMNICLAW_PRIVATE_KEY OMNICLAW_PRIVATE_KEY || true)" - -SELLER_POLICY="$ROOT_DIR/examples/agent/seller/policy.json" -BUYER_POLICY="$ROOT_DIR/examples/agent/buyer/policy.json" -SELLER_POLICY_RUNTIME="" -BUYER_POLICY_RUNTIME="" -SELLER_TOKEN="seller-agent-token" -BUYER_TOKEN="buyer-agent-token" -OMNICLAW_STORAGE_BACKEND="${OMNICLAW_STORAGE_BACKEND:-memory}" -OMNICLAW_LOG_LEVEL="${OMNICLAW_LOG_LEVEL:-INFO}" - -require_value() { - local name="$1" - local value="$2" - if [[ -z "$value" ]]; then - err "Missing required value: $name" - exit 1 - fi -} - -require_value "SELLER_CIRCLE_API_KEY or CIRCLE_API_KEY" "$SELLER_CIRCLE_API_KEY" -require_value "BUYER_CIRCLE_API_KEY or CIRCLE_API_KEY" "$BUYER_CIRCLE_API_KEY" -require_value "SELLER_ENTITY_SECRET or ENTITY_SECRET" "$SELLER_ENTITY_SECRET" -require_value "BUYER_ENTITY_SECRET or ENTITY_SECRET" "$BUYER_ENTITY_SECRET" -require_value "SELLER_OMNICLAW_PRIVATE_KEY or OMNICLAW_PRIVATE_KEY" "$SELLER_PRIVATE_KEY" -require_value "BUYER_OMNICLAW_PRIVATE_KEY or OMNICLAW_PRIVATE_KEY" "$BUYER_PRIVATE_KEY" - -if [[ ! -f "$SELLER_POLICY" ]]; then - err "Seller policy file not found: $SELLER_POLICY" - exit 1 -fi -if [[ ! -f "$BUYER_POLICY" ]]; then - err "Buyer policy file not found: $BUYER_POLICY" - exit 1 -fi - -if [[ "$CHECK_ONLY" -eq 1 ]]; then - section "Preflight OK" - info "env-file: $ENV_FILE" - info "network: $NETWORK" - info "rpc-url: $RPC_URL" - info "seller policy: $SELLER_POLICY" - info "buyer policy: $BUYER_POLICY" - info "buyer/seller creds: found" - exit 0 -fi - -mkdir -p "$LOG_DIR" -SELLER_CP_LOG="$LOG_DIR/seller-control-plane.log" -BUYER_CP_LOG="$LOG_DIR/buyer-control-plane.log" -SELLER_GATE_LOG="$LOG_DIR/seller-gateway.log" -BUYER_CLI_LOG="$LOG_DIR/buyer-cli.log" -SELLER_CLI_LOG="$LOG_DIR/seller-cli.log" -SELLER_POLICY_RUNTIME="$LOG_DIR/seller-policy.runtime.json" -BUYER_POLICY_RUNTIME="$LOG_DIR/buyer-policy.runtime.json" -cp "$SELLER_POLICY" "$SELLER_POLICY_RUNTIME" -cp "$BUYER_POLICY" "$BUYER_POLICY_RUNTIME" - -cleanup() { - local code=$? - if [[ ${#PIDS[@]} -gt 0 ]]; then - section "Cleanup" - for idx in "${!PIDS[@]}"; do - local pid="${PIDS[$idx]}" - local name="${PID_NAMES[$idx]}" - if kill -0 "$pid" >/dev/null 2>&1; then - info "Stopping $name (pid $pid)" - kill "$pid" >/dev/null 2>&1 || true - fi - done - for pid in "${PIDS[@]}"; do - wait "$pid" 2>/dev/null || true - done - fi - exit "$code" -} -trap cleanup EXIT INT TERM - -start_bg() { - local name="$1" - local logfile="$2" - shift 2 - ("$@") >"$logfile" 2>&1 & - local pid=$! - PIDS+=("$pid") - PID_NAMES+=("$name") - sleep 0.4 - if ! kill -0 "$pid" >/dev/null 2>&1; then - err "$name exited immediately. Tail of $logfile:" - tail -n 80 "$logfile" || true - exit 1 - fi - ok "Started $name (pid $pid)" - info "log: $logfile" -} - -wait_for_http_ok() { - local name="$1" - local url="$2" - local timeout_secs="${3:-90}" - local start_ts - start_ts="$(date +%s)" - while true; do - if curl -fsS "$url" >/dev/null 2>&1; then - ok "$name is ready" - return 0 - fi - if (( "$(date +%s)" - start_ts >= timeout_secs )); then - err "$name did not become ready in ${timeout_secs}s" - return 1 - fi - sleep 1 - done -} - -wait_for_http_status() { - local name="$1" - local url="$2" - local expected_status="$3" - local timeout_secs="${4:-90}" - local start_ts http_code - start_ts="$(date +%s)" - while true; do - http_code="$(curl -s -o /dev/null -w "%{http_code}" "$url" || true)" - if [[ "$http_code" == "$expected_status" ]]; then - ok "$name returned expected HTTP $expected_status" - return 0 - fi - if (( "$(date +%s)" - start_ts >= timeout_secs )); then - err "$name did not return HTTP $expected_status in ${timeout_secs}s (last: $http_code)" - return 1 - fi - sleep 1 - done -} - -run_cli() { - local side="$1" - shift - local server_url token config_dir log_file circle_key entity_secret private_key pyhashseed - if [[ "$side" == "seller" ]]; then - server_url="http://127.0.0.1:$SELLER_CP_PORT" - token="$SELLER_TOKEN" - config_dir="$LOG_DIR/seller-cli-config" - log_file="$SELLER_CLI_LOG" - circle_key="$SELLER_CIRCLE_API_KEY" - entity_secret="$SELLER_ENTITY_SECRET" - private_key="$SELLER_PRIVATE_KEY" - else - server_url="http://127.0.0.1:$BUYER_CP_PORT" - token="$BUYER_TOKEN" - config_dir="$LOG_DIR/buyer-cli-config" - log_file="$BUYER_CLI_LOG" - circle_key="$BUYER_CIRCLE_API_KEY" - entity_secret="$BUYER_ENTITY_SECRET" - private_key="$BUYER_PRIVATE_KEY" - fi - pyhashseed="${OMNICLAW_DEMO_PYTHONHASHSEED:-}" - - mkdir -p "$config_dir" - { - echo - echo ">>> [$side] omniclaw-cli $*" - } >>"$log_file" - - ( - cd "$ROOT_DIR" - OMNICLAW_SERVER_URL="$server_url" \ - OMNICLAW_TOKEN="$token" \ - OMNICLAW_CONFIG_DIR="$config_dir" \ - OMNICLAW_CLI_HUMAN=1 \ - OMNICLAW_CLI_NO_BANNER=1 \ - CIRCLE_API_KEY="$circle_key" \ - ENTITY_SECRET="$entity_secret" \ - OMNICLAW_PRIVATE_KEY="$private_key" \ - OMNICLAW_NETWORK="$NETWORK" \ - OMNICLAW_RPC_URL="$RPC_URL" \ - PYTHONHASHSEED="$pyhashseed" \ - uv run python -m omniclaw.cli_agent "$@" - ) 2>&1 | tee -a "$log_file" -} - -run_cli_try() { - local side="$1" - shift - local rc log_file - set +e - run_cli "$side" "$@" - rc=$? - set -e - if [[ "$side" == "seller" ]]; then - log_file="$SELLER_CLI_LOG" - else - log_file="$BUYER_CLI_LOG" - fi - if [[ $rc -eq 139 ]] || ([[ $rc -ne 0 ]] && tail -n 160 "$log_file" | rg -q "OverflowError: Python int too large to convert to C int"); then - warn "Detected transient CLI/runtime failure. Retrying once with PYTHONHASHSEED=0." - set +e - OMNICLAW_DEMO_PYTHONHASHSEED=0 run_cli "$side" "$@" - rc=$? - set -e - fi - if [[ $rc -ne 0 ]]; then - warn "Command failed (side=$side): omniclaw-cli $* (exit $rc)" - fi - return $rc -} - -run_api() { - local side="$1" - local method="$2" - local path="$3" - local query="${4:-}" - local json_body="${5:-}" - local server_url token log_file url tmp status - - if [[ "$side" == "seller" ]]; then - server_url="http://127.0.0.1:$SELLER_CP_PORT" - token="$SELLER_TOKEN" - log_file="$SELLER_CLI_LOG" - else - server_url="http://127.0.0.1:$BUYER_CP_PORT" - token="$BUYER_TOKEN" - log_file="$BUYER_CLI_LOG" - fi - - url="$server_url$path" - if [[ -n "$query" ]]; then - url="$url?$query" - fi - - { - echo - echo ">>> [$side-api] $method $url" - } >>"$log_file" - - tmp="$(mktemp)" - if [[ -n "$json_body" ]]; then - status="$(curl -sS -o "$tmp" -w "%{http_code}" -X "$method" \ - -H "Authorization: Bearer $token" \ - -H "Content-Type: application/json" \ - -d "$json_body" \ - "$url")" - else - status="$(curl -sS -o "$tmp" -w "%{http_code}" -X "$method" \ - -H "Authorization: Bearer $token" \ - "$url")" - fi - - cat "$tmp" | python3 -m json.tool 2>/dev/null | tee -a "$log_file" || cat "$tmp" | tee -a "$log_file" - rm -f "$tmp" - - if [[ "$status" -ge 400 ]]; then - warn "API call failed (side=$side, status=$status): $method $path" - return 1 - fi - return 0 -} - -section "Config" -info "network: $NETWORK" -info "rpc-url: $RPC_URL" -info "seller control plane: http://127.0.0.1:$SELLER_CP_PORT" -info "buyer control plane: http://127.0.0.1:$BUYER_CP_PORT" -info "seller paid URL: http://127.0.0.1:$SELLER_GATE_PORT$ENDPOINT" -info "logs: $LOG_DIR" -info "seller policy copy: $SELLER_POLICY_RUNTIME" -info "buyer policy copy: $BUYER_POLICY_RUNTIME" - -for port in "$SELLER_CP_PORT" "$BUYER_CP_PORT" "$SELLER_GATE_PORT"; do - state="$(port_in_use "$port")" - if [[ "$state" == "in-use" ]]; then - err "Port $port is already in use. Free it or override with --seller-cp-port/--buyer-cp-port/--seller-gate-port." - exit 1 - fi -done - -section "Start Control Planes" -start_bg "seller-control-plane" "$SELLER_CP_LOG" env \ - CIRCLE_API_KEY="$SELLER_CIRCLE_API_KEY" \ - ENTITY_SECRET="$SELLER_ENTITY_SECRET" \ - OMNICLAW_PRIVATE_KEY="$SELLER_PRIVATE_KEY" \ - OMNICLAW_AGENT_POLICY_PATH="$SELLER_POLICY_RUNTIME" \ - OMNICLAW_AGENT_TOKEN="$SELLER_TOKEN" \ - OMNICLAW_NETWORK="$NETWORK" \ - OMNICLAW_RPC_URL="$RPC_URL" \ - OMNICLAW_STORAGE_BACKEND="$OMNICLAW_STORAGE_BACKEND" \ - OMNICLAW_LOG_LEVEL="$OMNICLAW_LOG_LEVEL" \ - PYTHONUNBUFFERED=1 \ - uv run uvicorn omniclaw.agent.server:app --host 0.0.0.0 --port "$SELLER_CP_PORT" --log-level info - -start_bg "buyer-control-plane" "$BUYER_CP_LOG" env \ - CIRCLE_API_KEY="$BUYER_CIRCLE_API_KEY" \ - ENTITY_SECRET="$BUYER_ENTITY_SECRET" \ - OMNICLAW_PRIVATE_KEY="$BUYER_PRIVATE_KEY" \ - OMNICLAW_AGENT_POLICY_PATH="$BUYER_POLICY_RUNTIME" \ - OMNICLAW_AGENT_TOKEN="$BUYER_TOKEN" \ - OMNICLAW_NETWORK="$NETWORK" \ - OMNICLAW_RPC_URL="$RPC_URL" \ - OMNICLAW_STORAGE_BACKEND="$OMNICLAW_STORAGE_BACKEND" \ - OMNICLAW_LOG_LEVEL="$OMNICLAW_LOG_LEVEL" \ - PYTHONUNBUFFERED=1 \ - uv run uvicorn omniclaw.agent.server:app --host 0.0.0.0 --port "$BUYER_CP_PORT" --log-level info - -wait_for_http_ok "seller-control-plane" "http://127.0.0.1:$SELLER_CP_PORT/api/v1/health" || { - tail -n 80 "$SELLER_CP_LOG" || true - exit 1 -} -wait_for_http_ok "buyer-control-plane" "http://127.0.0.1:$BUYER_CP_PORT/api/v1/health" || { - tail -n 80 "$BUYER_CP_LOG" || true - exit 1 -} - -section "Start Seller x402 Gate" -start_bg "seller-gateway" "$SELLER_GATE_LOG" env \ - CIRCLE_API_KEY="$SELLER_CIRCLE_API_KEY" \ - ENTITY_SECRET="$SELLER_ENTITY_SECRET" \ - OMNICLAW_PRIVATE_KEY="$SELLER_PRIVATE_KEY" \ - OMNICLAW_SERVER_URL="http://127.0.0.1:$SELLER_CP_PORT" \ - OMNICLAW_TOKEN="$SELLER_TOKEN" \ - OMNICLAW_CLI_HUMAN=1 \ - OMNICLAW_CLI_NO_BANNER=1 \ - OMNICLAW_NETWORK="$NETWORK" \ - OMNICLAW_RPC_URL="$RPC_URL" \ - OMNICLAW_LOG_LEVEL="$OMNICLAW_LOG_LEVEL" \ - OMNICLAW_CONFIG_DIR="$LOG_DIR/seller-gate-config" \ - PYTHONUNBUFFERED=1 \ - uv run python -m omniclaw.cli_agent serve --price "$PRICE" --endpoint "$ENDPOINT" --exec "$SELLER_EXEC_CMD" --port "$SELLER_GATE_PORT" - -wait_for_http_status \ - "seller-gateway (payment required check)" \ - "http://127.0.0.1:$SELLER_GATE_PORT$ENDPOINT" \ - "402" || { - tail -n 120 "$SELLER_GATE_LOG" || true - exit 1 -} - -section "Demo Snapshot" -run_api seller GET "/api/v1/health" || true -run_api seller GET "/api/v1/balance" || true -run_api seller GET "/api/v1/address" || true -run_api buyer GET "/api/v1/health" || true -run_api buyer GET "/api/v1/balance" || true -run_api buyer GET "/api/v1/address" || true -run_api buyer GET "/api/v1/can-pay" "recipient=http://127.0.0.1:$SELLER_GATE_PORT$ENDPOINT" || true - -if [[ -n "$AUTO_DEPOSIT_AMOUNT" ]]; then - section "Buyer Deposit" - run_api buyer POST "/api/v1/deposit" "amount=$AUTO_DEPOSIT_AMOUNT" || true -fi - -if [[ "$RUN_PAYMENT" -eq 1 ]]; then - section "Buyer Pays Seller" - IDEMPOTENCY_KEY="demo-${NETWORK,,}-${RUN_TS}" - PAYLOAD="{\"url\":\"http://127.0.0.1:$SELLER_GATE_PORT$ENDPOINT\",\"method\":\"GET\",\"idempotency_key\":\"$IDEMPOTENCY_KEY\"}" - run_api buyer POST "/api/v1/x402/pay" "" "$PAYLOAD" || true - - section "Ledger Snapshot" - run_api buyer GET "/api/v1/transactions" "limit=20" || true - run_api seller GET "/api/v1/transactions" "limit=20" || true -fi - -section "Done" -ok "Two-sided OmniClaw demo environment is up." -info "Seller logs: $SELLER_GATE_LOG" -info "Buyer logs: $BUYER_CP_LOG and $BUYER_CLI_LOG" -info "To tail all logs:" -printf ' tail -f %q %q %q %q %q\n' "$SELLER_CP_LOG" "$BUYER_CP_LOG" "$SELLER_GATE_LOG" "$BUYER_CLI_LOG" "$SELLER_CLI_LOG" - -if [[ "$HOLD" -eq 1 ]]; then - warn "Hold mode enabled. Press Ctrl-C to stop." - while true; do - sleep 1 - done -fi diff --git a/scripts/generate_cli_reference.py b/scripts/generate_cli_reference.py index ac47cb8..e0a962d 100755 --- a/scripts/generate_cli_reference.py +++ b/scripts/generate_cli_reference.py @@ -22,7 +22,6 @@ ("omniclaw-cli withdraw --help", ["omniclaw-cli", "withdraw", "--help"]), ("omniclaw-cli withdraw-trustless --help", ["omniclaw-cli", "withdraw-trustless", "--help"]), ("omniclaw-cli withdraw-trustless-complete --help", ["omniclaw-cli", "withdraw-trustless-complete", "--help"]), - ("omniclaw-cli serve --help", ["omniclaw-cli", "serve", "--help"]), ("omniclaw-cli create-intent --help", ["omniclaw-cli", "create-intent", "--help"]), ("omniclaw-cli confirm-intent --help", ["omniclaw-cli", "confirm-intent", "--help"]), ("omniclaw-cli get-intent --help", ["omniclaw-cli", "get-intent", "--help"]), @@ -69,20 +68,19 @@ ## Usage Notes -- same CLI, two roles: buyer uses `pay`, seller uses `serve` +- core CLI is buyer-side: use `pay`, `inspect-x402`, and policy checks before money moves - use `can-pay` before a new recipient when policy allow/deny matters - use `balance-detail` when Gateway state matters - use `--idempotency-key` for job-based payments -- for x402 URLs, `--amount` can be omitted because the payment requirements come from the seller endpoint -- `serve` binds to `0.0.0.0` even if the banner prints `localhost` +- for x402 URLs, `--amount` can be omitted because the payment requirements come from the paid endpoint ## Example Flows Buyer paying an x402 endpoint: ```bash -omniclaw-cli can-pay --recipient http://seller-host:8000/api/data -omniclaw-cli pay --recipient http://seller-host:8000/api/data --idempotency-key job-123 +omniclaw-cli can-pay --recipient http://paid-service:8000/api/data +omniclaw-cli pay --recipient http://paid-service:8000/api/data --idempotency-key job-123 ``` Buyer paying a direct address: @@ -95,16 +93,6 @@ --idempotency-key job-123 ``` -Seller exposing a paid endpoint: - -```bash -omniclaw-cli serve \\ - --price 0.01 \\ - --endpoint /api/data \\ - --exec "python app.py" \\ - --port 8000 -``` - ## Live Help Output """ diff --git a/scripts/reset_arc_vendor_demo_state.sh b/scripts/reset_arc_vendor_demo_state.sh deleted file mode 100755 index 79c0d9c..0000000 --- a/scripts/reset_arc_vendor_demo_state.sh +++ /dev/null @@ -1,11 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -BASE_URL="${1:-http://127.0.0.1:8010}" - -curl -fsS -X POST "${BASE_URL}/api/admin/reset" >/dev/null - -printf 'Arc vendor demo state reset at %s\n' "$BASE_URL" -printf 'Summary:\n' -curl -fsS "${BASE_URL}/api/summary" -printf '\n' diff --git a/scripts/start_arc_exact_facilitator.sh b/scripts/start_arc_exact_facilitator.sh deleted file mode 100755 index b983d86..0000000 --- a/scripts/start_arc_exact_facilitator.sh +++ /dev/null @@ -1,44 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -ROOT=$(cd "$(dirname "$0")/.." && pwd) -cd "$ROOT" - -if [[ -f .env ]]; then - set -a - source .env - set +a -fi - -export OMNICLAW_X402_FACILITATOR_PRIVATE_KEY="${OMNICLAW_X402_FACILITATOR_PRIVATE_KEY:-${SELLER_OMNICLAW_PRIVATE_KEY:-${OMNICLAW_PRIVATE_KEY:-}}}" -export OMNICLAW_X402_FACILITATOR_NETWORK_PROFILE="${OMNICLAW_X402_FACILITATOR_NETWORK_PROFILE:-ARC-TESTNET}" -export OMNICLAW_X402_FACILITATOR_RPC_URL="${OMNICLAW_X402_FACILITATOR_RPC_URL:-https://rpc.testnet.arc.network}" -export OMNICLAW_X402_FACILITATOR_NETWORKS="${OMNICLAW_X402_FACILITATOR_NETWORKS:-eip155:5042002}" -export OMNICLAW_X402_FACILITATOR_PORT="${OMNICLAW_X402_FACILITATOR_PORT:-4022}" -export OMNICLAW_X402_FACILITATOR_HOST="${OMNICLAW_X402_FACILITATOR_HOST:-0.0.0.0}" - -if [[ -z "$OMNICLAW_X402_FACILITATOR_PRIVATE_KEY" ]]; then - echo "Missing facilitator key. Set OMNICLAW_X402_FACILITATOR_PRIVATE_KEY, SELLER_OMNICLAW_PRIVATE_KEY, or OMNICLAW_PRIVATE_KEY." - exit 1 -fi - -cat </dev/null 2>&1 || true - fi - if [[ -f "$RUNTIME_DIR/kiosk.pid" ]]; then - kill "$(cat "$RUNTIME_DIR/kiosk.pid")" >/dev/null 2>&1 || true - fi -} -trap cleanup EXIT - -cleanup - -uv run python scripts/start_x402_exact_testnet_facilitator.py \ - >"$RUNTIME_DIR/facilitator.log" 2>&1 & -echo "$!" > "$RUNTIME_DIR/facilitator.pid" - -sleep 2 - -uv run uvicorn app:app \ - --app-dir examples/arc-marketplace-showcase \ - --host 0.0.0.0 \ - --port "$ARC_MARKETPLACE_PORT" \ - >"$RUNTIME_DIR/kiosk.log" 2>&1 & -echo "$!" > "$RUNTIME_DIR/kiosk.pid" - -sleep 2 - -cat < - -Services: - Kiosk UI: http://127.0.0.1:$ARC_MARKETPLACE_PORT - Facilitator: $OMNICLAW_X402_EXACT_FACILITATOR_URL - -Paid URLs: - $ARC_MARKETPLACE_BUYER_BASE_URL/buy/prime-market-scan - $ARC_MARKETPLACE_BUYER_BASE_URL/buy/risk-oracle-brief - $ARC_MARKETPLACE_BUYER_BASE_URL/buy/settlement-receipt-kit - -OpenClaw prompt: - pay for this url: $ARC_MARKETPLACE_BUYER_BASE_URL/buy/prime-market-scan - -Buyer CLI equivalent: - omniclaw-cli inspect-x402 --recipient "$ARC_MARKETPLACE_BUYER_BASE_URL/buy/prime-market-scan" - omniclaw-cli pay --recipient "$ARC_MARKETPLACE_BUYER_BASE_URL/buy/prime-market-scan" --idempotency-key "arc-kiosk-\$(date +%s)" - -Logs: - tail -f $RUNTIME_DIR/facilitator.log - tail -f $RUNTIME_DIR/kiosk.log - -Press Ctrl+C to stop both services. - -EOF - -tail -f "$RUNTIME_DIR/facilitator.log" "$RUNTIME_DIR/kiosk.log" diff --git a/scripts/start_arc_marketplace_showcase_docker.sh b/scripts/start_arc_marketplace_showcase_docker.sh deleted file mode 100755 index ff6ad97..0000000 --- a/scripts/start_arc_marketplace_showcase_docker.sh +++ /dev/null @@ -1,267 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -ROOT=$(cd "$(dirname "$0")/.." && pwd) -cd "$ROOT" - -if [[ -f .env ]]; then - set -a - source .env - set +a -fi - -IMAGE_TAG="${OMNICLAW_AGENT_IMAGE:-omniclaw-agent:local}" -NETWORK_NAME="${ARC_MARKETPLACE_DOCKER_NETWORK:-omniclaw-buyer_default}" -SUBNET="${ARC_MARKETPLACE_DOCKER_SUBNET:-172.18.0.0/16}" -FACILITATOR_IP="${ARC_MARKETPLACE_FACILITATOR_IP:-172.18.0.50}" -KIOSK_IP="${ARC_MARKETPLACE_KIOSK_IP:-172.18.0.51}" -BUYER_IP="${ARC_MARKETPLACE_BUYER_IP:-172.18.0.52}" -ARC_RPC_URL="${ARC_MARKETPLACE_RPC_URL:-https://rpc.testnet.arc.network}" -ARC_USDC_ADDRESS="${ARC_MARKETPLACE_USDC_ADDRESS:-0x3600000000000000000000000000000000000000}" -SELLER_KEY="${SELLER_OMNICLAW_PRIVATE_KEY:-${OMNICLAW_X402_FACILITATOR_PRIVATE_KEY:-}}" -BUYER_KEY="${BUYER_OMNICLAW_PRIVATE_KEY:-${OMNICLAW_PRIVATE_KEY:-}}" -BUYER_TOKEN="${ARC_MARKETPLACE_BUYER_TOKEN:-payment-agent-token}" -POLICY_PATH="$ROOT/.runtime/arc-marketplace-showcase/buyer.policy.json" - -if ! docker image inspect "$IMAGE_TAG" >/dev/null 2>&1; then - echo "Missing Docker image $IMAGE_TAG" - echo "Build it with: DOCKER_BUILDKIT=0 docker build -t $IMAGE_TAG -f Dockerfile.agent ." - exit 1 -fi - -if [[ -z "$SELLER_KEY" ]]; then - echo "Missing SELLER_OMNICLAW_PRIVATE_KEY or OMNICLAW_X402_FACILITATOR_PRIVATE_KEY" - exit 1 -fi - -if [[ -z "$BUYER_KEY" ]]; then - echo "Missing BUYER_OMNICLAW_PRIVATE_KEY or OMNICLAW_PRIVATE_KEY" - exit 1 -fi - -if [[ -z "${BUYER_CIRCLE_API_KEY:-${CIRCLE_API_KEY:-}}" ]]; then - echo "Missing BUYER_CIRCLE_API_KEY or CIRCLE_API_KEY" - exit 1 -fi - -if [[ -z "${BUYER_ENTITY_SECRET:-${ENTITY_SECRET:-}}" ]]; then - echo "Missing BUYER_ENTITY_SECRET or ENTITY_SECRET" - exit 1 -fi - -if ! docker network inspect "$NETWORK_NAME" >/dev/null 2>&1; then - docker network create --subnet "$SUBNET" "$NETWORK_NAME" >/dev/null -fi - -wait_for_http() { - local label="$1" - local url="$2" - local container="$3" - local deadline=$((SECONDS + 180)) - - while ((SECONDS < deadline)); do - if curl -fsS --max-time 3 "$url" >/dev/null 2>&1; then - return 0 - fi - sleep 2 - done - - echo "Timed out waiting for $label at $url" - echo "Recent $container logs:" - docker logs --tail 80 "$container" || true - return 1 -} - -mkdir -p "$(dirname "$POLICY_PATH")" - -SELLER_ADDR=$(SELLER_KEY="$SELLER_KEY" uv run python - <<'PY' -from eth_account import Account -import os -print(Account.from_key(os.environ["SELLER_KEY"]).address) -PY -) - -BUYER_ADDR=$(BUYER_KEY="$BUYER_KEY" uv run python - <<'PY' -from eth_account import Account -import os -print(Account.from_key(os.environ["BUYER_KEY"]).address) -PY -) - -BUYER_USDC_BALANCE=$(BUYER_ADDR="$BUYER_ADDR" ARC_RPC_URL="$ARC_RPC_URL" ARC_USDC_ADDRESS="$ARC_USDC_ADDRESS" uv run python - <<'PY' -import os -from decimal import Decimal - -try: - from web3 import Web3 - - w3 = Web3(Web3.HTTPProvider(os.environ["ARC_RPC_URL"], request_kwargs={"timeout": 5})) - token = w3.eth.contract( - address=Web3.to_checksum_address(os.environ["ARC_USDC_ADDRESS"]), - abi=[ - { - "inputs": [{"name": "account", "type": "address"}], - "name": "balanceOf", - "outputs": [{"type": "uint256"}], - "stateMutability": "view", - "type": "function", - } - ], - ) - balance = token.functions.balanceOf(Web3.to_checksum_address(os.environ["BUYER_ADDR"])).call() - print(Decimal(balance) / Decimal(10**6)) -except Exception: - print("unknown") -PY -) - -SELLER_NATIVE_BALANCE=$(SELLER_ADDR="$SELLER_ADDR" ARC_RPC_URL="$ARC_RPC_URL" uv run python - <<'PY' -import os -from decimal import Decimal - -try: - from web3 import Web3 - - w3 = Web3(Web3.HTTPProvider(os.environ["ARC_RPC_URL"], request_kwargs={"timeout": 5})) - balance = w3.eth.get_balance(Web3.to_checksum_address(os.environ["SELLER_ADDR"])) - print(Decimal(balance) / Decimal(10**18)) -except Exception: - print("unknown") -PY -) - -BUYER_ADDR="$BUYER_ADDR" BUYER_TOKEN="$BUYER_TOKEN" KIOSK_IP="$KIOSK_IP" python3 - <<'PY' -import json -import os -from pathlib import Path - -policy = { - "version": "2.0", - "tokens": { - os.environ["BUYER_TOKEN"]: { - "wallet_alias": "payment-agent", - "active": True, - "label": "Arc Marketplace Buyer Agent", - } - }, - "wallets": { - "payment-agent": { - "name": "Arc Marketplace Buyer Agent", - "address": os.environ["BUYER_ADDR"], - "limits": { - "daily_max": "10.00", - "hourly_max": "5.00", - "per_tx_max": "1.00", - "per_tx_min": "0.01", - }, - "rate_limits": {"per_minute": 10, "per_hour": 100}, - "recipients": { - "mode": "whitelist", - "addresses": [], - "domains": [ - "localhost", - "127.0.0.1", - os.environ["KIOSK_IP"], - "omniclaw-arc-kiosk", - ], - }, - "confirm_threshold": None, - } - }, -} - -path = Path(".runtime/arc-marketplace-showcase/buyer.policy.json") -path.write_text(json.dumps(policy, indent=2) + "\n") -PY - -docker rm -f omniclaw-arc-facilitator omniclaw-arc-kiosk omniclaw-arc-buyer >/dev/null 2>&1 || true - -docker run -d \ - --name omniclaw-arc-facilitator \ - --network "$NETWORK_NAME" \ - --ip "$FACILITATOR_IP" \ - -p 4022:4022 \ - -v "$ROOT:/workspace" \ - -w /workspace \ - -e OMNICLAW_X402_FACILITATOR_PRIVATE_KEY="$SELLER_KEY" \ - -e OMNICLAW_X402_FACILITATOR_NETWORK_PROFILE="ARC-TESTNET" \ - -e OMNICLAW_X402_FACILITATOR_RPC_URL="$ARC_RPC_URL" \ - -e OMNICLAW_X402_FACILITATOR_NETWORKS="eip155:5042002" \ - -e OMNICLAW_X402_FACILITATOR_PORT="4022" \ - -e UV_PROJECT_ENVIRONMENT="/tmp/omniclaw-arc-facilitator-venv" \ - "$IMAGE_TAG" \ - sh -lc 'git config --global --add safe.directory /workspace && PYTHONPATH=/workspace/src:/workspace uv run python scripts/start_x402_exact_testnet_facilitator.py' >/dev/null - -docker run -d \ - --name omniclaw-arc-kiosk \ - --network "$NETWORK_NAME" \ - --ip "$KIOSK_IP" \ - -p 8020:8020 \ - -v "$ROOT:/workspace" \ - -w /workspace \ - -e OMNICLAW_X402_EXACT_PAY_TO="$SELLER_ADDR" \ - -e OMNICLAW_X402_EXACT_NETWORK_PROFILE="ARC-TESTNET" \ - -e OMNICLAW_X402_EXACT_NETWORK="eip155:5042002" \ - -e OMNICLAW_X402_EXACT_FACILITATOR_URL="http://$FACILITATOR_IP:4022" \ - -e ARC_MARKETPLACE_PORT="8020" \ - -e ARC_MARKETPLACE_PUBLIC_BASE_URL="http://127.0.0.1:8020" \ - -e ARC_MARKETPLACE_BUYER_BASE_URL="http://$KIOSK_IP:8020" \ - -e ARC_MARKETPLACE_BUYER_ENGINE_URL="http://$BUYER_IP:8080" \ - -e ARC_MARKETPLACE_BUYER_TOKEN="$BUYER_TOKEN" \ - -e ARC_MARKETPLACE_EXPLORER_BASE_URL="https://testnet.arcscan.app/tx/" \ - -e UV_PROJECT_ENVIRONMENT="/tmp/omniclaw-arc-kiosk-venv" \ - "$IMAGE_TAG" \ - sh -lc 'git config --global --add safe.directory /workspace && PYTHONPATH=/workspace/src:/workspace uv run uvicorn app:app --app-dir examples/arc-marketplace-showcase --host 0.0.0.0 --port 8020' >/dev/null - -docker run -d \ - --name omniclaw-arc-buyer \ - --network "$NETWORK_NAME" \ - --ip "$BUYER_IP" \ - -p 8080:8080 \ - -v "$ROOT:/workspace" \ - -w /workspace \ - -e CIRCLE_API_KEY="${BUYER_CIRCLE_API_KEY:-$CIRCLE_API_KEY}" \ - -e ENTITY_SECRET="${BUYER_ENTITY_SECRET:-$ENTITY_SECRET}" \ - -e OMNICLAW_PRIVATE_KEY="$BUYER_KEY" \ - -e OMNICLAW_NETWORK="ARC-TESTNET" \ - -e OMNICLAW_RPC_URL="$ARC_RPC_URL" \ - -e OMNICLAW_AGENT_POLICY_PATH="/workspace/.runtime/arc-marketplace-showcase/buyer.policy.json" \ - -e OMNICLAW_AGENT_TOKEN="$BUYER_TOKEN" \ - -e OMNICLAW_STORAGE_BACKEND="memory" \ - -e OMNICLAW_POLICY_RELOAD_INTERVAL="0" \ - -e UV_PROJECT_ENVIRONMENT="/tmp/omniclaw-arc-buyer-venv" \ - "$IMAGE_TAG" \ - sh -lc 'git config --global --add safe.directory /workspace && PYTHONPATH=/workspace/src:/workspace uv run uvicorn omniclaw.agent.server:app --host 0.0.0.0 --port 8080 --log-level info' >/dev/null - -wait_for_http "facilitator" "http://127.0.0.1:4022/supported" "omniclaw-arc-facilitator" -wait_for_http "vendor kiosk" "http://127.0.0.1:8020/api/catalog" "omniclaw-arc-kiosk" -wait_for_http "buyer policy engine" "http://127.0.0.1:8080/api/v1/health" "omniclaw-arc-buyer" - -printf '\nOmniClaw Arc Marketplace Docker showcase is ready.\n\n' -printf 'Network: %s\n' "$NETWORK_NAME" -printf 'Facilitator: http://%s:4022\n' "$FACILITATOR_IP" -printf 'Vendor kiosk: http://%s:8020\n' "$KIOSK_IP" -printf 'Buyer engine: http://%s:8080\n' "$BUYER_IP" -printf 'Browser UI: http://127.0.0.1:8020\n' -printf 'Buyer address: %s\n' "$BUYER_ADDR" -printf 'Seller address: %s\n' "$SELLER_ADDR" -printf 'Buyer Arc USDC: %s\n' "$BUYER_USDC_BALANCE" -printf 'Seller Arc gas: %s\n' "$SELLER_NATIVE_BALANCE" -printf '\nPaid products:\n' -printf ' Prime Market Scan: $0.25 http://%s:8020/buy/prime-market-scan\n' "$KIOSK_IP" -printf ' Risk Oracle Brief: $0.15 http://%s:8020/buy/risk-oracle-brief\n' "$KIOSK_IP" -printf ' Settlement Receipt Kit: $0.10 http://%s:8020/buy/settlement-receipt-kit\n' "$KIOSK_IP" -printf '\nOpenClaw config:\n' -printf ' OMNICLAW_SERVER_URL=http://%s:8080\n' "$BUYER_IP" -printf ' OMNICLAW_TOKEN=%s\n' "$BUYER_TOKEN" -printf '\nOpenClaw prompt:\n' -printf ' pay for this url: http://%s:8020/buy/prime-market-scan\n' "$KIOSK_IP" -printf '\nCLI test:\n' -printf ' OMNICLAW_SERVER_URL=http://127.0.0.1:8080 OMNICLAW_TOKEN=%s omniclaw-cli inspect-x402 --recipient "http://%s:8020/buy/prime-market-scan"\n' "$BUYER_TOKEN" "$KIOSK_IP" -printf ' OMNICLAW_SERVER_URL=http://127.0.0.1:8080 OMNICLAW_TOKEN=%s omniclaw-cli pay --recipient "http://%s:8020/buy/prime-market-scan" --idempotency-key "arc-kiosk-$(date +%%s)"\n' "$BUYER_TOKEN" "$KIOSK_IP" -printf '\nIf buyer Arc USDC is below $0.25, test the $0.10 endpoint first:\n' -printf ' OMNICLAW_SERVER_URL=http://127.0.0.1:8080 OMNICLAW_TOKEN=%s omniclaw-cli pay --recipient "http://%s:8020/buy/settlement-receipt-kit" --idempotency-key "arc-kiosk-$(date +%%s)"\n' "$BUYER_TOKEN" "$KIOSK_IP" -printf '\nLogs:\n' -printf ' docker logs -f omniclaw-arc-facilitator\n' -printf ' docker logs -f omniclaw-arc-kiosk\n' -printf ' docker logs -f omniclaw-arc-buyer\n\n' diff --git a/scripts/start_arc_vendor_demo.sh b/scripts/start_arc_vendor_demo.sh deleted file mode 100755 index 0b23b4b..0000000 --- a/scripts/start_arc_vendor_demo.sh +++ /dev/null @@ -1,100 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -ROOT=$(cd "$(dirname "$0")/.." && pwd) -cd "$ROOT" - -if [[ -f .env ]]; then - set -a - source .env - set +a -fi - -export OMNICLAW_NETWORK="ARC-TESTNET" -export OMNICLAW_RPC_URL="https://rpc.testnet.arc.network" -export BUSINESS_COMPUTE_NETWORK="ARC-TESTNET" -export BUSINESS_COMPUTE_EXPLORER_BASE_URL="https://testnet.arcscan.app" -export BUSINESS_COMPUTE_ENABLE_LOCAL_BUYER="false" -# Demo-only cleanup: strip persisted wallet bindings from copied example policies -# so Arc runs start from a clean runtime state. -export OMNICLAW_DEMO_RESET_POLICY_WALLETS="1" - -HOST_IP=$(hostname -I | awk '{print $1}') -PUBLIC_BASE="${BUSINESS_COMPUTE_PUBLIC_BASE_URL:-http://${HOST_IP}:8010}" - -docker rm -f omniclaw-business-compute-demo business-compute-redis >/dev/null 2>&1 || true -docker compose -p omniclaw-buyer -f examples/local-economy/docker-compose.payment-agent.yml down -v >/dev/null 2>&1 || true -docker compose -p omniclaw-seller -f examples/local-economy/docker-compose.seller-agent.yml down -v >/dev/null 2>&1 || true - -bash scripts/start_local_economy.sh >/dev/null - -export PAYMENT_AGENT_POLICY_FILE="${PAYMENT_AGENT_POLICY_FILE:-$ROOT/.runtime/payment-agent.policy.runtime.json}" -export SELLER_AGENT_POLICY_FILE="${SELLER_AGENT_POLICY_FILE:-$ROOT/.runtime/seller-agent.policy.runtime.json}" - -docker rm -f omniclaw-business-compute-demo >/dev/null 2>&1 || true -docker rm -f business-compute-redis >/dev/null 2>&1 || true -docker run -d --name business-compute-redis --network omniclaw-buyer_default redis:7-alpine >/dev/null - -docker run -d \ - --name omniclaw-business-compute-demo \ - --network omniclaw-buyer_default \ - -p 8010:8010 \ - -v "$ROOT:/workspace" \ - -w /workspace \ - -e SELLER_OMNICLAW_SERVER_URL="http://seller-agent:9091" \ - -e SELLER_OMNICLAW_TOKEN="seller-agent-token" \ - -e BUYER_OMNICLAW_SERVER_URL="http://payment-agent:9090" \ - -e BUYER_OMNICLAW_TOKEN="payment-agent-token" \ - -e BUSINESS_COMPUTE_PORT="8010" \ - -e BUSINESS_COMPUTE_REDIS_URL="redis://business-compute-redis:6379/0" \ - -e BUSINESS_COMPUTE_PUBLIC_BASE_URL="$PUBLIC_BASE" \ - -e BUSINESS_COMPUTE_NETWORK="$BUSINESS_COMPUTE_NETWORK" \ - -e BUSINESS_COMPUTE_EXPLORER_BASE_URL="$BUSINESS_COMPUTE_EXPLORER_BASE_URL" \ - -e BUSINESS_COMPUTE_ENABLE_LOCAL_BUYER="false" \ - -e UV_PROJECT_ENVIRONMENT="/tmp/omniclaw-business-demo-venv" \ - omniclaw-agent:local \ - sh -lc 'PYTHONPATH=/workspace/src:/workspace uvx --from uvicorn --with fastapi[standard] --with httpx --with redis uvicorn examples.business-compute.app:app --host 0.0.0.0 --port 8010' >/dev/null - -docker network connect omniclaw-seller_default omniclaw-business-compute-demo >/dev/null 2>&1 || true - -BUSINESS_IP=$(docker inspect omniclaw-business-compute-demo --format '{{with index .NetworkSettings.Networks "omniclaw-buyer_default"}}{{.IPAddress}}{{end}}') -AGENT_BASE="http://${BUSINESS_IP}:8010" -python3 - </dev/null - -printf '\nArc vendor demo is live.\n\n' -printf 'Network: %s\n' "$OMNICLAW_NETWORK" -printf 'RPC: %s\n' "$OMNICLAW_RPC_URL" -printf 'Explorer: %s\n' "$BUSINESS_COMPUTE_EXPLORER_BASE_URL" -printf '\nSeller vendor app:\n' -printf ' Browser URL: http://127.0.0.1:8010\n' -printf ' Buyer-facing base: %s\n' "$PUBLIC_BASE" -printf ' Buyer execution base: %s\n' "$AGENT_BASE" -printf ' Example paid URL: %s/compute?job=prime-count&size=1000\n' "$PUBLIC_BASE" -printf ' Example buyer CLI URL: %s/compute?job=prime-count&size=1000\n' "$AGENT_BASE" -printf '\nBuyer policy engine for Telegram/OpenClaw CLI:\n' -printf ' Server URL: http://%s:9090\n' "$HOST_IP" -printf ' Token: payment-agent-token\n' -printf ' Wallet alias: payment-agent\n' -printf ' Network: %s\n' "$OMNICLAW_NETWORK" -printf '\nSeller policy engine:\n' -printf ' Server URL: http://%s:9091\n' "$HOST_IP" -printf ' Token: seller-agent-token\n' -printf ' Wallet alias: seller-agent\n' -printf '\nOpenClaw prompt:\n' -printf ' pay for this url: %s/compute?job=prime-count&size=1000\n' "$AGENT_BASE" -printf '\nBusiness logs:\n' -printf ' docker logs -f omniclaw-business-compute-demo\n\n' - -exec docker logs -f omniclaw-business-compute-demo diff --git a/scripts/start_business_compute_demo.sh b/scripts/start_business_compute_demo.sh deleted file mode 100755 index a94705f..0000000 --- a/scripts/start_business_compute_demo.sh +++ /dev/null @@ -1,58 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail -ROOT=$(cd "$(dirname "$0")/.." && pwd) -cd "$ROOT" - -if [[ -f .env ]]; then - set -a - source .env - set +a -fi - -bash scripts/start_local_economy.sh >/dev/null - -export PAYMENT_AGENT_POLICY_FILE="${PAYMENT_AGENT_POLICY_FILE:-$ROOT/.runtime/payment-agent.policy.runtime.json}" -export SELLER_AGENT_POLICY_FILE="${SELLER_AGENT_POLICY_FILE:-$ROOT/.runtime/seller-agent.policy.runtime.json}" - -docker rm -f omniclaw-business-compute-demo >/dev/null 2>&1 || true -docker rm -f business-compute-redis >/dev/null 2>&1 || true -docker run -d --name business-compute-redis --network omniclaw-buyer_default redis:7-alpine >/dev/null - -docker run -d \ - --name omniclaw-business-compute-demo \ - --network omniclaw-buyer_default \ - -p 8010:8010 \ - -v "$ROOT:/workspace" \ - -w /workspace \ - -e SELLER_OMNICLAW_SERVER_URL="http://seller-agent:9091" \ - -e SELLER_OMNICLAW_TOKEN="seller-agent-token" \ - -e BUYER_OMNICLAW_SERVER_URL="http://payment-agent:9090" \ - -e BUYER_OMNICLAW_TOKEN="payment-agent-token" \ - -e BUSINESS_COMPUTE_PORT="8010" \ - -e BUSINESS_COMPUTE_REDIS_URL="redis://business-compute-redis:6379/0" \ - -e UV_PROJECT_ENVIRONMENT="/tmp/omniclaw-business-demo-venv" \ - omniclaw-agent:local \ - sh -lc 'PYTHONPATH=/workspace/src:/workspace uvx --from uvicorn --with fastapi[standard] --with httpx --with redis uvicorn examples.business-compute.app:app --host 0.0.0.0 --port 8010' >/dev/null - -docker network connect omniclaw-seller_default omniclaw-business-compute-demo >/dev/null 2>&1 || true - -BUSINESS_IP=$(docker inspect omniclaw-business-compute-demo --format '{{with index .NetworkSettings.Networks "omniclaw-buyer_default"}}{{.IPAddress}}{{end}}') -python3 - </dev/null - -printf 'Business compute demo: http://127.0.0.1:8010\n' -printf 'Buyer pay URL: http://%s:8010/compute?job=prime-count&size=1000\n' "$BUSINESS_IP" -printf 'Business container logs: docker logs -f omniclaw-business-compute-demo\n' - -exec docker logs -f omniclaw-business-compute-demo diff --git a/scripts/start_external_x402_seller.py b/scripts/start_external_x402_seller.py deleted file mode 100644 index 1914b9b..0000000 --- a/scripts/start_external_x402_seller.py +++ /dev/null @@ -1,22 +0,0 @@ -#!/usr/bin/env python3 -from __future__ import annotations - -import os -import sys -from pathlib import Path - -os.environ.setdefault("OMNICLAW_X402_EXACT_NETWORK_PROFILE", "BASE-SEPOLIA") -os.environ.setdefault("OMNICLAW_X402_EXACT_FACILITATOR_URL", "https://x402.org/facilitator") - -ROOT = Path(__file__).resolve().parent.parent -if str(ROOT) not in sys.path: - sys.path.insert(0, str(ROOT)) - -from scripts.start_x402_exact_testnet_seller import app # noqa: E402 - - -if __name__ == "__main__": - import uvicorn - - port = int(os.environ.get("OMNICLAW_X402_EXACT_PORT", "4021")) - uvicorn.run(app, host="0.0.0.0", port=port) diff --git a/scripts/start_local_economy.sh b/scripts/start_local_economy.sh deleted file mode 100755 index 8ca47d3..0000000 --- a/scripts/start_local_economy.sh +++ /dev/null @@ -1,103 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail -ROOT=$(cd "$(dirname "$0")/.." && pwd) -cd "$ROOT" - -preserve_env_var() { - local name="$1" - local sentinel="__OMNICLAW_UNSET__" - local current="${!name-$sentinel}" - printf '%s' "$current" -} - -restore_env_var() { - local name="$1" - local value="$2" - if [[ "$value" != "__OMNICLAW_UNSET__" ]]; then - export "$name=$value" - fi -} - -PRE_OMNICLAW_NETWORK="$(preserve_env_var OMNICLAW_NETWORK)" -PRE_OMNICLAW_RPC_URL="$(preserve_env_var OMNICLAW_RPC_URL)" -PRE_CIRCLE_API_KEY="$(preserve_env_var CIRCLE_API_KEY)" -PRE_ENTITY_SECRET="$(preserve_env_var ENTITY_SECRET)" -PRE_OMNICLAW_PRIVATE_KEY="$(preserve_env_var OMNICLAW_PRIVATE_KEY)" -PRE_BUYER_CIRCLE_API_KEY="$(preserve_env_var BUYER_CIRCLE_API_KEY)" -PRE_SELLER_CIRCLE_API_KEY="$(preserve_env_var SELLER_CIRCLE_API_KEY)" -PRE_BUYER_ENTITY_SECRET="$(preserve_env_var BUYER_ENTITY_SECRET)" -PRE_SELLER_ENTITY_SECRET="$(preserve_env_var SELLER_ENTITY_SECRET)" -PRE_BUYER_OMNICLAW_PRIVATE_KEY="$(preserve_env_var BUYER_OMNICLAW_PRIVATE_KEY)" -PRE_SELLER_OMNICLAW_PRIVATE_KEY="$(preserve_env_var SELLER_OMNICLAW_PRIVATE_KEY)" - -if [[ -f .env ]]; then - set -a - source .env - set +a -fi - -# Restore caller-provided env so wrapper scripts can force a specific network or key set. -restore_env_var OMNICLAW_NETWORK "$PRE_OMNICLAW_NETWORK" -restore_env_var OMNICLAW_RPC_URL "$PRE_OMNICLAW_RPC_URL" -restore_env_var CIRCLE_API_KEY "$PRE_CIRCLE_API_KEY" -restore_env_var ENTITY_SECRET "$PRE_ENTITY_SECRET" -restore_env_var OMNICLAW_PRIVATE_KEY "$PRE_OMNICLAW_PRIVATE_KEY" -restore_env_var BUYER_CIRCLE_API_KEY "$PRE_BUYER_CIRCLE_API_KEY" -restore_env_var SELLER_CIRCLE_API_KEY "$PRE_SELLER_CIRCLE_API_KEY" -restore_env_var BUYER_ENTITY_SECRET "$PRE_BUYER_ENTITY_SECRET" -restore_env_var SELLER_ENTITY_SECRET "$PRE_SELLER_ENTITY_SECRET" -restore_env_var BUYER_OMNICLAW_PRIVATE_KEY "$PRE_BUYER_OMNICLAW_PRIVATE_KEY" -restore_env_var SELLER_OMNICLAW_PRIVATE_KEY "$PRE_SELLER_OMNICLAW_PRIVATE_KEY" -STATE_DIR="${STATE_DIR:-$ROOT/.runtime}" -mkdir -p "$STATE_DIR" -cp examples/local-economy/payment-agent.policy.json "$STATE_DIR/payment-agent.policy.runtime.json" -cp examples/local-economy/seller-agent.policy.json "$STATE_DIR/seller-agent.policy.runtime.json" -if [[ "${OMNICLAW_DEMO_RESET_POLICY_WALLETS:-0}" == "1" ]]; then - python3 - </dev/null 2>&1; then - echo "Building $IMAGE_TAG ..." - DOCKER_BUILDKIT=0 docker build -t "$IMAGE_TAG" -f Dockerfile.agent . -fi - -docker compose -p "${BUYER_PROJECT_NAME:-omniclaw-buyer}" -f examples/local-economy/docker-compose.payment-agent.yml up -d --no-build --remove-orphans -sleep 2 -docker compose -p "${SELLER_PROJECT_NAME:-omniclaw-seller}" -f examples/local-economy/docker-compose.seller-agent.yml up -d --no-build --remove-orphans -HOST_IP=$(hostname -I | awk '{print $1}') -printf 'Buyer server: http://localhost:9090\n' -printf 'Buyer token: payment-agent-token\n' -printf 'Buyer wallet: payment-agent\n' -printf 'Seller server: http://localhost:9091\n' -printf 'Seller token: seller-agent-token\n' -printf 'Seller wallet: seller-agent\n' -printf 'Seller paid URL for buyer: http://172.17.0.1:8000/ping\n' -printf 'Seller paid URL for other local/LAN clients: http://%s:8000/ping\n' "$HOST_IP" -printf 'Runtime buyer policy: %s\n' "$PAYMENT_AGENT_POLICY_FILE" -printf 'Runtime seller policy: %s\n' "$SELLER_AGENT_POLICY_FILE" diff --git a/scripts/start_x402_exact_testnet_facilitator.py b/scripts/start_x402_exact_testnet_facilitator.py deleted file mode 100644 index 9b00138..0000000 --- a/scripts/start_x402_exact_testnet_facilitator.py +++ /dev/null @@ -1,17 +0,0 @@ -#!/usr/bin/env python3 -from __future__ import annotations - -from omniclaw.facilitator import ( - create_exact_facilitator_app, - load_exact_facilitator_config_from_env, -) - - -config = load_exact_facilitator_config_from_env() -app = create_exact_facilitator_app(config) - - -if __name__ == "__main__": - import uvicorn - - uvicorn.run(app, host=config.host, port=config.port) diff --git a/scripts/start_x402_exact_testnet_seller.py b/scripts/start_x402_exact_testnet_seller.py deleted file mode 100644 index 16283d3..0000000 --- a/scripts/start_x402_exact_testnet_seller.py +++ /dev/null @@ -1,124 +0,0 @@ -#!/usr/bin/env python3 -from __future__ import annotations - -import os - -from eth_account import Account -from fastapi import FastAPI -from fastapi.responses import JSONResponse -from web3 import Web3 -from omniclaw.facilitator.networks import ( - build_exact_asset_amount, - resolve_exact_settlement_network_profile, -) -from omniclaw.protocols.x402_compat import patch_x402_web3_compat - - -patch_x402_web3_compat() - -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.server import x402ResourceServer - - -def _env(name: str, default: str) -> str: - value = os.environ.get(name, "").strip() - return value or default - - -APP_PORT = int(_env("OMNICLAW_X402_EXACT_PORT", "4021")) -NETWORK_PROFILE = resolve_exact_settlement_network_profile( - _env("OMNICLAW_X402_EXACT_NETWORK_PROFILE", _env("OMNICLAW_NETWORK", "BASE-SEPOLIA")) -) - - -def _resolve_pay_to() -> str: - explicit = os.environ.get("OMNICLAW_X402_EXACT_PAY_TO", "").strip() - if explicit: - return Web3.to_checksum_address(explicit) - - private_key = os.environ.get("OMNICLAW_PRIVATE_KEY", "").strip() - if private_key: - return Account.from_key(private_key).address - - raise RuntimeError( - "Set OMNICLAW_X402_EXACT_PAY_TO or OMNICLAW_PRIVATE_KEY before starting the seller" - ) - - -PAY_TO = _resolve_pay_to() -NETWORK = _env("OMNICLAW_X402_EXACT_NETWORK", NETWORK_PROFILE.caip2) -PRICE = _env("OMNICLAW_X402_EXACT_PRICE", "$0.25") -FACILITATOR_URL = _env("OMNICLAW_X402_EXACT_FACILITATOR_URL", "https://x402.org/facilitator") - - -app = FastAPI(title=f"OmniClaw Exact Seller ({NETWORK_PROFILE.label})") - -facilitator = HTTPFacilitatorClient(FacilitatorConfig(url=FACILITATOR_URL)) -server = x402ResourceServer(facilitator) -exact_scheme = ExactEvmServerScheme() -exact_scheme.register_money_parser( - lambda amount, network: build_exact_asset_amount( - profile=NETWORK_PROFILE, - decimal_amount=amount, - network=str(network), - ) -) -server.register("eip155:*", exact_scheme) - -routes = { - "GET /compute": RouteConfig( - accepts=[ - PaymentOption( - scheme="exact", - price=PRICE, - network=NETWORK, - pay_to=PAY_TO, - ) - ], - description=f"{NETWORK_PROFILE.label} exact x402 compute job", - mime_type="application/json", - ) -} - -app.add_middleware(PaymentMiddlewareASGI, routes=routes, server=server) - - -@app.get("/compute") -async def compute(size: int = 70000) -> JSONResponse: - return JSONResponse( - { - "service": "x402-exact-testnet-seller", - "job": "prime-count", - "input": {"size": size}, - "output": {"prime_count": _prime_count(size)}, - "network": NETWORK, - "network_profile": NETWORK_PROFILE.label, - "asset": NETWORK_PROFILE.default_asset_address, - "price": PRICE, - "facilitator": FACILITATOR_URL, - } - ) - - -def _prime_count(limit: int) -> int: - if limit < 2: - return 0 - sieve = bytearray(b"\x01") * (limit + 1) - sieve[0:2] = b"\x00\x00" - n = 2 - while n * n <= limit: - if sieve[n]: - start = n * n - step = n - sieve[start : limit + 1 : step] = b"\x00" * (((limit - start) // step) + 1) - n += 1 - return int(sum(sieve)) - - -if __name__ == "__main__": - import uvicorn - - uvicorn.run(app, host="0.0.0.0", port=APP_PORT) diff --git a/scripts/verify_release_artifact.py b/scripts/verify_release_artifact.py index 461c9c2..598b4a9 100755 --- a/scripts/verify_release_artifact.py +++ b/scripts/verify_release_artifact.py @@ -14,14 +14,14 @@ REQUIRED_MODULES = [ "omniclaw/__init__.py", - "omniclaw/admin_cli.py", "omniclaw/cli/__init__.py", + "omniclaw/cli/app.py", "omniclaw/cli_agent.py", ] EXPECTED_ENTRYPOINTS = { - "omniclaw": "omniclaw.admin_cli:main", - "omniclaw-cli": "omniclaw.cli_agent:main", + "omniclaw": "omniclaw.cli:main", + "omniclaw-cli": "omniclaw.cli:main", } @@ -46,7 +46,9 @@ def verify_wheel_contents(wheel_path: Path) -> None: parser = configparser.ConfigParser() parser.read_string(zf.read(entry_points_name).decode()) - scripts = dict(parser.items("console_scripts")) if parser.has_section("console_scripts") else {} + scripts = ( + dict(parser.items("console_scripts")) if parser.has_section("console_scripts") else {} + ) for script_name, target in EXPECTED_ENTRYPOINTS.items(): actual = scripts.get(script_name) @@ -78,7 +80,8 @@ def smoke_install(wheel_path: Path) -> None: "import pathlib, sysconfig; " "site = pathlib.Path(sysconfig.get_paths()['purelib']); " "required = [site / 'omniclaw' / '__init__.py', " - "site / 'omniclaw' / 'admin_cli.py', site / 'omniclaw' / 'cli' / '__init__.py', " + "site / 'omniclaw' / 'cli' / '__init__.py', " + "site / 'omniclaw' / 'cli' / 'app.py', " "site / 'omniclaw' / 'cli_agent.py']; " "missing = [str(p) for p in required if not p.exists()]; " "assert not missing, f'missing installed files: {missing}'; " diff --git a/scripts/x402_simple_server.py b/scripts/x402_simple_server.py deleted file mode 100644 index 983bc97..0000000 --- a/scripts/x402_simple_server.py +++ /dev/null @@ -1,65 +0,0 @@ -""" -Simple x402 Facilitator Mock Server. -Implements the x402 protocol (402 Payment Required) for testing. -""" - -import time -import uuid - -import uvicorn -from fastapi import FastAPI, Header, Request, Response - -app = FastAPI() - -# In-memory store for paid requests (just for testing idempotency logic) -PAID_REQUESTS = {} - - -@app.get("/weather") -async def get_weather(request: Request, authorization: str = Header(None)): - if not authorization or not authorization.startswith("x402 "): - return Response( - status_code=402, - headers={ - "WWW-Authenticate": 'x402 payment_url="http://localhost:8000/x402/facilitator", invoice_id="' - + str(uuid.uuid4()) - + '"', - "x402-amount": "1000", - "x402-token": "USDC", - }, - content="Payment Required", - ) - return {"weather": "sunny", "temperature": 25} - - -@app.get("/premium-content") -async def get_premium(request: Request, authorization: str = Header(None)): - if not authorization or not authorization.startswith("x402 "): - return Response( - status_code=402, - headers={ - "WWW-Authenticate": 'x402 payment_url="http://localhost:8000/x402/facilitator", invoice_id="' - + str(uuid.uuid4()) - + '"', - "x402-amount": "10000", - "x402-token": "USDC", - }, - content="Payment Required", - ) - return {"content": "Ultra secret data 💎"} - - -@app.post("/x402/facilitator") -async def facilitator(request: Request): - data = await request.json() - # Mock successful settlement - return { - "status": "success", - "transaction_id": f"mock_tx_{uuid.uuid4().hex[:8]}", - "settled_at": int(time.time()), - "facilitator_sig": "mock_signature_0x123", - } - - -if __name__ == "__main__": - uvicorn.run(app, host="0.0.0.0", port=8000) diff --git a/src/omniclaw/__init__.py b/src/omniclaw/__init__.py index 7062310..34c01e9 100644 --- a/src/omniclaw/__init__.py +++ b/src/omniclaw/__init__.py @@ -107,8 +107,6 @@ ERC20ApprovalError, GatewayAPIError, GatewayBalance, - # Middleware - GatewayMiddleware, # Wallet GatewayWalletManager, InvalidPriceError, @@ -129,7 +127,6 @@ NonceReusedError, PaymentPayload, PaymentRequiredError, - PaymentRequiredHTTPError, PaymentRequirements, SettlementError, SignatureVerificationError, @@ -141,7 +138,6 @@ VerifyResponse, WithdrawError, WithdrawResult, - parse_price, ) from omniclaw.trust.gate import TrustGate @@ -218,10 +214,6 @@ "NanopaymentProtocolAdapter", # Wallet "GatewayWalletManager", - # Middleware - "GatewayMiddleware", - "PaymentRequiredHTTPError", - "parse_price", # Types "DepositResult", "GatewayBalance", diff --git a/src/omniclaw/admin_cli.py b/src/omniclaw/admin_cli.py deleted file mode 100644 index ee134a6..0000000 --- a/src/omniclaw/admin_cli.py +++ /dev/null @@ -1,387 +0,0 @@ -from __future__ import annotations - -import argparse -import json -import os -import warnings -from collections.abc import Sequence - -from omniclaw.onboarding import print_doctor_status - -# Suppress deprecation warnings from downstream dependencies. -warnings.filterwarnings("ignore", category=DeprecationWarning, module="pkg_resources") -warnings.filterwarnings("ignore", message=".*pkg_resources is deprecated.*") - -BANNER = r""" - ____ __ __ _ _ ___ ____ _ ___ __ - / __ \| \/ | \ | |_ _/ ___| | / \ \ / / - | | | | |\/| | \| || | | | | / _ \ \ /\ / / - | |__| | | | | |\ || | |___| |___ / ___ \ V V / - \____/|_| |_|_| \_|___\____|_____/_/ \_\_/\_/ - - OmniClaw is the economy and control layer for AI agent payments. - Economic Execution and Control Layer for Agentic Systems -""" - -ENV_VARS = { - "required": { - "CIRCLE_API_KEY": "Circle API key for wallet/payment operations", - "OMNICLAW_PRIVATE_KEY": "Private key for nanopayment signing", - }, - "optional": { - "ENTITY_SECRET": ( - "Existing Circle Entity Secret. Set this directly if your API key already has one." - ), - "OMNICLAW_RPC_URL": "RPC endpoint for trust gate (ERC-8004)", - "OMNICLAW_NETWORK": "Default network profile, for example BASE-SEPOLIA or ARC-TESTNET", - "OMNICLAW_STORAGE_BACKEND": "Storage backend: memory or redis", - "OMNICLAW_REDIS_URL": "Redis connection URL (when using redis)", - "OMNICLAW_LOG_LEVEL": "Logging: DEBUG, INFO, WARNING, ERROR", - "OMNICLAW_X402_FACILITATOR_PRIVATE_KEY": ( - "Private key used by a self-hosted x402 exact facilitator" - ), - "OMNICLAW_X402_FACILITATOR_NETWORK_PROFILE": ("Self-hosted facilitator network profile"), - "OMNICLAW_X402_FACILITATOR_RPC_URL": "Self-hosted facilitator RPC endpoint", - "OMNICLAW_X402_FACILITATOR_NETWORKS": ( - "Comma-separated CAIP-2 networks accepted by the facilitator" - ), - }, - "production": { - "OMNICLAW_ENV": "Set to production for mainnet/strict mode", - "OMNICLAW_STRICT_SETTLEMENT": "Enable strict settlement validation", - "OMNICLAW_WEBHOOK_VERIFICATION_KEY": "Public key for webhook signatures", - "OMNICLAW_SELLER_NONCE_REDIS_URL": "Redis for distributed nonce (multi-instance)", - }, -} - - -def print_banner() -> None: - print(f"\033[1;36m{BANNER}\033[0m") - print("\033[90mOmniClaw Financial Infrastructure - v2.0 Production-Ready\033[0m\n") - - -def print_env_vars() -> None: - print("\n=== OmniClaw Environment Variables ===\n") - - print("Required:") - for var, desc in ENV_VARS["required"].items(): - value = os.environ.get(var, "") - status = f"✓ {value[:20]}..." if value else "✗ not set" - print(f" {var}") - print(f" {desc}") - print(f" {status}\n") - - print("\nOptional:") - for var, desc in ENV_VARS["optional"].items(): - value = os.environ.get(var, "") - status = f"✓ {value[:30]}..." if value else "○ default" - print(f" {var}") - print(f" {desc}") - print(f" {status}\n") - - print("\nProduction:") - for var, desc in ENV_VARS["production"].items(): - value = os.environ.get(var, "") - status = f"✓ {value[:30]}..." if value else "○ not set" - print(f" {var}") - print(f" {desc}") - print(f" {status}\n") - - -def build_parser() -> argparse.ArgumentParser: - parser = argparse.ArgumentParser(prog="omniclaw") - subparsers = parser.add_subparsers(dest="command") - - doctor_parser = subparsers.add_parser( - "doctor", - help="Inspect OmniClaw setup, managed credentials, and recovery state", - ) - doctor_parser.add_argument("--api-key", help="Override CIRCLE_API_KEY for diagnostics") - doctor_parser.add_argument("--entity-secret", help="Override ENTITY_SECRET for diagnostics") - doctor_parser.add_argument("--json", action="store_true", help="Output machine-readable JSON") - - subparsers.add_parser("env", help="List all available environment variables") - - setup_parser = subparsers.add_parser( - "setup", - help="Quickly set up your Financial Policy Engine credentials (.env.agent)", - ) - setup_parser.add_argument("--api-key", help="Circle API Key") - setup_parser.add_argument( - "--entity-secret", - default=None, - help=( - "Existing 64-char Circle Entity Secret. If omitted, OmniClaw uses a " - "managed/env secret or generates and registers a new one." - ), - ) - setup_parser.add_argument( - "--network", - default="ARC-TESTNET", - help="Circle Network (default: ARC-TESTNET)", - ) - - server_parser = subparsers.add_parser( - "server", - help="Start the OmniClaw Financial Policy Engine server", - ) - server_parser.add_argument("--host", default="0.0.0.0", help="Host to bind to") - server_parser.add_argument("--port", type=int, default=8080, help="Port to listen on") - server_parser.add_argument("--reload", action="store_true", help="Enable auto-reload") - - facilitator_parser = subparsers.add_parser( - "facilitator", - help="Run OmniClaw-operated x402 facilitator services", - ) - facilitator_sub = facilitator_parser.add_subparsers(dest="facilitator_command") - exact_parser = facilitator_sub.add_parser( - "exact", - help="Start a self-hosted x402 exact facilitator", - ) - exact_parser.add_argument("--host", default="0.0.0.0", help="Host to bind to") - exact_parser.add_argument("--port", type=int, default=4022, help="Port to listen on") - exact_parser.add_argument( - "--network-profile", - default=None, - help="OmniClaw network profile, for example BASE-SEPOLIA or ARC-TESTNET", - ) - exact_parser.add_argument( - "--network", - action="append", - default=None, - help="Accepted CAIP-2 network. Repeat to support multiple networks.", - ) - exact_parser.add_argument("--rpc-url", default=None, help="RPC URL for settlement") - exact_parser.add_argument( - "--private-key", - default=None, - help="Facilitator settlement private key. Prefer env in shared shells.", - ) - exact_parser.add_argument("--title", default=None, help="FastAPI title") - - policy_parser = subparsers.add_parser("policy", help="Policy utilities (lint/validate)") - policy_sub = policy_parser.add_subparsers(dest="policy_command") - lint_parser = policy_sub.add_parser("lint", help="Validate policy.json") - lint_parser.add_argument( - "--path", - default=os.environ.get("OMNICLAW_AGENT_POLICY_PATH", "/config/policy.json"), - help="Path to policy.json", - ) - - return parser - - -def handle_setup(args: argparse.Namespace) -> int: - from omniclaw.onboarding import create_env_file, resolve_entity_secret, validate_entity_secret - - api_key = args.api_key or os.getenv("CIRCLE_API_KEY") - if not api_key: - api_key = input("Enter your Circle API Key: ").strip() - - if not api_key: - print("❌ Error: Circle API Key is required.") - return 1 - - entity_secret = args.entity_secret or resolve_entity_secret(api_key) - if entity_secret: - entity_secret = validate_entity_secret(entity_secret) - print("✅ Using existing Circle Entity Secret.") - else: - print("💡 No Entity Secret found for this API key.") - entity_secret = input( - "Enter your 64-char Entity Secret (or press Enter to generate): " - ).strip() - if not entity_secret: - from omniclaw.onboarding import auto_setup_entity_secret - - print("🚀 Generating and registering new Entity Secret...") - entity_secret = auto_setup_entity_secret(api_key) - else: - entity_secret = validate_entity_secret(entity_secret) - - env_path = ".env.agent" - create_env_file(api_key, entity_secret, env_path=env_path, network=args.network, overwrite=True) - print(f"✨ Successfully configured {env_path}!") - print("To start the server locally, run: omniclaw server") - print( - "To start via Docker, run: " - "docker compose -f examples/local-economy/docker-compose.payment-agent.yml up -d" - ) - return 0 - - -def handle_server(args: argparse.Namespace) -> int: - import uvicorn - from dotenv import load_dotenv - - from omniclaw.onboarding import auto_setup_entity_secret, resolve_entity_secret - - if os.path.exists(".env.agent"): - load_dotenv(".env.agent") - print("📄 Loaded configuration from .env.agent") - elif os.path.exists(".env"): - load_dotenv(".env") - print("📄 Loaded configuration from .env") - - api_key = os.getenv("CIRCLE_API_KEY") - entity_secret = os.getenv("ENTITY_SECRET") - - if api_key and not entity_secret: - print("💡 Found API Key but no Entity Secret. Attempting auto-setup...") - entity_secret = resolve_entity_secret(api_key) - if not entity_secret: - print("🚀 Generating new Entity Secret for this machine...") - entity_secret = auto_setup_entity_secret(api_key) - - if entity_secret: - os.environ["ENTITY_SECRET"] = entity_secret - print("✅ Credentials verified and injected.") - else: - print("❌ Error: Failed to resolve or generate Entity Secret.") - return 1 - - print(f"🚀 Starting OmniClaw Financial Policy Engine on {args.host}:{args.port}...") - uvicorn.run( - "omniclaw.agent.server:app", - host=args.host, - port=args.port, - reload=args.reload, - log_level=os.getenv("OMNICLAW_LOG_LEVEL", "info").lower(), - ) - return 0 - - -def handle_facilitator_exact(args: argparse.Namespace) -> int: - import uvicorn - from dotenv import load_dotenv - - from omniclaw.facilitator import ( - ExactFacilitatorConfig, - create_exact_facilitator_app, - resolve_exact_settlement_network_profile, - ) - - if os.path.exists(".env.agent"): - load_dotenv(".env.agent") - print("📄 Loaded configuration from .env.agent") - elif os.path.exists(".env"): - load_dotenv(".env") - print("📄 Loaded configuration from .env") - - profile_name = ( - args.network_profile - or os.getenv("OMNICLAW_X402_FACILITATOR_NETWORK_PROFILE") - or os.getenv("OMNICLAW_NETWORK") - or "BASE-SEPOLIA" - ) - profile = resolve_exact_settlement_network_profile(profile_name) - private_key = ( - args.private_key - or os.getenv("OMNICLAW_X402_FACILITATOR_PRIVATE_KEY") - or os.getenv("OMNICLAW_PRIVATE_KEY") - or "" - ).strip() - if not private_key: - print( - "❌ Error: set OMNICLAW_X402_FACILITATOR_PRIVATE_KEY or pass --private-key " - "to run an exact facilitator." - ) - return 1 - - explicit_env_networks = tuple( - value.strip() - for value in os.getenv("OMNICLAW_X402_FACILITATOR_NETWORKS", "").split(",") - if value.strip() - ) - networks = tuple(args.network or ()) or explicit_env_networks or (profile.caip2,) - rpc_url = ( - args.rpc_url - or os.getenv("OMNICLAW_X402_FACILITATOR_RPC_URL") - or profile.default_rpc_url - or "" - ).strip() - if not rpc_url: - print( - "❌ Error: set OMNICLAW_X402_FACILITATOR_RPC_URL or pass --rpc-url " - f"for {profile.label}." - ) - return 1 - - config = ExactFacilitatorConfig( - private_key=private_key, - rpc_url=rpc_url, - networks=networks, - network_profile=profile.label, - port=args.port, - host=args.host, - title=args.title or f"OmniClaw Exact Facilitator ({profile.label})", - ) - app = create_exact_facilitator_app(config) - print(f"🚀 Starting OmniClaw x402 exact facilitator on {config.host}:{config.port}") - print(f" Profile: {profile.label}") - print(f" Networks: {', '.join(config.networks)}") - print(f" RPC: {config.rpc_url}") - uvicorn.run( - app, - host=config.host, - port=config.port, - log_level=os.getenv("OMNICLAW_LOG_LEVEL", "info").lower(), - ) - return 0 - - -def handle_policy_lint(args: argparse.Namespace) -> int: - from omniclaw.agent.policy_schema import validate_policy - - try: - with open(args.path) as f: - data = json.load(f) - validate_policy(data) - print(f"✅ policy.json is valid: {args.path}") - return 0 - except Exception as exc: - print(f"❌ Invalid policy.json: {exc}") - return 1 - - -def main(argv: Sequence[str] | None = None) -> int: - parser = build_parser() - args = parser.parse_args(argv) - print_banner() - - if args.command == "doctor": - print_doctor_status( - api_key=args.api_key, - entity_secret=args.entity_secret, - as_json=args.json, - ) - return 0 - - if args.command == "env": - print_env_vars() - return 0 - - if args.command == "setup": - return handle_setup(args) - - if args.command == "server": - return handle_server(args) - - if args.command == "facilitator" and args.facilitator_command == "exact": - return handle_facilitator_exact(args) - - if args.command == "policy" and args.policy_command == "lint": - return handle_policy_lint(args) - - parser.print_help() - print("\nCommands:") - print(" setup - Quick credentials configuration") - print(" server - Start the Financial Policy Engine server") - print(" facilitator - Run OmniClaw-operated x402 facilitator services") - print(" doctor - Inspect setup and credentials") - print(" env - List all environment variables") - return 1 - - -if __name__ == "__main__": - raise SystemExit(main()) diff --git a/src/omniclaw/agent/models.py b/src/omniclaw/agent/models.py index 0854a9f..bac77ac 100644 --- a/src/omniclaw/agent/models.py +++ b/src/omniclaw/agent/models.py @@ -204,19 +204,3 @@ class X402InspectResponse(BaseModel): selected_amount_usdc: str | None = None selected_pay_to: str | None = None seller_accepts: list[dict[str, Any]] = Field(default_factory=list) - - -class X402VerifyRequest(BaseModel): - """X402 Verification request.""" - - signature: str = Field(..., description="Payment signature/proof") - amount: str = Field(..., description="Amount paid") - sender: str = Field(..., description="Sender address") - resource: str = Field(..., description="Resource URL") - - -class X402RequirementsRequest(BaseModel): - """X402 requirements request for seller-side paid endpoints.""" - - amount: str = Field(..., description="Price in USD or atomic units") - resource: str = Field(..., description="Protected resource URL") diff --git a/src/omniclaw/agent/routes.py b/src/omniclaw/agent/routes.py index c3cd987..12fff49 100644 --- a/src/omniclaw/agent/routes.py +++ b/src/omniclaw/agent/routes.py @@ -27,14 +27,11 @@ WalletInfo, X402InspectRequest, X402InspectResponse, - X402RequirementsRequest, - X402VerifyRequest, ) from omniclaw.agent.policy import PolicyManager, WalletManager from omniclaw.core.logging import get_logger from omniclaw.core.types import PaymentMethod from omniclaw.guards.confirmations import ConfirmationStore -from omniclaw.ledger import LedgerEntry, LedgerEntryStatus, LedgerEntryType if TYPE_CHECKING: from omniclaw import OmniClaw @@ -80,7 +77,6 @@ async def _choose_x402_route( prefer_gateway=False, source_network=agent_network, ) - gateway_available_balance: str | None = None gateway_ready: bool | None = None gateway_reason: str | None = None @@ -1319,110 +1315,3 @@ async def x402_inspect( selected_pay_to=selected_kind.recipient if selected_kind else None, seller_accepts=seller_accepts, ) - - -@router.post("/x402/verify") -async def x402_verify( - request: X402VerifyRequest, - agent: AuthenticatedAgent = Depends(get_current_agent), - client: OmniClaw = Depends(get_omniclaw_client), -): - """Verify and settle an incoming x402 payment signature (for 'omniclaw-cli serve').""" - import base64 - import json - - try: - if not client._nano_client: - return {"valid": False, "error": "Nanopayment client not initialized"} - - sig_data = json.loads(base64.b64decode(request.signature)) - if int(sig_data.get("x402Version", 2)) == 2 and not sig_data.get("accepted"): - return { - "valid": False, - "error": "Missing accepted requirements in PAYMENT-SIGNATURE payload", - } - - from omniclaw.protocols.nanopayments.middleware import GatewayMiddleware - from omniclaw.protocols.nanopayments.types import PaymentPayload, PaymentRequirements - - payload = PaymentPayload.from_dict(sig_data) - amount_text = request.amount if request.amount.startswith("$") else f"${request.amount}" - - seller_address = await client.get_payment_address(agent.wallet_id) - if not seller_address: - return {"valid": False, "error": "Seller payment address not found"} - - middleware = GatewayMiddleware( - seller_address=seller_address, - nanopayment_client=client._nano_client, - ) - requirements_body = await middleware._build_402_response(amount_text) - requirements = PaymentRequirements.from_dict(requirements_body) - - result = await client._nano_client.settle(payload, requirements) - - if result.success: - await client._ledger.record( - LedgerEntry( - wallet_id=agent.wallet_id, - recipient=result.payer or "", - amount=Decimal(str(request.amount)), - entry_type=LedgerEntryType.PAYMENT, - status=LedgerEntryStatus.COMPLETED, - tx_hash=result.transaction, - method="nanopayment_receive", - purpose=f"x402 settlement for {request.resource}", - metadata={ - "direction": "incoming", - "resource": request.resource, - "payer": result.payer, - "transaction_id": result.transaction, - }, - ) - ) - return { - "valid": True, - "sender": result.payer, - "amount": request.amount, - "transaction": result.transaction, - } - return {"valid": False, "error": result.error_reason or "Settlement failed"} - - except Exception as e: - logger.error(f"x402 verify failed: {e}") - return {"valid": False, "error": str(e)} - - -@router.post("/x402/requirements") -async def x402_requirements( - request: X402RequirementsRequest, - agent: AuthenticatedAgent = Depends(get_current_agent), - client: OmniClaw = Depends(get_omniclaw_client), -): - """Build x402 payment requirements for a seller-side paid endpoint.""" - try: - if not client._nano_client: - raise HTTPException(status_code=500, detail="Nanopayment client not initialized") - - from omniclaw.protocols.nanopayments.middleware import GatewayMiddleware - - seller_address = await client.get_payment_address(agent.wallet_id) - if not seller_address: - raise HTTPException(status_code=404, detail="Seller payment address not found") - - middleware = GatewayMiddleware( - seller_address=seller_address, - nanopayment_client=client._nano_client, - ) - body = await middleware._build_402_response(request.amount) - header_value = middleware._encode_requirements(body) - return { - "status_code": 402, - "detail": body, - "headers": {"PAYMENT-REQUIRED": header_value}, - } - except HTTPException: - raise - except Exception as e: - logger.error(f"x402 requirements failed: {e}") - raise HTTPException(status_code=500, detail=str(e)) from e diff --git a/src/omniclaw/cli/app.py b/src/omniclaw/cli/app.py index 2b943b0..3212068 100644 --- a/src/omniclaw/cli/app.py +++ b/src/omniclaw/cli/app.py @@ -10,7 +10,6 @@ from .commands import intents as intents_cmd from .commands import ledger as ledger_cmd from .commands import payments as payments_cmd -from .commands import serve as serve_cmd from .commands import status as status_cmd from .commands import wallet as wallet_cmd from .config import is_quiet @@ -22,8 +21,8 @@ app = typer.Typer( help=( - "omniclaw-cli - zero-trust execution layer for policy-controlled agent payments, " - "x402 services, and agentic commerce" + "omniclaw-cli - zero-trust execution layer for policy-controlled agent payments " + "and x402 buyer flows" ) ) @@ -66,7 +65,6 @@ def callback() -> None: intents_cmd.register(app, intents_app) ledger_cmd.register(app) confirmations_cmd.register(app, confirmations_app) -serve_cmd.register(app) status_cmd.register(app) app.add_typer(wallet_app, name="wallet") diff --git a/src/omniclaw/cli/commands/serve.py b/src/omniclaw/cli/commands/serve.py deleted file mode 100644 index 300c2f5..0000000 --- a/src/omniclaw/cli/commands/serve.py +++ /dev/null @@ -1,169 +0,0 @@ -from __future__ import annotations - -import base64 -import json -import os -import subprocess -from urllib.parse import urlencode - -import typer - -from ..config import get_client, is_quiet - -try: - from fastapi import FastAPI, Request, Response - from fastapi.responses import JSONResponse - - _FASTAPI_AVAILABLE = True -except Exception: - FastAPI = None # type: ignore[assignment] - Request = None # type: ignore[assignment] - Response = None # type: ignore[assignment] - JSONResponse = None # type: ignore[assignment] - _FASTAPI_AVAILABLE = False - - -def serve( - price: float = typer.Option(..., "--price", help="Price per request in USDC"), - endpoint: str = typer.Option(..., "--endpoint", help="Endpoint path to expose"), - exec_cmd: str = typer.Option(..., "--exec", help="Command to execute on success"), - port: int = typer.Option(8000, "--port", help="Local port to listen on"), -) -> None: - """Expose a local service behind an x402 payment gate. - - Uses the production GatewayMiddleware for full x402 v2 protocol compliance: - - Returns proper 402 responses with all required fields - - Parses PAYMENT-SIGNATURE headers - - Settles atomically via Circle Gateway /settle - """ - try: - import uvicorn - except ImportError as err: - typer.echo("Error: FastAPI/uvicorn not installed. Run: pip install fastapi uvicorn") - raise typer.Exit(1) from err - if not _FASTAPI_AVAILABLE: - typer.echo("Error: FastAPI not installed. Run: pip install fastapi", err=True) - raise typer.Exit(1) - - server_app = FastAPI(title="OmniClaw x402 Payment Gate") - ctrl_client = get_client() - - # Price string in USD format - price_usd = f"${price}" - - @server_app.api_route(endpoint, methods=["GET", "POST", "PUT", "DELETE"]) - async def payment_gate(request: Request): - try: - headers = dict(request.headers) - sig_header = headers.get("payment-signature") or headers.get("PAYMENT-SIGNATURE") - - if not sig_header: - requirements_resp = ctrl_client.post( - "/api/v1/x402/requirements", - json={ - "amount": price_usd, - "resource": str(request.url), - }, - ) - requirements_resp.raise_for_status() - requirements = requirements_resp.json() - return JSONResponse( - status_code=requirements.get("status_code", 402), - content=requirements.get("detail", {}), - headers=requirements.get("headers", {}), - ) - - verify_resp = ctrl_client.post( - "/api/v1/x402/verify", - json={ - "signature": sig_header, - "amount": str(price), - "sender": headers.get("x-forwarded-for", ""), - "resource": str(request.url), - }, - ) - verify_resp.raise_for_status() - verify_data = verify_resp.json() - if not verify_data.get("valid"): - requirements_resp = ctrl_client.post( - "/api/v1/x402/requirements", - json={ - "amount": price_usd, - "resource": str(request.url), - }, - ) - requirements_resp.raise_for_status() - requirements = requirements_resp.json() - return JSONResponse( - status_code=402, - content=requirements.get("detail", {"error": verify_data.get("error")}), - headers=requirements.get("headers", {}), - ) - - # Payment settled successfully — execute the command - try: - env = os.environ.copy() - env["OMNICLAW_PAYER_ADDRESS"] = verify_data.get("sender") or "unknown" - env["OMNICLAW_AMOUNT_USD"] = str(price) - env["OMNICLAW_TX_HASH"] = verify_data.get("transaction") or "" - env["OMNICLAW_REQUEST_METHOD"] = request.method - env["OMNICLAW_REQUEST_PATH"] = request.url.path - env["OMNICLAW_REQUEST_URL"] = str(request.url) - env["OMNICLAW_REQUEST_QUERY"] = urlencode(list(request.query_params.multi_items())) - - raw_body = await request.body() - env["OMNICLAW_REQUEST_BODY_BASE64"] = base64.b64encode(raw_body).decode() - if raw_body: - try: - env["OMNICLAW_REQUEST_BODY_TEXT"] = raw_body.decode("utf-8") - except UnicodeDecodeError: - env["OMNICLAW_REQUEST_BODY_TEXT"] = "" - else: - env["OMNICLAW_REQUEST_BODY_TEXT"] = "" - - result = subprocess.run( - exec_cmd, shell=True, capture_output=True, text=True, env=env - ) - media_type = "text/plain" - stripped = result.stdout.lstrip() - if stripped.startswith("{") or stripped.startswith("["): - media_type = "application/json" - response = Response(content=result.stdout, media_type=media_type) - except Exception as e: - response = JSONResponse( - status_code=500, - content={"detail": f"Execution failed: {e}"}, - ) - - response.headers["PAYMENT-RESPONSE"] = base64.b64encode( - json.dumps( - { - "success": True, - "transaction": verify_data.get("transaction", ""), - "network": "", - "payer": verify_data.get("sender", ""), - } - ).encode() - ).decode() - - return response - except Exception as e: - return JSONResponse( - status_code=500, - content={"detail": f"Payment processing failed: {e}"}, - ) - - if not is_quiet(): - typer.echo(f"OmniClaw service exposed at http://localhost:{port}{endpoint}") - typer.echo(f"Price: ${price} USDC per request") - typer.echo(f"Exec: {exec_cmd}") - typer.echo("x402 v2 Protocol — Circle Gateway settlement") - typer.echo("") - - uvicorn.run(server_app, host="0.0.0.0", port=port) - - -def register(app: typer.Typer, group: typer.Typer | None = None) -> None: - app.command()(serve) - if group is not None and group is not app: - group.command()(serve) diff --git a/src/omniclaw/client.py b/src/omniclaw/client.py index 7a89777..89daa8d 100644 --- a/src/omniclaw/client.py +++ b/src/omniclaw/client.py @@ -3,7 +3,6 @@ from __future__ import annotations import asyncio -import contextvars import ipaddress import os import re @@ -60,12 +59,10 @@ from omniclaw.protocols.nanopayments import ( DepositResult, GatewayBalance, - GatewayMiddleware, NanopaymentAdapter, NanopaymentClient, NanopaymentNotInitializedError, NanopaymentProtocolAdapter, - PaymentInfo, WithdrawResult, ) from omniclaw.protocols.transfer import TransferAdapter @@ -77,10 +74,6 @@ from omniclaw.wallet.service import WalletService from omniclaw.webhooks import WebhookParser -_current_payment_info: contextvars.ContextVar[PaymentInfo | None] = contextvars.ContextVar( - "_current_payment_info", default=None -) - class OmniClaw: """ @@ -191,8 +184,6 @@ def __init__( self._nano_client: NanopaymentClient | None = None self._nano_adapter: NanopaymentAdapter | None = None self._nano_http: httpx.AsyncClient | None = None - self._gateway_middleware: GatewayMiddleware | None = None - self._gateway_default_address: str | None = None if self._config.nanopayments_enabled: self._init_nanopayments() @@ -246,7 +237,6 @@ def _enforce_production_startup_requirements(self) -> None: return required_env = [ - "OMNICLAW_SELLER_NONCE_REDIS_URL", "OMNICLAW_WEBHOOK_VERIFICATION_KEY", "OMNICLAW_WEBHOOK_DEDUP_DB_PATH", ] @@ -389,173 +379,6 @@ def nanopayment_adapter(self) -> NanopaymentAdapter | None: """ return self._nano_adapter - async def gateway( - self, - seller_address: str | None = None, - facilitator: str | None = None, - ) -> GatewayMiddleware: - """ - Get the GatewayMiddleware for protecting seller endpoints with x402 payments. - - Usage (FastAPI): - from fastapi import Depends - - app = FastAPI() - - @app.get("/premium") - async def premium(payment=Depends(omniclaw.gateway().require("$0.001"))): - return {"data": "paid content", "paid_by": payment.payer} - - Args: - seller_address: The address that receives payments. - - For Circle Gateway: uses your wallet's nano address - - For other facilitators: any EVM address you control - facilitator: Choose which facilitator to use: - - "circle" (default): Circle Gateway (needs wallet) - - "coinbase": Coinbase CDP - - "ordern": OrderN - - "rbx": RBX - - "thirdweb": Thirdweb - - "omniclaw": OmniClaw self-hosted exact facilitator - - Raises: - NanopaymentNotInitializedError: If nanopayments are disabled and facilitator is Circle. - """ - # Return cached middleware if available and no overrides specified - if self._gateway_middleware is not None and seller_address is None and facilitator is None: - return self._gateway_middleware - - # For Circle, we need nanopayments initialized - if (facilitator is None or facilitator == "circle") and ( - not self._nano_client or not self._nano_adapter - ): - raise NanopaymentNotInitializedError() - - # If no seller_address provided, try to get from wallet - if not seller_address: - if self._nano_adapter: - # Direct private key mode: use adapter address - seller_address = self._nano_adapter.address - if not seller_address: - raise ValueError( - "seller_address is required. " - "Provide your payment address, or create a wallet first." - ) - - # Create facilitator if not Circle - facilitator_client = None - if facilitator and facilitator != "circle": - from omniclaw.seller.facilitator_generic import create_facilitator - - facilitator_client = create_facilitator( - provider=facilitator, - environment=self._config.nanopayments_environment, - ) - - from omniclaw.protocols.nanopayments.middleware import GatewayMiddleware - - # For Circle, we need the nanopayment client - if facilitator_client is None and self._nano_client: - client_to_use = self._nano_client - else: - client_to_use = None - - self._gateway_middleware = GatewayMiddleware( - seller_address=seller_address, - nanopayment_client=client_to_use, - facilitator=facilitator_client, - ) - - return self._gateway_middleware - - def sell( - self, - price: str, - seller_address: str | None = None, - facilitator: str | None = None, - ) -> Any: - """ - Decorator factory for marking a FastAPI route as a paid endpoint. - - Returns a FastAPI Depends() that gates the route with x402 payment. - - Usage: - from fastapi import FastAPI - - @app.get("/premium") - async def premium(payment=omniclaw.sell("$0.001")): - payment_info = omniclaw.current_payment() - return {"data": "paid content", "paid_by": payment_info.payer} - - Args: - price: Price in USD string (e.g. "$0.001", "1.00"). - seller_address: Your payment address. - - For Circle Gateway: your wallet's nano address - - For other facilitators: any EVM address you control - facilitator: Choose which facilitator: - - "circle" (default): Circle Gateway (needs wallet) - - "coinbase": Coinbase CDP - - "ordern": OrderN - - "rbx": RBX - - "thirdweb": Thirdweb - - "omniclaw": OmniClaw self-hosted exact facilitator - - Returns: - A FastAPI Depends() callable. - - Examples: - # Circle Gateway (needs wallet) - client.sell("$0.01") - client.sell("$0.01", seller_address="0xYourNanoAddress") - - # Other facilitators (just provide your address) - client.sell("$0.01", facilitator="coinbase") - client.sell("$0.01", seller_address="0xYourAddress", facilitator="coinbase") - client.sell("$0.01", seller_address="0xYourAddress", facilitator="omniclaw") - """ - from fastapi import Depends, Request - - def base_dependency_factory(): - return self.gateway( - seller_address=seller_address, - facilitator=facilitator, - ) - - price_str = price - - async def wrapper(request: Request) -> PaymentInfo: - gateway_mw = await base_dependency_factory() - base_dep = gateway_mw.require(price_str) - payment_info: PaymentInfo = await base_dep(request) - _current_payment_info.set(payment_info) - return payment_info - - return Depends(wrapper) - - def current_payment(self) -> PaymentInfo: - """ - Get the current payment within a @sell() decorated function. - - Returns the PaymentInfo for the in-progress payment, including - the payer's address, amount, network, and settlement transaction. - - Usage: - @agent.sell(price="$0.001") - async def get_data(): - payment = agent.current_payment() - return {"data": "...", "paid_by": payment.payer} - - Returns: - PaymentInfo for the current request. - - Raises: - ValueError: If called outside of a @sell() decorated function. - """ - info: PaymentInfo | None = _current_payment_info.get() - if info is None: - raise ValueError("current_payment() called outside of a @sell() decorated function") - return info - # ------------------------------------------------------------------------- # Gateway Wallet management (on-chain deposit/withdraw) # ------------------------------------------------------------------------- diff --git a/src/omniclaw/facilitator/__init__.py b/src/omniclaw/facilitator/__init__.py deleted file mode 100644 index b7d18af..0000000 --- a/src/omniclaw/facilitator/__init__.py +++ /dev/null @@ -1,23 +0,0 @@ -"""Internal facilitator surfaces for OmniClaw-operated x402 settlement.""" - -from omniclaw.facilitator.exact import ( - CompatFacilitatorWeb3Signer, - ExactFacilitatorConfig, - create_exact_facilitator_app, - load_exact_facilitator_config_from_env, -) -from omniclaw.facilitator.networks import ( - ExactSettlementNetworkProfile, - build_exact_asset_amount, - resolve_exact_settlement_network_profile, -) - -__all__ = [ - "CompatFacilitatorWeb3Signer", - "ExactFacilitatorConfig", - "ExactSettlementNetworkProfile", - "build_exact_asset_amount", - "create_exact_facilitator_app", - "load_exact_facilitator_config_from_env", - "resolve_exact_settlement_network_profile", -] diff --git a/src/omniclaw/facilitator/exact.py b/src/omniclaw/facilitator/exact.py deleted file mode 100644 index 5596f28..0000000 --- a/src/omniclaw/facilitator/exact.py +++ /dev/null @@ -1,198 +0,0 @@ -from __future__ import annotations - -import os -from collections.abc import Callable -from dataclasses import dataclass -from typing import Any - -from fastapi import FastAPI, HTTPException -from pydantic import BaseModel, ConfigDict, Field - -from omniclaw.facilitator.networks import resolve_exact_settlement_network_profile -from omniclaw.protocols.x402_compat import ( - get_signed_raw_transaction_bytes, - patch_x402_web3_compat, -) - -patch_x402_web3_compat() - -from x402 import x402Facilitator # noqa: E402 -from x402.mechanisms.evm.exact import register_exact_evm_facilitator # noqa: E402 -from x402.mechanisms.evm.signers import FacilitatorWeb3Signer # noqa: E402 -from x402.schemas import PaymentPayload, PaymentRequirements # noqa: E402 - - -def _env(name: str, default: str = "") -> str: - value = os.environ.get(name, "").strip() - return value or default - - -def _required_env(*names: str) -> str: - for name in names: - value = os.environ.get(name, "").strip() - if value: - return value - missing = ", ".join(names) - raise RuntimeError(f"Missing required environment variable. Set one of: {missing}") - - -def _normalize_tx_hash(tx_hash: Any) -> str: - value = tx_hash.hex() if hasattr(tx_hash, "hex") else str(tx_hash) - return value if value.startswith("0x") else f"0x{value}" - - -@dataclass(frozen=True) -class ExactFacilitatorConfig: - private_key: str - rpc_url: str - networks: tuple[str, ...] - network_profile: str | None = None - port: int = 4022 - host: str = "0.0.0.0" - title: str = "OmniClaw Exact Facilitator" - - -class CompatFacilitatorWeb3Signer(FacilitatorWeb3Signer): - """Handle eth-account and web3 compatibility differences in this runtime.""" - - def write_contract( - self, - address: str, - abi: list[dict[str, Any]], - function_name: str, - *args: Any, - ) -> str: - from web3 import Web3 - - contract = self._w3.eth.contract( - address=Web3.to_checksum_address(address), - abi=abi, - ) - func = getattr(contract.functions, function_name) - tx = func(*args).build_transaction( - { - "from": self._account.address, - "nonce": self._w3.eth.get_transaction_count(self._account.address), - "gas": 300000, - "gasPrice": self._w3.eth.gas_price, - } - ) - signed_tx = self._account.sign_transaction(tx) - tx_hash = self._w3.eth.send_raw_transaction(get_signed_raw_transaction_bytes(signed_tx)) - return _normalize_tx_hash(tx_hash) - - def send_transaction(self, to: str, data: bytes) -> str: - from web3 import Web3 - - tx = { - "from": self._account.address, - "to": Web3.to_checksum_address(to), - "data": data, - "nonce": self._w3.eth.get_transaction_count(self._account.address), - "gas": 300000, - "gasPrice": self._w3.eth.gas_price, - } - signed_tx = self._account.sign_transaction(tx) - tx_hash = self._w3.eth.send_raw_transaction(get_signed_raw_transaction_bytes(signed_tx)) - return _normalize_tx_hash(tx_hash) - - -class FacilitatorRequest(BaseModel): - model_config = ConfigDict(extra="forbid", populate_by_name=True) - - x402_version: int = Field(alias="x402Version") - payment_payload: dict[str, Any] = Field(alias="paymentPayload") - payment_requirements: dict[str, Any] = Field(alias="paymentRequirements") - - -def load_exact_facilitator_config_from_env() -> ExactFacilitatorConfig: - profile_name = _env( - "OMNICLAW_X402_FACILITATOR_NETWORK_PROFILE", - _env("OMNICLAW_NETWORK", "BASE-SEPOLIA"), - ) - profile = resolve_exact_settlement_network_profile(profile_name) - explicit_networks = tuple( - value.strip() - for value in _env("OMNICLAW_X402_FACILITATOR_NETWORKS", "").split(",") - if value.strip() - ) - networks = explicit_networks or (profile.caip2,) - rpc_url = _env("OMNICLAW_X402_FACILITATOR_RPC_URL", profile.default_rpc_url or "") - if not rpc_url: - raise RuntimeError( - "Missing OMNICLAW_X402_FACILITATOR_RPC_URL and no default RPC is known for " - f"{profile.label}" - ) - - return ExactFacilitatorConfig( - port=int(_env("OMNICLAW_X402_FACILITATOR_PORT", "4022")), - host=_env("OMNICLAW_X402_FACILITATOR_HOST", "0.0.0.0"), - rpc_url=rpc_url, - private_key=_required_env( - "OMNICLAW_X402_FACILITATOR_PRIVATE_KEY", - "OMNICLAW_PRIVATE_KEY", - ), - networks=networks, - network_profile=profile.label, - title=_env( - "OMNICLAW_X402_FACILITATOR_TITLE", - f"OmniClaw Exact Facilitator ({profile.label})", - ), - ) - - -def create_exact_facilitator_app( - config: ExactFacilitatorConfig, - *, - signer_factory: Callable[..., Any] = CompatFacilitatorWeb3Signer, - facilitator_factory: Callable[[], Any] = x402Facilitator, - register_facilitator: Callable[..., Any] = register_exact_evm_facilitator, -) -> FastAPI: - app = FastAPI(title=config.title) - - signer = signer_factory( - private_key=config.private_key, - rpc_url=config.rpc_url, - ) - facilitator = facilitator_factory() - register_facilitator( - facilitator, - signer=signer, - networks=list(config.networks), - ) - - app.state.omniclaw_exact_facilitator_config = config - app.state.omniclaw_exact_facilitator = facilitator - - @app.get("/supported") - async def supported() -> dict[str, Any]: - result = facilitator.get_supported() - if hasattr(result, "model_dump"): - return result.model_dump(by_alias=True, exclude_none=True) - return result - - @app.post("/verify") - async def verify(request: FacilitatorRequest) -> dict[str, Any]: - if request.x402_version != 2: - raise HTTPException(status_code=400, detail="Only x402Version=2 is supported") - - payload = PaymentPayload.model_validate(request.payment_payload) - requirements = PaymentRequirements.model_validate(request.payment_requirements) - result = await facilitator.verify(payload, requirements) - if hasattr(result, "model_dump"): - return result.model_dump(by_alias=True, exclude_none=True) - return result - - @app.post("/settle") - async def settle(request: FacilitatorRequest) -> dict[str, Any]: - if request.x402_version != 2: - raise HTTPException(status_code=400, detail="Only x402Version=2 is supported") - - payload = PaymentPayload.model_validate(request.payment_payload) - requirements = PaymentRequirements.model_validate(request.payment_requirements) - result = await facilitator.settle(payload, requirements) - if hasattr(result, "model_dump"): - return result.model_dump(by_alias=True, exclude_none=True) - return result - - return app diff --git a/src/omniclaw/facilitator/networks.py b/src/omniclaw/facilitator/networks.py deleted file mode 100644 index 8643b59..0000000 --- a/src/omniclaw/facilitator/networks.py +++ /dev/null @@ -1,121 +0,0 @@ -from __future__ import annotations - -from dataclasses import dataclass -from decimal import Decimal - -from x402.schemas import AssetAmount - -from omniclaw.core.cctp_constants import USDC_CONTRACTS -from omniclaw.core.types import Network, network_to_caip2, normalize_network - - -@dataclass(frozen=True) -class ExactSettlementNetworkProfile: - network: Network - caip2: str - label: str - default_rpc_url: str | None = None - explorer_base_url: str | None = None - default_asset_address: str | None = None - default_asset_name: str = "USDC" - default_asset_version: str = "2" - default_asset_decimals: int = 6 - - -_DEFAULT_PROFILE_METADATA: dict[Network, dict[str, str | None]] = { - Network.ETH: { - "default_rpc_url": "https://ethereum-rpc.publicnode.com", - "explorer_base_url": "https://etherscan.io/tx/", - }, - Network.ETH_SEPOLIA: { - "default_rpc_url": "https://ethereum-sepolia-rpc.publicnode.com", - "explorer_base_url": "https://sepolia.etherscan.io/tx/", - }, - Network.BASE: { - "default_rpc_url": "https://mainnet.base.org", - "explorer_base_url": "https://basescan.org/tx/", - }, - Network.BASE_SEPOLIA: { - "default_rpc_url": "https://sepolia.base.org", - "explorer_base_url": "https://sepolia.basescan.org/tx/", - }, - Network.OP: { - "default_rpc_url": "https://mainnet.optimism.io", - "explorer_base_url": "https://optimistic.etherscan.io/tx/", - }, - Network.OP_SEPOLIA: { - "default_rpc_url": "https://sepolia.optimism.io", - "explorer_base_url": "https://sepolia-optimism.etherscan.io/tx/", - }, - Network.ARB: { - "default_rpc_url": "https://arb1.arbitrum.io/rpc", - "explorer_base_url": "https://arbiscan.io/tx/", - }, - Network.ARB_SEPOLIA: { - "default_rpc_url": "https://sepolia-rollup.arbitrum.io/rpc", - "explorer_base_url": "https://sepolia.arbiscan.io/tx/", - }, - Network.UNI: { - "default_rpc_url": "https://mainnet.unichain.org", - "explorer_base_url": "https://uniscan.xyz/tx/", - }, - Network.UNI_SEPOLIA: { - "default_rpc_url": "https://sepolia.unichain.org", - "explorer_base_url": "https://sepolia.uniscan.xyz/tx/", - }, - Network.ARC_TESTNET: { - "default_rpc_url": "https://rpc.testnet.arc.network", - "explorer_base_url": "https://testnet.arcscan.app/tx/", - }, -} - - -def resolve_exact_settlement_network_profile( - network: Network | str | None, -) -> ExactSettlementNetworkProfile: - normalized = normalize_network(network or Network.BASE_SEPOLIA) - if normalized is None: - raise ValueError("Exact settlement network profile cannot be resolved from None") - if not normalized.is_evm(): - raise ValueError(f"Exact settlement only supports EVM networks, got: {normalized.value}") - - caip2 = network_to_caip2(normalized) - if not caip2: - raise ValueError(f"No CAIP-2 mapping available for exact settlement: {normalized.value}") - - metadata = _DEFAULT_PROFILE_METADATA.get(normalized, {}) - return ExactSettlementNetworkProfile( - network=normalized, - caip2=caip2, - label=normalized.value, - default_rpc_url=metadata.get("default_rpc_url"), - explorer_base_url=metadata.get("explorer_base_url"), - default_asset_address=USDC_CONTRACTS.get(normalized.value), - ) - - -def build_exact_asset_amount( - *, - profile: ExactSettlementNetworkProfile, - decimal_amount: float | str | Decimal, - network: str, -) -> AssetAmount | None: - if network != profile.caip2 or not profile.default_asset_address: - return None - - amount_decimal = Decimal(str(decimal_amount)) - scaled = amount_decimal * (Decimal(10) ** profile.default_asset_decimals) - if scaled != scaled.to_integral_value(): - raise ValueError( - f"Amount {decimal_amount} cannot be represented with " - f"{profile.default_asset_decimals} decimals" - ) - - return AssetAmount( - amount=str(int(scaled)), - asset=profile.default_asset_address, - extra={ - "name": profile.default_asset_name, - "version": profile.default_asset_version, - }, - ) diff --git a/src/omniclaw/onboarding.py b/src/omniclaw/onboarding.py index d465140..13dbcb7 100644 --- a/src/omniclaw/onboarding.py +++ b/src/omniclaw/onboarding.py @@ -795,8 +795,8 @@ def icon(ok: bool) -> str: for step in next_steps: print(f" - {step}") if status.get("can_sync_to_env"): - print("\n 💡 TIP: You have a saved Entity Secret but it's not in your environment.") - print(" Run: omniclaw setup # to sync it automatically") + print("\n TIP: You have a saved Entity Secret but it's not in your environment.") + print(" Export ENTITY_SECRET in the deployment environment before production use.") print() print("Ready to use." if status["ready"] else "Setup needs attention.") diff --git a/src/omniclaw/protocols/nanopayments/__init__.py b/src/omniclaw/protocols/nanopayments/__init__.py index 17082c3..ea22b6a 100644 --- a/src/omniclaw/protocols/nanopayments/__init__.py +++ b/src/omniclaw/protocols/nanopayments/__init__.py @@ -8,17 +8,11 @@ - EIP3009Signer: Cryptographic signing of payment authorizations - NanopaymentClient: Circle Gateway REST API wrapper - NanopaymentAdapter: Buyer-side payment execution - - GatewayMiddleware: Seller-side x402 payment gate - GatewayWalletManager: On-chain deposit/withdraw operations Usage: Buyer side: result = await nanopayment_adapter.pay_x402_url(url="https://api.provider.com/data") - - Seller side: - @app.get("/premium") - async def premium(payment=Depends(gateway.require("$0.001"))): - return {"data": "paid content", "paid_by": payment.payer} """ from omniclaw.protocols.nanopayments.constants import ( @@ -86,7 +80,6 @@ async def premium(payment=Depends(gateway.require("$0.001"))): EIP3009Authorization, GatewayBalance, NanopaymentResult, - PaymentInfo, PaymentPayload, PaymentPayloadInner, PaymentRequirements, @@ -105,12 +98,6 @@ async def premium(payment=Depends(gateway.require("$0.001"))): from omniclaw.protocols.nanopayments.wallet import GatewayWalletManager -from omniclaw.protocols.nanopayments.middleware import ( - GatewayMiddleware, - PaymentRequiredHTTPError, - parse_price, -) - from omniclaw.protocols.nanopayments.adapter import ( NanopaymentAdapter, NanopaymentProtocolAdapter, @@ -145,7 +132,6 @@ async def premium(payment=Depends(gateway.require("$0.001"))): "EIP3009Authorization", "GatewayBalance", "NanopaymentResult", - "PaymentInfo", "PaymentPayload", "PaymentPayloadInner", "PaymentRequirements", @@ -162,10 +148,6 @@ async def premium(payment=Depends(gateway.require("$0.001"))): "GatewayWalletManager", # Adapter "NanopaymentAdapter", - # Middleware - "GatewayMiddleware", - "PaymentRequiredHTTPError", - "parse_price", # Adapter wrapper "NanopaymentProtocolAdapter", # Exceptions diff --git a/src/omniclaw/protocols/nanopayments/adapter.py b/src/omniclaw/protocols/nanopayments/adapter.py index 379c74f..6eecaa6 100644 --- a/src/omniclaw/protocols/nanopayments/adapter.py +++ b/src/omniclaw/protocols/nanopayments/adapter.py @@ -87,6 +87,53 @@ def _is_success_status(status_code: int) -> bool: return status_code in _SUCCESS_STATUS_CODES +def _find_raw_gateway_kind(payment_required: dict[str, Any]) -> dict[str, Any] | None: + """Return the unmodified Gateway accept object advertised by the seller.""" + accepts = payment_required.get("accepts") + if not isinstance(accepts, list): + return None + for accept in accepts: + if not isinstance(accept, dict): + continue + extra = accept.get("extra") + if isinstance(extra, dict) and extra.get("name") == "GatewayWalletBatched": + return dict(accept) + return None + + +def _with_gateway_verifying_contract( + accepted: dict[str, Any], + verifying_contract: str, +) -> dict[str, Any]: + """Preserve seller-advertised Gateway metadata while filling the contract if absent.""" + result = dict(accepted) + extra = dict(result.get("extra") or {}) + if verifying_contract and not extra.get("verifyingContract"): + extra["verifyingContract"] = verifying_contract + result["extra"] = extra + return result + + +def _gateway_valid_before(accepted: dict[str, Any]) -> int: + extra = accepted.get("extra") if isinstance(accepted.get("extra"), dict) else {} + min_validity = _positive_int(extra.get("minValiditySeconds")) + max_timeout = _positive_int(accepted.get("maxTimeoutSeconds")) + window = max(min_validity or 0, 345600) + if min_validity: + window = min_validity + 60 + if max_timeout: + window = min(window, max_timeout) + return int(time.time()) + window + + +def _positive_int(value: Any) -> int | None: + try: + parsed = int(str(value)) + except (TypeError, ValueError): + return None + return parsed if parsed > 0 else None + + # ============================================================================= # CIRCUIT BREAKER # ============================================================================= @@ -303,6 +350,7 @@ def _sign( requirements: PaymentRequirementsKind, resource: ResourceInfoType | None = None, amount_atomic: int | None = None, + valid_before: int | None = None, ) -> PaymentPayload: """Sign a payment using the EIP-3009 signer.""" if amount_atomic is None: @@ -310,6 +358,7 @@ def _sign( payload = self._signer.sign_transfer_with_authorization( requirements=requirements, amount_atomic=amount_atomic, + valid_before=valid_before, ) # Attach resource info if provided if resource is not None: @@ -325,7 +374,7 @@ def _sign( @staticmethod def _encode_payment_signature_header( payload: PaymentPayload, - accepted: PaymentRequirementsKind, + accepted: PaymentRequirementsKind | dict[str, Any], ) -> str: """ Encode the x402 v2 payment header sent back to the seller. @@ -336,7 +385,9 @@ def _encode_payment_signature_header( accepting the paid retry. """ payment_payload = payload.to_dict() - payment_payload["accepted"] = accepted.to_dict() + payment_payload["accepted"] = ( + accepted.to_dict() if hasattr(accepted, "to_dict") else dict(accepted) + ) return base64.b64encode(json.dumps(payment_payload).encode("utf-8")).decode("ascii") # ------------------------------------------------------------------------- @@ -411,7 +462,7 @@ async def pay_x402_url( # Step 2: Not a payment response if initial_resp.status_code != 402: return NanopaymentResult( - success=True, + success=_is_success_status(initial_resp.status_code), payer="", seller="", transaction="", @@ -477,6 +528,7 @@ async def pay_x402_url( raise UnsupportedSchemeError( scheme=str([k.extra.name for k in requirements.accepts]), ) + raw_gateway_kind = _find_raw_gateway_kind(req_data) or gateway_kind.to_dict() # Step 5: Get verifying contract if missing verifying_contract = gateway_kind.extra.verifying_contract @@ -484,6 +536,7 @@ async def pay_x402_url( verifying_contract = await self._client.get_verifying_contract( gateway_kind.network, ) + raw_gateway_kind = _with_gateway_verifying_contract(raw_gateway_kind, verifying_contract) # Build updated requirements with verifying contract from omniclaw.protocols.nanopayments.types import ( @@ -522,12 +575,13 @@ async def pay_x402_url( payload = self._sign( requirements=updated_kind, resource=resource, + valid_before=_gateway_valid_before(raw_gateway_kind), ) # Step 8: Retry with payment header payment_sig_header = self._encode_payment_signature_header( payload=payload, - accepted=updated_kind, + accepted=raw_gateway_kind, ) retry_headers = dict(headers) diff --git a/src/omniclaw/protocols/nanopayments/client.py b/src/omniclaw/protocols/nanopayments/client.py index 4cc5b3f..cade4f1 100644 --- a/src/omniclaw/protocols/nanopayments/client.py +++ b/src/omniclaw/protocols/nanopayments/client.py @@ -150,7 +150,7 @@ def _caip2_to_circle_domain_id(caip2: str) -> int: domain = CAIP2_TO_CIRCLE_DOMAIN.get(caip2) if domain is not None: return domain - return 0 + raise UnsupportedNetworkError(network=caip2) def _to_int(value: Any) -> int: @@ -599,9 +599,17 @@ async def verify( Raises: GatewayAPIError: On HTTP errors. """ + circle_payload = _convert_payload_for_circle(payload.to_dict()) + circle_requirements = _convert_requirements_for_circle(requirements.to_dict()) + selected_requirement = ( + circle_requirements["accepts"][0] + if circle_requirements.get("accepts") + else circle_requirements + ) + circle_payload["accepted"] = selected_requirement body: dict[str, Any] = { - "paymentPayload": _convert_payload_for_circle(payload.to_dict()), - "paymentRequirements": _convert_requirements_for_circle(requirements.to_dict()), + "paymentPayload": circle_payload, + "paymentRequirements": selected_requirement, } async with NanopaymentHTTPClient( @@ -759,7 +767,7 @@ async def check_balance( "token": "USDC", "sources": [ { - "network": network, + "domain": expected_domain, "depositor": address, } ], @@ -818,6 +826,18 @@ async def check_balance( ) +def _select_gateway_balance(balances: list[Any], domain_id: int) -> dict[str, Any]: + for balance in balances: + if not isinstance(balance, dict): + continue + try: + if int(str(balance.get("domain"))) == domain_id: + return balance + except (TypeError, ValueError): + continue + return {} + + # ============================================================================= # INTERNAL HELPERS # ============================================================================= diff --git a/src/omniclaw/protocols/nanopayments/exceptions.py b/src/omniclaw/protocols/nanopayments/exceptions.py index 0c37b07..ed7ec43 100644 --- a/src/omniclaw/protocols/nanopayments/exceptions.py +++ b/src/omniclaw/protocols/nanopayments/exceptions.py @@ -502,7 +502,7 @@ def __init__(self, reason: str) -> None: class MiddlewareError(NanopaymentError): - """Base for GatewayMiddleware errors.""" + """Base for x402 seller-gate errors kept for compatibility.""" pass @@ -522,9 +522,6 @@ def __init__(self, price: str) -> None: class PaymentRequiredError(MiddlewareError): """ Raised when a payment is required but not provided. - - This is used internally by GatewayMiddleware to trigger - the 402 response. """ def __init__(self, requirements_body: dict[str, Any]) -> None: diff --git a/src/omniclaw/protocols/nanopayments/middleware.py b/src/omniclaw/protocols/nanopayments/middleware.py deleted file mode 100644 index 2aa724d..0000000 --- a/src/omniclaw/protocols/nanopayments/middleware.py +++ /dev/null @@ -1,777 +0,0 @@ -""" -GatewayMiddleware: Seller-side x402 payment gate for FastAPI/Starlette. - -The Python equivalent of Circle's createGatewayMiddleware(). -Sellers use this to protect their endpoints with x402 payments. - -Usage: - @app.get("/premium") - async def premium(payment=Depends(gateway.require("$0.001"))): - return {"data": "paid content", "paid_by": payment.payer} - -The 402 response structure (x402 v2): - { - "x402Version": 2, - "accepts": [{ - "scheme": "exact", - "network": "eip155:5042002", - "asset": "0xUSDC", - "amount": "1000", # atomic units - "maxTimeoutSeconds": 345600, - "payTo": "0xSeller", - "extra": { - "name": "GatewayWalletBatched", - "version": "1", - "verifyingContract": "0xGateway" - } - }] - } -""" - -from __future__ import annotations - -import base64 -import inspect -import json -from dataclasses import dataclass -from typing import Any - -from omniclaw.protocols.nanopayments import ( - MAX_TIMEOUT_SECONDS, - X402_VERSION, -) -from omniclaw.protocols.nanopayments.client import NanopaymentClient -from omniclaw.protocols.nanopayments.exceptions import ( - InvalidPriceError, - NoNetworksAvailableError, -) -from omniclaw.protocols.nanopayments.types import ( - PaymentInfo, - PaymentPayload, - PaymentRequirements, - SupportedKind, -) - -# ============================================================================= -# SETTLEMENT RESPONSE (x402 v2 PAYMENT-RESPONSE header format) -# ============================================================================= - - -@dataclass -class SettlementResponse: - """ - x402 v2 SettlementResponse format for PAYMENT-RESPONSE header. - - Per x402 v2 spec, this header is required on ALL responses (success AND failure) - from paid endpoints. The header is base64-encoded JSON with: - {success, transaction, network, payer, errorReason?} - """ - - success: bool - transaction: str - network: str - payer: str - error_reason: str | None = None - - def to_base64_header(self) -> str: - """Encode as base64 for the PAYMENT-RESPONSE header.""" - return base64.b64encode(json.dumps(self.to_dict()).encode()).decode() - - def to_dict(self) -> dict: - """Convert to dict for JSON serialization.""" - d = { - "success": self.success, - "transaction": self.transaction, - "network": self.network, - "payer": self.payer, - } - if self.error_reason: - d["errorReason"] = self.error_reason - return d - - -# ============================================================================= -# PRICE PARSING -# ============================================================================= - - -def parse_price(price_str: str) -> int: - """ - Parse a price string to USDC atomic units (6 decimals). - - Accepts: - "$0.001" -> 1000 - "0.001" -> 1000 - "$1" -> 1000000 - "1000000" -> 1000000 (atomic) - "1000" -> 1000000 (dollars) - - Returns: - Amount in USDC atomic units (int). - - Raises: - InvalidPriceError: If the price string cannot be parsed. - """ - if not price_str: - raise InvalidPriceError(price=price_str) - - original = price_str.strip() - - # Remove dollar sign - numeric = original[1:].strip() if original.startswith("$") else original - - # Check if it's a decimal (has a decimal point) - if "." in numeric: - # It's a dollar amount with decimals — convert to atomic - try: - from decimal import Decimal, InvalidOperation - - value = Decimal(numeric) - return int(value * Decimal(1_000_000)) - except (ValueError, InvalidOperation, ArithmeticError): - raise InvalidPriceError(price=price_str) from None - - # It's a plain integer — treat as atomic units if >= 1M, - # otherwise as whole dollars multiplied by 1M - try: - value = int(numeric) - except ValueError: - raise InvalidPriceError(price=price_str) from None - - if value >= 1_000_000: - return value - return value * 1_000_000 - - -# ============================================================================= -# GATEWAY MIDDLEWARE -# ============================================================================= - - -class GatewayMiddleware: - """ - FastAPI/Starlette middleware for x402 payment gating. - - Sellers use this to protect their endpoints. - When a buyer requests without payment: returns 402. - When a buyer requests with valid payment: settles and serves content. - - Args: - seller_address: EOA address that receives payments. - nanopayment_client: NanopaymentClient for fetching supported networks. - supported_kinds: Pre-fetched supported payment kinds. If None, fetches automatically. - auto_fetch_networks: If True, fetches networks on first request if not provided. - facilitator: Optional custom facilitator. If None, uses nanopayment_client (Circle). - """ - - def __init__( - self, - seller_address: str, - nanopayment_client: NanopaymentClient, - supported_kinds: list[SupportedKind] | None = None, - auto_fetch_networks: bool = True, - facilitator: Any = None, - ) -> None: - # Validate seller_address is a valid EVM address - if not seller_address: - raise ValueError("seller_address is required") - if not seller_address.startswith("0x"): - raise ValueError("seller_address must be an EVM address (starts with 0x)") - if len(seller_address) != 42: - raise ValueError( - f"seller_address must be 42 characters (42 hex chars), got {len(seller_address)}" - ) - # Validate hex characters - try: - int(seller_address[2:], 16) - except ValueError: - raise ValueError("seller_address contains invalid hex characters") from None - - self._seller_address = seller_address.lower() # Normalize to lowercase - self._client = nanopayment_client - self._supported_kinds: list[SupportedKind] | None = supported_kinds - self._auto_fetch = auto_fetch_networks - self._facilitator = facilitator - self._facilitator_name = facilitator.name if facilitator else "circle" - - def _uses_gateway_batched_scheme(self) -> bool: - """Return True when this middleware should advertise Circle's GatewayWalletBatched.""" - return not self._facilitator or str(self._facilitator_name).lower() == "circle" - - # ------------------------------------------------------------------------- - # Supported networks management - # ------------------------------------------------------------------------- - - async def _get_supported_kinds(self) -> list[SupportedKind]: - """Get supported payment kinds, fetching if needed.""" - if self._supported_kinds is not None: - return self._supported_kinds - - # If using custom facilitator (not Circle), try to get from facilitator - if self._facilitator: - try: - facilitator_networks = await self._facilitator.get_supported_networks() - # Convert facilitator networks to SupportedKind format - supported = [] - for net in facilitator_networks: - network = net.get("network") or net.get("chainId") - if network: - supported.append( - SupportedKind( - x402_version=2, - scheme="exact", - network=network, - extra={ - "verifyingContract": net.get("verifyingContract", "0x"), - "usdcAddress": net.get( - "usdcAddress", "0x036CbD53842c5426634e7929541eC2318f3dCF7e" - ), - }, - ) - ) - if supported: - self._supported_kinds = supported - return self._supported_kinds - except Exception: - pass - - # Fallback: use default networks for non-Circle facilitators - if self._facilitator and not self._supported_kinds: - # Use Base Sepolia and Ethereum as defaults for other facilitators - self._supported_kinds = [ - SupportedKind( - x402_version=2, - scheme="exact", - network="eip155:84532", # Base Sepolia - extra={ - "verifyingContract": "0xfab807B4563D2292a72a3e53F5CcF5E3B7eD86d4", - "usdcAddress": "0x036CbD53842c5426634e7929541eC2318f3dCF7e", - }, - ), - SupportedKind( - x402_version=2, - scheme="exact", - network="eip155:1", # Ethereum Mainnet - extra={ - "verifyingContract": "0x097707E2b3cD7C6D6fC8E2D3B5F5cC5E7F7E7E7E7", - "usdcAddress": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", - }, - ), - ] - return self._supported_kinds - - # Use Circle client - if self._client: - self._supported_kinds = await self._client.get_supported(force_refresh=True) - if not self._supported_kinds: - raise NoNetworksAvailableError() - return self._supported_kinds - - return [] - - # ------------------------------------------------------------------------- - # Accepts array builder - # ------------------------------------------------------------------------- - - def _build_accepts_array( - self, - price_atomic: int, - kinds: list[SupportedKind] | None = None, - ) -> list[dict[str, Any]]: - """ - Build the accepts array for a 402 response. - - Args: - price_atomic: Price in USDC atomic units. - kinds: Optional pre-fetched kinds. If None, fetches synchronously. - - For each supported network, creates an entry with: - - scheme: "exact" - - network: CAIP-2 - - asset: USDC address - - amount: price in atomic units - - maxTimeoutSeconds: 345600 (4 days) - - payTo: seller address - - extra: GatewayWalletBatched metadata - """ - if kinds is None: - kinds = self._supported_kinds - if kinds is None: - return [] # No networks available - - accepts = [] - use_gateway_batched = self._uses_gateway_batched_scheme() - for kind in kinds: - verifying_contract = kind.verifying_contract - usdc_address = kind.usdc_address - - if not usdc_address: - continue - - accept: dict[str, Any] = { - "scheme": "exact", - "network": kind.network, - "asset": usdc_address, - "amount": str(price_atomic), - "maxTimeoutSeconds": MAX_TIMEOUT_SECONDS, - "payTo": self._seller_address, - } - if use_gateway_batched: - if not verifying_contract: - continue - accept["extra"] = { - "name": "GatewayWalletBatched", - "version": "1", - "verifyingContract": verifying_contract, - } - accepts.append(accept) - - return accepts - - # ------------------------------------------------------------------------- - # 402 response builder - # ------------------------------------------------------------------------- - - async def _build_402_response( - self, - price_usd: str, - *, - resource_url: str = "", - method: str = "GET", - ) -> dict[str, Any]: - """ - Build the x402 v2 402 response body. - - Returns: - Dict with x402Version and accepts array. - """ - price_atomic = parse_price(price_usd) - - create_accepts = getattr(self._facilitator, "create_accepts", None) - if self._facilitator and inspect.iscoroutinefunction(create_accepts): - accepts = await create_accepts( - resource_url=resource_url, - method=method, - price=price_usd, - server_wallet_address=self._seller_address, - ) - return { - "x402Version": X402_VERSION, - "accepts": accepts, - } - - # Get supported kinds - handle both Circle and other facilitators - kinds = None - if self._supported_kinds is None and self._facilitator: - # Try to get from facilitator - kinds = await self._get_supported_kinds() - elif self._supported_kinds: - kinds = self._supported_kinds - elif self._client: - # Try to get from Circle client - kinds = await self._get_supported_kinds() - - accepts = self._build_accepts_array(price_atomic, kinds) - - return { - "x402Version": X402_VERSION, - "accepts": accepts, - } - - # ------------------------------------------------------------------------- - # Payment handling - # ------------------------------------------------------------------------- - - def _parse_payment_signature( - self, - header_value: str, - ) -> PaymentPayload: - """ - Parse and validate the PAYMENT-SIGNATURE header. - - Args: - header_value: The base64-encoded JSON PaymentPayload. - - Returns: - Parsed PaymentPayload. - - Raises: - ValueError: If parsing fails. - """ - try: - decoded = base64.b64decode(header_value) - data = json.loads(decoded) - if int(data.get("x402Version", X402_VERSION)) == X402_VERSION and not data.get( - "accepted" - ): - raise ValueError("Missing accepted requirements in PAYMENT-SIGNATURE payload") - return PaymentPayload.from_dict(data) - except Exception as exc: - raise ValueError(f"Failed to parse PAYMENT-SIGNATURE: {exc}") from exc - - def _encode_requirements( - self, - body: dict[str, Any], - ) -> str: - """Encode requirements dict as base64 for the PAYMENT-REQUIRED header.""" - return base64.b64encode(json.dumps(body).encode()).decode() - - # ------------------------------------------------------------------------- - # Public handler - # ------------------------------------------------------------------------- - - async def handle( - self, - request_headers: dict[str, str], - price_usd: str, - *, - resource_url: str = "", - method: str = "GET", - ) -> PaymentInfo: - """ - Handle payment for a request. - - Checks for PAYMENT-SIGNATURE header. If present, verifies and settles. - If absent, raises HTTPException(402). - - Args: - request_headers: Request headers dict. - price_usd: Price in USD string (e.g. "$0.001"). - - Returns: - PaymentInfo if payment verified and settled. - - Raises: - HTTPException(402): If payment is missing or invalid. - The detail dict contains the requirements for payment. - """ - # Check for PAYMENT-SIGNATURE header - sig_header = request_headers.get("payment-signature") or request_headers.get( - "PAYMENT-SIGNATURE" - ) - - if not sig_header: - # Build 402 response - body = await self._build_402_response( - price_usd, - resource_url=resource_url, - method=method, - ) - header_value = self._encode_requirements(body) - raise PaymentRequiredHTTPError( - status_code=402, - detail=body, - headers={"PAYMENT-REQUIRED": header_value}, - ) - - # Parse and verify payment - try: - payload = self._parse_payment_signature(sig_header) - except ValueError as exc: - raise PaymentRequiredHTTPError( - status_code=402, - detail={"error": str(exc)}, - headers={}, - ) from None - - # Build requirements from the payment payload. - # Circle Gateway uses GatewayWalletBatched metadata; external facilitators use standard exact. - gateway_kind = None - facilitator_requirements: dict[str, Any] | None = None - if payload.payload.authorization: - auth = payload.payload.authorization - expected_amount = str(parse_price(price_usd)) - if str(auth.value) != expected_amount: - raise PaymentRequiredHTTPError( - status_code=402, - detail={ - "error": ( - f"Amount mismatch. Expected {expected_amount} atomic units, " - f"got {auth.value}." - ) - }, - headers={}, - ) - accepted = payload.accepted.to_dict() if payload.accepted else None - if not accepted: - raise PaymentRequiredHTTPError( - status_code=402, - detail={"error": "Missing accepted requirements in PAYMENT-SIGNATURE payload"}, - headers={}, - ) - if str(accepted.get("network", "")) != payload.network: - raise PaymentRequiredHTTPError( - status_code=402, - detail={"error": "Accepted requirements network does not match payload"}, - headers={}, - ) - if str(accepted.get("amount", "")) != expected_amount: - raise PaymentRequiredHTTPError( - status_code=402, - detail={"error": "Accepted requirements amount does not match price"}, - headers={}, - ) - if str(accepted.get("payTo", "")).lower() != self._seller_address.lower(): - raise PaymentRequiredHTTPError( - status_code=402, - detail={"error": "Accepted requirements payTo does not match seller"}, - headers={}, - ) - # Build requirements from payload - from omniclaw.protocols.nanopayments.types import ( - PaymentRequirementsExtra, - PaymentRequirementsKind, - ) - - # Get supported kinds and find matching network - supported_kinds = await self._get_supported_kinds() - - # Find the kind matching the payment's network - matching_kind = None - verifying_contract = None - usdc_address = None - - for kind in supported_kinds: - if kind.network == payload.network: - matching_kind = kind - verifying_contract = kind.verifying_contract - usdc_address = kind.usdc_address - break - - # If no supported kinds at all, we can't process this payment - if not supported_kinds: - raise PaymentRequiredHTTPError( - status_code=502, - detail={"error": "No supported payment networks available"}, - headers={}, - ) - - if matching_kind is None: - raise PaymentRequiredHTTPError( - status_code=402, - detail={"error": f"Unsupported payment network: {payload.network}"}, - headers={}, - ) - - if not usdc_address: - raise PaymentRequiredHTTPError( - status_code=502, - detail={"error": f"Missing contract addresses for network {payload.network}"}, - headers={}, - ) - if str(accepted.get("asset", "")).lower() != usdc_address.lower(): - raise PaymentRequiredHTTPError( - status_code=402, - detail={"error": "Accepted requirements asset does not match network"}, - headers={}, - ) - - if self._uses_gateway_batched_scheme(): - if not verifying_contract: - raise PaymentRequiredHTTPError( - status_code=502, - detail={ - "error": f"Missing verifying contract for network {payload.network}" - }, - headers={}, - ) - accepted_extra = accepted.get("extra") or {} - if str(accepted_extra.get("verifyingContract", "")).lower() != ( - verifying_contract.lower() - ): - raise PaymentRequiredHTTPError( - status_code=402, - detail={ - "error": "Accepted requirements verifying contract does not match network" - }, - headers={}, - ) - gateway_kind = PaymentRequirementsKind( - scheme="exact", - network=payload.network, - asset=usdc_address, - amount=auth.value, - max_timeout_seconds=MAX_TIMEOUT_SECONDS, - pay_to=self._seller_address, - extra=PaymentRequirementsExtra( - name="GatewayWalletBatched", - version="1", - verifying_contract=verifying_contract, - ), - ) - else: - facilitator_requirements = { - "x402Version": X402_VERSION, - "accepts": [ - { - "scheme": "exact", - "network": payload.network, - "asset": usdc_address, - "amount": auth.value, - "maxTimeoutSeconds": MAX_TIMEOUT_SECONDS, - "payTo": self._seller_address, - } - ], - } - - if gateway_kind is None and facilitator_requirements is None: - raise PaymentRequiredHTTPError( - status_code=402, - detail={"error": "Missing authorization in PAYMENT-SIGNATURE payload"}, - headers={}, - ) - - requirements = None - if gateway_kind is not None: - requirements = PaymentRequirements( - x402_version=X402_VERSION, - accepts=(gateway_kind,), - ) - - # Settle the payment - use facilitator if provided, otherwise use Circle client - try: - if self._facilitator: - payload_dict = payload.to_dict() - req_dict = ( - facilitator_requirements - if facilitator_requirements is not None - else requirements.to_dict() - ) - settle_resp = await self._facilitator.settle(payload_dict, req_dict) - settle_success = settle_resp.success - settle_payer = settle_resp.payer - settle_tx = settle_resp.transaction - else: - settle_resp = await self._client.settle( - payload=payload, - requirements=requirements, - ) - settle_success = settle_resp.success - settle_payer = settle_resp.payer - settle_tx = settle_resp.transaction - except Exception as exc: - raise PaymentRequiredHTTPError( - status_code=402, - detail={"error": f"Settlement failed: {exc}"}, - headers={}, - ) from None - - return PaymentInfo( - verified=settle_success, - payer=settle_payer or payload.payload.authorization.from_address, - amount=payload.payload.authorization.value, - network=payload.network, - transaction=settle_tx, - ) - - # ------------------------------------------------------------------------- - # FastAPI dependency - # ------------------------------------------------------------------------- - - def require(self, price: str): - """ - Returns a FastAPI dependency for route protection. - - Usage: - @app.get("/premium") - async def premium(payment=Depends(gateway.require("$0.001"))): - return {"data": "paid content", "paid_by": payment.payer} - - Note: - This requires a Request object to be in scope. Use the `handle` - method directly for more control. - - IMPORTANT: Per x402 v2 spec, you MUST include the PAYMENT-RESPONSE header - in your response. Use build_payment_response_header() or payment_response_headers() - to get the header value. - """ - from fastapi import HTTPException, Request - - async def dependency(request: Request) -> PaymentInfo: - headers = dict(request.headers) - try: - return await self.handle( - headers, - price, - resource_url=str(request.url), - method=request.method, - ) - except PaymentRequiredHTTPError as exc: - raise HTTPException( - status_code=exc.status_code, - detail=exc.detail, - headers=exc.headers, - ) from None - - return dependency - - # ------------------------------------------------------------------------- - # PAYMENT-RESPONSE header helpers (x402 v2 spec) - # ------------------------------------------------------------------------- - - def build_payment_response_header(self, payment_info: PaymentInfo) -> str: - """ - Build PAYMENT-RESPONSE header value from PaymentInfo. - - Per x402 v2 spec, this header is required on ALL responses (success AND failure) - from paid endpoints. The header is base64-encoded JSON. - - Args: - payment_info: The PaymentInfo returned by handle() or require() - - Returns: - Base64-encoded JSON string for the PAYMENT-RESPONSE header. - """ - return SettlementResponse( - success=payment_info.verified, - transaction=payment_info.transaction or "", - network=payment_info.network, - payer=payment_info.payer, - ).to_base64_header() - - def payment_response_headers(self, payment_info: PaymentInfo) -> dict[str, str]: - """ - Get headers dict including PAYMENT-RESPONSE for route handlers. - - Convenience method that returns a dict with the PAYMENT-RESPONSE header - already set. Merge this with your response headers. - - Usage: - @app.get("/premium") - async def premium(payment=Depends(gateway.require("$0.001"))): - return JSONResponse( - {"data": "premium data"}, - headers=gateway.payment_response_headers(payment) - ) - - Args: - payment_info: The PaymentInfo returned by handle() or require() - - Returns: - Dict with "PAYMENT-RESPONSE" key. - """ - return {"PAYMENT-RESPONSE": self.build_payment_response_header(payment_info)} - - -# ============================================================================= -# HTTP EXCEPTION HELPER -# ============================================================================= - - -class PaymentRequiredHTTPError(Exception): - """ - Raised internally to trigger a 402 response. - - Not a real HTTPException — caught by the FastAPI dependency wrapper. - """ - - def __init__( - self, - status_code: int, - detail: dict[str, Any], - headers: dict[str, str], - ) -> None: - self.status_code = status_code - self.detail = detail - self.headers = headers - super().__init__(str(detail)) diff --git a/src/omniclaw/protocols/nanopayments/types.py b/src/omniclaw/protocols/nanopayments/types.py index 379e06c..66973d7 100644 --- a/src/omniclaw/protocols/nanopayments/types.py +++ b/src/omniclaw/protocols/nanopayments/types.py @@ -666,54 +666,6 @@ def to_dict(self) -> dict[str, Any]: } -# ============================================================================= -# SELLER PAYMENT INFO -# ============================================================================= - - -@dataclass(frozen=True) -class PaymentInfo: - """ - Payment information for a seller receiving payment. - - Attached to requests in @agent.sell() decorated functions - via agent.current_payment(). - """ - - verified: bool - """True if the payment was verified and settled.""" - - payer: str - """Buyer's address.""" - - amount: str - """Amount in USDC atomic units.""" - - network: str - """CAIP-2 network identifier.""" - - transaction: str | None - """Batch reference from settlement.""" - - @property - def amount_decimal(self) -> str: - """Amount as decimal USDC string.""" - from decimal import Decimal - - return str(Decimal(self.amount) / Decimal("1000000")) - - def to_dict(self) -> dict[str, Any]: - """Convert to dict.""" - return { - "verified": self.verified, - "payer": self.payer, - "amount": self.amount, - "amount_decimal": self.amount_decimal, - "network": self.network, - "transaction": self.transaction, - } - - # ============================================================================= # WALLET OPERATION RESULTS # ============================================================================= diff --git a/src/omniclaw/protocols/nanopayments/wallet.py b/src/omniclaw/protocols/nanopayments/wallet.py index ca89ab9..f62a0d6 100644 --- a/src/omniclaw/protocols/nanopayments/wallet.py +++ b/src/omniclaw/protocols/nanopayments/wallet.py @@ -21,7 +21,6 @@ from __future__ import annotations -import asyncio import logging import re from typing import Any @@ -220,20 +219,27 @@ def __init__( usdc_address: str | None = None, ) -> None: self._signer = EIP3009Signer(private_key) - self._address = self._signer.address + self._address = web3.Web3.to_checksum_address(self._signer.address) self._network = network chain_id = int(network.split(":")[1]) self._chain_id = chain_id self._w3 = web3.Web3(web3.HTTPProvider(rpc_url)) - # Fix for POA chains (Polygon, etc.) - use legacy buildTransaction - from web3.middleware import geth_poa_middleware + # Fix for POA chains across web3.py versions. + try: + from web3.middleware import ExtraDataToPOAMiddleware + + self._w3.middleware_onion.inject(ExtraDataToPOAMiddleware, layer=0) + except ImportError: + from web3.middleware import geth_poa_middleware - self._w3.middleware_onion.inject(geth_poa_middleware, layer=0) + self._w3.middleware_onion.inject(geth_poa_middleware, layer=0) self._client = nanopayment_client - self._gateway_address = gateway_address - self._usdc_address = usdc_address + self._gateway_address = ( + web3.Web3.to_checksum_address(gateway_address) if gateway_address else None + ) + self._usdc_address = web3.Web3.to_checksum_address(usdc_address) if usdc_address else None self._gateway_contract: web3.Contract | None = None @property @@ -254,16 +260,19 @@ async def _resolve_gateway_address(self) -> str: """Resolve Gateway Wallet contract address for current network.""" if self._gateway_address: return self._gateway_address - return await self._client.get_verifying_contract(self._network) + return web3.Web3.to_checksum_address( + await self._client.get_verifying_contract(self._network) + ) async def _resolve_usdc_address(self) -> str: """Resolve USDC token contract address for current network.""" if self._usdc_address: return self._usdc_address - return await self._client.get_usdc_address(self._network) + return web3.Web3.to_checksum_address(await self._client.get_usdc_address(self._network)) def _get_gateway_contract(self, gateway_address: str) -> web3.Contract: """Get or create Gateway contract instance.""" + gateway_address = web3.Web3.to_checksum_address(gateway_address) if ( self._gateway_contract is None or self._gateway_contract.address.lower() != gateway_address.lower() @@ -296,6 +305,7 @@ def _encode_gateway_call(self, gateway: web3.Contract, fn_name: str, args: list[ def _usdc_contract(self, address: str) -> web3.Contract: """Get USDC contract instance.""" + address = web3.Web3.to_checksum_address(address) return self._w3.eth.contract(address=address, abi=_USDC_ABI) def _decimal_to_atomic(self, amount_decimal: str) -> int: @@ -321,6 +331,7 @@ def _build_tx( self, to: str, data: str, value: int = 0, nonce: int | None = None ) -> dict[str, Any]: """Build a base transaction dict.""" + to = web3.Web3.to_checksum_address(to) if nonce is None: nonce = self._w3.eth.get_transaction_count(self._address) tx: dict[str, Any] = { @@ -471,7 +482,9 @@ async def deposit( approve_func = usdc.functions.approve(gateway_address, amount) approve_tx = self._build_tx( to=usdc_address, - data=approve_func.build_transaction({"gas": 50000})["data"], + data=approve_func.build_transaction({"from": self._address, "gas": 50000})[ + "data" + ], ) approval_tx_hash = self._sign_and_send( approve_tx, @@ -489,7 +502,7 @@ async def deposit( deposit_func = gateway.functions.deposit(usdc_address, amount) deposit_tx = self._build_tx( to=gateway_address, - data=deposit_func.build_transaction({"gas": 100000})["data"], + data=deposit_func.build_transaction({"from": self._address, "gas": 100000})["data"], value=0, ) @@ -905,29 +918,22 @@ def estimate_gas_cost_eth(self) -> str: def check_gas_reserve(self) -> tuple[bool, str]: """ - Check if the wallet has enough USDC for gas on Arc network. - - For Arc (and other chains that use USDC as gas), check USDC balance - instead of ETH for deposit transaction gas. + Check if the wallet has enough native gas balance for the deposit transactions. Returns: Tuple of (has_sufficient_gas, message). - has_sufficient_gas is True if USDC balance >= 2x estimated gas cost. + has_sufficient_gas is True if native gas balance >= 2x estimated gas cost. """ - # For Arc - use USDC as gas token try: - usdc_addr = asyncio.get_event_loop().run_until_complete(self._resolve_usdc_address()) - usdc = self._usdc_contract(usdc_addr) - usdc_balance = usdc.functions.balanceOf(self._address).call() - - # Estimate gas cost in USDC (approximate) - gas_cost_wei = self.estimate_gas_cost_wei() - gas_cost_usdc = gas_cost_wei / 1e6 # Convert to USDC - - balance_usdc = usdc_balance / 1e6 - has_sufficient = usdc_balance >= (gas_cost_wei * 2) - - msg = f"USDC balance: {balance_usdc:.6f}, Gas cost: {gas_cost_usdc:.6f}" + balance_wei = self.get_gas_balance_wei() + required_wei = self.estimate_gas_cost_wei() * 2 + has_sufficient = balance_wei >= required_wei + + msg = ( + "native gas balance: " + f"{self._w3.from_wei(balance_wei, 'ether')}, " + f"required reserve: {self._w3.from_wei(required_wei, 'ether')}" + ) return has_sufficient, msg except Exception as e: diff --git a/src/omniclaw/protocols/x402.py b/src/omniclaw/protocols/x402.py index 05a2106..1989a17 100644 --- a/src/omniclaw/protocols/x402.py +++ b/src/omniclaw/protocols/x402.py @@ -628,13 +628,7 @@ def _resolve_exact_balance_rpc_url(self, selected_network: Network | None) -> st if selected_network is None: return str(config_rpc_url) if config_rpc_url else None - try: - from omniclaw.facilitator.networks import resolve_exact_settlement_network_profile - - profile = resolve_exact_settlement_network_profile(selected_network) - return profile.default_rpc_url - except Exception: - return str(config_rpc_url) if config_rpc_url else None + return str(config_rpc_url) if config_rpc_url else None def _check_direct_exact_balance(self, selected_requirements: Any) -> dict[str, Any]: """ diff --git a/src/omniclaw/seller/__init__.py b/src/omniclaw/seller/__init__.py deleted file mode 100644 index 03b85ff..0000000 --- a/src/omniclaw/seller/__init__.py +++ /dev/null @@ -1,68 +0,0 @@ -""" -OmniClaw Seller SDK. - -x402 payment integration for sellers. - -Usage: - from omniclaw.seller import Seller, create_seller - - seller = create_seller( - seller_address="0x742d...", - name="Weather API", - ) - - seller.add_endpoint("/weather", "$0.001", "Current weather") - seller.serve(port=4023) -""" - -# Main Seller class -# Circle Gateway Facilitator -from omniclaw.seller.facilitator import ( - CircleGatewayFacilitator, - SettleResult, - VerifyResult, -) - -# Multi-facilitator support (Coinbase, OrderN, RBX, Thirdweb) -from omniclaw.seller.facilitator_generic import ( - SUPPORTED_FACILITATORS, - BaseFacilitator, - CoinbaseFacilitator, - OrderNFacilitator, - RBXFacilitator, - ThirdwebFacilitator, - create_facilitator, -) -from omniclaw.seller.seller import ( - Endpoint, - PaymentRecord, - PaymentScheme, - PaymentStatus, - Seller, - SellerConfig, - create_seller, -) - -__all__ = [ - # Main classes - "Seller", - "create_seller", - # Types - "PaymentScheme", - "PaymentStatus", - "PaymentRecord", - "Endpoint", - "SellerConfig", - # Circle Gateway Facilitator - "CircleGatewayFacilitator", - "VerifyResult", - "SettleResult", - # Multi-facilitator - "CoinbaseFacilitator", - "OrderNFacilitator", - "RBXFacilitator", - "ThirdwebFacilitator", - "BaseFacilitator", - "create_facilitator", - "SUPPORTED_FACILITATORS", -] diff --git a/src/omniclaw/seller/facilitator.py b/src/omniclaw/seller/facilitator.py deleted file mode 100644 index 6a9310d..0000000 --- a/src/omniclaw/seller/facilitator.py +++ /dev/null @@ -1,370 +0,0 @@ -""" -Circle Gateway Facilitator for x402 Payments. - -This module provides direct integration with Circle's Gateway API to verify -and settle x402 payments without using third-party facilitators. - -Circle's Gateway provides: -- POST /gateway/v1/x402/verify - Verify payment payload -- POST /gateway/v1/x402/settle - Settle payment - -Usage: - from omniclaw.seller.facilitator import CircleGatewayFacilitator - - facilitator = CircleGatewayFacilitator( - circle_api_key="YOUR_API_KEY", - environment="testnet", # or "mainnet" - ) - - # Verify payment before serving resource - result = await facilitator.verify(payment_payload, payment_requirements) - - # Settle payment after serving resource - result = await facilitator.settle(payment_payload, payment_requirements) -""" - -from __future__ import annotations - -import os -from dataclasses import dataclass -from typing import Any - -import httpx - -from omniclaw.core.exceptions import ProtocolError - -GATEWAY_API_TESTNET = "https://gateway-api-testnet.circle.com" -GATEWAY_API_MAINNET = "https://gateway-api.circle.com" - -SETTLE_ENDPOINT = "/v1/x402/settle" -VERIFY_ENDPOINT = "/v1/x402/verify" -SUPPORTED_ENDPOINT = "/v1/x402/supported" - - -@dataclass -class FacilitatorConfig: - """Configuration for the Circle Gateway facilitator.""" - - circle_api_key: str - """Circle API key for authentication.""" - - environment: str = "testnet" - """Environment: 'testnet' or 'mainnet'.""" - - timeout: float = 30.0 - """HTTP request timeout in seconds.""" - - @property - def base_url(self) -> str: - """Get the API base URL based on environment.""" - if self.environment == "mainnet": - return GATEWAY_API_MAINNET - return GATEWAY_API_TESTNET - - @property - def headers(self) -> dict[str, str]: - """Get headers for API requests.""" - return { - "Content-Type": "application/json", - "Authorization": f"Bearer {self.circle_api_key}", - } - - -@dataclass -class VerifyResult: - """Result of payment verification.""" - - is_valid: bool - """Whether the payment payload passed all validation checks.""" - - payer: str | None - """The payer address extracted from the payment payload.""" - - invalid_reason: str | None = None - """Reason for validation failure (if invalid).""" - - -@dataclass -class SettleResult: - """Result of payment settlement.""" - - success: bool - """Whether the settlement was successful.""" - - transaction: str | None - """Transaction UUID on success, empty string on failure.""" - - network: str | None - """CAIP-2 network identifier.""" - - error_reason: str | None = None - """Error code if settlement failed.""" - - payer: str | None = None - """The sender address.""" - - -class CircleGatewayFacilitator: - """ - Circle Gateway facilitator for x402 payments. - - This facilitator uses Circle's Gateway API directly to verify and settle - payments. No third-party facilitator needed. - - Benefits: - - Direct integration with Circle's infrastructure - - No additional fees beyond Circle's standard pricing - - Supports both verify (read-only) and settle (execution) - - Works with EIP-3009 USDC payments - """ - - def __init__( - self, - circle_api_key: str | None = None, - environment: str = "testnet", - timeout: float = 30.0, - base_url: str | None = None, - ): - """ - Initialize the Circle Gateway facilitator. - - Args: - circle_api_key: Circle API key. Falls back to CIRCLE_API_KEY env var. - environment: 'testnet' or 'mainnet' - timeout: HTTP request timeout - base_url: Custom base URL (overrides environment) - """ - api_key = circle_api_key or os.environ.get("CIRCLE_API_KEY") - if not api_key: - raise ValueError("circle_api_key is required") - - self._config = FacilitatorConfig( - circle_api_key=api_key, - environment=environment, - timeout=timeout, - ) - self._base_url_override = base_url - - self._client = httpx.AsyncClient(timeout=timeout) - - @property - def base_url(self) -> str: - """Get the API base URL.""" - return self._base_url_override or self._config.base_url - - @property - def name(self) -> str: - """Get the facilitator name.""" - return "circle" - - @property - def environment(self) -> str: - """Get the environment.""" - return self._config.environment - - async def verify( - self, - payment_payload: dict[str, Any], - payment_requirements: dict[str, Any], - ) -> VerifyResult: - """ - Verify an x402 payment payload. - - This performs read-only validation: - - Scheme validation - - Network validation - - Token validation - - Signature validation - - Temporal constraints (validBefore, validAfter) - - Address and amount matching - - Note: This does NOT check balance or nonce - those happen at settle time. - - Args: - payment_payload: The payment payload from the client's PAYMENT-SIGNATURE header - payment_requirements: The payment requirements from the 402 response - - Returns: - VerifyResult with validation status - """ - url = f"{self.base_url}{VERIFY_ENDPOINT}" - - body = { - "paymentPayload": payment_payload, - "paymentRequirements": payment_requirements, - } - - try: - response = await self._client.post( - url, - json=body, - headers=self._config.headers, - ) - - if response.status_code == 400: - data = response.json() - return VerifyResult( - is_valid=False, - payer=data.get("payer"), - invalid_reason=data.get("invalidReason", "invalid_request"), - ) - - response.raise_for_status() - data = response.json() - - return VerifyResult( - is_valid=data.get("isValid", False), - payer=data.get("payer"), - invalid_reason=data.get("invalidReason"), - ) - - except httpx.TimeoutException as e: - raise ProtocolError( - message=f"Facilitator verify timeout: {e}", - protocol="x402", - details={"url": url}, - ) from e - except httpx.HTTPStatusError as e: - raise ProtocolError( - message=f"Facilitator verify failed: {e.response.status_code}", - protocol="x402", - details={"status": e.response.status_code, "body": e.response.text[:500]}, - ) from e - except Exception as e: - raise ProtocolError( - message=f"Facilitator verify error: {e}", - protocol="x402", - ) from e - - async def settle( - self, - payment_payload: dict[str, Any], - payment_requirements: dict[str, Any], - ) -> SettleResult: - """ - Settle an x402 payment. - - This submits the payment to Circle Gateway for settlement: - - Verifies the authorization - - Locks the sender's balance - - Queues for batch processing - - Args: - payment_payload: The payment payload from the client's PAYMENT-SIGNATURE header - payment_requirements: The payment requirements from the 402 response - - Returns: - SettleResult with settlement status - """ - url = f"{self.base_url}{SETTLE_ENDPOINT}" - - body = { - "paymentPayload": payment_payload, - "paymentRequirements": payment_requirements, - } - - try: - response = await self._client.post( - url, - json=body, - headers=self._config.headers, - ) - - if response.status_code == 400: - data = response.json() - return SettleResult( - success=False, - transaction=data.get("transaction", ""), - network=data.get("network"), - error_reason=data.get("errorReason", "invalid_request"), - payer=data.get("payer"), - ) - - response.raise_for_status() - data = response.json() - - return SettleResult( - success=data.get("success", False), - transaction=data.get("transaction"), - network=data.get("network"), - error_reason=data.get("errorReason"), - payer=data.get("payer"), - ) - - except httpx.TimeoutException as e: - raise ProtocolError( - message=f"Facilitator settle timeout: {e}", - protocol="x402", - details={"url": url}, - ) from e - except httpx.HTTPStatusError as e: - raise ProtocolError( - message=f"Facilitator settle failed: {e.response.status_code}", - protocol="x402", - details={"status": e.response.status_code, "body": e.response.text[:500]}, - ) from e - except Exception as e: - raise ProtocolError( - message=f"Facilitator settle error: {e}", - protocol="x402", - ) from e - - async def get_supported_networks(self) -> list[dict[str, Any]]: - """ - Get supported payment schemes and networks. - - Returns: - List of supported network configurations - """ - url = f"{self.base_url}{SUPPORTED_ENDPOINT}" - - try: - response = await self._client.get(url, headers=self._config.headers) - response.raise_for_status() - data = response.json() - return data.get("supportedNetworks", []) - - except Exception as e: - raise ProtocolError( - message=f"Failed to get supported networks: {e}", - protocol="x402", - ) from e - - async def close(self) -> None: - """Close the HTTP client.""" - await self._client.aclose() - - async def __aenter__(self) -> CircleGatewayFacilitator: - """Async context manager entry.""" - return self - - async def __aexit__(self, exc_type, exc_val, exc_tb) -> None: - """Async context manager exit.""" - await self.close() - - -def create_facilitator( - circle_api_key: str | None = None, - environment: str = "testnet", - **kwargs, -) -> CircleGatewayFacilitator: - """ - Factory function to create a facilitator. - - Supports: circle, coinbase, ordern, rbx, thirdweb - - Args: - circle_api_key: Circle API key (or use api_key=) - environment: 'testnet' or 'mainnet' - **kwargs: Additional arguments - - Returns: - Facilitator instance - """ - # Import here to avoid circular imports - from omniclaw.seller.facilitator_generic import create_facilitator as _create - - # Support both circle_api_key and api_key - api_key = circle_api_key or kwargs.pop("api_key", None) - - return _create(provider="circle", api_key=api_key, environment=environment, **kwargs) diff --git a/src/omniclaw/seller/facilitator_generic.py b/src/omniclaw/seller/facilitator_generic.py deleted file mode 100644 index 974f7ee..0000000 --- a/src/omniclaw/seller/facilitator_generic.py +++ /dev/null @@ -1,927 +0,0 @@ -""" -Generic x402 Facilitator interface. - -Supports the top facilitators: -1. Circle Gateway - Circle's native facilitator -2. Coinbase CDP - Coinbase's facilitator -3. OrderN - https://ordern.ai (x402 facilitator) -4. RBX - https://rbx.io (x402 facilitator) -5. Thirdweb - thirdweb's facilitator - -Usage: - from omniclaw.seller import create_facilitator - - # Circle's facilitator - facilitator = create_facilitator("circle", api_key="...") - - # Coinbase's facilitator - facilitator = create_facilitator("coinbase", api_key="...") - - # OrderN facilitator - facilitator = create_facilitator("ordern", api_key="...") - - # Auto-detect from provider string - facilitator = create_facilitator("circle") -""" - -from __future__ import annotations - -from abc import ABC, abstractmethod -from decimal import Decimal -from typing import Any -from urllib.parse import urlencode - -from omniclaw.seller.facilitator import ( - CircleGatewayFacilitator as CircleImpl, -) -from omniclaw.seller.facilitator import ( - SettleResult, - VerifyResult, -) - -# Re-export Circle's implementation -CircleGatewayFacilitator = CircleImpl - - -async def _fetch_supported_networks( - client: Any, - base_url: str, - headers: dict[str, str], - candidate_paths: list[str], -) -> list[dict[str, Any]]: - """Fetch supported networks from known provider endpoints.""" - last_error: Exception | None = None - for path in candidate_paths: - try: - response = await client.get(f"{base_url}{path}", headers=headers) - response.raise_for_status() - data = response.json() - if isinstance(data, dict): - if isinstance(data.get("supportedNetworks"), list): - return data["supportedNetworks"] - if isinstance(data.get("networks"), list): - return data["networks"] - if isinstance(data.get("kinds"), list): - return data["kinds"] - if isinstance(data.get("data"), list): - return data["data"] - if isinstance(data, list): - return data - last_error = ValueError(f"Unsupported /supported schema from {path}") - except Exception as exc: - last_error = exc - continue - if last_error is not None: - raise RuntimeError(f"Unable to fetch supported networks: {last_error}") from last_error - raise RuntimeError("Unable to fetch supported networks: no candidate paths configured") - - -class BaseFacilitator(ABC): - """ - Abstract base class for x402 facilitators. - - Facilitators handle payment verification and settlement on behalf of sellers. - """ - - @property - @abstractmethod - def name(self) -> str: - """Facilitator name.""" - pass - - @property - @abstractmethod - def base_url(self) -> str: - """API base URL.""" - pass - - @property - @abstractmethod - def environment(self) -> str: - """Environment (testnet/mainnet).""" - pass - - @abstractmethod - async def verify( - self, - payment_payload: dict[str, Any], - payment_requirements: dict[str, Any], - ) -> VerifyResult: - """ - Verify payment payload (read-only). - - Args: - payment_payload: The payment payload from client - payment_requirements: The payment requirements from 402 response - - Returns: - VerifyResult with validation status - """ - pass - - @abstractmethod - async def settle( - self, - payment_payload: dict[str, Any], - payment_requirements: dict[str, Any], - ) -> SettleResult: - """ - Settle payment (execute on-chain). - - Args: - payment_payload: The payment payload from client - payment_requirements: The payment requirements from 402 response - - Returns: - SettleResult with settlement status - """ - pass - - @abstractmethod - async def get_supported_networks(self) -> list[dict[str, Any]]: - """Get supported networks.""" - pass - - @abstractmethod - async def close(self) -> None: - """Close HTTP client.""" - pass - - -def _parse_price_to_atomic(price: str) -> str: - value = str(price).strip() - if not value: - raise ValueError("price is required") - if value.startswith("$"): - value = value[1:].strip() - decimal_value = Decimal(value) - scaled = decimal_value * Decimal(1_000_000) - if scaled != scaled.to_integral_value(): - raise ValueError(f"price {price!r} cannot be represented with 6 decimals") - return str(int(scaled)) - - -# ============================================================================= -# Coinbase Facilitator -# ============================================================================= - - -class CoinbaseFacilitator(BaseFacilitator): - """ - Coinbase CDP x402 Facilitator. - - https://docs.cdp.coinbase.com/x402/docs/facilitator - """ - - COINBASE_TESTNET = "https://api.cdp.coinbase.com/platform" - COINBASE_MAINNET = "https://api.cdp.coinbase.com/platform" - - def __init__( - self, - api_key: str, - environment: str = "testnet", - timeout: float = 30.0, - ): - import httpx - - self._api_key = api_key - self._environment = environment - self._timeout = timeout - self._base_url = ( - self.COINBASE_TESTNET if environment == "testnet" else self.COINBASE_MAINNET - ) - self._client = httpx.AsyncClient(timeout=timeout) - - @property - def name(self) -> str: - return "coinbase" - - @property - def base_url(self) -> str: - return self._base_url - - @property - def environment(self) -> str: - return self._environment - - async def verify(self, payment_payload: dict, payment_requirements: dict) -> VerifyResult: - url = f"{self._base_url}/v2/x402/verify" - headers = {"Authorization": f"Bearer {self._api_key}", "Content-Type": "application/json"} - try: - r = await self._client.post( - url, - json={ - "paymentPayload": payment_payload, - "paymentRequirements": payment_requirements, - }, - headers=headers, - ) - r.raise_for_status() - data = r.json() - data = data.get("result", data) if isinstance(data, dict) else {} - return VerifyResult( - is_valid=data.get("isValid", False), - payer=data.get("payer"), - invalid_reason=data.get("invalidReason"), - ) - except Exception as e: - return VerifyResult(is_valid=False, payer=None, invalid_reason=str(e)) - - async def settle(self, payment_payload: dict, payment_requirements: dict) -> SettleResult: - url = f"{self._base_url}/v2/x402/settle" - headers = {"Authorization": f"Bearer {self._api_key}", "Content-Type": "application/json"} - try: - r = await self._client.post( - url, - json={ - "paymentPayload": payment_payload, - "paymentRequirements": payment_requirements, - }, - headers=headers, - ) - r.raise_for_status() - data = r.json() - data = data.get("result", data) if isinstance(data, dict) else {} - return SettleResult( - success=data.get("success", False), - transaction=data.get("transaction"), - network=data.get("network"), - error_reason=data.get("errorReason"), - payer=data.get("payer"), - ) - except Exception as e: - return SettleResult( - success=False, transaction=None, network=None, error_reason=str(e), payer=None - ) - - async def get_supported_networks(self) -> list: - headers = {"Authorization": f"Bearer {self._api_key}", "Accept": "application/json"} - return await _fetch_supported_networks( - client=self._client, - base_url=self._base_url, - headers=headers, - candidate_paths=["/v2/x402/supported", "/v1/x402/supported", "/x402/supported"], - ) - - async def close(self): - await self._client.aclose() - - -# ============================================================================= -# OrderN Facilitator (https://ordern.ai) -# ============================================================================= - - -class OrderNFacilitator(BaseFacilitator): - """ - OrderN x402 Facilitator. - - https://ordern.ai - """ - - ORDERN_TESTNET = "https://api.testnet.ordern.ai" - ORDERN_MAINNET = "https://api.ordern.ai" - - def __init__(self, api_key: str, environment: str = "testnet", timeout: float = 30.0): - import httpx - - self._api_key = api_key - self._environment = environment - self._timeout = timeout - self._base_url = self.ORDERN_TESTNET if environment == "testnet" else self.ORDERN_MAINNET - self._client = httpx.AsyncClient(timeout=timeout) - - @property - def name(self) -> str: - return "ordern" - - @property - def base_url(self) -> str: - return self._base_url - - @property - def environment(self) -> str: - return self._environment - - async def verify(self, payment_payload: dict, payment_requirements: dict) -> VerifyResult: - url = f"{self._base_url}/v1/x402/verify" - headers = {"Authorization": f"Bearer {self._api_key}", "Content-Type": "application/json"} - try: - r = await self._client.post( - url, - json={ - "paymentPayload": payment_payload, - "paymentRequirements": payment_requirements, - }, - headers=headers, - ) - r.raise_for_status() - data = r.json() - data = data.get("result", data) if isinstance(data, dict) else {} - return VerifyResult( - is_valid=data.get("isValid", False), - payer=data.get("payer"), - invalid_reason=data.get("invalidReason"), - ) - except Exception as e: - return VerifyResult(is_valid=False, payer=None, invalid_reason=str(e)) - - async def settle(self, payment_payload: dict, payment_requirements: dict) -> SettleResult: - url = f"{self._base_url}/v1/x402/settle" - headers = {"Authorization": f"Bearer {self._api_key}", "Content-Type": "application/json"} - try: - r = await self._client.post( - url, - json={ - "paymentPayload": payment_payload, - "paymentRequirements": payment_requirements, - }, - headers=headers, - ) - r.raise_for_status() - data = r.json() - data = data.get("result", data) if isinstance(data, dict) else {} - return SettleResult( - success=data.get("success", False), - transaction=data.get("transaction"), - network=data.get("network"), - error_reason=data.get("errorReason"), - payer=data.get("payer"), - ) - except Exception as e: - return SettleResult( - success=False, transaction=None, network=None, error_reason=str(e), payer=None - ) - - async def get_supported_networks(self) -> list: - headers = {"Authorization": f"Bearer {self._api_key}", "Accept": "application/json"} - return await _fetch_supported_networks( - client=self._client, - base_url=self._base_url, - headers=headers, - candidate_paths=["/v1/x402/supported", "/x402/supported", "/api/v1/x402/supported"], - ) - - async def close(self): - await self._client.aclose() - - -# ============================================================================= -# RBX Facilitator (https://rbx.io) -# ============================================================================= - - -class RBXFacilitator(BaseFacilitator): - """ - RBX x402 Facilitator. - - https://rbx.io - """ - - RBX_TESTNET = "https://api.testnet.rbx.io" - RBX_MAINNET = "https://api.rbx.io" - - def __init__(self, api_key: str, environment: str = "testnet", timeout: float = 30.0): - import httpx - - self._api_key = api_key - self._environment = environment - self._timeout = timeout - self._base_url = self.RBX_TESTNET if environment == "testnet" else self.RBX_MAINNET - self._client = httpx.AsyncClient(timeout=timeout) - - @property - def name(self) -> str: - return "rbx" - - @property - def base_url(self) -> str: - return self._base_url - - @property - def environment(self) -> str: - return self._environment - - async def verify(self, payment_payload: dict, payment_requirements: dict) -> VerifyResult: - url = f"{self._base_url}/x402/verify" - headers = {"Authorization": f"Bearer {self._api_key}", "Content-Type": "application/json"} - try: - r = await self._client.post( - url, - json={ - "paymentPayload": payment_payload, - "paymentRequirements": payment_requirements, - }, - headers=headers, - ) - r.raise_for_status() - data = r.json() - data = data.get("result", data) if isinstance(data, dict) else {} - return VerifyResult( - is_valid=data.get("isValid", False), - payer=data.get("payer"), - invalid_reason=data.get("invalidReason"), - ) - except Exception as e: - return VerifyResult(is_valid=False, payer=None, invalid_reason=str(e)) - - async def settle(self, payment_payload: dict, payment_requirements: dict) -> SettleResult: - url = f"{self._base_url}/x402/settle" - headers = {"Authorization": f"Bearer {self._api_key}", "Content-Type": "application/json"} - try: - r = await self._client.post( - url, - json={ - "paymentPayload": payment_payload, - "paymentRequirements": payment_requirements, - }, - headers=headers, - ) - r.raise_for_status() - data = r.json() - data = data.get("result", data) if isinstance(data, dict) else {} - return SettleResult( - success=data.get("success", False), - transaction=data.get("transaction"), - network=data.get("network"), - error_reason=data.get("errorReason"), - payer=data.get("payer"), - ) - except Exception as e: - return SettleResult( - success=False, transaction=None, network=None, error_reason=str(e), payer=None - ) - - async def get_supported_networks(self) -> list: - headers = {"Authorization": f"Bearer {self._api_key}", "Accept": "application/json"} - return await _fetch_supported_networks( - client=self._client, - base_url=self._base_url, - headers=headers, - candidate_paths=["/x402/supported", "/v1/x402/supported", "/api/v1/x402/supported"], - ) - - async def close(self): - await self._client.aclose() - - -# ============================================================================= -# Thirdweb Facilitator -# ============================================================================= - - -class ThirdwebFacilitator(BaseFacilitator): - """ - Thirdweb x402 Facilitator using the public HTTP API. - - https://portal.thirdweb.com/reference#tag/x402 - """ - - THIRDWEB_API = "https://api.thirdweb.com" - - def __init__( - self, - api_key: str, - environment: str = "testnet", - timeout: float = 30.0, - server_wallet_address: str | None = None, - default_network: str | None = None, - ): - import os - - import httpx - - self._api_key = api_key - self._environment = environment - self._timeout = timeout - self._server_wallet_address = ( - server_wallet_address or os.environ.get("THIRDWEB_SERVER_WALLET_ADDRESS") or "" - ).strip() - self._default_network = ( - default_network - or os.environ.get("THIRDWEB_X402_NETWORK") - or os.environ.get("OMNICLAW_X402_NETWORK") - or "base-sepolia" - ).strip() - self._base_url = self.THIRDWEB_API - self._client = httpx.AsyncClient(timeout=timeout) - - @property - def name(self) -> str: - return "thirdweb" - - @property - def base_url(self) -> str: - return self._base_url - - @property - def environment(self) -> str: - return self._environment - - async def create_accepts( - self, - *, - resource_url: str, - method: str = "GET", - network: str | None = None, - price: str, - server_wallet_address: str | None = None, - ) -> list[dict[str, Any]]: - """Create x402 accepts through Thirdweb's public HTTP API.""" - wallet_address = (server_wallet_address or self._server_wallet_address).strip() - if not wallet_address: - raise ValueError( - "THIRDWEB_SERVER_WALLET_ADDRESS is required to create Thirdweb x402 accepts" - ) - - url = f"{self._base_url}/v1/payments/x402/accepts" - headers = {"x-secret-key": self._api_key, "Content-Type": "application/json"} - response = await self._client.post( - url, - json={ - "resourceUrl": resource_url, - "method": method.upper(), - "network": network or self._default_network, - "price": price, - "serverWalletAddress": wallet_address, - }, - headers=headers, - ) - response.raise_for_status() - data = response.json() - data = data.get("result", data) if isinstance(data, dict) else {} - accepts = data.get("accepts", data if isinstance(data, list) else []) - if not isinstance(accepts, list): - raise RuntimeError("Thirdweb accepts response did not contain an accepts array") - return accepts - - async def fetch_with_payment( - self, - *, - url: str, - from_address: str, - method: str = "GET", - chain_id: str | None = None, - max_value: str | None = None, - asset: str | None = None, - headers: dict[str, str] | None = None, - body: Any = None, - ) -> dict[str, Any]: - """Proxy/pay an x402 URL through Thirdweb's public fetch API.""" - query = { - "url": url, - "from": from_address, - "method": method.upper(), - } - if chain_id: - query["chainId"] = chain_id - if max_value: - query["maxValue"] = max_value - if asset: - query["asset"] = asset - - request_headers = {"x-secret-key": self._api_key} - if headers: - request_headers.update(headers) - response = await self._client.post( - f"{self._base_url}/v1/payments/x402/fetch?{urlencode(query)}", - headers=request_headers, - json=body if isinstance(body, dict) else None, - content=None if isinstance(body, dict) else body, - ) - response.raise_for_status() - data = response.json() - return data.get("result", data) if isinstance(data, dict) else {"result": data} - - async def discover_resources(self, **query: Any) -> dict[str, Any]: - """Read Thirdweb x402 discovery resources.""" - params = {key: value for key, value in query.items() if value is not None} - suffix = f"?{urlencode(params)}" if params else "" - response = await self._client.get( - f"{self._base_url}/v1/payments/x402/discovery/resources{suffix}", - headers={"x-secret-key": self._api_key, "Accept": "application/json"}, - ) - response.raise_for_status() - data = response.json() - return data.get("result", data) if isinstance(data, dict) else {"result": data} - - async def verify(self, payment_payload: dict, payment_requirements: dict) -> VerifyResult: - url = f"{self._base_url}/v1/payments/x402/verify" - headers = {"x-secret-key": self._api_key, "Content-Type": "application/json"} - try: - r = await self._client.post( - url, - json={ - "paymentPayload": payment_payload, - "paymentRequirements": payment_requirements, - }, - headers=headers, - ) - r.raise_for_status() - data = r.json() - data = data.get("result", data) if isinstance(data, dict) else {} - return VerifyResult( - is_valid=data.get("isValid", False), - payer=data.get("payer"), - invalid_reason=data.get("invalidReason"), - ) - except Exception as e: - return VerifyResult(is_valid=False, payer=None, invalid_reason=str(e)) - - async def settle(self, payment_payload: dict, payment_requirements: dict) -> SettleResult: - url = f"{self._base_url}/v1/payments/x402/settle" - headers = {"x-secret-key": self._api_key, "Content-Type": "application/json"} - try: - r = await self._client.post( - url, - json={ - "paymentPayload": payment_payload, - "paymentRequirements": payment_requirements, - "waitUntil": "confirmed", - }, - headers=headers, - ) - r.raise_for_status() - data = r.json() - data = data.get("result", data) if isinstance(data, dict) else {} - return SettleResult( - success=data.get("success", False), - transaction=data.get("transaction"), - network=data.get("network"), - error_reason=data.get("errorReason"), - payer=data.get("payer"), - ) - except Exception as e: - return SettleResult( - success=False, transaction=None, network=None, error_reason=str(e), payer=None - ) - - async def get_supported_networks(self) -> list: - headers = {"x-secret-key": self._api_key, "Accept": "application/json"} - return await _fetch_supported_networks( - client=self._client, - base_url=self._base_url, - headers=headers, - candidate_paths=[ - "/v1/payments/x402/supported", - "/v1/payments/x402/accepts", - ], - ) - - async def close(self): - await self._client.aclose() - - -class OmniClawExactFacilitator(BaseFacilitator): - """ - OmniClaw self-hosted exact x402 facilitator. - - Use this for vendor SDK integrations that want to monetize routes with - `client.sell(..., facilitator="omniclaw")` while running their own - OmniClaw exact facilitator for verify/settle. - """ - - def __init__( - self, - api_key: str | None = None, - environment: str = "testnet", - timeout: float = 30.0, - base_url: str | None = None, - network_profile: str | None = None, - network: str | None = None, - asset: str | None = None, - name: str = "omniclaw", - ): - import os - - import httpx - - from omniclaw.facilitator.networks import resolve_exact_settlement_network_profile - - profile = resolve_exact_settlement_network_profile( - network_profile - or os.environ.get("OMNICLAW_X402_EXACT_NETWORK_PROFILE") - or os.environ.get("OMNICLAW_X402_FACILITATOR_NETWORK_PROFILE") - or os.environ.get("OMNICLAW_NETWORK") - or "BASE-SEPOLIA" - ) - self._environment = environment - self._name = name - self._base_url = ( - base_url - or os.environ.get("OMNICLAW_X402_SELF_HOSTED_FACILITATOR_URL") - or os.environ.get("OMNICLAW_X402_EXACT_FACILITATOR_URL") - or "http://127.0.0.1:4022" - ).rstrip("/") - self._network = network or profile.caip2 - self._asset = asset or profile.default_asset_address - if not self._asset: - raise ValueError(f"No exact settlement asset configured for {profile.label}") - self._asset_name = profile.default_asset_name - self._asset_version = profile.default_asset_version - self._client = httpx.AsyncClient(timeout=timeout) - - @property - def name(self) -> str: - return self._name - - @property - def base_url(self) -> str: - return self._base_url - - @property - def environment(self) -> str: - return self._environment - - async def create_accepts( - self, - *, - resource_url: str, - method: str = "GET", - network: str | None = None, - price: str, - server_wallet_address: str | None = None, - ) -> list[dict[str, Any]]: - if not server_wallet_address: - raise ValueError("server_wallet_address is required") - return [ - { - "scheme": "exact", - "network": network or self._network, - "asset": self._asset, - "amount": _parse_price_to_atomic(price), - "payTo": server_wallet_address, - "maxTimeoutSeconds": 300, - "extra": { - "name": self._asset_name, - "version": self._asset_version, - }, - } - ] - - async def verify(self, payment_payload: dict, payment_requirements: dict) -> VerifyResult: - try: - response = await self._client.post( - f"{self._base_url}/verify", - json={ - "x402Version": 2, - "paymentPayload": payment_payload, - "paymentRequirements": payment_requirements, - }, - ) - response.raise_for_status() - data = response.json() - return VerifyResult( - is_valid=data.get("isValid", data.get("is_valid", False)), - payer=data.get("payer"), - invalid_reason=data.get("invalidReason") or data.get("invalid_reason"), - ) - except Exception as exc: - return VerifyResult(is_valid=False, payer=None, invalid_reason=str(exc)) - - async def settle(self, payment_payload: dict, payment_requirements: dict) -> SettleResult: - try: - response = await self._client.post( - f"{self._base_url}/settle", - json={ - "x402Version": 2, - "paymentPayload": payment_payload, - "paymentRequirements": payment_requirements, - }, - ) - response.raise_for_status() - data = response.json() - return SettleResult( - success=data.get("success", False), - transaction=data.get("transaction"), - network=data.get("network"), - error_reason=data.get("errorReason") or data.get("error_reason"), - payer=data.get("payer"), - ) - except Exception as exc: - return SettleResult( - success=False, - transaction=None, - network=None, - error_reason=str(exc), - payer=None, - ) - - async def get_supported_networks(self) -> list[dict[str, Any]]: - response = await self._client.get(f"{self._base_url}/supported") - response.raise_for_status() - data = response.json() - if isinstance(data, dict) and isinstance(data.get("kinds"), list): - return data["kinds"] - if isinstance(data, dict) and isinstance(data.get("supported"), list): - return data["supported"] - if isinstance(data, list): - return data - return [ - { - "x402Version": 2, - "scheme": "exact", - "network": self._network, - "extra": {"usdcAddress": self._asset}, - } - ] - - async def close(self): - await self._client.aclose() - - -# ============================================================================= -# Factory Function -# ============================================================================= - - -SUPPORTED_FACILITATORS = { - "circle": CircleGatewayFacilitator, - "coinbase": CoinbaseFacilitator, - "ordern": OrderNFacilitator, - "rbx": RBXFacilitator, - "thirdweb": ThirdwebFacilitator, - "omniclaw": OmniClawExactFacilitator, - "selfhosted": OmniClawExactFacilitator, - "self-hosted": OmniClawExactFacilitator, -} - - -def create_facilitator( - provider: str = "circle", - api_key: str | None = None, - environment: str = "testnet", - **kwargs, -) -> BaseFacilitator: - """ - Factory to create a facilitator. - - Supports managed and self-hosted facilitators: - - circle: Circle Gateway (https://circle.com) - - coinbase: Coinbase CDP (https://coinbase.com) - - ordern: OrderN (https://ordern.ai) - - rbx: RBX (https://rbx.io) - - thirdweb: Thirdweb (https://thirdweb.com) - - omniclaw: OmniClaw self-hosted exact facilitator - - Args: - provider: Facilitator name ("circle", "coinbase", "ordern", "rbx", "thirdweb", "omniclaw") - api_key: API key for the facilitator - environment: "testnet" or "mainnet" - **kwargs: Additional options - - Returns: - BaseFacilitator instance - """ - import os - - provider = provider.lower() - - if provider in {"omniclaw", "selfhosted", "self-hosted"}: - return OmniClawExactFacilitator( - api_key=api_key, - environment=environment, - name=provider, - **kwargs, - ) - - if api_key is not None: - key = api_key - elif provider == "thirdweb": - key = ( - os.environ.get("THIRDWEB_SECRET_KEY") - or os.environ.get("FACILITATOR_API_KEY") - or os.environ.get("CIRCLE_API_KEY") - ) - else: - key = os.environ.get("FACILITATOR_API_KEY") or os.environ.get("CIRCLE_API_KEY") - - if not key: - raise ValueError("api_key is required") - - if provider not in SUPPORTED_FACILITATORS: - raise ValueError( - f"Unknown facilitator: {provider}. Use: {', '.join(SUPPORTED_FACILITATORS.keys())}" - ) - - facilitator_class = SUPPORTED_FACILITATORS[provider] - - # CircleGatewayFacilitator uses circle_api_key, others use api_key - if provider == "circle": - return facilitator_class(circle_api_key=key, environment=environment, **kwargs) - return facilitator_class(api_key=key, environment=environment, **kwargs) - - -__all__ = [ - "BaseFacilitator", - "CircleGatewayFacilitator", - "CoinbaseFacilitator", - "OrderNFacilitator", - "RBXFacilitator", - "ThirdwebFacilitator", - "OmniClawExactFacilitator", - "VerifyResult", - "SettleResult", - "create_facilitator", - "SUPPORTED_FACILITATORS", -] diff --git a/src/omniclaw/seller/seller.py b/src/omniclaw/seller/seller.py deleted file mode 100644 index 322ac98..0000000 --- a/src/omniclaw/seller/seller.py +++ /dev/null @@ -1,1429 +0,0 @@ -""" -OmniClaw Seller SDK - Complete Economic Infrastructure for Sellers. - -This provides everything needed to accept payments: -- Multiple endpoint configuration -- Both payment methods (basic x402 + Circle) -- Webhook notifications -- Transaction history - -Usage: - from omniclaw.seller import Seller, create_seller - - seller = create_seller( - seller_address="0x...", - name="Weather API", - ) - - # Add protected endpoints - seller.protect("/weather", "$0.001", "Current weather") - seller.protect("/forecast", "$0.01", "7-day forecast") - - # Start server - seller.serve(port=4023) -""" - -import asyncio -import base64 -import hashlib -import json -import logging -import os -import re -import time -from collections.abc import Callable -from dataclasses import dataclass, field -from datetime import datetime -from decimal import Decimal, InvalidOperation -from enum import Enum -from typing import Any - -import httpx -from eth_account import Account -from eth_account.messages import encode_typed_data - -logger = logging.getLogger(__name__) - -_EVM_ADDRESS_RE = re.compile(r"^0x[0-9a-fA-F]{40}$") -USDC_DECIMAL_PLACES = 6 - - -def _parse_price_to_decimal(price: str) -> Decimal: - """Parse price string to Decimal USD amount.""" - cleaned = price.strip().lstrip("$").strip() - if not cleaned: - raise ValueError(f"Empty price: {price!r}") - try: - val = Decimal(cleaned) - except InvalidOperation: - raise ValueError(f"Invalid price: {price!r}") from None - if val <= 0: - raise ValueError(f"Price must be positive: {price!r}") - return val - - -def _usd_to_atomic(price_usd: Decimal) -> int: - """Convert USD Decimal to USDC atomic units (6 decimals).""" - atomic = price_usd * Decimal(10**USDC_DECIMAL_PLACES) - if atomic != int(atomic): - raise ValueError(f"Price has too many decimals: {price_usd}") - return int(atomic) - - -def _accepted_requirements_match( - payment_payload: dict[str, Any], - accepted: dict[str, Any], -) -> tuple[bool, str]: - """Validate x402 v2 payload.accepted against the server-selected requirement.""" - payload_accepted = payment_payload.get("accepted") - if int(payment_payload.get("x402Version", 2)) == 2 and not isinstance(payload_accepted, dict): - return False, "Missing accepted requirements in PAYMENT-SIGNATURE payload" - if not isinstance(payload_accepted, dict): - return True, "" - - checks = ( - ("scheme", False, False), - ("network", False, False), - ("asset", True, True), - ("amount", False, False), - ("payTo", True, True), - ) - for requirement_field, casefold, optional in checks: - expected = accepted.get(requirement_field) - actual = payload_accepted.get(requirement_field) - if optional and (expected is None or actual is None): - continue - expected_text = str(expected) - actual_text = str(actual) - if casefold: - expected_text = expected_text.lower() - actual_text = actual_text.lower() - if actual_text != expected_text: - return False, f"Accepted requirements mismatch: {requirement_field}" - - expected_extra = accepted.get("extra") or {} - actual_extra = payload_accepted.get("extra") or {} - expected_contract = expected_extra.get("verifyingContract") - actual_contract = actual_extra.get("verifyingContract") - if expected_contract and str(actual_contract).lower() != str(expected_contract).lower(): - return False, "Accepted requirements mismatch: verifyingContract" - - return True, "" - - -# ============================================================================= -# TYPES -# ============================================================================= - - -class PaymentScheme(str, Enum): - """Payment schemes supported.""" - - EXACT = "exact" # Basic x402 (EIP-3009) - GATEWAY_BATCHED = "GatewayWalletBatched" # Circle Nanopayment - - -class PaymentStatus(str, Enum): - """Payment status.""" - - PENDING = "pending" - VERIFIED = "verified" - SETTLED = "settled" - FAILED = "failed" - - -@dataclass -class PaymentRecord: - """Record of a payment.""" - - id: str - scheme: str - buyer_address: str - seller_address: str - amount: int - amount_usd: float - resource_url: str - status: PaymentStatus - created_at: datetime = field(default_factory=datetime.now) - verified_at: datetime | None = None - tx_hash: str | None = None - - -@dataclass -class Endpoint: - """Protected endpoint configuration.""" - - path: str - price_usd: Decimal - description: str - schemes: list[PaymentScheme] = field( - default_factory=lambda: [PaymentScheme.EXACT, PaymentScheme.GATEWAY_BATCHED] - ) - requires_guild: str | None = None - - -@dataclass -class SellerConfig: - """Seller configuration.""" - - seller_address: str - name: str - description: str = "" - network: str = "eip155:84532" # Base Sepolia - usdc_contract: str = "0x036CbD53842c5426634e7929541eC2318f3dCF7e" - gateway_contract: str = "" - webhook_url: str = "" - webhook_secret: str = "" - - -# ============================================================================= -# CORE SELLER -# ============================================================================= - - -class Seller: - """ - Complete seller infrastructure for accepting payments. - - Features: - - Multiple protected endpoints - - Both payment methods (basic x402 + Circle) - - Transaction history - - Webhook notifications - - Optional Circle Gateway facilitator integration - - Usage: - seller = Seller( - seller_address="0x742d...", - name="My API", - ) - - seller.protect("/weather", "$0.001", "Weather data") - seller.protect("/premium", "$0.01", "Premium content") - - seller.serve(port=4023) - - With Circle Gateway facilitator: - from omniclaw.seller.facilitator import create_facilitator - - facilitator = create_facilitator(circle_api_key="...") - seller = Seller( - seller_address="0x742d...", - name="My API", - facilitator=facilitator, # Uses Circle Gateway for verify/settle - ) - """ - - def __init__( - self, - seller_address: str, - name: str, - description: str = "", - network: str = "eip155:84532", - usdc_contract: str = "0x036CbD53842c5426634e7929541eC2318f3dCF7e", - webhook_url: str = "", - webhook_secret: str = "", - facilitator: Any = None, - ): - """ - Initialize seller. - - Args: - seller_address: EVM address for receiving payments - name: Seller name - description: Seller description - network: CAIP-2 network identifier - usdc_contract: USDC contract address - webhook_url: URL for payment notifications - webhook_secret: Secret for webhook signing - facilitator: Optional CircleGatewayFacilitator for verify/settle - """ - self.config = SellerConfig( - seller_address=seller_address, - name=name, - description=description, - network=network, - usdc_contract=usdc_contract, - webhook_url=webhook_url, - webhook_secret=webhook_secret, - ) - - self._endpoints: dict[str, Endpoint] = {} - self._payments: dict[str, PaymentRecord] = {} - self._used_nonces: dict[tuple[str, str, str, str, str], int] = {} - self._nonce_lock = asyncio.Lock() - self._facilitator = facilitator - - self._gateway_contract = os.environ.get("CIRCLE_GATEWAY_CONTRACT", "") - self._strict_gateway_contract = ( - os.environ.get("OMNICLAW_SELLER_STRICT_GATEWAY_CONTRACT", "false").lower() == "true" - ) - if not self._gateway_contract: - logger.warning( - "CIRCLE_GATEWAY_CONTRACT env var not set. " - "GatewayWalletBatched scheme will not work until this is configured. " - "Fetch it from Circle Gateway /v1/x402/supported endpoint." - ) - - self._nonce_redis_client: Any | None = None - self._nonce_redis_url = os.environ.get("OMNICLAW_SELLER_NONCE_REDIS_URL") - runtime_env = os.environ.get("OMNICLAW_ENV", "").lower() - self._is_production_env = runtime_env in {"prod", "production", "mainnet"} - self._require_distributed_nonce = ( - os.environ.get("OMNICLAW_SELLER_REQUIRE_DISTRIBUTED_NONCE", "false").lower() == "true" - ) - self._nonce_ttl_floor_seconds = int( - os.environ.get("OMNICLAW_SELLER_NONCE_TTL_FLOOR_SECONDS", "300") - ) - if self._nonce_redis_url: - try: - import redis.asyncio as redis_asyncio - - self._nonce_redis_client = redis_asyncio.from_url( - self._nonce_redis_url, - decode_responses=True, - ) - logger.info("Seller nonce replay protection using Redis backend") - except Exception as exc: - if self._require_distributed_nonce: - raise RuntimeError( - "Distributed nonce protection required but Redis client init failed" - ) from exc - logger.warning( - "Redis nonce backend unavailable, falling back to local memory: %s", exc - ) - elif self._require_distributed_nonce: - raise RuntimeError( - "OMNICLAW_SELLER_REQUIRE_DISTRIBUTED_NONCE=true but " - "OMNICLAW_SELLER_NONCE_REDIS_URL is not set" - ) - elif self._is_production_env: - raise RuntimeError( - "Production seller requires distributed nonce protection. " - "Set OMNICLAW_SELLER_NONCE_REDIS_URL (or explicitly run non-production env)." - ) - - def protect( - self, - path: str, - price: str, - description: str = "", - schemes: list[PaymentScheme] | None = None, - ) -> Callable: - """ - Decorator to protect an endpoint. - - Args: - path: Route path (e.g., "/weather") - price: Price in USD (e.g., "$0.001") - description: Endpoint description - schemes: Payment schemes to accept (default: both) - - Usage: - @seller.protect("/weather", "$0.001", "Weather data") - def weather(): - return {"temp": 72} - """ - price_usd = _parse_price_to_decimal(price) - - # Default to accepting both - if schemes is None: - schemes = [PaymentScheme.EXACT, PaymentScheme.GATEWAY_BATCHED] - - endpoint = Endpoint( - path=path, - price_usd=price_usd, - description=description, - schemes=schemes, - ) - - self._endpoints[path] = endpoint - - def decorator(func: Callable) -> Callable: - return func - - return decorator - - def add_endpoint( - self, - path: str, - price: str, - description: str = "", - schemes: list[PaymentScheme] | None = None, - ) -> None: - """ - Add a protected endpoint programmatically. - - Args: - path: Route path - price: Price in USD - description: Description - schemes: Payment schemes - """ - price_usd = _parse_price_to_decimal(price) - - if schemes is None: - schemes = [PaymentScheme.EXACT, PaymentScheme.GATEWAY_BATCHED] - - if ( - self._strict_gateway_contract - and PaymentScheme.GATEWAY_BATCHED in schemes - and not self._gateway_contract - ): - raise ValueError( - "GatewayWalletBatched configured but CIRCLE_GATEWAY_CONTRACT is not set. " - "Set CIRCLE_GATEWAY_CONTRACT or disable GatewayWalletBatched for this endpoint." - ) - - self._endpoints[path] = Endpoint( - path=path, - price_usd=price_usd, - description=description, - schemes=schemes, - ) - - def _create_accepts(self, endpoint: Endpoint) -> list[dict]: - """Create accepts array for an endpoint.""" - amount_atomic = _usd_to_atomic(endpoint.price_usd) - accepts = [] - - for scheme in endpoint.schemes: - if scheme == PaymentScheme.EXACT: - accepts.append( - { - "scheme": "exact", - "network": self.config.network, - "asset": self.config.usdc_contract, - "amount": str(amount_atomic), - "payTo": self.config.seller_address, - "maxTimeoutSeconds": 345600, - } - ) - elif scheme == PaymentScheme.GATEWAY_BATCHED: - if not self._gateway_contract: - logger.warning( - f"Skipping GatewayWalletBatched for {endpoint.path}: " - f"CIRCLE_GATEWAY_CONTRACT not configured." - ) - continue - accepts.append( - { - "scheme": "exact", - "network": self.config.network, - "asset": self.config.usdc_contract, - "amount": str(amount_atomic), - "payTo": self.config.seller_address, - "maxTimeoutSeconds": 345600, - "extra": { - "name": "GatewayWalletBatched", - "version": "1", - "verifyingContract": self._gateway_contract, - }, - } - ) - - return accepts - - async def _check_and_mark_nonce( - self, - *, - network: str, - payer: str, - nonce: str, - valid_before: int, - verifying_contract: str = "", - pay_to: str = "", - ) -> bool: - """Atomically mark nonce usage; returns False when nonce is already used.""" - now = int(time.time()) - ttl = max( - (valid_before - now) + self._nonce_ttl_floor_seconds, self._nonce_ttl_floor_seconds - ) - replay_scope = ( - str(network).lower(), - str(verifying_contract).lower(), - str(pay_to).lower(), - str(payer).lower(), - str(nonce), - ) - - if self._nonce_redis_client is not None: - key = "omniclaw:seller:nonce:" + ":".join(replay_scope) - result = await self._nonce_redis_client.set(key, "1", ex=ttl, nx=True) - return bool(result) - - async with self._nonce_lock: - self._prune_local_nonces(now) - nonce_key = replay_scope - expiry = self._used_nonces.get(nonce_key) - if expiry is not None and expiry > now: - return False - self._used_nonces[nonce_key] = now + ttl - return True - - def _prune_local_nonces(self, now: int | None = None) -> None: - """Drop expired in-memory nonce markers.""" - current = now or int(time.time()) - expired = [key for key, expiry in self._used_nonces.items() if expiry <= current] - for key in expired: - del self._used_nonces[key] - - def _check_and_mark_nonce_sync( - self, - *, - network: str, - payer: str, - nonce: str, - valid_before: int, - verifying_contract: str = "", - pay_to: str = "", - ) -> tuple[bool, str]: - """Sync helper for nonce marking; rejects usage from running event loops.""" - try: - asyncio.get_running_loop() - return False, "Sync verify called from async loop; use verify_payment_async()" - except RuntimeError: - pass - - try: - marked = asyncio.run( - self._check_and_mark_nonce( - network=network, - payer=payer, - nonce=nonce, - valid_before=valid_before, - verifying_contract=verifying_contract, - pay_to=pay_to, - ) - ) - if not marked: - return False, "Nonce already used" - return True, "" - except Exception as exc: - return False, f"Nonce replay protection error: {exc}" - - def create_402_response(self, path: str, url: str) -> tuple[dict, str]: - """Create 402 response for a path.""" - endpoint = self._endpoints.get(path) - - if not endpoint: - return {}, "Endpoint not found" - - payment_required = { - "x402Version": 2, - "error": "Payment required", - "resource": { - "url": url, - "description": endpoint.description or f"Access to {path}", - "mimeType": "application/json", - }, - "accepts": self._create_accepts(endpoint), - } - - header = base64.b64encode(json.dumps(payment_required).encode()).decode() - - return {"payment-required": header}, json.dumps({"error": "Payment required"}) - - def verify_payment( - self, - payment_payload: dict, - accepted: dict, - verify_signature: bool = True, - settle_payment: bool = False, - ) -> tuple[bool, str, PaymentRecord | None]: - """ - Verify a payment. - - Args: - payment_payload: The payment payload from the request header - accepted: The accepted payment requirements from 402 response - verify_signature: Whether to verify EIP-3009 signature (default: True) - settle_payment: Whether to settle via facilitator (default: False) - - Returns: - (is_valid, error, payment_record) - """ - try: - if settle_payment and not self._facilitator: - return ( - False, - "Settlement requested but no facilitator is configured", - None, - ) - - scheme = payment_payload.get("scheme") - payment_data = payment_payload.get("payload", {}) - authorization = payment_data.get("authorization", {}) - signature = payment_data.get("signature", "") - - accepted_ok, accepted_error = _accepted_requirements_match(payment_payload, accepted) - if not accepted_ok: - return False, accepted_error, None - - # Use Circle Gateway facilitator if available - if self._facilitator: - return self._verify_with_facilitator( - payment_payload=payment_payload, - accepted=accepted, - verify_signature=verify_signature, - settle_payment=settle_payment, - ) - - is_valid, error = self._validate_payment_fields( - payment_payload=payment_payload, - accepted=accepted, - authorization=authorization, - verify_signature=verify_signature, - signature=signature, - ) - if not is_valid: - return False, error, None - - # Get buyer address - buyer_address = authorization.get("from", "").lower() - - # Generate payment ID - payment_id = hashlib.sha256(f"{buyer_address}{time.time()}".encode()).hexdigest()[:16] - - # Check timeout - valid_after = int(authorization.get("validAfter", 0)) - valid_before = int(authorization.get("validBefore", 0)) - current = int(time.time()) - - if current < valid_after: - return False, "Payment not yet valid", None - if current > valid_before: - return False, "Payment expired", None - - # Check amount - paid = int(authorization.get("value", "0")) - required = int(accepted.get("amount", "0")) - - if paid < required: - return False, f"Insufficient: {paid} < {required}", None - - # Check recipient - to_address = authorization.get("to", "").lower() - if to_address != self.config.seller_address.lower(): - return False, "Wrong recipient", None - - nonce = str(authorization.get("nonce", "")) - network = str(accepted.get("network", self.config.network)) - pay_to = str(accepted.get("payTo", self.config.seller_address)) - verifying_contract = str((accepted.get("extra") or {}).get("verifyingContract", "")) - if nonce: - nonce_marked, nonce_error = self._check_and_mark_nonce_sync( - network=network, - payer=buyer_address, - nonce=nonce, - valid_before=valid_before, - verifying_contract=verifying_contract, - pay_to=pay_to, - ) - if not nonce_marked: - return False, nonce_error, None - - # Create payment record - record = PaymentRecord( - id=payment_id, - scheme=scheme, - buyer_address=buyer_address, - seller_address=self.config.seller_address, - amount=paid, - amount_usd=paid / 1_000_000, - resource_url=accepted.get("resource", {}).get("url", ""), - status=PaymentStatus.VERIFIED, - ) - - # Store payment - self._payments[payment_id] = record - - # Send webhook - if self.config.webhook_url: - self._send_webhook(record) - - return True, "", record - - except Exception as e: - return False, str(e), None - - async def verify_payment_async( - self, - payment_payload: dict, - accepted: dict, - verify_signature: bool = True, - settle_payment: bool = False, - ) -> tuple[bool, str, PaymentRecord | None]: - """ - Verify a payment (async version for use with asyncio). - - Args: - payment_payload: The payment payload from the request header - accepted: The accepted payment requirements from 402 response - verify_signature: Whether to verify EIP-3009 signature (default: True) - settle_payment: Whether to settle via facilitator (default: False) - - Returns: - (is_valid, error, payment_record) - """ - import hashlib - - try: - if settle_payment and not self._facilitator: - return ( - False, - "Settlement requested but no facilitator is configured", - None, - ) - - scheme = payment_payload.get("scheme") - payment_data = payment_payload.get("payload", {}) - authorization = payment_data.get("authorization", {}) - signature = payment_data.get("signature", "") - - accepted_ok, accepted_error = _accepted_requirements_match(payment_payload, accepted) - if not accepted_ok: - return False, accepted_error, None - - # Use Circle Gateway facilitator if available - if self._facilitator: - payment_requirements = { - "scheme": accepted.get("scheme", "exact"), - "network": accepted.get("network", self.config.network), - "asset": accepted.get("asset", self.config.usdc_contract), - "amount": accepted.get("amount", "0"), - "payTo": accepted.get("payTo", self.config.seller_address), - "maxTimeoutSeconds": accepted.get("maxTimeoutSeconds", 345600), - "extra": accepted.get("extra", {}), - } - - # Call facilitator directly (async) - if settle_payment: - result = await self._facilitator.settle(payment_payload, payment_requirements) - status = PaymentStatus.SETTLED if result.success else PaymentStatus.FAILED - - if not result.success: - return False, f"Settlement failed: {result.error_reason}", None - - buyer_address = result.payer or "" - - else: - result = await self._facilitator.verify(payment_payload, payment_requirements) - - if not result.is_valid: - return False, f"Verification failed: {result.invalid_reason}", None - - buyer_address = result.payer or "" - status = PaymentStatus.VERIFIED - - nonce = str(authorization.get("nonce", "")) - valid_before = int(authorization.get("validBefore", 0)) - network = str(accepted.get("network", self.config.network)) - pay_to = str(accepted.get("payTo", self.config.seller_address)) - verifying_contract = str((accepted.get("extra") or {}).get("verifyingContract", "")) - if nonce: - nonce_marked = await self._check_and_mark_nonce( - network=network, - payer=buyer_address.lower(), - nonce=nonce, - valid_before=valid_before, - verifying_contract=verifying_contract, - pay_to=pay_to, - ) - if not nonce_marked: - return False, "Nonce already used", None - - payment_id = hashlib.sha256(f"{buyer_address}{time.time()}".encode()).hexdigest()[ - :16 - ] - paid = int(payment_requirements["amount"]) - - record = PaymentRecord( - id=payment_id, - scheme=scheme or "exact", - buyer_address=buyer_address.lower(), - seller_address=self.config.seller_address, - amount=paid, - amount_usd=paid / 1_000_000, - resource_url=accepted.get("resource", {}).get("url", ""), - status=status, - ) - - self._payments[payment_id] = record - - if self.config.webhook_url: - self._send_webhook(record) - - return True, "", record - - # Non-facilitator path (local verification) - is_valid, error = self._validate_payment_fields( - payment_payload=payment_payload, - accepted=accepted, - authorization=authorization, - verify_signature=verify_signature, - signature=signature, - ) - if not is_valid: - return False, error, None - - buyer_address = authorization.get("from", "").lower() - payment_id = hashlib.sha256(f"{buyer_address}{time.time()}".encode()).hexdigest()[:16] - - valid_after = int(authorization.get("validAfter", 0)) - valid_before = int(authorization.get("validBefore", 0)) - current = int(time.time()) - - if current < valid_after: - return False, "Payment not yet valid", None - if current > valid_before: - return False, "Payment expired", None - - paid = int(authorization.get("value", "0")) - required = int(accepted.get("amount", "0")) - - if paid < required: - return False, f"Insufficient: {paid} < {required}", None - - to_address = authorization.get("to", "").lower() - if to_address != self.config.seller_address.lower(): - return False, "Wrong recipient", None - - nonce = str(authorization.get("nonce", "")) - valid_before = int(authorization.get("validBefore", 0)) - network = str(accepted.get("network", self.config.network)) - pay_to = str(accepted.get("payTo", self.config.seller_address)) - verifying_contract = str((accepted.get("extra") or {}).get("verifyingContract", "")) - nonce_marked = await self._check_and_mark_nonce( - network=network, - payer=buyer_address, - nonce=nonce, - valid_before=valid_before, - verifying_contract=verifying_contract, - pay_to=pay_to, - ) - if not nonce_marked: - return False, "Nonce already used", None - - record = PaymentRecord( - id=payment_id, - scheme=scheme, - buyer_address=buyer_address, - seller_address=self.config.seller_address, - amount=paid, - amount_usd=paid / 1_000_000, - resource_url=accepted.get("resource", {}).get("url", ""), - status=PaymentStatus.VERIFIED, - ) - - self._payments[payment_id] = record - if self.config.webhook_url: - self._send_webhook(record) - - return True, "", record - - except Exception as e: - return False, str(e), None - - def _verify_eip3009_signature( - self, - authorization: dict, - signature: str, - accepted: dict, - ) -> tuple[bool, str]: - """ - Verify EIP-3009 TransferWithAuthorization signature. - - Args: - authorization: The authorization dict from payment payload - signature: The hex-encoded signature - network: CAIP-2 network identifier - verifying_contract: The contract address for EIP-712 domain - - Returns: - (is_valid, error_message) - """ - try: - # Parse network to get chain ID - chain_id = 84532 # Default to Base Sepolia - network = str(accepted.get("network", self.config.network)) - if ":" in network: - chain_id = int(network.split(":")[-1]) - - extra = accepted.get("extra", {}) or {} - if extra: - required_extra_fields = ("name", "version", "verifyingContract") - missing = [field for field in required_extra_fields if not extra.get(field)] - if missing: - return False, f"Missing required EIP-712 domain fields: {', '.join(missing)}" - domain_name = str(extra.get("name")) - domain_version = str(extra.get("version")) - verifying_contract = str(extra.get("verifyingContract")).strip() - else: - domain_name = "USDC" - domain_version = "2" - verifying_contract = str(accepted.get("asset", self.config.usdc_contract)).strip() - - if not _EVM_ADDRESS_RE.match(verifying_contract): - return False, f"Invalid verifyingContract: {verifying_contract}" - - # Build EIP-712 domain - domain = { - "name": domain_name, - "version": domain_version, - "chainId": chain_id, - "verifyingContract": verifying_contract, - } - - # Build EIP-712 message for TransferWithAuthorization - message = { - "types": { - "EIP712Domain": [ - {"name": "name", "type": "string"}, - {"name": "version", "type": "string"}, - {"name": "chainId", "type": "uint256"}, - {"name": "verifyingContract", "type": "address"}, - ], - "TransferWithAuthorization": [ - {"name": "from", "type": "address"}, - {"name": "to", "type": "address"}, - {"name": "value", "type": "uint256"}, - {"name": "validAfter", "type": "uint256"}, - {"name": "validBefore", "type": "uint256"}, - {"name": "nonce", "type": "bytes32"}, - ], - }, - "primaryType": "TransferWithAuthorization", - "domain": domain, - "message": { - "from": authorization.get("from"), - "to": authorization.get("to"), - "value": int(authorization.get("value", 0)), - "validAfter": int(authorization.get("validAfter", 0)), - "validBefore": int(authorization.get("validBefore", 0)), - "nonce": authorization.get("nonce", "0x" + "00" * 32), - }, - } - - # Recover signer from signature - signable = encode_typed_data(full_message=message) - signer = Account.recover_message(signable, signature=signature) - expected_signer = authorization.get("from", "").lower() - - if signer.lower() != expected_signer: - return False, f"Signature mismatch: {signer} != {expected_signer}" - - return True, "" - - except Exception as e: - return False, f"Signature verification error: {str(e)}" - - def _validate_payment_fields( - self, - payment_payload: dict[str, Any], - accepted: dict[str, Any], - authorization: dict[str, Any], - verify_signature: bool, - signature: str, - ) -> tuple[bool, str]: - """Validate payload against server-selected accepted requirements.""" - payer = str(authorization.get("from", "")).lower() - payee = str(authorization.get("to", "")).lower() - nonce = str(authorization.get("nonce", "")) - - if not _EVM_ADDRESS_RE.match(payer): - return False, "Invalid payer address" - if not _EVM_ADDRESS_RE.match(payee): - return False, "Invalid recipient address" - if not nonce: - return False, "Missing nonce" - expected_scheme = str(accepted.get("scheme", "exact")).lower() - payload_scheme = str(payment_payload.get("scheme", "exact")).lower() - if payload_scheme and payload_scheme != expected_scheme: - return False, f"Scheme mismatch: {payload_scheme} != {expected_scheme}" - - expected_network = str(accepted.get("network", self.config.network)) - payload_network = str(payment_payload.get("network", expected_network)) - if payload_network and payload_network != expected_network: - return False, f"Network mismatch: {payload_network} != {expected_network}" - - required_payto = str(accepted.get("payTo", self.config.seller_address)).lower() - if required_payto != self.config.seller_address.lower(): - return False, "Server payTo mismatch" - if payee != required_payto: - return False, "Wrong recipient" - - if verify_signature: - if not signature: - return False, "Missing signature" - is_valid_sig, sig_error = self._verify_eip3009_signature( - authorization=authorization, - signature=signature, - accepted=accepted, - ) - if not is_valid_sig: - return False, f"Invalid signature: {sig_error}" - - return True, "" - - def _select_accepted_for_payload( - self, payload: dict[str, Any], path: str - ) -> dict[str, Any] | None: - """Pick server-defined accepted requirement matching incoming payload fields.""" - endpoint = self._endpoints.get(path) - if not endpoint: - return None - accepts = self._create_accepts(endpoint) - if not accepts: - return None - - payload_accepted = payload.get("accepted") - if int(payload.get("x402Version", 2)) == 2 and not isinstance(payload_accepted, dict): - return None - payload_accepted = payload_accepted or {} - payload_network = str(payload_accepted.get("network", payload.get("network", ""))) - payload_scheme = str(payload_accepted.get("scheme", payload.get("scheme", "exact"))).lower() - payload_data = payload.get("payload", {}) or {} - auth = payload_data.get("authorization", {}) or {} - payload_value = str(auth.get("value", "")) - - for accepted in accepts: - if payload_scheme and payload_scheme != str(accepted.get("scheme", "")).lower(): - continue - if payload_network and payload_network != str(accepted.get("network", "")): - continue - if ( - str(payload_accepted.get("asset", accepted.get("asset", ""))).lower() - != str(accepted.get("asset", "")).lower() - ): - continue - if ( - str(payload_accepted.get("payTo", accepted.get("payTo", ""))).lower() - != str(accepted.get("payTo", "")).lower() - ): - continue - accepted_amount = str(accepted.get("amount", "0")) - if str(payload_accepted.get("amount", accepted_amount)) != accepted_amount: - continue - if payload_value and int(payload_value) < int(accepted_amount): - continue - return accepted - return None - - def _verify_with_facilitator( - self, - payment_payload: dict, - accepted: dict, - verify_signature: bool, - settle_payment: bool, - ) -> tuple[bool, str, PaymentRecord | None]: - """ - Verify and optionally settle payment using Circle Gateway facilitator. - - Args: - payment_payload: The payment payload from the request header - accepted: The accepted payment requirements from 402 response - verify_signature: Whether to verify signature (passed to facilitator) - settle_payment: Whether to settle via facilitator - - Returns: - (is_valid, error, payment_record) - """ - # Build the proper format for facilitator - payment_requirements = { - "scheme": accepted.get("scheme", "exact"), - "network": accepted.get("network", self.config.network), - "asset": accepted.get("asset", self.config.usdc_contract), - "amount": accepted.get("amount", "0"), - "payTo": accepted.get("payTo", self.config.seller_address), - "maxTimeoutSeconds": accepted.get("maxTimeoutSeconds", 345600), - "extra": accepted.get("extra", {}), - } - - # Use asyncio.run() to properly handle async facilitator calls from sync context - async def _do_verify(): - if settle_payment: - return await self._facilitator.settle(payment_payload, payment_requirements) - else: - return await self._facilitator.verify(payment_payload, payment_requirements) - - try: - asyncio.get_running_loop() - return False, "Sync verify called from async loop; use verify_payment_async()", None - except RuntimeError: - pass - - try: - result = asyncio.run(_do_verify()) - except Exception as e: - return False, f"Facilitator error: {e}", None - - # Process result - if settle_payment: - status = PaymentStatus.SETTLED if result.success else PaymentStatus.FAILED - - if not result.success: - return False, f"Settlement failed: {result.error_reason}", None - - buyer_address = result.payer or "" - - else: - if not result.is_valid: - return False, f"Verification failed: {result.invalid_reason}", None - - buyer_address = result.payer or "" - - authorization = (payment_payload.get("payload") or {}).get("authorization") or {} - nonce = str(authorization.get("nonce", "")) - valid_before = int(authorization.get("validBefore", 0)) - network = str(accepted.get("network", self.config.network)) - pay_to = str(accepted.get("payTo", self.config.seller_address)) - verifying_contract = str((accepted.get("extra") or {}).get("verifyingContract", "")) - if nonce: - nonce_marked, nonce_error = self._check_and_mark_nonce_sync( - network=network, - payer=buyer_address.lower(), - nonce=nonce, - valid_before=valid_before, - verifying_contract=verifying_contract, - pay_to=pay_to, - ) - if not nonce_marked: - return False, nonce_error, None - - payment_id = hashlib.sha256(f"{buyer_address}{time.time()}".encode()).hexdigest()[:16] - paid = int(payment_requirements["amount"]) - status = PaymentStatus.SETTLED if settle_payment else PaymentStatus.VERIFIED - - # Create payment record - record = PaymentRecord( - id=payment_id, - scheme=payment_payload.get("scheme", "exact"), - buyer_address=buyer_address.lower(), - seller_address=self.config.seller_address, - amount=paid, - amount_usd=paid / 1_000_000, - resource_url=accepted.get("resource", {}).get("url", ""), - status=status, - ) - - # Store payment - self._payments[payment_id] = record - - # Send webhook - if self.config.webhook_url: - self._send_webhook(record) - - return True, "", record - - def _send_webhook(self, record: PaymentRecord) -> None: - """Send webhook notification for payment.""" - if not self.config.webhook_url: - return - - payload = { - "event": "payment.received", - "payment": { - "id": record.id, - "scheme": record.scheme, - "buyer": record.buyer_address, - "amount": str(record.amount), - "amount_usd": str(record.amount_usd), - "status": record.status.value, - "timestamp": record.created_at.isoformat(), - }, - } - - # Sign payload - if self.config.webhook_secret: - import hmac - - signature = hmac.new( - self.config.webhook_secret.encode(), json.dumps(payload).encode(), "sha256" - ).hexdigest() - headers = {"X-Signature": signature} - else: - headers = {} - - try: - httpx.post( - self.config.webhook_url, - json=payload, - headers=headers, - timeout=5.0, - ) - except Exception as e: - print(f"Webhook failed: {e}") - - def _build_payment_response_header( - self, - *, - success: bool, - payer: str, - transaction: str = "", - error_reason: str | None = None, - ) -> str: - """Build base64-encoded PAYMENT-RESPONSE header payload.""" - body: dict[str, Any] = { - "success": success, - "transaction": transaction, - "network": self.config.network, - "payer": payer, - } - if error_reason: - body["errorReason"] = error_reason - return base64.b64encode(json.dumps(body).encode()).decode() - - def get_payment(self, payment_id: str) -> PaymentRecord | None: - """Get payment by ID.""" - return self._payments.get(payment_id) - - def list_payments( - self, - buyer_address: str | None = None, - status: PaymentStatus | None = None, - limit: int = 100, - ) -> list[PaymentRecord]: - """List payments with optional filters.""" - payments = list(self._payments.values()) - - if buyer_address: - payments = [p for p in payments if p.buyer_address == buyer_address.lower()] - - if status: - payments = [p for p in payments if p.status == status] - - return payments[-limit:] - - def get_earnings(self) -> dict: - """Get total earnings.""" - total = sum( - p.amount_usd for p in self._payments.values() if p.status == PaymentStatus.VERIFIED - ) - count = len([p for p in self._payments.values() if p.status == PaymentStatus.VERIFIED]) - - return { - "total_usd": total, - "count": count, - "by_scheme": { - "exact": sum( - p.amount_usd - for p in self._payments.values() - if p.scheme == "exact" and p.status == PaymentStatus.VERIFIED - ), - "gateway_batched": sum( - p.amount_usd - for p in self._payments.values() - if p.scheme == "GatewayWalletBatched" and p.status == PaymentStatus.VERIFIED - ), - }, - } - - def get_endpoints(self) -> dict[str, Endpoint]: - """Get all protected endpoints.""" - return self._endpoints - - def serve(self, port: int = 4023, host: str = "0.0.0.0") -> None: - """ - Start the seller server. - - Args: - port: Port to listen on - host: Host to bind to - """ - try: - import uvicorn - from fastapi import FastAPI, Request - from fastapi.responses import JSONResponse - except ImportError: - print("FastAPI required: pip install fastapi uvicorn") - return - - app = FastAPI( - title=f"OmniClaw Seller: {self.config.name}", - description=self.config.description, - ) - - # Add endpoints - for path, _endpoint in self._endpoints.items(): - methods = ["GET"] - - # Create route handler - async def handler(request: Request, path=path): - payment = request.headers.get("payment-signature") - - if not payment: - # Return 402 - headers, body = self.create_402_response(path, str(request.url)) - return JSONResponse( - status_code=402, - content=json.loads(body), - headers=headers, - ) - - # Verify payment - try: - payload = json.loads(base64.b64decode(payment)) - payer = str( - (((payload.get("payload") or {}).get("authorization") or {}).get("from")) - or "" - ).lower() - accepted = self._select_accepted_for_payload(payload, path) - if not accepted: - headers, body = self.create_402_response(path, str(request.url)) - headers["PAYMENT-RESPONSE"] = self._build_payment_response_header( - success=False, - payer=payer, - error_reason="no_matching_payment_requirement", - ) - body = json.dumps( - {"error": "No server-accepted payment kind matched payload"} - ) - return JSONResponse( - status_code=402, - content=json.loads(body), - headers=headers, - ) - - is_valid, error, record = await self.verify_payment_async( - payload, - accepted, - settle_payment=True, - ) - - if not is_valid: - headers, body = self.create_402_response(path, str(request.url)) - headers["PAYMENT-RESPONSE"] = self._build_payment_response_header( - success=False, - payer=payer, - error_reason=error or "verification_failed", - ) - body = json.dumps({"error": error}) - return JSONResponse( - status_code=402, - content=json.loads(body), - headers=headers, - ) - - # Payment valid - return data - success_headers = { - "PAYMENT-RESPONSE": self._build_payment_response_header( - success=True, - payer=payer, - transaction=record.id if record else "", - ) - } - return JSONResponse( - status_code=200, - content={"status": "ok", "payment_id": record.id if record else None}, - headers=success_headers, - ) - - except Exception as e: - headers, body = self.create_402_response(path, str(request.url)) - headers["PAYMENT-RESPONSE"] = self._build_payment_response_header( - success=False, - payer="", - error_reason="payload_parse_error", - ) - return JSONResponse( - status_code=402, - content={"error": str(e)}, - headers=headers, - ) - - # Add route - app.add_api_route(path, handler, methods=methods) - - # Management endpoints - @app.get("/_/health") - async def health(): - return { - "status": "ok", - "seller": self.config.name, - "endpoints": len(self._endpoints), - "payments": len(self._payments), - "earnings": self.get_earnings(), - } - - @app.get("/_/payments") - async def list_payments(limit: int = 100): - return {"payments": self.list_payments(limit=limit)} - - print(f"\n🏪 OmniClaw Seller: {self.config.name}") - print(f" Address: {self.config.seller_address}") - print(f" Endpoints: {len(self._endpoints)}") - for path, ep in self._endpoints.items(): - schemes = [s.value for s in ep.schemes] - print(f" - {path}: ${ep.price_usd} ({', '.join(schemes)})") - print(f"\n Running on http://{host}:{port}") - - uvicorn.run(app, host=host, port=port) - - -# ============================================================================= -# FACTORY FUNCTION -# ============================================================================= - - -def create_seller( - seller_address: str, - name: str, - description: str = "", - network: str = "eip155:84532", - webhook_url: str = "", - webhook_secret: str = "", - facilitator: Any = None, - circle_api_key: str | None = None, - facilitator_environment: str = "testnet", -) -> Seller: - """ - Create a new seller. - - Args: - seller_address: EVM address for payments - name: Seller name - description: Seller description - network: CAIP-2 network - webhook_url: Webhook URL for notifications - webhook_secret: Webhook secret for signing - facilitator: Optional CircleGatewayFacilitator instance - circle_api_key: If provided (and no facilitator), creates facilitator automatically - facilitator_environment: Environment for auto-created facilitator ('testnet' or 'mainnet') - - Returns: - Seller instance - """ - # Auto-create facilitator if API key provided but no facilitator - if facilitator is None and circle_api_key: - from omniclaw.seller.facilitator import create_facilitator - - facilitator = create_facilitator( - circle_api_key=circle_api_key, - environment=facilitator_environment, - ) - - return Seller( - seller_address=seller_address, - name=name, - description=description, - network=network, - webhook_url=webhook_url, - webhook_secret=webhook_secret, - facilitator=facilitator, - ) - - -# ============================================================================= -# EXPORTS -# ============================================================================= - -__all__ = [ - "Seller", - "create_seller", - "PaymentScheme", - "PaymentStatus", - "PaymentRecord", - "Endpoint", - "SellerConfig", -] diff --git a/tests/test_arc_marketplace_showcase.py b/tests/test_arc_marketplace_showcase.py deleted file mode 100644 index b10d182..0000000 --- a/tests/test_arc_marketplace_showcase.py +++ /dev/null @@ -1,78 +0,0 @@ -from __future__ import annotations - -import importlib.util -import sys -from pathlib import Path -from uuid import uuid4 - -from fastapi.testclient import TestClient - -ROOT = Path(__file__).resolve().parents[1] -SHOWCASE_APP = ROOT / "examples" / "arc-marketplace-showcase" / "app.py" -DUMMY_PRIVATE_KEY = "0x" + "11" * 32 - - -def _load_showcase_module(monkeypatch): - monkeypatch.setenv("OMNICLAW_PRIVATE_KEY", DUMMY_PRIVATE_KEY) - monkeypatch.delenv("OMNICLAW_X402_EXACT_PAY_TO", raising=False) - monkeypatch.setenv("OMNICLAW_X402_EXACT_NETWORK_PROFILE", "ARC-TESTNET") - monkeypatch.setenv("OMNICLAW_X402_EXACT_FACILITATOR_URL", "http://127.0.0.1:4022") - monkeypatch.setenv("ARC_MARKETPLACE_PUBLIC_BASE_URL", "http://127.0.0.1:8020") - monkeypatch.setenv("ARC_MARKETPLACE_BUYER_BASE_URL", "http://buyer.local:8020") - - module_name = f"arc_showcase_{uuid4().hex}" - spec = importlib.util.spec_from_file_location(module_name, SHOWCASE_APP) - assert spec is not None - assert spec.loader is not None - - module = importlib.util.module_from_spec(spec) - sys.modules[module_name] = module - spec.loader.exec_module(module) - return module - - -def test_arc_marketplace_catalog_uses_arc_exact_profile(monkeypatch): - module = _load_showcase_module(monkeypatch) - - with TestClient(module.app) as client: - response = client.get("/api/catalog") - - assert response.status_code == 200 - catalog = response.json() - assert catalog["network_profile"] == "ARC-TESTNET" - assert catalog["network"] == "eip155:5042002" - assert catalog["asset"] == "0x3600000000000000000000000000000000000000" - assert catalog["facilitator_url"] == "http://127.0.0.1:4022" - assert catalog["explorer_base_url"] == "https://testnet.arcscan.app/tx/" - assert catalog["buyer_engine_configured"] is False - assert [product["slug"] for product in catalog["products"]] == [ - "prime-market-scan", - "risk-oracle-brief", - "settlement-receipt-kit", - ] - assert catalog["products"][0]["pay_url"] == "http://buyer.local:8020/buy/prime-market-scan" - - -def test_arc_marketplace_paid_routes_advertise_arc_exact(monkeypatch): - module = _load_showcase_module(monkeypatch) - - route = module.routes["GET /buy/prime-market-scan"] - payment_option = route.accepts[0] - - assert payment_option.scheme == "exact" - assert payment_option.price == "$0.25" - assert payment_option.network == "eip155:5042002" - assert payment_option.pay_to == module.PAY_TO - - -def test_arc_marketplace_mini_agent_reports_missing_buyer_engine(monkeypatch): - module = _load_showcase_module(monkeypatch) - - with TestClient(module.app) as client: - response = client.post("/api/agent/inspect/prime-market-scan") - - assert response.status_code == 200 - body = response.json() - assert body["ok"] is False - assert body["status_code"] == 503 - assert "Buyer Financial Policy Engine" in body["error"] diff --git a/tests/test_cli_facilitator.py b/tests/test_cli_facilitator.py deleted file mode 100644 index df1602e..0000000 --- a/tests/test_cli_facilitator.py +++ /dev/null @@ -1,52 +0,0 @@ -from argparse import Namespace - -from omniclaw.admin_cli import build_parser, handle_facilitator_exact - - -def test_facilitator_exact_parser_accepts_arc_profile() -> None: - parser = build_parser() - - args = parser.parse_args( - [ - "facilitator", - "exact", - "--network-profile", - "ARC-TESTNET", - "--network", - "eip155:5042002", - "--port", - "4122", - ] - ) - - assert args.command == "facilitator" - assert args.facilitator_command == "exact" - assert args.network_profile == "ARC-TESTNET" - assert args.network == ["eip155:5042002"] - assert args.port == 4122 - - -def test_facilitator_exact_requires_private_key(monkeypatch, capsys, tmp_path) -> None: - monkeypatch.chdir(tmp_path) - for name in ( - "OMNICLAW_X402_FACILITATOR_PRIVATE_KEY", - "OMNICLAW_PRIVATE_KEY", - "OMNICLAW_X402_FACILITATOR_NETWORK_PROFILE", - "OMNICLAW_X402_FACILITATOR_RPC_URL", - "OMNICLAW_X402_FACILITATOR_NETWORKS", - "OMNICLAW_NETWORK", - ): - monkeypatch.delenv(name, raising=False) - - args = Namespace( - host="127.0.0.1", - port=4022, - network_profile="BASE-SEPOLIA", - network=None, - rpc_url="https://sepolia.base.org", - private_key=None, - title=None, - ) - - assert handle_facilitator_exact(args) == 1 - assert "OMNICLAW_X402_FACILITATOR_PRIVATE_KEY" in capsys.readouterr().out diff --git a/tests/test_client.py b/tests/test_client.py index bd9db09..dd6f9e0 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -4,7 +4,6 @@ Tests the main SDK entry point with per-wallet/wallet-set guards. """ -import inspect import os import tempfile from decimal import Decimal @@ -389,18 +388,6 @@ async def _mock_pay(**kwargs): assert captured_keys[0] == captured_keys[1] -class TestSellerDependency: - def test_sell_returns_fastapi_dependency_that_accepts_request(self, client): - dependency = client.sell( - "$0.01", - seller_address="0x742d35Cc6634C0532925a3b844Bc9e7595f1E123", - ) - - assert hasattr(dependency, "dependency") - signature = inspect.signature(dependency.dependency) - assert "request" in signature.parameters - - class TestSettlementReconciliation: @pytest.mark.asyncio async def test_finalize_pending_settlement_marks_completed(self, client): diff --git a/tests/test_e2e_seller_buyer.py b/tests/test_e2e_seller_buyer.py deleted file mode 100644 index 385ecd0..0000000 --- a/tests/test_e2e_seller_buyer.py +++ /dev/null @@ -1,460 +0,0 @@ -""" -Full End-to-End Test: Seller → Buyer Flow. - -This tests the complete flow: -1. Seller starts server with protected endpoints -2. Buyer makes request to seller -3. Seller returns 402 (Payment Required) -4. Buyer parses 402, detects seller accepts "exact" (basic x402) -5. Buyer routes to basic x402 -6. Payment succeeds - -This tests WITHOUT Circle nanopayment first - just basic x402. - -Run: - # Terminal 1: Start seller server - python scripts/x402_simple_server.py - - # Terminal 2: Run this test - pytest tests/test_e2e_seller_buyer.py -v -s -""" - -import base64 -import json -import os -import signal -import subprocess -import sys -import time - -import httpx -import pytest - -# ============================================================================= -# CONFIGURATION -# ============================================================================= - -SELLER_SERVER = "http://127.0.0.1:4022" - -# Seller's address (matches the server) -SELLER_ADDRESS = "0x742d35Cc6634C0532925a3b844Bc9e7595f1E123" - -# Buyer's wallet (simulated) -BUYER_WALLET_ID = "test-buyer-wallet" -BUYER_ADDRESS = "0xAAAA1111BBBB2222CCCC3333DDDD4444EEEE5555" - - -# ============================================================================= -# STEP 1: Verify Server Running -# ============================================================================= - - -def is_server_running(): - """Check if test server is running.""" - try: - import socket - - sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - result = sock.connect_ex(("127.0.0.1", 4022)) - sock.close() - return result == 0 - except Exception: - return False - - -def require_server(): - """Assert server is running.""" - assert is_server_running(), "Test server is not running" - - -def _wait_for_server(timeout_seconds: float = 20.0) -> bool: - deadline = time.time() + timeout_seconds - while time.time() < deadline: - if is_server_running(): - return True - time.sleep(0.2) - return False - - -@pytest.fixture(scope="module", autouse=True) -def ensure_test_server(): - """Start the local x402 test server for this module when needed.""" - if is_server_running(): - yield - return - - process = subprocess.Popen( # noqa: S603 - [sys.executable, "scripts/x402_simple_server.py"], - stdout=subprocess.DEVNULL, - stderr=subprocess.DEVNULL, - preexec_fn=os.setsid if os.name != "nt" else None, - ) - try: - if not _wait_for_server(): - pytest.skip("x402 test server not available") - yield - finally: - if process.poll() is None: - if os.name == "nt": - process.terminate() - else: - os.killpg(os.getpgid(process.pid), signal.SIGTERM) - process.wait(timeout=10) - - -# ============================================================================= -# STEP 2: Test Flow - No Payment = 402 -# ============================================================================= - - -class TestStep1RequestWithoutPayment: - """Step 1: Buyer requests without payment → Seller returns 402.""" - - def test_seller_returns_402(self): - """Seller should return 402 when no payment provided.""" - print("\n" + "=" * 70) - print("STEP 1: Request Without Payment") - print("=" * 70) - print(" Buyer → GET /weather") - print(" Seller → 402 Payment Required") - - require_server() - - response = httpx.get(f"{SELLER_SERVER}/weather", timeout=5.0) - - print(f"\n Result: {response.status_code}") - - # Should get 402 - assert response.status_code == 402 - - # Should have payment-required header - assert "payment-required" in response.headers - - print(" ✓ Got 402 with payment-required header") - - return response - - -# ============================================================================= -# STEP 3: Parse 402 Response -# ============================================================================= - - -class TestStep2Parse402Response: - """Step 2: Buyer parses 402 to see what seller accepts.""" - - def test_parse_payment_required_header(self): - """Parse the 402 response to see accepted payment methods.""" - print("\n" + "=" * 70) - print("STEP 2: Parse 402 Response") - print("=" * 70) - - require_server() - - # Get 402 response - response = httpx.get(f"{SELLER_SERVER}/weather", timeout=5.0) - - # Parse header - header = response.headers["payment-required"] - decoded = json.loads(base64.b64decode(header)) - - print("\n Parsed 402 response:") - print(f" x402Version: {decoded.get('x402Version')}") - print(f" Error: {decoded.get('error')}") - - # Check accepts array - accepts = decoded.get("accepts", []) - print(f"\n Seller accepts {len(accepts)} payment scheme(s):") - - for accept in accepts: - scheme = accept.get("scheme") - amount = accept.get("amount") - network = accept.get("network") - print(f" - {scheme}: {amount} on {network}") - - # Verify structure - assert decoded.get("x402Version") == 2 - assert len(accepts) > 0 - - # Check what seller supports - schemes = [a.get("scheme") for a in accepts] - has_exact = "exact" in schemes - has_circle = "GatewayWalletBatched" in schemes - - print(f"\n Has 'exact' (basic x402): {has_exact}") - print(f" Has 'GatewayWalletBatched': {has_circle}") - - return schemes - - -# ============================================================================= -# STEP 4: Smart Routing Decision -# ============================================================================= - - -class TestStep3SmartRouting: - """Step 3: Buyer decides which payment method to use.""" - - def test_route_to_basic_x402(self): - """Based on 402 response, route to appropriate payment method.""" - print("\n" + "=" * 70) - print("STEP 3: Smart Routing Decision") - print("=" * 70) - - require_server() - - # Get seller's accepts - response = httpx.get(f"{SELLER_SERVER}/weather", timeout=5.0) - header = response.headers["payment-required"] - decoded = json.loads(base64.b64decode(header)) - - accepts = decoded.get("accepts", []) - schemes = [a.get("scheme") for a in accepts] - - print(f"\n Seller accepts: {schemes}") - - # BUYER'S DECISION LOGIC: - # 1. Does seller support Circle nanopayment? - supports_circle = "GatewayWalletBatched" in schemes - - # 2. Does buyer have Circle Gateway balance? - buyer_has_gateway = False # Let's say buyer doesn't have Gateway set up - - # 3. Route decision - if supports_circle and buyer_has_gateway: - method = "Circle Nanopayment (gasless)" - print(f" ✓ Route: {method}") - else: - method = "Basic x402 (on-chain)" - print(f" ✓ Route: {method}") - - # Since our test seller only supports "exact" (no Circle), - # and buyer has no Gateway, route to basic x402 - assert method == "Basic x402 (on-chain)" - - return method - - -# ============================================================================= -# STEP 5: Different Routes Have Different Prices -# ============================================================================= - - -class TestStep4DifferentPrices: - """Step 4: Verify different endpoints have different prices.""" - - def test_weather_price(self): - """Weather endpoint: $0.001""" - print("\n" + "=" * 70) - print("STEP 4: Different Prices") - print("=" * 70) - - require_server() - - response = httpx.get(f"{SELLER_SERVER}/weather", timeout=5.0) - header = response.headers["payment-required"] - decoded = json.loads(base64.b64decode(header)) - - amount = decoded["accepts"][0]["amount"] - # amount is in atomic units (6 decimals) - price_usd = int(amount) / 1000000 - - print(f"\n /weather: ${price_usd} ({amount} atomic)") - - assert price_usd == 0.001 - - def test_premium_price(self): - """Premium endpoint: $0.01""" - require_server() - - response = httpx.get(f"{SELLER_SERVER}/premium/content", timeout=5.0) - header = response.headers["payment-required"] - decoded = json.loads(base64.b64decode(header)) - - amount = decoded["accepts"][0]["amount"] - price_usd = int(amount) / 1000000 - - print(f" /premium/content: ${price_usd} ({amount} atomic)") - - assert price_usd == 0.01 - - -# ============================================================================= -# STEP 6: Network Compatibility -# ============================================================================= - - -class TestStep5NetworkCompatibility: - """Step 5: Verify buyer and seller are on same network.""" - - def test_same_network(self): - """Buyer and seller should be on same network.""" - print("\n" + "=" * 70) - print("STEP 5: Network Compatibility") - print("=" * 70) - - require_server() - - response = httpx.get(f"{SELLER_SERVER}/weather", timeout=5.0) - header = response.headers["payment-required"] - decoded = json.loads(base64.b64decode(header)) - - seller_network = decoded["accepts"][0]["network"] - buyer_network = "eip155:84532" # Base Sepolia - - print(f"\n Buyer network: {buyer_network}") - print(f" Seller network: {seller_network}") - - compatible = buyer_network == seller_network - print(f"\n Compatible: {compatible}") - - assert compatible - - -# ============================================================================= -# STEP 7: Full Simulation (with mocked payment) -# ============================================================================= - - -class TestStep6FullSimulation: - """Step 6: Simulate complete payment flow.""" - - @pytest.mark.asyncio - async def test_complete_flow_simulation(self): - """Simulate complete buyer → seller flow.""" - print("\n" + "=" * 70) - print("STEP 6: Complete Flow Simulation") - print("=" * 70) - - require_server() - - async with httpx.AsyncClient() as client: - # === STEP 1: Initial request === - print("\n [1] Buyer requests /weather") - response = await client.get(f"{SELLER_SERVER}/weather") - - if response.status_code != 402: - pytest.fail("Expected 402") - - print(" → Got 402 Payment Required") - - # === STEP 2: Parse 402 === - header = response.headers["payment-required"] - decoded = json.loads(base64.b64decode(header)) - - accepts = decoded["accepts"] - schemes = [a.get("scheme") for a in accepts] - - print("\n [2] Buyer parses 402") - print(f" Seller accepts: {schemes}") - - # === STEP 3: Route decision === - supports_circle = "GatewayWalletBatched" in schemes - - print("\n [3] Routing decision") - print(f" Supports Circle: {supports_circle}") - - route = "Circle Nanopayment" if supports_circle else "Basic x402" - - print(f" → Using: {route}") - - # === STEP 4: Create payment (simulated) === - print("\n [4] Create payment") - print(f" From: {BUYER_ADDRESS[:20]}...") - print(f" To: {SELLER_ADDRESS[:20]}...") - print(" Amount: 1000 atomic ($0.001)") - - # In real flow, buyer would: - # 1. Sign EIP-3009 authorization - # 2. Create payment payload - # 3. Send with PAYMENT-SIGNATURE header - - # === STEP 5: Send payment (will fail verification but shows flow) === - valid_after = int(time.time()) - 60 - valid_before = int(time.time()) + 300 - - authorization = { - "from": BUYER_ADDRESS, - "to": SELLER_ADDRESS, - "value": "1000", - "validAfter": str(valid_after), - "validBefore": str(valid_before), - "nonce": "0x1234567890abcdef", - } - - payload = { - "x402Version": 2, - "scheme": "exact", - "accepted": accepts[0], - "payload": { - "authorization": authorization, - "signature": "0xabcd... (real signature would be here)", - }, - } - - import base64 as b64 - - payment_header = b64.b64encode(json.dumps(payload).encode()).decode() - - print("\n [5] Send payment header") - print(f" Header length: {len(payment_header)} chars") - - # === STEP 6: Verify (shows what seller would do) === - print("\n [6] Seller verifies payment") - - # Check timeout - current = int(time.time()) - is_valid_time = valid_after <= current <= valid_before - print(f" Timeout OK: {is_valid_time}") - - # Check amount - expected = int(accepts[0]["amount"]) - paid = int(authorization["value"]) - is_valid_amount = paid >= expected - print(f" Amount OK: {is_valid_amount} ({paid} >= {expected})") - - # Check recipient - is_valid_recipient = authorization["to"].lower() == SELLER_ADDRESS.lower() - print(f" Recipient OK: {is_valid_recipient}") - - # === RESULT === - all_valid = is_valid_time and is_valid_amount and is_valid_recipient - - print("\n ✓ Flow completed successfully!") - print(f" Payment would be: {'VALID' if all_valid else 'INVALID'}") - - assert response.status_code == 402 - - -# ============================================================================= -# STEP 8: End-to-End with Real Server -# ============================================================================= - - -class TestStep7RealServerTest: - """Test against real running server.""" - - def test_health_endpoint(self): - """Verify server is running and healthy.""" - print("\n" + "=" * 70) - print("REAL SERVER TEST: Health Check") - print("=" * 70) - - require_server() - - response = httpx.get(f"{SELLER_SERVER}/", timeout=5.0) - - print(f"\n Status: {response.status_code}") - - # Any response means server is running - assert response.status_code in [200, 404] - - print(" ✓ Server is running") - - -# ============================================================================= -# RUN ALL -# ============================================================================= - -if __name__ == "__main__": - pytest.main([__file__, "-v", "-s"]) diff --git a/tests/test_exact_facilitator_app.py b/tests/test_exact_facilitator_app.py deleted file mode 100644 index e30c749..0000000 --- a/tests/test_exact_facilitator_app.py +++ /dev/null @@ -1,153 +0,0 @@ -from __future__ import annotations - -from fastapi.testclient import TestClient - -from omniclaw.facilitator.exact import ( - ExactFacilitatorConfig, - _normalize_tx_hash, - create_exact_facilitator_app, -) - - -class _FakeResult: - def __init__(self, data): - self._data = data - - def model_dump(self, by_alias: bool = True, exclude_none: bool = True): - return dict(self._data) - - -class _FakeFacilitator: - def get_supported(self): - return _FakeResult({"kinds": [{"scheme": "exact", "network": "eip155:84532"}]}) - - async def verify(self, payload, requirements): - return _FakeResult({"isValid": True, "payer": "0xabc"}) - - async def settle(self, payload, requirements): - return _FakeResult({"success": True, "transaction": "0xsettled"}) - - -def test_normalize_tx_hash_adds_prefix_when_missing(): - assert _normalize_tx_hash("abc123") == "0xabc123" - assert _normalize_tx_hash("0xabc123") == "0xabc123" - - -def test_create_exact_facilitator_app_registers_networks(): - recorded = {} - - def fake_signer_factory(**kwargs): - recorded["signer_kwargs"] = kwargs - return object() - - def fake_register(facilitator, *, signer, networks): - recorded["networks"] = networks - recorded["signer"] = signer - - app = create_exact_facilitator_app( - ExactFacilitatorConfig( - private_key="0x123", - rpc_url="https://rpc.example", - networks=("eip155:84532", "arc:testnet"), - title="Test Facilitator", - ), - signer_factory=fake_signer_factory, - facilitator_factory=_FakeFacilitator, - register_facilitator=fake_register, - ) - - assert app.title == "Test Facilitator" - assert recorded["signer_kwargs"] == { - "private_key": "0x123", - "rpc_url": "https://rpc.example", - } - assert recorded["networks"] == ["eip155:84532", "arc:testnet"] - - -def test_exact_facilitator_app_routes_work(): - app = create_exact_facilitator_app( - ExactFacilitatorConfig( - private_key="0x123", - rpc_url="https://rpc.example", - networks=("eip155:84532",), - ), - signer_factory=lambda **kwargs: object(), - facilitator_factory=_FakeFacilitator, - register_facilitator=lambda facilitator, *, signer, networks: None, - ) - - client = TestClient(app) - - supported = client.get("/supported") - assert supported.status_code == 200 - assert supported.json()["kinds"][0]["scheme"] == "exact" - - payload = { - "x402Version": 2, - "paymentPayload": { - "x402Version": 2, - "payload": { - "signature": "0x1234", - "authorization": { - "from": "0x1111111111111111111111111111111111111111", - "to": "0x2222222222222222222222222222222222222222", - "value": "250000", - "validAfter": "0", - "validBefore": "9999999999", - "nonce": "0x" + "11" * 32, - }, - }, - "accepted": { - "scheme": "exact", - "network": "eip155:84532", - "asset": "0x036CbD53842c5426634e7929541eC2318f3dCF7e", - "amount": "250000", - "payTo": "0x2222222222222222222222222222222222222222", - "maxTimeoutSeconds": 300, - "extra": {"name": "USDC", "version": "2"}, - }, - }, - "paymentRequirements": { - "scheme": "exact", - "network": "eip155:84532", - "asset": "0x036CbD53842c5426634e7929541eC2318f3dCF7e", - "amount": "250000", - "payTo": "0x2222222222222222222222222222222222222222", - "maxTimeoutSeconds": 300, - "resource": "http://127.0.0.1:4021/compute?size=74000", - "description": "compute", - "mimeType": "application/json", - "outputSchema": None, - "extra": {"name": "USDC", "version": "2"}, - }, - } - - verify = client.post("/verify", json=payload) - assert verify.status_code == 200 - assert verify.json() == {"isValid": True, "payer": "0xabc"} - - settle = client.post("/settle", json=payload) - assert settle.status_code == 200 - assert settle.json() == {"success": True, "transaction": "0xsettled"} - - -def test_exact_facilitator_app_rejects_wrong_version(): - app = create_exact_facilitator_app( - ExactFacilitatorConfig( - private_key="0x123", - rpc_url="https://rpc.example", - networks=("eip155:84532",), - ), - signer_factory=lambda **kwargs: object(), - facilitator_factory=_FakeFacilitator, - register_facilitator=lambda facilitator, *, signer, networks: None, - ) - - client = TestClient(app) - response = client.post( - "/verify", - json={"x402Version": 1, "paymentPayload": {}, "paymentRequirements": {}}, - ) - - assert response.status_code == 400 - assert response.json()["detail"] == "Only x402Version=2 is supported" diff --git a/tests/test_exact_network_profiles.py b/tests/test_exact_network_profiles.py deleted file mode 100644 index 395b9a7..0000000 --- a/tests/test_exact_network_profiles.py +++ /dev/null @@ -1,72 +0,0 @@ -from __future__ import annotations - -from decimal import Decimal - -from x402.schemas import AssetAmount - -from omniclaw.facilitator.exact import load_exact_facilitator_config_from_env -from omniclaw.facilitator.networks import ( - build_exact_asset_amount, - resolve_exact_settlement_network_profile, -) - - -def test_resolve_arc_exact_network_profile(): - profile = resolve_exact_settlement_network_profile("ARC-TESTNET") - - assert profile.label == "ARC-TESTNET" - assert profile.caip2 == "eip155:5042002" - assert profile.default_rpc_url == "https://rpc.testnet.arc.network" - assert profile.explorer_base_url == "https://testnet.arcscan.app/tx/" - assert profile.default_asset_address == "0x3600000000000000000000000000000000000000" - - -def test_resolve_base_sepolia_exact_network_profile(): - profile = resolve_exact_settlement_network_profile("BASE-SEPOLIA") - - assert profile.label == "BASE-SEPOLIA" - assert profile.caip2 == "eip155:84532" - assert profile.default_rpc_url == "https://sepolia.base.org" - - -def test_load_exact_facilitator_config_uses_profile_defaults(monkeypatch): - monkeypatch.setenv("OMNICLAW_X402_FACILITATOR_NETWORK_PROFILE", "ARC-TESTNET") - monkeypatch.setenv( - "OMNICLAW_X402_FACILITATOR_PRIVATE_KEY", - "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", - ) - monkeypatch.delenv("OMNICLAW_X402_FACILITATOR_RPC_URL", raising=False) - monkeypatch.delenv("OMNICLAW_X402_FACILITATOR_NETWORKS", raising=False) - - config = load_exact_facilitator_config_from_env() - - assert config.network_profile == "ARC-TESTNET" - assert config.rpc_url == "https://rpc.testnet.arc.network" - assert config.networks == ("eip155:5042002",) - - -def test_build_exact_asset_amount_for_arc(): - profile = resolve_exact_settlement_network_profile("ARC-TESTNET") - - result = build_exact_asset_amount( - profile=profile, - decimal_amount=Decimal("0.25"), - network="eip155:5042002", - ) - - assert isinstance(result, AssetAmount) - assert result.amount == "250000" - assert result.asset == "0x3600000000000000000000000000000000000000" - assert result.extra == {"name": "USDC", "version": "2"} - - -def test_build_exact_asset_amount_ignores_other_networks(): - profile = resolve_exact_settlement_network_profile("ARC-TESTNET") - - result = build_exact_asset_amount( - profile=profile, - decimal_amount="0.25", - network="eip155:84532", - ) - - assert result is None diff --git a/tests/test_facilitator_e2e.py b/tests/test_facilitator_e2e.py deleted file mode 100644 index c0eb7ae..0000000 --- a/tests/test_facilitator_e2e.py +++ /dev/null @@ -1,563 +0,0 @@ -""" -Real end-to-end test with mock facilitator server. - -This tests the actual HTTP flow to verify the facilitator integration works. -""" - -import json - -import httpx -import pytest - -from omniclaw.seller import create_facilitator, create_seller - - -def json_from_request(request: httpx.Request): - return json.loads(request.content.decode()) - - -class MockFacilitatorServer: - """Mock facilitator server for testing.""" - - def __init__(self, should_fail_verify=False, should_fail_settle=False): - self.should_fail_verify = should_fail_verify - self.should_fail_settle = should_fail_settle - self.verify_called = False - self.settle_called = False - - async def handle_verify(self, request): - self.verify_called = True - if self.should_fail_verify: - return httpx.Response( - 400, - json={ - "isValid": False, - "invalidReason": "insufficient_balance", - "payer": "0xbuyer", - }, - ) - return httpx.Response( - 200, json={"isValid": True, "payer": "0xbuyer1234567890abcdef1234567890abcdef12"} - ) - - async def handle_settle(self, request): - self.settle_called = True - if self.should_fail_settle: - return httpx.Response( - 400, json={"success": False, "errorReason": "invalid_signature", "transaction": ""} - ) - return httpx.Response( - 200, - json={ - "success": True, - "transaction": "tx_abc123", - "network": "eip155:84532", - "payer": "0xbuyer1234567890abcdef1234567890abcdef12", - }, - ) - - -@pytest.mark.asyncio -async def test_facilitator_verify_endpoint(): - """Test that facilitator verify endpoint is called correctly.""" - - # Create real facilitator with mock transport - facilitator = create_facilitator(provider="circle", api_key="test_key") - - # Test verify call with mock - payment_payload = { - "x402Version": 2, - "scheme": "exact", - "payload": { - "authorization": { - "from": "0xbuyer", - "to": "0xseller", - "value": "1000", - "validAfter": 0, - "validBefore": 9999999999, - }, - "signature": "0xsig", - }, - } - - payment_requirements = { - "scheme": "exact", - "network": "eip155:84532", - "asset": "0xUSDC", - "amount": "1000", - "payTo": "0xseller", - "maxTimeoutSeconds": 300, - } - - # The verify method should return a result (network call would fail with test key) - result = await facilitator.verify(payment_payload, payment_requirements) - - # Result should have the expected structure - assert hasattr(result, "is_valid") - assert hasattr(result, "payer") - assert hasattr(result, "invalid_reason") - - -@pytest.mark.asyncio -async def test_facilitator_settle_endpoint(): - """Test that facilitator settle endpoint is called correctly.""" - - facilitator = create_facilitator(provider="circle", api_key="test_key") - - payment_payload = { - "x402Version": 2, - "scheme": "exact", - "payload": { - "authorization": { - "from": "0xbuyer", - "to": "0xseller", - "value": "1000", - }, - "signature": "0xsig", - }, - } - - payment_requirements = { - "scheme": "exact", - "network": "eip155:84532", - "amount": "1000", - "payTo": "0xseller", - } - - result = await facilitator.settle(payment_payload, payment_requirements) - - # Result should have the expected structure - assert hasattr(result, "success") - assert hasattr(result, "transaction") - assert hasattr(result, "network") - assert hasattr(result, "error_reason") - - -def test_all_facilitators_have_correct_interface(): - """Verify all facilitators implement the same interface.""" - - providers = ["circle", "coinbase", "ordern", "rbx", "thirdweb", "omniclaw"] - - for provider in providers: - f = create_facilitator(provider=provider, api_key="test_key") - - # Check all required properties exist - assert hasattr(f, "name"), f"{provider} missing name property" - assert hasattr(f, "base_url"), f"{provider} missing base_url property" - assert hasattr(f, "environment"), f"{provider} missing environment property" - - # Check all required methods exist - assert hasattr(f, "verify"), f"{provider} missing verify method" - assert hasattr(f, "settle"), f"{provider} missing settle method" - assert hasattr(f, "get_supported_networks"), ( - f"{provider} missing get_supported_networks method" - ) - assert hasattr(f, "close"), f"{provider} missing close method" - - # Check name matches - assert f.name == provider, f"{provider} name should be {provider}" - - -def test_facilitator_urls_for_testnet(): - """Verify all facilitators have correct testnet URLs.""" - - facilitators = { - "circle": "https://gateway-api-testnet.circle.com", - "coinbase": "https://api.cdp.coinbase.com/platform", - "ordern": "https://api.testnet.ordern.ai", - "rbx": "https://api.testnet.rbx.io", - "thirdweb": "https://api.thirdweb.com", - "omniclaw": "http://127.0.0.1:4022", - } - - for provider, expected_url in facilitators.items(): - f = create_facilitator(provider=provider, api_key="test_key", environment="testnet") - assert f.base_url == expected_url, f"{provider}: expected {expected_url}, got {f.base_url}" - - -def test_facilitator_urls_for_mainnet(): - """Verify all facilitators have correct mainnet URLs.""" - - facilitators = { - "circle": "https://gateway-api.circle.com", - "coinbase": "https://api.cdp.coinbase.com/platform", - "ordern": "https://api.ordern.ai", - "rbx": "https://api.rbx.io", - "thirdweb": "https://api.thirdweb.com", - "omniclaw": "http://127.0.0.1:4022", - } - - for provider, expected_url in facilitators.items(): - f = create_facilitator(provider=provider, api_key="test_key", environment="mainnet") - assert f.base_url == expected_url, f"{provider}: expected {expected_url}, got {f.base_url}" - - -def test_thirdweb_uses_native_secret_env(monkeypatch): - """Thirdweb should not require aliasing its secret key to FACILITATOR_API_KEY.""" - monkeypatch.delenv("FACILITATOR_API_KEY", raising=False) - monkeypatch.delenv("CIRCLE_API_KEY", raising=False) - monkeypatch.setenv("THIRDWEB_SECRET_KEY", "thirdweb_secret") - - facilitator = create_facilitator(provider="thirdweb") - - assert facilitator._api_key == "thirdweb_secret" - - -@pytest.mark.asyncio -async def test_thirdweb_accepts_uses_public_http_api(): - """Thirdweb seller requirements must come from the documented accepts API.""" - facilitator = create_facilitator( - provider="thirdweb", - api_key="thirdweb_secret", - server_wallet_address="0x" + "a" * 40, - default_network="base-sepolia", - ) - requests = [] - - async def handler(request: httpx.Request) -> httpx.Response: - requests.append(request) - assert str(request.url) == "https://api.thirdweb.com/v1/payments/x402/accepts" - assert request.headers["x-secret-key"] == "thirdweb_secret" - body = json_from_request(request) - assert body["resourceUrl"] == "https://seller.example.com/compute" - assert body["method"] == "GET" - assert body["network"] == "base-sepolia" - assert body["price"] == "$0.01" - assert body["serverWalletAddress"] == "0x" + "a" * 40 - return httpx.Response( - 200, - json={ - "result": { - "accepts": [ - { - "scheme": "exact", - "network": "eip155:84532", - "amount": "10000", - "payTo": "0x" + "b" * 40, - "asset": "0x036CbD53842c5426634e7929541eC2318f3dCF7e", - } - ] - } - }, - ) - - facilitator._client = httpx.AsyncClient(transport=httpx.MockTransport(handler)) - try: - accepts = await facilitator.create_accepts( - resource_url="https://seller.example.com/compute", - method="GET", - price="$0.01", - ) - finally: - await facilitator.close() - - assert len(requests) == 1 - assert accepts[0]["scheme"] == "exact" - assert accepts[0]["network"] == "eip155:84532" - - -@pytest.mark.asyncio -async def test_thirdweb_verify_uses_public_http_api(): - """Thirdweb integration must use the public HTTP API directly.""" - facilitator = create_facilitator(provider="thirdweb", api_key="thirdweb_secret") - requests = [] - - async def handler(request: httpx.Request) -> httpx.Response: - requests.append(request) - assert str(request.url) == "https://api.thirdweb.com/v1/payments/x402/verify" - assert request.headers["x-secret-key"] == "thirdweb_secret" - body = json_from_request(request) - assert body["paymentPayload"]["signature"] == "0xsig" - assert body["paymentRequirements"]["network"] == "eip155:84532" - return httpx.Response( - 200, - json={ - "result": { - "isValid": True, - "payer": "0xbuyer1234567890abcdef1234567890abcdef12", - } - }, - ) - - facilitator._client = httpx.AsyncClient(transport=httpx.MockTransport(handler)) - try: - result = await facilitator.verify( - {"signature": "0xsig"}, - {"network": "eip155:84532", "amount": "1000"}, - ) - finally: - await facilitator.close() - - assert len(requests) == 1 - assert result.is_valid is True - assert result.payer == "0xbuyer1234567890abcdef1234567890abcdef12" - - -@pytest.mark.asyncio -async def test_thirdweb_settle_uses_public_http_api(): - """Thirdweb settle must call the documented HTTP endpoint with waitUntil.""" - facilitator = create_facilitator(provider="thirdweb", api_key="thirdweb_secret") - requests = [] - - async def handler(request: httpx.Request) -> httpx.Response: - requests.append(request) - assert str(request.url) == "https://api.thirdweb.com/v1/payments/x402/settle" - assert request.headers["x-secret-key"] == "thirdweb_secret" - body = json_from_request(request) - assert body["paymentPayload"]["signature"] == "0xsig" - assert body["paymentRequirements"]["network"] == "eip155:84532" - assert body["waitUntil"] == "confirmed" - return httpx.Response( - 200, - json={ - "result": { - "success": True, - "transaction": "0xsettled", - "network": "eip155:84532", - "payer": "0xbuyer1234567890abcdef1234567890abcdef12", - } - }, - ) - - facilitator._client = httpx.AsyncClient(transport=httpx.MockTransport(handler)) - try: - result = await facilitator.settle( - {"signature": "0xsig"}, - {"network": "eip155:84532", "amount": "1000"}, - ) - finally: - await facilitator.close() - - assert len(requests) == 1 - assert result.success is True - assert result.transaction == "0xsettled" - assert result.network == "eip155:84532" - - -@pytest.mark.asyncio -async def test_thirdweb_fetch_uses_public_http_api(): - """Thirdweb fetch support should use the documented HTTP endpoint.""" - facilitator = create_facilitator(provider="thirdweb", api_key="thirdweb_secret") - requests = [] - - async def handler(request: httpx.Request) -> httpx.Response: - requests.append(request) - assert str(request.url).startswith("https://api.thirdweb.com/v1/payments/x402/fetch?") - assert request.headers["x-secret-key"] == "thirdweb_secret" - params = dict(request.url.params) - assert params["url"] == "https://seller.example.com/compute" - assert params["from"] == "0x" + "a" * 40 - assert params["chainId"] == "eip155:84532" - return httpx.Response(200, json={"result": {"status": 200, "body": {"ok": True}}}) - - facilitator._client = httpx.AsyncClient(transport=httpx.MockTransport(handler)) - try: - result = await facilitator.fetch_with_payment( - url="https://seller.example.com/compute", - from_address="0x" + "a" * 40, - chain_id="eip155:84532", - ) - finally: - await facilitator.close() - - assert len(requests) == 1 - assert result["status"] == 200 - assert result["body"]["ok"] is True - - -@pytest.mark.asyncio -async def test_thirdweb_discovery_resources_uses_public_http_api(): - """Thirdweb discovery support should use the documented resources endpoint.""" - facilitator = create_facilitator(provider="thirdweb", api_key="thirdweb_secret") - requests = [] - - async def handler(request: httpx.Request) -> httpx.Response: - requests.append(request) - assert str(request.url).startswith( - "https://api.thirdweb.com/v1/payments/x402/discovery/resources" - ) - assert request.headers["x-secret-key"] == "thirdweb_secret" - assert dict(request.url.params)["network"] == "eip155:84532" - return httpx.Response(200, json={"result": {"resources": [{"url": "https://api"}]}}) - - facilitator._client = httpx.AsyncClient(transport=httpx.MockTransport(handler)) - try: - result = await facilitator.discover_resources(network="eip155:84532") - finally: - await facilitator.close() - - assert len(requests) == 1 - assert result["resources"][0]["url"] == "https://api" - - -def test_omniclaw_self_hosted_does_not_require_api_key(monkeypatch): - monkeypatch.delenv("FACILITATOR_API_KEY", raising=False) - monkeypatch.delenv("CIRCLE_API_KEY", raising=False) - - facilitator = create_facilitator( - provider="omniclaw", - base_url="http://127.0.0.1:4022", - network_profile="ARC-TESTNET", - ) - - assert facilitator.name == "omniclaw" - assert facilitator.base_url == "http://127.0.0.1:4022" - - -@pytest.mark.asyncio -async def test_omniclaw_self_hosted_creates_accepts_for_arc(): - facilitator = create_facilitator( - provider="omniclaw", - base_url="http://127.0.0.1:4022", - network_profile="ARC-TESTNET", - ) - try: - accepts = await facilitator.create_accepts( - resource_url="https://vendor.example.com/compute", - method="GET", - price="$0.25", - server_wallet_address="0x" + "a" * 40, - ) - finally: - await facilitator.close() - - assert accepts == [ - { - "scheme": "exact", - "network": "eip155:5042002", - "asset": "0x3600000000000000000000000000000000000000", - "amount": "250000", - "payTo": "0x" + "a" * 40, - "maxTimeoutSeconds": 300, - "extra": {"name": "USDC", "version": "2"}, - } - ] - - -@pytest.mark.asyncio -async def test_omniclaw_self_hosted_verify_and_settle_use_local_api(): - facilitator = create_facilitator( - provider="omniclaw", - base_url="http://127.0.0.1:4022", - network_profile="ARC-TESTNET", - ) - requests = [] - - async def handler(request: httpx.Request) -> httpx.Response: - requests.append(request) - body = json_from_request(request) - assert body["x402Version"] == 2 - assert body["paymentPayload"]["signature"] == "0xsig" - assert body["paymentRequirements"]["network"] == "eip155:5042002" - if str(request.url).endswith("/verify"): - return httpx.Response(200, json={"isValid": True, "payer": "0x" + "b" * 40}) - if str(request.url).endswith("/settle"): - return httpx.Response( - 200, - json={ - "success": True, - "transaction": "0xsettled", - "network": "eip155:5042002", - "payer": "0x" + "b" * 40, - }, - ) - raise AssertionError(f"unexpected URL {request.url}") - - facilitator._client = httpx.AsyncClient(transport=httpx.MockTransport(handler)) - try: - verify = await facilitator.verify( - {"signature": "0xsig"}, - {"network": "eip155:5042002"}, - ) - settle = await facilitator.settle( - {"signature": "0xsig"}, - {"network": "eip155:5042002"}, - ) - finally: - await facilitator.close() - - assert len(requests) == 2 - assert verify.is_valid is True - assert settle.success is True - assert settle.transaction == "0xsettled" - - -def test_circle_facilitator_custom_base_url_override(): - """Circle facilitator must honor explicit base_url overrides.""" - custom = "https://gateway-proxy.internal.example" - f = create_facilitator( - provider="circle", - api_key="test_key", - environment="testnet", - base_url=custom, - ) - assert f.base_url == custom - - -def test_seller_with_each_facilitator(): - """Test seller works with each facilitator type.""" - - sellers = {} - - for provider in ["circle", "coinbase", "ordern", "rbx", "thirdweb", "omniclaw"]: - facilitator = create_facilitator(provider=provider, api_key="test_key") - - seller = create_seller( - seller_address="0x742d35Cc6634C0532925a3b844Bc9e7595f1E123", - name=f"Test {provider}", - facilitator=facilitator, - ) - - sellers[provider] = seller - - # Verify facilitator is set - assert seller._facilitator is not None - assert seller._facilitator.name == provider - - print(f"Successfully created sellers for: {', '.join(sellers.keys())}") - - -def test_seller_auto_creates_facilitator(): - """Test seller auto-creates facilitator from API key.""" - - seller = create_seller( - seller_address="0x742d35Cc6634C0532925a3b844Bc9e7595f1E123", - name="Test Seller", - circle_api_key="test_key_123", - ) - - assert seller._facilitator is not None - assert seller._facilitator.name == "circle" - assert seller._facilitator.environment == "testnet" - - -def test_create_facilitator_requires_api_key(): - """Test that creating facilitator without API key fails.""" - - import os - - # Save original env - orig_facilitator = os.environ.get("FACILITATOR_API_KEY") - orig_circle = os.environ.get("CIRCLE_API_KEY") - - # Remove env vars - if orig_facilitator: - del os.environ["FACILITATOR_API_KEY"] - if orig_circle: - del os.environ["CIRCLE_API_KEY"] - - try: - from omniclaw.seller import create_facilitator - - with pytest.raises(ValueError, match="api_key"): - create_facilitator(provider="circle", api_key=None) - finally: - # Restore env - if orig_facilitator: - os.environ["FACILITATOR_API_KEY"] = orig_facilitator - if orig_circle: - os.environ["CIRCLE_API_KEY"] = orig_circle - - -if __name__ == "__main__": - pytest.main([__file__, "-v"]) diff --git a/tests/test_facilitator_integration.py b/tests/test_facilitator_integration.py deleted file mode 100644 index 2446ca1..0000000 --- a/tests/test_facilitator_integration.py +++ /dev/null @@ -1,348 +0,0 @@ -""" -End-to-end test for Circle Gateway Facilitator integration. - -This tests the complete flow: -1. Seller creates with facilitator -2. Client makes request to protected endpoint -3. Seller returns 402 with payment requirements -4. Client pays (simulated) -5. Seller verifies via facilitator -6. Seller settles via facilitator -""" - -from unittest.mock import Mock - -import pytest - -from omniclaw.seller import CircleGatewayFacilitator, create_seller -from omniclaw.seller.facilitator import SettleResult, VerifyResult - - -def create_mock_facilitator(verify_result=None, settle_result=None): - """Create a mock facilitator with configurable results.""" - - async def mock_verify(payload, requirements): - if verify_result: - return verify_result - return VerifyResult( - is_valid=True, - payer="0xbuyer1234567890abcdef1234567890abcdef12", - invalid_reason=None, - ) - - async def mock_settle(payload, requirements): - if settle_result: - return settle_result - return SettleResult( - success=True, - transaction="tx_123456", - network="eip155:84532", - error_reason=None, - payer="0xbuyer1234567890abcdef1234567890abcdef12", - ) - - facilitator = Mock(spec=CircleGatewayFacilitator) - facilitator.verify = mock_verify - facilitator.settle = mock_settle - facilitator.base_url = "https://gateway-api-testnet.circle.com" - facilitator.environment = "testnet" - return facilitator - - -class TestFacilitatorIntegration: - """Test facilitator integration end-to-end.""" - - def test_seller_created_with_facilitator(self): - """Test seller has facilitator configured.""" - facilitator = create_mock_facilitator() - seller = create_seller( - seller_address="0x742d35Cc6634C0532925a3b844Bc9e7595f1E123", - name="Weather API", - facilitator=facilitator, - ) - assert seller._facilitator is not None - - def test_seller_without_facilitator(self): - """Test seller without facilitator works.""" - seller = create_seller( - seller_address="0x742d35Cc6634C0532925a3b844Bc9e7595f1E123", - name="Weather API", - ) - assert seller._facilitator is None - - def test_verify_payment_with_facilitator(self): - """Test payment verification via facilitator.""" - facilitator = create_mock_facilitator() - seller = create_seller( - seller_address="0x742d35Cc6634C0532925a3b844Bc9e7595f1E123", - name="Weather API", - facilitator=facilitator, - ) - - payment_payload = { - "x402Version": 2, - "scheme": "exact", - "payload": { - "authorization": { - "from": "0xbuyer1234567890abcdef1234567890abcdef12", - "to": "0x742d35Cc6634C0532925a3b844Bc9e7595f1E123", - "value": "1000", - "validAfter": 0, - "validBefore": 9999999999, - "nonce": "0x" + "00" * 32, - }, - "signature": "0xabc123...", - }, - } - - accepted = { - "scheme": "exact", - "network": "eip155:84532", - "asset": "0x036CbD53842c5426634e7929541eC2318f3dCF7e", - "amount": "1000", - "payTo": "0x742d35Cc6634C0532925a3b844Bc9e7595f1E123", - "maxTimeoutSeconds": 345600, - } - payment_payload["network"] = accepted["network"] - payment_payload["accepted"] = accepted - - is_valid, error, record = seller.verify_payment( - payment_payload=payment_payload, - accepted=accepted, - verify_signature=False, - settle_payment=False, - ) - - assert is_valid is True - assert error == "" - assert record is not None - assert record.buyer_address == "0xbuyer1234567890abcdef1234567890abcdef12" - assert record.status.value == "verified" - - def test_settle_payment_with_facilitator(self): - """Test payment settlement via facilitator.""" - facilitator = create_mock_facilitator() - seller = create_seller( - seller_address="0x742d35Cc6634C0532925a3b844Bc9e7595f1E123", - name="Weather API", - facilitator=facilitator, - ) - - payment_payload = { - "x402Version": 2, - "scheme": "exact", - "payload": { - "authorization": { - "from": "0xbuyer1234567890abcdef1234567890abcdef12", - "to": "0x742d35Cc6634C0532925a3b844Bc9e7595f1E123", - "value": "1000", - "validAfter": 0, - "validBefore": 9999999999, - "nonce": "0x" + "00" * 32, - }, - "signature": "0xabc123...", - }, - } - - accepted = { - "scheme": "exact", - "network": "eip155:84532", - "asset": "0x036CbD53842c5426634e7929541eC2318f3dCF7e", - "amount": "1000", - "payTo": "0x742d35Cc6634C0532925a3b844Bc9e7595f1E123", - "maxTimeoutSeconds": 345600, - } - payment_payload["network"] = accepted["network"] - payment_payload["accepted"] = accepted - - is_valid, error, record = seller.verify_payment( - payment_payload=payment_payload, - accepted=accepted, - verify_signature=False, - settle_payment=True, - ) - - assert is_valid is True - assert error == "" - assert record is not None - assert record.status.value == "settled" - - def test_facilitator_verification_failure(self): - """Test facilitator returns invalid.""" - facilitator = create_mock_facilitator( - verify_result=VerifyResult( - is_valid=False, - payer="0xbuyer123", - invalid_reason="insufficient_balance", - ) - ) - seller = create_seller( - seller_address="0x742d35Cc6634C0532925a3b844Bc9e7595f1E123", - name="Weather API", - facilitator=facilitator, - ) - - payment_payload = { - "x402Version": 2, - "scheme": "exact", - "payload": { - "authorization": { - "from": "0xbuyer123", - "to": "0x742d35Cc6634C0532925a3b844Bc9e7595f1E123", - "value": "1000", - "validAfter": 0, - "validBefore": 9999999999, - "nonce": "0x" + "00" * 32, - }, - "signature": "0xabc123...", - }, - } - - accepted = { - "scheme": "exact", - "network": "eip155:84532", - "amount": "1000", - "payTo": "0x742d35Cc6634C0532925a3b844Bc9e7595f1E123", - } - payment_payload["network"] = accepted["network"] - payment_payload["accepted"] = accepted - - is_valid, error, record = seller.verify_payment( - payment_payload=payment_payload, - accepted=accepted, - settle_payment=False, - ) - - assert is_valid is False - assert "insufficient_balance" in error - assert record is None - - def test_facilitator_settlement_failure(self): - """Test facilitator settlement fails.""" - facilitator = create_mock_facilitator( - settle_result=SettleResult( - success=False, - transaction="", - network="eip155:84532", - error_reason="invalid_signature", - payer="0xbuyer123", - ) - ) - seller = create_seller( - seller_address="0x742d35Cc6634C0532925a3b844Bc9e7595f1E123", - name="Weather API", - facilitator=facilitator, - ) - - payment_payload = { - "x402Version": 2, - "scheme": "exact", - "payload": { - "authorization": { - "from": "0xbuyer123", - "to": "0x742d35Cc6634C0532925a3b844Bc9e7595f1E123", - "value": "1000", - "validAfter": 0, - "validBefore": 9999999999, - "nonce": "0x" + "00" * 32, - }, - "signature": "0xinvalid...", - }, - } - - accepted = { - "scheme": "exact", - "network": "eip155:84532", - "amount": "1000", - "payTo": "0x742d35Cc6634C0532925a3b844Bc9e7595f1E123", - } - payment_payload["network"] = accepted["network"] - payment_payload["accepted"] = accepted - - is_valid, error, record = seller.verify_payment( - payment_payload=payment_payload, - accepted=accepted, - settle_payment=True, - ) - - assert is_valid is False - assert "Settlement failed" in error - assert "invalid_signature" in error - assert record is None - - -class TestFacilitatorFactory: - """Test facilitator factory function.""" - - def test_create_facilitator_testnet(self): - """Test creating facilitator for testnet.""" - from omniclaw.seller import create_facilitator - - facilitator = create_facilitator( - provider="circle", - api_key="test_key", - environment="testnet", - ) - - assert facilitator.base_url == "https://gateway-api-testnet.circle.com" - assert facilitator.environment == "testnet" - - def test_create_facilitator_mainnet(self): - """Test creating facilitator for mainnet.""" - from omniclaw.seller import create_facilitator - - facilitator = create_facilitator( - provider="circle", - api_key="test_key", - environment="mainnet", - ) - - assert facilitator.base_url == "https://gateway-api.circle.com" - assert facilitator.environment == "mainnet" - - def test_create_facilitator_requires_api_key(self): - """Test facilitator requires API key.""" - from omniclaw.seller import create_facilitator - - with pytest.raises(ValueError, match="api_key"): - create_facilitator(provider="circle", api_key="") - - def test_create_all_facilitators(self): - """Test creating all supported facilitators.""" - from omniclaw.seller import SUPPORTED_FACILITATORS, create_facilitator - - for name in SUPPORTED_FACILITATORS: - f = create_facilitator(provider=name, api_key="test_key") - assert f.name == name, f"Expected {name}, got {f.name}" - - -class TestSellerAutoFacilitator: - """Test seller auto-creates facilitator from API key.""" - - def test_seller_with_circle_api_key(self): - """Test seller auto-creates facilitator when API key provided.""" - seller = create_seller( - seller_address="0x742d35Cc6634C0532925a3b844Bc9e7595f1E123", - name="Weather API", - circle_api_key="test_key_123", - ) - - assert seller._facilitator is not None - assert seller._facilitator.environment == "testnet" - - def test_seller_with_mainnet_environment(self): - """Test seller with mainnet environment.""" - seller = create_seller( - seller_address="0x742d35Cc6634C0532925a3b844Bc9e7595f1E123", - name="Weather API", - circle_api_key="test_key_123", - facilitator_environment="mainnet", - ) - - assert seller._facilitator is not None - assert seller._facilitator.environment == "mainnet" - assert seller._facilitator.base_url == "https://gateway-api.circle.com" - - -if __name__ == "__main__": - pytest.main([__file__, "-v"]) diff --git a/tests/test_facilitator_live_integration.py b/tests/test_facilitator_live_integration.py deleted file mode 100644 index e7276d8..0000000 --- a/tests/test_facilitator_live_integration.py +++ /dev/null @@ -1,537 +0,0 @@ -""" -Real integration tests for x402 facilitators. - -These tests hit actual facilitator APIs and require valid API keys. -Run with: pytest tests/test_facilitator_live_integration.py -v - -For CI/local testing without keys, use environment variables: -- COINBASE_API_KEY -- ORDERN_API_KEY -- RBX_API_KEY -- THIRDWEB_API_KEY -- CIRCLE_API_KEY (for Circle facilitator) - -Each test can be run individually: -- pytest tests/test_facilitator_live_integration.py::test_coinbase_verify -v -- pytest tests/test_facilitator_live_integration.py::test_ordern_verify -v -- etc. -""" - -import os - -import pytest - -# Test payment payload/requirements for verification -TEST_PAYLOAD = { - "x402Version": 2, - "scheme": "exact", - "payload": { - "authorization": { - "from": "0x742d35Cc6634C0532925a3b844Bc9e7595f1E123", - "to": "0x742d35Cc6634C0532925a3b844Bc9e7595f5e4a0", - "value": "1000000", # 1 USDC (6 decimals) - "validAfter": 0, - "validBefore": 9999999999, - }, - "signature": "0xsig", - }, -} - -TEST_REQUIREMENTS = { - "scheme": "exact", - "network": "eip155:84532", - "asset": "0x4AE85d4018745B8C52bfec71E7f8Ca34E9E3c8A7", # USDC on Base - "amount": "1000000", - "payTo": "0x742d35Cc6634C0532925a3b844Bc9e7595f5e4a0", - "maxTimeoutSeconds": 300, -} - - -def get_api_key(provider: str) -> str | None: - """Get API key from environment or return None if not set.""" - env_vars = { - "coinbase": "COINBASE_API_KEY", - "ordern": "ORDERN_API_KEY", - "rbx": "RBX_API_KEY", - "thirdweb": "THIRDWEB_API_KEY", - "circle": "CIRCLE_API_KEY", - } - return os.environ.get(env_vars.get(provider.lower(), "")) - - -def requires_api_key(provider: str): - """Decorator to skip test if API key is not available.""" - key = get_api_key(provider) - if not key: - pytest.skip( - f"API key not set for {provider}. Set {provider.upper()}_API_KEY environment variable." - ) - return key - - -# ============================================================================= -# Coinbase Integration Tests -# ============================================================================= - - -@pytest.mark.asyncio -async def test_coinbase_supported_networks(): - """Test fetching supported networks from Coinbase facilitator.""" - api_key = requires_api_key("coinbase") - - from omniclaw.seller import create_facilitator - - facilitator = create_facilitator(provider="coinbase", api_key=api_key, environment="testnet") - - try: - networks = await facilitator.get_supported_networks() - - # Should return a list of networks - assert networks is not None - assert isinstance(networks, list) - - # Print for debugging - print(f"\nCoinbase supported networks: {networks}") - - finally: - await facilitator.close() - - -@pytest.mark.asyncio -async def test_coinbase_verify(): - """Test Coinbase verify endpoint with test payload.""" - api_key = requires_api_key("coinbase") - - from omniclaw.seller import create_facilitator - - facilitator = create_facilitator(provider="coinbase", api_key=api_key, environment="testnet") - - try: - result = await facilitator.verify(TEST_PAYLOAD, TEST_REQUIREMENTS) - - # Should return a VerifyResult - assert hasattr(result, "is_valid") - assert hasattr(result, "payer") - - print(f"\nCoinbase verify result: is_valid={result.is_valid}, payer={result.payer}") - - finally: - await facilitator.close() - - -@pytest.mark.asyncio -async def test_coinbase_settle(): - """Test Coinbase settle endpoint with test payload.""" - api_key = requires_api_key("coinbase") - - from omniclaw.seller import create_facilitator - - facilitator = create_facilitator(provider="coinbase", api_key=api_key, environment="testnet") - - try: - result = await facilitator.settle(TEST_PAYLOAD, TEST_REQUIREMENTS) - - # Should return a SettleResult - assert hasattr(result, "success") - assert hasattr(result, "transaction") - assert hasattr(result, "error_reason") - - print(f"\nCoinbase settle result: success={result.success}, error={result.error_reason}") - - finally: - await facilitator.close() - - -@pytest.mark.asyncio -async def test_coinbase_urls(): - """Verify Coinbase URLs for testnet and mainnet.""" - api_key = requires_api_key("coinbase") - - from omniclaw.seller import create_facilitator - - # Testnet - facilitator_testnet = create_facilitator( - provider="coinbase", api_key=api_key, environment="testnet" - ) - assert facilitator_testnet.base_url == "https://api.cdp.coinbase.com/platform" - await facilitator_testnet.close() - - # Mainnet - facilitator_mainnet = create_facilitator( - provider="coinbase", api_key=api_key, environment="mainnet" - ) - assert facilitator_mainnet.base_url == "https://api.cdp.coinbase.com/platform" - await facilitator_mainnet.close() - - print("\nCoinbase URLs verified successfully") - - -# ============================================================================= -# OrderN Integration Tests -# ============================================================================= - - -@pytest.mark.asyncio -async def test_ordern_supported_networks(): - """Test fetching supported networks from OrderN facilitator.""" - api_key = requires_api_key("ordern") - - from omniclaw.seller import create_facilitator - - facilitator = create_facilitator(provider="ordern", api_key=api_key, environment="testnet") - - try: - networks = await facilitator.get_supported_networks() - - assert networks is not None - assert isinstance(networks, list) - - print(f"\nOrderN supported networks: {networks}") - - finally: - await facilitator.close() - - -@pytest.mark.asyncio -async def test_ordern_verify(): - """Test OrderN verify endpoint.""" - api_key = requires_api_key("ordern") - - from omniclaw.seller import create_facilitator - - facilitator = create_facilitator(provider="ordern", api_key=api_key, environment="testnet") - - try: - result = await facilitator.verify(TEST_PAYLOAD, TEST_REQUIREMENTS) - - assert hasattr(result, "is_valid") - assert hasattr(result, "payer") - - print(f"\nOrderN verify result: is_valid={result.is_valid}, payer={result.payer}") - - finally: - await facilitator.close() - - -@pytest.mark.asyncio -async def test_ordern_settle(): - """Test OrderN settle endpoint.""" - api_key = requires_api_key("ordern") - - from omniclaw.seller import create_facilitator - - facilitator = create_facilitator(provider="ordern", api_key=api_key, environment="testnet") - - try: - result = await facilitator.settle(TEST_PAYLOAD, TEST_REQUIREMENTS) - - assert hasattr(result, "success") - assert hasattr(result, "transaction") - - print(f"\nOrderN settle result: success={result.success}, error={result.error_reason}") - - finally: - await facilitator.close() - - -@pytest.mark.asyncio -async def test_ordern_urls(): - """Verify OrderN URLs.""" - api_key = requires_api_key("ordern") - - from omniclaw.seller import create_facilitator - - # Testnet - f_testnet = create_facilitator(provider="ordern", api_key=api_key, environment="testnet") - assert f_testnet.base_url == "https://api.testnet.ordern.ai" - await f_testnet.close() - - # Mainnet - f_mainnet = create_facilitator(provider="ordern", api_key=api_key, environment="mainnet") - assert f_mainnet.base_url == "https://api.ordern.ai" - await f_mainnet.close() - - print("\nOrderN URLs verified successfully") - - -# ============================================================================= -# RBX Integration Tests -# ============================================================================= - - -@pytest.mark.asyncio -async def test_rbx_supported_networks(): - """Test fetching supported networks from RBX facilitator.""" - api_key = requires_api_key("rbx") - - from omniclaw.seller import create_facilitator - - facilitator = create_facilitator(provider="rbx", api_key=api_key, environment="testnet") - - try: - networks = await facilitator.get_supported_networks() - - assert networks is not None - assert isinstance(networks, list) - - print(f"\nRBX supported networks: {networks}") - - finally: - await facilitator.close() - - -@pytest.mark.asyncio -async def test_rbx_verify(): - """Test RBX verify endpoint.""" - api_key = requires_api_key("rbx") - - from omniclaw.seller import create_facilitator - - facilitator = create_facilitator(provider="rbx", api_key=api_key, environment="testnet") - - try: - result = await facilitator.verify(TEST_PAYLOAD, TEST_REQUIREMENTS) - - assert hasattr(result, "is_valid") - assert hasattr(result, "payer") - - print(f"\nRBX verify result: is_valid={result.is_valid}, payer={result.payer}") - - finally: - await facilitator.close() - - -@pytest.mark.asyncio -async def test_rbx_settle(): - """Test RBX settle endpoint.""" - api_key = requires_api_key("rbx") - - from omniclaw.seller import create_facilitator - - facilitator = create_facilitator(provider="rbx", api_key=api_key, environment="testnet") - - try: - result = await facilitator.settle(TEST_PAYLOAD, TEST_REQUIREMENTS) - - assert hasattr(result, "success") - assert hasattr(result, "transaction") - - print(f"\nRBX settle result: success={result.success}, error={result.error_reason}") - - finally: - await facilitator.close() - - -@pytest.mark.asyncio -async def test_rbx_urls(): - """Verify RBX URLs.""" - api_key = requires_api_key("rbx") - - from omniclaw.seller import create_facilitator - - f_testnet = create_facilitator(provider="rbx", api_key=api_key, environment="testnet") - assert f_testnet.base_url == "https://api.testnet.rbx.io" - await f_testnet.close() - - f_mainnet = create_facilitator(provider="rbx", api_key=api_key, environment="mainnet") - assert f_mainnet.base_url == "https://api.rbx.io" - await f_mainnet.close() - - print("\nRBX URLs verified successfully") - - -# ============================================================================= -# Thirdweb Integration Tests -# ============================================================================= - - -@pytest.mark.asyncio -async def test_thirdweb_supported_networks(): - """Test fetching supported networks from Thirdweb facilitator.""" - api_key = requires_api_key("thirdweb") - - from omniclaw.seller import create_facilitator - - facilitator = create_facilitator(provider="thirdweb", api_key=api_key, environment="testnet") - - try: - networks = await facilitator.get_supported_networks() - - assert networks is not None - assert isinstance(networks, list) - - print(f"\nThirdweb supported networks: {networks}") - - finally: - await facilitator.close() - - -@pytest.mark.asyncio -async def test_thirdweb_verify(): - """Test Thirdweb verify endpoint.""" - api_key = requires_api_key("thirdweb") - - from omniclaw.seller import create_facilitator - - facilitator = create_facilitator(provider="thirdweb", api_key=api_key, environment="testnet") - - try: - result = await facilitator.verify(TEST_PAYLOAD, TEST_REQUIREMENTS) - - assert hasattr(result, "is_valid") - assert hasattr(result, "payer") - - print(f"\nThirdweb verify result: is_valid={result.is_valid}, payer={result.payer}") - - finally: - await facilitator.close() - - -@pytest.mark.asyncio -async def test_thirdweb_settle(): - """Test Thirdweb settle endpoint.""" - api_key = requires_api_key("thirdweb") - - from omniclaw.seller import create_facilitator - - facilitator = create_facilitator(provider="thirdweb", api_key=api_key, environment="testnet") - - try: - result = await facilitator.settle(TEST_PAYLOAD, TEST_REQUIREMENTS) - - assert hasattr(result, "success") - assert hasattr(result, "transaction") - - print(f"\nThirdweb settle result: success={result.success}, error={result.error_reason}") - - finally: - await facilitator.close() - - -@pytest.mark.asyncio -async def test_thirdweb_urls(): - """Verify Thirdweb URLs.""" - api_key = requires_api_key("thirdweb") - - from omniclaw.seller import create_facilitator - - f_testnet = create_facilitator(provider="thirdweb", api_key=api_key, environment="testnet") - assert f_testnet.base_url == "https://api.thirdweb.com" - await f_testnet.close() - - f_mainnet = create_facilitator(provider="thirdweb", api_key=api_key, environment="mainnet") - assert f_mainnet.base_url == "https://api.thirdweb.com" - await f_mainnet.close() - - print("\nThirdweb URLs verified successfully") - - -# ============================================================================= -# Circle Gateway Integration Tests -# ============================================================================= - - -@pytest.mark.asyncio -async def test_circle_verify(): - """Test Circle Gateway verify endpoint.""" - api_key = requires_api_key("circle") - - from omniclaw.seller import create_facilitator - - facilitator = create_facilitator(provider="circle", api_key=api_key, environment="testnet") - - try: - result = await facilitator.verify(TEST_PAYLOAD, TEST_REQUIREMENTS) - - assert hasattr(result, "is_valid") - assert hasattr(result, "payer") - - print(f"\nCircle verify result: is_valid={result.is_valid}, payer={result.payer}") - - finally: - await facilitator.close() - - -@pytest.mark.asyncio -async def test_circle_settle(): - """Test Circle Gateway settle endpoint.""" - api_key = requires_api_key("circle") - - from omniclaw.seller import create_facilitator - - facilitator = create_facilitator(provider="circle", api_key=api_key, environment="testnet") - - try: - result = await facilitator.settle(TEST_PAYLOAD, TEST_REQUIREMENTS) - - assert hasattr(result, "success") - assert hasattr(result, "transaction") - - print(f"\nCircle settle result: success={result.success}, error={result.error_reason}") - - finally: - await facilitator.close() - - -@pytest.mark.asyncio -async def test_circle_urls(): - """Verify Circle Gateway URLs.""" - api_key = requires_api_key("circle") - - from omniclaw.seller import create_facilitator - - f_testnet = create_facilitator(provider="circle", api_key=api_key, environment="testnet") - assert f_testnet.base_url == "https://gateway-api-testnet.circle.com" - await f_testnet.close() - - f_mainnet = create_facilitator(provider="circle", api_key=api_key, environment="mainnet") - assert f_mainnet.base_url == "https://gateway-api.circle.com" - await f_mainnet.close() - - print("\nCircle Gateway URLs verified successfully") - - -# ============================================================================= -# Test All Facilitators Together -# ============================================================================= - - -@pytest.mark.asyncio -async def test_all_facilitators_interface(): - """Verify all facilitators have the correct interface.""" - providers = ["coinbase", "ordern", "rbx", "thirdweb", "circle"] - - results = {} - - for provider in providers: - api_key = get_api_key(provider) - if not api_key: - print(f"\nSkipping {provider} - no API key") - continue - - from omniclaw.seller import create_facilitator - - facilitator = create_facilitator(provider=provider, api_key=api_key, environment="testnet") - - # Verify interface - assert hasattr(facilitator, "name") - assert hasattr(facilitator, "base_url") - assert hasattr(facilitator, "environment") - assert hasattr(facilitator, "verify") - assert hasattr(facilitator, "settle") - assert hasattr(facilitator, "get_supported_networks") - assert hasattr(facilitator, "close") - - results[provider] = "OK" - - await facilitator.close() - - print(f"\nFacilitators tested: {results}") - - # Skip if no facilitators could be tested (no API keys in CI) - if len(results) == 0: - pytest.skip("No facilitators were tested - no API keys available") - - -if __name__ == "__main__": - pytest.main([__file__, "-v", "-s"]) diff --git a/tests/test_nanopayments_exceptions.py b/tests/test_nanopayments_exceptions.py index c4303eb..034100e 100644 --- a/tests/test_nanopayments_exceptions.py +++ b/tests/test_nanopayments_exceptions.py @@ -219,7 +219,7 @@ def test_unsupported_scheme(self): class TestMiddlewareErrors: - """Tests for GatewayMiddleware errors.""" + """Tests for compatibility x402 402 errors.""" def test_invalid_price(self): exc = InvalidPriceError("not a number") diff --git a/tests/test_nanopayments_middleware.py b/tests/test_nanopayments_middleware.py deleted file mode 100644 index 987541e..0000000 --- a/tests/test_nanopayments_middleware.py +++ /dev/null @@ -1,514 +0,0 @@ -""" -Tests for GatewayMiddleware (Phase 7: seller-side payment gate). - -Tests verify: -- 402 response structure (x402 v2 spec) -- maxTimeoutSeconds is 345600 -- extra.name is "GatewayWalletBatched" -- parse_price handles all formats -- Payment handling -""" - -import base64 -import json -from unittest.mock import AsyncMock, MagicMock - -import pytest - -from omniclaw.protocols.nanopayments import ( - MAX_TIMEOUT_SECONDS, - X402_VERSION, -) -from omniclaw.protocols.nanopayments.adapter import NanopaymentAdapter -from omniclaw.protocols.nanopayments.client import NanopaymentClient -from omniclaw.protocols.nanopayments.exceptions import InvalidPriceError -from omniclaw.protocols.nanopayments.middleware import ( - GatewayMiddleware, - PaymentRequiredHTTPError, - parse_price, -) -from omniclaw.protocols.nanopayments.types import ( - EIP3009Authorization, - PaymentPayload, - PaymentPayloadInner, - PaymentRequirementsExtra, - PaymentRequirementsKind, - SupportedKind, -) - -# ============================================================================= -# PARSE_PRICE TESTS -# ============================================================================= - - -class TestParsePrice: - def test_dollar_sign_removed(self): - assert parse_price("$0.001") == 1000 - assert parse_price("$1") == 1_000_000 - assert parse_price("$0.000001") == 1 - - def test_decimal_without_dollar(self): - assert parse_price("0.001") == 1000 - assert parse_price("1.00") == 1_000_000 - assert parse_price("0.5") == 500_000 - - def test_integer_plain_dollars(self): - """Integer <= 1M is treated as whole dollars.""" - assert parse_price("100") == 100_000_000 # $100 - assert parse_price("1") == 1_000_000 # $1 - - def test_integer_atomic_units(self): - """Integer > 1M is treated as atomic units.""" - assert parse_price("1000000") == 1_000_000 # 1M atomic = $1 - - def test_whitespace_stripped(self): - assert parse_price(" $0.001 ") == 1000 - assert parse_price(" 0.001 ") == 1000 - - def test_large_dollar_amount(self): - assert parse_price("$100") == 100_000_000 - assert parse_price("$999.99") == 999_990_000 - - def test_invalid_price_raises(self): - with pytest.raises(InvalidPriceError): - parse_price("not a price") - with pytest.raises(InvalidPriceError): - parse_price("") - with pytest.raises(InvalidPriceError): - parse_price(None) # type: ignore - - def test_edge_cases(self): - assert parse_price("$0.000001") == 1 # minimum USDC - assert parse_price("0") == 0 - - -# ============================================================================= -# GATEWAY MIDDLEWARE TESTS -# ============================================================================= - - -def _make_kinds() -> list[SupportedKind]: - """Real SupportedKind objects for testing.""" - return [ - SupportedKind( - x402_version=2, - scheme="exact", - network="eip155:5042002", - extra={ - "verifyingContract": "0x" + "c" * 40, - "usdcAddress": "0xUsdcArcTestnet", - }, - ), - SupportedKind( - x402_version=2, - scheme="exact", - network="eip155:1", - extra={ - "verifyingContract": "0x" + "d" * 40, - "usdcAddress": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", - }, - ), - ] - - -def _make_client() -> MagicMock: - """NanopaymentClient mock.""" - mock = MagicMock(spec=NanopaymentClient) - mock.get_supported = AsyncMock(return_value=_make_kinds()) - return mock - - -class TestGatewayMiddleware: - """Tests for GatewayMiddleware 402 response structure.""" - - @pytest.mark.asyncio - async def test_402_body_has_correct_x402_version(self): - middleware = GatewayMiddleware( - seller_address="0x" + "a" * 40, - nanopayment_client=_make_client(), - supported_kinds=_make_kinds(), - ) - body = await middleware._build_402_response("$0.001") - assert body["x402Version"] == X402_VERSION - - async def test_402_body_has_correct_scheme(self): - middleware = GatewayMiddleware( - seller_address="0x" + "a" * 40, - nanopayment_client=_make_client(), - supported_kinds=_make_kinds(), - ) - body = await middleware._build_402_response("$0.001") - for accept in body["accepts"]: - assert accept["scheme"] == "exact" - - async def test_402_body_has_max_timeout(self): - middleware = GatewayMiddleware( - seller_address="0x" + "a" * 40, - nanopayment_client=_make_client(), - supported_kinds=_make_kinds(), - ) - body = await middleware._build_402_response("$0.001") - for accept in body["accepts"]: - assert accept["maxTimeoutSeconds"] == MAX_TIMEOUT_SECONDS == 345600 - - async def test_402_body_has_gateway_wallet_batched(self): - middleware = GatewayMiddleware( - seller_address="0x" + "a" * 40, - nanopayment_client=_make_client(), - supported_kinds=_make_kinds(), - ) - body = await middleware._build_402_response("$0.001") - for accept in body["accepts"]: - assert accept["extra"]["name"] == "GatewayWalletBatched" - - @pytest.mark.asyncio - async def test_non_circle_facilitator_advertises_standard_exact(self): - facilitator = MagicMock() - facilitator.name = "coinbase" - - middleware = GatewayMiddleware( - seller_address="0x" + "a" * 40, - nanopayment_client=_make_client(), - supported_kinds=_make_kinds(), - facilitator=facilitator, - ) - - body = await middleware._build_402_response("$0.001") - - for accept in body["accepts"]: - assert accept["scheme"] == "exact" - assert "extra" not in accept - - async def test_external_facilitator_can_create_accepts(self): - facilitator = AsyncMock() - facilitator.name = "thirdweb" - facilitator.create_accepts.return_value = [ - { - "scheme": "exact", - "network": "eip155:84532", - "amount": "10000", - "payTo": "0x" + "b" * 40, - "asset": "0x036CbD53842c5426634e7929541eC2318f3dCF7e", - } - ] - middleware = GatewayMiddleware( - seller_address="0x" + "a" * 40, - nanopayment_client=None, - facilitator=facilitator, - ) - - body = await middleware._build_402_response( - "$0.01", - resource_url="https://seller.example.com/compute", - method="GET", - ) - - facilitator.create_accepts.assert_awaited_once_with( - resource_url="https://seller.example.com/compute", - method="GET", - price="$0.01", - server_wallet_address="0x" + "a" * 40, - ) - assert body["x402Version"] == 2 - assert body["accepts"][0]["network"] == "eip155:84532" - - async def test_402_body_has_verifying_contract(self): - middleware = GatewayMiddleware( - seller_address="0x" + "a" * 40, - nanopayment_client=_make_client(), - supported_kinds=_make_kinds(), - ) - body = await middleware._build_402_response("$0.001") - for accept in body["accepts"]: - assert "verifyingContract" in accept["extra"] - assert accept["extra"]["verifyingContract"].startswith("0x") - - async def test_402_body_has_correct_amount(self): - middleware = GatewayMiddleware( - seller_address="0x" + "a" * 40, - nanopayment_client=_make_client(), - supported_kinds=_make_kinds(), - ) - body = await middleware._build_402_response("$0.001") - for accept in body["accepts"]: - assert accept["amount"] == "1000" # 0.001 * 1_000_000 - - async def test_402_body_pay_to_is_seller_address(self): - seller = "0x" + "a" * 40 - middleware = GatewayMiddleware( - seller_address=seller, - nanopayment_client=_make_client(), - supported_kinds=_make_kinds(), - ) - body = await middleware._build_402_response("$0.001") - for accept in body["accepts"]: - assert accept["payTo"] == seller - - async def test_402_body_one_entry_per_network(self): - middleware = GatewayMiddleware( - seller_address="0x" + "a" * 40, - nanopayment_client=_make_client(), - supported_kinds=_make_kinds(), - ) - body = await middleware._build_402_response("$1.00") - assert len(body["accepts"]) == 2 - - async def test_payment_required_header_is_valid_base64(self): - """PAYMENT-REQUIRED header must be valid base64.""" - middleware = GatewayMiddleware( - seller_address="0x" + "a" * 40, - nanopayment_client=_make_client(), - supported_kinds=_make_kinds(), - ) - body = await middleware._build_402_response("$0.001") - header = middleware._encode_requirements(body) - - decoded = base64.b64decode(header) - parsed = json.loads(decoded) - assert parsed["x402Version"] == 2 - - -# ============================================================================= -# HANDLE TESTS -# ============================================================================= - - -class TestHandle: - def test_payment_signature_header_includes_accepted_requirements(self): - """Buyer retry header must include x402 v2 accepted requirements.""" - authorization = EIP3009Authorization.create( - from_address="0x" + "a" * 40, - to="0x" + "b" * 40, - value="440", - valid_before=9999999999, - nonce="0x" + "c" * 64, - ) - payload = PaymentPayload( - x402_version=2, - scheme="exact", - network="eip155:11155111", - payload=PaymentPayloadInner( - signature="0x" + "d" * 130, - authorization=authorization, - ), - ) - accepted = PaymentRequirementsKind( - scheme="exact", - network="eip155:11155111", - asset="0x1c7d4b196cb0c7b01d743fbc6116a902379c7238", - amount="440", - max_timeout_seconds=345600, - pay_to="0x" + "b" * 40, - extra=PaymentRequirementsExtra( - name="GatewayWalletBatched", - version="1", - verifying_contract="0x" + "e" * 40, - ), - ) - - sig_header = NanopaymentAdapter._encode_payment_signature_header(payload, accepted) - decoded = json.loads(base64.b64decode(sig_header)) - - assert decoded["x402Version"] == 2 - assert decoded["scheme"] == "exact" - assert decoded["network"] == "eip155:11155111" - assert decoded["accepted"]["scheme"] == "exact" - assert decoded["accepted"]["network"] == "eip155:11155111" - assert decoded["accepted"]["amount"] == "440" - assert decoded["accepted"]["extra"]["name"] == "GatewayWalletBatched" - - @pytest.mark.asyncio - async def test_handle_without_payment_raises_402(self): - """Request without PAYMENT-SIGNATURE header returns 402.""" - middleware = GatewayMiddleware( - seller_address="0x" + "a" * 40, - nanopayment_client=_make_client(), - supported_kinds=_make_kinds(), - ) - - with pytest.raises(PaymentRequiredHTTPError) as exc_info: - await middleware.handle({}, "$0.001") - - assert exc_info.value.status_code == 402 - assert "PAYMENT-REQUIRED" in exc_info.value.headers - - @pytest.mark.asyncio - async def test_handle_with_valid_payment_returns_payment_info(self): - """Valid PAYMENT-SIGNATURE returns PaymentInfo.""" - mock_client = _make_client() - mock_client.settle = AsyncMock( - return_value=MagicMock( - success=True, - transaction="batch-123", - payer="0x" + "a" * 40, - ) - ) - - authorization = EIP3009Authorization.create( - from_address="0x" + "a" * 40, - to="0x" + "a" * 40, - value="1000", - valid_before=9999999999, - nonce="0x" + "b" * 64, - ) - payload = PaymentPayload( - x402_version=2, - scheme="exact", - network="eip155:5042002", - payload=PaymentPayloadInner( - signature="0x" + "c" * 130, - authorization=authorization, - ), - accepted=PaymentRequirementsKind( - scheme="exact", - network="eip155:5042002", - asset="0xUsdcArcTestnet", - amount="1000", - max_timeout_seconds=345600, - pay_to="0x" + "a" * 40, - extra=PaymentRequirementsExtra( - name="GatewayWalletBatched", - version="1", - verifying_contract="0x" + "c" * 40, - ), - ), - ) - - sig_header = base64.b64encode(json.dumps(payload.to_dict()).encode()).decode() - - middleware = GatewayMiddleware( - seller_address="0x" + "a" * 40, - nanopayment_client=mock_client, - supported_kinds=_make_kinds(), - ) - - info = await middleware.handle( - {"payment-signature": sig_header}, - "$0.001", - ) - - assert info.verified is True - assert info.transaction == "batch-123" - - @pytest.mark.asyncio - async def test_handle_rejects_v2_payment_without_accepted(self): - """OmniClaw seller must reject malformed x402 v2 retry payloads.""" - authorization = EIP3009Authorization.create( - from_address="0x" + "a" * 40, - to="0x" + "a" * 40, - value="1000", - valid_before=9999999999, - nonce="0x" + "b" * 64, - ) - payload = PaymentPayload( - x402_version=2, - scheme="exact", - network="eip155:5042002", - payload=PaymentPayloadInner( - signature="0x" + "c" * 130, - authorization=authorization, - ), - ) - sig_header = base64.b64encode(json.dumps(payload.to_dict()).encode()).decode() - - middleware = GatewayMiddleware( - seller_address="0x" + "a" * 40, - nanopayment_client=_make_client(), - supported_kinds=_make_kinds(), - ) - - with pytest.raises(PaymentRequiredHTTPError) as exc_info: - await middleware.handle({"payment-signature": sig_header}, "$0.001") - - assert "Missing accepted requirements" in exc_info.value.detail["error"] - - @pytest.mark.asyncio - async def test_handle_with_invalid_signature_raises_402(self): - """Invalid PAYMENT-SIGNATURE header returns 402.""" - middleware = GatewayMiddleware( - seller_address="0x" + "a" * 40, - nanopayment_client=_make_client(), - supported_kinds=_make_kinds(), - ) - - with pytest.raises(PaymentRequiredHTTPError) as exc_info: - await middleware.handle( - {"payment-signature": "not-valid-base64!!!"}, - "$0.001", - ) - - assert exc_info.value.status_code == 402 - - @pytest.mark.asyncio - async def test_handle_with_missing_payment_and_no_networks_returns_empty(self): - """No supported networks: empty accepts array.""" - middleware = GatewayMiddleware( - seller_address="0x" + "a" * 40, - nanopayment_client=_make_client(), - supported_kinds=[], # No networks - ) - - with pytest.raises(PaymentRequiredHTTPError) as exc_info: - await middleware.handle({}, "$0.001") - - body = exc_info.value.detail - assert body["x402Version"] == 2 - assert body["accepts"] == [] - - @pytest.mark.asyncio - async def test_non_circle_facilitator_settle_uses_standard_exact_requirements(self): - facilitator = AsyncMock() - facilitator.name = "coinbase" - facilitator.settle.return_value = MagicMock( - success=True, - transaction="fac-123", - payer="0x" + "a" * 40, - ) - - authorization = EIP3009Authorization.create( - from_address="0x" + "a" * 40, - to="0x" + "a" * 40, - value="1000", - valid_before=9999999999, - nonce="0x" + "b" * 64, - ) - payload = PaymentPayload( - x402_version=2, - scheme="exact", - network="eip155:5042002", - payload=PaymentPayloadInner( - signature="0x" + "c" * 130, - authorization=authorization, - ), - accepted=PaymentRequirementsKind( - scheme="exact", - network="eip155:5042002", - asset="0xUsdcArcTestnet", - amount="1000", - max_timeout_seconds=345600, - pay_to="0x" + "a" * 40, - extra=PaymentRequirementsExtra( - name="", - version="", - verifying_contract="", - ), - ), - ) - sig_header = base64.b64encode(json.dumps(payload.to_dict()).encode()).decode() - - middleware = GatewayMiddleware( - seller_address="0x" + "a" * 40, - nanopayment_client=_make_client(), - supported_kinds=_make_kinds(), - facilitator=facilitator, - ) - - info = await middleware.handle({"payment-signature": sig_header}, "$0.001") - - assert info.verified is True - assert info.transaction == "fac-123" - facilitator.settle.assert_awaited_once() - _, req_dict = facilitator.settle.await_args.args - accepted = req_dict["accepts"][0] - assert accepted["scheme"] == "exact" - assert "extra" not in accepted diff --git a/tests/test_nanopayments_types.py b/tests/test_nanopayments_types.py index b8bfd51..1c2b8fb 100644 --- a/tests/test_nanopayments_types.py +++ b/tests/test_nanopayments_types.py @@ -9,7 +9,6 @@ EIP3009Authorization, GatewayBalance, NanopaymentResult, - PaymentInfo, PaymentPayload, PaymentPayloadInner, PaymentRequirements, @@ -449,33 +448,6 @@ def test_response_data_none_for_direct_transfer(self): assert result.response_data is None -class TestPaymentInfo: - """Tests for PaymentInfo.""" - - def test_amount_decimal_conversion(self): - info = PaymentInfo( - verified=True, - payer="0xBuyer123", - amount="1000000", # 1 USDC - network="eip155:5042002", - transaction="batch-ref-123", - ) - assert info.amount_decimal == "1" - - def test_to_dict(self): - info = PaymentInfo( - verified=True, - payer="0xBuyer123", - amount="1000000", - network="eip155:5042002", - transaction="batch-ref-123", - ) - d = info.to_dict() - assert d["verified"] is True - assert d["payer"] == "0xBuyer123" - assert d["amount_decimal"] == "1" - - class TestDepositResult: """Tests for DepositResult.""" diff --git a/tests/test_seller_side.py b/tests/test_seller_side.py deleted file mode 100644 index 9852037..0000000 --- a/tests/test_seller_side.py +++ /dev/null @@ -1,638 +0,0 @@ -""" -Seller-Side x402 Tests. - -Tests the seller/server side of x402 payments: -- Decimal price parsing (no float precision bugs) -- 402 response generation (x402 v2 spec) -- Payment verification flow with facilitator -- Multi-facilitator support (Circle, Coinbase, etc.) -- Scheme detection (exact vs GatewayWalletBatched) -- Network/USDC contract configuration - -Run with: - pytest tests/test_seller_side.py -v -s -""" - -import base64 -import json -from decimal import Decimal -from unittest.mock import AsyncMock, MagicMock - -import pytest - -from omniclaw.seller import PaymentScheme, Seller, create_seller - - -def _with_accepted(payload: dict, accepted: dict) -> dict: - """Attach required x402 v2 selected requirements to a test payment payload.""" - payload["x402Version"] = 2 - payload["accepted"] = accepted - payload.setdefault("network", accepted.get("network")) - return payload - - -# ============================================================================= -# TEST PRICE PARSING (Decimal precision — critical fix) -# ============================================================================= - - -class TestPriceParsing: - """Verify Decimal-based price parsing — no float, no rounding.""" - - def test_parse_one_tenth_cent(self): - """$0.001 → 1000 atomic units.""" - seller = Seller(seller_address="0x742d35Cc6634C0532925a3b844Bc9e7595f1E123", name="Test") - seller.add_endpoint("/test", "$0.001", "Test") - endpoints = seller.get_endpoints() - accepts = seller._create_accepts(endpoints["/test"]) - assert accepts[0]["amount"] == "1000" - - def test_parse_one_cent(self): - """$0.01 → 10000 atomic units.""" - seller = Seller(seller_address="0x742d35Cc6634C0532925a3b844Bc9e7595f1E123", name="Test") - seller.add_endpoint("/test", "$0.01", "Test") - accepts = seller._create_accepts(seller.get_endpoints()["/test"]) - assert accepts[0]["amount"] == "10000" - - def test_parse_one_dollar(self): - """$1.00 → 1000000 atomic units.""" - seller = Seller(seller_address="0x742d35Cc6634C0532925a3b844Bc9e7595f1E123", name="Test") - seller.add_endpoint("/test", "$1.00", "Test") - accepts = seller._create_accepts(seller.get_endpoints()["/test"]) - assert accepts[0]["amount"] == "1000000" - - def test_different_prices_produce_different_amounts(self): - """$0.001 and $0.01 MUST produce different atomic values (critical bug fix).""" - seller = Seller(seller_address="0x742d35Cc6634C0532925a3b844Bc9e7595f1E123", name="Test") - seller.add_endpoint("/cheap", "$0.001") - seller.add_endpoint("/expensive", "$0.01") - - a = seller._create_accepts(seller.get_endpoints()["/cheap"])[0]["amount"] - b = seller._create_accepts(seller.get_endpoints()["/expensive"])[0]["amount"] - assert a != b - assert a == "1000" - assert b == "10000" - - def test_zero_price_rejected(self): - """Zero price is not allowed.""" - seller = Seller(seller_address="0x742d35Cc6634C0532925a3b844Bc9e7595f1E123", name="Test") - with pytest.raises(ValueError, match="positive"): - seller.add_endpoint("/test", "$0") - - def test_negative_price_rejected(self): - """Negative price is not allowed.""" - seller = Seller(seller_address="0x742d35Cc6634C0532925a3b844Bc9e7595f1E123", name="Test") - with pytest.raises(ValueError): - seller.add_endpoint("/test", "-$1.00") - - def test_invalid_format_rejected(self): - """Non-numeric strings should raise.""" - seller = Seller(seller_address="0x742d35Cc6634C0532925a3b844Bc9e7595f1E123", name="Test") - with pytest.raises(ValueError): - seller.add_endpoint("/test", "abc") - - -# ============================================================================= -# TEST 402 RESPONSE GENERATION (x402 v2) -# ============================================================================= - - -class Test402ResponseGeneration: - """Test generating 402 Payment Required responses.""" - - def test_create_basic_402_response(self): - """402 response has correct x402 v2 structure.""" - seller = Seller( - seller_address="0x742d35Cc6634C0532925a3b844Bc9e7595f1E123", - name="Test API", - network="eip155:84532", - ) - seller.add_endpoint("/test", "$0.001", "Test endpoint") - - headers, body = seller.create_402_response("/test", "http://localhost/test") - - assert "payment-required" in headers - decoded = json.loads(base64.b64decode(headers["payment-required"])) - - assert decoded["x402Version"] == 2 - assert "accepts" in decoded - assert len(decoded["accepts"]) > 0 - - accept = decoded["accepts"][0] - assert accept["amount"] == "1000" - assert accept["scheme"] == "exact" - assert accept["network"] == "eip155:84532" - assert accept["asset"] == "0x036CbD53842c5426634e7929541eC2318f3dCF7e" - assert accept["payTo"] == "0x742d35Cc6634C0532925a3b844Bc9e7595f1E123" - - def test_unregistered_endpoint_returns_empty(self): - """Unregistered path returns empty response.""" - seller = Seller( - seller_address="0x742d35Cc6634C0532925a3b844Bc9e7595f1E123", - name="Test", - ) - headers, body = seller.create_402_response("/nonexistent", "http://localhost/x") - assert headers == {} - - -# ============================================================================= -# TEST ENDPOINT CREATION -# ============================================================================= - - -class TestEndpointCreation: - """Test creating seller endpoints.""" - - def test_add_multiple_endpoints(self): - """Adding multiple endpoints works.""" - seller = Seller( - seller_address="0x742d35Cc6634C0532925a3b844Bc9e7595f1E123", - name="Test", - ) - seller.add_endpoint("/weather", "$0.001", "Weather data") - seller.add_endpoint("/premium", "$0.01", "Premium content") - - endpoints = seller.get_endpoints() - assert len(endpoints) == 2 - assert "/weather" in endpoints - assert "/premium" in endpoints - - def test_protect_decorator_registers_endpoint(self): - """@seller.protect registers endpoint.""" - seller = Seller( - seller_address="0x742d35Cc6634C0532925a3b844Bc9e7595f1E123", - name="Test", - ) - - @seller.protect("/get_weather", "$0.001", "Weather") - def get_weather(): - pass - - endpoints = seller.get_endpoints() - assert "/get_weather" in endpoints - - def test_endpoint_stores_decimal_price(self): - """Endpoints store Decimal price, not float.""" - seller = Seller( - seller_address="0x742d35Cc6634C0532925a3b844Bc9e7595f1E123", - name="Test", - ) - seller.add_endpoint("/test", "$0.001") - ep = seller.get_endpoints()["/test"] - assert isinstance(ep.price_usd, Decimal) - assert ep.price_usd == Decimal("0.001") - - def test_both_schemes_added_by_default(self): - """Endpoints support both exact + GatewayWalletBatched by default.""" - seller = Seller( - seller_address="0x742d35Cc6634C0532925a3b844Bc9e7595f1E123", - name="Test", - ) - seller.add_endpoint("/test", "$0.001") - ep = seller.get_endpoints()["/test"] - assert PaymentScheme.EXACT in ep.schemes - assert PaymentScheme.GATEWAY_BATCHED in ep.schemes - - -# ============================================================================= -# TEST GATEWAY CONTRACT CONFIGURATION -# ============================================================================= - - -class TestGatewayContractConfig: - """Test that fake gateway contract is never used.""" - - def test_no_fake_gateway_contract(self): - """Gateway contract must NOT contain placeholder/fake values.""" - seller = Seller( - seller_address="0x742d35Cc6634C0532925a3b844Bc9e7595f1E123", - name="Test", - ) - # Without CIRCLE_GATEWAY_CONTRACT env var, should be empty - assert seller._gateway_contract != "0x1234567890abcdef1234567890abcdef12345678" - - def test_gateway_batched_skipped_without_contract(self, monkeypatch): - """GatewayWalletBatched should be skipped if no gateway contract configured.""" - monkeypatch.delenv("CIRCLE_GATEWAY_CONTRACT", raising=False) - seller = Seller( - seller_address="0x742d35Cc6634C0532925a3b844Bc9e7595f1E123", - name="Test", - ) - seller.add_endpoint("/test", "$0.001") - accepts = seller._create_accepts(seller.get_endpoints()["/test"]) - schemes = [a["scheme"] for a in accepts] - # Without gateway contract, only "exact" should be present - assert "exact" in schemes - assert not any( - (a.get("extra", {}) or {}).get("name") == "GatewayWalletBatched" for a in accepts - ) - - def test_gateway_batched_included_with_contract(self, monkeypatch): - """GatewayWalletBatched should be included when gateway contract is set.""" - monkeypatch.setenv("CIRCLE_GATEWAY_CONTRACT", "0xABCD1234ABCD1234ABCD1234ABCD1234ABCD1234") - seller = Seller( - seller_address="0x742d35Cc6634C0532925a3b844Bc9e7595f1E123", - name="Test", - ) - seller.add_endpoint("/test", "$0.001") - accepts = seller._create_accepts(seller.get_endpoints()["/test"]) - schemes = [a["scheme"] for a in accepts] - assert "exact" in schemes - # Verify the correct contract is used - gw_accept = [ - a for a in accepts if (a.get("extra", {}) or {}).get("name") == "GatewayWalletBatched" - ][0] - assert ( - gw_accept["extra"]["verifyingContract"] == "0xABCD1234ABCD1234ABCD1234ABCD1234ABCD1234" - ) - assert gw_accept["extra"]["version"] == "1" - - def test_exact_accept_does_not_advertise_gateway_metadata(self): - seller = Seller( - seller_address="0x742d35Cc6634C0532925a3b844Bc9e7595f1E123", - name="Test", - ) - seller.add_endpoint("/test", "$0.001", schemes=[PaymentScheme.EXACT]) - accept = seller._create_accepts(seller.get_endpoints()["/test"])[0] - assert accept["scheme"] == "exact" - assert "extra" not in accept - - -class TestSellerSecurityHardening: - """Regression tests for seller-side anti-tamper and replay protections.""" - - def test_replay_nonce_is_rejected(self): - import time - - seller = Seller( - seller_address="0x742d35Cc6634C0532925a3b844Bc9e7595f1E123", - name="Test", - ) - seller.add_endpoint("/test", "$0.001") - accepted = seller._create_accepts(seller.get_endpoints()["/test"])[0] - - payload = _with_accepted( - { - "scheme": "exact", - "network": accepted["network"], - "payload": { - "authorization": { - "from": "0xAAAA1111BBBB2222CCCC3333DDDD4444EEEE5555", - "to": "0x742d35Cc6634C0532925a3b844Bc9e7595f1E123", - "value": "1000", - "validAfter": "0", - "validBefore": str(int(time.time()) + 300), - "nonce": "0x" + "11" * 32, - }, - "signature": "", - }, - }, - accepted, - ) - - is_valid, _, record = seller.verify_payment(payload, accepted, verify_signature=False) - assert is_valid is True - assert record is not None - - is_valid_2, err_2, _ = seller.verify_payment(payload, accepted, verify_signature=False) - assert is_valid_2 is False - assert "nonce" in err_2.lower() - - def test_v2_payment_without_accepted_is_rejected(self): - import time - - seller = Seller( - seller_address="0x742d35Cc6634C0532925a3b844Bc9e7595f1E123", - name="Test", - ) - seller.add_endpoint("/test", "$0.001") - accepted = seller._create_accepts(seller.get_endpoints()["/test"])[0] - - payload = { - "x402Version": 2, - "scheme": "exact", - "network": accepted["network"], - "payload": { - "authorization": { - "from": "0xAAAA1111BBBB2222CCCC3333DDDD4444EEEE5555", - "to": "0x742d35Cc6634C0532925a3b844Bc9e7595f1E123", - "value": "1000", - "validAfter": "0", - "validBefore": str(int(time.time()) + 300), - "nonce": "0x" + "22" * 32, - }, - "signature": "", - }, - } - - is_valid, error, record = seller.verify_payment(payload, accepted, verify_signature=False) - - assert is_valid is False - assert record is None - assert "Missing accepted requirements" in error - - def test_strict_gateway_contract_mode_rejects_missing_contract(self, monkeypatch): - monkeypatch.setenv("OMNICLAW_SELLER_STRICT_GATEWAY_CONTRACT", "true") - monkeypatch.delenv("CIRCLE_GATEWAY_CONTRACT", raising=False) - seller = Seller( - seller_address="0x742d35Cc6634C0532925a3b844Bc9e7595f1E123", - name="Strict Test", - ) - with pytest.raises(ValueError, match="CIRCLE_GATEWAY_CONTRACT"): - seller.add_endpoint("/strict", "$0.001") - - def test_require_distributed_nonce_without_redis_fails_fast(self, monkeypatch): - monkeypatch.setenv("OMNICLAW_SELLER_REQUIRE_DISTRIBUTED_NONCE", "true") - monkeypatch.delenv("OMNICLAW_SELLER_NONCE_REDIS_URL", raising=False) - with pytest.raises(RuntimeError, match="REQUIRE_DISTRIBUTED_NONCE"): - Seller( - seller_address="0x742d35Cc6634C0532925a3b844Bc9e7595f1E123", - name="Nonce Strict", - ) - - def test_gateway_signature_verification_requires_complete_domain_fields(self): - seller = Seller( - seller_address="0x742d35Cc6634C0532925a3b844Bc9e7595f1E123", - name="Domain Strict", - ) - accepted = { - "network": "eip155:84532", - "asset": "0x036CbD53842c5426634e7929541eC2318f3dCF7e", - "extra": { - "name": "GatewayWalletBatched", - # version and verifyingContract intentionally missing - }, - } - ok, error = seller._verify_eip3009_signature( - authorization={ - "from": "0xAAAA1111BBBB2222CCCC3333DDDD4444EEEE5555", - "to": "0x742d35Cc6634C0532925a3b844Bc9e7595f1E123", - "value": "1000", - "validAfter": "0", - "validBefore": "9999999999", - "nonce": "0x" + "11" * 32, - }, - signature="0x", - accepted=accepted, - ) - assert ok is False - assert "Missing required EIP-712 domain fields" in error - - -# ============================================================================= -# TEST PAYMENT VERIFICATION -# ============================================================================= - - -class TestPaymentVerification: - """Test payment verification logic.""" - - def test_basic_verify_timeout_check(self): - """Expired payment should be rejected.""" - import time - - seller = Seller( - seller_address="0x742d35Cc6634C0532925a3b844Bc9e7595f1E123", - name="Test", - ) - seller.add_endpoint("/test", "$0.001") - endpoints = seller.get_endpoints() - accepted = seller._create_accepts(endpoints["/test"])[0] - - payload = _with_accepted( - { - "scheme": "exact", - "payload": { - "authorization": { - "from": "0xAAAA1111BBBB2222CCCC3333DDDD4444EEEE5555", - "to": "0x742d35Cc6634C0532925a3b844Bc9e7595f1E123", - "value": "1000", - "validAfter": "0", - "validBefore": str(int(time.time()) - 100), # Already expired - "nonce": "0x" + "00" * 32, - }, - "signature": "", - }, - }, - accepted, - ) - - is_valid, error, record = seller.verify_payment(payload, accepted, verify_signature=False) - assert is_valid is False - assert "expired" in error.lower() - - def test_basic_verify_wrong_recipient(self): - """Payment to wrong address should be rejected.""" - import time - - seller = Seller( - seller_address="0x742d35Cc6634C0532925a3b844Bc9e7595f1E123", - name="Test", - ) - seller.add_endpoint("/test", "$0.001") - accepted = seller._create_accepts(seller.get_endpoints()["/test"])[0] - - payload = _with_accepted( - { - "scheme": "exact", - "payload": { - "authorization": { - "from": "0xAAAA1111BBBB2222CCCC3333DDDD4444EEEE5555", - "to": "0xDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEF", - "value": "1000", - "validAfter": "0", - "validBefore": str(int(time.time()) + 300), - "nonce": "0x" + "00" * 32, - }, - "signature": "", - }, - }, - accepted, - ) - - is_valid, error, record = seller.verify_payment(payload, accepted, verify_signature=False) - assert is_valid is False - assert "recipient" in error.lower() - - def test_basic_verify_insufficient_amount(self): - """Underpayment should be rejected.""" - import time - - seller = Seller( - seller_address="0x742d35Cc6634C0532925a3b844Bc9e7595f1E123", - name="Test", - ) - seller.add_endpoint("/test", "$0.001") - accepted = seller._create_accepts(seller.get_endpoints()["/test"])[0] - - payload = _with_accepted( - { - "scheme": "exact", - "payload": { - "authorization": { - "from": "0xAAAA1111BBBB2222CCCC3333DDDD4444EEEE5555", - "to": "0x742d35Cc6634C0532925a3b844Bc9e7595f1E123", - "value": "500", # Less than 1000 required - "validAfter": "0", - "validBefore": str(int(time.time()) + 300), - "nonce": "0x" + "00" * 32, - }, - "signature": "", - }, - }, - accepted, - ) - - is_valid, error, record = seller.verify_payment(payload, accepted, verify_signature=False) - assert is_valid is False - assert "insufficient" in error.lower() - - def test_basic_verify_valid_payment(self): - """Valid payment should pass.""" - import time - - seller = Seller( - seller_address="0x742d35Cc6634C0532925a3b844Bc9e7595f1E123", - name="Test", - ) - seller.add_endpoint("/test", "$0.001") - accepted = seller._create_accepts(seller.get_endpoints()["/test"])[0] - - payload = _with_accepted( - { - "scheme": "exact", - "payload": { - "authorization": { - "from": "0xAAAA1111BBBB2222CCCC3333DDDD4444EEEE5555", - "to": "0x742d35Cc6634C0532925a3b844Bc9e7595f1E123", - "value": "1000", - "validAfter": "0", - "validBefore": str(int(time.time()) + 300), - "nonce": "0x" + "00" * 32, - }, - "signature": "", - }, - }, - accepted, - ) - - is_valid, error, record = seller.verify_payment(payload, accepted, verify_signature=False) - assert is_valid is True - assert record is not None - assert record.amount == 1000 - - -# ============================================================================= -# TEST FACILITATOR INTEGRATION -# ============================================================================= - - -class TestFacilitatorIntegration: - """Test seller with Circle Gateway facilitator.""" - - @pytest.mark.asyncio - async def test_verify_routes_to_facilitator(self): - """When facilitator is configured, verify routes to it.""" - mock_facilitator = AsyncMock() - mock_facilitator.verify.return_value = MagicMock( - is_valid=True, - payer="0xAAAA1111BBBB2222CCCC3333DDDD4444EEEE5555", - invalid_reason=None, - ) - - seller = Seller( - seller_address="0x742d35Cc6634C0532925a3b844Bc9e7595f1E123", - name="Test", - facilitator=mock_facilitator, - ) - seller.add_endpoint("/test", "$0.001") - accepted = seller._create_accepts(seller.get_endpoints()["/test"])[0] - - is_valid, error, record = await seller.verify_payment_async( - _with_accepted({"scheme": "exact", "payload": {}}, accepted), - accepted, - ) - - assert is_valid is True - mock_facilitator.verify.assert_called_once() - - @pytest.mark.asyncio - async def test_settle_routes_to_facilitator(self): - """When facilitator is configured, settle routes to it.""" - mock_facilitator = AsyncMock() - mock_facilitator.settle.return_value = MagicMock( - success=True, - payer="0xAAAA1111BBBB2222CCCC3333DDDD4444EEEE5555", - error_reason=None, - ) - - seller = Seller( - seller_address="0x742d35Cc6634C0532925a3b844Bc9e7595f1E123", - name="Test", - facilitator=mock_facilitator, - ) - seller.add_endpoint("/test", "$0.001") - accepted = seller._create_accepts(seller.get_endpoints()["/test"])[0] - - is_valid, error, record = await seller.verify_payment_async( - _with_accepted({"scheme": "exact", "payload": {}}, accepted), - accepted, - settle_payment=True, - ) - - assert is_valid is True - mock_facilitator.settle.assert_called_once() - - @pytest.mark.asyncio - async def test_facilitator_rejection_returns_error(self): - """Facilitator rejection returns proper error.""" - mock_facilitator = AsyncMock() - mock_facilitator.verify.return_value = MagicMock( - is_valid=False, - payer=None, - invalid_reason="invalid_signature", - ) - - seller = Seller( - seller_address="0x742d35Cc6634C0532925a3b844Bc9e7595f1E123", - name="Test", - facilitator=mock_facilitator, - ) - seller.add_endpoint("/test", "$0.001") - accepted = seller._create_accepts(seller.get_endpoints()["/test"])[0] - - is_valid, error, record = await seller.verify_payment_async( - _with_accepted({"scheme": "exact", "payload": {}}, accepted), - accepted, - ) - - assert is_valid is False - assert "invalid_signature" in error - - -# ============================================================================= -# TEST CREATE_SELLER FACTORY -# ============================================================================= - - -class TestCreateSeller: - """Test the create_seller factory function.""" - - def test_create_basic_seller(self): - """Factory creates a proper Seller instance.""" - seller = create_seller( - seller_address="0x742d35Cc6634C0532925a3b844Bc9e7595f1E123", - name="Test API", - ) - assert isinstance(seller, Seller) - assert seller.config.seller_address == "0x742d35Cc6634C0532925a3b844Bc9e7595f1E123" - assert seller.config.name == "Test API" - assert seller.config.network == "eip155:84532" - - -# ============================================================================= -# RUN TESTS -# ============================================================================= - -if __name__ == "__main__": - pytest.main([__file__, "-v", "-s"]) diff --git a/tests/test_server_integration.py b/tests/test_server_integration.py deleted file mode 100644 index 01ad9dc..0000000 --- a/tests/test_server_integration.py +++ /dev/null @@ -1,617 +0,0 @@ -""" -Real Server Integration Tests. - -These tests run against an actual x402 test server to verify -the complete end-to-end flow works. - -Prerequisites: -1. Start the test server: - python scripts/x402_simple_server.py - -2. Run these tests: - pytest tests/test_server_integration.py -v -s - -Note: These tests require: -- USDC contract deployed on Base Sepolia -- ETH for gas fees -- Optional: Circle API key for nanopayment tests -""" - -import json -import os -import signal -import subprocess -import sys -import time - -import httpx -import pytest - -# ============================================================================= -# TEST SERVER CONFIGURATION -# ============================================================================= - -SERVER_HOST = "127.0.0.1" -SERVER_PORT = 4022 -SERVER_URL = f"http://{SERVER_HOST}:{SERVER_PORT}" - - -# Check if server is running -def is_server_running() -> bool: - """Check if the test server is running.""" - try: - import socket - - sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - result = sock.connect_ex((SERVER_HOST, SERVER_PORT)) - sock.close() - return result == 0 - except Exception: - return False - - -def require_server(): - """Assert the test server is running.""" - assert is_server_running(), "Test server is not running" - - -def _wait_for_server(timeout_seconds: float = 20.0) -> bool: - """Wait until the local test server starts accepting requests.""" - deadline = time.time() + timeout_seconds - while time.time() < deadline: - if is_server_running(): - return True - time.sleep(0.2) - return False - - -@pytest.fixture(scope="module", autouse=True) -def ensure_test_server(): - """ - Ensure an x402 test server is available for this module. - - If one is not already running, start scripts/x402_simple_server.py for the - duration of these tests. - """ - if is_server_running(): - yield - return - - process = subprocess.Popen( # noqa: S603 - [sys.executable, "scripts/x402_simple_server.py"], - stdout=subprocess.DEVNULL, - stderr=subprocess.DEVNULL, - preexec_fn=os.setsid if os.name != "nt" else None, - ) - - try: - if not _wait_for_server(): - pytest.skip("x402 test server not available") - yield - finally: - if process.poll() is None: - if os.name == "nt": - process.terminate() - else: - os.killpg(os.getpgid(process.pid), signal.SIGTERM) - process.wait(timeout=10) - - -# ============================================================================= -# TEST 1: Server Connectivity -# ============================================================================= - - -class TestServerConnectivity: - """Test basic server connectivity.""" - - def test_server_is_reachable(self): - """Test that the test server is reachable.""" - print("\n" + "=" * 60) - print("SERVER: Connectivity Test") - print("=" * 60) - - require_server() - - # Try to connect - try: - response = httpx.get(f"{SERVER_URL}/health", timeout=5.0) - print(f" Server status: {response.status_code}") - assert response.status_code in [200, 404] # 404 if no /health endpoint - except Exception as e: - pytest.fail(f"Server not reachable: {e}") - - def test_server_lists_routes(self): - """Test that we can see available routes.""" - print("\n" + "=" * 60) - print("SERVER: List Routes") - print("=" * 60) - - require_server() - - # Routes we expect - expected_routes = ["/weather", "/premium/content", "/premium/data"] - - for route in expected_routes: - # These should return 402 (not 404) since they're valid routes - response = httpx.get(f"{SERVER_URL}{route}", timeout=5.0) - print(f" {route}: {response.status_code}") - assert response.status_code == 402, ( - f"Expected 402 for {route}, got {response.status_code}" - ) - - -# ============================================================================= -# TEST 2: 402 Response Parsing -# ============================================================================= - - -class Test402ResponseParsing: - """Test parsing 402 Payment Required responses.""" - - def test_parse_payment_required_header(self): - """Test parsing the PAYMENT-REQUIRED header.""" - print("\n" + "=" * 60) - print("SERVER: Parse 402 Response") - print("=" * 60) - - require_server() - - response = httpx.get(f"{SERVER_URL}/weather", timeout=5.0) - - assert response.status_code == 402 - - # Get the header - header = response.headers.get("payment-required") - assert header is not None - - print(f" Status: {response.status_code}") - print(f" Header present: {header[:50]}...") - - # Parse it - import base64 - - decoded = json.loads(base64.b64decode(header)) - - print(f" x402 Version: {decoded.get('x402Version')}") - print(f" Scheme: {decoded.get('scheme')}") - print(f" Accepts: {len(decoded.get('accepts', []))} options") - - for accept in decoded.get("accepts", []): - print( - f" - {accept.get('scheme')}: {accept.get('amount')} on {accept.get('network')}" - ) - - assert decoded.get("x402Version") == 2 - assert len(decoded.get("accepts", [])) > 0 - - def test_402_response_body_has_resource(self): - """Test that 402 response body contains resource info.""" - print("\n" + "=" * 60) - print("SERVER: 402 Response Body") - print("=" * 60) - - require_server() - - response = httpx.get(f"{SERVER_URL}/weather", timeout=5.0) - - # Parse body (even though it's empty in our server, check structure) - if response.text: - body = response.json() - print(f" Body keys: {list(body.keys())}") - - if "resource" in body: - print(f" Resource: {body['resource']}") - - # Our server returns empty body with header - # Some servers return body with resource info - print(f" Body: {response.text[:100] if response.text else '(empty)'}") - - def test_multiple_routes_have_different_prices(self): - """Test that different routes have different prices.""" - print("\n" + "=" * 60) - print("SERVER: Different Prices") - print("=" * 60) - - require_server() - - routes_prices = {} - - for route in ["/weather", "/premium/content", "/premium/data"]: - response = httpx.get(f"{SERVER_URL}{route}", timeout=5.0) - header = response.headers.get("payment-required") - - import base64 - - decoded = json.loads(base64.b64decode(header)) - - # Get first accept's amount - amount = decoded.get("accepts", [{}])[0].get("amount") - routes_prices[route] = amount - - print(f" {route}: {amount}") - - # Weather should be cheaper than premium - assert routes_prices["/weather"] != routes_prices["/premium/content"] - - -# ============================================================================= -# TEST 3: Free Endpoints -# ============================================================================= - - -class TestFreeEndpoints: - """Test endpoints that don't require payment.""" - - def test_health_endpoint_no_payment(self): - """Test that health/check endpoints don't require payment.""" - print("\n" + "=" * 60) - print("SERVER: Free Endpoints") - print("=" * 60) - - require_server() - - # Try a few common free endpoints - free_routes = ["/", "/health", "/api/status"] - - for route in free_routes: - try: - response = httpx.get(f"{SERVER_URL}{route}", timeout=5.0) - print(f" {route}: {response.status_code}") - except Exception: - print(f" {route}: Not found") - - -# ============================================================================= -# TEST 4: Full Payment Flow (without actual payment) -# ============================================================================= - - -class TestFullPaymentFlow: - """Test the complete x402 payment flow.""" - - def test_flow_1_request_without_payment(self): - """Step 1: Request without payment → get 402.""" - print("\n" + "=" * 60) - print("PAYMENT FLOW: Step 1 - Request Without Payment") - print("=" * 60) - - require_server() - - response = httpx.get(f"{SERVER_URL}/weather", timeout=5.0) - - print(f" Status: {response.status_code}") - assert response.status_code == 402 - - header = response.headers.get("payment-required") - assert header is not None - - print(" Got 402 with payment-required header ✓") - - def test_flow_2_parse_402_and_detect_scheme(self): - """Step 2: Parse 402 and detect payment scheme.""" - print("\n" + "=" * 60) - print("PAYMENT FLOW: Step 2 - Parse and Detect Scheme") - print("=" * 60) - - require_server() - - response = httpx.get(f"{SERVER_URL}/weather", timeout=5.0) - - import base64 - - header = response.headers.get("payment-required") - decoded = json.loads(base64.b64decode(header)) - - accepts = decoded.get("accepts", []) - - schemes = [a.get("scheme") for a in accepts] - - print(f" Seller accepts: {schemes}") - - # Our test server only supports "exact" (basic x402) - assert "exact" in schemes - - print(" Detected scheme: exact ✓") - - def test_flow_3_determine_payment_method(self): - """Step 3: Determine payment method based on accepts.""" - print("\n" + "=" * 60) - print("PAYMENT FLOW: Step 3 - Determine Payment Method") - print("=" * 60) - - require_server() - - response = httpx.get(f"{SERVER_URL}/weather", timeout=5.0) - - import base64 - - header = response.headers.get("payment-required") - decoded = json.loads(base64.b64decode(header)) - - accepts = decoded.get("accepts", []) - - # Check what the seller supports - supports_basic = any(a.get("scheme") == "exact" for a in accepts) - supports_circle = any(a.get("scheme") == "GatewayWalletBatched" for a in accepts) - - print(f" Basic x402 (exact): {supports_basic}") - print(f" Circle nanopayment: {supports_circle}") - - # Our test server only supports basic - if supports_basic and not supports_circle: - print(" → Will use: Basic x402 (on-chain settlement)") - elif supports_circle: - print(" → Will use: Circle nanopayment (gasless)") - - assert supports_basic - - def test_flow_4_with_invalid_payment(self): - """Step 4: Try with invalid payment → still get 402.""" - print("\n" + "=" * 60) - print("PAYMENT FLOW: Step 4 - Invalid Payment") - print("=" * 60) - - require_server() - - # Send invalid payment header - headers = {"payment-signature": "invalid-signature-data"} - - response = httpx.get(f"{SERVER_URL}/weather", headers=headers, timeout=5.0) - - print(f" Status: {response.status_code}") - - # Should still be 402 (payment invalid) - assert response.status_code == 402 - - print(" Invalid payment rejected ✓") - - -# ============================================================================= -# TEST 5: Smart Routing Decision -# ============================================================================= - - -class TestSmartRouting: - """Test smart routing based on seller capabilities.""" - - def test_route_to_basic_x402_when_no_circle(self): - """Test routing when seller supports Circle but buyer doesn't.""" - print("\n" + "=" * 60) - print("ROUTING: Buyer has NO Circle Gateway") - print("=" * 60) - - require_server() - - # Get seller capabilities - response = httpx.get(f"{SERVER_URL}/weather", timeout=5.0) - - import base64 - - header = response.headers.get("payment-required") - decoded = json.loads(base64.b64decode(header)) - - accepts = decoded.get("accepts", []) - - # Decision logic - buyer has NO gateway - supports_circle = any(a.get("scheme") == "GatewayWalletBatched" for a in accepts) - buyer_has_gateway = False - - method = "Circle Nanopayment" if supports_circle and buyer_has_gateway else "Basic x402" - - print(f" Seller accepts: {[a['scheme'] for a in accepts]}") - print(f" Buyer has Circle: {buyer_has_gateway}") - print(f" → Routing to: {method}") - - assert method == "Basic x402" - - def test_check_network_compatibility(self): - """Test checking network compatibility.""" - print("\n" + "=" * 60) - print("ROUTING: Network Compatibility") - print("=" * 60) - - require_server() - - response = httpx.get(f"{SERVER_URL}/weather", timeout=5.0) - - import base64 - - header = response.headers.get("payment-required") - decoded = json.loads(base64.b64decode(header)) - - accepts = decoded.get("accepts", []) - - # Check network - buyer_network = "eip155:84532" # Base Sepolia - - for accept in accepts: - seller_network = accept.get("network") - compatible = buyer_network == seller_network - - print(f" Buyer: {buyer_network}") - print(f" Seller: {seller_network}") - print(f" Compatible: {compatible}") - - -# ============================================================================= -# TEST 6: Client Integration (with mocked signer) -# ============================================================================= - - -class TestClientIntegration: - """Test client integration with the server.""" - - @pytest.mark.asyncio - async def test_client_makes_request(self): - """Test that client can make requests to server.""" - print("\n" + "=" * 60) - print("CLIENT: Make HTTP Request") - print("=" * 60) - - require_server() - - async with httpx.AsyncClient() as client: - response = await client.get(f"{SERVER_URL}/weather") - - print(f" Status: {response.status_code}") - print(f" Headers: {dict(response.headers)}") - - assert response.status_code == 402 - - @pytest.mark.asyncio - async def test_client_parses_402(self): - """Test that client can parse 402 response.""" - print("\n" + "=" * 60) - print("CLIENT: Parse 402 Response") - print("=" * 60) - - require_server() - - async with httpx.AsyncClient() as client: - response = await client.get(f"{SERVER_URL}/weather") - - if response.status_code == 402: - import base64 - - header = response.headers.get("payment-required") - decoded = json.loads(base64.b64decode(header)) - - # Scheme is in accepts[0], not at top level - accepts = decoded.get("accepts", []) - scheme = accepts[0].get("scheme") if accepts else None - - print(f" Parsed: scheme={scheme}") - print(f" Accepts: {len(accepts)}") - - assert scheme == "exact" - - @pytest.mark.asyncio - async def test_client_smart_routing_decision(self): - """Test client makes smart routing decision.""" - print("\n" + "=" * 60) - print("CLIENT: Smart Routing Decision") - print("=" * 60) - - require_server() - - async with httpx.AsyncClient() as client: - # Step 1: Request - response = await client.get(f"{SERVER_URL}/weather") - - # Step 2: If 402, parse - if response.status_code == 402: - import base64 - - header = response.headers.get("payment-required") - decoded = json.loads(base64.b64decode(header)) - - accepts = decoded.get("accepts", []) - - # Step 3: Route decision - buyer has NO gateway - has_circle = any(a.get("scheme") == "GatewayWalletBatched" for a in accepts) - buyer_has_gateway = False - - if has_circle and buyer_has_gateway: - print(" → Use Circle Nanopayment (gasless)") - route = "nanopayment" - else: - print(" → Use Basic x402 (on-chain)") - route = "basic_x402" - - assert route == "basic_x402" - - -# ============================================================================= -# TEST 7: Error Scenarios -# ============================================================================= - - -class TestErrorScenarios: - """Test error handling.""" - - def test_server_timeout(self): - """Test handling server timeout.""" - print("\n" + "=" * 60) - print("ERROR: Server Timeout") - print("=" * 60) - - require_server() - - try: - # Very short timeout - httpx.get(f"{SERVER_URL}/weather", timeout=0.001) - except httpx.TimeoutException: - print(" ✓ Timeout handled correctly") - except Exception as e: - print(f" Error: {type(e).__name__}") - - def test_invalid_url(self): - """Test handling invalid URL.""" - print("\n" + "=" * 60) - print("ERROR: Invalid URL") - print("=" * 60) - - require_server() - - try: - response = httpx.get(f"{SERVER_URL}/nonexistent-route-xyz") - print(f" Status: {response.status_code}") - except Exception as e: - print(f" Error: {type(e).__name__}") - - def test_server_unavailable(self): - """Test handling when server is unavailable.""" - print("\n" + "=" * 60) - print("ERROR: Server Unavailable") - print("=" * 60) - - # Try to connect to non-existent server - try: - httpx.get("http://127.0.0.1:9999/health", timeout=2.0) - except httpx.ConnectError: - print(" ✓ Connection error handled correctly") - except Exception as e: - print(f" Error: {type(e).__name__}") - - -# ============================================================================= -# TEST 8: Performance -# ============================================================================= - - -class TestPerformance: - """Test performance characteristics.""" - - def test_response_time(self): - """Test server response time.""" - print("\n" + "=" * 60) - print("PERFORMANCE: Response Time") - print("=" * 60) - - require_server() - - import time - - # Warm up - httpx.get(f"{SERVER_URL}/weather", timeout=5.0) - - # Measure - times = [] - for _ in range(5): - start = time.time() - httpx.get(f"{SERVER_URL}/weather", timeout=5.0) - elapsed = time.time() - start - times.append(elapsed) - print(f" Response time: {elapsed * 1000:.2f}ms") - - avg = sum(times) / len(times) - print(f" Average: {avg * 1000:.2f}ms") - - -# ============================================================================= -# RUN TESTS -# ============================================================================= - -if __name__ == "__main__": - pytest.main([__file__, "-v", "-s"]) diff --git a/tests/test_two_agent_demo.py b/tests/test_two_agent_demo.py deleted file mode 100644 index cfbcbce..0000000 --- a/tests/test_two_agent_demo.py +++ /dev/null @@ -1,593 +0,0 @@ -#!/usr/bin/env python3 -""" -OmniClaw Two-Agent Integration Test -==================================== -Real end-to-end test: Buyer pays Seller through Circle Nanopayments. - -This test runs TWO completely separate agents, each with their own: -- Control Plane server (separate ports) -- Policy file (separate wallets/tokens) -- CLI config (separate configs via env vars) - -Flow: - 1. Start Seller Control Plane (port 8081) → creates seller wallet - 2. Start Buyer Control Plane (port 8082) → creates buyer wallet - 3. Seller: omniclaw-cli serve → x402 paywall on port 9001 - 4. Buyer: Check gateway balance → deposit → pay seller URL - 5. Verify: Seller received payment via Circle Gateway settle() - -Usage: - python3 tests/test_two_agent_demo.py -""" - -import asyncio -import base64 -import contextlib -import json -import os -import signal -import subprocess -import sys -import time - -import httpx - -# ============================================================================ -# CONFIG -# ============================================================================ - -# Shared Circle credentials (same org, different wallets) -CIRCLE_API_KEY = os.environ.get( - "CIRCLE_API_KEY", - "TEST_API_KEY:1965c7f496f043a3c462a58b205ed3be:9f78727fe0a8309e78ed651a6ab79efe", -) -ENTITY_SECRET = os.environ.get( - "ENTITY_SECRET", - "95894cd2a82d2bd76f4668c5008e74c3057026072a79fc37a67014c08e14501c", -) - -# Agent tokens (must match policy files) -SELLER_TOKEN = "seller-agent-token" -BUYER_TOKEN = "buyer-agent-token" - -# Ports -SELLER_CP_PORT = 8081 # Seller control plane -BUYER_CP_PORT = 8082 # Buyer control plane -SELLER_SERVICE_PORT = 9001 # Seller x402 service - -# Paths -BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) -SELLER_POLICY = os.path.join(BASE_DIR, "examples/agent/seller/policy.json") -BUYER_POLICY = os.path.join(BASE_DIR, "examples/agent/buyer/policy.json") - - -# ============================================================================ -# PRETTY PRINT -# ============================================================================ - -def banner(msg): - print(f"\n{'='*60}") - print(f" {msg}") - print(f"{'='*60}\n") - - -def step(num, msg): - print(f"\n ▶ Step {num}: {msg}") - - -def ok(msg): - print(f" ✅ {msg}") - - -def fail(msg): - print(f" ❌ {msg}") - - -def info(msg): - print(f" ℹ️ {msg}") - - -# ============================================================================ -# PROCESS MANAGEMENT -# ============================================================================ - -processes = [] - - -def start_control_plane(name, port, policy_path): - """Start an OmniClaw Control Plane server.""" - env = os.environ.copy() - env["CIRCLE_API_KEY"] = CIRCLE_API_KEY - env["ENTITY_SECRET"] = ENTITY_SECRET - env["OMNICLAW_AGENT_POLICY_PATH"] = policy_path - env["OMNICLAW_NETWORK"] = "ETH-SEPOLIA" - env["OMNICLAW_RPC_URL"] = "https://ethereum-sepolia-rpc.publicnode.com" - env["OMNICLAW_STORAGE_BACKEND"] = "memory" # Each agent gets own memory - env["OMNICLAW_LOG_LEVEL"] = "WARNING" # Quiet - - proc = subprocess.Popen( - [ - sys.executable, "-m", "uvicorn", - "omniclaw.agent.server:app", - "--host", "0.0.0.0", - "--port", str(port), - "--log-level", "warning", - ], - env=env, - cwd=BASE_DIR, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - ) - processes.append(proc) - info(f"{name} Control Plane starting on port {port} (PID: {proc.pid})") - return proc - - -def cleanup(): - """Kill all background processes.""" - for proc in processes: - try: - os.kill(proc.pid, signal.SIGTERM) - proc.wait(timeout=5) - except Exception: - with contextlib.suppress(Exception): - os.kill(proc.pid, signal.SIGKILL) - processes.clear() - - -def wait_for_server(port, name, timeout=60): - """Wait for a server to be ready.""" - start = time.time() - while time.time() - start < timeout: - try: - resp = httpx.get(f"http://localhost:{port}/api/v1/health", timeout=2) - if resp.status_code == 200: - ok(f"{name} is ready on port {port}") - return True - except Exception: - pass - time.sleep(1) - fail(f"{name} failed to start within {timeout}s") - return False - - -# ============================================================================ -# FUNDING (SEPOLIA) -# ============================================================================ - -def fund_buyer_from_metamask(buyer_address: str, amount_usdc: float = 0.5): - """Fund the buyer agent's actual address with Sepolia USDC via provided PK.""" - info(f"Funding Buyer EOA {buyer_address} with {amount_usdc} USDC from Metamask...") - try: - from web3 import Web3 - w3 = Web3(Web3.HTTPProvider("https://ethereum-sepolia-rpc.publicnode.com")) - - # Provided by user for testing - pk = "68315157c4a27ce4650fa6a8de2da92bf4ed0b9b24bf119e798ef37f94700562" - account = w3.eth.account.from_key(pk) - - usdc_address = w3.to_checksum_address("0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238") - - # Standard ERC20 ABI for transfer and balance - erc20_abi = [ - {"constant": False, "inputs": [{"name": "_to", "type": "address"}, {"name": "_value", "type": "uint256"}], "name": "transfer", "outputs": [{"name": "", "type": "bool"}], "type": "function"}, - {"constant": True, "inputs": [{"name": "_owner", "type": "address"}], "name": "balanceOf", "outputs": [{"name": "balance", "type": "uint256"}], "type": "function"}, - ] - - usdc_contract = w3.eth.contract(address=usdc_address, abi=erc20_abi) - - # Check buyer current balance - buyer_checksum = w3.to_checksum_address(buyer_address) - current_bal = usdc_contract.functions.balanceOf(buyer_checksum).call() - amount_atomic = int(amount_usdc * 1_000_000) - - # 1. First, send some native Sepolia ETH for gas (0.005 ETH) - eth_bal = w3.eth.get_balance(buyer_checksum) - if eth_bal < w3.to_wei(0.002, 'ether'): - info("Sending 0.005 Sepolia ETH for gas...") - eth_tx = { - 'to': buyer_checksum, - 'value': w3.to_wei(0.005, 'ether'), - 'gas': 21000, - 'maxFeePerGas': w3.to_wei('20', 'gwei'), - 'maxPriorityFeePerGas': w3.to_wei('2', 'gwei'), - 'nonce': w3.eth.get_transaction_count(account.address), - 'chainId': 11155111 - } - signed_eth = w3.eth.account.sign_transaction(eth_tx, private_key=pk) - w3.eth.send_raw_transaction(signed_eth.raw_transaction) - ok("Sent native Sepolia ETH for gas.") - - if current_bal >= amount_atomic: - ok(f"Buyer already has {current_bal / 1_000_000} USDC in EOA. Skipping USDC funding.") - return True - - # 2. Check metamask USDC balance - funder_bal = usdc_contract.functions.balanceOf(account.address).call() - if funder_bal < amount_atomic: - fail(f"Metamask wallet {account.address} only has {funder_bal / 1_000_000} USDC. Cannot fund.") - return False - - nonce = w3.eth.get_transaction_count(account.address) - tx = usdc_contract.functions.transfer(buyer_checksum, amount_atomic).build_transaction({ - 'chainId': 11155111, - 'gas': 100000, - 'maxFeePerGas': w3.to_wei('20', 'gwei'), - 'maxPriorityFeePerGas': w3.to_wei('2', 'gwei'), - 'nonce': nonce, - }) - - signed_tx = w3.eth.account.sign_transaction(tx, private_key=pk) - tx_hash = w3.eth.send_raw_transaction(signed_tx.raw_transaction) - ok(f"Sent USDC tx: {tx_hash.hex()}. Waiting for confirmation...") - - w3.eth.wait_for_transaction_receipt(tx_hash, timeout=120) - ok("Funding confirmed! Buyer EOA now has USDC and ETH.") - return True - - except Exception as e: - fail(f"Funding failed: {e}") - return False - -# ============================================================================ -# TEST FLOW -# ============================================================================ - -async def run_test(): - banner("OmniClaw Two-Agent Demo Test") - print(" Buyer → deposits to Gateway → pays Seller URL") - print(" Seller → accepts payment → Circle Gateway settles") - print(" Both agents are SEPARATE — different wallets, different control planes\n") - - try: - # ================================================================ - # STEP 1: Start both Control Planes - # ================================================================ - step(1, "Starting Control Planes (with in-memory storage)") - - start_control_plane("Seller", SELLER_CP_PORT, SELLER_POLICY) - start_control_plane("Buyer", BUYER_CP_PORT, BUYER_POLICY) - - if not wait_for_server(SELLER_CP_PORT, "Seller CP", timeout=90): - return False - if not wait_for_server(BUYER_CP_PORT, "Buyer CP", timeout=90): - return False - - # Wait a bit for wallet initialization to complete (background task) - info("Waiting 15s for wallet initialization (background async tasks)...") - await asyncio.sleep(15) - - # ================================================================ - # STEP 2: Verify wallets exist - # ================================================================ - step(2, "Verifying agent wallets") - - seller_client = httpx.AsyncClient( - base_url=f"http://localhost:{SELLER_CP_PORT}", - headers={"Authorization": f"Bearer {SELLER_TOKEN}"}, - timeout=30, - ) - buyer_client = httpx.AsyncClient( - base_url=f"http://localhost:{BUYER_CP_PORT}", - headers={"Authorization": f"Bearer {BUYER_TOKEN}"}, - timeout=30, - ) - - # Seller address - for _attempt in range(10): - try: - resp = await seller_client.get("/api/v1/address") - if resp.status_code == 200: - seller_addr = resp.json().get("address") - ok(f"Seller wallet: {seller_addr}") - break - elif resp.status_code == 425: - info(f"Seller wallet initializing... (attempt {_attempt + 1})") - await asyncio.sleep(5) - else: - info(f"Seller address response: {resp.status_code} {resp.text}") - await asyncio.sleep(3) - except Exception as e: - info(f"Retry {_attempt + 1}: {e}") - await asyncio.sleep(3) - else: - fail("Seller wallet not ready after 10 attempts") - return False - - # Buyer address - for attempt in range(10): - try: - resp = await buyer_client.get("/api/v1/address") - if resp.status_code == 200: - buyer_addr = resp.json().get("address") - ok(f"Buyer wallet: {buyer_addr}") - break - elif resp.status_code == 425: - info(f"Buyer wallet initializing... (attempt {attempt + 1})") - await asyncio.sleep(5) - else: - info(f"Buyer address response: {resp.status_code} {resp.text}") - await asyncio.sleep(3) - except Exception as e: - info(f"Retry {attempt + 1}: {e}") - await asyncio.sleep(3) - else: - fail("Buyer wallet not ready after 10 attempts") - return False - - # ================================================================ - # STEP 2.5: Fund Buyer with Sepolia USDC via provided private key - # ================================================================ - step(2.5, "Funding Buyer Agent EOA with provided Metamask key") - if not fund_buyer_from_metamask(buyer_addr, 0.5): - info("Continuing test anyway, but payment might fail due to no funds...") - - # Trigger manual deposit to Gateway - try: - info("Triggering Gateway deposit transaction...") - dep_resp = await buyer_client.post("/api/v1/deposit", params={"amount": "0.5"}, timeout=120) - if dep_resp.status_code == 200: - dep_data = dep_resp.json() - ok(f"Deposit triggered! Hash: {dep_data.get('deposit_tx_hash')}") - else: - info(f"Deposit error: {dep_resp.status_code} {dep_resp.text}") - except Exception as e: - info(f"Deposit trigger failed: {e}") - - # ================================================================ - # STEP 3: Get seller's nano address (for Gateway payments) - # ================================================================ - step(3, "Getting seller nano address (Gateway wallet)") - - resp = await seller_client.get("/api/v1/nano-address") - if resp.status_code == 200: - seller_nano_addr = resp.json().get("address") - ok(f"Seller nano address: {seller_nano_addr}") - else: - info(f"Seller nano-address not available: {resp.status_code} {resp.text}") - seller_nano_addr = seller_addr - info(f"Using regular address: {seller_nano_addr}") - - # ================================================================ - # STEP 4: Check buyer Gateway balance - # ================================================================ - step(4, "Checking buyer Gateway balance") - - resp = await buyer_client.get("/api/v1/nano-address") - if resp.status_code == 200: - buyer_nano_addr = resp.json().get("address") - ok(f"Buyer nano address: {buyer_nano_addr}") - else: - buyer_nano_addr = buyer_addr - info(f"Buyer nano address fallback: {buyer_nano_addr}") - - # Check gateway balance via NanopaymentClient - from omniclaw.protocols.nanopayments.client import NanopaymentClient - nano_client = NanopaymentClient(api_key=CIRCLE_API_KEY) - - info("Circle Gateway requires 15+ minutes of block confirmations on Sepolia for finality!") - info("Checking Gateway Wallet balance once, then proceeding to verify 402 flow...") - await asyncio.sleep(5) - - try: - buyer_gw_balance = await nano_client.check_balance( - address=buyer_nano_addr, - network="eip155:11155111", - ) - if buyer_gw_balance.available >= 10_000: - ok(f"Buyer Gateway balance ready: {buyer_gw_balance.available} atomic") - else: - info("Gateway Balance is current 0, as expected. Deposit is pending finality.") - info(f"Buyer nano Gateway address to monitor: {buyer_nano_addr}") - info("Continuing test to verify 402 flow regardless...") - except Exception as e: - info(f"Error checking balance: {e}. Continuing...") - - # ================================================================ - # STEP 5: Start Seller x402 Service (omniclaw-cli serve) - # ================================================================ - step(5, "Starting Seller x402 service (omniclaw-cli serve)") - - # The serve command needs to talk to the seller's control plane - serve_env = os.environ.copy() - serve_env["OMNICLAW_SERVER_URL"] = f"http://localhost:{SELLER_CP_PORT}" - serve_env["OMNICLAW_TOKEN"] = SELLER_TOKEN - serve_env["CIRCLE_API_KEY"] = CIRCLE_API_KEY - serve_env["OMNICLAW_CONFIG_DIR"] = "/tmp/omniclaw_seller_test" - - serve_proc = subprocess.Popen( - [ - sys.executable, "-m", "omniclaw.cli_agent", - "serve", - "--price", "0.01", - "--endpoint", "/api/data", - "--exec", "echo '{\"result\": \"premium data from Agent A\"}'", - "--port", str(SELLER_SERVICE_PORT), - ], - env=serve_env, - cwd=BASE_DIR, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - ) - processes.append(serve_proc) - info(f"Seller service starting on port {SELLER_SERVICE_PORT} (PID: {serve_proc.pid})") - - # Wait for it to be ready - await asyncio.sleep(5) - for _attempt in range(10): - try: - resp = httpx.get( - f"http://localhost:{SELLER_SERVICE_PORT}/api/data", - timeout=5, - ) - if resp.status_code == 402: - ok("Seller service returned 402 (Payment Required)") - break - else: - info(f"Unexpected status: {resp.status_code}") - await asyncio.sleep(2) - except Exception as e: - info(f"Waiting for seller service... ({e})") - await asyncio.sleep(2) - else: - fail("Seller service did not start") - return False - - # ================================================================ - # STEP 6: Verify 402 response has correct x402 v2 structure - # ================================================================ - step(6, "Verifying 402 response (x402 v2 compliance)") - - resp = httpx.get( - f"http://localhost:{SELLER_SERVICE_PORT}/api/data", - timeout=10, - ) - - assert resp.status_code == 402, f"Expected 402, got {resp.status_code}" - - # Check PAYMENT-REQUIRED header - payment_required = resp.headers.get("payment-required") or resp.headers.get("PAYMENT-REQUIRED") - if payment_required: - req_data = json.loads(base64.b64decode(payment_required)) - ok("PAYMENT-REQUIRED header present") - ok(f"x402Version: {req_data.get('x402Version')}") - - accepts = req_data.get("accepts", []) - if accepts: - kind = accepts[0] - ok(f"scheme: {kind.get('scheme')}") - ok(f"network: {kind.get('network')}") - ok(f"asset: {kind.get('asset', 'MISSING')}") - ok(f"amount: {kind.get('amount')} atomic") - ok(f"maxTimeoutSeconds: {kind.get('maxTimeoutSeconds', 'MISSING')}") - ok(f"payTo: {kind.get('payTo')}") - - extra = kind.get("extra", {}) - ok(f"extra.name: {extra.get('name', 'MISSING')}") - ok(f"extra.version: {extra.get('version', 'MISSING')}") - ok(f"extra.verifyingContract: {extra.get('verifyingContract', 'MISSING')}") - - # Validate completeness - required_fields = ["scheme", "network", "asset", "amount", "maxTimeoutSeconds", "payTo", "extra"] - missing = [f for f in required_fields if f not in kind or kind[f] is None] - if missing: - fail(f"Missing required fields: {missing}") - else: - ok("✅ ALL x402 v2 fields present — Circle compliant!") - - if extra.get("name") == "GatewayWalletBatched": - ok("✅ GatewayWalletBatched scheme — gasless nanopayment!") - else: - fail(f"Expected GatewayWalletBatched, got {extra.get('name')}") - else: - fail("No accepts array in 402 response") - else: - # Check response body instead - body = resp.json() - info(f"402 body: {json.dumps(body, indent=2)}") - if "accepts" in body: - ok("Found accepts in response body") - else: - fail("No PAYMENT-REQUIRED header found") - - # ================================================================ - # STEP 7: Buyer attempts payment (omniclaw-cli pay via API) - # ================================================================ - step(7, "Buyer paying Seller (x402 nanopayment flow)") - - info("Sending payment request to Buyer Control Plane...") - info(f"Target: http://localhost:{SELLER_SERVICE_PORT}/api/data") - - try: - pay_resp = await buyer_client.post( - "/api/v1/pay", - json={ - "recipient": f"http://localhost:{SELLER_SERVICE_PORT}/api/data", - "method": "GET", - }, - timeout=60, - ) - - pay_data = pay_resp.json() - info(f"Payment response status: {pay_resp.status_code}") - info(f"Payment result: {json.dumps(pay_data, indent=2)}") - - if pay_data.get("success"): - ok("🎉 PAYMENT SUCCESSFUL!") - ok(f"Amount: {pay_data.get('amount')} USDC") - ok(f"Method: {pay_data.get('method')}") - ok(f"Transaction: {pay_data.get('transaction_id', 'N/A')}") - ok(f"Status: {pay_data.get('status')}") - - # Step 8: Verify settlement - step(8, "Verifying settlement (seller side)") - info("Circle Gateway settles in batches — balance credited immediately") - - try: - seller_balance = await nano_client.check_balance( - address=seller_nano_addr, - network="eip155:11155111", - ) - ok(f"Seller Gateway balance: {seller_balance.available} atomic ({seller_balance.available / 1_000_000:.6f} USDC)") - except Exception as e: - info(f"Could not check seller balance: {e}") - - else: - error = pay_data.get("error", "Unknown error") - info(f"Payment did not succeed: {error}") - - if "insufficient" in error.lower() or "balance" in error.lower(): - info("⚠️ This is expected if buyer has no Gateway balance!") - info("The 402 and settlement flows are working correctly.") - info("To complete the demo, deposit USDC to the buyer's Gateway wallet.") - ok("✅ x402 protocol flow is WORKING (needs Gateway balance to complete)") - else: - fail(f"Unexpected error: {error}") - - except Exception as e: - info(f"Payment request failed: {e}") - import traceback - traceback.print_exc() - - # ================================================================ - # Summary - # ================================================================ - banner("Test Summary") - print(" ✅ Seller Control Plane: Started and healthy") - print(" ✅ Buyer Control Plane: Started and healthy") - print(" ✅ Separate wallets: Each agent has its own wallet") - print(" ✅ Seller serve: Returns x402 v2 compliant 402") - print(" ✅ GatewayWalletBatched: Circle Nanopayment scheme") - print(" ✅ Buyer→Seller: x402 payment flow initiated") - print("") - print(" For a fully complete payment, ensure:") - print(f" 1. Buyer has Gateway balance (fund: {buyer_nano_addr})") - print(" 2. Seller accepts on correct network") - print("") - - return True - - finally: - # Cleanup - info("Cleaning up processes...") - cleanup() - - -# ============================================================================ -# MAIN -# ============================================================================ - -if __name__ == "__main__": - try: - result = asyncio.run(run_test()) - sys.exit(0 if result else 1) - except KeyboardInterrupt: - print("\n\n Interrupted. Cleaning up...") - cleanup() - sys.exit(130) - except Exception as e: - print(f"\n ❌ Fatal error: {e}") - import traceback - traceback.print_exc() - cleanup() - sys.exit(1) diff --git a/uv.lock b/uv.lock index 968404d..edf1b20 100644 --- a/uv.lock +++ b/uv.lock @@ -1,6 +1,11 @@ version = 1 revision = 3 requires-python = ">=3.10" +resolution-markers = [ + "python_full_version >= '3.14'", + "python_full_version == '3.13.*'", + "python_full_version < '3.13'", +] [[package]] name = "aiohappyeyeballs" @@ -1055,6 +1060,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl", hash = "sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16", size = 469047, upload-time = "2025-07-17T16:51:58.613Z" }, ] +[[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 = "docutils" version = "0.22.4" @@ -1064,6 +1078,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/02/10/5da547df7a391dcde17f59520a231527b8571e6f46fc8efb02ccb370ab12/docutils-0.22.4-py3-none-any.whl", hash = "sha256:d0013f540772d1420576855455d050a2180186c91c15779301ac2ccb3eeb68de", size = 633196, upload-time = "2025-12-18T19:00:18.077Z" }, ] +[[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" @@ -1080,7 +1107,7 @@ wheels = [ [[package]] name = "eth-account" -version = "0.12.0" +version = "0.13.7" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "bitarray" }, @@ -1094,9 +1121,9 @@ dependencies = [ { name = "pydantic" }, { name = "rlp" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/5b/d3/043670c7c744b87df077b70f02d73a9adf128a9fdbf75bdd98594864a81d/eth-account-0.12.0.tar.gz", hash = "sha256:7e4fe05460c8fc7cf59373293f7120c0ad2a59bd3b6f9bce557ff50a113c8095", size = 7474151, upload-time = "2024-04-01T18:21:48.906Z" } +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/25/3a/05a03b1ede3ae4d050df23028bb0710583f3f15d5283bde10bc6a9b1e45e/eth_account-0.12.0-py3-none-any.whl", hash = "sha256:b72ffc3485fe66bc5c07a0b5fec3bb59261ff69dcf6edaf973073c3736d3df6e", size = 354917, upload-time = "2024-04-01T18:21:41.796Z" }, + { 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]] @@ -1211,6 +1238,174 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/8f/ea/18f6d0457f9efb2fc6fa594857f92810cadb03024975726db6546b3d6fcf/fastapi-0.135.2-py3-none-any.whl", hash = "sha256:0af0447d541867e8db2a6a25c23a8c4bd80e2394ac5529bd87501bbb9e240ca5", size = 117407, upload-time = "2026-03-23T14:12:43.284Z" }, ] +[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.24" +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/6e/58/74797ae9e4610cfa0c6b34c8309096d3b20bb29be3b8b5fbf1004d10fa5f/fastapi_cli-0.0.24.tar.gz", hash = "sha256:1afc9c9e21d7ebc8a3ca5e31790cd8d837742be7e4f8b9236e99cb3451f0de00", size = 19043, upload-time = "2026-02-24T10:45:10.476Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/4b/68f9fe268e535d79c76910519530026a4f994ce07189ac0dded45c6af825/fastapi_cli-0.0.24-py3-none-any.whl", hash = "sha256:4a1f78ed798f106b4fee85ca93b85d8fe33c0a3570f775964d37edb80b8f0edc", size = 12304, upload-time = "2026-02-24T10:45:09.552Z" }, +] + +[package.optional-dependencies] +standard = [ + { name = "fastapi-cloud-cli" }, + { name = "uvicorn", extra = ["standard"] }, +] + +[[package]] +name = "fastapi-cloud-cli" +version = "0.17.1" +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/96/57/cee8e91b83f39e75ae5562a2237261442a8179dcb3b631c7398113157398/fastapi_cloud_cli-0.17.1.tar.gz", hash = "sha256:0baece208fa88063bec46dccb5fb512f3199162092165e57654b44e64adbc44d", size = 47409, upload-time = "2026-04-27T13:38:07.094Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/a0/e252b68cf155409afabea037ab2971f41509481838847f6503fe890884ea/fastapi_cloud_cli-0.17.1-py3-none-any.whl", hash = "sha256:325e0199bdac7cb86f5df4f4a1d2070054095588088ef7b923a60cec458dcd63", size = 34046, upload-time = "2026-04-27T13:38:08.319Z" }, +] + +[[package]] +name = "fastar" +version = "0.11.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/03/0f/0aeb3fc50046617702acc0078b277b58367fd62eb727b9ec733ae0e8bbcc/fastar-0.11.0.tar.gz", hash = "sha256:aa7f100f7313c03fdb20f1385927ba95671071ba308ad0c1763fef295e1895ce", size = 70238, upload-time = "2026-04-13T17:11:17.143Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9b/4a/0d79fe52243a4130aa41d0a3a9eea22e00427db761e1a6782ee817c50222/fastar-0.11.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:e7c906ad371ca365591ebcb7630009923f3eceb20956814494d15591a78e9e46", size = 709786, upload-time = "2026-04-13T17:09:53.974Z" }, + { url = "https://files.pythonhosted.org/packages/9f/e4/77c94eaafc035e39f5ce5176e32743da4e3fe890f28790e708e53d8f75cd/fastar-0.11.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6919497b35fa5bd978d2c26ee117cf1771b90ee5073f7518e44b9bc364b57715", size = 632127, upload-time = "2026-04-13T17:09:39.023Z" }, + { url = "https://files.pythonhosted.org/packages/3c/f6/97658dd992f4e45747d35adb24c0b100f6b6d451490685ae3fe8a3a2ee1b/fastar-0.11.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:56b50206aeedd99e22b83289e6fb3ff8f7d7da4407d2419902e4716b4f90585a", size = 869608, upload-time = "2026-04-13T17:09:08.268Z" }, + { url = "https://files.pythonhosted.org/packages/e9/fc/81c1ec4d8146a437399e7b95631b51be312f323a9ce64569f932db6c3914/fastar-0.11.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7a1811a69ae81d469720df0c8af3f84f834a93b5e4f8be0e0e8bde6a52fa11f2", size = 762925, upload-time = "2026-04-13T17:07:52.788Z" }, + { url = "https://files.pythonhosted.org/packages/b9/35/49baf480ecb197aea7ce2515c503a2f25061958dd3b4c98e98a3a11cdcc7/fastar-0.11.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:10486238c55589a3947c38f9cfb88a67d8a608eb8dddc722038237d0278a41d7", size = 759913, upload-time = "2026-04-13T17:08:07.324Z" }, + { url = "https://files.pythonhosted.org/packages/94/eb/946f1980267f2824efb7d7c518d47a49b89c0e9cd7c449301f5a7531558a/fastar-0.11.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1555ef9992d368a6ec39092276990cef8d329c39a1d86ebd847eaa3b10efd472", size = 926054, upload-time = "2026-04-13T17:08:22.196Z" }, + { url = "https://files.pythonhosted.org/packages/0c/19/d5eb611085ce054382570d8d4e24a5e2ff23cd6d2404528a6643841d6059/fastar-0.11.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b1f4aca0a9620b76988bbf6225cdea6678a392902444ca18bb8a51495b165a89", size = 818594, upload-time = "2026-04-13T17:08:52.366Z" }, + { url = "https://files.pythonhosted.org/packages/4a/52/18e8d55c0d3d917713f381cb2d0cb793da00c209c802e011d8dc72018cd5/fastar-0.11.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:75beeecac7d11a666a6c4a0b7f7e80842ae5cf523f2f890b99c78fc82b403545", size = 823005, upload-time = "2026-04-13T17:09:23.051Z" }, + { url = "https://files.pythonhosted.org/packages/2c/b4/0fecdcf33e5aaffe777b96a1c10a3204fe0b05bf18e971033a0bfedafc1c/fastar-0.11.0-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:a08cdf5d16daa401c65c9c7493a18db7dc515c52155a17071ec7098bb07da9d3", size = 887115, upload-time = "2026-04-13T17:08:37.385Z" }, + { url = "https://files.pythonhosted.org/packages/08/f8/2a6ad1c2523eb72a4595a9331162fc67ce0f0aee3348728598026c516986/fastar-0.11.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:6e210375e5a7ba53586cbd6017aa417d2d2ceacbe8671682470281bd0a15e8ef", size = 973595, upload-time = "2026-04-13T17:10:09.258Z" }, + { url = "https://files.pythonhosted.org/packages/5c/a6/2aa48843228673feacc2b80876b8924e63ea9c5f5f607bd7a72416b86bae/fastar-0.11.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:a2988eb2604b8e15670f355425e8c800e4dcd4edfbcbfe194397f8f17b7eb19e", size = 1036988, upload-time = "2026-04-13T17:10:26.133Z" }, + { url = "https://files.pythonhosted.org/packages/92/ac/3dd14b21c323e8484f47c910110d1d93139ba44621ac2c4c597dbe9fcdb7/fastar-0.11.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:34abc857b46068fdf91d157bd0203bfd6791dc7a432d1ed180f5af6c2f5bcce9", size = 1078267, upload-time = "2026-04-13T17:10:43.645Z" }, + { url = "https://files.pythonhosted.org/packages/de/a1/3f89e58d6fa99160c9e7e17220c8ab5040b5cc017c4fac2356c6ed18453d/fastar-0.11.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:0d884be84e37a01053776395441fc960031974e0265801ce574efc3d05e0cdaf", size = 1032551, upload-time = "2026-04-13T17:11:00.667Z" }, + { url = "https://files.pythonhosted.org/packages/f6/ea/24dd3cfc2096933d7d2a80c926e79602cff1fa481124ed2165b60c1dd9ef/fastar-0.11.0-cp310-cp310-win32.whl", hash = "sha256:c721c1ad758e3e4c2c1fd9e96911a0fa58c0a6be5668f1bcfd0b741e72c7cb63", size = 456022, upload-time = "2026-04-13T17:11:41.859Z" }, + { url = "https://files.pythonhosted.org/packages/82/ef/6eb39ee9cdd59822d1c7337c4d28fdc948885bdf455af9e70efa9879e06f/fastar-0.11.0-cp310-cp310-win_amd64.whl", hash = "sha256:ba4180b7c3080f55f9035fdd7d8c39fe0e1485087a68ff615bb4784a10b8106b", size = 488392, upload-time = "2026-04-13T17:11:27.486Z" }, + { url = "https://files.pythonhosted.org/packages/11/7a/fb367bdaf4efa2c7952a45aeab2e87a564293ecffe150af673ec8edfda46/fastar-0.11.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:b82fd6f996e65a86f67a6bd64dd22ef3e8ae2dcaed0ae3b550e71f7e1bbb1df5", size = 709869, upload-time = "2026-04-13T17:09:55.62Z" }, + { url = "https://files.pythonhosted.org/packages/80/ff/b87efb0dcfd081c62c7c7601d7681dabe63103cd51fc16f8d57a1ab45961/fastar-0.11.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:27eed386fd0558e6daa29211111bbd7b740f7c7e881197f8a00ac7c0f3cdb1d7", size = 631668, upload-time = "2026-04-13T17:09:40.537Z" }, + { url = "https://files.pythonhosted.org/packages/24/7c/0ed6dd38b9adc04b3a8ec3b7045908e7c2170ba0ff6e6d2c51bc9fc770f3/fastar-0.11.0-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:a6931bebc1d8e95ddeef55732c195449e6b44ef33aa31b325505097ed3b4d6aa", size = 869663, upload-time = "2026-04-13T17:09:09.78Z" }, + { url = "https://files.pythonhosted.org/packages/58/ce/8b7fb3f23855accebaaf2d2637eac7f261a7a5d936f861a172079f1ef511/fastar-0.11.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:891f72ce42a5e28a74fbd4d5fbf1a3ac1a1163d13cbc200cbd005fb0fabc54bd", size = 762938, upload-time = "2026-04-13T17:07:54.51Z" }, + { url = "https://files.pythonhosted.org/packages/07/cc/5491e2b677bb841f768e3aba052d0344338a5c78aa5d4c18b443831a8e8d/fastar-0.11.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5b83c1f61f7017d6e1498568038f8745440cfc16ca2f697ec81bac83050108f6", size = 759232, upload-time = "2026-04-13T17:08:08.864Z" }, + { url = "https://files.pythonhosted.org/packages/4e/b7/643630bdbd179e41e9fae31c03b4cf6061dbf4d6fbbae8425d16eb12545d/fastar-0.11.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:db73a9b765a516e73983b25341e7b5e0189733878279e278b2295131b0e3a21e", size = 926271, upload-time = "2026-04-13T17:08:23.68Z" }, + { url = "https://files.pythonhosted.org/packages/09/5d/37ade50003b4540e0a53ef100f6692d7ab2ac1122d5acf39920cc09a3e8b/fastar-0.11.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:625827d52eb4e8fec942e0233f125ff8010fcf6a67c0a974a8e5f4666b771e3c", size = 818634, upload-time = "2026-04-13T17:08:54.268Z" }, + { url = "https://files.pythonhosted.org/packages/c3/ff/135d177de32cc1e837c99019e4643e6e79352bde49544d4ece5b5eebf56b/fastar-0.11.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d7f5fd8fa21ec0a88296a38dc5d7fc35efd3b26d46a17b8b7c73c5563925ca15", size = 822755, upload-time = "2026-04-13T17:09:25.01Z" }, + { url = "https://files.pythonhosted.org/packages/27/cb/b835dbe76ceac7fa6105851468c259ffd06830eb9c029402e499d0ec153b/fastar-0.11.0-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:8c15af91b8cd87ddf23ea55355ae513c1de3ab67178f26dad017c9e9c0af6096", size = 887101, upload-time = "2026-04-13T17:08:39.248Z" }, + { url = "https://files.pythonhosted.org/packages/9e/54/aa8289eb57fc550535470397cb051f5a58a7c89ca4de31d5502b916dd894/fastar-0.11.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:03a112395a8b0bff251423bd1564c012f0cc058ad8b6bd8fba96f3d7fc117e44", size = 973606, upload-time = "2026-04-13T17:10:10.98Z" }, + { url = "https://files.pythonhosted.org/packages/1f/fd/776d50a0897c01dc6bfd0926772ee913436fdae91b9affaf0a0cbd09f0a1/fastar-0.11.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:f2994bb8f5f8c11eb12beae1e6e77a907173c9819236b8a4c8f0573652ceccce", size = 1036696, upload-time = "2026-04-13T17:10:28.502Z" }, + { url = "https://files.pythonhosted.org/packages/c8/f1/cf0f9b499fb37ac065c8a01ec642f96a3c5eb849c38ae983b59f3b3245e0/fastar-0.11.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:dcf99e4b5973d842c7f19c776c3a83cdc0977d505edce6206438505c0456b517", size = 1078182, upload-time = "2026-04-13T17:10:45.318Z" }, + { url = "https://files.pythonhosted.org/packages/f8/9e/21e4701aec4a1123d4dc4d31578dc18875582b5710e4725f7ceb752a248b/fastar-0.11.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:29c9c386dc0d5dda78845a8e6b1480d26ab861c1e0b68f42ae5735cb70ca07f1", size = 1032336, upload-time = "2026-04-13T17:11:02.364Z" }, + { url = "https://files.pythonhosted.org/packages/ce/e2/5872b28c72c27ec1a00760eace6ff35f714f41ebbd5208cf016b12e29250/fastar-0.11.0-cp311-cp311-win32.whl", hash = "sha256:030b2580fc394f2c9b7890b6735810404e9b9ed5e0344db150b945965b5482b7", size = 457368, upload-time = "2026-04-13T17:11:43.528Z" }, + { url = "https://files.pythonhosted.org/packages/fd/6e/ce6832a16193eb4466f4108be8809c249b51cb1f89dd7894545700d079d5/fastar-0.11.0-cp311-cp311-win_amd64.whl", hash = "sha256:83ab57ae067969cd0b483ac3b6dccc4b595fc77f5c820760998648d4c42822b5", size = 488605, upload-time = "2026-04-13T17:11:29.161Z" }, + { url = "https://files.pythonhosted.org/packages/15/5a/9cfb80661cf38fd7b0889224beb7d2746784d4ade2a931ed9775a18d8602/fastar-0.11.0-cp311-cp311-win_arm64.whl", hash = "sha256:27b1a4cee2298b704de8151d310462ee7335ed036011ca9aa6e784b30b6c73a9", size = 464580, upload-time = "2026-04-13T17:11:18.583Z" }, + { url = "https://files.pythonhosted.org/packages/0f/06/a5773706afc8bd496769786590bbc56d2d0ee419a299cc12ea3f5717fcf3/fastar-0.11.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:3c51f1c2cdddbd1420d2897ace7738e36c65e17f6ae84e0bfe763f8d1068bb97", size = 708394, upload-time = "2026-04-13T17:09:57.269Z" }, + { url = "https://files.pythonhosted.org/packages/cc/a6/d5e2a4e48495616440a21eed07558219ca90243ad00b0502586f95bd4833/fastar-0.11.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0d9d6b052baf5380baea866675dab6ccd04ec2460d12b1c46f10ce3f4ee6a820", size = 628417, upload-time = "2026-04-13T17:09:42.145Z" }, + { url = "https://files.pythonhosted.org/packages/ab/69/9816d69ac8265c9e50456637a487ccfb7a9c566efd9dbcd673df9c2558c2/fastar-0.11.0-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:bd2f05666d4df7e14885b5c38fefd92a785917387513d33d837ff42ec143a22f", size = 863950, upload-time = "2026-04-13T17:09:11.506Z" }, + { url = "https://files.pythonhosted.org/packages/5b/0d/f88daad53aff2e754b6b5ff2a7113f72447a34f6ef17cc23ca99988117b7/fastar-0.11.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c1e6e74aba1ae77ca4aedcaf1697cd413319f4c88a5ccbe5b42c709517c5097e", size = 760737, upload-time = "2026-04-13T17:07:55.958Z" }, + { url = "https://files.pythonhosted.org/packages/2f/a6/82ef4ecd969d50d92ed3ed9dbd8fe77faa24be5e5736f716edc9f4ce8d62/fastar-0.11.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:38ef77fe940bbc9b37a98bd838727f844b11731cd39358a2640ff864fb385086", size = 757603, upload-time = "2026-04-13T17:08:10.623Z" }, + { url = "https://files.pythonhosted.org/packages/03/35/50249f0d827251f8ac511495e2eacccebda80a00a0ad73e9615b8113b84f/fastar-0.11.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8955e61b32d6aff82c983217abf80933fd823b0e727586fc72f08043d996fd59", size = 923952, upload-time = "2026-04-13T17:08:25.526Z" }, + { url = "https://files.pythonhosted.org/packages/7b/d8/faee41659e9c379d906d24eaee6d6833ac8cfef0a5df480e5c2a8d3efb33/fastar-0.11.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:483532442cdb08fbff0169510224eae0836f2f672cea6aacb52847d90fefdc46", size = 816574, upload-time = "2026-04-13T17:08:56.076Z" }, + { url = "https://files.pythonhosted.org/packages/22/47/0448ea7992b997dad2bf004bfd98eca74b5858630eae080b50c7b17d9ddc/fastar-0.11.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ef5a6071121e05d8287fc75bccb054bcbac8bb0501200a0c0a8feeace5303ea4", size = 819382, upload-time = "2026-04-13T17:09:26.66Z" }, + { url = "https://files.pythonhosted.org/packages/33/ef/0d63eb43586831b7a6f8b22c4d77125a7c594423af1f4f090fa9541b9b40/fastar-0.11.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:e45e598af5afe8412197d4786efd6cf29be02e7d3d4f6a3461149eae5d7e94f1", size = 885254, upload-time = "2026-04-13T17:08:40.9Z" }, + { url = "https://files.pythonhosted.org/packages/01/25/edd584675d69e49a165052c3ee886df1c5d574f3e7d813c990306387c623/fastar-0.11.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2e160919b1c47ddb8538e7e8eb4cd527281b40f0bf75110a75993838ef61f286", size = 971239, upload-time = "2026-04-13T17:10:12.997Z" }, + { url = "https://files.pythonhosted.org/packages/a5/37/e8bb24f506ba2b08fbaf36c5800e843bd4d542954e9331f00418e2d23349/fastar-0.11.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:4bb4dc0fc8f7a6807febcebce8a2f3626ba4955a9263d81ecc630aad83be84c0", size = 1035185, upload-time = "2026-04-13T17:10:30.207Z" }, + { url = "https://files.pythonhosted.org/packages/9a/bf/be753736296338149ee4cb3e92e2b5423d6ba17c7b951d15218fd7e99bbf/fastar-0.11.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:4ec95af56aa173f6e320e1183001bf108ba59beaf13edd1fc8200648db203588", size = 1072191, upload-time = "2026-04-13T17:10:47.072Z" }, + { url = "https://files.pythonhosted.org/packages/d2/cd/a81c1aaafb5a22ce57c98ae22f39c89413ed53e4ee6e1b1444b0bd666a6c/fastar-0.11.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:136cf342735464091c39dc3708168f9fdeb9ebea40b1ead937c61afaf46143d9", size = 1028054, upload-time = "2026-04-13T17:11:04.293Z" }, + { url = "https://files.pythonhosted.org/packages/ec/88/1ce4eed3d70627c95f49ca017f6bbbf2ddcc4b0c601d293259de7689bc20/fastar-0.11.0-cp312-cp312-win32.whl", hash = "sha256:35f23c11b556cc4d3704587faacbc0037f7bdf6c4525cd1d09c70bda4b1c6809", size = 454198, upload-time = "2026-04-13T17:11:45.168Z" }, + { url = "https://files.pythonhosted.org/packages/8f/1d/26ce92f4331cd61a69840db9ca6115829805eec24f285481a854f578e917/fastar-0.11.0-cp312-cp312-win_amd64.whl", hash = "sha256:920bc56c3c0b8a8ca492904941d1883c1c947c858cd93343356c29122a38f44c", size = 486697, upload-time = "2026-04-13T17:11:31.084Z" }, + { url = "https://files.pythonhosted.org/packages/ed/96/e6eda4480559c69b05d466e7b5ea9170e81fef3795a73e059959a3258319/fastar-0.11.0-cp312-cp312-win_arm64.whl", hash = "sha256:395248faf89e8a6bd5dc1fd544c8465113b627cb6d7c8b296796b60ebea33593", size = 462591, upload-time = "2026-04-13T17:11:20.577Z" }, + { url = "https://files.pythonhosted.org/packages/c9/d6/3be260037e86fb694e88d47f583bac3a0188c99cee1a6b257ac26cb6b53c/fastar-0.11.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:33f544b08b4541b678e53749b4552a44720d96761fb79c172b005b1089c443ed", size = 707975, upload-time = "2026-04-13T17:09:58.866Z" }, + { url = "https://files.pythonhosted.org/packages/e1/cd/7867aefb1784662554a335f2952c75a50f0c70585ed0d2210d6cc15e5627/fastar-0.11.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:91c1c792447e4a642745f347ff9847c52af39633071c57ee67ed53c157fc3506", size = 628460, upload-time = "2026-04-13T17:09:43.776Z" }, + { url = "https://files.pythonhosted.org/packages/e5/2b/d11d84bdd5e0e377771b955755771e3460b290da5809cb78c1b735ee2228/fastar-0.11.0-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:881247e6b6eaea59fc6569f9b61447aa6b9fc2ee864e048b4643d69c52745805", size = 863054, upload-time = "2026-04-13T17:09:13.048Z" }, + { url = "https://files.pythonhosted.org/packages/25/39/d3f428b318fa940b1b6e785b8d54fc895dfb5d5b945ef8d5442ffa904fb2/fastar-0.11.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:863b7929845c9fec92ef6c8d59579cf46af5136655e5342f8df5cebe46cab06c", size = 760247, upload-time = "2026-04-13T17:07:57.396Z" }, + { url = "https://files.pythonhosted.org/packages/9e/04/03949aee82aabb8ede06ac5a4a5579ffaf98a8fe59ce958494508ff15513/fastar-0.11.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:96b4a57df12bf3211662627a3ea29d62ecb314a2434a0d0843f9fc23e47536e5", size = 756512, upload-time = "2026-04-13T17:08:12.415Z" }, + { url = "https://files.pythonhosted.org/packages/3f/0c/2ca1ae0a3828ca51047962d932b80daca2522db73e8cb9d040cb6ebe28d5/fastar-0.11.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ceef1c2c4df7b7b8ebd3f5d718bbf457b9bbdf25ce0bd07870211ec4fbd9aff4", size = 922183, upload-time = "2026-04-13T17:08:27.187Z" }, + { url = "https://files.pythonhosted.org/packages/65/68/7fe808b1f73a68e686f25434f538c6dc10ef4dfb3db0ace22cd861744bf8/fastar-0.11.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b8e545918441910a779659d4759ad0eef349e935fbdb4668a666d3681567eb05", size = 816394, upload-time = "2026-04-13T17:08:57.657Z" }, + { url = "https://files.pythonhosted.org/packages/1f/17/07d086080f8a83b8d7966955e29bcdbd6a060f5bd949dc9d5abd3658cead/fastar-0.11.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:28095bb8f821e85fc2764e1a55f03e5e2876dee2abe7cd0ee9420d929905d643", size = 818983, upload-time = "2026-04-13T17:09:28.46Z" }, + { url = "https://files.pythonhosted.org/packages/fb/e2/2c4edf0910af2e814ff6d65b77a91196d472ca8a9fb2033bd983f6856caa/fastar-0.11.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:0fafb95ecbe70f666a5e9b35dd63974ccdc9bb3d99ccdbd4014a823ec3e659b5", size = 884689, upload-time = "2026-04-13T17:08:42.763Z" }, + { url = "https://files.pythonhosted.org/packages/fa/ba/04fdcbd6558e60de4ced3b55230fac47675d181252582b2fcec3c74608e5/fastar-0.11.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:af48fed039b94016629dcdad1c95c90c486326dd068de2b0a4df419ee09b6821", size = 970677, upload-time = "2026-04-13T17:10:15.124Z" }, + { url = "https://files.pythonhosted.org/packages/df/b3/2b860a9658550167dbd5824c85e88d0b4b912bf493e42a6322544d6e483d/fastar-0.11.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:74cd96163f39b8638ab4e8d49708ca887959672a22871d8170d01f067319533b", size = 1034026, upload-time = "2026-04-13T17:10:32.318Z" }, + { url = "https://files.pythonhosted.org/packages/b7/9b/fa42ea1188b144bac4b1b60753dfd449974a4d5eda132029ee7711569f94/fastar-0.11.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:4e8b993cb5613bab495ed482810bedc0986633fcb9a3b55c37ec88e0d6714f6a", size = 1071147, upload-time = "2026-04-13T17:10:48.833Z" }, + { url = "https://files.pythonhosted.org/packages/95/c8/d2e501556dca9f1fbc9246111a31792fb49ad908fa4927f34938a97a3604/fastar-0.11.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:dfe39d91fc28e37e06162d94afe01050220edb7df554acb5b702b5503e564816", size = 1028377, upload-time = "2026-04-13T17:11:06.374Z" }, + { url = "https://files.pythonhosted.org/packages/db/33/5f11f23eca0a569cd052507bc45dda2e5468697f8665728d25be44120f7d/fastar-0.11.0-cp313-cp313-win32.whl", hash = "sha256:c5f63d4d99ff4bfb37c659982ec413358bdee747005348756cc50a04d412d989", size = 454089, upload-time = "2026-04-13T17:11:46.821Z" }, + { url = "https://files.pythonhosted.org/packages/da/2f/35ff03c939cba7a255a9132367873fec6c355fd06a7f84fedcbaf4c8129f/fastar-0.11.0-cp313-cp313-win_amd64.whl", hash = "sha256:8690ed1928d31ded3ada308e1086525fb3871f5fa81e1b69601a3f7774004583", size = 486312, upload-time = "2026-04-13T17:11:32.86Z" }, + { url = "https://files.pythonhosted.org/packages/ef/71/ee9246cbfcbfd4144558f35e7e9a306ffe0a7564730a5188c45f21d2dab8/fastar-0.11.0-cp313-cp313-win_arm64.whl", hash = "sha256:d977ded9d98a0719a305e0a4d5ee811f1d3e856d853a50acb8ae833c3cd6d5d2", size = 461975, upload-time = "2026-04-13T17:11:22.589Z" }, + { url = "https://files.pythonhosted.org/packages/7a/cd/3644c48ecac456f928c12d47ec3bed36c36555b17c3859856f1ff860265d/fastar-0.11.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:71375bd6f03c2a43eb47bd949ea38ff45434917f9cdac79675c5b9f60de4fa73", size = 707860, upload-time = "2026-04-13T17:10:00.371Z" }, + { url = "https://files.pythonhosted.org/packages/69/ca/dee04476ae3626b2b040a60ad84628f77e1ffd8444232f2426b0ca1e0d7e/fastar-0.11.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:eddfd9cab16e19ae247fe44bf992cb403ccfe27d3931d6de29a4695d95ad386c", size = 628216, upload-time = "2026-04-13T17:09:45.355Z" }, + { url = "https://files.pythonhosted.org/packages/dc/5e/9395c7353d079cb4f5be0f7982ce0dc9f2e7dec5fd175eef466729d6023a/fastar-0.11.0-cp314-cp314-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:7c371f1d4386c699018bb64eb2fa785feacf32785559049d2bb72fe4af023f53", size = 864378, upload-time = "2026-04-13T17:09:14.611Z" }, + { url = "https://files.pythonhosted.org/packages/fa/ba/1e4f67148223ff219612b6281a6000357abbcc2417964fa5c83f11d68fce/fastar-0.11.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cad7fa41e3e66554387481c1a09365e4638becd322904932674159d5f4046728", size = 760921, upload-time = "2026-04-13T17:07:59.138Z" }, + { url = "https://files.pythonhosted.org/packages/0f/82/09d11fb6d12f17993ffaf32ffd30c3c121a11e2966e84f19fb6f66430118/fastar-0.11.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:cf36652fa71b83761717c9899b98732498f8a2cb6327ff16bbf07f6be85c3437", size = 757012, upload-time = "2026-04-13T17:08:14.186Z" }, + { url = "https://files.pythonhosted.org/packages/52/1f/5aeeacc4cb65615e2c9292cd9c5b0cd6fb6d2e6ee472ca6adc6c1b1b22ef/fastar-0.11.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f68ff8c17833053da4841720e95edde80ce45bb994b6b7d51418dddaac70ee47", size = 924510, upload-time = "2026-04-13T17:08:28.741Z" }, + { url = "https://files.pythonhosted.org/packages/bb/1a/1e5bdabbeaf2e856928956292609f2ff6a650f94480fb8afaca30229e483/fastar-0.11.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4563ed37a12ea1cdc398af8571258d24b988bf342b7b3bf5451bd5891243280c", size = 816602, upload-time = "2026-04-13T17:08:59.461Z" }, + { url = "https://files.pythonhosted.org/packages/87/24/f960147910da3bed41a3adfcb026e17d5f50f4cf467a3324237a7088f61a/fastar-0.11.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cee63c9875cba3b70dc44338c560facc5d6e763047dcc4a30501f9a68cf5f890", size = 819452, upload-time = "2026-04-13T17:09:29.926Z" }, + { url = "https://files.pythonhosted.org/packages/cc/f4/3e77d7901d5707fd7f8a352e153c8ae09ea974e6fabad0b7c4eb9944b8d4/fastar-0.11.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:bd76bfffae6d0a91f4ac4a612f721e7aec108db97dccdd120ae063cd66959f27", size = 885254, upload-time = "2026-04-13T17:08:44.285Z" }, + { url = "https://files.pythonhosted.org/packages/47/01/1585edd5ec47782ae93cd94edf05828e0ab02ef00aec00aea4194a600464/fastar-0.11.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:8f5b707501ec01c1bc0518f741f01d322e50c9adc19a451aa24f67a2316e9397", size = 971496, upload-time = "2026-04-13T17:10:17.024Z" }, + { url = "https://files.pythonhosted.org/packages/f1/e9/6874c9d1236ded565a0bed54b320ac9f165f287b1d89490fb70f9f323c81/fastar-0.11.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:37c0b5a88a657839aad98b0a6c9e4ac4c2c15d6b49c44ee3935c6b08e9d3e479", size = 1034685, upload-time = "2026-04-13T17:10:34.063Z" }, + { url = "https://files.pythonhosted.org/packages/14/d8/4ab20613ce2983427aee958e39be878dba874aa227c530a845e32429c4f6/fastar-0.11.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:6c55f536c62a6efb180c1af0d5182948bff576bbfe6276e8e1359c9c7d2215d8", size = 1072675, upload-time = "2026-04-13T17:10:50.53Z" }, + { url = "https://files.pythonhosted.org/packages/1f/ae/5ac3b7c20ce4b08f011dd2b979f96caabe64f9b10b157f211ea91bdfadca/fastar-0.11.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:3082eeca59e189b9039335862f4c2780c0c8871d656bfdf559db4414a105b251", size = 1029330, upload-time = "2026-04-13T17:11:08.138Z" }, + { url = "https://files.pythonhosted.org/packages/8a/e7/37cd6a1d4e288292170b64e19d79ecce2a7de8bb76790323399a2abc4619/fastar-0.11.0-cp314-cp314-win32.whl", hash = "sha256:b201a0a4e29f9fec2a177e13154b8725ec65ab9f83bd6415483efaa2aa18344b", size = 453940, upload-time = "2026-04-13T17:11:48.713Z" }, + { url = "https://files.pythonhosted.org/packages/ff/1c/795c878b1ee29d79021cf8ed81f18f2b25ccde58453b0d34b9bdc7e025ea/fastar-0.11.0-cp314-cp314-win_amd64.whl", hash = "sha256:868fddb26072a43e870a8819134b9f80ee602931be5a76e6fb873e04da343637", size = 486334, upload-time = "2026-04-13T17:11:34.882Z" }, + { url = "https://files.pythonhosted.org/packages/ff/a4/113f104301df8bddcc0b3775b611a30cb7610baa3add933c7ccac9386467/fastar-0.11.0-cp314-cp314-win_arm64.whl", hash = "sha256:3db39c9cc42abb0c780a26b299f24dfbc8be455985e969e15336d70d7b2f833b", size = 461534, upload-time = "2026-04-13T17:11:24.329Z" }, + { url = "https://files.pythonhosted.org/packages/5a/a6/5c5f2c2c8e0c63e56a5636ebc7721589c889e94c0092cec7eb28ae7207e6/fastar-0.11.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:49c3299dec5e125e7ebaa27545714da9c7391777366015427e0ae62d548b442b", size = 707156, upload-time = "2026-04-13T17:10:02.176Z" }, + { url = "https://files.pythonhosted.org/packages/df/f7/982c01b61f0fc135ad2b16d01e6d0ee53cf8791e68827f5f7c5a65b2e5b1/fastar-0.11.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:3328ed1ed56d31f5198350b17dd60449b8d6b9d47abb4688bab6aef4450a165b", size = 627032, upload-time = "2026-04-13T17:09:46.978Z" }, + { url = "https://files.pythonhosted.org/packages/2b/c3/38f1dac77ae0c71c37b176277c96d830796b8ce2fe69705f917829b53829/fastar-0.11.0-cp314-cp314t-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:bd3eca3bbfec84a614bcb4143b4ad4f784d0895babc26cfc88436af88ca23c7a", size = 864403, upload-time = "2026-04-13T17:09:16.58Z" }, + { url = "https://files.pythonhosted.org/packages/6e/f0/e69c363bdb3e5a5848e937b662b5469581ee6682c51bc1c0556494773929/fastar-0.11.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ff86a967acb0d621dd24063dda090daa67bf4993b9570e97fe156de88a9006ca", size = 759480, upload-time = "2026-04-13T17:08:00.599Z" }, + { url = "https://files.pythonhosted.org/packages/3b/29/4d8737590c2a6357d614d7cc7288e8f68e7e449680b8922997cc4349e65e/fastar-0.11.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:86eaf7c0e985d93a7734168be2fb232b2a8cca53e41431c2782d7c12b12c03b1", size = 756219, upload-time = "2026-04-13T17:08:15.699Z" }, + { url = "https://files.pythonhosted.org/packages/bb/ec/400de7b3b7d48801908f19cf5462177104395799472671b3e8152b2b04ca/fastar-0.11.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:91f07b0b8eb67e2f177733a1f884edad7dfb9f8977ffef15927b20cb9604027d", size = 923669, upload-time = "2026-04-13T17:08:30.574Z" }, + { url = "https://files.pythonhosted.org/packages/5d/01/8926c53da923fed7ab4b96e7fbf7f73b663beb4f02095b654d6fab46f9ad/fastar-0.11.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f85c896885eb4abf1a635d54dea22cac6ae48d04fc2ea26ae652fcf1febe1220", size = 815729, upload-time = "2026-04-13T17:09:01.204Z" }, + { url = "https://files.pythonhosted.org/packages/89/f0/5fef4c7946e352651b504b1a4235dac3505e7cfd24020788ab50552e84bf/fastar-0.11.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:075c07095c8de4b774ba8f28b9c0a02b1a2cd254da50cbe464dd3bb2432e9158", size = 819812, upload-time = "2026-04-13T17:09:31.907Z" }, + { url = "https://files.pythonhosted.org/packages/b3/c8/0ebc3298b4a45e7bddc50b169ae6a6f5b80c939394d4befe6e60de535ee7/fastar-0.11.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:07f028933820c65750baf3383b807ecce1cd9385cf00ce192b79d263ad6b856c", size = 884074, upload-time = "2026-04-13T17:08:45.802Z" }, + { url = "https://files.pythonhosted.org/packages/ae/9f/7baa4cdff8d6fbca41fa5c764b48a941fed8a9ec6c4cc92de65895a28299/fastar-0.11.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:039f875efa0f01fa43c20bf4e2fc7305489c61d0ac76eda991acfba7820a0e63", size = 969450, upload-time = "2026-04-13T17:10:18.667Z" }, + { url = "https://files.pythonhosted.org/packages/d4/dc/1ebbfb58a47056ba866494f19efbcdd2ba2897096b94f36e796594b4d05b/fastar-0.11.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:fff12452a9a5c6814a012445f26365541cc3d99dcca61f09762e6a389f7a32ea", size = 1033775, upload-time = "2026-04-13T17:10:36.165Z" }, + { url = "https://files.pythonhosted.org/packages/c2/5f/ce4e3914066f08c99eb8c32952cc07c1a013e81b1db1b0f598130bf6b974/fastar-0.11.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:2bf733e09f942b6fa876efe30a90508d1f4caef5630c00fb2a84fba355873712", size = 1072158, upload-time = "2026-04-13T17:10:52.497Z" }, + { url = "https://files.pythonhosted.org/packages/03/2a/6bca72992c84151c387cc6558f3867f5ebe5fb3684ee6fa9b76280ba4b8e/fastar-0.11.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:d1531fa848fdd3677d2dce0a4b436ea64d9ae38fb8babe2ddbc180dd153cb7a3", size = 1028577, upload-time = "2026-04-13T17:11:09.934Z" }, + { url = "https://files.pythonhosted.org/packages/83/18/7a7c15657a3da5569b26fc51cde6a80f8d84cb54b3b1aea6d74a103db4ad/fastar-0.11.0-cp314-cp314t-win32.whl", hash = "sha256:5744551bc67c6fc6581cbd0e34a0fd6e2cd0bd30b43e94b1c3119cf35064b162", size = 453601, upload-time = "2026-04-13T17:11:53.726Z" }, + { url = "https://files.pythonhosted.org/packages/6d/d8/331b59a6de279f3ad75c10c02c40a12f21d64a437d9c3d6f1af2dcbd7a76/fastar-0.11.0-cp314-cp314t-win_amd64.whl", hash = "sha256:f4ce44e3b56c47cf38244b98d29f269b259740a580c47a2552efa5b96a5458fb", size = 486436, upload-time = "2026-04-13T17:11:40.089Z" }, + { url = "https://files.pythonhosted.org/packages/6b/fd/5390ec4f49100f3ecb9968a392f9e6d039f1e3fe0ecd28443716ff01e589/fastar-0.11.0-cp314-cp314t-win_arm64.whl", hash = "sha256:76c1359314355eafbc6989f20fb1ad565a3d10200117923b9da765a17e2f6f11", size = 461049, upload-time = "2026-04-13T17:11:25.918Z" }, + { url = "https://files.pythonhosted.org/packages/cc/5c/9bbeffbf1905391446dd98aa520422ce7affde5c9a7c22d757cc5d7c1397/fastar-0.11.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:1266d6a004f427b0d61bd6c7b544d84cc964691b2232c2f4d635a1b75f2f6d5e", size = 711644, upload-time = "2026-04-13T17:10:07.663Z" }, + { url = "https://files.pythonhosted.org/packages/7e/af/ae5cf39d4fb82d0c592705f5ec6db1b065be5265c151b108f86126ee8773/fastar-0.11.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:298a827ec04ade43733f6ca960d0faec38706aa1494175869ea7ea17f5bad5d3", size = 634371, upload-time = "2026-04-13T17:09:52.083Z" }, + { url = "https://files.pythonhosted.org/packages/7e/36/8d4569e26473c72ccb02d1c5df3ed710073f1c06eca09c26d52ea79fd815/fastar-0.11.0-pp311-pypy311_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:8800e2387e463a0e5799416a1cbe72dd0fde7270a20e4bde684145e7878f6516", size = 870850, upload-time = "2026-04-13T17:09:21.439Z" }, + { url = "https://files.pythonhosted.org/packages/bf/46/724dc796e1756d3977970f820d30d59bb8cab8e3671b285f1d82ab513aec/fastar-0.11.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7496def0a2befd82d429cb004ef7ca831585cc887947bd6b9abb68a5ef852b0b", size = 764469, upload-time = "2026-04-13T17:08:05.638Z" }, + { url = "https://files.pythonhosted.org/packages/99/e3/74d6859e632e8fb9339a14f652fb9f800c2bd6aa53071e311c0be3fbab8b/fastar-0.11.0-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:878eaf15463eb572e3538af7ca3a8534e5e279cf8196db902d24e5725c4af86e", size = 761375, upload-time = "2026-04-13T17:08:20.669Z" }, + { url = "https://files.pythonhosted.org/packages/a3/e7/cc70e2be5ef8731a7525552b1c35c1448cf9eae6a62cb3a56f12c1bf27ea/fastar-0.11.0-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0324ed1d1ef0186e1bbd843b17807d6d837d0906899d4c99378b02c5d86bdd9c", size = 928189, upload-time = "2026-04-13T17:08:35.663Z" }, + { url = "https://files.pythonhosted.org/packages/3c/33/c9a969e78dca323547276a6fee5f4f9588f7cd5ab45acec3778c67399589/fastar-0.11.0-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bdf9bd863205590beaf8ef6e66f315310196632180dceaf674985d01a876cac3", size = 820864, upload-time = "2026-04-13T17:09:06.366Z" }, + { url = "https://files.pythonhosted.org/packages/84/bd/6b9434b541fe55c125b5f2e017a565596a2d215aa09207e4555e4585064f/fastar-0.11.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:59af8dbb683b24b90fb5b506de080faeab0a17a908e6c2a5d93a97260ed75d7b", size = 824060, upload-time = "2026-04-13T17:09:37.377Z" }, + { url = "https://files.pythonhosted.org/packages/24/8d/871d5f8cf4c6f13987119fb0a9ae8be131e34f2756c2524e9974adf33824/fastar-0.11.0-pp311-pypy311_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:9f3df73a3c4292cfe15696cdf59cdb6c309ab59d30b34c733be13c6e32d9a264", size = 889217, upload-time = "2026-04-13T17:08:50.884Z" }, + { url = "https://files.pythonhosted.org/packages/d0/26/cca0fd2704f3ed20165e5613ed911549aef3aaf3b0b5b02fee0e8e23e6cc/fastar-0.11.0-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:aa3762cbb16e41a76b61f4a6914937a71aab3a7b6c2d82ca233bc686ebaf756b", size = 975418, upload-time = "2026-04-13T17:10:24.307Z" }, + { url = "https://files.pythonhosted.org/packages/99/94/8bbb0b13f5b6cbe2492f0b7cbba5103e6163976a3331466d010e781fa189/fastar-0.11.0-pp311-pypy311_pp73-musllinux_1_2_armv7l.whl", hash = "sha256:a8c7bc8ac74cb359bb546b199288c83236372d094b402e557c197e85527495cd", size = 1038492, upload-time = "2026-04-13T17:10:41.939Z" }, + { url = "https://files.pythonhosted.org/packages/ed/d3/5b7df222a30eac2822ffd00f82fd4c2ce84fba4b369d1e1a03732fd177fc/fastar-0.11.0-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:587cbd060a2699c5f66281081395bb4657b2b1e0eef5c206b1aabf740019d670", size = 1080210, upload-time = "2026-04-13T17:10:58.462Z" }, + { url = "https://files.pythonhosted.org/packages/ec/6d/56ef943ea524784598c035ccbd42e564e937da0438ae3f55f0e76cb95571/fastar-0.11.0-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:6a1c56957ac82408be37a3f63594bc83e0919e8760492a4475e542f9f1828778", size = 1034886, upload-time = "2026-04-13T17:11:15.617Z" }, +] + [[package]] name = "filelock" version = "3.20.3" @@ -1518,14 +1713,14 @@ wheels = [ [[package]] name = "importlib-metadata" -version = "9.0.0" +version = "8.7.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "zipp" }, + { name = "zipp", marker = "python_full_version < '3.13'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a9/01/15bb152d77b21318514a96f43af312635eb2500c96b55398d020c93d86ea/importlib_metadata-9.0.0.tar.gz", hash = "sha256:a4f57ab599e6a2e3016d7595cfd72eb4661a5106e787a95bcc90c7105b831efc", size = 56405, upload-time = "2026-03-20T06:42:56.999Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f3/49/3b30cad09e7771a4982d9975a8cbf64f00d4a1ececb53297f1d9a7be1b10/importlib_metadata-8.7.1.tar.gz", hash = "sha256:49fef1ae6440c182052f407c8d34a68f72efc36db9ca90dc0113398f2fdde8bb", size = 57107, upload-time = "2025-12-21T10:00:19.278Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/38/3d/2d244233ac4f76e38533cfcb2991c9eb4c7bf688ae0a036d30725b8faafe/importlib_metadata-9.0.0-py3-none-any.whl", hash = "sha256:2d21d1cc5a017bd0559e36150c21c830ab1dc304dedd1b7ea85d20f45ef3edd7", size = 27789, upload-time = "2026-03-20T06:42:55.665Z" }, + { url = "https://files.pythonhosted.org/packages/fa/5e/f8e9a1d23b9c20a551a8a02ea3637b4642e22c2626e3a13a9a29cdea99eb/importlib_metadata-8.7.1-py3-none-any.whl", hash = "sha256:5a1f80bf1daa489495071efbb095d75a634cf28a8bc299581244063b53176151", size = 27865, upload-time = "2025-12-21T10:00:18.329Z" }, ] [[package]] @@ -1583,30 +1778,15 @@ wheels = [ ] [[package]] -name = "jsonschema" -version = "4.26.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "attrs" }, - { name = "jsonschema-specifications" }, - { name = "referencing" }, - { name = "rpds-py" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/b3/fc/e067678238fa451312d4c62bf6e6cf5ec56375422aee02f9cb5f909b3047/jsonschema-4.26.0.tar.gz", hash = "sha256:0c26707e2efad8aa1bfc5b7ce170f3fccc2e4918ff85989ba9ffa9facb2be326", size = 366583, upload-time = "2026-01-07T13:41:07.246Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/69/90/f63fb5873511e014207a475e2bb4e8b2e570d655b00ac19a9a0ca0a385ee/jsonschema-4.26.0-py3-none-any.whl", hash = "sha256:d489f15263b8d200f8387e64b4c3a75f06629559fb73deb8fdfb525f2dab50ce", size = 90630, upload-time = "2026-01-07T13:41:05.306Z" }, -] - -[[package]] -name = "jsonschema-specifications" -version = "2025.9.1" +name = "jinja2" +version = "3.1.6" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "referencing" }, + { name = "markupsafe" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/19/74/a633ee74eb36c44aa6d1095e7cc5569bebf04342ee146178e2d36600708b/jsonschema_specifications-2025.9.1.tar.gz", hash = "sha256:b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d", size = 32855, upload-time = "2025-09-08T01:34:59.186Z" } +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/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl", hash = "sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe", size = 18437, upload-time = "2025-09-08T01:34:57.871Z" }, + { 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]] @@ -1709,98 +1889,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/51/0e/b756c7708143a63fca65a51ca07990fa647db2cc8fcd65177b9e96680255/librt-0.7.7-cp314-cp314t-win_arm64.whl", hash = "sha256:142c2cd91794b79fd0ce113bd658993b7ede0fe93057668c2f98a45ca00b7e91", size = 39724, upload-time = "2026-01-01T23:52:09.745Z" }, ] -[[package]] -name = "lru-dict" -version = "1.4.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/06/0a/dec86efe38b350314c49a8d39ef01ba7cf8bbbef1d177646320eedea7159/lru_dict-1.4.1.tar.gz", hash = "sha256:cc518ff2d38cc7a8ab56f9a6ae557f91e2e1524b57ed8e598e97f45a2bd708fc", size = 13439, upload-time = "2025-11-02T10:02:13.548Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a4/6c/396716746ca46fd2ac52a7a6cbd7b4cf848e5d430f431dacd209290dfa71/lru_dict-1.4.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:3766e397aa6de1ca3442729bc1fa75834ab7b0a6b017e6e197d3a66b61abde59", size = 16757, upload-time = "2025-11-02T10:00:55.767Z" }, - { url = "https://files.pythonhosted.org/packages/2d/93/c163ffb71beb18f18459461658fd16c8b8c86aed858f2dc7c7e636318f61/lru_dict-1.4.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:658e152d3a4ad0e1d75e6f53b1fa353779539920b38be99f4ea33d3bad41efdb", size = 11243, upload-time = "2025-11-02T10:00:56.715Z" }, - { url = "https://files.pythonhosted.org/packages/44/e3/fa96d54032531c67eeacf0ab6f56e10e05f25d382a29f6a381ac8ecf3814/lru_dict-1.4.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:98af7044b5c3d85a649e1afb8891829ff5210caf9143acc741b3e98ab1b66ff6", size = 11726, upload-time = "2025-11-02T10:00:57.377Z" }, - { url = "https://files.pythonhosted.org/packages/7a/23/bae4f32fb014fd2dc5512e9267a3b1ec34c3b55d16a2202a1193d9ae635d/lru_dict-1.4.1-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:906d99705b79a00b5668bdb8782ad823ccc8d26e1fc6b56327ae469a8d12e9b4", size = 29823, upload-time = "2025-11-02T10:00:58.34Z" }, - { url = "https://files.pythonhosted.org/packages/9f/3b/8c3d1e6a188ce65e0161b86dbd18f2290950baf1e9e28e4948fc123d9a67/lru_dict-1.4.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:885643fd968336d8652fddb0778184e2eeff7b7aebced6de268af6d6caef42d5", size = 30812, upload-time = "2025-11-02T10:00:59.358Z" }, - { url = "https://files.pythonhosted.org/packages/ed/11/7f061507eda944150ed959e99a3700ce6358c1241c7f697b2f1ade48646b/lru_dict-1.4.1-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:24c779334bed82f1a7eb2d1ebcba2b7aa9a1555d40a3b53e05eb6b9dfcb0609c", size = 32480, upload-time = "2025-11-02T10:01:00.141Z" }, - { url = "https://files.pythonhosted.org/packages/75/e7/94ac30d33c6f8a8eca5d7e81c0ce26fb7b79b18ea65accdcb2a652b19abc/lru_dict-1.4.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:c6099e2ecb118dfeae4a197bfcc702ea5841bfd86f19d1b340e932d0f5c47c10", size = 30199, upload-time = "2025-11-02T10:01:01.31Z" }, - { url = "https://files.pythonhosted.org/packages/4a/81/c93ee7365db67dfb497e6218aa0395b9ec878c07c732d348bfbd651bcc95/lru_dict-1.4.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:4e0db4f3105108598749550e639b283b07df0bb91cac3b47e86ffebcab721cc7", size = 31489, upload-time = "2025-11-02T10:01:02.363Z" }, - { url = "https://files.pythonhosted.org/packages/9f/0b/634e8b4eca2497647f802bbe1ae3f0e1e9a0de1d555cf77c022527b2682f/lru_dict-1.4.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e21f67ba374d1945051b547e719d44a8c7880718f67a15a03e7a12e1d12ea96b", size = 29522, upload-time = "2025-11-02T10:01:03.399Z" }, - { url = "https://files.pythonhosted.org/packages/de/cc/591b959d77cc0e0ac016f11baf26d03d566bb88a53fa9b41e157bc68bc4b/lru_dict-1.4.1-cp310-cp310-win32.whl", hash = "sha256:f309b4018dd41f33bf3bd4cc0f62421da8bcca513ea044dbb22f3cd029935012", size = 13066, upload-time = "2025-11-02T10:01:04.457Z" }, - { url = "https://files.pythonhosted.org/packages/d9/bc/c14b67fdbdb5a2a81cfb907ea8a8b0c9da5aed899f34921ebf097e22a966/lru_dict-1.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:e84cd1065955897de01f1fb4cbd6f87cab7706e920283bb98c27341d76dd9a8d", size = 14008, upload-time = "2025-11-02T10:01:05.421Z" }, - { url = "https://files.pythonhosted.org/packages/4c/ff/1d02bc444174f07d3ce747568989969c97dc77d0513f4c3b8b6224cb976f/lru_dict-1.4.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:cc74c49cf1c26d6c28d8f6988cf0354696ca38a4f6012fa63055d2800791784b", size = 16760, upload-time = "2025-11-02T10:01:06.492Z" }, - { url = "https://files.pythonhosted.org/packages/0b/d8/e2e970272ea5fe7ba6349a5e7d0bb0fd814f5d1b88a53bc72b8c2a5e034f/lru_dict-1.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0158db85dfb2cd2fd2ddaa47709bdb073f814e0a8a149051b70b07e59ac83231", size = 11249, upload-time = "2025-11-02T10:01:07.261Z" }, - { url = "https://files.pythonhosted.org/packages/a5/26/860b5e60f339f8038118028388926224c8b70779e8243d68772e0e0d0ab3/lru_dict-1.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c8ac5cfd56e036bd8d7199626147044485fa64a163a5bde96bfa5a1c7fea2273", size = 11728, upload-time = "2025-11-02T10:01:08.185Z" }, - { url = "https://files.pythonhosted.org/packages/61/55/fc8f71953fd343ede33810b0a000b4130e03635ae09b28569e45735ded2f/lru_dict-1.4.1-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2eb2058cb7b329b4b72baee4cd1bb322af1feec73de79e68edb35d333c90b698", size = 30795, upload-time = "2025-11-02T10:01:08.862Z" }, - { url = "https://files.pythonhosted.org/packages/4c/26/ad549550e6a236818a91434570d38d7a93824b0410d3db1c845a53238e1f/lru_dict-1.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6ffbb6f3c1e906e92d9129c14a88d81358be1e0b60195c1729b215a52e9670de", size = 31807, upload-time = "2025-11-02T10:01:09.581Z" }, - { url = "https://files.pythonhosted.org/packages/7c/39/72dae9ac0e95a8576a45e3bd62a6fc3e7dbb116794efa1337c7b450d4836/lru_dict-1.4.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:11b289d78a48a086846e46d2275707d33523f5d543475336c29c56fd5d0e65dc", size = 33437, upload-time = "2025-11-02T10:01:10.676Z" }, - { url = "https://files.pythonhosted.org/packages/a8/46/221479834703a5397fa32f07212ace38f104a31ad1af8a921cf25e053677/lru_dict-1.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:3fe10c1f45712e191eecb2a69604d566c64ddfe01136fd467c890ed558c3ad40", size = 31168, upload-time = "2025-11-02T10:01:11.47Z" }, - { url = "https://files.pythonhosted.org/packages/6e/13/98d36e2522fda7f6625c15332562f81f1465161a5ae021d9b3b408f8c427/lru_dict-1.4.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e04820e3473bd7f55440f24c946ca4335e392d5e3e0e1e948020e94cd1954372", size = 32454, upload-time = "2025-11-02T10:01:12.522Z" }, - { url = "https://files.pythonhosted.org/packages/49/18/345ff2a98d27cddae40c84cf0466fcc329f3965cd21322bb561a94e4d332/lru_dict-1.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:edc004c88911a8f9715e716116d2520c13db89afd6c37cc0f28042ba10635163", size = 30574, upload-time = "2025-11-02T10:01:13.293Z" }, - { url = "https://files.pythonhosted.org/packages/d7/92/dfea71402a7ca46332bcb854827ee68bbc9be205e2558c3a40293eca9782/lru_dict-1.4.1-cp311-cp311-win32.whl", hash = "sha256:b0b5360264b37676c405ea0a560744d7dcb2d47adff1e7837113c15fabcc7a71", size = 13031, upload-time = "2025-11-02T10:01:13.96Z" }, - { url = "https://files.pythonhosted.org/packages/3a/7b/4c7d566d77ec3ad9128f07407494c2aec57909f8dd59f0c9910bd4c05840/lru_dict-1.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:bb4b37daad9fe4e796c462f4876cf34e52564630902bdf59a271bc482b48a361", size = 14007, upload-time = "2025-11-02T10:01:14.857Z" }, - { url = "https://files.pythonhosted.org/packages/4f/a8/89e4c26e0e751321b41b0a3007384f97d9eae7a863c49af1c68c43005ca3/lru_dict-1.4.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:7fa342c6e6bc811ee6a17eb569d37b149340d5aa5a637a53438e316a95783838", size = 16683, upload-time = "2025-11-02T10:01:15.891Z" }, - { url = "https://files.pythonhosted.org/packages/f1/34/b3c6fdd120af68b6eeb524d0de3293ff27918ec57f45eed6bef1789fd085/lru_dict-1.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:bd86bd202a7c1585d9dc7e5b0c3d52cf76dc56b261b4bbecfeefbbae31a5c97d", size = 11216, upload-time = "2025-11-02T10:01:16.867Z" }, - { url = "https://files.pythonhosted.org/packages/e9/7e/280267ae23f1ec1074ddaab787c5e041e090220e8e37828d51ff4e681dfd/lru_dict-1.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4617554f3e42a8f520c8494842c23b98f5b7f4d5e0410e91a4c3ad0ea5f7e094", size = 11687, upload-time = "2025-11-02T10:01:17.485Z" }, - { url = "https://files.pythonhosted.org/packages/ca/18/fec42416ceff98ae2760067ec72b0b9fc02840e729bbc18059c6a02cb01f/lru_dict-1.4.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:40927a6a4284d437047f547e652b15f6f0f40210deb6b9e5b77e556ff0faea0f", size = 31960, upload-time = "2025-11-02T10:01:18.158Z" }, - { url = "https://files.pythonhosted.org/packages/c2/ef/38e7ee1a5d32b9b1629d045fa5a495375383aacfb2945f4d9535b9af9630/lru_dict-1.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e2c07ecb6d42494e45d00c2541e6b0ae7659fc3cf89681521ba94b15c682d4fe", size = 32882, upload-time = "2025-11-02T10:01:18.841Z" }, - { url = "https://files.pythonhosted.org/packages/72/82/d56653ca144c291ab37bea5f23c5078ffbe64f7f5b466f91d400590b9106/lru_dict-1.4.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:85b28aa2de7c5f1f6c68221857accd084438df98edbd4f57595795734225770c", size = 34268, upload-time = "2025-11-02T10:01:19.564Z" }, - { url = "https://files.pythonhosted.org/packages/94/ae/382651533d60f0b598757efda56dc87cad5ac311fba8e61f86fb916bf236/lru_dict-1.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:cbbbb4b51e2529ccf7ee8a3c3b834052dbd54871a216cfd229dd2b1194ff293a", size = 32156, upload-time = "2025-11-02T10:01:20.22Z" }, - { url = "https://files.pythonhosted.org/packages/aa/d1/d9df7e9272ccbc96f04c477dfb9abb91fa8fabde86b7fa190cb7b3c7a024/lru_dict-1.4.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:e47040421a13de8bc6404557b3700c33f1f2683cbcce22fe5cacec4c938ce54b", size = 33395, upload-time = "2025-11-02T10:01:20.901Z" }, - { url = "https://files.pythonhosted.org/packages/e9/6e/dafe0f5943a7b3ab24d3429032ff85873acd626087934b8161b55340c13a/lru_dict-1.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:451f7249866cb9564bb40d73bec7ac865574dafd0a4cc91627bbf35be7e99291", size = 31591, upload-time = "2025-11-02T10:01:21.606Z" }, - { url = "https://files.pythonhosted.org/packages/a6/4d/9dd35444592bfb6805548e15971cfce821400966a51130b78dc021ee8f03/lru_dict-1.4.1-cp312-cp312-win32.whl", hash = "sha256:e8996f3f94870ecb236c55d280839390edae7f201858fee770267eac27b8b47d", size = 13119, upload-time = "2025-11-02T10:01:22.61Z" }, - { url = "https://files.pythonhosted.org/packages/8d/82/7e72e30d6c15d65466b3baca87cce15e20848ba6a488868aa54e901141a6/lru_dict-1.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:d90774db1b60c0d5c829cfa5d7fda6db96ed1519296f626575598f9f170cca37", size = 14109, upload-time = "2025-11-02T10:01:23.322Z" }, - { url = "https://files.pythonhosted.org/packages/85/95/ee171a68ae381ab988c50e3b7b136b1c598f5f683ba4a1e10c51e2480408/lru_dict-1.4.1-cp313-cp313-android_21_arm64_v8a.whl", hash = "sha256:2a5644bb1db0514abdad5e2f3d8f1beb6f7560c8cceb62079c40a4269de34b3c", size = 12248, upload-time = "2025-11-02T10:01:24.291Z" }, - { url = "https://files.pythonhosted.org/packages/a1/82/8de8e8fd96c44d46891415834ceb9f51c552840bda2d118394aca5e3153a/lru_dict-1.4.1-cp313-cp313-android_21_x86_64.whl", hash = "sha256:4209864be09ec20f6059fef8544697eb3d3729d63a983bf66457054bf3e40601", size = 12243, upload-time = "2025-11-02T10:01:25.254Z" }, - { url = "https://files.pythonhosted.org/packages/53/97/251cfb357c547a8fd06c2bc40db8a7f7eed7dbacef30d8d7e543522360e1/lru_dict-1.4.1-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:8fef8dd72484b4280799c502c116acfdfcf0dedf3508bc9d0d19e684a6a23267", size = 10938, upload-time = "2025-11-02T10:01:25.89Z" }, - { url = "https://files.pythonhosted.org/packages/58/14/602791d219bc87197ae80f5fa0f77ca0af8e83e9a06c7cdb89db5575839e/lru_dict-1.4.1-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:d64ddbe4c426fdc4cfc1abaea71d587d439397386a7b35d588f4fd64b695a83d", size = 11261, upload-time = "2025-11-02T10:01:26.879Z" }, - { url = "https://files.pythonhosted.org/packages/10/5d/a30a6fad150f20f084de8e243882a0488ad4929db41a2c8ce9be6cf56563/lru_dict-1.4.1-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:000ba9a2ab4dd1ad2d91764a6d5cce75a59de51534cdda478d1ddaa3cd8d5c48", size = 10801, upload-time = "2025-11-02T10:01:27.553Z" }, - { url = "https://files.pythonhosted.org/packages/64/4d/cee327e024d42972c598b7e0cd5063a1b1d7451efba31f7de7b6ca91e7d0/lru_dict-1.4.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ffad2758ce21d8fd6f0ae2628b31330732db8429a4b5994d2e107bed0ee11e68", size = 16688, upload-time = "2025-11-02T10:01:28.17Z" }, - { url = "https://files.pythonhosted.org/packages/58/c8/2f86a1e448c5257b31424b96bf1385e7f96ec7841c2376db02811bbd395f/lru_dict-1.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1671e8d92fe35dfb38d3505a56338792d3e225032f8e94888b6e95b323120380", size = 11214, upload-time = "2025-11-02T10:01:28.888Z" }, - { url = "https://files.pythonhosted.org/packages/06/41/507c615cffaba67c35affd77dec25d3183bb87f404b41c8bb2b3053481ac/lru_dict-1.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d5f01ada0cf0c1aa2bdc684e5ac0f6548be7eccc3ce8b4c0361db8445f867f04", size = 11689, upload-time = "2025-11-02T10:01:29.508Z" }, - { url = "https://files.pythonhosted.org/packages/4e/c1/35aa1359f80174016b389f8be5fd48c4a5af0a04a73afb4906e5d4279f4a/lru_dict-1.4.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:74204239e30b8ec7976257c5b64565d7e3e8aea0cad0dd50a9b99e171aaf3898", size = 32034, upload-time = "2025-11-02T10:01:30.164Z" }, - { url = "https://files.pythonhosted.org/packages/c2/93/46301015bddd4552a1b76982ef788a7fb2a886efff83ad2c178cc7e68349/lru_dict-1.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a7da0e451faa4d6dcae21c0f2527c540000b2f23ed8326a0bc1d870130fd12b1", size = 32919, upload-time = "2025-11-02T10:01:30.971Z" }, - { url = "https://files.pythonhosted.org/packages/e6/cb/6d67145619d8ec3bba15fe145ff702ecf44991e33345d38c763501c1608a/lru_dict-1.4.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:071468a716768a9afca64659c390c1abb6d937b1897e07a0b70383f75637fce0", size = 34334, upload-time = "2025-11-02T10:01:31.663Z" }, - { url = "https://files.pythonhosted.org/packages/a5/44/50daaec6793ec2042079ed6a8b6a687b4be51b270b1d8ec5efd280116493/lru_dict-1.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e77d209bcd396eb236c197bf4c95fab6848c61e0c1a5031cdde7f5c787e209f4", size = 32211, upload-time = "2025-11-02T10:01:32.468Z" }, - { url = "https://files.pythonhosted.org/packages/bd/53/355397949215e6b77b6771b973ee1dbc21fdd9f955925e47dce50d9d4727/lru_dict-1.4.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:b21688fd7ece56d04c0c13b42fd9f904d46fc9ff21e3de87d98f3f5a14c67f74", size = 33461, upload-time = "2025-11-02T10:01:33.2Z" }, - { url = "https://files.pythonhosted.org/packages/ff/fa/d660fa63144f38a0fd5b437a140517e3cff482d955ef6b9b4cf7651b9d85/lru_dict-1.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:989ef7352b347c82e5d5047f3b7ddf34b5a938e3f7b08775cacc9f28e97dd2a8", size = 31651, upload-time = "2025-11-02T10:01:33.874Z" }, - { url = "https://files.pythonhosted.org/packages/2e/77/0fae8d0702f7546f436efe06a684b301aad5c8a167bb2df6e42b0f821de5/lru_dict-1.4.1-cp313-cp313-win32.whl", hash = "sha256:a36e6e95b5d474ef90d04a5e3ad81ca362b473ec9534ed964222f3c0444138b8", size = 13120, upload-time = "2025-11-02T10:01:34.595Z" }, - { url = "https://files.pythonhosted.org/packages/4a/20/56a3f0d74c8fe32c01d3978387f66c9fb180c7f15bfd9fcecaa01b4e7736/lru_dict-1.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:8e73a1ec2d0f476d666ce7c91464b22854086951b319544d1850c508f5ce381f", size = 14112, upload-time = "2025-11-02T10:01:35.268Z" }, - { url = "https://files.pythonhosted.org/packages/98/02/8e04a8d744b466d4153502e2d92b453c2e5a549d49bf7fabfdca1621828a/lru_dict-1.4.1-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:7b770c7db258625e57b6ea8e2e0503ba0fbbdcde374baacf9adb256eb9c5adfa", size = 11119, upload-time = "2025-11-02T10:01:36.239Z" }, - { url = "https://files.pythonhosted.org/packages/50/f8/ee96f30127ff47c29966603f040e0485700fe0ca0e7d7b1ecbc9bf999eea/lru_dict-1.4.1-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:45d4dc338237cedcbacedab1afd9707b8f9867d8b601ec04e0395ec73f57405c", size = 11435, upload-time = "2025-11-02T10:01:36.857Z" }, - { url = "https://files.pythonhosted.org/packages/a0/5a/897b33ba1974b6487848cafa5de7e93a7c4f5d9d3f43319ee010f6882830/lru_dict-1.4.1-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:5b31e9b6636f8945ad69c630c1891d810d62a91d99e792ef0b9ca865b6c26745", size = 10988, upload-time = "2025-11-02T10:01:37.531Z" }, - { url = "https://files.pythonhosted.org/packages/19/8e/b87d0f2bfcad0169afc00e23e014bad9af252206ec2cbc6079f12bece58e/lru_dict-1.4.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:f9335d46c83882a1b5deffed8098a2dd9ad66d2bd6263f416fc4c73f63e26904", size = 16733, upload-time = "2025-11-02T10:01:38.194Z" }, - { url = "https://files.pythonhosted.org/packages/ab/19/d2384266864b1e5b1cc20527ae468550d3b23a71636371b40e4663276294/lru_dict-1.4.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:17844b4f8dd996144d53380395d73832e2508159ad49ed4fbcb62f1787a5feaf", size = 11222, upload-time = "2025-11-02T10:01:39.093Z" }, - { url = "https://files.pythonhosted.org/packages/66/8a/94dec42ae6b5c8bdc53a86867924fa22634516434f129dca187ccc0853b8/lru_dict-1.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:2b569c7813adb753b7b631097c34e6dbc194cb1814f22299c2d2a94894779877", size = 11733, upload-time = "2025-11-02T10:01:39.739Z" }, - { url = "https://files.pythonhosted.org/packages/3f/19/0b6de1db804cf094e201c5541d58e6a96359eb5beed048fa64d0589b6520/lru_dict-1.4.1-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:33cf1eb368d3989b8f00945937cfbfc2095d8ad2b1d2274ce1bde0af6f6d1e66", size = 32251, upload-time = "2025-11-02T10:01:40.421Z" }, - { url = "https://files.pythonhosted.org/packages/97/38/89d9425dde436b9bd894234171988289b259aeeab5965bd2c21d5104cb41/lru_dict-1.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:22d5879ec5d5955f9dde105997bdf7ec9e0522bf99612a80b55b09f356a08368", size = 33405, upload-time = "2025-11-02T10:01:41.917Z" }, - { url = "https://files.pythonhosted.org/packages/0e/24/f1a189399ee107a64f955c9d6c84d3b0aee9b64b31fc5684b1eaeb3a6fc0/lru_dict-1.4.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2084363e4488aa5b4f8b26bd3cc148d70a15be92e3d347621a5b830b2b1e0a82", size = 35135, upload-time = "2025-11-02T10:01:42.935Z" }, - { url = "https://files.pythonhosted.org/packages/f8/57/58e9dcf0853d639e2995e5d9f84649ff8d6792a04a418628672a130137f4/lru_dict-1.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:8198ab8ad7cc81b86340243ddd5cca882ead87daed0c9fa6cce377a10a7f2e47", size = 32620, upload-time = "2025-11-02T10:01:43.627Z" }, - { url = "https://files.pythonhosted.org/packages/a9/bb/664922f0cf076b1e3c2e43e8258582d507b07c19bd441a72dd5547a483e9/lru_dict-1.4.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:f1f4ae6967d5873e684ce8b986e2e43985d0a1be735b09584737ad5634ff48f3", size = 34077, upload-time = "2025-11-02T10:01:44.694Z" }, - { url = "https://files.pythonhosted.org/packages/58/38/b7a6fa85b150232cada26a50c89dc4bcf9acd6ada00e987b074c3b4e57f2/lru_dict-1.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:a9bb130b5eaddd6453ca3dc38ce4a75f743512ad135b6f3994999dde0680bd79", size = 31905, upload-time = "2025-11-02T10:01:45.668Z" }, - { url = "https://files.pythonhosted.org/packages/86/7d/9c86393946d621f4aec852d543df4023241d85106e9e1e2a0e4057861f71/lru_dict-1.4.1-cp314-cp314-win32.whl", hash = "sha256:5534c69a52add5757714456d08ce3831d36b86c98972394ba900493bb0bd97f8", size = 13435, upload-time = "2025-11-02T10:01:46.397Z" }, - { url = "https://files.pythonhosted.org/packages/20/3f/b017cbeea55a8a1d18037840a8a9c9cdae29554e9985b55d4e8694305035/lru_dict-1.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:96fd677b6d912229f2d02ba61a5a1210176963c4770c1bb765b8da937cec3834", size = 14423, upload-time = "2025-11-02T10:01:47.473Z" }, - { url = "https://files.pythonhosted.org/packages/aa/73/13132af7a5155edde66979b53eb509465304e6e5a2b00769246448479c73/lru_dict-1.4.1-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:6699bfebbf11dd9ff1387be7996fac6d1009fe6a6f48091ef6e069e6f19c7bce", size = 17184, upload-time = "2025-11-02T10:01:48.161Z" }, - { url = "https://files.pythonhosted.org/packages/ef/82/094985beb3e49461bf65a3c40df8de2018b8484e4ef129295090460ca5d9/lru_dict-1.4.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:a276f8f6f43861c3f05986824741d00e3133a973c3396598375310129535382d", size = 11459, upload-time = "2025-11-02T10:01:48.823Z" }, - { url = "https://files.pythonhosted.org/packages/14/29/836abc49f8c2b6c2efccd2ac2b2c0ad3e55b7d75a05a20cc061f17871e39/lru_dict-1.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:090c7b6a3d54fa7f3d69ba4802abe2f33c9583b16b33f52bcb521c701f7ea46c", size = 11938, upload-time = "2025-11-02T10:01:49.448Z" }, - { url = "https://files.pythonhosted.org/packages/c2/83/1bb4e8fbc0b753fea825564d9d96180813a71715d46a9b6bb30a6dea4ce0/lru_dict-1.4.1-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b21d06dec64fb1952385262d9fcefaec147921dc0b55210007091a79da440d93", size = 36389, upload-time = "2025-11-02T10:01:50.49Z" }, - { url = "https://files.pythonhosted.org/packages/54/f2/7df3b6d0dbc66f3be9aa6261750967cdc5619c89a563420c52200d2dd547/lru_dict-1.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b9613908a38cf8aa47f6c138ba031a8ac4ed38460299e84a2b07dba7b3b45aae", size = 38706, upload-time = "2025-11-02T10:01:51.197Z" }, - { url = "https://files.pythonhosted.org/packages/97/5f/e3ba3eeb9b864a09b92e24fbf179aef4ef48588e763a9d8d2bc10bd2c6f8/lru_dict-1.4.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7558302ce8bbfcd29f08e695e07bf7a0d799c2979636d6a6a0b4e207f840969f", size = 38892, upload-time = "2025-11-02T10:01:51.899Z" }, - { url = "https://files.pythonhosted.org/packages/e8/e3/12e0888aab0bf3ab9ce35e9849f239994a0feff6fe49380859bf57124a17/lru_dict-1.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:3910396142322fb2718546115bb2a56f50ebc9144b5140327053cca084e0d375", size = 37254, upload-time = "2025-11-02T10:01:52.587Z" }, - { url = "https://files.pythonhosted.org/packages/cc/dc/06cd981718d039eb07a9c03263094aca6269721c470f765a292b24381a20/lru_dict-1.4.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:f3f4fad5c4a9458954b275de6a6e31c67a26fbef7037c6a7354e22523a77db26", size = 37422, upload-time = "2025-11-02T10:01:53.271Z" }, - { url = "https://files.pythonhosted.org/packages/e8/82/ea88e618f39d78ff3c15b71f01a0b1a6c6ac2034ce5c6428ae41b1c30ea5/lru_dict-1.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:85fc29363e2d3ba0a5f87b5e17f54b1078aea6d24c6dfc792725854b9d0f8d17", size = 35909, upload-time = "2025-11-02T10:01:54.356Z" }, - { url = "https://files.pythonhosted.org/packages/89/36/1dd91c602f623839cec24d6c77fa3fd1a8878bf2d716871197cd3bf084dc/lru_dict-1.4.1-cp314-cp314t-win32.whl", hash = "sha256:b3853518dfa50f28af0d6e2dcf8bb8b0a1687c5f4eb913c0b35b0da5c6d276ce", size = 13816, upload-time = "2025-11-02T10:01:55.417Z" }, - { url = "https://files.pythonhosted.org/packages/ce/a3/113410f7b2e61e9d6f13f1f17c584dbd08b5796e65d772ecd5b063fab3af/lru_dict-1.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:ff3af42922205620fdc920dcdf580c4c16b32c84a537a03b04b523e5c641a8a9", size = 15204, upload-time = "2025-11-02T10:01:56.06Z" }, - { url = "https://files.pythonhosted.org/packages/ec/de/18ac3957e1aa6674a0a828748c819265f79b524ff30cbb0ac7f08ab786c8/lru_dict-1.4.1-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:cc9dd191870555624bbf3903c8afa3f01815ca3256ed8b35cb323f0db3ce4f98", size = 10467, upload-time = "2025-11-02T10:02:05.717Z" }, - { url = "https://files.pythonhosted.org/packages/0c/53/2a0bedaa64950cc56ade72e2f5a292318473585d9a3adc797d13b38082e7/lru_dict-1.4.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:afdf92b332632aa6e4b8646e93723f50f41fece2a80a54d2b44e8ac67f913ceb", size = 10871, upload-time = "2025-11-02T10:02:06.353Z" }, - { url = "https://files.pythonhosted.org/packages/4e/e2/d5ea49d62ea142559fd9cafd8505d4a4f87a1d81953a9c99fa61e7ccbd6b/lru_dict-1.4.1-pp310-pypy310_pp73-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:3d6770adafae25663b682420891a10a5894595f02b1e4d87766f7adc8e56e72a", size = 12969, upload-time = "2025-11-02T10:02:07.196Z" }, - { url = "https://files.pythonhosted.org/packages/a2/67/0672caac9a04dc9011f7a27fc2ec2003f0bfa008070b29940d05b4dae56a/lru_dict-1.4.1-pp310-pypy310_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:018cd3b41224ca81eb83cdf6db024409a920e5c1d3ce4e8b323cb66e24a73132", size = 13959, upload-time = "2025-11-02T10:02:08.267Z" }, - { url = "https://files.pythonhosted.org/packages/e3/7e/313385214a5011cf9fe8376928f66f70bfedc48d8f7ab424292224ed4907/lru_dict-1.4.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:781dbcf0c83160e525482a4ebcd7c5065851a6c7295f1cda78248a2029f23f39", size = 14084, upload-time = "2025-11-02T10:02:08.993Z" }, - { url = "https://files.pythonhosted.org/packages/8e/47/08c61cad038706b3a89b8c7587ec74ed9731c1e536329745cccb6c840916/lru_dict-1.4.1-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:9219f13e4101c064f70e1815d7c51f9be9e053983e74dfb7bcfdf92f5fcbb0e0", size = 10384, upload-time = "2025-11-02T10:02:09.656Z" }, - { url = "https://files.pythonhosted.org/packages/6b/a1/022c4d7c68c076370231488c97cf7451131fb9ca0d60d1b2785e7baa1f5b/lru_dict-1.4.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:b7e1ac7fb6e91e4d3212e153f9e2d98d163a4439b9bf9df247c22519262c26fe", size = 10822, upload-time = "2025-11-02T10:02:10.609Z" }, - { url = "https://files.pythonhosted.org/packages/65/b4/4c0a0877a77fececa9f58d804569e2aac1bfbe588e3a70e79647b5d8f7d4/lru_dict-1.4.1-pp311-pypy311_pp73-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:23424321b761c43f3021a596565f8205ecec0e175822e7a5d9b2a175578aa7de", size = 12968, upload-time = "2025-11-02T10:02:11.405Z" }, - { url = "https://files.pythonhosted.org/packages/22/06/d7e393d07dc31e656330d5a058f34e972bf590e7dc882922b426f3aec4a0/lru_dict-1.4.1-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:804ee76f98afc3d50e9a2e9c835a6820877aa6391f2add520a57f86b3f55ec3a", size = 13904, upload-time = "2025-11-02T10:02:12.144Z" }, - { url = "https://files.pythonhosted.org/packages/e8/1e/0eee8bcc16bf01b265ac83e4b870596e2f3bcc40d88aa7ec25407180fe44/lru_dict-1.4.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:3be24e24c8998302ea1c28f997505fa6843f507aad3c7d5c3a82cc01c5c11be4", size = 14062, upload-time = "2025-11-02T10:02:12.878Z" }, -] - [[package]] name = "macholib" version = "1.16.4" @@ -1825,6 +1913,91 @@ 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" @@ -2104,7 +2277,7 @@ dependencies = [ { name = "typer" }, { name = "uvicorn", extra = ["standard"] }, { name = "web3" }, - { name = "x402", extra = ["httpx"] }, + { name = "x402", extra = ["evm", "fastapi", "httpx"] }, ] [package.optional-dependencies] @@ -2133,7 +2306,7 @@ requires-dist = [ { name = "build", marker = "extra == 'dev'", specifier = ">=1.0.0" }, { name = "circle-developer-controlled-wallets", specifier = "==9.1.0" }, { name = "cryptography", specifier = "==44.0.0" }, - { name = "eth-account", specifier = "==0.12.0" }, + { name = "eth-account", specifier = ">=0.13.6" }, { name = "fastapi", specifier = ">=0.100.0" }, { name = "hatch", marker = "extra == 'dev'", specifier = ">=1.14.1" }, { name = "httpx", specifier = ">=0.28.1" }, @@ -2152,8 +2325,8 @@ requires-dist = [ { name = "twine", marker = "extra == 'dev'", specifier = ">=6.1.0" }, { name = "typer", specifier = ">=0.15.0" }, { name = "uvicorn", extras = ["standard"], specifier = ">=0.20.0" }, - { name = "web3", specifier = ">=6.0.0" }, - { name = "x402", extras = ["httpx"], specifier = ">=2.0.0" }, + { name = "web3", specifier = ">=7.0.0" }, + { name = "x402", extras = ["evm", "fastapi", "httpx"], specifier = ">=2.5.0" }, ] provides-extras = ["dev"] @@ -2363,21 +2536,6 @@ wheels = [ { 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 = "protobuf" -version = "7.34.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/6b/6b/a0e95cad1ad7cc3f2c6821fcab91671bd5b78bd42afb357bb4765f29bc41/protobuf-7.34.1.tar.gz", hash = "sha256:9ce42245e704cc5027be797c1db1eb93184d44d1cdd71811fb2d9b25ad541280", size = 454708, upload-time = "2026-03-20T17:34:47.036Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ec/11/3325d41e6ee15bf1125654301211247b042563bcc898784351252549a8ad/protobuf-7.34.1-cp310-abi3-macosx_10_9_universal2.whl", hash = "sha256:d8b2cc79c4d8f62b293ad9b11ec3aebce9af481fa73e64556969f7345ebf9fc7", size = 429247, upload-time = "2026-03-20T17:34:37.024Z" }, - { url = "https://files.pythonhosted.org/packages/eb/9d/aa69df2724ff63efa6f72307b483ce0827f4347cc6d6df24b59e26659fef/protobuf-7.34.1-cp310-abi3-manylinux2014_aarch64.whl", hash = "sha256:5185e0e948d07abe94bb76ec9b8416b604cfe5da6f871d67aad30cbf24c3110b", size = 325753, upload-time = "2026-03-20T17:34:38.751Z" }, - { url = "https://files.pythonhosted.org/packages/92/e8/d174c91fd48e50101943f042b09af9029064810b734e4160bbe282fa1caa/protobuf-7.34.1-cp310-abi3-manylinux2014_s390x.whl", hash = "sha256:403b093a6e28a960372b44e5eb081775c9b056e816a8029c61231743d63f881a", size = 340198, upload-time = "2026-03-20T17:34:39.871Z" }, - { url = "https://files.pythonhosted.org/packages/53/1b/3b431694a4dc6d37b9f653f0c64b0a0d9ec074ee810710c0c3da21d67ba7/protobuf-7.34.1-cp310-abi3-manylinux2014_x86_64.whl", hash = "sha256:8ff40ce8cd688f7265326b38d5a1bed9bfdf5e6723d49961432f83e21d5713e4", size = 324267, upload-time = "2026-03-20T17:34:41.1Z" }, - { url = "https://files.pythonhosted.org/packages/85/29/64de04a0ac142fb685fd09999bc3d337943fb386f3a0ec57f92fd8203f97/protobuf-7.34.1-cp310-abi3-win32.whl", hash = "sha256:34b84ce27680df7cca9f231043ada0daa55d0c44a2ddfaa58ec1d0d89d8bf60a", size = 426628, upload-time = "2026-03-20T17:34:42.536Z" }, - { url = "https://files.pythonhosted.org/packages/4d/87/cb5e585192a22b8bd457df5a2c16a75ea0db9674c3a0a39fc9347d84e075/protobuf-7.34.1-cp310-abi3-win_amd64.whl", hash = "sha256:e97b55646e6ce5cbb0954a8c28cd39a5869b59090dfaa7df4598a7fba869468c", size = 437901, upload-time = "2026-03-20T17:34:44.112Z" }, - { url = "https://files.pythonhosted.org/packages/88/95/608f665226bca68b736b79e457fded9a2a38c4f4379a4a7614303d9db3bc/protobuf-7.34.1-py3-none-any.whl", hash = "sha256:bb3812cd53aefea2b028ef42bd780f5b96407247f20c6ef7c679807e9d188f11", size = 170715, upload-time = "2026-03-20T17:34:45.384Z" }, -] - [[package]] name = "ptyprocess" version = "0.7.0" @@ -2446,6 +2604,11 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f4/9d/d5c855424e2e5b6b626fbc6ec514d8e655a600377ce283008b115abb7445/pydantic-2.12.0-py3-none-any.whl", hash = "sha256:f6a1da352d42790537e95e83a8bdfb91c7efbae63ffd0b86fa823899e807116f", size = 459730, upload-time = "2025-10-07T15:58:01.576Z" }, ] +[package.optional-dependencies] +email = [ + { name = "email-validator" }, +] + [[package]] name = "pydantic-core" version = "2.41.1" @@ -2556,6 +2719,33 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/1d/62/755d2bd2593f701c5839fc084e9c2c5e2418f460383ad04e3b5d0befc3ca/pydantic_core-2.41.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:f1fc716c0eb1663c59699b024428ad5ec2bcc6b928527b8fe28de6cb89f47efb", size = 2144046, upload-time = "2025-10-07T10:50:40.686Z" }, ] +[[package]] +name = "pydantic-extra-types" +version = "2.11.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/71/dba38ee2651f84f7842206adbd2233d8bbdb59fb85e9fa14232486a8c471/pydantic_extra_types-2.11.1.tar.gz", hash = "sha256:46792d2307383859e923d8fcefa82108b1a141f8a9c0198982b3832ab5ef1049", size = 172002, upload-time = "2026-03-16T08:08:03.92Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/17/c1/3226e6d7f5a4f736f38ac11a6fbb262d701889802595cdb0f53a885ac2e0/pydantic_extra_types-2.11.1-py3-none-any.whl", hash = "sha256:1722ea2bddae5628ace25f2aa685b69978ef533123e5638cfbddb999e0100ec1", size = 79526, upload-time = "2026-03-16T08:08:02.533Z" }, +] + +[[package]] +name = "pydantic-settings" +version = "2.14.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, + { name = "python-dotenv" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/07/60/1d1e59c9c90d54591469ada7d268251f71c24bdb765f1a8a832cee8c6653/pydantic_settings-2.14.1.tar.gz", hash = "sha256:e874d3bec7e787b0c9958277956ed9b4dd5de6a80e162188fdaff7c5e26fd5fa", size = 235551, upload-time = "2026-05-08T13:40:06.542Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ae/8d/f1af3832f5e6eb13ba94ee809e72b8ecb5eef226d27ee0bef7d963d943c7/pydantic_settings-2.14.1-py3-none-any.whl", hash = "sha256:6e3c7edfd8277687cdc598f56e5cff0e9bfff0910a3749deaa8d4401c3a2b9de", size = 60964, upload-time = "2026-05-08T13:40:04.958Z" }, +] + [[package]] name = "pygments" version = "2.19.2" @@ -2682,6 +2872,15 @@ 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.28" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/82/54/a85eb421fbdd5007bc5af39d0f4ed9fa609e0fedbfdc2adcf0b34526870e/python_multipart-0.0.28.tar.gz", hash = "sha256:8550da197eac0f7ab748961fc9509b999fa2662ea25cef857f05249f6893c0f8", size = 45314, upload-time = "2026-05-10T11:05:16.596Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f3/a2/43bbc5860b5034e2af4ef99a0e04d726ff329c43e192ef3abaa8d7ecfce5/python_multipart-0.0.28-py3-none-any.whl", hash = "sha256:10faac07eb966c3f48dc415f9dee46c04cb10d58d30a35677db8027c825ed9b6", size = 29438, upload-time = "2026-05-10T11:05:15.052Z" }, +] + [[package]] name = "pyunormalize" version = "17.0.0" @@ -2812,20 +3011,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/89/f0/8956f8a86b20d7bb9d6ac0187cf4cd54d8065bc9a1a09eb8011d4d326596/redis-7.1.0-py3-none-any.whl", hash = "sha256:23c52b208f92b56103e17c5d06bdc1a6c2c0b3106583985a76a18f83b265de2b", size = 354159, upload-time = "2025-11-19T15:54:38.064Z" }, ] -[[package]] -name = "referencing" -version = "0.37.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "attrs" }, - { name = "rpds-py" }, - { name = "typing-extensions", marker = "python_full_version < '3.13'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/22/f5/df4e9027acead3ecc63e50fe1e36aca1523e1719559c499951bb4b53188f/referencing-0.37.0.tar.gz", hash = "sha256:44aefc3142c5b842538163acb373e24cce6632bd54bdb01b21ad5863489f50d8", size = 78036, upload-time = "2025-10-13T15:30:48.871Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl", hash = "sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231", size = 26766, upload-time = "2025-10-13T15:30:47.625Z" }, -] - [[package]] name = "regex" version = "2026.2.28" @@ -2996,6 +3181,141 @@ 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.19.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "rich" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8f/10/dc6e64e85244971671981dc26b09353a1564f5e61b977c80180dc42ad90b/rich_toolkit-0.19.9.tar.gz", hash = "sha256:fce5c6f41f79382ecf60a79851b2543f627568e3e07c78ab4b8542e1ca247d1c", size = 197653, upload-time = "2026-05-13T09:55:04.286Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8f/60/5a7de329d0b5b619757c169bbf8a5146c20fe49bd4d74045937fcd45a7d0/rich_toolkit-0.19.9-py3-none-any.whl", hash = "sha256:a1341f88feed5f295f001bb1c6b6cf1e208674187dd900416a30fd9d6f74fcce", size = 33711, upload-time = "2026-05-13T09:55:05.345Z" }, +] + +[[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" @@ -3008,128 +3328,6 @@ 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 = "rpds-py" -version = "0.30.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/20/af/3f2f423103f1113b36230496629986e0ef7e199d2aa8392452b484b38ced/rpds_py-0.30.0.tar.gz", hash = "sha256:dd8ff7cf90014af0c0f787eea34794ebf6415242ee1d6fa91eaba725cc441e84", size = 69469, upload-time = "2025-11-30T20:24:38.837Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/06/0c/0c411a0ec64ccb6d104dcabe0e713e05e153a9a2c3c2bd2b32ce412166fe/rpds_py-0.30.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:679ae98e00c0e8d68a7fda324e16b90fd5260945b45d3b824c892cec9eea3288", size = 370490, upload-time = "2025-11-30T20:21:33.256Z" }, - { url = "https://files.pythonhosted.org/packages/19/6a/4ba3d0fb7297ebae71171822554abe48d7cab29c28b8f9f2c04b79988c05/rpds_py-0.30.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4cc2206b76b4f576934f0ed374b10d7ca5f457858b157ca52064bdfc26b9fc00", size = 359751, upload-time = "2025-11-30T20:21:34.591Z" }, - { url = "https://files.pythonhosted.org/packages/cd/7c/e4933565ef7f7a0818985d87c15d9d273f1a649afa6a52ea35ad011195ea/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:389a2d49eded1896c3d48b0136ead37c48e221b391c052fba3f4055c367f60a6", size = 389696, upload-time = "2025-11-30T20:21:36.122Z" }, - { url = "https://files.pythonhosted.org/packages/5e/01/6271a2511ad0815f00f7ed4390cf2567bec1d4b1da39e2c27a41e6e3b4de/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:32c8528634e1bf7121f3de08fa85b138f4e0dc47657866630611b03967f041d7", size = 403136, upload-time = "2025-11-30T20:21:37.728Z" }, - { url = "https://files.pythonhosted.org/packages/55/64/c857eb7cd7541e9b4eee9d49c196e833128a55b89a9850a9c9ac33ccf897/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f207f69853edd6f6700b86efb84999651baf3789e78a466431df1331608e5324", size = 524699, upload-time = "2025-11-30T20:21:38.92Z" }, - { url = "https://files.pythonhosted.org/packages/9c/ed/94816543404078af9ab26159c44f9e98e20fe47e2126d5d32c9d9948d10a/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:67b02ec25ba7a9e8fa74c63b6ca44cf5707f2fbfadae3ee8e7494297d56aa9df", size = 412022, upload-time = "2025-11-30T20:21:40.407Z" }, - { url = "https://files.pythonhosted.org/packages/61/b5/707f6cf0066a6412aacc11d17920ea2e19e5b2f04081c64526eb35b5c6e7/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c0e95f6819a19965ff420f65578bacb0b00f251fefe2c8b23347c37174271f3", size = 390522, upload-time = "2025-11-30T20:21:42.17Z" }, - { url = "https://files.pythonhosted.org/packages/13/4e/57a85fda37a229ff4226f8cbcf09f2a455d1ed20e802ce5b2b4a7f5ed053/rpds_py-0.30.0-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:a452763cc5198f2f98898eb98f7569649fe5da666c2dc6b5ddb10fde5a574221", size = 404579, upload-time = "2025-11-30T20:21:43.769Z" }, - { url = "https://files.pythonhosted.org/packages/f9/da/c9339293513ec680a721e0e16bf2bac3db6e5d7e922488de471308349bba/rpds_py-0.30.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e0b65193a413ccc930671c55153a03ee57cecb49e6227204b04fae512eb657a7", size = 421305, upload-time = "2025-11-30T20:21:44.994Z" }, - { url = "https://files.pythonhosted.org/packages/f9/be/522cb84751114f4ad9d822ff5a1aa3c98006341895d5f084779b99596e5c/rpds_py-0.30.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:858738e9c32147f78b3ac24dc0edb6610000e56dc0f700fd5f651d0a0f0eb9ff", size = 572503, upload-time = "2025-11-30T20:21:46.91Z" }, - { url = "https://files.pythonhosted.org/packages/a2/9b/de879f7e7ceddc973ea6e4629e9b380213a6938a249e94b0cdbcc325bb66/rpds_py-0.30.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:da279aa314f00acbb803da1e76fa18666778e8a8f83484fba94526da5de2cba7", size = 598322, upload-time = "2025-11-30T20:21:48.709Z" }, - { url = "https://files.pythonhosted.org/packages/48/ac/f01fc22efec3f37d8a914fc1b2fb9bcafd56a299edbe96406f3053edea5a/rpds_py-0.30.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:7c64d38fb49b6cdeda16ab49e35fe0da2e1e9b34bc38bd78386530f218b37139", size = 560792, upload-time = "2025-11-30T20:21:50.024Z" }, - { url = "https://files.pythonhosted.org/packages/e2/da/4e2b19d0f131f35b6146425f846563d0ce036763e38913d917187307a671/rpds_py-0.30.0-cp310-cp310-win32.whl", hash = "sha256:6de2a32a1665b93233cde140ff8b3467bdb9e2af2b91079f0333a0974d12d464", size = 221901, upload-time = "2025-11-30T20:21:51.32Z" }, - { url = "https://files.pythonhosted.org/packages/96/cb/156d7a5cf4f78a7cc571465d8aec7a3c447c94f6749c5123f08438bcf7bc/rpds_py-0.30.0-cp310-cp310-win_amd64.whl", hash = "sha256:1726859cd0de969f88dc8673bdd954185b9104e05806be64bcd87badbe313169", size = 235823, upload-time = "2025-11-30T20:21:52.505Z" }, - { url = "https://files.pythonhosted.org/packages/4d/6e/f964e88b3d2abee2a82c1ac8366da848fce1c6d834dc2132c3fda3970290/rpds_py-0.30.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a2bffea6a4ca9f01b3f8e548302470306689684e61602aa3d141e34da06cf425", size = 370157, upload-time = "2025-11-30T20:21:53.789Z" }, - { url = "https://files.pythonhosted.org/packages/94/ba/24e5ebb7c1c82e74c4e4f33b2112a5573ddc703915b13a073737b59b86e0/rpds_py-0.30.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dc4f992dfe1e2bc3ebc7444f6c7051b4bc13cd8e33e43511e8ffd13bf407010d", size = 359676, upload-time = "2025-11-30T20:21:55.475Z" }, - { url = "https://files.pythonhosted.org/packages/84/86/04dbba1b087227747d64d80c3b74df946b986c57af0a9f0c98726d4d7a3b/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:422c3cb9856d80b09d30d2eb255d0754b23e090034e1deb4083f8004bd0761e4", size = 389938, upload-time = "2025-11-30T20:21:57.079Z" }, - { url = "https://files.pythonhosted.org/packages/42/bb/1463f0b1722b7f45431bdd468301991d1328b16cffe0b1c2918eba2c4eee/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:07ae8a593e1c3c6b82ca3292efbe73c30b61332fd612e05abee07c79359f292f", size = 402932, upload-time = "2025-11-30T20:21:58.47Z" }, - { url = "https://files.pythonhosted.org/packages/99/ee/2520700a5c1f2d76631f948b0736cdf9b0acb25abd0ca8e889b5c62ac2e3/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:12f90dd7557b6bd57f40abe7747e81e0c0b119bef015ea7726e69fe550e394a4", size = 525830, upload-time = "2025-11-30T20:21:59.699Z" }, - { url = "https://files.pythonhosted.org/packages/e0/ad/bd0331f740f5705cc555a5e17fdf334671262160270962e69a2bdef3bf76/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:99b47d6ad9a6da00bec6aabe5a6279ecd3c06a329d4aa4771034a21e335c3a97", size = 412033, upload-time = "2025-11-30T20:22:00.991Z" }, - { url = "https://files.pythonhosted.org/packages/f8/1e/372195d326549bb51f0ba0f2ecb9874579906b97e08880e7a65c3bef1a99/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:33f559f3104504506a44bb666b93a33f5d33133765b0c216a5bf2f1e1503af89", size = 390828, upload-time = "2025-11-30T20:22:02.723Z" }, - { url = "https://files.pythonhosted.org/packages/ab/2b/d88bb33294e3e0c76bc8f351a3721212713629ffca1700fa94979cb3eae8/rpds_py-0.30.0-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:946fe926af6e44f3697abbc305ea168c2c31d3e3ef1058cf68f379bf0335a78d", size = 404683, upload-time = "2025-11-30T20:22:04.367Z" }, - { url = "https://files.pythonhosted.org/packages/50/32/c759a8d42bcb5289c1fac697cd92f6fe01a018dd937e62ae77e0e7f15702/rpds_py-0.30.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:495aeca4b93d465efde585977365187149e75383ad2684f81519f504f5c13038", size = 421583, upload-time = "2025-11-30T20:22:05.814Z" }, - { url = "https://files.pythonhosted.org/packages/2b/81/e729761dbd55ddf5d84ec4ff1f47857f4374b0f19bdabfcf929164da3e24/rpds_py-0.30.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d9a0ca5da0386dee0655b4ccdf46119df60e0f10da268d04fe7cc87886872ba7", size = 572496, upload-time = "2025-11-30T20:22:07.713Z" }, - { url = "https://files.pythonhosted.org/packages/14/f6/69066a924c3557c9c30baa6ec3a0aa07526305684c6f86c696b08860726c/rpds_py-0.30.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8d6d1cc13664ec13c1b84241204ff3b12f9bb82464b8ad6e7a5d3486975c2eed", size = 598669, upload-time = "2025-11-30T20:22:09.312Z" }, - { url = "https://files.pythonhosted.org/packages/5f/48/905896b1eb8a05630d20333d1d8ffd162394127b74ce0b0784ae04498d32/rpds_py-0.30.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3896fa1be39912cf0757753826bc8bdc8ca331a28a7c4ae46b7a21280b06bb85", size = 561011, upload-time = "2025-11-30T20:22:11.309Z" }, - { url = "https://files.pythonhosted.org/packages/22/16/cd3027c7e279d22e5eb431dd3c0fbc677bed58797fe7581e148f3f68818b/rpds_py-0.30.0-cp311-cp311-win32.whl", hash = "sha256:55f66022632205940f1827effeff17c4fa7ae1953d2b74a8581baaefb7d16f8c", size = 221406, upload-time = "2025-11-30T20:22:13.101Z" }, - { url = "https://files.pythonhosted.org/packages/fa/5b/e7b7aa136f28462b344e652ee010d4de26ee9fd16f1bfd5811f5153ccf89/rpds_py-0.30.0-cp311-cp311-win_amd64.whl", hash = "sha256:a51033ff701fca756439d641c0ad09a41d9242fa69121c7d8769604a0a629825", size = 236024, upload-time = "2025-11-30T20:22:14.853Z" }, - { url = "https://files.pythonhosted.org/packages/14/a6/364bba985e4c13658edb156640608f2c9e1d3ea3c81b27aa9d889fff0e31/rpds_py-0.30.0-cp311-cp311-win_arm64.whl", hash = "sha256:47b0ef6231c58f506ef0b74d44e330405caa8428e770fec25329ed2cb971a229", size = 229069, upload-time = "2025-11-30T20:22:16.577Z" }, - { url = "https://files.pythonhosted.org/packages/03/e7/98a2f4ac921d82f33e03f3835f5bf3a4a40aa1bfdc57975e74a97b2b4bdd/rpds_py-0.30.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a161f20d9a43006833cd7068375a94d035714d73a172b681d8881820600abfad", size = 375086, upload-time = "2025-11-30T20:22:17.93Z" }, - { url = "https://files.pythonhosted.org/packages/4d/a1/bca7fd3d452b272e13335db8d6b0b3ecde0f90ad6f16f3328c6fb150c889/rpds_py-0.30.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6abc8880d9d036ecaafe709079969f56e876fcf107f7a8e9920ba6d5a3878d05", size = 359053, upload-time = "2025-11-30T20:22:19.297Z" }, - { url = "https://files.pythonhosted.org/packages/65/1c/ae157e83a6357eceff62ba7e52113e3ec4834a84cfe07fa4b0757a7d105f/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca28829ae5f5d569bb62a79512c842a03a12576375d5ece7d2cadf8abe96ec28", size = 390763, upload-time = "2025-11-30T20:22:21.661Z" }, - { url = "https://files.pythonhosted.org/packages/d4/36/eb2eb8515e2ad24c0bd43c3ee9cd74c33f7ca6430755ccdb240fd3144c44/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a1010ed9524c73b94d15919ca4d41d8780980e1765babf85f9a2f90d247153dd", size = 408951, upload-time = "2025-11-30T20:22:23.408Z" }, - { url = "https://files.pythonhosted.org/packages/d6/65/ad8dc1784a331fabbd740ef6f71ce2198c7ed0890dab595adb9ea2d775a1/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f8d1736cfb49381ba528cd5baa46f82fdc65c06e843dab24dd70b63d09121b3f", size = 514622, upload-time = "2025-11-30T20:22:25.16Z" }, - { url = "https://files.pythonhosted.org/packages/63/8e/0cfa7ae158e15e143fe03993b5bcd743a59f541f5952e1546b1ac1b5fd45/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d948b135c4693daff7bc2dcfc4ec57237a29bd37e60c2fabf5aff2bbacf3e2f1", size = 414492, upload-time = "2025-11-30T20:22:26.505Z" }, - { url = "https://files.pythonhosted.org/packages/60/1b/6f8f29f3f995c7ffdde46a626ddccd7c63aefc0efae881dc13b6e5d5bb16/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47f236970bccb2233267d89173d3ad2703cd36a0e2a6e92d0560d333871a3d23", size = 394080, upload-time = "2025-11-30T20:22:27.934Z" }, - { url = "https://files.pythonhosted.org/packages/6d/d5/a266341051a7a3ca2f4b750a3aa4abc986378431fc2da508c5034d081b70/rpds_py-0.30.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:2e6ecb5a5bcacf59c3f912155044479af1d0b6681280048b338b28e364aca1f6", size = 408680, upload-time = "2025-11-30T20:22:29.341Z" }, - { url = "https://files.pythonhosted.org/packages/10/3b/71b725851df9ab7a7a4e33cf36d241933da66040d195a84781f49c50490c/rpds_py-0.30.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a8fa71a2e078c527c3e9dc9fc5a98c9db40bcc8a92b4e8858e36d329f8684b51", size = 423589, upload-time = "2025-11-30T20:22:31.469Z" }, - { url = "https://files.pythonhosted.org/packages/00/2b/e59e58c544dc9bd8bd8384ecdb8ea91f6727f0e37a7131baeff8d6f51661/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:73c67f2db7bc334e518d097c6d1e6fed021bbc9b7d678d6cc433478365d1d5f5", size = 573289, upload-time = "2025-11-30T20:22:32.997Z" }, - { url = "https://files.pythonhosted.org/packages/da/3e/a18e6f5b460893172a7d6a680e86d3b6bc87a54c1f0b03446a3c8c7b588f/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5ba103fb455be00f3b1c2076c9d4264bfcb037c976167a6047ed82f23153f02e", size = 599737, upload-time = "2025-11-30T20:22:34.419Z" }, - { url = "https://files.pythonhosted.org/packages/5c/e2/714694e4b87b85a18e2c243614974413c60aa107fd815b8cbc42b873d1d7/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7cee9c752c0364588353e627da8a7e808a66873672bcb5f52890c33fd965b394", size = 563120, upload-time = "2025-11-30T20:22:35.903Z" }, - { url = "https://files.pythonhosted.org/packages/6f/ab/d5d5e3bcedb0a77f4f613706b750e50a5a3ba1c15ccd3665ecc636c968fd/rpds_py-0.30.0-cp312-cp312-win32.whl", hash = "sha256:1ab5b83dbcf55acc8b08fc62b796ef672c457b17dbd7820a11d6c52c06839bdf", size = 223782, upload-time = "2025-11-30T20:22:37.271Z" }, - { url = "https://files.pythonhosted.org/packages/39/3b/f786af9957306fdc38a74cef405b7b93180f481fb48453a114bb6465744a/rpds_py-0.30.0-cp312-cp312-win_amd64.whl", hash = "sha256:a090322ca841abd453d43456ac34db46e8b05fd9b3b4ac0c78bcde8b089f959b", size = 240463, upload-time = "2025-11-30T20:22:39.021Z" }, - { url = "https://files.pythonhosted.org/packages/f3/d2/b91dc748126c1559042cfe41990deb92c4ee3e2b415f6b5234969ffaf0cc/rpds_py-0.30.0-cp312-cp312-win_arm64.whl", hash = "sha256:669b1805bd639dd2989b281be2cfd951c6121b65e729d9b843e9639ef1fd555e", size = 230868, upload-time = "2025-11-30T20:22:40.493Z" }, - { url = "https://files.pythonhosted.org/packages/ed/dc/d61221eb88ff410de3c49143407f6f3147acf2538c86f2ab7ce65ae7d5f9/rpds_py-0.30.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:f83424d738204d9770830d35290ff3273fbb02b41f919870479fab14b9d303b2", size = 374887, upload-time = "2025-11-30T20:22:41.812Z" }, - { url = "https://files.pythonhosted.org/packages/fd/32/55fb50ae104061dbc564ef15cc43c013dc4a9f4527a1f4d99baddf56fe5f/rpds_py-0.30.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e7536cd91353c5273434b4e003cbda89034d67e7710eab8761fd918ec6c69cf8", size = 358904, upload-time = "2025-11-30T20:22:43.479Z" }, - { url = "https://files.pythonhosted.org/packages/58/70/faed8186300e3b9bdd138d0273109784eea2396c68458ed580f885dfe7ad/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2771c6c15973347f50fece41fc447c054b7ac2ae0502388ce3b6738cd366e3d4", size = 389945, upload-time = "2025-11-30T20:22:44.819Z" }, - { url = "https://files.pythonhosted.org/packages/bd/a8/073cac3ed2c6387df38f71296d002ab43496a96b92c823e76f46b8af0543/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0a59119fc6e3f460315fe9d08149f8102aa322299deaa5cab5b40092345c2136", size = 407783, upload-time = "2025-11-30T20:22:46.103Z" }, - { url = "https://files.pythonhosted.org/packages/77/57/5999eb8c58671f1c11eba084115e77a8899d6e694d2a18f69f0ba471ec8b/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:76fec018282b4ead0364022e3c54b60bf368b9d926877957a8624b58419169b7", size = 515021, upload-time = "2025-11-30T20:22:47.458Z" }, - { url = "https://files.pythonhosted.org/packages/e0/af/5ab4833eadc36c0a8ed2bc5c0de0493c04f6c06de223170bd0798ff98ced/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:692bef75a5525db97318e8cd061542b5a79812d711ea03dbc1f6f8dbb0c5f0d2", size = 414589, upload-time = "2025-11-30T20:22:48.872Z" }, - { url = "https://files.pythonhosted.org/packages/b7/de/f7192e12b21b9e9a68a6d0f249b4af3fdcdff8418be0767a627564afa1f1/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9027da1ce107104c50c81383cae773ef5c24d296dd11c99e2629dbd7967a20c6", size = 394025, upload-time = "2025-11-30T20:22:50.196Z" }, - { url = "https://files.pythonhosted.org/packages/91/c4/fc70cd0249496493500e7cc2de87504f5aa6509de1e88623431fec76d4b6/rpds_py-0.30.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:9cf69cdda1f5968a30a359aba2f7f9aa648a9ce4b580d6826437f2b291cfc86e", size = 408895, upload-time = "2025-11-30T20:22:51.87Z" }, - { url = "https://files.pythonhosted.org/packages/58/95/d9275b05ab96556fefff73a385813eb66032e4c99f411d0795372d9abcea/rpds_py-0.30.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a4796a717bf12b9da9d3ad002519a86063dcac8988b030e405704ef7d74d2d9d", size = 422799, upload-time = "2025-11-30T20:22:53.341Z" }, - { url = "https://files.pythonhosted.org/packages/06/c1/3088fc04b6624eb12a57eb814f0d4997a44b0d208d6cace713033ff1a6ba/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5d4c2aa7c50ad4728a094ebd5eb46c452e9cb7edbfdb18f9e1221f597a73e1e7", size = 572731, upload-time = "2025-11-30T20:22:54.778Z" }, - { url = "https://files.pythonhosted.org/packages/d8/42/c612a833183b39774e8ac8fecae81263a68b9583ee343db33ab571a7ce55/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ba81a9203d07805435eb06f536d95a266c21e5b2dfbf6517748ca40c98d19e31", size = 599027, upload-time = "2025-11-30T20:22:56.212Z" }, - { url = "https://files.pythonhosted.org/packages/5f/60/525a50f45b01d70005403ae0e25f43c0384369ad24ffe46e8d9068b50086/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:945dccface01af02675628334f7cf49c2af4c1c904748efc5cf7bbdf0b579f95", size = 563020, upload-time = "2025-11-30T20:22:58.2Z" }, - { url = "https://files.pythonhosted.org/packages/0b/5d/47c4655e9bcd5ca907148535c10e7d489044243cc9941c16ed7cd53be91d/rpds_py-0.30.0-cp313-cp313-win32.whl", hash = "sha256:b40fb160a2db369a194cb27943582b38f79fc4887291417685f3ad693c5a1d5d", size = 223139, upload-time = "2025-11-30T20:23:00.209Z" }, - { url = "https://files.pythonhosted.org/packages/f2/e1/485132437d20aa4d3e1d8b3fb5a5e65aa8139f1e097080c2a8443201742c/rpds_py-0.30.0-cp313-cp313-win_amd64.whl", hash = "sha256:806f36b1b605e2d6a72716f321f20036b9489d29c51c91f4dd29a3e3afb73b15", size = 240224, upload-time = "2025-11-30T20:23:02.008Z" }, - { url = "https://files.pythonhosted.org/packages/24/95/ffd128ed1146a153d928617b0ef673960130be0009c77d8fbf0abe306713/rpds_py-0.30.0-cp313-cp313-win_arm64.whl", hash = "sha256:d96c2086587c7c30d44f31f42eae4eac89b60dabbac18c7669be3700f13c3ce1", size = 230645, upload-time = "2025-11-30T20:23:03.43Z" }, - { url = "https://files.pythonhosted.org/packages/ff/1b/b10de890a0def2a319a2626334a7f0ae388215eb60914dbac8a3bae54435/rpds_py-0.30.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:eb0b93f2e5c2189ee831ee43f156ed34e2a89a78a66b98cadad955972548be5a", size = 364443, upload-time = "2025-11-30T20:23:04.878Z" }, - { url = "https://files.pythonhosted.org/packages/0d/bf/27e39f5971dc4f305a4fb9c672ca06f290f7c4e261c568f3dea16a410d47/rpds_py-0.30.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:922e10f31f303c7c920da8981051ff6d8c1a56207dbdf330d9047f6d30b70e5e", size = 353375, upload-time = "2025-11-30T20:23:06.342Z" }, - { url = "https://files.pythonhosted.org/packages/40/58/442ada3bba6e8e6615fc00483135c14a7538d2ffac30e2d933ccf6852232/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cdc62c8286ba9bf7f47befdcea13ea0e26bf294bda99758fd90535cbaf408000", size = 383850, upload-time = "2025-11-30T20:23:07.825Z" }, - { url = "https://files.pythonhosted.org/packages/14/14/f59b0127409a33c6ef6f5c1ebd5ad8e32d7861c9c7adfa9a624fc3889f6c/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:47f9a91efc418b54fb8190a6b4aa7813a23fb79c51f4bb84e418f5476c38b8db", size = 392812, upload-time = "2025-11-30T20:23:09.228Z" }, - { url = "https://files.pythonhosted.org/packages/b3/66/e0be3e162ac299b3a22527e8913767d869e6cc75c46bd844aa43fb81ab62/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1f3587eb9b17f3789ad50824084fa6f81921bbf9a795826570bda82cb3ed91f2", size = 517841, upload-time = "2025-11-30T20:23:11.186Z" }, - { url = "https://files.pythonhosted.org/packages/3d/55/fa3b9cf31d0c963ecf1ba777f7cf4b2a2c976795ac430d24a1f43d25a6ba/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:39c02563fc592411c2c61d26b6c5fe1e51eaa44a75aa2c8735ca88b0d9599daa", size = 408149, upload-time = "2025-11-30T20:23:12.864Z" }, - { url = "https://files.pythonhosted.org/packages/60/ca/780cf3b1a32b18c0f05c441958d3758f02544f1d613abf9488cd78876378/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:51a1234d8febafdfd33a42d97da7a43f5dcb120c1060e352a3fbc0c6d36e2083", size = 383843, upload-time = "2025-11-30T20:23:14.638Z" }, - { url = "https://files.pythonhosted.org/packages/82/86/d5f2e04f2aa6247c613da0c1dd87fcd08fa17107e858193566048a1e2f0a/rpds_py-0.30.0-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:eb2c4071ab598733724c08221091e8d80e89064cd472819285a9ab0f24bcedb9", size = 396507, upload-time = "2025-11-30T20:23:16.105Z" }, - { url = "https://files.pythonhosted.org/packages/4b/9a/453255d2f769fe44e07ea9785c8347edaf867f7026872e76c1ad9f7bed92/rpds_py-0.30.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6bdfdb946967d816e6adf9a3d8201bfad269c67efe6cefd7093ef959683c8de0", size = 414949, upload-time = "2025-11-30T20:23:17.539Z" }, - { url = "https://files.pythonhosted.org/packages/a3/31/622a86cdc0c45d6df0e9ccb6becdba5074735e7033c20e401a6d9d0e2ca0/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c77afbd5f5250bf27bf516c7c4a016813eb2d3e116139aed0096940c5982da94", size = 565790, upload-time = "2025-11-30T20:23:19.029Z" }, - { url = "https://files.pythonhosted.org/packages/1c/5d/15bbf0fb4a3f58a3b1c67855ec1efcc4ceaef4e86644665fff03e1b66d8d/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:61046904275472a76c8c90c9ccee9013d70a6d0f73eecefd38c1ae7c39045a08", size = 590217, upload-time = "2025-11-30T20:23:20.885Z" }, - { url = "https://files.pythonhosted.org/packages/6d/61/21b8c41f68e60c8cc3b2e25644f0e3681926020f11d06ab0b78e3c6bbff1/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4c5f36a861bc4b7da6516dbdf302c55313afa09b81931e8280361a4f6c9a2d27", size = 555806, upload-time = "2025-11-30T20:23:22.488Z" }, - { url = "https://files.pythonhosted.org/packages/f9/39/7e067bb06c31de48de3eb200f9fc7c58982a4d3db44b07e73963e10d3be9/rpds_py-0.30.0-cp313-cp313t-win32.whl", hash = "sha256:3d4a69de7a3e50ffc214ae16d79d8fbb0922972da0356dcf4d0fdca2878559c6", size = 211341, upload-time = "2025-11-30T20:23:24.449Z" }, - { url = "https://files.pythonhosted.org/packages/0a/4d/222ef0b46443cf4cf46764d9c630f3fe4abaa7245be9417e56e9f52b8f65/rpds_py-0.30.0-cp313-cp313t-win_amd64.whl", hash = "sha256:f14fc5df50a716f7ece6a80b6c78bb35ea2ca47c499e422aa4463455dd96d56d", size = 225768, upload-time = "2025-11-30T20:23:25.908Z" }, - { url = "https://files.pythonhosted.org/packages/86/81/dad16382ebbd3d0e0328776d8fd7ca94220e4fa0798d1dc5e7da48cb3201/rpds_py-0.30.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:68f19c879420aa08f61203801423f6cd5ac5f0ac4ac82a2368a9fcd6a9a075e0", size = 362099, upload-time = "2025-11-30T20:23:27.316Z" }, - { url = "https://files.pythonhosted.org/packages/2b/60/19f7884db5d5603edf3c6bce35408f45ad3e97e10007df0e17dd57af18f8/rpds_py-0.30.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ec7c4490c672c1a0389d319b3a9cfcd098dcdc4783991553c332a15acf7249be", size = 353192, upload-time = "2025-11-30T20:23:29.151Z" }, - { url = "https://files.pythonhosted.org/packages/bf/c4/76eb0e1e72d1a9c4703c69607cec123c29028bff28ce41588792417098ac/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f251c812357a3fed308d684a5079ddfb9d933860fc6de89f2b7ab00da481e65f", size = 384080, upload-time = "2025-11-30T20:23:30.785Z" }, - { url = "https://files.pythonhosted.org/packages/72/87/87ea665e92f3298d1b26d78814721dc39ed8d2c74b86e83348d6b48a6f31/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ac98b175585ecf4c0348fd7b29c3864bda53b805c773cbf7bfdaffc8070c976f", size = 394841, upload-time = "2025-11-30T20:23:32.209Z" }, - { url = "https://files.pythonhosted.org/packages/77/ad/7783a89ca0587c15dcbf139b4a8364a872a25f861bdb88ed99f9b0dec985/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3e62880792319dbeb7eb866547f2e35973289e7d5696c6e295476448f5b63c87", size = 516670, upload-time = "2025-11-30T20:23:33.742Z" }, - { url = "https://files.pythonhosted.org/packages/5b/3c/2882bdac942bd2172f3da574eab16f309ae10a3925644e969536553cb4ee/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4e7fc54e0900ab35d041b0601431b0a0eb495f0851a0639b6ef90f7741b39a18", size = 408005, upload-time = "2025-11-30T20:23:35.253Z" }, - { url = "https://files.pythonhosted.org/packages/ce/81/9a91c0111ce1758c92516a3e44776920b579d9a7c09b2b06b642d4de3f0f/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47e77dc9822d3ad616c3d5759ea5631a75e5809d5a28707744ef79d7a1bcfcad", size = 382112, upload-time = "2025-11-30T20:23:36.842Z" }, - { url = "https://files.pythonhosted.org/packages/cf/8e/1da49d4a107027e5fbc64daeab96a0706361a2918da10cb41769244b805d/rpds_py-0.30.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:b4dc1a6ff022ff85ecafef7979a2c6eb423430e05f1165d6688234e62ba99a07", size = 399049, upload-time = "2025-11-30T20:23:38.343Z" }, - { url = "https://files.pythonhosted.org/packages/df/5a/7ee239b1aa48a127570ec03becbb29c9d5a9eb092febbd1699d567cae859/rpds_py-0.30.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4559c972db3a360808309e06a74628b95eaccbf961c335c8fe0d590cf587456f", size = 415661, upload-time = "2025-11-30T20:23:40.263Z" }, - { url = "https://files.pythonhosted.org/packages/70/ea/caa143cf6b772f823bc7929a45da1fa83569ee49b11d18d0ada7f5ee6fd6/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:0ed177ed9bded28f8deb6ab40c183cd1192aa0de40c12f38be4d59cd33cb5c65", size = 565606, upload-time = "2025-11-30T20:23:42.186Z" }, - { url = "https://files.pythonhosted.org/packages/64/91/ac20ba2d69303f961ad8cf55bf7dbdb4763f627291ba3d0d7d67333cced9/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ad1fa8db769b76ea911cb4e10f049d80bf518c104f15b3edb2371cc65375c46f", size = 591126, upload-time = "2025-11-30T20:23:44.086Z" }, - { url = "https://files.pythonhosted.org/packages/21/20/7ff5f3c8b00c8a95f75985128c26ba44503fb35b8e0259d812766ea966c7/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:46e83c697b1f1c72b50e5ee5adb4353eef7406fb3f2043d64c33f20ad1c2fc53", size = 553371, upload-time = "2025-11-30T20:23:46.004Z" }, - { url = "https://files.pythonhosted.org/packages/72/c7/81dadd7b27c8ee391c132a6b192111ca58d866577ce2d9b0ca157552cce0/rpds_py-0.30.0-cp314-cp314-win32.whl", hash = "sha256:ee454b2a007d57363c2dfd5b6ca4a5d7e2c518938f8ed3b706e37e5d470801ed", size = 215298, upload-time = "2025-11-30T20:23:47.696Z" }, - { url = "https://files.pythonhosted.org/packages/3e/d2/1aaac33287e8cfb07aab2e6b8ac1deca62f6f65411344f1433c55e6f3eb8/rpds_py-0.30.0-cp314-cp314-win_amd64.whl", hash = "sha256:95f0802447ac2d10bcc69f6dc28fe95fdf17940367b21d34e34c737870758950", size = 228604, upload-time = "2025-11-30T20:23:49.501Z" }, - { url = "https://files.pythonhosted.org/packages/e8/95/ab005315818cc519ad074cb7784dae60d939163108bd2b394e60dc7b5461/rpds_py-0.30.0-cp314-cp314-win_arm64.whl", hash = "sha256:613aa4771c99f03346e54c3f038e4cc574ac09a3ddfb0e8878487335e96dead6", size = 222391, upload-time = "2025-11-30T20:23:50.96Z" }, - { url = "https://files.pythonhosted.org/packages/9e/68/154fe0194d83b973cdedcdcc88947a2752411165930182ae41d983dcefa6/rpds_py-0.30.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:7e6ecfcb62edfd632e56983964e6884851786443739dbfe3582947e87274f7cb", size = 364868, upload-time = "2025-11-30T20:23:52.494Z" }, - { url = "https://files.pythonhosted.org/packages/83/69/8bbc8b07ec854d92a8b75668c24d2abcb1719ebf890f5604c61c9369a16f/rpds_py-0.30.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a1d0bc22a7cdc173fedebb73ef81e07faef93692b8c1ad3733b67e31e1b6e1b8", size = 353747, upload-time = "2025-11-30T20:23:54.036Z" }, - { url = "https://files.pythonhosted.org/packages/ab/00/ba2e50183dbd9abcce9497fa5149c62b4ff3e22d338a30d690f9af970561/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0d08f00679177226c4cb8c5265012eea897c8ca3b93f429e546600c971bcbae7", size = 383795, upload-time = "2025-11-30T20:23:55.556Z" }, - { url = "https://files.pythonhosted.org/packages/05/6f/86f0272b84926bcb0e4c972262f54223e8ecc556b3224d281e6598fc9268/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5965af57d5848192c13534f90f9dd16464f3c37aaf166cc1da1cae1fd5a34898", size = 393330, upload-time = "2025-11-30T20:23:57.033Z" }, - { url = "https://files.pythonhosted.org/packages/cb/e9/0e02bb2e6dc63d212641da45df2b0bf29699d01715913e0d0f017ee29438/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9a4e86e34e9ab6b667c27f3211ca48f73dba7cd3d90f8d5b11be56e5dbc3fb4e", size = 518194, upload-time = "2025-11-30T20:23:58.637Z" }, - { url = "https://files.pythonhosted.org/packages/ee/ca/be7bca14cf21513bdf9c0606aba17d1f389ea2b6987035eb4f62bd923f25/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e5d3e6b26f2c785d65cc25ef1e5267ccbe1b069c5c21b8cc724efee290554419", size = 408340, upload-time = "2025-11-30T20:24:00.2Z" }, - { url = "https://files.pythonhosted.org/packages/c2/c7/736e00ebf39ed81d75544c0da6ef7b0998f8201b369acf842f9a90dc8fce/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:626a7433c34566535b6e56a1b39a7b17ba961e97ce3b80ec62e6f1312c025551", size = 383765, upload-time = "2025-11-30T20:24:01.759Z" }, - { url = "https://files.pythonhosted.org/packages/4a/3f/da50dfde9956aaf365c4adc9533b100008ed31aea635f2b8d7b627e25b49/rpds_py-0.30.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:acd7eb3f4471577b9b5a41baf02a978e8bdeb08b4b355273994f8b87032000a8", size = 396834, upload-time = "2025-11-30T20:24:03.687Z" }, - { url = "https://files.pythonhosted.org/packages/4e/00/34bcc2565b6020eab2623349efbdec810676ad571995911f1abdae62a3a0/rpds_py-0.30.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fe5fa731a1fa8a0a56b0977413f8cacac1768dad38d16b3a296712709476fbd5", size = 415470, upload-time = "2025-11-30T20:24:05.232Z" }, - { url = "https://files.pythonhosted.org/packages/8c/28/882e72b5b3e6f718d5453bd4d0d9cf8df36fddeb4ddbbab17869d5868616/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:74a3243a411126362712ee1524dfc90c650a503502f135d54d1b352bd01f2404", size = 565630, upload-time = "2025-11-30T20:24:06.878Z" }, - { url = "https://files.pythonhosted.org/packages/3b/97/04a65539c17692de5b85c6e293520fd01317fd878ea1995f0367d4532fb1/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:3e8eeb0544f2eb0d2581774be4c3410356eba189529a6b3e36bbbf9696175856", size = 591148, upload-time = "2025-11-30T20:24:08.445Z" }, - { url = "https://files.pythonhosted.org/packages/85/70/92482ccffb96f5441aab93e26c4d66489eb599efdcf96fad90c14bbfb976/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:dbd936cde57abfee19ab3213cf9c26be06d60750e60a8e4dd85d1ab12c8b1f40", size = 556030, upload-time = "2025-11-30T20:24:10.956Z" }, - { url = "https://files.pythonhosted.org/packages/20/53/7c7e784abfa500a2b6b583b147ee4bb5a2b3747a9166bab52fec4b5b5e7d/rpds_py-0.30.0-cp314-cp314t-win32.whl", hash = "sha256:dc824125c72246d924f7f796b4f63c1e9dc810c7d9e2355864b3c3a73d59ade0", size = 211570, upload-time = "2025-11-30T20:24:12.735Z" }, - { url = "https://files.pythonhosted.org/packages/d0/02/fa464cdfbe6b26e0600b62c528b72d8608f5cc49f96b8d6e38c95d60c676/rpds_py-0.30.0-cp314-cp314t-win_amd64.whl", hash = "sha256:27f4b0e92de5bfbc6f86e43959e6edd1425c33b5e69aab0984a72047f2bcf1e3", size = 226532, upload-time = "2025-11-30T20:24:14.634Z" }, - { url = "https://files.pythonhosted.org/packages/69/71/3f34339ee70521864411f8b6992e7ab13ac30d8e4e3309e07c7361767d91/rpds_py-0.30.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:c2262bdba0ad4fc6fb5545660673925c2d2a5d9e2e0fb603aad545427be0fc58", size = 372292, upload-time = "2025-11-30T20:24:16.537Z" }, - { url = "https://files.pythonhosted.org/packages/57/09/f183df9b8f2d66720d2ef71075c59f7e1b336bec7ee4c48f0a2b06857653/rpds_py-0.30.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:ee6af14263f25eedc3bb918a3c04245106a42dfd4f5c2285ea6f997b1fc3f89a", size = 362128, upload-time = "2025-11-30T20:24:18.086Z" }, - { url = "https://files.pythonhosted.org/packages/7a/68/5c2594e937253457342e078f0cc1ded3dd7b2ad59afdbf2d354869110a02/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3adbb8179ce342d235c31ab8ec511e66c73faa27a47e076ccc92421add53e2bb", size = 391542, upload-time = "2025-11-30T20:24:20.092Z" }, - { url = "https://files.pythonhosted.org/packages/49/5c/31ef1afd70b4b4fbdb2800249f34c57c64beb687495b10aec0365f53dfc4/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:250fa00e9543ac9b97ac258bd37367ff5256666122c2d0f2bc97577c60a1818c", size = 404004, upload-time = "2025-11-30T20:24:22.231Z" }, - { url = "https://files.pythonhosted.org/packages/e3/63/0cfbea38d05756f3440ce6534d51a491d26176ac045e2707adc99bb6e60a/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9854cf4f488b3d57b9aaeb105f06d78e5529d3145b1e4a41750167e8c213c6d3", size = 527063, upload-time = "2025-11-30T20:24:24.302Z" }, - { url = "https://files.pythonhosted.org/packages/42/e6/01e1f72a2456678b0f618fc9a1a13f882061690893c192fcad9f2926553a/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:993914b8e560023bc0a8bf742c5f303551992dcb85e247b1e5c7f4a7d145bda5", size = 413099, upload-time = "2025-11-30T20:24:25.916Z" }, - { url = "https://files.pythonhosted.org/packages/b8/25/8df56677f209003dcbb180765520c544525e3ef21ea72279c98b9aa7c7fb/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58edca431fb9b29950807e301826586e5bbf24163677732429770a697ffe6738", size = 392177, upload-time = "2025-11-30T20:24:27.834Z" }, - { url = "https://files.pythonhosted.org/packages/4a/b4/0a771378c5f16f8115f796d1f437950158679bcd2a7c68cf251cfb00ed5b/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:dea5b552272a944763b34394d04577cf0f9bd013207bc32323b5a89a53cf9c2f", size = 406015, upload-time = "2025-11-30T20:24:29.457Z" }, - { url = "https://files.pythonhosted.org/packages/36/d8/456dbba0af75049dc6f63ff295a2f92766b9d521fa00de67a2bd6427d57a/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ba3af48635eb83d03f6c9735dfb21785303e73d22ad03d489e88adae6eab8877", size = 423736, upload-time = "2025-11-30T20:24:31.22Z" }, - { url = "https://files.pythonhosted.org/packages/13/64/b4d76f227d5c45a7e0b796c674fd81b0a6c4fbd48dc29271857d8219571c/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:dff13836529b921e22f15cb099751209a60009731a68519630a24d61f0b1b30a", size = 573981, upload-time = "2025-11-30T20:24:32.934Z" }, - { url = "https://files.pythonhosted.org/packages/20/91/092bacadeda3edf92bf743cc96a7be133e13a39cdbfd7b5082e7ab638406/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:1b151685b23929ab7beec71080a8889d4d6d9fa9a983d213f07121205d48e2c4", size = 599782, upload-time = "2025-11-30T20:24:35.169Z" }, - { url = "https://files.pythonhosted.org/packages/d1/b7/b95708304cd49b7b6f82fdd039f1748b66ec2b21d6a45180910802f1abf1/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:ac37f9f516c51e5753f27dfdef11a88330f04de2d564be3991384b2f3535d02e", size = 562191, upload-time = "2025-11-30T20:24:36.853Z" }, -] - [[package]] name = "ruff" version = "0.14.11" @@ -3169,6 +3367,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b7/46/f5af3402b579fd5e11573ce652019a67074317e18c1935cc0b4ba9b35552/secretstorage-3.5.0-py3-none-any.whl", hash = "sha256:0ce65888c0725fcb2c5bc0fdb8e5438eece02c523557ea40ce0703c266248137", size = 15554, upload-time = "2025-11-23T19:02:51.545Z" }, ] +[[package]] +name = "sentry-sdk" +version = "2.60.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/54/a2/2e6c090db384cc515069f4f85542bd5baf6786852073020ea73d4a76d3ea/sentry_sdk-2.60.0.tar.gz", hash = "sha256:0bd25e54e78ca02d0be512529fa644bbbf9e8470d7b26371294012d4ca93c978", size = 452946, upload-time = "2026-05-13T13:34:52.516Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/29/41/f2b800b7f12a05dd48c2a6280d4dd812d1425fc66ed3fe3fd99420c41d1a/sentry_sdk-2.60.0-py3-none-any.whl", hash = "sha256:28a536c03291c8bcb363cf35c611b32738ec118ff64d8d6383b096448ac4c803", size = 475616, upload-time = "2026-05-13T13:34:50.259Z" }, +] + [[package]] name = "setuptools" version = "80.9.0" @@ -3343,6 +3554,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/4a/91/48db081e7a63bb37284f9fbcefda7c44c277b18b0e13fbc36ea2335b71e6/typer-0.24.1-py3-none-any.whl", hash = "sha256:112c1f0ce578bfb4cab9ffdabc68f031416ebcc216536611ba21f04e9aa84c9e", size = 56085, upload-time = "2026-02-21T16:54:41.616Z" }, ] +[[package]] +name = "types-requests" +version = "2.33.0.20260513" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6d/f7/3228dd3794941bcb92ca6ca2045a6671a828ec0b47becbef23310bc45559/types_requests-2.33.0.20260513.tar.gz", hash = "sha256:bd845450e954e751373d5d33526742592f298808a3ee3bda7e858e46b839b57f", size = 24714, upload-time = "2026-05-13T05:39:23.481Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/51/f5/233a78be8367a9888de718f002fb27b1ea4be39471cd88aedeafceed872e/types_requests-2.33.0.20260513-py3-none-any.whl", hash = "sha256:d5a965f9d18b6e06b72039a69565de9027e58f36a7f709857da747fbe7521122", size = 21390, upload-time = "2026-05-13T05:39:22.262Z" }, +] + [[package]] name = "typing-extensions" version = "4.15.0" @@ -3600,7 +3823,7 @@ wheels = [ [[package]] name = "web3" -version = "6.11.0" +version = "7.16.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "aiohttp" }, @@ -3610,86 +3833,76 @@ dependencies = [ { name = "eth-typing" }, { name = "eth-utils" }, { name = "hexbytes" }, - { name = "jsonschema" }, - { name = "lru-dict" }, - { name = "protobuf" }, + { 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/54/4d/720345f2dd680d7fdebbbce91a22f3db0f068e931e0969ecc16014e9fe4a/web3-6.11.0.tar.gz", hash = "sha256:050dea52ae73d787272e7ecba7249f096595938c90cce1a384c20375c6b0f720", size = 1481313, upload-time = "2023-10-11T19:32:44.129Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f1/d9/bdfa9e715804020c3f3676346065c18adbc207c9343a3458246d7430f45c/web3-7.16.0.tar.gz", hash = "sha256:b4a75a3fa94fef4d23d502eb3c2244146ef9a1ee0082cf1cb0a91586ba0510c3", size = 2211469, upload-time = "2026-05-01T21:22:20.666Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f5/ff/02b06afffe33e4f0918978e7883d4eafdad7f265d2b83019336184c3b6da/web3-6.11.0-py3-none-any.whl", hash = "sha256:44e79da6a4765eacf137f2f388e37aa0c1e24a93bdfb462cffe9441d1be3d509", size = 1603854, upload-time = "2023-10-11T19:32:41.087Z" }, + { url = "https://files.pythonhosted.org/packages/2c/f9/5345c13f8469f3ce344b4d9934c0387b83a49420192482111e9c1fa95ec2/web3-7.16.0-py3-none-any.whl", hash = "sha256:760b2718c473980d70708c3593d9d28395db4b482f45e38a63a36fa028178f51", size = 1372181, upload-time = "2026-05-01T21:22:17.015Z" }, ] [[package]] name = "websockets" -version = "16.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/04/24/4b2031d72e840ce4c1ccb255f693b15c334757fc50023e4db9537080b8c4/websockets-16.0.tar.gz", hash = "sha256:5f6261a5e56e8d5c42a4497b364ea24d94d9563e8fbd44e78ac40879c60179b5", size = 179346, upload-time = "2026-01-10T09:23:47.181Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/20/74/221f58decd852f4b59cc3354cccaf87e8ef695fede361d03dc9a7396573b/websockets-16.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:04cdd5d2d1dacbad0a7bf36ccbcd3ccd5a30ee188f2560b7a62a30d14107b31a", size = 177343, upload-time = "2026-01-10T09:22:21.28Z" }, - { url = "https://files.pythonhosted.org/packages/19/0f/22ef6107ee52ab7f0b710d55d36f5a5d3ef19e8a205541a6d7ffa7994e5a/websockets-16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:8ff32bb86522a9e5e31439a58addbb0166f0204d64066fb955265c4e214160f0", size = 175021, upload-time = "2026-01-10T09:22:22.696Z" }, - { url = "https://files.pythonhosted.org/packages/10/40/904a4cb30d9b61c0e278899bf36342e9b0208eb3c470324a9ecbaac2a30f/websockets-16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:583b7c42688636f930688d712885cf1531326ee05effd982028212ccc13e5957", size = 175320, upload-time = "2026-01-10T09:22:23.94Z" }, - { url = "https://files.pythonhosted.org/packages/9d/2f/4b3ca7e106bc608744b1cdae041e005e446124bebb037b18799c2d356864/websockets-16.0-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7d837379b647c0c4c2355c2499723f82f1635fd2c26510e1f587d89bc2199e72", size = 183815, upload-time = "2026-01-10T09:22:25.469Z" }, - { url = "https://files.pythonhosted.org/packages/86/26/d40eaa2a46d4302becec8d15b0fc5e45bdde05191e7628405a19cf491ccd/websockets-16.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:df57afc692e517a85e65b72e165356ed1df12386ecb879ad5693be08fac65dde", size = 185054, upload-time = "2026-01-10T09:22:27.101Z" }, - { url = "https://files.pythonhosted.org/packages/b0/ba/6500a0efc94f7373ee8fefa8c271acdfd4dca8bd49a90d4be7ccabfc397e/websockets-16.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:2b9f1e0d69bc60a4a87349d50c09a037a2607918746f07de04df9e43252c77a3", size = 184565, upload-time = "2026-01-10T09:22:28.293Z" }, - { url = "https://files.pythonhosted.org/packages/04/b4/96bf2cee7c8d8102389374a2616200574f5f01128d1082f44102140344cc/websockets-16.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:335c23addf3d5e6a8633f9f8eda77efad001671e80b95c491dd0924587ece0b3", size = 183848, upload-time = "2026-01-10T09:22:30.394Z" }, - { url = "https://files.pythonhosted.org/packages/02/8e/81f40fb00fd125357814e8c3025738fc4ffc3da4b6b4a4472a82ba304b41/websockets-16.0-cp310-cp310-win32.whl", hash = "sha256:37b31c1623c6605e4c00d466c9d633f9b812ea430c11c8a278774a1fde1acfa9", size = 178249, upload-time = "2026-01-10T09:22:32.083Z" }, - { url = "https://files.pythonhosted.org/packages/b4/5f/7e40efe8df57db9b91c88a43690ac66f7b7aa73a11aa6a66b927e44f26fa/websockets-16.0-cp310-cp310-win_amd64.whl", hash = "sha256:8e1dab317b6e77424356e11e99a432b7cb2f3ec8c5ab4dabbcee6add48f72b35", size = 178685, upload-time = "2026-01-10T09:22:33.345Z" }, - { url = "https://files.pythonhosted.org/packages/f2/db/de907251b4ff46ae804ad0409809504153b3f30984daf82a1d84a9875830/websockets-16.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:31a52addea25187bde0797a97d6fc3d2f92b6f72a9370792d65a6e84615ac8a8", size = 177340, upload-time = "2026-01-10T09:22:34.539Z" }, - { url = "https://files.pythonhosted.org/packages/f3/fa/abe89019d8d8815c8781e90d697dec52523fb8ebe308bf11664e8de1877e/websockets-16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:417b28978cdccab24f46400586d128366313e8a96312e4b9362a4af504f3bbad", size = 175022, upload-time = "2026-01-10T09:22:36.332Z" }, - { url = "https://files.pythonhosted.org/packages/58/5d/88ea17ed1ded2079358b40d31d48abe90a73c9e5819dbcde1606e991e2ad/websockets-16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:af80d74d4edfa3cb9ed973a0a5ba2b2a549371f8a741e0800cb07becdd20f23d", size = 175319, upload-time = "2026-01-10T09:22:37.602Z" }, - { url = "https://files.pythonhosted.org/packages/d2/ae/0ee92b33087a33632f37a635e11e1d99d429d3d323329675a6022312aac2/websockets-16.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:08d7af67b64d29823fed316505a89b86705f2b7981c07848fb5e3ea3020c1abe", size = 184631, upload-time = "2026-01-10T09:22:38.789Z" }, - { url = "https://files.pythonhosted.org/packages/c8/c5/27178df583b6c5b31b29f526ba2da5e2f864ecc79c99dae630a85d68c304/websockets-16.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7be95cfb0a4dae143eaed2bcba8ac23f4892d8971311f1b06f3c6b78952ee70b", size = 185870, upload-time = "2026-01-10T09:22:39.893Z" }, - { url = "https://files.pythonhosted.org/packages/87/05/536652aa84ddc1c018dbb7e2c4cbcd0db884580bf8e95aece7593fde526f/websockets-16.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d6297ce39ce5c2e6feb13c1a996a2ded3b6832155fcfc920265c76f24c7cceb5", size = 185361, upload-time = "2026-01-10T09:22:41.016Z" }, - { url = "https://files.pythonhosted.org/packages/6d/e2/d5332c90da12b1e01f06fb1b85c50cfc489783076547415bf9f0a659ec19/websockets-16.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1c1b30e4f497b0b354057f3467f56244c603a79c0d1dafce1d16c283c25f6e64", size = 184615, upload-time = "2026-01-10T09:22:42.442Z" }, - { url = "https://files.pythonhosted.org/packages/77/fb/d3f9576691cae9253b51555f841bc6600bf0a983a461c79500ace5a5b364/websockets-16.0-cp311-cp311-win32.whl", hash = "sha256:5f451484aeb5cafee1ccf789b1b66f535409d038c56966d6101740c1614b86c6", size = 178246, upload-time = "2026-01-10T09:22:43.654Z" }, - { url = "https://files.pythonhosted.org/packages/54/67/eaff76b3dbaf18dcddabc3b8c1dba50b483761cccff67793897945b37408/websockets-16.0-cp311-cp311-win_amd64.whl", hash = "sha256:8d7f0659570eefb578dacde98e24fb60af35350193e4f56e11190787bee77dac", size = 178684, upload-time = "2026-01-10T09:22:44.941Z" }, - { url = "https://files.pythonhosted.org/packages/84/7b/bac442e6b96c9d25092695578dda82403c77936104b5682307bd4deb1ad4/websockets-16.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:71c989cbf3254fbd5e84d3bff31e4da39c43f884e64f2551d14bb3c186230f00", size = 177365, upload-time = "2026-01-10T09:22:46.787Z" }, - { url = "https://files.pythonhosted.org/packages/b0/fe/136ccece61bd690d9c1f715baaeefd953bb2360134de73519d5df19d29ca/websockets-16.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:8b6e209ffee39ff1b6d0fa7bfef6de950c60dfb91b8fcead17da4ee539121a79", size = 175038, upload-time = "2026-01-10T09:22:47.999Z" }, - { url = "https://files.pythonhosted.org/packages/40/1e/9771421ac2286eaab95b8575b0cb701ae3663abf8b5e1f64f1fd90d0a673/websockets-16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:86890e837d61574c92a97496d590968b23c2ef0aeb8a9bc9421d174cd378ae39", size = 175328, upload-time = "2026-01-10T09:22:49.809Z" }, - { url = "https://files.pythonhosted.org/packages/18/29/71729b4671f21e1eaa5d6573031ab810ad2936c8175f03f97f3ff164c802/websockets-16.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9b5aca38b67492ef518a8ab76851862488a478602229112c4b0d58d63a7a4d5c", size = 184915, upload-time = "2026-01-10T09:22:51.071Z" }, - { url = "https://files.pythonhosted.org/packages/97/bb/21c36b7dbbafc85d2d480cd65df02a1dc93bf76d97147605a8e27ff9409d/websockets-16.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e0334872c0a37b606418ac52f6ab9cfd17317ac26365f7f65e203e2d0d0d359f", size = 186152, upload-time = "2026-01-10T09:22:52.224Z" }, - { url = "https://files.pythonhosted.org/packages/4a/34/9bf8df0c0cf88fa7bfe36678dc7b02970c9a7d5e065a3099292db87b1be2/websockets-16.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a0b31e0b424cc6b5a04b8838bbaec1688834b2383256688cf47eb97412531da1", size = 185583, upload-time = "2026-01-10T09:22:53.443Z" }, - { url = "https://files.pythonhosted.org/packages/47/88/4dd516068e1a3d6ab3c7c183288404cd424a9a02d585efbac226cb61ff2d/websockets-16.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:485c49116d0af10ac698623c513c1cc01c9446c058a4e61e3bf6c19dff7335a2", size = 184880, upload-time = "2026-01-10T09:22:55.033Z" }, - { url = "https://files.pythonhosted.org/packages/91/d6/7d4553ad4bf1c0421e1ebd4b18de5d9098383b5caa1d937b63df8d04b565/websockets-16.0-cp312-cp312-win32.whl", hash = "sha256:eaded469f5e5b7294e2bdca0ab06becb6756ea86894a47806456089298813c89", size = 178261, upload-time = "2026-01-10T09:22:56.251Z" }, - { url = "https://files.pythonhosted.org/packages/c3/f0/f3a17365441ed1c27f850a80b2bc680a0fa9505d733fe152fdf5e98c1c0b/websockets-16.0-cp312-cp312-win_amd64.whl", hash = "sha256:5569417dc80977fc8c2d43a86f78e0a5a22fee17565d78621b6bb264a115d4ea", size = 178693, upload-time = "2026-01-10T09:22:57.478Z" }, - { url = "https://files.pythonhosted.org/packages/cc/9c/baa8456050d1c1b08dd0ec7346026668cbc6f145ab4e314d707bb845bf0d/websockets-16.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:878b336ac47938b474c8f982ac2f7266a540adc3fa4ad74ae96fea9823a02cc9", size = 177364, upload-time = "2026-01-10T09:22:59.333Z" }, - { url = "https://files.pythonhosted.org/packages/7e/0c/8811fc53e9bcff68fe7de2bcbe75116a8d959ac699a3200f4847a8925210/websockets-16.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:52a0fec0e6c8d9a784c2c78276a48a2bdf099e4ccc2a4cad53b27718dbfd0230", size = 175039, upload-time = "2026-01-10T09:23:01.171Z" }, - { url = "https://files.pythonhosted.org/packages/aa/82/39a5f910cb99ec0b59e482971238c845af9220d3ab9fa76dd9162cda9d62/websockets-16.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e6578ed5b6981005df1860a56e3617f14a6c307e6a71b4fff8c48fdc50f3ed2c", size = 175323, upload-time = "2026-01-10T09:23:02.341Z" }, - { url = "https://files.pythonhosted.org/packages/bd/28/0a25ee5342eb5d5f297d992a77e56892ecb65e7854c7898fb7d35e9b33bd/websockets-16.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:95724e638f0f9c350bb1c2b0a7ad0e83d9cc0c9259f3ea94e40d7b02a2179ae5", size = 184975, upload-time = "2026-01-10T09:23:03.756Z" }, - { url = "https://files.pythonhosted.org/packages/f9/66/27ea52741752f5107c2e41fda05e8395a682a1e11c4e592a809a90c6a506/websockets-16.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c0204dc62a89dc9d50d682412c10b3542d748260d743500a85c13cd1ee4bde82", size = 186203, upload-time = "2026-01-10T09:23:05.01Z" }, - { url = "https://files.pythonhosted.org/packages/37/e5/8e32857371406a757816a2b471939d51c463509be73fa538216ea52b792a/websockets-16.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:52ac480f44d32970d66763115edea932f1c5b1312de36df06d6b219f6741eed8", size = 185653, upload-time = "2026-01-10T09:23:06.301Z" }, - { url = "https://files.pythonhosted.org/packages/9b/67/f926bac29882894669368dc73f4da900fcdf47955d0a0185d60103df5737/websockets-16.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6e5a82b677f8f6f59e8dfc34ec06ca6b5b48bc4fcda346acd093694cc2c24d8f", size = 184920, upload-time = "2026-01-10T09:23:07.492Z" }, - { url = "https://files.pythonhosted.org/packages/3c/a1/3d6ccdcd125b0a42a311bcd15a7f705d688f73b2a22d8cf1c0875d35d34a/websockets-16.0-cp313-cp313-win32.whl", hash = "sha256:abf050a199613f64c886ea10f38b47770a65154dc37181bfaff70c160f45315a", size = 178255, upload-time = "2026-01-10T09:23:09.245Z" }, - { url = "https://files.pythonhosted.org/packages/6b/ae/90366304d7c2ce80f9b826096a9e9048b4bb760e44d3b873bb272cba696b/websockets-16.0-cp313-cp313-win_amd64.whl", hash = "sha256:3425ac5cf448801335d6fdc7ae1eb22072055417a96cc6b31b3861f455fbc156", size = 178689, upload-time = "2026-01-10T09:23:10.483Z" }, - { url = "https://files.pythonhosted.org/packages/f3/1d/e88022630271f5bd349ed82417136281931e558d628dd52c4d8621b4a0b2/websockets-16.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8cc451a50f2aee53042ac52d2d053d08bf89bcb31ae799cb4487587661c038a0", size = 177406, upload-time = "2026-01-10T09:23:12.178Z" }, - { url = "https://files.pythonhosted.org/packages/f2/78/e63be1bf0724eeb4616efb1ae1c9044f7c3953b7957799abb5915bffd38e/websockets-16.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:daa3b6ff70a9241cf6c7fc9e949d41232d9d7d26fd3522b1ad2b4d62487e9904", size = 175085, upload-time = "2026-01-10T09:23:13.511Z" }, - { url = "https://files.pythonhosted.org/packages/bb/f4/d3c9220d818ee955ae390cf319a7c7a467beceb24f05ee7aaaa2414345ba/websockets-16.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:fd3cb4adb94a2a6e2b7c0d8d05cb94e6f1c81a0cf9dc2694fb65c7e8d94c42e4", size = 175328, upload-time = "2026-01-10T09:23:14.727Z" }, - { url = "https://files.pythonhosted.org/packages/63/bc/d3e208028de777087e6fb2b122051a6ff7bbcca0d6df9d9c2bf1dd869ae9/websockets-16.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:781caf5e8eee67f663126490c2f96f40906594cb86b408a703630f95550a8c3e", size = 185044, upload-time = "2026-01-10T09:23:15.939Z" }, - { url = "https://files.pythonhosted.org/packages/ad/6e/9a0927ac24bd33a0a9af834d89e0abc7cfd8e13bed17a86407a66773cc0e/websockets-16.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:caab51a72c51973ca21fa8a18bd8165e1a0183f1ac7066a182ff27107b71e1a4", size = 186279, upload-time = "2026-01-10T09:23:17.148Z" }, - { url = "https://files.pythonhosted.org/packages/b9/ca/bf1c68440d7a868180e11be653c85959502efd3a709323230314fda6e0b3/websockets-16.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:19c4dc84098e523fd63711e563077d39e90ec6702aff4b5d9e344a60cb3c0cb1", size = 185711, upload-time = "2026-01-10T09:23:18.372Z" }, - { url = "https://files.pythonhosted.org/packages/c4/f8/fdc34643a989561f217bb477cbc47a3a07212cbda91c0e4389c43c296ebf/websockets-16.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:a5e18a238a2b2249c9a9235466b90e96ae4795672598a58772dd806edc7ac6d3", size = 184982, upload-time = "2026-01-10T09:23:19.652Z" }, - { url = "https://files.pythonhosted.org/packages/dd/d1/574fa27e233764dbac9c52730d63fcf2823b16f0856b3329fc6268d6ae4f/websockets-16.0-cp314-cp314-win32.whl", hash = "sha256:a069d734c4a043182729edd3e9f247c3b2a4035415a9172fd0f1b71658a320a8", size = 177915, upload-time = "2026-01-10T09:23:21.458Z" }, - { url = "https://files.pythonhosted.org/packages/8a/f1/ae6b937bf3126b5134ce1f482365fde31a357c784ac51852978768b5eff4/websockets-16.0-cp314-cp314-win_amd64.whl", hash = "sha256:c0ee0e63f23914732c6d7e0cce24915c48f3f1512ec1d079ed01fc629dab269d", size = 178381, upload-time = "2026-01-10T09:23:22.715Z" }, - { url = "https://files.pythonhosted.org/packages/06/9b/f791d1db48403e1f0a27577a6beb37afae94254a8c6f08be4a23e4930bc0/websockets-16.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:a35539cacc3febb22b8f4d4a99cc79b104226a756aa7400adc722e83b0d03244", size = 177737, upload-time = "2026-01-10T09:23:24.523Z" }, - { url = "https://files.pythonhosted.org/packages/bd/40/53ad02341fa33b3ce489023f635367a4ac98b73570102ad2cdd770dacc9a/websockets-16.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:b784ca5de850f4ce93ec85d3269d24d4c82f22b7212023c974c401d4980ebc5e", size = 175268, upload-time = "2026-01-10T09:23:25.781Z" }, - { url = "https://files.pythonhosted.org/packages/74/9b/6158d4e459b984f949dcbbb0c5d270154c7618e11c01029b9bbd1bb4c4f9/websockets-16.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:569d01a4e7fba956c5ae4fc988f0d4e187900f5497ce46339c996dbf24f17641", size = 175486, upload-time = "2026-01-10T09:23:27.033Z" }, - { url = "https://files.pythonhosted.org/packages/e5/2d/7583b30208b639c8090206f95073646c2c9ffd66f44df967981a64f849ad/websockets-16.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:50f23cdd8343b984957e4077839841146f67a3d31ab0d00e6b824e74c5b2f6e8", size = 185331, upload-time = "2026-01-10T09:23:28.259Z" }, - { url = "https://files.pythonhosted.org/packages/45/b0/cce3784eb519b7b5ad680d14b9673a31ab8dcb7aad8b64d81709d2430aa8/websockets-16.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:152284a83a00c59b759697b7f9e9cddf4e3c7861dd0d964b472b70f78f89e80e", size = 186501, upload-time = "2026-01-10T09:23:29.449Z" }, - { url = "https://files.pythonhosted.org/packages/19/60/b8ebe4c7e89fb5f6cdf080623c9d92789a53636950f7abacfc33fe2b3135/websockets-16.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:bc59589ab64b0022385f429b94697348a6a234e8ce22544e3681b2e9331b5944", size = 186062, upload-time = "2026-01-10T09:23:31.368Z" }, - { url = "https://files.pythonhosted.org/packages/88/a8/a080593f89b0138b6cba1b28f8df5673b5506f72879322288b031337c0b8/websockets-16.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:32da954ffa2814258030e5a57bc73a3635463238e797c7375dc8091327434206", size = 185356, upload-time = "2026-01-10T09:23:32.627Z" }, - { url = "https://files.pythonhosted.org/packages/c2/b6/b9afed2afadddaf5ebb2afa801abf4b0868f42f8539bfe4b071b5266c9fe/websockets-16.0-cp314-cp314t-win32.whl", hash = "sha256:5a4b4cc550cb665dd8a47f868c8d04c8230f857363ad3c9caf7a0c3bf8c61ca6", size = 178085, upload-time = "2026-01-10T09:23:33.816Z" }, - { url = "https://files.pythonhosted.org/packages/9f/3e/28135a24e384493fa804216b79a6a6759a38cc4ff59118787b9fb693df93/websockets-16.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b14dc141ed6d2dde437cddb216004bcac6a1df0935d79656387bd41632ba0bbd", size = 178531, upload-time = "2026-01-10T09:23:35.016Z" }, - { url = "https://files.pythonhosted.org/packages/72/07/c98a68571dcf256e74f1f816b8cc5eae6eb2d3d5cfa44d37f801619d9166/websockets-16.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:349f83cd6c9a415428ee1005cadb5c2c56f4389bc06a9af16103c3bc3dcc8b7d", size = 174947, upload-time = "2026-01-10T09:23:36.166Z" }, - { url = "https://files.pythonhosted.org/packages/7e/52/93e166a81e0305b33fe416338be92ae863563fe7bce446b0f687b9df5aea/websockets-16.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:4a1aba3340a8dca8db6eb5a7986157f52eb9e436b74813764241981ca4888f03", size = 175260, upload-time = "2026-01-10T09:23:37.409Z" }, - { url = "https://files.pythonhosted.org/packages/56/0c/2dbf513bafd24889d33de2ff0368190a0e69f37bcfa19009ef819fe4d507/websockets-16.0-pp311-pypy311_pp73-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f4a32d1bd841d4bcbffdcb3d2ce50c09c3909fbead375ab28d0181af89fd04da", size = 176071, upload-time = "2026-01-10T09:23:39.158Z" }, - { url = "https://files.pythonhosted.org/packages/a5/8f/aea9c71cc92bf9b6cc0f7f70df8f0b420636b6c96ef4feee1e16f80f75dd/websockets-16.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0298d07ee155e2e9fda5be8a9042200dd2e3bb0b8a38482156576f863a9d457c", size = 176968, upload-time = "2026-01-10T09:23:41.031Z" }, - { url = "https://files.pythonhosted.org/packages/9a/3f/f70e03f40ffc9a30d817eef7da1be72ee4956ba8d7255c399a01b135902a/websockets-16.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:a653aea902e0324b52f1613332ddf50b00c06fdaf7e92624fbf8c77c78fa5767", size = 178735, upload-time = "2026-01-10T09:23:42.259Z" }, - { url = "https://files.pythonhosted.org/packages/6f/28/258ebab549c2bf3e64d2b0217b973467394a9cea8c42f70418ca2c5d0d2e/websockets-16.0-py3-none-any.whl", hash = "sha256:1637db62fad1dc833276dded54215f2c7fa46912301a24bd94d45d46a011ceec", size = 171598, upload-time = "2026-01-10T09:23:45.395Z" }, +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]] @@ -3707,6 +3920,17 @@ wheels = [ ] [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" }, +] httpx = [ { name = "httpx" }, ]