diff --git a/.cargo/config.toml b/.cargo/config.toml index 39a35e3..a67bbc9 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -2,9 +2,4 @@ rustflags = ["-C", "target-cpu=native"] [target.x86_64-unknown-linux-gnu] -rustflags = [ - "-C", - "target-cpu=native", - # (Nightly) Make the current crate share its generic instantiations - "-Zshare-generics=y", -] +rustflags = ["-C", "target-cpu=native"] diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..3e0e929 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,46 @@ +# Project-specific ignores +/.db + +# Generated by Cargo +# will have compiled files and executables +debug +target + +# These are backup files generated by rustfmt +**/*.rs.bk + +# MSVC Windows builds of rustc generate these, which store debugging information +*.pdb + +# Generated by cargo mutants +# Contains mutation testing data +**/mutants.out/ + +# MacOS files +.DS_Store + +# IntelliJ IDEA +.idea + +# Hardhat/Foundry files +cache +cache-hardhat +artifacts +artifacts-hardhat +broadcast + +# Js +node_modules +dist +*.tsbuildinfo +.eslintcache +.rollup.cache + +# Project files +.db + +# WAL artifacts +.journal +*.wal +checkpoint.meta +checkpoint.meta.tmp diff --git a/.editorconfig b/.editorconfig index ad9775b..ba4ca1a 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,7 +1,10 @@ -[*.{js,jsx,ts,tsx,css,scss,html,json,md}] +[*.{js,jsx,ts,tsx,css,scss,html,json,md,sql}] charset = utf-8 insert_final_newline = true end_of_line = lf indent_style = space indent_size = 2 -max_line_length = 80 \ No newline at end of file +max_line_length = 80 + +[*.md] +trim_trailing_whitespace = false diff --git a/.env b/.env index ebc5ed4..d560587 100644 --- a/.env +++ b/.env @@ -1 +1,2 @@ FOUNDRY_OUT="target/foundry" +SQLX_OFFLINE=true diff --git a/.env.test b/.env.test new file mode 100644 index 0000000..1b7dc0e --- /dev/null +++ b/.env.test @@ -0,0 +1,16 @@ +# All addresses here are for Scroll Sepolia testnet deployment. +MASTER_ADDRESS=0x96B7c62f1FC932466264C19376263C980848b760 + +L1_MESSENGER=0x50c7d3e7f7c656493D1D76aaa1a836CedfCBB16A +L2_MESSENGER=0xBa50f5340FB9F3Bd074bD638c9BE13eCB36E603d +EAS_SCROLL=0xaEF4103A04090071165F78D45D83A0C0782c2B2a # scroll sepolia +EAS_MAINNET=0xC2679fBD37d54388Ce493F1DB75320D236e1815e # sepolia + +FEE_ORACLE=0x79bd2bb088b4E75AB26869cbEc1571019aBa06C0 +NFT_GENERATOR=0x8F1be71d553cEff8DEf08Af49214896e6D976e74 + +ANCHORING_MANAGER_IMPL=0x5a5FBf844CBDD1aB6CbAb9151a8C1d855e296A94 +ANCHORING_MANAGER=0x7249802D50CB719547F7685d090d4b84e7183C32 + +ANCHORING_GATEWAY_IMPL=0xf8dF10964a6E583560A71f5aB10d85cF9497F30d +ANCHORING_GATEWAY=0x39Ea3C70A5e26C8dB4a1A5E9A7Fc7Bab04684C08 \ No newline at end of file diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..2c56297 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,10 @@ +# These are supported funding model platforms +# See https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/displaying-a-sponsor-button-in-your-repository + +# You can support the UTS project by sending ETH directly to the operator +# address displayed on the calendar or relayer home page: +# Calendar: https://lgm1.calendar.test.timestamps.now/ +# Relayer: https://lich.relayer.test.timestamps.now/ +custom: + - https://lgm1.calendar.test.timestamps.now/ + - https://lich.relayer.test.timestamps.now/ diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 0000000..b070926 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,82 @@ +name: Bug Report +description: Report a bug or unexpected behavior +labels: ["bug"] +body: + - type: markdown + attributes: + value: | + Thanks for taking the time to report a bug! Please fill out the sections below. + + - type: dropdown + id: component + attributes: + label: Component + description: Which component is affected? + options: + - uts-cli + - uts-core + - uts-bmt + - uts-calendar + - uts-relayer + - uts-stamper + - TypeScript SDK + - Python SDK + - Go SDK + - Smart Contracts + - Documentation + - Other + validations: + required: true + + - type: textarea + id: description + attributes: + label: Description + description: A clear and concise description of the bug. + placeholder: Describe the bug... + validations: + required: true + + - type: textarea + id: reproduce + attributes: + label: Steps to Reproduce + description: Steps to reproduce the behavior. + placeholder: | + 1. Run `uts stamp ...` + 2. Observe error... + validations: + required: true + + - type: textarea + id: expected + attributes: + label: Expected Behavior + description: What you expected to happen. + validations: + required: true + + - type: textarea + id: actual + attributes: + label: Actual Behavior + description: What actually happened. Include error messages or logs if applicable. + validations: + required: true + + - type: textarea + id: environment + attributes: + label: Environment + description: Any relevant environment information. + placeholder: | + - OS: Ubuntu 24.04 + - Rust: 1.94.0-nightly + - uts-cli version: 0.1.0-alpha.0 + render: markdown + + - type: textarea + id: additional + attributes: + label: Additional Context + description: Any other context, screenshots, or logs. diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..db716df --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,5 @@ +blank_issues_enabled: true +contact_links: + - name: Documentation + url: https://book.timestamps.now/ + about: Read the UTS documentation diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml new file mode 100644 index 0000000..290e307 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -0,0 +1,58 @@ +name: Feature Request +description: Suggest a new feature or improvement +labels: ["enhancement"] +body: + - type: markdown + attributes: + value: | + Thanks for suggesting a feature! Please describe your idea below. + + - type: dropdown + id: component + attributes: + label: Component + description: Which component does this relate to? + options: + - uts-cli + - uts-core + - uts-bmt + - uts-calendar + - uts-relayer + - uts-stamper + - TypeScript SDK + - Python SDK + - Go SDK + - Smart Contracts + - Documentation + - Other + validations: + required: true + + - type: textarea + id: problem + attributes: + label: Problem Statement + description: What problem does this feature solve? Is it related to a frustration? + placeholder: I'm always frustrated when... + validations: + required: true + + - type: textarea + id: solution + attributes: + label: Proposed Solution + description: Describe the solution you'd like. + validations: + required: true + + - type: textarea + id: alternatives + attributes: + label: Alternatives Considered + description: Any alternative solutions or features you've considered. + + - type: textarea + id: additional + attributes: + label: Additional Context + description: Any other context, mockups, or references. diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..1f13f5b --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,30 @@ +## Description + + + +## Related Issues + + + +## Changes + + + +- + +## Type of Change + +- [ ] Bug fix (non-breaking change that fixes an issue) +- [ ] New feature (non-breaking change that adds functionality) +- [ ] Breaking change (fix or feature that would cause existing functionality to change) +- [ ] Documentation update +- [ ] Refactoring (no functional changes) +- [ ] CI/CD or tooling changes + +## Checklist + +- [ ] My code follows the project's style guidelines +- [ ] I have performed a self-review of my changes +- [ ] I have added tests that prove my fix/feature works +- [ ] Existing tests pass locally with my changes +- [ ] I have updated documentation as needed diff --git a/.github/workflows/ci-contracts.yml b/.github/workflows/ci-contracts.yml index afd2ef4..9533c1e 100644 --- a/.github/workflows/ci-contracts.yml +++ b/.github/workflows/ci-contracts.yml @@ -19,7 +19,7 @@ concurrency: jobs: check: - name: Build & Test + name: Solidity Build & Test runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/ci-python.yml b/.github/workflows/ci-python.yml index 89620c7..7e4420f 100644 --- a/.github/workflows/ci-python.yml +++ b/.github/workflows/ci-python.yml @@ -15,7 +15,7 @@ concurrency: jobs: check: - name: Lint, Format & Test + name: Python Lint, Format & Test runs-on: ubuntu-latest defaults: run: diff --git a/.github/workflows/ci-rust.yml b/.github/workflows/ci-rust.yml index aec39f2..00079a9 100644 --- a/.github/workflows/ci-rust.yml +++ b/.github/workflows/ci-rust.yml @@ -24,7 +24,7 @@ env: jobs: fmt: - name: Formatting + name: Rust Formatting runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 @@ -46,8 +46,9 @@ jobs: run: taplo fmt --check clippy: - name: Clippy + name: Rust Clippy runs-on: ubuntu-latest + needs: [fmt] steps: - uses: actions/checkout@v4 @@ -64,8 +65,9 @@ jobs: - run: cargo clippy --workspace --all-targets -- -D warnings test: - name: Tests + name: Rust Tests runs-on: ubuntu-latest + needs: [clippy] steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/ci-typescript.yml b/.github/workflows/ci-typescript.yml index 299efe7..c855083 100644 --- a/.github/workflows/ci-typescript.yml +++ b/.github/workflows/ci-typescript.yml @@ -23,7 +23,7 @@ concurrency: jobs: check: - name: Lint, Typecheck & Test + name: TypeScript Lint, Typecheck & Test runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/publish-book.yml b/.github/workflows/publish-book.yml index ce23d55..3ee8ff9 100644 --- a/.github/workflows/publish-book.yml +++ b/.github/workflows/publish-book.yml @@ -6,7 +6,7 @@ on: workflow_dispatch: jobs: - deploy: + deploy-book: runs-on: ubuntu-latest permissions: contents: read diff --git a/.github/workflows/publish-docs.yml b/.github/workflows/publish-docs.yml index 3f0efa1..d7856a8 100644 --- a/.github/workflows/publish-docs.yml +++ b/.github/workflows/publish-docs.yml @@ -6,7 +6,7 @@ on: workflow_dispatch: jobs: - ci: + deploy-docs: runs-on: ubuntu-latest permissions: contents: read diff --git a/.gitignore b/.gitignore index 3e0e929..5c1fc23 100644 --- a/.gitignore +++ b/.gitignore @@ -44,3 +44,6 @@ dist *.wal checkpoint.meta checkpoint.meta.tmp + +# Book build +book/build diff --git a/.gitmodules b/.gitmodules index 23acfb1..8449479 100644 --- a/.gitmodules +++ b/.gitmodules @@ -7,3 +7,12 @@ [submodule "lib/openzeppelin-contracts-upgradeable"] path = lib/openzeppelin-contracts-upgradeable url = https://github.com/OpenZeppelin/openzeppelin-contracts-upgradeable +[submodule "lib/scroll-contracts"] + path = lib/scroll-contracts + url = https://github.com/scroll-tech/scroll-contracts +[submodule "lib/eas-contracts"] + path = lib/eas-contracts + url = https://github.com/ethereum-attestation-service/eas-contracts +[submodule "lib/solady"] + path = lib/solady + url = https://github.com/Vectorized/solady diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 04e49ab..166a277 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -23,4 +23,11 @@ repos: entry: npx prettier --write language: system files: \.(js|jsx|ts|tsx|json|css|scss|md|html|toml)$ + exclude: \.sqlx/.*\.json$ + pass_filenames: true + - id: black + name: black + entry: black + language: system + types: [python] pass_filenames: true diff --git a/.prettierignore b/.prettierignore index 0b79f98..664d87d 100644 --- a/.prettierignore +++ b/.prettierignore @@ -5,4 +5,8 @@ pnpm-lock.yaml crates/* lib/* -target/* \ No newline at end of file +target/* + +**/.sqlx/*.json + +apps/docs/content/**/*.md diff --git a/.vscode/settings.json b/.vscode/settings.json index 91e9922..0e267e1 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,5 +1,7 @@ { "solidity.packageDefaultDependenciesContractsDirectory": "contracts", "solidity.packageDefaultDependenciesDirectory": "lib", - "solidity.exclude": ["lib/**"] + "solidity.exclude": ["lib/**"], + "python-envs.defaultEnvManager": "ms-python.python:poetry", + "python-envs.defaultPackageManager": "ms-python.python:poetry" } diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..0cda246 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,120 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Common Development Commands + +### Rust Backend + +- **Build all crates**: `cargo build` +- **Run tests**: `cargo test` +- **Run specific test**: `cargo test test_name` +- **Install CLI**: `cargo install --path crates/cli` +- **Check formatting**: `cargo fmt --check` +- **Format code**: `cargo fmt` + +### TypeScript/JavaScript Frontend & SDK + +- **Install dependencies**: `pnpm install` +- **Build all packages**: `pnpm run build` +- **Run all tests**: `pnpm run test` +- **Lint code**: `pnpm run lint` +- **Type check**: `pnpm run typecheck` +- **Format code**: `pnpm run format` + +### Smart Contracts (Foundry) + +- **Compile contracts**: `forge build` +- **Run contract tests**: `forge test` +- **Run specific test**: `forge test --match-test test_name` +- **Deploy scripts**: `forge script ` + +### Monorepo Commands + +- **Build entire monorepo**: `pnpm run build` (builds all TypeScript packages) +- **Test entire monorepo**: `pnpm run test` (runs all TypeScript tests) +- **Lint entire monorepo**: `pnpm run lint` + +## Ground Truth Reference + +**IMPORTANT**: The Rust codebase (`crates/`) is the authoritative reference implementation. When implementing features in TypeScript, Python, Go, or other languages: + +1. **Always check the Rust implementation first** - `crates/core/` for types and codec, `crates/bmt/` for Merkle tree +2. **TypeScript and Python SDKs may be outdated** - They exist for reference but may not reflect current Rust implementation +3. **When in doubt, trust Rust** - Binary format, attestation types, error codes, and algorithm details should match Rust exactly + +### Key Rust Modules to Reference + +- **Types & Codec**: `crates/core/src/codec/v1/` - All timestamp and attestation types +- **Attestations**: `crates/core/src/codec/v1/attestation.rs` - All attestation type definitions +- **Errors**: `crates/core/src/error.rs` - Error codes and types +- **Merkle Tree**: `crates/bmt/src/lib.rs` - Binary Merkle Tree implementation +- **Verifier**: `crates/core/src/verifier/` - Attestation verification logic + +## High-Level Architecture + +The Universal Timestamps (UTS) project is a multi-language monorepo implementing a decentralized timestamping protocol that extends OpenTimestamps with Ethereum Attestation Service (EAS) integration. + +### Core Components + +#### Rust Backend Services (`crates/`) + +- **`uts-bmt`**: Binary Merkle Tree implementation with flat-array, power-of-two structure +- **`uts-core`**: Core types, OTS codec, and verification logic +- **`uts-journal`**: RocksDB-backed write-ahead log with at-least-once delivery +- **`uts-calendar`**: HTTP calendar server for digest submission and proof serving +- **`uts-stamper`**: Batching engine that builds Merkle trees and submits EAS attestations +- **`uts-cli`**: Command-line interface for stamping, verifying, and inspecting timestamps +- **`uts-contracts`**: Rust bindings for EAS and L2AnchoringManager contracts +- **`uts-relayer`**: L2→L1→L2 relay service for cross-chain anchoring +- **`uts-beacon-injector`**: Injects drand beacon randomness into timestamping pipeline + +#### TypeScript Packages (`packages/`) + +- **`@uts/sdk`**: TypeScript/JavaScript SDK for client-side interaction with UTS protocol +- **`@uts/contracts`**: Contract type definitions and ABIs for TypeScript projects + +#### Web Application (`apps/web`) + +- Vue 3 frontend application with TypeScript +- Uses Pinia for state management and Tailwind CSS for styling +- Integrates with `@uts/sdk` for timestamping functionality + +#### Smart Contracts (`contracts/`) + +- **L1 contracts**: `L1AnchoringGateway.sol` for anchoring L2 roots on Ethereum +- **L2 contracts**: `L2AnchoringManager.sol` for managing L2 anchoring process +- **Core utilities**: `EASHelper.sol`, `MerkleTree.sol` +- Built on Foundry with EAS and OpenZeppelin integrations + +### Dual-Layer Architecture + +The system implements two complementary timestamping pipelines: + +1. **L2 Direct Path (Fast)**: User digests are batched into Merkle trees and timestamped directly on L2 (Scroll) via EAS attestations, providing low-latency, low-cost timestamps. + +2. **L1 Anchoring Path (Secure)**: L2 attestation roots are batched again and anchored on L1 Ethereum through a relayer service, providing high-security guarantees with finality on the main chain. + +### Technology Stack + +- **Backend**: Rust with Tokio async runtime, Axum web framework, RocksDB storage +- **Blockchain**: Ethereum ecosystem with EAS (Ethereum Attestation Service), Foundry toolchain +- **Frontend**: Vue 3 with TypeScript, Pinia, Tailwind CSS +- **Database**: SQLite with SQLx for database interactions +- **Package Management**: pnpm workspaces for JavaScript/TypeScript monorepo + +### Development Environment Requirements + +- **Rust**: >= 1.94.0-nightly (e7d44143a 2025-12-24) +- **Cargo**: >= 1.94.0-nightly (3861f60f6 2025-12-19) +- **pnpm**: >= 10.26.2 +- **Foundry**: Latest version for smart contract development + +### Documentation + +Comprehensive documentation is available in the `book/` directory using mdBook format, covering: + +- System architecture and component diagrams +- Core primitives and data structures +- Both timestamping pipelines (calendar and L1 anchoring) +- Storage architecture and security considerations diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..fe4cdf2 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,133 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, caste, color, religion, or sexual +identity and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment for our +community include: + +* Demonstrating empathy and kindness toward other people +* Being respectful of differing opinions, viewpoints, and experiences +* Giving and gracefully accepting constructive feedback +* Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +* Focusing on what is best not just for us as individuals, but for the overall + community + +Examples of unacceptable behavior include: + +* The use of sexualized language or imagery, and sexual attention or advances of + any kind +* Trolling, insulting or derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or email address, + without their explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. + +Community leaders have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, and will communicate reasons for moderation +decisions when appropriate. + +## Scope + +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. +Examples of representing our community include using an official email address, +posting via an official social media account, or acting as an appointed +representative at an online or offline event. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported to the community leaders responsible for enforcement at +**light.tsing@gmail.com**. + +All complaints will be reviewed and investigated promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the +reporter of any incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining +the consequences for any action they deem in violation of this Code of Conduct: + +### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed +unprofessional or unwelcome in the community. + +**Consequence**: A private, written warning from community leaders, providing +clarity around the nature of the violation and an explanation of why the +behavior was inappropriate. A public apology may be requested. + +### 2. Warning + +**Community Impact**: A violation through a single incident or series of +actions. + +**Consequence**: A warning with consequences for continued behavior. No +interaction with the people involved, including unsolicited interaction with +those enforcing the Code of Conduct, for a specified period of time. This +includes avoiding interactions in community spaces as well as external channels +like social media. Violating these terms may lead to a temporary or permanent +ban. + +### 3. Temporary Ban + +**Community Impact**: A serious violation of community standards, including +sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public +communication with the community for a specified period of time. No public or +private interaction with the people involved, including unsolicited interaction +with those enforcing the Code of Conduct, is allowed during this period. +Violating these terms may lead to a permanent ban. + +### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community +standards, including sustained inappropriate behavior, harassment of an +individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within the +community. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 2.1, available at +[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1]. + +Community Impact Guidelines were inspired by +[Mozilla's code of conduct enforcement ladder][Mozilla CoC]. + +For answers to common questions about this code of conduct, see the FAQ at +[https://www.contributor-covenant.org/faq][FAQ]. Translations are available at +[https://www.contributor-covenant.org/translations][translations]. + +[homepage]: https://www.contributor-covenant.org +[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html +[Mozilla CoC]: https://github.com/mozilla/diversity +[FAQ]: https://www.contributor-covenant.org/faq +[translations]: https://www.contributor-covenant.org/translations diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..859a25c --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,116 @@ +# Contributing to UTS + +Thank you for your interest in contributing to Universal Timestamps! This guide +will help you get started. + +## Code of Conduct + +This project adheres to the [Contributor Covenant Code of Conduct](CODE_OF_CONDUCT.md). +By participating, you are expected to uphold this code. Please report +unacceptable behavior to **light.tsing@gmail.com**. + +## How to Contribute + +### Reporting Bugs + +- Check the [existing issues](https://github.com/lightsing/uts/issues) to avoid + duplicates. +- Use the [bug report template](https://github.com/lightsing/uts/issues/new?template=bug_report.yml) + and fill in as much detail as possible. + +### Suggesting Features + +- Open a [feature request](https://github.com/lightsing/uts/issues/new?template=feature_request.yml) + describing the problem you want to solve and your proposed solution. + +### Submitting Pull Requests + +1. **Fork** the repository and create a branch from `main`. +2. **Make your changes** — keep PRs focused on a single concern. +3. **Add or update tests** for any changed functionality. +4. **Ensure all checks pass** (see [Development Setup](#development-setup)). +5. **Open a pull request** with a clear description of what and why. + +## Development Setup + +### Prerequisites + +- **Rust** >= 1.94.0-nightly (e7d44143a 2025-12-24) +- **Cargo** >= 1.94.0-nightly (3861f60f6 2025-12-19) +- **pnpm** >= 10.26.2 +- **Foundry** (for smart contract development) + +### Building + +```bash +# Rust crates +cargo build + +# TypeScript packages +pnpm install +pnpm run build +``` + +### Testing + +```bash +# Rust tests +cargo test + +# TypeScript tests +pnpm run test + +# Smart contract tests +forge test +``` + +### Linting & Formatting + +```bash +# Rust +cargo fmt --check +cargo clippy --all-targets + +# TypeScript +pnpm run lint +pnpm run format:check +``` + +## Project Structure + +| Directory | Description | +| --- | --- | +| `crates/` | Rust crates (core library, servers, CLI) | +| `packages/` | TypeScript, Python, and Go SDKs | +| `contracts/` | Solidity smart contracts (Foundry) | +| `apps/web/` | Vue 3 web frontend | +| `book/` | Documentation (mdBook) | + +### Key Crates + +- **`uts-core`** — Core types, OTS codec, and verification logic +- **`uts-bmt`** — Binary Merkle Tree implementation +- **`uts-calendar`** — HTTP calendar server for digest submission +- **`uts-stamper`** — Batching engine for Merkle trees and EAS attestations +- **`uts-relayer`** — L2→L1→L2 relay service for cross-chain anchoring +- **`uts-cli`** — Command-line interface + +### Ground Truth + +The **Rust codebase** (`crates/`) is the authoritative reference implementation. +When implementing features in other languages, always check the Rust +implementation first. + +## Licensing + +Most of the project is dual-licensed under **MIT OR Apache-2.0**. The server +components (`uts-calendar` and `uts-relayer`) are licensed under **AGPL-3.0**. + +By submitting a contribution, you agree that your contribution will be licensed +under the same terms as the component you are contributing to. + +## Getting Help + +- **Documentation**: [book.timestamps.now](https://book.timestamps.now/) +- **Issues**: [GitHub Issues](https://github.com/lightsing/uts/issues) +- **Discussions**: Use GitHub Issues for questions and discussions diff --git a/Cargo.lock b/Cargo.lock index 2b3cacc..2621083 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -17,16 +17,6 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" -[[package]] -name = "aead" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" -dependencies = [ - "crypto-common 0.1.7", - "generic-array", -] - [[package]] name = "aho-corasick" version = "1.1.4" @@ -63,43 +53,16 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c880a97d28a3681c0267bd29cff89621202715b065127cd445fa0f0fe0aa2880" -[[package]] -name = "alloy" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f609fb6392508278b276906d6247ea44f5777e448db95444fa39e89b7aee896a" -dependencies = [ - "alloy-consensus", - "alloy-contract", - "alloy-core", - "alloy-eips", - "alloy-genesis", - "alloy-network", - "alloy-node-bindings", - "alloy-provider", - "alloy-pubsub", - "alloy-rpc-client", - "alloy-rpc-types", - "alloy-serde", - "alloy-signer", - "alloy-signer-local", - "alloy-transport", - "alloy-transport-http", - "alloy-transport-ipc", - "alloy-transport-ws", - "alloy-trie", -] - [[package]] name = "alloy-chains" -version = "0.2.24" +version = "0.2.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b163ff4acf0eac29af05a911397cc418a76e153467b859398adc26cb9335a611" +checksum = "6d9d22005bf31b018f31ef9ecadb5d2c39cf4f6acc8db0456f72c815f3d7f757" dependencies = [ "alloy-primitives", "num_enum", "serde", - "strum", + "strum 0.27.2", ] [[package]] @@ -126,7 +89,7 @@ dependencies = [ "serde", "serde_json", "serde_with", - "thiserror 2.0.17", + "thiserror 2.0.18", ] [[package]] @@ -145,9 +108,9 @@ dependencies = [ [[package]] name = "alloy-contract" -version = "1.2.1" +version = "1.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f01b6d8e5b4f3222aaf7f18613a7292e2fbc9163fe120649cd1b078ca534349" +checksum = "ca63b7125a981415898ffe2a2a696c83696c9c6bdb1671c8a912946bbd8e49e7" dependencies = [ "alloy-consensus", "alloy-dyn-abi", @@ -163,27 +126,14 @@ dependencies = [ "futures", "futures-util", "serde_json", - "thiserror 2.0.17", -] - -[[package]] -name = "alloy-core" -version = "1.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d4087016b0896051dd3d03e0bedda2f4d4d1689af8addc8450288c63a9e5f68" -dependencies = [ - "alloy-dyn-abi", - "alloy-json-abi", - "alloy-primitives", - "alloy-rlp", - "alloy-sol-types", + "thiserror 2.0.18", ] [[package]] name = "alloy-dyn-abi" -version = "1.5.2" +version = "1.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "369f5707b958927176265e8a58627fc6195e5dfa5c55689396e68b241b3a72e6" +checksum = "cc2db5c583aaef0255aa63a4fe827f826090142528bba48d1bf4119b62780cad" dependencies = [ "alloy-json-abi", "alloy-primitives", @@ -205,7 +155,7 @@ dependencies = [ "alloy-rlp", "crc", "serde", - "thiserror 2.0.17", + "thiserror 2.0.18", ] [[package]] @@ -229,16 +179,15 @@ dependencies = [ "alloy-primitives", "alloy-rlp", "borsh", - "k256", "serde", - "thiserror 2.0.17", + "thiserror 2.0.18", ] [[package]] name = "alloy-eip7928" -version = "0.3.2" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3231de68d5d6e75332b7489cfcc7f4dfabeba94d990a10e4b923af0e6623540" +checksum = "f8222b1d88f9a6d03be84b0f5e76bb60cd83991b43ad8ab6477f0e4a7809b98d" dependencies = [ "alloy-primitives", "alloy-rlp", @@ -266,43 +215,15 @@ dependencies = [ "either", "serde", "serde_with", - "sha2 0.10.9", - "thiserror 2.0.17", -] - -[[package]] -name = "alloy-genesis" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe3192fca2eb0b0c4b122b3c2d8254496b88a4e810558dddd3ea2f30ad9469df" -dependencies = [ - "alloy-eips", - "alloy-primitives", - "alloy-serde", - "alloy-trie", - "borsh", - "serde", - "serde_with", -] - -[[package]] -name = "alloy-hardforks" -version = "0.2.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3165210652f71dfc094b051602bafd691f506c54050a174b1cba18fb5ef706a3" -dependencies = [ - "alloy-chains", - "alloy-eip2124", - "alloy-primitives", - "auto_impl", - "dyn-clone", + "sha2", + "thiserror 2.0.18", ] [[package]] name = "alloy-json-abi" -version = "1.5.2" +version = "1.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84e3cf01219c966f95a460c95f1d4c30e12f6c18150c21a30b768af2a2a29142" +checksum = "e9dbe713da0c737d9e5e387b0ba790eb98b14dd207fe53eef50e19a5a8ec3dac" dependencies = [ "alloy-primitives", "alloy-sol-type-parser", @@ -312,24 +233,24 @@ dependencies = [ [[package]] name = "alloy-json-rpc" -version = "1.2.1" +version = "1.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4ab3330e491053e9608b2a315f147357bb8acb9377a988c1203f2e8e2b296c9" +checksum = "ff42cd777eea61f370c0b10f2648a1c81e0b783066cd7269228aa993afd487f7" dependencies = [ "alloy-primitives", "alloy-sol-types", "http", "serde", "serde_json", - "thiserror 2.0.17", + "thiserror 2.0.18", "tracing", ] [[package]] name = "alloy-network" -version = "1.2.1" +version = "1.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1e22ff194b1e34b4defd1e257e3fe4dce0eee37451c7757a1510d6b23e7379a" +checksum = "8cbca04f9b410fdc51aaaf88433cbac761213905a65fe832058bcf6690585762" dependencies = [ "alloy-consensus", "alloy-consensus-any", @@ -348,7 +269,7 @@ dependencies = [ "futures-utils-wasm", "serde", "serde_json", - "thiserror 2.0.17", + "thiserror 2.0.18", ] [[package]] @@ -364,32 +285,11 @@ dependencies = [ "serde", ] -[[package]] -name = "alloy-node-bindings" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e9e5dae7d2be44904dba55bb8b538e5de89fdb9e50b3f0f163277b729285011" -dependencies = [ - "alloy-genesis", - "alloy-hardforks", - "alloy-network", - "alloy-primitives", - "alloy-signer", - "alloy-signer-local", - "k256", - "rand 0.8.5", - "serde_json", - "tempfile", - "thiserror 2.0.17", - "tracing", - "url", -] - [[package]] name = "alloy-primitives" -version = "1.5.2" +version = "1.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6a0fb18dd5fb43ec5f0f6a20be1ce0287c79825827de5744afaa6c957737c33" +checksum = "de3b431b4e72cd8bd0ec7a50b4be18e73dab74de0dba180eef171055e5d5926e" dependencies = [ "alloy-rlp", "bytes", @@ -398,7 +298,7 @@ dependencies = [ "derive_more", "foldhash 0.2.0", "hashbrown 0.16.1", - "indexmap 2.12.1", + "indexmap 2.13.0", "itoa", "k256", "keccak-asm", @@ -409,15 +309,14 @@ dependencies = [ "ruint", "rustc-hash", "serde", - "sha3 0.10.8", - "tiny-keccak", + "sha3", ] [[package]] name = "alloy-provider" -version = "1.2.1" +version = "1.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f5dde1abc3d582e53d139904fcdd8b2103f0bd03e8f2acb4292edbbaeaa7e6e" +checksum = "d181c8cc7cf4805d7e589bf4074d56d55064fa1a979f005a45a62b047616d870" dependencies = [ "alloy-chains", "alloy-consensus", @@ -425,25 +324,19 @@ dependencies = [ "alloy-json-rpc", "alloy-network", "alloy-network-primitives", - "alloy-node-bindings", "alloy-primitives", "alloy-pubsub", "alloy-rpc-client", - "alloy-rpc-types-anvil", - "alloy-rpc-types-debug", "alloy-rpc-types-eth", - "alloy-rpc-types-trace", - "alloy-rpc-types-txpool", "alloy-signer", "alloy-sol-types", "alloy-transport", "alloy-transport-http", - "alloy-transport-ipc", "alloy-transport-ws", "async-stream", "async-trait", "auto_impl", - "dashmap 6.1.0", + "dashmap", "either", "futures", "futures-utils-wasm", @@ -453,7 +346,7 @@ dependencies = [ "reqwest 0.12.28", "serde", "serde_json", - "thiserror 2.0.17", + "thiserror 2.0.18", "tokio", "tracing", "url", @@ -462,9 +355,9 @@ dependencies = [ [[package]] name = "alloy-pubsub" -version = "1.2.1" +version = "1.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "acbfe0a3c553a027f722185fb574124d205147fffb309cae52d0a2094f076887" +checksum = "e8bd82953194dec221aa4cbbbb0b1e2df46066fe9d0333ac25b43a311e122d13" dependencies = [ "alloy-json-rpc", "alloy-primitives", @@ -484,9 +377,9 @@ dependencies = [ [[package]] name = "alloy-rlp" -version = "0.3.12" +version = "0.3.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f70d83b765fdc080dbcd4f4db70d8d23fe4761f2f02ebfa9146b833900634b4" +checksum = "e93e50f64a77ad9c5470bf2ad0ca02f228da70c792a8f06634801e202579f35e" dependencies = [ "alloy-rlp-derive", "arrayvec", @@ -495,27 +388,26 @@ dependencies = [ [[package]] name = "alloy-rlp-derive" -version = "0.3.12" +version = "0.3.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64b728d511962dda67c1bc7ea7c03736ec275ed2cf4c35d9585298ac9ccf3b73" +checksum = "ce8849c74c9ca0f5a03da1c865e3eb6f768df816e67dd3721a398a8a7e398011" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.117", ] [[package]] name = "alloy-rpc-client" -version = "1.2.1" +version = "1.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a94bdef2710322c6770be08689fee0878c2ad75615b8fc40e05d7f3c9618c0b" +checksum = "f2792758a93ae32a32e9047c843d536e1448044f78422d71bf7d7c05149e103f" dependencies = [ "alloy-json-rpc", "alloy-primitives", "alloy-pubsub", "alloy-transport", "alloy-transport-http", - "alloy-transport-ipc", "alloy-transport-ws", "futures", "pin-project", @@ -530,75 +422,17 @@ dependencies = [ "wasmtimer", ] -[[package]] -name = "alloy-rpc-types" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "811a573c8080e1b492d488e6a240ec5dd7677d7167e91ce9cb4d0ec1fcac8027" -dependencies = [ - "alloy-primitives", - "alloy-rpc-types-anvil", - "alloy-rpc-types-debug", - "alloy-rpc-types-engine", - "alloy-rpc-types-eth", - "alloy-rpc-types-trace", - "alloy-rpc-types-txpool", - "alloy-serde", - "serde", -] - -[[package]] -name = "alloy-rpc-types-anvil" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "838ca94be532a929f27961851000ec8bbbaeb06e2a2bcca44fac7855a2fe0f6f" -dependencies = [ - "alloy-primitives", - "alloy-rpc-types-eth", - "alloy-serde", - "serde", -] - [[package]] name = "alloy-rpc-types-any" -version = "1.2.1" +version = "1.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12df0b34551ca2eab8ec83b56cb709ee5da991737282180d354a659b907f00dc" +checksum = "dd720b63f82b457610f2eaaf1f32edf44efffe03ae25d537632e7d23e7929e1a" dependencies = [ "alloy-consensus-any", "alloy-rpc-types-eth", "alloy-serde", ] -[[package]] -name = "alloy-rpc-types-debug" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c49a3a168a5bf18f1cf7ed5723a650aebe714edf7665b53dacf5707716733d0" -dependencies = [ - "alloy-primitives", - "derive_more", - "serde", - "serde_with", -] - -[[package]] -name = "alloy-rpc-types-engine" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffe16cd1dea6089902ec609e04261a9ae6d11ec66005ba24c1f97f0eefbc0fa9" -dependencies = [ - "alloy-consensus", - "alloy-eips", - "alloy-primitives", - "alloy-rlp", - "alloy-serde", - "derive_more", - "rand 0.8.5", - "serde", - "strum", -] - [[package]] name = "alloy-rpc-types-eth" version = "1.7.3" @@ -617,33 +451,7 @@ dependencies = [ "serde", "serde_json", "serde_with", - "thiserror 2.0.17", -] - -[[package]] -name = "alloy-rpc-types-trace" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cafe859944638c5d57d1a3a0034cdb5d07c98c37de8adce5508f28834acf958f" -dependencies = [ - "alloy-primitives", - "alloy-rpc-types-eth", - "alloy-serde", - "serde", - "serde_json", - "thiserror 2.0.17", -] - -[[package]] -name = "alloy-rpc-types-txpool" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "afaa06544e36f223b99b1415a12911230fd527994f020736c3c7950d5080208e" -dependencies = [ - "alloy-primitives", - "alloy-rpc-types-eth", - "alloy-serde", - "serde", + "thiserror 2.0.18", ] [[package]] @@ -659,9 +467,9 @@ dependencies = [ [[package]] name = "alloy-signer" -version = "1.2.1" +version = "1.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "acff6b251740ef473932386d3b71657d3825daebf2217fb41a7ef676229225d4" +checksum = "2425c6f314522c78e8198979c8cbf6769362be4da381d4152ea8eefce383535d" dependencies = [ "alloy-primitives", "async-trait", @@ -669,14 +477,14 @@ dependencies = [ "either", "elliptic-curve", "k256", - "thiserror 2.0.17", + "thiserror 2.0.18", ] [[package]] name = "alloy-signer-local" -version = "1.2.1" +version = "1.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9129ef31975d987114c27c9930ee817cf3952355834d47f2fdf4596404507e8" +checksum = "c3ecb71ee53d8d9c3fa7bac17542c8116ebc7a9726c91b1bf333ec3d04f5a789" dependencies = [ "alloy-consensus", "alloy-network", @@ -687,66 +495,63 @@ dependencies = [ "coins-bip39", "k256", "rand 0.8.5", - "thiserror 2.0.17", + "thiserror 2.0.18", "zeroize", ] [[package]] name = "alloy-sol-macro" -version = "1.5.2" +version = "1.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09eb18ce0df92b4277291bbaa0ed70545d78b02948df756bbd3d6214bf39a218" +checksum = "ab81bab693da9bb79f7a95b64b394718259fdd7e41dceeced4cad57cb71c4f6a" dependencies = [ "alloy-sol-macro-expander", "alloy-sol-macro-input", "proc-macro-error2", "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.117", ] [[package]] name = "alloy-sol-macro-expander" -version = "1.5.2" +version = "1.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95d9fa2daf21f59aa546d549943f10b5cce1ae59986774019fbedae834ffe01b" +checksum = "489f1620bb7e2483fb5819ed01ab6edc1d2f93939dce35a5695085a1afd1d699" dependencies = [ - "alloy-json-abi", "alloy-sol-macro-input", "const-hex", "heck", - "indexmap 2.12.1", + "indexmap 2.13.0", "proc-macro-error2", "proc-macro2", "quote", - "syn 2.0.111", + "sha3", + "syn 2.0.117", "syn-solidity", - "tiny-keccak", ] [[package]] name = "alloy-sol-macro-input" -version = "1.5.2" +version = "1.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9396007fe69c26ee118a19f4dee1f5d1d6be186ea75b3881adf16d87f8444686" +checksum = "56cef806ad22d4392c5fc83cf8f2089f988eb99c7067b4e0c6f1971fc1cca318" dependencies = [ - "alloy-json-abi", "const-hex", "dunce", "heck", "macro-string", "proc-macro2", "quote", - "serde_json", - "syn 2.0.111", + "syn 2.0.117", "syn-solidity", ] [[package]] name = "alloy-sol-type-parser" -version = "1.5.2" +version = "1.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af67a0b0dcebe14244fc92002cd8d96ecbf65db4639d479f5fcd5805755a4c27" +checksum = "a6df77fea9d6a2a75c0ef8d2acbdfd92286cc599983d3175ccdc170d3433d249" dependencies = [ "serde", "winnow", @@ -754,9 +559,9 @@ dependencies = [ [[package]] name = "alloy-sol-types" -version = "1.5.2" +version = "1.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09aeea64f09a7483bdcd4193634c7e5cf9fd7775ee767585270cd8ce2d69dc95" +checksum = "64612d29379782a5dde6f4b6570d9c756d734d760c0c94c254d361e678a6591f" dependencies = [ "alloy-json-abi", "alloy-primitives", @@ -766,9 +571,9 @@ dependencies = [ [[package]] name = "alloy-transport" -version = "1.2.1" +version = "1.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bec1fb08ee484e615f24867c0b154fff5722bb00176102a16868c6532b7c3623" +checksum = "fa186e560d523d196580c48bf00f1bf62e63041f28ecf276acc22f8b27bb9f53" dependencies = [ "alloy-json-rpc", "auto_impl", @@ -776,10 +581,11 @@ dependencies = [ "derive_more", "futures", "futures-utils-wasm", + "governor", "parking_lot", "serde", "serde_json", - "thiserror 2.0.17", + "thiserror 2.0.18", "tokio", "tower", "tracing", @@ -789,12 +595,13 @@ dependencies = [ [[package]] name = "alloy-transport-http" -version = "1.2.1" +version = "1.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64b722073c76f2de7e118d546ee1921c50710f97feb32aed50db94cfa5b663e1" +checksum = "aa501ad58dd20acddbfebc65b52e60f05ebf97c52fa40d1b35e91f5e2da0ad0e" dependencies = [ "alloy-json-rpc", "alloy-transport", + "itertools 0.14.0", "reqwest 0.12.28", "serde_json", "tower", @@ -802,31 +609,11 @@ dependencies = [ "url", ] -[[package]] -name = "alloy-transport-ipc" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bdedcf401aab4b96d8b5e6638b79d04a6afb96c0bfcb50a2324fbadfe65c47b3" -dependencies = [ - "alloy-json-rpc", - "alloy-pubsub", - "alloy-transport", - "bytes", - "futures", - "interprocess", - "pin-project", - "serde", - "serde_json", - "tokio", - "tokio-util", - "tracing", -] - [[package]] name = "alloy-transport-ws" -version = "1.2.1" +version = "1.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "942210908f0c56941097f5653a5f334546940e6fd9073495b257e52216469feb" +checksum = "b9f00445db69d63298e2b00a0ea1d859f00e6424a3144ffc5eba9c31da995e16" dependencies = [ "alloy-pubsub", "alloy-transport", @@ -841,17 +628,17 @@ dependencies = [ [[package]] name = "alloy-trie" -version = "0.9.2" +version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b77b56af09ead281337d06b1d036c88e2dc8a2e45da512a532476dbee94912b" +checksum = "3f14b5d9b2c2173980202c6ff470d96e7c5e202c65a9f67884ad565226df7fbb" dependencies = [ "alloy-primitives", "alloy-rlp", - "arrayvec", "derive_more", "nybbles", "serde", "smallvec", + "thiserror 2.0.18", "tracing", ] @@ -861,10 +648,10 @@ version = "1.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6fa0c53e8c1e1ef4d01066b01c737fb62fc9397ab52c6e7bb5669f97d281b9bc" dependencies = [ - "darling", + "darling 0.21.3", "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.117", ] [[package]] @@ -884,9 +671,9 @@ checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" [[package]] name = "anstream" -version = "0.6.21" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" dependencies = [ "anstyle", "anstyle-parse", @@ -899,15 +686,15 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.13" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" [[package]] name = "anstyle-parse" -version = "0.2.7" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" dependencies = [ "utf8parse", ] @@ -934,9 +721,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.100" +version = "1.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" [[package]] name = "ark-ff" @@ -1023,7 +810,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "62945a2f7e6de02a31fe400aa489f0e0f5b2502e69f95f853adb82a96c7a6b60" dependencies = [ "quote", - "syn 2.0.111", + "syn 2.0.117", ] [[package]] @@ -1061,7 +848,7 @@ dependencies = [ "num-traits", "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.117", ] [[package]] @@ -1128,31 +915,16 @@ dependencies = [ ] [[package]] -name = "arrayref" -version = "0.3.9" +name = "arraydeque" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" +checksum = "7d902e3d592a523def97af8f317b08ce16b7ab854c1985a0c671e6f15cebc236" [[package]] name = "arrayvec" version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" -dependencies = [ - "serde", - "zeroize", -] - -[[package]] -name = "async-lock" -version = "3.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "290f7f2596bd5b78a9fec8088ccd89180d7f9f55b94b0576823bbbdc72ee8311" -dependencies = [ - "event-listener", - "event-listener-strategy", - "pin-project-lite", -] [[package]] name = "async-stream" @@ -1173,7 +945,7 @@ checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.117", ] [[package]] @@ -1184,7 +956,7 @@ checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.117", ] [[package]] @@ -1198,6 +970,15 @@ dependencies = [ "rustc_version 0.4.1", ] +[[package]] +name = "atoi" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f28d99ec8bfea296261ca1af174f24225171fea9664ba9003cbebee704810528" +dependencies = [ + "num-traits", +] + [[package]] name = "atomic-waker" version = "1.1.2" @@ -1212,7 +993,7 @@ checksum = "ffdcb70bdbc4d478427380519163274ac86e52916e10f0a8889adf0f96d3fee7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.117", ] [[package]] @@ -1223,9 +1004,9 @@ checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] name = "aws-lc-rs" -version = "1.16.0" +version = "1.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9a7b350e3bb1767102698302bc37256cbd48422809984b98d292c40e2579aa9" +checksum = "94bffc006df10ac2a68c83692d734a465f8ee6c5b384d8545a636f81d858f4bf" dependencies = [ "aws-lc-sys", "zeroize", @@ -1233,9 +1014,9 @@ dependencies = [ [[package]] name = "aws-lc-sys" -version = "0.37.1" +version = "0.38.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b092fe214090261288111db7a2b2c2118e5a7f30dc2569f1732c4069a6840549" +checksum = "4321e568ed89bb5a7d291a7f37997c2c0df89809d7b6d12062c81ddb54aa782e" dependencies = [ "cc", "cmake", @@ -1252,7 +1033,6 @@ dependencies = [ "axum-core", "axum-macros", "bytes", - "form_urlencoded", "futures-util", "http", "http-body", @@ -1268,20 +1048,18 @@ dependencies = [ "serde_core", "serde_json", "serde_path_to_error", - "serde_urlencoded", "sync_wrapper", "tokio", "tower", "tower-layer", "tower-service", - "tracing", ] [[package]] name = "axum-core" -version = "0.5.5" +version = "0.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59446ce19cd142f8833f856eb31f3eb097812d1479ab224f54d72428ca21ea22" +checksum = "08c78f31d7b1291f7ee735c1c6780ccde7785daae9a9206026862dab7d8792d1" dependencies = [ "bytes", "futures-core", @@ -1293,7 +1071,6 @@ dependencies = [ "sync_wrapper", "tower-layer", "tower-service", - "tracing", ] [[package]] @@ -1304,7 +1081,18 @@ checksum = "604fde5e028fea851ce1d8570bbdc034bec850d157f7569d10f347d06808c05c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.117", +] + +[[package]] +name = "backon" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cffb0e931875b666fc4fcb20fee52e9bbd1ef836fd9e9e04ec21555f9f85f7ef" +dependencies = [ + "fastrand", + "gloo-timers", + "tokio", ] [[package]] @@ -1342,9 +1130,9 @@ checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] name = "base64ct" -version = "1.8.1" +version = "1.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e050f626429857a27ddccb31e0aca21356bfa709c04041aefddac081a8f068a" +checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" [[package]] name = "bech32" @@ -1373,7 +1161,7 @@ dependencies = [ "regex", "rustc-hash", "shlex", - "syn 2.0.111", + "syn 2.0.117", ] [[package]] @@ -1391,67 +1179,31 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" -[[package]] -name = "bitcode" -version = "0.6.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a6ed1b54d8dc333e7be604d00fa9262f4635485ffea923647b6521a5fff045d" -dependencies = [ - "arrayvec", - "bitcode_derive", - "bytemuck", - "glam", - "serde", -] - -[[package]] -name = "bitcode_derive" -version = "0.6.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "238b90427dfad9da4a9abd60f3ec1cdee6b80454bde49ed37f1781dd8e9dc7f9" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.111", -] - [[package]] name = "bitcoin-io" version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2dee39a0ee5b4095224a0cfc6bf4cc1baf0f9624b96b367e53b66d974e51d953" -[[package]] -name = "bitcoin-private" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73290177011694f38ec25e165d0387ab7ea749a4b81cd4c80dae5988229f7a57" - [[package]] name = "bitcoin_hashes" -version = "0.12.0" +version = "0.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d7066118b13d4b20b23645932dfb3a81ce7e29f95726c2036fa33cd7b092501" +checksum = "26ec84b80c482df901772e931a9a681e26a1b9ee2302edeff23cb30328745c8b" dependencies = [ - "bitcoin-private", + "bitcoin-io", + "hex-conservative", ] [[package]] -name = "bitcoin_hashes" -version = "0.14.1" +name = "bitflags" +version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26ec84b80c482df901772e931a9a681e26a1b9ee2302edeff23cb30328745c8b" +checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" dependencies = [ - "bitcoin-io", - "hex-conservative", + "serde_core", ] -[[package]] -name = "bitflags" -version = "2.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" - [[package]] name = "bitvec" version = "1.0.1" @@ -1464,29 +1216,6 @@ dependencies = [ "wyz", ] -[[package]] -name = "blake3" -version = "1.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3888aaa89e4b2a40fca9848e400f6a658a5a3978de7be858e209cafa8be9a4a0" -dependencies = [ - "arrayref", - "arrayvec", - "cc", - "cfg-if", - "constant_time_eq", - "zeroize", -] - -[[package]] -name = "block-buffer" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4" -dependencies = [ - "generic-array", -] - [[package]] name = "block-buffer" version = "0.10.4" @@ -1496,15 +1225,6 @@ dependencies = [ "generic-array", ] -[[package]] -name = "block-buffer" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96eb4cdd6cf1b31d671e9efe75c5d1ec614776856cefbe109ca373554a6d514f" -dependencies = [ - "hybrid-array", -] - [[package]] name = "blst" version = "0.3.16" @@ -1537,7 +1257,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.117", ] [[package]] @@ -1546,15 +1266,15 @@ version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bf88ba1141d185c399bee5288d850d63b8369520c1eafc32a0430b5b6c287bf4" dependencies = [ - "sha2 0.10.9", + "sha2", "tinyvec", ] [[package]] name = "bump-scope" -version = "1.5.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a117a0e80c453511760a08c607060a778967a8dec5415c1d1a75e20630825824" +checksum = "8cc7ed7bc865061d84b6c54280822da1e07ce6bb614cf9af9f38b64f8cedee52" dependencies = [ "allocator-api2 0.2.21", "allocator-api2 0.3.1", @@ -1563,9 +1283,9 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.19.1" +version = "3.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510" +checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" [[package]] name = "byte-slice-cast" @@ -1573,12 +1293,6 @@ version = "1.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7575182f7272186991736b70173b0ea045398f984bf5ebbb3804736ce1330c9d" -[[package]] -name = "bytemuck" -version = "1.24.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fbdf580320f38b612e485521afda1ee26d10cc9884efaaa750d383e13e3c5f4" - [[package]] name = "byteorder" version = "1.5.0" @@ -1587,9 +1301,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.11.0" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" dependencies = [ "serde", ] @@ -1606,9 +1320,9 @@ dependencies = [ [[package]] name = "c-kzg" -version = "2.1.5" +version = "2.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e00bf4b112b07b505472dbefd19e37e53307e2bfed5a79e0cc161d58ccd0e687" +checksum = "6648ed1e4ea8e8a1a4a2c78e1cda29a3fd500bc622899c340d8525ea9a76b24a" dependencies = [ "blst", "cc", @@ -1627,9 +1341,9 @@ checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" [[package]] name = "cc" -version = "1.2.50" +version = "1.2.57" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f50d563227a1c37cc0a263f64eca3334388c01c5e4c4861a9def205c614383c" +checksum = "7a0dd1ca384932ff3641c8718a02769f1698e7563dc6974ffd03346116310423" dependencies = [ "find-msvc-tools", "jobserver", @@ -1664,17 +1378,6 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" -[[package]] -name = "chacha20" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3613f74bd2eac03dad61bd53dbe620703d4371614fe0bc3b9f04dd36fe4e818" -dependencies = [ - "cfg-if", - "cipher", - "cpufeatures 0.2.17", -] - [[package]] name = "chacha20" version = "0.10.0" @@ -1686,24 +1389,11 @@ dependencies = [ "rand_core 0.10.0", ] -[[package]] -name = "chacha20poly1305" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10cd79432192d1c0f4e1a0fef9527696cc039165d729fb41b3f4f4f354c2dc35" -dependencies = [ - "aead", - "chacha20 0.9.1", - "cipher", - "poly1305", - "zeroize", -] - [[package]] name = "chrono" -version = "0.4.42" +version = "0.4.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2" +checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" dependencies = [ "iana-time-zone", "num-traits", @@ -1738,17 +1428,6 @@ dependencies = [ "half", ] -[[package]] -name = "cipher" -version = "0.4.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" -dependencies = [ - "crypto-common 0.1.7", - "inout", - "zeroize", -] - [[package]] name = "clang-sys" version = "1.8.1" @@ -1762,9 +1441,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.53" +version = "4.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9e340e012a1bf4935f5282ed1436d1489548e8f72308207ea5df0e23d2d03f8" +checksum = "b193af5b67834b676abd72466a96c1024e6a6ad978a1f484bd90b85c94041351" dependencies = [ "clap_builder", "clap_derive", @@ -1772,9 +1451,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.53" +version = "4.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d76b5d13eaa18c901fd2f7fca939fefe3a0727a953561fefdf3b2922b8569d00" +checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" dependencies = [ "anstream", "anstyle", @@ -1784,21 +1463,21 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.49" +version = "4.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a0b5487afeab2deb2ff4e03a807ad1a03ac532ff5a2cee5d86884440c7f7671" +checksum = "1110bd8a634a1ab8cb04345d8d878267d57c3cf1b38d91b71af6686408bbca6a" dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.117", ] [[package]] name = "clap_lex" -version = "0.7.6" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d" +checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" [[package]] name = "cmake" @@ -1821,7 +1500,7 @@ dependencies = [ "hmac", "k256", "serde", - "sha2 0.10.9", + "sha2", "thiserror 1.0.69", ] @@ -1837,7 +1516,7 @@ dependencies = [ "once_cell", "pbkdf2", "rand 0.8.5", - "sha2 0.10.9", + "sha2", "thiserror 1.0.69", ] @@ -1853,10 +1532,10 @@ dependencies = [ "const-hex", "digest 0.10.7", "generic-array", - "ripemd 0.1.3", + "ripemd", "serde", - "sha2 0.10.9", - "sha3 0.10.8", + "sha2", + "sha3", "thiserror 1.0.69", ] @@ -1889,9 +1568,9 @@ dependencies = [ [[package]] name = "colorchoice" -version = "1.0.4" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" [[package]] name = "combine" @@ -1904,145 +1583,39 @@ dependencies = [ ] [[package]] -name = "commonware-codec" -version = "0.0.63" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d473c9be55b1143abebd3fa6a748fe9d35151df4614ea7f68b314e504559af08" -dependencies = [ - "bytes", - "paste", - "thiserror 2.0.17", -] - -[[package]] -name = "commonware-cryptography" -version = "0.0.63" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7157f6e4a84b7a70108f15bf402f539b48d52adf727b9938fe59f752efc5b3e" -dependencies = [ - "blake3", - "blst", - "bytes", - "cfg-if", - "chacha20poly1305", - "commonware-codec", - "commonware-utils", - "ed25519-consensus", - "getrandom 0.2.16", - "p256", - "rand 0.8.5", - "rand_chacha 0.3.1", - "rand_core 0.6.4", - "rayon", - "sha2 0.10.9", - "thiserror 2.0.17", - "x25519-dalek", - "zeroize", -] - -[[package]] -name = "commonware-macros" -version = "0.0.63" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff368ef4db2d10686c1a89b7f4a3f1e7dddd9641a96cac42b72bf71a10360291" -dependencies = [ - "futures", - "proc-macro-crate", - "proc-macro2", - "quote", - "syn 1.0.109", - "tracing", - "tracing-subscriber", -] - -[[package]] -name = "commonware-runtime" -version = "0.0.63" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de4d08e6e11088ed5b42e5ad93b18ffd745cbcc04c6ef7f723f5d237e146abc8" -dependencies = [ - "async-lock", - "axum", - "bytes", - "cfg-if", - "commonware-macros", - "commonware-utils", - "criterion 0.5.1", - "futures", - "getrandom 0.2.16", - "governor", - "libc", - "opentelemetry", - "opentelemetry-otlp", - "opentelemetry_sdk", - "prometheus-client", - "rand 0.8.5", - "rayon", - "sha2 0.10.9", - "sysinfo", - "thiserror 2.0.17", - "tokio", - "tracing", - "tracing-opentelemetry", - "tracing-subscriber", -] - -[[package]] -name = "commonware-storage" -version = "0.0.63" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3116985d7a761aa7bf26d5226d51b8de0eaa6458277963f3c5d903f4078d504" -dependencies = [ - "bytes", - "cfg-if", - "commonware-codec", - "commonware-cryptography", - "commonware-macros", - "commonware-runtime", - "commonware-utils", - "crc32fast", - "futures", - "futures-util", - "prometheus-client", - "rayon", - "thiserror 2.0.17", - "tracing", - "zstd", -] - -[[package]] -name = "commonware-utils" -version = "0.0.63" +name = "concurrent-queue" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "209c03e7057e9d4d5f0bade24685e2bc8845471b89e4ab54684f1af8c287ad3b" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" dependencies = [ - "bytes", - "cfg-if", - "commonware-codec", - "futures", - "getrandom 0.2.16", - "num-bigint", - "num-integer", - "num-rational", - "num-traits", - "rand 0.8.5", - "thiserror 2.0.17", + "crossbeam-utils", ] [[package]] -name = "concurrent-queue" -version = "2.5.0" +name = "config" +version = "0.15.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +checksum = "4fe5feec195269515c4722937cd7ffcfe7b4205d18d2e6577b7223ecb159ab00" dependencies = [ - "crossbeam-utils", + "async-trait", + "convert_case 0.6.0", + "json5", + "pathdiff", + "ron", + "rust-ini", + "serde-untagged", + "serde_core", + "serde_json", + "toml", + "winnow", + "yaml-rust2", ] [[package]] name = "const-hex" -version = "1.17.0" +version = "1.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3bb320cac8a0750d7f25280aa97b09c26edfe161164238ecbbb31092b079e735" +checksum = "531185e432bb31db1ecda541e9e7ab21468d4d844ad7505e0546a49b4945d49b" dependencies = [ "cfg-if", "cpufeatures 0.2.17", @@ -2057,10 +1630,24 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" [[package]] -name = "const-oid" -version = "0.10.1" +name = "const-random" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87e00182fe74b066627d63b85fd550ac2998d4b0bd86bfed477a0ae4c7c71359" +dependencies = [ + "const-random-macro", +] + +[[package]] +name = "const-random-macro" +version = "0.1.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0dabb6555f92fb9ee4140454eb5dcd14c7960e1225c6d1a6cc361f032947713e" +checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e" +dependencies = [ + "getrandom 0.2.17", + "once_cell", + "tiny-keccak", +] [[package]] name = "const_format" @@ -2082,29 +1669,22 @@ dependencies = [ "unicode-xid", ] -[[package]] -name = "constant_time_eq" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6" - [[package]] name = "convert_case" -version = "0.10.0" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "633458d4ef8c78b72454de2d54fd6ab2e60f9e02be22f3c6104cdc8a4e0fceb9" +checksum = "ec182b0ca2f35d8fc196cf3404988fd8b8c739a4d270ff118a398feb0cbec1ca" dependencies = [ "unicode-segmentation", ] [[package]] -name = "core-foundation" -version = "0.9.4" +name = "convert_case" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +checksum = "633458d4ef8c78b72454de2d54fd6ab2e60f9e02be22f3c6104cdc8a4e0fceb9" dependencies = [ - "core-foundation-sys", - "libc", + "unicode-segmentation", ] [[package]] @@ -2156,54 +1736,18 @@ version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" -[[package]] -name = "crc32fast" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "criterion" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2b12d017a929603d80db1831cd3a24082f8137ce19c69e6447f54f5fc8d692f" -dependencies = [ - "anes", - "cast", - "ciborium", - "clap", - "criterion-plot 0.5.0", - "futures", - "is-terminal", - "itertools 0.10.5", - "num-traits", - "once_cell", - "oorandom", - "plotters", - "rayon", - "regex", - "serde", - "serde_derive", - "serde_json", - "tinytemplate", - "walkdir", -] - [[package]] name = "criterion" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d883447757bb0ee46f233e9dc22eb84d93a9508c9b868687b274fc431d886bf" +checksum = "950046b2aa2492f9a536f5f4f9a3de7b9e2476e575e05bd6c333371add4d98f3" dependencies = [ "alloca", "anes", "cast", "ciborium", "clap", - "criterion-plot 0.8.1", + "criterion-plot", "itertools 0.13.0", "num-traits", "oorandom", @@ -2219,19 +1763,9 @@ dependencies = [ [[package]] name = "criterion-plot" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b50826342786a51a89e2da3a28f1c32b06e387201bc2d19791f622c673706b1" -dependencies = [ - "cast", - "itertools 0.10.5", -] - -[[package]] -name = "criterion-plot" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed943f81ea2faa8dcecbbfa50164acf95d555afec96a27871663b300e387b2e4" +checksum = "d8d80a2f4f5b554395e47b5d8305bc3d27813bacb73493eb1001e8f76dae29ea" dependencies = [ "cast", "itertools 0.13.0", @@ -2256,6 +1790,15 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "crossbeam-queue" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f58bbc28f91df819d0aa2a2c00cd19754769c2fad90579b3592b1c9ba7a3115" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "crossbeam-utils" version = "0.8.21" @@ -2287,105 +1830,77 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" dependencies = [ "generic-array", - "rand_core 0.6.4", "typenum", ] [[package]] -name = "crypto-common" -version = "0.2.0-rc.6" +name = "darling" +version = "0.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7fa010a85c7440677a0f4c59cf7ebabef52d7d8b4f79051e5fa60d3f0dd87d0" +checksum = "9cdf337090841a411e2a7f3deb9187445851f91b309c0c0a29e05f74a00a48c0" dependencies = [ - "hybrid-array", + "darling_core 0.21.3", + "darling_macro 0.21.3", ] [[package]] -name = "curve25519-dalek" -version = "4.1.3" +name = "darling" +version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" +checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" dependencies = [ - "cfg-if", - "cpufeatures 0.2.17", - "curve25519-dalek-derive", - "fiat-crypto", - "rustc_version 0.4.1", - "subtle", - "zeroize", + "darling_core 0.23.0", + "darling_macro 0.23.0", ] [[package]] -name = "curve25519-dalek-derive" -version = "0.1.1" +name = "darling_core" +version = "0.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" +checksum = "1247195ecd7e3c85f83c8d2a366e4210d588e802133e1e355180a9870b517ea4" dependencies = [ + "fnv", + "ident_case", "proc-macro2", "quote", - "syn 2.0.111", + "serde", + "strsim", + "syn 2.0.117", ] [[package]] -name = "curve25519-dalek-ng" -version = "4.1.1" +name = "darling_core" +version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c359b7249347e46fb28804470d071c921156ad62b3eef5d34e2ba867533dec8" +checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0" dependencies = [ - "byteorder", - "digest 0.9.0", - "rand_core 0.6.4", - "subtle-ng", - "zeroize", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.117", ] [[package]] -name = "darling" +name = "darling_macro" version = "0.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9cdf337090841a411e2a7f3deb9187445851f91b309c0c0a29e05f74a00a48c0" +checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81" dependencies = [ - "darling_core", - "darling_macro", -] - -[[package]] -name = "darling_core" -version = "0.21.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1247195ecd7e3c85f83c8d2a366e4210d588e802133e1e355180a9870b517ea4" -dependencies = [ - "fnv", - "ident_case", - "proc-macro2", + "darling_core 0.21.3", "quote", - "serde", - "strsim", - "syn 2.0.111", + "syn 2.0.117", ] [[package]] name = "darling_macro" -version = "0.21.3" +version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81" +checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" dependencies = [ - "darling_core", + "darling_core 0.23.0", "quote", - "syn 2.0.111", -] - -[[package]] -name = "dashmap" -version = "5.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856" -dependencies = [ - "cfg-if", - "hashbrown 0.14.5", - "lock_api", - "once_cell", - "parking_lot_core", + "syn 2.0.117", ] [[package]] @@ -2404,9 +1919,9 @@ dependencies = [ [[package]] name = "data-encoding" -version = "2.9.0" +version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476" +checksum = "d7a1e2f27636f116493b8b860f5546edb47c8d8f8ea73e1d2a20be88e28d1fea" [[package]] name = "der" @@ -2414,15 +1929,16 @@ version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" dependencies = [ - "const-oid 0.9.6", + "const-oid", + "pem-rfc7468", "zeroize", ] [[package]] name = "deranged" -version = "0.5.5" +version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ececcb659e7ba858fb4f10388c250a7252eb0a27373f1a72b8748afdd248e587" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" dependencies = [ "powerfmt", "serde_core", @@ -2454,11 +1970,11 @@ version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" dependencies = [ - "convert_case", + "convert_case 0.10.0", "proc-macro2", "quote", "rustc_version 0.4.1", - "syn 2.0.111", + "syn 2.0.117", "unicode-xid", ] @@ -2477,23 +1993,12 @@ version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ - "block-buffer 0.10.4", - "const-oid 0.9.6", - "crypto-common 0.1.7", + "block-buffer", + "const-oid", + "crypto-common", "subtle", ] -[[package]] -name = "digest" -version = "0.11.0-rc.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea390c940e465846d64775e55e3115d5dc934acb953de6f6e6360bc232fe2bf7" -dependencies = [ - "block-buffer 0.11.0", - "const-oid 0.10.1", - "crypto-common 0.2.0-rc.6", -] - [[package]] name = "displaydoc" version = "0.2.5" @@ -2502,20 +2007,23 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.117", ] [[package]] -name = "doctest-file" -version = "1.0.0" +name = "dlv-list" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aac81fa3e28d21450aa4d2ac065992ba96a1d7303efbce51a95f4fd175b67562" +checksum = "442039f5147480ba31067cb00ada1adae6892028e40e45fc5de7b7df6dcc1b5f" +dependencies = [ + "const-random", +] [[package]] -name = "dtoa" -version = "1.0.10" +name = "dotenvy" +version = "0.15.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6add3b8cff394282be81f3fc1a0605db594ed69890078ca6e2cab1c408bcf04" +checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" [[package]] name = "dunce" @@ -2544,20 +2052,6 @@ dependencies = [ "spki", ] -[[package]] -name = "ed25519-consensus" -version = "2.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c8465edc8ee7436ffea81d21a019b16676ee3db267aa8d5a8d729581ecf998b" -dependencies = [ - "curve25519-dalek-ng", - "hex", - "rand_core 0.6.4", - "sha2 0.9.9", - "thiserror 1.0.69", - "zeroize", -] - [[package]] name = "educe" version = "0.6.0" @@ -2567,7 +2061,7 @@ dependencies = [ "enum-ordinalize", "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.117", ] [[package]] @@ -2599,6 +2093,15 @@ dependencies = [ "zeroize", ] +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + [[package]] name = "enum-ordinalize" version = "4.3.2" @@ -2616,20 +2119,7 @@ checksum = "8ca9601fb2d62598ee17836250842873a413586e5d7ed88b356e38ddbb0ec631" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", -] - -[[package]] -name = "env_logger" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4cd405aab171cb85d6735e5c8d9db038c17d3ca007a4d2c25f337935c3d90580" -dependencies = [ - "humantime", - "is-terminal", - "log", - "regex", - "termcolor", + "syn 2.0.117", ] [[package]] @@ -2638,6 +2128,17 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" +[[package]] +name = "erased-serde" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2add8a07dd6a8d93ff627029c51de145e12686fbc36ecb298ac22e74cf02dec" +dependencies = [ + "serde", + "serde_core", + "typeid", +] + [[package]] name = "errno" version = "0.3.14" @@ -2649,23 +2150,24 @@ dependencies = [ ] [[package]] -name = "event-listener" -version = "5.4.1" +name = "etcetera" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" +checksum = "136d1b5283a1ab77bd9257427ffd09d8667ced0570b6f938942bc7568ed5b943" dependencies = [ - "concurrent-queue", - "parking", - "pin-project-lite", + "cfg-if", + "home", + "windows-sys 0.48.0", ] [[package]] -name = "event-listener-strategy" -version = "0.5.4" +name = "event-listener" +version = "5.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" +checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" dependencies = [ - "event-listener", + "concurrent-queue", + "parking", "pin-project-lite", ] @@ -2717,17 +2219,11 @@ dependencies = [ "subtle", ] -[[package]] -name = "fiat-crypto" -version = "0.2.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" - [[package]] name = "find-msvc-tools" -version = "0.1.5" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a3076410a55c90011c298b04d0cfa770b00fa04e1e3c97d3f6c9de105a03844" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" [[package]] name = "fixed-hash" @@ -2741,6 +2237,17 @@ dependencies = [ "static_assertions", ] +[[package]] +name = "flume" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da0e4dd2a88388a1f4ccc7c9ce104604dab68d9f408dc34cd45823d5a9069095" +dependencies = [ + "futures-core", + "futures-sink", + "spin", +] + [[package]] name = "fnv" version = "1.0.7" @@ -2797,9 +2304,9 @@ checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" [[package]] name = "futures" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" +checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" dependencies = [ "futures-channel", "futures-core", @@ -2812,9 +2319,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" dependencies = [ "futures-core", "futures-sink", @@ -2822,49 +2329,60 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" [[package]] name = "futures-executor" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" dependencies = [ "futures-core", "futures-task", "futures-util", ] +[[package]] +name = "futures-intrusive" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d930c203dd0b6ff06e0201a4a2fe9149b43c684fd4420555b26d21b1a02956f" +dependencies = [ + "futures-core", + "lock_api", + "parking_lot", +] + [[package]] name = "futures-io" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" [[package]] name = "futures-macro" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.117", ] [[package]] name = "futures-sink" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" [[package]] name = "futures-task" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" [[package]] name = "futures-timer" @@ -2874,9 +2392,9 @@ checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24" [[package]] name = "futures-util" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" dependencies = [ "futures-channel", "futures-core", @@ -2886,7 +2404,6 @@ dependencies = [ "futures-task", "memchr", "pin-project-lite", - "pin-utils", "slab", ] @@ -2909,9 +2426,9 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.16" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" dependencies = [ "cfg-if", "js-sys", @@ -2929,20 +2446,20 @@ dependencies = [ "cfg-if", "js-sys", "libc", - "r-efi", + "r-efi 5.3.0", "wasip2", "wasm-bindgen", ] [[package]] name = "getrandom" -version = "0.4.1" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "139ef39800118c7683f2fd3c98c1b23c09ae076556b435f8e9064ae108aaeeec" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" dependencies = [ "cfg-if", "libc", - "r-efi", + "r-efi 6.0.0", "rand_core 0.10.0", "wasip2", "wasip3", @@ -2954,36 +2471,43 @@ version = "0.32.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" -[[package]] -name = "glam" -version = "0.32.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34627c5158214743a374170fed714833fdf4e4b0cbcc1ea98417866a4c5d4441" - [[package]] name = "glob" version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" +[[package]] +name = "gloo-timers" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbb143cf96099802033e0d4f4963b19fd2e0b728bcf076cd9cf7f6634f092994" +dependencies = [ + "futures-channel", + "futures-core", + "js-sys", + "wasm-bindgen", +] + [[package]] name = "governor" -version = "0.6.3" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68a7f542ee6b35af73b06abc0dad1c1bae89964e4e253bc4b587b91c9637867b" +checksum = "be93b4ec2e4710b04d9264c0c7350cdd62a8c20e5e4ac732552ebb8f0debe8eb" dependencies = [ "cfg-if", - "dashmap 5.5.3", - "futures", + "futures-sink", "futures-timer", + "futures-util", + "getrandom 0.3.4", "no-std-compat", "nonzero_ext", "parking_lot", "portable-atomic", "quanta", - "rand 0.8.5", "smallvec", "spinning_top", + "web-time", ] [[package]] @@ -2999,9 +2523,9 @@ dependencies = [ [[package]] name = "h2" -version = "0.4.12" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3c0b69cfcb4e1b9f1bf2f53f95f766e4661169728ec61cd3fe5a0166f2d1386" +checksum = "2f44da3a8150a6703ed5d34e164b875fd14c2cdab9af1252a9a1020bde2bdc54" dependencies = [ "atomic-waker", "bytes", @@ -3009,7 +2533,7 @@ dependencies = [ "futures-core", "futures-sink", "http", - "indexmap 2.12.1", + "indexmap 2.13.0", "slab", "tokio", "tokio-util", @@ -3056,11 +2580,22 @@ version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" dependencies = [ + "allocator-api2 0.2.21", + "equivalent", "foldhash 0.2.0", "serde", "serde_core", ] +[[package]] +name = "hashlink" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1" +dependencies = [ + "hashbrown 0.15.5", +] + [[package]] name = "heck" version = "0.5.0" @@ -3088,6 +2623,15 @@ dependencies = [ "arrayvec", ] +[[package]] +name = "hkdf" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" +dependencies = [ + "hmac", +] + [[package]] name = "hmac" version = "0.12.1" @@ -3097,6 +2641,15 @@ dependencies = [ "digest 0.10.7", ] +[[package]] +name = "home" +version = "0.5.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d" +dependencies = [ + "windows-sys 0.61.2", +] + [[package]] name = "http" version = "1.4.0" @@ -3142,22 +2695,6 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" -[[package]] -name = "humantime" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "135b12329e5e3ce057a9f972339ea52bc954fe1e9358ef27f95e89716fbc5424" - -[[package]] -name = "hybrid-array" -version = "0.4.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f471e0a81b2f90ffc0cb2f951ae04da57de8baa46fa99112b062a5173a5088d0" -dependencies = [ - "bytemuck", - "typenum", -] - [[package]] name = "hyper" version = "1.8.1" @@ -3195,7 +2732,7 @@ dependencies = [ "tokio", "tokio-rustls", "tower-service", - "webpki-roots 1.0.4", + "webpki-roots 1.0.6", ] [[package]] @@ -3216,14 +2753,13 @@ dependencies = [ [[package]] name = "hyper-util" -version = "0.1.19" +version = "0.1.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "727805d60e7938b76b826a6ef209eb70eaa1812794f9424d4a4e2d740662df5f" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" dependencies = [ "base64 0.22.1", "bytes", "futures-channel", - "futures-core", "futures-util", "http", "http-body", @@ -3240,9 +2776,9 @@ dependencies = [ [[package]] name = "iana-time-zone" -version = "0.1.64" +version = "0.1.65" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" dependencies = [ "android_system_properties", "core-foundation-sys", @@ -3250,7 +2786,7 @@ dependencies = [ "js-sys", "log", "wasm-bindgen", - "windows-core 0.62.2", + "windows-core", ] [[package]] @@ -3393,7 +2929,7 @@ checksum = "a0eb5a3343abf848c0984fe4604b2b105da9539376e24fc0a3b0007411ae4fd9" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.117", ] [[package]] @@ -3415,9 +2951,9 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.12.1" +version = "2.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ad4bb2b565bca0645f4d68c5c9af97fba094e9791da685bf83cb5f3ce74acf2" +checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" dependencies = [ "equivalent", "hashbrown 0.16.1", @@ -3425,57 +2961,22 @@ dependencies = [ "serde_core", ] -[[package]] -name = "inout" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" -dependencies = [ - "generic-array", -] - -[[package]] -name = "interprocess" -version = "2.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d941b405bd2322993887859a8ee6ac9134945a24ec5ec763a8a962fc64dfec2d" -dependencies = [ - "doctest-file", - "futures-core", - "libc", - "recvmsg", - "tokio", - "widestring", - "windows-sys 0.52.0", -] - [[package]] name = "ipnet" -version = "2.11.0" +version = "2.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" +checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" [[package]] name = "iri-string" -version = "0.7.9" +version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f867b9d1d896b67beb18518eda36fdb77a32ea590de864f1325b294a6d14397" +checksum = "c91338f0783edbd6195decb37bae672fd3b165faffb89bf7b9e6942f8b1a731a" dependencies = [ "memchr", "serde", ] -[[package]] -name = "is-terminal" -version = "0.4.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46" -dependencies = [ - "hermit-abi", - "libc", - "windows-sys 0.61.2", -] - [[package]] name = "is_terminal_polyfill" version = "1.70.2" @@ -3511,15 +3012,15 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.16" +version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ee5b5339afb4c41626dde77b7a611bd4f2c202b897852b4bcf5d03eddc61010" +checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" [[package]] name = "jiff" -version = "0.2.20" +version = "0.2.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c867c356cc096b33f4981825ab281ecba3db0acefe60329f044c1789d94c6543" +checksum = "1a3546dc96b6d42c5f24902af9e2538e82e39ad350b0c766eb3fbf2d8f3d8359" dependencies = [ "jiff-static", "jiff-tzdb-platform", @@ -3532,20 +3033,20 @@ dependencies = [ [[package]] name = "jiff-static" -version = "0.2.20" +version = "0.2.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7946b4325269738f270bb55b3c19ab5c5040525f83fd625259422a9d25d9be5" +checksum = "2a8c8b344124222efd714b73bb41f8b5120b27a7cc1c75593a6ff768d9d05aa4" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.117", ] [[package]] name = "jiff-tzdb" -version = "0.1.5" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68971ebff725b9e2ca27a601c5eb38a4c5d64422c4cbab0c535f248087eda5c2" +checksum = "c900ef84826f1338a557697dc8fc601df9ca9af4ac137c7fb61d4c6f2dfd3076" [[package]] name = "jiff-tzdb-platform" @@ -3590,14 +3091,25 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.83" +version = "0.3.91" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "464a3709c7f55f1f721e5389aa6ea4e3bc6aba669353300af094b29ffbdde1d8" +checksum = "b49715b7073f385ba4bc528e5747d02e66cb39c6146efb66b781f131f0fb399c" dependencies = [ "once_cell", "wasm-bindgen", ] +[[package]] +name = "json5" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96b0db21af676c1ce64250b5f40f3ce2cf27e4e47cb91ed91eb6fe9350b430c1" +dependencies = [ + "pest", + "pest_derive", + "serde", +] + [[package]] name = "k256" version = "0.13.4" @@ -3609,33 +3121,24 @@ dependencies = [ "elliptic-curve", "once_cell", "serdect", - "sha2 0.10.9", + "sha2", "signature", ] [[package]] name = "keccak" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ecc2af9a1119c51f12a14607e783cb977bde58bc069ff0c3da1095e635d70654" -dependencies = [ - "cpufeatures 0.2.17", -] - -[[package]] -name = "keccak" -version = "0.2.0-rc.0" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d546793a04a1d3049bd192856f804cfe96356e2cf36b54b4e575155babe9f41" +checksum = "cb26cec98cce3a3d96cbb7bced3c4b16e3d13f27ec56dbd62cbc8f39cfb9d653" dependencies = [ "cpufeatures 0.2.17", ] [[package]] name = "keccak-asm" -version = "0.1.4" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "505d1856a39b200489082f90d897c3f07c455563880bc5952e38eabf731c83b6" +checksum = "b646a74e746cd25045aa0fd42f4f7f78aa6d119380182c7e63a5593c4ab8df6f" dependencies = [ "digest 0.10.7", "sha3-asm", @@ -3646,6 +3149,9 @@ name = "lazy_static" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +dependencies = [ + "spin", +] [[package]] name = "leb128fmt" @@ -3655,9 +3161,9 @@ checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" [[package]] name = "libc" -version = "0.2.178" +version = "0.2.183" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37c93d8daa9d8a012fd8ab92f088405fb202ea0b6ab73ee2482ae66af4f42091" +checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d" [[package]] name = "libloading" @@ -3671,9 +3177,21 @@ dependencies = [ [[package]] name = "libm" -version = "0.2.15" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + +[[package]] +name = "libredox" +version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" +checksum = "1744e39d1d6a9948f4f388969627434e31128196de472883b39f148769bfe30a" +dependencies = [ + "bitflags", + "libc", + "plain", + "redox_syscall 0.7.3", +] [[package]] name = "librocksdb-sys" @@ -3686,15 +3204,24 @@ dependencies = [ "cc", "libc", "libz-sys", - "lz4-sys", - "zstd-sys", +] + +[[package]] +name = "libsqlite3-sys" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149" +dependencies = [ + "cc", + "pkg-config", + "vcpkg", ] [[package]] name = "libz-sys" -version = "1.1.23" +version = "1.1.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15d118bbf3771060e7311cc7bb0545b01d08a8b4a7de949198dec1fa0ca1c0f7" +checksum = "d52f4c29e2a68ac30c9087e1b772dc9f44a2b66ed44edf2266cf2be9b03dafc1" dependencies = [ "cc", "pkg-config", @@ -3703,9 +3230,9 @@ dependencies = [ [[package]] name = "linux-raw-sys" -version = "0.11.0" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" [[package]] name = "litemap" @@ -3730,11 +3257,11 @@ checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" [[package]] name = "lru" -version = "0.13.0" +version = "0.16.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "227748d55f2f0ab4735d87fd623798cb6b664512fe979705f829c9f81c934465" +checksum = "a1dc47f592c06f33f8e3aea9591776ec7c9f9e4124778ff8a3c3b87159f7e593" dependencies = [ - "hashbrown 0.15.5", + "hashbrown 0.16.1", ] [[package]] @@ -3743,16 +3270,6 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" -[[package]] -name = "lz4-sys" -version = "1.11.1+lz4-1.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6bd8c0d6c6ed0cd30b3652886bb8711dc4bb01d637a68105a3d5158039b418e6" -dependencies = [ - "cc", - "libc", -] - [[package]] name = "macro-string" version = "0.1.4" @@ -3761,7 +3278,7 @@ checksum = "1b27834086c65ec3f9387b096d66e99f221cf081c2b738042aa252bcd41204e3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.117", ] [[package]] @@ -3780,12 +3297,22 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" [[package]] -name = "memchr" -version = "2.7.6" +name = "md-5" +version = "0.10.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" - -[[package]] +checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" +dependencies = [ + "cfg-if", + "digest 0.10.7", +] + +[[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] name = "mime" version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -3819,17 +3346,17 @@ dependencies = [ [[package]] name = "native-tls" -version = "0.2.14" +version = "0.2.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e" +checksum = "465500e14ea162429d264d44189adc38b199b62b1c21eea9f69e4b73cb03bbf2" dependencies = [ "libc", "log", "openssl", - "openssl-probe 0.1.6", + "openssl-probe", "openssl-sys", "schannel", - "security-framework 2.11.1", + "security-framework", "security-framework-sys", "tempfile", ] @@ -3856,15 +3383,6 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "38bf9645c8b145698bb0b18a4637dcacbc421ea49bef2317e4fd8065a387cf21" -[[package]] -name = "ntapi" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c70f219e21142367c70c0b30c6a9e3a14d55b4d12a204d897fbec83a0363f081" -dependencies = [ - "winapi", -] - [[package]] name = "nu-ansi-term" version = "0.50.3" @@ -3884,11 +3402,27 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-bigint-dig" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e661dda6640fad38e827a6d4a310ff4763082116fe217f279885c97f511bb0b7" +dependencies = [ + "lazy_static", + "libm", + "num-integer", + "num-iter", + "num-traits", + "rand 0.8.5", + "smallvec", + "zeroize", +] + [[package]] name = "num-conv" -version = "0.1.0" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" +checksum = "cf97ec579c3c42f953ef76dbf8d55ac91fb219dde70e49aa4a6b7d74e9919050" [[package]] name = "num-integer" @@ -3900,12 +3434,12 @@ dependencies = [ ] [[package]] -name = "num-rational" -version = "0.4.2" +name = "num-iter" +version = "0.1.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" +checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" dependencies = [ - "num-bigint", + "autocfg", "num-integer", "num-traits", ] @@ -3932,9 +3466,9 @@ dependencies = [ [[package]] name = "num_enum" -version = "0.7.5" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1207a7e20ad57b847bbddc6776b968420d38292bbfe2089accff5e19e82454c" +checksum = "5d0bca838442ec211fa11de3a8b0e0e8f3a4522575b5c4c06ed722e005036f26" dependencies = [ "num_enum_derive", "rustversion", @@ -3942,20 +3476,20 @@ dependencies = [ [[package]] name = "num_enum_derive" -version = "0.7.5" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff32365de1b6743cb203b710788263c44a03de03802daf96092f2da4fe6ba4d7" +checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.117", ] [[package]] name = "nybbles" -version = "0.4.6" +version = "0.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c4b5ecbd0beec843101bffe848217f770e8b8da81d8355b7d6e226f2199b3dc" +checksum = "0d49ff0c0d00d4a502b39df9af3a525e1efeb14b9dabb5bb83335284c1309210" dependencies = [ "alloy-rlp", "cfg-if", @@ -3976,9 +3510,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.21.3" +version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" [[package]] name = "once_cell_polyfill" @@ -3992,17 +3526,11 @@ version = "11.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e" -[[package]] -name = "opaque-debug" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" - [[package]] name = "openssl" -version = "0.10.75" +version = "0.10.76" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08838db121398ad17ab8531ce9de97b244589089e290a384c900cb9ff7434328" +checksum = "951c002c75e16ea2c65b8c7e4d3d51d5530d8dfa7d060b4776828c88cfb18ecf" dependencies = [ "bitflags", "cfg-if", @@ -4021,150 +3549,42 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.117", ] -[[package]] -name = "openssl-probe" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" - [[package]] name = "openssl-probe" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" -[[package]] -name = "openssl-src" -version = "300.5.4+3.5.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a507b3792995dae9b0df8a1c1e3771e8418b7c2d9f0baeba32e6fe8b06c7cb72" -dependencies = [ - "cc", -] - [[package]] name = "openssl-sys" -version = "0.9.111" +version = "0.9.112" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82cab2d520aa75e3c58898289429321eb788c3106963d0dc886ec7a5f4adc321" +checksum = "57d55af3b3e226502be1526dfdba67ab0e9c96fc293004e79576b2b9edb0dbdb" dependencies = [ "cc", "libc", - "openssl-src", "pkg-config", "vcpkg", ] [[package]] -name = "opentelemetry" -version = "0.28.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "236e667b670a5cdf90c258f5a55794ec5ac5027e960c224bff8367a59e1e6426" -dependencies = [ - "futures-core", - "futures-sink", - "js-sys", - "pin-project-lite", - "thiserror 2.0.17", - "tracing", -] - -[[package]] -name = "opentelemetry-http" -version = "0.28.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8863faf2910030d139fb48715ad5ff2f35029fc5f244f6d5f689ddcf4d26253" -dependencies = [ - "async-trait", - "bytes", - "http", - "opentelemetry", - "reqwest 0.12.28", - "tracing", -] - -[[package]] -name = "opentelemetry-otlp" -version = "0.28.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5bef114c6d41bea83d6dc60eb41720eedd0261a67af57b66dd2b84ac46c01d91" -dependencies = [ - "async-trait", - "futures-core", - "http", - "opentelemetry", - "opentelemetry-http", - "opentelemetry-proto", - "opentelemetry_sdk", - "prost", - "reqwest 0.12.28", - "thiserror 2.0.17", - "tracing", -] - -[[package]] -name = "opentelemetry-proto" -version = "0.28.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56f8870d3024727e99212eb3bb1762ec16e255e3e6f58eeb3dc8db1aa226746d" -dependencies = [ - "opentelemetry", - "opentelemetry_sdk", - "prost", - "tonic", -] - -[[package]] -name = "opentelemetry_sdk" -version = "0.28.0" +name = "ordered-multimap" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84dfad6042089c7fc1f6118b7040dc2eb4ab520abbf410b79dc481032af39570" +checksum = "49203cdcae0030493bad186b28da2fa25645fa276a51b6fec8010d281e02ef79" dependencies = [ - "async-trait", - "futures-channel", - "futures-executor", - "futures-util", - "glob", - "opentelemetry", - "percent-encoding", - "rand 0.8.5", - "serde_json", - "thiserror 2.0.17", - "tokio", - "tokio-stream", - "tracing", -] - -[[package]] -name = "opentimestamps" -version = "0.2.0" -source = "git+https://github.com/opentimestamps/rust-opentimestamps#c87e3e18284fd1bb9456e50721aa4e2796aed057" -dependencies = [ - "bitcoin_hashes 0.12.0", - "env_logger", - "log", + "dlv-list", + "hashbrown 0.14.5", ] [[package]] name = "owo-colors" -version = "4.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c6901729fa79e91a0913333229e9ca5dc725089d1c363b2f4b4760709dc4a52" - -[[package]] -name = "p256" -version = "0.13.2" +version = "4.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b" -dependencies = [ - "ecdsa", - "elliptic-curve", - "primeorder", - "sha2 0.10.9", -] +checksum = "d211803b9b6b570f68772237e415a029d5a50c65d382910b879fb19d3271f94d" [[package]] name = "page_size" @@ -4201,7 +3621,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.117", ] [[package]] @@ -4228,7 +3648,7 @@ checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" dependencies = [ "cfg-if", "libc", - "redox_syscall", + "redox_syscall 0.5.18", "smallvec", "windows-link", ] @@ -4239,6 +3659,12 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" +[[package]] +name = "pathdiff" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" + [[package]] name = "pbkdf2" version = "0.12.2" @@ -4249,6 +3675,15 @@ dependencies = [ "hmac", ] +[[package]] +name = "pem-rfc7468" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" +dependencies = [ + "base64ct", +] + [[package]] name = "percent-encoding" version = "2.3.2" @@ -4257,14 +3692,47 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "pest" -version = "2.8.4" +version = "2.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cbcfd20a6d4eeba40179f05735784ad32bdaef05ce8e8af05f180d45bb3e7e22" +checksum = "e0848c601009d37dfa3430c4666e147e49cdcf1b92ecd3e63657d8a5f19da662" dependencies = [ "memchr", "ucd-trie", ] +[[package]] +name = "pest_derive" +version = "2.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11f486f1ea21e6c10ed15d5a7c77165d0ee443402f0780849d1768e7d9d6fe77" +dependencies = [ + "pest", + "pest_generator", +] + +[[package]] +name = "pest_generator" +version = "2.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8040c4647b13b210a963c1ed407c1ff4fdfa01c31d6d2a098218702e6664f94f" +dependencies = [ + "pest", + "pest_meta", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "pest_meta" +version = "2.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89815c69d36021a140146f26659a81d6c2afa33d216d736dd4be5381a7362220" +dependencies = [ + "pest", + "sha2", +] + [[package]] name = "pharos" version = "0.5.3" @@ -4275,31 +3743,74 @@ dependencies = [ "rustc_version 0.4.1", ] +[[package]] +name = "phf" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1562dc717473dbaa4c1f85a36410e03c047b2e7df7f45ee938fbef64ae7fadf" +dependencies = [ + "phf_macros", + "phf_shared", + "serde", +] + +[[package]] +name = "phf_generator" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "135ace3a761e564ec88c03a77317a7c6b80bb7f7135ef2544dbe054243b89737" +dependencies = [ + "fastrand", + "phf_shared", +] + +[[package]] +name = "phf_macros" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812f032b54b1e759ccd5f8b6677695d5268c588701effba24601f6932f8269ef" +dependencies = [ + "phf_generator", + "phf_shared", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "phf_shared" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e57fef6bc5981e38c2ce2d63bfa546861309f875b8a75f092d1d54ae2d64f266" +dependencies = [ + "siphasher", +] + [[package]] name = "pin-project" -version = "1.1.10" +version = "1.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "677f1add503faace112b9f1373e43e9e054bfdd22ff1a63c1bc485eaec6a6a8a" +checksum = "f1749c7ed4bcaf4c3d0a3efc28538844fb29bcdd7d2b67b2be7e20ba861ff517" dependencies = [ "pin-project-internal", ] [[package]] name = "pin-project-internal" -version = "1.1.10" +version = "1.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" +checksum = "d9b20ed30f105399776b9c883e68e536ef602a16ae6f596d2c473591d6ad64c6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.117", ] [[package]] name = "pin-project-lite" -version = "0.2.16" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" [[package]] name = "pin-utils" @@ -4307,6 +3818,17 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "pkcs1" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" +dependencies = [ + "der", + "pkcs8", + "spki", +] + [[package]] name = "pkcs8" version = "0.10.2" @@ -4323,6 +3845,12 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" +[[package]] +name = "plain" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" + [[package]] name = "plotters" version = "0.3.7" @@ -4351,28 +3879,17 @@ dependencies = [ "plotters-backend", ] -[[package]] -name = "poly1305" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf" -dependencies = [ - "cpufeatures 0.2.17", - "opaque-debug", - "universal-hash", -] - [[package]] name = "portable-atomic" -version = "1.12.0" +version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f59e70c4aef1e55797c2e8fd94a4f2a973fc972cfde0e0b05f683667b0cd39dd" +checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" [[package]] name = "portable-atomic-util" -version = "0.2.5" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a9db96d7fa8782dd8c15ce32ffe8680bbd1e978a43bf51a34d39483540495f5" +checksum = "091397be61a01d4be58e7841595bd4bfedb15f1cd54977d79b8271e94ed799a3" dependencies = [ "portable-atomic", ] @@ -4408,16 +3925,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" dependencies = [ "proc-macro2", - "syn 2.0.111", -] - -[[package]] -name = "primeorder" -version = "0.13.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6" -dependencies = [ - "elliptic-curve", + "syn 2.0.117", ] [[package]] @@ -4433,9 +3941,9 @@ dependencies = [ [[package]] name = "proc-macro-crate" -version = "3.4.0" +version = "3.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983" +checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" dependencies = [ "toml_edit", ] @@ -4459,46 +3967,23 @@ dependencies = [ "proc-macro-error-attr2", "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.117", ] [[package]] name = "proc-macro2" -version = "1.0.103" +version = "1.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" dependencies = [ "unicode-ident", ] -[[package]] -name = "prometheus-client" -version = "0.22.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "504ee9ff529add891127c4827eb481bd69dc0ebc72e9a682e187db4caa60c3ca" -dependencies = [ - "dtoa", - "itoa", - "parking_lot", - "prometheus-client-derive-encode", -] - -[[package]] -name = "prometheus-client-derive-encode" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "440f724eba9f6996b75d63681b0a92b06947f1457076d503a4d2e2c8f56442b8" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.111", -] - [[package]] name = "proptest" -version = "1.9.0" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bee689443a2bd0a16ab0348b52ee43e3b2d1b1f931c8aa5c9f8de4c86fbe8c40" +checksum = "37566cb3fdacef14c0737f9546df7cfeadbfbc9fef10991038bf5015d0c80532" dependencies = [ "bit-set", "bit-vec", @@ -4513,29 +3998,6 @@ dependencies = [ "unarray", ] -[[package]] -name = "prost" -version = "0.13.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2796faa41db3ec313a31f7624d9286acf277b52de526150b7e69f3debf891ee5" -dependencies = [ - "bytes", - "prost-derive", -] - -[[package]] -name = "prost-derive" -version = "0.13.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a56d757972c98b346a9b766e3f02746cde6dd1cd1d1d563472929fdd74bec4d" -dependencies = [ - "anyhow", - "itertools 0.14.0", - "proc-macro2", - "quote", - "syn 2.0.111", -] - [[package]] name = "quanta" version = "0.12.6" @@ -4571,7 +4033,7 @@ dependencies = [ "rustc-hash", "rustls", "socket2", - "thiserror 2.0.17", + "thiserror 2.0.18", "tokio", "tracing", "web-time", @@ -4579,9 +4041,9 @@ dependencies = [ [[package]] name = "quinn-proto" -version = "0.11.13" +version = "0.11.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1906b49b0c3bc04b5fe5d86a77925ae6524a19b816ae38ce1e426255f1d8a31" +checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098" dependencies = [ "aws-lc-rs", "bytes", @@ -4593,7 +4055,7 @@ dependencies = [ "rustls", "rustls-pki-types", "slab", - "thiserror 2.0.17", + "thiserror 2.0.18", "tinyvec", "tracing", "web-time", @@ -4615,9 +4077,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.42" +version = "1.0.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" dependencies = [ "proc-macro2", ] @@ -4628,6 +4090,12 @@ version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + [[package]] name = "radium" version = "0.7.0" @@ -4653,7 +4121,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" dependencies = [ "rand_chacha 0.9.0", - "rand_core 0.9.3", + "rand_core 0.9.5", "serde", ] @@ -4663,8 +4131,8 @@ version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bc266eb313df6c5c09c1c7b1fbe2510961e5bcd3add930c1e31f7ed9da0feff8" dependencies = [ - "chacha20 0.10.0", - "getrandom 0.4.1", + "chacha20", + "getrandom 0.4.2", "rand_core 0.10.0", ] @@ -4685,7 +4153,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" dependencies = [ "ppv-lite86", - "rand_core 0.9.3", + "rand_core 0.9.5", ] [[package]] @@ -4694,14 +4162,14 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom 0.2.16", + "getrandom 0.2.17", ] [[package]] name = "rand_core" -version = "0.9.3" +version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" dependencies = [ "getrandom 0.3.4", "serde", @@ -4719,14 +4187,14 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "513962919efc330f829edb2535844d1b912b0fbe2ca165d613e4e8788bb05a5a" dependencies = [ - "rand_core 0.9.3", + "rand_core 0.9.5", ] [[package]] name = "rapidhash" -version = "4.2.0" +version = "4.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2988730ee014541157f48ce4dcc603940e00915edc3c7f9a8d78092256bb2493" +checksum = "b5e48930979c155e2f33aa36ab3119b5ee81332beb6482199a8ecd6029b80b59" dependencies = [ "rustversion", ] @@ -4760,12 +4228,6 @@ dependencies = [ "crossbeam-utils", ] -[[package]] -name = "recvmsg" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3edd4d5d42c92f0a659926464d4cce56b562761267ecf0f469d85b7de384175" - [[package]] name = "redox_syscall" version = "0.5.18" @@ -4776,9 +4238,18 @@ dependencies = [ ] [[package]] -name = "ref-cast" -version = "1.0.25" -source = "registry+https://github.com/rust-lang/crates.io-index" +name = "redox_syscall" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce70a74e890531977d37e532c34d45e9055d2409ed08ddba14529471ed0be16" +dependencies = [ + "bitflags", +] + +[[package]] +name = "ref-cast" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f354300ae66f76f1c85c5f84693f0ce81d747e2c3f21a45fef496d89c960bf7d" dependencies = [ "ref-cast-impl", @@ -4792,14 +4263,14 @@ checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.117", ] [[package]] name = "regex" -version = "1.12.2" +version = "1.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" dependencies = [ "aho-corasick", "memchr", @@ -4809,9 +4280,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.13" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" dependencies = [ "aho-corasick", "memchr", @@ -4820,9 +4291,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.8" +version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" [[package]] name = "reqwest" @@ -4832,9 +4303,7 @@ checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" dependencies = [ "base64 0.22.1", "bytes", - "futures-channel", "futures-core", - "futures-util", "http", "http-body", "http-body-util", @@ -4864,7 +4333,7 @@ dependencies = [ "wasm-bindgen", "wasm-bindgen-futures", "web-sys", - "webpki-roots 1.0.4", + "webpki-roots 1.0.6", ] [[package]] @@ -4893,6 +4362,8 @@ dependencies = [ "rustls", "rustls-pki-types", "rustls-platform-verifier", + "serde", + "serde_json", "sync_wrapper", "tokio", "tokio-native-tls", @@ -4924,7 +4395,7 @@ checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" dependencies = [ "cc", "cfg-if", - "getrandom 0.2.16", + "getrandom 0.2.17", "libc", "untrusted", "windows-sys 0.52.0", @@ -4939,15 +4410,6 @@ dependencies = [ "digest 0.10.7", ] -[[package]] -name = "ripemd" -version = "0.2.0-rc.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fc804eb044d044dcfa77f973ace1f4faa1084f130d9a7ec49c274ce47afda67" -dependencies = [ - "digest 0.11.0-rc.4", -] - [[package]] name = "rlp" version = "0.5.2" @@ -4968,11 +4430,45 @@ dependencies = [ "librocksdb-sys", ] +[[package]] +name = "ron" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd490c5b18261893f14449cbd28cb9c0b637aebf161cd77900bfdedaff21ec32" +dependencies = [ + "bitflags", + "once_cell", + "serde", + "serde_derive", + "typeid", + "unicode-ident", +] + +[[package]] +name = "rsa" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8573f03f5883dcaebdfcf4725caa1ecb9c15b2ef50c43a07b816e06799bb12d" +dependencies = [ + "const-oid", + "digest 0.10.7", + "num-bigint-dig", + "num-integer", + "num-traits", + "pkcs1", + "pkcs8", + "rand_core 0.6.4", + "signature", + "spki", + "subtle", + "zeroize", +] + [[package]] name = "ruint" -version = "1.17.0" +version = "1.17.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a68df0380e5c9d20ce49534f292a36a7514ae21350726efe1865bdb1fa91d278" +checksum = "c141e807189ad38a07276942c6623032d3753c8859c146104ac2e4d68865945a" dependencies = [ "alloy-rlp", "ark-ff 0.3.0", @@ -5002,6 +4498,16 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48fd7bd8a6377e15ad9d42a8ec25371b94ddc67abe7c8b9127bec79bebaaae18" +[[package]] +name = "rust-ini" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "796e8d2b6696392a43bea58116b667fb4c29727dc5abd27d6acf338bb4f688c7" +dependencies = [ + "cfg-if", + "ordered-multimap", +] + [[package]] name = "rustc-demangle" version = "0.1.27" @@ -5040,9 +4546,9 @@ dependencies = [ [[package]] name = "rustix" -version = "1.1.3" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" dependencies = [ "bitflags", "errno", @@ -5053,11 +4559,12 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.35" +version = "0.23.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "533f54bc6a7d4f647e46ad909549eda97bf5afc1585190ef692b4286b198bd8f" +checksum = "758025cb5fccfd3bc2fd74708fd4682be41d99e5dff73c377c0646c6012c73a4" dependencies = [ "aws-lc-rs", + "log", "once_cell", "ring", "rustls-pki-types", @@ -5072,17 +4579,17 @@ version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "612460d5f7bea540c490b2b6395d8e34a953e52b491accd6c86c8164c5932a63" dependencies = [ - "openssl-probe 0.2.1", + "openssl-probe", "rustls-pki-types", "schannel", - "security-framework 3.5.1", + "security-framework", ] [[package]] name = "rustls-pki-types" -version = "1.13.2" +version = "1.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21e6f2ab2928ca4291b86736a8bd920a277a399bba1589409d72154ff87c1282" +checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd" dependencies = [ "web-time", "zeroize", @@ -5094,7 +4601,7 @@ version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d99feebc72bae7ab76ba994bb5e121b8d83d910ca40b36e0921f53becc41784" dependencies = [ - "core-foundation 0.10.1", + "core-foundation", "core-foundation-sys", "jni", "log", @@ -5103,7 +4610,7 @@ dependencies = [ "rustls-native-certs", "rustls-platform-verifier-android", "rustls-webpki", - "security-framework 3.5.1", + "security-framework", "security-framework-sys", "webpki-root-certs", "windows-sys 0.61.2", @@ -5117,9 +4624,9 @@ checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" [[package]] name = "rustls-webpki" -version = "0.103.8" +version = "0.103.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ffdfa2f5286e2247234e03f680868ac2815974dc39e00ea15adc445d0aafe52" +checksum = "d7df23109aa6c1567d1c575b9952556388da57401e4ace1d15f79eedad0d8f53" dependencies = [ "aws-lc-rs", "ring", @@ -5147,9 +4654,9 @@ dependencies = [ [[package]] name = "ryu" -version = "1.0.21" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62049b2877bf12821e8f9ad256ee38fdc31db7387ec2d3b3f403024de2034aea" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" [[package]] name = "same-file" @@ -5162,9 +4669,9 @@ dependencies = [ [[package]] name = "schannel" -version = "0.1.28" +version = "0.1.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1" +checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" dependencies = [ "windows-sys 0.61.2", ] @@ -5183,9 +4690,9 @@ dependencies = [ [[package]] name = "schemars" -version = "1.1.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9558e172d4e8533736ba97870c4b2cd63f84b382a3d6eb063da41b91cce17289" +checksum = "a2b42f36aa1cd011945615b92222f6bf73c599a102a300334cd7f8dbeec726cc" dependencies = [ "dyn-clone", "ref-cast", @@ -5220,7 +4727,7 @@ version = "0.30.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b50c5943d326858130af85e049f2661ba3c78b26589b8ab98e65e80ae44a1252" dependencies = [ - "bitcoin_hashes 0.14.1", + "bitcoin_hashes", "rand 0.8.5", "secp256k1-sys", "serde", @@ -5237,25 +4744,12 @@ dependencies = [ [[package]] name = "security-framework" -version = "2.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" -dependencies = [ - "bitflags", - "core-foundation 0.9.4", - "core-foundation-sys", - "libc", - "security-framework-sys", -] - -[[package]] -name = "security-framework" -version = "3.5.1" +version = "3.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3297343eaf830f66ede390ea39da1d462b6b0c1b000f420d0a83f898bbbe6ef" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" dependencies = [ "bitflags", - "core-foundation 0.10.1", + "core-foundation", "core-foundation-sys", "libc", "security-framework-sys", @@ -5263,9 +4757,9 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.15.0" +version = "2.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc1f0cbffaac4852523ce30d8bd3c5cdc873501d96ff467ca09b6767bb8cd5c0" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" dependencies = [ "core-foundation-sys", "libc", @@ -5311,6 +4805,18 @@ dependencies = [ "serde_derive", ] +[[package]] +name = "serde-untagged" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9faf48a4a2d2693be24c6289dbe26552776eb7737074e6722891fadbe6c5058" +dependencies = [ + "erased-serde", + "serde", + "serde_core", + "typeid", +] + [[package]] name = "serde_core" version = "1.0.228" @@ -5328,14 +4834,14 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.117", ] [[package]] name = "serde_json" -version = "1.0.147" +version = "1.0.149" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6af14725505314343e673e9ecb7cd7e8a36aa9791eb936235a3567cc31447ae4" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" dependencies = [ "itoa", "memchr", @@ -5355,6 +4861,15 @@ dependencies = [ "serde_core", ] +[[package]] +name = "serde_spanned" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8bbf91e5a4d6315eee45e704372590b30e260ee83af6639d64557f51b067776" +dependencies = [ + "serde_core", +] + [[package]] name = "serde_urlencoded" version = "0.7.1" @@ -5369,17 +4884,17 @@ dependencies = [ [[package]] name = "serde_with" -version = "3.16.1" +version = "3.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fa237f2807440d238e0364a218270b98f767a00d3dada77b1c53ae88940e2e7" +checksum = "dd5414fad8e6907dbdd5bc441a50ae8d6e26151a03b1de04d89a5576de61d01f" dependencies = [ "base64 0.22.1", "chrono", "hex", "indexmap 1.9.3", - "indexmap 2.12.1", + "indexmap 2.13.0", "schemars 0.9.0", - "schemars 1.1.0", + "schemars 1.2.1", "serde_core", "serde_json", "serde_with_macros", @@ -5388,14 +4903,14 @@ dependencies = [ [[package]] name = "serde_with_macros" -version = "3.16.1" +version = "3.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52a8e3ca0ca629121f70ab50f95249e5a6f925cc0f6ffe8256c45b728875706c" +checksum = "d3db8978e608f1fe7357e211969fd9abdcae80bac1ba7a3369bb7eb6b404eb65" dependencies = [ - "darling", + "darling 0.23.0", "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.117", ] [[package]] @@ -5419,30 +4934,6 @@ dependencies = [ "digest 0.10.7", ] -[[package]] -name = "sha1" -version = "0.11.0-rc.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa1ae819b9870cadc959a052363de870944a1646932d274a4e270f64bf79e5ef" -dependencies = [ - "cfg-if", - "cpufeatures 0.2.17", - "digest 0.11.0-rc.4", -] - -[[package]] -name = "sha2" -version = "0.9.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d58a1e1bf39749807d89cf2d98ac2dfa0ff1cb3faa38fbb64dd88ac8013d800" -dependencies = [ - "block-buffer 0.9.0", - "cfg-if", - "cpufeatures 0.2.17", - "digest 0.9.0", - "opaque-debug", -] - [[package]] name = "sha2" version = "0.10.9" @@ -5454,17 +4945,6 @@ dependencies = [ "digest 0.10.7", ] -[[package]] -name = "sha2" -version = "0.11.0-rc.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19d43dc0354d88b791216bb5c1bfbb60c0814460cc653ae0ebd71f286d0bd927" -dependencies = [ - "cfg-if", - "cpufeatures 0.2.17", - "digest 0.11.0-rc.4", -] - [[package]] name = "sha3" version = "0.10.8" @@ -5472,24 +4952,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "75872d278a8f37ef87fa0ddbda7802605cb18344497949862c0d4dcb291eba60" dependencies = [ "digest 0.10.7", - "keccak 0.1.5", -] - -[[package]] -name = "sha3" -version = "0.11.0-rc.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2103ca0e6f4e9505eae906de5e5883e06fc3b2232fb5d6914890c7bbcb62f478" -dependencies = [ - "digest 0.11.0-rc.4", - "keccak 0.2.0-rc.0", + "keccak", ] [[package]] name = "sha3-asm" -version = "0.1.4" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c28efc5e327c837aa837c59eae585fc250715ef939ac32881bcc11677cd02d46" +checksum = "b31139435f327c93c6038ed350ae4588e2c70a13d50599509fee6349967ba35a" dependencies = [ "cc", "cfg-if", @@ -5512,10 +4982,11 @@ checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] name = "signal-hook-registry" -version = "1.4.7" +version = "1.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7664a098b8e616bdfcc2dc0e9ac44eb231eedf41db4e9fe95d8d32ec728dedad" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" dependencies = [ + "errno", "libc", ] @@ -5529,11 +5000,17 @@ dependencies = [ "rand_core 0.6.4", ] +[[package]] +name = "siphasher" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2aa850e253778c88a04c3d7323b043aeda9d3e30d5971937c1855769763678e" + [[package]] name = "slab" -version = "0.4.11" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" [[package]] name = "smallvec" @@ -5546,12 +5023,21 @@ dependencies = [ [[package]] name = "socket2" -version = "0.6.1" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17129e116933cf371d018bb80ae557e889637989d8638274fb25622827b03881" +checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" dependencies = [ "libc", - "windows-sys 0.60.2", + "windows-sys 0.61.2", +] + +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +dependencies = [ + "lock_api", ] [[package]] @@ -5573,6 +5059,194 @@ dependencies = [ "der", ] +[[package]] +name = "sqlx" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fefb893899429669dcdd979aff487bd78f4064e5e7907e4269081e0ef7d97dc" +dependencies = [ + "sqlx-core", + "sqlx-macros", + "sqlx-mysql", + "sqlx-postgres", + "sqlx-sqlite", +] + +[[package]] +name = "sqlx-core" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee6798b1838b6a0f69c007c133b8df5866302197e404e8b6ee8ed3e3a5e68dc6" +dependencies = [ + "base64 0.22.1", + "bytes", + "crc", + "crossbeam-queue", + "either", + "event-listener", + "futures-core", + "futures-intrusive", + "futures-io", + "futures-util", + "hashbrown 0.15.5", + "hashlink", + "indexmap 2.13.0", + "log", + "memchr", + "once_cell", + "percent-encoding", + "serde", + "serde_json", + "sha2", + "smallvec", + "thiserror 2.0.18", + "tokio", + "tokio-stream", + "tracing", + "url", +] + +[[package]] +name = "sqlx-macros" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2d452988ccaacfbf5e0bdbc348fb91d7c8af5bee192173ac3636b5fb6e6715d" +dependencies = [ + "proc-macro2", + "quote", + "sqlx-core", + "sqlx-macros-core", + "syn 2.0.117", +] + +[[package]] +name = "sqlx-macros-core" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19a9c1841124ac5a61741f96e1d9e2ec77424bf323962dd894bdb93f37d5219b" +dependencies = [ + "dotenvy", + "either", + "heck", + "hex", + "once_cell", + "proc-macro2", + "quote", + "serde", + "serde_json", + "sha2", + "sqlx-core", + "sqlx-mysql", + "sqlx-postgres", + "sqlx-sqlite", + "syn 2.0.117", + "tokio", + "url", +] + +[[package]] +name = "sqlx-mysql" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa003f0038df784eb8fecbbac13affe3da23b45194bd57dba231c8f48199c526" +dependencies = [ + "atoi", + "base64 0.22.1", + "bitflags", + "byteorder", + "bytes", + "crc", + "digest 0.10.7", + "dotenvy", + "either", + "futures-channel", + "futures-core", + "futures-io", + "futures-util", + "generic-array", + "hex", + "hkdf", + "hmac", + "itoa", + "log", + "md-5", + "memchr", + "once_cell", + "percent-encoding", + "rand 0.8.5", + "rsa", + "serde", + "sha1", + "sha2", + "smallvec", + "sqlx-core", + "stringprep", + "thiserror 2.0.18", + "tracing", + "whoami", +] + +[[package]] +name = "sqlx-postgres" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db58fcd5a53cf07c184b154801ff91347e4c30d17a3562a635ff028ad5deda46" +dependencies = [ + "atoi", + "base64 0.22.1", + "bitflags", + "byteorder", + "crc", + "dotenvy", + "etcetera", + "futures-channel", + "futures-core", + "futures-util", + "hex", + "hkdf", + "hmac", + "home", + "itoa", + "log", + "md-5", + "memchr", + "once_cell", + "rand 0.8.5", + "serde", + "serde_json", + "sha2", + "smallvec", + "sqlx-core", + "stringprep", + "thiserror 2.0.18", + "tracing", + "whoami", +] + +[[package]] +name = "sqlx-sqlite" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2d12fe70b2c1b4401038055f90f151b78208de1f9f89a7dbfd41587a10c3eea" +dependencies = [ + "atoi", + "flume", + "futures-channel", + "futures-core", + "futures-executor", + "futures-intrusive", + "futures-util", + "libsqlite3-sys", + "log", + "percent-encoding", + "serde", + "serde_urlencoded", + "sqlx-core", + "thiserror 2.0.18", + "tracing", + "url", +] + [[package]] name = "stable_deref_trait" version = "1.2.1" @@ -5585,6 +5259,17 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" +[[package]] +name = "stringprep" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b4df3d392d81bd458a8a621b8bffbd2302a12ffe288a9d931670948749463b1" +dependencies = [ + "unicode-bidi", + "unicode-normalization", + "unicode-properties", +] + [[package]] name = "strsim" version = "0.11.1" @@ -5597,7 +5282,16 @@ version = "0.27.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf" dependencies = [ - "strum_macros", + "strum_macros 0.27.2", +] + +[[package]] +name = "strum" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9628de9b8791db39ceda2b119bbe13134770b56c138ec1d3af810d045c04f9bd" +dependencies = [ + "strum_macros 0.28.0", ] [[package]] @@ -5609,20 +5303,26 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.117", ] [[package]] -name = "subtle" -version = "2.6.1" +name = "strum_macros" +version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" +checksum = "ab85eea0270ee17587ed4156089e10b9e6880ee688791d45a905f5b1ca36f664" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.117", +] [[package]] -name = "subtle-ng" -version = "2.5.0" +name = "subtle" +version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "734676eb262c623cec13c3155096e08d1f8f29adce39ba17948b18dad1e54142" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "syn" @@ -5637,9 +5337,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.111" +version = "2.0.117" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" dependencies = [ "proc-macro2", "quote", @@ -5648,14 +5348,14 @@ dependencies = [ [[package]] name = "syn-solidity" -version = "1.5.2" +version = "1.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f92d01b5de07eaf324f7fca61cc6bd3d82bbc1de5b6c963e6fe79e86f36580d" +checksum = "53f425ae0b12e2f5ae65542e00898d500d4d318b4baf09f40fd0d410454e9947" dependencies = [ "paste", "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.117", ] [[package]] @@ -5675,21 +5375,7 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", -] - -[[package]] -name = "sysinfo" -version = "0.33.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fc858248ea01b66f19d8e8a6d55f41deaf91e9d495246fd01368d99935c6c01" -dependencies = [ - "core-foundation-sys", - "libc", - "memchr", - "ntapi", - "rayon", - "windows", + "syn 2.0.117", ] [[package]] @@ -5700,26 +5386,17 @@ checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" [[package]] name = "tempfile" -version = "3.24.0" +version = "3.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "655da9c7eb6305c55742045d5a8d2037996d61d8de95806335c7c86ce0f82e9c" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ "fastrand", - "getrandom 0.3.4", + "getrandom 0.4.2", "once_cell", "rustix", "windows-sys 0.61.2", ] -[[package]] -name = "termcolor" -version = "1.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" -dependencies = [ - "winapi-util", -] - [[package]] name = "thiserror" version = "1.0.69" @@ -5731,11 +5408,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.17" +version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" dependencies = [ - "thiserror-impl 2.0.17", + "thiserror-impl 2.0.18", ] [[package]] @@ -5746,18 +5423,18 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.117", ] [[package]] name = "thiserror-impl" -version = "2.0.17" +version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.117", ] [[package]] @@ -5780,30 +5457,30 @@ dependencies = [ [[package]] name = "time" -version = "0.3.44" +version = "0.3.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91e7d9e3bb61134e77bde20dd4825b97c010155709965fedf0f49bb138e52a9d" +checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" dependencies = [ "deranged", "itoa", "num-conv", "powerfmt", - "serde", + "serde_core", "time-core", "time-macros", ] [[package]] name = "time-core" -version = "0.1.6" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40868e7c1d2f0b8d73e4a8c7f0ff63af4f6d19be117e90bd73eb1d62cf831c6b" +checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" [[package]] name = "time-macros" -version = "0.2.24" +version = "0.2.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30cfb0125f12d9c277f35663a0a33f8c30190f4e4574868a330595412d34ebf3" +checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" dependencies = [ "num-conv", "time-core", @@ -5840,9 +5517,9 @@ dependencies = [ [[package]] name = "tinyvec" -version = "1.10.0" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" +checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" dependencies = [ "tinyvec_macros", ] @@ -5855,9 +5532,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.48.0" +version = "1.50.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff360e02eab121e0bc37a2d3b4d4dc622e6eda3a8e5253d5435ecf5bd4c68408" +checksum = "27ad5e34374e03cfffefc301becb44e9dc3c17584f414349ebe29ed26661822d" dependencies = [ "bytes", "libc", @@ -5872,13 +5549,13 @@ dependencies = [ [[package]] name = "tokio-macros" -version = "2.6.0" +version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" +checksum = "5c55a2eff8b69ce66c84f85e1da1c233edc36ceb85a2058d11b0d6a3c7e7569c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.117", ] [[package]] @@ -5903,9 +5580,9 @@ dependencies = [ [[package]] name = "tokio-stream" -version = "0.1.17" +version = "0.1.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047" +checksum = "32da49809aab5c3bc678af03902d4ccddea2a87d028d86392a4b1560c6906c70" dependencies = [ "futures-core", "pin-project-lite", @@ -5931,9 +5608,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.17" +version = "0.7.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2efa149fe76073d6e8fd97ef4f4eca7b67f599660115591483572e406e165594" +checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" dependencies = [ "bytes", "futures-core", @@ -5942,22 +5619,35 @@ dependencies = [ "tokio", ] +[[package]] +name = "toml" +version = "1.0.6+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "399b1124a3c9e16766831c6bba21e50192572cdd98706ea114f9502509686ffc" +dependencies = [ + "serde_core", + "serde_spanned", + "toml_datetime", + "toml_parser", + "winnow", +] + [[package]] name = "toml_datetime" -version = "0.7.5+spec-1.1.0" +version = "1.0.0+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" +checksum = "32c2555c699578a4f59f0cc68e5116c8d7cabbd45e1409b989d4be085b53f13e" dependencies = [ "serde_core", ] [[package]] name = "toml_edit" -version = "0.23.10+spec-1.0.0" +version = "0.25.4+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84c8b9f757e028cee9fa244aea147aab2a9ec09d5325a9b01e0a49730c2b5269" +checksum = "7193cbd0ce53dc966037f54351dbbcf0d5a642c7f0038c382ef9e677ce8c13f2" dependencies = [ - "indexmap 2.12.1", + "indexmap 2.13.0", "toml_datetime", "toml_parser", "winnow", @@ -5965,39 +5655,18 @@ dependencies = [ [[package]] name = "toml_parser" -version = "1.0.6+spec-1.1.0" +version = "1.0.9+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3198b4b0a8e11f09dd03e133c0280504d0801269e9afa46362ffde1cbeebf44" +checksum = "702d4415e08923e7e1ef96cd5727c0dfed80b4d2fa25db9647fe5eb6f7c5a4c4" dependencies = [ "winnow", ] -[[package]] -name = "tonic" -version = "0.12.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "877c5b330756d856ffcc4553ab34a5684481ade925ecc54bcd1bf02b1d0d4d52" -dependencies = [ - "async-trait", - "base64 0.22.1", - "bytes", - "http", - "http-body", - "http-body-util", - "percent-encoding", - "pin-project", - "prost", - "tokio-stream", - "tower-layer", - "tower-service", - "tracing", -] - [[package]] name = "tower" -version = "0.5.2" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" dependencies = [ "futures-core", "futures-util", @@ -6006,7 +5675,6 @@ dependencies = [ "tokio", "tower-layer", "tower-service", - "tracing", ] [[package]] @@ -6059,7 +5727,7 @@ checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.117", ] [[package]] @@ -6093,53 +5761,22 @@ dependencies = [ "tracing-core", ] -[[package]] -name = "tracing-opentelemetry" -version = "0.29.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "721f2d2569dce9f3dfbbddee5906941e953bfcdf736a62da3377f5751650cc36" -dependencies = [ - "js-sys", - "once_cell", - "opentelemetry", - "opentelemetry_sdk", - "smallvec", - "tracing", - "tracing-core", - "tracing-log", - "tracing-subscriber", - "web-time", -] - -[[package]] -name = "tracing-serde" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "704b1aeb7be0d0a84fc9828cae51dab5970fee5088f83d1dd7ee6f6246fc6ff1" -dependencies = [ - "serde", - "tracing-core", -] - [[package]] name = "tracing-subscriber" -version = "0.3.22" +version = "0.3.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e" +checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" dependencies = [ "matchers", "nu-ansi-term", "once_cell", - "regex-automata", - "serde", - "serde_json", + "regex-automata", "sharded-slab", "smallvec", "thread_local", "tracing", "tracing-core", "tracing-log", - "tracing-serde", ] [[package]] @@ -6162,11 +5799,17 @@ dependencies = [ "rand 0.9.2", "rustls", "rustls-pki-types", - "sha1 0.10.6", - "thiserror 2.0.17", + "sha1", + "thiserror 2.0.18", "utf-8", ] +[[package]] +name = "typeid" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c" + [[package]] name = "typenum" version = "1.19.0" @@ -6197,11 +5840,32 @@ version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94" +[[package]] +name = "unicode-bidi" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" + [[package]] name = "unicode-ident" -version = "1.0.22" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-normalization" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-properties" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" +checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d" [[package]] name = "unicode-segmentation" @@ -6215,16 +5879,6 @@ version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" -[[package]] -name = "universal-hash" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" -dependencies = [ - "crypto-common 0.1.7", - "subtle", -] - [[package]] name = "untrusted" version = "0.9.0" @@ -6233,14 +5887,15 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "url" -version = "2.5.7" +version = "2.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" dependencies = [ "form_urlencoded", "idna", "percent-encoding", "serde", + "serde_derive", ] [[package]] @@ -6261,44 +5916,66 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" +[[package]] +name = "uts-beacon-injector" +version = "0.1.0-alpha.1" +dependencies = [ + "alloy-primitives", + "alloy-provider", + "alloy-rpc-client", + "alloy-signer-local", + "alloy-sol-types", + "config", + "eyre", + "reqwest 0.13.2", + "serde", + "tokio", + "tokio-util", + "tracing", + "tracing-subscriber", + "url", + "uts-bmt", + "uts-contracts", +] + [[package]] name = "uts-bmt" -version = "0.1.0" +version = "0.1.2" dependencies = [ - "bytemuck", - "commonware-cryptography", - "commonware-storage", - "criterion 0.8.1", - "digest 0.11.0-rc.4", - "hybrid-array", - "sha2 0.11.0-rc.3", - "sha3 0.11.0-rc.3", + "alloy-primitives", + "alloy-sol-types", + "digest 0.10.7", + "sha2", + "sha3", ] [[package]] name = "uts-calendar" -version = "0.1.0" +version = "0.1.0-alpha.1" dependencies = [ + "alloy-chains", "alloy-primitives", "alloy-provider", + "alloy-rpc-client", "alloy-signer", "alloy-signer-local", "axum", "bump-scope", - "bytemuck", "bytes", - "criterion 0.8.1", - "digest 0.11.0-rc.4", + "config", + "criterion", + "digest 0.10.7", "eyre", - "hashbrown 0.15.5", "itoa", "rocksdb", - "sha3 0.11.0-rc.3", + "serde", + "sha3", + "sqlx", "tokio", + "tokio-util", "tower-http", "tracing", "tracing-subscriber", - "uts-bmt", "uts-contracts", "uts-core", "uts-journal", @@ -6307,102 +5984,170 @@ dependencies = [ [[package]] name = "uts-cli" -version = "0.1.0" +version = "0.1.0-alpha.1" dependencies = [ - "alloy-provider", - "bytemuck", "clap", "color-eyre", - "digest 0.11.0-rc.4", + "digest 0.10.7", "eyre", "futures", - "jiff", - "rand 0.10.0", - "reqwest 0.13.2", - "ripemd 0.2.0-rc.3", - "sha1 0.11.0-rc.3", - "sha2 0.11.0-rc.3", - "sha3 0.11.0-rc.3", + "ripemd", + "sha1", + "sha2", + "sha3", "tokio", "tracing", + "tracing-subscriber", "url", - "uts-bmt", "uts-core", + "uts-sdk", ] [[package]] name = "uts-contracts" -version = "0.1.0" +version = "0.1.0-alpha.1" dependencies = [ - "alloy", "alloy-contract", "alloy-primitives", "alloy-sol-types", - "eyre", - "futures", - "tokio", + "alloy-transport", + "clap", + "phf", + "serde", ] [[package]] name = "uts-core" -version = "0.1.0" +version = "0.1.0-alpha.1" dependencies = [ + "allocator-api2 0.4.0", "alloy-chains", + "alloy-contract", "alloy-primitives", "alloy-provider", - "alloy-rpc-types-eth", "alloy-sol-types", "auto_impl", + "backon", "bytes", - "criterion 0.5.1", - "digest 0.11.0-rc.4", - "hex", - "once_cell", - "opentimestamps", + "digest 0.10.7", + "http", + "http-body-util", "paste", - "ripemd 0.2.0-rc.3", + "reqwest 0.13.2", + "ripemd", "serde", "serde_json", "serde_with", - "sha1 0.11.0-rc.3", - "sha2 0.11.0-rc.3", - "sha3 0.11.0-rc.3", - "thiserror 2.0.17", + "sha1", + "sha2", + "sha3", + "thiserror 2.0.18", "tokio", "tracing", + "url", "uts-bmt", "uts-contracts", ] [[package]] name = "uts-journal" -version = "0.1.0" +version = "0.1.1" dependencies = [ "eyre", + "rocksdb", "tempfile", - "thiserror 2.0.17", + "thiserror 2.0.18", + "tokio", + "tracing", +] + +[[package]] +name = "uts-relayer" +version = "0.1.0-alpha.1" +dependencies = [ + "alloy-contract", + "alloy-primitives", + "alloy-provider", + "alloy-rpc-client", + "alloy-rpc-types-eth", + "alloy-signer-local", + "alloy-sol-types", + "axum", + "config", + "eyre", + "futures", + "jiff", + "rustls", + "serde", + "sha3", + "sqlx", + "strum 0.28.0", + "tokio", + "tokio-util", + "tracing", + "tracing-subscriber", + "uts-bmt", + "uts-contracts", + "uts-sql", +] + +[[package]] +name = "uts-sdk" +version = "0.1.0-alpha.1" +dependencies = [ + "alloy-primitives", + "alloy-provider", + "alloy-rpc-client", + "backon", + "bytes", + "digest 0.10.7", + "futures", + "http", + "http-body-util", + "jiff", + "rand 0.10.0", + "reqwest 0.13.2", + "ripemd", + "sha1", + "sha2", + "sha3", + "thiserror 2.0.18", "tokio", "tracing", + "url", + "uts-bmt", + "uts-contracts", + "uts-core", +] + +[[package]] +name = "uts-sql" +version = "0.1.0-alpha.1" +dependencies = [ + "alloy-primitives", + "sqlx", ] [[package]] name = "uts-stamper" -version = "0.1.0" +version = "0.1.0-alpha.1" dependencies = [ "alloy-primitives", "alloy-provider", - "bitcode", - "bytemuck", - "digest 0.11.0-rc.4", + "digest 0.10.7", + "eyre", "rocksdb", "serde", - "thiserror 2.0.17", + "sqlx", + "strum 0.28.0", "tokio", + "tokio-util", "tracing", "uts-bmt", "uts-contracts", "uts-core", "uts-journal", + "uts-sql", ] [[package]] @@ -6459,11 +6204,11 @@ checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] name = "wasip2" -version = "1.0.1+wasi-0.2.4" +version = "1.0.2+wasi-0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" +checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" dependencies = [ - "wit-bindgen 0.46.0", + "wit-bindgen", ] [[package]] @@ -6472,14 +6217,20 @@ version = "0.4.0+wasi-0.3.0-rc-2026-01-06" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" dependencies = [ - "wit-bindgen 0.51.0", + "wit-bindgen", ] +[[package]] +name = "wasite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" + [[package]] name = "wasm-bindgen" -version = "0.2.106" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d759f433fa64a2d763d1340820e46e111a7a5ab75f993d1852d70b03dbb80fd" +checksum = "6532f9a5c1ece3798cb1c2cfdba640b9b3ba884f5db45973a6f442510a87d38e" dependencies = [ "cfg-if", "once_cell", @@ -6490,11 +6241,12 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.56" +version = "0.4.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "836d9622d604feee9e5de25ac10e3ea5f2d65b41eac0d9ce72eb5deae707ce7c" +checksum = "e9c5522b3a28661442748e09d40924dfb9ca614b21c00d3fd135720e48b67db8" dependencies = [ "cfg-if", + "futures-util", "js-sys", "once_cell", "wasm-bindgen", @@ -6503,9 +6255,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.106" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48cb0d2638f8baedbc542ed444afc0644a29166f1595371af4fecf8ce1e7eeb3" +checksum = "18a2d50fcf105fb33bb15f00e7a77b772945a2ee45dcf454961fd843e74c18e6" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -6513,22 +6265,22 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.106" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cefb59d5cd5f92d9dcf80e4683949f15ca4b511f4ac0a6e14d4e1ac60c6ecd40" +checksum = "03ce4caeaac547cdf713d280eda22a730824dd11e6b8c3ca9e42247b25c631e3" dependencies = [ "bumpalo", "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.117", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.106" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cbc538057e648b67f72a982e708d485b2efa771e1ac05fec311f9f63e5800db4" +checksum = "75a326b8c223ee17883a4251907455a2431acc2791c98c26279376490c378c16" dependencies = [ "unicode-ident", ] @@ -6550,7 +6302,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" dependencies = [ "anyhow", - "indexmap 2.12.1", + "indexmap 2.13.0", "wasm-encoder", "wasmparser", ] @@ -6563,7 +6315,7 @@ checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" dependencies = [ "bitflags", "hashbrown 0.15.5", - "indexmap 2.12.1", + "indexmap 2.13.0", "semver 1.0.27", ] @@ -6583,9 +6335,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.83" +version = "0.3.91" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b32828d774c412041098d182a8b38b16ea816958e07cf40eec2bc080ae137ac" +checksum = "854ba17bb104abfb26ba36da9729addc7ce7f06f5c0f90f3c391f8461cca21f9" dependencies = [ "js-sys", "wasm-bindgen", @@ -6616,23 +6368,27 @@ version = "0.26.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" dependencies = [ - "webpki-roots 1.0.4", + "webpki-roots 1.0.6", ] [[package]] name = "webpki-roots" -version = "1.0.4" +version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2878ef029c47c6e8cf779119f20fcf52bde7ad42a731b2a304bc221df17571e" +checksum = "22cfaf3c063993ff62e73cb4311efde4db1efb31ab78a3e5c457939ad5cc0bed" dependencies = [ "rustls-pki-types", ] [[package]] -name = "widestring" -version = "1.2.1" +name = "whoami" +version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72069c3113ab32ab29e5584db3c6ec55d416895e60715417b5b883a357c3e471" +checksum = "5d4a4db5077702ca3015d3d02d74974948aba2ad9e12ab7df718ee64ccd7e97d" +dependencies = [ + "libredox", + "wasite", +] [[package]] name = "winapi" @@ -6665,52 +6421,19 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" -[[package]] -name = "windows" -version = "0.57.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12342cb4d8e3b046f3d80effd474a7a02447231330ef77d71daa6fbc40681143" -dependencies = [ - "windows-core 0.57.0", - "windows-targets 0.52.6", -] - -[[package]] -name = "windows-core" -version = "0.57.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2ed2439a290666cd67ecce2b0ffaad89c2a56b976b736e6ece670297897832d" -dependencies = [ - "windows-implement 0.57.0", - "windows-interface 0.57.0", - "windows-result 0.1.2", - "windows-targets 0.52.6", -] - [[package]] name = "windows-core" version = "0.62.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" dependencies = [ - "windows-implement 0.60.2", - "windows-interface 0.59.3", + "windows-implement", + "windows-interface", "windows-link", - "windows-result 0.4.1", + "windows-result", "windows-strings", ] -[[package]] -name = "windows-implement" -version = "0.57.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9107ddc059d5b6fbfbffdfa7a7fe3e22a226def0b2608f72e9d552763d3e1ad7" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.111", -] - [[package]] name = "windows-implement" version = "0.60.2" @@ -6719,18 +6442,7 @@ checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", -] - -[[package]] -name = "windows-interface" -version = "0.57.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29bee4b38ea3cde66011baa44dba677c432a78593e202392d1e9070cf2a7fca7" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.111", + "syn 2.0.117", ] [[package]] @@ -6741,7 +6453,7 @@ checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.117", ] [[package]] @@ -6750,15 +6462,6 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" -[[package]] -name = "windows-result" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e383302e8ec8515204254685643de10811af0ed97ea37210dc26fb0032647f8" -dependencies = [ - "windows-targets 0.52.6", -] - [[package]] name = "windows-result" version = "0.4.1" @@ -6786,6 +6489,15 @@ dependencies = [ "windows-targets 0.42.2", ] +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + [[package]] name = "windows-sys" version = "0.52.0" @@ -6828,6 +6540,21 @@ dependencies = [ "windows_x86_64_msvc 0.42.2", ] +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + [[package]] name = "windows-targets" version = "0.52.6" @@ -6867,6 +6594,12 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + [[package]] name = "windows_aarch64_gnullvm" version = "0.52.6" @@ -6885,6 +6618,12 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + [[package]] name = "windows_aarch64_msvc" version = "0.52.6" @@ -6903,6 +6642,12 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + [[package]] name = "windows_i686_gnu" version = "0.52.6" @@ -6933,6 +6678,12 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + [[package]] name = "windows_i686_msvc" version = "0.52.6" @@ -6951,6 +6702,12 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + [[package]] name = "windows_x86_64_gnu" version = "0.52.6" @@ -6969,6 +6726,12 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + [[package]] name = "windows_x86_64_gnullvm" version = "0.52.6" @@ -6987,6 +6750,12 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + [[package]] name = "windows_x86_64_msvc" version = "0.52.6" @@ -7001,19 +6770,13 @@ checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" [[package]] name = "winnow" -version = "0.7.14" +version = "0.7.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829" +checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" dependencies = [ "memchr", ] -[[package]] -name = "wit-bindgen" -version = "0.46.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" - [[package]] name = "wit-bindgen" version = "0.51.0" @@ -7042,9 +6805,9 @@ checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" dependencies = [ "anyhow", "heck", - "indexmap 2.12.1", + "indexmap 2.13.0", "prettyplease", - "syn 2.0.111", + "syn 2.0.117", "wasm-metadata", "wit-bindgen-core", "wit-component", @@ -7060,7 +6823,7 @@ dependencies = [ "prettyplease", "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.117", "wit-bindgen-core", "wit-bindgen-rust", ] @@ -7073,7 +6836,7 @@ checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" dependencies = [ "anyhow", "bitflags", - "indexmap 2.12.1", + "indexmap 2.13.0", "log", "serde", "serde_derive", @@ -7092,7 +6855,7 @@ checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" dependencies = [ "anyhow", "id-arena", - "indexmap 2.12.1", + "indexmap 2.13.0", "log", "semver 1.0.27", "serde", @@ -7121,7 +6884,7 @@ dependencies = [ "pharos", "rustc_version 0.4.1", "send_wrapper", - "thiserror 2.0.17", + "thiserror 2.0.18", "wasm-bindgen", "wasm-bindgen-futures", "web-sys", @@ -7137,15 +6900,14 @@ dependencies = [ ] [[package]] -name = "x25519-dalek" -version = "2.0.1" +name = "yaml-rust2" +version = "0.10.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7e468321c81fb07fa7f4c636c3972b9100f0346e5b6a9f2bd0603a52f7ed277" +checksum = "2462ea039c445496d8793d052e13787f2b90e750b833afee748e601c17621ed9" dependencies = [ - "curve25519-dalek", - "rand_core 0.6.4", - "serde", - "zeroize", + "arraydeque", + "encoding_rs", + "hashlink", ] [[package]] @@ -7167,28 +6929,28 @@ checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.117", "synstructure", ] [[package]] name = "zerocopy" -version = "0.8.31" +version = "0.8.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd74ec98b9250adb3ca554bdde269adf631549f51d8a8f8f0a10b50f1cb298c3" +checksum = "f2578b716f8a7a858b7f02d5bd870c14bf4ddbbcf3a4c05414ba6503640505e3" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.31" +version = "0.8.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8a8d209fdf45cf5138cbb5a506f6b52522a25afccc534d1475dad8e31105c6a" +checksum = "7e6cc098ea4d3bd6246687de65af3f920c430e236bee1e3bf2e441463f08a02f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.117", ] [[package]] @@ -7208,7 +6970,7 @@ checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.117", "synstructure", ] @@ -7223,13 +6985,13 @@ dependencies = [ [[package]] name = "zeroize_derive" -version = "1.4.2" +version = "1.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" +checksum = "85a5b4158499876c763cb03bc4e49185d3cccbabb15b33c627f7884f43db852e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.117", ] [[package]] @@ -7262,39 +7024,11 @@ checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.111", + "syn 2.0.117", ] [[package]] name = "zmij" -version = "0.1.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1dccf46b25b205e4bebe1d5258a991df1cc17801017a845cb5b3fe0269781aa" - -[[package]] -name = "zstd" -version = "0.13.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a" -dependencies = [ - "zstd-safe", -] - -[[package]] -name = "zstd-safe" -version = "7.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d" -dependencies = [ - "zstd-sys", -] - -[[package]] -name = "zstd-sys" -version = "2.0.16+zstd.1.5.7" +version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91e19ebc2adc8f83e43039e79776e3fda8ca919132d68a1fed6a5faca2683748" -dependencies = [ - "cc", - "pkg-config", -] +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/Cargo.toml b/Cargo.toml index 5fd53e0..f0e5c8d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,16 +1,13 @@ [workspace] -default-members = ["crates/cli"] -members = ["crates/*"] +members = ["crates/*", "packages/sdk-rs"] resolver = "3" [workspace.package] authors = ["Akase Haruka ", "Rohit Narurkar "] edition = "2024" homepage = "https://github.com/lightsing/uts/" -keywords = [] # TODO: add keywords -license = "MIT OR Apache-2.0" repository = "https://github.com/lightsing/uts.git" -version = "0.1.0" +version = "0.1.0-alpha.1" [workspace.lints.rust] missing-debug-implementations = "warn" @@ -29,21 +26,25 @@ unnecessary-box-returns = "warn" unnecessary-debug-formatting = "warn" [workspace.dependencies] +allocator-api2 = "0.4" alloy = "1" alloy-chains = "0.2" -alloy-contract = "1.2" +alloy-contract = "1.7" alloy-primitives = "1.5" -alloy-provider = "1.2" -alloy-rpc-types-eth = "1.6" -alloy-signer = "1.1" -alloy-signer-local = "1.1" +alloy-provider = { version = "1.7", default-features = false } +alloy-rpc-client = "1.7" +alloy-rpc-types-eth = "1.7" +alloy-signer = "1.7" +alloy-signer-local = "1.7" alloy-sol-types = "1.5" +alloy-transport = "1.7" +alloy-transport-ws = "1.7" auto_impl = "1.3" axum = { version = "0.8", default-features = false } axum-extra = "0.12" +backon = "1.6" bitcode = "0.6" -bump-scope = { version = "1.5", features = ["nightly"] } -bytemuck = "1" +bump-scope = { version = "2.2", features = ["allocator-api2-04"] } bytes = "1.11" cfg-if = "1.0" clap = { version = "4.5", features = ["derive"] } @@ -56,42 +57,48 @@ dyn-clone = "1.0" eyre = "0.6" futures = "0.3" hex = "0.4" +http = "1.4" +http-body-util = "0.1.3" itoa = "1.0" -jiff = "0.2.20" +jiff = "0.2" once_cell = { version = "1.21", default-features = false } paste = "1.0" +phf = { version = "0.13", default-features = false } rand = "0.10" regex = "1.12" reqwest = { version = "0.13", default-features = false } -rocksdb = "0.24" +rocksdb = { version = "0.24", default-features = false, features = ["bindgen-runtime"] } +rustls = "0.23" serde = "1.0" -serde-wasm-bindgen = "0.6" serde_json = "1.0" -serde_with = "3.16" -strum = "0.27" +serde_with = "3.17" +sqlx = "0.8" +strum = "0.28" thiserror = "2" tokio = { version = "1", features = ["rt"] } -toml = "0.9" +tokio-util = "0.7" +toml = "1.0" tower-http = "0.6" tracing = "0.1" tracing-subscriber = { version = "0.3", features = ["env-filter"] } url = "2.5" -wasm-bindgen = "0.2" -crypto-common = "0.2.0-rc.5" -digest = "0.11.0-rc.4" -hybrid-array = "0.4.5" -ripemd = "0.2.0-rc.3" -sha1 = "0.11.0-rc.3" -sha2 = "0.11.0-rc.3" -sha3 = "0.11.0-rc.3" + +crypto-common = "0.1" +digest = "0.10" +ripemd = "0.1" +sha1 = "0.10" +sha2 = "0.10" +sha3 = "0.10" tempfile = "3" -uts-bmt = { path = "crates/bmt" } -uts-contracts = { path = "crates/contracts" } -uts-core = { path = "crates/core" } -uts-journal = { path = "crates/journal" } -uts-stamper = { path = "crates/stamper" } +uts-bmt = { version = "0.1.2", path = "crates/bmt" } +uts-contracts = { version = "=0.1.0-alpha.1", path = "crates/contracts" } +uts-core = { version = "=0.1.0-alpha.1", path = "crates/core" } +uts-journal = { version = "0.1.1", path = "crates/journal" } +uts-sdk = { version = "=0.1.0-alpha.1", path = "packages/sdk-rs" } +uts-sql = { version = "=0.1.0-alpha.1", path = "crates/sql" } +uts-stamper = { version = "=0.1.0-alpha.1", path = "crates/stamper" } [profile.bench] codegen-units = 1 diff --git a/Dockerfile b/Dockerfile index 144bd91..f12097a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -15,20 +15,33 @@ RUN mkdir -p target/x86_64-unknown-linux-gnu/release && \ ln -s target/x86_64-unknown-linux-gnu/release target/x86_64-unknown-linux-gnu.2.17/release COPY --from=planner /app/recipe.json recipe.json -RUN cargo chef cook --release --recipe-path recipe.json --zigbuild --target x86_64-unknown-linux-gnu.2.17 +RUN cargo chef cook --release --recipe-path recipe.json --zigbuild --target x86_64-unknown-linux-gnu.2.17 --features performance COPY . . -RUN cargo zigbuild --release --bin uts-calendar --target x86_64-unknown-linux-gnu.2.17 --features performance -RUN cargo zigbuild --release --bin uts +RUN cargo zigbuild --release --workspace --target x86_64-unknown-linux-gnu.2.17 --features performance -FROM debian:trixie-slim AS cli-runtime +FROM debian:trixie-slim AS runtime + +WORKDIR /app +RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates && rm -rf /var/lib/apt/lists/* + +FROM runtime AS cli-runtime WORKDIR /app COPY --from=builder /app/target/x86_64-unknown-linux-gnu/release/uts /app/uts ENTRYPOINT ["/app/uts"] -FROM debian:trixie-slim AS calendar-runtime +FROM runtime AS calendar-runtime -WORKDIR /app COPY --from=builder /app/target/x86_64-unknown-linux-gnu/release/uts-calendar /app/uts-calendar ENTRYPOINT ["/app/uts-calendar"] + +FROM runtime AS relayer-runtime + +COPY --from=builder /app/target/x86_64-unknown-linux-gnu/release/uts-relayer /app/uts-relayer +ENTRYPOINT ["/app/uts-relayer"] + +FROM runtime AS beacon-injector-runtime + +COPY --from=builder /app/target/x86_64-unknown-linux-gnu/release/uts-beacon-injector /app/uts-beacon-injector +ENTRYPOINT ["/app/uts-beacon-injector"] diff --git a/LICENSE-AGPL b/LICENSE-AGPL new file mode 100644 index 0000000..be3f7b2 --- /dev/null +++ b/LICENSE-AGPL @@ -0,0 +1,661 @@ + GNU AFFERO GENERAL PUBLIC LICENSE + Version 3, 19 November 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +our General Public Licenses are intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + Developers that use our General Public Licenses protect your rights +with two steps: (1) assert copyright on the software, and (2) offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. + + A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. + + The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. + + An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU Affero General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Remote Network Interaction; Use with the GNU General Public License. + + Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the work with which it is combined will remain governed by version +3 of the GNU General Public License. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU Affero General Public License from time to time. Such new versions +will be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU Affero General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU Affero General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU Affero General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If your software can interact with users remotely through a computer +network, you should also make sure that it provides a way for users to +get its source. For example, if your program is a web application, its +interface could display a "Source" link that leads users to an archive +of the code. There are many ways you could offer source, and different +solutions will be better for different programs; see section 13 for the +specific requirements. + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU AGPL, see +. diff --git a/LICENSE-APACHE b/LICENSE-APACHE new file mode 100644 index 0000000..d645695 --- /dev/null +++ b/LICENSE-APACHE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/LICENSE-MIT b/LICENSE-MIT new file mode 100644 index 0000000..537b52b --- /dev/null +++ b/LICENSE-MIT @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 UTS Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index 9bc639b..72b0760 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,67 @@ # Universal Timestamps (UTS) +[![License](https://img.shields.io/badge/license-MIT%2FApache--2.0-blue.svg)](LICENSE-MIT) +[![CI (Rust)](https://github.com/lightsing/uts/actions/workflows/ci-rust.yml/badge.svg)](https://github.com/lightsing/uts/actions/workflows/ci-rust.yml) +![NPM Version](https://img.shields.io/npm/v/%40universal-timestamps%2Fsdk) + Universal Timestamps is a super set of [opentimestamps](https://opentimestamps.org/). +UTS batches user-submitted digests into Merkle trees and anchors the roots on-chain +via [EAS](https://attest.org/) attestations, providing trustless, verifiable +timestamps without relying on a single trusted calendar server. + +## Quick Start + +```bash +cargo install uts-cli --version 0.1.0-alpha.1 --locked +``` + +## Links + +- **Book**: +- **Calendar**: +- **Relayer**: + ## Development ### Pre-requisites -- Rust >= 1.94.0-nightly (e7d44143a 2025-12-24) -- Cargo >= 1.94.0-nightly (3861f60f6 2025-12-19) +- Rust Toolchain == 1.94.0 - pnpm >= 10.26.2 + +See [CONTRIBUTING.md](CONTRIBUTING.md) for full development setup and guidelines. + +## Supporting the Project + +You can support the UTS project by sending ETH directly to the operator address +displayed on the [calendar](https://lgm1.calendar.test.timestamps.now/) or +[relayer](https://lich.relayer.test.timestamps.now/) home page. + +## About Our Codenames: The Universe's Natural Clocks + +When choosing a codename theme, pulsars serve as the perfect physical metaphor. + +Often referred to as the "lighthouses of the universe" and "natural atomic clocks," +pulsars rotate at high speeds with incredibly stable and precise periods, emitting +regular pulses of electromagnetic radiation. Their timekeeping precision can even +rival that of humanity's most advanced atomic clocks. + +Adopting astronomically significant pulsars like LGM-1, Vela, or Swift as codenames +for our various environments or microservices does more than just inject a touch of +hardcore, geeky romance into the codebase. It represents our highest engineering +aspiration: an architecture that runs as eternally and precisely as the stars +themselves. + +Allocation Tracker: https://github.com/lightsing/uts/issues/46 + +## License + +This project uses a split licensing model: + +- **Core Libraries** are dual-licensed under [MIT](LICENSE-MIT) or + [Apache-2.0](LICENSE-APACHE), at your option. +- **All SDKs** are dual-licensed under [MIT](LICENSE-MIT) or + [Apache-2.0](LICENSE-APACHE), at your option. +- **Server components** are licensed under [AGPL-3.0](LICENSE-AGPL). + +See the individual directory for per-package license declarations. diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..a9fdda7 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,66 @@ +# Security Policy + +## Reporting a Vulnerability + +The UTS team takes security issues seriously. We appreciate your efforts to +responsibly disclose any vulnerabilities you find. + +**Please do NOT report security vulnerabilities through public GitHub issues.** + +Instead, please report them via one of the following methods: + +- **GitHub Private Vulnerability Reporting**: Use the + [Security Advisories](https://github.com/lightsing/uts/security/advisories) + page to privately report a vulnerability. +- **Email**: Send an email to **light.tsing@gmail.com** with the subject line + `[UTS Security]` followed by a brief description. + +### What to Include + +Please include as much of the following information as possible to help us +understand and reproduce the issue: + +- Description of the vulnerability +- Steps to reproduce or proof-of-concept +- Affected component(s) (e.g., `uts-core`, `uts-calendar`, `uts-relayer`, + smart contracts) +- Impact assessment (what an attacker could achieve) +- Any suggested fix or mitigation + +### Response Timeline + +- **Acknowledgment**: We will acknowledge receipt of your report within + **48 hours**. +- **Assessment**: We will provide an initial assessment within **1 week**. +- **Fix & Disclosure**: We aim to release a fix and coordinate disclosure within + **90 days**, depending on severity and complexity. + +## Supported Versions + +| Component | Version | Supported | +| --- | --- | --- | +| uts-cli | 0.1.0-alpha.x | :white_check_mark: | +| uts-core | 0.1.0-alpha.x | :white_check_mark: | +| uts-calendar | 0.1.0-alpha.x | :white_check_mark: | +| uts-relayer | 0.1.0-alpha.x | :white_check_mark: | +| Smart Contracts | Latest deployed | :white_check_mark: | + +## Security Considerations + +UTS is a timestamping protocol that anchors data on-chain. Key areas of security +concern include: + +- **Merkle tree integrity**: Correctness of the Binary Merkle Tree + implementation in `uts-bmt` +- **Proof verification**: Ensuring timestamp proofs cannot be forged or tampered + with in `uts-core` +- **Smart contract security**: EAS attestation and anchoring contract + correctness +- **Server-side security**: Calendar and relayer service hardening +- **Cross-chain anchoring**: L2-to-L1 relay integrity + +## Acknowledgments + +We are grateful to the security researchers and community members who help keep +UTS safe. Contributors who report valid vulnerabilities will be acknowledged +here (with their permission). diff --git a/apps/docs/.postcssrc.json b/apps/docs/.postcssrc.json new file mode 100644 index 0000000..edc6a49 --- /dev/null +++ b/apps/docs/.postcssrc.json @@ -0,0 +1,8 @@ +{ + "plugins": { + "postcss-rtlcss": { + "ltrPrefix": ":where([dir=\"ltr\"])", + "rtlPrefix": ":where([dir=\"rtl\"])" + } + } +} diff --git a/apps/docs/.vitepress/config.ts b/apps/docs/.vitepress/config.ts new file mode 100644 index 0000000..179bb13 --- /dev/null +++ b/apps/docs/.vitepress/config.ts @@ -0,0 +1,87 @@ +import { + defineConfig, + resolveSiteDataByRoute, + type HeadConfig, +} from 'vitepress' +import { + groupIconMdPlugin, + groupIconVitePlugin, +} from 'vitepress-plugin-group-icons' +import llmstxt from 'vitepress-plugin-llms' + +const prod = !!process.env.NETLIFY + +export default defineConfig({ + title: 'UTS', + description: 'Universal Timestamps - Decentralized timestamping protocol', + + rewrites: { + 'en/:rest*': ':rest*', + }, + + lastUpdated: true, + cleanUrls: true, + metaChunk: true, + + markdown: { + math: true, + config(md) { + md.use(groupIconMdPlugin) + }, + }, + + sitemap: { + hostname: 'https://docs.timestamps.now', + }, + + head: [ + ['link', { rel: 'icon', type: 'image/svg+xml', href: '/uts-logo.svg' }], + ['meta', { name: 'theme-color', content: '#5f67ee' }], + ['meta', { property: 'og:type', content: 'website' }], + ['meta', { property: 'og:site_name', content: 'UTS Documentation' }], + ['meta', { property: 'og:url', content: 'https://docs.timestamps.now/' }], + ], + + themeConfig: { + logo: { src: '/uts-logo.svg', width: 24, height: 24 }, + + socialLinks: [{ icon: 'github', link: 'https://github.com/lightsing/uts' }], + + search: { + provider: 'local', + }, + }, + + locales: { + root: { label: 'English', lang: 'en-US', dir: 'ltr' }, + zh: { label: '简体中文', lang: 'zh-Hans', dir: 'ltr' }, + }, + + vite: { + plugins: [ + groupIconVitePlugin(), + prod && + llmstxt({ + workDir: 'en', + ignoreFiles: ['index.md'], + }), + ], + experimental: { + enableNativePlugin: true, + }, + }, + + transformPageData: prod + ? (pageData, ctx) => { + const site = resolveSiteDataByRoute( + ctx.siteConfig.site, + pageData.relativePath, + ) + const title = `${pageData.title || site.title} | ${pageData.description || site.description}` + ;((pageData.frontmatter.head ??= []) as HeadConfig[]).push( + ['meta', { property: 'og:locale', content: site.lang }], + ['meta', { property: 'og:title', content: title }], + ) + } + : undefined, +}) diff --git a/apps/docs/.vitepress/theme/index.ts b/apps/docs/.vitepress/theme/index.ts new file mode 100644 index 0000000..0e7ba37 --- /dev/null +++ b/apps/docs/.vitepress/theme/index.ts @@ -0,0 +1,5 @@ +import Theme from 'vitepress/theme' +import 'virtual:group-icons.css' +import './styles.css' + +export default Theme diff --git a/apps/docs/.vitepress/theme/styles.css b/apps/docs/.vitepress/theme/styles.css new file mode 100644 index 0000000..9f205bb --- /dev/null +++ b/apps/docs/.vitepress/theme/styles.css @@ -0,0 +1,31 @@ +:root { + --vp-home-hero-name-color: transparent; + --vp-home-hero-name-background: -webkit-linear-gradient( + 120deg, + #6366f1 30%, + #8b5cf6 + ); + --vp-home-hero-image-background-image: linear-gradient( + -45deg, + #6366f1 50%, + #8b5cf6 50% + ); + --vp-home-hero-image-filter: blur(44px); +} + +@media (min-width: 640px) { + :root { + --vp-home-hero-image-filter: blur(56px); + } +} + +@media (min-width: 960px) { + :root { + --vp-home-hero-image-filter: blur(68px); + } +} + +.VPHero .VPImage { + filter: drop-shadow(-2px 4px 6px rgba(0, 0, 0, 0.2)); + padding: 18px; +} diff --git a/apps/docs/config.ts b/apps/docs/config.ts new file mode 100644 index 0000000..dd107db --- /dev/null +++ b/apps/docs/config.ts @@ -0,0 +1,81 @@ +import { createRequire } from 'module' +import { defineAdditionalConfig, type DefaultTheme } from 'vitepress' + +const require = createRequire(import.meta.url) + +export default defineAdditionalConfig({ + description: 'Universal Timestamps - Decentralized timestamping protocol', + + themeConfig: { + nav: nav(), + + sidebar: { + '/guide/': { base: '/guide/', items: sidebarGuide() }, + '/developer/': { base: '/developer/', items: sidebarDeveloper() }, + '/reference/': { base: '/reference/', items: sidebarReference() }, + }, + + editLink: { + pattern: 'https://github.com/lightsing/uts/edit/main/apps/docs/:path', + text: 'Edit this page on GitHub', + }, + + footer: { + message: 'Released under the MIT License.', + copyright: 'Copyright © 2026-present Universal Timestamps', + }, + }, +}) + +function nav(): DefaultTheme.NavItem[] { + return [ + { + text: 'Guide', + link: '/guide/what-is-uts', + activeMatch: '/guide/', + }, + { + text: 'Developer', + link: '/developer/overview', + activeMatch: '/developer/', + }, + { + text: 'Reference', + link: '/reference/further-reading', + activeMatch: '/reference/', + }, + ] +} + +function sidebarGuide(): DefaultTheme.SidebarItem[] { + return [ + { + text: 'Getting Started', + collapsed: false, + items: [ + { text: 'What is UTS?', link: 'what-is-uts' }, + { text: 'Stamp via CLI', link: 'stamp-via-cli' }, + ], + }, + ] +} + +function sidebarDeveloper(): DefaultTheme.SidebarItem[] { + return [ + { + text: 'Developer Guide', + collapsed: false, + items: [{ text: 'Architecture Overview', link: 'overview' }], + }, + ] +} + +function sidebarReference(): DefaultTheme.SidebarItem[] { + return [ + { + text: 'API Reference', + collapsed: false, + items: [{ text: 'Further Reading', link: 'further-reading' }], + }, + ] +} diff --git a/apps/docs/en/developer/overview.md b/apps/docs/en/developer/overview.md new file mode 100644 index 0000000..7f8f506 --- /dev/null +++ b/apps/docs/en/developer/overview.md @@ -0,0 +1,82 @@ +# Architecture Overview + +UTS implements a dual-layer timestamping architecture. + +## High-Level Flow + +``` +User → Calendar Server → Merkle Tree → EAS Attestation → Blockchain +``` + +## Components + +### Calendar Server + +HTTP server that: + +- Accepts digest submissions +- Batches digests into Merkle trees +- Returns timestamp proofs + +### Stamper + +Batching engine that: + +- Builds Merkle trees from pending digests +- Submits attestations via EAS +- Manages calendar state + +### Relayer + +Cross-chain service that: + +- Anchors L2 roots to L1 +- Bridges attestation proofs back to L2 + +## Dual-Layer Architecture + +### L2 Direct Path (Fast) + +1. User submits digest to calendar +2. Stamper batches into Merkle tree +3. Root attested on L2 via EAS (Scroll) +4. User receives proof in seconds + +### L1 Anchoring Path (Secure) + +1. L2 attestation roots collected +2. Batched into L1 Merkle tree +3. Anchored on Ethereum mainnet +4. Provides maximum security + +## Data Flow + +``` +┌─────────────┐ ┌─────────────┐ ┌─────────────┐ +│ User │────▶│ Calendar │────▶│ Stamper │ +└─────────────┘ └─────────────┘ └─────────────┘ + │ + ▼ + ┌─────────────┐ + │ EAS │ + └─────────────┘ + │ + ┌────────────────────┴────────────────────┐ + ▼ ▼ + ┌─────────────┐ ┌─────────────┐ + │ L2 (Scroll)│ │ L1 Relayer │ + └─────────────┘ └─────────────┘ + │ + ▼ + ┌─────────────┐ + │ Ethereum │ + └─────────────┘ +``` + +## Learn More + +For detailed implementation, see the [Reference Book](https://book.timestamps.now): + +- [System Architecture](https://book.timestamps.now/architecture.html) +- [Calendar Pipeline](https://book.timestamps.now/calendar-pipeline/submission.html) +- [L1 Anchoring](https://book.timestamps.now/l1-anchoring/contracts.html) diff --git a/apps/docs/en/guide/stamp-via-cli.md b/apps/docs/en/guide/stamp-via-cli.md new file mode 100644 index 0000000..c6c3f2e --- /dev/null +++ b/apps/docs/en/guide/stamp-via-cli.md @@ -0,0 +1,67 @@ +# Stamp via CLI + +Get your first timestamp in under 5 minutes. + +## Prerequisites + +- Rust Nightly +- A file to timestamp + +## Install the CLI + +```bash +cargo install uts-cli --version 0.1.0-alpha.0 --locked +``` + +Or build from source: + +```bash +git clone https://github.com/lightsing/uts.git +cd uts +cargo install --path crates/cli +``` + +## Timestamp a File + +```bash +uts stamp myfile.txt +``` + +This creates `myfile.txt.ots` containing a pending timestamp proof. + +> **Note**: +> +> The pending timestamp cannot prove the existence of the file, and relies +> on the liveness of the calendar server to retrieve the full proof later. + +## Upgrade the Timestamp + +Wait for the calendar server to attest the batch on-chain (usually ~10 seconds), then run: + +```bash +uts upgrade myfile.txt.ots +``` + +This command retrieves the full self-contained proof. + +> **Note**: +> +> Due to the privacy-preserving design of UTS, the calendar server does not +> associate the file hash with the timestamp proof. +> +> Before the [UIP-1](https://github.com/lightsing/uts/issues/59) upgrade, the +> loss of the `.ots` file means permanent loss of the timestamp proof. +> +> Always keep a backup of the `.ots` file if it's important to you. + +## Verify the Timestamp + +```bash +uts verify myfile.txt +``` + +The command checks the file with the upgraded proof and confirms if the timestamp is valid. + +## Next Steps + +- [Architecture](/developer/overview) — How it works diff --git a/apps/docs/en/guide/what-is-uts.md b/apps/docs/en/guide/what-is-uts.md new file mode 100644 index 0000000..d10771b --- /dev/null +++ b/apps/docs/en/guide/what-is-uts.md @@ -0,0 +1,34 @@ +# What is UTS? + +**Universal Timestamps (UTS)** is a superset of the [OpenTimestamps][ots] protocol, +designed to provide a decentralized, trustless way to prove that data existed +prior a specific point in time. + +By leveraging the security and immutability of public blockchains, UTS allows +anyone to create cryptographic proofs of existence that are publicly verifiable +without relying on a trusted third party. + +## Public Good Infrastructure + +The cost of creating on-chain attestations is covered by the protocol operators, +making UTS free for users. + +### The Calendar Server + +The calendar server batches incoming timestamp requests into Merkle trees and +periodically attests the tree roots on-chain. + +This aggregation allows UTS to batch thousands of timestamps into a single +on-chain transaction, making it cost-effective. + +Current operational calendar servers: + +- [lgm1.calendar.test.timestamps.now](https://lgm1.calendar.test.timestamps.now) (Testnet) + +## Learn More + +- [Stamp via CLI](/guide/stamp-via-cli) - Get your first timestamp from CLI +- [Architecture Overview](/developer/overview) — How the system works +- [Reference Book](https://book.timestamps.now) — Detailed technical documentation + +[ots]: https://opentimestamps.org/ diff --git a/apps/docs/en/index.md b/apps/docs/en/index.md new file mode 100644 index 0000000..74ef34c --- /dev/null +++ b/apps/docs/en/index.md @@ -0,0 +1,54 @@ +--- +layout: home + +hero: + name: UTS + text: Universal Timestamps + tagline: Decentralized timestamping protocol for verifiable proofs of existence + image: + src: /uts-logo.svg + alt: UTS Logo + actions: + - theme: brand + text: Get Started + link: /guide/what-is-uts + - theme: alt + text: View on GitHub + link: https://github.com/lightsing/uts + +features: + - icon: 🔐 + title: Cryptographic Proofs + details: Create tamper-proof timestamps backed by blockchain consensus. Anyone can verify without trusting a third party. + - icon: ⚡ + title: Zero Cost & Fast + details: Public good infrastructure with no fees for users. Get timestamped in seconds, not hours. +--- + +## Quick Start + +### Try our official web app + +Visit [timestamps.now](https://timestamps.now) to timestamp your first file in seconds. No sign-up required! + +### Via CLI + +Install the CLI and timestamp your first file: + +```bash +cargo install uts-cli --version 0.1.0-alpha.0 --locked +uts stamp myfile.txt +# wait ~10 seconds for the batch to be attested on-chain +uts upgrade myfile.txt.ots +uts verify myfile.txt +``` + +## How It Works + +1. **Submit** - Send your data hash to a calendar server, get a pending timestamp in return +2. **Batch** - Your hash joins a Merkle tree with others +3. **Attest** - The tree root is timestamped on-chain via EAS +4. **Upgrade** - Upgrade the timestamp to self-contained. +5. **Verify** - Anyone can verify the timestamp proof + +For implementation details, see the [Reference Book](https://book.timestamps.now). diff --git a/apps/docs/en/reference/further-reading.md b/apps/docs/en/reference/further-reading.md new file mode 100644 index 0000000..5b4c82d --- /dev/null +++ b/apps/docs/en/reference/further-reading.md @@ -0,0 +1,63 @@ +# Further Reading + +## Official Resources + +### Reference Book + +The [Reference Book](https://book.timestamps.now) provides detailed technical documentation: + +- [System Architecture](https://book.timestamps.now/architecture.html) +- [Core Primitives](https://book.timestamps.now/core-primitives/merkle-tree.html) +- [Calendar Pipeline](https://book.timestamps.now/calendar-pipeline/submission.html) +- [L1 Anchoring](https://book.timestamps.now/l1-anchoring/contracts.html) +- [Security](https://book.timestamps.now/security.html) + +### GitHub + +- [Main Repository](https://github.com/lightsing/uts) +- [Issues](https://github.com/lightsing/uts/issues) +- [Discussions](https://github.com/lightsing/uts/discussions) + +## Related Projects + +### OpenTimestamps + +UTS builds upon [OpenTimestamps](https://opentimestamps.org/): + +- [OpenTimestamps Documentation](https://opentimestamps.org/) +- [GitHub](https://github.com/opentimestamps) + +### Ethereum Attestation Service + +UTS uses [EAS](https://attest.org/) for on-chain attestations: + +- [EAS Documentation](https://docs.attest.org/) +- [EAS SDK](https://github.com/ethereum-attestation-service/eas-sdk) +- [Schema Registry](https://easscan.org/) + +### Scroll + +L2 timestamping is deployed on [Scroll](https://scroll.io/): + +- [Scroll Documentation](https://docs.scroll.io/) +- [Scroll Bridge](https://scroll.io/bridge) +- [Block Explorer](https://scrollscan.com/) + +## Protocol Specifications + +### Binary Format + +The UTS proof format extends OpenTimestamps: + +- [OTS Format Spec](https://github.com/opentimestamps/opentimestamps-server/blob/master/doc/ots-format.md) +- [UTS Extensions](https://book.timestamps.now/core-primitives/ots-codec.html) + +### Attestation Schema + +UID: `0x5c5b8b295ff43c8e442be11d569e94a4cd5476f5e23df0f71bdd408df6b9649c` + +EAS schema for attestations: + +``` +bytes32 contentHash +``` diff --git a/apps/docs/lunaria.config.json b/apps/docs/lunaria.config.json new file mode 100644 index 0000000..e7ef982 --- /dev/null +++ b/apps/docs/lunaria.config.json @@ -0,0 +1,31 @@ +{ + "$schema": "./node_modules/@lunariajs/core/config.schema.json", + "repository": { + "name": "lightsing/uts", + "rootDir": "apps/docs" + }, + "files": [ + { + "location": "**/config.ts", + "pattern": "@lang/@path", + "type": "universal" + }, + { + "location": "**/*.md", + "pattern": "@lang/@path", + "type": "universal" + } + ], + "defaultLocale": { + "label": "English", + "lang": "en" + }, + "locales": [ + { + "label": "Chinese", + "lang": "zh" + } + ], + "outDir": ".vitepress/dist/_translations", + "ignoreKeywords": ["lunaria-ignore"] +} diff --git a/apps/docs/package.json b/apps/docs/package.json new file mode 100644 index 0000000..6883809 --- /dev/null +++ b/apps/docs/package.json @@ -0,0 +1,21 @@ +{ + "name": "docs", + "private": true, + "type": "module", + "scripts": { + "dev": "vitepress dev", + "build": "vitepress build", + "preview": "vitepress preview", + "lunaria:build": "lunaria build", + "lunaria:open": "open-cli .vitepress/dist/_translations/index.html" + }, + "devDependencies": { + "@lunariajs/core": "^0.1.1", + "markdown-it-mathjax3": "^4.3.2", + "open-cli": "^8.0.0", + "postcss-rtlcss": "^5.7.1", + "vitepress": "2.0.0-alpha.16", + "vitepress-plugin-group-icons": "^1.7.1", + "vitepress-plugin-llms": "^1.11.0" + } +} diff --git a/apps/docs/public/uts-logo.svg b/apps/docs/public/uts-logo.svg new file mode 100644 index 0000000..ec20d74 --- /dev/null +++ b/apps/docs/public/uts-logo.svg @@ -0,0 +1,4 @@ + + + UTS + \ No newline at end of file diff --git a/apps/docs/tsconfig.json b/apps/docs/tsconfig.json new file mode 100644 index 0000000..aad8273 --- /dev/null +++ b/apps/docs/tsconfig.json @@ -0,0 +1,4 @@ +{ + "include": [".vitepress/**/*.ts", "config.ts"], + "extends": "../../tsconfig.json" +} diff --git a/apps/web/package.json b/apps/web/package.json index 48a2f2d..48b7078 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -17,10 +17,10 @@ "@uts/sdk": "workspace:*", "@vueuse/core": "^14.2.1", "date-fns": "^4.1.0", - "ethers": "^6.16.0", "jszip": "^3.10.1", "lucide-vue-next": "^0.575.0", "pinia": "^3.0.4", + "viem": "^2.47.0", "vue": "^3.5.24" }, "devDependencies": { diff --git a/apps/web/src/components/upgrade/UpgradePanel.vue b/apps/web/src/components/upgrade/UpgradePanel.vue index 7dcb840..47f9b48 100644 --- a/apps/web/src/components/upgrade/UpgradePanel.vue +++ b/apps/web/src/components/upgrade/UpgradePanel.vue @@ -1,6 +1,6 @@ + + + + + + +
+
+ + +
+
+
+
+ +
+
+
+

SYS.NODE [ {{NODE_NAME}} ]

+ + +
+

Universal.Timestamps.Calendar.v{{VERSION}}

+
+
+ +
+
+ + + + + +
+
+
+
+ + +
+ + +
+

+ > Universal Timestamps Project _ +

+
+

+ Welcome to the uts calendar server. This infrastructure is operated as a 100% free public good. + Submit digests, retrieve proofs, and verify data integrity on-chain at zero cost. +

+ + +
+ +
+ SYSTEM_WARNING: TESTNET_ENVIRONMENT + This node is connected to the experimental testnet. Proofs generated here are for testing purposes only. Do not rely on this network for production data. +
+
+ + +
+ +
+ OPERATOR_ADDRESS: + {{NODE_ADDRESS}} +
+
+
+
+ + +
+ +
+
+ +
+

Total_Processed

+
+ N/A +
+
+
+ + +
+
+ +
+

Pending_Digests

+
+ N/A +
+
+
+ + +
+
+ +
+

OnChain_Success

+
+ N/A +
+
+
+ + +
+
+ +
+

Failed_Attempts

+
+ N/A +
+
+
+
+ + +
+

+ + Stamper_Daemon_Status +

+ +
+ +
+
+ > mempool.tx_queue + N/A +
+
+ > last_sync.timestamp + N/A +
+
+ > network.chain_id + + N/A + +
+
+ > latest.block_height + N/A +
+
+ + +
+
+ > Transaction_Hash +
+ + + +
+ AWAITING_ONCHAIN_CONFIRMATION... +
+
+
+
+ + +
+
+

> Client_Integration

+

> Official UTS CLI tool for local interaction.

+
+ +
+

+ To interact with this calendar node locally, install the official UTS CLI tool: +

+
+
Cargo
+ + $ cargo install uts-cli --version 0.1.0-alpha.0 --locked + +
+

> Requires Rust toolchain to be installed.

+
+
+ + +
+
+

> Protocol_API_Interface

+

> UTS is an enhanced superset of opentimestamps.

+
+ +
+ +
+
+ POST +

/digest

+
+

+ Submit a digest to the calendar server. The server writes it to its journal and returns a pending proof. +

+
+
BASH
+
Loading...
+
+
+ + +
+
+ GET +

/timestamp/{commitment}

+
+

+ Retrieve the completed proof. Once the stamper commits the root on-chain (EAS), it returns the complete merkle proof. +

+
+
BASH
+
Loading...
+
+
+
+
+ +
+ +
+

END OF TRANSMISSION // UNIVERSAL TIMESTAMPS CALENDAR SERVER

+
+ + + + + diff --git a/crates/calendar/src/lib.rs b/crates/calendar/src/lib.rs index fd04d11..4e4a9c2 100644 --- a/crates/calendar/src/lib.rs +++ b/crates/calendar/src/lib.rs @@ -1,19 +1,32 @@ -#![feature(thread_sleep_until)] -#![feature(allocator_api)] - //! Calendar server library. +// Copyright (C) 2026 UTS Contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + #[macro_use] extern crate tracing; +use crate::config::AppConfig; use alloy_signer::k256::ecdsa::SigningKey; use alloy_signer_local::LocalSigner; -use digest::{OutputSizeUser, typenum::Unsigned}; use rocksdb::DB; -use sha3::Keccak256; use std::sync::Arc; use uts_journal::Journal; +/// Config +pub mod config; /// Calendar server routes and handlers. pub mod routes; /// Time-related utilities and background tasks. @@ -22,16 +35,20 @@ pub mod time; /// Application state shared across handlers. #[derive(Debug)] pub struct AppState { + /// Application configuration. + pub config: AppConfig, /// Local signer for signing OTS timestamps. pub signer: LocalSigner, /// Journal - pub journal: Journal<{ ::OutputSize::USIZE }>, + pub journal: Journal, /// RocksDB - pub db: Arc, + pub kv_db: Arc, + /// Sqlite pool + pub sql_pool: sqlx::SqlitePool, } /// Signal for graceful shutdown. -pub async fn shutdown_signal() { +pub async fn shutdown_signal(fatal_error_happens: impl Future) { use tokio::signal; let ctrl_c = async { @@ -54,5 +71,6 @@ pub async fn shutdown_signal() { tokio::select! { _ = ctrl_c => {}, _ = terminate => {}, + _ = fatal_error_happens => {}, } } diff --git a/crates/calendar/src/main.rs b/crates/calendar/src/main.rs index 6f8fe78..78a2fba 100644 --- a/crates/calendar/src/main.rs +++ b/crates/calendar/src/main.rs @@ -1,108 +1,175 @@ //! Calendar server -use alloy_primitives::b256; +// Copyright (C) 2026 UTS Contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + use alloy_provider::{Provider, ProviderBuilder, network::EthereumWallet}; -use alloy_signer_local::{LocalSigner, MnemonicBuilder}; +use alloy_rpc_client::ClientBuilder; +use alloy_signer_local::MnemonicBuilder; use axum::{ Router, extract::DefaultBodyLimit, - http::Method, + http::{Method, StatusCode}, + response::Html, routing::{get, post}, }; -use digest::{OutputSizeUser, typenum::Unsigned}; +use bytes::Bytes; +use eyre::{Context, ContextCompat}; use rocksdb::DB; use sha3::Keccak256; -use std::{env, path::PathBuf, sync::Arc}; +use sqlx::sqlite::{SqliteConnectOptions, SqlitePoolOptions}; +use std::{convert::Infallible, env, fs, sync::Arc, time::Duration}; +use tokio_util::sync::CancellationToken; use tower_http::{cors, cors::CorsLayer}; -use tracing::info; -use uts_calendar::{AppState, routes, shutdown_signal, time}; -use uts_contracts::uts::UniversalTimestamps; -use uts_journal::{Journal, JournalConfig, checkpoint::CheckpointConfig}; -use uts_stamper::{Stamper, StamperConfig}; +use tracing::{error, info}; +use uts_calendar::{AppState, config::AppConfig, routes, shutdown_signal, time}; +use uts_contracts::eas::{EAS, EAS_ADDRESSES}; +use uts_journal::{Journal, JournalConfig}; +use uts_stamper::{Stamper, StamperConfig, sql}; const RING_BUFFER_CAPACITY: usize = 1 << 20; // 1 million entries +const INDEX_PAGE_TEMPLATE: &str = include_str!("index.html"); #[tokio::main] async fn main() -> eyre::Result<()> { tracing_subscriber::fmt::init(); + let config = AppConfig::new()?; + tokio::spawn(time::async_updater()); - let signer = LocalSigner::from_bytes(&b256!( - "9ba9926331eb5f4995f1e358f57ba1faab8b005b51928d2fdaea16e69a6ad225" - ))?; + let token = CancellationToken::new(); // journal let journal = Journal::with_capacity_and_config( RING_BUFFER_CAPACITY, JournalConfig { - consumer_checkpoint: CheckpointConfig { - path: PathBuf::from("./.journal/.checkpoint"), - ..Default::default() - }, - wal_dir: PathBuf::from("./.journal"), + db_path: config.db.journal.db_path.clone(), }, )?; - let key = MnemonicBuilder::from_phrase(env::var("MNEMONIC")?.as_str()) - .index(0u32)? + let key = MnemonicBuilder::from_phrase(&*config.blockchain.wallet.mnemonic) + .index(config.blockchain.wallet.index)? .build()?; + let address = key.address(); info!("Using address: {:?}", key.address()); - let provider = ProviderBuilder::new() - .wallet(EthereumWallet::new(key)) - .connect("https://sepolia-rpc.scroll.io") - .await?; - provider.get_chain_id().await?; // sanity check - let contract = UniversalTimestamps::new(uts_contracts::uts::DEFAULT_ADDRESS, provider.clone()); + let provider = ProviderBuilder::new() + .with_simple_nonce_management() + .wallet(EthereumWallet::new(key.clone())) + .connect_client( + ClientBuilder::default() + .layer(config.blockchain.rpc.retry.layer()) + .layer(config.blockchain.rpc.throttle.layer()) + .http(config.blockchain.rpc.url.parse()?), + ); + let chain_id = provider.get_chain_id().await?; // sanity check + let eas_address = *EAS_ADDRESSES + .get(&chain_id) + .context("eas default address not found")?; + let contract = EAS::new(eas_address, provider.clone()); // stamper let reader = journal.reader(); - let db = Arc::new(DB::open_default("./.db/tries")?); - let mut stamper = - Stamper::::OutputSize::USIZE }>::new( - reader, - db.clone(), - contract, - // TODO: tune configuration - StamperConfig { - max_interval_seconds: 10, - max_entries_per_timestamp: 1 << 10, // 1024 entries - min_leaves: 1 << 4, - max_cache_size: 256, - }, - ); - // TODO: graceful shutdown - tokio::spawn(async move { - stamper.run().await; - }); + fs::create_dir_all(config.db.kv.path.parent().unwrap())?; + let db = Arc::new(DB::open_default(&config.db.kv.path)?); + fs::create_dir_all(config.db.sql.filename.parent().unwrap())?; + let sql = SqlitePoolOptions::new() + .connect_with( + SqliteConnectOptions::new() + .filename(&config.db.sql.filename) + .create_if_missing(true) + .foreign_keys(true), + ) + .await?; + sql::migrate(&sql) + .await + .context("failed to run database migrations")?; - let app = Router::new() + let mut stamper = Stamper::::new( + reader, + db.clone(), + sql.clone(), + contract, + StamperConfig { + max_interval_seconds: config.stamper.max_interval_seconds, + max_entries_per_timestamp: config.stamper.max_entries_per_timestamp, + min_leaves: config.stamper.min_leaves, + }, + ); + + { + let token = token.clone(); + tokio::spawn(async move { + if let Err(e) = stamper.run(token.clone()).await { + error!("stamper fatal error: {e}"); + token.cancel(); + } + }); + } + + let listener = tokio::net::TcpListener::bind(&*config.server.bind_address).await?; + + // compatible API. + let public_api = Router::new() .route( "/digest", post(routes::ots::submit_digest) - .layer(DefaultBodyLimit::max(routes::ots::MAX_DIGEST_SIZE)), + .layer::<_, Infallible>( + CorsLayer::new() + .allow_methods([Method::POST]) + .allow_origin(cors::Any) + .max_age(Duration::from_hours(24)), + ) + .layer::<_, Infallible>(DefaultBodyLimit::max(routes::ots::MAX_DIGEST_SIZE)), ) - .route("/timestamp/{commitment}", get(routes::ots::get_timestamp)) - .with_state(Arc::new(AppState { - signer, - journal: journal.clone(), - db, - })) - .layer( - CorsLayer::new() - .allow_methods([Method::GET, Method::POST]) - .allow_origin(cors::Any), + .route( + "/timestamp/{commitment}", + get(routes::ots::get_timestamp).layer::<_, Infallible>( + CorsLayer::new() + .allow_methods([Method::GET]) + .allow_origin(cors::Any) + .max_age(Duration::from_hours(24)), + ), ); - let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await?; + let html = INDEX_PAGE_TEMPLATE + .replace("{{VERSION}}", env!("CARGO_PKG_VERSION")) + .replace("{{NODE_NAME}}", config.server.node_name.as_str()) + .replace("{{NODE_ADDRESS}}", &address.to_string()); + let html = Bytes::from(html); + + let app = Router::new() + .route("/", get(|| async move { Html(html.clone()) })) + .route("/healthcheck", get(|| async { StatusCode::NO_CONTENT })) + .route("/metrics", get(routes::metrics)) + .merge(public_api) + .with_state(Arc::new(AppState { + config, + signer: key.clone(), + journal: journal.clone(), + kv_db: db, + sql_pool: sql, + })); axum::serve(listener, app) - .with_graceful_shutdown(shutdown_signal()) + .with_graceful_shutdown(shutdown_signal(async move { + token.cancelled().await; + error!("fatal error signal received, shutting down"); + })) .await?; - // this will join the journal's background task and ensure flush of all pending commits - journal.shutdown()?; - Ok(()) } diff --git a/crates/calendar/src/routes.rs b/crates/calendar/src/routes.rs index ca72c74..1decd01 100644 --- a/crates/calendar/src/routes.rs +++ b/crates/calendar/src/routes.rs @@ -1,2 +1,52 @@ +use crate::{ + AppState, + routes::{headers::CONTENT_TYPE_JSON, responses::internal_server_error}, +}; +use alloy_primitives::private::serde::Serialize; +use axum::{ + Json, + extract::State, + http::{HeaderMap, StatusCode, header::CONTENT_TYPE}, + response::{IntoResponse, Response}, +}; +use std::sync::Arc; +use uts_stamper::sql; + /// ots related routes pub mod ots; + +mod headers; +mod responses; + +#[derive(Debug, Serialize)] +struct Metrics { + total: u64, + pending: u64, + stamper: sql::Stats, +} + +/// Handler for the `/metrics` endpoint, which returns JSON-formatted metrics about the calendar server. +pub async fn metrics(State(state): State>) -> Response { + let mut headers = HeaderMap::with_capacity(2); + headers.insert(CONTENT_TYPE, CONTENT_TYPE_JSON.clone()); + // cache for 1 minute + headers.insert( + axum::http::header::CACHE_CONTROL, + "public, max-age=10".parse().unwrap(), + ); + + let Ok(stamper) = sql::get_stats(&state.sql_pool).await else { + return internal_server_error(); + }; + + let total = state.journal.write_index(); + let pending = total - state.journal.consumed_index(); + + let metrics = Metrics { + total, + pending, + stamper, + }; + + (StatusCode::OK, headers, Json(metrics)).into_response() +} diff --git a/crates/calendar/src/routes/headers.rs b/crates/calendar/src/routes/headers.rs new file mode 100644 index 0000000..f90fe31 --- /dev/null +++ b/crates/calendar/src/routes/headers.rs @@ -0,0 +1,10 @@ +use axum::http::HeaderValue; + +pub static NO_CACHE: HeaderValue = HeaderValue::from_static("no-cache"); +pub static NO_STORE: HeaderValue = HeaderValue::from_static("no-store"); +pub static PUBLIC_IMMUTABLE: HeaderValue = + HeaderValue::from_static("public, max-age=31536000, immutable"); + +pub static CONTENT_TYPE_JSON: HeaderValue = HeaderValue::from_static("application/json"); +pub static CONTENT_TYPE_OCTET_STREAM: HeaderValue = + HeaderValue::from_static("application/octet-stream"); diff --git a/crates/calendar/src/routes/ots.rs b/crates/calendar/src/routes/ots.rs index 4a2ae98..5c5b21b 100644 --- a/crates/calendar/src/routes/ots.rs +++ b/crates/calendar/src/routes/ots.rs @@ -1,64 +1,55 @@ -use crate::{AppState, time::current_time_sec}; +use crate::{ + AppState, + routes::{headers::*, responses::*}, + time::current_time_sec, +}; +use alloy_chains::Chain; use alloy_primitives::B256; use alloy_signer::SignerSync; use axum::{ body::Bytes, extract::{Path, State}, - http::StatusCode, + http::{HeaderMap, header}, response::{IntoResponse, Response}, }; use bump_scope::Bump; use bytes::BytesMut; -use digest::Digest; +use digest::{Digest, Output}; use sha3::Keccak256; use std::{cell::RefCell, sync::Arc}; -use uts_bmt::UnorderedMerkleTree; use uts_core::{ + alloc::SliceExt, codec::{ Encode, - v1::{EthereumUTSAttestation, PendingAttestation, Timestamp}, + v1::{EASTimestamped, PendingAttestation, Timestamp}, }, - utils::Hexed, }; -use uts_stamper::DbExt; +use uts_journal::Error; +use uts_stamper::{kv::DbExt, sql, sql::AttestationResult}; /// Maximum digest size accepted by the endpoint. pub const MAX_DIGEST_SIZE: usize = 64; // e.g., SHA3-512 -// Test this with official ots client: -// ots stamp -c "http://localhost:3000/" -m 1 -// cargo run --bin uts-info -- .ots -// Sample: -// ``` -// OTS Detached Timestamp found: -// Version 1 Proof digest of SHA256 877c470874fa92e5609a1396b1188ffa3e539d83ec2748a7cb6fb2d4430d45a2 -// execute APPEND ec04517482d3be52b6123ca37f683285 -// result 877c470874fa92e5609a1396b1188ffa3e539d83ec2748a7cb6fb2d4430d45a2ec04517482d3be52b6123ca37f683285 -// execute SHA256 -// result 2edc60a195a879bd446c5473921c46db14c4b1974516682ecae2b406121a5732 -// execute PREPEND 5137456900000000 -// result 51374569000000002edc60a195a879bd446c5473921c46db14c4b1974516682ecae2b406121a5732 -// execute APPEND 9f947a5cf576ba4f68593ac5e350204cc8b38bf0fd5f6f2d4436820d3164dfeaf7405188dfc4bad66e8f42e6fd0a6ffdcceebda548d01224113baab1a568a2b8 -// result 51374569000000002edc60a195a879bd446c5473921c46db14c4b1974516682ecae2b406121a57329f947a5cf576ba4f68593ac5e350204cc8b38bf0fd5f6f2d4436820d3164dfeaf7405188dfc4bad66e8f42e6fd0a6ffdcceebda548d01224113baab1a568a2b8 -// execute KECCAK256 -// result c15b4e8b93e9aaee5b8c736f5b73e5f313062e389925a0b1fc6495053f99d352 -// result attested by Pending: update URI https://localhost:3000 -// ``` /// Submit digest to calendar server and get pending timestamp in response. pub async fn submit_digest(State(state): State>, digest: Bytes) -> Response { - let (output, commitment) = submit_digest_inner(digest, &state.signer); + let (output, commitment) = submit_digest_inner(digest, &state.signer, &state.config.server.uri); match state.journal.try_commit(&commitment) { - Err(_) => { - return (StatusCode::SERVICE_UNAVAILABLE, r#"{"err":"server busy"}"#).into_response(); - } // journal is full - Ok(fut) => fut.await, + // journal is full + Err(Error::Full) => return service_unavailable(), + // journal is in fatal error status + Err(Error::Fatal) => return internal_server_error(), + Ok(()) => {} } output.into_response() } // TODO: We need to benchmark this. /// inner function to submit digest, returns (encoded timestamp, commitment) -pub fn submit_digest_inner(digest: Bytes, signer: impl SignerSync) -> (Bytes, [u8; 32]) { +pub fn submit_digest_inner( + digest: Bytes, + signer: impl SignerSync, + pending_uri: &str, +) -> (Bytes, [u8; 32]) { const PRE_ALLOCATION_SIZE_HINT: usize = 4096; thread_local! { // We don't have `.await` in this function, so it's safe to borrow thread local. @@ -92,15 +83,16 @@ pub fn submit_digest_inner(digest: Bytes, signer: impl SignerSync) -> (Bytes, [u hasher.finalize_reset() }); - let undeniable_sig = signer.sign_hash_sync(&hash.0.into()).unwrap(); + let hash = B256::from_slice(&hash); + let undeniable_sig = signer.sign_hash_sync(&hash).unwrap(); undeniable_sig.as_erc2098() }; #[cfg(any(debug_assertions, not(feature = "performance")))] trace!( - recv_timestamp = ?Hexed(&recv_timestamp), - digest = ?Hexed(&digest), - undeniable_sig = ?Hexed(&undeniable_sig), + recv_timestamp = ?uts_core::utils::Hexed(&recv_timestamp), + digest = ?uts_core::utils::Hexed(&digest), + undeniable_sig = ?uts_core::utils::Hexed(&undeniable_sig), ); BUMP.with(|bump| { @@ -109,8 +101,8 @@ pub fn submit_digest_inner(digest: Bytes, signer: impl SignerSync) -> (Bytes, [u let mut builder = Timestamp::builder_in(&*bump); builder - .prepend(recv_timestamp.to_vec_in(&bump)) - .append(undeniable_sig.to_vec_in(&bump)) + .prepend(SliceExt::to_vec_in(recv_timestamp.as_slice(), &bump)) + .append(SliceExt::to_vec_in(undeniable_sig.as_slice(), &bump)) .keccak256(); let mut commitment = [0u8; 32]; @@ -118,13 +110,11 @@ pub fn submit_digest_inner(digest: Bytes, signer: impl SignerSync) -> (Bytes, [u let timestamp = builder .attest(PendingAttestation { - uri: "http://localhost:3000".into(), + uri: pending_uri.into(), }) .unwrap(); // copy data out of bump - // TODO: eliminate this allocation by reusing from a pool - // TODO: wrap the buffer with a drop trait to return to pool let mut buf = BytesMut::with_capacity(128); timestamp.encode(&mut buf).unwrap(); @@ -140,51 +130,54 @@ pub async fn get_timestamp( State(state): State>, Path(commitment): Path, ) -> Response { - const PRE_ALLOCATION_SIZE_HINT: usize = 4096; - thread_local! { - // We don't have `.await` in this function, so it's safe to borrow thread local. - static BUMP: RefCell = RefCell::new(Bump::with_size(PRE_ALLOCATION_SIZE_HINT)); - } + let Some(root) = + DbExt::::get_root_for_leaf(&*state.kv_db, commitment).expect("DB error") + else { + return not_found(); + }; + + let id = match sql::get_attestation_result(&state.sql_pool, root).await { + Ok((_, AttestationResult::Pending)) => return not_found(), + Ok((_, AttestationResult::MaxAttemptsExceeded)) => return internal_server_error(), + Ok((id, AttestationResult::Success)) => id, + Err(e) => { + error!("SQL error: {e}"); + return internal_server_error(); + } + }; - let Some(root) = state.db.get_root_for_leaf(commitment).expect("DB error") else { - return (StatusCode::NOT_FOUND, r#"{"err":"timestamp not found"}"#).into_response(); + let Ok(Some(attestation)) = sql::get_last_successful_attest_attempt(&state.sql_pool, id).await + else { + error!("get_last_successful_attest_attempt failed for attestation_id {id} unexpectedly"); + return internal_server_error(); }; - let entry = state - .db - .load_entry(root) + + let trie = DbExt::::load_trie(&*state.kv_db, root) .expect("DB error") .expect("bug: entry not found"); - let trie: UnorderedMerkleTree = entry.trie(); + let commitment = Output::::from_slice(commitment.as_slice()); let proof = trie - .get_proof_iter(bytemuck::cast_ref(&*commitment)) + .get_proof_iter(commitment) .expect("bug: proof not found"); - let output = BUMP.with(|bump| { - let mut bump = bump.borrow_mut(); - bump.reset(); - let mut builder = Timestamp::builder_in(&*bump); - builder.merkle_proof(proof); + let mut builder = Timestamp::builder(); + builder.merkle_proof(proof); - let timestamp = builder - .attest(EthereumUTSAttestation::new( - entry.chain_id, - entry.height, - Default::default(), - )) - .unwrap(); + let timestamp = builder + .attest(EASTimestamped { + chain: Chain::from_id(attestation.chain_id), + }) + .unwrap(); - // copy data out of bump - // TODO: eliminate this allocation by reusing from a pool - // TODO: wrap the buffer with a drop trait to return to pool - let mut buf = BytesMut::with_capacity(128); - timestamp.encode(&mut buf).unwrap(); + let mut buf = BytesMut::with_capacity(128); + timestamp.encode(&mut buf).unwrap(); - #[cfg(any(debug_assertions, not(feature = "performance")))] - trace!(encoded_length = buf.len(), timestamp = ?timestamp); - - buf.freeze() - }); + #[cfg(any(debug_assertions, not(feature = "performance")))] + trace!(encoded_length = buf.len(), timestamp = ?timestamp); - output.into_response() + let mut headers = HeaderMap::with_capacity(2); + headers.insert(header::CACHE_CONTROL, PUBLIC_IMMUTABLE.clone()); + headers.insert(header::CONTENT_TYPE, CONTENT_TYPE_OCTET_STREAM.clone()); + buf.freeze().into_response() } diff --git a/crates/calendar/src/routes/responses.rs b/crates/calendar/src/routes/responses.rs new file mode 100644 index 0000000..3756dd9 --- /dev/null +++ b/crates/calendar/src/routes/responses.rs @@ -0,0 +1,42 @@ +use crate::routes::headers::{CONTENT_TYPE_JSON, NO_CACHE, NO_STORE}; +use axum::{ + http::{HeaderMap, StatusCode, header}, + response::{IntoResponse, Response}, +}; + +#[inline] +pub fn service_unavailable() -> Response { + let mut headers = HeaderMap::with_capacity(2); + headers.insert(header::CACHE_CONTROL, NO_CACHE.clone()); + headers.insert(header::CONTENT_TYPE, CONTENT_TYPE_JSON.clone()); + + ( + StatusCode::SERVICE_UNAVAILABLE, + headers, + r#"{"err":"server busy"}"#, + ) + .into_response() +} + +#[inline] +pub fn not_found() -> Response { + let mut headers = HeaderMap::with_capacity(2); + headers.insert(header::CACHE_CONTROL, NO_CACHE.clone()); + headers.insert(header::CONTENT_TYPE, CONTENT_TYPE_JSON.clone()); + + (StatusCode::NOT_FOUND, headers, r#"{"err":"not found"}"#).into_response() +} + +#[inline] +pub fn internal_server_error() -> Response { + let mut headers = HeaderMap::with_capacity(2); + headers.insert(header::CACHE_CONTROL, NO_STORE.clone()); + headers.insert(header::CONTENT_TYPE, CONTENT_TYPE_JSON.clone()); + + ( + StatusCode::INTERNAL_SERVER_ERROR, + headers, + r#"{"err":"internal error"}"#, + ) + .into_response() +} diff --git a/crates/calendar/src/time.rs b/crates/calendar/src/time.rs index 86c4b52..3009c73 100644 --- a/crates/calendar/src/time.rs +++ b/crates/calendar/src/time.rs @@ -46,7 +46,11 @@ pub fn updater() { // Note: This behavior is different from the async version, which skips missed ticks. let now_instant = Instant::now(); if next_tick > now_instant { - std::thread::sleep_until(next_tick); + let now = Instant::now(); + + if let Some(delay) = next_tick.checked_duration_since(now) { + std::thread::sleep(delay); + } } else { // If we've fallen behind, resynchronize to avoid accumulating drift. next_tick = now_instant; diff --git a/crates/cli/Cargo.toml b/crates/cli/Cargo.toml index 4710edf..84627ec 100644 --- a/crates/cli/Cargo.toml +++ b/crates/cli/Cargo.toml @@ -1,9 +1,17 @@ [package] authors.workspace = true +categories = ["command-line-utilities"] +description = "Universal Timestamps Command Line Interface." edition.workspace = true homepage.workspace = true -keywords.workspace = true -license.workspace = true +include = [ + "src/**/*", + "Cargo.toml", + "LICENSE-*", + "README.md", +] +keywords = ["cli", "command-line", "uts", "timestamping"] +license = "MIT OR Apache-2.0" name = "uts-cli" repository.workspace = true version.workspace = true @@ -16,31 +24,26 @@ name = "uts" path = "src/main.rs" [dependencies] -alloy-provider = { workspace = true } -bytemuck = { workspace = true } clap = { workspace = true, features = ["derive"] } color-eyre = { workspace = true } digest = { workspace = true } eyre = { workspace = true } futures = { workspace = true } -jiff = { workspace = true } -rand = { workspace = true } -reqwest = { workspace = true, default-features = false, features = ["http2"] } ripemd = { workspace = true } sha1 = { workspace = true } sha2 = { workspace = true } sha3 = { workspace = true } tokio = { workspace = true, features = ["full"] } tracing = { workspace = true } +tracing-subscriber = { workspace = true, features = ["fmt", "env-filter"] } url = { workspace = true } -uts-bmt = { workspace = true } -uts-core = { workspace = true, features = ["std", "ethereum-uts-verifier", "io-utils"] } +uts-core = { workspace = true } +uts-sdk = { workspace = true, features = ["full"] } [features] -default = ["reqwest-default-tls"] -reqwest-default-tls = ["reqwest/default-tls"] -reqwest-native-tls = ["reqwest/native-tls"] -reqwest-native-tls-no-alpn = ["reqwest/native-tls-no-alpn"] -reqwest-native-tls-vendored = ["reqwest/native-tls-vendored"] -reqwest-native-tls-vendored-no-alpn = ["reqwest/native-tls-vendored-no-alpn"] -reqwest-rustls = ["reqwest/rustls"] +default = ["reqwest-rustls"] +performance = ["tracing/max_level_info"] + +reqwest-default-tls = ["uts-sdk/reqwest-default-tls"] +reqwest-native-tls = ["uts-sdk/reqwest-native-tls"] +reqwest-rustls = ["uts-sdk/reqwest-rustls"] diff --git a/crates/cli/LICENSE-APACHE b/crates/cli/LICENSE-APACHE new file mode 120000 index 0000000..1cd601d --- /dev/null +++ b/crates/cli/LICENSE-APACHE @@ -0,0 +1 @@ +../../LICENSE-APACHE \ No newline at end of file diff --git a/crates/cli/LICENSE-MIT b/crates/cli/LICENSE-MIT new file mode 120000 index 0000000..b2cfbdc --- /dev/null +++ b/crates/cli/LICENSE-MIT @@ -0,0 +1 @@ +../../LICENSE-MIT \ No newline at end of file diff --git a/crates/cli/src/client.rs b/crates/cli/src/client.rs deleted file mode 100644 index f0d0e53..0000000 --- a/crates/cli/src/client.rs +++ /dev/null @@ -1,9 +0,0 @@ -use reqwest::Client; -use std::sync::LazyLock; - -pub static CLIENT: LazyLock = LazyLock::new(|| { - Client::builder() - .user_agent(concat!("uts/", env!("CARGO_PKG_VERSION"))) - .build() - .expect("Failed to build HTTP client") -}); diff --git a/crates/cli/src/commands.rs b/crates/cli/src/commands.rs index a4f8391..5044354 100644 --- a/crates/cli/src/commands.rs +++ b/crates/cli/src/commands.rs @@ -1,6 +1,7 @@ use clap::Subcommand; mod inspect; +mod purge; mod stamp; mod upgrade; mod verify; @@ -15,6 +16,8 @@ pub enum Commands { Stamp(stamp::Stamp), /// Upgrade timestamp Upgrade(upgrade::Upgrade), + /// Purge stale pending attestations + Purge(purge::Purge), } impl Commands { @@ -24,6 +27,7 @@ impl Commands { Commands::Verify(cmd) => cmd.run().await, Commands::Stamp(cmd) => cmd.run().await, Commands::Upgrade(cmd) => cmd.run().await, + Commands::Purge(cmd) => cmd.run().await, } } } diff --git a/crates/cli/src/commands/inspect.rs b/crates/cli/src/commands/inspect.rs index ff47f6a..f46c2bd 100644 --- a/crates/cli/src/commands/inspect.rs +++ b/crates/cli/src/commands/inspect.rs @@ -1,5 +1,6 @@ use clap::Args; use std::{fs, io, io::Seek, path::PathBuf}; +use tracing::{error, info}; use uts_core::codec::{ Decode, Reader, VersionedProof, v1::{DetachedTimestamp, Timestamp}, @@ -17,18 +18,18 @@ impl Inspect { match VersionedProof::::decode(&mut Reader(&mut fh)) { Ok(ots) => { - eprintln!("OTS Detached Timestamp found:\n{ots}"); + info!("OTS Detached Timestamp found:\n{ots}"); return Ok(()); } Err(e) => { - eprintln!("Not a valid Detached Timestamp OTS file (trying raw timestamp): {e}\n"); + error!("Not a valid Detached Timestamp OTS file (trying raw timestamp): {e}\n"); } }; fh.seek(io::SeekFrom::Start(0))?; let raw = Timestamp::decode(&mut Reader(&mut fh))?; - eprintln!("Raw Timestamp found:\n{raw}"); + info!("Raw Timestamp found:\n{raw}"); Ok(()) } } diff --git a/crates/cli/src/commands/purge.rs b/crates/cli/src/commands/purge.rs new file mode 100644 index 0000000..3c66bcc --- /dev/null +++ b/crates/cli/src/commands/purge.rs @@ -0,0 +1,114 @@ +use clap::Args; +use std::{collections::HashSet, path::PathBuf}; +use tracing::{error, info, warn}; +use uts_core::codec::{Decode, Encode, VersionedProof, v1::DetachedTimestamp}; +use uts_sdk::Sdk; + +#[derive(Debug, Args)] +pub struct Purge { + /// Files to purge pending attestations from. May be specified multiple times. + #[arg(value_name = "FILE", num_args = 1..)] + files: Vec, + /// Skip the interactive confirmation prompt and purge all pending attestations. + #[arg(short = 'y', long = "yes", default_value_t = false)] + yes: bool, +} + +impl Purge { + pub async fn run(self) -> eyre::Result<()> { + for path in &self.files { + if let Err(e) = self.purge_one(path).await { + error!("[{}] failed to purge: {e}", path.display()); + } + } + Ok(()) + } + + async fn purge_one(&self, path: &PathBuf) -> eyre::Result<()> { + let file = tokio::fs::read(path).await?; + let mut proof = VersionedProof::::decode(&mut &*file)?; + + let pending = Sdk::list_pending(&proof); + if pending.is_empty() { + info!( + "[{}] no pending attestations found, skipping", + path.display() + ); + return Ok(()); + } + + info!( + "[{}] found {} pending attestation(s):", + path.display(), + pending.len() + ); + for (i, uri) in pending.iter().enumerate() { + info!(" [{}] {uri}", i + 1); + } + + let uris_to_purge = if self.yes { + // Purge all when --yes flag is used + None + } else { + // Interactive selection + print!("Enter numbers to purge (comma-separated), 'all', or 'none' to skip: "); + use std::io::Write; + std::io::stdout().flush()?; + let mut input = String::new(); + std::io::stdin().read_line(&mut input)?; + let input = input.trim(); + + if input.eq_ignore_ascii_case("none") || input.is_empty() { + info!("[{}] skipped", path.display()); + return Ok(()); + } + + if input.eq_ignore_ascii_case("all") { + None + } else { + let mut selected = HashSet::new(); + for part in input.split(',') { + let part = part.trim(); + match part.parse::() { + Ok(n) if n >= 1 && n <= pending.len() => { + selected.insert(pending[n - 1].clone()); + } + _ => { + warn!("ignoring invalid selection: {part}"); + } + } + } + if selected.is_empty() { + info!("[{}] no valid selections, skipping", path.display()); + return Ok(()); + } + Some(selected) + } + }; + + let result = Sdk::purge_pending_by_uris(&mut proof, uris_to_purge.as_ref()); + + if result.purged.is_empty() { + info!("[{}] nothing to purge", path.display()); + return Ok(()); + } + + if !result.has_remaining { + warn!( + "[{}] purging would leave no attestations in the file, skipping", + path.display() + ); + return Ok(()); + } + + let mut buf = Vec::new(); + proof.encode(&mut buf)?; + tokio::fs::write(path, buf).await?; + info!( + "[{}] purged {} pending attestation(s)", + path.display(), + result.purged.len() + ); + Ok(()) + } +} diff --git a/crates/cli/src/commands/stamp.rs b/crates/cli/src/commands/stamp.rs index e747ced..41a9040 100644 --- a/crates/cli/src/commands/stamp.rs +++ b/crates/cli/src/commands/stamp.rs @@ -1,29 +1,15 @@ -use crate::client::CLIENT; -use bytemuck::Pod; use clap::{Args, ValueEnum}; use digest::{Digest, FixedOutputReset, Output}; -use futures::TryFutureExt; -use std::{collections::HashMap, future::ready, io, path::PathBuf, sync::LazyLock, time::Duration}; +use futures::future::join_all; +use std::path::PathBuf; use tokio::{fs, io::AsyncWriteExt}; +use tracing::{error, info}; use url::Url; -use uts_bmt::UnorderedMerkleTree; -use uts_core::{ - codec::{ - Decode, Encode, VersionedProof, - v1::{DetachedTimestamp, DigestHeader, Timestamp, TimestampBuilder, opcode::DigestOpExt}, - }, - utils::{HashAsyncFsExt, Hexed}, +use uts_core::codec::{ + Encode, VersionedProof, + v1::{DetachedTimestamp, opcode::DigestOpExt}, }; - -static DEFAULT_CALENDARS: LazyLock> = LazyLock::new(|| { - vec![ - // Url::parse("https://a.pool.opentimestamps.org/").unwrap(), - // Url::parse("https://b.pool.opentimestamps.org/").unwrap(), - // Url::parse("https://a.pool.eternitywall.com/").unwrap(), - // Url::parse("https://ots.btc.catallaxy.com/").unwrap(), - Url::parse("http://127.0.0.1:3000/").unwrap(), - ] -}); +use uts_sdk::Sdk; #[derive(Debug, Args)] pub struct Stamp { @@ -34,14 +20,14 @@ pub struct Stamp { #[arg(short = 'c', long = "calendar", value_name = "URL", num_args = 0..)] calendars: Vec, /// Consider the timestamp complete if at least M calendars reply prior to the timeout - #[arg(short = 'm', default_value = "1")] - quorum: usize, + #[arg(short = 'm')] + quorum: Option, /// Hasher to use when digesting files. Default is Keccak256. #[arg(short = 'H', long = "hasher", default_value = "keccak256")] hasher: Hasher, - /// Timeout in seconds to wait for calendar responses. Default is 60 seconds. - #[arg(long = "timeout", default_value = "5")] - timeout: u64, + /// Timeout in seconds to wait for calendar responses + #[arg(long = "timeout")] + timeout: Option, } #[derive(Default, Debug, Copy, Clone, ValueEnum)] @@ -66,91 +52,37 @@ impl Stamp { async fn run_inner(self) -> eyre::Result<()> where D: Digest + FixedOutputReset + DigestOpExt + Send, - Output: Pod + Copy, + Output: Copy, { - let digests = - futures::future::join_all(self.files.iter().map(|f| hash_file::(f.clone()))) - .await - .into_iter() - .collect::, _>>()?; - - for (header, path) in digests.iter().zip(self.files.iter()) { - eprintln!("File: {} {}", header, path.display()); + let mut builder = if self.calendars.is_empty() { + Sdk::builder() + } else { + Sdk::try_builder_from_calendars(self.calendars).expect("none empty") + }; + if let Some(quorum) = self.quorum { + builder = builder.with_quorum(quorum) } - - let mut builders: HashMap = HashMap::from_iter( - self.files - .iter() - .map(|path| (path.clone(), Timestamp::builder())), - ); - - let nonced_digest = builders - .iter_mut() - .zip(digests.iter()) - .map(|((_, builder), digest)| { - let mut hasher = D::new(); - Digest::update(&mut hasher, digest.digest()); - let nonce: [u8; 32] = rand::random(); - Digest::update(&mut hasher, &nonce); - builder.append(nonce.to_vec()).digest::(); - hasher.finalize() - }) - .collect::>(); - - let internal_tire = UnorderedMerkleTree::::new(&nonced_digest); - let root = internal_tire.root(); - eprintln!("Internal Merkle root: {}", Hexed(root)); - - for ((_, builder), leaf) in builders.iter_mut().zip(nonced_digest) { - let proof = internal_tire.get_proof_iter(&leaf).expect("infallible"); - builder.merkle_proof(proof); + if let Some(timeout) = self.timeout { + builder = builder.with_timeout_seconds(timeout) } - let calendars = if self.calendars.is_empty() { - &*DEFAULT_CALENDARS - } else { - &*self.calendars - }; + let sdk = builder.build()?; - if self.quorum > calendars.len() { - eyre::bail!( - "Quorum of {} cannot be achieved with only {} calendars", - self.quorum, - self.calendars.len() - ); - } + let stamps = sdk.stamp_files::(&self.files).await?; - let stamps = futures::future::join_all( - calendars + let tasks = join_all( + self.files + .clone() .into_iter() - .map(|calendar| request_calendar(calendar.clone(), self.timeout, root)), + .zip(stamps) + .map(|(path, timestamp)| write_stamp(path, timestamp)), ) - .await - .into_iter() - .filter_map(|res| res.ok()) - .collect::>(); - if stamps.len() < self.quorum { - eyre::bail!( - "Only received {} valid responses from calendars, which does not meet the quorum of {}", - stamps.len(), - self.quorum - ); - } - let merged = if stamps.len() == 1 { - stamps.into_iter().next().unwrap() - } else { - Timestamp::merge(stamps) - }; + .await; - let writes = - futures::future::join_all(builders.into_iter().zip(digests).map( - |((path, builder), header)| write_stamp(path, builder, merged.clone(), header), - )) - .await; - for (res, path) in writes.into_iter().zip(self.files.iter()) { - match res { - Ok(_) => eprintln!("Successfully wrote timestamp for {}", path.display()), - Err(e) => eprintln!("Failed to write timestamp for {}: {}", path.display(), e), + for (path, result) in self.files.iter().zip(tasks) { + match result { + Ok(()) => info!("[{}] successfully stamped", path.display()), + Err(e) => error!("[{}] failed to stamp: {e}", path.display()), } } @@ -158,52 +90,7 @@ impl Stamp { } } -async fn hash_file(path: PathBuf) -> io::Result { - let mut hasher = D::new(); - let file = fs::File::open(path).await?; - HashAsyncFsExt::update(&mut hasher, file).await?; - Ok(DigestHeader::new::(hasher.finalize())) -} - -async fn request_calendar(calendar: Url, timeout: u64, root: &[u8]) -> eyre::Result { - eprintln!("Submitting to remote calendar: {calendar}"); - let url = calendar.join(&"digest")?; - let response = CLIENT - .post(url) - .header("Accept", "application/vnd.opentimestamps.v1") - .body(root.to_vec()) - .timeout(Duration::from_secs(timeout)) - .send() - .and_then(|r| ready(r.error_for_status())) - .and_then(|r| r.bytes()) - .await - .inspect_err(|e| { - if e.is_status() { - eprintln!("Calendar {} responded with error: {}", calendar, e); - } else if e.is_timeout() { - eprintln!("Calendar {} timed out after {} seconds", calendar, timeout); - } else { - eprintln!("Failed to submit to calendar {}: {}", calendar, e); - } - })?; - - let ts = Timestamp::decode(&mut &*response).inspect_err(|e| { - eprintln!( - "Failed to decode response from calendar {}: {}", - calendar, e - ); - })?; - Ok(ts) -} - -async fn write_stamp( - mut path: PathBuf, - builder: TimestampBuilder, - merged: Timestamp, - header: DigestHeader, -) -> eyre::Result<()> { - let timestamp = builder.concat(merged.clone()); - let timestamp = DetachedTimestamp::from_parts(header, timestamp); +async fn write_stamp(mut path: PathBuf, timestamp: DetachedTimestamp) -> eyre::Result<()> { let timestamp = VersionedProof::::new(timestamp); let mut buf = Vec::new(); timestamp.encode(&mut buf)?; diff --git a/crates/cli/src/commands/upgrade.rs b/crates/cli/src/commands/upgrade.rs index cc939a4..063c859 100644 --- a/crates/cli/src/commands/upgrade.rs +++ b/crates/cli/src/commands/upgrade.rs @@ -1,99 +1,83 @@ -use crate::client::CLIENT; use clap::Args; -use eyre::bail; -use futures::TryFutureExt; -use reqwest::StatusCode; -use std::{fs, future::ready, path::PathBuf, time::Duration}; -use url::Url; -use uts_core::{ - codec::{ - Decode, Encode, VersionedProof, - v1::{Attestation, DetachedTimestamp, PendingAttestation, Timestamp}, - }, - utils::Hexed, -}; +use std::path::PathBuf; +use tracing::{error, info, warn}; +use uts_core::codec::{Decode, Encode, VersionedProof, v1::DetachedTimestamp}; +use uts_sdk::{Sdk, UpgradeResult}; #[derive(Debug, Args)] pub struct Upgrade { /// Files to timestamp. May be specified multiple times. #[arg(value_name = "FILE", num_args = 1..)] files: Vec, - /// Timeout in seconds to wait for calendar responses. Default is 5 seconds. - #[arg(long = "timeout", default_value = "5")] - timeout: u64, + /// Whether to keep pending attestations in the proof. Default is false, which means pending + /// attestations will be removed from the proof once the upgrade process is complete. + #[arg(short, long, default_value_t = false)] + keep_pending: bool, + /// Timeout in seconds to wait for calendar responses. + #[arg(long = "timeout")] + timeout: Option, } impl Upgrade { pub async fn run(self) -> eyre::Result<()> { - // timestamp files are small, so we can read them all synchronously before upgrading. - let files = self - .files - .iter() - .map(|path| fs::read(path)) - .collect::, _>>()?; + let mut builder = Sdk::builder(); + if self.keep_pending { + builder = builder.keep_pending(); + } + if let Some(timeout) = self.timeout { + builder = builder.with_timeout_seconds(timeout); + } + let sdk = builder.build()?; let results = futures::future::join_all( self.files .iter() - .cloned() - .zip(files) - .into_iter() - .map(|(path, file)| upgrade_one(path, file, self.timeout)), + .map(|path| upgrade_one(sdk.clone(), path.clone())), ) .await .into_iter() .collect::>(); + for (path, result) in self.files.iter().zip(results) { - match result { - Ok(_) => eprintln!("Upgraded: {}", path.display()), - Err(e) => eprintln!("Failed to upgrade {}: {e}", path.display()), + if let Err(e) = result { + error!("[{}] failed to upgrade: {e}", path.display()) } } Ok(()) } } -async fn upgrade_one(path: PathBuf, file: Vec, timeout: u64) -> eyre::Result<()> { +async fn upgrade_one(sdk: Sdk, path: PathBuf) -> eyre::Result<()> { + info!("[{}] upgrading attestation...", path.display()); + let file = tokio::fs::read(&path).await?; let mut proof = VersionedProof::::decode(&mut &*file)?; + let results = sdk.upgrade(&mut proof).await?; - for step in proof.proof.pending_attestations_mut() { - let pending_uri = { - let Timestamp::Attestation(attestation) = step else { - unreachable!("bug: PendingAttestationIterMut should only yield Attestations"); - }; - let commitment = attestation.value().expect("finalized when decode"); - let pending_uri = PendingAttestation::from_raw(&*attestation)?.uri; - Url::parse(&pending_uri)?.join(&format!("timestamp/{}", Hexed(commitment)))? - }; - - let result = CLIENT - .get(pending_uri) - .header("Accept", "application/vnd.opentimestamps.v1") - .timeout(Duration::from_secs(timeout)) - .send() - .and_then(|r| ready(r.error_for_status())) - .and_then(|r| r.bytes()) - .await; - + let mut changed = false; + for (calendar_server, result) in results { match result { - Ok(response) => { - let attestation = Timestamp::decode(&mut &*response)?; - *step = Timestamp::merge(vec![attestation, step.clone()]) - } - Err(e) => { - if let Some(status) = e.status() - && status == StatusCode::NOT_FOUND - { - bail!("calendar not ready yet."); - } - return Err(e.into()); + UpgradeResult::Upgraded => { + info!( + "[{}] attestation from {calendar_server} upgraded successfully", + path.display() + ); + changed = true; } + UpgradeResult::Pending => info!( + "[{}] attestation from {calendar_server} is still pending, skipping", + path.display() + ), + UpgradeResult::Failed(e) => warn!( + "[{}] failed to upgrade attestation from {calendar_server}: {e}", + path.display() + ), } } - let mut buf = Vec::new(); - proof.encode(&mut buf)?; - tokio::fs::write(path, buf).await?; - + if changed { + let mut buf = Vec::new(); + proof.encode(&mut buf)?; + tokio::fs::write(path, buf).await?; + } Ok(()) } diff --git a/crates/cli/src/commands/verify.rs b/crates/cli/src/commands/verify.rs index 3f817a3..2f136e6 100644 --- a/crates/cli/src/commands/verify.rs +++ b/crates/cli/src/commands/verify.rs @@ -1,146 +1,38 @@ -use alloy_provider::ProviderBuilder; use clap::Args; -use digest::{Digest, DynDigest}; -use eyre::bail; -use jiff::{Timestamp, tz::TimeZone}; -use std::{ - fs::File, - io::{BufReader, Read}, - path::PathBuf, - process, -}; -use uts_core::{ - codec::{ - Decode, Reader, VersionedProof, - v1::{ - Attestation, DetachedTimestamp, EthereumUTSAttestation, PendingAttestation, opcode::*, - }, - }, - utils::Hexed, - verifier::{AttestationVerifier, EthereumUTSVerifier}, -}; +use std::{fs::File, path::PathBuf}; +use tracing::info; +use uts_core::codec::{Decode, Reader, VersionedProof, v1::DetachedTimestamp}; +use uts_sdk::Sdk; #[derive(Debug, Args)] pub struct Verify { file: PathBuf, stamp_file: Option, - /// Optional Ethereum provider URL for verifying Ethereum UTS attestations. - /// If not provided, a default provider will be used based on the chain ID. - #[arg(long)] - eth_provider: Option, } impl Verify { pub async fn run(self) -> eyre::Result<()> { + let sdk = Sdk::new(); + let stamp_file = self.stamp_file.unwrap_or_else(|| { let mut default = self.file.clone(); default.add_extension("ots"); default }); let timestamp = - VersionedProof::::decode(&mut Reader(File::open(stamp_file)?))? + VersionedProof::::decode(&mut Reader(File::open(&stamp_file)?))? .proof; + let results = sdk.verify(&self.file, ×tamp).await?; - let digest_header = timestamp.header(); - let mut hasher = match digest_header.kind().tag() { - SHA1 => Box::new(sha1::Sha1::new()) as Box, - RIPEMD160 => Box::new(ripemd::Ripemd160::new()) as Box, - SHA256 => Box::new(sha2::Sha256::new()) as Box, - KECCAK256 => Box::new(sha3::Keccak256::new()) as Box, - _ => bail!("Unsupported digest type: {}", digest_header.kind()), - }; - - let mut file = BufReader::new(File::open(self.file)?); - let mut buffer = [0u8; 64 * 1024]; // 64KB buffer - loop { - let bytes_read = file.read(&mut buffer)?; - if bytes_read == 0 { - break; - } - hasher.update(&buffer[..bytes_read]); - } - let expected = hasher.finalize(); - - if *expected != *digest_header.digest() { - eprintln!( - "Digest mismatch! Expected: {}, Found: {}", - Hexed(&expected), - Hexed(digest_header.digest()) + for result in results.iter() { + info!( + "Attestation: {}, Status: {:?}", + result.attestation, result.status ); - process::exit(1); - } - eprintln!("Digest matches: {}", Hexed(&expected)); - - timestamp.try_finalize()?; - - for attestation in timestamp.attestations() { - if attestation.tag == PendingAttestation::TAG { - continue; // skip pending attestations - } - - if attestation.tag == EthereumUTSAttestation::TAG { - let eth_attestation = EthereumUTSAttestation::from_raw(&attestation)?; - eprintln!("Attested by {eth_attestation}"); - let provider_url = if let Some(url) = self.eth_provider.as_deref() { - url - } else { - match eth_attestation.chain.id() { - 1 => "https://0xrpc.io/eth", - 11155111 => "https://0xrpc.io/sep", - 534352 => "https://rpc.scroll.io", - 534351 => "https://sepolia-rpc.scroll.io", - _ => bail!("Unsupported chain: {}", eth_attestation.chain), - } - }; - let provider = ProviderBuilder::new().connect(provider_url).await?; - let verifier = EthereumUTSVerifier::new(provider).await?; - let result = verifier - .verify(ð_attestation, attestation.value().unwrap()) - .await?; - if let Some(block_number) = result.block_number { - if let Some(block_hash) = result.block_hash { - eprintln!("\tblock: #{block_number} {block_hash}"); - } else { - eprintln!("\tblock: {block_number}"); - } - } - if let Some(log_index) = result.log_index { - eprintln!("\tlog index: {log_index}"); - } - if let Some(transaction_hash) = result.transaction_hash { - if let Some((_, etherscan_url)) = eth_attestation.chain.etherscan_urls() { - eprintln!("\ttransaction: {etherscan_url}/tx/{transaction_hash}"); - } else { - eprintln!("\ttransaction hash: {transaction_hash}"); - } - } - - if let Some((_, etherscan_url)) = eth_attestation.chain.etherscan_urls() { - eprintln!( - "\tuts contract: {etherscan_url}/address/{}", - result.inner.address - ); - } else { - eprintln!("\tuts contract: {}", result.inner.address); - } - if let Some((_, etherscan_url)) = eth_attestation.chain.etherscan_urls() { - eprintln!( - "\ttx sender: {etherscan_url}/address/{}", - result.inner.sender - ); - } else { - eprintln!("\ttx sender: {}", result.inner.sender); - } - let ts = Timestamp::from_second(result.inner.timestamp.to())?; - let zdt = ts.to_zoned(TimeZone::system()); - eprintln!("\ttime attested: {zdt}"); - eprintln!("\tmerkle root: {}", result.inner.root); - continue; - } - - eprintln!("Unverifiable attestation: {attestation}"); } + let overall = sdk.aggregate_verify_results(&results); + info!("Overall verification result: {overall:?}"); Ok(()) } } diff --git a/crates/cli/src/main.rs b/crates/cli/src/main.rs index 810c9a2..2d60f1a 100644 --- a/crates/cli/src/main.rs +++ b/crates/cli/src/main.rs @@ -1,8 +1,48 @@ //! UTS Cli + +// MIT License +// +// Copyright (c) 2025 UTS Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// +// Apache License, Version 2.0 +// +// Copyright (c) 2025 UTS Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + use crate::commands::Commands; use clap::Parser; +use tracing::warn; +use tracing_subscriber::{EnvFilter, filter::LevelFilter}; -mod client; mod commands; #[derive(Debug, Parser)] @@ -15,5 +55,16 @@ struct Cli { async fn main() -> eyre::Result<()> { color_eyre::install()?; + tracing_subscriber::fmt() + .with_env_filter( + EnvFilter::builder() + .with_default_directive(LevelFilter::INFO.into()) + .from_env_lossy(), + ) + .without_time() + .with_target(false) + .init(); + + warn!("UTS is current in TESTING, not for production use."); Cli::parse().command.run().await } diff --git a/crates/contracts/Cargo.toml b/crates/contracts/Cargo.toml index ac9d9ac..2aaeed0 100644 --- a/crates/contracts/Cargo.toml +++ b/crates/contracts/Cargo.toml @@ -1,26 +1,33 @@ [package] authors.workspace = true +categories = ["api-bindings"] +description = "Ethereum Smart Contracts Interfaces for Universal Timestamps Project" edition.workspace = true homepage.workspace = true -keywords.workspace = true -license.workspace = true +include = [ + "src/**/*", + "Cargo.toml", + "LICENSE-*", +] +keywords = ["contracts", "solidity", "uts", "ethereum"] +license = "MIT OR Apache-2.0" name = "uts-contracts" repository.workspace = true version.workspace = true +[lints] +workspace = true + [dependencies] alloy-contract = { workspace = true } alloy-primitives = { workspace = true } -alloy-sol-types = { workspace = true, features = ["json"] } - -[dev-dependencies] -alloy = { workspace = true, features = ["full", "node-bindings", "signer-mnemonic"] } -eyre = { workspace = true } -futures = { workspace = true } -tokio = { workspace = true, features = ["full"] } - -[lints] -workspace = true +alloy-sol-types = { workspace = true } +alloy-transport = { workspace = true, optional = true } +clap = { workspace = true, optional = true } +phf = { workspace = true, features = ["std", "macros"] } +serde = { workspace = true, optional = true } [features] -erc1967 = [] +provider-helper = ["dep:alloy-transport", "alloy-transport/throttle"] +provider-helper-clap = ["provider-helper", "dep:clap", "clap/derive"] +provider-helper-serde = ["provider-helper", "dep:serde", "serde/derive"] diff --git a/crates/contracts/LICENSE-APACHE b/crates/contracts/LICENSE-APACHE new file mode 120000 index 0000000..1cd601d --- /dev/null +++ b/crates/contracts/LICENSE-APACHE @@ -0,0 +1 @@ +../../LICENSE-APACHE \ No newline at end of file diff --git a/crates/contracts/LICENSE-MIT b/crates/contracts/LICENSE-MIT new file mode 120000 index 0000000..b2cfbdc --- /dev/null +++ b/crates/contracts/LICENSE-MIT @@ -0,0 +1 @@ +../../LICENSE-MIT \ No newline at end of file diff --git a/crates/contracts/abi/ERC1967Proxy.json b/crates/contracts/abi/ERC1967Proxy.json deleted file mode 100644 index 0650ce4..0000000 --- a/crates/contracts/abi/ERC1967Proxy.json +++ /dev/null @@ -1 +0,0 @@ -{"abi":[{"type":"constructor","inputs":[{"name":"implementation","type":"address","internalType":"address"},{"name":"_data","type":"bytes","internalType":"bytes"}],"stateMutability":"payable"},{"type":"fallback","stateMutability":"payable"},{"type":"event","name":"Upgraded","inputs":[{"name":"implementation","type":"address","indexed":true,"internalType":"address"}],"anonymous":false},{"type":"error","name":"AddressEmptyCode","inputs":[{"name":"target","type":"address","internalType":"address"}]},{"type":"error","name":"ERC1967InvalidImplementation","inputs":[{"name":"implementation","type":"address","internalType":"address"}]},{"type":"error","name":"ERC1967NonPayable","inputs":[]},{"type":"error","name":"FailedCall","inputs":[]}],"bytecode":{"object":"0x608060405260405161037338038061037383398101604081905261002291610219565b61002c8282610033565b50506102fa565b61003c82610091565b6040516001600160a01b038316907fbc7cd75a20ee27fd9adebab32041f755214dbc6bffa90cc0225b39da2e5c2d3b905f90a280511561008557610080828261010c565b505050565b61008d6101ad565b5050565b806001600160a01b03163b5f036100cb57604051634c9c8ce360e01b81526001600160a01b03821660048201526024015b60405180910390fd5b7f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc80546001600160a01b0319166001600160a01b0392909216919091179055565b60605f61011984846101ce565b905080801561013a57505f3d118061013a57505f846001600160a01b03163b115b1561014f576101476101e1565b9150506101a7565b801561017957604051639996b31560e01b81526001600160a01b03851660048201526024016100c2565b3d1561018c576101876101fa565b6101a5565b60405163d6bda27560e01b815260040160405180910390fd5b505b92915050565b34156101cc5760405163b398979f60e01b815260040160405180910390fd5b565b5f805f835160208501865af49392505050565b6040513d81523d5f602083013e3d602001810160405290565b6040513d5f823e3d81fd5b634e487b7160e01b5f52604160045260245ffd5b5f806040838503121561022a575f80fd5b82516001600160a01b0381168114610240575f80fd5b602084810151919350906001600160401b038082111561025e575f80fd5b818601915086601f830112610271575f80fd5b81518181111561028357610283610205565b604051601f8201601f19908116603f011681019083821181831017156102ab576102ab610205565b8160405282815289868487010111156102c2575f80fd5b5f93505b828410156102e357848401860151818501870152928501926102c6565b5f8684830101528096505050505050509250929050565b606d806103065f395ff3fe6080604052600a600c565b005b60186014601a565b6050565b565b5f604b7f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc546001600160a01b031690565b905090565b365f80375f80365f845af43d5f803e8080156069573d5ff35b3d5ffd","sourceMap":"600:1117:28:-:0;;;1081:133;;;;;;;;;;;;;;;;;;:::i;:::-;1155:52;1185:14;1201:5;1155:29;:52::i;:::-;1081:133;;600:1117;;2264:344:29;2355:37;2374:17;2355:18;:37::i;:::-;2407:36;;-1:-1:-1;;;;;2407:36:29;;;;;;;;2458:11;;:15;2454:148;;2489:53;2518:17;2537:4;2489:28;:53::i;:::-;;2264:344;;:::o;2454:148::-;2573:18;:16;:18::i;:::-;2264:344;;:::o;1671:281::-;1748:17;-1:-1:-1;;;;;1748:29:29;;1781:1;1748:34;1744:119;;1805:47;;-1:-1:-1;;;1805:47:29;;-1:-1:-1;;;;;1523:32:39;;1805:47:29;;;1505:51:39;1478:18;;1805:47:29;;;;;;;;1744:119;811:66;1872:73;;-1:-1:-1;;;;;;1872:73:29;-1:-1:-1;;;;;1872:73:29;;;;;;;;;;1671:281::o;4691:549:34:-;4774:12;4798;4813:47;4847:6;4855:4;4813:33;:47::i;:::-;4798:62;;4874:7;:72;;;;-1:-1:-1;4918:1:34;4583:16:36;4886:33:34;:59;;;;4944:1;4923:6;-1:-1:-1;;;;;4923:18:34;;:22;4886:59;4870:364;;;4969:25;:23;:25::i;:::-;4962:32;;;;;4870:364;5015:7;5011:223;;;5045:24;;-1:-1:-1;;;5045:24:34;;-1:-1:-1;;;;;1523:32:39;;5045:24:34;;;1505:51:39;1478:18;;5045:24:34;1359:203:39;5011:223:34;4583:16:36;5090:33:34;5086:148;;5139:27;:25;:27::i;:::-;5086:148;;;5204:19;;-1:-1:-1;;;5204:19:34;;;;;;;;;;;5086:148;4788:452;4691:549;;;;;:::o;6113:122:29:-;6163:9;:13;6159:70;;6199:19;;-1:-1:-1;;;6199:19:29;;;;;;;;;;;6159:70;6113:122::o;3383:242:36:-;3466:12;3604:4;3598;3591;3585:11;3578:4;3572;3568:15;3560:6;3553:5;3540:69;3529:80;3383:242;-1:-1:-1;;;3383:242:36:o;4698:334::-;4829:4;4823:11;4862:16;4847:32;;4932:16;4926:4;4919;4907:17;;4892:57;4997:16;4991:4;4987:27;4979:6;4975:40;4969:4;4962:54;4698:334;:::o;5099:223::-;5203:4;5197:11;5247:16;5241:4;5236:3;5221:43;5289:16;5284:3;5277:29;14:127:39;75:10;70:3;66:20;63:1;56:31;106:4;103:1;96:15;130:4;127:1;120:15;146:1208;234:6;242;295:2;283:9;274:7;270:23;266:32;263:52;;;311:1;308;301:12;263:52;337:16;;-1:-1:-1;;;;;382:31:39;;372:42;;362:70;;428:1;425;418:12;362:70;475:2;506:18;;;500:25;451:5;;-1:-1:-1;475:2:39;-1:-1:-1;;;;;574:14:39;;;571:34;;;601:1;598;591:12;571:34;639:6;628:9;624:22;614:32;;684:7;677:4;673:2;669:13;665:27;655:55;;706:1;703;696:12;655:55;735:2;729:9;757:2;753;750:10;747:36;;;763:18;;:::i;:::-;838:2;832:9;806:2;892:13;;-1:-1:-1;;888:22:39;;;912:2;884:31;880:40;868:53;;;936:18;;;956:22;;;933:46;930:72;;;982:18;;:::i;:::-;1022:10;1018:2;1011:22;1057:2;1049:6;1042:18;1097:7;1092:2;1087;1083;1079:11;1075:20;1072:33;1069:53;;;1118:1;1115;1108:12;1069:53;1140:1;1131:10;;1150:129;1164:2;1161:1;1158:9;1150:129;;;1252:10;;;1248:19;;1242:26;1221:14;;;1217:23;;1210:59;1175:10;;;;1150:129;;;1321:1;1316:2;1311;1303:6;1299:15;1295:24;1288:35;1342:6;1332:16;;;;;;;;146:1208;;;;;:::o;1359:203::-;600:1117:28;;;;;;","linkReferences":{}},"deployedBytecode":{"object":"0x6080604052600a600c565b005b60186014601a565b6050565b565b5f604b7f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc546001600160a01b031690565b905090565b365f80375f80365f845af43d5f803e8080156069573d5ff35b3d5ffd","sourceMap":"600:1117:28:-:0;;;2676:11:30;:9;:11::i;:::-;600:1117:28;2350:83:30;2398:28;2408:17;:15;:17::i;:::-;2398:9;:28::i;:::-;2350:83::o;1583:132:28:-;1650:7;1676:32;811:66:29;1519:53;-1:-1:-1;;;;;1519:53:29;;1441:138;1676:32:28;1669:39;;1583:132;:::o;949:922:30:-;1293:14;1287:4;1281;1268:40;1513:4;1507;1491:14;1485:4;1469:14;1462:5;1449:69;1598:16;1592:4;1586;1571:44;1636:6;1703:69;;;;1824:16;1818:4;1811:30;1703:69;1741:16;1735:4;1728:30","linkReferences":{}},"methodIdentifiers":{},"rawMetadata":"{\"compiler\":{\"version\":\"0.8.24+commit.e11b9ed9\"},\"language\":\"Solidity\",\"output\":{\"abi\":[{\"inputs\":[{\"internalType\":\"address\",\"name\":\"implementation\",\"type\":\"address\"},{\"internalType\":\"bytes\",\"name\":\"_data\",\"type\":\"bytes\"}],\"stateMutability\":\"payable\",\"type\":\"constructor\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"target\",\"type\":\"address\"}],\"name\":\"AddressEmptyCode\",\"type\":\"error\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"implementation\",\"type\":\"address\"}],\"name\":\"ERC1967InvalidImplementation\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"ERC1967NonPayable\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"FailedCall\",\"type\":\"error\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"implementation\",\"type\":\"address\"}],\"name\":\"Upgraded\",\"type\":\"event\"},{\"stateMutability\":\"payable\",\"type\":\"fallback\"}],\"devdoc\":{\"details\":\"This contract implements an upgradeable proxy. It is upgradeable because calls are delegated to an implementation address that can be changed. This address is stored in storage in the location specified by https://eips.ethereum.org/EIPS/eip-1967[ERC-1967], so that it doesn't conflict with the storage layout of the implementation behind the proxy.\",\"errors\":{\"AddressEmptyCode(address)\":[{\"details\":\"There's no code at `target` (it is not a contract).\"}],\"ERC1967InvalidImplementation(address)\":[{\"details\":\"The `implementation` of the proxy is invalid.\"}],\"ERC1967NonPayable()\":[{\"details\":\"An upgrade function sees `msg.value > 0` that may be lost.\"}],\"FailedCall()\":[{\"details\":\"A call to an address target failed. The target may have reverted.\"}]},\"events\":{\"Upgraded(address)\":{\"details\":\"Emitted when the implementation is upgraded.\"}},\"kind\":\"dev\",\"methods\":{\"constructor\":{\"details\":\"Initializes the upgradeable proxy with an initial implementation specified by `implementation`. If `_data` is nonempty, it's used as data in a delegate call to `implementation`. This will typically be an encoded function call, and allows initializing the storage of the proxy like a Solidity constructor. Requirements: - If `data` is empty, `msg.value` must be zero.\"}},\"version\":1},\"userdoc\":{\"kind\":\"user\",\"methods\":{},\"version\":1}},\"settings\":{\"compilationTarget\":{\"lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/contracts/proxy/ERC1967/ERC1967Proxy.sol\":\"ERC1967Proxy\"},\"evmVersion\":\"cancun\",\"libraries\":{},\"metadata\":{\"appendCBOR\":false,\"bytecodeHash\":\"none\"},\"optimizer\":{\"enabled\":true,\"runs\":200},\"remappings\":[\":@openzeppelin/contracts-upgradeable/=lib/openzeppelin-contracts-upgradeable/contracts/\",\":@openzeppelin/contracts/=lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/contracts/\",\":erc4626-tests/=lib/openzeppelin-contracts-upgradeable/lib/erc4626-tests/\",\":forge-std/=lib/forge-std/src/\",\":halmos-cheatcodes/=lib/openzeppelin-contracts-upgradeable/lib/halmos-cheatcodes/src/\",\":openzeppelin-contracts-upgradeable/=lib/openzeppelin-contracts-upgradeable/\",\":openzeppelin-contracts/=lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/\",\":openzeppelin-foundry-upgrades/=lib/openzeppelin-foundry-upgrades/src/\"]},\"sources\":{\"lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/contracts/interfaces/IERC1967.sol\":{\"keccak256\":\"0xbf2aefe54b76d7f7bcd4f6da1080b7b1662611937d870b880db584d09cea56b5\",\"license\":\"MIT\",\"urls\":[\"bzz-raw://f5e7e2f12e0feec75296e57f51f82fdaa8bd1551f4b8cc6560442c0bf60f818c\",\"dweb:/ipfs/QmcW9wDMaQ8RbQibMarfp17a3bABzY5KraWe2YDwuUrUoz\"]},\"lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/contracts/proxy/ERC1967/ERC1967Proxy.sol\":{\"keccak256\":\"0xa3066ff86b94128a9d3956a63a0511fa1aae41bd455772ab587b32ff322acb2e\",\"license\":\"MIT\",\"urls\":[\"bzz-raw://bf7b192fd82acf6187970c80548f624b1b9c80425b62fa49e7fdb538a52de049\",\"dweb:/ipfs/QmWXG1YCde1tqDYTbNwjkZDWVgPEjzaQGSDqWkyKLzaNua\"]},\"lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/contracts/proxy/ERC1967/ERC1967Utils.sol\":{\"keccak256\":\"0xa1ad192cd45317c788618bef5cb1fb3ca4ce8b230f6433ac68cc1d850fb81618\",\"license\":\"MIT\",\"urls\":[\"bzz-raw://b43447bb85a53679d269a403c693b9d88d6c74177dfb35eddca63abaf7cf110a\",\"dweb:/ipfs/QmXSDmpd4bNZj1PDgegr6C4w1jDaWHXCconC3rYiw9TSkQ\"]},\"lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/contracts/proxy/Proxy.sol\":{\"keccak256\":\"0x80935e4fae2c414f4e7789e13a820d06901182a5733ab006f8d68b5b09db993f\",\"license\":\"MIT\",\"urls\":[\"bzz-raw://752d991d6ca1087587b48103bc623f74888054f58581ff29166d90889c4765c5\",\"dweb:/ipfs/QmRBsa6K2ChKxVWYY54YiyYhDBPbmY5HyKCtij5LoWh56o\"]},\"lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/contracts/proxy/beacon/IBeacon.sol\":{\"keccak256\":\"0x20462ddb2665e9521372c76b001d0ce196e59dbbd989de9af5576cad0bd5628b\",\"license\":\"MIT\",\"urls\":[\"bzz-raw://f417fd12aeec8fbfaceaa30e3a08a0724c0bc39de363e2acf6773c897abbaf6d\",\"dweb:/ipfs/QmU4Hko6sApdweVM92CsiuLKkCk8HfyBeutF89PCTz5Tye\"]},\"lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/contracts/utils/Address.sol\":{\"keccak256\":\"0x0fa9e0d3a859900b5a46f70a03c73adf259603d5e05027a37fe0b45529d85346\",\"license\":\"MIT\",\"urls\":[\"bzz-raw://c2add4da0240c9f2ce47649c8bb6b11b40e98cf6f88b8bdc76b2704e89391710\",\"dweb:/ipfs/QmNQTwF2uVzu4CRtNxr8bxyP9XuW6VsZuo2Nr4KR2bZr3d\"]},\"lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/contracts/utils/Errors.sol\":{\"keccak256\":\"0x6afa713bfd42cf0f7656efa91201007ac465e42049d7de1d50753a373648c123\",\"license\":\"MIT\",\"urls\":[\"bzz-raw://ba1d02f4847670a1b83dec9f7d37f0b0418d6043447b69f3a29a5f9efc547fcf\",\"dweb:/ipfs/QmQ7iH2keLNUKgq2xSWcRmuBE5eZ3F5whYAkAGzCNNoEWB\"]},\"lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/contracts/utils/LowLevelCall.sol\":{\"keccak256\":\"0x5b4802a4352474792df3107e961d1cc593e47b820c14f69d3505cb28f5a6a583\",\"license\":\"MIT\",\"urls\":[\"bzz-raw://a6f86fd01f829499fe0545ff5dda07d4521988e88bfe0bf801fc15650921ed56\",\"dweb:/ipfs/QmUUKu4ZDffHAmfkf3asuQfmLTyfpuy2Amdncc3SqfzKPG\"]},\"lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/contracts/utils/StorageSlot.sol\":{\"keccak256\":\"0xcf74f855663ce2ae00ed8352666b7935f6cddea2932fdf2c3ecd30a9b1cd0e97\",\"license\":\"MIT\",\"urls\":[\"bzz-raw://9f660b1f351b757dfe01438e59888f31f33ded3afcf5cb5b0d9bf9aa6f320a8b\",\"dweb:/ipfs/QmarDJ5hZEgBtCmmrVzEZWjub9769eD686jmzb2XpSU1cM\"]}},\"version\":1}","metadata":{"compiler":{"version":"0.8.24+commit.e11b9ed9"},"language":"Solidity","output":{"abi":[{"inputs":[{"internalType":"address","name":"implementation","type":"address"},{"internalType":"bytes","name":"_data","type":"bytes"}],"stateMutability":"payable","type":"constructor"},{"inputs":[{"internalType":"address","name":"target","type":"address"}],"type":"error","name":"AddressEmptyCode"},{"inputs":[{"internalType":"address","name":"implementation","type":"address"}],"type":"error","name":"ERC1967InvalidImplementation"},{"inputs":[],"type":"error","name":"ERC1967NonPayable"},{"inputs":[],"type":"error","name":"FailedCall"},{"inputs":[{"internalType":"address","name":"implementation","type":"address","indexed":true}],"type":"event","name":"Upgraded","anonymous":false},{"inputs":[],"stateMutability":"payable","type":"fallback"}],"devdoc":{"kind":"dev","methods":{"constructor":{"details":"Initializes the upgradeable proxy with an initial implementation specified by `implementation`. If `_data` is nonempty, it's used as data in a delegate call to `implementation`. This will typically be an encoded function call, and allows initializing the storage of the proxy like a Solidity constructor. Requirements: - If `data` is empty, `msg.value` must be zero."}},"version":1},"userdoc":{"kind":"user","methods":{},"version":1}},"settings":{"remappings":["@openzeppelin/contracts-upgradeable/=lib/openzeppelin-contracts-upgradeable/contracts/","@openzeppelin/contracts/=lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/contracts/","erc4626-tests/=lib/openzeppelin-contracts-upgradeable/lib/erc4626-tests/","forge-std/=lib/forge-std/src/","halmos-cheatcodes/=lib/openzeppelin-contracts-upgradeable/lib/halmos-cheatcodes/src/","openzeppelin-contracts-upgradeable/=lib/openzeppelin-contracts-upgradeable/","openzeppelin-contracts/=lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/","openzeppelin-foundry-upgrades/=lib/openzeppelin-foundry-upgrades/src/"],"optimizer":{"enabled":true,"runs":200},"metadata":{"bytecodeHash":"none","appendCBOR":false},"compilationTarget":{"lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/contracts/proxy/ERC1967/ERC1967Proxy.sol":"ERC1967Proxy"},"evmVersion":"cancun","libraries":{}},"sources":{"lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/contracts/interfaces/IERC1967.sol":{"keccak256":"0xbf2aefe54b76d7f7bcd4f6da1080b7b1662611937d870b880db584d09cea56b5","urls":["bzz-raw://f5e7e2f12e0feec75296e57f51f82fdaa8bd1551f4b8cc6560442c0bf60f818c","dweb:/ipfs/QmcW9wDMaQ8RbQibMarfp17a3bABzY5KraWe2YDwuUrUoz"],"license":"MIT"},"lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/contracts/proxy/ERC1967/ERC1967Proxy.sol":{"keccak256":"0xa3066ff86b94128a9d3956a63a0511fa1aae41bd455772ab587b32ff322acb2e","urls":["bzz-raw://bf7b192fd82acf6187970c80548f624b1b9c80425b62fa49e7fdb538a52de049","dweb:/ipfs/QmWXG1YCde1tqDYTbNwjkZDWVgPEjzaQGSDqWkyKLzaNua"],"license":"MIT"},"lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/contracts/proxy/ERC1967/ERC1967Utils.sol":{"keccak256":"0xa1ad192cd45317c788618bef5cb1fb3ca4ce8b230f6433ac68cc1d850fb81618","urls":["bzz-raw://b43447bb85a53679d269a403c693b9d88d6c74177dfb35eddca63abaf7cf110a","dweb:/ipfs/QmXSDmpd4bNZj1PDgegr6C4w1jDaWHXCconC3rYiw9TSkQ"],"license":"MIT"},"lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/contracts/proxy/Proxy.sol":{"keccak256":"0x80935e4fae2c414f4e7789e13a820d06901182a5733ab006f8d68b5b09db993f","urls":["bzz-raw://752d991d6ca1087587b48103bc623f74888054f58581ff29166d90889c4765c5","dweb:/ipfs/QmRBsa6K2ChKxVWYY54YiyYhDBPbmY5HyKCtij5LoWh56o"],"license":"MIT"},"lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/contracts/proxy/beacon/IBeacon.sol":{"keccak256":"0x20462ddb2665e9521372c76b001d0ce196e59dbbd989de9af5576cad0bd5628b","urls":["bzz-raw://f417fd12aeec8fbfaceaa30e3a08a0724c0bc39de363e2acf6773c897abbaf6d","dweb:/ipfs/QmU4Hko6sApdweVM92CsiuLKkCk8HfyBeutF89PCTz5Tye"],"license":"MIT"},"lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/contracts/utils/Address.sol":{"keccak256":"0x0fa9e0d3a859900b5a46f70a03c73adf259603d5e05027a37fe0b45529d85346","urls":["bzz-raw://c2add4da0240c9f2ce47649c8bb6b11b40e98cf6f88b8bdc76b2704e89391710","dweb:/ipfs/QmNQTwF2uVzu4CRtNxr8bxyP9XuW6VsZuo2Nr4KR2bZr3d"],"license":"MIT"},"lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/contracts/utils/Errors.sol":{"keccak256":"0x6afa713bfd42cf0f7656efa91201007ac465e42049d7de1d50753a373648c123","urls":["bzz-raw://ba1d02f4847670a1b83dec9f7d37f0b0418d6043447b69f3a29a5f9efc547fcf","dweb:/ipfs/QmQ7iH2keLNUKgq2xSWcRmuBE5eZ3F5whYAkAGzCNNoEWB"],"license":"MIT"},"lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/contracts/utils/LowLevelCall.sol":{"keccak256":"0x5b4802a4352474792df3107e961d1cc593e47b820c14f69d3505cb28f5a6a583","urls":["bzz-raw://a6f86fd01f829499fe0545ff5dda07d4521988e88bfe0bf801fc15650921ed56","dweb:/ipfs/QmUUKu4ZDffHAmfkf3asuQfmLTyfpuy2Amdncc3SqfzKPG"],"license":"MIT"},"lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/contracts/utils/StorageSlot.sol":{"keccak256":"0xcf74f855663ce2ae00ed8352666b7935f6cddea2932fdf2c3ecd30a9b1cd0e97","urls":["bzz-raw://9f660b1f351b757dfe01438e59888f31f33ded3afcf5cb5b0d9bf9aa6f320a8b","dweb:/ipfs/QmarDJ5hZEgBtCmmrVzEZWjub9769eD686jmzb2XpSU1cM"],"license":"MIT"}},"version":1},"storageLayout":{"storage":[],"types":{}},"ast":{"absolutePath":"lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/contracts/proxy/ERC1967/ERC1967Proxy.sol","id":40451,"exportedSymbols":{"ERC1967Proxy":[40450],"ERC1967Utils":[40744],"Proxy":[40780]},"nodeType":"SourceUnit","src":"114:1604:28","nodes":[{"id":40414,"nodeType":"PragmaDirective","src":"114:24:28","nodes":[],"literals":["solidity","^","0.8",".22"]},{"id":40416,"nodeType":"ImportDirective","src":"140:35:28","nodes":[],"absolutePath":"lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/contracts/proxy/Proxy.sol","file":"../Proxy.sol","nameLocation":"-1:-1:-1","scope":40451,"sourceUnit":40781,"symbolAliases":[{"foreign":{"id":40415,"name":"Proxy","nodeType":"Identifier","overloadedDeclarations":[],"referencedDeclaration":40780,"src":"148:5:28","typeDescriptions":{}},"nameLocation":"-1:-1:-1"}],"unitAlias":""},{"id":40418,"nodeType":"ImportDirective","src":"176:48:28","nodes":[],"absolutePath":"lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/contracts/proxy/ERC1967/ERC1967Utils.sol","file":"./ERC1967Utils.sol","nameLocation":"-1:-1:-1","scope":40451,"sourceUnit":40745,"symbolAliases":[{"foreign":{"id":40417,"name":"ERC1967Utils","nodeType":"Identifier","overloadedDeclarations":[],"referencedDeclaration":40744,"src":"184:12:28","typeDescriptions":{}},"nameLocation":"-1:-1:-1"}],"unitAlias":""},{"id":40450,"nodeType":"ContractDefinition","src":"600:1117:28","nodes":[{"id":40437,"nodeType":"FunctionDefinition","src":"1081:133:28","nodes":[],"body":{"id":40436,"nodeType":"Block","src":"1145:69:28","nodes":[],"statements":[{"expression":{"arguments":[{"id":40432,"name":"implementation","nodeType":"Identifier","overloadedDeclarations":[],"referencedDeclaration":40424,"src":"1185:14:28","typeDescriptions":{"typeIdentifier":"t_address","typeString":"address"}},{"id":40433,"name":"_data","nodeType":"Identifier","overloadedDeclarations":[],"referencedDeclaration":40426,"src":"1201:5:28","typeDescriptions":{"typeIdentifier":"t_bytes_memory_ptr","typeString":"bytes memory"}}],"expression":{"argumentTypes":[{"typeIdentifier":"t_address","typeString":"address"},{"typeIdentifier":"t_bytes_memory_ptr","typeString":"bytes memory"}],"expression":{"id":40429,"name":"ERC1967Utils","nodeType":"Identifier","overloadedDeclarations":[],"referencedDeclaration":40744,"src":"1155:12:28","typeDescriptions":{"typeIdentifier":"t_type$_t_contract$_ERC1967Utils_$40744_$","typeString":"type(library ERC1967Utils)"}},"id":40431,"isConstant":false,"isLValue":false,"isPure":false,"lValueRequested":false,"memberLocation":"1168:16:28","memberName":"upgradeToAndCall","nodeType":"MemberAccess","referencedDeclaration":40559,"src":"1155:29:28","typeDescriptions":{"typeIdentifier":"t_function_internal_nonpayable$_t_address_$_t_bytes_memory_ptr_$returns$__$","typeString":"function (address,bytes memory)"}},"id":40434,"isConstant":false,"isLValue":false,"isPure":false,"kind":"functionCall","lValueRequested":false,"nameLocations":[],"names":[],"nodeType":"FunctionCall","src":"1155:52:28","tryCall":false,"typeDescriptions":{"typeIdentifier":"t_tuple$__$","typeString":"tuple()"}},"id":40435,"nodeType":"ExpressionStatement","src":"1155:52:28"}]},"documentation":{"id":40422,"nodeType":"StructuredDocumentation","src":"637:439:28","text":" @dev Initializes the upgradeable proxy with an initial implementation specified by `implementation`.\n If `_data` is nonempty, it's used as data in a delegate call to `implementation`. This will typically be an\n encoded function call, and allows initializing the storage of the proxy like a Solidity constructor.\n Requirements:\n - If `data` is empty, `msg.value` must be zero."},"implemented":true,"kind":"constructor","modifiers":[],"name":"","nameLocation":"-1:-1:-1","parameters":{"id":40427,"nodeType":"ParameterList","parameters":[{"constant":false,"id":40424,"mutability":"mutable","name":"implementation","nameLocation":"1101:14:28","nodeType":"VariableDeclaration","scope":40437,"src":"1093:22:28","stateVariable":false,"storageLocation":"default","typeDescriptions":{"typeIdentifier":"t_address","typeString":"address"},"typeName":{"id":40423,"name":"address","nodeType":"ElementaryTypeName","src":"1093:7:28","stateMutability":"nonpayable","typeDescriptions":{"typeIdentifier":"t_address","typeString":"address"}},"visibility":"internal"},{"constant":false,"id":40426,"mutability":"mutable","name":"_data","nameLocation":"1130:5:28","nodeType":"VariableDeclaration","scope":40437,"src":"1117:18:28","stateVariable":false,"storageLocation":"memory","typeDescriptions":{"typeIdentifier":"t_bytes_memory_ptr","typeString":"bytes"},"typeName":{"id":40425,"name":"bytes","nodeType":"ElementaryTypeName","src":"1117:5:28","typeDescriptions":{"typeIdentifier":"t_bytes_storage_ptr","typeString":"bytes"}},"visibility":"internal"}],"src":"1092:44:28"},"returnParameters":{"id":40428,"nodeType":"ParameterList","parameters":[],"src":"1145:0:28"},"scope":40450,"stateMutability":"payable","virtual":false,"visibility":"public"},{"id":40449,"nodeType":"FunctionDefinition","src":"1583:132:28","nodes":[],"body":{"id":40448,"nodeType":"Block","src":"1659:56:28","nodes":[],"statements":[{"expression":{"arguments":[],"expression":{"argumentTypes":[],"expression":{"id":40444,"name":"ERC1967Utils","nodeType":"Identifier","overloadedDeclarations":[],"referencedDeclaration":40744,"src":"1676:12:28","typeDescriptions":{"typeIdentifier":"t_type$_t_contract$_ERC1967Utils_$40744_$","typeString":"type(library ERC1967Utils)"}},"id":40445,"isConstant":false,"isLValue":false,"isPure":false,"lValueRequested":false,"memberLocation":"1689:17:28","memberName":"getImplementation","nodeType":"MemberAccess","referencedDeclaration":40496,"src":"1676:30:28","typeDescriptions":{"typeIdentifier":"t_function_internal_view$__$returns$_t_address_$","typeString":"function () view returns (address)"}},"id":40446,"isConstant":false,"isLValue":false,"isPure":false,"kind":"functionCall","lValueRequested":false,"nameLocations":[],"names":[],"nodeType":"FunctionCall","src":"1676:32:28","tryCall":false,"typeDescriptions":{"typeIdentifier":"t_address","typeString":"address"}},"functionReturnParameters":40443,"id":40447,"nodeType":"Return","src":"1669:39:28"}]},"baseFunctions":[40761],"documentation":{"id":40438,"nodeType":"StructuredDocumentation","src":"1220:358:28","text":" @dev Returns the current implementation address.\n TIP: To get this value clients can read directly from the storage slot shown below (specified by ERC-1967) using\n the https://eth.wiki/json-rpc/API#eth_getstorageat[`eth_getStorageAt`] RPC call.\n `0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc`"},"implemented":true,"kind":"function","modifiers":[],"name":"_implementation","nameLocation":"1592:15:28","overrides":{"id":40440,"nodeType":"OverrideSpecifier","overrides":[],"src":"1632:8:28"},"parameters":{"id":40439,"nodeType":"ParameterList","parameters":[],"src":"1607:2:28"},"returnParameters":{"id":40443,"nodeType":"ParameterList","parameters":[{"constant":false,"id":40442,"mutability":"mutable","name":"","nameLocation":"-1:-1:-1","nodeType":"VariableDeclaration","scope":40449,"src":"1650:7:28","stateVariable":false,"storageLocation":"default","typeDescriptions":{"typeIdentifier":"t_address","typeString":"address"},"typeName":{"id":40441,"name":"address","nodeType":"ElementaryTypeName","src":"1650:7:28","stateMutability":"nonpayable","typeDescriptions":{"typeIdentifier":"t_address","typeString":"address"}},"visibility":"internal"}],"src":"1649:9:28"},"scope":40450,"stateMutability":"view","virtual":true,"visibility":"internal"}],"abstract":false,"baseContracts":[{"baseName":{"id":40420,"name":"Proxy","nameLocations":["625:5:28"],"nodeType":"IdentifierPath","referencedDeclaration":40780,"src":"625:5:28"},"id":40421,"nodeType":"InheritanceSpecifier","src":"625:5:28"}],"canonicalName":"ERC1967Proxy","contractDependencies":[],"contractKind":"contract","documentation":{"id":40419,"nodeType":"StructuredDocumentation","src":"226:373:28","text":" @dev This contract implements an upgradeable proxy. It is upgradeable because calls are delegated to an\n implementation address that can be changed. This address is stored in storage in the location specified by\n https://eips.ethereum.org/EIPS/eip-1967[ERC-1967], so that it doesn't conflict with the storage layout of the\n implementation behind the proxy."},"fullyImplemented":true,"linearizedBaseContracts":[40450,40780],"name":"ERC1967Proxy","nameLocation":"609:12:28","scope":40451,"usedErrors":[40470,40483,41236,41627],"usedEvents":[40389]}],"license":"MIT"},"id":28} \ No newline at end of file diff --git a/crates/contracts/abi/IUniversalTimestamps.json b/crates/contracts/abi/IUniversalTimestamps.json deleted file mode 100644 index 9a80366..0000000 --- a/crates/contracts/abi/IUniversalTimestamps.json +++ /dev/null @@ -1 +0,0 @@ -{"abi":[{"type":"function","name":"attest","inputs":[{"name":"root","type":"bytes32","internalType":"bytes32"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"timestamp","inputs":[{"name":"root","type":"bytes32","internalType":"bytes32"}],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"event","name":"Attested","inputs":[{"name":"root","type":"bytes32","indexed":true,"internalType":"bytes32"},{"name":"sender","type":"address","indexed":true,"internalType":"address"},{"name":"timestamp","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false}],"bytecode":{"object":"0x","sourceMap":"","linkReferences":{}},"deployedBytecode":{"object":"0x","sourceMap":"","linkReferences":{}},"methodIdentifiers":{"attest(bytes32)":"23c3617f","timestamp(bytes32)":"4d003070"},"rawMetadata":"{\"compiler\":{\"version\":\"0.8.24+commit.e11b9ed9\"},\"language\":\"Solidity\",\"output\":{\"abi\":[{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"bytes32\",\"name\":\"root\",\"type\":\"bytes32\"},{\"indexed\":true,\"internalType\":\"address\",\"name\":\"sender\",\"type\":\"address\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"timestamp\",\"type\":\"uint256\"}],\"name\":\"Attested\",\"type\":\"event\"},{\"inputs\":[{\"internalType\":\"bytes32\",\"name\":\"root\",\"type\":\"bytes32\"}],\"name\":\"attest\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"bytes32\",\"name\":\"root\",\"type\":\"bytes32\"}],\"name\":\"timestamp\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"}],\"devdoc\":{\"kind\":\"dev\",\"methods\":{},\"version\":1},\"userdoc\":{\"kind\":\"user\",\"methods\":{},\"version\":1}},\"settings\":{\"compilationTarget\":{\"contracts/IUniversalTimestamps.sol\":\"IUniversalTimestamps\"},\"evmVersion\":\"cancun\",\"libraries\":{},\"metadata\":{\"appendCBOR\":false,\"bytecodeHash\":\"none\"},\"optimizer\":{\"enabled\":true,\"runs\":200},\"remappings\":[\":@openzeppelin/contracts-upgradeable/=lib/openzeppelin-contracts-upgradeable/contracts/\",\":@openzeppelin/contracts/=lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/contracts/\",\":erc4626-tests/=lib/openzeppelin-contracts-upgradeable/lib/erc4626-tests/\",\":forge-std/=lib/forge-std/src/\",\":halmos-cheatcodes/=lib/openzeppelin-contracts-upgradeable/lib/halmos-cheatcodes/src/\",\":openzeppelin-contracts-upgradeable/=lib/openzeppelin-contracts-upgradeable/\",\":openzeppelin-contracts/=lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/\",\":openzeppelin-foundry-upgrades/=lib/openzeppelin-foundry-upgrades/src/\"]},\"sources\":{\"contracts/IUniversalTimestamps.sol\":{\"keccak256\":\"0xfa9490d2704cebe76fa78d15f51ed3bf13577ffaf782fe0337db402872571df0\",\"license\":\"MIT\",\"urls\":[\"bzz-raw://c42b87e7040e6d72f5b49a2bff481191678e15df1c19793e9c55800e8f5276cf\",\"dweb:/ipfs/QmNr4ET95fihTnh3YF6WjjKUsCFU7KkoPkvKAizb83pyCC\"]}},\"version\":1}","metadata":{"compiler":{"version":"0.8.24+commit.e11b9ed9"},"language":"Solidity","output":{"abi":[{"inputs":[{"internalType":"bytes32","name":"root","type":"bytes32","indexed":true},{"internalType":"address","name":"sender","type":"address","indexed":true},{"internalType":"uint256","name":"timestamp","type":"uint256","indexed":false}],"type":"event","name":"Attested","anonymous":false},{"inputs":[{"internalType":"bytes32","name":"root","type":"bytes32"}],"stateMutability":"nonpayable","type":"function","name":"attest"},{"inputs":[{"internalType":"bytes32","name":"root","type":"bytes32"}],"stateMutability":"view","type":"function","name":"timestamp","outputs":[{"internalType":"uint256","name":"","type":"uint256"}]}],"devdoc":{"kind":"dev","methods":{},"version":1},"userdoc":{"kind":"user","methods":{},"version":1}},"settings":{"remappings":["@openzeppelin/contracts-upgradeable/=lib/openzeppelin-contracts-upgradeable/contracts/","@openzeppelin/contracts/=lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/contracts/","erc4626-tests/=lib/openzeppelin-contracts-upgradeable/lib/erc4626-tests/","forge-std/=lib/forge-std/src/","halmos-cheatcodes/=lib/openzeppelin-contracts-upgradeable/lib/halmos-cheatcodes/src/","openzeppelin-contracts-upgradeable/=lib/openzeppelin-contracts-upgradeable/","openzeppelin-contracts/=lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/","openzeppelin-foundry-upgrades/=lib/openzeppelin-foundry-upgrades/src/"],"optimizer":{"enabled":true,"runs":200},"metadata":{"bytecodeHash":"none","appendCBOR":false},"compilationTarget":{"contracts/IUniversalTimestamps.sol":"IUniversalTimestamps"},"evmVersion":"cancun","libraries":{}},"sources":{"contracts/IUniversalTimestamps.sol":{"keccak256":"0xfa9490d2704cebe76fa78d15f51ed3bf13577ffaf782fe0337db402872571df0","urls":["bzz-raw://c42b87e7040e6d72f5b49a2bff481191678e15df1c19793e9c55800e8f5276cf","dweb:/ipfs/QmNr4ET95fihTnh3YF6WjjKUsCFU7KkoPkvKAizb83pyCC"],"license":"MIT"}},"version":1},"storageLayout":{"storage":[],"types":{}},"ast":{"absolutePath":"contracts/IUniversalTimestamps.sol","id":187,"exportedSymbols":{"IUniversalTimestamps":[186]},"nodeType":"SourceUnit","src":"33:262:1","nodes":[{"id":165,"nodeType":"PragmaDirective","src":"33:24:1","nodes":[],"literals":["solidity","^","0.8",".24"]},{"id":186,"nodeType":"ContractDefinition","src":"59:235:1","nodes":[{"id":173,"nodeType":"EventDefinition","src":"96:80:1","nodes":[],"anonymous":false,"eventSelector":"61cae4201bb8c0117495b22a70f5202410666b349c27302dac280dc054b60f2a","name":"Attested","nameLocation":"102:8:1","parameters":{"id":172,"nodeType":"ParameterList","parameters":[{"constant":false,"id":167,"indexed":true,"mutability":"mutable","name":"root","nameLocation":"127:4:1","nodeType":"VariableDeclaration","scope":173,"src":"111:20:1","stateVariable":false,"storageLocation":"default","typeDescriptions":{"typeIdentifier":"t_bytes32","typeString":"bytes32"},"typeName":{"id":166,"name":"bytes32","nodeType":"ElementaryTypeName","src":"111:7:1","typeDescriptions":{"typeIdentifier":"t_bytes32","typeString":"bytes32"}},"visibility":"internal"},{"constant":false,"id":169,"indexed":true,"mutability":"mutable","name":"sender","nameLocation":"149:6:1","nodeType":"VariableDeclaration","scope":173,"src":"133:22:1","stateVariable":false,"storageLocation":"default","typeDescriptions":{"typeIdentifier":"t_address","typeString":"address"},"typeName":{"id":168,"name":"address","nodeType":"ElementaryTypeName","src":"133:7:1","stateMutability":"nonpayable","typeDescriptions":{"typeIdentifier":"t_address","typeString":"address"}},"visibility":"internal"},{"constant":false,"id":171,"indexed":false,"mutability":"mutable","name":"timestamp","nameLocation":"165:9:1","nodeType":"VariableDeclaration","scope":173,"src":"157:17:1","stateVariable":false,"storageLocation":"default","typeDescriptions":{"typeIdentifier":"t_uint256","typeString":"uint256"},"typeName":{"id":170,"name":"uint256","nodeType":"ElementaryTypeName","src":"157:7:1","typeDescriptions":{"typeIdentifier":"t_uint256","typeString":"uint256"}},"visibility":"internal"}],"src":"110:65:1"}},{"id":178,"nodeType":"FunctionDefinition","src":"182:39:1","nodes":[],"functionSelector":"23c3617f","implemented":false,"kind":"function","modifiers":[],"name":"attest","nameLocation":"191:6:1","parameters":{"id":176,"nodeType":"ParameterList","parameters":[{"constant":false,"id":175,"mutability":"mutable","name":"root","nameLocation":"206:4:1","nodeType":"VariableDeclaration","scope":178,"src":"198:12:1","stateVariable":false,"storageLocation":"default","typeDescriptions":{"typeIdentifier":"t_bytes32","typeString":"bytes32"},"typeName":{"id":174,"name":"bytes32","nodeType":"ElementaryTypeName","src":"198:7:1","typeDescriptions":{"typeIdentifier":"t_bytes32","typeString":"bytes32"}},"visibility":"internal"}],"src":"197:14:1"},"returnParameters":{"id":177,"nodeType":"ParameterList","parameters":[],"src":"220:0:1"},"scope":186,"stateMutability":"nonpayable","virtual":false,"visibility":"external"},{"id":185,"nodeType":"FunctionDefinition","src":"227:65:1","nodes":[],"functionSelector":"4d003070","implemented":false,"kind":"function","modifiers":[],"name":"timestamp","nameLocation":"236:9:1","parameters":{"id":181,"nodeType":"ParameterList","parameters":[{"constant":false,"id":180,"mutability":"mutable","name":"root","nameLocation":"254:4:1","nodeType":"VariableDeclaration","scope":185,"src":"246:12:1","stateVariable":false,"storageLocation":"default","typeDescriptions":{"typeIdentifier":"t_bytes32","typeString":"bytes32"},"typeName":{"id":179,"name":"bytes32","nodeType":"ElementaryTypeName","src":"246:7:1","typeDescriptions":{"typeIdentifier":"t_bytes32","typeString":"bytes32"}},"visibility":"internal"}],"src":"245:14:1"},"returnParameters":{"id":184,"nodeType":"ParameterList","parameters":[{"constant":false,"id":183,"mutability":"mutable","name":"","nameLocation":"-1:-1:-1","nodeType":"VariableDeclaration","scope":185,"src":"283:7:1","stateVariable":false,"storageLocation":"default","typeDescriptions":{"typeIdentifier":"t_uint256","typeString":"uint256"},"typeName":{"id":182,"name":"uint256","nodeType":"ElementaryTypeName","src":"283:7:1","typeDescriptions":{"typeIdentifier":"t_uint256","typeString":"uint256"}},"visibility":"internal"}],"src":"282:9:1"},"scope":186,"stateMutability":"view","virtual":false,"visibility":"external"}],"abstract":false,"baseContracts":[],"canonicalName":"IUniversalTimestamps","contractDependencies":[],"contractKind":"interface","fullyImplemented":false,"linearizedBaseContracts":[186],"name":"IUniversalTimestamps","nameLocation":"69:20:1","scope":187,"usedErrors":[],"usedEvents":[173]}],"license":"MIT"},"id":1} \ No newline at end of file diff --git a/crates/contracts/abi/UniversalTimestamps.json b/crates/contracts/abi/UniversalTimestamps.json deleted file mode 100644 index 1110a7d..0000000 --- a/crates/contracts/abi/UniversalTimestamps.json +++ /dev/null @@ -1 +0,0 @@ -{"abi":[{"type":"constructor","inputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"UPGRADE_INTERFACE_VERSION","inputs":[],"outputs":[{"name":"","type":"string","internalType":"string"}],"stateMutability":"view"},{"type":"function","name":"attest","inputs":[{"name":"root","type":"bytes32","internalType":"bytes32"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"initialize","inputs":[{"name":"initialOwner","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"owner","inputs":[],"outputs":[{"name":"","type":"address","internalType":"address"}],"stateMutability":"view"},{"type":"function","name":"proxiableUUID","inputs":[],"outputs":[{"name":"","type":"bytes32","internalType":"bytes32"}],"stateMutability":"view"},{"type":"function","name":"renounceOwnership","inputs":[],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"timestamp","inputs":[{"name":"root","type":"bytes32","internalType":"bytes32"}],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"view"},{"type":"function","name":"transferOwnership","inputs":[{"name":"newOwner","type":"address","internalType":"address"}],"outputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"upgradeToAndCall","inputs":[{"name":"newImplementation","type":"address","internalType":"address"},{"name":"data","type":"bytes","internalType":"bytes"}],"outputs":[],"stateMutability":"payable"},{"type":"event","name":"Attested","inputs":[{"name":"root","type":"bytes32","indexed":true,"internalType":"bytes32"},{"name":"sender","type":"address","indexed":true,"internalType":"address"},{"name":"timestamp","type":"uint256","indexed":false,"internalType":"uint256"}],"anonymous":false},{"type":"event","name":"Initialized","inputs":[{"name":"version","type":"uint64","indexed":false,"internalType":"uint64"}],"anonymous":false},{"type":"event","name":"OwnershipTransferred","inputs":[{"name":"previousOwner","type":"address","indexed":true,"internalType":"address"},{"name":"newOwner","type":"address","indexed":true,"internalType":"address"}],"anonymous":false},{"type":"event","name":"Upgraded","inputs":[{"name":"implementation","type":"address","indexed":true,"internalType":"address"}],"anonymous":false},{"type":"error","name":"AddressEmptyCode","inputs":[{"name":"target","type":"address","internalType":"address"}]},{"type":"error","name":"ERC1967InvalidImplementation","inputs":[{"name":"implementation","type":"address","internalType":"address"}]},{"type":"error","name":"ERC1967NonPayable","inputs":[]},{"type":"error","name":"FailedCall","inputs":[]},{"type":"error","name":"InvalidInitialization","inputs":[]},{"type":"error","name":"NotInitializing","inputs":[]},{"type":"error","name":"OwnableInvalidOwner","inputs":[{"name":"owner","type":"address","internalType":"address"}]},{"type":"error","name":"OwnableUnauthorizedAccount","inputs":[{"name":"account","type":"address","internalType":"address"}]},{"type":"error","name":"UUPSUnauthorizedCallContext","inputs":[]},{"type":"error","name":"UUPSUnsupportedProxiableUUID","inputs":[{"name":"slot","type":"bytes32","internalType":"bytes32"}]}],"bytecode":{"object":"0x60a060405230608052348015610013575f80fd5b5061001c610021565b6100d3565b7ff0c57e16840df040f15088dc2f81fe391c3923bec73e23a9662efc9c229c6a00805468010000000000000000900460ff16156100715760405163f92ee8a960e01b815260040160405180910390fd5b80546001600160401b03908116146100d05780546001600160401b0319166001600160401b0390811782556040519081527fc7f505b2f371ae2175ee4913f4499e1f2633a7b5936321eed1cdaeb6115181d29060200160405180910390a15b50565b608051610b046100f95f395f81816104bb015281816104e401526106280152610b045ff3fe608060405260043610610084575f3560e01c8063715018a611610057578063715018a6146101025780638da5cb5b14610116578063ad3cb1cc1461015c578063c4d66de814610199578063f2fde38b146101b8575f80fd5b806323c3617f146100885780634d003070146100a95780634f1ef286146100db57806352d1902d146100ee575b5f80fd5b348015610093575f80fd5b506100a76100a236600461095e565b6101d7565b005b3480156100b4575f80fd5b506100c86100c336600461095e565b610295565b6040519081526020015b60405180910390f35b6100a76100e93660046109a4565b6102ae565b3480156100f9575f80fd5b506100c86102c9565b34801561010d575f80fd5b506100a76102e4565b348015610121575f80fd5b507f9016d09d72d40fdae2fd8ceac6b6234c7706214fd39c1cd1e609a0528c199300546040516001600160a01b0390911681526020016100d2565b348015610167575f80fd5b5061018c604051806040016040528060058152602001640352e302e360dc1b81525081565b6040516100d29190610a60565b3480156101a4575f80fd5b506100a76101b3366004610aac565b6102f7565b3480156101c3575f80fd5b506100a76101d2366004610aac565b6103f0565b806102295760405162461bcd60e51b815260206004820152601860248201527f5554533a20526f6f742063616e6e6f74206265207a65726f000000000000000060448201526064015b60405180910390fd5b5f61023261042d565b5f8381526020829052604081205491925003610291575f828152602082815260409182902042908190559151918252339184917f61cae4201bb8c0117495b22a70f5202410666b349c27302dac280dc054b60f2a910160405180910390a35b5050565b5f61029e61042d565b5f92835260205250604090205490565b6102b66104b0565b6102bf82610554565b610291828261055c565b5f6102d261061d565b505f80516020610ae483398151915290565b6102ec610666565b6102f55f6106c1565b565b5f610300610731565b805490915060ff600160401b820416159067ffffffffffffffff165f811580156103275750825b90505f8267ffffffffffffffff1660011480156103435750303b155b905081158015610351575080155b1561036f5760405163f92ee8a960e01b815260040160405180910390fd5b845467ffffffffffffffff19166001178555831561039957845460ff60401b1916600160401b1785555b6103a286610759565b83156103e857845460ff60401b19168555604051600181527fc7f505b2f371ae2175ee4913f4499e1f2633a7b5936321eed1cdaeb6115181d29060200160405180910390a15b505050505050565b6103f8610666565b6001600160a01b03811661042157604051631e4fbdf760e01b81525f6004820152602401610220565b61042a816106c1565b50565b60408051808201909152601f81527f7574732e73746f726167652e556e6976657273616c54696d657374616d7073006020909101527f6191da0f5f254a176c2a5b8e81a37f62349600d58cdbf87518a33cdde24d517a5f908152807f500a69046951d8ea21a7dabf6fe6e1792e3ffa4dc61a276ae65b0e2b034681005b92915050565b306001600160a01b037f000000000000000000000000000000000000000000000000000000000000000016148061053657507f00000000000000000000000000000000000000000000000000000000000000006001600160a01b031661052a5f80516020610ae4833981519152546001600160a01b031690565b6001600160a01b031614155b156102f55760405163703e46dd60e11b815260040160405180910390fd5b61042a610666565b816001600160a01b03166352d1902d6040518163ffffffff1660e01b8152600401602060405180830381865afa9250505080156105b6575060408051601f3d908101601f191682019092526105b391810190610acc565b60015b6105de57604051634c9c8ce360e01b81526001600160a01b0383166004820152602401610220565b5f80516020610ae4833981519152811461060e57604051632a87526960e21b815260048101829052602401610220565b610618838361076a565b505050565b306001600160a01b037f000000000000000000000000000000000000000000000000000000000000000016146102f55760405163703e46dd60e11b815260040160405180910390fd5b336106987f9016d09d72d40fdae2fd8ceac6b6234c7706214fd39c1cd1e609a0528c199300546001600160a01b031690565b6001600160a01b0316146102f55760405163118cdaa760e01b8152336004820152602401610220565b7f9016d09d72d40fdae2fd8ceac6b6234c7706214fd39c1cd1e609a0528c19930080546001600160a01b031981166001600160a01b03848116918217845560405192169182907f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e0905f90a3505050565b5f807ff0c57e16840df040f15088dc2f81fe391c3923bec73e23a9662efc9c229c6a006104aa565b6107616107bf565b61042a816107e4565b610773826107ec565b6040516001600160a01b038316907fbc7cd75a20ee27fd9adebab32041f755214dbc6bffa90cc0225b39da2e5c2d3b905f90a28051156107b757610618828261084f565b6102916108ef565b6107c761090e565b6102f557604051631afcd79f60e31b815260040160405180910390fd5b6103f86107bf565b806001600160a01b03163b5f0361082157604051634c9c8ce360e01b81526001600160a01b0382166004820152602401610220565b5f80516020610ae483398151915280546001600160a01b0319166001600160a01b0392909216919091179055565b60605f61085c8484610927565b905080801561087d57505f3d118061087d57505f846001600160a01b03163b115b156108925761088a61093a565b9150506104aa565b80156108bc57604051639996b31560e01b81526001600160a01b0385166004820152602401610220565b3d156108cf576108ca610953565b6108e8565b60405163d6bda27560e01b815260040160405180910390fd5b5092915050565b34156102f55760405163b398979f60e01b815260040160405180910390fd5b5f610917610731565b54600160401b900460ff16919050565b5f805f835160208501865af49392505050565b6040513d81523d5f602083013e3d602001810160405290565b6040513d5f823e3d81fd5b5f6020828403121561096e575f80fd5b5035919050565b80356001600160a01b038116811461098b575f80fd5b919050565b634e487b7160e01b5f52604160045260245ffd5b5f80604083850312156109b5575f80fd5b6109be83610975565b9150602083013567ffffffffffffffff808211156109da575f80fd5b818501915085601f8301126109ed575f80fd5b8135818111156109ff576109ff610990565b604051601f8201601f19908116603f01168101908382118183101715610a2757610a27610990565b81604052828152886020848701011115610a3f575f80fd5b826020860160208301375f6020848301015280955050505050509250929050565b5f602080835283518060208501525f5b81811015610a8c57858101830151858201604001528201610a70565b505f604082860101526040601f19601f8301168501019250505092915050565b5f60208284031215610abc575f80fd5b610ac582610975565b9392505050565b5f60208284031215610adc575f80fd5b505191905056fe360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc","sourceMap":"1042:1794:2:-:0;;;1084:4:33;1041:48;;1489:53:2;;;;;;;;;-1:-1:-1;1513:22:2;:20;:22::i;:::-;1042:1794;;7709:422:32;3147:66;7898:15;;;;;;;7894:76;;;7936:23;;-1:-1:-1;;;7936:23:32;;;;;;;;;;;7894:76;7983:14;;-1:-1:-1;;;;;7983:14:32;;;:34;7979:146;;8033:33;;-1:-1:-1;;;;;;8033:33:32;-1:-1:-1;;;;;8033:33:32;;;;;8085:29;;158:50:39;;;8085:29:32;;146:2:39;131:18;8085:29:32;;;;;;;7979:146;7758:373;7709:422::o;14:200:39:-;1042:1794:2;;;;;;;;;;;;;;;;;;;;;;","linkReferences":{}},"deployedBytecode":{"object":"0x608060405260043610610084575f3560e01c8063715018a611610057578063715018a6146101025780638da5cb5b14610116578063ad3cb1cc1461015c578063c4d66de814610199578063f2fde38b146101b8575f80fd5b806323c3617f146100885780634d003070146100a95780634f1ef286146100db57806352d1902d146100ee575b5f80fd5b348015610093575f80fd5b506100a76100a236600461095e565b6101d7565b005b3480156100b4575f80fd5b506100c86100c336600461095e565b610295565b6040519081526020015b60405180910390f35b6100a76100e93660046109a4565b6102ae565b3480156100f9575f80fd5b506100c86102c9565b34801561010d575f80fd5b506100a76102e4565b348015610121575f80fd5b507f9016d09d72d40fdae2fd8ceac6b6234c7706214fd39c1cd1e609a0528c199300546040516001600160a01b0390911681526020016100d2565b348015610167575f80fd5b5061018c604051806040016040528060058152602001640352e302e360dc1b81525081565b6040516100d29190610a60565b3480156101a4575f80fd5b506100a76101b3366004610aac565b6102f7565b3480156101c3575f80fd5b506100a76101d2366004610aac565b6103f0565b806102295760405162461bcd60e51b815260206004820152601860248201527f5554533a20526f6f742063616e6e6f74206265207a65726f000000000000000060448201526064015b60405180910390fd5b5f61023261042d565b5f8381526020829052604081205491925003610291575f828152602082815260409182902042908190559151918252339184917f61cae4201bb8c0117495b22a70f5202410666b349c27302dac280dc054b60f2a910160405180910390a35b5050565b5f61029e61042d565b5f92835260205250604090205490565b6102b66104b0565b6102bf82610554565b610291828261055c565b5f6102d261061d565b505f80516020610ae483398151915290565b6102ec610666565b6102f55f6106c1565b565b5f610300610731565b805490915060ff600160401b820416159067ffffffffffffffff165f811580156103275750825b90505f8267ffffffffffffffff1660011480156103435750303b155b905081158015610351575080155b1561036f5760405163f92ee8a960e01b815260040160405180910390fd5b845467ffffffffffffffff19166001178555831561039957845460ff60401b1916600160401b1785555b6103a286610759565b83156103e857845460ff60401b19168555604051600181527fc7f505b2f371ae2175ee4913f4499e1f2633a7b5936321eed1cdaeb6115181d29060200160405180910390a15b505050505050565b6103f8610666565b6001600160a01b03811661042157604051631e4fbdf760e01b81525f6004820152602401610220565b61042a816106c1565b50565b60408051808201909152601f81527f7574732e73746f726167652e556e6976657273616c54696d657374616d7073006020909101527f6191da0f5f254a176c2a5b8e81a37f62349600d58cdbf87518a33cdde24d517a5f908152807f500a69046951d8ea21a7dabf6fe6e1792e3ffa4dc61a276ae65b0e2b034681005b92915050565b306001600160a01b037f000000000000000000000000000000000000000000000000000000000000000016148061053657507f00000000000000000000000000000000000000000000000000000000000000006001600160a01b031661052a5f80516020610ae4833981519152546001600160a01b031690565b6001600160a01b031614155b156102f55760405163703e46dd60e11b815260040160405180910390fd5b61042a610666565b816001600160a01b03166352d1902d6040518163ffffffff1660e01b8152600401602060405180830381865afa9250505080156105b6575060408051601f3d908101601f191682019092526105b391810190610acc565b60015b6105de57604051634c9c8ce360e01b81526001600160a01b0383166004820152602401610220565b5f80516020610ae4833981519152811461060e57604051632a87526960e21b815260048101829052602401610220565b610618838361076a565b505050565b306001600160a01b037f000000000000000000000000000000000000000000000000000000000000000016146102f55760405163703e46dd60e11b815260040160405180910390fd5b336106987f9016d09d72d40fdae2fd8ceac6b6234c7706214fd39c1cd1e609a0528c199300546001600160a01b031690565b6001600160a01b0316146102f55760405163118cdaa760e01b8152336004820152602401610220565b7f9016d09d72d40fdae2fd8ceac6b6234c7706214fd39c1cd1e609a0528c19930080546001600160a01b031981166001600160a01b03848116918217845560405192169182907f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e0905f90a3505050565b5f807ff0c57e16840df040f15088dc2f81fe391c3923bec73e23a9662efc9c229c6a006104aa565b6107616107bf565b61042a816107e4565b610773826107ec565b6040516001600160a01b038316907fbc7cd75a20ee27fd9adebab32041f755214dbc6bffa90cc0225b39da2e5c2d3b905f90a28051156107b757610618828261084f565b6102916108ef565b6107c761090e565b6102f557604051631afcd79f60e31b815260040160405180910390fd5b6103f86107bf565b806001600160a01b03163b5f0361082157604051634c9c8ce360e01b81526001600160a01b0382166004820152602401610220565b5f80516020610ae483398151915280546001600160a01b0319166001600160a01b0392909216919091179055565b60605f61085c8484610927565b905080801561087d57505f3d118061087d57505f846001600160a01b03163b115b156108925761088a61093a565b9150506104aa565b80156108bc57604051639996b31560e01b81526001600160a01b0385166004820152602401610220565b3d156108cf576108ca610953565b6108e8565b60405163d6bda27560e01b815260040160405180910390fd5b5092915050565b34156102f55760405163b398979f60e01b815260040160405180910390fd5b5f610917610731565b54600160401b900460ff16919050565b5f805f835160208501865af49392505050565b6040513d81523d5f602083013e3d602001810160405290565b6040513d5f823e3d81fd5b5f6020828403121561096e575f80fd5b5035919050565b80356001600160a01b038116811461098b575f80fd5b919050565b634e487b7160e01b5f52604160045260245ffd5b5f80604083850312156109b5575f80fd5b6109be83610975565b9150602083013567ffffffffffffffff808211156109da575f80fd5b818501915085601f8301126109ed575f80fd5b8135818111156109ff576109ff610990565b604051601f8201601f19908116603f01168101908382118183101715610a2757610a27610990565b81604052828152886020848701011115610a3f575f80fd5b826020860160208301375f6020848301015280955050505050509250929050565b5f602080835283518060208501525f5b81811015610a8c57858101830151858201604001528201610a70565b505f604082860101526040601f19601f8301168501019250505092915050565b5f60208284031215610abc575f80fd5b610ac582610975565b9392505050565b5f60208284031215610adc575f80fd5b505191905056fe360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc","sourceMap":"1042:1794:2:-:0;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;2140:354;;;;;;;;;;-1:-1:-1;2140:354:2;;;;;:::i;:::-;;:::i;:::-;;1896:138;;;;;;;;;;-1:-1:-1;1896:138:2;;;;;:::i;:::-;;:::i;:::-;;;345:25:39;;;333:2;318:18;1896:138:2;;;;;;;;3911:214:33;;;;;;:::i;:::-;;:::i;3466:126::-;;;;;;;;;;;;;:::i;3176:101:22:-;;;;;;;;;;;;;:::i;2462:144::-;;;;;;;;;;-1:-1:-1;1334:22:22;2591:8;2462:144;;-1:-1:-1;;;;;2591:8:22;;;2019:51:39;;2007:2;1992:18;2462:144:22;1873:203:39;1732:58:33;;;;;;;;;;;;;;;;;;;;;;;;;-1:-1:-1;;;1732:58:33;;;;;;;;;;;;:::i;1548:106:2:-;;;;;;;;;;-1:-1:-1;1548:106:2;;;;;:::i;:::-;;:::i;3426:215:22:-;;;;;;;;;;-1:-1:-1;3426:215:22;;;;;:::i;:::-;;:::i;2140:354:2:-;2197:4;2189:55;;;;-1:-1:-1;;;2189:55:2;;3027:2:39;2189:55:2;;;3009:21:39;3066:2;3046:18;;;3039:30;3105:26;3085:18;;;3078:54;3149:18;;2189:55:2;;;;;;;;;2255:36;2294:32;:30;:32::i;:::-;2340:12;:18;;;;;;;;;;;2255:71;;-1:-1:-1;2340:23:2;2336:152;;2379:12;:18;;;;;;;;;;;;2400:15;2379:36;;;;2434:43;;345:25:39;;;2449:10:2;;2379:18;;2434:43;;318:18:39;2434:43:2;;;;;;;2336:152;2179:315;2140:354;:::o;1896:138::-;1952:7;1978:32;:30;:32::i;:::-;:43;:49;;;;;-1:-1:-1;1978:49:2;;;;;1896:138::o;3911:214:33:-;2568:13;:11;:13::i;:::-;4026:36:::1;4044:17;4026;:36::i;:::-;4072:46;4094:17;4113:4;4072:21;:46::i;3466:126::-:0;3527:7;2839:20;:18;:20::i;:::-;-1:-1:-1;;;;;;;;;;;;3466:126:33;:::o;3176:101:22:-;2355:13;:11;:13::i;:::-;3240:30:::1;3267:1;3240:18;:30::i;:::-;3176:101::o:0;1548:106:2:-;4158:30:32;4191:26;:24;:26::i;:::-;4302:15;;4158:59;;-1:-1:-1;4302:15:32;-1:-1:-1;;;4302:15:32;;;4301:16;;4348:14;;4279:19;4724:16;;:34;;;;;4744:14;4724:34;4704:54;;4768:17;4788:11;:16;;4803:1;4788:16;:50;;;;-1:-1:-1;4816:4:32;4808:25;:30;4788:50;4768:70;;4854:12;4853:13;:30;;;;;4871:12;4870:13;4853:30;4849:91;;;4906:23;;-1:-1:-1;;;4906:23:32;;;;;;;;;;;4849:91;4949:18;;-1:-1:-1;;4949:18:32;4966:1;4949:18;;;4977:67;;;;5011:22;;-1:-1:-1;;;;5011:22:32;-1:-1:-1;;;5011:22:32;;;4977:67;1619:28:2::1;1634:12;1619:14;:28::i;:::-;5068:14:32::0;5064:101;;;5098:23;;-1:-1:-1;;;;5098:23:32;;;5140:14;;-1:-1:-1;3331:50:39;;5140:14:32;;3319:2:39;3304:18;5140:14:32;;;;;;;5064:101;4092:1079;;;;;1548:106:2;:::o;3426:215:22:-;2355:13;:11;:13::i;:::-;-1:-1:-1;;;;;3510:22:22;::::1;3506:91;;3555:31;::::0;-1:-1:-1;;;3555:31:22;;3583:1:::1;3555:31;::::0;::::1;2019:51:39::0;1992:18;;3555:31:22::1;1873:203:39::0;3506:91:22::1;3606:28;3625:8;3606:18;:28::i;:::-;3426:215:::0;:::o;1660:230:2:-;1787:10;;;;;;;;;;;;;;;;;;1846:57:37;1724:36:2;1833:71:37;;;1724:36:2;1925:37:37;1787:24:2;1772:39;1660:230;-1:-1:-1;;1660:230:2:o;4328:312:33:-;4408:4;-1:-1:-1;;;;;4417:6:33;4400:23;;;:120;;;4514:6;-1:-1:-1;;;;;4478:42:33;:32;-1:-1:-1;;;;;;;;;;;1519:53:29;-1:-1:-1;;;;;1519:53:29;;1441:138;4478:32:33;-1:-1:-1;;;;;4478:42:33;;;4400:120;4383:251;;;4594:29;;-1:-1:-1;;;4594:29:33;;;;;;;;;;;2750:84:2;2355:13:22;:11;:13::i;5782:538:33:-;5899:17;-1:-1:-1;;;;;5881:50:33;;:52;;;;;;;;;;;;;;;;;;;;;;;;;;;;;-1:-1:-1;5881:52:33;;;;;;;;-1:-1:-1;;5881:52:33;;;;;;;;;;;;:::i;:::-;;;5877:437;;6243:60;;-1:-1:-1;;;6243:60:33;;-1:-1:-1;;;;;2037:32:39;;6243:60:33;;;2019:51:39;1992:18;;6243:60:33;1873:203:39;5877:437:33;-1:-1:-1;;;;;;;;;;;5975:40:33;;5971:120;;6042:34;;-1:-1:-1;;;6042:34:33;;;;;345:25:39;;;318:18;;6042:34:33;199:177:39;5971:120:33;6104:54;6134:17;6153:4;6104:29;:54::i;:::-;5934:235;5782:538;;:::o;4757:213::-;4831:4;-1:-1:-1;;;;;4840:6:33;4823:23;;4819:145;;4924:29;;-1:-1:-1;;;4924:29:33;;;;;;;;;;;2679:162:22;987:10:25;2738:7:22;1334:22;2591:8;-1:-1:-1;;;;;2591:8:22;;2462:144;2738:7;-1:-1:-1;;;;;2738:23:22;;2734:101;;2784:40;;-1:-1:-1;;;2784:40:22;;987:10:25;2784:40:22;;;2019:51:39;1992:18;;2784:40:22;1873:203:39;3795:248:22;1334:22;3944:8;;-1:-1:-1;;;;;;3962:19:22;;-1:-1:-1;;;;;3962:19:22;;;;;;;;3996:40;;3944:8;;;;;3996:40;;3868:24;;3996:40;3858:185;;3795:248;:::o;9071:205:32:-;9129:30;;3147:66;9186:27;8819:122;1868:127:22;6929:20:32;:18;:20::i;:::-;1950:38:22::1;1975:12;1950:24;:38::i;2264:344:29:-:0;2355:37;2374:17;2355:18;:37::i;:::-;2407:36;;-1:-1:-1;;;;;2407:36:29;;;;;;;;2458:11;;:15;2454:148;;2489:53;2518:17;2537:4;2489:28;:53::i;2454:148::-;2573:18;:16;:18::i;7082:141:32:-;7149:17;:15;:17::i;:::-;7144:73;;7189:17;;-1:-1:-1;;;7189:17:32;;;;;;;;;;;2001:235:22;6929:20:32;:18;:20::i;1671:281:29:-;1748:17;-1:-1:-1;;;;;1748:29:29;;1781:1;1748:34;1744:119;;1805:47;;-1:-1:-1;;;1805:47:29;;-1:-1:-1;;;;;2037:32:39;;1805:47:29;;;2019:51:39;1992:18;;1805:47:29;1873:203:39;1744:119:29;-1:-1:-1;;;;;;;;;;;1872:73:29;;-1:-1:-1;;;;;;1872:73:29;-1:-1:-1;;;;;1872:73:29;;;;;;;;;;1671:281::o;4691:549:34:-;4774:12;4798;4813:47;4847:6;4855:4;4813:33;:47::i;:::-;4798:62;;4874:7;:72;;;;-1:-1:-1;4918:1:34;4583:16:36;4886:33:34;:59;;;;4944:1;4923:6;-1:-1:-1;;;;;4923:18:34;;:22;4886:59;4870:364;;;4969:25;:23;:25::i;:::-;4962:32;;;;;4870:364;5015:7;5011:223;;;5045:24;;-1:-1:-1;;;5045:24:34;;-1:-1:-1;;;;;2037:32:39;;5045:24:34;;;2019:51:39;1992:18;;5045:24:34;1873:203:39;5011:223:34;4583:16:36;5090:33:34;5086:148;;5139:27;:25;:27::i;:::-;5086:148;;;5204:19;;-1:-1:-1;;;5204:19:34;;;;;;;;;;;5086:148;4788:452;4691:549;;;;:::o;6113:122:29:-;6163:9;:13;6159:70;;6199:19;;-1:-1:-1;;;6199:19:29;;;;;;;;;;;8485:120:32;8535:4;8558:26;:24;:26::i;:::-;:40;-1:-1:-1;;;8558:40:32;;;;;;-1:-1:-1;8485:120:32:o;3383:242:36:-;3466:12;3604:4;3598;3591;3585:11;3578:4;3572;3568:15;3560:6;3553:5;3540:69;3529:80;3383:242;-1:-1:-1;;;3383:242:36:o;4698:334::-;4829:4;4823:11;4862:16;4847:32;;4932:16;4926:4;4919;4907:17;;4892:57;4997:16;4991:4;4987:27;4979:6;4975:40;4969:4;4962:54;4698:334;:::o;5099:223::-;5203:4;5197:11;5247:16;5241:4;5236:3;5221:43;5289:16;5284:3;5277:29;14:180:39;73:6;126:2;114:9;105:7;101:23;97:32;94:52;;;142:1;139;132:12;94:52;-1:-1:-1;165:23:39;;14:180;-1:-1:-1;14:180:39:o;381:173::-;449:20;;-1:-1:-1;;;;;498:31:39;;488:42;;478:70;;544:1;541;534:12;478:70;381:173;;;:::o;559:127::-;620:10;615:3;611:20;608:1;601:31;651:4;648:1;641:15;675:4;672:1;665:15;691:995;768:6;776;829:2;817:9;808:7;804:23;800:32;797:52;;;845:1;842;835:12;797:52;868:29;887:9;868:29;:::i;:::-;858:39;;948:2;937:9;933:18;920:32;971:18;1012:2;1004:6;1001:14;998:34;;;1028:1;1025;1018:12;998:34;1066:6;1055:9;1051:22;1041:32;;1111:7;1104:4;1100:2;1096:13;1092:27;1082:55;;1133:1;1130;1123:12;1082:55;1169:2;1156:16;1191:2;1187;1184:10;1181:36;;;1197:18;;:::i;:::-;1272:2;1266:9;1240:2;1326:13;;-1:-1:-1;;1322:22:39;;;1346:2;1318:31;1314:40;1302:53;;;1370:18;;;1390:22;;;1367:46;1364:72;;;1416:18;;:::i;:::-;1456:10;1452:2;1445:22;1491:2;1483:6;1476:18;1531:7;1526:2;1521;1517;1513:11;1509:20;1506:33;1503:53;;;1552:1;1549;1542:12;1503:53;1608:2;1603;1599;1595:11;1590:2;1582:6;1578:15;1565:46;1653:1;1648:2;1643;1635:6;1631:15;1627:24;1620:35;1674:6;1664:16;;;;;;;691:995;;;;;:::o;2081:548::-;2193:4;2222:2;2251;2240:9;2233:21;2283:6;2277:13;2326:6;2321:2;2310:9;2306:18;2299:34;2351:1;2361:140;2375:6;2372:1;2369:13;2361:140;;;2470:14;;;2466:23;;2460:30;2436:17;;;2455:2;2432:26;2425:66;2390:10;;2361:140;;;2365:3;2550:1;2545:2;2536:6;2525:9;2521:22;2517:31;2510:42;2620:2;2613;2609:7;2604:2;2596:6;2592:15;2588:29;2577:9;2573:45;2569:54;2561:62;;;;2081:548;;;;:::o;2634:186::-;2693:6;2746:2;2734:9;2725:7;2721:23;2717:32;2714:52;;;2762:1;2759;2752:12;2714:52;2785:29;2804:9;2785:29;:::i;:::-;2775:39;2634:186;-1:-1:-1;;;2634:186:39:o;3392:184::-;3462:6;3515:2;3503:9;3494:7;3490:23;3486:32;3483:52;;;3531:1;3528;3521:12;3483:52;-1:-1:-1;3554:16:39;;3392:184;-1:-1:-1;3392:184:39:o","linkReferences":{},"immutableReferences":{"41074":[{"start":1211,"length":32},{"start":1252,"length":32},{"start":1576,"length":32}]}},"methodIdentifiers":{"UPGRADE_INTERFACE_VERSION()":"ad3cb1cc","attest(bytes32)":"23c3617f","initialize(address)":"c4d66de8","owner()":"8da5cb5b","proxiableUUID()":"52d1902d","renounceOwnership()":"715018a6","timestamp(bytes32)":"4d003070","transferOwnership(address)":"f2fde38b","upgradeToAndCall(address,bytes)":"4f1ef286"},"rawMetadata":"{\"compiler\":{\"version\":\"0.8.24+commit.e11b9ed9\"},\"language\":\"Solidity\",\"output\":{\"abi\":[{\"inputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"constructor\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"target\",\"type\":\"address\"}],\"name\":\"AddressEmptyCode\",\"type\":\"error\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"implementation\",\"type\":\"address\"}],\"name\":\"ERC1967InvalidImplementation\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"ERC1967NonPayable\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"FailedCall\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"InvalidInitialization\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"NotInitializing\",\"type\":\"error\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"owner\",\"type\":\"address\"}],\"name\":\"OwnableInvalidOwner\",\"type\":\"error\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"account\",\"type\":\"address\"}],\"name\":\"OwnableUnauthorizedAccount\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"UUPSUnauthorizedCallContext\",\"type\":\"error\"},{\"inputs\":[{\"internalType\":\"bytes32\",\"name\":\"slot\",\"type\":\"bytes32\"}],\"name\":\"UUPSUnsupportedProxiableUUID\",\"type\":\"error\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"bytes32\",\"name\":\"root\",\"type\":\"bytes32\"},{\"indexed\":true,\"internalType\":\"address\",\"name\":\"sender\",\"type\":\"address\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"timestamp\",\"type\":\"uint256\"}],\"name\":\"Attested\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":false,\"internalType\":\"uint64\",\"name\":\"version\",\"type\":\"uint64\"}],\"name\":\"Initialized\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"previousOwner\",\"type\":\"address\"},{\"indexed\":true,\"internalType\":\"address\",\"name\":\"newOwner\",\"type\":\"address\"}],\"name\":\"OwnershipTransferred\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"address\",\"name\":\"implementation\",\"type\":\"address\"}],\"name\":\"Upgraded\",\"type\":\"event\"},{\"inputs\":[],\"name\":\"UPGRADE_INTERFACE_VERSION\",\"outputs\":[{\"internalType\":\"string\",\"name\":\"\",\"type\":\"string\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"bytes32\",\"name\":\"root\",\"type\":\"bytes32\"}],\"name\":\"attest\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"initialOwner\",\"type\":\"address\"}],\"name\":\"initialize\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"owner\",\"outputs\":[{\"internalType\":\"address\",\"name\":\"\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"proxiableUUID\",\"outputs\":[{\"internalType\":\"bytes32\",\"name\":\"\",\"type\":\"bytes32\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"renounceOwnership\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"bytes32\",\"name\":\"root\",\"type\":\"bytes32\"}],\"name\":\"timestamp\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"newOwner\",\"type\":\"address\"}],\"name\":\"transferOwnership\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"newImplementation\",\"type\":\"address\"},{\"internalType\":\"bytes\",\"name\":\"data\",\"type\":\"bytes\"}],\"name\":\"upgradeToAndCall\",\"outputs\":[],\"stateMutability\":\"payable\",\"type\":\"function\"}],\"devdoc\":{\"details\":\"Records and exposes timestamps for attested Merkle roots using ERC-7201 namespaced storage (`uts.storage.UniversalTimestamps`) derived via {SlotDerivation}, and is implemented as a UUPS upgradeable contract via OpenZeppelin's Initializable, OwnableUpgradeable, and UUPSUpgradeable base contracts. Storage is kept in a dedicated namespaced struct to remain layout-compatible across upgrades, while upgrades are authorized by the contract owner through {_authorizeUpgrade}.\",\"errors\":{\"AddressEmptyCode(address)\":[{\"details\":\"There's no code at `target` (it is not a contract).\"}],\"ERC1967InvalidImplementation(address)\":[{\"details\":\"The `implementation` of the proxy is invalid.\"}],\"ERC1967NonPayable()\":[{\"details\":\"An upgrade function sees `msg.value > 0` that may be lost.\"}],\"FailedCall()\":[{\"details\":\"A call to an address target failed. The target may have reverted.\"}],\"InvalidInitialization()\":[{\"details\":\"The contract is already initialized.\"}],\"NotInitializing()\":[{\"details\":\"The contract is not initializing.\"}],\"OwnableInvalidOwner(address)\":[{\"details\":\"The owner is not a valid owner account. (eg. `address(0)`)\"}],\"OwnableUnauthorizedAccount(address)\":[{\"details\":\"The caller account is not authorized to perform an operation.\"}],\"UUPSUnauthorizedCallContext()\":[{\"details\":\"The call is from an unauthorized context.\"}],\"UUPSUnsupportedProxiableUUID(bytes32)\":[{\"details\":\"The storage `slot` is unsupported as a UUID.\"}]},\"events\":{\"Initialized(uint64)\":{\"details\":\"Triggered when the contract has been initialized or reinitialized.\"},\"Upgraded(address)\":{\"details\":\"Emitted when the implementation is upgraded.\"}},\"kind\":\"dev\",\"methods\":{\"attest(bytes32)\":{\"params\":{\"root\":\"The Merkle Root to be attested\"}},\"constructor\":{\"custom:oz-upgrades-unsafe-allow\":\"constructor\"},\"owner()\":{\"details\":\"Returns the address of the current owner.\"},\"proxiableUUID()\":{\"details\":\"Implementation of the ERC-1822 {proxiableUUID} function. This returns the storage slot used by the implementation. It is used to validate the implementation's compatibility when performing an upgrade. IMPORTANT: A proxy pointing at a proxiable contract should not be considered proxiable itself, because this risks bricking a proxy that upgrades to it, by delegating to itself until out of gas. Thus it is critical that this function revert if invoked through a proxy. This is guaranteed by the `notDelegated` modifier.\"},\"renounceOwnership()\":{\"details\":\"Leaves the contract without owner. It will not be possible to call `onlyOwner` functions. Can only be called by the current owner. NOTE: Renouncing ownership will leave the contract without an owner, thereby disabling any functionality that is only available to the owner.\"},\"transferOwnership(address)\":{\"details\":\"Transfers ownership of the contract to a new account (`newOwner`). Can only be called by the current owner.\"},\"upgradeToAndCall(address,bytes)\":{\"custom:oz-upgrades-unsafe-allow-reachable\":\"delegatecall\",\"details\":\"Upgrade the implementation of the proxy to `newImplementation`, and subsequently execute the function call encoded in `data`. Calls {_authorizeUpgrade}. Emits an {Upgraded} event.\"}},\"title\":\"UniversalTimestamps\",\"version\":1},\"userdoc\":{\"kind\":\"user\",\"methods\":{\"attest(bytes32)\":{\"notice\":\"Attest Merkle Root\"}},\"version\":1}},\"settings\":{\"compilationTarget\":{\"contracts/UniversalTimestamps.sol\":\"UniversalTimestamps\"},\"evmVersion\":\"cancun\",\"libraries\":{},\"metadata\":{\"appendCBOR\":false,\"bytecodeHash\":\"none\"},\"optimizer\":{\"enabled\":true,\"runs\":200},\"remappings\":[\":@openzeppelin/contracts-upgradeable/=lib/openzeppelin-contracts-upgradeable/contracts/\",\":@openzeppelin/contracts/=lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/contracts/\",\":erc4626-tests/=lib/openzeppelin-contracts-upgradeable/lib/erc4626-tests/\",\":forge-std/=lib/forge-std/src/\",\":halmos-cheatcodes/=lib/openzeppelin-contracts-upgradeable/lib/halmos-cheatcodes/src/\",\":openzeppelin-contracts-upgradeable/=lib/openzeppelin-contracts-upgradeable/\",\":openzeppelin-contracts/=lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/\",\":openzeppelin-foundry-upgrades/=lib/openzeppelin-foundry-upgrades/src/\"]},\"sources\":{\"contracts/IUniversalTimestamps.sol\":{\"keccak256\":\"0xfa9490d2704cebe76fa78d15f51ed3bf13577ffaf782fe0337db402872571df0\",\"license\":\"MIT\",\"urls\":[\"bzz-raw://c42b87e7040e6d72f5b49a2bff481191678e15df1c19793e9c55800e8f5276cf\",\"dweb:/ipfs/QmNr4ET95fihTnh3YF6WjjKUsCFU7KkoPkvKAizb83pyCC\"]},\"contracts/UniversalTimestamps.sol\":{\"keccak256\":\"0xb1596ca55406c833dc01c604dd4d6fb3791a9f9cc0f4cad6292757abc52acb4b\",\"license\":\"MIT\",\"urls\":[\"bzz-raw://e8c53b928701b425781b8bce6e51fc3414d33d9d32e3b192777b6c5157396bfa\",\"dweb:/ipfs/QmQHq55s3BHW4YEhBe8bRGmxFvSc6xhWF4pUub8is9oSkf\"]},\"lib/openzeppelin-contracts-upgradeable/contracts/access/OwnableUpgradeable.sol\":{\"keccak256\":\"0x85c3b9bac35a90dce9ed9b31532c3739cae432359d8d7ff59cb6712f21c7ed14\",\"license\":\"MIT\",\"urls\":[\"bzz-raw://a084d32ad4ad5b1d4494124d7695334dbeff81c2d1846a01ef1215153dd38eed\",\"dweb:/ipfs/QmbzDrfeogDd3n65mADjLuy97oAMgh2CtiUxKKEpM3WB8b\"]},\"lib/openzeppelin-contracts-upgradeable/contracts/proxy/utils/Initializable.sol\":{\"keccak256\":\"0x30d125b8417684dbfea3e8d57284b353a86b22077237b4aaf098c0b54b153e16\",\"license\":\"MIT\",\"urls\":[\"bzz-raw://2813775a6326190e75dfa9005c1abbdb1e541c195c0bf5656dd4199e8c66fd8d\",\"dweb:/ipfs/QmYDKANBezQXNrEDyJ69RVXkgypW1hWj7MAvjfdNHTZY8L\"]},\"lib/openzeppelin-contracts-upgradeable/contracts/proxy/utils/UUPSUpgradeable.sol\":{\"keccak256\":\"0x4918e374e9ce84e9b196486bafbd46851d5e72ab315e31f0b1d7c443dcfea5bf\",\"license\":\"MIT\",\"urls\":[\"bzz-raw://2ced247afc54a93a13922ebbd63add61130abe483ab5b5b78e7e991d564d150e\",\"dweb:/ipfs/QmTfxjcTgfekiguegjvYMyfqhyRNffui17f8xi86BCZNVt\"]},\"lib/openzeppelin-contracts-upgradeable/contracts/utils/ContextUpgradeable.sol\":{\"keccak256\":\"0xad316bdc3ee64a0e29773256245045dc57b92660799ff14f668f7c0da9456a9d\",\"license\":\"MIT\",\"urls\":[\"bzz-raw://66463434d266816fca2a3a2734ceee88544e61b7cc3899c50333b46e8e771455\",\"dweb:/ipfs/QmPYCzHjki1HQLvBub3uUqoUKGrwdgR3xP9Zpya14YTdXS\"]},\"lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/contracts/interfaces/IERC1967.sol\":{\"keccak256\":\"0xbf2aefe54b76d7f7bcd4f6da1080b7b1662611937d870b880db584d09cea56b5\",\"license\":\"MIT\",\"urls\":[\"bzz-raw://f5e7e2f12e0feec75296e57f51f82fdaa8bd1551f4b8cc6560442c0bf60f818c\",\"dweb:/ipfs/QmcW9wDMaQ8RbQibMarfp17a3bABzY5KraWe2YDwuUrUoz\"]},\"lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/contracts/interfaces/draft-IERC1822.sol\":{\"keccak256\":\"0x82f757819bf2429a0d4db141b99a4bbe5039e4ef86dfb94e2e6d40577ed5b28b\",\"license\":\"MIT\",\"urls\":[\"bzz-raw://37c30ed931e19fb71fdb806bb504cfdb9913b7127545001b64d4487783374422\",\"dweb:/ipfs/QmUBHpv4hm3ZmwJ4GH8BeVzK4mv41Q6vBbWXxn8HExPXza\"]},\"lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/contracts/proxy/ERC1967/ERC1967Utils.sol\":{\"keccak256\":\"0xa1ad192cd45317c788618bef5cb1fb3ca4ce8b230f6433ac68cc1d850fb81618\",\"license\":\"MIT\",\"urls\":[\"bzz-raw://b43447bb85a53679d269a403c693b9d88d6c74177dfb35eddca63abaf7cf110a\",\"dweb:/ipfs/QmXSDmpd4bNZj1PDgegr6C4w1jDaWHXCconC3rYiw9TSkQ\"]},\"lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/contracts/proxy/beacon/IBeacon.sol\":{\"keccak256\":\"0x20462ddb2665e9521372c76b001d0ce196e59dbbd989de9af5576cad0bd5628b\",\"license\":\"MIT\",\"urls\":[\"bzz-raw://f417fd12aeec8fbfaceaa30e3a08a0724c0bc39de363e2acf6773c897abbaf6d\",\"dweb:/ipfs/QmU4Hko6sApdweVM92CsiuLKkCk8HfyBeutF89PCTz5Tye\"]},\"lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/contracts/proxy/utils/Initializable.sol\":{\"keccak256\":\"0xdb4d24ee2c087c391d587cd17adfe5b3f9d93b3110b1388c2ab6c7c0ad1dcd05\",\"license\":\"MIT\",\"urls\":[\"bzz-raw://ab7b6d5b9e2b88176312967fe0f0e78f3d9a1422fa5e4b64e2440c35869b5d08\",\"dweb:/ipfs/QmXKYWWyzcLg1B2k7Sb1qkEXgLCYfXecR9wYW5obRzWP1Q\"]},\"lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/contracts/proxy/utils/UUPSUpgradeable.sol\":{\"keccak256\":\"0x1a26353563a2c63b4120ea0b94727253eeff84fe2241d42c1452308b9080e66a\",\"license\":\"MIT\",\"urls\":[\"bzz-raw://49a95e36d267828b4357186a79917002d616d8634e25d1f9818e2354cd2e7d34\",\"dweb:/ipfs/QmWDkqE4KkyLAS2UkLsRgXE1FGB1qfEgBC3zMXBVsVWfdk\"]},\"lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/contracts/utils/Address.sol\":{\"keccak256\":\"0x0fa9e0d3a859900b5a46f70a03c73adf259603d5e05027a37fe0b45529d85346\",\"license\":\"MIT\",\"urls\":[\"bzz-raw://c2add4da0240c9f2ce47649c8bb6b11b40e98cf6f88b8bdc76b2704e89391710\",\"dweb:/ipfs/QmNQTwF2uVzu4CRtNxr8bxyP9XuW6VsZuo2Nr4KR2bZr3d\"]},\"lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/contracts/utils/Errors.sol\":{\"keccak256\":\"0x6afa713bfd42cf0f7656efa91201007ac465e42049d7de1d50753a373648c123\",\"license\":\"MIT\",\"urls\":[\"bzz-raw://ba1d02f4847670a1b83dec9f7d37f0b0418d6043447b69f3a29a5f9efc547fcf\",\"dweb:/ipfs/QmQ7iH2keLNUKgq2xSWcRmuBE5eZ3F5whYAkAGzCNNoEWB\"]},\"lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/contracts/utils/LowLevelCall.sol\":{\"keccak256\":\"0x5b4802a4352474792df3107e961d1cc593e47b820c14f69d3505cb28f5a6a583\",\"license\":\"MIT\",\"urls\":[\"bzz-raw://a6f86fd01f829499fe0545ff5dda07d4521988e88bfe0bf801fc15650921ed56\",\"dweb:/ipfs/QmUUKu4ZDffHAmfkf3asuQfmLTyfpuy2Amdncc3SqfzKPG\"]},\"lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/contracts/utils/SlotDerivation.sol\":{\"keccak256\":\"0x94045fd4f268edf2b2d01ef119268548c320366d6f5294ad30c1b8f9d4f5225f\",\"license\":\"MIT\",\"urls\":[\"bzz-raw://edfda81f426f8948b3834115c21e83c48180e6db0d2a8cd2debb2185ed349337\",\"dweb:/ipfs/QmdYZneFyDAux1BuWQxLAdqtABrGS2k9WYCa7C9dvpKkWv\"]},\"lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/contracts/utils/StorageSlot.sol\":{\"keccak256\":\"0xcf74f855663ce2ae00ed8352666b7935f6cddea2932fdf2c3ecd30a9b1cd0e97\",\"license\":\"MIT\",\"urls\":[\"bzz-raw://9f660b1f351b757dfe01438e59888f31f33ded3afcf5cb5b0d9bf9aa6f320a8b\",\"dweb:/ipfs/QmarDJ5hZEgBtCmmrVzEZWjub9769eD686jmzb2XpSU1cM\"]}},\"version\":1}","metadata":{"compiler":{"version":"0.8.24+commit.e11b9ed9"},"language":"Solidity","output":{"abi":[{"inputs":[],"stateMutability":"nonpayable","type":"constructor"},{"inputs":[{"internalType":"address","name":"target","type":"address"}],"type":"error","name":"AddressEmptyCode"},{"inputs":[{"internalType":"address","name":"implementation","type":"address"}],"type":"error","name":"ERC1967InvalidImplementation"},{"inputs":[],"type":"error","name":"ERC1967NonPayable"},{"inputs":[],"type":"error","name":"FailedCall"},{"inputs":[],"type":"error","name":"InvalidInitialization"},{"inputs":[],"type":"error","name":"NotInitializing"},{"inputs":[{"internalType":"address","name":"owner","type":"address"}],"type":"error","name":"OwnableInvalidOwner"},{"inputs":[{"internalType":"address","name":"account","type":"address"}],"type":"error","name":"OwnableUnauthorizedAccount"},{"inputs":[],"type":"error","name":"UUPSUnauthorizedCallContext"},{"inputs":[{"internalType":"bytes32","name":"slot","type":"bytes32"}],"type":"error","name":"UUPSUnsupportedProxiableUUID"},{"inputs":[{"internalType":"bytes32","name":"root","type":"bytes32","indexed":true},{"internalType":"address","name":"sender","type":"address","indexed":true},{"internalType":"uint256","name":"timestamp","type":"uint256","indexed":false}],"type":"event","name":"Attested","anonymous":false},{"inputs":[{"internalType":"uint64","name":"version","type":"uint64","indexed":false}],"type":"event","name":"Initialized","anonymous":false},{"inputs":[{"internalType":"address","name":"previousOwner","type":"address","indexed":true},{"internalType":"address","name":"newOwner","type":"address","indexed":true}],"type":"event","name":"OwnershipTransferred","anonymous":false},{"inputs":[{"internalType":"address","name":"implementation","type":"address","indexed":true}],"type":"event","name":"Upgraded","anonymous":false},{"inputs":[],"stateMutability":"view","type":"function","name":"UPGRADE_INTERFACE_VERSION","outputs":[{"internalType":"string","name":"","type":"string"}]},{"inputs":[{"internalType":"bytes32","name":"root","type":"bytes32"}],"stateMutability":"nonpayable","type":"function","name":"attest"},{"inputs":[{"internalType":"address","name":"initialOwner","type":"address"}],"stateMutability":"nonpayable","type":"function","name":"initialize"},{"inputs":[],"stateMutability":"view","type":"function","name":"owner","outputs":[{"internalType":"address","name":"","type":"address"}]},{"inputs":[],"stateMutability":"view","type":"function","name":"proxiableUUID","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}]},{"inputs":[],"stateMutability":"nonpayable","type":"function","name":"renounceOwnership"},{"inputs":[{"internalType":"bytes32","name":"root","type":"bytes32"}],"stateMutability":"view","type":"function","name":"timestamp","outputs":[{"internalType":"uint256","name":"","type":"uint256"}]},{"inputs":[{"internalType":"address","name":"newOwner","type":"address"}],"stateMutability":"nonpayable","type":"function","name":"transferOwnership"},{"inputs":[{"internalType":"address","name":"newImplementation","type":"address"},{"internalType":"bytes","name":"data","type":"bytes"}],"stateMutability":"payable","type":"function","name":"upgradeToAndCall"}],"devdoc":{"kind":"dev","methods":{"attest(bytes32)":{"params":{"root":"The Merkle Root to be attested"}},"constructor":{"custom:oz-upgrades-unsafe-allow":"constructor"},"owner()":{"details":"Returns the address of the current owner."},"proxiableUUID()":{"details":"Implementation of the ERC-1822 {proxiableUUID} function. This returns the storage slot used by the implementation. It is used to validate the implementation's compatibility when performing an upgrade. IMPORTANT: A proxy pointing at a proxiable contract should not be considered proxiable itself, because this risks bricking a proxy that upgrades to it, by delegating to itself until out of gas. Thus it is critical that this function revert if invoked through a proxy. This is guaranteed by the `notDelegated` modifier."},"renounceOwnership()":{"details":"Leaves the contract without owner. It will not be possible to call `onlyOwner` functions. Can only be called by the current owner. NOTE: Renouncing ownership will leave the contract without an owner, thereby disabling any functionality that is only available to the owner."},"transferOwnership(address)":{"details":"Transfers ownership of the contract to a new account (`newOwner`). Can only be called by the current owner."},"upgradeToAndCall(address,bytes)":{"custom:oz-upgrades-unsafe-allow-reachable":"delegatecall","details":"Upgrade the implementation of the proxy to `newImplementation`, and subsequently execute the function call encoded in `data`. Calls {_authorizeUpgrade}. Emits an {Upgraded} event."}},"version":1},"userdoc":{"kind":"user","methods":{"attest(bytes32)":{"notice":"Attest Merkle Root"}},"version":1}},"settings":{"remappings":["@openzeppelin/contracts-upgradeable/=lib/openzeppelin-contracts-upgradeable/contracts/","@openzeppelin/contracts/=lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/contracts/","erc4626-tests/=lib/openzeppelin-contracts-upgradeable/lib/erc4626-tests/","forge-std/=lib/forge-std/src/","halmos-cheatcodes/=lib/openzeppelin-contracts-upgradeable/lib/halmos-cheatcodes/src/","openzeppelin-contracts-upgradeable/=lib/openzeppelin-contracts-upgradeable/","openzeppelin-contracts/=lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/","openzeppelin-foundry-upgrades/=lib/openzeppelin-foundry-upgrades/src/"],"optimizer":{"enabled":true,"runs":200},"metadata":{"bytecodeHash":"none","appendCBOR":false},"compilationTarget":{"contracts/UniversalTimestamps.sol":"UniversalTimestamps"},"evmVersion":"cancun","libraries":{}},"sources":{"contracts/IUniversalTimestamps.sol":{"keccak256":"0xfa9490d2704cebe76fa78d15f51ed3bf13577ffaf782fe0337db402872571df0","urls":["bzz-raw://c42b87e7040e6d72f5b49a2bff481191678e15df1c19793e9c55800e8f5276cf","dweb:/ipfs/QmNr4ET95fihTnh3YF6WjjKUsCFU7KkoPkvKAizb83pyCC"],"license":"MIT"},"contracts/UniversalTimestamps.sol":{"keccak256":"0xb1596ca55406c833dc01c604dd4d6fb3791a9f9cc0f4cad6292757abc52acb4b","urls":["bzz-raw://e8c53b928701b425781b8bce6e51fc3414d33d9d32e3b192777b6c5157396bfa","dweb:/ipfs/QmQHq55s3BHW4YEhBe8bRGmxFvSc6xhWF4pUub8is9oSkf"],"license":"MIT"},"lib/openzeppelin-contracts-upgradeable/contracts/access/OwnableUpgradeable.sol":{"keccak256":"0x85c3b9bac35a90dce9ed9b31532c3739cae432359d8d7ff59cb6712f21c7ed14","urls":["bzz-raw://a084d32ad4ad5b1d4494124d7695334dbeff81c2d1846a01ef1215153dd38eed","dweb:/ipfs/QmbzDrfeogDd3n65mADjLuy97oAMgh2CtiUxKKEpM3WB8b"],"license":"MIT"},"lib/openzeppelin-contracts-upgradeable/contracts/proxy/utils/Initializable.sol":{"keccak256":"0x30d125b8417684dbfea3e8d57284b353a86b22077237b4aaf098c0b54b153e16","urls":["bzz-raw://2813775a6326190e75dfa9005c1abbdb1e541c195c0bf5656dd4199e8c66fd8d","dweb:/ipfs/QmYDKANBezQXNrEDyJ69RVXkgypW1hWj7MAvjfdNHTZY8L"],"license":"MIT"},"lib/openzeppelin-contracts-upgradeable/contracts/proxy/utils/UUPSUpgradeable.sol":{"keccak256":"0x4918e374e9ce84e9b196486bafbd46851d5e72ab315e31f0b1d7c443dcfea5bf","urls":["bzz-raw://2ced247afc54a93a13922ebbd63add61130abe483ab5b5b78e7e991d564d150e","dweb:/ipfs/QmTfxjcTgfekiguegjvYMyfqhyRNffui17f8xi86BCZNVt"],"license":"MIT"},"lib/openzeppelin-contracts-upgradeable/contracts/utils/ContextUpgradeable.sol":{"keccak256":"0xad316bdc3ee64a0e29773256245045dc57b92660799ff14f668f7c0da9456a9d","urls":["bzz-raw://66463434d266816fca2a3a2734ceee88544e61b7cc3899c50333b46e8e771455","dweb:/ipfs/QmPYCzHjki1HQLvBub3uUqoUKGrwdgR3xP9Zpya14YTdXS"],"license":"MIT"},"lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/contracts/interfaces/IERC1967.sol":{"keccak256":"0xbf2aefe54b76d7f7bcd4f6da1080b7b1662611937d870b880db584d09cea56b5","urls":["bzz-raw://f5e7e2f12e0feec75296e57f51f82fdaa8bd1551f4b8cc6560442c0bf60f818c","dweb:/ipfs/QmcW9wDMaQ8RbQibMarfp17a3bABzY5KraWe2YDwuUrUoz"],"license":"MIT"},"lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/contracts/interfaces/draft-IERC1822.sol":{"keccak256":"0x82f757819bf2429a0d4db141b99a4bbe5039e4ef86dfb94e2e6d40577ed5b28b","urls":["bzz-raw://37c30ed931e19fb71fdb806bb504cfdb9913b7127545001b64d4487783374422","dweb:/ipfs/QmUBHpv4hm3ZmwJ4GH8BeVzK4mv41Q6vBbWXxn8HExPXza"],"license":"MIT"},"lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/contracts/proxy/ERC1967/ERC1967Utils.sol":{"keccak256":"0xa1ad192cd45317c788618bef5cb1fb3ca4ce8b230f6433ac68cc1d850fb81618","urls":["bzz-raw://b43447bb85a53679d269a403c693b9d88d6c74177dfb35eddca63abaf7cf110a","dweb:/ipfs/QmXSDmpd4bNZj1PDgegr6C4w1jDaWHXCconC3rYiw9TSkQ"],"license":"MIT"},"lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/contracts/proxy/beacon/IBeacon.sol":{"keccak256":"0x20462ddb2665e9521372c76b001d0ce196e59dbbd989de9af5576cad0bd5628b","urls":["bzz-raw://f417fd12aeec8fbfaceaa30e3a08a0724c0bc39de363e2acf6773c897abbaf6d","dweb:/ipfs/QmU4Hko6sApdweVM92CsiuLKkCk8HfyBeutF89PCTz5Tye"],"license":"MIT"},"lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/contracts/proxy/utils/Initializable.sol":{"keccak256":"0xdb4d24ee2c087c391d587cd17adfe5b3f9d93b3110b1388c2ab6c7c0ad1dcd05","urls":["bzz-raw://ab7b6d5b9e2b88176312967fe0f0e78f3d9a1422fa5e4b64e2440c35869b5d08","dweb:/ipfs/QmXKYWWyzcLg1B2k7Sb1qkEXgLCYfXecR9wYW5obRzWP1Q"],"license":"MIT"},"lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/contracts/proxy/utils/UUPSUpgradeable.sol":{"keccak256":"0x1a26353563a2c63b4120ea0b94727253eeff84fe2241d42c1452308b9080e66a","urls":["bzz-raw://49a95e36d267828b4357186a79917002d616d8634e25d1f9818e2354cd2e7d34","dweb:/ipfs/QmWDkqE4KkyLAS2UkLsRgXE1FGB1qfEgBC3zMXBVsVWfdk"],"license":"MIT"},"lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/contracts/utils/Address.sol":{"keccak256":"0x0fa9e0d3a859900b5a46f70a03c73adf259603d5e05027a37fe0b45529d85346","urls":["bzz-raw://c2add4da0240c9f2ce47649c8bb6b11b40e98cf6f88b8bdc76b2704e89391710","dweb:/ipfs/QmNQTwF2uVzu4CRtNxr8bxyP9XuW6VsZuo2Nr4KR2bZr3d"],"license":"MIT"},"lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/contracts/utils/Errors.sol":{"keccak256":"0x6afa713bfd42cf0f7656efa91201007ac465e42049d7de1d50753a373648c123","urls":["bzz-raw://ba1d02f4847670a1b83dec9f7d37f0b0418d6043447b69f3a29a5f9efc547fcf","dweb:/ipfs/QmQ7iH2keLNUKgq2xSWcRmuBE5eZ3F5whYAkAGzCNNoEWB"],"license":"MIT"},"lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/contracts/utils/LowLevelCall.sol":{"keccak256":"0x5b4802a4352474792df3107e961d1cc593e47b820c14f69d3505cb28f5a6a583","urls":["bzz-raw://a6f86fd01f829499fe0545ff5dda07d4521988e88bfe0bf801fc15650921ed56","dweb:/ipfs/QmUUKu4ZDffHAmfkf3asuQfmLTyfpuy2Amdncc3SqfzKPG"],"license":"MIT"},"lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/contracts/utils/SlotDerivation.sol":{"keccak256":"0x94045fd4f268edf2b2d01ef119268548c320366d6f5294ad30c1b8f9d4f5225f","urls":["bzz-raw://edfda81f426f8948b3834115c21e83c48180e6db0d2a8cd2debb2185ed349337","dweb:/ipfs/QmdYZneFyDAux1BuWQxLAdqtABrGS2k9WYCa7C9dvpKkWv"],"license":"MIT"},"lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/contracts/utils/StorageSlot.sol":{"keccak256":"0xcf74f855663ce2ae00ed8352666b7935f6cddea2932fdf2c3ecd30a9b1cd0e97","urls":["bzz-raw://9f660b1f351b757dfe01438e59888f31f33ded3afcf5cb5b0d9bf9aa6f320a8b","dweb:/ipfs/QmarDJ5hZEgBtCmmrVzEZWjub9769eD686jmzb2XpSU1cM"],"license":"MIT"}},"version":1},"storageLayout":{"storage":[],"types":{}},"ast":{"absolutePath":"contracts/UniversalTimestamps.sol","id":327,"exportedSymbols":{"IUniversalTimestamps":[186],"Initializable":[41058],"OwnableUpgradeable":[40327],"SlotDerivation":[41925],"UUPSUpgradeable":[41224],"UniversalTimestamps":[326]},"nodeType":"SourceUnit","src":"33:2804:2","nodes":[{"id":188,"nodeType":"PragmaDirective","src":"33:24:2","nodes":[],"literals":["solidity","^","0.8",".24"]},{"id":190,"nodeType":"ImportDirective","src":"59:96:2","nodes":[],"absolutePath":"lib/openzeppelin-contracts-upgradeable/contracts/proxy/utils/Initializable.sol","file":"@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol","nameLocation":"-1:-1:-1","scope":327,"sourceUnit":40332,"symbolAliases":[{"foreign":{"id":189,"name":"Initializable","nodeType":"Identifier","overloadedDeclarations":[],"referencedDeclaration":41058,"src":"67:13:2","typeDescriptions":{}},"nameLocation":"-1:-1:-1"}],"unitAlias":""},{"id":192,"nodeType":"ImportDirective","src":"156:101:2","nodes":[],"absolutePath":"lib/openzeppelin-contracts-upgradeable/contracts/access/OwnableUpgradeable.sol","file":"@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol","nameLocation":"-1:-1:-1","scope":327,"sourceUnit":40328,"symbolAliases":[{"foreign":{"id":191,"name":"OwnableUpgradeable","nodeType":"Identifier","overloadedDeclarations":[],"referencedDeclaration":40327,"src":"164:18:2","typeDescriptions":{}},"nameLocation":"-1:-1:-1"}],"unitAlias":""},{"id":194,"nodeType":"ImportDirective","src":"258:100:2","nodes":[],"absolutePath":"lib/openzeppelin-contracts-upgradeable/contracts/proxy/utils/UUPSUpgradeable.sol","file":"@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol","nameLocation":"-1:-1:-1","scope":327,"sourceUnit":40336,"symbolAliases":[{"foreign":{"id":193,"name":"UUPSUpgradeable","nodeType":"Identifier","overloadedDeclarations":[],"referencedDeclaration":41224,"src":"266:15:2","typeDescriptions":{}},"nameLocation":"-1:-1:-1"}],"unitAlias":""},{"id":196,"nodeType":"ImportDirective","src":"359:64:2","nodes":[],"absolutePath":"contracts/IUniversalTimestamps.sol","file":"./IUniversalTimestamps.sol","nameLocation":"-1:-1:-1","scope":327,"sourceUnit":187,"symbolAliases":[{"foreign":{"id":195,"name":"IUniversalTimestamps","nodeType":"Identifier","overloadedDeclarations":[],"referencedDeclaration":186,"src":"367:20:2","typeDescriptions":{}},"nameLocation":"-1:-1:-1"}],"unitAlias":""},{"id":198,"nodeType":"ImportDirective","src":"424:80:2","nodes":[],"absolutePath":"lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/contracts/utils/SlotDerivation.sol","file":"@openzeppelin/contracts/utils/SlotDerivation.sol","nameLocation":"-1:-1:-1","scope":327,"sourceUnit":41926,"symbolAliases":[{"foreign":{"id":197,"name":"SlotDerivation","nodeType":"Identifier","overloadedDeclarations":[],"referencedDeclaration":41925,"src":"432:14:2","typeDescriptions":{}},"nameLocation":"-1:-1:-1"}],"unitAlias":""},{"id":326,"nodeType":"ContractDefinition","src":"1042:1794:2","nodes":[{"id":210,"nodeType":"UsingForDirective","src":"1153:32:2","nodes":[],"global":false,"libraryName":{"id":208,"name":"SlotDerivation","nameLocations":["1159:14:2"],"nodeType":"IdentifierPath","referencedDeclaration":41925,"src":"1159:14:2"},"typeName":{"id":209,"name":"string","nodeType":"ElementaryTypeName","src":"1178:6:2","typeDescriptions":{"typeIdentifier":"t_string_storage_ptr","typeString":"string"}}},{"id":213,"nodeType":"VariableDeclaration","src":"1191:70:2","nodes":[],"constant":true,"mutability":"constant","name":"_NAMESPACE","nameLocation":"1215:10:2","scope":326,"stateVariable":true,"storageLocation":"default","typeDescriptions":{"typeIdentifier":"t_string_memory_ptr","typeString":"string"},"typeName":{"id":211,"name":"string","nodeType":"ElementaryTypeName","src":"1191:6:2","typeDescriptions":{"typeIdentifier":"t_string_storage_ptr","typeString":"string"}},"value":{"hexValue":"7574732e73746f726167652e556e6976657273616c54696d657374616d7073","id":212,"isConstant":false,"isLValue":false,"isPure":true,"kind":"string","lValueRequested":false,"nodeType":"Literal","src":"1228:33:2","typeDescriptions":{"typeIdentifier":"t_stringliteral_6191da0f5f254a176c2a5b8e81a37f62349600d58cdbf87518a33cdde24d517b","typeString":"literal_string \"uts.storage.UniversalTimestamps\""},"value":"uts.storage.UniversalTimestamps"},"visibility":"private"},{"id":219,"nodeType":"StructDefinition","src":"1341:89:2","nodes":[],"canonicalName":"UniversalTimestamps.UniversalTimestampsStorage","documentation":{"id":214,"nodeType":"StructuredDocumentation","src":"1268:68:2","text":"@custom:storage-location erc7201:uts.storage.UniversalTimestamps"},"members":[{"constant":false,"id":218,"mutability":"mutable","name":"timestamps","nameLocation":"1413:10:2","nodeType":"VariableDeclaration","scope":219,"src":"1385:38:2","stateVariable":false,"storageLocation":"default","typeDescriptions":{"typeIdentifier":"t_mapping$_t_bytes32_$_t_uint256_$","typeString":"mapping(bytes32 => uint256)"},"typeName":{"id":217,"keyName":"","keyNameLocation":"-1:-1:-1","keyType":{"id":215,"name":"bytes32","nodeType":"ElementaryTypeName","src":"1393:7:2","typeDescriptions":{"typeIdentifier":"t_bytes32","typeString":"bytes32"}},"nodeType":"Mapping","src":"1385:27:2","typeDescriptions":{"typeIdentifier":"t_mapping$_t_bytes32_$_t_uint256_$","typeString":"mapping(bytes32 => uint256)"},"valueName":"","valueNameLocation":"-1:-1:-1","valueType":{"id":216,"name":"uint256","nodeType":"ElementaryTypeName","src":"1404:7:2","typeDescriptions":{"typeIdentifier":"t_uint256","typeString":"uint256"}}},"visibility":"internal"}],"name":"UniversalTimestampsStorage","nameLocation":"1348:26:2","scope":326,"visibility":"public"},{"id":227,"nodeType":"FunctionDefinition","src":"1489:53:2","nodes":[],"body":{"id":226,"nodeType":"Block","src":"1503:39:2","nodes":[],"statements":[{"expression":{"arguments":[],"expression":{"argumentTypes":[],"id":223,"name":"_disableInitializers","nodeType":"Identifier","overloadedDeclarations":[],"referencedDeclaration":41012,"src":"1513:20:2","typeDescriptions":{"typeIdentifier":"t_function_internal_nonpayable$__$returns$__$","typeString":"function ()"}},"id":224,"isConstant":false,"isLValue":false,"isPure":false,"kind":"functionCall","lValueRequested":false,"nameLocations":[],"names":[],"nodeType":"FunctionCall","src":"1513:22:2","tryCall":false,"typeDescriptions":{"typeIdentifier":"t_tuple$__$","typeString":"tuple()"}},"id":225,"nodeType":"ExpressionStatement","src":"1513:22:2"}]},"documentation":{"id":220,"nodeType":"StructuredDocumentation","src":"1436:48:2","text":"@custom:oz-upgrades-unsafe-allow constructor"},"implemented":true,"kind":"constructor","modifiers":[],"name":"","nameLocation":"-1:-1:-1","parameters":{"id":221,"nodeType":"ParameterList","parameters":[],"src":"1500:2:2"},"returnParameters":{"id":222,"nodeType":"ParameterList","parameters":[],"src":"1503:0:2"},"scope":326,"stateMutability":"nonpayable","virtual":false,"visibility":"public"},{"id":239,"nodeType":"FunctionDefinition","src":"1548:106:2","nodes":[],"body":{"id":238,"nodeType":"Block","src":"1609:45:2","nodes":[],"statements":[{"expression":{"arguments":[{"id":235,"name":"initialOwner","nodeType":"Identifier","overloadedDeclarations":[],"referencedDeclaration":229,"src":"1634:12:2","typeDescriptions":{"typeIdentifier":"t_address","typeString":"address"}}],"expression":{"argumentTypes":[{"typeIdentifier":"t_address","typeString":"address"}],"id":234,"name":"__Ownable_init","nodeType":"Identifier","overloadedDeclarations":[],"referencedDeclaration":40187,"src":"1619:14:2","typeDescriptions":{"typeIdentifier":"t_function_internal_nonpayable$_t_address_$returns$__$","typeString":"function (address)"}},"id":236,"isConstant":false,"isLValue":false,"isPure":false,"kind":"functionCall","lValueRequested":false,"nameLocations":[],"names":[],"nodeType":"FunctionCall","src":"1619:28:2","tryCall":false,"typeDescriptions":{"typeIdentifier":"t_tuple$__$","typeString":"tuple()"}},"id":237,"nodeType":"ExpressionStatement","src":"1619:28:2"}]},"functionSelector":"c4d66de8","implemented":true,"kind":"function","modifiers":[{"id":232,"kind":"modifierInvocation","modifierName":{"id":231,"name":"initializer","nameLocations":["1597:11:2"],"nodeType":"IdentifierPath","referencedDeclaration":40898,"src":"1597:11:2"},"nodeType":"ModifierInvocation","src":"1597:11:2"}],"name":"initialize","nameLocation":"1557:10:2","parameters":{"id":230,"nodeType":"ParameterList","parameters":[{"constant":false,"id":229,"mutability":"mutable","name":"initialOwner","nameLocation":"1576:12:2","nodeType":"VariableDeclaration","scope":239,"src":"1568:20:2","stateVariable":false,"storageLocation":"default","typeDescriptions":{"typeIdentifier":"t_address","typeString":"address"},"typeName":{"id":228,"name":"address","nodeType":"ElementaryTypeName","src":"1568:7:2","stateMutability":"nonpayable","typeDescriptions":{"typeIdentifier":"t_address","typeString":"address"}},"visibility":"internal"}],"src":"1567:22:2"},"returnParameters":{"id":233,"nodeType":"ParameterList","parameters":[],"src":"1609:0:2"},"scope":326,"stateMutability":"nonpayable","virtual":false,"visibility":"public"},{"id":253,"nodeType":"FunctionDefinition","src":"1660:230:2","nodes":[],"body":{"id":252,"nodeType":"Block","src":"1762:128:2","nodes":[],"statements":[{"assignments":[246],"declarations":[{"constant":false,"id":246,"mutability":"mutable","name":"slot","nameLocation":"1780:4:2","nodeType":"VariableDeclaration","scope":252,"src":"1772:12:2","stateVariable":false,"storageLocation":"default","typeDescriptions":{"typeIdentifier":"t_bytes32","typeString":"bytes32"},"typeName":{"id":245,"name":"bytes32","nodeType":"ElementaryTypeName","src":"1772:7:2","typeDescriptions":{"typeIdentifier":"t_bytes32","typeString":"bytes32"}},"visibility":"internal"}],"id":250,"initialValue":{"arguments":[],"expression":{"argumentTypes":[],"expression":{"id":247,"name":"_NAMESPACE","nodeType":"Identifier","overloadedDeclarations":[],"referencedDeclaration":213,"src":"1787:10:2","typeDescriptions":{"typeIdentifier":"t_string_memory_ptr","typeString":"string memory"}},"id":248,"isConstant":false,"isLValue":false,"isPure":false,"lValueRequested":false,"memberLocation":"1798:11:2","memberName":"erc7201Slot","nodeType":"MemberAccess","referencedDeclaration":41808,"src":"1787:22:2","typeDescriptions":{"typeIdentifier":"t_function_internal_pure$_t_string_memory_ptr_$returns$_t_bytes32_$attached_to$_t_string_memory_ptr_$","typeString":"function (string memory) pure returns (bytes32)"}},"id":249,"isConstant":false,"isLValue":false,"isPure":false,"kind":"functionCall","lValueRequested":false,"nameLocations":[],"names":[],"nodeType":"FunctionCall","src":"1787:24:2","tryCall":false,"typeDescriptions":{"typeIdentifier":"t_bytes32","typeString":"bytes32"}},"nodeType":"VariableDeclarationStatement","src":"1772:39:2"},{"AST":{"nativeSrc":"1846:38:2","nodeType":"YulBlock","src":"1846:38:2","statements":[{"nativeSrc":"1860:14:2","nodeType":"YulAssignment","src":"1860:14:2","value":{"name":"slot","nativeSrc":"1870:4:2","nodeType":"YulIdentifier","src":"1870:4:2"},"variableNames":[{"name":"$.slot","nativeSrc":"1860:6:2","nodeType":"YulIdentifier","src":"1860:6:2"}]}]},"evmVersion":"cancun","externalReferences":[{"declaration":243,"isOffset":false,"isSlot":true,"src":"1860:6:2","suffix":"slot","valueSize":1},{"declaration":246,"isOffset":false,"isSlot":false,"src":"1870:4:2","valueSize":1}],"flags":["memory-safe"],"id":251,"nodeType":"InlineAssembly","src":"1821:63:2"}]},"implemented":true,"kind":"function","modifiers":[],"name":"_getUniversalTimestampsStorage","nameLocation":"1669:30:2","parameters":{"id":240,"nodeType":"ParameterList","parameters":[],"src":"1699:2:2"},"returnParameters":{"id":244,"nodeType":"ParameterList","parameters":[{"constant":false,"id":243,"mutability":"mutable","name":"$","nameLocation":"1759:1:2","nodeType":"VariableDeclaration","scope":253,"src":"1724:36:2","stateVariable":false,"storageLocation":"storage","typeDescriptions":{"typeIdentifier":"t_struct$_UniversalTimestampsStorage_$219_storage_ptr","typeString":"struct UniversalTimestamps.UniversalTimestampsStorage"},"typeName":{"id":242,"nodeType":"UserDefinedTypeName","pathNode":{"id":241,"name":"UniversalTimestampsStorage","nameLocations":["1724:26:2"],"nodeType":"IdentifierPath","referencedDeclaration":219,"src":"1724:26:2"},"referencedDeclaration":219,"src":"1724:26:2","typeDescriptions":{"typeIdentifier":"t_struct$_UniversalTimestampsStorage_$219_storage_ptr","typeString":"struct UniversalTimestamps.UniversalTimestampsStorage"}},"visibility":"internal"}],"src":"1723:38:2"},"scope":326,"stateMutability":"pure","virtual":false,"visibility":"private"},{"id":267,"nodeType":"FunctionDefinition","src":"1896:138:2","nodes":[],"body":{"id":266,"nodeType":"Block","src":"1961:73:2","nodes":[],"statements":[{"expression":{"baseExpression":{"expression":{"arguments":[],"expression":{"argumentTypes":[],"id":260,"name":"_getUniversalTimestampsStorage","nodeType":"Identifier","overloadedDeclarations":[],"referencedDeclaration":253,"src":"1978:30:2","typeDescriptions":{"typeIdentifier":"t_function_internal_pure$__$returns$_t_struct$_UniversalTimestampsStorage_$219_storage_ptr_$","typeString":"function () pure returns (struct UniversalTimestamps.UniversalTimestampsStorage storage pointer)"}},"id":261,"isConstant":false,"isLValue":false,"isPure":false,"kind":"functionCall","lValueRequested":false,"nameLocations":[],"names":[],"nodeType":"FunctionCall","src":"1978:32:2","tryCall":false,"typeDescriptions":{"typeIdentifier":"t_struct$_UniversalTimestampsStorage_$219_storage_ptr","typeString":"struct UniversalTimestamps.UniversalTimestampsStorage storage pointer"}},"id":262,"isConstant":false,"isLValue":true,"isPure":false,"lValueRequested":false,"memberLocation":"2011:10:2","memberName":"timestamps","nodeType":"MemberAccess","referencedDeclaration":218,"src":"1978:43:2","typeDescriptions":{"typeIdentifier":"t_mapping$_t_bytes32_$_t_uint256_$","typeString":"mapping(bytes32 => uint256)"}},"id":264,"indexExpression":{"id":263,"name":"root","nodeType":"Identifier","overloadedDeclarations":[],"referencedDeclaration":255,"src":"2022:4:2","typeDescriptions":{"typeIdentifier":"t_bytes32","typeString":"bytes32"}},"isConstant":false,"isLValue":true,"isPure":false,"lValueRequested":false,"nodeType":"IndexAccess","src":"1978:49:2","typeDescriptions":{"typeIdentifier":"t_uint256","typeString":"uint256"}},"functionReturnParameters":259,"id":265,"nodeType":"Return","src":"1971:56:2"}]},"baseFunctions":[185],"functionSelector":"4d003070","implemented":true,"kind":"function","modifiers":[],"name":"timestamp","nameLocation":"1905:9:2","parameters":{"id":256,"nodeType":"ParameterList","parameters":[{"constant":false,"id":255,"mutability":"mutable","name":"root","nameLocation":"1923:4:2","nodeType":"VariableDeclaration","scope":267,"src":"1915:12:2","stateVariable":false,"storageLocation":"default","typeDescriptions":{"typeIdentifier":"t_bytes32","typeString":"bytes32"},"typeName":{"id":254,"name":"bytes32","nodeType":"ElementaryTypeName","src":"1915:7:2","typeDescriptions":{"typeIdentifier":"t_bytes32","typeString":"bytes32"}},"visibility":"internal"}],"src":"1914:14:2"},"returnParameters":{"id":259,"nodeType":"ParameterList","parameters":[{"constant":false,"id":258,"mutability":"mutable","name":"","nameLocation":"-1:-1:-1","nodeType":"VariableDeclaration","scope":267,"src":"1952:7:2","stateVariable":false,"storageLocation":"default","typeDescriptions":{"typeIdentifier":"t_uint256","typeString":"uint256"},"typeName":{"id":257,"name":"uint256","nodeType":"ElementaryTypeName","src":"1952:7:2","typeDescriptions":{"typeIdentifier":"t_uint256","typeString":"uint256"}},"visibility":"internal"}],"src":"1951:9:2"},"scope":326,"stateMutability":"view","virtual":false,"visibility":"external"},{"id":315,"nodeType":"FunctionDefinition","src":"2140:354:2","nodes":[],"body":{"id":314,"nodeType":"Block","src":"2179:315:2","nodes":[],"statements":[{"expression":{"arguments":[{"commonType":{"typeIdentifier":"t_bytes32","typeString":"bytes32"},"id":279,"isConstant":false,"isLValue":false,"isPure":false,"lValueRequested":false,"leftExpression":{"id":274,"name":"root","nodeType":"Identifier","overloadedDeclarations":[],"referencedDeclaration":270,"src":"2197:4:2","typeDescriptions":{"typeIdentifier":"t_bytes32","typeString":"bytes32"}},"nodeType":"BinaryOperation","operator":"!=","rightExpression":{"arguments":[{"hexValue":"30","id":277,"isConstant":false,"isLValue":false,"isPure":true,"kind":"number","lValueRequested":false,"nodeType":"Literal","src":"2213:1:2","typeDescriptions":{"typeIdentifier":"t_rational_0_by_1","typeString":"int_const 0"},"value":"0"}],"expression":{"argumentTypes":[{"typeIdentifier":"t_rational_0_by_1","typeString":"int_const 0"}],"id":276,"isConstant":false,"isLValue":false,"isPure":true,"lValueRequested":false,"nodeType":"ElementaryTypeNameExpression","src":"2205:7:2","typeDescriptions":{"typeIdentifier":"t_type$_t_bytes32_$","typeString":"type(bytes32)"},"typeName":{"id":275,"name":"bytes32","nodeType":"ElementaryTypeName","src":"2205:7:2","typeDescriptions":{}}},"id":278,"isConstant":false,"isLValue":false,"isPure":true,"kind":"typeConversion","lValueRequested":false,"nameLocations":[],"names":[],"nodeType":"FunctionCall","src":"2205:10:2","tryCall":false,"typeDescriptions":{"typeIdentifier":"t_bytes32","typeString":"bytes32"}},"src":"2197:18:2","typeDescriptions":{"typeIdentifier":"t_bool","typeString":"bool"}},{"hexValue":"5554533a20526f6f742063616e6e6f74206265207a65726f","id":280,"isConstant":false,"isLValue":false,"isPure":true,"kind":"string","lValueRequested":false,"nodeType":"Literal","src":"2217:26:2","typeDescriptions":{"typeIdentifier":"t_stringliteral_2804b13209a936ca289456f44fff96ae78a8d5be97dfafdb6227532f3504fdd2","typeString":"literal_string \"UTS: Root cannot be zero\""},"value":"UTS: Root cannot be zero"}],"expression":{"argumentTypes":[{"typeIdentifier":"t_bool","typeString":"bool"},{"typeIdentifier":"t_stringliteral_2804b13209a936ca289456f44fff96ae78a8d5be97dfafdb6227532f3504fdd2","typeString":"literal_string \"UTS: Root cannot be zero\""}],"id":273,"name":"require","nodeType":"Identifier","overloadedDeclarations":[-18,-18],"referencedDeclaration":-18,"src":"2189:7:2","typeDescriptions":{"typeIdentifier":"t_function_require_pure$_t_bool_$_t_string_memory_ptr_$returns$__$","typeString":"function (bool,string memory) pure"}},"id":281,"isConstant":false,"isLValue":false,"isPure":false,"kind":"functionCall","lValueRequested":false,"nameLocations":[],"names":[],"nodeType":"FunctionCall","src":"2189:55:2","tryCall":false,"typeDescriptions":{"typeIdentifier":"t_tuple$__$","typeString":"tuple()"}},"id":282,"nodeType":"ExpressionStatement","src":"2189:55:2"},{"assignments":[285],"declarations":[{"constant":false,"id":285,"mutability":"mutable","name":"$","nameLocation":"2290:1:2","nodeType":"VariableDeclaration","scope":314,"src":"2255:36:2","stateVariable":false,"storageLocation":"storage","typeDescriptions":{"typeIdentifier":"t_struct$_UniversalTimestampsStorage_$219_storage_ptr","typeString":"struct UniversalTimestamps.UniversalTimestampsStorage"},"typeName":{"id":284,"nodeType":"UserDefinedTypeName","pathNode":{"id":283,"name":"UniversalTimestampsStorage","nameLocations":["2255:26:2"],"nodeType":"IdentifierPath","referencedDeclaration":219,"src":"2255:26:2"},"referencedDeclaration":219,"src":"2255:26:2","typeDescriptions":{"typeIdentifier":"t_struct$_UniversalTimestampsStorage_$219_storage_ptr","typeString":"struct UniversalTimestamps.UniversalTimestampsStorage"}},"visibility":"internal"}],"id":288,"initialValue":{"arguments":[],"expression":{"argumentTypes":[],"id":286,"name":"_getUniversalTimestampsStorage","nodeType":"Identifier","overloadedDeclarations":[],"referencedDeclaration":253,"src":"2294:30:2","typeDescriptions":{"typeIdentifier":"t_function_internal_pure$__$returns$_t_struct$_UniversalTimestampsStorage_$219_storage_ptr_$","typeString":"function () pure returns (struct UniversalTimestamps.UniversalTimestampsStorage storage pointer)"}},"id":287,"isConstant":false,"isLValue":false,"isPure":false,"kind":"functionCall","lValueRequested":false,"nameLocations":[],"names":[],"nodeType":"FunctionCall","src":"2294:32:2","tryCall":false,"typeDescriptions":{"typeIdentifier":"t_struct$_UniversalTimestampsStorage_$219_storage_ptr","typeString":"struct UniversalTimestamps.UniversalTimestampsStorage storage pointer"}},"nodeType":"VariableDeclarationStatement","src":"2255:71:2"},{"condition":{"commonType":{"typeIdentifier":"t_uint256","typeString":"uint256"},"id":294,"isConstant":false,"isLValue":false,"isPure":false,"lValueRequested":false,"leftExpression":{"baseExpression":{"expression":{"id":289,"name":"$","nodeType":"Identifier","overloadedDeclarations":[],"referencedDeclaration":285,"src":"2340:1:2","typeDescriptions":{"typeIdentifier":"t_struct$_UniversalTimestampsStorage_$219_storage_ptr","typeString":"struct UniversalTimestamps.UniversalTimestampsStorage storage pointer"}},"id":290,"isConstant":false,"isLValue":true,"isPure":false,"lValueRequested":false,"memberLocation":"2342:10:2","memberName":"timestamps","nodeType":"MemberAccess","referencedDeclaration":218,"src":"2340:12:2","typeDescriptions":{"typeIdentifier":"t_mapping$_t_bytes32_$_t_uint256_$","typeString":"mapping(bytes32 => uint256)"}},"id":292,"indexExpression":{"id":291,"name":"root","nodeType":"Identifier","overloadedDeclarations":[],"referencedDeclaration":270,"src":"2353:4:2","typeDescriptions":{"typeIdentifier":"t_bytes32","typeString":"bytes32"}},"isConstant":false,"isLValue":true,"isPure":false,"lValueRequested":false,"nodeType":"IndexAccess","src":"2340:18:2","typeDescriptions":{"typeIdentifier":"t_uint256","typeString":"uint256"}},"nodeType":"BinaryOperation","operator":"==","rightExpression":{"hexValue":"30","id":293,"isConstant":false,"isLValue":false,"isPure":true,"kind":"number","lValueRequested":false,"nodeType":"Literal","src":"2362:1:2","typeDescriptions":{"typeIdentifier":"t_rational_0_by_1","typeString":"int_const 0"},"value":"0"},"src":"2340:23:2","typeDescriptions":{"typeIdentifier":"t_bool","typeString":"bool"}},"id":313,"nodeType":"IfStatement","src":"2336:152:2","trueBody":{"id":312,"nodeType":"Block","src":"2365:123:2","statements":[{"expression":{"id":302,"isConstant":false,"isLValue":false,"isPure":false,"lValueRequested":false,"leftHandSide":{"baseExpression":{"expression":{"id":295,"name":"$","nodeType":"Identifier","overloadedDeclarations":[],"referencedDeclaration":285,"src":"2379:1:2","typeDescriptions":{"typeIdentifier":"t_struct$_UniversalTimestampsStorage_$219_storage_ptr","typeString":"struct UniversalTimestamps.UniversalTimestampsStorage storage pointer"}},"id":298,"isConstant":false,"isLValue":true,"isPure":false,"lValueRequested":false,"memberLocation":"2381:10:2","memberName":"timestamps","nodeType":"MemberAccess","referencedDeclaration":218,"src":"2379:12:2","typeDescriptions":{"typeIdentifier":"t_mapping$_t_bytes32_$_t_uint256_$","typeString":"mapping(bytes32 => uint256)"}},"id":299,"indexExpression":{"id":297,"name":"root","nodeType":"Identifier","overloadedDeclarations":[],"referencedDeclaration":270,"src":"2392:4:2","typeDescriptions":{"typeIdentifier":"t_bytes32","typeString":"bytes32"}},"isConstant":false,"isLValue":true,"isPure":false,"lValueRequested":true,"nodeType":"IndexAccess","src":"2379:18:2","typeDescriptions":{"typeIdentifier":"t_uint256","typeString":"uint256"}},"nodeType":"Assignment","operator":"=","rightHandSide":{"expression":{"id":300,"name":"block","nodeType":"Identifier","overloadedDeclarations":[],"referencedDeclaration":-4,"src":"2400:5:2","typeDescriptions":{"typeIdentifier":"t_magic_block","typeString":"block"}},"id":301,"isConstant":false,"isLValue":false,"isPure":false,"lValueRequested":false,"memberLocation":"2406:9:2","memberName":"timestamp","nodeType":"MemberAccess","src":"2400:15:2","typeDescriptions":{"typeIdentifier":"t_uint256","typeString":"uint256"}},"src":"2379:36:2","typeDescriptions":{"typeIdentifier":"t_uint256","typeString":"uint256"}},"id":303,"nodeType":"ExpressionStatement","src":"2379:36:2"},{"eventCall":{"arguments":[{"id":305,"name":"root","nodeType":"Identifier","overloadedDeclarations":[],"referencedDeclaration":270,"src":"2443:4:2","typeDescriptions":{"typeIdentifier":"t_bytes32","typeString":"bytes32"}},{"expression":{"id":306,"name":"msg","nodeType":"Identifier","overloadedDeclarations":[],"referencedDeclaration":-15,"src":"2449:3:2","typeDescriptions":{"typeIdentifier":"t_magic_message","typeString":"msg"}},"id":307,"isConstant":false,"isLValue":false,"isPure":false,"lValueRequested":false,"memberLocation":"2453:6:2","memberName":"sender","nodeType":"MemberAccess","src":"2449:10:2","typeDescriptions":{"typeIdentifier":"t_address","typeString":"address"}},{"expression":{"id":308,"name":"block","nodeType":"Identifier","overloadedDeclarations":[],"referencedDeclaration":-4,"src":"2461:5:2","typeDescriptions":{"typeIdentifier":"t_magic_block","typeString":"block"}},"id":309,"isConstant":false,"isLValue":false,"isPure":false,"lValueRequested":false,"memberLocation":"2467:9:2","memberName":"timestamp","nodeType":"MemberAccess","src":"2461:15:2","typeDescriptions":{"typeIdentifier":"t_uint256","typeString":"uint256"}}],"expression":{"argumentTypes":[{"typeIdentifier":"t_bytes32","typeString":"bytes32"},{"typeIdentifier":"t_address","typeString":"address"},{"typeIdentifier":"t_uint256","typeString":"uint256"}],"id":304,"name":"Attested","nodeType":"Identifier","overloadedDeclarations":[],"referencedDeclaration":173,"src":"2434:8:2","typeDescriptions":{"typeIdentifier":"t_function_event_nonpayable$_t_bytes32_$_t_address_$_t_uint256_$returns$__$","typeString":"function (bytes32,address,uint256)"}},"id":310,"isConstant":false,"isLValue":false,"isPure":false,"kind":"functionCall","lValueRequested":false,"nameLocations":[],"names":[],"nodeType":"FunctionCall","src":"2434:43:2","tryCall":false,"typeDescriptions":{"typeIdentifier":"t_tuple$__$","typeString":"tuple()"}},"id":311,"nodeType":"EmitStatement","src":"2429:48:2"}]}}]},"baseFunctions":[178],"documentation":{"id":268,"nodeType":"StructuredDocumentation","src":"2040:95:2","text":" @notice Attest Merkle Root\n @param root The Merkle Root to be attested"},"functionSelector":"23c3617f","implemented":true,"kind":"function","modifiers":[],"name":"attest","nameLocation":"2149:6:2","parameters":{"id":271,"nodeType":"ParameterList","parameters":[{"constant":false,"id":270,"mutability":"mutable","name":"root","nameLocation":"2164:4:2","nodeType":"VariableDeclaration","scope":315,"src":"2156:12:2","stateVariable":false,"storageLocation":"default","typeDescriptions":{"typeIdentifier":"t_bytes32","typeString":"bytes32"},"typeName":{"id":269,"name":"bytes32","nodeType":"ElementaryTypeName","src":"2156:7:2","typeDescriptions":{"typeIdentifier":"t_bytes32","typeString":"bytes32"}},"visibility":"internal"}],"src":"2155:14:2"},"returnParameters":{"id":272,"nodeType":"ParameterList","parameters":[],"src":"2179:0:2"},"scope":326,"stateMutability":"nonpayable","virtual":false,"visibility":"external"},{"id":325,"nodeType":"FunctionDefinition","src":"2750:84:2","nodes":[],"body":{"id":324,"nodeType":"Block","src":"2832:2:2","nodes":[],"statements":[]},"baseFunctions":[41178],"documentation":{"id":316,"nodeType":"StructuredDocumentation","src":"2500:245:2","text":" @dev Authorizes an upgrade to `newImplementation`.\n This function is restricted to the contract owner via the {onlyOwner} modifier,\n ensuring that only the owner can authorize upgrades to the implementation."},"implemented":true,"kind":"function","modifiers":[{"id":322,"kind":"modifierInvocation","modifierName":{"id":321,"name":"onlyOwner","nameLocations":["2822:9:2"],"nodeType":"IdentifierPath","referencedDeclaration":40222,"src":"2822:9:2"},"nodeType":"ModifierInvocation","src":"2822:9:2"}],"name":"_authorizeUpgrade","nameLocation":"2759:17:2","overrides":{"id":320,"nodeType":"OverrideSpecifier","overrides":[],"src":"2813:8:2"},"parameters":{"id":319,"nodeType":"ParameterList","parameters":[{"constant":false,"id":318,"mutability":"mutable","name":"newImplementation","nameLocation":"2785:17:2","nodeType":"VariableDeclaration","scope":325,"src":"2777:25:2","stateVariable":false,"storageLocation":"default","typeDescriptions":{"typeIdentifier":"t_address","typeString":"address"},"typeName":{"id":317,"name":"address","nodeType":"ElementaryTypeName","src":"2777:7:2","stateMutability":"nonpayable","typeDescriptions":{"typeIdentifier":"t_address","typeString":"address"}},"visibility":"internal"}],"src":"2776:27:2"},"returnParameters":{"id":323,"nodeType":"ParameterList","parameters":[],"src":"2832:0:2"},"scope":326,"stateMutability":"nonpayable","virtual":false,"visibility":"internal"}],"abstract":false,"baseContracts":[{"baseName":{"id":200,"name":"Initializable","nameLocations":["1074:13:2"],"nodeType":"IdentifierPath","referencedDeclaration":41058,"src":"1074:13:2"},"id":201,"nodeType":"InheritanceSpecifier","src":"1074:13:2"},{"baseName":{"id":202,"name":"OwnableUpgradeable","nameLocations":["1089:18:2"],"nodeType":"IdentifierPath","referencedDeclaration":40327,"src":"1089:18:2"},"id":203,"nodeType":"InheritanceSpecifier","src":"1089:18:2"},{"baseName":{"id":204,"name":"UUPSUpgradeable","nameLocations":["1109:15:2"],"nodeType":"IdentifierPath","referencedDeclaration":41224,"src":"1109:15:2"},"id":205,"nodeType":"InheritanceSpecifier","src":"1109:15:2"},{"baseName":{"id":206,"name":"IUniversalTimestamps","nameLocations":["1126:20:2"],"nodeType":"IdentifierPath","referencedDeclaration":186,"src":"1126:20:2"},"id":207,"nodeType":"InheritanceSpecifier","src":"1126:20:2"}],"canonicalName":"UniversalTimestamps","contractDependencies":[],"contractKind":"contract","documentation":{"id":199,"nodeType":"StructuredDocumentation","src":"506:535:2","text":" @title UniversalTimestamps\n @dev Records and exposes timestamps for attested Merkle roots using ERC-7201\n namespaced storage (`uts.storage.UniversalTimestamps`) derived via\n {SlotDerivation}, and is implemented as a UUPS upgradeable contract via\n OpenZeppelin's Initializable, OwnableUpgradeable, and UUPSUpgradeable\n base contracts. Storage is kept in a dedicated namespaced struct to remain\n layout-compatible across upgrades, while upgrades are authorized by the\n contract owner through {_authorizeUpgrade}."},"fullyImplemented":true,"linearizedBaseContracts":[326,186,41224,40412,40327,40381,41058],"name":"UniversalTimestamps","nameLocation":"1051:19:2","scope":327,"usedErrors":[40163,40168,40470,40483,40807,40810,41081,41086,41236,41627],"usedEvents":[173,40174,40389,40815]}],"license":"MIT"},"id":2} \ No newline at end of file diff --git a/crates/contracts/src/eas.rs b/crates/contracts/src/eas.rs new file mode 100644 index 0000000..4b6325d --- /dev/null +++ b/crates/contracts/src/eas.rs @@ -0,0 +1,165 @@ +use alloy_primitives::{Address, B256, U256, address, b256}; +use alloy_sol_types::SolValue; + +/// The schema ID for the un-revocable the content hash attestation. +/// +/// raw schema: `(bytes32 contentHash)` +pub const SCHEMA_ID: B256 = + b256!("0x5c5b8b295ff43c8e442be11d569e94a4cd5476f5e23df0f71bdd408df6b9649c"); + +mod inner { + alloy_sol_types::sol! { + #![sol(all_derives)] + + /// A struct representing a single attestation. + struct Attestation { + bytes32 uid; // A unique identifier of the attestation. + bytes32 schema; // The unique identifier of the schema. + uint64 time; // The time when the attestation was created (Unix timestamp). + uint64 expirationTime; // The time when the attestation expires (Unix timestamp). + uint64 revocationTime; // The time when the attestation was revoked (Unix timestamp). + bytes32 refUID; // The UID of the related attestation. + address recipient; // The recipient of the attestation. + address attester; // The attester/sender of the attestation. + bool revocable; // Whether the attestation is revocable. + bytes data; // Custom attestation data. + } + + /// A struct representing the arguments of the attestation request. + struct AttestationRequestData { + address recipient; // The recipient of the attestation. + uint64 expirationTime; // The time when the attestation expires (Unix timestamp). + bool revocable; // Whether the attestation is revocable. + bytes32 refUID; // The UID of the related attestation. + bytes data; // Custom attestation data. + uint256 value; // An explicit ETH amount to send to the resolver. This is important to prevent accidental user errors. + } + + /// A struct representing the full arguments of the attestation request. + struct AttestationRequest { + bytes32 schema; // The unique identifier of the schema. + AttestationRequestData data; // The arguments of the attestation request. + } + + /// A struct representing the full arguments of the multi attestation request. + struct MultiAttestationRequest { + bytes32 schema; // The unique identifier of the schema. + AttestationRequestData[] data; // The arguments of the attestation request. + } + + #[sol(rpc)] + interface IEAS { + /// Emitted when an attestation has been made. + /// + /// - `recipient` The recipient of the attestation. + /// - `attester` The attesting account. + /// - `uid` The UID of the new attestation. + /// - `schemaUID` The UID of the schema. + /// + event Attested(address indexed recipient, address indexed attester, bytes32 uid, bytes32 indexed schemaUID); + + /// Emitted when a data has been timestamped. + /// + /// - `data` The data. + /// - `timestamp` The timestamp. + /// + event Timestamped(bytes32 indexed data, uint64 indexed timestamp); + + /// Attests to a specific schema. + /// + /// # Arguments + /// The arguments of the attestation request. + /// + /// # Returns + /// The UID of the new attestation. + /// + function attest(AttestationRequest calldata request) external payable returns (bytes32); + + /// Attests to multiple schemas. + /// + /// # Arguments + /// The arguments of the multi attestation requests. The requests should be grouped by + /// distinct schema ids to benefit from the best batching optimization. + /// + /// # Returns + /// The UIDs of the new attestations. + /// + function multiAttest(MultiAttestationRequest[] calldata multiRequests) external payable returns (bytes32[] memory); + + /// Timestamps the specified bytes32 data. + /// + /// # Arguments + /// The data to timestamp. + /// + /// # Returns + /// The timestamp the data was timestamped with. + /// + function timestamp(bytes32 data) external returns (uint64); + + /// Returns an existing attestation by UID. + /// + /// # Arguments + /// The UID of the attestation to retrieve. + /// + /// # Returns + /// The attestation data members. + /// + function getAttestation(bytes32 uid) external view returns (Attestation memory); + + /// Returns the timestamp that the specified data was timestamped with. + /// + /// # Arguments + /// The data to query. + /// + /// # Returns + /// The timestamp the data was timestamped with. + /// + function getTimestamp(bytes32 data) external view returns (uint64); + } + } + pub use IEAS::*; +} + +pub use inner::{Attestation, AttestationRequest, AttestationRequestData}; + +pub use inner::IEASInstance as EAS; + +/// events +pub mod events { + pub use super::inner::{Attested, Timestamped}; +} + +/// calls +pub mod calls { + pub use super::inner::IEAS::{ + attestCall as AttestCall, getAttestationCall as GetAttestationCall, + getTimestampCall as GetTimestampCall, multiAttestCall as MultiAttestCall, + timestampCall as TimestampCall, + }; +} + +impl AttestationRequest { + /// Creates a new attestation request. + #[inline] + pub fn new(root: B256) -> Self { + Self { + schema: SCHEMA_ID, + data: AttestationRequestData { + recipient: Address::ZERO, + expirationTime: 0, + revocable: false, + refUID: B256::ZERO, + data: root.abi_encode().into(), + value: U256::ZERO, + }, + } + } +} + +/// The EAS contract addresses on different chains. +pub static EAS_ADDRESSES: phf::Map = phf::phf_map! { + 1u64 => address!("0xA1207F3BBa224E2c9c3c6D5aF63D0eb1582Ce587"), + 534351u64 => address!("0xaEF4103A04090071165F78D45D83A0C0782c2B2a"), + 534352u64 => address!("0xC47300428b6AD2c7D03BB76D05A176058b47E6B0"), + 11155111u64 => address!("0xC47300428b6AD2c7D03BB76D05A176058b47E6B0"), +}; diff --git a/crates/contracts/src/fee_oracle.rs b/crates/contracts/src/fee_oracle.rs new file mode 100644 index 0000000..11c289f --- /dev/null +++ b/crates/contracts/src/fee_oracle.rs @@ -0,0 +1,43 @@ +mod inner { + alloy_sol_types::sol! { + #![sol(all_derives)] + + #[sol(rpc)] + interface IFeeOracle { + /// Calculate the final fee a user must pay for L1 anchoring. + /// + /// # Returns + /// fee The required fee in Wei. + /// + function getFloorFee() external view returns (uint256); + + function setL1Overhead(uint256 l1Overhead) external; + function setL1FeeScalar(uint256 l1FeeScalar) external; + function setL1GasEstimated(uint256 l1GasEstimated) external; + function setCrossDomainGasEstimated(uint256 crossDomainGasEstimated) external; + function setL2ExecutionScalar(uint256 l2ExecutionScalar) external; + function setL2ExecutionOverhead(uint256 l2ExecutionOverhead) external; + function setExpectedBatchSize(uint256 expectedBatchSize) external; + function setFeeMultiplier(uint256 feeMultiplier) external; + } + } +} + +pub use inner::IFeeOracle::IFeeOracleInstance as FeeOracle; + +/// calls +pub mod calls { + pub use crate::fee_oracle::inner::IFeeOracle::getFloorFeeCall as GetFloorFeeCall; + + /// Admin calls + pub mod admins { + pub use crate::fee_oracle::inner::IFeeOracle::{ + setCrossDomainGasEstimatedCall as SetCrossDomainGasEstimatedCall, + setExpectedBatchSizeCall as SetExpectedBatchSizeCall, + setFeeMultiplierCall as SetFeeMultiplierCall, setL1FeeScalarCall as SetL1FeeScalarCall, + setL1GasEstimatedCall as SetL1GasEstimatedCall, setL1OverheadCall as SetL1OverheadCall, + setL2ExecutionOverheadCall as SetL2ExecutionOverheadCall, + setL2ExecutionScalarCall as SetL2ExecutionScalarCall, + }; + } +} diff --git a/crates/contracts/src/gateway.rs b/crates/contracts/src/gateway.rs new file mode 100644 index 0000000..4cb98d3 --- /dev/null +++ b/crates/contracts/src/gateway.rs @@ -0,0 +1,39 @@ +mod inner { + alloy_sol_types::sol! { + #![sol(all_derives)] + + #[sol(rpc)] + interface IL1AnchoringGateway { + /// Emitted when a new batch of Merkle roots is submitted to L1 for anchoring. + event BatchSubmitted( + bytes32 indexed merkleRoot, uint256 indexed startIndex, uint256 count, address indexed submitter + ); + + /// Submit a SINGLE aggregated Merkle Root to L1 and trigger L2 verification. + /// + /// # Arguments + /// - `merkleRoot` The root of the Merkle Tree containing all roots in this batch. + /// - `startIndex` The queue index of the first root in this batch. + /// - `count` The number of roots in this batch. + /// - `gasLimit` The gas limit for L2 execution of this batch. Caller should estimate + /// the gas cost based on the batch size and current L2 gas price, and provide enough + /// ETH to cover both L1 Gas and L2 Execution Gas. + /// - `msg.value` Caller must send enough ETH to cover L1 Gas + L2 Execution Gas. + /// + function submitBatch(bytes32 merkleRoot, uint256 startIndex, uint256 count, uint256 gasLimit) external payable; + } + } + pub use IL1AnchoringGateway::*; +} + +pub use inner::IL1AnchoringGatewayInstance as L1AnchoringGateway; + +/// events +pub mod events { + pub use super::inner::BatchSubmitted; +} + +/// calls +pub mod calls { + pub use super::inner::submitBatchCall as SubmitBatchCall; +} diff --git a/crates/contracts/src/lib.rs b/crates/contracts/src/lib.rs index f955e44..edf4f60 100644 --- a/crates/contracts/src/lib.rs +++ b/crates/contracts/src/lib.rs @@ -1,107 +1,57 @@ //! Solidity contracts for UTS -/// UniversalTimestamps contract -pub mod uts { - use alloy_primitives::{Address, address}; - - #[doc(hidden)] - pub mod binding { - use alloy_sol_types::sol; - - sol!( - #[sol(rpc, all_derives)] - IUniversalTimestamps, - "abi/IUniversalTimestamps.json" - ); - sol!( - #[sol(rpc)] - UniversalTimestamps, - "abi/UniversalTimestamps.json" - ); - } - - pub use binding::IUniversalTimestamps::{ - Attested, IUniversalTimestampsInstance as UniversalTimestamps, - }; - - pub use binding::UniversalTimestamps::{BYTECODE, DEPLOYED_BYTECODE, deploy, deploy_builder}; - - /// Default address for the UniversalTimestamps contract. - pub const DEFAULT_ADDRESS: Address = address!("0xceB7a9E77bd00D0391349B9bC989167cAB5e35e7"); - - #[cfg(test)] - mod tests { - use super::*; - use crate::erc1967::ERC1967ProxyInstance; - use alloy::{ - network::EthereumWallet, - primitives::{B256, Bytes, U256, b256}, - providers::ProviderBuilder, - signers::local::MnemonicBuilder, - }; - use futures::StreamExt; - use std::env; - - const ROOT: B256 = - b256!("5cd5c6763b9f2b3fb1cd66a15fe92b7ac913eec295d9927886e175f144ce3308"); - - #[tokio::test] - async fn test() -> eyre::Result<()> { - let provider = ProviderBuilder::new().connect_anvil_with_wallet(); - let imp = deploy(&provider).await?; - let proxy = - ERC1967ProxyInstance::deploy(&provider, *imp.address(), Bytes::new()).await?; - let uts = UniversalTimestamps::new(*proxy.address(), &provider); - - let attested_log = uts.Attested_filter().watch().await?; - - let _ = uts.attest(ROOT).send().await?.watch().await?; - - let timestamp = uts.timestamp(ROOT).call().await?; - assert_ne!(timestamp, U256::ZERO); - - let (attested, _log) = attested_log.into_stream().next().await.unwrap()?; - assert_eq!(attested.root, ROOT); - - Ok(()) - } - - #[tokio::test] - #[ignore] - async fn deploy_to_sepolia() -> eyre::Result<()> { - let signer = MnemonicBuilder::from_phrase(env::var("MNEMONIC")?.as_str()) - .index(0u32)? - .build()?; - - let provider = ProviderBuilder::new() - .wallet(EthereumWallet::new(signer)) - .connect("https://0xrpc.io/sep") - .await?; - - let imp = deploy(&provider).await?; - println!("Implementation deployed at: {:?}", imp.address()); - - let proxy = - ERC1967ProxyInstance::deploy(&provider, *imp.address(), Bytes::new()).await?; - println!("Proxy deployed at: {:?}", proxy.address()); - - Ok(()) - } - } -} - -/// ERC-1967 Proxy contract -#[cfg(any(test, feature = "erc1967"))] -pub mod erc1967 { - mod binding { - use alloy_sol_types::sol; - - sol!( - #[sol(rpc)] - ERC1967Proxy, - "abi/ERC1967Proxy.json" - ); - } - - pub use binding::ERC1967Proxy::*; -} +// MIT License +// +// Copyright (c) 2025 UTS Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// +// Apache License, Version 2.0 +// +// Copyright (c) 2025 UTS Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/// EAS contract +pub mod eas; + +/// Incomplete Solidity interfaces for the L1 Gateway contract. This is not a complete interface, +/// but only includes the events and functions that are relevant to the server/backend. +pub mod gateway; + +/// Incomplete Solidity interfaces for the L2 Anchoring Manager. This is not a complete interface, +/// but only includes the events and functions that are relevant to the server/backend. +pub mod manager; + +/// Solidity interfaces for the Fee Oracle. +pub mod fee_oracle; + +/// Helper for construct retry and throttle provider layers. +#[cfg(feature = "provider-helper")] +pub mod provider_helper; diff --git a/crates/contracts/src/manager.rs b/crates/contracts/src/manager.rs new file mode 100644 index 0000000..e280da3 --- /dev/null +++ b/crates/contracts/src/manager.rs @@ -0,0 +1,95 @@ +mod inner { + alloy_sol_types::sol! { + #![sol(all_derives)] + + #[sol(rpc)] + interface IL2AnchoringManager { + /// Emitted when a user pays to have their root anchored to L1. + event L1AnchoringQueued( + bytes32 indexed attestationId, + bytes32 indexed root, + uint256 queueIndex, + uint256 fee, + uint256 blockNumber, + uint256 timestamp + ); + + /// Emitted when L1 notifies that a batch of roots has been anchored on L1. + /// - `claimedRoot` The Merkle root claimed to be anchored on L1. + /// - `startIndex` The starting index of the batch in the queue. + /// - `count` The number of items in the batch. + /// - `l1BlockAttested` The L1 block number at which the batch was anchored. It would + /// be 0 if the root was timestamped before the batch submission. + /// - `l1TimestampAttested` The timestamp at which the batch was anchored on L1. + /// - `l2BlockNumber` The L2 block number at which the notification is received. + /// - `l2TimestampReceived` The timestamp when the notification is received. + /// + event L1BatchArrived( + bytes32 indexed claimedRoot, + uint256 indexed startIndex, + uint256 count, + uint256 l1BlockAttested, + uint256 l1TimestampAttested, + uint256 l2BlockNumber, + uint256 l2TimestampReceived + ); + + /// Emitted when a batch of roots is finalized after L1 confirmation. + /// - `merkleRoot` The Merkle root of the batch. + /// - `startIndex` The starting index of the batch in the queue. + /// - `count` The number of items in the batch. + /// - `l1BlockAttested` The L1 block number at which the batch was anchored. It would be + /// 0 if the root was timestamped before the batch submission. + /// - `l1TimestampAttested` The timestamp at which the batch was anchored on L1. + /// - `l2BlockNumber` The L2 block number at which the batch is finalized. + /// - `l2TimestampFinalized` The timestamp when the batch is finalized.- + /// + event L1BatchFinalized( + bytes32 indexed merkleRoot, + uint256 indexed startIndex, + uint256 count, + uint256 l1BlockAttested, + uint256 l1TimestampAttested, + uint256 l2BlockNumber, + uint256 l2TimestampFinalized + ); + + /// Emitted when a user claims their NFT after batch confirmation. + /// + event NFTClaimed(address indexed submitter, uint256 indexed tokenId, bytes32 indexed root, uint256 timestamp); + + /// see also submitForL1Anchoring(bytes32 attestationId, address refundAddress). + function submitForL1Anchoring(bytes32 attestationId) external payable; + + /// Finalize the batch confirmation after receiving the L1 notification. This will + /// verify the Merkle root and update the confirmed index. This can be called by anyone + /// after the notification is received to save the cost of L2 execution since the cross + /// chain gas price is higher than L2 execution. + /// + function finalizeBatch() external; + + /// Withdraw accumulated fees to the collector. + /// + function withdrawFees(address to, uint256 amount) external; + } + } + + pub use IL2AnchoringManager::*; +} + +pub use inner::IL2AnchoringManagerInstance as L2AnchoringManager; + +/// events +pub mod events { + pub use super::inner::{L1AnchoringQueued, L1BatchArrived, L1BatchFinalized, NFTClaimed}; +} + +/// calls +pub mod calls { + pub use super::inner::finalizeBatchCall as FinalizeBatchCall; + + /// admin calls + pub mod admins { + pub use crate::manager::inner::withdrawFeesCall as WithdrawFeesCall; + } +} diff --git a/crates/contracts/src/provider_helper.rs b/crates/contracts/src/provider_helper.rs new file mode 100644 index 0000000..8ba5dd3 --- /dev/null +++ b/crates/contracts/src/provider_helper.rs @@ -0,0 +1,83 @@ +use alloy_transport::layers::{RetryBackoffLayer, ThrottleLayer}; + +/// Parameters for retrying rate-limited requests with exponential backoff. +#[derive(Debug, Copy, Clone)] +#[cfg_attr(feature = "provider-helper-clap", derive(clap::Args))] +#[cfg_attr( + feature = "provider-helper-serde", + derive(serde::Serialize, serde::Deserialize), + serde(rename_all = "kebab-case") +)] +pub struct RetryBackoffArgs { + /// Max number of retries for rate-limited requests. + #[cfg_attr( + feature = "provider-helper-clap", + arg(long, help = "Maximum number of retries", default_value = "10") + )] + pub max_rate_limit_retries: u32, + /// Initial backoff in milliseconds for retrying rate-limited requests. + #[cfg_attr( + feature = "provider-helper-clap", + arg(long, help = "Initial backoff in milliseconds", default_value = "100") + )] + pub initial_backoff: u64, + /// Compute units per second for rate-limiting purposes. + #[cfg_attr( + feature = "provider-helper-clap", + arg(long, help = "Compute units per second", default_value = "100") + )] + pub compute_units_per_second: u64, +} + +/// Parameters for throttling requests to the RPC provider. +#[derive(Debug, Copy, Clone)] +#[cfg_attr(feature = "provider-helper-clap", derive(clap::Args))] +#[cfg_attr( + feature = "provider-helper-serde", + derive(serde::Serialize, serde::Deserialize), + serde(rename_all = "kebab-case") +)] +pub struct ThrottleArgs { + /// Requests per second to throttle. + #[cfg_attr( + feature = "provider-helper-clap", + arg(long, help = "Requests per second to throttle", default_value = "5") + )] + pub requests_per_second: u32, +} + +impl Default for RetryBackoffArgs { + fn default() -> Self { + Self { + max_rate_limit_retries: 10, + initial_backoff: 100, + compute_units_per_second: 20, + } + } +} + +impl Default for ThrottleArgs { + fn default() -> Self { + Self { + requests_per_second: 25, + } + } +} + +impl RetryBackoffArgs { + /// Get a `RetryBackoffLayer` from the retry backoff arguments. + pub fn layer(&self) -> RetryBackoffLayer { + RetryBackoffLayer::new( + self.max_rate_limit_retries, + self.initial_backoff, + self.compute_units_per_second, + ) + } +} + +impl ThrottleArgs { + /// Get a `RetryBackoffLayer` from the retry backoff arguments. + pub fn layer(&self) -> ThrottleLayer { + ThrottleLayer::new(self.requests_per_second) + } +} diff --git a/crates/core/Cargo.toml b/crates/core/Cargo.toml index 1d3bb43..080bfe7 100644 --- a/crates/core/Cargo.toml +++ b/crates/core/Cargo.toml @@ -1,49 +1,86 @@ [package] -description = "Core types and traits for OpenTimestamps in Rust" +description = "Core types and traits for Univeral Timestamps in Rust" documentation = "https://docs.rs/uts-core/" edition.workspace = true homepage.workspace = true -keywords.workspace = true -license.workspace = true +include = [ + "src/**/*", + "Cargo.toml", + "LICENSE*", + "README*", +] +keywords = ["uts", "timestamping"] +license = "MIT OR Apache-2.0" name = "uts-core" repository.workspace = true version.workspace = true [dependencies] +allocator-api2 = { workspace = true } alloy-chains = { workspace = true } +alloy-contract = { workspace = true, optional = true } alloy-primitives = { workspace = true } alloy-provider = { workspace = true, optional = true } -alloy-rpc-types-eth = { workspace = true, optional = true } alloy-sol-types = { workspace = true, optional = true } auto_impl.workspace = true +backon = { workspace = true, optional = true } bytes = { workspace = true, optional = true } -digest.workspace = true -hex.workspace = true -once_cell = { workspace = true, features = ["alloc"] } +digest = { workspace = true } +http = { workspace = true, optional = true } +http-body-util = { workspace = true, optional = true } paste.workspace = true +reqwest = { workspace = true, optional = true, default-features = false } ripemd.workspace = true serde = { workspace = true, optional = true } +serde_json = { workspace = true, optional = true } serde_with = { workspace = true, optional = true } sha1.workspace = true sha2.workspace = true sha3.workspace = true thiserror.workspace = true tokio = { workspace = true, optional = true } -tracing = { workspace = true, optional = true } +tracing = { workspace = true } +url = { workspace = true, optional = true } uts-bmt = { workspace = true } uts-contracts = { workspace = true, optional = true } [features] +default = [] + bytes = ["dep:bytes"] -default = ["std"] -ethereum-uts-verifier = ["verifier", "dep:alloy-provider", "dep:alloy-rpc-types-eth", "dep:alloy-sol-types", "dep:uts-contracts"] -io-utils = ["dep:tokio", "tokio/fs"] serde = ["dep:serde", "dep:serde_with", "serde/derive", "serde_with/hex"] -std = [] -tracing = ["dep:tracing"] + +bitcoin-verifier = [ + "dep:reqwest", + "dep:url", + "dep:backon", + "dep:http", + "dep:http-body-util", + "dep:serde", + "dep:serde_json", + + "verifier", + "reqwest/json", + "serde/derive", +] +eas-verifier = [ + "dep:alloy-provider", + "dep:alloy-contract", + "dep:alloy-sol-types", + "dep:uts-contracts", + + "verifier", +] verifier = [] +io-utils = ["dep:tokio", "tokio/fs"] + +nightly = [] + +reqwest-default-tls = ["reqwest/default-tls", "alloy-provider?/reqwest-default-tls"] +reqwest-native-tls = ["reqwest/native-tls", "alloy-provider?/reqwest-native-tls"] +reqwest-rustls = ["reqwest/rustls", "alloy-provider?/reqwest-rustls-tls"] + [dev-dependencies] -criterion = { version = "0.5", features = ["html_reports"] } -opentimestamps = { git = "https://github.com/opentimestamps/rust-opentimestamps" } serde_json.workspace = true +tokio = { workspace = true, features = ["full"] } diff --git a/crates/core/LICENSE-APACHE b/crates/core/LICENSE-APACHE new file mode 120000 index 0000000..1cd601d --- /dev/null +++ b/crates/core/LICENSE-APACHE @@ -0,0 +1 @@ +../../LICENSE-APACHE \ No newline at end of file diff --git a/crates/core/LICENSE-MIT b/crates/core/LICENSE-MIT new file mode 120000 index 0000000..b2cfbdc --- /dev/null +++ b/crates/core/LICENSE-MIT @@ -0,0 +1 @@ +../../LICENSE-MIT \ No newline at end of file diff --git a/crates/core/src/codec.rs b/crates/core/src/codec.rs index acc83ed..8520cb8 100644 --- a/crates/core/src/codec.rs +++ b/crates/core/src/codec.rs @@ -1,13 +1,14 @@ -use crate::error::{DecodeError, EncodeError}; -use alloc::alloc::Global; +use crate::{ + alloc::{Allocator, Global}, + error::{DecodeError, EncodeError}, +}; use auto_impl::auto_impl; -use core::{alloc::Allocator, ops::RangeBounds}; +use core::ops::RangeBounds; mod proof; pub use proof::{Proof, Version, VersionedProof}; mod imp; -#[cfg(feature = "std")] pub use imp::{Reader, Writer}; /// Types and helpers for the version 1 serialization format. diff --git a/crates/core/src/codec/imp.rs b/crates/core/src/codec/imp.rs index d393fd9..7d30615 100644 --- a/crates/core/src/codec/imp.rs +++ b/crates/core/src/codec/imp.rs @@ -1,14 +1,11 @@ -use crate::codec::*; -use alloc::vec::Vec; +use crate::{alloc::vec::Vec, codec::*}; mod alloy; #[cfg(feature = "bytes")] mod bytes; mod primitives; -#[cfg(feature = "std")] mod std_io; -#[cfg(feature = "std")] pub use std_io::{Reader, Writer}; impl Encoder for Vec { @@ -23,6 +20,18 @@ impl Encoder for Vec { } } +impl Encoder for std::vec::Vec { + fn encode_byte(&mut self, byte: u8) -> Result<(), EncodeError> { + self.push(byte); + Ok(()) + } + + fn write_all(&mut self, buf: impl AsRef<[u8]>) -> Result<(), EncodeError> { + self.extend_from_slice(buf.as_ref()); + Ok(()) + } +} + impl Decoder for &[u8] { fn decode_byte(&mut self) -> Result { let Some((a, b)) = self.split_at_checked(1) else { diff --git a/crates/core/src/codec/imp/primitives.rs b/crates/core/src/codec/imp/primitives.rs index afd129d..0bf89c4 100644 --- a/crates/core/src/codec/imp/primitives.rs +++ b/crates/core/src/codec/imp/primitives.rs @@ -31,7 +31,7 @@ macro_rules! leb128 { } } - impl crate::codec::DecodeIn for $ty { + impl crate::codec::DecodeIn for $ty { #[inline] fn decode_in(decoder: &mut impl crate::codec::Decoder, _alloc: A) -> Result { let mut ret: $ty = 0; @@ -40,9 +40,34 @@ macro_rules! leb128 { loop { // Bottom 7 bits are value bits let byte = decoder.decode_byte()?; - ret |= ((byte & 0x7f) as $ty) - .shl_exact(shift) - .ok_or($crate::error::DecodeError::LEB128Overflow(<$ty>::BITS))?; + let value = (byte & 0x7f) as $ty; + + // This is a stable port of `shl_exact` + // FIXME: This should be replaced with `shl_exact` once it is stabilized. + // ``` + // ret |= ((byte & 0x7f) as $ty) + // .shl_exact(shift) + // .ok_or($crate::error::DecodeError::LEB128Overflow(<$ty>::BITS))?; + // ``` + // #[unstable(feature = "exact_bitshifts", issue = "144336")] + // #[must_use = "this returns the result of the operation, \ + // without modifying the original"] + // #[inline] + // pub const fn shl_exact(self, rhs: u32) -> Option<$SelfT> { + // if rhs < self.leading_zeros() || rhs < self.leading_ones() { + // // SAFETY: rhs is checked above + // Some(unsafe { self.unchecked_shl(rhs) }) + // } else { + // None + // } + // } + if shift < value.leading_zeros() || shift < value.leading_ones() { + // SAFETY: shift is checked above + ret |= unsafe { ((byte & 0x7f) as $ty).unchecked_shl(shift) }; + } else { + return Err($crate::error::DecodeError::LEB128Overflow(<$ty>::BITS)); + } + // Top bit is a continue bit if byte & 0x80 == 0 { break; diff --git a/crates/core/src/codec/proof.rs b/crates/core/src/codec/proof.rs index ac0ec6a..d1566eb 100644 --- a/crates/core/src/codec/proof.rs +++ b/crates/core/src/codec/proof.rs @@ -1,9 +1,10 @@ use crate::{ + alloc::{Allocator, Global}, codec::{DecodeIn, Decoder, Encode, Encoder}, error::{DecodeError, EncodeError}, }; -use alloc::alloc::{Allocator, Global}; use core::fmt; +use std::ops::{Deref, DerefMut}; /// Version number of the serialization format. pub type Version = u32; @@ -75,3 +76,17 @@ impl + fmt::Display, A: Allocator> fmt::Display for VersionedProof, A: Allocator> Deref for VersionedProof { + type Target = T; + + fn deref(&self) -> &Self::Target { + &self.proof + } +} + +impl, A: Allocator> DerefMut for VersionedProof { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.proof + } +} diff --git a/crates/core/src/codec/v1.rs b/crates/core/src/codec/v1.rs index 9091bee..b29f9c2 100644 --- a/crates/core/src/codec/v1.rs +++ b/crates/core/src/codec/v1.rs @@ -7,8 +7,8 @@ pub mod opcode; mod timestamp; pub use attestation::{ - Attestation, AttestationTag, BitcoinAttestation, EthereumUTSAttestation, PendingAttestation, - RawAttestation, + Attestation, AttestationTag, BitcoinAttestation, EASAttestation, EASTimestamped, + PendingAttestation, RawAttestation, }; pub use detached_timestamp::DetachedTimestamp; pub use digest::DigestHeader; @@ -24,7 +24,6 @@ impl core::fmt::Display for FinalizationError { } } -#[cfg(feature = "std")] impl std::error::Error for FinalizationError {} /// Trait for objects that may have input data. @@ -61,7 +60,7 @@ pub trait ConsistentWith: MayHaveInput { fn is_consistent_with(&self, other: &T) -> bool { self.input() .zip(other.to_input()) - .map_or(true, |(a, b)| a == b) + .is_none_or(|(a, b)| a == b) } /// Checks if self is consistent with the given input. diff --git a/crates/core/src/codec/v1/attestation.rs b/crates/core/src/codec/v1/attestation.rs index c1f336e..80354ec 100644 --- a/crates/core/src/codec/v1/attestation.rs +++ b/crates/core/src/codec/v1/attestation.rs @@ -4,33 +4,43 @@ //! comes from some server or from a blockchain. use crate::{ + alloc::{Allocator, Global, vec::Vec}, codec::{Decode, DecodeIn, Decoder, Encode, Encoder, v1::MayHaveInput}, error::{DecodeError, EncodeError}, - utils::{Hexed, OnceLock}, -}; -use alloc::{ - alloc::{Allocator, Global}, - borrow::Cow, - vec::Vec, + utils::Hexed, }; use alloy_chains::Chain; -use alloy_primitives::{Address, BlockNumber, ChainId, TxHash}; +use alloy_primitives::{B256, FixedBytes, fixed_bytes as tag}; use core::fmt; +use std::{borrow::Cow, sync::OnceLock}; /// Size in bytes of the tag identifying the attestation type. const TAG_SIZE: usize = 8; +/// Tag identifying the attestation kind. +pub type AttestationTag = FixedBytes; + /// Tag indicating a Bitcoin attestation. -const BITCOIN_TAG: &[u8; 8] = b"\x05\x88\x96\x0d\x73\xd7\x19\x01"; +static BITCOIN_TAG: AttestationTag = tag!("0x0588960d73d71901"); + /// Tag indicating a pending attestation. -const PENDING_TAG: &[u8; 8] = b"\x83\xdf\xe3\x0d\x2e\xf9\x0c\x8e"; -/// Tag indicating an Ethereum UTS contract attestation. +static PENDING_TAG: AttestationTag = tag!("0x83dfe30d2ef90c8e"); + +/// Tag indicating an EAS attestation. +/// +/// The attestation emits an event with signature `Attested(address,address,bytes32,bytes32)`, and +/// `selector = 0x8bf46bf4cfd674fa735a3d63ec1c9ad4153f033c290341f3a588b75685141b35`. /// -/// TAG = keccak256("EthereumUTSAttestation")[:8] -const ETHEREUM_UTS_TAG: &[u8; 8] = b"\xea\xf2\xbc\x69\x3c\x93\x25\x1c"; +/// `TAG = selector[..8]` +static EAS_ATTEST_TAG: AttestationTag = tag!("0x8bf46bf4cfd674fa"); -/// Tag identifying the attestation kind. -pub type AttestationTag = [u8; TAG_SIZE]; +/// Tag indicating an EAS Timestamp Log attestation. +/// +/// The attestation emits an event with signature `Timestamped(bytes32,uint64)`, and +/// `selector = 0x5aafceeb1c7ad58e4a84898bdee37c02c0fc46e7d24e6b60e8209449f183459f`. +/// +/// `TAG = selector[..8]` +static EAS_TIMESTAMP_TAG: AttestationTag = tag!("0x5aafceeb1c7ad58e"); /// Raw Proof that some data existed at a given time. #[derive(Clone)] @@ -52,8 +62,7 @@ impl fmt::Debug for RawAttestation { impl DecodeIn for RawAttestation { fn decode_in(decoder: &mut impl Decoder, alloc: A) -> Result { - let mut tag = [0u8; TAG_SIZE]; - decoder.read_exact(&mut tag)?; + let tag = decoder.decode()?; let len = decoder.decode()?; let mut data = Vec::with_capacity_in(len, alloc); @@ -137,7 +146,7 @@ impl fmt::Display for BitcoinAttestation { } impl Attestation<'_> for BitcoinAttestation { - const TAG: AttestationTag = *BITCOIN_TAG; + const TAG: AttestationTag = BITCOIN_TAG; fn from_raw_data(data: &[u8]) -> Result { let height = u32::decode(&mut &*data)?; @@ -151,138 +160,67 @@ impl Attestation<'_> for BitcoinAttestation { } } -/// Attestation by an Ethereum UTS contract. +/// Attestation by `EAS::attest` using schema `0x5c5b8b295ff43c8e442be11d569e94a4cd5476f5e23df0f71bdd408df6b9649c`. #[derive(Debug, Clone, PartialEq, Eq)] -pub struct EthereumUTSAttestation { +pub struct EASAttestation { pub chain: Chain, - pub height: BlockNumber, - /// Optional extra metadata about the attestation, such as the contract address and transaction hash. - pub metadata: EthereumUTSAttestationExtraMetadata, -} - -/// Extra metadata for an Ethereum UTS attestation. -/// -/// The tx field is only present if the contract field is present, -/// and should be ignored if the contract field is None. -#[derive(Default, Debug, Clone, PartialEq, Eq)] -pub struct EthereumUTSAttestationExtraMetadata { - contract: Option
, - tx: Option, -} - -impl EthereumUTSAttestation { - /// Creates a new Ethereum UTS attestation with the given chain id, block number, and extra metadata. - pub fn new( - chain_id: ChainId, - height: BlockNumber, - metadata: EthereumUTSAttestationExtraMetadata, - ) -> Self { - Self { - chain: Chain::from_id(chain_id), - height, - metadata, - } - } + pub uid: B256, } -impl fmt::Display for EthereumUTSAttestation { +impl fmt::Display for EASAttestation { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!( - f, - "UTS on chain {} at block #{}({})", - self.chain, self.height, self.metadata - ) + write!(f, "EAS attestation {} on {}", self.uid, self.chain) } } -impl Attestation<'_> for EthereumUTSAttestation { - const TAG: AttestationTag = *ETHEREUM_UTS_TAG; +impl<'a> Attestation<'a> for EASAttestation { + const TAG: AttestationTag = EAS_ATTEST_TAG; - fn from_raw_data(data: &[u8]) -> Result { + fn from_raw_data(data: &'a [u8]) -> Result { let data = &mut &data[..]; - let chain = Chain::decode(data)?; - let height = BlockNumber::decode(data)?; - let metadata = EthereumUTSAttestationExtraMetadata::decode(data)?; - Ok(EthereumUTSAttestation { - chain, - height, - metadata, + let chain_id = u64::decode(data)?; + let uid = B256::decode(data)?; + Ok(EASAttestation { + chain: Chain::from_id(chain_id), + uid, }) } fn to_raw_data_in(&self, alloc: A) -> Result, EncodeError> { - // chain id + block number + optional address + optional tx hash - const SIZE: usize = size_of::() + size_of::() + 20 + 32; - let mut buffer = Vec::with_capacity_in(SIZE, alloc); - buffer.encode(self.chain)?; - buffer.encode(self.height)?; - buffer.encode(&self.metadata)?; + let mut buffer = + Vec::with_capacity_in(u64::BITS.div_ceil(7) as usize + size_of::(), alloc); + buffer.encode(self.chain.id())?; + buffer.encode(self.uid)?; Ok(buffer) } } -impl Encode for EthereumUTSAttestationExtraMetadata { - fn encode(&self, encoder: &mut impl Encoder) -> Result<(), EncodeError> { - if let Some(contract) = self.contract { - encoder.encode(&contract)?; - if let Some(tx) = self.tx { - encoder.encode(&tx)?; - } - } - Ok(()) - } -} - -impl Decode for EthereumUTSAttestationExtraMetadata { - fn decode(decoder: &mut impl Decoder) -> Result { - let contract = Address::decode_trailing(decoder)?; - let tx = if contract.is_some() { - TxHash::decode_trailing(decoder)? - } else { - None - }; - Ok(Self { contract, tx }) - } +/// Attestation by `EAS::timestamp` +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct EASTimestamped { + pub chain: Chain, } -impl fmt::Display for EthereumUTSAttestationExtraMetadata { +impl fmt::Display for EASTimestamped { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match (self.contract, self.tx) { - (Some(contract), Some(tx)) => write!(f, "{contract} by tx: {tx}"), - (Some(contract), None) => write!(f, "{contract}"), - (None, Some(_)) => unreachable!("Tx should not be present without contract"), - (None, None) => write!(f, "no extra metadata"), - } + write!(f, "EAS timestamped on {}", self.chain) } } -impl EthereumUTSAttestationExtraMetadata { - /// Creates new extra metadata with the given contract address and no transaction hash. - pub fn new(contract: Address) -> Self { - Self { - contract: Some(contract), - tx: None, - } - } - - /// Creates new extra metadata with the given contract address and transaction hash. - pub fn new_with_tx(contract: Address, tx: TxHash) -> Self { - Self { - contract: Some(contract), - tx: Some(tx), - } - } +impl<'a> Attestation<'a> for EASTimestamped { + const TAG: AttestationTag = EAS_TIMESTAMP_TAG; - /// Returns the contract address if present, or None if not. - #[inline] - pub fn contract(&self) -> Option
{ - self.contract + fn from_raw_data(data: &'a [u8]) -> Result { + let chain_id = u64::decode(&mut &data[..])?; + Ok(EASTimestamped { + chain: Chain::from_id(chain_id), + }) } - /// Returns the transaction hash if present, or None if not. - #[inline] - pub fn tx(&self) -> Option { - self.tx + fn to_raw_data_in(&self, alloc: A) -> Result, EncodeError> { + let mut buffer = Vec::with_capacity_in(u64::BITS.div_ceil(7) as usize, alloc); + buffer.encode(self.chain.id())?; + Ok(buffer) } } @@ -309,7 +247,7 @@ impl fmt::Display for PendingAttestation<'_> { } impl<'a> Attestation<'a> for PendingAttestation<'a> { - const TAG: AttestationTag = *PENDING_TAG; + const TAG: AttestationTag = PENDING_TAG; fn from_raw_data(data: &'a [u8]) -> Result { let data = &mut &data[..]; @@ -354,9 +292,12 @@ impl fmt::Display for RawAttestation { let att = BitcoinAttestation::from_raw(self).expect("Valid Bitcoin attestation"); write!(f, "{}", att) } - tag if *tag == *ETHEREUM_UTS_TAG => { - let att = - EthereumUTSAttestation::from_raw(self).expect("Valid Ethereum UTS attestation"); + tag if *tag == *EAS_ATTEST_TAG => { + let att = EASAttestation::from_raw(self).expect("Valid EAS attestation"); + write!(f, "{}", att) + } + tag if *tag == *EAS_TIMESTAMP_TAG => { + let att = EASTimestamped::from_raw(self).expect("Valid EAS Timestamp attestation"); write!(f, "{}", att) } _ => write!(f, "Unknown Attestation with tag {}", Hexed(&self.tag)), @@ -370,18 +311,3 @@ impl MayHaveInput for RawAttestation { self.value.get().map(|v| v.as_slice()) } } - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_ethereum_uts_tag() { - use sha3::{Digest, Keccak256}; - - let mut hasher = Keccak256::new(); - hasher.update(b"EthereumUTSAttestation"); - let result = hasher.finalize().to_vec(); - assert_eq!(&result[..8], ETHEREUM_UTS_TAG); - } -} diff --git a/crates/core/src/codec/v1/detached_timestamp.rs b/crates/core/src/codec/v1/detached_timestamp.rs index 353ac91..01a6822 100644 --- a/crates/core/src/codec/v1/detached_timestamp.rs +++ b/crates/core/src/codec/v1/detached_timestamp.rs @@ -1,8 +1,10 @@ -use crate::codec::{ - Decode, DecodeIn, Encode, Encoder, Proof, Version, - v1::{DigestHeader, FinalizationError, Timestamp}, +use crate::{ + alloc::{Allocator, Global}, + codec::{ + Decode, DecodeIn, Encode, Encoder, Proof, Version, + v1::{DigestHeader, FinalizationError, Timestamp}, + }, }; -use alloc::alloc::{Allocator, Global}; use core::{fmt, fmt::Formatter}; use std::ops::{Deref, DerefMut}; @@ -114,15 +116,15 @@ impl DetachedTimestamp { } } -impl Deref for DetachedTimestamp { - type Target = Timestamp; +impl Deref for DetachedTimestamp { + type Target = Timestamp; fn deref(&self) -> &Self::Target { &self.timestamp } } -impl DerefMut for DetachedTimestamp { +impl DerefMut for DetachedTimestamp { fn deref_mut(&mut self) -> &mut Self::Target { &mut self.timestamp } diff --git a/crates/core/src/codec/v1/digest.rs b/crates/core/src/codec/v1/digest.rs index 93a6118..eeab95b 100644 --- a/crates/core/src/codec/v1/digest.rs +++ b/crates/core/src/codec/v1/digest.rs @@ -1,4 +1,5 @@ use crate::{ + alloc::Allocator, codec::{ DecodeIn, Decoder, Encode, Encoder, v1::opcode::{DigestOp, DigestOpExt}, @@ -6,7 +7,6 @@ use crate::{ error::{DecodeError, EncodeError}, utils::Hexed, }; -use alloc::alloc::Allocator; use core::fmt; use digest::{Output, typenum::Unsigned}; @@ -61,7 +61,7 @@ impl DigestHeader { } impl Encode for DigestHeader { - #[cfg_attr(feature = "tracing", tracing::instrument(skip(writer), err))] + #[tracing::instrument(skip_all, err)] #[inline] fn encode(&self, encoder: &mut impl Encoder) -> Result<(), EncodeError> { encoder.encode(self.kind)?; @@ -71,7 +71,7 @@ impl Encode for DigestHeader { } impl DecodeIn for DigestHeader { - #[cfg_attr(feature = "tracing", tracing::instrument(skip(reader), ret, err))] + #[tracing::instrument(skip_all, ret(level = "trace"), err)] #[inline] fn decode_in(decoder: &mut impl Decoder, _alloc: A) -> Result { let kind = decoder.decode()?; diff --git a/crates/core/src/codec/v1/opcode.rs b/crates/core/src/codec/v1/opcode.rs index 237bd34..278d0e1 100644 --- a/crates/core/src/codec/v1/opcode.rs +++ b/crates/core/src/codec/v1/opcode.rs @@ -3,10 +3,11 @@ //! It contains opcode information and utilities to work with opcodes. use crate::{ + alloc::{Allocator, Global, vec::Vec}, codec::{Decode, DecodeIn, Decoder, Encode, Encoder}, error::{DecodeError, EncodeError}, }; -use alloc::{alloc::Allocator, vec::Vec}; +use alloy_primitives::hex; use core::{fmt, hint::unreachable_unchecked}; use digest::Digest; use ripemd::Ripemd160; @@ -94,7 +95,7 @@ impl OpCode { /// Panics if the opcode is a control opcode. #[inline] pub fn execute(&self, input: impl AsRef<[u8]>, immediate: impl AsRef<[u8]>) -> Vec { - self.execute_in(input, immediate, alloc::alloc::Global) + self.execute_in(input, immediate, Global) } /// Executes the opcode on the given input data, with an optional immediate value. @@ -284,7 +285,6 @@ macro_rules! define_opcodes { } } - #[cfg(feature = "std")] impl std::error::Error for OpCodeFromStrError {} impl core::str::FromStr for OpCode { @@ -325,18 +325,18 @@ macro_rules! define_digest_opcodes { } /// Executes the digest operation on the input data. - pub fn execute(&self, input: impl AsRef<[u8]>) -> ::alloc::vec::Vec { - self.execute_in(input, ::alloc::alloc::Global) + pub fn execute(&self, input: impl AsRef<[u8]>) -> $crate::alloc::vec::Vec { + self.execute_in(input, $crate::alloc::Global) } /// Executes the digest operation on the input data. - pub fn execute_in(&self, input: impl AsRef<[u8]>, alloc: A) -> ::alloc::vec::Vec { + pub fn execute_in(&self, input: impl AsRef<[u8]>, alloc: A) -> $crate::alloc::vec::Vec { match *self { $( Self::$variant => { paste::paste! { let mut hasher = [<$variant:camel>]::new(); hasher.update(input); - hasher.finalize().to_vec_in(alloc) + $crate::alloc::SliceExt::to_vec_in(hasher.finalize().as_slice(), alloc) } }, )* // SAFETY: unreachable as all variants are covered. @@ -361,7 +361,7 @@ macro_rules! define_digest_opcodes { macro_rules! impl_simple_step { ($variant:ident) => {paste::paste! { - impl $crate::codec::v1::timestamp::builder::TimestampBuilder { + impl $crate::codec::v1::timestamp::builder::TimestampBuilder { #[doc = concat!("Push the `", stringify!($variant), "` opcode.")] pub fn [< $variant:lower >](&mut self) -> &mut Self { self.push_step(OpCode::[<$variant>]) @@ -377,9 +377,9 @@ macro_rules! impl_simple_step { macro_rules! impl_step_with_data { ($variant:ident) => {paste::paste! { - impl $crate::codec::v1::timestamp::builder::TimestampBuilder { + impl $crate::codec::v1::timestamp::builder::TimestampBuilder { #[doc = concat!("Push the `", stringify!($variant), "` opcode.")] - pub fn [< $variant:lower >](&mut self, data: ::alloc::vec::Vec) -> &mut Self { + pub fn [< $variant:lower >](&mut self, data: $crate::alloc::vec::Vec) -> &mut Self { self.push_immediate_step(OpCode::[<$variant>], data) } } diff --git a/crates/core/src/codec/v1/timestamp.rs b/crates/core/src/codec/v1/timestamp.rs index 0ca4e2a..02472a3 100644 --- a/crates/core/src/codec/v1/timestamp.rs +++ b/crates/core/src/codec/v1/timestamp.rs @@ -1,14 +1,16 @@ //! ** The implementation here is subject to change as this is a read-only version. ** use crate::{ + alloc::{Allocator, Global, vec, vec::Vec}, codec::v1::{ Attestation, FinalizationError, MayHaveInput, PendingAttestation, attestation::RawAttestation, opcode::OpCode, }, - utils::{Hexed, OnceLock}, + utils::Hexed, }; -use alloc::{alloc::Global, vec::Vec}; -use core::{alloc::Allocator, fmt::Debug}; +use allocator_api2::SliceExt; +use core::fmt::Debug; +use std::sync::OnceLock; pub(crate) mod builder; mod decode; @@ -163,6 +165,145 @@ impl Timestamp { pub fn pending_attestations_mut(&mut self) -> PendingAttestationIterMut<'_, A> { PendingAttestationIterMut { stack: vec![self] } } + + /// Retains only the attestations for which the predicate returns `true`, + /// removing all others from this timestamp tree. + /// + /// The predicate receives each [`RawAttestation`] and should return `true` + /// to keep the attestation or `false` to remove it. This is analogous to + /// [`Vec::retain`] but operates on the attestation leaves of the timestamp + /// tree. + /// + /// Returns `Some(count)` where `count` is the number of attestations removed, + /// or `None` if the entire timestamp would be empty after filtering (all + /// attestations were removed). + /// + /// When a FORK node is left with only one remaining branch after filtering, + /// it is collapsed into that branch to maintain the invariant that FORKs + /// have at least two children. + pub fn retain_attestations(&mut self, mut f: F) -> Option + where + F: FnMut(&RawAttestation) -> bool, + { + self.retain_attestations_inner(&mut f) + } + + fn retain_attestations_inner(&mut self, f: &mut F) -> Option + where + F: FnMut(&RawAttestation) -> bool, + { + // Phase 1: recursively filter children and compute result + let (removed_count, should_collapse) = match self { + Timestamp::Attestation(attestation) => { + return if f(attestation) { Some(0) } else { None }; + } + Timestamp::Step(step) if step.op == OpCode::FORK => { + let mut removed = 0usize; + step.next + .retain_mut(|child| match child.retain_attestations_inner(f) { + None => { + removed += 1; + false + } + Some(count) => { + removed += count; + true + } + }); + + if step.next.is_empty() { + return None; + } + + (removed, step.next.len() == 1) + } + Timestamp::Step(step) => { + debug_assert!(step.next.len() == 1, "non-FORK must have exactly one child"); + return step.next[0].retain_attestations_inner(f); + } + }; + + // Phase 2: collapse single-branch FORK + if should_collapse && let Timestamp::Step(step) = self { + let remaining = step.next.pop().unwrap(); + *self = remaining; + } + + Some(removed_count) + } + + /// Retains only the attestations for which the predicate returns `true`, + /// removing all others from this timestamp tree. + /// + /// Unlike [`retain_attestations`](Self::retain_attestations), this variant + /// provides mutable access to each [`RawAttestation`] in the predicate, + /// allowing in-place modification of attestations during filtering. + /// + /// Returns `Some(count)` where `count` is the number of attestations removed, + /// or `None` if the entire timestamp would be empty after filtering. + /// + /// When a FORK node is left with only one remaining branch after filtering, + /// it is collapsed into that branch to maintain the invariant that FORKs + /// have at least two children. + pub fn retain_attestations_mut(&mut self, mut f: F) -> Option + where + F: FnMut(&mut RawAttestation) -> bool, + { + self.retain_attestations_mut_inner(&mut f) + } + + fn retain_attestations_mut_inner(&mut self, f: &mut F) -> Option + where + F: FnMut(&mut RawAttestation) -> bool, + { + let (removed_count, should_collapse) = match self { + Timestamp::Attestation(attestation) => { + return if f(attestation) { Some(0) } else { None }; + } + Timestamp::Step(step) if step.op == OpCode::FORK => { + let mut removed = 0usize; + step.next + .retain_mut(|child| match child.retain_attestations_mut_inner(f) { + None => { + removed += 1; + false + } + Some(count) => { + removed += count; + true + } + }); + + if step.next.is_empty() { + return None; + } + + (removed, step.next.len() == 1) + } + Timestamp::Step(step) => { + debug_assert!(step.next.len() == 1, "non-FORK must have exactly one child"); + return step.next[0].retain_attestations_mut_inner(f); + } + }; + + if should_collapse && let Timestamp::Step(step) = self { + let remaining = step.next.pop().unwrap(); + *self = remaining; + } + + Some(removed_count) + } + + /// Purges all pending attestations from this timestamp tree. + /// + /// This is a convenience wrapper around [`retain_attestations`](Self::retain_attestations) + /// that removes all attestations tagged as pending. + /// + /// Returns `Some(count)` where `count` is the number of pending attestations removed, + /// or `None` if the entire timestamp consists only of pending attestations. + pub fn purge_pending(&mut self) -> Option { + self.retain_attestations(|att| att.tag != PendingAttestation::TAG) + } } impl Timestamp { @@ -186,11 +327,11 @@ impl Timestamp { /// /// Returns an error if the timestamp is already finalized with different input data. pub fn try_finalize(&self, input: &[u8]) -> Result<(), FinalizationError> { - let init_fn = || input.to_vec_in(self.allocator().clone()); + let init_fn = || SliceExt::to_vec_in(input, self.allocator().clone()); match self { Self::Attestation(attestation) => { if let Some(already) = attestation.value.get() { - return if &input != already { + return if input != already { Err(FinalizationError) } else { Ok(()) @@ -200,7 +341,7 @@ impl Timestamp { } Self::Step(step) => { if let Some(already) = step.input.get() { - return if &input != already { + return if input != already { Err(FinalizationError) } else { Ok(()) @@ -248,7 +389,7 @@ impl Timestamp { // if any timestamp is finalized, ensure they are with the same input, // finalize unfinalized timestamps with that input let finalized_input = timestamps.iter().find_map(|ts| ts.input()); - if let Some(ref input) = finalized_input { + if let Some(input) = finalized_input { for ts in timestamps.iter().filter(|ts| !ts.is_finalized()) { ts.try_finalize(input)?; } @@ -355,3 +496,141 @@ impl<'a, A: Allocator> Iterator for PendingAttestationIterMut<'a, A> { None } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::{ + alloc::vec as alloc_vec, + codec::v1::{BitcoinAttestation, PendingAttestation}, + }; + use std::borrow::Cow; + + fn make_pending(uri: &str) -> Timestamp { + Timestamp::builder() + .attest(PendingAttestation { + uri: Cow::Borrowed(uri), + }) + .unwrap() + } + + fn make_bitcoin(height: u32) -> Timestamp { + Timestamp::builder() + .attest(BitcoinAttestation { height }) + .unwrap() + } + + #[test] + fn purge_pending_single_pending() { + let mut ts = make_pending("https://example.com"); + assert!( + ts.purge_pending().is_none(), + "all-pending should return None" + ); + } + + #[test] + fn purge_pending_single_confirmed() { + let mut ts = make_bitcoin(100); + assert_eq!(ts.purge_pending(), Some(0)); + } + + #[test] + fn purge_pending_fork_mixed() { + // FORK with one pending and one confirmed branch + let pending = make_pending("https://example.com"); + let confirmed = make_bitcoin(100); + let mut ts = Timestamp::merge(alloc_vec![pending, confirmed]); + + let result = ts.purge_pending(); + assert_eq!(result, Some(1)); + // After purge, the FORK should be collapsed since only 1 branch remains + assert!( + !matches!(ts, Timestamp::Step(ref s) if s.op == OpCode::FORK), + "FORK with 1 branch should be collapsed" + ); + } + + #[test] + fn purge_pending_fork_all_pending() { + let p1 = make_pending("https://a.example.com"); + let p2 = make_pending("https://b.example.com"); + let mut ts = Timestamp::merge(alloc_vec![p1, p2]); + + assert!(ts.purge_pending().is_none()); + } + + #[test] + fn purge_pending_fork_all_confirmed() { + let c1 = make_bitcoin(100); + let c2 = make_bitcoin(200); + let mut ts = Timestamp::merge(alloc_vec![c1, c2]); + + assert_eq!(ts.purge_pending(), Some(0)); + // FORK should remain since both branches are kept + assert!(matches!(ts, Timestamp::Step(ref s) if s.op == OpCode::FORK)); + } + + #[test] + fn purge_pending_nested_fork() { + // Outer FORK: [inner FORK: [pending, confirmed], confirmed] + let inner_pending = make_pending("https://inner.example.com"); + let inner_confirmed = make_bitcoin(100); + let inner_fork = Timestamp::merge(alloc_vec![inner_pending, inner_confirmed]); + let outer_confirmed = make_bitcoin(200); + let mut ts = Timestamp::merge(alloc_vec![inner_fork, outer_confirmed]); + + let result = ts.purge_pending(); + assert_eq!(result, Some(1)); + // Outer FORK remains (2 branches), inner FORK collapsed + assert!(matches!(ts, Timestamp::Step(ref s) if s.op == OpCode::FORK)); + } + + #[test] + fn retain_attestations_selective() { + // FORK with two different pending attestations and one confirmed + let p1 = make_pending("https://a.example.com"); + let p2 = make_pending("https://b.example.com"); + let confirmed = make_bitcoin(100); + let mut ts = Timestamp::merge(alloc_vec![p1, p2, confirmed]); + + // Retain confirmed + second pending, removing first pending + let result = ts.retain_attestations(|att| { + if att.tag != PendingAttestation::TAG { + return true; + } + let p = PendingAttestation::from_raw(att).unwrap(); + p.uri != "https://a.example.com" + }); + assert_eq!(result, Some(1)); + // FORK should remain since 2 branches are still present (p2 + confirmed) + assert!(matches!(ts, Timestamp::Step(ref s) if s.op == OpCode::FORK)); + } + + #[test] + fn retain_attestations_keep_all() { + let p1 = make_pending("https://a.example.com"); + let confirmed = make_bitcoin(100); + let mut ts = Timestamp::merge(alloc_vec![p1, confirmed]); + + // Retain everything + let result = ts.retain_attestations(|_| true); + assert_eq!(result, Some(0)); + // FORK should remain unchanged + assert!(matches!(ts, Timestamp::Step(ref s) if s.op == OpCode::FORK)); + } + + #[test] + fn retain_attestations_remove_by_type() { + // Test removing confirmed attestations (not just pending) + let pending = make_pending("https://example.com"); + let confirmed = make_bitcoin(100); + let mut ts = Timestamp::merge(alloc_vec![pending, confirmed]); + + // Remove bitcoin attestations, keep pending + let result = ts.retain_attestations(|att| att.tag == PendingAttestation::TAG); + assert_eq!(result, Some(1)); + // FORK collapsed since only 1 branch remains + assert!(!matches!(ts, Timestamp::Step(ref s) if s.op == OpCode::FORK)); + } +} diff --git a/crates/core/src/codec/v1/timestamp/builder.rs b/crates/core/src/codec/v1/timestamp/builder.rs index 9c36254..92d4fb4 100644 --- a/crates/core/src/codec/v1/timestamp/builder.rs +++ b/crates/core/src/codec/v1/timestamp/builder.rs @@ -1,15 +1,16 @@ //! Timestamp Builder use crate::{ + alloc::{Allocator, Global, vec, vec::Vec}, codec::v1::{ Attestation, Timestamp, opcode::{DigestOpExt, OpCode}, timestamp::Step, }, error::EncodeError, - utils::OnceLock, }; -use alloc::alloc::{Allocator, Global}; +use allocator_api2::SliceExt; +use std::sync::OnceLock; use uts_bmt::{NodePosition, SiblingIter}; #[derive(Debug, Clone)] @@ -64,16 +65,17 @@ impl TimestampBuilder { } /// Pushes the steps corresponding to the given Merkle proof to the timestamp. - pub fn merkle_proof(&mut self, mut proof: SiblingIter<'_, D>) -> &mut Self { + pub fn merkle_proof(&mut self, proof: SiblingIter<'_, D>) -> &mut Self { let alloc = self.allocator().clone(); - while let Some((side, sibling_hash)) = proof.next() { + for (side, sibling_hash) in proof { + let sibling_hash = SliceExt::to_vec_in(sibling_hash.as_slice(), alloc.clone()); match side { NodePosition::Left => self - .prepend([uts_bmt::INNER_NODE_PREFIX].to_vec_in(alloc.clone())) - .append(sibling_hash.to_vec_in(alloc.clone())), + .prepend(vec![in alloc.clone(); uts_bmt::INNER_NODE_PREFIX]) + .append(sibling_hash), NodePosition::Right => self - .prepend(sibling_hash.to_vec_in(alloc.clone())) - .prepend([uts_bmt::INNER_NODE_PREFIX].to_vec_in(alloc.clone())), + .prepend(sibling_hash) + .prepend(vec![in alloc.clone(); uts_bmt::INNER_NODE_PREFIX]), } .digest::(); } @@ -96,7 +98,7 @@ impl TimestampBuilder { /// commitment. pub fn commitment(&self, input: impl AsRef<[u8]>) -> Vec { let alloc = self.allocator().clone(); - let mut commitment = input.as_ref().to_vec_in(alloc.clone()); + let mut commitment = SliceExt::to_vec_in(input.as_ref(), alloc.clone()); for step in &self.steps { commitment = step.op.execute_in(&commitment, &step.data, alloc.clone()); } diff --git a/crates/core/src/codec/v1/timestamp/fmt.rs b/crates/core/src/codec/v1/timestamp/fmt.rs index 17c6d6a..c7749d1 100644 --- a/crates/core/src/codec/v1/timestamp/fmt.rs +++ b/crates/core/src/codec/v1/timestamp/fmt.rs @@ -61,7 +61,7 @@ impl Timestamp { } let result = if let Some(value) = step.next.first().and_then(|next| next.input()) { - Some(value.to_vec_in(step.allocator().clone())) + Some(SliceExt::to_vec_in(value, step.allocator().clone())) } else if let Some(input) = input { let result = op.execute_in(input, &step.data, step.allocator().clone()); indent(f, depth, false)?; diff --git a/crates/core/src/error.rs b/crates/core/src/error.rs index e5a4946..1d3f2ab 100644 --- a/crates/core/src/error.rs +++ b/crates/core/src/error.rs @@ -37,7 +37,6 @@ pub enum DecodeError { #[error("unexpected end of file")] UnexpectedEof, /// General I/O error - #[cfg(feature = "std")] #[error("I/O error: {0}")] Io(std::io::Error), } @@ -55,12 +54,10 @@ pub enum EncodeError { #[error("URI too long")] UriTooLong, /// General I/O error - #[cfg(feature = "std")] #[error("I/O error: {0}")] Io(#[from] std::io::Error), } -#[cfg(feature = "std")] impl From for DecodeError { fn from(err: std::io::Error) -> Self { match err.kind() { diff --git a/crates/core/src/lib.rs b/crates/core/src/lib.rs index a5fe5f4..217fbe5 100644 --- a/crates/core/src/lib.rs +++ b/crates/core/src/lib.rs @@ -1,12 +1,44 @@ -#![feature(exact_bitshifts)] -#![feature(allocator_api)] -#![cfg_attr(not(feature = "std"), no_std)] //! # Universal Timestamps Core Library -extern crate alloc; -extern crate core; +// MIT License +// +// Copyright (c) 2025 UTS Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// +// Apache License, Version 2.0 +// +// Copyright (c) 2025 UTS Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. -mod tracing; +extern crate core; #[cfg(test)] pub mod fixtures; @@ -18,3 +50,15 @@ pub mod error; pub mod utils; #[cfg(feature = "verifier")] pub mod verifier; + +/// Re-export the allocator2 API for use. +#[cfg(not(feature = "nightly"))] +pub mod alloc { + pub use allocator_api2::{alloc::*, *}; +} + +#[cfg(feature = "nightly")] +pub mod alloc { + pub use alloc::*; + pub use core::alloc::*; +} diff --git a/crates/core/src/tracing.rs b/crates/core/src/tracing.rs deleted file mode 100644 index 8b99c0b..0000000 --- a/crates/core/src/tracing.rs +++ /dev/null @@ -1,20 +0,0 @@ -#![allow(unused)] - -#[cfg(not(feature = "tracing"))] -macro_rules! noop_macro { - ($($t:tt)*) => {}; -} - -#[cfg(not(feature = "tracing"))] -pub(crate) use noop_macro as debug; -#[cfg(not(feature = "tracing"))] -pub(crate) use noop_macro as error; -#[cfg(not(feature = "tracing"))] -pub(crate) use noop_macro as info; -#[cfg(not(feature = "tracing"))] -pub(crate) use noop_macro as trace; -#[cfg(not(feature = "tracing"))] -pub(crate) use noop_macro as warn; - -#[cfg(feature = "tracing")] -pub use ::tracing::{debug, error, info, trace, warn}; diff --git a/crates/core/src/utils.rs b/crates/core/src/utils.rs index 1b50d53..0ffa830 100644 --- a/crates/core/src/utils.rs +++ b/crates/core/src/utils.rs @@ -1,9 +1,6 @@ mod hex; pub use hex::Hexed; -mod sync; -pub use sync::OnceLock; - mod hash; #[cfg(feature = "io-utils")] pub use hash::HashAsyncFsExt; diff --git a/crates/core/src/utils/sync/race.rs b/crates/core/src/utils/sync/race.rs deleted file mode 100644 index 1ee7567..0000000 --- a/crates/core/src/utils/sync/race.rs +++ /dev/null @@ -1,20 +0,0 @@ -#[derive(Default, Debug, Clone)] -#[repr(transparent)] -pub struct OnceLock(once_cell::race::OnceBox); - -impl OnceLock { - pub const fn new() -> OnceLock { - OnceLock(once_cell::race::OnceBox::new()) - } - - pub fn get(&self) -> Option<&T> { - self.0.get() - } - - pub fn get_or_init(&self, init: F) -> &T - where - F: FnOnce() -> T, - { - self.0.get_or_init(|| alloc::boxed::Box::new(init())) - } -} diff --git a/crates/core/src/utils/sync/std.rs b/crates/core/src/utils/sync/std.rs deleted file mode 100644 index 3078519..0000000 --- a/crates/core/src/utils/sync/std.rs +++ /dev/null @@ -1,20 +0,0 @@ -#[derive(Default, Debug, Clone)] -#[repr(transparent)] -pub struct OnceLock(std::sync::OnceLock); - -impl OnceLock { - pub const fn new() -> OnceLock { - OnceLock(std::sync::OnceLock::new()) - } - - pub fn get(&self) -> Option<&T> { - self.0.get() - } - - pub fn get_or_init(&self, init: F) -> &T - where - F: FnOnce() -> T, - { - self.0.get_or_init(init) - } -} diff --git a/crates/core/src/verifier.rs b/crates/core/src/verifier.rs index 500ad52..d168c52 100644 --- a/crates/core/src/verifier.rs +++ b/crates/core/src/verifier.rs @@ -3,10 +3,15 @@ use crate::{ error::DecodeError, }; -#[cfg(feature = "ethereum-uts-verifier")] -mod ethereum_uts; -#[cfg(feature = "ethereum-uts-verifier")] -pub use ethereum_uts::EthereumUTSVerifier; +#[cfg(feature = "bitcoin-verifier")] +mod bitcoin; +#[cfg(feature = "eas-verifier")] +mod eas; + +#[cfg(feature = "bitcoin-verifier")] +pub use bitcoin::{BitcoinVerifier, BitcoinVerifierError}; +#[cfg(feature = "eas-verifier")] +pub use eas::{EASVerifier, EASVerifierError}; #[derive(Debug, thiserror::Error)] pub enum VerifyError { @@ -23,10 +28,14 @@ pub enum VerifyError { /// An error occurred while decoding the attestation. #[error("error decoding attestation: {0}")] Decode(DecodeError), - /// An error occurred while verifying the ethereum uts attestation. - #[cfg(feature = "ethereum-uts-verifier")] - #[error("error verifying ethereum uts attestation: {0}")] - EthereumUTS(#[from] ethereum_uts::EthereumUTSVerifierError), + /// An error occurred while verifying the eas attestation. + #[cfg(feature = "eas-verifier")] + #[error("error verifying eas attestation: {0}")] + EAS(#[from] EASVerifierError), + /// An error occurred while verifying the bitcoin attestation. + #[cfg(feature = "bitcoin-verifier")] + #[error("error verifying bitcoin attestation: {0}")] + Bitcoin(#[from] BitcoinVerifierError), } pub trait AttestationVerifier

diff --git a/crates/core/src/verifier/bitcoin.rs b/crates/core/src/verifier/bitcoin.rs new file mode 100644 index 0000000..cfd7b3b --- /dev/null +++ b/crates/core/src/verifier/bitcoin.rs @@ -0,0 +1,204 @@ +use crate::{ + codec::v1::BitcoinAttestation, + verifier::{AttestationVerifier, VerifyError}, +}; +use alloy_primitives::{hex, hex::FromHexError}; +use backon::{ExponentialBuilder, Retryable}; +use http_body_util::LengthLimitError; +use reqwest::Client; +use serde::{Deserialize, Serialize, de::DeserializeOwned}; +use serde_json::json; +use tracing::instrument; +use url::Url; + +const RESPONSE_SIZE_LIMIT: usize = 10 * 1024; // 10 KiB + +#[derive(Debug, Clone)] +pub struct BitcoinVerifier { + client: Client, + provider: Url, + retry: ExponentialBuilder, +} + +#[derive(Debug, thiserror::Error)] +pub enum BitcoinVerifierError { + #[error("error making JSON-RPC request: {0}")] + Request(#[from] reqwest::Error), + #[error("error processing JSON-RPC response: {0}")] + Response(Box), + #[error("error parsing JSON-RPC response: {0}")] + Json(#[from] serde_json::Error), + #[error(transparent)] + Rpc(JsonRpcError), + #[error("invalid hex")] + Hex(#[from] FromHexError), + #[error("invalid attestation")] + Invalid, +} + +#[derive(Debug, Deserialize, thiserror::Error)] +#[error("rpc error {code}: {message}")] +pub struct JsonRpcError { + code: i32, + message: String, +} + +#[derive(Debug, Clone, Deserialize)] +pub struct BitcoinHeader { + pub hash: String, + pub merkleroot: String, + pub height: u32, + pub time: u64, +} + +#[derive(Deserialize)] +struct JsonRpcResponse { + result: T, + error: Option, +} + +impl BitcoinVerifier { + pub fn new(provider: Url) -> Self { + Self { + client: Client::new(), + provider, + retry: ExponentialBuilder::default(), + } + } + + pub fn from_parts(client: Client, provider: Url, retry: ExponentialBuilder) -> Self { + Self { + client, + provider, + retry, + } + } + + pub async fn get_blockhash(&self, height: u32) -> Result { + self.req("getblockhash", [height]).await + } + + pub async fn get_block_header( + &self, + hash: &str, + ) -> Result { + self.req("getblockheader", (hash, true)).await + } + + pub async fn verify( + &self, + attestation: &BitcoinAttestation, + value: &[u8], + ) -> Result { + let height = attestation.height; + + let blockhash = self.get_blockhash(height).await?; + + let header = self.get_block_header(&blockhash).await?; + + // Bitcoin reverses the blockhash in RPC responses, so we need to reverse it back to get the correct hash. + let mut hash = hex::decode(&header.merkleroot)?; + hash.reverse(); + if hash != value { + return Err(BitcoinVerifierError::Invalid); + } + + Ok(header) + } + + #[instrument(skip(self, params), level = "trace", err(level = "warn"))] + async fn req( + &self, + method: &str, + params: P, + ) -> Result { + let body = json!({ + "jsonrpc": "1.0", + "id": 1, + "method": method, + "params": params, + }); + let req = self.client.post(self.provider.clone()).json(&body); + + { + move || { + let req = req.try_clone().expect("infallible"); + + async move { + let res = req.send().await?.error_for_status()?; + + let res: http::Response = res.into(); + let body = http_body_util::Limited::new(res.into_body(), RESPONSE_SIZE_LIMIT); + let bytes = http_body_util::BodyExt::collect(body) + .await + .map_err(BitcoinVerifierError::Response)? + .to_bytes(); + + let response: JsonRpcResponse = serde_json::from_slice(&bytes)?; + if let Some(error) = response.error { + Err(BitcoinVerifierError::Rpc(error)) + } else { + Ok(response.result) + } + } + } + } + .retry(self.retry) + .when(|e| { + use BitcoinVerifierError::*; + match e { + Request(_) => true, + Response(e) => e.downcast_ref::().is_none(), + _ => false, + } + }) + .await + } +} + +impl AttestationVerifier for BitcoinVerifier { + type Output = BitcoinHeader; + + async fn verify( + &self, + attestation: &BitcoinAttestation, + value: &[u8], + ) -> Result { + Ok(self.verify(attestation, value).await?) + } +} + +impl BitcoinVerifierError { + /// The error indicates this attestation is invalid and cannot be verified. + #[inline] + pub fn is_fatal(&self) -> bool { + matches!(self, BitcoinVerifierError::Invalid) + } + + /// The error indicates this attestation may be valid but cannot be verified at the moment. + #[inline] + pub fn should_retry(&self) -> bool { + !matches!(self, BitcoinVerifierError::Invalid) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[ignore] + #[tokio::test] + async fn test_get_blockhash() { + let verifier = + BitcoinVerifier::new(Url::parse("https://bitcoin-rpc.publicnode.com").unwrap()); + let hash = verifier.get_blockhash(0).await.unwrap(); + assert_eq!( + hash, + "000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f" + ); + let header = verifier.get_block_header(&hash).await.unwrap(); + assert_eq!(header.hash, hash); + assert_eq!(header.height, 0); + assert_eq!(header.time, 1231006505); + } +} diff --git a/crates/core/src/verifier/eas.rs b/crates/core/src/verifier/eas.rs new file mode 100644 index 0000000..bb17051 --- /dev/null +++ b/crates/core/src/verifier/eas.rs @@ -0,0 +1,124 @@ +use super::{AttestationVerifier, VerifyError}; +use crate::codec::v1::{EASAttestation, EASTimestamped}; +use alloy_primitives::{Address, B256}; +use alloy_provider::Provider; +use alloy_sol_types::SolValue; +use uts_contracts::eas::{self, EAS}; + +#[derive(Debug, Clone)] +pub struct EASVerifier { + eas: EAS

, +} + +#[derive(Debug, thiserror::Error)] +pub enum EASVerifierError { + #[error("invalid value length for EAS attestation")] + InvalidLength, + #[error("invalid attestation data")] + InvalidData(#[from] alloy_sol_types::Error), + #[error("unexpected schema used for attestation")] + InvalidSchema, + #[error("attestation cannot be revocable")] + RevocableAttestation, + #[error("attested hash is not equal to the expected hash")] + Mismatched { expected: B256, actual: B256 }, + #[error("not found")] + NotFound, + #[error(transparent)] + Rpc(#[from] alloy_contract::Error), +} + +impl EASVerifier

{ + pub const fn new(address: Address, provider: P) -> Self { + let eas = EAS::new(address, provider); + Self { eas } + } +} + +impl AttestationVerifier for EASVerifier

{ + type Output = eas::Attestation; + + async fn verify( + &self, + attestation: &EASAttestation, + value: &[u8], + ) -> Result { + self.verify_attestation(attestation, value) + .await + .map_err(VerifyError::EAS) + } +} + +impl AttestationVerifier for EASVerifier

{ + type Output = u64; + + async fn verify( + &self, + attestation: &EASTimestamped, + value: &[u8], + ) -> Result { + self.verify_timestamped(attestation, value) + .await + .map_err(VerifyError::EAS) + } +} + +impl EASVerifier

{ + pub async fn verify_attestation( + &self, + attestation: &EASAttestation, + value: &[u8], + ) -> Result { + let hash = B256::try_from(value).map_err(|_| EASVerifierError::InvalidLength)?; + + let attestation = self.eas.getAttestation(attestation.uid).call().await?; + + if attestation.schema != eas::SCHEMA_ID { + return Err(EASVerifierError::InvalidSchema); + } + if attestation.revocable { + return Err(EASVerifierError::RevocableAttestation); + } + + let attested_hash = B256::abi_decode(&attestation.data)?; + + if attested_hash != hash { + return Err(EASVerifierError::Mismatched { + expected: hash, + actual: attested_hash, + }); + } + + Ok(attestation) + } + + pub async fn verify_timestamped( + &self, + _attestation: &EASTimestamped, + value: &[u8], + ) -> Result { + let hash = B256::try_from(value).map_err(|_| EASVerifierError::InvalidLength)?; + + let timestamp = self.eas.getTimestamp(hash).call().await?; + + if timestamp == 0 { + return Err(EASVerifierError::NotFound); + } + + Ok(timestamp) + } +} + +impl EASVerifierError { + /// The error indicates this attestation is invalid and cannot be verified. + #[inline] + pub fn is_fatal(&self) -> bool { + !matches!(self, EASVerifierError::Rpc(_)) + } + + /// The error indicates this attestation may be valid but cannot be verified at the moment. + #[inline] + pub fn should_retry(&self) -> bool { + matches!(self, EASVerifierError::Rpc(_)) + } +} diff --git a/crates/core/src/verifier/ethereum_uts.rs b/crates/core/src/verifier/ethereum_uts.rs deleted file mode 100644 index ec0d6ca..0000000 --- a/crates/core/src/verifier/ethereum_uts.rs +++ /dev/null @@ -1,145 +0,0 @@ -use super::{AttestationVerifier, VerifyError}; -use crate::codec::v1::EthereumUTSAttestation; -use alloy_primitives::{Address, ChainId, TxHash}; -use alloy_provider::{Provider, transport::TransportError}; -use alloy_rpc_types_eth::{Filter, Log}; -use alloy_sol_types::SolEvent; -use digest::OutputSizeUser; -use sha3::Keccak256; -use uts_contracts::uts::Attested; - -#[derive(Debug, Clone)] -pub struct EthereumUTSVerifier { - provider: P, - chain_id: ChainId, -} - -#[derive(Debug, thiserror::Error)] -pub enum EthereumUTSVerifierError { - #[error("invalid value length for Ethereum UTS attestation")] - InvalidLength, - #[error("chain ID mismatch")] - ChainIdMismatch, - #[error("root not found in attested logs")] - NotFound, - #[error("contract address mismatch, expected {expected}, found {found}")] - ContractMismatch { expected: Address, found: Address }, - #[error("transaction hash mismatch, expected {expected}, found {found}")] - TransactionMismatch { expected: TxHash, found: TxHash }, - #[error(transparent)] - Rpc(#[from] TransportError), -} - -impl EthereumUTSVerifier

{ - pub async fn new(provider: P) -> Result { - let chain_id = provider.get_chain_id().await?; - Ok(Self { provider, chain_id }) - } -} - -impl AttestationVerifier for EthereumUTSVerifier

{ - type Output = Log; - - async fn verify( - &self, - attestation: &EthereumUTSAttestation, - value: &[u8], - ) -> Result { - Ok(self.verify_attestation(attestation, value).await?) - } -} - -impl EthereumUTSVerifier

{ - async fn verify_attestation( - &self, - attestation: &EthereumUTSAttestation, - value: &[u8], - ) -> Result, EthereumUTSVerifierError> { - if value.len() != Keccak256::output_size() { - return Err(EthereumUTSVerifierError::InvalidLength); - } - if attestation.chain.id() != self.chain_id { - return Err(EthereumUTSVerifierError::ChainIdMismatch); - } - - let filter = Filter::new() - .from_block(attestation.height) - .to_block(attestation.height) - .event_signature(Attested::SIGNATURE_HASH); - let logs = self.provider.get_logs(&filter).await?; - - let Some(log) = logs - .into_iter() - .filter_map(|log| { - Attested::decode_log(&log.inner) - .map(|inner| Log { - inner, - block_hash: log.block_hash, - block_number: log.block_number, - block_timestamp: log.block_timestamp, - transaction_hash: log.transaction_hash, - transaction_index: log.transaction_index, - log_index: log.log_index, - removed: log.removed, - }) - .ok() - }) - .find(|log| log.inner.data.root == value) - else { - return Err(EthereumUTSVerifierError::NotFound); - }; - - // perform additional checks if available - if let Some(contract) = attestation.metadata.contract() { - if log.inner.address != contract { - return Err(EthereumUTSVerifierError::ContractMismatch { - expected: contract, - found: log.inner.address, - }); - } - if let Some(expect_tx) = attestation.metadata.tx() - && let Some(found_tx) = log.transaction_hash - && expect_tx != found_tx - { - return Err(EthereumUTSVerifierError::TransactionMismatch { - expected: expect_tx, - found: found_tx, - }); - } - } - Ok(log) - } -} - -impl EthereumUTSVerifierError { - /// The error indicates this attestation is invalid and cannot be verified. - #[inline] - pub fn is_fatal(&self) -> bool { - matches!( - self, - EthereumUTSVerifierError::InvalidLength | EthereumUTSVerifierError::NotFound - ) - } - - /// The error indicates this attestation is valid but not attested by the expected contract or transaction. - #[inline] - pub fn is_mismatch(&self) -> bool { - matches!( - self, - EthereumUTSVerifierError::ContractMismatch { .. } - | EthereumUTSVerifierError::TransactionMismatch { .. } - ) - } - - /// The error indicates this attestation may be valid but cannot be verified at the moment. - #[inline] - pub fn should_retry(&self) -> bool { - matches!(self, EthereumUTSVerifierError::Rpc(_)) - } - - /// The error indicates this attestation may be valid but the provider is not suitable for verifying it. - #[inline] - pub fn is_wrong_provider(&self) -> bool { - matches!(self, EthereumUTSVerifierError::ChainIdMismatch) - } -} diff --git a/crates/journal/Cargo.toml b/crates/journal/Cargo.toml index 11ae72b..1652cce 100644 --- a/crates/journal/Cargo.toml +++ b/crates/journal/Cargo.toml @@ -1,15 +1,22 @@ [package] authors.workspace = true -description = "High performance append-only journal for UTS" +categories = ["data-structures", "database"] +description = "RocksDB-backed append-only journal for UTS" edition.workspace = true homepage.workspace = true -keywords.workspace = true -license.workspace = true +include = [ + "src/**/*", + "Cargo.toml", + "LICENSE", +] +keywords = ["uts", "journal", "rocksdb"] +license = "AGPL-3.0-or-later" name = "uts-journal" repository.workspace = true -version.workspace = true +version = "0.1.1" [dependencies] +rocksdb = { workspace = true } thiserror = { workspace = true } tracing = { workspace = true } diff --git a/crates/journal/LICENSE-AGPL b/crates/journal/LICENSE-AGPL new file mode 120000 index 0000000..5f5cf25 --- /dev/null +++ b/crates/journal/LICENSE-AGPL @@ -0,0 +1 @@ +../../LICENSE-AGPL \ No newline at end of file diff --git a/crates/journal/README.md b/crates/journal/README.md deleted file mode 100644 index 39aecea..0000000 --- a/crates/journal/README.md +++ /dev/null @@ -1,33 +0,0 @@ -# uts-journal - -**High-performance, append-only journal designed for the Universal Timestamps (UTS) protocol.** - -`uts-journal` is an embedded, lock-free, ring-buffer-based Write-Ahead Log (WAL) implemented in Rust. -It is designed to handle extremely high throughput (target: 1M+ TPS) with sub-millisecond durability guarantees, -specifically optimized for the low-latency requirements of timestamping services. - -## Architecture & Design Rationale - -### Why not Kafka? - -While distributed message queues are the standard for microservices decoupling, `uts-journal` was built to satisfy a -specific set of constraints where generic solutions fall short: - -| Feature | Distributed MQ (e.g., Kafka) | uts-journal | -|------------------------|--------------------------------|--------------------------------------| -| **Topology** | External Service (Networked) | Embedded Library (In-Process) | -| **Durability Latency** | Network RTT + Disk IO (~2-5ms) | Disk IO only (<1ms possible) | -| **Throughput Control** | Limited by Network Bandwidth | Limited by PCIe/Memory Bandwidth | -| **Consistency** | Eventual / ISync Replicas | Strong Local Consistency | -| **Resource Usage** | Heavy (JVM/Broker overhead) | Minimal (Zero-copy, Zero-allocation) | - -### The "Request-Persist-Return" Loop - -UTS requires a synchronous acknowledgement model: **HTTP POST → Sequence → Persist → Return**. -To achieve 1M TPS under this constraint: - -1. **Group Commit:** `uts-journal` allows thousands of concurrent write requests to queue in the ring buffer. -2. **Batched IO:** A dedicated WAL worker thread detects pending writes and flushes them to stable storage in minimal syscalls. -3. **Wake-on-Persist:** Once the persist boundary advances, the worker efficiently wakes only the relevant `Waker`s associated with the committed slots. - -This architecture converts random IOPS into sequential throughput, allowing the system to handle massive concurrency on a single node. diff --git a/crates/journal/src/checkpoint.rs b/crates/journal/src/checkpoint.rs deleted file mode 100644 index b5108de..0000000 --- a/crates/journal/src/checkpoint.rs +++ /dev/null @@ -1,181 +0,0 @@ -use std::{ - fs, - fs::File, - io, - io::Read, - path::{Path, PathBuf}, - sync::{Mutex, atomic::AtomicU64}, - time::{Duration, Instant}, -}; - -/// Configuration for checkpointing mechanism. -#[derive(Debug, Clone)] -pub struct CheckpointConfig { - /// Path to the checkpoint file where the last persisted index is stored. This file will be - /// created if it does not exist, and updated atomically when a new checkpoint is flushed to - /// disk. - pub path: PathBuf, - /// Flush interval for checkpointing, used to determine when to flush the persisted checkpoint - /// to disk. - pub min_interval: Duration, - /// Flush threshold for checkpointing, used to determine when to flush the persisted checkpoint - /// to disk based on the number of new entries since the last flush. - pub min_advance: u64, - /// Suffix for temporary checkpoint file when performing checkpointing. The checkpoint will be - /// atomically renamed to the final checkpoint file after flush. - pub temp_suffix: &'static str, -} - -impl Default for CheckpointConfig { - fn default() -> Self { - Self { - path: PathBuf::from("checkpoint.meta"), - min_interval: Duration::from_secs(1), - min_advance: 128, - temp_suffix: ".tmp", - } - } -} - -/// Checkpointing for tracking `consumed_index`. -#[derive(Debug)] -pub struct Checkpoint { - config: CheckpointConfig, - current: AtomicU64, - inner: Mutex, -} - -#[derive(Debug)] -struct CheckpointInner { - temp_path: PathBuf, - - /// The index of the last persisted checkpoint. This is updated when a new checkpoint is - /// flushed to disk. - persisted_index: u64, - last_flush_time: Instant, -} - -impl Checkpoint { - /// Creates a new checkpoint instance with the given configuration. This will attempt to recover - /// the last persisted checkpoint index from disk, and initialize the internal state accordingly. - #[instrument(skip_all, err)] - pub fn new(config: CheckpointConfig) -> io::Result { - let parent = config.path.parent().ok_or(io::Error::new( - io::ErrorKind::NotFound, - "parent directory does not exist", - ))?; - fs::create_dir_all(parent)?; - - let mut inner = CheckpointInner { - temp_path: config.path.with_added_extension(config.temp_suffix), - - persisted_index: 0, - last_flush_time: Instant::now(), - }; - let recovered = inner.recover(&config)?; - - Ok(Self { - config, - current: AtomicU64::new(recovered), - inner: Mutex::new(inner), - }) - } - - /// Returns the last persisted checkpoint index, which is updated when a new checkpoint is - /// flushed to disk. This requires acquiring the lock on the inner state, and may lag behind - /// the current index until the next flush to disk. - #[instrument(skip(self), ret)] - pub fn persisted_index(&self) -> u64 { - let inner = self.inner.lock().unwrap(); - inner.persisted_index - } - - /// Returns the current checkpoint index, which may be ahead of the last persisted index. - /// This is updated atomically when `update` is called, and can be read without acquiring the - /// lock on the inner state. - /// - /// The persisted index may lag behind the current index until the next flush to disk. - #[instrument(skip(self), ret)] - pub fn current_index(&self) -> u64 { - self.current.load(std::sync::atomic::Ordering::Acquire) - } - - /// Updates the current checkpoint index. This will trigger a flush to disk if: - /// - the new index has advanced by at least `min_advance` since the last persisted index - /// - or, the time since the last flush has exceeded `min_interval`. - #[instrument(skip(self), err)] - pub fn update(&self, new_index: u64) -> io::Result<()> { - let mut inner = self.inner.lock().unwrap(); - self.current - .store(new_index, std::sync::atomic::Ordering::Release); - inner.update(new_index, &self.config, false) - } - - /// Flush the current checkpoint to disk immediately, regardless of the configured flush - /// interval and flush threshold. - #[instrument(skip(self), err)] - pub fn flush(&self) -> io::Result<()> { - let mut inner = self.inner.lock().unwrap(); - let new_index = self.current.load(std::sync::atomic::Ordering::Acquire); - inner.update(new_index, &self.config, true) - } -} - -impl CheckpointInner { - #[instrument(skip(self), err)] - fn recover(&mut self, config: &CheckpointConfig) -> io::Result { - // Try to recover from the temp checkpoint file first - if let Ok(index) = recover_from_disk(&self.temp_path) { - self.persisted_index = index; - fs::rename(&self.temp_path, &config.path)?; - } else { - match recover_from_disk(&config.path) { - Ok(index) => self.persisted_index = index, - Err(e) if e.kind() == io::ErrorKind::NotFound => self.persisted_index = 0, - Err(e) => return Err(e), - } - } - Ok(self.persisted_index) - } - - fn update( - &mut self, - new_index: u64, - config: &CheckpointConfig, - forced: bool, - ) -> io::Result<()> { - if new_index <= self.persisted_index { - warn!( - "New checkpoint index {} is not greater than persisted index {}, skipping update", - new_index, self.persisted_index - ); - return Ok(()); - } - - let now = Instant::now(); - let should_flush = new_index - self.persisted_index >= config.min_advance; - let timeouts = now.duration_since(self.last_flush_time) >= config.min_interval; - if forced || should_flush || timeouts { - fs::write(&self.temp_path, &new_index.to_le_bytes())?; - fs::rename(&self.temp_path, &config.path)?; - self.persisted_index = new_index; - self.last_flush_time = now; - } - Ok(()) - } -} - -fn recover_from_disk(path: &Path) -> io::Result { - let mut file = File::open(&path)?; - let metadata = file.metadata()?; - if metadata.len() != 8 { - return Err(io::Error::new( - io::ErrorKind::InvalidData, - "invalid checkpoint file", - )); - } - let mut buf = [0u8; 8]; - file.read_exact(&mut buf)?; - let index = u64::from_le_bytes(buf); - Ok(index) -} diff --git a/crates/journal/src/error.rs b/crates/journal/src/error.rs deleted file mode 100644 index 3355ad5..0000000 --- a/crates/journal/src/error.rs +++ /dev/null @@ -1,11 +0,0 @@ -/// Error indicating that the journal buffer is not available now. -#[derive(Debug, thiserror::Error)] -pub enum JournalUnavailable { - /// The journal is shutting down, no new entries can be accepted. - #[error("journal is shutting down")] - Shutdown, - /// The journal buffer is full, new entries cannot be accepted until some entries are consumed - /// and the buffer has space. - #[error("journal buffer is full")] - Full, -} diff --git a/crates/journal/src/helper.rs b/crates/journal/src/helper.rs new file mode 100644 index 0000000..3a506dc --- /dev/null +++ b/crates/journal/src/helper.rs @@ -0,0 +1,21 @@ +use crate::{Error, JournalInner}; + +pub(crate) trait FatalErrorExt { + fn stop_if_error(self, journal: &JournalInner) -> Result; +} + +impl FatalErrorExt for Result { + fn stop_if_error(self, journal: &JournalInner) -> Result { + match self { + Ok(val) => Ok(val), + Err(e) => { + error!("RocksDB error: {e}"); + journal + .fatal_error + .store(true, std::sync::atomic::Ordering::Release); + journal.notify_consumer(); + Err(Error::Fatal) + } + } + } +} diff --git a/crates/journal/src/lib.rs b/crates/journal/src/lib.rs index 34c28b3..560a174 100644 --- a/crates/journal/src/lib.rs +++ b/crates/journal/src/lib.rs @@ -1,156 +1,160 @@ -//! Journal implementation for UTS +//! RocksDB-backed journal implementation for UTS +//! +//! This crate provides the same functionality as `uts-journal` but uses RocksDB +//! for persistence instead of a custom WAL, providing better reliability and +//! crash recovery guarantees. + +// Copyright (C) 2026 UTS Contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + #[macro_use] extern crate tracing; -use crate::{ - checkpoint::{Checkpoint, CheckpointConfig}, - error::JournalUnavailable, - reader::JournalReader, - wal::Wal, -}; +use crate::{helper::FatalErrorExt, reader::JournalReader}; +use rocksdb::{ColumnFamily, DB, Options, WriteBatch}; use std::{ - cell::UnsafeCell, - fmt, io, - ops::Deref, + fmt, path::PathBuf, - pin::Pin, sync::{ Arc, Mutex, atomic::{AtomicBool, AtomicU64, Ordering}, }, - task::{Poll, Waker}, + task::Waker, }; -/// Checkpointing -pub mod checkpoint; -/// Error types. -pub mod error; /// Journal reader. pub mod reader; -/// Write-Ahead Log backend. -pub mod wal; + +mod helper; +/// Error indicating that the journal buffer is not available now. +#[derive(Debug, thiserror::Error)] +pub enum Error { + /// The journal is in a fatal state, caller should stop using it and drop it as soon as possible. + #[error("fatal error happened")] + Fatal, + /// The journal buffer is full, new entries cannot be accepted until some entries are consumed + /// and the buffer has space. + #[error("journal buffer is full")] + Full, +} + +const CF_ENTRIES: &str = "entries"; +const CF_META: &str = "meta"; /// Configuration for the journal. #[derive(Debug, Clone)] pub struct JournalConfig { - /// Configuration for the consumer checkpoint, which tracks the `consumed_index` of the journal. - pub consumer_checkpoint: CheckpointConfig, - /// Directory for the write-ahead log (WAL) backend, which persists committed entries to disk - /// for durability and crash recovery, allowing the journal to recover from crashes without data - /// loss. - pub wal_dir: PathBuf, + /// Directory for the RocksDB database that stores journal entries and metadata. + pub db_path: PathBuf, } impl Default for JournalConfig { fn default() -> Self { Self { - consumer_checkpoint: CheckpointConfig::default(), - wal_dir: PathBuf::from("wal"), + db_path: PathBuf::from("journal_db"), } } } -/// An `At-Least-Once` journal for storing fixed-size entries in a ring buffer. +/// An `At-Least-Once` journal for storing fixed-size entries, with +/// RocksDB-backed persistence. /// -/// All index here are monotonic u64, wrapping around on overflow. +/// All indices here are monotonic u64, wrapping around on overflow. /// -/// Following invariants are maintained: -/// `consumed_index` <= `persisted_index` <= `filled_index` <= `write_index`. +/// Invariant: `consumed_index` <= `write_index`. +/// +/// Writes go directly to RocksDB (synchronous and durable), so there is no +/// separate "persisted" boundary — `write_index` **is** the persisted boundary. #[derive(Clone)] -pub struct Journal { - inner: Arc>, - /// Wal backend for recovery. - wal: Wal<{ ENTRY_SIZE }>, +pub struct Journal { + inner: Arc, } -pub(crate) struct JournalInner { - /// The ring buffer storing the entries. - /// The capacity of the ring buffer, **MUST** be power of two. - buffer: Box<[UnsafeCell<[u8; ENTRY_SIZE]>]>, - /// The co-ring buffer storing the wakers. - /// The capacity of the ring buffer, **MUST** be power of two. - waker_buffer: Box<[WakerEntry]>, - /// Mask for indexing into the ring buffer. - index_mask: u64, - /// Next Write Position, aka: - /// - Total entries reserved count. - /// - Position to write the next entry to. +pub(crate) struct JournalInner { + /// The RocksDB instance storing entries and metadata. + db: DB, + /// Maximum number of in-flight (written but not yet consumed) entries. + capacity: u64, + /// Next write position – also the durable frontier because every commit + /// is a synchronous RocksDB write. write_index: AtomicU64, - /// Filled Boundary, aka: - /// - Total entries that have been fully written to the ring buffer. - /// - Advanced in order after each writer finishes copying data into its reserved slot. - /// - The WAL worker uses this (not `write_index`) to determine how far it can safely read. - filled_index: AtomicU64, - /// WAL Committed Boundary, aka.: - /// - Total committed entries count. - /// - Position has not yet been persisted to durable storage. - persisted_index: AtomicU64, - /// Free Boundary, aka.: - /// - Total consumed entries count. - /// - Position that has not yet been consumed by readers. - consumed_checkpoint: Checkpoint, - /// Whether a reader has taken ownership of this journal. + /// Last consumed index, updated by the reader's `commit()`. + consumed_index: AtomicU64, + /// Serializes the write path so entries are numbered consecutively. + write_lock: Mutex<()>, + /// Whether a reader has been acquired. reader_taken: AtomicBool, - /// Waker for the consumer to notify new persisted entries. + /// Waker for the consumer waiting for new entries. consumer_wait: Mutex>, - /// Shutdown flag - shutdown: AtomicBool, + /// Whether the journal is in a fatal error state. If true, all operations will fail and the + /// journal should be dropped. + fatal_error: AtomicBool, } -unsafe impl Sync for JournalInner {} -unsafe impl Send for JournalInner {} - -impl fmt::Debug for Journal { +impl fmt::Debug for Journal { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("Journal").finish() } } -impl Journal { - /// Create a new journal with the specified capacity. - /// - /// The capacity will be rounded up to the next power of two. - pub fn with_capacity(capacity: usize) -> io::Result { +impl Journal { + /// Create a new journal with the specified capacity and default configuration. + pub fn with_capacity(capacity: usize) -> Result { Self::with_capacity_and_config(capacity, JournalConfig::default()) } - /// Create a new journal with the specified capacity. - /// - /// The capacity will be rounded up to the next power of two. - pub fn with_capacity_and_config(capacity: usize, config: JournalConfig) -> io::Result { - let capacity = capacity.next_power_of_two(); - let index_mask = capacity as u64 - 1; + /// Create a new journal with the specified capacity and configuration. + pub fn with_capacity_and_config(capacity: usize, config: JournalConfig) -> Result { + let capacity = capacity as u64; - let mut buffer = Vec::with_capacity(capacity); - buffer.resize_with(capacity, || UnsafeCell::new([0u8; ENTRY_SIZE])); - let buffer = buffer.into_boxed_slice(); + let mut global_options = Options::default(); + global_options.create_if_missing(true); + global_options.create_missing_column_families(true); - let mut waker_buffer = Vec::with_capacity(capacity); - waker_buffer.resize_with(capacity, Default::default); - let waker_buffer = waker_buffer.into_boxed_slice(); + let db = match DB::open_cf(&global_options, &config.db_path, [CF_ENTRIES, CF_META]) { + Ok(db) => db, + Err(e) => { + error!("Failed to open RocksDB at {:?}: {e}", config.db_path); + return Err(Error::Fatal); + } + }; let inner = Arc::new(JournalInner { - buffer, - waker_buffer, - index_mask, + db, + capacity, write_index: AtomicU64::new(0), - filled_index: AtomicU64::new(0), - persisted_index: AtomicU64::new(0), - consumed_checkpoint: Checkpoint::new(config.consumer_checkpoint)?, + consumed_index: AtomicU64::new(0), + write_lock: Mutex::new(()), reader_taken: AtomicBool::new(false), consumer_wait: Mutex::new(None), - shutdown: AtomicBool::new(false), + fatal_error: AtomicBool::new(false), }); - let wal = Wal::new(config.wal_dir, inner.clone())?; + // Recover state + let write_index = inner.read_write_index_from_db()?; + let consumed_index = inner.read_consumed_index_from_db()?; + if consumed_index > write_index { + error!("Consumed index {consumed_index} is greater than write index {write_index}"); + return Err(Error::Fatal); + } + info!("Journal recovered: write_index={write_index}, consumed_index={consumed_index}"); - Ok(Self { inner, wal }) - } + inner.set_write_index(write_index); + inner.set_consumed_index(consumed_index); - /// Get the capacity of the journal. - #[inline] - fn capacity(&self) -> usize { - self.inner.capacity() + Ok(Self { inner }) } /// Acquires a reader for this journal. @@ -158,216 +162,178 @@ impl Journal { /// # Panics /// /// Panics if a reader is already taken. - pub fn reader(&self) -> JournalReader { + pub fn reader(&self) -> JournalReader { self.try_reader().expect("Journal reader already taken") } /// Try acquires a reader for this journal. /// /// If a reader is already taken, returns None. - pub fn try_reader(&self) -> Option> { + pub fn try_reader(&self) -> Option { if self.inner.reader_taken.swap(true, Ordering::AcqRel) { return None; } - Some(JournalReader::new(self.inner.clone())) } /// Commit a new entry to the journal. /// + /// The entry is written to RocksDB synchronously. + /// /// # Panics /// - /// Panics if: - /// - the journal is full. - /// - the journal is shut down. - pub fn commit(&self, data: &[u8; ENTRY_SIZE]) -> CommitFuture<'_, ENTRY_SIZE> { - self.try_commit(data).expect("Journal buffer is full") + /// Panics if the journal is full or has encountered a fatal error. + pub fn commit(&self, data: &[u8]) { + self.try_commit(data).expect("Journal is unavailable") } /// Try commit a new entry to the journal. /// - /// Returns a future that resolves when the entry has been safely persisted. - /// Returns `BufferFull` error if the journal is full. - pub fn try_commit( - &self, - data: &[u8; ENTRY_SIZE], - ) -> Result, JournalUnavailable> { - if self.inner.shutdown.load(Ordering::Acquire) { - return Err(JournalUnavailable::Shutdown); + /// The entry is written to RocksDB synchronously. + pub fn try_commit(&self, data: &[u8]) -> Result<(), Error> { + if self.inner.fatal_error.load(Ordering::Acquire) { + return Err(Error::Fatal); } - let mut current_written = self.inner.write_index.load(Ordering::Relaxed); - loop { - // 1. Check if there is space in the buffer. - let consumed = self.inner.consumed_checkpoint.current_index(); - if current_written.wrapping_sub(consumed) >= self.capacity() as u64 { - return Err(JournalUnavailable::Full); - } + // Serialize writes so indices are strictly consecutive. + let _guard = self.inner.write_lock.lock().unwrap(); - // 2. Try to reserve a slot. - match self.inner.write_index.compare_exchange_weak( - current_written, - current_written.wrapping_add(1), - Ordering::AcqRel, - Ordering::Relaxed, - ) { - Ok(_) => break, - Err(actual) => current_written = actual, - } - } + let write_idx = self.inner.write_index(); + let consumed = self.inner.consumed_index(); - // 3. Write the data to the slot. - let slot = unsafe { &mut *self.data_slot_ptr(current_written) }; - slot.copy_from_slice(data); - - // 4. Publish the filled slot. - // Spin-wait until all prior slots are filled, then advance `filled_index`. - // The Release ordering ensures the slot write above is visible to the WAL worker - // before it reads `filled_index`. - while self - .inner - .filled_index - .compare_exchange_weak( - current_written, - current_written.wrapping_add(1), - Ordering::Release, - Ordering::Relaxed, - ) - .is_err() - { - std::hint::spin_loop(); + if write_idx.wrapping_sub(consumed) >= self.inner.capacity { + return Err(Error::Full); } - // 5. Notify WAL worker if needed. - let committed = self.inner.persisted_index.load(Ordering::Relaxed); - // Explain: If there is no pending committed entry before ours, - // the WAL worker may be sleeping, so we need to wake it up. - if current_written == committed { - // Notify the WAL worker thread to persist new entries. - self.wal.unpark(); - } + let cf_entries = self.inner.cf_entries(); + // Write entry + updated write_index atomically via WriteBatch. + let new_write_idx = write_idx.wrapping_add(1); + let mut batch = WriteBatch::default(); + batch.put_cf(cf_entries, write_idx.to_be_bytes(), data); + self.inner + .write_write_index_batched(&mut batch, new_write_idx); + self.inner.db.write(batch).stop_if_error(&self.inner)?; - Ok(CommitFuture { - journal: self, - slot: current_written, - active_waker: None, - }) - } + self.inner.set_write_index(new_write_idx); - /// Shut down the journal, flushing all checkpoints and shutting down the WAL. - pub fn shutdown(&self) -> io::Result<()> { - self.inner.shutdown.store(true, Ordering::SeqCst); + // drop write_lock before notifying consumer + drop(_guard); + + // Notify consumer if it is waiting for entries. + self.inner.notify_consumer(); - self.inner.consumed_checkpoint.flush()?; - self.wal.shutdown(); Ok(()) } - /// Get a mut ptr to the slot at the given index. + /// Get the current consumed index. #[inline] - fn data_slot_ptr(&self, index: u64) -> *mut [u8; ENTRY_SIZE] { - self.inner.data_slot_ptr(index) + pub fn consumed_index(&self) -> u64 { + self.inner.consumed_index() } - /// Get a ref to the waker entry at the given index. + /// Get the current write index. #[inline] - fn waker_slot(&self, index: u64) -> &WakerEntry { - self.inner.waker_slot(index) + pub fn write_index(&self) -> u64 { + self.inner.write_index() } } -impl JournalInner { - /// Get the capacity of the journal. - #[inline] - fn capacity(&self) -> usize { - self.buffer.len() +impl JournalInner { + const META_WRITE_INDEX_KEY: &[u8] = &[0x00]; + const META_CONSUMED_INDEX_KEY: &[u8] = &[0x01]; + + /// Wake the consumer waker if the write index has reached its target. + fn notify_consumer(&self) { + let mut guard = self.consumer_wait.lock().unwrap(); + if let Some(wait) = guard.as_ref() + && (self.write_index() >= wait.target_index || self.fatal_error.load(Ordering::Acquire)) + { + guard.take().unwrap().waker.wake(); + } } - /// Get a mut ptr to the slot at the given index. #[inline] - const fn data_slot_ptr(&self, index: u64) -> *mut [u8; ENTRY_SIZE] { - let slot_idx = index & self.index_mask; - self.buffer[slot_idx as usize].get() + pub(crate) fn consumed_index(&self) -> u64 { + self.consumed_index.load(Ordering::Acquire) } - /// Get a ref to the waker entry at the given index. #[inline] - const fn waker_slot(&self, index: u64) -> &WakerEntry { - let slot_idx = index & self.index_mask; - &self.waker_buffer[slot_idx as usize] + pub(crate) fn set_consumed_index(&self, idx: u64) { + self.consumed_index.store(idx, Ordering::Release); } -} - -/// Future returned by `Journal::commit` representing the commit operation. -/// The future resolves when the entry has been safely persisted. -#[derive(Debug)] -pub struct CommitFuture<'a, const ENTRY_SIZE: usize> { - journal: &'a Journal, - slot: u64, - /// Whether the waker has been registered. - active_waker: Option, -} -impl Future for CommitFuture<'_, ENTRY_SIZE> { - type Output = (); + #[inline] + pub(crate) fn write_index(&self) -> u64 { + self.write_index.load(Ordering::Acquire) + } - fn poll(mut self: Pin<&mut Self>, cx: &mut std::task::Context<'_>) -> Poll { - if self.journal.inner.persisted_index.load(Ordering::Acquire) > self.slot { - return Poll::Ready(()); - } + #[inline] + pub(crate) fn set_write_index(&self, idx: u64) { + self.write_index.store(idx, Ordering::Release); + } - let should_register = match &self.active_waker { - None => true, - // waker changed, need to update, rare case - Some(w) => !w.will_wake(cx.waker()), - }; + #[inline] + pub(crate) fn cf_entries(&self) -> &ColumnFamily { + self.db.cf_handle(CF_ENTRIES).expect("missing entries CF") + } - if should_register { - let entry = self.journal.waker_slot(self.slot); - let mut guard = entry.lock().expect("Mutex poisoned"); + #[inline] + pub(crate) fn cf_meta(&self) -> &ColumnFamily { + self.db.cf_handle(CF_META).expect("missing meta CF") + } - if self.journal.inner.persisted_index.load(Ordering::Acquire) > self.slot { - return Poll::Ready(()); - } + #[inline] + pub(crate) fn read_consumed_index_from_db(&self) -> Result { + self.read_meta(Self::META_CONSUMED_INDEX_KEY) + } - *guard = Some(cx.waker().clone()); - self.active_waker = Some(cx.waker().clone()); - } + #[inline] + pub(crate) fn read_write_index_from_db(&self) -> Result { + self.read_meta(Self::META_WRITE_INDEX_KEY) + } - if self.journal.inner.persisted_index.load(Ordering::Acquire) > self.slot { - return Poll::Ready(()); + #[inline] + fn read_meta(&self, key: &[u8]) -> Result { + let cf = self.cf_meta(); + let Some(value) = self.db.get_cf(cf, key).stop_if_error(self)? else { + // If the key is missing, assume index 0 (fresh journal). + return Ok(0); + }; + if value.len() != 8 { + error!( + "Invalid meta value for key {:?}: expected 8 bytes, got {}", + key, + value.len() + ); + return Err(Error::Fatal); } - - Poll::Pending + Ok(u64::from_le_bytes(value.as_slice().try_into().unwrap())) } -} -/// A consumer wait entry. -struct ConsumerWait { - waker: Waker, - target_index: u64, -} - -/// A waker entry in the co-ring buffer. -/// -/// Aligned to cache line size to prevent false sharing. -#[derive(Default)] -#[repr(C, align(64))] -struct WakerEntry(Mutex>); + #[inline] + pub(crate) fn write_consumed_index_batched(&self, batch: &mut WriteBatch, new: u64) { + self.write_meta_batched(Self::META_CONSUMED_INDEX_KEY, batch, new) + } -impl Deref for WakerEntry { - type Target = Mutex>; + #[inline] + pub(crate) fn write_write_index_batched(&self, batch: &mut WriteBatch, new: u64) { + self.write_meta_batched(Self::META_WRITE_INDEX_KEY, batch, new) + } #[inline] - fn deref(&self) -> &Self::Target { - &self.0 + fn write_meta_batched(&self, key: &[u8], batch: &mut WriteBatch, new: u64) { + batch.put_cf(self.cf_meta(), key, new.to_le_bytes()) } } +/// A consumer wait entry. +pub(crate) struct ConsumerWait { + pub(crate) waker: Waker, + pub(crate) target_index: u64, +} + #[cfg(test)] pub(crate) mod tests { - use crate::checkpoint::CheckpointConfig; - pub const ENTRY_SIZE: usize = 8; pub const TEST_DATA: &[[u8; ENTRY_SIZE]] = &[ [0u8; ENTRY_SIZE], @@ -381,18 +347,14 @@ pub(crate) mod tests { [8u8; ENTRY_SIZE], [9u8; ENTRY_SIZE], ]; - pub type Journal = crate::Journal; + pub type Journal = crate::Journal; - /// Create a journal with an isolated temporary directory for WAL and checkpoint files. + /// Create a journal with an isolated temporary directory for the RocksDB database. /// Returns the journal and the temp dir guard (must be kept alive for the test duration). pub fn test_journal(capacity: usize) -> (Journal, tempfile::TempDir) { let tmp = tempfile::tempdir().expect("failed to create temp dir"); let config = crate::JournalConfig { - consumer_checkpoint: CheckpointConfig { - path: tmp.path().join("checkpoint.meta"), - ..Default::default() - }, - wal_dir: tmp.path().join("wal"), + db_path: tmp.path().join("journal_db"), }; let journal = Journal::with_capacity_and_config(capacity, config).expect("failed to create journal"); @@ -416,7 +378,6 @@ pub(crate) mod tests { "reader acquisition should succeed after drop" ); - journal.shutdown()?; Ok(()) } @@ -425,19 +386,18 @@ pub(crate) mod tests { let (journal, _tmp) = test_journal(4); let mut reader = journal.reader(); - journal.commit(&TEST_DATA[0]).await; - journal.commit(&TEST_DATA[1]).await; + journal.commit(&TEST_DATA[0]); + journal.commit(&TEST_DATA[1]); { - let entries = reader.read(2); + let entries = reader.read(2)?; assert_eq!(entries.len(), 2); - assert_eq!(entries[0], TEST_DATA[0]); - assert_eq!(entries[1], TEST_DATA[1]); + assert_eq!(*entries[0], TEST_DATA[0]); + assert_eq!(*entries[1], TEST_DATA[1]); } reader.commit()?; assert_eq!(reader.available(), 0); - journal.shutdown()?; Ok(()) } @@ -445,58 +405,51 @@ pub(crate) mod tests { async fn commit_returns_error_when_full() -> eyre::Result<()> { let (journal, _tmp) = test_journal(2); - journal.commit(&TEST_DATA[1]).await; - journal.commit(&TEST_DATA[2]).await; + journal.commit(&TEST_DATA[1]); + journal.commit(&TEST_DATA[2]); let err = journal .try_commit(&TEST_DATA[3]) .expect_err("buffer should report full on third commit"); assert!( - matches!(err, crate::error::JournalUnavailable::Full), + matches!(err, crate::Error::Full), "expected Full, got {err:?}" ); - journal.shutdown()?; Ok(()) } #[tokio::test(flavor = "current_thread")] - async fn reader_handles_wrap_around_reads() -> eyre::Result<()> { + async fn reader_handles_sequential_reads() -> eyre::Result<()> { let (journal, _tmp) = test_journal(4); let mut reader = journal.reader(); for entry in TEST_DATA.iter().take(4) { - journal.commit(entry).await; + journal.commit(entry); } { - let entries = reader.read(2); + let entries = reader.read(2)?; assert_eq!(entries.len(), 2); - assert_eq!(entries[0], TEST_DATA[0]); - assert_eq!(entries[1], TEST_DATA[1]); + assert_eq!(*entries[0], TEST_DATA[0]); + assert_eq!(*entries[1], TEST_DATA[1]); } reader.commit()?; for entry in TEST_DATA.iter().skip(4).take(2) { - journal.commit(entry).await; - } - - { - let entries = reader.read(4); - assert_eq!(entries.len(), 2); - assert_eq!(entries[0], TEST_DATA[2]); - assert_eq!(entries[1], TEST_DATA[3]); + journal.commit(entry); } { - let entries = reader.read(4); - assert_eq!(entries.len(), 2); - assert_eq!(entries[0], TEST_DATA[4]); - assert_eq!(entries[1], TEST_DATA[5]); + let entries = reader.read(4)?; + assert_eq!(entries.len(), 4); + assert_eq!(*entries[0], TEST_DATA[2]); + assert_eq!(*entries[1], TEST_DATA[3]); + assert_eq!(*entries[2], TEST_DATA[4]); + assert_eq!(*entries[3], TEST_DATA[5]); } reader.commit()?; assert_eq!(reader.available(), 0); - journal.shutdown()?; Ok(()) } } diff --git a/crates/journal/src/reader.rs b/crates/journal/src/reader.rs index 9bf0702..e1444e2 100644 --- a/crates/journal/src/reader.rs +++ b/crates/journal/src/reader.rs @@ -1,6 +1,7 @@ -use crate::{ConsumerWait, JournalInner}; +use crate::{ConsumerWait, Error, JournalInner, helper::FatalErrorExt}; +use rocksdb::{Direction, IteratorMode, ReadOptions, WriteBatch}; use std::{ - fmt, io, + fmt, pin::Pin, sync::{Arc, atomic::Ordering}, task::{Context, Poll}, @@ -8,13 +9,18 @@ use std::{ /// A reader for consuming settled entries from the journal. /// -/// Reader **WON'T** advance the shared consumed boundary until `commit()` is called. -pub struct JournalReader { - journal: Arc>, +/// Reader **WON'T** advance the shared consumed boundary until `commit()` is +/// called. Entries are fetched from RocksDB into an internal buffer on each +/// [`read`](JournalReader::read) call. +pub struct JournalReader { + journal: Arc, + /// Local consumed cursor – how far this reader has read. consumed: u64, + /// Internal buffer populated by [`read`](JournalReader::read). + read_buf: Vec>, } -impl fmt::Debug for JournalReader { +impl fmt::Debug for JournalReader { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("JournalReader") .field("consumed", &self.consumed) @@ -22,63 +28,66 @@ impl fmt::Debug for JournalReader { } } -impl Drop for JournalReader { +impl Drop for JournalReader { fn drop(&mut self) { self.journal.reader_taken.store(false, Ordering::Release); } } -impl JournalReader { - pub(super) fn new(journal: Arc>) -> Self { - let consumed = journal.consumed_checkpoint.current_index(); - Self { journal, consumed } +impl JournalReader { + pub(super) fn new(journal: Arc) -> Self { + let consumed = journal.consumed_index(); + Self { + journal, + consumed, + read_buf: Vec::new(), + } } - /// Returns the number of available entries that are settled but not yet consumed by this reader. + /// Returns the number of available entries that have been written but not + /// yet consumed by this reader. #[inline] pub fn available(&self) -> usize { - if self.journal.shutdown.load(Ordering::Acquire) { - return 0; - } - let persisted = self.journal.persisted_index.load(Ordering::Acquire); - persisted.wrapping_sub(self.consumed) as usize + let write_idx = self.journal.write_index(); + write_idx.wrapping_sub(self.consumed) as usize } /// Wait until at least `min` entries are available. - pub async fn wait_at_least(&mut self, min: usize) { + pub async fn wait_at_least(&mut self, min: usize) -> Result<(), Error> { + if self.journal.fatal_error.load(Ordering::Acquire) { + return Err(Error::Fatal); + } if self.available() >= min { - return; + return Ok(()); } let target_index = self.consumed.wrapping_add(min as u64); { - // panics if the target_index exceeds buffer size, otherwise we might wait forever - // this happens if: - // - asks for more entries than the buffer can hold - // - didn't commit previously read entries, then asks for more than new entries than the buffer can hold - // this is considered a misuse of the API / design flaw in the caller, so we panics - let journal_buffer_size = self.journal.buffer.len() as u64; - let current_consumed = self.journal.consumed_checkpoint.current_index(); - let max_possible_target = current_consumed.wrapping_add(journal_buffer_size); + let capacity = self.journal.capacity; + let current_consumed = self.journal.consumed_index(); + let max_possible_target = current_consumed.wrapping_add(capacity); if target_index > max_possible_target { panic!( - "requested ({target_index}) exceeds max possible ({max_possible_target}): journal.buffer.len()={journal_buffer_size}, journal.consumed_index={current_consumed}" + "requested ({target_index}) exceeds max possible ({max_possible_target}): journal.capacity={capacity}, journal.consumed_index={current_consumed}" ); } } - // Slow path - struct WaitForBatch<'a, const ENTRY_SIZE: usize> { - reader: &'a JournalReader, + // Slow path – register a waker and park until the writer catches up. + struct WaitForBatch<'a> { + reader: &'a JournalReader, target_index: u64, } - impl Future for WaitForBatch<'_, ENTRY_SIZE> { - type Output = (); - fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<()> { - if self.reader.journal.persisted_index.load(Ordering::Acquire) >= self.target_index - { - return Poll::Ready(()); + impl Future for WaitForBatch<'_> { + type Output = Result<(), Error>; + fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + if self.reader.journal.fatal_error.load(Ordering::Acquire) { + return Poll::Ready(Err(Error::Fatal)); + } + + if self.reader.journal.write_index() >= self.target_index { + return Poll::Ready(Ok(())); } let mut guard = self @@ -87,9 +96,8 @@ impl JournalReader { .consumer_wait .lock() .expect("Mutex poisoned"); - if self.reader.journal.persisted_index.load(Ordering::Acquire) >= self.target_index - { - return Poll::Ready(()); + if self.reader.journal.write_index() >= self.target_index { + return Poll::Ready(Ok(())); } *guard = Some(ConsumerWait { waker: cx.waker().clone(), @@ -100,43 +108,108 @@ impl JournalReader { } } + // handle cancellation by clearing the wait slot if the future is dropped while pending. + impl Drop for WaitForBatch<'_> { + fn drop(&mut self) { + let mut guard = self + .reader + .journal + .consumer_wait + .lock() + .expect("Mutex poisoned"); + if let Some(wait) = guard.as_ref() { + debug_assert_eq!(wait.target_index, self.target_index); + // At the same time, only one JournalReader can wait, so if there's a wait + // registered, it must be ours. + guard.take(); + } + } + } + WaitForBatch { reader: self, target_index, } - .await; + .await } - /// Read available entries, up to `max`. - /// Bumps the internal consumed index by the number of entries yielded. + /// Read available entries from RocksDB, up to `max`. /// - /// Caller is responsible for calling `commit()` after processing the entries. - pub fn read(&mut self, max: usize) -> &[[u8; ENTRY_SIZE]] { + /// Bumps the internal consumed cursor by the number of entries yielded. + /// Caller is responsible for calling [`commit`](JournalReader::commit) + /// after processing the entries. + pub fn read(&mut self, max: usize) -> Result<&[Box<[u8]>], Error> { + if self.journal.fatal_error.load(Ordering::Acquire) { + return Err(Error::Fatal); + } + let available = self.available(); if available == 0 { - return &[]; + return Ok(&[]); } let count = available.min(max); - let start_idx = self.consumed; - - // handle wrap-around - let buffer_len = self.journal.buffer.len(); - let slot_idx = (start_idx & self.journal.index_mask) as usize; - let continuous_len = count.min(buffer_len - slot_idx); + self.read_buf.clear(); + + let start_key = self.consumed.to_be_bytes(); + let end_key = self + .consumed + .checked_add(count as u64) + .expect("let's handle overflow 10000 years later") + .to_be_bytes(); + + let mut options = ReadOptions::default(); + options.set_iterate_lower_bound(start_key); + options.set_iterate_upper_bound(end_key); + options.set_auto_readahead_size(true); + + let iter = self.journal.db.iterator_cf_opt( + self.journal.cf_entries(), + options, + IteratorMode::From(&start_key, Direction::Forward), + ); + for (idx, data) in iter.enumerate() { + let (_key, value) = data.stop_if_error(&self.journal)?; + debug_assert_eq!((self.consumed + idx as u64).to_be_bytes(), _key.as_ref(),); + self.read_buf.push(value); + } - // push local consumed index - self.consumed += continuous_len as u64; + let read = self.read_buf.len(); + if read != count { + error!( + "journal reader short read: expected {count} entries, got {read}; treating as fatal" + ); + return Err(Error::Fatal); + } - // return slice - let ptr = self.journal.buffer[slot_idx].get(); - // SAFETY: bounds checked above - unsafe { std::slice::from_raw_parts(ptr, continuous_len) } + self.consumed += count as u64; + Ok(&self.read_buf) } - /// Commit current consumed index. - pub fn commit(&mut self) -> io::Result<()> { - self.journal.consumed_checkpoint.update(self.consumed) + /// Commit the current consumed index, persisting it to RocksDB and + /// deleting consumed entries. + pub fn commit(&mut self) -> Result<(), Error> { + if self.journal.fatal_error.load(Ordering::Acquire) { + return Err(Error::Fatal); + } + + let old_consumed = self.journal.consumed_index(); + + let mut batch = WriteBatch::default(); + self.journal + .write_consumed_index_batched(&mut batch, self.consumed); + // Garbage-collect consumed entries. + batch.delete_range_cf( + self.journal.cf_entries(), + old_consumed.to_be_bytes(), + self.consumed.to_be_bytes(), + ); + self.journal.db.write(batch).stop_if_error(&self.journal)?; + + self.journal + .consumed_index + .store(self.consumed, Ordering::Release); + Ok(()) } } @@ -146,22 +219,21 @@ mod tests { use tokio::time::{Duration, sleep, timeout}; #[tokio::test(flavor = "current_thread")] - async fn available_tracks_persisted_entries() -> eyre::Result<()> { + async fn available_tracks_written_entries() -> eyre::Result<()> { let (journal, _tmp) = test_journal(4); let mut reader = journal.reader(); assert_eq!(reader.available(), 0); - journal.commit(&TEST_DATA[0]).await; + journal.commit(&TEST_DATA[0]); assert_eq!(reader.available(), 1); - journal.commit(&TEST_DATA[1]).await; + journal.commit(&TEST_DATA[1]); assert_eq!(reader.available(), 2); - let slice = reader.read(1); + let slice = reader.read(1)?; assert_eq!(slice.len(), 1); assert_eq!(reader.available(), 1); - journal.shutdown()?; Ok(()) } @@ -171,40 +243,47 @@ mod tests { let mut reader = journal.reader(); for entry in TEST_DATA.iter().take(3) { - journal.commit(entry).await; + journal.commit(entry); } - let slice = reader.read(2); + let slice = reader.read(2)?; assert_eq!(slice.len(), 2); assert_eq!(reader.available(), 1); assert_eq!( - reader.journal.consumed_checkpoint.current_index(), + reader + .journal + .consumed_index + .load(std::sync::atomic::Ordering::Acquire), 0, "global consumed boundary should not advance before commit", ); reader.commit()?; - assert_eq!(reader.journal.consumed_checkpoint.current_index(), 2); - journal.shutdown()?; + assert_eq!( + reader + .journal + .consumed_index + .load(std::sync::atomic::Ordering::Acquire), + 2 + ); Ok(()) } #[tokio::test(flavor = "current_thread")] - async fn wait_at_least_resumes_after_persistence() -> eyre::Result<()> { + async fn wait_at_least_resumes_after_write() -> eyre::Result<()> { let (journal, _tmp) = test_journal(4); let mut reader = journal.reader(); let journal_clone = journal.clone(); let task = tokio::spawn(async move { sleep(Duration::from_millis(5)).await; - journal_clone.commit(&TEST_DATA[0]).await; + journal_clone.commit(&TEST_DATA[0]); }); - reader.wait_at_least(1).await; + reader.wait_at_least(1).await?; assert_eq!(reader.available(), 1); task.await?; - journal.shutdown()?; Ok(()) } @@ -216,45 +295,46 @@ mod tests { let journal_clone = journal.clone(); let task = tokio::spawn(async move { for entry in TEST_DATA.iter().take(4) { - journal_clone.commit(entry).await; + journal_clone.commit(entry); sleep(Duration::from_millis(5)).await; } }); - timeout(Duration::from_secs(10), reader.wait_at_least(3)).await?; + timeout(Duration::from_secs(10), reader.wait_at_least(3)).await??; assert!(reader.available() >= 3); task.await?; - journal.shutdown()?; Ok(()) } #[tokio::test(flavor = "current_thread")] #[should_panic( - expected = "requested (5) exceeds max possible (4): journal.buffer.len()=4, journal.consumed_index=0" + expected = "requested (5) exceeds max possible (4): journal.capacity=4, journal.consumed_index=0" )] - async fn wait_at_least_exceeds_buffer_size() { + async fn wait_at_least_exceeds_capacity() { let (journal, _tmp) = test_journal(4); let mut reader = journal.reader(); timeout(Duration::from_secs(1), reader.wait_at_least(5)) .await + .unwrap() .unwrap(); } #[tokio::test(flavor = "current_thread")] #[should_panic( - expected = "requested (5) exceeds max possible (4): journal.buffer.len()=4, journal.consumed_index=0" + expected = "requested (5) exceeds max possible (4): journal.capacity=4, journal.consumed_index=0" )] async fn wait_at_least_dirty_read_exceeds_available() { let (journal, _tmp) = test_journal(4); - journal.commit(&TEST_DATA[0]).await; + journal.commit(&TEST_DATA[0]); let mut reader = journal.reader(); - reader.read(1); + reader.read(1).unwrap(); timeout(Duration::from_secs(1), reader.wait_at_least(4)) .await + .unwrap() .unwrap(); } } diff --git a/crates/journal/src/wal.rs b/crates/journal/src/wal.rs deleted file mode 100644 index 97666b6..0000000 --- a/crates/journal/src/wal.rs +++ /dev/null @@ -1,856 +0,0 @@ -use crate::JournalInner; -use std::{ - fmt, - fmt::Formatter, - fs, - fs::{File, OpenOptions}, - io, - io::{BufWriter, Read, Seek, SeekFrom, Write}, - path::{Path, PathBuf}, - sync::{ - Arc, Mutex, - atomic::{AtomicBool, Ordering}, - }, - thread, - thread::JoinHandle, -}; - -const MAX_SPIN: usize = 100; -const MAX_IO_BATCH: u64 = 128; - -/// Write-Ahead Log -/// -/// Busy-Wait + Parking when there's no work to do. -/// -/// Using segmented log files named as `{base_dir}/{segment_id}.wal`, where `segment_id` is a -/// monotonically increasing integer. -/// -/// Each segment file contains a fixed number of entries -/// (at least to be the size of the journal buffer) to simplify recovery and management. -#[derive(Clone)] -pub struct Wal { - inner: Arc>, -} - -struct WalInner { - worker: Mutex>>, - journal: Arc>, - shutdown_flag: Arc, -} - -impl Drop for WalInner { - fn drop(&mut self) { - // Signal the WAL worker to exit if it hasn't been shut down yet. - // This prevents orphaned worker threads from spinning after the journal is dropped. - if !self.shutdown_flag.swap(true, Ordering::AcqRel) { - if let Some(worker) = self.worker.lock().unwrap().as_ref() { - worker.thread().unpark(); - } - } - } -} - -impl fmt::Debug for Wal { - fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - f.debug_struct("Wal") - .field( - "write_index", - &self.inner.journal.write_index.load(Ordering::Acquire), - ) - .field( - "persisted_index", - &self.inner.journal.persisted_index.load(Ordering::Acquire), - ) - .finish() - } -} - -impl Wal { - /// Create a new WAL instance with the given base directory for storing log segments and - /// a reference to the journal. This will recover existing segments from the base directory, - /// and start a background worker thread to handle persistence of log entries. - pub(crate) fn new>( - base_dir: P, - journal: Arc>, - ) -> io::Result { - let base_dir = base_dir.as_ref(); - fs::create_dir_all(base_dir)?; - if !base_dir.is_dir() { - return Err(io::Error::new( - io::ErrorKind::Other, - "Base path is not a directory", - )); - } - - let shutdown_flag = Arc::new(AtomicBool::new(false)); - - let current_segment_id = recover(base_dir, &journal)?; - let mut current_file = open_segment_file(base_dir, current_segment_id)?; - current_file.seek(SeekFrom::End(0))?; - - let worker = WalWorker { - current_segment_id, - current_file: BufWriter::new(current_file), - persisted_index: journal.persisted_index.load(Ordering::Acquire), - - base_dir: base_dir.to_path_buf(), - journal: journal.clone(), - shutdown_flag: shutdown_flag.clone(), - }; - - let handle = thread::Builder::new() - .name("wal-worker".to_string()) - .spawn(move || { - let mut worker = worker; - worker.run() - }) - .expect("Failed to spawn WAL worker thread"); - - let inner = WalInner { - worker: Mutex::new(Some(handle)), - journal, - shutdown_flag, - }; - - Ok(Self { - inner: Arc::new(inner), - }) - } - - /// Unpark the WAL worker thread to wake it up if it's parked. - /// This should be called after new entries are written to the journal, to ensure that the - /// worker thread can persist them in a timely manner. - pub fn unpark(&self) { - if self.inner.shutdown_flag.load(Ordering::Acquire) { - return; - } - self.inner - .worker - .lock() - .unwrap() - .as_ref() - .expect("WAL worker thread should be running") - .thread() - .unpark(); - } - - /// Shut down the WAL worker thread gracefully. This will set the shutdown flag, unpark the - /// worker thread if it's parked, and wait for it to finish. - pub fn shutdown(&self) { - if self.inner.shutdown_flag.swap(true, Ordering::AcqRel) { - // already shutdown - return; - } - let worker = self - .inner - .worker - .lock() - .unwrap() - .take() - .expect("WAL worker thread should be running"); - worker.thread().unpark(); - worker.join().expect("Failed to join WAL worker thread"); - } -} - -struct WalWorker { - current_segment_id: u64, - current_file: BufWriter, - persisted_index: u64, - - base_dir: PathBuf, - journal: Arc>, - shutdown_flag: Arc, -} - -impl WalWorker { - fn run(&mut self) { - let mut spin_count = 0; - - while !self.shutdown_flag.load(Ordering::Acquire) { - let filled = self.journal.filled_index.load(Ordering::Acquire); - let available = filled.wrapping_sub(self.persisted_index); - - if available > 0 { - spin_count = 0; - - let new_persisted_index = self - .write(available.min(MAX_IO_BATCH)) - .expect("Failed to write WAL entries"); - - self.notify_writer(new_persisted_index); - self.update_index(new_persisted_index); - self.notify_consumer(); - - continue; - } - - // no new data to persist, spin for a while before parking - if spin_count <= MAX_SPIN { - spin_count += 1; - std::hint::spin_loop(); - continue; - } - - // park until unparked by a new commit - thread::park(); - } - - // cleanup before exiting: persist any remaining entries - let filled = self.journal.filled_index.load(Ordering::Acquire); - let available = filled.wrapping_sub(self.persisted_index); - let new_persisted_index = self - .write(available.min(MAX_IO_BATCH)) - .expect("Failed to write WAL entries"); - - self.notify_writer(new_persisted_index); - self.update_index(new_persisted_index); - self.notify_consumer(); - } - - /// update the persisted index - fn update_index(&mut self, new_persisted_index: u64) { - self.persisted_index = new_persisted_index; - self.journal - .persisted_index - .store(self.persisted_index, Ordering::Release); - } - - /// Write `n` entries from the journal to the current WAL segment file, rotating files as needed. - fn write(&mut self, n: u64) -> io::Result { - let segment_size: u64 = self.journal.capacity() as u64; - let new_persisted_index = self.persisted_index + n; - - // write ALL available entries to segment files, rotating files as needed - for i in self.persisted_index..new_persisted_index { - let seg_id = i / segment_size; - if seg_id != self.current_segment_id { - // rotate to new segment file - self.current_segment_id = seg_id; - let new_file = open_segment_file(&self.base_dir, seg_id)?; - self.current_file = BufWriter::new(new_file); - let base_dir = self.base_dir.clone(); - thread::spawn(move || truncate_old_segments(base_dir, seg_id)); - } - - self.current_file - .write_all(unsafe { &*self.journal.data_slot_ptr(i) })?; - } - - self.current_file.flush()?; - self.current_file.get_ref().sync_all()?; - Ok(new_persisted_index) - } - - fn notify_writer(&mut self, new_persisted_index: u64) { - // notify waiters only after data is persisted - for i in self.persisted_index..new_persisted_index { - let entry = self.journal.waker_slot(i); - if let Some(waker) = entry.lock().unwrap().take() { - waker.wake(); - } - } - } - - fn notify_consumer(&mut self) { - // notify consumer if needed - let mut guard = self.journal.consumer_wait.lock().unwrap(); - if let Some(wait) = guard.as_ref() { - // Only wake if the new_persisted_index is reached - if self.persisted_index >= wait.target_index { - guard.take().unwrap().waker.wake(); - } - } - } -} - -fn recover( - base_dir: &Path, - journal: &JournalInner, -) -> io::Result { - let mut segments = scan_segments(base_dir)?; - if segments.is_empty() { - info!("No WAL segments found, starting fresh"); - return Ok(0); - } - - // remove the last segment for recovery, as it may be incomplete - let last_segment_id = segments.pop().expect("segments is not empty"); - - let segment_size = journal.capacity() as u64; - let complete_segment_size = segment_size * ENTRY_SIZE as u64; - for segment_id in segments.iter().copied() { - let path = base_dir.join(format_segment_file_name(segment_id)); - let metadata = fs::metadata(&path)?; - let file_size = metadata.len(); - - if file_size != complete_segment_size { - return Err(io::Error::new( - io::ErrorKind::InvalidData, - format!("Incomplete WAL segment: {}", path.display()), - )); - } - } - - // handle last segment, which may be partially written - let path = base_dir.join(format_segment_file_name(last_segment_id)); - let metadata = fs::metadata(&path)?; - let file_size = metadata.len(); - let valid_count = file_size / ENTRY_SIZE as u64; - if file_size % ENTRY_SIZE as u64 != 0 { - warn!("Detected partial write in last segment #{last_segment_id}. Truncating."); - let f = OpenOptions::new().write(true).open(&path)?; - f.set_len(valid_count * ENTRY_SIZE as u64)?; - f.sync_all()?; - } - - let write_index = if last_segment_id == 0 { - valid_count - } else { - last_segment_id * segment_size + valid_count - }; - // consumed_checkpoint just recovered. - let consumed_index = journal.consumed_checkpoint.persisted_index(); - // Data loss happens, don't continue to recover, as it may cause more damage. - // User intervention is needed to fix the issue. - if consumed_index > write_index { - return Err(io::Error::new( - io::ErrorKind::InvalidData, - format!( - "Consumed index {consumed_index} is greater than recovered write index {write_index}" - ), - )); - } - - journal.write_index.store(write_index, Ordering::Relaxed); - journal.filled_index.store(write_index, Ordering::Relaxed); - journal - .persisted_index - .store(write_index, Ordering::Relaxed); - info!("WAL Recovered: write_index={write_index}, consumed_index={consumed_index}"); - - // replay data to ring buffer - replay_data(&base_dir, journal)?; - Ok(last_segment_id) -} - -fn scan_segments(base_dir: &Path) -> io::Result> { - let mut segments: Vec = Vec::new(); - - let entries = fs::read_dir(&base_dir)?; - - for entry in entries { - let entry = entry?; - let file_type = entry.file_type()?; - if !file_type.is_file() { - continue; - } - - let file_name = entry.file_name(); - let Some(file_name) = file_name.to_str().and_then(|n| n.strip_suffix(".wal")) else { - continue; - }; - - let Ok(id) = file_name.parse::() else { - return Err(io::Error::new( - io::ErrorKind::InvalidData, - format!("Invalid WAL file name: {}", entry.path().display()), - )); - }; - segments.push(id); - } - segments.sort_unstable(); - - Ok(segments) -} - -fn replay_data( - base_dir: &Path, - journal: &JournalInner<{ ENTRY_SIZE }>, -) -> io::Result<()> { - let segment_size = journal.capacity() as u64; - let entry_size = ENTRY_SIZE as u64; - - let write_index = journal.write_index.load(Ordering::Relaxed); - let consumed_index = journal.consumed_checkpoint.persisted_index(); - - let mut current_file: Option = None; - let mut current_seg_id: u64 = u64::MAX; - - for idx in consumed_index..write_index { - let seg_id = idx / segment_size; - - if seg_id != current_seg_id { - current_file = Some(open_segment_file(&base_dir, seg_id)?); - current_seg_id = seg_id; - } - - let offset = (idx % segment_size) * entry_size; - let slot_ptr = journal.data_slot_ptr(idx); - - if let Some(ref mut f) = current_file { - f.seek(SeekFrom::Start(offset))?; - let buffer = unsafe { &mut *slot_ptr }; - f.read_exact(buffer)?; - } - } - Ok(()) -} - -#[inline] -fn format_segment_file_name(segment_id: u64) -> String { - format!("{segment_id:012}.wal") -} - -fn open_segment_file(base_dir: &Path, segment_id: u64) -> io::Result { - let path = base_dir.join(format_segment_file_name(segment_id)); - OpenOptions::new() - .read(true) - .write(true) - .create(true) - .open(path) -} - -#[instrument(err)] -fn truncate_old_segments(base_dir: PathBuf, current_segment_id: u64) -> io::Result<()> { - let Some(to_delete) = current_segment_id.checked_sub(2) else { - // not segments to truncate - return Ok(()); - }; - let path = base_dir.join(format_segment_file_name(to_delete)); - if path.exists() { - fs::remove_file(path)?; - } - Ok(()) -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::{ - JournalConfig, - checkpoint::CheckpointConfig, - tests::{ENTRY_SIZE, TEST_DATA, test_journal}, - }; - use std::sync::atomic::Ordering; - - type Journal = crate::Journal; - - /// Helper: create a journal config pointing at the given temp directory. - fn test_config(tmp: &Path) -> JournalConfig { - JournalConfig { - consumer_checkpoint: CheckpointConfig { - path: tmp.join("checkpoint.meta"), - ..Default::default() - }, - wal_dir: tmp.join("wal"), - } - } - - // ── Segment file helpers ───────────────────────────────────────────── - - #[test] - fn format_segment_file_name_pads_to_twelve_digits() { - assert_eq!(format_segment_file_name(0), "000000000000.wal"); - assert_eq!(format_segment_file_name(1), "000000000001.wal"); - assert_eq!( - format_segment_file_name(999_999_999_999), - "999999999999.wal" - ); - } - - #[test] - fn open_segment_file_creates_file() -> io::Result<()> { - let tmp = tempfile::tempdir()?; - let f = open_segment_file(tmp.path(), 0)?; - assert!(tmp.path().join("000000000000.wal").exists()); - drop(f); - Ok(()) - } - - // ── scan_segments ──────────────────────────────────────────────────── - - #[test] - fn scan_segments_empty_directory() -> io::Result<()> { - let tmp = tempfile::tempdir()?; - let segments = scan_segments(tmp.path())?; - assert!(segments.is_empty()); - Ok(()) - } - - #[test] - fn scan_segments_returns_sorted_ids() -> io::Result<()> { - let tmp = tempfile::tempdir()?; - // Create files out of order. - File::create(tmp.path().join("000000000002.wal"))?; - File::create(tmp.path().join("000000000000.wal"))?; - File::create(tmp.path().join("000000000001.wal"))?; - - let segments = scan_segments(tmp.path())?; - assert_eq!(segments, vec![0, 1, 2]); - Ok(()) - } - - #[test] - fn scan_segments_skips_non_wal_files() -> io::Result<()> { - let tmp = tempfile::tempdir()?; - File::create(tmp.path().join("000000000000.wal"))?; - File::create(tmp.path().join("readme.txt"))?; - - let segments = scan_segments(tmp.path())?; - assert_eq!(segments, vec![0]); - Ok(()) - } - - #[test] - fn scan_segments_rejects_invalid_wal_names() -> io::Result<()> { - let tmp = tempfile::tempdir()?; - File::create(tmp.path().join("not_a_number.wal"))?; - - let err = scan_segments(tmp.path()).unwrap_err(); - assert_eq!(err.kind(), io::ErrorKind::InvalidData); - Ok(()) - } - - // ── truncate_old_segments ──────────────────────────────────────────── - - #[test] - fn truncate_old_segments_removes_two_behind() -> io::Result<()> { - let tmp = tempfile::tempdir()?; - File::create(tmp.path().join(format_segment_file_name(0)))?; - File::create(tmp.path().join(format_segment_file_name(1)))?; - File::create(tmp.path().join(format_segment_file_name(2)))?; - - truncate_old_segments(tmp.path().to_path_buf(), 2)?; - assert!( - !tmp.path().join(format_segment_file_name(0)).exists(), - "segment 0 should be deleted when current is 2" - ); - assert!(tmp.path().join(format_segment_file_name(1)).exists()); - assert!(tmp.path().join(format_segment_file_name(2)).exists()); - Ok(()) - } - - #[test] - fn truncate_old_segments_noop_when_too_few() -> io::Result<()> { - let tmp = tempfile::tempdir()?; - File::create(tmp.path().join(format_segment_file_name(0)))?; - File::create(tmp.path().join(format_segment_file_name(1)))?; - - // current_segment_id = 1, checked_sub(2) underflows → noop - truncate_old_segments(tmp.path().to_path_buf(), 1)?; - assert!(tmp.path().join(format_segment_file_name(0)).exists()); - assert!(tmp.path().join(format_segment_file_name(1)).exists()); - Ok(()) - } - - // ── WAL persistence end-to-end ─────────────────────────────────────── - - #[tokio::test(flavor = "current_thread")] - async fn wal_persists_entries_to_segment_file() -> eyre::Result<()> { - let (journal, tmp) = test_journal(4); - - journal.commit(&TEST_DATA[0]).await; - journal.commit(&TEST_DATA[1]).await; - - // WAL file should exist and contain the two entries. - let wal_path = tmp.path().join("wal").join(format_segment_file_name(0)); - assert!(wal_path.exists(), "WAL segment file should exist"); - - let data = fs::read(&wal_path)?; - assert_eq!( - data.len(), - ENTRY_SIZE * 2, - "segment should contain exactly 2 entries" - ); - assert_eq!(&data[..ENTRY_SIZE], &TEST_DATA[0]); - assert_eq!(&data[ENTRY_SIZE..ENTRY_SIZE * 2], &TEST_DATA[1]); - Ok(()) - } - - #[tokio::test(flavor = "current_thread")] - async fn wal_segment_rotation() -> eyre::Result<()> { - // Capacity 4 → segment_size = 4 entries per segment. - let (journal, tmp) = test_journal(4); - let mut reader = journal.reader(); - - // Fill first segment (4 entries). - for entry in TEST_DATA.iter().take(4) { - journal.commit(entry).await; - } - - // Consume some entries to free ring buffer space. - reader.read(4); - reader.commit()?; - - // Write one more entry, causing rotation to segment 1. - journal.commit(&TEST_DATA[4]).await; - - let seg0 = tmp.path().join("wal").join(format_segment_file_name(0)); - let seg1 = tmp.path().join("wal").join(format_segment_file_name(1)); - assert!(seg0.exists(), "segment 0 should exist"); - assert!(seg1.exists(), "segment 1 should exist after rotation"); - - let seg1_data = fs::read(&seg1)?; - assert_eq!(seg1_data.len(), ENTRY_SIZE); - assert_eq!(&seg1_data[..ENTRY_SIZE], &TEST_DATA[4]); - Ok(()) - } - - // ── Recovery ───────────────────────────────────────────────────────── - - #[tokio::test(flavor = "current_thread")] - async fn recover_from_empty_dir() -> eyre::Result<()> { - let tmp = tempfile::tempdir()?; - let config = test_config(tmp.path()); - - // First journal writes nothing. - let journal = Journal::with_capacity_and_config(4, config.clone())?; - journal.shutdown()?; - drop(journal); - - // Second journal recovers with indices at 0. - let journal = Journal::with_capacity_and_config(4, config)?; - assert_eq!(journal.inner.write_index.load(Ordering::Acquire), 0); - assert_eq!(journal.inner.persisted_index.load(Ordering::Acquire), 0); - journal.shutdown()?; - Ok(()) - } - - #[tokio::test(flavor = "current_thread")] - async fn recover_replays_data_into_ring_buffer() -> eyre::Result<()> { - let tmp = tempfile::tempdir()?; - let config = test_config(tmp.path()); - - // Write entries and shut down. - { - let journal = Journal::with_capacity_and_config(4, config.clone())?; - journal.commit(&TEST_DATA[0]).await; - journal.commit(&TEST_DATA[1]).await; - journal.commit(&TEST_DATA[2]).await; - journal.shutdown()?; - } - - // Recover and verify data via reader. - { - let journal = Journal::with_capacity_and_config(4, config)?; - assert_eq!(journal.inner.write_index.load(Ordering::Acquire), 3); - assert_eq!(journal.inner.persisted_index.load(Ordering::Acquire), 3); - - let mut reader = journal.reader(); - let entries = reader.read(3); - assert_eq!(entries.len(), 3); - assert_eq!(entries[0], TEST_DATA[0]); - assert_eq!(entries[1], TEST_DATA[1]); - assert_eq!(entries[2], TEST_DATA[2]); - journal.shutdown()?; - } - Ok(()) - } - - #[tokio::test(flavor = "current_thread")] - async fn recover_respects_consumed_checkpoint() -> eyre::Result<()> { - let tmp = tempfile::tempdir()?; - let config = test_config(tmp.path()); - - // Write entries, consume some, then shut down. - { - let journal = Journal::with_capacity_and_config(4, config.clone())?; - journal.commit(&TEST_DATA[0]).await; - journal.commit(&TEST_DATA[1]).await; - journal.commit(&TEST_DATA[2]).await; - let mut reader = journal.reader(); - reader.read(2); - reader.commit()?; - journal.shutdown()?; - } - - // Recover - reader should only see unconsumed entries. - { - let journal = Journal::with_capacity_and_config(4, config)?; - assert_eq!(journal.inner.write_index.load(Ordering::Acquire), 3); - let mut reader = journal.reader(); - let entries = reader.read(10); - assert_eq!(entries.len(), 1, "only 1 unconsumed entry should remain"); - assert_eq!(entries[0], TEST_DATA[2]); - journal.shutdown()?; - } - Ok(()) - } - - #[test] - fn recover_truncates_partial_last_segment() -> io::Result<()> { - let tmp = tempfile::tempdir()?; - let wal_dir = tmp.path().join("wal"); - fs::create_dir_all(&wal_dir)?; - - // Write 2.5 entries worth of data (partial entry at end). - let mut data = Vec::new(); - data.extend_from_slice(&TEST_DATA[0]); - data.extend_from_slice(&TEST_DATA[1]); - data.extend_from_slice(&[0xFF; ENTRY_SIZE / 2]); // partial write - fs::write(wal_dir.join(format_segment_file_name(0)), &data)?; - - let config = test_config(tmp.path()); - let journal = Journal::with_capacity_and_config(4, config).unwrap(); - - // Should recover 2 valid entries (truncated partial write). - assert_eq!(journal.inner.write_index.load(Ordering::Acquire), 2); - assert_eq!(journal.inner.persisted_index.load(Ordering::Acquire), 2); - - // Verify truncated file size. - let file_size = fs::metadata(wal_dir.join(format_segment_file_name(0)))?.len(); - assert_eq!(file_size, (ENTRY_SIZE * 2) as u64); - journal.shutdown().unwrap(); - Ok(()) - } - - #[test] - fn recover_detects_incomplete_non_last_segment() { - let tmp = tempfile::tempdir().unwrap(); - let wal_dir = tmp.path().join("wal"); - fs::create_dir_all(&wal_dir).unwrap(); - - // Segment 0 is incomplete (not full), segment 1 exists. - // For capacity 4, a full segment should be 4 * ENTRY_SIZE bytes. - fs::write( - wal_dir.join(format_segment_file_name(0)), - &[0u8; ENTRY_SIZE * 2], - ) - .unwrap(); - fs::write(wal_dir.join(format_segment_file_name(1)), &[0u8; 0]).unwrap(); - - let config = test_config(tmp.path()); - let err = Journal::with_capacity_and_config(4, config).unwrap_err(); - assert_eq!(err.kind(), io::ErrorKind::InvalidData); - } - - #[test] - fn recover_detects_consumed_ahead_of_written() { - let tmp = tempfile::tempdir().unwrap(); - let wal_dir = tmp.path().join("wal"); - fs::create_dir_all(&wal_dir).unwrap(); - - // Write a checkpoint claiming index 10 consumed... - let checkpoint_path = tmp.path().join("checkpoint.meta"); - fs::write(&checkpoint_path, &10u64.to_le_bytes()).unwrap(); - - // ...but WAL only has 2 entries. - let mut data = Vec::new(); - data.extend_from_slice(&TEST_DATA[0]); - data.extend_from_slice(&TEST_DATA[1]); - fs::write(wal_dir.join(format_segment_file_name(0)), &data).unwrap(); - - let config = JournalConfig { - consumer_checkpoint: CheckpointConfig { - path: checkpoint_path, - ..Default::default() - }, - wal_dir, - }; - let err = Journal::with_capacity_and_config(4, config).unwrap_err(); - assert_eq!(err.kind(), io::ErrorKind::InvalidData); - } - - // ── Shutdown ───────────────────────────────────────────────────────── - - #[tokio::test(flavor = "current_thread")] - async fn shutdown_rejects_new_commits() -> eyre::Result<()> { - let (journal, _tmp) = test_journal(4); - - journal.commit(&TEST_DATA[0]).await; - journal.shutdown()?; - - let err = journal - .try_commit(&TEST_DATA[1]) - .expect_err("commit after shutdown should fail"); - assert!( - matches!(err, crate::error::JournalUnavailable::Shutdown), - "expected Shutdown, got {err:?}" - ); - Ok(()) - } - - #[tokio::test(flavor = "current_thread")] - async fn shutdown_is_idempotent() -> eyre::Result<()> { - let (journal, _tmp) = test_journal(4); - journal.commit(&TEST_DATA[0]).await; - journal.shutdown()?; - journal.shutdown()?; - Ok(()) - } - - // ── WAL worker wakes on commit ─────────────────────────────────────── - - #[tokio::test(flavor = "current_thread")] - async fn commit_future_resolves_after_wal_persistence() -> eyre::Result<()> { - let (journal, _tmp) = test_journal(4); - - // The commit future should only resolve after the WAL worker persists. - journal.commit(&TEST_DATA[0]).await; - - // If we got here, the persisted_index must have advanced. - assert!( - journal.inner.persisted_index.load(Ordering::Acquire) >= 1, - "persisted_index should be >= 1 after commit future resolves" - ); - Ok(()) - } - - #[tokio::test(flavor = "current_thread")] - async fn multiple_commits_advance_persisted_index() -> eyre::Result<()> { - let (journal, _tmp) = test_journal(4); - - for i in 0..4 { - journal.commit(&TEST_DATA[i]).await; - } - - assert_eq!(journal.inner.persisted_index.load(Ordering::Acquire), 4); - Ok(()) - } - - // ── Recovery across segments ───────────────────────────────────────── - - #[tokio::test(flavor = "current_thread")] - async fn recover_across_segment_rotation() -> eyre::Result<()> { - let tmp = tempfile::tempdir()?; - let config = test_config(tmp.path()); - - // Fill more than one segment. Capacity 4 → segment_size = 4. - { - let journal = Journal::with_capacity_and_config(4, config.clone())?; - let mut reader = journal.reader(); - - // Fill first segment. - for entry in TEST_DATA.iter().take(4) { - journal.commit(entry).await; - } - - // Consume to free ring buffer. - reader.read(4); - reader.commit()?; - - // Write into second segment. - journal.commit(&TEST_DATA[4]).await; - journal.commit(&TEST_DATA[5]).await; - - // Consume second batch. - reader.read(2); - reader.commit()?; - - journal.shutdown()?; - } - - // Recover and write more. - { - let journal = Journal::with_capacity_and_config(4, config)?; - assert_eq!(journal.inner.write_index.load(Ordering::Acquire), 6); - - // Should be able to write new entries. - journal.commit(&TEST_DATA[6]).await; - assert_eq!(journal.inner.persisted_index.load(Ordering::Acquire), 7); - journal.shutdown()?; - } - Ok(()) - } -} diff --git a/crates/relayer/.sqlx/query-0138ce9992340d06256843c2292a0f2317202a0a3b1cfb52d5a0a4ba86aeb1ef.json b/crates/relayer/.sqlx/query-0138ce9992340d06256843c2292a0f2317202a0a3b1cfb52d5a0a4ba86aeb1ef.json new file mode 100644 index 0000000..c586e1f --- /dev/null +++ b/crates/relayer/.sqlx/query-0138ce9992340d06256843c2292a0f2317202a0a3b1cfb52d5a0a4ba86aeb1ef.json @@ -0,0 +1,44 @@ +{ + "db_name": "SQLite", + "query": "\n INSERT INTO l1_batch (l2_chain_id, start_index, count, root, status)\n VALUES (?1, ?2, ?3, ?4, ?5)\n ON CONFLICT(l2_chain_id, start_index) DO UPDATE SET\n count = excluded.count,\n root = excluded.root,\n status = excluded.status\n WHERE\n CASE excluded.status\n WHEN 'Collected' THEN 1\n WHEN 'L1Sent' THEN 2\n WHEN 'L1Mined' THEN 3\n WHEN 'L2Received' THEN 4\n WHEN 'L2FinalizeTxSent' THEN 5\n WHEN 'L2Finalized' THEN 6\n ELSE 0\n END\n >=\n CASE l1_batch.status\n WHEN 'Collected' THEN 1\n WHEN 'L1Sent' THEN 2\n WHEN 'L1Mined' THEN 3\n WHEN 'L2Received' THEN 4\n WHEN 'L2FinalizeTxSent' THEN 5\n WHEN 'L2Finalized' THEN 6\n ELSE 0\n END\n RETURNING\n id,\n l1_tx_hash as \"l1_tx_hash: TextWrapper\",\n l2_tx_hash as \"l2_tx_hash: TextWrapper\",\n created_at,\n updated_at\n ", + "describe": { + "columns": [ + { + "name": "id", + "ordinal": 0, + "type_info": "Integer" + }, + { + "name": "l1_tx_hash: TextWrapper", + "ordinal": 1, + "type_info": "Text" + }, + { + "name": "l2_tx_hash: TextWrapper", + "ordinal": 2, + "type_info": "Text" + }, + { + "name": "created_at", + "ordinal": 3, + "type_info": "Integer" + }, + { + "name": "updated_at", + "ordinal": 4, + "type_info": "Integer" + } + ], + "parameters": { + "Right": 5 + }, + "nullable": [ + false, + true, + true, + false, + false + ] + }, + "hash": "0138ce9992340d06256843c2292a0f2317202a0a3b1cfb52d5a0a4ba86aeb1ef" +} diff --git a/crates/relayer/.sqlx/query-056fa0e19151915971d355c3233079ba8c40e71daf0e7e0de578bfc6a5812a51.json b/crates/relayer/.sqlx/query-056fa0e19151915971d355c3233079ba8c40e71daf0e7e0de578bfc6a5812a51.json new file mode 100644 index 0000000..843cc93 --- /dev/null +++ b/crates/relayer/.sqlx/query-056fa0e19151915971d355c3233079ba8c40e71daf0e7e0de578bfc6a5812a51.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "\n INSERT INTO tx_receipt (internal_transaction_id, gas_used, effective_gas_price, from_address, to_address)\n VALUES (?1, ?2, ?3, ?4, ?5)\n ON CONFLICT(internal_transaction_id) DO UPDATE SET\n gas_used = excluded.gas_used,\n effective_gas_price = excluded.effective_gas_price,\n from_address = excluded.from_address,\n to_address = excluded.to_address\n ", + "describe": { + "columns": [], + "parameters": { + "Right": 5 + }, + "nullable": [] + }, + "hash": "056fa0e19151915971d355c3233079ba8c40e71daf0e7e0de578bfc6a5812a51" +} diff --git a/crates/relayer/.sqlx/query-07ffab279ced5752bf3535f57d510b934052bd0d7eaa7fc0050ab44cd4f1099c.json b/crates/relayer/.sqlx/query-07ffab279ced5752bf3535f57d510b934052bd0d7eaa7fc0050ab44cd4f1099c.json new file mode 100644 index 0000000..1202628 --- /dev/null +++ b/crates/relayer/.sqlx/query-07ffab279ced5752bf3535f57d510b934052bd0d7eaa7fc0050ab44cd4f1099c.json @@ -0,0 +1,20 @@ +{ + "db_name": "SQLite", + "query": "\n INSERT INTO l1_batch (l2_chain_id, start_index, count, root, status)\n VALUES (?1, ?2, ?3, ?4, ?5)\n ON CONFLICT(l2_chain_id, start_index, count) DO UPDATE SET\n root = excluded.root,\n status = excluded.status\n RETURNING id\n ", + "describe": { + "columns": [ + { + "name": "id", + "ordinal": 0, + "type_info": "Integer" + } + ], + "parameters": { + "Right": 5 + }, + "nullable": [ + false + ] + }, + "hash": "07ffab279ced5752bf3535f57d510b934052bd0d7eaa7fc0050ab44cd4f1099c" +} diff --git a/crates/relayer/.sqlx/query-08540c9521786361d7b5957079ac3a993f0a393fe017dee526ef8b2a23e7bb7f.json b/crates/relayer/.sqlx/query-08540c9521786361d7b5957079ac3a993f0a393fe017dee526ef8b2a23e7bb7f.json new file mode 100644 index 0000000..5f50588 --- /dev/null +++ b/crates/relayer/.sqlx/query-08540c9521786361d7b5957079ac3a993f0a393fe017dee526ef8b2a23e7bb7f.json @@ -0,0 +1,20 @@ +{ + "db_name": "SQLite", + "query": "\n SELECT COUNT(*) as \"count!\"\n FROM l1_anchoring_queued\n WHERE queue_index >= ?1\n ", + "describe": { + "columns": [ + { + "name": "count!", + "ordinal": 0, + "type_info": "Integer" + } + ], + "parameters": { + "Right": 1 + }, + "nullable": [ + false + ] + }, + "hash": "08540c9521786361d7b5957079ac3a993f0a393fe017dee526ef8b2a23e7bb7f" +} diff --git a/crates/relayer/.sqlx/query-0d0d2ef416a8f8ff4375465ccf7e4e84764dbd9a62fb81615b7adf8b1bb3c3a7.json b/crates/relayer/.sqlx/query-0d0d2ef416a8f8ff4375465ccf7e4e84764dbd9a62fb81615b7adf8b1bb3c3a7.json new file mode 100644 index 0000000..e36221d --- /dev/null +++ b/crates/relayer/.sqlx/query-0d0d2ef416a8f8ff4375465ccf7e4e84764dbd9a62fb81615b7adf8b1bb3c3a7.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "\n INSERT INTO batch_fee (internal_batch_id, l1_gas_fee, l2_gas_fee, cross_chain_fee)\n VALUES (?1, 0, ?2, 0)\n ON CONFLICT(internal_batch_id) DO UPDATE SET l2_gas_fee = excluded.l2_gas_fee\n ", + "describe": { + "columns": [], + "parameters": { + "Right": 2 + }, + "nullable": [] + }, + "hash": "0d0d2ef416a8f8ff4375465ccf7e4e84764dbd9a62fb81615b7adf8b1bb3c3a7" +} diff --git a/crates/relayer/.sqlx/query-158542f76f97e59891337373d52948cf757e95cf05319b6592e5e4d9eac7a822.json b/crates/relayer/.sqlx/query-158542f76f97e59891337373d52948cf757e95cf05319b6592e5e4d9eac7a822.json new file mode 100644 index 0000000..09e3920 --- /dev/null +++ b/crates/relayer/.sqlx/query-158542f76f97e59891337373d52948cf757e95cf05319b6592e5e4d9eac7a822.json @@ -0,0 +1,32 @@ +{ + "db_name": "SQLite", + "query": "\n SELECT SUM(CAST(l1_gas_fee AS REAL)) as l1_gas_fee,\n SUM(CAST(l2_gas_fee AS REAL)) as l2_gas_fee,\n SUM(CAST(cross_chain_fee AS REAL)) as cross_chain_fee\n FROM batch_fee\n ", + "describe": { + "columns": [ + { + "name": "l1_gas_fee", + "ordinal": 0, + "type_info": "Float" + }, + { + "name": "l2_gas_fee", + "ordinal": 1, + "type_info": "Float" + }, + { + "name": "cross_chain_fee", + "ordinal": 2, + "type_info": "Float" + } + ], + "parameters": { + "Right": 0 + }, + "nullable": [ + true, + true, + true + ] + }, + "hash": "158542f76f97e59891337373d52948cf757e95cf05319b6592e5e4d9eac7a822" +} diff --git a/crates/relayer/.sqlx/query-25ef4786ec076cd2ad773442169639666f9a6db7681ba1ff51d4bf74183bea45.json b/crates/relayer/.sqlx/query-25ef4786ec076cd2ad773442169639666f9a6db7681ba1ff51d4bf74183bea45.json new file mode 100644 index 0000000..a350cf7 --- /dev/null +++ b/crates/relayer/.sqlx/query-25ef4786ec076cd2ad773442169639666f9a6db7681ba1ff51d4bf74183bea45.json @@ -0,0 +1,20 @@ +{ + "db_name": "SQLite", + "query": "\n INSERT INTO eth_log (internal_transaction_id, log_index)\n VALUES (?1, ?2)\n ON CONFLICT DO NOTHING\n RETURNING id as \"id!\"\n ", + "describe": { + "columns": [ + { + "name": "id!", + "ordinal": 0, + "type_info": "Integer" + } + ], + "parameters": { + "Right": 2 + }, + "nullable": [ + true + ] + }, + "hash": "25ef4786ec076cd2ad773442169639666f9a6db7681ba1ff51d4bf74183bea45" +} diff --git a/crates/relayer/.sqlx/query-29e16caa9d112a70d7854da7a3eea6da0af8542596c2dea77e9e6cf63e86596e.json b/crates/relayer/.sqlx/query-29e16caa9d112a70d7854da7a3eea6da0af8542596c2dea77e9e6cf63e86596e.json new file mode 100644 index 0000000..3b21049 --- /dev/null +++ b/crates/relayer/.sqlx/query-29e16caa9d112a70d7854da7a3eea6da0af8542596c2dea77e9e6cf63e86596e.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "\n INSERT INTO batch_fee (internal_batch_id, l1_gas_fee, l2_gas_fee, cross_chain_fee)\n VALUES (?1, ?2, 0, 0)\n ON CONFLICT(internal_batch_id) DO UPDATE SET l1_gas_fee = excluded.l1_gas_fee\n ", + "describe": { + "columns": [], + "parameters": { + "Right": 2 + }, + "nullable": [] + }, + "hash": "29e16caa9d112a70d7854da7a3eea6da0af8542596c2dea77e9e6cf63e86596e" +} diff --git a/crates/relayer/.sqlx/query-2b356a04e85a23ac4fc1e980138929aa699c73aee41c31b9a98f85028463c8a2.json b/crates/relayer/.sqlx/query-2b356a04e85a23ac4fc1e980138929aa699c73aee41c31b9a98f85028463c8a2.json new file mode 100644 index 0000000..735d3e5 --- /dev/null +++ b/crates/relayer/.sqlx/query-2b356a04e85a23ac4fc1e980138929aa699c73aee41c31b9a98f85028463c8a2.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "\n UPDATE l1_batch\n SET status = ?1, updated_at = unixepoch()\n WHERE id = ?2\n ", + "describe": { + "columns": [], + "parameters": { + "Right": 2 + }, + "nullable": [] + }, + "hash": "2b356a04e85a23ac4fc1e980138929aa699c73aee41c31b9a98f85028463c8a2" +} diff --git a/crates/relayer/.sqlx/query-419ced47684dffefe634ed640500a66b859afb5dd47b7d26764c8b7412e18403.json b/crates/relayer/.sqlx/query-419ced47684dffefe634ed640500a66b859afb5dd47b7d26764c8b7412e18403.json new file mode 100644 index 0000000..432d15d --- /dev/null +++ b/crates/relayer/.sqlx/query-419ced47684dffefe634ed640500a66b859afb5dd47b7d26764c8b7412e18403.json @@ -0,0 +1,20 @@ +{ + "db_name": "SQLite", + "query": "\n INSERT INTO eth_transaction (internal_block_id, transaction_index, transaction_hash)\n VALUES (?1, ?2, ?3)\n ON CONFLICT DO NOTHING\n RETURNING id as \"id!\"\n ", + "describe": { + "columns": [ + { + "name": "id!", + "ordinal": 0, + "type_info": "Integer" + } + ], + "parameters": { + "Right": 3 + }, + "nullable": [ + true + ] + }, + "hash": "419ced47684dffefe634ed640500a66b859afb5dd47b7d26764c8b7412e18403" +} diff --git a/crates/relayer/.sqlx/query-431385c65fad9e61d0cceb06cb5b07466aca845c11b2ca3e23b8b68113237796.json b/crates/relayer/.sqlx/query-431385c65fad9e61d0cceb06cb5b07466aca845c11b2ca3e23b8b68113237796.json new file mode 100644 index 0000000..e5a1db9 --- /dev/null +++ b/crates/relayer/.sqlx/query-431385c65fad9e61d0cceb06cb5b07466aca845c11b2ca3e23b8b68113237796.json @@ -0,0 +1,20 @@ +{ + "db_name": "SQLite", + "query": "\n SELECT COUNT(*) as \"count!\"\n FROM l2_anchoring_queued\n WHERE queue_index >= ?1\n ", + "describe": { + "columns": [ + { + "name": "count!", + "ordinal": 0, + "type_info": "Integer" + } + ], + "parameters": { + "Right": 1 + }, + "nullable": [ + false + ] + }, + "hash": "431385c65fad9e61d0cceb06cb5b07466aca845c11b2ca3e23b8b68113237796" +} diff --git a/crates/relayer/.sqlx/query-4421a93ad62a7a1ba73a34913a0958ff0206e090db7244e3b352bc3a8587bd5e.json b/crates/relayer/.sqlx/query-4421a93ad62a7a1ba73a34913a0958ff0206e090db7244e3b352bc3a8587bd5e.json new file mode 100644 index 0000000..5a36c1e --- /dev/null +++ b/crates/relayer/.sqlx/query-4421a93ad62a7a1ba73a34913a0958ff0206e090db7244e3b352bc3a8587bd5e.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "\n UPDATE l1_batch\n SET status = ?1, updated_at = unixepoch()\n WHERE id = ?2 AND status = ?3\n ", + "describe": { + "columns": [], + "parameters": { + "Right": 3 + }, + "nullable": [] + }, + "hash": "4421a93ad62a7a1ba73a34913a0958ff0206e090db7244e3b352bc3a8587bd5e" +} diff --git a/crates/relayer/.sqlx/query-4c83b31037d2789327f5dbdc6b42c5011e09f2bbd317f107fb078510c963c62e.json b/crates/relayer/.sqlx/query-4c83b31037d2789327f5dbdc6b42c5011e09f2bbd317f107fb078510c963c62e.json new file mode 100644 index 0000000..961024e --- /dev/null +++ b/crates/relayer/.sqlx/query-4c83b31037d2789327f5dbdc6b42c5011e09f2bbd317f107fb078510c963c62e.json @@ -0,0 +1,20 @@ +{ + "db_name": "SQLite", + "query": "\n INSERT INTO eth_transaction (internal_block_id, transaction_index, transaction_hash)\n VALUES (?1, ?2, ?3)\n ON CONFLICT DO UPDATE SET transaction_hash = excluded.transaction_hash\n RETURNING id as \"id!\"\n ", + "describe": { + "columns": [ + { + "name": "id!", + "ordinal": 0, + "type_info": "Integer" + } + ], + "parameters": { + "Right": 3 + }, + "nullable": [ + false + ] + }, + "hash": "4c83b31037d2789327f5dbdc6b42c5011e09f2bbd317f107fb078510c963c62e" +} diff --git a/crates/relayer/.sqlx/query-51354b3d377266583cf45e7afaf72f87ff3efb1fd5a274150d9fd8fce66a4cd5.json b/crates/relayer/.sqlx/query-51354b3d377266583cf45e7afaf72f87ff3efb1fd5a274150d9fd8fce66a4cd5.json new file mode 100644 index 0000000..6c24231 --- /dev/null +++ b/crates/relayer/.sqlx/query-51354b3d377266583cf45e7afaf72f87ff3efb1fd5a274150d9fd8fce66a4cd5.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "\n INSERT INTO l1_anchoring_queued (internal_log_id, attestation_id, root, queue_index, fee, block_number, timestamp)\n VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)\n ON CONFLICT DO NOTHING\n ", + "describe": { + "columns": [], + "parameters": { + "Right": 7 + }, + "nullable": [] + }, + "hash": "51354b3d377266583cf45e7afaf72f87ff3efb1fd5a274150d9fd8fce66a4cd5" +} diff --git a/crates/relayer/.sqlx/query-5f81a3ea416c03d4704e0b70ca357bf194549d72f988c7fce8473e6f3cf502c7.json b/crates/relayer/.sqlx/query-5f81a3ea416c03d4704e0b70ca357bf194549d72f988c7fce8473e6f3cf502c7.json new file mode 100644 index 0000000..a62a7d5 --- /dev/null +++ b/crates/relayer/.sqlx/query-5f81a3ea416c03d4704e0b70ca357bf194549d72f988c7fce8473e6f3cf502c7.json @@ -0,0 +1,20 @@ +{ + "db_name": "SQLite", + "query": "\n SELECT COUNT(*) FROM l1_batch\n ", + "describe": { + "columns": [ + { + "name": "COUNT(*)", + "ordinal": 0, + "type_info": "Integer" + } + ], + "parameters": { + "Right": 0 + }, + "nullable": [ + false + ] + }, + "hash": "5f81a3ea416c03d4704e0b70ca357bf194549d72f988c7fce8473e6f3cf502c7" +} diff --git a/crates/relayer/.sqlx/query-67f8ef9871348e781d1ee740768b4e8631b05605414529a3d3c56c45c64a9b69.json b/crates/relayer/.sqlx/query-67f8ef9871348e781d1ee740768b4e8631b05605414529a3d3c56c45c64a9b69.json new file mode 100644 index 0000000..c9b0d70 --- /dev/null +++ b/crates/relayer/.sqlx/query-67f8ef9871348e781d1ee740768b4e8631b05605414529a3d3c56c45c64a9b69.json @@ -0,0 +1,20 @@ +{ + "db_name": "SQLite", + "query": "\n INSERT INTO eth_block (chain_id, block_hash, block_number)\n VALUES (?1, ?2, ?3)\n ON CONFLICT DO UPDATE SET block_number = excluded.block_number\n RETURNING id\n ", + "describe": { + "columns": [ + { + "name": "id", + "ordinal": 0, + "type_info": "Integer" + } + ], + "parameters": { + "Right": 3 + }, + "nullable": [ + false + ] + }, + "hash": "67f8ef9871348e781d1ee740768b4e8631b05605414529a3d3c56c45c64a9b69" +} diff --git a/crates/relayer/.sqlx/query-693189ef6710fb08b1552c4ca0c56990b91161865a0112bae8007f087798bfad.json b/crates/relayer/.sqlx/query-693189ef6710fb08b1552c4ca0c56990b91161865a0112bae8007f087798bfad.json new file mode 100644 index 0000000..137590e --- /dev/null +++ b/crates/relayer/.sqlx/query-693189ef6710fb08b1552c4ca0c56990b91161865a0112bae8007f087798bfad.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "\n INSERT INTO l1_batch_finalized (l1_batch_id, internal_log_id, l2_block_number, l2_timestamp_finalized)\n VALUES (?1, ?2, ?3, ?4)\n ON CONFLICT DO NOTHING\n ", + "describe": { + "columns": [], + "parameters": { + "Right": 4 + }, + "nullable": [] + }, + "hash": "693189ef6710fb08b1552c4ca0c56990b91161865a0112bae8007f087798bfad" +} diff --git a/crates/relayer/.sqlx/query-787c9b7656116830c1f1c63c94e82120dae486a09ca80bd7d2c0a96d5d390e6a.json b/crates/relayer/.sqlx/query-787c9b7656116830c1f1c63c94e82120dae486a09ca80bd7d2c0a96d5d390e6a.json new file mode 100644 index 0000000..f7ed241 --- /dev/null +++ b/crates/relayer/.sqlx/query-787c9b7656116830c1f1c63c94e82120dae486a09ca80bd7d2c0a96d5d390e6a.json @@ -0,0 +1,20 @@ +{ + "db_name": "SQLite", + "query": "\n INSERT INTO eth_block (chain_id, block_hash, block_number)\n VALUES (?1, ?2, ?3)\n ON CONFLICT DO NOTHING\n RETURNING id\n ", + "describe": { + "columns": [ + { + "name": "id", + "ordinal": 0, + "type_info": "Integer" + } + ], + "parameters": { + "Right": 3 + }, + "nullable": [ + false + ] + }, + "hash": "787c9b7656116830c1f1c63c94e82120dae486a09ca80bd7d2c0a96d5d390e6a" +} diff --git a/crates/relayer/.sqlx/query-85a9fa1d680ce9e346d84a0d32a22e84c1661d46d99cfa5774de4dff356cf405.json b/crates/relayer/.sqlx/query-85a9fa1d680ce9e346d84a0d32a22e84c1661d46d99cfa5774de4dff356cf405.json new file mode 100644 index 0000000..6abf8aa --- /dev/null +++ b/crates/relayer/.sqlx/query-85a9fa1d680ce9e346d84a0d32a22e84c1661d46d99cfa5774de4dff356cf405.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "\n INSERT INTO l1_batch_arrived (l1_batch_id, internal_log_id, l1_block_attested, l1_timestamp_attested, l2_block_number, l2_timestamp_received)\n VALUES (?1, ?2, ?3, ?4, ?5, ?6)\n ", + "describe": { + "columns": [], + "parameters": { + "Right": 6 + }, + "nullable": [] + }, + "hash": "85a9fa1d680ce9e346d84a0d32a22e84c1661d46d99cfa5774de4dff356cf405" +} diff --git a/crates/relayer/.sqlx/query-8735f150e5b45df4743bfd92e20a36882f0ad7df85e08e100314a29ce214633f.json b/crates/relayer/.sqlx/query-8735f150e5b45df4743bfd92e20a36882f0ad7df85e08e100314a29ce214633f.json new file mode 100644 index 0000000..e0413a7 --- /dev/null +++ b/crates/relayer/.sqlx/query-8735f150e5b45df4743bfd92e20a36882f0ad7df85e08e100314a29ce214633f.json @@ -0,0 +1,62 @@ +{ + "db_name": "SQLite", + "query": "\n SELECT\n id,\n l2_chain_id,\n start_index,\n count,\n root as \"root: TextWrapper\",\n status as \"status: L1BatchStatus\",\n created_at,\n updated_at\n FROM l1_batch\n ORDER BY id DESC LIMIT 1\n ", + "describe": { + "columns": [ + { + "name": "id", + "ordinal": 0, + "type_info": "Integer" + }, + { + "name": "l2_chain_id", + "ordinal": 1, + "type_info": "Integer" + }, + { + "name": "start_index", + "ordinal": 2, + "type_info": "Integer" + }, + { + "name": "count", + "ordinal": 3, + "type_info": "Integer" + }, + { + "name": "root: TextWrapper", + "ordinal": 4, + "type_info": "Text" + }, + { + "name": "status: L1BatchStatus", + "ordinal": 5, + "type_info": "Text" + }, + { + "name": "created_at", + "ordinal": 6, + "type_info": "Integer" + }, + { + "name": "updated_at", + "ordinal": 7, + "type_info": "Integer" + } + ], + "parameters": { + "Right": 0 + }, + "nullable": [ + false, + false, + false, + false, + false, + false, + false, + false + ] + }, + "hash": "8735f150e5b45df4743bfd92e20a36882f0ad7df85e08e100314a29ce214633f" +} diff --git a/crates/relayer/.sqlx/query-8bd5749e9b68d07e8a8ae10f75bcb78d194007e9d462c0030461ef61f20b64f0.json b/crates/relayer/.sqlx/query-8bd5749e9b68d07e8a8ae10f75bcb78d194007e9d462c0030461ef61f20b64f0.json new file mode 100644 index 0000000..38476bd --- /dev/null +++ b/crates/relayer/.sqlx/query-8bd5749e9b68d07e8a8ae10f75bcb78d194007e9d462c0030461ef61f20b64f0.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "\n INSERT INTO batch_fee (internal_batch_id, l1_gas_fee, l2_gas_fee, cross_chain_fee)\n VALUES (?1, 0, 0, ?2)\n ON CONFLICT(internal_batch_id) DO UPDATE SET cross_chain_fee = excluded.cross_chain_fee\n ", + "describe": { + "columns": [], + "parameters": { + "Right": 2 + }, + "nullable": [] + }, + "hash": "8bd5749e9b68d07e8a8ae10f75bcb78d194007e9d462c0030461ef61f20b64f0" +} diff --git a/crates/relayer/.sqlx/query-a3154aca322524aa441b759f1fd06d5319e38f999b16d23659b0eca2a0525ed8.json b/crates/relayer/.sqlx/query-a3154aca322524aa441b759f1fd06d5319e38f999b16d23659b0eca2a0525ed8.json new file mode 100644 index 0000000..88b276e --- /dev/null +++ b/crates/relayer/.sqlx/query-a3154aca322524aa441b759f1fd06d5319e38f999b16d23659b0eca2a0525ed8.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "\n UPDATE l1_batch\n SET l2_tx_hash = ?1, status = ?2, updated_at = unixepoch()\n WHERE id = ?3\n ", + "describe": { + "columns": [], + "parameters": { + "Right": 3 + }, + "nullable": [] + }, + "hash": "a3154aca322524aa441b759f1fd06d5319e38f999b16d23659b0eca2a0525ed8" +} diff --git a/crates/relayer/.sqlx/query-b03c1f317f3cdd1ea774359763fc72d9e5c933bdad7e923826e447a03d2987f3.json b/crates/relayer/.sqlx/query-b03c1f317f3cdd1ea774359763fc72d9e5c933bdad7e923826e447a03d2987f3.json new file mode 100644 index 0000000..5af381e --- /dev/null +++ b/crates/relayer/.sqlx/query-b03c1f317f3cdd1ea774359763fc72d9e5c933bdad7e923826e447a03d2987f3.json @@ -0,0 +1,20 @@ +{ + "db_name": "SQLite", + "query": "\n SELECT root as \"root: TextWrapper\"\n FROM l1_anchoring_queued\n WHERE queue_index >= ?1\n ORDER BY queue_index\n LIMIT ?2\n ", + "describe": { + "columns": [ + { + "name": "root: TextWrapper", + "ordinal": 0, + "type_info": "Text" + } + ], + "parameters": { + "Right": 2 + }, + "nullable": [ + false + ] + }, + "hash": "b03c1f317f3cdd1ea774359763fc72d9e5c933bdad7e923826e447a03d2987f3" +} diff --git a/crates/relayer/.sqlx/query-b0bae6a356c6ed9438b20155b470f932ce9c58dd3f187443cc465b9074d256ff.json b/crates/relayer/.sqlx/query-b0bae6a356c6ed9438b20155b470f932ce9c58dd3f187443cc465b9074d256ff.json new file mode 100644 index 0000000..0a1e2a3 --- /dev/null +++ b/crates/relayer/.sqlx/query-b0bae6a356c6ed9438b20155b470f932ce9c58dd3f187443cc465b9074d256ff.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "\n INSERT INTO l2_anchoring_queued (internal_log_id, attestation_id, root, queue_index, fee, block_number, timestamp)\n VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)\n ON CONFLICT DO NOTHING\n ", + "describe": { + "columns": [], + "parameters": { + "Right": 7 + }, + "nullable": [] + }, + "hash": "b0bae6a356c6ed9438b20155b470f932ce9c58dd3f187443cc465b9074d256ff" +} diff --git a/crates/relayer/.sqlx/query-bf299edb0d76536088a0f109fa27ea585800bea3e048f157e6a308d88ac66c38.json b/crates/relayer/.sqlx/query-bf299edb0d76536088a0f109fa27ea585800bea3e048f157e6a308d88ac66c38.json new file mode 100644 index 0000000..60e46fa --- /dev/null +++ b/crates/relayer/.sqlx/query-bf299edb0d76536088a0f109fa27ea585800bea3e048f157e6a308d88ac66c38.json @@ -0,0 +1,20 @@ +{ + "db_name": "SQLite", + "query": "\n SELECT root as \"root: TextWrapper\"\n FROM l2_anchoring_queued\n WHERE queue_index >= ?1\n ORDER BY queue_index\n LIMIT ?2\n ", + "describe": { + "columns": [ + { + "name": "root: TextWrapper", + "ordinal": 0, + "type_info": "Text" + } + ], + "parameters": { + "Right": 2 + }, + "nullable": [ + false + ] + }, + "hash": "bf299edb0d76536088a0f109fa27ea585800bea3e048f157e6a308d88ac66c38" +} diff --git a/crates/relayer/.sqlx/query-c51afa7c515a0fe748f75fc072f05552a929304b1da57f28a9347bab75300b27.json b/crates/relayer/.sqlx/query-c51afa7c515a0fe748f75fc072f05552a929304b1da57f28a9347bab75300b27.json new file mode 100644 index 0000000..0f52ed7 --- /dev/null +++ b/crates/relayer/.sqlx/query-c51afa7c515a0fe748f75fc072f05552a929304b1da57f28a9347bab75300b27.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "\n INSERT INTO indexer_cursors (chain_id, event_signature_hash, last_indexed_block)\n VALUES (?1, ?2, ?3)\n ON CONFLICT(chain_id, event_signature_hash) DO UPDATE SET last_indexed_block = excluded.last_indexed_block, updated_at = CURRENT_TIMESTAMP\n ", + "describe": { + "columns": [], + "parameters": { + "Right": 3 + }, + "nullable": [] + }, + "hash": "c51afa7c515a0fe748f75fc072f05552a929304b1da57f28a9347bab75300b27" +} diff --git a/crates/relayer/.sqlx/query-d212440f3955c55af0c723154198bdb4c53e933d905577baf2759c4b9a12f896.json b/crates/relayer/.sqlx/query-d212440f3955c55af0c723154198bdb4c53e933d905577baf2759c4b9a12f896.json new file mode 100644 index 0000000..485288e --- /dev/null +++ b/crates/relayer/.sqlx/query-d212440f3955c55af0c723154198bdb4c53e933d905577baf2759c4b9a12f896.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "\n UPDATE l1_batch\n SET l1_tx_hash = ?1, status = ?2, updated_at = unixepoch()\n WHERE id = ?3\n ", + "describe": { + "columns": [], + "parameters": { + "Right": 3 + }, + "nullable": [] + }, + "hash": "d212440f3955c55af0c723154198bdb4c53e933d905577baf2759c4b9a12f896" +} diff --git a/crates/relayer/.sqlx/query-e090eb29f1861e95fd74fc12878a825d508e46b374a42c5fdd6d90043038f1f9.json b/crates/relayer/.sqlx/query-e090eb29f1861e95fd74fc12878a825d508e46b374a42c5fdd6d90043038f1f9.json new file mode 100644 index 0000000..7daf36b --- /dev/null +++ b/crates/relayer/.sqlx/query-e090eb29f1861e95fd74fc12878a825d508e46b374a42c5fdd6d90043038f1f9.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "\n INSERT INTO l1_batch_arrived (l1_batch_id, internal_log_id, l1_block_attested, l1_timestamp_attested, l2_block_number, l2_timestamp_received)\n VALUES (?1, ?2, ?3, ?4, ?5, ?6)\n ON CONFLICT DO NOTHING\n ", + "describe": { + "columns": [], + "parameters": { + "Right": 6 + }, + "nullable": [] + }, + "hash": "e090eb29f1861e95fd74fc12878a825d508e46b374a42c5fdd6d90043038f1f9" +} diff --git a/crates/relayer/.sqlx/query-e8fdb00094a7ad83caa03fd3da3b26fc07c9e9907529f37d684e22c7fd18b1aa.json b/crates/relayer/.sqlx/query-e8fdb00094a7ad83caa03fd3da3b26fc07c9e9907529f37d684e22c7fd18b1aa.json new file mode 100644 index 0000000..ddd3b2d --- /dev/null +++ b/crates/relayer/.sqlx/query-e8fdb00094a7ad83caa03fd3da3b26fc07c9e9907529f37d684e22c7fd18b1aa.json @@ -0,0 +1,20 @@ +{ + "db_name": "SQLite", + "query": "\n SELECT last_indexed_block FROM indexer_cursors\n WHERE chain_id = ?1 AND event_signature_hash = ?2\n ", + "describe": { + "columns": [ + { + "name": "last_indexed_block", + "ordinal": 0, + "type_info": "Integer" + } + ], + "parameters": { + "Right": 2 + }, + "nullable": [ + false + ] + }, + "hash": "e8fdb00094a7ad83caa03fd3da3b26fc07c9e9907529f37d684e22c7fd18b1aa" +} diff --git a/crates/relayer/.sqlx/query-eb05840588e22f9478be2b750c09a20981e7b6b4aa791440b10ce11c0127557f.json b/crates/relayer/.sqlx/query-eb05840588e22f9478be2b750c09a20981e7b6b4aa791440b10ce11c0127557f.json new file mode 100644 index 0000000..36037e2 --- /dev/null +++ b/crates/relayer/.sqlx/query-eb05840588e22f9478be2b750c09a20981e7b6b4aa791440b10ce11c0127557f.json @@ -0,0 +1,20 @@ +{ + "db_name": "SQLite", + "query": "\n INSERT INTO eth_log (internal_transaction_id, log_index)\n VALUES (?1, ?2)\n ON CONFLICT DO UPDATE SET log_index = excluded.log_index\n RETURNING id as \"id!\"\n ", + "describe": { + "columns": [ + { + "name": "id!", + "ordinal": 0, + "type_info": "Integer" + } + ], + "parameters": { + "Right": 2 + }, + "nullable": [ + false + ] + }, + "hash": "eb05840588e22f9478be2b750c09a20981e7b6b4aa791440b10ce11c0127557f" +} diff --git a/crates/relayer/.sqlx/query-ed8ab2be359596c22f4ca369fe7be6360e8bc398ff3ee80ec2add00d646c8a76.json b/crates/relayer/.sqlx/query-ed8ab2be359596c22f4ca369fe7be6360e8bc398ff3ee80ec2add00d646c8a76.json new file mode 100644 index 0000000..c54c279 --- /dev/null +++ b/crates/relayer/.sqlx/query-ed8ab2be359596c22f4ca369fe7be6360e8bc398ff3ee80ec2add00d646c8a76.json @@ -0,0 +1,74 @@ +{ + "db_name": "SQLite", + "query": "\n SELECT\n id,\n l2_chain_id,\n start_index,\n count,\n root as \"root: TextWrapper\",\n l1_tx_hash as \"l1_tx_hash: TextWrapper\",\n l2_tx_hash as \"l2_tx_hash: TextWrapper\",\n status as \"status: L1BatchStatus\",\n created_at,\n updated_at\n FROM l1_batch\n ORDER BY id DESC LIMIT 1\n ", + "describe": { + "columns": [ + { + "name": "id", + "ordinal": 0, + "type_info": "Integer" + }, + { + "name": "l2_chain_id", + "ordinal": 1, + "type_info": "Integer" + }, + { + "name": "start_index", + "ordinal": 2, + "type_info": "Integer" + }, + { + "name": "count", + "ordinal": 3, + "type_info": "Integer" + }, + { + "name": "root: TextWrapper", + "ordinal": 4, + "type_info": "Text" + }, + { + "name": "l1_tx_hash: TextWrapper", + "ordinal": 5, + "type_info": "Text" + }, + { + "name": "l2_tx_hash: TextWrapper", + "ordinal": 6, + "type_info": "Text" + }, + { + "name": "status: L1BatchStatus", + "ordinal": 7, + "type_info": "Text" + }, + { + "name": "created_at", + "ordinal": 8, + "type_info": "Integer" + }, + { + "name": "updated_at", + "ordinal": 9, + "type_info": "Integer" + } + ], + "parameters": { + "Right": 0 + }, + "nullable": [ + false, + false, + false, + false, + false, + true, + true, + false, + false, + false + ] + }, + "hash": "ed8ab2be359596c22f4ca369fe7be6360e8bc398ff3ee80ec2add00d646c8a76" +} diff --git a/crates/relayer/.sqlx/query-efce26860bb51dd670c6242c3cd59f229f4fe7a4910d9fcc6bec3a3937928da4.json b/crates/relayer/.sqlx/query-efce26860bb51dd670c6242c3cd59f229f4fe7a4910d9fcc6bec3a3937928da4.json new file mode 100644 index 0000000..ebfcacd --- /dev/null +++ b/crates/relayer/.sqlx/query-efce26860bb51dd670c6242c3cd59f229f4fe7a4910d9fcc6bec3a3937928da4.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "\n INSERT INTO l1_batch_finalized (l1_batch_id, internal_log_id, l2_block_number, l2_timestamp_finalized)\n VALUES (?1, ?2, ?3, ?4)\n ", + "describe": { + "columns": [], + "parameters": { + "Right": 4 + }, + "nullable": [] + }, + "hash": "efce26860bb51dd670c6242c3cd59f229f4fe7a4910d9fcc6bec3a3937928da4" +} diff --git a/crates/relayer/Cargo.toml b/crates/relayer/Cargo.toml new file mode 100644 index 0000000..6072c6f --- /dev/null +++ b/crates/relayer/Cargo.toml @@ -0,0 +1,57 @@ +[package] +authors.workspace = true +description = "UTS L2->L1->L2 Relayer Server" +edition.workspace = true +homepage.workspace = true +include = [ + "src/**/*", + "Cargo.toml", + "migrations/*.sql", + ".sqlx/*.json", + "LICENSE", +] +keywords = ["uts", "ethereum"] +license = "AGPL-3.0-or-later" +name = "uts-relayer" +repository.workspace = true +version.workspace = true + +[lints] +workspace = true + +[dependencies] +alloy-contract = { workspace = true, features = ["pubsub"] } +alloy-primitives = { workspace = true, features = ["serde"] } +alloy-provider = { workspace = true, default-features = false, features = ["ws", "reqwest"] } +alloy-rpc-client = { workspace = true } +alloy-rpc-types-eth = { workspace = true } +alloy-signer-local = { workspace = true, features = ["mnemonic"] } +alloy-sol-types = { workspace = true } +axum = { workspace = true, default-features = false, features = [ + "macros", + "http2", + "tokio", + "json", +] } +config = { workspace = true } +eyre = { workspace = true } +futures = { workspace = true } +jiff = { workspace = true, features = ["serde"] } +rustls = { workspace = true, optional = true } +serde = { workspace = true } +sha3 = { workspace = true } +sqlx = { workspace = true, features = ["runtime-tokio", "sqlite", "migrate"] } +strum = { workspace = true, features = ["derive"] } +tokio = { workspace = true, features = ["full"] } +tokio-util = { workspace = true } +tracing = { workspace = true } +tracing-subscriber = { workspace = true } +uts-bmt = { workspace = true } +uts-contracts = { workspace = true, features = ["provider-helper-serde"] } +uts-sql = { workspace = true } + +[features] +default = ["reqwest-rustls-tls"] + +performance = ["tracing/release_max_level_info"] +reqwest-rustls-tls = ["rustls", "alloy-provider/reqwest-rustls-tls"] diff --git a/crates/relayer/LICENSE-AGPL b/crates/relayer/LICENSE-AGPL new file mode 120000 index 0000000..5f5cf25 --- /dev/null +++ b/crates/relayer/LICENSE-AGPL @@ -0,0 +1 @@ +../../LICENSE-AGPL \ No newline at end of file diff --git a/crates/relayer/config.toml b/crates/relayer/config.toml new file mode 100644 index 0000000..f4286ba --- /dev/null +++ b/crates/relayer/config.toml @@ -0,0 +1,37 @@ +[server] +bind-address = "0.0.0.0:3000" +node-name = "Lich // PSR B1257+12" + +[blockchain] +gateway-address = "0x39Ea3C70A5e26C8dB4a1A5E9A7Fc7Bab04684C08" +manager-address = "0x7249802D50CB719547F7685d090d4b84e7183C32" + +[blockchain.rpc] +l1 = "https://eth-sepolia.g.alchemy.com/v2/uymDlBa_asemsxbDEdpTU" +l2-ws = "wss://scroll-sepolia.g.alchemy.com/v2/uymDlBa_asemsxbDEdpTU" + +[blockchain.rpc.retry] +compute-units-per-second = 20 +initial-backoff = 100 +max-rate-limit-retries = 10 + +[blockchain.rpc.throttle] +requests-per-second = 25 + +[blockchain.wallet] +index = 0 +mnemonic = "tag text lens coyote rescue reason drive tag cousin rice just tongue hotel effort focus grape venue pool hard coffee busy tomorrow comic under" + +[indexer.l2] +batch-size = 10 +start-block = 17049361 + +[relayer] +batch-max-size = 512 +batch-max-wait-seconds = 60 +l1-batch-submission-fee = "0x38d7ea4c68000" # 0.001 ETH +l1-batch-submission-gas-limit = 200_000 +tick-interval-seconds = 10 + +[db.sql] +filename = "./.db/relayer.sqlite" diff --git a/crates/relayer/migrations/20260306051302_indexer.sql b/crates/relayer/migrations/20260306051302_indexer.sql new file mode 100644 index 0000000..049134a --- /dev/null +++ b/crates/relayer/migrations/20260306051302_indexer.sql @@ -0,0 +1,149 @@ +PRAGMA foreign_keys = ON; + +CREATE TABLE IF NOT EXISTS indexer_cursors ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + chain_id INTEGER NOT NULL, + event_signature_hash TEXT NOT NULL, + last_indexed_block INTEGER NOT NULL, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP +); +CREATE UNIQUE INDEX IF NOT EXISTS idx_indexer_cursors_chain_id_event_signature ON indexer_cursors (chain_id, event_signature_hash); + +CREATE TABLE IF NOT EXISTS eth_block ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + chain_id INTEGER NOT NULL, + block_hash TEXT NOT NULL, + block_number INTEGER NOT NULL +); +CREATE UNIQUE INDEX IF NOT EXISTS idx_eth_block_chain_id_block_number ON eth_block (chain_id, block_number); +CREATE UNIQUE INDEX IF NOT EXISTS idx_eth_block_chain_id_block_hash ON eth_block (chain_id, block_hash); + +CREATE TABLE IF NOT EXISTS eth_transaction ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + internal_block_id INTEGER NOT NULL REFERENCES eth_block (id) ON DELETE CASCADE, + transaction_index INTEGER NOT NULL, + transaction_hash TEXT NOT NULL +); +CREATE UNIQUE INDEX IF NOT EXISTS idx_eth_transaction_block_id_transaction_index ON eth_transaction (internal_block_id, transaction_index); +CREATE UNIQUE INDEX IF NOT EXISTS idx_eth_transaction_transaction_hash ON eth_transaction (transaction_hash); + +CREATE TABLE IF NOT EXISTS eth_log ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + internal_transaction_id INTEGER NOT NULL REFERENCES eth_transaction (id) ON DELETE CASCADE, + log_index INTEGER NOT NULL +); +CREATE UNIQUE INDEX IF NOT EXISTS idx_eth_log_transaction_id_log_index ON eth_log (internal_transaction_id, log_index); + +-- /// Emitted when a user pays to have their root anchored to L1. +-- event L1AnchoringQueued( +-- bytes32 indexed attestationId, +-- bytes32 indexed root, +-- uint256 queueIndex, +-- uint256 fee, +-- uint256 blockNumber, +-- uint256 timestamp +-- ) +CREATE TABLE IF NOT EXISTS l1_anchoring_queued ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + internal_log_id INTEGER NOT NULL REFERENCES eth_log (id) ON DELETE CASCADE, + + attestation_id TEXT NOT NULL, + root TEXT NOT NULL, + queue_index INTEGER NOT NULL, + fee INTEGER NOT NULL, + block_number INTEGER NOT NULL, + timestamp INTEGER NOT NULL +); +CREATE UNIQUE INDEX IF NOT EXISTS idx_l1_anchoring_queued_log_id ON l1_anchoring_queued (internal_log_id); +CREATE INDEX IF NOT EXISTS idx_l1_anchoring_queued_attestation_id ON l1_anchoring_queued (attestation_id); +CREATE INDEX IF NOT EXISTS idx_l1_anchoring_queued_root ON l1_anchoring_queued (root); +CREATE INDEX IF NOT EXISTS idx_l1_anchoring_queued_queue_index ON l1_anchoring_queued (queue_index); +CREATE INDEX IF NOT EXISTS idx_l1_anchoring_queued_block_number ON l1_anchoring_queued (block_number); +CREATE INDEX IF NOT EXISTS idx_l1_anchoring_queued_timestamp ON l1_anchoring_queued (timestamp); + +CREATE TABLE IF NOT EXISTS l1_batch ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + + l2_chain_id INTEGER NOT NULL, + start_index INTEGER NOT NULL, + + count INTEGER NOT NULL, + root TEXT NOT NULL, + + l1_tx_hash TEXT NULL, + l2_tx_hash TEXT NULL, + + status TEXT NOT NULL, + created_at INTEGER NOT NULL DEFAULT (unixepoch()), + updated_at INTEGER NOT NULL DEFAULT (unixepoch()) +); +CREATE UNIQUE INDEX IF NOT EXISTS idx_l1_batch_start_index ON l1_batch (l2_chain_id, start_index); +CREATE INDEX IF NOT EXISTS idx_l1_batch_root ON l1_batch (root); +CREATE INDEX IF NOT EXISTS idx_l1_batch_status ON l1_batch (status); + +-- /// Emitted when L1 notifies that a batch of roots has been anchored on L1. +-- /// - `claimedRoot` The Merkle root claimed to be anchored on L1. +-- /// - `startIndex` The starting index of the batch in the queue. +-- /// - `count` The number of items in the batch. +-- /// - `l1BlockAttested` The L1 block number at which the batch was anchored. It would +-- /// be 0 if the root was timestamped before the batch submission. +-- /// - `l1TimestampAttested` The timestamp at which the batch was anchored on L1. +-- /// - `l2BlockNumber` The L2 block number at which the notification is received. +-- /// - `l2TimestampReceived` The timestamp when the notification is received. +-- /// +-- event L1BatchArrived( +-- bytes32 indexed claimedRoot, +-- uint256 indexed startIndex, +-- uint256 count, +-- uint256 l1BlockAttested, +-- uint256 l1TimestampAttested, +-- uint256 l2BlockNumber, +-- uint256 l2TimestampReceived +-- ); +CREATE TABLE IF NOT EXISTS l1_batch_arrived ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + l1_batch_id INTEGER NOT NULL REFERENCES l1_batch (id) ON DELETE CASCADE, + internal_log_id INTEGER NOT NULL REFERENCES eth_log (id) ON DELETE CASCADE, + + l1_block_attested INTEGER NOT NULL, + l1_timestamp_attested INTEGER NOT NULL, + l2_block_number INTEGER NOT NULL, + l2_timestamp_received INTEGER NOT NULL +); +CREATE UNIQUE INDEX IF NOT EXISTS idx_l1_batch_arrived_log_id ON l1_batch_arrived (internal_log_id); +CREATE INDEX IF NOT EXISTS idx_l1_batch_arrived_l1_block_attested ON l1_batch_arrived (l1_block_attested); +CREATE INDEX IF NOT EXISTS idx_l1_batch_arrived_l1_timestamp_attested ON l1_batch_arrived (l1_timestamp_attested); +CREATE INDEX IF NOT EXISTS idx_l1_batch_arrived_l2_block_number ON l1_batch_arrived (l2_block_number); +CREATE INDEX IF NOT EXISTS idx_l1_batch_arrived_l2_timestamp_received ON l1_batch_arrived (l2_timestamp_received); + +-- /// Emitted when a batch of roots is finalized after L1 confirmation. +-- /// - `merkleRoot` The Merkle root of the batch. +-- /// - `startIndex` The starting index of the batch in the queue. +-- /// - `count` The number of items in the batch. +-- /// - `l1BlockAttested` The L1 block number at which the batch was anchored. It would be +-- /// 0 if the root was timestamped before the batch submission. +-- /// - `l1TimestampAttested` The timestamp at which the batch was anchored on L1. +-- /// - `l2BlockNumber` The L2 block number at which the batch is finalized. +-- /// - `l2TimestampFinalized` The timestamp when the batch is finalized.- +-- /// +-- event L1BatchFinalized( +-- bytes32 indexed merkleRoot, +-- uint256 indexed startIndex, +-- uint256 count, +-- uint256 l1BlockAttested, +-- uint256 l1TimestampAttested, +-- uint256 l2BlockNumber, +-- uint256 l2TimestampFinalized +-- ); +CREATE TABLE IF NOT EXISTS l1_batch_finalized ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + l1_batch_id INTEGER NOT NULL REFERENCES l1_batch (id) ON DELETE CASCADE, + internal_log_id INTEGER NOT NULL REFERENCES eth_log (id) ON DELETE CASCADE, + + l2_block_number INTEGER NOT NULL, + l2_timestamp_finalized INTEGER NOT NULL +); +CREATE UNIQUE INDEX IF NOT EXISTS idx_l1_batch_finalized_log_id ON l1_batch_finalized (internal_log_id); +CREATE INDEX IF NOT EXISTS idx_l1_batch_finalized_l2_block_number ON l1_batch_finalized (l2_block_number); +CREATE INDEX IF NOT EXISTS idx_l1_batch_finalized_l2_timestamp_finalized ON l1_batch_finalized (l2_timestamp_finalized); + diff --git a/crates/relayer/migrations/20260307041712_tx_history.sql b/crates/relayer/migrations/20260307041712_tx_history.sql new file mode 100644 index 0000000..17ab8f2 --- /dev/null +++ b/crates/relayer/migrations/20260307041712_tx_history.sql @@ -0,0 +1,12 @@ +CREATE TABLE IF NOT EXISTS tx_receipt ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + internal_transaction_id INTEGER NOT NULL REFERENCES eth_transaction (id) ON DELETE CASCADE, + + gas_used INTEGER NOT NULL, + effective_gas_price TEXT NOT NULL, + from_address TEXT NOT NULL, + to_address TEXT NOT NULL -- all tx are not contract creation, so to_address is always non-null +); +CREATE UNIQUE INDEX IF NOT EXISTS idx_tx_receipt_transaction_id ON tx_receipt (internal_transaction_id); +CREATE INDEX IF NOT EXISTS idx_tx_receipt_from_address ON tx_receipt (from_address); +CREATE INDEX IF NOT EXISTS idx_tx_receipt_to_address ON tx_receipt (to_address); diff --git a/crates/relayer/migrations/20260307073019_stats.sql b/crates/relayer/migrations/20260307073019_stats.sql new file mode 100644 index 0000000..eb1d432 --- /dev/null +++ b/crates/relayer/migrations/20260307073019_stats.sql @@ -0,0 +1,9 @@ +CREATE TABLE IF NOT EXISTS batch_fee ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + internal_batch_id INTEGER NOT NULL REFERENCES l1_batch (id) ON DELETE CASCADE, + + l1_gas_fee TEXT NOT NULL, + l2_gas_fee TEXT NOT NULL, + cross_chain_fee TEXT NOT NULL +); +CREATE UNIQUE INDEX IF NOT EXISTS idx_batch_fee_batch_id ON batch_fee (internal_batch_id); diff --git a/crates/relayer/src/config.rs b/crates/relayer/src/config.rs new file mode 100644 index 0000000..ea0375c --- /dev/null +++ b/crates/relayer/src/config.rs @@ -0,0 +1,139 @@ +use alloy_primitives::{Address, BlockNumber, U256}; +use config::{Config, ConfigError, Environment, File}; +use serde::Deserialize; +use std::path::PathBuf; +use uts_contracts::provider_helper::{RetryBackoffArgs, ThrottleArgs}; + +/// Application configuration loaded from defaults, a config file, and environment variables. +#[derive(Debug, Clone, Deserialize)] +#[serde(rename_all = "kebab-case")] +pub struct AppConfig { + /// Server configuration, including node name and bind address. + pub server: ServerConfig, + /// Blockchain configuration, including RPC URL and wallet credentials. + pub blockchain: BlockchainConfig, + /// Indexer configuration. + pub indexer: IndexerConfig, + /// Relayer configuration. + pub relayer: RelayerConfig, + /// Database configuration for journal, key-value store, and SQL database. + pub db: DbConfig, +} + +/// Server configuration, including node name and bind address. +#[derive(Debug, Clone, Deserialize)] +#[serde(rename_all = "kebab-case")] +pub struct ServerConfig { + /// A human-readable name for the calendar node, used in homepage. + pub node_name: String, + /// The address and port to bind the server to. + pub bind_address: String, +} + +/// Blockchain configuration, including RPC URL and wallet credentials. +#[derive(Debug, Clone, Deserialize)] +#[serde(rename_all = "kebab-case")] +pub struct BlockchainConfig { + /// The address of the L2AnchoringManager contract on the L2 chain. + pub manager_address: Address, + /// The address of the L1AnchoringGateway contract on the L1 chain. + pub gateway_address: Address, + /// Rpc configuration. + pub rpc: RpcConfig, + /// Wallet configuration, including mnemonic and index for key derivation. + pub wallet: WalletConfig, +} + +/// Rpc Config +#[derive(Debug, Clone, Deserialize)] +#[serde(rename_all = "kebab-case")] +pub struct RpcConfig { + /// The RPC URL of the Ethereum L2 node to connect to. + pub l1: String, + /// The RPC URL of the Ethereum L2 node to connect to. + pub l2_ws: String, + /// Retry backoff configuration for handling rate-limited requests to the RPC provider. + #[serde(default)] + pub retry: RetryBackoffArgs, + /// Throttle configuration for limiting the rate of requests to the RPC provider. + #[serde(default)] + pub throttle: ThrottleArgs, +} + +/// Wallet configuration, including mnemonic and index for key derivation. +#[derive(Debug, Clone, Deserialize)] +#[serde(rename_all = "kebab-case")] +pub struct WalletConfig { + /// The mnemonic phrase for the wallet, used to derive the signing key. + pub mnemonic: String, + /// The index for key derivation from the mnemonic, following BIP-44. + pub index: u32, +} + +/// Indexer configuration. +#[derive(Debug, Clone, Deserialize)] +pub struct IndexerConfig { + /// Configuration for the L2 indexer. + pub l2: L2IndexerConfig, +} + +/// Configuration for the L2 indexer. +#[derive(Debug, Clone, Deserialize)] +#[serde(rename_all = "kebab-case")] +pub struct L2IndexerConfig { + /// The block number to start indexing from. This should be the block number of the deployment + /// transaction of the L2AnchoringManager contract, or a safe block number before that to ensure + /// we don't miss any events. + pub start_block: BlockNumber, + /// The batch size for fetching events. This determines how many blocks to fetch in each query + /// when calling `eth_getLogs`. + pub batch_size: u64, +} + +/// Configuration for the Relayer +#[derive(Debug, Clone, Deserialize)] +#[serde(rename_all = "kebab-case")] +pub struct RelayerConfig { + /// The maximum number of L2 anchoring events to include in a single batch when sending to L1. + pub batch_max_size: i64, + /// The maximum time in seconds to wait before sealing a batch and sending it to L1. + pub batch_max_wait_seconds: i64, + /// The gas limit to use for submitting a batch to L1. + pub l1_batch_submission_gas_limit: u64, + /// The fee in wei to pay for submitting a batch to L1. + pub l1_batch_submission_fee: U256, + /// The interval in seconds at which the relayer's main loop runs. + pub tick_interval_seconds: u64, +} + +/// Database configuration for journal, key-value store, and SQL database. +#[derive(Debug, Clone, Deserialize)] +#[serde(rename_all = "kebab-case")] +pub struct DbConfig { + /// Configuration for the SQL database, including filename. + pub sql: SqlConfig, +} + +/// Configuration for the SQL database, including filename. +#[derive(Debug, Clone, Deserialize)] +#[serde(rename_all = "kebab-case")] +pub struct SqlConfig { + /// The file system path where the SQL database (e.g. SQLite) is stored. + pub filename: PathBuf, +} + +impl AppConfig { + /// Load configuration from defaults, a config file, and environment variables. + pub fn new() -> Result { + let settings = Config::builder() + .add_source(File::with_name("config").required(false)) + .add_source( + Environment::with_prefix("CALENDAR") + .separator("_") + .try_parsing(true), + ) + .build()?; + + settings.try_deserialize() + } +} diff --git a/crates/relayer/src/index.html b/crates/relayer/src/index.html new file mode 100644 index 0000000..8f73bd4 --- /dev/null +++ b/crates/relayer/src/index.html @@ -0,0 +1,558 @@ + + + + + + + {{NODE_NAME}} - UTS L1 Relayer + + + + + + + +

+
+ + +
+
+
+
+ +
+
+
+

SYS.RELAYER [ {{NODE_NAME}} ]

+ + +
+

Universal.Timestamps.Relayer.v{{VERSION}}

+
+
+ +
+
+ + + + + +
+
+
+
+ + +
+ + +
+

+ > UTS Interchain Relayer Engine _ +

+
+

+ This daemon is responsible for packing L2 user attestations into batches and submitting them to the Ethereum L1 gateway. +

+ + +
+ +
+ SYSTEM_WARNING: TESTNET_ENVIRONMENT + This node is connected to the experimental testnet. Proofs generated here are for testing purposes only. Do not rely on this network for production data. +
+
+ + +
+ +
+ RELAYER_OPERATOR: + {{NODE_ADDRESS}} +
+
+
+
+ + +
+ +
+
+ +
+

Total_L1_Batches

+
+ N/A +
+
+
+ + +
+
+ +
+

Mempool_Queue

+
+ N/A +
+

Pending L2 Attestations

+
+ + +
+
+ +
+

Network_Gas_Fees

+
+
+ L1 + N/A +
+
+ L2 + N/A +
+
+

Cumulative Gas Fees

+
+ + +
+
+ +
+

Cross_Chain_Fees

+
+ N/A +
+

Cumulative Msg Value

+
+
+ + +
+
+ +
+

System_Unit_Economics

+

Sum of (L1 + L2 + Cross-Chain) / 0 Total Packed Entries

+
+
+
+ Total_Avg_Cost_Per_Entry + N/A +
+
+ + +
+ +
+ +
+

END OF TRANSMISSION // UTS RELAYER DAEMON

+
+ + + + + diff --git a/crates/relayer/src/indexer.rs b/crates/relayer/src/indexer.rs new file mode 100644 index 0000000..5cdd76a --- /dev/null +++ b/crates/relayer/src/indexer.rs @@ -0,0 +1,183 @@ +use crate::sql; +use alloy_primitives::{BlockNumber, ChainId}; +use alloy_sol_types::SolEvent; +use eyre::ContextCompat; +use futures::StreamExt; +use sqlx::{SqliteConnection, SqlitePool}; +use std::{fmt::Debug, pin::Pin}; +use tokio::select; +use tokio_util::sync::CancellationToken; + +type BoxedFuture<'a, T> = Pin + Send + 'a>>; + +const REWIND_BLOCKS: u64 = 100; + +mod l2; +pub use l2::L2Indexer; + +struct Subscriber<'a, Provider, Event, F> { + chain_id: ChainId, + db: SqlitePool, + event_filter: alloy_contract::Event<&'a Provider, Event>, + insert_event_fn: F, + cancellation_token: CancellationToken, +} + +impl Subscriber<'_, Provider, Event, F> +where + Provider: alloy_provider::Provider + 'static, + Event: SolEvent + Debug + 'static, + F: for<'a> Fn( + &'a mut SqliteConnection, + ChainId, + i64, + Event, + ) -> BoxedFuture<'a, eyre::Result<()>>, +{ + async fn run(self) -> eyre::Result<()> { + let Self { + chain_id, + db, + event_filter, + insert_event_fn, + cancellation_token, + } = self; + + let subscription = event_filter.subscribe().await?; + info!(event = Event::SIGNATURE, "Started event subscription"); + let mut stream = subscription.into_stream(); + loop { + select! { + maybe_event = stream.next() => { + match maybe_event { + Some(Ok((event, log))) => { + let mut tx = db.begin().await?; + let block_number = log.block_number.context("Log missing block number")?; + info!( + event = Event::SIGNATURE, + block_number, + transaction_hash = ?log.transaction_hash, + log_index = ?log.log_index, + event_data = ?event, + "Received event" + ); + // This update is unsafe, cuz we might not handle all events in this block. + // But during the startup phase, the scanner will rewind the cursor a few blocks to make sure we don't miss any events. + sql::indexer::update_cursor(&mut *tx, chain_id, Event::SIGNATURE_HASH, block_number).await?; + let id = sql::eth::insert_log(&mut *tx, chain_id, &log).await?; + insert_event_fn(&mut tx, chain_id, id, event).await?; + tx.commit().await?; + }, + Some(Err(e)) => { + error!("Error while receiving event: {e:?}"); + cancellation_token.cancel(); + break; + } + None => { + error!("Event stream ended unexpectedly"); + cancellation_token.cancel(); + break; + } + } + } + _ = cancellation_token.cancelled() => { + info!("Cancellation requested, stopping event subscription"); + break; + } + } + } + Ok(()) + } +} + +struct Scanner<'a, Provider, Event, F> { + chain_id: ChainId, + db: SqlitePool, + default_start_block: BlockNumber, + batch_size: u64, + event_filter: alloy_contract::Event<&'a Provider, Event>, + insert_event_fn: F, + cancellation_token: CancellationToken, +} + +impl Scanner<'_, Provider, Event, F> +where + Provider: alloy_provider::Provider + 'static, + Event: SolEvent + Debug + 'static, + F: for<'a> Fn( + &'a mut SqliteConnection, + ChainId, + i64, + Event, + ) -> BoxedFuture<'a, eyre::Result<()>>, +{ + async fn run(self) -> eyre::Result<()> { + let cancellation_token = self.cancellation_token.clone(); + select! { + res = self.run_inner() => { + res + } + _ = cancellation_token.cancelled() => { + info!("Cancellation requested, stopping event scanning"); + Ok(()) + } + } + } + + async fn run_inner(self) -> eyre::Result<()> { + let Self { + chain_id, + db, + default_start_block, + batch_size, + mut event_filter, + insert_event_fn: insert_event, + .. + } = self; + + let mut start_block = sql::indexer::load_cursor(&db, chain_id, Event::SIGNATURE_HASH) + .await? + .map(|blk| { + // Rewind a few blocks to make sure we don't miss any events. See above comment in + // the subscription handler for details. + blk.saturating_sub(REWIND_BLOCKS) + }) + .unwrap_or(default_start_block); + info!(event = Event::SIGNATURE, start_block, "Starting event scan"); + + let mut end_block = event_filter.provider.get_block_number().await?; + + while start_block <= end_block { + let batch_end = std::cmp::min(start_block + batch_size - 1, end_block); + trace!( + event = Event::SIGNATURE, + start_block, + end_block = batch_end, + "Scanning for events" + ); + event_filter = event_filter.from_block(start_block).to_block(batch_end); + + let mut tx = db.begin().await?; + for (event, log) in event_filter.query().await? { + info!( + event = Event::SIGNATURE, + block_number = log.block_number, + transaction_hash = ?log.transaction_hash, + log_index = ?log.log_index, + event_data = ?event, + "Received event" + ); + let id = sql::eth::insert_log(&mut *tx, chain_id, &log).await?; + insert_event(&mut tx, chain_id, id, event).await?; + } + sql::indexer::update_cursor(&mut *tx, chain_id, Event::SIGNATURE_HASH, batch_end) + .await?; + + start_block = batch_end + 1; + tx.commit().await?; + end_block = event_filter.provider.get_block_number().await?; + } + + Ok(()) + } +} diff --git a/crates/relayer/src/indexer/l2.rs b/crates/relayer/src/indexer/l2.rs new file mode 100644 index 0000000..618e04e --- /dev/null +++ b/crates/relayer/src/indexer/l2.rs @@ -0,0 +1,241 @@ +use crate::{ + config::L2IndexerConfig, + indexer::{BoxedFuture, Scanner, Subscriber}, + sql, +}; +use alloy_contract::Event; +use alloy_primitives::ChainId; +use alloy_provider::Provider; +use sqlx::{SqliteConnection, SqlitePool}; +use std::sync::Arc; +use tokio::task::JoinSet; +use tokio_util::sync::CancellationToken; +use uts_contracts::manager::{ + L2AnchoringManager, + events::{L1AnchoringQueued, L1BatchArrived, L1BatchFinalized}, +}; + +/// Indexer for L2 events. +#[derive(Debug)] +pub struct L2Indexer { + db: SqlitePool, + chain_id: ChainId, + manager: L2AnchoringManager

, + config: L2IndexerConfig, + cancellation_token: CancellationToken, +} + +impl L2Indexer

{ + /// Create a new instance of the indexer. + pub async fn new( + db: SqlitePool, + manager: L2AnchoringManager

, + config: L2IndexerConfig, + cancellation_token: CancellationToken, + ) -> eyre::Result> { + let chain_id = manager.provider().get_chain_id().await?; + Ok(Arc::new(Self { + db, + chain_id, + manager, + config, + cancellation_token, + })) + } + + /// Start the scanners. + pub async fn start_scanners(self: Arc) -> eyre::Result<()> { + let mut join_set = JoinSet::new(); + + join_set.spawn(self.clone().scan_l1anchoring_queued()); + join_set.spawn(self.clone().scan_l1batch_arrived()); + join_set.spawn(self.clone().scan_l1batch_finalized()); + + for task in join_set.join_all().await { + task? + } + Ok(()) + } + + /// Start the subscribers. + pub async fn start_subscribers(self: Arc) -> eyre::Result<()> { + let mut join_set = JoinSet::new(); + + join_set.spawn(self.clone().subscribe_l1anchoring_queued()); + join_set.spawn(self.clone().subscribe_l1batch_arrived()); + join_set.spawn(self.clone().subscribe_l1batch_finalized()); + + for task in join_set.join_all().await { + task? + } + Ok(()) + } + + #[instrument(skip(self), err)] + async fn scan_l1anchoring_queued(self: Arc) -> eyre::Result<()> { + let scanner = Scanner { + chain_id: self.chain_id, + db: self.db.clone(), + default_start_block: self.config.start_block, + batch_size: self.config.batch_size, + event_filter: self.l1anchoring_queued_filter(), + insert_event_fn: insert_l1anchoring_queued, + cancellation_token: self.cancellation_token.clone(), + }; + + if let Err(e) = scanner.run().await { + error!("Error while scanning for events: {e:?}"); + self.cancellation_token.cancel(); + return Err(e); + } + Ok(()) + } + + #[instrument(skip(self), err)] + async fn subscribe_l1anchoring_queued(self: Arc) -> eyre::Result<()> { + let subscriber = Subscriber { + chain_id: self.chain_id, + db: self.db.clone(), + event_filter: self.l1anchoring_queued_filter(), + insert_event_fn: insert_l1anchoring_queued, + cancellation_token: self.cancellation_token.clone(), + }; + + if let Err(e) = subscriber.run().await { + error!("Error while subscribing to events: {e:?}"); + self.cancellation_token.cancel(); + } + Ok(()) + } + + #[instrument(skip(self), err)] + async fn scan_l1batch_arrived(self: Arc) -> eyre::Result<()> { + let scanner = Scanner { + chain_id: self.chain_id, + db: self.db.clone(), + default_start_block: self.config.start_block, + batch_size: self.config.batch_size, + event_filter: self.l1batch_arrived_filter(), + insert_event_fn: insert_l1batch_arrived, + cancellation_token: self.cancellation_token.clone(), + }; + + if let Err(e) = scanner.run().await { + error!("Error while scanning for events: {e:?}"); + self.cancellation_token.cancel(); + } + Ok(()) + } + + #[instrument(skip(self), err)] + async fn subscribe_l1batch_arrived(self: Arc) -> eyre::Result<()> { + let subscriber = Subscriber { + chain_id: self.chain_id, + db: self.db.clone(), + event_filter: self.l1batch_arrived_filter(), + insert_event_fn: insert_l1batch_arrived, + cancellation_token: self.cancellation_token.clone(), + }; + + if let Err(e) = subscriber.run().await { + error!("Error while subscribing to events: {e:?}"); + self.cancellation_token.cancel(); + } + + Ok(()) + } + + #[instrument(skip(self), err)] + async fn scan_l1batch_finalized(self: Arc) -> eyre::Result<()> { + let scanner = Scanner { + chain_id: self.chain_id, + db: self.db.clone(), + default_start_block: self.config.start_block, + batch_size: self.config.batch_size, + event_filter: self.l1batch_finalized_filter(), + insert_event_fn: insert_l1batch_finalized, + cancellation_token: self.cancellation_token.clone(), + }; + + if let Err(e) = scanner.run().await { + error!("Error while scanning for events: {e:?}"); + self.cancellation_token.cancel(); + } + Ok(()) + } + + #[instrument(skip(self), err)] + async fn subscribe_l1batch_finalized(self: Arc) -> eyre::Result<()> { + let subscriber = Subscriber { + chain_id: self.chain_id, + db: self.db.clone(), + event_filter: self.l1batch_finalized_filter(), + insert_event_fn: insert_l1batch_finalized, + cancellation_token: self.cancellation_token.clone(), + }; + + if let Err(e) = subscriber.run().await { + error!("Error while subscribing to events: {e:?}"); + self.cancellation_token.cancel(); + } + + Ok(()) + } + + #[inline] + fn l1anchoring_queued_filter(&self) -> Event<&P, L1AnchoringQueued> { + self.manager + .L1AnchoringQueued_filter() + .address(*self.manager.address()) + } + + #[inline] + fn l1batch_arrived_filter(&self) -> Event<&P, L1BatchArrived> { + self.manager + .L1BatchArrived_filter() + .address(*self.manager.address()) + } + + #[inline] + fn l1batch_finalized_filter(&self) -> Event<&P, L1BatchFinalized> { + self.manager + .L1BatchFinalized_filter() + .address(*self.manager.address()) + } +} + +#[inline] +fn insert_l1anchoring_queued( + executor: &'_ mut SqliteConnection, + _chain_id: ChainId, + log_id: i64, + event: L1AnchoringQueued, +) -> BoxedFuture<'_, eyre::Result<()>> { + Box::pin(sql::anchoring_requests::insert_l1anchoring_queued( + executor, log_id, event, + )) +} + +#[inline] +fn insert_l1batch_arrived( + executor: &'_ mut SqliteConnection, + chain_id: ChainId, + log_id: i64, + event: L1BatchArrived, +) -> BoxedFuture<'_, eyre::Result<()>> { + Box::pin(sql::l1_batch::insert_l1batch_arrived( + executor, chain_id, log_id, event, + )) +} + +#[inline] +fn insert_l1batch_finalized( + executor: &'_ mut SqliteConnection, + chain_id: ChainId, + log_id: i64, + event: L1BatchFinalized, +) -> BoxedFuture<'_, eyre::Result<()>> { + Box::pin(sql::l1_batch::insert_l1batch_finalized( + executor, chain_id, log_id, event, + )) +} diff --git a/crates/relayer/src/lib.rs b/crates/relayer/src/lib.rs new file mode 100644 index 0000000..00e344a --- /dev/null +++ b/crates/relayer/src/lib.rs @@ -0,0 +1,137 @@ +//! Relayer for L1 anchoring. + +// Copyright (C) 2026 UTS Contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +#[macro_use] +extern crate tracing; + +use crate::sql::{l1_batch::L1Batch, stats::CulminationCosts}; +use axum::{ + Json, + extract::State, + http::{HeaderMap, StatusCode, header::CONTENT_TYPE}, + response::{IntoResponse, Response}, +}; +use serde::Serialize; +use std::sync::Arc; +use tokio_util::sync::CancellationToken; + +uts_sql::migrator!("./migrations"); + +/// Indexer. +pub mod indexer; + +/// Relayer. +pub mod relayer; + +pub(crate) mod sql; + +/// Configuration. +pub mod config; + +/// Application state shared across handlers. +#[derive(Debug)] +pub struct AppState { + /// Sqlite pool + pub db: sqlx::SqlitePool, +} + +#[derive(Debug, Serialize)] +struct Metrics { + total: i64, + pending: i64, + latest_batch: Option, + costs: CulminationCosts, +} + +fn internal_server_error() -> Response { + let mut headers = HeaderMap::with_capacity(1); + headers.insert(CONTENT_TYPE, "application/json".parse().unwrap()); + ( + StatusCode::INTERNAL_SERVER_ERROR, + headers, + r#"{"error": "Internal Server Error"}"#, + ) + .into_response() +} + +/// Handler for the `/metrics` endpoint, which returns JSON-formatted metrics about the calendar server. +pub async fn metrics(State(state): State>) -> Response { + let mut headers = HeaderMap::with_capacity(2); + headers.insert(CONTENT_TYPE, "application/json".parse().unwrap()); + // cache for 10s + headers.insert( + axum::http::header::CACHE_CONTROL, + "public, max-age=10".parse().unwrap(), + ); + + let Ok(total) = sql::l1_batch::count_l1batches(&state.db).await else { + return internal_server_error(); + }; + let Ok(latest_batch) = sql::l1_batch::get_latest_l1batch(&state.db).await else { + return internal_server_error(); + }; + let Ok(pending) = sql::anchoring_requests::count_pending_events( + &state.db, + latest_batch.map(|b| b.start_index + b.count).unwrap_or(1), + ) + .await + else { + return internal_server_error(); + }; + let Ok(costs) = sql::stats::get_culmination_costs(&state.db).await else { + return internal_server_error(); + }; + + let metrics = Metrics { + total, + pending, + latest_batch, + costs, + }; + + (StatusCode::OK, headers, Json(metrics)).into_response() +} + +/// Signal for graceful shutdown. +pub async fn shutdown_signal(cancellation_token: CancellationToken) { + use tokio::signal; + + let ctrl_c = async { + signal::ctrl_c() + .await + .expect("failed to install Ctrl+C handler"); + }; + + #[cfg(unix)] + let terminate = async { + signal::unix::signal(signal::unix::SignalKind::terminate()) + .expect("failed to install signal handler") + .recv() + .await; + }; + + #[cfg(not(unix))] + let terminate = std::future::pending::<()>(); + + tokio::select! { + _ = ctrl_c => {}, + _ = terminate => {}, + _ = cancellation_token.cancelled() => {}, + }; + + cancellation_token.cancel(); +} diff --git a/crates/relayer/src/main.rs b/crates/relayer/src/main.rs new file mode 100644 index 0000000..b8a8c3c --- /dev/null +++ b/crates/relayer/src/main.rs @@ -0,0 +1,140 @@ +//! The relayer for collecting L2 events and submitting them to L1. + +// Copyright (C) 2026 UTS Contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +use alloy_primitives::bytes::Bytes; +use alloy_provider::{ProviderBuilder, WsConnect}; +use alloy_rpc_client::ClientBuilder; +use alloy_signer_local::MnemonicBuilder; +use axum::{Router, http::StatusCode, response::Html, routing::get}; +use sqlx::{ + migrate, + sqlite::{SqliteConnectOptions, SqlitePoolOptions}, +}; +use std::{fs, sync::Arc}; +use tokio_util::sync::CancellationToken; +use tracing::info; +use uts_contracts::{gateway::L1AnchoringGateway, manager::L2AnchoringManager}; +use uts_relayer::{ + AppState, config::AppConfig, indexer::L2Indexer, relayer::Relayer, shutdown_signal, +}; + +const INDEX_PAGE_TEMPLATE: &str = include_str!("index.html"); + +#[tokio::main] +async fn main() -> eyre::Result<()> { + tracing_subscriber::fmt::init(); + + // FIXME: some crate enables `rustls/ring` cause `rustls` don't know which provider to use and thus requires the user to explicitly set the provider. + #[cfg(feature = "reqwest-rustls-tls")] + rustls::crypto::aws_lc_rs::default_provider() + .install_default() + .ok(); + + let config = AppConfig::new()?; + + let cancellation_token = CancellationToken::new(); + + fs::create_dir_all(config.db.sql.filename.parent().unwrap())?; + let db = SqlitePoolOptions::new() + .connect_with( + SqliteConnectOptions::new() + .filename(config.db.sql.filename) + .create_if_missing(true) + .foreign_keys(true), + ) + .await?; + migrate!().run(&db).await?; + + let key = MnemonicBuilder::from_phrase(&*config.blockchain.wallet.mnemonic) + .index(config.blockchain.wallet.index)? + .build()?; + let address = key.address(); + info!("Using address: {address}"); + + let l1_provider = ProviderBuilder::new() + .with_simple_nonce_management() + .wallet(key.clone()) + .connect_client( + ClientBuilder::default() + .layer(config.blockchain.rpc.retry.layer()) + .layer(config.blockchain.rpc.throttle.layer()) + .http(config.blockchain.rpc.l1.parse()?), + ); + let l2_provider = ProviderBuilder::new() + .with_simple_nonce_management() + .wallet(key.clone()) + .connect_client( + ClientBuilder::default() + .layer(config.blockchain.rpc.retry.layer()) + .layer(config.blockchain.rpc.throttle.layer()) + .ws(WsConnect::new(&*config.blockchain.rpc.l2_ws)) + .await?, + ); + + let gateway = L1AnchoringGateway::new(config.blockchain.gateway_address, l1_provider); + let manager = L2AnchoringManager::new(config.blockchain.manager_address, l2_provider); + + let l2_indexer = L2Indexer::new( + db.clone(), + manager.clone(), + config.indexer.l2, + cancellation_token.clone(), + ) + .await?; + + let relayer = Relayer::new( + db.clone(), + key.address(), + gateway, + manager, + config.relayer, + cancellation_token.clone(), + ) + .await?; + + let html = INDEX_PAGE_TEMPLATE + .replace("{{VERSION}}", env!("CARGO_PKG_VERSION")) + .replace("{{NODE_NAME}}", config.server.node_name.as_str()) + .replace("{{NODE_ADDRESS}}", &address.to_string()); + let html = Bytes::from(html); + + let app = Router::new() + .route("/", get(|| async move { Html(html.clone()) })) + .route("/healthcheck", get(|| async { StatusCode::NO_CONTENT })) + .route("/metrics", get(uts_relayer::metrics)) + .with_state(Arc::new(AppState { db })); + + let listener = tokio::net::TcpListener::bind(&*config.server.bind_address).await?; + + tokio::spawn( + axum::serve(listener, app) + .with_graceful_shutdown(shutdown_signal(cancellation_token.clone())) + .into_future(), + ); + + // spawn the subscriber tasks first. + tokio::spawn(l2_indexer.clone().start_subscribers()); + + // block waiting for catch up, ensure data is up to date before trying to submit any transactions to L1. + l2_indexer.start_scanners().await?; + + info!("L2 indexer is up to date, starting relayer"); + + relayer.run().await?; + + Ok(()) +} diff --git a/crates/relayer/src/relayer.rs b/crates/relayer/src/relayer.rs new file mode 100644 index 0000000..c63bf00 --- /dev/null +++ b/crates/relayer/src/relayer.rs @@ -0,0 +1,323 @@ +use crate::{ + config::RelayerConfig, + sql, + sql::{ + eth::insert_tx_receipt, + l1_batch::{L1Batch, L1BatchStatus}, + }, +}; +use alloy_primitives::{Address, B256, ChainId, U256}; +use alloy_provider::{PendingTransactionBuilder, Provider}; +use eyre::{Context, ContextCompat, bail}; +use jiff::Timestamp; +use sha3::{Keccak256, digest::Output}; +use sqlx::SqlitePool; +use std::time::Duration; +use tokio::{select, time::sleep}; +use tokio_util::sync::CancellationToken; +use uts_bmt::MerkleTree; +use uts_contracts::{ + eas::events::Timestamped, + gateway::L1AnchoringGateway, + manager::{L2AnchoringManager, events::L1BatchFinalized}, +}; + +/// Relayer responsible for packing L2 anchoring requests into batches, submitting them to L1, and finalizing them on L2. +#[derive(Debug)] +pub struct Relayer { + db: SqlitePool, + wallet: Address, + l1_chain_id: ChainId, + l2_chain_id: ChainId, + gateway: L1AnchoringGateway, + manager: L2AnchoringManager, + config: RelayerConfig, + cancellation_token: CancellationToken, +} + +impl Relayer { + /// Create a new instance of the relayer. + pub async fn new( + db: SqlitePool, + wallet: Address, + gateway: L1AnchoringGateway, + manager: L2AnchoringManager, + config: RelayerConfig, + cancellation_token: CancellationToken, + ) -> eyre::Result { + let l1_chain_id = gateway.provider().get_chain_id().await?; + let l2_chain_id = manager.provider().get_chain_id().await?; + Ok(Self { + db, + wallet, + l1_chain_id, + l2_chain_id, + gateway, + manager, + config, + cancellation_token, + }) + } + + /// Run the relayer loop until cancellation is requested. + pub async fn run(self) -> eyre::Result<()> { + loop { + select! { + may_err = self.tick() => { + match may_err { + Ok(should_tick_immediately) => { + if should_tick_immediately { + continue; + } + sleep(Duration::from_secs(self.config.tick_interval_seconds)).await; + } + Err(e) => { + error!("Error in relayer tick: {e:?}"); + self.cancellation_token.cancel(); + return Err(e) + } + } + } + _ = self.cancellation_token.cancelled() => { + info!("Cancellation requested, stopping relayer"); + break; + } + } + } + Ok(()) + } + + /// Perform one tick of the relayer loop. + /// + /// # Returns + /// + /// Whether you should immediately tick again without waiting for the next interval. + async fn tick(&self) -> eyre::Result { + let latest_batch = sql::l1_batch::get_latest_l1batch(&self.db).await?; + + match latest_batch { + None + | Some(L1Batch { + status: L1BatchStatus::L2Finalized, + .. + }) => self.may_pack_new_batch(latest_batch).await, + Some(batch) if batch.status == L1BatchStatus::Collected => { + self.send_attest_tx(batch).await?; + Ok(true) + } + Some(batch) if batch.status == L1BatchStatus::L1Sent => { + self.watch_l1_tx(batch).await?; + Ok(true) + } + Some(batch) if batch.status == L1BatchStatus::L1Mined => { + trace!( + "Batch {} is mined but not yet received on L2. Waiting for it to arrive...", + batch.id + ); + Ok(false) + } + Some(batch) if batch.status == L1BatchStatus::L2Received => { + self.send_finalize_batch_tx(batch).await?; + Ok(true) + } + Some(batch) if batch.status == L1BatchStatus::L2FinalizeTxSent => { + self.watch_finalize_batch_tx(batch).await?; + Ok(true) + } + _ => bail!("unreachable state"), + } + } + + /// Watch the L1 transaction for the given batch until it's mined, and update the batch status accordingly. + /// + /// # Returns + /// + /// Returns true if a new batch if packed and ready to be sent, so the relayer should tick again + /// immediately without waiting for the next interval. + #[instrument(skip_all, err)] + async fn may_pack_new_batch(&self, last_batch: Option) -> eyre::Result { + trace!("Checking if we can pack a new batch..."); + + // 1. Calculate the next batch's start index + let next_start_index = last_batch.map(|b| b.start_index + b.count).unwrap_or(1); + + // 2. Check required conditions for packing a new batch + let pending_counts = + sql::anchoring_requests::count_pending_events(&self.db, next_start_index).await?; + if pending_counts == 0 { + return Ok(false); + } + + let now = Timestamp::now(); + let last_batch_time = last_batch.map(|b| b.updated_at).unwrap_or(Timestamp::MIN); + let elapsed = now.duration_since(last_batch_time).as_secs(); + + if pending_counts < self.config.batch_max_size + && elapsed < self.config.batch_max_wait_seconds + { + return Ok(false); + } + + let counts = pending_counts.min(self.config.batch_max_size); + + info!( + start_index = next_start_index, + counts, + max_size_reached = pending_counts >= self.config.batch_max_size, + max_wait_reached = elapsed >= self.config.batch_max_wait_seconds, + "Sealing a new batch" + ); + + let leaves = + sql::anchoring_requests::load_roots_in_range(&self.db, next_start_index, counts) + .await? + .into_iter() + .map(|hash| Output::::from(hash.0)) + .collect::>(); + let trie = MerkleTree::::new(&leaves); + let root = B256::from_slice(trie.root()); + + sql::l1_batch::upsert_l1batch( + &self.db, + self.l2_chain_id, + root, + next_start_index, + counts, + L1BatchStatus::Collected, + ) + .await?; + + Ok(true) + } + + #[instrument(skip_all, fields(batch_id = batch.id), err)] + async fn send_attest_tx(&self, batch: L1Batch) -> eyre::Result<()> { + let pending_tx = self + .gateway + .submitBatch( + batch.root, + U256::from(batch.start_index), + U256::from(batch.count), + U256::from(self.config.l1_batch_submission_gas_limit), + ) + .value(self.config.l1_batch_submission_fee) + .send() + .await?; + + let tx_hash = pending_tx.tx_hash(); + info!(%tx_hash, "Batch submission transaction broadcasted"); + sql::l1_batch::update_l1batch_to_l1_sent(&self.db, batch.id, *tx_hash).await?; + + Ok(()) + } + + #[instrument(skip_all, fields(batch_id = batch.id), err)] + async fn watch_l1_tx(&self, batch: L1Batch) -> eyre::Result<()> { + let tx_hash = batch + .l1_tx_hash + .context("Batch is in L1Sent status but missing tx hash")?; + + info!(%tx_hash, "Watching for batch submission transaction to be mined..."); + + let provider = self.gateway.provider(); + let receipt = PendingTransactionBuilder::new(provider.root().clone(), tx_hash) + .get_receipt() + .await + .context("get receipt")?; + info!(%tx_hash, block_number = receipt.block_number, "Batch submission transaction mined"); + + // sanity check: make sure the transaction was successful + if !receipt.status() { + bail!("Batch submission transaction reverted"); + } + if receipt.decoded_log::().is_none() { + warn!( + "No Timestamped event found, this means the merkle root is timestamped by others before" + ); + } + + let block_number = receipt + .block_number + .context("missing block number in receipt")?; + let prev = provider + .get_balance(self.wallet) + .block_id((block_number - 1).into()) + .await?; + let current = provider + .get_balance(self.wallet) + .block_id(block_number.into()) + .await?; + let gas_fee = U256::from(u128::from(receipt.gas_used) * receipt.effective_gas_price); + let cross_chain_fee = if prev > current + gas_fee { + prev - current - gas_fee + } else { + U256::ZERO + }; + + let mut db_tx = self.db.begin().await?; + sql::l1_batch::compare_and_set_l1batch_status( + &mut *db_tx, + batch.id, + L1BatchStatus::L1Sent, + L1BatchStatus::L1Mined, + ) + .await?; + insert_tx_receipt(&mut *db_tx, self.l1_chain_id, &receipt).await?; + sql::stats::set_l1_gas_fee(&mut *db_tx, batch.id, gas_fee).await?; + sql::stats::set_cross_chain_fee(&mut *db_tx, batch.id, cross_chain_fee).await?; + db_tx.commit().await?; + + Ok(()) + } + + #[instrument(skip_all, fields(batch_id = batch.id), err)] + async fn send_finalize_batch_tx(&self, batch: L1Batch) -> eyre::Result<()> { + info!( + "Batch {} arrived on L2. Submitting finalization tx...", + batch.id + ); + + let pending_tx = self.manager.finalizeBatch().send().await?; + let tx_hash = *pending_tx.tx_hash(); + + sql::l1_batch::update_l1batch_to_l2finalize_tx_sent(&self.db, batch.id, tx_hash).await?; + info!(%tx_hash, "Finalize transaction broadcasted"); + + Ok(()) + } + + #[instrument(skip_all, fields(batch_id = batch.id), err)] + async fn watch_finalize_batch_tx(&self, batch: L1Batch) -> eyre::Result<()> { + let tx_hash = batch + .l2_tx_hash + .context("Batch is in L2FinalizeTxSent status but missing L2 tx hash")?; + + info!(%tx_hash, "Watching for finalize transaction to be mined..."); + + let provider = self.manager.provider(); + let receipt = PendingTransactionBuilder::new(provider.root().clone(), tx_hash) + .get_receipt() + .await + .context("get receipt")?; + let block_number = receipt + .block_number + .context("missing block number in log")?; + info!(%tx_hash, block_number, "Finalize transaction mined"); + let gas_fee = U256::from(u128::from(receipt.gas_used) * receipt.effective_gas_price); + + let mut db_tx = self.db.begin().await?; + insert_tx_receipt(&mut *db_tx, self.l2_chain_id, &receipt).await?; + sql::stats::set_l2_gas_fee(&mut *db_tx, batch.id, gas_fee).await?; + db_tx.commit().await?; + + // sanity check: make sure the transaction was successful + if !receipt.status() { + bail!("Finalize transaction reverted"); + } + if receipt.decoded_log::().is_none() { + bail!("No L1BatchFinalized event found in finalize transaction receipt"); + }; + + Ok(()) + } +} diff --git a/crates/relayer/src/sql.rs b/crates/relayer/src/sql.rs new file mode 100644 index 0000000..f30973a --- /dev/null +++ b/crates/relayer/src/sql.rs @@ -0,0 +1,5 @@ +pub mod anchoring_requests; +pub mod eth; +pub mod indexer; +pub mod l1_batch; +pub mod stats; diff --git a/crates/relayer/src/sql/anchoring_requests.rs b/crates/relayer/src/sql/anchoring_requests.rs new file mode 100644 index 0000000..fd1422c --- /dev/null +++ b/crates/relayer/src/sql/anchoring_requests.rs @@ -0,0 +1,80 @@ +use alloy_primitives::B256; +use eyre::Context; +use sqlx::{Executor, Sqlite}; +use uts_contracts::manager::events::L1AnchoringQueued; +use uts_sql::TextWrapper; + +#[instrument(skip(executor), ret(level = "trace"), err)] +pub async fn insert_l1anchoring_queued<'e, E: Executor<'e, Database = Sqlite>>( + executor: E, + log_id: i64, + event: L1AnchoringQueued, +) -> eyre::Result<()> { + let attestation_id = TextWrapper(event.attestationId); + let root = TextWrapper(event.root); + let queue_index: i64 = event.queueIndex.to(); + let fee: i64 = event.fee.to(); + let block_number: i64 = event.blockNumber.to(); + let timestamp: i64 = event.timestamp.to(); + + sqlx::query!( + r#" + INSERT INTO l1_anchoring_queued (internal_log_id, attestation_id, root, queue_index, fee, block_number, timestamp) + VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7) + ON CONFLICT DO NOTHING + "#, + log_id, + attestation_id, + root, + queue_index, + fee, + block_number, + timestamp, + ) + .execute(executor) + .await.context("insert l1anchoring queued")?; + Ok(()) +} + +#[instrument(skip(executor), ret(level = "trace"), err)] +pub async fn count_pending_events<'e, E: Executor<'e, Database = Sqlite>>( + executor: E, + next_start_index: i64, +) -> eyre::Result { + let count = sqlx::query_scalar!( + r#" + SELECT COUNT(*) as "count!" + FROM l1_anchoring_queued + WHERE queue_index >= ?1 + "#, + next_start_index, + ) + .fetch_one(executor) + .await + .context("count pending events")?; + Ok(count) +} + +#[instrument(skip(executor), ret(level = "trace"), err)] +pub async fn load_roots_in_range<'e, E: Executor<'e, Database = Sqlite>>( + executor: E, + start_index: i64, + count: i64, +) -> eyre::Result> { + let rows = sqlx::query_scalar!( + r#" + SELECT root as "root: TextWrapper" + FROM l1_anchoring_queued + WHERE queue_index >= ?1 + ORDER BY queue_index + LIMIT ?2 + "#, + start_index, + count, + ) + .fetch_all(executor) + .await + .context("load roots in range")?; + + Ok(rows.into_iter().map(|r| r.0).collect()) +} diff --git a/crates/relayer/src/sql/eth.rs b/crates/relayer/src/sql/eth.rs new file mode 100644 index 0000000..9039104 --- /dev/null +++ b/crates/relayer/src/sql/eth.rs @@ -0,0 +1,166 @@ +use alloy_primitives::{B256, BlockNumber, ChainId, U256}; +use alloy_rpc_types_eth::{Log, TransactionReceipt}; +use eyre::{Context, ContextCompat}; +use sqlx::{Executor, Sqlite}; +use uts_sql::TextWrapper; + +/// Insert a new Ethereum block into the database. If a block with the same (chain_id, block_hash) already exists, do nothing. +#[instrument(skip(executor), ret(level = "trace"), err)] +pub async fn insert_block<'e, E: Executor<'e, Database = Sqlite>>( + executor: E, + chain_id: ChainId, + block_hash: B256, + block_number: BlockNumber, +) -> eyre::Result { + let chain_id: i64 = chain_id.try_into().context("chain id overflow")?; + let block_number: i64 = block_number.try_into().context("block number overflow")?; + let block_hash = TextWrapper(block_hash); + let block_id = sqlx::query_scalar!( + r#" + INSERT INTO eth_block (chain_id, block_hash, block_number) + VALUES (?1, ?2, ?3) + ON CONFLICT DO UPDATE SET block_number = excluded.block_number + RETURNING id + "#, + chain_id, + block_hash, + block_number, + ) + .fetch_one(executor) + .await + .context("insert block")?; + Ok(block_id) +} + +/// Insert a new Ethereum transaction into the database. If a transaction with the same (internal_block_id, transaction_index) already exists, do nothing. +#[instrument(skip(executor), ret(level = "trace"), err)] +pub async fn insert_transaction<'e, E: Executor<'e, Database = Sqlite>>( + executor: E, + block_id: i64, + transaction_index: u64, + transaction_hash: B256, +) -> eyre::Result { + let transaction_index: i64 = transaction_index + .try_into() + .context("transaction index overflow")?; + let transaction_hash = TextWrapper(transaction_hash); + let transaction_id = sqlx::query_scalar!( + r#" + INSERT INTO eth_transaction (internal_block_id, transaction_index, transaction_hash) + VALUES (?1, ?2, ?3) + ON CONFLICT DO UPDATE SET transaction_hash = excluded.transaction_hash + RETURNING id as "id!" + "#, + block_id, + transaction_index, + transaction_hash, + ) + .fetch_one(executor) + .await + .context("insert transaction")?; + Ok(transaction_id) +} + +#[instrument( + skip(executor, log), + fields( + %address = log.address(), + %topic0 = log.topic0().unwrap_or_default(), + %transaction_hash = log.transaction_hash.unwrap_or_default(), + %log_index = log.log_index.unwrap_or_default(), + ), + ret(level = "trace"), + err +)] +pub async fn insert_log(executor: &mut E, chain_id: ChainId, log: &Log) -> eyre::Result +where + for<'e> &'e mut E: Executor<'e, Database = Sqlite>, +{ + let block_hash = log.block_hash.context("missing block hash")?; + let block_number = log.block_number.context("missing block number")?; + let block_id = insert_block(&mut *executor, chain_id, block_hash, block_number).await?; + + let transaction_index = log.transaction_index.context("missing transaction index")?; + let transaction_hash = log.transaction_hash.context("missing transaction hash")?; + let transaction_id = insert_transaction( + &mut *executor, + block_id, + transaction_index, + transaction_hash, + ) + .await?; + + let log_index: i64 = log + .log_index + .context("missing log index")? + .try_into() + .context("i64 overflow")?; + + let log_id = sqlx::query_scalar!( + r#" + INSERT INTO eth_log (internal_transaction_id, log_index) + VALUES (?1, ?2) + ON CONFLICT DO UPDATE SET log_index = excluded.log_index + RETURNING id as "id!" + "#, + transaction_id, + log_index, + ) + .fetch_one(&mut *executor) + .await + .context("insert log")?; + + Ok(log_id) +} + +#[instrument(skip(executor, tx_receipt), fields(%tx_hash = tx_receipt.transaction_hash), err)] +pub async fn insert_tx_receipt( + executor: &'_ mut E, + chain_id: ChainId, + tx_receipt: &TransactionReceipt, +) -> eyre::Result<()> +where + for<'e> &'e mut E: Executor<'e, Database = Sqlite>, +{ + let block_hash = tx_receipt.block_hash.context("missing block hash")?; + let block_number = tx_receipt.block_number.context("missing block number")?; + let block_id = insert_block(&mut *executor, chain_id, block_hash, block_number).await?; + + let transaction_index = tx_receipt + .transaction_index + .context("missing transaction index")?; + let transaction_id = insert_transaction( + &mut *executor, + block_id, + transaction_index, + tx_receipt.transaction_hash, + ) + .await?; + + let from_address = TextWrapper(tx_receipt.from); + let to_address = TextWrapper(tx_receipt.to.context("missing to address")?); + let gas_used: i64 = tx_receipt.gas_used.try_into().context("i64 overflow")?; + let effective_gas_price = TextWrapper(U256::from(tx_receipt.effective_gas_price)); + + sqlx::query!( + r#" + INSERT INTO tx_receipt (internal_transaction_id, gas_used, effective_gas_price, from_address, to_address) + VALUES (?1, ?2, ?3, ?4, ?5) + ON CONFLICT(internal_transaction_id) DO UPDATE SET + gas_used = excluded.gas_used, + effective_gas_price = excluded.effective_gas_price, + from_address = excluded.from_address, + to_address = excluded.to_address + "#, + transaction_id, + gas_used, + effective_gas_price, + from_address, + to_address, + ) + .execute(&mut *executor) + .await + .context("insert tx receipt")?; + + Ok(()) +} diff --git a/crates/relayer/src/sql/indexer.rs b/crates/relayer/src/sql/indexer.rs new file mode 100644 index 0000000..8f7ab98 --- /dev/null +++ b/crates/relayer/src/sql/indexer.rs @@ -0,0 +1,58 @@ +use alloy_primitives::{B256, BlockNumber, ChainId}; +use eyre::Context; +use sqlx::{Executor, Sqlite}; +use uts_sql::TextWrapper; + +// CREATE TABLE IF NOT EXISTS indexer_cursors ( +// chain_id INTEGER PRIMARY KEY, +// event_signature TEXT NOT NULL, +// last_indexed_block INTEGER NOT NULL, +// updated_at DATETIME DEFAULT CURRENT_TIMESTAMP +// ); +#[instrument(skip(executor), err)] +pub async fn update_cursor<'e, E: Executor<'e, Database = Sqlite>>( + executor: E, + chain_id: ChainId, + event_signature_hash: B256, + last_indexed_block: BlockNumber, +) -> eyre::Result<()> { + let chain_id: i64 = chain_id.try_into().context("i64 overflow")?; + let event_signature_hash = TextWrapper(event_signature_hash); + let last_indexed_block: i64 = last_indexed_block.try_into().context("i64 overflow")?; + sqlx::query!( + r#" + INSERT INTO indexer_cursors (chain_id, event_signature_hash, last_indexed_block) + VALUES (?1, ?2, ?3) + ON CONFLICT(chain_id, event_signature_hash) DO UPDATE SET last_indexed_block = excluded.last_indexed_block, updated_at = CURRENT_TIMESTAMP + "#, + chain_id, + event_signature_hash, + last_indexed_block, + ) + .execute(executor) + .await.context("update cursor")?; + Ok(()) +} + +#[instrument(skip(executor), ret(level = "trace"), err)] +pub async fn load_cursor<'e, E: Executor<'e, Database = Sqlite>>( + executor: E, + chain_id: ChainId, + event_signature_hash: B256, +) -> eyre::Result> { + let chain_id: i64 = chain_id.try_into().context("i64 overflow")?; + let event_signature_hash = TextWrapper(event_signature_hash); + let record = sqlx::query!( + r#" + SELECT last_indexed_block FROM indexer_cursors + WHERE chain_id = ?1 AND event_signature_hash = ?2 + "#, + chain_id, + event_signature_hash, + ) + .fetch_optional(executor) + .await + .context("load cursor")?; + + Ok(record.map(|r| r.last_indexed_block as BlockNumber)) +} diff --git a/crates/relayer/src/sql/l1_batch.rs b/crates/relayer/src/sql/l1_batch.rs new file mode 100644 index 0000000..beaa4fd --- /dev/null +++ b/crates/relayer/src/sql/l1_batch.rs @@ -0,0 +1,321 @@ +use alloy_primitives::{B256, ChainId}; +use eyre::{Context, ContextCompat}; +use jiff::Timestamp; +use serde::Serialize; +use sqlx::{Executor, Sqlite, SqliteConnection}; +use uts_contracts::manager::events::{L1BatchArrived, L1BatchFinalized}; +use uts_sql::{TextWrapper, define_text_enum}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, strum::IntoStaticStr, strum::EnumString, Serialize)] +pub enum L1BatchStatus { + Collected, + L1Sent, + L1Mined, + L2Received, + L2FinalizeTxSent, + L2Finalized, +} +define_text_enum!(L1BatchStatus); + +#[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize)] +pub struct L1Batch { + pub id: i64, + pub l2_chain_id: ChainId, + pub start_index: i64, + pub count: i64, + pub root: B256, + pub l1_tx_hash: Option, + pub l2_tx_hash: Option, + pub status: L1BatchStatus, + pub created_at: Timestamp, + pub updated_at: Timestamp, +} + +pub async fn count_l1batches<'e, E: Executor<'e, Database = Sqlite>>( + executor: E, +) -> eyre::Result { + let count = sqlx::query_scalar!( + r#" + SELECT COUNT(*) FROM l1_batch + "# + ) + .fetch_one(executor) + .await + .context("count l1 batches")?; + Ok(count) +} + +/// Insert a new L1 batch or update the existing one with the same (l2_chain_id, start_index) combination. +#[instrument(skip(executor), ret(level = "trace"), err)] +pub async fn upsert_l1batch<'e, E: Executor<'e, Database = Sqlite>>( + executor: E, + l2_chain_id: ChainId, + root: B256, + start_index: i64, + count: i64, + status: L1BatchStatus, +) -> eyre::Result> { + let l2_chain_id: i64 = l2_chain_id.try_into().context("i64 overflow")?; + let root = TextWrapper(root); + + let rec = sqlx::query!( + r#" + INSERT INTO l1_batch (l2_chain_id, start_index, count, root, status) + VALUES (?1, ?2, ?3, ?4, ?5) + ON CONFLICT(l2_chain_id, start_index) DO UPDATE SET + count = excluded.count, + root = excluded.root, + status = excluded.status + WHERE + CASE excluded.status + WHEN 'Collected' THEN 1 + WHEN 'L1Sent' THEN 2 + WHEN 'L1Mined' THEN 3 + WHEN 'L2Received' THEN 4 + WHEN 'L2FinalizeTxSent' THEN 5 + WHEN 'L2Finalized' THEN 6 + ELSE 0 + END + >= + CASE l1_batch.status + WHEN 'Collected' THEN 1 + WHEN 'L1Sent' THEN 2 + WHEN 'L1Mined' THEN 3 + WHEN 'L2Received' THEN 4 + WHEN 'L2FinalizeTxSent' THEN 5 + WHEN 'L2Finalized' THEN 6 + ELSE 0 + END + RETURNING + id, + l1_tx_hash as "l1_tx_hash: TextWrapper", + l2_tx_hash as "l2_tx_hash: TextWrapper", + created_at, + updated_at + "#, + l2_chain_id, + start_index, + count, + root, + status + ) + .fetch_optional(executor) + .await + .context("upsert l1batch")?; + + let Some(rec) = rec else { + return Ok(None); + }; + + Ok(Some(L1Batch { + id: rec.id, + l2_chain_id: l2_chain_id as u64, + start_index, + count, + root: root.0, + status, + l1_tx_hash: rec.l1_tx_hash.map(|h| h.0), + l2_tx_hash: rec.l2_tx_hash.map(|h| h.0), + created_at: Timestamp::from_second(rec.created_at).context("timestamp overflow")?, + updated_at: Timestamp::from_second(rec.updated_at).context("timestamp overflow")?, + })) +} + +#[instrument(skip(executor), err)] +pub async fn update_l1batch_to_l1_sent<'e, E: Executor<'e, Database = Sqlite>>( + executor: E, + batch_id: i64, + l1_tx_hash: B256, +) -> eyre::Result<()> { + let l1_tx_hash = TextWrapper(l1_tx_hash); + sqlx::query!( + r#" + UPDATE l1_batch + SET l1_tx_hash = ?1, status = ?2, updated_at = unixepoch() + WHERE id = ?3 + "#, + l1_tx_hash, + L1BatchStatus::L1Sent, + batch_id + ) + .execute(executor) + .await + .context("update l1batch to l1 sent")?; + Ok(()) +} + +#[instrument(skip(executor), err)] +pub async fn update_l1batch_to_l2finalize_tx_sent<'e, E: Executor<'e, Database = Sqlite>>( + executor: E, + batch_id: i64, + l2_tx_hash: B256, +) -> eyre::Result<()> { + let l2_tx_hash = TextWrapper(l2_tx_hash); + sqlx::query!( + r#" + UPDATE l1_batch + SET l2_tx_hash = ?1, status = ?2, updated_at = unixepoch() + WHERE id = ?3 + "#, + l2_tx_hash, + L1BatchStatus::L2FinalizeTxSent, + batch_id + ) + .execute(executor) + .await + .context("update l1batch to l2 finalize tx sent")?; + Ok(()) +} + +#[instrument(skip(executor), err)] +pub async fn compare_and_set_l1batch_status<'e, E: Executor<'e, Database = Sqlite>>( + executor: E, + batch_id: i64, + current_status: L1BatchStatus, + new_status: L1BatchStatus, +) -> eyre::Result<()> { + sqlx::query!( + r#" + UPDATE l1_batch + SET status = ?1, updated_at = unixepoch() + WHERE id = ?2 AND status = ?3 + "#, + new_status, + batch_id, + current_status + ) + .execute(executor) + .await + .context("compare and set l1batch status")?; + Ok(()) +} + +#[instrument(skip(executor), ret(level = "trace"), err)] +pub async fn get_latest_l1batch<'e, E: Executor<'e, Database = Sqlite>>( + executor: E, +) -> eyre::Result> { + let rec = sqlx::query!( + r#" + SELECT + id, + l2_chain_id, + start_index, + count, + root as "root: TextWrapper", + l1_tx_hash as "l1_tx_hash: TextWrapper", + l2_tx_hash as "l2_tx_hash: TextWrapper", + status as "status: L1BatchStatus", + created_at, + updated_at + FROM l1_batch + ORDER BY id DESC LIMIT 1 + "# + ) + .fetch_optional(executor) + .await + .context("select latest l1batch")?; + + let Some(rec) = rec else { + return Ok(None); + }; + Ok(Some(L1Batch { + id: rec.id, + l2_chain_id: rec.l2_chain_id as u64, + start_index: rec.start_index, + count: rec.count, + root: rec.root.0, + status: rec.status, + l1_tx_hash: rec.l1_tx_hash.map(|h| h.0), + l2_tx_hash: rec.l2_tx_hash.map(|h| h.0), + created_at: Timestamp::from_second(rec.created_at).context("timestamp overflow")?, + updated_at: Timestamp::from_second(rec.updated_at).context("timestamp overflow")?, + })) +} + +#[instrument(skip(executor), err)] +pub async fn insert_l1batch_arrived( + executor: &'_ mut SqliteConnection, + chain_id: ChainId, + log_id: i64, + event: L1BatchArrived, +) -> eyre::Result<()> { + let Some(l1_batch) = upsert_l1batch( + &mut *executor, + chain_id, + event.claimedRoot, + event.startIndex.to(), + event.count.to(), + L1BatchStatus::L2Received, + ) + .await? + else { + warn!("Existing L1 batch has newer status, skipping insert_l1batch_arrived"); + return Ok(()); + }; + + let l1_block_attested: i64 = event.l1BlockAttested.try_into().context("i64 overflow")?; + let l1_timestamp_attested: i64 = event + .l1TimestampAttested + .try_into() + .context("i64 overflow")?; + let l2_block_number: i64 = event.l2BlockNumber.try_into().context("i64 overflow")?; + let l2_timestamp_received: i64 = event + .l2TimestampReceived + .try_into() + .context("i64 overflow")?; + + sqlx::query!( + r#" + INSERT INTO l1_batch_arrived (l1_batch_id, internal_log_id, l1_block_attested, l1_timestamp_attested, l2_block_number, l2_timestamp_received) + VALUES (?1, ?2, ?3, ?4, ?5, ?6) + ON CONFLICT DO NOTHING + "#, + l1_batch.id, + log_id, + l1_block_attested, + l1_timestamp_attested, + l2_block_number, + l2_timestamp_received, + ).execute(executor).await.context("insert l1 batch arrived")?; + + Ok(()) +} + +#[instrument(skip(executor), err)] +pub async fn insert_l1batch_finalized( + executor: &'_ mut SqliteConnection, + chain_id: ChainId, + log_id: i64, + event: L1BatchFinalized, +) -> eyre::Result<()> { + let l1_batch = upsert_l1batch( + &mut *executor, + chain_id, + event.merkleRoot, + event.startIndex.to(), + event.count.to(), + L1BatchStatus::L2Finalized, + ) + .await? + .context("finalized is the final status, but upsert did not return the batch")?; + + let l2_block_number: i64 = event.l2BlockNumber.try_into().context("i64 overflow")?; + let l2_timestamp_finalized: i64 = event + .l2TimestampFinalized + .try_into() + .context("i64 overflow")?; + + sqlx::query!( + r#" + INSERT INTO l1_batch_finalized (l1_batch_id, internal_log_id, l2_block_number, l2_timestamp_finalized) + VALUES (?1, ?2, ?3, ?4) + ON CONFLICT DO NOTHING + "#, + l1_batch.id, + log_id, + l2_block_number, + l2_timestamp_finalized, + ).execute(executor).await.context("insert l1 batch finalized")?; + + Ok(()) +} diff --git a/crates/relayer/src/sql/stats.rs b/crates/relayer/src/sql/stats.rs new file mode 100644 index 0000000..2cc2ac4 --- /dev/null +++ b/crates/relayer/src/sql/stats.rs @@ -0,0 +1,108 @@ +use alloy_primitives::U256; +use eyre::Context; +use serde::Serialize; +use sqlx::{Executor, Sqlite}; +use uts_sql::TextWrapper; + +// CREATE TABLE IF NOT EXISTS batch_fee ( +// id INTEGER PRIMARY KEY AUTOINCREMENT, +// internal_batch_id INTEGER NOT NULL REFERENCES l1_batch (id) ON DELETE CASCADE, +// +// l1_gas_fee TEXT NOT NULL DEFAULT 0, +// l2_gas_fee TEXT NOT NULL DEFAULT 0, +// cross_chain_fee TEXT NOT NULL DEFAULT 0, +// ); +// CREATE UNIQUE INDEX IF NOT EXISTS idx_batch_fee_batch_id ON batch_fee (internal_batch_id); +pub async fn set_l1_gas_fee<'e, E: Executor<'e, Database = Sqlite>>( + executor: E, + batch_id: i64, + l1_gas_fee: U256, +) -> eyre::Result<()> { + let l1_gas_fee = TextWrapper(l1_gas_fee); + sqlx::query!( + r#" + INSERT INTO batch_fee (internal_batch_id, l1_gas_fee, l2_gas_fee, cross_chain_fee) + VALUES (?1, ?2, 0, 0) + ON CONFLICT(internal_batch_id) DO UPDATE SET l1_gas_fee = excluded.l1_gas_fee + "#, + batch_id, + l1_gas_fee, + ) + .execute(executor) + .await + .context("insert or update batch fee")?; + + Ok(()) +} + +pub async fn set_l2_gas_fee<'e, E: Executor<'e, Database = Sqlite>>( + executor: E, + batch_id: i64, + l2_gas_fee: U256, +) -> eyre::Result<()> { + let l2_gas_fee = TextWrapper(l2_gas_fee); + sqlx::query!( + r#" + INSERT INTO batch_fee (internal_batch_id, l1_gas_fee, l2_gas_fee, cross_chain_fee) + VALUES (?1, 0, ?2, 0) + ON CONFLICT(internal_batch_id) DO UPDATE SET l2_gas_fee = excluded.l2_gas_fee + "#, + batch_id, + l2_gas_fee, + ) + .execute(executor) + .await + .context("insert or update batch fee")?; + + Ok(()) +} + +pub async fn set_cross_chain_fee<'e, E: Executor<'e, Database = Sqlite>>( + executor: E, + batch_id: i64, + cross_chain_fee: U256, +) -> eyre::Result<()> { + let cross_chain_fee = TextWrapper(cross_chain_fee); + sqlx::query!( + r#" + INSERT INTO batch_fee (internal_batch_id, l1_gas_fee, l2_gas_fee, cross_chain_fee) + VALUES (?1, 0, 0, ?2) + ON CONFLICT(internal_batch_id) DO UPDATE SET cross_chain_fee = excluded.cross_chain_fee + "#, + batch_id, + cross_chain_fee, + ) + .execute(executor) + .await + .context("insert or update batch fee")?; + Ok(()) +} + +#[derive(Debug, Default, Serialize)] +pub struct CulminationCosts { + pub l1_gas_fee: f32, + pub l2_gas_fee: f32, + pub cross_chain_fee: f32, +} + +pub async fn get_culmination_costs<'e, E: Executor<'e, Database = Sqlite>>( + executor: E, +) -> eyre::Result { + let record = sqlx::query!( + r#" + SELECT SUM(CAST(l1_gas_fee AS REAL)) as l1_gas_fee, + SUM(CAST(l2_gas_fee AS REAL)) as l2_gas_fee, + SUM(CAST(cross_chain_fee AS REAL)) as cross_chain_fee + FROM batch_fee + "#, + ) + .fetch_one(executor) + .await + .context("get culmination costs")?; + + Ok(CulminationCosts { + l1_gas_fee: record.l1_gas_fee.unwrap_or(0.0) as f32, + l2_gas_fee: record.l2_gas_fee.unwrap_or(0.0) as f32, + cross_chain_fee: record.cross_chain_fee.unwrap_or(0.0) as f32, + }) +} diff --git a/crates/sql/Cargo.toml b/crates/sql/Cargo.toml new file mode 100644 index 0000000..a0ce4fb --- /dev/null +++ b/crates/sql/Cargo.toml @@ -0,0 +1,18 @@ +[package] +authors.workspace = true +categories = ["database"] +description = "SQLite database helpers for Universal Timestamps Project" +edition.workspace = true +homepage.workspace = true +keywords = ["sql", "sqlx", "uts"] +license = "AGPL-3.0-or-later" +name = "uts-sql" +repository.workspace = true +version.workspace = true + +[dependencies] +alloy-primitives = { workspace = true } +sqlx = { workspace = true, features = ["sqlite"] } + +[lints] +workspace = true diff --git a/crates/sql/LICENSE-AGPL b/crates/sql/LICENSE-AGPL new file mode 120000 index 0000000..5f5cf25 --- /dev/null +++ b/crates/sql/LICENSE-AGPL @@ -0,0 +1 @@ +../../LICENSE-AGPL \ No newline at end of file diff --git a/crates/sql/src/alloy.rs b/crates/sql/src/alloy.rs new file mode 100644 index 0000000..6b188a8 --- /dev/null +++ b/crates/sql/src/alloy.rs @@ -0,0 +1,99 @@ +use crate::TextWrapper; +use alloy_primitives::{Address, FixedBytes, Uint, hex}; +use sqlx::{ + Encode, Sqlite, Type, + encode::IsNull, + error::BoxDynError, + sqlite::{SqliteArgumentValue, SqliteTypeInfo}, +}; + +impl Type for TextWrapper> { + fn type_info() -> SqliteTypeInfo { + >::type_info() + } + + fn compatible(ty: &SqliteTypeInfo) -> bool { + >::compatible(ty) + } +} + +impl Encode<'_, Sqlite> for TextWrapper> { + fn encode_by_ref( + &self, + args: &mut Vec>, + ) -> Result { + let s = hex::encode(self.0.as_slice()); + args.push(SqliteArgumentValue::Text(s.into())); + Ok(IsNull::No) + } +} + +impl<'r, const N: usize> ::sqlx::Decode<'r, Sqlite> for TextWrapper> { + fn decode(value: ::sqlx::sqlite::SqliteValueRef<'r>) -> Result { + let text = <&str as ::sqlx::Decode>::decode(value)?; + let bytes = hex::decode(text)?; + if bytes.len() != N { + return Err(format!("expected {N} bytes but got {}", bytes.len()).into()); + } + let mut ret = [0u8; N]; + ret.copy_from_slice(&bytes); + Ok(TextWrapper(ret.into())) + } +} + +impl Type for TextWrapper> { + fn type_info() -> SqliteTypeInfo { + >::type_info() + } + + fn compatible(ty: &SqliteTypeInfo) -> bool { + >::compatible(ty) + } +} + +impl Encode<'_, Sqlite> for TextWrapper> { + fn encode_by_ref( + &self, + args: &mut Vec>, + ) -> Result { + let s = self.0.to_string(); + args.push(SqliteArgumentValue::Text(s.into())); + Ok(IsNull::No) + } +} + +impl<'r, const BITS: usize, const LIMBS: usize> ::sqlx::Decode<'r, Sqlite> + for TextWrapper> +{ + fn decode(value: ::sqlx::sqlite::SqliteValueRef<'r>) -> Result { + let text = <&str as ::sqlx::Decode>::decode(value)?; + let uint = text.parse::>()?; + Ok(TextWrapper(uint)) + } +} + +impl Type for TextWrapper

{ + fn type_info() -> SqliteTypeInfo { + >::type_info() + } + + fn compatible(ty: &SqliteTypeInfo) -> bool { + >::compatible(ty) + } +} + +impl Encode<'_, Sqlite> for TextWrapper
{ + fn encode_by_ref( + &self, + args: &mut Vec>, + ) -> Result { + TextWrapper(self.0.0).encode_by_ref(args) + } +} + +impl<'r> ::sqlx::Decode<'r, Sqlite> for TextWrapper
{ + fn decode(value: ::sqlx::sqlite::SqliteValueRef<'r>) -> Result { + let text_wrapper = > as ::sqlx::Decode>::decode(value)?; + Ok(TextWrapper(Address(text_wrapper.0))) + } +} diff --git a/crates/sql/src/lib.rs b/crates/sql/src/lib.rs new file mode 100644 index 0000000..cc588ad --- /dev/null +++ b/crates/sql/src/lib.rs @@ -0,0 +1,23 @@ +//! This crate provides utilities for working with SQL databases. + +// Copyright (C) 2026 UTS Contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +mod alloy; +mod macros; + +/// Wrapper type for implementing sqlx Encode and Decode for types by converting them to and from text. +#[derive(Debug)] +pub struct TextWrapper(pub T); diff --git a/crates/sql/src/macros.rs b/crates/sql/src/macros.rs new file mode 100644 index 0000000..d2b0a0e --- /dev/null +++ b/crates/sql/src/macros.rs @@ -0,0 +1,58 @@ +/// A helper macro to define a text-based enum that can be stored in a SQLite database. +/// The enum must implement `Into<&'static str>` and `FromStr`. +#[macro_export] +macro_rules! define_text_enum { + ($ty:ty) => { + impl ::sqlx::Type for $ty { + fn type_info() -> ::sqlx::sqlite::SqliteTypeInfo { + >::type_info() + } + + fn compatible(ty: &::sqlx::sqlite::SqliteTypeInfo) -> bool { + ::sqlx::TypeInfo::name(ty) == "TEXT" + } + } + + impl<'q> ::sqlx::Encode<'q, ::sqlx::Sqlite> for $ty { + fn encode_by_ref( + &self, + args: &mut Vec<::sqlx::sqlite::SqliteArgumentValue<'q>>, + ) -> Result<::sqlx::encode::IsNull, ::sqlx::error::BoxDynError> { + let s: &'static str = self.into(); + args.push(::sqlx::sqlite::SqliteArgumentValue::Text(s.into())); + + Ok(::sqlx::encode::IsNull::No) + } + } + + impl<'r> ::sqlx::Decode<'r, sqlx::Sqlite> for $ty { + fn decode( + value: ::sqlx::sqlite::SqliteValueRef<'r>, + ) -> Result { + let text = <&str as ::sqlx::Decode<::sqlx::Sqlite>>::decode(value)?; + ::core::str::FromStr::from_str(text) + .map_err(|e| Box::new(e) as ::sqlx::error::BoxDynError) + } + } + }; +} + +/// A helper macro to define a `migrate` function that runs database migrations from a specified directory. +#[macro_export] +macro_rules! migrator { + ($dir:literal) => { + /// Run database migrations. This should be run at the start of the application to ensure the + /// database schema is up to date. + #[inline] + pub async fn migrate<'a, A>(migrator: A) -> Result<(), ::sqlx::migrate::MigrateError> + where + A: ::sqlx::Acquire<'a>, + ::Target: ::sqlx::migrate::Migrate, + { + ::sqlx::migrate!($dir).run(migrator).await + } + }; + () => { + migrator!("./migrations"); + }; +} diff --git a/crates/stamper/.sqlx/query-10f71d6880933717311b006461b2a4fa2b14bd3f0566b5ff0052b36e5c134958.json b/crates/stamper/.sqlx/query-10f71d6880933717311b006461b2a4fa2b14bd3f0566b5ff0052b36e5c134958.json new file mode 100644 index 0000000..ea9cccc --- /dev/null +++ b/crates/stamper/.sqlx/query-10f71d6880933717311b006461b2a4fa2b14bd3f0566b5ff0052b36e5c134958.json @@ -0,0 +1,32 @@ +{ + "db_name": "SQLite", + "query": "\n SELECT\n chain_id,\n tx_hash as \"tx_hash!: TextWrapper\",\n block_number as \"block_number!\"\n FROM attest_attempts\n WHERE tx_hash IS NOT NULL AND block_number IS NOT NULL\n ORDER BY created_at DESC\n LIMIT 1\n ", + "describe": { + "columns": [ + { + "name": "chain_id", + "ordinal": 0, + "type_info": "Integer" + }, + { + "name": "tx_hash!: TextWrapper", + "ordinal": 1, + "type_info": "Text" + }, + { + "name": "block_number!", + "ordinal": 2, + "type_info": "Integer" + } + ], + "parameters": { + "Right": 0 + }, + "nullable": [ + false, + true, + true + ] + }, + "hash": "10f71d6880933717311b006461b2a4fa2b14bd3f0566b5ff0052b36e5c134958" +} diff --git a/crates/stamper/.sqlx/query-285c2e7fa17b5daf195f7f967d9c78c1261f72ff4a8f0cac619942be58259c60.json b/crates/stamper/.sqlx/query-285c2e7fa17b5daf195f7f967d9c78c1261f72ff4a8f0cac619942be58259c60.json new file mode 100644 index 0000000..532d99f --- /dev/null +++ b/crates/stamper/.sqlx/query-285c2e7fa17b5daf195f7f967d9c78c1261f72ff4a8f0cac619942be58259c60.json @@ -0,0 +1,26 @@ +{ + "db_name": "SQLite", + "query": "\n SELECT id, result as \"result: AttestationResult\"\n FROM pending_attestations\n WHERE trie_root = ?1\n ", + "describe": { + "columns": [ + { + "name": "id", + "ordinal": 0, + "type_info": "Integer" + }, + { + "name": "result: AttestationResult", + "ordinal": 1, + "type_info": "Text" + } + ], + "parameters": { + "Right": 1 + }, + "nullable": [ + false, + false + ] + }, + "hash": "285c2e7fa17b5daf195f7f967d9c78c1261f72ff4a8f0cac619942be58259c60" +} diff --git a/crates/stamper/.sqlx/query-2e7fbf51720dc5f655ea7e45fc6e7b236b75030807a8fdb6a4f5e32eae008241.json b/crates/stamper/.sqlx/query-2e7fbf51720dc5f655ea7e45fc6e7b236b75030807a8fdb6a4f5e32eae008241.json new file mode 100644 index 0000000..3edd5cf --- /dev/null +++ b/crates/stamper/.sqlx/query-2e7fbf51720dc5f655ea7e45fc6e7b236b75030807a8fdb6a4f5e32eae008241.json @@ -0,0 +1,20 @@ +{ + "db_name": "SQLite", + "query": "\n SELECT COUNT(*) as \"count!\"\n FROM attest_attempts\n WHERE attestation_id = ?1\n ", + "describe": { + "columns": [ + { + "name": "count!", + "ordinal": 0, + "type_info": "Integer" + } + ], + "parameters": { + "Right": 1 + }, + "nullable": [ + false + ] + }, + "hash": "2e7fbf51720dc5f655ea7e45fc6e7b236b75030807a8fdb6a4f5e32eae008241" +} diff --git a/crates/stamper/.sqlx/query-47699945784474239a10b7a1e98f0f5ab837c0a056b8f4188dad920aac0d1dcc.json b/crates/stamper/.sqlx/query-47699945784474239a10b7a1e98f0f5ab837c0a056b8f4188dad920aac0d1dcc.json new file mode 100644 index 0000000..c008757 --- /dev/null +++ b/crates/stamper/.sqlx/query-47699945784474239a10b7a1e98f0f5ab837c0a056b8f4188dad920aac0d1dcc.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "\n UPDATE pending_attestations\n SET result = 'success', updated_at = unixepoch()\n WHERE id = ?1\n ", + "describe": { + "columns": [], + "parameters": { + "Right": 1 + }, + "nullable": [] + }, + "hash": "47699945784474239a10b7a1e98f0f5ab837c0a056b8f4188dad920aac0d1dcc" +} diff --git a/crates/stamper/.sqlx/query-5a94601de5bc75c94410eb9cdbece19229bffa2f8a871c32062836fb8054543e.json b/crates/stamper/.sqlx/query-5a94601de5bc75c94410eb9cdbece19229bffa2f8a871c32062836fb8054543e.json new file mode 100644 index 0000000..20a7a85 --- /dev/null +++ b/crates/stamper/.sqlx/query-5a94601de5bc75c94410eb9cdbece19229bffa2f8a871c32062836fb8054543e.json @@ -0,0 +1,20 @@ +{ + "db_name": "SQLite", + "query": "INSERT INTO pending_attestations (trie_root) VALUES (?) RETURNING id", + "describe": { + "columns": [ + { + "name": "id", + "ordinal": 0, + "type_info": "Integer" + } + ], + "parameters": { + "Right": 1 + }, + "nullable": [ + false + ] + }, + "hash": "5a94601de5bc75c94410eb9cdbece19229bffa2f8a871c32062836fb8054543e" +} diff --git a/crates/stamper/.sqlx/query-5fe8d835ece42f34ee0cf551cb9bd7e38819dd31587817edaeb5b339a5c7e6b6.json b/crates/stamper/.sqlx/query-5fe8d835ece42f34ee0cf551cb9bd7e38819dd31587817edaeb5b339a5c7e6b6.json new file mode 100644 index 0000000..0d22ff7 --- /dev/null +++ b/crates/stamper/.sqlx/query-5fe8d835ece42f34ee0cf551cb9bd7e38819dd31587817edaeb5b339a5c7e6b6.json @@ -0,0 +1,32 @@ +{ + "db_name": "SQLite", + "query": "\n SELECT\n SUM(CASE WHEN result = 'pending' THEN 1 ELSE 0 END) as \"pending!\",\n SUM(CASE WHEN result = 'success' THEN 1 ELSE 0 END) as \"success!\",\n SUM(CASE WHEN result <> 'pending' AND result <> 'success' THEN 1 ELSE 0 END) as \"failed!\"\n FROM pending_attestations\n ", + "describe": { + "columns": [ + { + "name": "pending!", + "ordinal": 0, + "type_info": "Integer" + }, + { + "name": "success!", + "ordinal": 1, + "type_info": "Integer" + }, + { + "name": "failed!", + "ordinal": 2, + "type_info": "Integer" + } + ], + "parameters": { + "Right": 0 + }, + "nullable": [ + true, + true, + true + ] + }, + "hash": "5fe8d835ece42f34ee0cf551cb9bd7e38819dd31587817edaeb5b339a5c7e6b6" +} diff --git a/crates/stamper/.sqlx/query-644b92230bcf8037b56a569573f3cca6ebc858802925d44fb8fd8eb4eb370fdb.json b/crates/stamper/.sqlx/query-644b92230bcf8037b56a569573f3cca6ebc858802925d44fb8fd8eb4eb370fdb.json new file mode 100644 index 0000000..007dafe --- /dev/null +++ b/crates/stamper/.sqlx/query-644b92230bcf8037b56a569573f3cca6ebc858802925d44fb8fd8eb4eb370fdb.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "\n INSERT INTO attest_attempts (attestation_id, chain_id, tx_hash, block_number)\n VALUES (?1, ?2, ?3, ?4)\n ", + "describe": { + "columns": [], + "parameters": { + "Right": 4 + }, + "nullable": [] + }, + "hash": "644b92230bcf8037b56a569573f3cca6ebc858802925d44fb8fd8eb4eb370fdb" +} diff --git a/crates/stamper/.sqlx/query-9f7ca218a1cef9bb12c9739eea81deca800d0a7d75d079aaec5f00a6bb14f073.json b/crates/stamper/.sqlx/query-9f7ca218a1cef9bb12c9739eea81deca800d0a7d75d079aaec5f00a6bb14f073.json new file mode 100644 index 0000000..9c87fee --- /dev/null +++ b/crates/stamper/.sqlx/query-9f7ca218a1cef9bb12c9739eea81deca800d0a7d75d079aaec5f00a6bb14f073.json @@ -0,0 +1,38 @@ +{ + "db_name": "SQLite", + "query": "\n SELECT\n id as \"id!\",\n trie_root as \"trie_root: TextWrapper\",\n created_at as \"created_at!\",\n updated_at as \"updated_at!\"\n FROM pending_attestations\n WHERE result = 'pending'\n ", + "describe": { + "columns": [ + { + "name": "id!", + "ordinal": 0, + "type_info": "Integer" + }, + { + "name": "trie_root: TextWrapper", + "ordinal": 1, + "type_info": "Text" + }, + { + "name": "created_at!", + "ordinal": 2, + "type_info": "Integer" + }, + { + "name": "updated_at!", + "ordinal": 3, + "type_info": "Integer" + } + ], + "parameters": { + "Right": 0 + }, + "nullable": [ + true, + false, + false, + false + ] + }, + "hash": "9f7ca218a1cef9bb12c9739eea81deca800d0a7d75d079aaec5f00a6bb14f073" +} diff --git a/crates/stamper/.sqlx/query-b89448c339e61daff9480a7d85323da0c4285e545a7020219a272c36d03ae505.json b/crates/stamper/.sqlx/query-b89448c339e61daff9480a7d85323da0c4285e545a7020219a272c36d03ae505.json new file mode 100644 index 0000000..d7b3835 --- /dev/null +++ b/crates/stamper/.sqlx/query-b89448c339e61daff9480a7d85323da0c4285e545a7020219a272c36d03ae505.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "\n UPDATE pending_attestations\n SET result = 'max_attempts_exceeded', updated_at = unixepoch()\n WHERE id = ?1\n ", + "describe": { + "columns": [], + "parameters": { + "Right": 1 + }, + "nullable": [] + }, + "hash": "b89448c339e61daff9480a7d85323da0c4285e545a7020219a272c36d03ae505" +} diff --git a/crates/stamper/.sqlx/query-e04d392a8f4ded8fff0556e14f3f372bbd6eb218020932f772e0dd491bd6ab0a.json b/crates/stamper/.sqlx/query-e04d392a8f4ded8fff0556e14f3f372bbd6eb218020932f772e0dd491bd6ab0a.json new file mode 100644 index 0000000..845cde3 --- /dev/null +++ b/crates/stamper/.sqlx/query-e04d392a8f4ded8fff0556e14f3f372bbd6eb218020932f772e0dd491bd6ab0a.json @@ -0,0 +1,20 @@ +{ + "db_name": "SQLite", + "query": "SELECT created_at FROM attest_attempts ORDER BY created_at DESC LIMIT 1", + "describe": { + "columns": [ + { + "name": "created_at", + "ordinal": 0, + "type_info": "Integer" + } + ], + "parameters": { + "Right": 0 + }, + "nullable": [ + false + ] + }, + "hash": "e04d392a8f4ded8fff0556e14f3f372bbd6eb218020932f772e0dd491bd6ab0a" +} diff --git a/crates/stamper/.sqlx/query-fb8e841134910bba073883fc80ec77cc5eefc34757f3149f85b3378158013fb6.json b/crates/stamper/.sqlx/query-fb8e841134910bba073883fc80ec77cc5eefc34757f3149f85b3378158013fb6.json new file mode 100644 index 0000000..f6d3650 --- /dev/null +++ b/crates/stamper/.sqlx/query-fb8e841134910bba073883fc80ec77cc5eefc34757f3149f85b3378158013fb6.json @@ -0,0 +1,38 @@ +{ + "db_name": "SQLite", + "query": "\n SELECT\n attestation_id,\n chain_id,\n tx_hash as \"tx_hash: TextWrapper\",\n block_number as \"block_number!\"\n FROM attest_attempts\n WHERE attestation_id = ?1 AND tx_hash IS NOT NULL AND block_number IS NOT NULL\n ORDER BY created_at DESC\n LIMIT 1\n ", + "describe": { + "columns": [ + { + "name": "attestation_id", + "ordinal": 0, + "type_info": "Integer" + }, + { + "name": "chain_id", + "ordinal": 1, + "type_info": "Integer" + }, + { + "name": "tx_hash: TextWrapper", + "ordinal": 2, + "type_info": "Text" + }, + { + "name": "block_number!", + "ordinal": 3, + "type_info": "Integer" + } + ], + "parameters": { + "Right": 1 + }, + "nullable": [ + false, + false, + true, + true + ] + }, + "hash": "fb8e841134910bba073883fc80ec77cc5eefc34757f3149f85b3378158013fb6" +} diff --git a/crates/stamper/Cargo.toml b/crates/stamper/Cargo.toml index c408805..6d45be0 100644 --- a/crates/stamper/Cargo.toml +++ b/crates/stamper/Cargo.toml @@ -1,28 +1,40 @@ [package] authors.workspace = true +description = "Universal Timestamps Stamper Library for packing digest and publish root on chain." edition.workspace = true homepage.workspace = true -keywords.workspace = true -license.workspace = true +include = [ + "src/**/*", + ".sqlx/*.json", + "migrations/*.sql", + "Cargo.toml", + "LICENSE", +] +keywords = ["uts", "timestamping"] +license = "AGPL-3.0-or-later" name = "uts-stamper" repository.workspace = true version.workspace = true +[lints] +workspace = true + [dependencies] alloy-primitives = { workspace = true, features = ["serde"] } alloy-provider = { workspace = true } -bitcode = { workspace = true, features = ["serde"] } -bytemuck = { workspace = true } digest = { workspace = true } +eyre = { workspace = true } rocksdb = { workspace = true } serde = { workspace = true, features = ["derive"] } -thiserror = { workspace = true } +sqlx = { workspace = true, features = ["runtime-tokio", "sqlite", "migrate"] } +strum = { workspace = true, features = ["derive"] } tokio = { workspace = true, features = ["time", "macros"] } +tokio-util = { workspace = true } tracing = { workspace = true } uts-bmt = { workspace = true } uts-contracts = { workspace = true } uts-core = { workspace = true } uts-journal = { workspace = true } +uts-sql = { workspace = true } -[lints] -workspace = true +[features] diff --git a/crates/stamper/LICENSE-AGPL b/crates/stamper/LICENSE-AGPL new file mode 120000 index 0000000..5f5cf25 --- /dev/null +++ b/crates/stamper/LICENSE-AGPL @@ -0,0 +1 @@ +../../LICENSE-AGPL \ No newline at end of file diff --git a/crates/stamper/migrations/20260304064005_init.sql b/crates/stamper/migrations/20260304064005_init.sql new file mode 100644 index 0000000..9dafdc5 --- /dev/null +++ b/crates/stamper/migrations/20260304064005_init.sql @@ -0,0 +1,23 @@ +PRAGMA foreign_keys = ON; + +CREATE TABLE IF NOT EXISTS pending_attestations ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + trie_root TEXT NOT NULL, + created_at INTEGER NOT NULL DEFAULT (unixepoch()), + updated_at INTEGER NOT NULL DEFAULT (unixepoch()), + result TEXT NOT NULL DEFAULT 'pending' + +); +CREATE INDEX idx_attestations_result on pending_attestations (result); + +CREATE TABLE IF NOT EXISTS attest_attempts ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + attestation_id INTEGER NOT NULL, + chain_id INTEGER NOT NULL, + tx_hash TEXT, + block_number INTEGER, + created_at INTEGER NOT NULL DEFAULT (unixepoch()), + + FOREIGN KEY (attestation_id) REFERENCES pending_attestations (id) ON DELETE CASCADE +); +CREATE INDEX idx_attempts_attestation_id on attest_attempts (attestation_id); diff --git a/crates/stamper/src/kv.rs b/crates/stamper/src/kv.rs new file mode 100644 index 0000000..1a0cf3c --- /dev/null +++ b/crates/stamper/src/kv.rs @@ -0,0 +1,39 @@ +use alloy_primitives::B256; +use digest::{Digest, FixedOutputReset, Output}; +use rocksdb::DB; +use uts_bmt::MerkleTree; + +/// Extension trait for DB to load Merkle entries and leaf->root mappings +pub trait DbExt { + /// Load a Merkle entry from the database by root hash + fn load_trie(&self, root: B256) -> Result>, rocksdb::Error>; + + /// Get the root hash for a given leaf hash, if it exists + fn get_root_for_leaf(&self, leaf: B256) -> Result, rocksdb::Error>; +} + +impl DbExt for DB +where + Output: Copy, +{ + fn load_trie(&self, root: B256) -> Result>, rocksdb::Error> { + let Some(data) = self.get(root)? else { + return Ok(None); + }; + Ok(Some(MerkleTree::from_raw_bytes(&data))) + } + + fn get_root_for_leaf(&self, leaf: B256) -> Result, rocksdb::Error> { + let Some(root) = self.get(leaf)? else { + return Ok(None); + }; + if root.len() != 32 { + let trie = MerkleTree::::from_raw_bytes(&root); + debug_assert_eq!(trie.root().as_slice(), leaf); + debug_assert_eq!(trie.leaves().len(), 1); + return Ok(Some(leaf)); // it's a single-leaf tree + } + let hash: [u8; 32] = root.as_slice().try_into().expect("infallible"); + Ok(Some(B256::new(hash))) + } +} diff --git a/crates/stamper/src/lib.rs b/crates/stamper/src/lib.rs index ed087f3..d28f23f 100644 --- a/crates/stamper/src/lib.rs +++ b/crates/stamper/src/lib.rs @@ -1,30 +1,50 @@ -#![feature(generic_const_exprs)] -#![allow(incomplete_features)] - //! Timestamping +// Copyright (C) 2026 UTS Contributors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + #[macro_use] extern crate tracing; -use alloy_primitives::{B256, BlockNumber, ChainId, TxHash}; +use alloy_primitives::B256; use alloy_provider::Provider; -use bytemuck::{NoUninit, Pod}; use digest::{Digest, FixedOutputReset, Output, typenum::Unsigned}; +use eyre::{Context, bail}; use rocksdb::{DB, WriteBatch}; -use serde::{Deserialize, Serialize}; -use std::{ - borrow::Cow, - collections::{HashMap, VecDeque}, - fmt, - sync::Arc, - time::Duration, +use sqlx::SqlitePool; +use std::{fmt, marker::PhantomData, sync::Arc, time::Duration}; +use tokio::{ + select, + time::{Interval, MissedTickBehavior}, }; -use tokio::time::{Interval, MissedTickBehavior}; -use uts_bmt::UnorderedMerkleTree; -use uts_contracts::uts::UniversalTimestamps; +use tokio_util::sync::CancellationToken; +use uts_bmt::MerkleTree; +use uts_contracts::eas::EAS; use uts_core::utils::Hexed; use uts_journal::reader::JournalReader; +/// kv storage for leaf->root mappings, and root->full_tree mappings (for power-of-two sized trees) +pub mod kv; +/// sql storage for pending attestation metadata, and finalized timestamp metadata +pub mod sql; + +mod tx_sender; + +/// Maximum number of retries for transient errors in transaction sending. +pub const MAX_RETRIES: i64 = 3; + /// Stamper for timestamping /// /// A stamper will wait for, either: @@ -36,19 +56,20 @@ use uts_journal::reader::JournalReader; /// - if available entries size is not power of two, it will take: /// - the largest power of two less than available entries, if that is >= `min_leaves` /// - else, it will take all available entries -pub struct Stamper { +pub struct Stamper { /// Journal reader to read entries from - reader: JournalReader, + reader: JournalReader, /// Storage for merkle trees and leaf->root mappings - storage: Arc, - /// FIFO cache of recent merkle trees - cache: VecDeque>, - /// FIFO cache index of recent merkle trees - cache_index: HashMap, + kv_storage: Arc, + /// Sql db for storing timestamp metadata + sql_storage: SqlitePool, + /// The contract - contract: UniversalTimestamps

, + contract: EAS

, /// Stamper configuration config: StamperConfig, + + _marker: PhantomData, } /// Configuration for the Stamper @@ -62,146 +83,162 @@ pub struct StamperConfig { /// The minimum size of the Merkle tree leaves. /// It should be a power of two. pub min_leaves: usize, - /// The maximum number of recent Merkle trees to keep in cache. - pub max_cache_size: usize, -} - -/// Merkle entry stored in the database -#[derive(Debug, Serialize, Deserialize)] -pub struct MerkleEntry<'a> { - /// Chain ID of the timestamp transaction - pub chain_id: ChainId, - /// Transaction hash of the timestamp transaction - pub tx_hash: TxHash, - /// Block number of the timestamp transaction - pub height: BlockNumber, - - trie: Cow<'a, [u8]>, -} - -impl MerkleEntry<'_> { - /// Get the Merkle tree from the entry - pub fn trie(&self) -> UnorderedMerkleTree - where - D: Digest + FixedOutputReset, - Output: Pod + Copy, - { - // SAFETY: We trust that the data in the database is valid, and that the trie was serialized correctly. - unsafe { UnorderedMerkleTree::from_raw_bytes(&self.trie) } - } -} - -/// Errors that can occur during storage operations -#[derive(Debug, thiserror::Error)] -pub enum StorageError { - /// Errors from RocksDB - #[error(transparent)] - Rocks(#[from] rocksdb::Error), - /// Errors from bitcode serialization/deserialization - #[error(transparent)] - Bitcode(#[from] bitcode::Error), -} - -/// Extension trait for DB to load Merkle entries and leaf->root mappings -pub trait DbExt { - /// Load a Merkle entry from the database by root hash - fn load_entry(&self, root: B256) -> Result>, StorageError>; - - /// Get the root hash for a given leaf hash, if it exists - fn get_root_for_leaf(&self, leaf: B256) -> Result, StorageError>; -} - -impl DbExt for DB { - fn load_entry(&self, root: B256) -> Result>, StorageError> { - let Some(data) = self.get(root)? else { - return Ok(None); - }; - let entry: MerkleEntry<'static> = bitcode::deserialize(&data)?; - Ok(Some(entry)) - } - - fn get_root_for_leaf(&self, leaf: B256) -> Result, StorageError> { - let Some(root) = self.get(leaf)? else { - return Ok(None); - }; - if root.len() != 32 { - let _entry: MerkleEntry<'static> = bitcode::deserialize(&root)?; - return Ok(Some(leaf)); // it's a single-leaf tree - } - let hash: [u8; 32] = root.as_slice().try_into().expect("infallible"); - Ok(Some(B256::new(hash))) - } } -impl fmt::Debug for Stamper { +impl fmt::Debug for Stamper { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("Stamper") - .field("cache_size", &self.cache.len()) .field("config", &self.config) + .field("contract", &self.contract) .finish() } } -impl Stamper +impl Stamper where D: Digest + FixedOutputReset + 'static, - P: Provider, - Output: Pod + Copy, - [u8; ENTRY_SIZE]: NoUninit, + P: Provider + Clone + 'static, + Output: Copy, { - const _SIZE_MATCHES: () = assert!(D::OutputSize::USIZE == ENTRY_SIZE); - /// Create a new Stamper pub fn new( - reader: JournalReader, - storage: Arc, - contract: UniversalTimestamps

, + reader: JournalReader, + kv_storage: Arc, + sql_storage: SqlitePool, + contract: EAS

, config: StamperConfig, ) -> Self { Self { reader, - storage, - cache: VecDeque::with_capacity(config.max_cache_size), - cache_index: HashMap::with_capacity(config.max_cache_size), + kv_storage, + sql_storage, contract, config, + _marker: PhantomData, } } /// Work loop - pub async fn run(&mut self) { - let chain_id = self - .contract - .provider() - .get_chain_id() - .await - .expect("Failed to get chain ID"); + pub async fn run(&mut self, token: CancellationToken) -> eyre::Result<()> { let mut ticker = tokio::time::interval(Duration::from_secs(self.config.max_interval_seconds)); ticker.set_missed_tick_behavior(MissedTickBehavior::Delay); let mut leaves_buffer = Vec::with_capacity(self.config.max_entries_per_timestamp); + let (waker_tx, waker_rx) = tokio::sync::mpsc::channel::<()>(1); + + let tx_sender = tx_sender::TxSender { + eas: self.contract.clone(), + sql_storage: self.sql_storage.clone(), + waker: waker_rx, + token: token.clone(), + }; + tokio::spawn(async move { + tx_sender.run_until_cancelled().await; + }); + loop { - self.pack(chain_id, &mut ticker, &mut leaves_buffer).await; + select! { + _ = token.cancelled() => { + info!("Cancellation received, stopping stamper..."); + break; + } + res = self.pack(&mut ticker, &mut leaves_buffer) => { + match res { + Err(e) => { + error!(error = ?e, "Error in packing batch, stopping stamper..."); + break; + } + Ok(0) => continue, + // notify the tx sender to wake up + Ok(e) => { + info!(entries = e, "Batch packed successfully, waking up TxSender..."); + waker_tx.try_send(()).ok() + }, + }; + } + } } + Ok(()) } + /// Try to pack a batch of entries into a Merkle tree and push the pending attestation to SQL db. + /// + /// # Errors + /// + /// All errors here are considered **FATAL**, meaning the stamper cannot continue to run correctly, + /// and should be restarted after the error is resolved. + /// + /// The errors likely indicate an issue with the underlying storage (either journal or db), + /// and should be resolved by fixing the storage issue (e.g. clearing corrupted data, + /// increasing disk space, etc.) async fn pack( &mut self, - chain_id: ChainId, ticker: &mut Interval, - buffer: &mut Vec<[u8; ENTRY_SIZE]>, - ) { + buffer: &mut Vec>, + ) -> eyre::Result { + let target_size = self.wait_for_next_batch(ticker).await?; + // no entries to process, skip this round + if target_size == 0 { + return Ok(0); + }; + trace!(target_size); + + // Read entries, could need two reads if wrapping around + buffer.clear(); + for entry in self.reader.read(target_size)? { + let mut node = Output::::default(); + debug_assert_eq!(entry.len(), D::OutputSize::USIZE); + node.copy_from_slice(entry); + buffer.push(node); + } + debug_assert_eq!(buffer.len(), target_size); + + let merkle_tree = MerkleTree::::new_unhashed(buffer); + + let merkle_tree = tokio::task::spawn_blocking(move || { + let merkle_tree = merkle_tree.finalize(); // CPU intensive + let root = merkle_tree.root(); + info!(root = ?Hexed(root)); + merkle_tree + }) + .await + .context("failed to create Merkle tree")?; + + let root = B256::from_slice(merkle_tree.root()); + + let mut batch = WriteBatch::default(); + // store leaf->root mappings for quick lookup + for leaf in merkle_tree.leaves() { + batch.put(leaf, root); + } + // if it's a single-leaf tree, the root == the leaf, so we write mapping first. + batch.put(root, merkle_tree.to_raw_bytes()); + self.kv_storage + .write(batch) + .context("failed to write to kv db")?; + + sql::new_pending_attestation(&self.sql_storage, root) + .await + .context("failed to create pending attestation in sql db")?; + + self.reader + .commit() + .context("failed to commit journal reader")?; + Ok(target_size) + } + + async fn wait_for_next_batch(&mut self, ticker: &mut Interval) -> eyre::Result { let entries = self .reader .wait_at_least(self.config.max_entries_per_timestamp); - let target_size = tokio::select! { + let target_size = select! { _ = ticker.tick() => { // Timeout reached, create timestamp with available entries let current_available = self.reader.available(); if current_available == 0 { debug!("No available entries, skipping this round..."); - return; + return Ok(0); } debug!(current_available, "Timeout reached, creating timestamp"); @@ -212,6 +249,9 @@ where trace!("Current available is power of two, taking all"); current_available } else if next_power_of_two / 2 >= self.config.min_leaves { + // This is for avoiding creating small Merkle trees with too few leaves. + // e.g. if current_available = 3, min_leaves = 4 then next_power_of_two / 2 = 2, + // we will take 3 entries instead of 2 to create a Merkle tree with 4 leaves instead of 1 leaf. let target = next_power_of_two / 2; trace!(target, "Taking largest power of two less than available"); target @@ -220,85 +260,15 @@ where current_available } } - _ = entries => { + result = entries => { + if result.is_err() { + bail!("fatal error in journal while waiting for entries"); + } // Max entries reached, create timestamp debug!("Max entries reached, creating timestamp"); self.config.max_entries_per_timestamp } }; - trace!(target_size); - - // Read entries, could need two reads if wrapping around - buffer.clear(); - buffer.extend_from_slice(self.reader.read(target_size)); - let remaining = target_size - buffer.len(); - if remaining > 0 { - buffer.extend_from_slice(self.reader.read(remaining)); - } - debug_assert_eq!(buffer.len(), target_size); - - let merkle_tree = UnorderedMerkleTree::::new_unhashed(bytemuck::cast_slice(&buffer)); - let storage = self.storage.clone(); - - let merkle_tree = tokio::task::spawn_blocking(move || { - let merkle_tree = merkle_tree.finalize(); // CPU intensive - let root = merkle_tree.root(); - info!(root = ?Hexed(root)); - merkle_tree - }) - .await - .expect("Failed to create Merkle tree"); // FIXME: handle error properly - - let root = B256::new(bytemuck::cast(*merkle_tree.root())); - - // commit to blockchain - let receipt = self - .contract - .attest(root) - .send() - .await - .expect("failed to build transaction") - .get_receipt() - .await - .expect("failed to send transaction"); // FIXME: handle error properly - let block_number = receipt.block_number.expect("Transaction not yet mined"); - info!(%block_number, %receipt.transaction_hash, %root,"Timestamp attested on-chain"); - - // write to storage - let mut batch = WriteBatch::default(); - // store leaf->root mappings for quick lookup - for leaf in merkle_tree.leaves() { - batch.put(leaf, root); - } - // store the Merkle tree - let entry = MerkleEntry { - chain_id, - tx_hash: receipt.transaction_hash, - height: block_number, - trie: Cow::Borrowed(merkle_tree.as_raw_bytes()), - }; - let serialized_entry = - bitcode::serialize(&entry).expect("Failed to serialize Merkle entry"); - bitcode::deserialize::(&serialized_entry) - .expect("Failed to deserialize Merkle entry"); // sanity check - // if it's a single-leaf tree, the root == the leaf, so we write mapping first. - batch.put(root, serialized_entry); - storage.write(batch).expect("Failed to write to storage"); // FIXME: handle error properly - - if self.cache.len() >= self.config.max_cache_size { - let evicted = self - .cache - .pop_front() - .expect("infallible due to check above"); - - let root = evicted.root(); - let removed = self.cache_index.remove(&B256::new(bytemuck::cast(*root))); - debug_assert_eq!(removed, Some(0)); - - self.cache_index.iter_mut().for_each(|(_, idx)| *idx -= 1); - } - self.cache.push_back(merkle_tree); - self.cache_index.insert(root, self.cache.len() - 1); - self.reader.commit().expect("Failed to commit read entries"); // FIXME: handle error properly + Ok(target_size) } } diff --git a/crates/stamper/src/sql.rs b/crates/stamper/src/sql.rs new file mode 100644 index 0000000..aa3b20c --- /dev/null +++ b/crates/stamper/src/sql.rs @@ -0,0 +1,310 @@ +use crate::MAX_RETRIES; +use alloy_primitives::{B256, BlockNumber, ChainId, TxHash, private::serde::Serialize}; +use eyre::Context; +use sqlx::SqlitePool; +use uts_sql::{TextWrapper, define_text_enum, migrator}; + +migrator!("./migrations"); + +/// A struct representing a pending attestation that has been created but not yet attested on-chain. +#[derive(Debug, Copy, Clone)] +pub struct PendingAttestation { + /// The ID of the attestation in the database. + pub id: i64, + /// The merkle root that is being attested. + pub trie_root: B256, + /// The timestamp when the attestation was created (Unix timestamp). + pub created_at: u64, + /// The timestamp when the attestation was last updated (Unix timestamp). + pub updated_at: u64, + /// The current result of the attestation attempt (pending, success, or max attempts exceeded). + pub result: AttestationResult, +} + +/// An enum representing the result of an attestation attempt. +#[derive(Debug, Copy, Clone, strum::IntoStaticStr, strum::EnumString)] +#[strum(serialize_all = "snake_case")] +pub enum AttestationResult { + /// The attestation is still pending and has not yet been attested on-chain. + Pending, + /// The attestation was successfully attested on-chain. + Success, + /// Exceeded maximum number of attempts to attest on-chain. + MaxAttemptsExceeded, +} +define_text_enum!(AttestationResult); + +/// A struct representing an attempt to attest a merkle root on-chain +#[derive(Debug, Copy, Clone)] +pub struct AttestAttempt { + /// The ID of the attestation this attempt is for. + pub attestation_id: i64, + /// The chain ID on which the attest attempt was made. + pub chain_id: ChainId, + /// The transaction details if the attempt was successful, None if the attempt failed (e.g. due to revert or RPC error). + pub tx: Option, +} + +/// A struct representing a successful attestation transaction on-chain. +#[derive(Debug, Copy, Clone)] +pub struct AttestTx { + /// The hash of the transaction that attested the merkle root on-chain. + pub tx_hash: TxHash, + /// The block number in which the transaction was included. + pub block_number: BlockNumber, +} + +/// A struct representing statistics about pending attestations in the database. +#[derive(Debug, Copy, Clone, Serialize)] +pub struct Stats { + /// The total number of pending attestations in the database. + pub pending: usize, + /// The total number of successful attempts for pending attestations. + pub success: usize, + /// The total number of failed attempts for pending attestations. + pub failed: usize, + /// The unix timestamp of the last attestation attempt. + #[serde(skip_serializing_if = "Option::is_none")] + pub last_attempt: Option, + /// The details of the last successful attestation attempt, if any. + #[serde(skip_serializing_if = "Option::is_none")] + pub last_success: Option, +} + +/// A struct representing the details of a successful attestation attempt. +#[derive(Debug, Copy, Clone, Serialize)] +pub struct SuccessAttempt { + /// The chain ID on which the successful attest attempt was made. + pub chain_id: ChainId, + /// The hash of the transaction that successfully attested the merkle root on-chain. + pub tx_hash: TxHash, + /// The block number in which the successful transaction was included. + pub block_number: BlockNumber, +} + +/// Create a new pending attestation in the database and return its ID. +pub async fn new_pending_attestation(pool: &SqlitePool, root: B256) -> sqlx::Result { + let root = TextWrapper(root); + + let rec = sqlx::query!( + r#"INSERT INTO pending_attestations (trie_root) VALUES (?) RETURNING id"#, + root, + ) + .fetch_one(pool) + .await?; + + Ok(rec.id) +} + +/// Load all pending attestations from the database. +pub async fn load_all_pending_attestations( + pool: &SqlitePool, +) -> sqlx::Result> { + let recs = sqlx::query!( + r#" + SELECT + id as "id!", + trie_root as "trie_root: TextWrapper", + created_at as "created_at!", + updated_at as "updated_at!" + FROM pending_attestations + WHERE result = 'pending' + "# + ) + .fetch_all(pool) + .await?; + + Ok(recs + .into_iter() + .map(|rec| PendingAttestation { + id: rec.id, + trie_root: rec.trie_root.0, + created_at: rec.created_at as u64, + updated_at: rec.updated_at as u64, + // we only load pending attestations here + result: AttestationResult::Pending, + }) + .collect()) +} + +/// Get the attestation result for a given trie root. Returns the attestation ID and result if found. +pub async fn get_attestation_result( + pool: &SqlitePool, + trie_root: B256, +) -> sqlx::Result<(i64, AttestationResult)> { + let trie_root = TextWrapper(trie_root); + + let rec = sqlx::query!( + r#" + SELECT id, result as "result: AttestationResult" + FROM pending_attestations + WHERE trie_root = ?1 + "#, + trie_root, + ) + .fetch_one(pool) + .await?; + + Ok((rec.id, rec.result)) +} + +/// Get the last successful attest attempt for a given attestation ID, if any. +pub async fn get_last_successful_attest_attempt( + pool: &SqlitePool, + attestation_id: i64, +) -> sqlx::Result> { + let rec = sqlx::query!( + r#" + SELECT + attestation_id, + chain_id, + tx_hash as "tx_hash: TextWrapper", + block_number as "block_number!" + FROM attest_attempts + WHERE attestation_id = ?1 AND tx_hash IS NOT NULL AND block_number IS NOT NULL + ORDER BY created_at DESC + LIMIT 1 + "#, + attestation_id, + ) + .fetch_optional(pool) + .await?; + + Ok(rec.map(|r| AttestAttempt { + attestation_id: r.attestation_id, + chain_id: ChainId::from(r.chain_id as u64), + tx: r.tx_hash.map(|tx_hash| AttestTx { + tx_hash: tx_hash.0, + block_number: r.block_number as u64, + }), + })) +} + +/// Create a new attest attempt for a given attestation ID and chain ID. +/// +/// If the attempt was successful, also update the pending attestation result to success. +/// If the attempt failed, check if we have exceeded the maximum number of attempts and update the +/// result to max_attempts_exceeded if so. +pub async fn new_attest_attempt( + pool: &SqlitePool, + attestation_id: i64, + chain_id: ChainId, + may_success: Option, +) -> eyre::Result<()> { + let chain_id: i64 = chain_id.try_into().context("i64 overflow")?; + let tx_hash = may_success.map(|tx| TextWrapper(tx.tx_hash)); + let block_number: Option = match may_success { + Some(tx) => Some(tx.block_number.try_into().context("i64 overflow")?), + None => None, + }; + + let mut tx = pool.begin().await?; + + sqlx::query!( + r#" + INSERT INTO attest_attempts (attestation_id, chain_id, tx_hash, block_number) + VALUES (?1, ?2, ?3, ?4) + "#, + attestation_id, + chain_id, + tx_hash, + block_number, + ) + .execute(&mut *tx) + .await?; + + if may_success.is_some() { + sqlx::query!( + r#" + UPDATE pending_attestations + SET result = 'success', updated_at = unixepoch() + WHERE id = ?1 + "#, + attestation_id, + ) + .execute(&mut *tx) + .await?; + } else { + // check if we have exceeded max attempts + let total_attempts = sqlx::query_scalar!( + r#" + SELECT COUNT(*) as "count!" + FROM attest_attempts + WHERE attestation_id = ?1 + "#, + attestation_id, + ) + .fetch_one(&mut *tx) + .await?; + if total_attempts >= MAX_RETRIES { + sqlx::query!( + r#" + UPDATE pending_attestations + SET result = 'max_attempts_exceeded', updated_at = unixepoch() + WHERE id = ?1 + "#, + attestation_id, + ) + .execute(&mut *tx) + .await?; + } + } + + tx.commit().await?; + + Ok(()) +} + +/// Get stats +pub async fn get_stats(pool: &SqlitePool) -> sqlx::Result { + let mut tx = pool.begin().await?; + + let counts = sqlx::query!( + r#" + SELECT + SUM(CASE WHEN result = 'pending' THEN 1 ELSE 0 END) as "pending!", + SUM(CASE WHEN result = 'success' THEN 1 ELSE 0 END) as "success!", + SUM(CASE WHEN result <> 'pending' AND result <> 'success' THEN 1 ELSE 0 END) as "failed!" + FROM pending_attestations + "# + ) + .fetch_one(&mut *tx) + .await?; + + let last_attempt: Option = sqlx::query_scalar!( + r#"SELECT created_at FROM attest_attempts ORDER BY created_at DESC LIMIT 1"# + ) + .fetch_optional(&mut *tx) + .await?; + + let last_success_rec = sqlx::query!( + r#" + SELECT + chain_id, + tx_hash as "tx_hash!: TextWrapper", + block_number as "block_number!" + FROM attest_attempts + WHERE tx_hash IS NOT NULL AND block_number IS NOT NULL + ORDER BY created_at DESC + LIMIT 1 + "# + ) + .fetch_optional(&mut *tx) + .await?; + + let last_success = last_success_rec.map(|rec| SuccessAttempt { + chain_id: rec.chain_id as u64, + tx_hash: rec.tx_hash.0, + block_number: rec.block_number as u64, + }); + + tx.commit().await?; + + Ok(Stats { + pending: counts.pending as usize, + success: counts.success as usize, + failed: counts.failed as usize, + last_attempt: last_attempt.map(|ts| ts as u64), + last_success, + }) +} diff --git a/crates/stamper/src/tx_sender.rs b/crates/stamper/src/tx_sender.rs new file mode 100644 index 0000000..2fa3177 --- /dev/null +++ b/crates/stamper/src/tx_sender.rs @@ -0,0 +1,206 @@ +//! Attest Tx Sender +//! +//! # Note +//! +//! We need to separate the merkle tree creation and transaction sending into two separate tasks. +//! +//! The first task (merkle tree creation) is infallible in most cases unless there is a storage +//! failure, and any error should consider as **FATAL** and requires devops intervention. +//! +//! The second task (transaction sending) is more likely to encounter transient errors +//! (e.g. network issues, RPC errors, etc.). +//! +//! By separating these two tasks, we can ensure that transient errors in transaction sending do +//! not affect the merkle tree creation process, and the stamper can continue to consume journal +//! entries and create merkle trees without being blocked by transaction sending issues. + +use crate::{sql, sql::AttestTx}; +use alloy_primitives::ChainId; +use alloy_provider::Provider; +use eyre::bail; +use std::{future, pin::Pin}; +use tokio::sync::mpsc::Receiver; +use tokio_util::sync::CancellationToken; +use uts_contracts::eas::EAS; + +pub(super) struct TxSender

{ + /// The contract to interact with + /// + /// The underlying provider should configure with retry layer to handle transient errors, and + /// the TxSender will rely on the provider's retry mechanism. + pub eas: EAS

, + /// The SQL db + pub sql_storage: sqlx::SqlitePool, + /// Waker + pub waker: Receiver<()>, + /// Cancellation token to signal shutdown + pub token: CancellationToken, +} + +impl TxSender

{ + pub async fn run_until_cancelled(mut self) { + let Ok(chain_id) = self.eas.provider().get_chain_id().await else { + error!("Failed to get chain id, TxSender cannot start without it."); + self.token.cancel(); // signal cancellation to the main stamper loop + return; + }; + + info!("TxSender started on chain id: {}", chain_id); + loop { + let may_retry = match self.process_all_pending(chain_id).await { + Err(e) => { + error!(error = ?e, "Error processing pending attestations, will retry in the next cycle."); + Box::pin(tokio::time::sleep(std::time::Duration::from_secs(10))) + as Pin + Send>> + } + Ok(()) => Box::pin(future::pending()), + }; + + tokio::select! { + _ = self.token.cancelled() => { + info!("Cancellation received, stopping TxSender..."); + break; + } + may_msg = self.waker.recv() => { + if may_msg.is_none() { + // the sender has been dropped, which means the main stamper loop has exited, + // we can safely exit as well + info!("Waker channel closed, stopping TxSender..."); + break; + } + // got a wake-up signal, process pending attestations immediately + debug!("Wake up signal received, processing pending attestations..."); + } + _ = may_retry => { + // either an error happened and we need to retry after a delay + debug!("Retrying to process pending attestations after delay..."); + } + } + } + } + + async fn process_all_pending(&self, chain_id: ChainId) -> eyre::Result<()> { + let pending_attestations = sql::load_all_pending_attestations(&self.sql_storage).await?; + for attestation in pending_attestations { + if let Err(e) = self.process_one(&attestation, chain_id).await { + error!(error = ?e, "Error processing attestation id {}, will retry later.", attestation.id); + // log the failed attempt + sql::new_attest_attempt(&self.sql_storage, attestation.id, chain_id, None).await?; + } + } + + Ok(()) + } + + async fn process_one( + &self, + attestation: &sql::PendingAttestation, + chain_id: ChainId, + ) -> eyre::Result<()> { + let res = self.eas.timestamp(attestation.trie_root).send().await; + match res { + Ok(pending_tx) => { + let receipt = pending_tx.get_receipt().await?; + let Some(block_number) = receipt.block_number else { + bail!( + "Transaction for trie root {} is not yet included in a block", + attestation.trie_root + ); + }; + + sql::new_attest_attempt( + &self.sql_storage, + attestation.id, + chain_id, + Some(AttestTx { + tx_hash: receipt.transaction_hash, + block_number, + }), + ) + .await?; + } + Err(e) if e.as_revert_data().is_some() => { + // this timestamp is already timestamped + let ts = self.eas.getTimestamp(attestation.trie_root).call().await?; + assert!( + ts > 0, + "bug: if the timestamp is not found, it should not revert previously" + ); + + let provider = self.eas.provider(); + + // binary search the timestamp to find the block number and tx hash + const MAX_GET_LOGS_BLOCK: u64 = 10; // getLogs + let block_number = provider.get_block_number().await?; + + let mut low = 0; + let mut high = block_number; + + while low <= high { + let mid = (low + high) / 2; + let Some(block) = provider.get_block(mid.into()).await? else { + bail!("Failed to get block {mid}"); + }; + if block.header.timestamp > ts { + assert!(mid > 0, "bug: this should never happen"); + high = mid - 1; + } else if block.header.timestamp < ts { + low = mid + 1; + } else { + low = mid; + high = mid; + break; + } + if high - low <= MAX_GET_LOGS_BLOCK { + break; + } + } + + let Some((_, log)) = self + .eas + .Timestamped_filter() + .from_block(low) + .to_block(high) + .topic1(attestation.trie_root) + .query() + .await? + .into_iter() + .next() + else { + bail!( + "Failed to find log for trie root {} between blocks {} and {}", + attestation.trie_root, + low, + high + ); + }; + + let Some(tx_hash) = log.transaction_hash else { + bail!( + "Log for trie root {} does not have a transaction hash", + attestation.trie_root + ); + }; + let Some(block_number) = log.block_number else { + bail!( + "Log for trie root {} does not have a block number", + attestation.trie_root + ); + }; + + sql::new_attest_attempt( + &self.sql_storage, + attestation.id, + chain_id, + Some(AttestTx { + tx_hash, + block_number, + }), + ) + .await?; + } + Err(e) => return Err(e.into()), + } + Ok(()) + } +} diff --git a/dev-docs/e2e.md b/dev-docs/e2e.md deleted file mode 100644 index 1b49791..0000000 --- a/dev-docs/e2e.md +++ /dev/null @@ -1,76 +0,0 @@ -# OpenTimestamps End2End Procedures - -This is for referencing overall end2end procedures of OpenTimestamps protocol implementation. - -> opentimestamps clients has a white list of default servers (see below). -> Bad news is Even if we are fully compatible with ots, ots clients won't work with our servers. -> Good news is we can use ots servers in our clients. - ---- - -## Client Side - -1. Calc initial digest from (data, file) and adding nonce (random generated) to it. - This allows to separate the sub-timestamp for individual file without leaking any information about the adjacent files. -2. Construct Merkle tree. -3. Submit tree root digest to **aggregator** servers in parallel via [POST /digest](./ots-api.md#post-digest) endpoint. -4. Merge received pending attestations into a single attestation file. -5. Periodically check for completed attestations to **calendar** servers via [GET /attestation/{digest}](./ots-api.md#get-timestamphex_commitment) endpoint. - ---- - -## Aggregator Side - -1. Receive submitted digest via [POST /digest](./ots-api.md#post-digest) endpoint. -2. Insert the digest into local Merkle tree. -3. Periodically (default interval is 1s) submit the tree root to upstream calendar servers via [POST /digest](./ots-api.md#post-digest) endpoint. -4. **Return** pending attestation for the submitted digest. - -The aggregator only has `/digest` endpoint, and do not persist data. - -Default ots aggregator servers: - -- https://a.pool.opentimestamps.org -- https://b.pool.opentimestamps.org -- https://a.pool.eternitywall.com -- https://ots.btc.catallaxy.com - ---- - -## Calendar Server Side - -**Note: the following procedure is not a part of OpenTimestamps protocol.** -**Server does not have to follow steps 2-3 below.** - -### Digest Submission Procedure - -1. Receive submitted digest via [POST /digest](./ots-api.md#post-digest) endpoint. -2. Add `PREPEND` op: prepends current timestamp (u32) to the digest. -3. Add `APPEND` op: HMAC with server secret to the digest, truncated to 8 bytes, appended. - After this step, the digest becomes the attestation message (44 bytes). -4. Add `ATTESTATION` op: uri with self. -5. Insert the step 3 **message** into journal as pending attestation. (just a file opened for append in python implementation) -6. **Return** pending attestation for the request. - -### Attestation upgrade Procedure - -1. Receive upgrade request via [GET /timestamp/{hex_commitment}](./ots-api.md#get-timestamphex_commitment) endpoint. -2. Search for the commitment in db. - ---- - -Default calendar servers: - -- https://\*.calendar.opentimestamps.org', # Run by Peter Todd -- https://\*.calendar.eternitywall.com', # Run by Riccardo Casatta -- https://\*.calendar.catallaxy.com', # Run by Bull Bitcoin - ---- - -## Stamper Side - -1. Stream reading pending attestations from journal file. -2. When reaching certain batch size or time interval, construct a Merkle tree from the batch of attestation messages by hashing them. -3. Submit the tree root to Bitcoin network via `OP_RETURN` transaction. -4. After certain number of confirmations, generate and write back serialized timestamp for each attestation in the batch. -5. Repeat from step 1. diff --git a/dev-docs/milestones/M1-mvp-server.md b/dev-docs/milestones/M1-mvp-server.md deleted file mode 100644 index 409ab8e..0000000 --- a/dev-docs/milestones/M1-mvp-server.md +++ /dev/null @@ -1,6 +0,0 @@ -# Milestone 1: MVP otsclient compatible server - -# Target - -Implement a minimal OpenTimestamps server that is compatible with existing otsclient implementations. -The server should support the core functionalities required for timestamping and upgrading timestamps. diff --git a/dev-docs/ots-api.md b/dev-docs/ots-api.md deleted file mode 100644 index b33827b..0000000 --- a/dev-docs/ots-api.md +++ /dev/null @@ -1,89 +0,0 @@ -# OpenTimestamps API - -The API is summarized here for reference. - -- Core Endpoints -- Submit [POST /digest](#post-digest) -- Upgrade [GET /timestamp/{hex_commitment}](#get-timestamphex_commitment) -- Other Endpoints - - Fetch Tip [GET /tip](#get-tip) - ---- - -## POST /digest - -Submit a commitment to the calendar aggregator for stamping. - -Implementation: https://github.com/opentimestamps/opentimestamps-server/blob/6309db6b2c9ac79f6f444d85c9dc96e39219eb63/otsserver/rpc.py#L48-L81 - -### Request Body - -Raw digest bytes (≤64 bytes); Content-Length header required. - -### On Success - -200 application/octet-stream with serialized [Timestamp](../crates/core/src/codec/v1/timestamp.rs) tree. - -### On Failure - -400 invalid or missing Content-Length; 400 digest too long. - -### Example - -```bash -curl -X POST https://a.pool.opentimestamps.org/digest \ - -H "Content-Type: application/octet-stream" \ - --data-binary @digest.bin \ - --output tree.bin -``` - ---- - -## GET /timestamp/{hex_commitment} - -Retrieve upgraded timestamp data for a commitment. Commitment must be lowercase/uppercase hex. - -Implementation: https://github.com/opentimestamps/opentimestamps-server/blob/6309db6b2c9ac79f6f444d85c9dc96e39219eb63/otsserver/rpc.py#L122-L186 - -### On Success - -200 application/octet-stream with serialized [Timestamp](../crates/core/src/codec/v1/timestamp.rs) tree; -Cache-Control max-age=31536000 once confirmed. - -### On Failure - -400 non-hex input; 404 with text body (Pending… or Not found) and Cache-Control max-age=60. - -### Example - -```bash -curl https://alice.btc.calendar.opentimestamps.org/timestamp/6938f93117b90a25a186021b8694f7d3622299aba932130adb748f1a32f21347eddfa3df99c41b4b9cb34ff8 \ - -H "Accept: application/octet-stream" \ - --output upgraded.bin -``` - ---- - -## Get /tip - -Fetch the most recent unconfirmed Merkle tree tip being prepared for anchoring. - -Implementation: https://github.com/opentimestamps/opentimestamps-server/blob/6309db6b2c9ac79f6f444d85c9dc96e39219eb63/otsserver/rpc.py#L83-L101 - -### On Success - -200 application/octet-stream containing tip commitment; Cache-Control max-age=10. - -### On Failure - -204 when tip exists but has no payload; 404 when no unconfirmed transactions. - -### Example - -```bash -$ curl https://alice.btc.calendar.opentimestamps.org/tip --output tip.bin -$ hexdump -C tip.bin -00000000 45 3f 4d 34 fd 72 c5 70 44 60 eb c4 b6 f5 09 87 |E?M4.r.pD`......| -00000010 72 7d da 8c a8 9e 00 1e cf c7 29 17 b4 c4 1f b0 |r}........).....| -00000020 -``` diff --git a/foundry.lock b/foundry.lock index 1f56371..d0c7c5b 100644 --- a/foundry.lock +++ b/foundry.lock @@ -1,4 +1,10 @@ { + "lib/eas-contracts": { + "tag": { + "name": "v1.4.0", + "rev": "d223e17208aa110dd5ec694d77324a2321d93201" + } + }, "lib/forge-std": { "tag": { "name": "v1.12.0", @@ -16,5 +22,17 @@ "name": "v0.4.0", "rev": "cbce1e00305e943aa1661d43f41e5ac72c662b07" } + }, + "lib/scroll-contracts": { + "tag": { + "name": "v4.0.0", + "rev": "42de954bee237cfa478a5b443ac0aeb900aca5ad" + } + }, + "lib/solady": { + "tag": { + "name": "v0.1.26", + "rev": "acd959aa4bd04720d640bf4e6a5c71037510cc4b" + } } } \ No newline at end of file diff --git a/foundry.toml b/foundry.toml index eed2c8e..7fd8812 100644 --- a/foundry.toml +++ b/foundry.toml @@ -3,16 +3,18 @@ ast = true build_info = true extra_output = ["storageLayout"] ffi = true -fs_permissions = [{ access = "read", path = "target/foundry" }] +fs_permissions = [{ access = "read-write", path = "target/foundry" }] ignored_error_codes = [ 2018, # Function state mutability can be restricted to pure + 2394, # Transient storage as defined by EIP-1153 can break the composability of smart contracts ] libs = ["lib"] out = "target/foundry" script = "contract-scripts" -solc_version = "0.8.24" +solc_version = "0.8.28" src = "contracts" test = "contract-tests" +via_ir = true # Set EVM version explicitly (must match across environments) evm_version = "cancun" @@ -24,3 +26,10 @@ cbor_metadata = false # If using optimizer, keep settings consistent optimizer = true optimizer_runs = 200 + +[profile.test] +ignored_error_codes = [3860, 5574] + +[rpc_endpoints] +l1_sepolia = "https://0xrpc.io/sep/" +l2_scroll_sepolia = "https://sepolia-rpc.scroll.io/" diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..e62d725 --- /dev/null +++ b/go.mod @@ -0,0 +1,36 @@ +module github.com/lightsing/uts + +go 1.24.0 + +require github.com/ethereum/go-ethereum v1.17.1 + +require ( + github.com/Microsoft/go-winio v0.6.2 // indirect + github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20251001021608-1fe7b43fc4d6 // indirect + github.com/StackExchange/wmi v1.2.1 // indirect + github.com/bits-and-blooms/bitset v1.20.0 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/consensys/gnark-crypto v0.18.1 // indirect + github.com/crate-crypto/go-eth-kzg v1.4.0 // indirect + github.com/deckarep/golang-set/v2 v2.6.0 // indirect + github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 // indirect + github.com/ethereum/c-kzg-4844/v2 v2.1.6 // indirect + github.com/fsnotify/fsnotify v1.6.0 // indirect + github.com/go-logr/logr v1.4.3 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/go-ole/go-ole v1.3.0 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/gorilla/websocket v1.4.2 // indirect + github.com/holiman/uint256 v1.3.2 // indirect + github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible // indirect + github.com/supranational/blst v0.3.16 // indirect + github.com/tklauser/go-sysconf v0.3.12 // indirect + github.com/tklauser/numcpus v0.6.1 // indirect + go.opentelemetry.io/auto/sdk v1.2.1 // indirect + go.opentelemetry.io/otel v1.39.0 // indirect + go.opentelemetry.io/otel/metric v1.39.0 // indirect + go.opentelemetry.io/otel/trace v1.39.0 // indirect + golang.org/x/crypto v0.44.0 // indirect + golang.org/x/sync v0.18.0 // indirect + golang.org/x/sys v0.39.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..8d110d0 --- /dev/null +++ b/go.sum @@ -0,0 +1,227 @@ +github.com/DataDog/zstd v1.4.5 h1:EndNeuB0l9syBZhut0wns3gV1hL8zX8LIu6ZiVHWLIQ= +github.com/DataDog/zstd v1.4.5/go.mod h1:1jcaCB/ufaK+sKp1NBhlGmpz41jOoPQ35bpF36t7BBo= +github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= +github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= +github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20251001021608-1fe7b43fc4d6 h1:1zYrtlhrZ6/b6SAjLSfKzWtdgqK0U+HtH/VcBWh1BaU= +github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20251001021608-1fe7b43fc4d6/go.mod h1:ioLG6R+5bUSO1oeGSDxOV3FADARuMoytZCSX6MEMQkI= +github.com/StackExchange/wmi v1.2.1 h1:VIkavFPXSjcnS+O8yTq7NI32k0R5Aj+v39y29VYDOSA= +github.com/StackExchange/wmi v1.2.1/go.mod h1:rcmrprowKIVzvc+NUiLncP2uuArMWLCbu9SBzvHz7e8= +github.com/VictoriaMetrics/fastcache v1.13.0 h1:AW4mheMR5Vd9FkAPUv+NH6Nhw+fmbTMGMsNAoA/+4G0= +github.com/VictoriaMetrics/fastcache v1.13.0/go.mod h1:hHXhl4DA2fTL2HTZDJFXWgW0LNjo6B+4aj2Wmng3TjU= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/bits-and-blooms/bitset v1.20.0 h1:2F+rfL86jE2d/bmw7OhqUg2Sj/1rURkBn3MdfoPyRVU= +github.com/bits-and-blooms/bitset v1.20.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8= +github.com/cespare/cp v0.1.0 h1:SE+dxFebS7Iik5LK0tsi1k9ZCxEaFX4AjQmoyA+1dJk= +github.com/cespare/cp v0.1.0/go.mod h1:SOGHArjBr4JWaSDEVpWpo/hNg6RoKrls6Oh40hiwW+s= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cockroachdb/errors v1.11.3 h1:5bA+k2Y6r+oz/6Z/RFlNeVCesGARKuC6YymtcDrbC/I= +github.com/cockroachdb/errors v1.11.3/go.mod h1:m4UIW4CDjx+R5cybPsNrRbreomiFqt8o1h1wUVazSd8= +github.com/cockroachdb/fifo v0.0.0-20240606204812-0bbfbd93a7ce h1:giXvy4KSc/6g/esnpM7Geqxka4WSqI1SZc7sMJFd3y4= +github.com/cockroachdb/fifo v0.0.0-20240606204812-0bbfbd93a7ce/go.mod h1:9/y3cnZ5GKakj/H4y9r9GTjCvAFta7KLgSHPJJYc52M= +github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b h1:r6VH0faHjZeQy818SGhaone5OnYfxFR/+AzdY3sf5aE= +github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b/go.mod h1:Vz9DsVWQQhf3vs21MhPMZpMGSht7O/2vFW2xusFUVOs= +github.com/cockroachdb/pebble v1.1.5 h1:5AAWCBWbat0uE0blr8qzufZP5tBjkRyy/jWe1QWLnvw= +github.com/cockroachdb/pebble v1.1.5/go.mod h1:17wO9el1YEigxkP/YtV8NtCivQDgoCyBg5c4VR/eOWo= +github.com/cockroachdb/redact v1.1.5 h1:u1PMllDkdFfPWaNGMyLD1+so+aq3uUItthCFqzwPJ30= +github.com/cockroachdb/redact v1.1.5/go.mod h1:BVNblN9mBWFyMyqK1k3AAiSxhvhfK2oOZZ2lK+dpvRg= +github.com/cockroachdb/tokenbucket v0.0.0-20230807174530-cc333fc44b06 h1:zuQyyAKVxetITBuuhv3BI9cMrmStnpT18zmgmTxunpo= +github.com/cockroachdb/tokenbucket v0.0.0-20230807174530-cc333fc44b06/go.mod h1:7nc4anLGjupUW/PeY5qiNYsdNXj7zopG+eqsS7To5IQ= +github.com/consensys/gnark-crypto v0.18.1 h1:RyLV6UhPRoYYzaFnPQA4qK3DyuDgkTgskDdoGqFt3fI= +github.com/consensys/gnark-crypto v0.18.1/go.mod h1:L3mXGFTe1ZN+RSJ+CLjUt9x7PNdx8ubaYfDROyp2Z8c= +github.com/cpuguy83/go-md2man/v2 v2.0.5 h1:ZtcqGrnekaHpVLArFSe4HK5DoKx1T0rq2DwVB0alcyc= +github.com/cpuguy83/go-md2man/v2 v2.0.5/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/crate-crypto/go-eth-kzg v1.4.0 h1:WzDGjHk4gFg6YzV0rJOAsTK4z3Qkz5jd4RE3DAvPFkg= +github.com/crate-crypto/go-eth-kzg v1.4.0/go.mod h1:J9/u5sWfznSObptgfa92Jq8rTswn6ahQWEuiLHOjCUI= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dchest/siphash v1.2.3 h1:QXwFc8cFOR2dSa/gE6o/HokBMWtLUaNDVd+22aKHeEA= +github.com/dchest/siphash v1.2.3/go.mod h1:0NvQU092bT0ipiFN++/rXm69QG9tVxLAlQHIXMPAkHc= +github.com/deckarep/golang-set/v2 v2.6.0 h1:XfcQbWM1LlMB8BsJ8N9vW5ehnnPVIw0je80NsVHagjM= +github.com/deckarep/golang-set/v2 v2.6.0/go.mod h1:VAky9rY/yGXJOLEDv3OMci+7wtDpOF4IN+y82NBOac4= +github.com/decred/dcrd/crypto/blake256 v1.0.0 h1:/8DMNYp9SGi5f0w7uCm6d6M4OU2rGFK09Y2A4Xv7EE0= +github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 h1:YLtO71vCjJRCBcrPMtQ9nqBsqpA1m5sE92cU+pd5Mcc= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1/go.mod h1:hyedUtir6IdtD/7lIxGeCxkaw7y45JueMRL4DIyJDKs= +github.com/deepmap/oapi-codegen v1.6.0 h1:w/d1ntwh91XI0b/8ja7+u5SvA4IFfM0UNNLmiDR1gg0= +github.com/deepmap/oapi-codegen v1.6.0/go.mod h1:ryDa9AgbELGeB+YEXE1dR53yAjHwFvE9iAUlWl9Al3M= +github.com/emicklei/dot v1.6.2 h1:08GN+DD79cy/tzN6uLCT84+2Wk9u+wvqP+Hkx/dIR8A= +github.com/emicklei/dot v1.6.2/go.mod h1:DeV7GvQtIw4h2u73RKBkkFdvVAz0D9fzeJrgPW6gy/s= +github.com/ethereum/c-kzg-4844/v2 v2.1.6 h1:xQymkKCT5E2Jiaoqf3v4wsNgjZLY0lRSkZn27fRjSls= +github.com/ethereum/c-kzg-4844/v2 v2.1.6/go.mod h1:8HMkUZ5JRv4hpw/XUrYWSQNAUzhHMg2UDb/U+5m+XNw= +github.com/ethereum/go-bigmodexpfix v0.0.0-20250911101455-f9e208c548ab h1:rvv6MJhy07IMfEKuARQ9TKojGqLVNxQajaXEp/BoqSk= +github.com/ethereum/go-bigmodexpfix v0.0.0-20250911101455-f9e208c548ab/go.mod h1:IuLm4IsPipXKF7CW5Lzf68PIbZ5yl7FFd74l/E0o9A8= +github.com/ethereum/go-ethereum v1.17.1 h1:IjlQDjgxg2uL+GzPRkygGULPMLzcYWncEI7wbaizvho= +github.com/ethereum/go-ethereum v1.17.1/go.mod h1:7UWOVHL7K3b8RfVRea022btnzLCaanwHtBuH1jUCH/I= +github.com/ferranbt/fastssz v0.1.4 h1:OCDB+dYDEQDvAgtAGnTSidK1Pe2tW3nFV40XyMkTeDY= +github.com/ferranbt/fastssz v0.1.4/go.mod h1:Ea3+oeoRGGLGm5shYAeDgu6PGUlcvQhE2fILyD9+tGg= +github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= +github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= +github.com/gballet/go-libpcsclite v0.0.0-20190607065134-2772fd86a8ff h1:tY80oXqGNY4FhTFhk+o9oFHGINQ/+vhlm8HFzi6znCI= +github.com/gballet/go-libpcsclite v0.0.0-20190607065134-2772fd86a8ff/go.mod h1:x7DCsMOv1taUwEWCzT4cmDeAkigA5/QCwUodaVOe8Ww= +github.com/getsentry/sentry-go v0.27.0 h1:Pv98CIbtB3LkMWmXi4Joa5OOcwbmnX88sF5qbK3r3Ps= +github.com/getsentry/sentry-go v0.27.0/go.mod h1:lc76E2QywIyW8WuBnwl8Lc4bkmQH4+w1gwTf25trprY= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-ole/go-ole v1.2.5/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= +github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= +github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= +github.com/gofrs/flock v0.12.1 h1:MTLVXXHf8ekldpJk3AKicLij9MdwOWkZ+a/jHHZby9E= +github.com/gofrs/flock v0.12.1/go.mod h1:9zxTsyu5xtJ9DK+1tFZyibEV7y3uwDxPPfbxeeHCoD0= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI= +github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs= +github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= +github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= +github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/grafana/pyroscope-go v1.2.7 h1:VWBBlqxjyR0Cwk2W6UrE8CdcdD80GOFNutj0Kb1T8ac= +github.com/grafana/pyroscope-go v1.2.7/go.mod h1:o/bpSLiJYYP6HQtvcoVKiE9s5RiNgjYTj1DhiddP2Pc= +github.com/grafana/pyroscope-go/godeltaprof v0.1.9 h1:c1Us8i6eSmkW+Ez05d3co8kasnuOY813tbMN8i/a3Og= +github.com/grafana/pyroscope-go/godeltaprof v0.1.9/go.mod h1:2+l7K7twW49Ct4wFluZD3tZ6e0SjanjcUUBPVD/UuGU= +github.com/graph-gophers/graphql-go v1.3.0 h1:Eb9x/q6MFpCLz7jBCiP/WTxjSDrYLR1QY41SORZyNJ0= +github.com/graph-gophers/graphql-go v1.3.0/go.mod h1:9CQHMSxwO4MprSdzoIEobiHpoLtHm77vfxsvsIN5Vuc= +github.com/hashicorp/go-bexpr v0.1.10 h1:9kuI5PFotCboP3dkDYFr/wi0gg0QVbSNz5oFRpxn4uE= +github.com/hashicorp/go-bexpr v0.1.10/go.mod h1:oxlubA2vC/gFVfX1A6JGp7ls7uCDlfJn732ehYYg+g0= +github.com/holiman/billy v0.0.0-20250707135307-f2f9b9aae7db h1:IZUYC/xb3giYwBLMnr8d0TGTzPKFGNTCGgGLoyeX330= +github.com/holiman/billy v0.0.0-20250707135307-f2f9b9aae7db/go.mod h1:xTEYN9KCHxuYHs+NmrmzFcnvHMzLLNiGFafCb1n3Mfg= +github.com/holiman/bloomfilter/v2 v2.0.3 h1:73e0e/V0tCydx14a0SCYS/EWCxgwLZ18CZcZKVu0fao= +github.com/holiman/bloomfilter/v2 v2.0.3/go.mod h1:zpoh+gs7qcpqrHr3dB55AMiJwo0iURXE7ZOP9L9hSkA= +github.com/holiman/uint256 v1.3.2 h1:a9EgMPSC1AAaj1SZL5zIQD3WbwTuHrMGOerLjGmM/TA= +github.com/holiman/uint256 v1.3.2/go.mod h1:EOMSn4q6Nyt9P6efbI3bueV4e1b3dGlUCXeiRV4ng7E= +github.com/huin/goupnp v1.3.0 h1:UvLUlWDNpoUdYzb2TCn+MuTWtcjXKSza2n6CBdQ0xXc= +github.com/huin/goupnp v1.3.0/go.mod h1:gnGPsThkYa7bFi/KWmEysQRf48l2dvR5bxr2OFckNX8= +github.com/influxdata/influxdb-client-go/v2 v2.4.0 h1:HGBfZYStlx3Kqvsv1h2pJixbCl/jhnFtxpKFAv9Tu5k= +github.com/influxdata/influxdb-client-go/v2 v2.4.0/go.mod h1:vLNHdxTJkIf2mSLvGrpj8TCcISApPoXkaxP8g9uRlW8= +github.com/influxdata/influxdb1-client v0.0.0-20220302092344-a9ab5670611c h1:qSHzRbhzK8RdXOsAdfDgO49TtqC1oZ+acxPrkfTxcCs= +github.com/influxdata/influxdb1-client v0.0.0-20220302092344-a9ab5670611c/go.mod h1:qj24IKcXYK6Iy9ceXlo3Tc+vtHo9lIhSX5JddghvEPo= +github.com/influxdata/line-protocol v0.0.0-20200327222509-2487e7298839 h1:W9WBk7wlPfJLvMCdtV4zPulc4uCPrlywQOmbFOhgQNU= +github.com/influxdata/line-protocol v0.0.0-20200327222509-2487e7298839/go.mod h1:xaLFMmpvUxqXtVkUJfg9QmT88cDaCJ3ZKgdZ78oO8Qo= +github.com/jackpal/go-nat-pmp v1.0.2 h1:KzKSgb7qkJvOUTqYl9/Hg/me3pWgBmERKrTGD7BdWus= +github.com/jackpal/go-nat-pmp v1.0.2/go.mod h1:QPH045xvCAeXUZOxsnwmrtiCoxIr9eob+4orBN1SBKc= +github.com/klauspost/compress v1.17.8 h1:YcnTYrq7MikUT7k0Yb5eceMmALQPYBW/Xltxn0NAMnU= +github.com/klauspost/compress v1.17.8/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= +github.com/klauspost/cpuid/v2 v2.0.9 h1:lgaqFMSdTdQYdZ04uHyN2d/eKdOMyi2YLSvlQIBFYa4= +github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/leanovate/gopter v0.2.11 h1:vRjThO1EKPb/1NsDXuDrzldR28RLkBflWYcU9CvzWu4= +github.com/leanovate/gopter v0.2.11/go.mod h1:aK3tzZP/C+p1m3SPRE4SYZFGP7jjkuSI4f7Xvpt0S9c= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU= +github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo= +github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= +github.com/minio/sha256-simd v1.0.0 h1:v1ta+49hkWZyvaKwrQB8elexRqm6Y0aMLjCNsrYxo6g= +github.com/minio/sha256-simd v1.0.0/go.mod h1:OuYzVNI5vcoYIAmbIvHPl3N3jUzVedXbKy5RFepssQM= +github.com/mitchellh/mapstructure v1.4.1 h1:CpVNEelQCZBooIPDn+AR3NpivK/TIKU8bDxdASFVQag= +github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/mitchellh/pointerstructure v1.2.0 h1:O+i9nHnXS3l/9Wu7r4NrEdwA2VFTicjUEN1uBnDo34A= +github.com/mitchellh/pointerstructure v1.2.0/go.mod h1:BRAsLI5zgXmw97Lf6s25bs8ohIXc3tViBH44KcwB2g4= +github.com/opentracing/opentracing-go v1.1.0 h1:pWlfV3Bxv7k65HYwkikxat0+s3pV4bsqf19k25Ur8rU= +github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= +github.com/peterh/liner v1.1.1-0.20190123174540-a2c9a5303de7 h1:oYW+YCJ1pachXTQmzR3rNLYGGz4g/UgFcjb28p/viDM= +github.com/peterh/liner v1.1.1-0.20190123174540-a2c9a5303de7/go.mod h1:CRroGNssyjTd/qIG2FyxByd2S8JEAZXBl4qUrZf8GS0= +github.com/pion/dtls/v2 v2.2.7 h1:cSUBsETxepsCSFSxC3mc/aDo14qQLMSL+O6IjG28yV8= +github.com/pion/dtls/v2 v2.2.7/go.mod h1:8WiMkebSHFD0T+dIU+UeBaoV7kDhOW5oDCzZ7WZ/F9s= +github.com/pion/logging v0.2.2 h1:M9+AIj/+pxNsDfAT64+MAVgJO0rsyLnoJKCqf//DoeY= +github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms= +github.com/pion/stun/v2 v2.0.0 h1:A5+wXKLAypxQri59+tmQKVs7+l6mMM+3d+eER9ifRU0= +github.com/pion/stun/v2 v2.0.0/go.mod h1:22qRSh08fSEttYUmJZGlriq9+03jtVmXNODgLccj8GQ= +github.com/pion/transport/v2 v2.2.1 h1:7qYnCBlpgSJNYMbLCKuSY9KbQdBFoETvPNETv0y4N7c= +github.com/pion/transport/v2 v2.2.1/go.mod h1:cXXWavvCnFF6McHTft3DWS9iic2Mftcz1Aq29pGcU5g= +github.com/pion/transport/v3 v3.0.1 h1:gDTlPJwROfSfz6QfSi0ZmeCSkFcnWWiiR9ES0ouANiM= +github.com/pion/transport/v3 v3.0.1/go.mod h1:UY7kiITrlMv7/IKgd5eTUcaahZx5oUN3l9SzK5f5xE0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v1.15.0 h1:5fCgGYogn0hFdhyhLbw7hEsWxufKtY9klyvdNfFlFhM= +github.com/prometheus/client_golang v1.15.0/go.mod h1:e9yaBhRPU2pPNsZwE+JdQl0KEt1N9XgF6zxWmaC0xOk= +github.com/prometheus/client_model v0.3.0 h1:UBgGFHqYdG/TPFD1B1ogZywDqEkwp3fBMvqdiQ7Xew4= +github.com/prometheus/client_model v0.3.0/go.mod h1:LDGWKZIo7rky3hgvBe+caln+Dr3dPggB5dvjtD7w9+w= +github.com/prometheus/common v0.42.0 h1:EKsfXEYo4JpWMHH5cg+KOUWeuJSov1Id8zGR8eeI1YM= +github.com/prometheus/common v0.42.0/go.mod h1:xBwqVerjNdUDjgODMpudtOMwlOwf2SaTr1yjz4b7Zbc= +github.com/prometheus/procfs v0.9.0 h1:wzCHvIvM5SxWqYvwgVL7yJY8Lz3PKn49KQtpgMYJfhI= +github.com/prometheus/procfs v0.9.0/go.mod h1:+pB4zwohETzFnmlpe6yd2lSc+0/46IYZRB/chUwxUZY= +github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= +github.com/rs/cors v1.7.0 h1:+88SsELBHx5r+hZ8TCkggzSstaWNbDvThkVK8H6f9ik= +github.com/rs/cors v1.7.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU= +github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible h1:Bn1aCHHRnjv4Bl16T8rcaFjYSrGrIZvpiGO6P3Q4GpU= +github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/supranational/blst v0.3.16 h1:bTDadT+3fK497EvLdWRQEjiGnUtzJ7jjIUMF0jqwYhE= +github.com/supranational/blst v0.3.16/go.mod h1:jZJtfjgudtNl4en1tzwPIV3KjUnQUvG3/j+w+fVonLw= +github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 h1:epCh84lMvA70Z7CTTCmYQn2CKbY8j86K7/FAIr141uY= +github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45IWZaF22tdD+VEXcAWRA037jwmWEB5VWYORlTpc= +github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU= +github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI= +github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk= +github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY= +github.com/urfave/cli/v2 v2.27.5 h1:WoHEJLdsXr6dDWoJgMq/CboDmyY/8HMMH1fTECbih+w= +github.com/urfave/cli/v2 v2.27.5/go.mod h1:3Sevf16NykTbInEnD0yKkjDAeZDS0A6bzhBH5hrMvTQ= +github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4= +github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= +go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48= +go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8= +go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0= +go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs= +go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18= +go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE= +go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI= +go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA= +golang.org/x/crypto v0.44.0 h1:A97SsFvM3AIwEEmTBiaxPPTYpDC47w720rdiiUvgoAU= +golang.org/x/crypto v0.44.0/go.mod h1:013i+Nw79BMiQiMsOPcVCB5ZIJbYkerPrGnOa00tvmc= +golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df h1:UA2aFVmmsIlefxMk29Dp2juaUSth8Pyn3Tq5Y5mJGME= +golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc= +golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= +golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= +golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I= +golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= +golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= +golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= +golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY= +golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= +google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc= +gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/lib/eas-contracts b/lib/eas-contracts new file mode 160000 index 0000000..d223e17 --- /dev/null +++ b/lib/eas-contracts @@ -0,0 +1 @@ +Subproject commit d223e17208aa110dd5ec694d77324a2321d93201 diff --git a/lib/scroll-contracts b/lib/scroll-contracts new file mode 160000 index 0000000..42de954 --- /dev/null +++ b/lib/scroll-contracts @@ -0,0 +1 @@ +Subproject commit 42de954bee237cfa478a5b443ac0aeb900aca5ad diff --git a/lib/solady b/lib/solady new file mode 160000 index 0000000..acd959a --- /dev/null +++ b/lib/solady @@ -0,0 +1 @@ +Subproject commit acd959aa4bd04720d640bf4e6a5c71037510cc4b diff --git a/package.json b/package.json index f3e3c8d..1c75d58 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,7 @@ { "name": "uts-monorepo", "private": true, + "license": "MIT OR Apache-2.0", "type": "module", "scripts": { "build": "pnpm -r run build", @@ -18,6 +19,7 @@ "eslint-plugin-unused-imports": "^4.4.1", "pnpm": "^10.26.2", "prettier": "^3.8.1", - "typescript-eslint": "^8.56.1" + "typescript-eslint": "^8.56.1", + "wrangler": "^4.73.0" } } diff --git a/packages/contract-types/LICENSE-APACHE b/packages/contract-types/LICENSE-APACHE new file mode 120000 index 0000000..1cd601d --- /dev/null +++ b/packages/contract-types/LICENSE-APACHE @@ -0,0 +1 @@ +../../LICENSE-APACHE \ No newline at end of file diff --git a/packages/contract-types/LICENSE-MIT b/packages/contract-types/LICENSE-MIT new file mode 120000 index 0000000..b2cfbdc --- /dev/null +++ b/packages/contract-types/LICENSE-MIT @@ -0,0 +1 @@ +../../LICENSE-MIT \ No newline at end of file diff --git a/packages/contract-types/package.json b/packages/contract-types/package.json new file mode 100644 index 0000000..fa948a6 --- /dev/null +++ b/packages/contract-types/package.json @@ -0,0 +1,41 @@ +{ + "name": "@universal-timestamps/contracts", + "version": "0.1.0-alpha.1", + "description": "", + "type": "module", + "main": "./dist/index.js", + "module": "./dist/index.js", + "types": "./dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "uts-source": "./src/index.ts", + "import": "./dist/index.js", + "default": "./dist/index.js" + } + }, + "files": [ + "dist", + "!dist/.tsbuildinfo", + "!dist/test", + "src/**/*.ts" + ], + "scripts": { + "build": "rimraf dist && tsc && rollup -c", + "dev": "rollup -c --watch", + "lint": "eslint src --cache --fix", + "typecheck": "tsc --noEmit" + }, + "keywords": [], + "author": "", + "license": "(MIT OR Apache-2.0)", + "packageManager": "pnpm@10.30.3", + "devDependencies": { + "@rollup/plugin-node-resolve": "^16.0.3", + "@rollup/plugin-typescript": "^12.3.0", + "@wagmi/cli": "^2.10.0", + "rimraf": "^6.1.3", + "rollup": "^4.59.0", + "typescript": "^5.9.3" + } +} diff --git a/packages/sdk/rollup.config.js b/packages/contract-types/rollup.config.js similarity index 89% rename from packages/sdk/rollup.config.js rename to packages/contract-types/rollup.config.js index dcff556..e86fa25 100644 --- a/packages/sdk/rollup.config.js +++ b/packages/contract-types/rollup.config.js @@ -12,7 +12,6 @@ export default [ sourcemap: true, }, ], - external: [/node_modules/, '@noble/hashes', 'ethers'], plugins: [ resolve(), typescript({ diff --git a/packages/contract-types/src/generated.ts b/packages/contract-types/src/generated.ts new file mode 100644 index 0000000..683fed2 --- /dev/null +++ b/packages/contract-types/src/generated.ts @@ -0,0 +1,1120 @@ +////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// IEAS +////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +export const ieasAbi = [ + { + type: 'function', + inputs: [ + { + name: 'request', + internalType: 'struct AttestationRequest', + type: 'tuple', + components: [ + { name: 'schema', internalType: 'bytes32', type: 'bytes32' }, + { + name: 'data', + internalType: 'struct AttestationRequestData', + type: 'tuple', + components: [ + { name: 'recipient', internalType: 'address', type: 'address' }, + { + name: 'expirationTime', + internalType: 'uint64', + type: 'uint64', + }, + { name: 'revocable', internalType: 'bool', type: 'bool' }, + { name: 'refUID', internalType: 'bytes32', type: 'bytes32' }, + { name: 'data', internalType: 'bytes', type: 'bytes' }, + { name: 'value', internalType: 'uint256', type: 'uint256' }, + ], + }, + ], + }, + ], + name: 'attest', + outputs: [{ name: '', internalType: 'bytes32', type: 'bytes32' }], + stateMutability: 'payable', + }, + { + type: 'function', + inputs: [ + { + name: 'delegatedRequest', + internalType: 'struct DelegatedAttestationRequest', + type: 'tuple', + components: [ + { name: 'schema', internalType: 'bytes32', type: 'bytes32' }, + { + name: 'data', + internalType: 'struct AttestationRequestData', + type: 'tuple', + components: [ + { name: 'recipient', internalType: 'address', type: 'address' }, + { + name: 'expirationTime', + internalType: 'uint64', + type: 'uint64', + }, + { name: 'revocable', internalType: 'bool', type: 'bool' }, + { name: 'refUID', internalType: 'bytes32', type: 'bytes32' }, + { name: 'data', internalType: 'bytes', type: 'bytes' }, + { name: 'value', internalType: 'uint256', type: 'uint256' }, + ], + }, + { + name: 'signature', + internalType: 'struct Signature', + type: 'tuple', + components: [ + { name: 'v', internalType: 'uint8', type: 'uint8' }, + { name: 'r', internalType: 'bytes32', type: 'bytes32' }, + { name: 's', internalType: 'bytes32', type: 'bytes32' }, + ], + }, + { name: 'attester', internalType: 'address', type: 'address' }, + { name: 'deadline', internalType: 'uint64', type: 'uint64' }, + ], + }, + ], + name: 'attestByDelegation', + outputs: [{ name: '', internalType: 'bytes32', type: 'bytes32' }], + stateMutability: 'payable', + }, + { + type: 'function', + inputs: [{ name: 'uid', internalType: 'bytes32', type: 'bytes32' }], + name: 'getAttestation', + outputs: [ + { + name: '', + internalType: 'struct Attestation', + type: 'tuple', + components: [ + { name: 'uid', internalType: 'bytes32', type: 'bytes32' }, + { name: 'schema', internalType: 'bytes32', type: 'bytes32' }, + { name: 'time', internalType: 'uint64', type: 'uint64' }, + { + name: 'expirationTime', + internalType: 'uint64', + type: 'uint64', + }, + { + name: 'revocationTime', + internalType: 'uint64', + type: 'uint64', + }, + { name: 'refUID', internalType: 'bytes32', type: 'bytes32' }, + { name: 'recipient', internalType: 'address', type: 'address' }, + { name: 'attester', internalType: 'address', type: 'address' }, + { name: 'revocable', internalType: 'bool', type: 'bool' }, + { name: 'data', internalType: 'bytes', type: 'bytes' }, + ], + }, + ], + stateMutability: 'view', + }, + { + type: 'function', + inputs: [ + { name: 'revoker', internalType: 'address', type: 'address' }, + { name: 'data', internalType: 'bytes32', type: 'bytes32' }, + ], + name: 'getRevokeOffchain', + outputs: [{ name: '', internalType: 'uint64', type: 'uint64' }], + stateMutability: 'view', + }, + { + type: 'function', + inputs: [], + name: 'getSchemaRegistry', + outputs: [ + { name: '', internalType: 'contract ISchemaRegistry', type: 'address' }, + ], + stateMutability: 'view', + }, + { + type: 'function', + inputs: [{ name: 'data', internalType: 'bytes32', type: 'bytes32' }], + name: 'getTimestamp', + outputs: [{ name: '', internalType: 'uint64', type: 'uint64' }], + stateMutability: 'view', + }, + { + type: 'function', + inputs: [{ name: 'uid', internalType: 'bytes32', type: 'bytes32' }], + name: 'isAttestationValid', + outputs: [{ name: '', internalType: 'bool', type: 'bool' }], + stateMutability: 'view', + }, + { + type: 'function', + inputs: [ + { + name: 'multiRequests', + internalType: 'struct MultiAttestationRequest[]', + type: 'tuple[]', + components: [ + { name: 'schema', internalType: 'bytes32', type: 'bytes32' }, + { + name: 'data', + internalType: 'struct AttestationRequestData[]', + type: 'tuple[]', + components: [ + { name: 'recipient', internalType: 'address', type: 'address' }, + { + name: 'expirationTime', + internalType: 'uint64', + type: 'uint64', + }, + { name: 'revocable', internalType: 'bool', type: 'bool' }, + { name: 'refUID', internalType: 'bytes32', type: 'bytes32' }, + { name: 'data', internalType: 'bytes', type: 'bytes' }, + { name: 'value', internalType: 'uint256', type: 'uint256' }, + ], + }, + ], + }, + ], + name: 'multiAttest', + outputs: [{ name: '', internalType: 'bytes32[]', type: 'bytes32[]' }], + stateMutability: 'payable', + }, + { + type: 'function', + inputs: [ + { + name: 'multiDelegatedRequests', + internalType: 'struct MultiDelegatedAttestationRequest[]', + type: 'tuple[]', + components: [ + { name: 'schema', internalType: 'bytes32', type: 'bytes32' }, + { + name: 'data', + internalType: 'struct AttestationRequestData[]', + type: 'tuple[]', + components: [ + { name: 'recipient', internalType: 'address', type: 'address' }, + { + name: 'expirationTime', + internalType: 'uint64', + type: 'uint64', + }, + { name: 'revocable', internalType: 'bool', type: 'bool' }, + { name: 'refUID', internalType: 'bytes32', type: 'bytes32' }, + { name: 'data', internalType: 'bytes', type: 'bytes' }, + { name: 'value', internalType: 'uint256', type: 'uint256' }, + ], + }, + { + name: 'signatures', + internalType: 'struct Signature[]', + type: 'tuple[]', + components: [ + { name: 'v', internalType: 'uint8', type: 'uint8' }, + { name: 'r', internalType: 'bytes32', type: 'bytes32' }, + { name: 's', internalType: 'bytes32', type: 'bytes32' }, + ], + }, + { name: 'attester', internalType: 'address', type: 'address' }, + { name: 'deadline', internalType: 'uint64', type: 'uint64' }, + ], + }, + ], + name: 'multiAttestByDelegation', + outputs: [{ name: '', internalType: 'bytes32[]', type: 'bytes32[]' }], + stateMutability: 'payable', + }, + { + type: 'function', + inputs: [ + { + name: 'multiRequests', + internalType: 'struct MultiRevocationRequest[]', + type: 'tuple[]', + components: [ + { name: 'schema', internalType: 'bytes32', type: 'bytes32' }, + { + name: 'data', + internalType: 'struct RevocationRequestData[]', + type: 'tuple[]', + components: [ + { name: 'uid', internalType: 'bytes32', type: 'bytes32' }, + { name: 'value', internalType: 'uint256', type: 'uint256' }, + ], + }, + ], + }, + ], + name: 'multiRevoke', + outputs: [], + stateMutability: 'payable', + }, + { + type: 'function', + inputs: [ + { + name: 'multiDelegatedRequests', + internalType: 'struct MultiDelegatedRevocationRequest[]', + type: 'tuple[]', + components: [ + { name: 'schema', internalType: 'bytes32', type: 'bytes32' }, + { + name: 'data', + internalType: 'struct RevocationRequestData[]', + type: 'tuple[]', + components: [ + { name: 'uid', internalType: 'bytes32', type: 'bytes32' }, + { name: 'value', internalType: 'uint256', type: 'uint256' }, + ], + }, + { + name: 'signatures', + internalType: 'struct Signature[]', + type: 'tuple[]', + components: [ + { name: 'v', internalType: 'uint8', type: 'uint8' }, + { name: 'r', internalType: 'bytes32', type: 'bytes32' }, + { name: 's', internalType: 'bytes32', type: 'bytes32' }, + ], + }, + { name: 'revoker', internalType: 'address', type: 'address' }, + { name: 'deadline', internalType: 'uint64', type: 'uint64' }, + ], + }, + ], + name: 'multiRevokeByDelegation', + outputs: [], + stateMutability: 'payable', + }, + { + type: 'function', + inputs: [{ name: 'data', internalType: 'bytes32[]', type: 'bytes32[]' }], + name: 'multiRevokeOffchain', + outputs: [{ name: '', internalType: 'uint64', type: 'uint64' }], + stateMutability: 'nonpayable', + }, + { + type: 'function', + inputs: [{ name: 'data', internalType: 'bytes32[]', type: 'bytes32[]' }], + name: 'multiTimestamp', + outputs: [{ name: '', internalType: 'uint64', type: 'uint64' }], + stateMutability: 'nonpayable', + }, + { + type: 'function', + inputs: [ + { + name: 'request', + internalType: 'struct RevocationRequest', + type: 'tuple', + components: [ + { name: 'schema', internalType: 'bytes32', type: 'bytes32' }, + { + name: 'data', + internalType: 'struct RevocationRequestData', + type: 'tuple', + components: [ + { name: 'uid', internalType: 'bytes32', type: 'bytes32' }, + { name: 'value', internalType: 'uint256', type: 'uint256' }, + ], + }, + ], + }, + ], + name: 'revoke', + outputs: [], + stateMutability: 'payable', + }, + { + type: 'function', + inputs: [ + { + name: 'delegatedRequest', + internalType: 'struct DelegatedRevocationRequest', + type: 'tuple', + components: [ + { name: 'schema', internalType: 'bytes32', type: 'bytes32' }, + { + name: 'data', + internalType: 'struct RevocationRequestData', + type: 'tuple', + components: [ + { name: 'uid', internalType: 'bytes32', type: 'bytes32' }, + { name: 'value', internalType: 'uint256', type: 'uint256' }, + ], + }, + { + name: 'signature', + internalType: 'struct Signature', + type: 'tuple', + components: [ + { name: 'v', internalType: 'uint8', type: 'uint8' }, + { name: 'r', internalType: 'bytes32', type: 'bytes32' }, + { name: 's', internalType: 'bytes32', type: 'bytes32' }, + ], + }, + { name: 'revoker', internalType: 'address', type: 'address' }, + { name: 'deadline', internalType: 'uint64', type: 'uint64' }, + ], + }, + ], + name: 'revokeByDelegation', + outputs: [], + stateMutability: 'payable', + }, + { + type: 'function', + inputs: [{ name: 'data', internalType: 'bytes32', type: 'bytes32' }], + name: 'revokeOffchain', + outputs: [{ name: '', internalType: 'uint64', type: 'uint64' }], + stateMutability: 'nonpayable', + }, + { + type: 'function', + inputs: [{ name: 'data', internalType: 'bytes32', type: 'bytes32' }], + name: 'timestamp', + outputs: [{ name: '', internalType: 'uint64', type: 'uint64' }], + stateMutability: 'nonpayable', + }, + { + type: 'event', + anonymous: false, + inputs: [ + { + name: 'recipient', + internalType: 'address', + type: 'address', + indexed: true, + }, + { + name: 'attester', + internalType: 'address', + type: 'address', + indexed: true, + }, + { name: 'uid', internalType: 'bytes32', type: 'bytes32', indexed: false }, + { + name: 'schemaUID', + internalType: 'bytes32', + type: 'bytes32', + indexed: true, + }, + ], + name: 'Attested', + }, + { + type: 'event', + anonymous: false, + inputs: [ + { + name: 'recipient', + internalType: 'address', + type: 'address', + indexed: true, + }, + { + name: 'attester', + internalType: 'address', + type: 'address', + indexed: true, + }, + { name: 'uid', internalType: 'bytes32', type: 'bytes32', indexed: false }, + { + name: 'schemaUID', + internalType: 'bytes32', + type: 'bytes32', + indexed: true, + }, + ], + name: 'Revoked', + }, + { + type: 'event', + anonymous: false, + inputs: [ + { + name: 'revoker', + internalType: 'address', + type: 'address', + indexed: true, + }, + { + name: 'data', + internalType: 'bytes32', + type: 'bytes32', + indexed: true, + }, + { + name: 'timestamp', + internalType: 'uint64', + type: 'uint64', + indexed: true, + }, + ], + name: 'RevokedOffchain', + }, + { + type: 'event', + anonymous: false, + inputs: [ + { + name: 'data', + internalType: 'bytes32', + type: 'bytes32', + indexed: true, + }, + { + name: 'timestamp', + internalType: 'uint64', + type: 'uint64', + indexed: true, + }, + ], + name: 'Timestamped', + }, +] as const + +////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// IFeeOracle +////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +export const iFeeOracleAbi = [ + { + type: 'function', + inputs: [], + name: 'getFloorFee', + outputs: [{ name: '', internalType: 'uint256', type: 'uint256' }], + stateMutability: 'view', + }, + { + type: 'function', + inputs: [ + { + name: 'crossDomainGasEstimated', + internalType: 'uint256', + type: 'uint256', + }, + ], + name: 'setCrossDomainGasEstimated', + outputs: [], + stateMutability: 'nonpayable', + }, + { + type: 'function', + inputs: [ + { name: 'expectedBatchSize', internalType: 'uint256', type: 'uint256' }, + ], + name: 'setExpectedBatchSize', + outputs: [], + stateMutability: 'nonpayable', + }, + { + type: 'function', + inputs: [ + { name: 'feeMultiplier', internalType: 'uint256', type: 'uint256' }, + ], + name: 'setFeeMultiplier', + outputs: [], + stateMutability: 'nonpayable', + }, + { + type: 'function', + inputs: [{ name: 'l1FeeScalar', internalType: 'uint256', type: 'uint256' }], + name: 'setL1FeeScalar', + outputs: [], + stateMutability: 'nonpayable', + }, + { + type: 'function', + inputs: [ + { name: 'l1GasEstimated', internalType: 'uint256', type: 'uint256' }, + ], + name: 'setL1GasEstimated', + outputs: [], + stateMutability: 'nonpayable', + }, + { + type: 'function', + inputs: [{ name: 'l1Overhead', internalType: 'uint256', type: 'uint256' }], + name: 'setL1Overhead', + outputs: [], + stateMutability: 'nonpayable', + }, + { + type: 'function', + inputs: [ + { name: 'l2ExecutionOverhead', internalType: 'uint256', type: 'uint256' }, + ], + name: 'setL2ExecutionOverhead', + outputs: [], + stateMutability: 'nonpayable', + }, + { + type: 'function', + inputs: [ + { name: 'l2ExecutionScalar', internalType: 'uint256', type: 'uint256' }, + ], + name: 'setL2ExecutionScalar', + outputs: [], + stateMutability: 'nonpayable', + }, + { + type: 'event', + anonymous: false, + inputs: [ + { + name: 'crossDomainGasEstimated', + internalType: 'uint256', + type: 'uint256', + indexed: false, + }, + ], + name: 'CrossDomainGasEstimatedUpdated', + }, + { + type: 'event', + anonymous: false, + inputs: [ + { + name: 'expectedBatchSize', + internalType: 'uint256', + type: 'uint256', + indexed: false, + }, + ], + name: 'ExpectedBatchSizeUpdated', + }, + { + type: 'event', + anonymous: false, + inputs: [ + { + name: 'feeMultiplier', + internalType: 'uint256', + type: 'uint256', + indexed: false, + }, + ], + name: 'FeeMultiplierUpdated', + }, + { + type: 'event', + anonymous: false, + inputs: [ + { + name: 'l1FeeScalar', + internalType: 'uint256', + type: 'uint256', + indexed: false, + }, + ], + name: 'L1FeeScalarUpdated', + }, + { + type: 'event', + anonymous: false, + inputs: [ + { + name: 'l1GasEstimated', + internalType: 'uint256', + type: 'uint256', + indexed: false, + }, + ], + name: 'L1GasEstimatedUpdated', + }, + { + type: 'event', + anonymous: false, + inputs: [ + { + name: 'l1Overhead', + internalType: 'uint256', + type: 'uint256', + indexed: false, + }, + ], + name: 'L1OverheadUpdated', + }, + { + type: 'event', + anonymous: false, + inputs: [ + { + name: 'l2ExecutionOverhead', + internalType: 'uint256', + type: 'uint256', + indexed: false, + }, + ], + name: 'L2ExecutionOverheadUpdated', + }, + { + type: 'event', + anonymous: false, + inputs: [ + { + name: 'l2ExecutionScalar', + internalType: 'uint256', + type: 'uint256', + indexed: false, + }, + ], + name: 'L2ExecutionScalarUpdated', + }, +] as const + +////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// IL1AnchoringGateway +////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +export const il1AnchoringGatewayAbi = [ + { + type: 'function', + inputs: [ + { name: 'newMessenger', internalType: 'address', type: 'address' }, + ], + name: 'setL1ScrollMessenger', + outputs: [], + stateMutability: 'nonpayable', + }, + { + type: 'function', + inputs: [{ name: 'newManager', internalType: 'address', type: 'address' }], + name: 'setL2AnchoringManager', + outputs: [], + stateMutability: 'nonpayable', + }, + { + type: 'function', + inputs: [ + { name: 'merkleRoot', internalType: 'bytes32', type: 'bytes32' }, + { name: 'startIndex', internalType: 'uint256', type: 'uint256' }, + { name: 'count', internalType: 'uint256', type: 'uint256' }, + { name: 'gasLimit', internalType: 'uint256', type: 'uint256' }, + ], + name: 'submitBatch', + outputs: [], + stateMutability: 'payable', + }, + { + type: 'event', + anonymous: false, + inputs: [ + { + name: 'merkleRoot', + internalType: 'bytes32', + type: 'bytes32', + indexed: true, + }, + { + name: 'startIndex', + internalType: 'uint256', + type: 'uint256', + indexed: true, + }, + { + name: 'count', + internalType: 'uint256', + type: 'uint256', + indexed: false, + }, + { + name: 'submitter', + internalType: 'address', + type: 'address', + indexed: true, + }, + ], + name: 'BatchSubmitted', + }, + { + type: 'event', + anonymous: false, + inputs: [ + { + name: 'oldMessenger', + internalType: 'address', + type: 'address', + indexed: true, + }, + { + name: 'newMessenger', + internalType: 'address', + type: 'address', + indexed: true, + }, + ], + name: 'L1ScrollMessengerUpdated', + }, + { + type: 'event', + anonymous: false, + inputs: [ + { + name: 'oldManager', + internalType: 'address', + type: 'address', + indexed: true, + }, + { + name: 'newManager', + internalType: 'address', + type: 'address', + indexed: true, + }, + ], + name: 'L2AnchoringManagerUpdated', + }, +] as const + +////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// IL2AnchoringManager +////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +export const il2AnchoringManagerAbi = [ + { + type: 'function', + inputs: [ + { name: 'attestationId', internalType: 'bytes32', type: 'bytes32' }, + { name: 'batchStartIndexHint', internalType: 'uint256', type: 'uint256' }, + ], + name: 'claimNFT', + outputs: [], + stateMutability: 'nonpayable', + }, + { + type: 'function', + inputs: [], + name: 'clearBatch', + outputs: [], + stateMutability: 'nonpayable', + }, + { + type: 'function', + inputs: [], + name: 'finalizeBatch', + outputs: [], + stateMutability: 'nonpayable', + }, + { + type: 'function', + inputs: [ + { name: 'attestationId', internalType: 'bytes32', type: 'bytes32' }, + ], + name: 'isConfirmed', + outputs: [{ name: '', internalType: 'bool', type: 'bool' }], + stateMutability: 'view', + }, + { + type: 'function', + inputs: [ + { name: 'expectedRoot', internalType: 'bytes32', type: 'bytes32' }, + { name: 'startIndex', internalType: 'uint256', type: 'uint256' }, + { name: 'count', internalType: 'uint256', type: 'uint256' }, + { name: 'l1Timestamp', internalType: 'uint256', type: 'uint256' }, + { name: 'l1BlockNumber', internalType: 'uint256', type: 'uint256' }, + ], + name: 'notifyAnchored', + outputs: [], + stateMutability: 'nonpayable', + }, + { + type: 'function', + inputs: [{ name: 'oracle', internalType: 'address', type: 'address' }], + name: 'setFeeOracle', + outputs: [], + stateMutability: 'nonpayable', + }, + { + type: 'function', + inputs: [{ name: 'l1Gateway', internalType: 'address', type: 'address' }], + name: 'setL1Gateway', + outputs: [], + stateMutability: 'nonpayable', + }, + { + type: 'function', + inputs: [{ name: 'l2Messenger', internalType: 'address', type: 'address' }], + name: 'setL2Messenger', + outputs: [], + stateMutability: 'nonpayable', + }, + { + type: 'function', + inputs: [ + { name: 'attestationId', internalType: 'bytes32', type: 'bytes32' }, + { name: 'refundAddress', internalType: 'address', type: 'address' }, + ], + name: 'submitForL1Anchoring', + outputs: [], + stateMutability: 'payable', + }, + { + type: 'function', + inputs: [ + { name: 'attestationId', internalType: 'bytes32', type: 'bytes32' }, + ], + name: 'submitForL1Anchoring', + outputs: [], + stateMutability: 'payable', + }, + { + type: 'function', + inputs: [ + { name: 'to', internalType: 'address', type: 'address' }, + { name: 'amount', internalType: 'uint256', type: 'uint256' }, + ], + name: 'withdrawFees', + outputs: [], + stateMutability: 'nonpayable', + }, + { + type: 'event', + anonymous: false, + inputs: [ + { + name: 'oldOracle', + internalType: 'address', + type: 'address', + indexed: true, + }, + { + name: 'newOracle', + internalType: 'address', + type: 'address', + indexed: true, + }, + ], + name: 'FeeOracleUpdated', + }, + { + type: 'event', + anonymous: false, + inputs: [ + { name: 'to', internalType: 'address', type: 'address', indexed: true }, + { + name: 'amount', + internalType: 'uint256', + type: 'uint256', + indexed: false, + }, + ], + name: 'FeesWithdrawn', + }, + { + type: 'event', + anonymous: false, + inputs: [ + { + name: 'attestationId', + internalType: 'bytes32', + type: 'bytes32', + indexed: true, + }, + { name: 'root', internalType: 'bytes32', type: 'bytes32', indexed: true }, + { + name: 'queueIndex', + internalType: 'uint256', + type: 'uint256', + indexed: false, + }, + { name: 'fee', internalType: 'uint256', type: 'uint256', indexed: false }, + { + name: 'blockNumber', + internalType: 'uint256', + type: 'uint256', + indexed: false, + }, + { + name: 'timestamp', + internalType: 'uint256', + type: 'uint256', + indexed: false, + }, + ], + name: 'L1AnchoringQueued', + }, + { + type: 'event', + anonymous: false, + inputs: [ + { + name: 'claimedRoot', + internalType: 'bytes32', + type: 'bytes32', + indexed: true, + }, + { + name: 'startIndex', + internalType: 'uint256', + type: 'uint256', + indexed: true, + }, + { + name: 'count', + internalType: 'uint256', + type: 'uint256', + indexed: false, + }, + { + name: 'l1BlockAttested', + internalType: 'uint256', + type: 'uint256', + indexed: false, + }, + { + name: 'l1TimestampAttested', + internalType: 'uint256', + type: 'uint256', + indexed: false, + }, + { + name: 'l2BlockNumber', + internalType: 'uint256', + type: 'uint256', + indexed: false, + }, + { + name: 'l2TimestampReceived', + internalType: 'uint256', + type: 'uint256', + indexed: false, + }, + ], + name: 'L1BatchArrived', + }, + { + type: 'event', + anonymous: false, + inputs: [ + { + name: 'merkleRoot', + internalType: 'bytes32', + type: 'bytes32', + indexed: true, + }, + { + name: 'startIndex', + internalType: 'uint256', + type: 'uint256', + indexed: true, + }, + { + name: 'count', + internalType: 'uint256', + type: 'uint256', + indexed: false, + }, + { + name: 'l1BlockAttested', + internalType: 'uint256', + type: 'uint256', + indexed: false, + }, + { + name: 'l1TimestampAttested', + internalType: 'uint256', + type: 'uint256', + indexed: false, + }, + { + name: 'l2BlockNumber', + internalType: 'uint256', + type: 'uint256', + indexed: false, + }, + { + name: 'l2TimestampFinalized', + internalType: 'uint256', + type: 'uint256', + indexed: false, + }, + ], + name: 'L1BatchFinalized', + }, + { + type: 'event', + anonymous: false, + inputs: [ + { + name: 'oldGateway', + internalType: 'address', + type: 'address', + indexed: true, + }, + { + name: 'newGateway', + internalType: 'address', + type: 'address', + indexed: true, + }, + ], + name: 'L1GatewayUpdated', + }, + { + type: 'event', + anonymous: false, + inputs: [ + { + name: 'oldMessenger', + internalType: 'address', + type: 'address', + indexed: true, + }, + { + name: 'newMessenger', + internalType: 'address', + type: 'address', + indexed: true, + }, + ], + name: 'L2MessengerUpdated', + }, + { + type: 'event', + anonymous: false, + inputs: [ + { + name: 'submitter', + internalType: 'address', + type: 'address', + indexed: true, + }, + { + name: 'tokenId', + internalType: 'uint256', + type: 'uint256', + indexed: true, + }, + { name: 'root', internalType: 'bytes32', type: 'bytes32', indexed: true }, + { + name: 'timestamp', + internalType: 'uint256', + type: 'uint256', + indexed: false, + }, + ], + name: 'NFTClaimed', + }, + { + type: 'event', + anonymous: false, + inputs: [ + { + name: 'oldUts', + internalType: 'address', + type: 'address', + indexed: true, + }, + { + name: 'newUts', + internalType: 'address', + type: 'address', + indexed: true, + }, + ], + name: 'UtsUpdated', + }, +] as const diff --git a/packages/contract-types/src/index.ts b/packages/contract-types/src/index.ts new file mode 100644 index 0000000..43ec256 --- /dev/null +++ b/packages/contract-types/src/index.ts @@ -0,0 +1,44 @@ +// MIT License +// +// Copyright (c) 2025 UTS Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// +// Apache License, Version 2.0 +// +// Copyright (c) 2025 UTS Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +export { + iFeeOracleAbi, + ieasAbi, + il1AnchoringGatewayAbi, + il2AnchoringManagerAbi, +} from './generated.ts' diff --git a/packages/sdk/tsconfig.json b/packages/contract-types/tsconfig.json similarity index 100% rename from packages/sdk/tsconfig.json rename to packages/contract-types/tsconfig.json diff --git a/packages/contract-types/wagmi.config.ts b/packages/contract-types/wagmi.config.ts new file mode 100644 index 0000000..fd5e5bc --- /dev/null +++ b/packages/contract-types/wagmi.config.ts @@ -0,0 +1,19 @@ +import { defineConfig } from '@wagmi/cli' +import { foundry } from '@wagmi/cli/plugins' +import type { Config } from '@wagmi/cli' + +export default defineConfig({ + out: 'src/generated.ts', + contracts: [], + plugins: [ + foundry({ + project: '../..', + include: [ + 'IL1AnchoringGateway.sol/*.json', + 'IFeeOracle.sol/*.json', + 'IL2AnchoringManager.sol/*.json', + 'IEAS.sol/*.json', + ], + }), + ], +}) as Config diff --git a/packages/sdk-go/LICENSE-APACHE b/packages/sdk-go/LICENSE-APACHE new file mode 120000 index 0000000..1cd601d --- /dev/null +++ b/packages/sdk-go/LICENSE-APACHE @@ -0,0 +1 @@ +../../LICENSE-APACHE \ No newline at end of file diff --git a/packages/sdk-go/LICENSE-MIT b/packages/sdk-go/LICENSE-MIT new file mode 120000 index 0000000..b2cfbdc --- /dev/null +++ b/packages/sdk-go/LICENSE-MIT @@ -0,0 +1 @@ +../../LICENSE-MIT \ No newline at end of file diff --git a/packages/sdk-go/README.md b/packages/sdk-go/README.md new file mode 100644 index 0000000..84ab2d8 --- /dev/null +++ b/packages/sdk-go/README.md @@ -0,0 +1,159 @@ +# UTS Go SDK + +A Go SDK for the [Universal Timestamps (UTS)](https://book.timestamps.now/) protocol. + +UTS is a superset of OpenTimestamps that batches user-submitted digests into Merkle trees and anchors the roots on-chain via EAS attestations, providing trustless, verifiable timestamps without relying on a single trusted calendar server. + +## Installation + +```bash +go get github.com/lightsing/uts/packages/sdk-go +``` + +Requires Go 1.24 or later. + +## Quick Start + +```go +package main + +import ( + "context" + "crypto/sha256" + "fmt" + "log" + + "github.com/lightsing/uts/packages/sdk-go" + "github.com/lightsing/uts/packages/sdk-go/types" +) + +func main() { + ctx := context.Background() + + sdk := uts.NewSDK( + uts.WithCalendars("https://lgm1.calendar.test.timestamps.now/"), + ) + + data := []byte("Hello, UTS!") + hash := sha256.Sum256(data) + + header, err := types.NewDigestHeader(types.DigestSHA256, hash[:]) + if err != nil { + log.Fatal(err) + } + stamps, err := sdk.Stamp(ctx, []*types.DigestHeader{header}) + if err != nil { + log.Fatal(err) + } + + result, err := sdk.Verify(ctx, stamps[0]) + if err != nil { + log.Fatal(err) + } + + fmt.Printf("Status: %s\n", result.Status) + for _, att := range result.Attestations { + fmt.Printf(" %s\n", att) + } +} +``` + +## Features + +- **Timestamp Creation**: Submit digests to calendar servers and receive pending timestamps +- **Timestamp Verification**: Verify attestation proofs against Bitcoin block headers and EAS attestations +- **Pending Attestation Upgrade**: Convert pending attestations to confirmed ones +- **Binary Codec**: Encode and decode timestamps in the OpenTimestamps binary format +- **Binary Merkle Tree**: Efficient Merkle tree operations for batching digests + +## API Overview + +### SDK Constructor + +```go +sdk := uts.NewSDK( + uts.WithCalendars("https://calendar1.example.com/", "https://calendar2.example.com/"), + uts.WithTimeout(30 * time.Second), + uts.WithQuorum(2), + uts.WithNonceSize(32), + uts.WithHashAlgorithm(uts.HashKeccak256), + uts.WithBitcoinRPC(btcClient), + uts.WithEthereumRPC(chainID, rpcURL), +) +``` + +### Core Methods + +| Method | Description | +| ---------------------------------- | ---------------------------------- | +| `Stamp(ctx, headers)` | Submit digests to calendar servers | +| `Verify(ctx, stamp)` | Verify a detached timestamp | +| `Upgrade(ctx, stamp, keepPending)` | Upgrade pending attestations | + +### Types + +| Type | Description | +| -------------------- | ------------------------------------------ | +| `DigestHeader` | Digest algorithm and hash value | +| `DetachedTimestamp` | Header and timestamp proof | +| `Timestamp` | Sequence of operations/steps | +| `VerificationResult` | Verification status and attestations | +| `AttestationStatus` | Individual attestation verification result | + +### Attestation Types + +| Type | Tag | Description | +| -------------------- | -------------------- | ----------------------------- | +| `BitcoinAttestation` | `0x0588960d73d71901` | Bitcoin block height | +| `PendingAttestation` | `0x83dfe30d2ef90c8e` | Pending calendar URI | +| `EASAttestation` | `0x8bf46bf4cfd674fa` | EAS attestation UID and chain | +| `EASTimestamped` | `0x5aafceeb1c7ad58e` | EAS timestamp attestation | + +### Binary Codec + +```go +import "github.com/lightsing/uts/packages/sdk-go/codec" + +encoded, err := codec.EncodeDetachedTimestamp(stamp) + +stamp, err := codec.DecodeDetachedTimestamp(encoded) +``` + +### Hash Operations + +```go +import "github.com/lightsing/uts/packages/sdk-go/crypto" + +hash := crypto.SHA256(data) +hash := crypto.Keccak256(data) +``` + +### Merkle Tree + +```go +import "github.com/lightsing/uts/packages/sdk-go/crypto" + +tree := crypto.NewMerkleTree(leaves) +root := tree.Root() +proof, err := tree.GetProof(leaf) +``` + +## Compatibility + +The binary format is compatible with the TypeScript and Python SDKs. The Rust implementation in `crates/core` is the ground truth for: + +- Binary encoding/decoding format +- Attestation type definitions +- Error codes and handling +- Merkle tree algorithm details + +When implementing features or debugging issues, always cross-reference with the Rust code. + +## License + +Licensed under either of + +- Apache License, Version 2.0 ([LICENSE-APACHE](https://www.apache.org/licenses/LICENSE-2.0)) +- MIT license ([LICENSE-MIT](https://opensource.org/licenses/MIT)) + +at your option. diff --git a/packages/sdk-go/attestation/bitcoin.go b/packages/sdk-go/attestation/bitcoin.go new file mode 100644 index 0000000..162e046 --- /dev/null +++ b/packages/sdk-go/attestation/bitcoin.go @@ -0,0 +1,77 @@ +package attestation + +import ( + "context" + "encoding/hex" + "fmt" + + "github.com/lightsing/uts/packages/sdk-go/logging" + "github.com/lightsing/uts/packages/sdk-go/rpc" + "github.com/lightsing/uts/packages/sdk-go/types" +) + +type BitcoinRPCClient interface { + GetBlockHash(height int64) (string, error) + GetBlockHeader(hash string) (*rpc.BlockHeader, error) +} + +func VerifyBitcoin(ctx context.Context, client BitcoinRPCClient, digest []byte, att *types.BitcoinAttestation) *types.AttestationStatus { + logger := logging.Default() + logger.Debug(ctx, "VerifyBitcoin: verifying", "height", att.Height, "digest", hex.EncodeToString(digest)) + + blockHash, err := client.GetBlockHash(int64(att.Height)) + if err != nil { + logger.Warn(ctx, "VerifyBitcoin: failed to get block hash", "height", att.Height, "error", err) + return &types.AttestationStatus{ + Attestation: att, + Status: types.StatusUnknown, + Error: fmt.Errorf("failed to get block hash at height %d: %w", att.Height, err), + } + } + + header, err := client.GetBlockHeader(blockHash) + if err != nil { + logger.Warn(ctx, "VerifyBitcoin: failed to get block header", "hash", blockHash, "error", err) + return &types.AttestationStatus{ + Attestation: att, + Status: types.StatusUnknown, + Error: fmt.Errorf("failed to get block header for hash %s: %w", blockHash, err), + } + } + + merkleRootBytes, err := hex.DecodeString(header.MerkleRoot) + if err != nil { + return &types.AttestationStatus{ + Attestation: att, + Status: types.StatusUnknown, + Error: fmt.Errorf("failed to decode merkle root %s: %w", header.MerkleRoot, err), + } + } + + reversedMerkleRoot := rpc.ReverseBytes(merkleRootBytes) + + if len(reversedMerkleRoot) != len(digest) { + return &types.AttestationStatus{ + Attestation: att, + Status: types.StatusInvalid, + Error: fmt.Errorf("merkle root length mismatch: expected %d, got %d", len(digest), len(reversedMerkleRoot)), + } + } + + for i := range digest { + if digest[i] != reversedMerkleRoot[i] { + logger.Debug(ctx, "VerifyBitcoin: invalid - merkle root mismatch", "height", att.Height) + return &types.AttestationStatus{ + Attestation: att, + Status: types.StatusInvalid, + Error: fmt.Errorf("bitcoin attestation does not match the expected merkle root at height %d", att.Height), + } + } + } + + logger.Debug(ctx, "VerifyBitcoin: valid", "height", att.Height) + return &types.AttestationStatus{ + Attestation: att, + Status: types.StatusValid, + } +} diff --git a/packages/sdk-go/attestation/bitcoin_test.go b/packages/sdk-go/attestation/bitcoin_test.go new file mode 100644 index 0000000..13cebbf --- /dev/null +++ b/packages/sdk-go/attestation/bitcoin_test.go @@ -0,0 +1,126 @@ +package attestation + +import ( + "context" + "crypto/sha256" + "encoding/hex" + "errors" + "testing" + + "github.com/lightsing/uts/packages/sdk-go/rpc" + "github.com/lightsing/uts/packages/sdk-go/types" +) + +type mockBitcoinRPC struct { + blockHash string + header *rpc.BlockHeader + hashErr error + headerErr error +} + +func (m *mockBitcoinRPC) GetBlockHash(height int64) (string, error) { + if m.hashErr != nil { + return "", m.hashErr + } + return m.blockHash, nil +} + +func (m *mockBitcoinRPC) GetBlockHeader(hash string) (*rpc.BlockHeader, error) { + if m.headerErr != nil { + return nil, m.headerErr + } + return m.header, nil +} + +func TestVerifyBitcoin_Success(t *testing.T) { + digest := sha256.Sum256([]byte("test data")) + merkleRoot := rpc.ReverseBytes(digest[:]) + merkleRootHex := hex.EncodeToString(merkleRoot) + + mockRPC := &mockBitcoinRPC{ + blockHash: "000000000000000000002a5c2f3f8c8e7d9a6b5e4f3d2c1b0a9f8e7d6c5b4a3a2", + header: &rpc.BlockHeader{ + Hash: "000000000000000000002a5c2f3f8c8e7d9a6b5e4f3d2c1b0a9f8e7d6c5b4a3a2", + MerkleRoot: merkleRootHex, + Height: 800000, + Time: 1234567890, + }, + } + + att := &types.BitcoinAttestation{Height: 800000} + status := VerifyBitcoin(context.Background(), mockRPC, digest[:], att) + + if status.Status != types.StatusValid { + t.Errorf("expected status %v, got %v", types.StatusValid, status.Status) + } + if status.Error != nil { + t.Errorf("expected no error, got %v", status.Error) + } + if status.Attestation == nil { + t.Error("expected attestation to be set") + } +} + +func TestVerifyBitcoin_MismatchedMerkleRoot(t *testing.T) { + digest := sha256.Sum256([]byte("test data")) + wrongDigest := sha256.Sum256([]byte("wrong data")) + merkleRoot := rpc.ReverseBytes(wrongDigest[:]) + merkleRootHex := hex.EncodeToString(merkleRoot) + + mockRPC := &mockBitcoinRPC{ + blockHash: "000000000000000000002a5c2f3f8c8e7d9a6b5e4f3d2c1b0a9f8e7d6c5b4a3a2", + header: &rpc.BlockHeader{ + Hash: "000000000000000000002a5c2f3f8c8e7d9a6b5e4f3d2c1b0a9f8e7d6c5b4a3a2", + MerkleRoot: merkleRootHex, + Height: 800000, + Time: 1234567890, + }, + } + + att := &types.BitcoinAttestation{Height: 800000} + status := VerifyBitcoin(context.Background(), mockRPC, digest[:], att) + + if status.Status != types.StatusInvalid { + t.Errorf("expected status %v, got %v", types.StatusInvalid, status.Status) + } + if status.Error == nil { + t.Error("expected error for mismatched merkle root") + } +} + +func TestVerifyBitcoin_GetBlockHashError(t *testing.T) { + digest := sha256.Sum256([]byte("test data")) + + mockRPC := &mockBitcoinRPC{ + hashErr: errors.New("block not found"), + } + + att := &types.BitcoinAttestation{Height: 800000} + status := VerifyBitcoin(context.Background(), mockRPC, digest[:], att) + + if status.Status != types.StatusUnknown { + t.Errorf("expected status %v, got %v", types.StatusUnknown, status.Status) + } + if status.Error == nil { + t.Error("expected error for RPC failure") + } +} + +func TestVerifyBitcoin_GetBlockHeaderError(t *testing.T) { + digest := sha256.Sum256([]byte("test data")) + + mockRPC := &mockBitcoinRPC{ + blockHash: "000000000000000000002a5c2f3f8c8e7d9a6b5e4f3d2c1b0a9f8e7d6c5b4a3a2", + headerErr: errors.New("header not found"), + } + + att := &types.BitcoinAttestation{Height: 800000} + status := VerifyBitcoin(context.Background(), mockRPC, digest[:], att) + + if status.Status != types.StatusUnknown { + t.Errorf("expected status %v, got %v", types.StatusUnknown, status.Status) + } + if status.Error == nil { + t.Error("expected error for RPC failure") + } +} diff --git a/packages/sdk-go/attestation/doc.go b/packages/sdk-go/attestation/doc.go new file mode 100644 index 0000000..299e6be --- /dev/null +++ b/packages/sdk-go/attestation/doc.go @@ -0,0 +1,15 @@ +// Package attestation provides verification functions for timestamp attestations. +// +// This package implements verification logic for various attestation types +// including Bitcoin block headers and Ethereum attestation service attestations. +// +// Bitcoin Attestation Verification: +// +// Bitcoin attestations are verified by: +// 1. Fetching the block hash at the specified height via RPC +// 2. Fetching the block header using the hash +// 3. Comparing the merkle root (reversed) with the digest +// +// The merkle root byte reversal is necessary because Bitcoin displays +// hashes in little-endian format. +package attestation diff --git a/packages/sdk-go/attestation/eas.go b/packages/sdk-go/attestation/eas.go new file mode 100644 index 0000000..65f71f8 --- /dev/null +++ b/packages/sdk-go/attestation/eas.go @@ -0,0 +1,130 @@ +package attestation + +import ( + "context" + "encoding/hex" + "fmt" + + "github.com/lightsing/uts/packages/sdk-go/logging" + "github.com/lightsing/uts/packages/sdk-go/rpc" + "github.com/lightsing/uts/packages/sdk-go/types" +) + +type EASClient interface { + GetEASAttestation(ctx context.Context, chainID uint64, uid [32]byte) (*rpc.Attestation, error) + GetTimestamp(ctx context.Context, chainID uint64, data [32]byte) (uint64, error) +} + +func VerifyEASAttestation(ctx context.Context, client EASClient, digest []byte, att *types.EASAttestation) *types.AttestationStatus { + logger := logging.Default() + logger.Debug(ctx, "VerifyEASAttestation: verifying", "chain_id", att.ChainID, "uid", hex.EncodeToString(att.UID[:])) + + if len(digest) != 32 { + return &types.AttestationStatus{ + Attestation: att, + Status: types.StatusInvalid, + Error: fmt.Errorf("invalid digest length: expected 32, got %d", len(digest)), + } + } + + var digestHash [32]byte + copy(digestHash[:], digest) + + easAtt, err := client.GetEASAttestation(ctx, att.ChainID, att.UID) + if err != nil { + logger.Warn(ctx, "VerifyEASAttestation: failed to get attestation", "chain_id", att.ChainID, "error", err) + return &types.AttestationStatus{ + Attestation: att, + Status: types.StatusUnknown, + Error: fmt.Errorf("failed to get attestation: %w", err), + } + } + + if easAtt.Schema != rpc.SchemaID { + logger.Debug(ctx, "VerifyEASAttestation: invalid schema", "expected", hex.EncodeToString(rpc.SchemaID[:]), "got", hex.EncodeToString(easAtt.Schema[:])) + return &types.AttestationStatus{ + Attestation: att, + Status: types.StatusInvalid, + Error: fmt.Errorf("invalid schema: expected %x, got %x", rpc.SchemaID, easAtt.Schema), + } + } + + if easAtt.Revocable { + logger.Debug(ctx, "VerifyEASAttestation: invalid - attestation is revocable") + return &types.AttestationStatus{ + Attestation: att, + Status: types.StatusInvalid, + Error: fmt.Errorf("attestation cannot be revocable"), + } + } + + if len(easAtt.Data) != 32 { + return &types.AttestationStatus{ + Attestation: att, + Status: types.StatusInvalid, + Error: fmt.Errorf("invalid attestation data length: expected 32, got %d", len(easAtt.Data)), + } + } + + var attestedHash [32]byte + copy(attestedHash[:], easAtt.Data) + + if attestedHash != digestHash { + logger.Debug(ctx, "VerifyEASAttestation: invalid - hash mismatch") + return &types.AttestationStatus{ + Attestation: att, + Status: types.StatusInvalid, + Error: fmt.Errorf("attested hash mismatch: expected %x, got %x", digestHash, attestedHash), + } + } + + logger.Debug(ctx, "VerifyEASAttestation: valid", "chain_id", att.ChainID, "time", easAtt.Time) + return &types.AttestationStatus{ + Attestation: att, + Status: types.StatusValid, + } +} + +func VerifyEASTimestamped(ctx context.Context, client EASClient, digest []byte, att *types.EASTimestamped) *types.AttestationStatus { + logger := logging.Default() + logger.Debug(ctx, "VerifyEASTimestamped: verifying", "chain_id", att.ChainID, "digest", hex.EncodeToString(digest)) + + if len(digest) != 32 { + return &types.AttestationStatus{ + Attestation: att, + Status: types.StatusInvalid, + Error: fmt.Errorf("invalid digest length: expected 32, got %d", len(digest)), + } + } + + var digestHash [32]byte + copy(digestHash[:], digest) + + timestamp, err := client.GetTimestamp(ctx, att.ChainID, digestHash) + if err != nil { + logger.Warn(ctx, "VerifyEASTimestamped: failed to get timestamp", "chain_id", att.ChainID, "error", err) + return &types.AttestationStatus{ + Attestation: att, + Status: types.StatusUnknown, + Error: fmt.Errorf("failed to get timestamp: %w", err), + } + } + + if timestamp == 0 { + logger.Debug(ctx, "VerifyEASTimestamped: invalid - timestamp not found") + return &types.AttestationStatus{ + Attestation: att, + Status: types.StatusInvalid, + Error: fmt.Errorf("timestamp not found for digest %x", digestHash), + } + } + + logger.Debug(ctx, "VerifyEASTimestamped: valid", "chain_id", att.ChainID, "timestamp", timestamp) + return &types.AttestationStatus{ + Attestation: att, + Status: types.StatusValid, + Info: map[string]interface{}{ + "timestamp": timestamp, + }, + } +} diff --git a/packages/sdk-go/attestation/eas_test.go b/packages/sdk-go/attestation/eas_test.go new file mode 100644 index 0000000..8ffb110 --- /dev/null +++ b/packages/sdk-go/attestation/eas_test.go @@ -0,0 +1,305 @@ +package attestation + +import ( + "context" + "errors" + "testing" + + "github.com/lightsing/uts/packages/sdk-go/rpc" + "github.com/lightsing/uts/packages/sdk-go/types" +) + +type mockEASClient struct { + attestation *rpc.Attestation + timestamp uint64 + attErr error + tsErr error +} + +func (m *mockEASClient) GetEASAttestation(ctx context.Context, chainID uint64, uid [32]byte) (*rpc.Attestation, error) { + if m.attErr != nil { + return nil, m.attErr + } + return m.attestation, nil +} + +func (m *mockEASClient) GetTimestamp(ctx context.Context, chainID uint64, data [32]byte) (uint64, error) { + if m.tsErr != nil { + return 0, m.tsErr + } + return m.timestamp, nil +} + +func TestVerifyEASAttestation_Valid(t *testing.T) { + digest := [32]byte{0x01, 0x02, 0x03, 0x04} + attUID := [32]byte{0xaa, 0xbb, 0xcc, 0xdd} + + mockClient := &mockEASClient{ + attestation: &rpc.Attestation{ + UID: attUID, + Schema: rpc.SchemaID, + Revocable: false, + Data: digest[:], + }, + } + + att := &types.EASAttestation{ + ChainID: 534352, + UID: attUID, + } + + status := VerifyEASAttestation(context.Background(), mockClient, digest[:], att) + + if status.Status != types.StatusValid { + t.Errorf("expected status %v, got %v", types.StatusValid, status.Status) + } + if status.Error != nil { + t.Errorf("expected no error, got %v", status.Error) + } +} + +func TestVerifyEASAttestation_InvalidSchema(t *testing.T) { + digest := [32]byte{0x01, 0x02, 0x03, 0x04} + attUID := [32]byte{0xaa, 0xbb, 0xcc, 0xdd} + wrongSchema := [32]byte{0xff, 0xff, 0xff, 0xff} + + mockClient := &mockEASClient{ + attestation: &rpc.Attestation{ + UID: attUID, + Schema: wrongSchema, + Revocable: false, + Data: digest[:], + }, + } + + att := &types.EASAttestation{ + ChainID: 534352, + UID: attUID, + } + + status := VerifyEASAttestation(context.Background(), mockClient, digest[:], att) + + if status.Status != types.StatusInvalid { + t.Errorf("expected status %v, got %v", types.StatusInvalid, status.Status) + } + if status.Error == nil { + t.Error("expected error for invalid schema") + } +} + +func TestVerifyEASAttestation_Revocable(t *testing.T) { + digest := [32]byte{0x01, 0x02, 0x03, 0x04} + attUID := [32]byte{0xaa, 0xbb, 0xcc, 0xdd} + + mockClient := &mockEASClient{ + attestation: &rpc.Attestation{ + UID: attUID, + Schema: rpc.SchemaID, + Revocable: true, + Data: digest[:], + }, + } + + att := &types.EASAttestation{ + ChainID: 534352, + UID: attUID, + } + + status := VerifyEASAttestation(context.Background(), mockClient, digest[:], att) + + if status.Status != types.StatusInvalid { + t.Errorf("expected status %v, got %v", types.StatusInvalid, status.Status) + } + if status.Error == nil { + t.Error("expected error for revocable attestation") + } +} + +func TestVerifyEASAttestation_MismatchedHash(t *testing.T) { + digest := [32]byte{0x01, 0x02, 0x03, 0x04} + wrongDigest := [32]byte{0xff, 0xfe, 0xfd, 0xfc} + attUID := [32]byte{0xaa, 0xbb, 0xcc, 0xdd} + + mockClient := &mockEASClient{ + attestation: &rpc.Attestation{ + UID: attUID, + Schema: rpc.SchemaID, + Revocable: false, + Data: wrongDigest[:], + }, + } + + att := &types.EASAttestation{ + ChainID: 534352, + UID: attUID, + } + + status := VerifyEASAttestation(context.Background(), mockClient, digest[:], att) + + if status.Status != types.StatusInvalid { + t.Errorf("expected status %v, got %v", types.StatusInvalid, status.Status) + } + if status.Error == nil { + t.Error("expected error for mismatched hash") + } +} + +func TestVerifyEASAttestation_RPCError(t *testing.T) { + digest := [32]byte{0x01, 0x02, 0x03, 0x04} + attUID := [32]byte{0xaa, 0xbb, 0xcc, 0xdd} + + mockClient := &mockEASClient{ + attErr: errors.New("RPC error"), + } + + att := &types.EASAttestation{ + ChainID: 534352, + UID: attUID, + } + + status := VerifyEASAttestation(context.Background(), mockClient, digest[:], att) + + if status.Status != types.StatusUnknown { + t.Errorf("expected status %v, got %v", types.StatusUnknown, status.Status) + } + if status.Error == nil { + t.Error("expected error for RPC failure") + } +} + +func TestVerifyEASTimestamped_Valid(t *testing.T) { + digest := [32]byte{0x01, 0x02, 0x03, 0x04} + + mockClient := &mockEASClient{ + timestamp: 1234567890, + } + + att := &types.EASTimestamped{ + ChainID: 534352, + } + + status := VerifyEASTimestamped(context.Background(), mockClient, digest[:], att) + + if status.Status != types.StatusValid { + t.Errorf("expected status %v, got %v", types.StatusValid, status.Status) + } + if status.Error != nil { + t.Errorf("expected no error, got %v", status.Error) + } + if status.Info == nil { + t.Error("expected info to be set") + } + if ts, ok := status.Info["timestamp"].(uint64); !ok || ts != 1234567890 { + t.Errorf("expected timestamp 1234567890, got %v", status.Info["timestamp"]) + } +} + +func TestVerifyEASTimestamped_NotFound(t *testing.T) { + digest := [32]byte{0x01, 0x02, 0x03, 0x04} + + mockClient := &mockEASClient{ + timestamp: 0, + } + + att := &types.EASTimestamped{ + ChainID: 534352, + } + + status := VerifyEASTimestamped(context.Background(), mockClient, digest[:], att) + + if status.Status != types.StatusInvalid { + t.Errorf("expected status %v, got %v", types.StatusInvalid, status.Status) + } + if status.Error == nil { + t.Error("expected error for timestamp not found") + } +} + +func TestVerifyEASTimestamped_RPCError(t *testing.T) { + digest := [32]byte{0x01, 0x02, 0x03, 0x04} + + mockClient := &mockEASClient{ + tsErr: errors.New("RPC error"), + } + + att := &types.EASTimestamped{ + ChainID: 534352, + } + + status := VerifyEASTimestamped(context.Background(), mockClient, digest[:], att) + + if status.Status != types.StatusUnknown { + t.Errorf("expected status %v, got %v", types.StatusUnknown, status.Status) + } + if status.Error == nil { + t.Error("expected error for RPC failure") + } +} + +func TestVerifyEASAttestation_InvalidDigestLength(t *testing.T) { + digest := []byte{0x01, 0x02, 0x03} + attUID := [32]byte{0xaa, 0xbb, 0xcc, 0xdd} + + mockClient := &mockEASClient{} + + att := &types.EASAttestation{ + ChainID: 534352, + UID: attUID, + } + + status := VerifyEASAttestation(context.Background(), mockClient, digest, att) + + if status.Status != types.StatusInvalid { + t.Errorf("expected status %v, got %v", types.StatusInvalid, status.Status) + } + if status.Error == nil { + t.Error("expected error for invalid digest length") + } +} + +func TestVerifyEASTimestamped_InvalidDigestLength(t *testing.T) { + digest := []byte{0x01, 0x02, 0x03} + + mockClient := &mockEASClient{} + + att := &types.EASTimestamped{ + ChainID: 534352, + } + + status := VerifyEASTimestamped(context.Background(), mockClient, digest, att) + + if status.Status != types.StatusInvalid { + t.Errorf("expected status %v, got %v", types.StatusInvalid, status.Status) + } + if status.Error == nil { + t.Error("expected error for invalid digest length") + } +} + +func TestVerifyEASAttestation_InvalidDataLength(t *testing.T) { + digest := [32]byte{0x01, 0x02, 0x03, 0x04} + attUID := [32]byte{0xaa, 0xbb, 0xcc, 0xdd} + + mockClient := &mockEASClient{ + attestation: &rpc.Attestation{ + UID: attUID, + Schema: rpc.SchemaID, + Revocable: false, + Data: []byte{0x01, 0x02}, + }, + } + + att := &types.EASAttestation{ + ChainID: 534352, + UID: attUID, + } + + status := VerifyEASAttestation(context.Background(), mockClient, digest[:], att) + + if status.Status != types.StatusInvalid { + t.Errorf("expected status %v, got %v", types.StatusInvalid, status.Status) + } + if status.Error == nil { + t.Error("expected error for invalid data length") + } +} diff --git a/packages/sdk-go/attestation/verify.go b/packages/sdk-go/attestation/verify.go new file mode 100644 index 0000000..944468c --- /dev/null +++ b/packages/sdk-go/attestation/verify.go @@ -0,0 +1,28 @@ +package attestation + +import ( + "context" + + "github.com/lightsing/uts/packages/sdk-go/types" +) + +func Verify(ctx context.Context, btcRPC BitcoinRPCClient, easClient EASClient, digest []byte, att types.Attestation) *types.AttestationStatus { + switch a := att.(type) { + case *types.PendingAttestation: + return &types.AttestationStatus{ + Attestation: att, + Status: types.StatusPending, + } + case *types.BitcoinAttestation: + return VerifyBitcoin(ctx, btcRPC, digest, a) + case *types.EASAttestation: + return VerifyEASAttestation(ctx, easClient, digest, a) + case *types.EASTimestamped: + return VerifyEASTimestamped(ctx, easClient, digest, a) + default: + return &types.AttestationStatus{ + Attestation: att, + Status: types.StatusUnknown, + } + } +} diff --git a/packages/sdk-go/attestation/verify_test.go b/packages/sdk-go/attestation/verify_test.go new file mode 100644 index 0000000..40f7297 --- /dev/null +++ b/packages/sdk-go/attestation/verify_test.go @@ -0,0 +1,194 @@ +package attestation + +import ( + "context" + "crypto/sha256" + "encoding/hex" + "errors" + "testing" + + "github.com/lightsing/uts/packages/sdk-go/rpc" + "github.com/lightsing/uts/packages/sdk-go/types" +) + +type mockVerifyClients struct { + btcRPC *mockBitcoinRPC + eas *mockEASClient +} + +func newMockVerifyClients() *mockVerifyClients { + return &mockVerifyClients{ + btcRPC: &mockBitcoinRPC{}, + eas: &mockEASClient{}, + } +} + +func (m *mockVerifyClients) GetBlockHash(height int64) (string, error) { + return m.btcRPC.GetBlockHash(height) +} + +func (m *mockVerifyClients) GetBlockHeader(hash string) (*rpc.BlockHeader, error) { + return m.btcRPC.GetBlockHeader(hash) +} + +func (m *mockVerifyClients) GetEASAttestation(ctx context.Context, chainID uint64, uid [32]byte) (*rpc.Attestation, error) { + return m.eas.GetEASAttestation(ctx, chainID, uid) +} + +func (m *mockVerifyClients) GetTimestamp(ctx context.Context, chainID uint64, data [32]byte) (uint64, error) { + return m.eas.GetTimestamp(ctx, chainID, data) +} + +func TestVerify_PendingAttestation(t *testing.T) { + clients := newMockVerifyClients() + att := &types.PendingAttestation{URI: "https://example.com/calendar"} + + status := Verify(context.Background(), clients, clients, nil, att) + + if status.Status != types.StatusPending { + t.Errorf("expected status %v, got %v", types.StatusPending, status.Status) + } + if status.Attestation != att { + t.Error("expected attestation to be set") + } + if status.Error != nil { + t.Errorf("expected no error, got %v", status.Error) + } +} + +func TestVerify_BitcoinAttestation(t *testing.T) { + digest := sha256.Sum256([]byte("test data")) + merkleRoot := rpc.ReverseBytes(digest[:]) + merkleRootHex := hex.EncodeToString(merkleRoot) + + clients := newMockVerifyClients() + clients.btcRPC.blockHash = "000000000000000000002a5c2f3f8c8e7d9a6b5e4f3d2c1b0a9f8e7d6c5b4a3a2" + clients.btcRPC.header = &rpc.BlockHeader{ + Hash: "000000000000000000002a5c2f3f8c8e7d9a6b5e4f3d2c1b0a9f8e7d6c5b4a3a2", + MerkleRoot: merkleRootHex, + Height: 800000, + Time: 1234567890, + } + + att := &types.BitcoinAttestation{Height: 800000} + status := Verify(context.Background(), clients, clients, digest[:], att) + + if status.Status != types.StatusValid { + t.Errorf("expected status %v, got %v", types.StatusValid, status.Status) + } + if status.Error != nil { + t.Errorf("expected no error, got %v", status.Error) + } +} + +func TestVerify_BitcoinAttestation_Error(t *testing.T) { + digest := sha256.Sum256([]byte("test data")) + + clients := newMockVerifyClients() + clients.btcRPC.hashErr = errors.New("block not found") + + att := &types.BitcoinAttestation{Height: 800000} + status := Verify(context.Background(), clients, clients, digest[:], att) + + if status.Status != types.StatusUnknown { + t.Errorf("expected status %v, got %v", types.StatusUnknown, status.Status) + } + if status.Error == nil { + t.Error("expected error for RPC failure") + } +} + +func TestVerify_EASAttestation(t *testing.T) { + digest := [32]byte{0x01, 0x02, 0x03, 0x04} + attUID := [32]byte{0xaa, 0xbb, 0xcc, 0xdd} + + clients := newMockVerifyClients() + clients.eas.attestation = &rpc.Attestation{ + UID: attUID, + Schema: rpc.SchemaID, + Revocable: false, + Data: digest[:], + } + + att := &types.EASAttestation{ + ChainID: 534352, + UID: attUID, + } + status := Verify(context.Background(), clients, clients, digest[:], att) + + if status.Status != types.StatusValid { + t.Errorf("expected status %v, got %v", types.StatusValid, status.Status) + } + if status.Error != nil { + t.Errorf("expected no error, got %v", status.Error) + } +} + +func TestVerify_EASAttestation_Error(t *testing.T) { + digest := [32]byte{0x01, 0x02, 0x03, 0x04} + attUID := [32]byte{0xaa, 0xbb, 0xcc, 0xdd} + + clients := newMockVerifyClients() + clients.eas.attErr = errors.New("RPC error") + + att := &types.EASAttestation{ + ChainID: 534352, + UID: attUID, + } + status := Verify(context.Background(), clients, clients, digest[:], att) + + if status.Status != types.StatusUnknown { + t.Errorf("expected status %v, got %v", types.StatusUnknown, status.Status) + } + if status.Error == nil { + t.Error("expected error for RPC failure") + } +} + +func TestVerify_EASTimestamped(t *testing.T) { + digest := [32]byte{0x01, 0x02, 0x03, 0x04} + + clients := newMockVerifyClients() + clients.eas.timestamp = 1234567890 + + att := &types.EASTimestamped{ChainID: 534352} + status := Verify(context.Background(), clients, clients, digest[:], att) + + if status.Status != types.StatusValid { + t.Errorf("expected status %v, got %v", types.StatusValid, status.Status) + } + if status.Error != nil { + t.Errorf("expected no error, got %v", status.Error) + } +} + +func TestVerify_EASTimestamped_Error(t *testing.T) { + digest := [32]byte{0x01, 0x02, 0x03, 0x04} + + clients := newMockVerifyClients() + clients.eas.tsErr = errors.New("RPC error") + + att := &types.EASTimestamped{ChainID: 534352} + status := Verify(context.Background(), clients, clients, digest[:], att) + + if status.Status != types.StatusUnknown { + t.Errorf("expected status %v, got %v", types.StatusUnknown, status.Status) + } + if status.Error == nil { + t.Error("expected error for RPC failure") + } +} + +func TestVerify_UnknownAttestation(t *testing.T) { + clients := newMockVerifyClients() + att := types.NewUnknownAttestation([8]byte{0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99}, []byte{0x01, 0x02}) + + status := Verify(context.Background(), clients, clients, []byte{0x01}, att) + + if status.Status != types.StatusUnknown { + t.Errorf("expected status %v, got %v", types.StatusUnknown, status.Status) + } + if status.Attestation != att { + t.Error("expected attestation to be set") + } +} diff --git a/packages/sdk-go/codec/constants.go b/packages/sdk-go/codec/constants.go new file mode 100644 index 0000000..3f04b2f --- /dev/null +++ b/packages/sdk-go/codec/constants.go @@ -0,0 +1,108 @@ +package codec + +import "github.com/lightsing/uts/packages/sdk-go/types" + +const ( + OpSHA1 byte = 0x02 + OpRIPEMD160 byte = 0x03 + OpSHA256 byte = 0x08 + OpKECCAK256 byte = 0x67 + OpAPPEND byte = 0xf0 + OpPREPEND byte = 0xf1 + OpREVERSE byte = 0xf2 + OpHEXLIFY byte = 0xf3 + OpATTESTATION byte = 0x00 + OpFORK byte = 0xff +) + +var OpCodeMap = map[string]byte{ + "SHA1": OpSHA1, + "RIPEMD160": OpRIPEMD160, + "SHA256": OpSHA256, + "KECCAK256": OpKECCAK256, + "APPEND": OpAPPEND, + "PREPEND": OpPREPEND, + "REVERSE": OpREVERSE, + "HEXLIFY": OpHEXLIFY, + "ATTESTATION": OpATTESTATION, + "FORK": OpFORK, +} + +var OpCodeName = map[byte]string{ + OpSHA1: "SHA1", + OpRIPEMD160: "RIPEMD160", + OpSHA256: "SHA256", + OpKECCAK256: "KECCAK256", + OpAPPEND: "APPEND", + OpPREPEND: "PREPEND", + OpREVERSE: "REVERSE", + OpHEXLIFY: "HEXLIFY", + OpATTESTATION: "ATTESTATION", + OpFORK: "FORK", +} + +var DigestLengths = map[string]int{ + "SHA1": 20, + "RIPEMD160": 20, + "SHA256": 32, + "KECCAK256": 32, +} + +var MagicBytes = []byte{ + 0x00, 0x4f, 0x70, 0x65, 0x6e, 0x54, 0x69, 0x6d, + 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x73, 0x00, + 0x00, 0x50, 0x72, 0x6f, 0x6f, 0x66, 0x00, 0xbf, + 0x89, 0xe2, 0xe8, 0x84, 0xe8, 0x92, 0x94, +} + +var ( + BitcoinAttestationTag = types.BitcoinTag + PendingAttestationTag = types.PendingTag + EASAttestTag = types.EASAttestTag + EASTimestampTag = types.EASTimestampTag +) + +var SchemaID = [32]byte{ + 0x5c, 0x5b, 0x8b, 0x29, 0x5f, 0xf4, 0x3c, 0x8e, + 0x44, 0x2b, 0xe1, 0x1d, 0x56, 0x9e, 0x94, 0xa4, + 0xcd, 0x54, 0x76, 0xf5, 0xe2, 0x3d, 0xf0, 0xf7, + 0x1b, 0xdd, 0x40, 0x8d, 0xf6, 0xb9, 0x64, 0x9c, +} + +const ( + MaxURILen = 1000 + NoExpiration = uint64(0) + TagSize = 8 +) + +func GetOpCode(name string) (byte, bool) { + code, ok := OpCodeMap[name] + return code, ok +} + +func GetOpName(code byte) (string, bool) { + name, ok := OpCodeName[code] + return name, ok +} + +func GetDigestLength(name string) (int, bool) { + length, ok := DigestLengths[name] + return length, ok +} + +func IsValidOpCode(code byte) bool { + _, ok := OpCodeName[code] + return ok +} + +func IsDigestOp(code byte) bool { + return code == OpSHA1 || code == OpRIPEMD160 || code == OpSHA256 || code == OpKECCAK256 +} + +func IsControlOp(code byte) bool { + return code == OpATTESTATION || code == OpFORK +} + +func HasImmediate(code byte) bool { + return code == OpAPPEND || code == OpPREPEND +} diff --git a/packages/sdk-go/codec/decoder.go b/packages/sdk-go/codec/decoder.go new file mode 100644 index 0000000..baf22bd --- /dev/null +++ b/packages/sdk-go/codec/decoder.go @@ -0,0 +1,417 @@ +package codec + +import ( + "bytes" + "context" + + "github.com/lightsing/uts/packages/sdk-go/errors" + "github.com/lightsing/uts/packages/sdk-go/logging" + "github.com/lightsing/uts/packages/sdk-go/types" +) + +type Decoder struct { + data []byte + pos int +} + +func NewDecoder(data []byte) *Decoder { + return &Decoder{data: data, pos: 0} +} + +func (d *Decoder) Remaining() int { + return len(d.data) - d.pos +} + +func (d *Decoder) checkBounds(n int) error { + if d.pos+n > len(d.data) { + return errors.ErrUnexpectedEof() + } + return nil +} + +func (d *Decoder) CheckEOF() error { + if d.Remaining() > 0 { + return errors.NewDecodeError(errors.ErrCodeInvalidData, "expected end of stream but bytes remain", nil) + } + return nil +} + +func (d *Decoder) ReadByte() (byte, error) { + if err := d.checkBounds(1); err != nil { + return 0, err + } + b := d.data[d.pos] + d.pos++ + return b, nil +} + +func (d *Decoder) ReadBytes(n int) ([]byte, error) { + if err := d.checkBounds(n); err != nil { + return nil, err + } + result := d.data[d.pos : d.pos+n] + d.pos += n + return result, nil +} + +func (d *Decoder) ReadU32() (uint32, error) { + var result uint32 + var shift uint + + for { + b, err := d.ReadByte() + if err != nil { + return 0, err + } + + val := uint32(b & 0x7f) + if shift >= 28 { + if shift == 28 && val > 0x0f { + return 0, errors.ErrLEB128Overflow(32) + } + if shift > 28 { + return 0, errors.ErrLEB128Overflow(32) + } + } + + result |= val << shift + + if b&0x80 == 0 { + break + } + + shift += 7 + } + + return result, nil +} + +func (d *Decoder) ReadU64() (uint64, error) { + var result uint64 + var shift uint + + for { + b, err := d.ReadByte() + if err != nil { + return 0, err + } + + val := uint64(b & 0x7f) + if shift >= 63 { + if shift == 63 && val > 0x01 { + return 0, errors.ErrLEB128Overflow(64) + } + if shift > 63 { + return 0, errors.ErrLEB128Overflow(64) + } + } + + result |= val << shift + + if b&0x80 == 0 { + break + } + + shift += 7 + } + + return result, nil +} + +func (d *Decoder) ReadLengthPrefixed() ([]byte, error) { + length, err := d.ReadU32() + if err != nil { + return nil, err + } + return d.ReadBytes(int(length)) +} + +func (d *Decoder) PeekOp() (types.Op, bool) { + if d.Remaining() == 0 { + return 0, false + } + b := d.data[d.pos] + return types.NewOp(b) +} + +func (d *Decoder) ReadOp() (types.Op, error) { + b, err := d.ReadByte() + if err != nil { + return 0, err + } + op, ok := types.NewOp(b) + if !ok { + return 0, errors.ErrBadOpCode(b) + } + return op, nil +} + +func (d *Decoder) ReadMagic() (byte, error) { + magic, err := d.ReadBytes(len(MagicBytes)) + if err != nil { + return 0, err + } + if !bytes.Equal(magic, MagicBytes) { + return 0, errors.ErrBadMagic() + } + return d.ReadByte() +} + +func (d *Decoder) ReadHeader() (*types.DigestHeader, error) { + op, err := d.ReadOp() + if err != nil { + return nil, err + } + + digestOp := types.DigestOp(op) + if !digestOp.Valid() { + return nil, errors.ErrExpectedDigestOp(op.String()) + } + + digestLen := digestOp.OutputSize() + digest, err := d.ReadBytes(digestLen) + if err != nil { + return nil, err + } + + return types.NewDigestHeader(digestOp, digest) +} + +func (d *Decoder) readPendingAttestation() (*types.PendingAttestation, error) { + uriBytes, err := d.ReadLengthPrefixed() + if err != nil { + return nil, err + } + + uri := string(uriBytes) + if len(uri) > types.MaxURILen { + return nil, errors.ErrUriTooLong() + } + if !types.ValidateURI(uri) { + return nil, errors.ErrInvalidUriChar() + } + + return &types.PendingAttestation{URI: uri}, nil +} + +func (d *Decoder) readBitcoinAttestation() (*types.BitcoinAttestation, error) { + height, err := d.ReadU32() + if err != nil { + return nil, err + } + return &types.BitcoinAttestation{Height: height}, nil +} + +func (d *Decoder) readEASAttestation() (*types.EASAttestation, error) { + chainID, err := d.ReadU64() + if err != nil { + return nil, err + } + + uidBytes, err := d.ReadBytes(32) + if err != nil { + return nil, err + } + + var uid [32]byte + copy(uid[:], uidBytes) + + return &types.EASAttestation{ChainID: chainID, UID: uid}, nil +} + +func (d *Decoder) readEASTimestamped() (*types.EASTimestamped, error) { + chainID, err := d.ReadU64() + if err != nil { + return nil, err + } + return &types.EASTimestamped{ChainID: chainID}, nil +} + +func (d *Decoder) readAttestationFromData(data []byte, tag [8]byte) (types.Attestation, error) { + inner := NewDecoder(data) + + switch tag { + case types.BitcoinTag: + return inner.readBitcoinAttestation() + case types.PendingTag: + return inner.readPendingAttestation() + case types.EASAttestTag: + return inner.readEASAttestation() + case types.EASTimestampTag: + return inner.readEASTimestamped() + default: + unknown := types.NewUnknownAttestation(tag, data) + return unknown, nil + } +} + +func (d *Decoder) ReadAttestationStep() (*types.AttestationStep, error) { + op, err := d.ReadOp() + if err != nil { + return nil, err + } + if op != types.OpAttestation { + return nil, errors.NewDecodeError(errors.ErrCodeInvalidData, + "expected ATTESTATION op", map[string]interface{}{"op": op.String()}) + } + + tagBytes, err := d.ReadBytes(TagSize) + if err != nil { + return nil, err + } + var tag [8]byte + copy(tag[:], tagBytes) + + data, err := d.ReadLengthPrefixed() + if err != nil { + return nil, err + } + + att, err := d.readAttestationFromData(data, tag) + if err != nil { + return nil, err + } + + return types.NewAttestationStep(att), nil +} + +func (d *Decoder) ReadExecutionStep(op types.Op) (types.Step, error) { + switch op { + case types.OpAPPEND: + data, err := d.ReadLengthPrefixed() + if err != nil { + return nil, err + } + return types.NewAppendStep(data, nil), nil + case types.OpPREPEND: + data, err := d.ReadLengthPrefixed() + if err != nil { + return nil, err + } + return types.NewPrependStep(data, nil), nil + case types.OpREVERSE: + return types.NewReverseStep(nil), nil + case types.OpHEXLIFY: + return types.NewHexlifyStep(nil), nil + case types.OpSHA256: + return types.NewSHA256Step(nil), nil + case types.OpKECCAK256: + return types.NewKeccak256Step(nil), nil + case types.OpSHA1: + return types.NewSHA1Step(nil), nil + case types.OpRIPEMD160: + return types.NewRIPEMD160Step(nil), nil + default: + return nil, errors.ErrBadOpCode(byte(op)) + } +} + +func (d *Decoder) ReadForkStep() (*types.ForkStep, error) { + branches := make([]types.Timestamp, 0) + + for { + op, ok := d.PeekOp() + if !ok { + return nil, errors.ErrUnexpectedEof() + } + + if op == types.OpFORK { + if _, err := d.ReadOp(); err != nil { + return nil, err + } + ts, err := d.ReadTimestamp() + if err != nil { + return nil, err + } + branches = append(branches, ts) + } else { + ts, err := d.ReadTimestamp() + if err != nil { + return nil, err + } + branches = append(branches, ts) + break + } + } + + if len(branches) < 2 { + return nil, errors.NewDecodeError(errors.ErrCodeInvalidData, + "fork step must have at least 2 branches", nil) + } + + return types.NewForkStep(branches), nil +} + +func (d *Decoder) ReadStep() (types.Step, error) { + op, ok := d.PeekOp() + if !ok { + // Distinguish between true EOF and invalid opcode. + if d.Remaining() == 0 { + return nil, errors.ErrUnexpectedEof() + } + // There is data remaining but PeekOp could not decode a valid opcode. + // Report a bad opcode error for the offending byte. + return nil, errors.ErrBadOpCode(d.data[d.pos]) + } + + switch op { + case types.OpFORK: + return d.ReadForkStep() + case types.OpAttestation: + return d.ReadAttestationStep() + default: + _, err := d.ReadOp() + if err != nil { + return nil, err + } + return d.ReadExecutionStep(op) + } +} + +func (d *Decoder) ReadTimestamp() (types.Timestamp, error) { + logger := logging.Default() + logger.Trace(context.Background(), "Decoder: ReadTimestamp", "remaining", d.Remaining()) + steps := make(types.Timestamp, 0) + + for d.Remaining() > 0 { + step, err := d.ReadStep() + if err != nil { + return nil, err + } + steps = append(steps, step) + + if step.Op() == types.OpFORK || step.Op() == types.OpAttestation { + break + } + } + + logger.Trace(context.Background(), "Decoder: ReadTimestamp complete", "steps", len(steps)) + return steps, nil +} + +func DecodeDetachedTimestamp(data []byte) (*types.DetachedTimestamp, error) { + logger := logging.Default() + logger.Trace(context.Background(), "DecodeDetachedTimestamp: decoding", "input_len", len(data)) + dec := NewDecoder(data) + + version, err := dec.ReadMagic() + if err != nil { + return nil, err + } + if version != 0x01 { + return nil, errors.ErrBadVersion() + } + + header, err := dec.ReadHeader() + if err != nil { + return nil, err + } + + ts, err := dec.ReadTimestamp() + if err != nil { + return nil, err + } + + logger.Trace(context.Background(), "DecodeDetachedTimestamp: complete", "steps", len(ts)) + return types.NewDetachedTimestamp(header, ts), nil +} diff --git a/packages/sdk-go/codec/decoder_test.go b/packages/sdk-go/codec/decoder_test.go new file mode 100644 index 0000000..6e03c65 --- /dev/null +++ b/packages/sdk-go/codec/decoder_test.go @@ -0,0 +1,723 @@ +package codec + +import ( + "bytes" + "testing" + + "github.com/lightsing/uts/packages/sdk-go/errors" + "github.com/lightsing/uts/packages/sdk-go/types" +) + +func TestDecoder_Remaining(t *testing.T) { + data := []byte{0x01, 0x02, 0x03} + dec := NewDecoder(data) + + if dec.Remaining() != 3 { + t.Errorf("expected remaining 3, got %d", dec.Remaining()) + } + + dec.ReadByte() + if dec.Remaining() != 2 { + t.Errorf("expected remaining 2, got %d", dec.Remaining()) + } +} + +func TestDecoder_ReadByte(t *testing.T) { + tests := []struct { + name string + data []byte + want byte + wantErr bool + }{ + {"read single byte", []byte{0x42}, 0x42, false}, + {"empty data", []byte{}, 0, true}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + dec := NewDecoder(tt.data) + got, err := dec.ReadByte() + if (err != nil) != tt.wantErr { + t.Errorf("ReadByte() error = %v, wantErr %v", err, tt.wantErr) + return + } + if got != tt.want { + t.Errorf("ReadByte() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestDecoder_ReadBytes(t *testing.T) { + tests := []struct { + name string + data []byte + n int + want []byte + wantErr bool + }{ + {"read 3 bytes", []byte{0x01, 0x02, 0x03, 0x04}, 3, []byte{0x01, 0x02, 0x03}, false}, + {"read all bytes", []byte{0x01, 0x02}, 2, []byte{0x01, 0x02}, false}, + {"read too many", []byte{0x01}, 2, nil, true}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + dec := NewDecoder(tt.data) + got, err := dec.ReadBytes(tt.n) + if (err != nil) != tt.wantErr { + t.Errorf("ReadBytes() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !bytes.Equal(got, tt.want) { + t.Errorf("ReadBytes() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestDecoder_ReadU32(t *testing.T) { + tests := []struct { + name string + data []byte + want uint32 + wantErr bool + }{ + {"zero", []byte{0x00}, 0, false}, + {"single byte", []byte{0x7f}, 127, false}, + {"two bytes", []byte{0x80, 0x01}, 128, false}, + {"300", []byte{0xac, 0x02}, 300, false}, + {"max u32", []byte{0xff, 0xff, 0xff, 0xff, 0x0f}, 0xffffffff, false}, + {"empty", []byte{}, 0, true}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + dec := NewDecoder(tt.data) + got, err := dec.ReadU32() + if (err != nil) != tt.wantErr { + t.Errorf("ReadU32() error = %v, wantErr %v", err, tt.wantErr) + return + } + if got != tt.want { + t.Errorf("ReadU32() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestDecoder_ReadU64(t *testing.T) { + tests := []struct { + name string + data []byte + want uint64 + wantErr bool + }{ + {"zero", []byte{0x00}, 0, false}, + {"single byte", []byte{0x7f}, 127, false}, + {"two bytes", []byte{0x80, 0x01}, 128, false}, + {"large value", []byte{0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x7f}, 0x7fffffffffffffff, false}, + {"max u64", []byte{0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x01}, 0xffffffffffffffff, false}, + {"empty", []byte{}, 0, true}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + dec := NewDecoder(tt.data) + got, err := dec.ReadU64() + if (err != nil) != tt.wantErr { + t.Errorf("ReadU64() error = %v, wantErr %v", err, tt.wantErr) + return + } + if got != tt.want { + t.Errorf("ReadU64() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestDecoder_ReadLengthPrefixed(t *testing.T) { + tests := []struct { + name string + data []byte + want []byte + wantErr bool + }{ + {"empty", []byte{0x00}, []byte{}, false}, + {"short data", []byte{0x03, 0x01, 0x02, 0x03}, []byte{0x01, 0x02, 0x03}, false}, + {"incomplete", []byte{0x05, 0x01, 0x02}, nil, true}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + dec := NewDecoder(tt.data) + got, err := dec.ReadLengthPrefixed() + if (err != nil) != tt.wantErr { + t.Errorf("ReadLengthPrefixed() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !bytes.Equal(got, tt.want) { + t.Errorf("ReadLengthPrefixed() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestDecoder_PeekOp(t *testing.T) { + tests := []struct { + name string + data []byte + want types.Op + wantOk bool + consumed bool + }{ + {"peek SHA256", []byte{0x08}, types.OpSHA256, true, false}, + {"peek FORK", []byte{0xff}, types.OpFORK, true, false}, + {"invalid op", []byte{0x99}, 0, false, false}, + {"empty", []byte{}, 0, false, false}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + dec := NewDecoder(tt.data) + got, ok := dec.PeekOp() + if ok != tt.wantOk { + t.Errorf("PeekOp() ok = %v, want %v", ok, tt.wantOk) + return + } + if tt.wantOk && got != tt.want { + t.Errorf("PeekOp() = %v, want %v", got, tt.want) + } + if !tt.consumed && dec.Remaining() != len(tt.data) { + t.Errorf("PeekOp consumed data, remaining = %d, want %d", dec.Remaining(), len(tt.data)) + } + }) + } +} + +func TestDecoder_ReadOp(t *testing.T) { + tests := []struct { + name string + data []byte + want types.Op + wantErr bool + }{ + {"SHA256", []byte{0x08}, types.OpSHA256, false}, + {"FORK", []byte{0xff}, types.OpFORK, false}, + {"invalid", []byte{0x99}, 0, true}, + {"empty", []byte{}, 0, true}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + dec := NewDecoder(tt.data) + got, err := dec.ReadOp() + if (err != nil) != tt.wantErr { + t.Errorf("ReadOp() error = %v, wantErr %v", err, tt.wantErr) + return + } + if got != tt.want { + t.Errorf("ReadOp() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestDecoder_ReadMagic(t *testing.T) { + validMagic := append(append([]byte{}, MagicBytes...), 0x01) + + tests := []struct { + name string + data []byte + wantVer byte + wantErr bool + errMatches func(error) bool + }{ + {"valid magic", validMagic, 0x01, false, nil}, + {"invalid magic", append([]byte{0x00, 0x01, 0x02, 0x03}, make([]byte, 28)...), 0, true, func(err error) bool { + return err.(*errors.DecodeError).Code == errors.ErrCodeBadMagic + }}, + {"empty", []byte{}, 0, true, func(err error) bool { + return err.(*errors.DecodeError).Code == errors.ErrCodeUnexpectedEof + }}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + dec := NewDecoder(tt.data) + got, err := dec.ReadMagic() + if (err != nil) != tt.wantErr { + t.Errorf("ReadMagic() error = %v, wantErr %v", err, tt.wantErr) + return + } + if tt.wantErr && tt.errMatches != nil && !tt.errMatches(err) { + t.Errorf("ReadMagic() error = %v, doesn't match expected error type", err) + return + } + if got != tt.wantVer { + t.Errorf("ReadMagic() = %v, want %v", got, tt.wantVer) + } + }) + } +} + +func TestDecoder_ReadHeader(t *testing.T) { + sha256Digest := make([]byte, 32) + for i := range sha256Digest { + sha256Digest[i] = byte(i) + } + + data := append([]byte{byte(types.OpSHA256)}, sha256Digest...) + + tests := []struct { + name string + data []byte + wantKind types.DigestOp + wantLen int + wantErr bool + }{ + {"SHA256 header", data, types.DigestSHA256, 32, false}, + {"invalid op", []byte{0x99}, 0, 0, true}, + {"empty", []byte{}, 0, 0, true}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + dec := NewDecoder(tt.data) + got, err := dec.ReadHeader() + if (err != nil) != tt.wantErr { + t.Errorf("ReadHeader() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !tt.wantErr { + if got.Kind() != tt.wantKind { + t.Errorf("ReadHeader() kind = %v, want %v", got.Kind(), tt.wantKind) + } + if len(got.DigestBytes()) != tt.wantLen { + t.Errorf("ReadHeader() digest len = %v, want %v", len(got.DigestBytes()), tt.wantLen) + } + } + }) + } +} + +func TestDecoder_ReadExecutionStep(t *testing.T) { + t.Run("APPEND step", func(t *testing.T) { + data := []byte{0x03, 'a', 'b', 'c'} + dec := NewDecoder(data) + step, err := dec.ReadExecutionStep(types.OpAPPEND) + if err != nil { + t.Fatalf("ReadExecutionStep() error = %v", err) + } + appendStep, ok := step.(*types.AppendStep) + if !ok { + t.Fatalf("expected AppendStep, got %T", step) + } + if string(appendStep.Data) != "abc" { + t.Errorf("APPEND data = %v, want 'abc'", string(appendStep.Data)) + } + }) + + t.Run("PREPEND step", func(t *testing.T) { + data := []byte{0x02, 'x', 'y'} + dec := NewDecoder(data) + step, err := dec.ReadExecutionStep(types.OpPREPEND) + if err != nil { + t.Fatalf("ReadExecutionStep() error = %v", err) + } + prependStep, ok := step.(*types.PrependStep) + if !ok { + t.Fatalf("expected PrependStep, got %T", step) + } + if string(prependStep.Data) != "xy" { + t.Errorf("PREPEND data = %v, want 'xy'", string(prependStep.Data)) + } + }) + + t.Run("REVERSE step", func(t *testing.T) { + dec := NewDecoder([]byte{}) + step, err := dec.ReadExecutionStep(types.OpREVERSE) + if err != nil { + t.Fatalf("ReadExecutionStep() error = %v", err) + } + if _, ok := step.(*types.ReverseStep); !ok { + t.Fatalf("expected ReverseStep, got %T", step) + } + }) + + t.Run("HEXLIFY step", func(t *testing.T) { + dec := NewDecoder([]byte{}) + step, err := dec.ReadExecutionStep(types.OpHEXLIFY) + if err != nil { + t.Fatalf("ReadExecutionStep() error = %v", err) + } + if _, ok := step.(*types.HexlifyStep); !ok { + t.Fatalf("expected HexlifyStep, got %T", step) + } + }) +} + +func TestDecoder_ReadBitcoinAttestation(t *testing.T) { + data := []byte{0x80, 0x08} // 1024 in LEB128 + dec := NewDecoder(data) + + att, err := dec.readBitcoinAttestation() + if err != nil { + t.Fatalf("readBitcoinAttestation() error = %v", err) + } + if att.Height != 1024 { + t.Errorf("height = %d, want 1024", att.Height) + } +} + +func TestDecoder_ReadPendingAttestation(t *testing.T) { + t.Run("valid URI", func(t *testing.T) { + uri := "https://example.com/calendar" + data := append([]byte{byte(len(uri))}, []byte(uri)...) + dec := NewDecoder(data) + + att, err := dec.readPendingAttestation() + if err != nil { + t.Fatalf("readPendingAttestation() error = %v", err) + } + if att.URI != uri { + t.Errorf("URI = %v, want %v", att.URI, uri) + } + }) + + t.Run("invalid URI char", func(t *testing.T) { + uri := "https://example.com/calendar?query=bad char" + data := append([]byte{byte(len(uri))}, []byte(uri)...) + dec := NewDecoder(data) + + _, err := dec.readPendingAttestation() + if err == nil { + t.Error("expected error for invalid URI char") + } + }) +} + +func TestDecoder_ReadEASAttestation(t *testing.T) { + chainID := uint64(534352) // Scroll + chainIDBytes := EncodeU64(chainID) + uid := [32]byte{} + for i := range uid { + uid[i] = byte(i) + } + + data := append(chainIDBytes, uid[:]...) + dec := NewDecoder(data) + + att, err := dec.readEASAttestation() + if err != nil { + t.Fatalf("readEASAttestation() error = %v", err) + } + if att.ChainID != chainID { + t.Errorf("chainID = %d, want %d", att.ChainID, chainID) + } + if att.UID != uid { + t.Errorf("UID mismatch") + } +} + +func TestDecoder_ReadEASTimestamped(t *testing.T) { + chainID := uint64(1) + data := EncodeU64(chainID) + dec := NewDecoder(data) + + att, err := dec.readEASTimestamped() + if err != nil { + t.Fatalf("readEASTimestamped() error = %v", err) + } + if att.ChainID != chainID { + t.Errorf("chainID = %d, want %d", att.ChainID, chainID) + } +} + +func TestDecoder_ReadAttestationStep(t *testing.T) { + t.Run("Bitcoin attestation", func(t *testing.T) { + innerData := EncodeU32(800000) + data := []byte{byte(types.OpAttestation)} + data = append(data, types.BitcoinTag[:]...) + data = append(data, byte(len(innerData))) + data = append(data, innerData...) + + dec := NewDecoder(data) + step, err := dec.ReadAttestationStep() + if err != nil { + t.Fatalf("ReadAttestationStep() error = %v", err) + } + + btcAtt, ok := step.Attestation.(*types.BitcoinAttestation) + if !ok { + t.Fatalf("expected BitcoinAttestation, got %T", step.Attestation) + } + if btcAtt.Height != 800000 { + t.Errorf("height = %d, want 800000", btcAtt.Height) + } + }) + + t.Run("Unknown attestation", func(t *testing.T) { + unknownTag := [8]byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08} + innerData := []byte{0xde, 0xad, 0xbe, 0xef} + + data := []byte{byte(types.OpAttestation)} + data = append(data, unknownTag[:]...) + data = append(data, byte(len(innerData))) + data = append(data, innerData...) + + dec := NewDecoder(data) + step, err := dec.ReadAttestationStep() + if err != nil { + t.Fatalf("ReadAttestationStep() error = %v", err) + } + + unknownAtt, ok := step.Attestation.(*types.UnknownAttestation) + if !ok { + t.Fatalf("expected UnknownAttestation, got %T", step.Attestation) + } + tag := unknownAtt.Tag() + if tag != unknownTag { + t.Errorf("tag mismatch") + } + }) +} + +func TestDecoder_ReadTimestamp(t *testing.T) { + t.Run("simple timestamp", func(t *testing.T) { + enc := NewEncoder() + enc.WriteOp(types.OpREVERSE) + enc.WriteOp(types.OpHEXLIFY) + + dec := NewDecoder(enc.Bytes()) + ts, err := dec.ReadTimestamp() + if err != nil { + t.Fatalf("ReadTimestamp() error = %v", err) + } + if len(ts) != 2 { + t.Errorf("expected 2 steps, got %d", len(ts)) + } + }) + + t.Run("timestamp with attestation", func(t *testing.T) { + enc := NewEncoder() + enc.WriteOp(types.OpSHA256) + attStep := types.NewAttestationStep(&types.BitcoinAttestation{Height: 12345}) + if err := enc.WriteAttestationStep(attStep); err != nil { + t.Fatalf("encode error: %v", err) + } + + dec := NewDecoder(enc.Bytes()) + ts, err := dec.ReadTimestamp() + if err != nil { + t.Fatalf("ReadTimestamp() error = %v", err) + } + if len(ts) != 2 { + t.Errorf("expected 2 steps, got %d", len(ts)) + } + if ts[1].Op() != types.OpAttestation { + t.Errorf("expected ATTESTATION step, got %v", ts[1].Op()) + } + }) +} + +func TestDecoder_ReadForkStep(t *testing.T) { + t.Run("valid fork", func(t *testing.T) { + branch1 := types.Timestamp{types.NewSHA256Step(nil), types.NewAttestationStep(&types.BitcoinAttestation{Height: 100})} + branch2 := types.Timestamp{types.NewKeccak256Step(nil), types.NewAttestationStep(&types.BitcoinAttestation{Height: 200})} + forkStep := types.NewForkStep([]types.Timestamp{branch1, branch2}) + + enc := NewEncoder() + if err := enc.WriteForkStep(forkStep); err != nil { + t.Fatalf("WriteForkStep error: %v", err) + } + + dec := NewDecoder(enc.Bytes()) + fork, err := dec.ReadForkStep() + if err != nil { + t.Fatalf("ReadForkStep() error = %v", err) + } + if len(fork.Branches) != 2 { + t.Errorf("expected 2 branches, got %d", len(fork.Branches)) + } + }) +} + +func TestDecodeDetachedTimestamp(t *testing.T) { + digest := [32]byte{} + for i := range digest { + digest[i] = byte(i) + } + header, err := types.NewDigestHeader(types.DigestSHA256, digest[:]) + if err != nil { + t.Fatalf("NewDigestHeader() error = %v", err) + } + ts := types.Timestamp{ + types.NewReverseStep(nil), + types.NewAttestationStep(&types.BitcoinAttestation{Height: 800000}), + } + ots := types.NewDetachedTimestamp(header, ts) + + data, err := EncodeDetachedTimestamp(ots) + if err != nil { + t.Fatalf("EncodeDetachedTimestamp() error = %v", err) + } + + decoded, err := DecodeDetachedTimestamp(data) + if err != nil { + t.Fatalf("DecodeDetachedTimestamp() error = %v", err) + } + + if decoded.Header.Kind() != header.Kind() { + t.Errorf("header kind mismatch") + } + if len(decoded.Timestamp) != len(ts) { + t.Errorf("timestamp length mismatch: got %d, want %d", len(decoded.Timestamp), len(ts)) + } +} + +func TestRoundTrip(t *testing.T) { + t.Run("simple timestamp", func(t *testing.T) { + digest := [32]byte{0x01, 0x02, 0x03} + header, err := types.NewDigestHeader(types.DigestSHA256, digest[:]) + if err != nil { + t.Fatalf("NewDigestHeader() error = %v", err) + } + ts := types.Timestamp{ + types.NewAppendStep([]byte("hello"), nil), + types.NewReverseStep(nil), + types.NewAttestationStep(&types.EASAttestation{ + ChainID: 534352, + UID: [32]byte{0xaa, 0xbb, 0xcc}, + }), + } + original := types.NewDetachedTimestamp(header, ts) + + data, err := EncodeDetachedTimestamp(original) + if err != nil { + t.Fatalf("encode error: %v", err) + } + + decoded, err := DecodeDetachedTimestamp(data) + if err != nil { + t.Fatalf("decode error: %v", err) + } + + if decoded.Header.Kind() != original.Header.Kind() { + t.Errorf("header kind mismatch") + } + if len(decoded.Timestamp) != len(original.Timestamp) { + t.Errorf("timestamp length mismatch") + } + }) + + t.Run("fork timestamp", func(t *testing.T) { + digest := [32]byte{0xff} + header, err := types.NewDigestHeader(types.DigestSHA256, digest[:]) + if err != nil { + t.Fatalf("NewDigestHeader() error = %v", err) + } + ts := types.Timestamp{ + types.NewForkStep([]types.Timestamp{ + {types.NewReverseStep(nil), types.NewAttestationStep(&types.BitcoinAttestation{Height: 100})}, + {types.NewHexlifyStep(nil), types.NewAttestationStep(&types.BitcoinAttestation{Height: 200})}, + }), + } + original := types.NewDetachedTimestamp(header, ts) + + data, err := EncodeDetachedTimestamp(original) + if err != nil { + t.Fatalf("encode error: %v", err) + } + + decoded, err := DecodeDetachedTimestamp(data) + if err != nil { + t.Fatalf("decode error: %v", err) + } + + if len(decoded.Timestamp) != 1 { + t.Fatalf("expected 1 step, got %d", len(decoded.Timestamp)) + } + forkStep, ok := decoded.Timestamp[0].(*types.ForkStep) + if !ok { + t.Fatalf("expected ForkStep, got %T", decoded.Timestamp[0]) + } + if len(forkStep.Branches) != 2 { + t.Errorf("expected 2 branches, got %d", len(forkStep.Branches)) + } + }) + + t.Run("all attestation types", func(t *testing.T) { + digest := [32]byte{0x99} + header, err := types.NewDigestHeader(types.DigestKECCAK256, digest[:]) + if err != nil { + t.Fatalf("NewDigestHeader() error = %v", err) + } + uid := [32]byte{} + for i := range uid { + uid[i] = byte(i) + } + + ts := types.Timestamp{ + types.NewForkStep([]types.Timestamp{ + {types.NewAttestationStep(&types.PendingAttestation{URI: "https://calendar.example.com"})}, + {types.NewAttestationStep(&types.BitcoinAttestation{Height: 123456})}, + {types.NewAttestationStep(&types.EASAttestation{ChainID: 1, UID: uid})}, + {types.NewAttestationStep(&types.EASTimestamped{ChainID: 534352})}, + }), + } + original := types.NewDetachedTimestamp(header, ts) + + data, err := EncodeDetachedTimestamp(original) + if err != nil { + t.Fatalf("encode error: %v", err) + } + + decoded, err := DecodeDetachedTimestamp(data) + if err != nil { + t.Fatalf("decode error: %v", err) + } + + if len(decoded.Timestamp) != 1 { + t.Fatalf("expected 1 step (fork), got %d", len(decoded.Timestamp)) + } + forkStep, ok := decoded.Timestamp[0].(*types.ForkStep) + if !ok { + t.Fatalf("expected ForkStep, got %T", decoded.Timestamp[0]) + } + if len(forkStep.Branches) != 4 { + t.Errorf("expected 4 branches, got %d", len(forkStep.Branches)) + } + + for i, branch := range forkStep.Branches { + if len(branch) != 1 { + t.Errorf("branch %d: expected 1 step, got %d", i, len(branch)) + } + } + }) +} + +func TestDecodeErrors(t *testing.T) { + t.Run("bad magic", func(t *testing.T) { + _, err := DecodeDetachedTimestamp([]byte{0x00, 0x01, 0x02}) + if err == nil { + t.Error("expected error for bad magic") + } + }) + + t.Run("bad version", func(t *testing.T) { + data := append(append([]byte{}, MagicBytes...), 0x99) + _, err := DecodeDetachedTimestamp(data) + if err == nil { + t.Error("expected error for bad version") + } + }) + + t.Run("unexpected EOF", func(t *testing.T) { + data := append(append([]byte{}, MagicBytes...), 0x01) + _, err := DecodeDetachedTimestamp(data) + if err == nil { + t.Error("expected error for unexpected EOF") + } + }) +} diff --git a/packages/sdk-go/codec/doc.go b/packages/sdk-go/codec/doc.go new file mode 100644 index 0000000..544b964 --- /dev/null +++ b/packages/sdk-go/codec/doc.go @@ -0,0 +1,9 @@ +// Package codec provides constants and utilities for encoding/decoding UTS timestamps. +// +// This package contains the binary format constants used by the OpenTimestamps +// and UTS protocols, including: +// - OpCode definitions and mappings +// - Attestation tag identifiers +// - Magic bytes for file format identification +// - Schema IDs for EAS attestations +package codec diff --git a/packages/sdk-go/codec/encoder.go b/packages/sdk-go/codec/encoder.go new file mode 100644 index 0000000..1b69945 --- /dev/null +++ b/packages/sdk-go/codec/encoder.go @@ -0,0 +1,213 @@ +package codec + +import ( + "context" + "strings" + + "github.com/lightsing/uts/packages/sdk-go/errors" + "github.com/lightsing/uts/packages/sdk-go/logging" + "github.com/lightsing/uts/packages/sdk-go/types" +) + +type Encoder struct { + buf []byte +} + +func NewEncoder() *Encoder { + return &Encoder{ + buf: make([]byte, 0, 1024), + } +} + +func NewEncoderWithSize(size int) *Encoder { + return &Encoder{ + buf: make([]byte, 0, size), + } +} + +func (e *Encoder) Bytes() []byte { + return e.buf +} + +func (e *Encoder) ensureCapacity(n int) { + if cap(e.buf)-len(e.buf) < n { + newCap := cap(e.buf) * 2 + if newCap < len(e.buf)+n { + newCap = len(e.buf) + n + } + newBuf := make([]byte, len(e.buf), newCap) + copy(newBuf, e.buf) + e.buf = newBuf + } +} + +func (e *Encoder) WriteByte(b byte) *Encoder { + e.ensureCapacity(1) + e.buf = append(e.buf, b) + return e +} + +func (e *Encoder) WriteBytes(data []byte) *Encoder { + e.ensureCapacity(len(data)) + e.buf = append(e.buf, data...) + return e +} + +func (e *Encoder) WriteU32(n uint32) *Encoder { + e.buf = append(e.buf, EncodeU32(n)...) + return e +} + +func (e *Encoder) WriteU64(n uint64) *Encoder { + e.buf = append(e.buf, EncodeU64(n)...) + return e +} + +func (e *Encoder) WriteLengthPrefixed(data []byte) *Encoder { + e.WriteU32(uint32(len(data))) + e.WriteBytes(data) + return e +} + +func (e *Encoder) WriteOp(op types.Op) *Encoder { + return e.WriteByte(byte(op)) +} + +func (e *Encoder) WriteVersionedMagic(version byte) *Encoder { + e.WriteBytes(MagicBytes) + e.WriteByte(version) + return e +} + +func (e *Encoder) WriteHeader(header *types.DigestHeader) *Encoder { + e.WriteOp(types.Op(header.Kind())) + e.WriteBytes(header.DigestBytes()) + return e +} + +func (e *Encoder) WriteExecutionStep(step types.Step) *Encoder { + e.WriteOp(step.Op()) + switch s := step.(type) { + case *types.AppendStep: + e.WriteLengthPrefixed(s.Data) + case *types.PrependStep: + e.WriteLengthPrefixed(s.Data) + } + return e +} + +func (e *Encoder) WriteForkStep(step *types.ForkStep) error { + if len(step.Branches) < 2 { + return errors.NewEncodeError(errors.ErrCodeInvalidData, "FORK step must have at least 2 branches", nil) + } + for _, branch := range step.Branches[:len(step.Branches)-1] { + e.WriteOp(types.OpFORK) + if err := e.WriteTimestamp(branch); err != nil { + return err + } + } + return e.WriteTimestamp(step.Branches[len(step.Branches)-1]) +} + +func (e *Encoder) WritePendingAttestation(att *types.PendingAttestation) error { + uri := strings.TrimSuffix(att.URI, "/") + if len(uri) > types.MaxURILen { + return errors.NewEncodeError(errors.ErrCodeUriTooLong, "URI exceeds maximum length", nil) + } + if !types.ValidateURI(uri) { + return errors.NewEncodeError(errors.ErrCodeInvalidUriChar, "invalid character in URI", nil) + } + e.WriteLengthPrefixed([]byte(uri)) + return nil +} + +func (e *Encoder) WriteBitcoinAttestation(att *types.BitcoinAttestation) *Encoder { + e.WriteU32(att.Height) + return e +} + +func (e *Encoder) WriteEASAttestation(att *types.EASAttestation) *Encoder { + e.WriteU64(att.ChainID) + e.WriteBytes(att.UID[:]) + return e +} + +func (e *Encoder) WriteEASTimestamped(att *types.EASTimestamped) *Encoder { + e.WriteU64(att.ChainID) + return e +} + +func (e *Encoder) WriteUnknownAttestation(att *types.UnknownAttestation) *Encoder { + tag := att.Tag() + e.WriteBytes(tag[:]) + e.WriteLengthPrefixed(att.Data) + return e +} + +func (e *Encoder) WriteAttestationStep(step *types.AttestationStep) error { + e.WriteOp(types.OpAttestation) + inner := NewEncoder() + + switch att := step.Attestation.(type) { + case *types.PendingAttestation: + e.WriteBytes(types.PendingTag[:]) + if err := inner.WritePendingAttestation(att); err != nil { + return err + } + e.WriteLengthPrefixed(inner.Bytes()) + case *types.BitcoinAttestation: + e.WriteBytes(types.BitcoinTag[:]) + inner.WriteBitcoinAttestation(att) + e.WriteLengthPrefixed(inner.Bytes()) + case *types.EASAttestation: + e.WriteBytes(types.EASAttestTag[:]) + inner.WriteEASAttestation(att) + e.WriteLengthPrefixed(inner.Bytes()) + case *types.EASTimestamped: + e.WriteBytes(types.EASTimestampTag[:]) + inner.WriteEASTimestamped(att) + e.WriteLengthPrefixed(inner.Bytes()) + default: + tag := step.Attestation.Tag() + e.WriteBytes(tag[:]) + e.WriteLengthPrefixed([]byte{}) + } + return nil +} + +func (e *Encoder) WriteStep(step types.Step) error { + switch s := step.(type) { + case *types.ForkStep: + return e.WriteForkStep(s) + case *types.AttestationStep: + return e.WriteAttestationStep(s) + default: + e.WriteExecutionStep(s) + return nil + } +} + +func (e *Encoder) WriteTimestamp(ts types.Timestamp) error { + logger := logging.Default() + logger.Trace(context.Background(), "Encoder: WriteTimestamp", "steps", len(ts)) + for _, step := range ts { + if err := e.WriteStep(step); err != nil { + return err + } + } + return nil +} + +func EncodeDetachedTimestamp(ots *types.DetachedTimestamp) ([]byte, error) { + logger := logging.Default() + logger.Trace(context.Background(), "EncodeDetachedTimestamp: encoding", "digest_len", len(ots.Header.DigestBytes())) + enc := NewEncoder() + enc.WriteVersionedMagic(0x01) + enc.WriteHeader(ots.Header) + if err := enc.WriteTimestamp(ots.Timestamp); err != nil { + return nil, err + } + result := enc.Bytes() + logger.Trace(context.Background(), "EncodeDetachedTimestamp: complete", "output_len", len(result)) + return result, nil +} diff --git a/packages/sdk-go/codec/encoder_test.go b/packages/sdk-go/codec/encoder_test.go new file mode 100644 index 0000000..96ac387 --- /dev/null +++ b/packages/sdk-go/codec/encoder_test.go @@ -0,0 +1,456 @@ +package codec + +import ( + "bytes" + "testing" + + "github.com/lightsing/uts/packages/sdk-go/types" +) + +func TestNewEncoder(t *testing.T) { + enc := NewEncoder() + if enc == nil { + t.Fatal("NewEncoder returned nil") + } + if len(enc.Bytes()) != 0 { + t.Errorf("new encoder should have empty buffer, got %d bytes", len(enc.Bytes())) + } +} + +func TestEncoderWriteByte(t *testing.T) { + enc := NewEncoder() + enc.WriteByte(0x00).WriteByte(0xff).WriteByte(0x08) + + got := enc.Bytes() + want := []byte{0x00, 0xff, 0x08} + if !bytes.Equal(got, want) { + t.Errorf("WriteByte() = %v, want %v", got, want) + } +} + +func TestEncoderWriteBytes(t *testing.T) { + enc := NewEncoder() + data := []byte{0x01, 0x02, 0x03, 0x04} + enc.WriteBytes(data) + + if !bytes.Equal(enc.Bytes(), data) { + t.Errorf("WriteBytes() = %v, want %v", enc.Bytes(), data) + } +} + +func TestEncoderWriteU32(t *testing.T) { + tests := []struct { + name string + input uint32 + want []byte + }{ + {"zero", 0, []byte{0x00}}, + {"one", 1, []byte{0x01}}, + {"127", 127, []byte{0x7f}}, + {"128", 128, []byte{0x80, 0x01}}, + {"300", 300, []byte{0xac, 0x02}}, + {"max", 0xffffffff, []byte{0xff, 0xff, 0xff, 0xff, 0x0f}}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + enc := NewEncoder() + enc.WriteU32(tt.input) + if !bytes.Equal(enc.Bytes(), tt.want) { + t.Errorf("WriteU32(%d) = %v, want %v", tt.input, enc.Bytes(), tt.want) + } + }) + } +} + +func TestEncoderWriteU64(t *testing.T) { + tests := []struct { + name string + input uint64 + want []byte + }{ + {"zero", 0, []byte{0x00}}, + {"one", 1, []byte{0x01}}, + {"127", 127, []byte{0x7f}}, + {"128", 128, []byte{0x80, 0x01}}, + {"max uint32", 0xffffffff, []byte{0xff, 0xff, 0xff, 0xff, 0x0f}}, + {"large", 0x0fffffffffffffff, []byte{0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x0f}}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + enc := NewEncoder() + enc.WriteU64(tt.input) + if !bytes.Equal(enc.Bytes(), tt.want) { + t.Errorf("WriteU64(%d) = %v, want %v", tt.input, enc.Bytes(), tt.want) + } + }) + } +} + +func TestEncoderWriteLengthPrefixed(t *testing.T) { + enc := NewEncoder() + data := []byte("hello") + enc.WriteLengthPrefixed(data) + + want := append([]byte{0x05}, data...) + if !bytes.Equal(enc.Bytes(), want) { + t.Errorf("WriteLengthPrefixed() = %v, want %v", enc.Bytes(), want) + } +} + +func TestEncoderWriteHeader(t *testing.T) { + digest := make([]byte, 32) + for i := range digest { + digest[i] = byte(i) + } + + header, err := types.NewDigestHeader(types.DigestSHA256, digest) + if err != nil { + t.Fatalf("NewDigestHeader() error = %v", err) + } + enc := NewEncoder() + enc.WriteHeader(header) + + want := append([]byte{0x08}, digest...) + if !bytes.Equal(enc.Bytes(), want) { + t.Errorf("WriteHeader() = %v, want %v", enc.Bytes(), want) + } +} + +func TestEncoderWriteHeaderSHA1(t *testing.T) { + digest := make([]byte, 20) + for i := range digest { + digest[i] = byte(i) + } + + header, err := types.NewDigestHeader(types.DigestSHA1, digest) + if err != nil { + t.Fatalf("NewDigestHeader() error = %v", err) + } + enc := NewEncoder() + enc.WriteHeader(header) + + want := append([]byte{0x02}, digest...) + if !bytes.Equal(enc.Bytes(), want) { + t.Errorf("WriteHeader() = %v, want %v", enc.Bytes(), want) + } +} + +func TestEncoderWriteVersionedMagic(t *testing.T) { + enc := NewEncoder() + enc.WriteVersionedMagic(0x01) + + want := append(MagicBytes, 0x01) + if !bytes.Equal(enc.Bytes(), want) { + t.Errorf("WriteVersionedMagic() = %v, want %v", enc.Bytes(), want) + } +} + +func TestEncoderWriteAppendStep(t *testing.T) { + enc := NewEncoder() + step := types.NewAppendStep([]byte("test"), nil) + enc.WriteExecutionStep(step) + + want := []byte{0xf0, 0x04, 't', 'e', 's', 't'} + if !bytes.Equal(enc.Bytes(), want) { + t.Errorf("WriteAppendStep() = %v, want %v", enc.Bytes(), want) + } +} + +func TestEncoderWritePrependStep(t *testing.T) { + enc := NewEncoder() + step := types.NewPrependStep([]byte("test"), nil) + enc.WriteExecutionStep(step) + + want := []byte{0xf1, 0x04, 't', 'e', 's', 't'} + if !bytes.Equal(enc.Bytes(), want) { + t.Errorf("WritePrependStep() = %v, want %v", enc.Bytes(), want) + } +} + +func TestEncoderWriteHashSteps(t *testing.T) { + tests := []struct { + name string + op byte + step types.Step + }{ + {"SHA256", 0x08, types.NewSHA256Step(nil)}, + {"KECCAK256", 0x67, types.NewKeccak256Step(nil)}, + {"SHA1", 0x02, types.NewSHA1Step(nil)}, + {"RIPEMD160", 0x03, types.NewRIPEMD160Step(nil)}, + {"REVERSE", 0xf2, types.NewReverseStep(nil)}, + {"HEXLIFY", 0xf3, types.NewHexlifyStep(nil)}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + enc := NewEncoder() + enc.WriteExecutionStep(tt.step) + if !bytes.Equal(enc.Bytes(), []byte{tt.op}) { + t.Errorf("WriteExecutionStep() = %v, want %v", enc.Bytes(), []byte{tt.op}) + } + }) + } +} + +func TestEncoderWriteForkStep(t *testing.T) { + enc := NewEncoder() + branches := []types.Timestamp{ + {types.NewSHA256Step(nil)}, + {types.NewSHA256Step(nil)}, + } + step := types.NewForkStep(branches) + err := enc.WriteForkStep(step) + if err != nil { + t.Fatalf("WriteForkStep error: %v", err) + } + + want := []byte{0xff, 0x08, 0x08} + if !bytes.Equal(enc.Bytes(), want) { + t.Errorf("WriteForkStep() = %v, want %v", enc.Bytes(), want) + } +} + +func TestEncoderWriteForkStepError(t *testing.T) { + enc := NewEncoder() + step := types.NewForkStep([]types.Timestamp{}) + err := enc.WriteForkStep(step) + if err == nil { + t.Error("expected error for empty fork step") + } + + step = types.NewForkStep([]types.Timestamp{{types.NewSHA256Step(nil)}}) + err = enc.WriteForkStep(step) + if err == nil { + t.Error("expected error for single branch fork step") + } +} + +func TestEncoderWritePendingAttestation(t *testing.T) { + enc := NewEncoder() + att := &types.PendingAttestation{URI: "https://example.com"} + err := enc.WritePendingAttestation(att) + if err != nil { + t.Fatalf("WritePendingAttestation error: %v", err) + } + + uri := "https://example.com" + want := append([]byte{byte(len(uri))}, uri...) + if !bytes.Equal(enc.Bytes(), want) { + t.Errorf("WritePendingAttestation() = %v, want %v", enc.Bytes(), want) + } +} + +func TestEncoderWritePendingAttestationTrimSlash(t *testing.T) { + enc := NewEncoder() + att := &types.PendingAttestation{URI: "https://example.com/"} + err := enc.WritePendingAttestation(att) + if err != nil { + t.Fatalf("WritePendingAttestation error: %v", err) + } + + uri := "https://example.com" + want := append([]byte{byte(len(uri))}, uri...) + if !bytes.Equal(enc.Bytes(), want) { + t.Errorf("WritePendingAttestation() = %v, want %v", enc.Bytes(), want) + } +} + +func TestEncoderWritePendingAttestationInvalidURI(t *testing.T) { + enc := NewEncoder() + att := &types.PendingAttestation{URI: "https://example.com/invalid!@#$"} + err := enc.WritePendingAttestation(att) + if err == nil { + t.Error("expected error for invalid URI") + } +} + +func TestEncoderWritePendingAttestationURITooLong(t *testing.T) { + enc := NewEncoder() + uri := make([]byte, types.MaxURILen+1) + for i := range uri { + uri[i] = 'a' + } + att := &types.PendingAttestation{URI: string(uri)} + err := enc.WritePendingAttestation(att) + if err == nil { + t.Error("expected error for URI too long") + } +} + +func TestEncoderWriteBitcoinAttestation(t *testing.T) { + enc := NewEncoder() + att := &types.BitcoinAttestation{Height: 500000} + enc.WriteBitcoinAttestation(att) + + want := EncodeU32(500000) + if !bytes.Equal(enc.Bytes(), want) { + t.Errorf("WriteBitcoinAttestation() = %v, want %v", enc.Bytes(), want) + } +} + +func TestEncoderWriteEASAttestation(t *testing.T) { + enc := NewEncoder() + var uid [32]byte + for i := range uid { + uid[i] = byte(i) + } + att := &types.EASAttestation{ChainID: 1, UID: uid} + enc.WriteEASAttestation(att) + + enc2 := NewEncoder() + enc2.WriteU64(1) + enc2.WriteBytes(uid[:]) + if !bytes.Equal(enc.Bytes(), enc2.Bytes()) { + t.Errorf("WriteEASAttestation() = %v, want %v", enc.Bytes(), enc2.Bytes()) + } +} + +func TestEncoderWriteEASTimestamped(t *testing.T) { + enc := NewEncoder() + att := &types.EASTimestamped{ChainID: 534352} + enc.WriteEASTimestamped(att) + + want := EncodeU64(534352) + if !bytes.Equal(enc.Bytes(), want) { + t.Errorf("WriteEASTimestamped() = %v, want %v", enc.Bytes(), want) + } +} + +func TestEncoderWriteAttestationStepPending(t *testing.T) { + enc := NewEncoder() + att := &types.PendingAttestation{URI: "https://example.com"} + step := types.NewAttestationStep(att) + err := enc.WriteAttestationStep(step) + if err != nil { + t.Fatalf("WriteAttestationStep error: %v", err) + } + + if enc.Bytes()[0] != 0x00 { + t.Errorf("expected attestation opcode 0x00, got 0x%02x", enc.Bytes()[0]) + } + tag := enc.Bytes()[1:9] + if !bytes.Equal(tag, types.PendingTag[:]) { + t.Errorf("tag = %v, want %v", tag, types.PendingTag[:]) + } +} + +func TestEncoderWriteAttestationStepBitcoin(t *testing.T) { + enc := NewEncoder() + att := &types.BitcoinAttestation{Height: 123} + step := types.NewAttestationStep(att) + err := enc.WriteAttestationStep(step) + if err != nil { + t.Fatalf("WriteAttestationStep error: %v", err) + } + + if enc.Bytes()[0] != 0x00 { + t.Errorf("expected attestation opcode 0x00, got 0x%02x", enc.Bytes()[0]) + } + tag := enc.Bytes()[1:9] + if !bytes.Equal(tag, types.BitcoinTag[:]) { + t.Errorf("tag = %v, want %v", tag, types.BitcoinTag[:]) + } +} + +func TestEncoderWriteAttestationStepEASAttestation(t *testing.T) { + enc := NewEncoder() + var uid [32]byte + att := &types.EASAttestation{ChainID: 1, UID: uid} + step := types.NewAttestationStep(att) + err := enc.WriteAttestationStep(step) + if err != nil { + t.Fatalf("WriteAttestationStep error: %v", err) + } + + if enc.Bytes()[0] != 0x00 { + t.Errorf("expected attestation opcode 0x00, got 0x%02x", enc.Bytes()[0]) + } + tag := enc.Bytes()[1:9] + if !bytes.Equal(tag, types.EASAttestTag[:]) { + t.Errorf("tag = %v, want %v", tag, types.EASAttestTag[:]) + } +} + +func TestEncoderWriteAttestationStepEASTimestamped(t *testing.T) { + enc := NewEncoder() + att := &types.EASTimestamped{ChainID: 534352} + step := types.NewAttestationStep(att) + err := enc.WriteAttestationStep(step) + if err != nil { + t.Fatalf("WriteAttestationStep error: %v", err) + } + + if enc.Bytes()[0] != 0x00 { + t.Errorf("expected attestation opcode 0x00, got 0x%02x", enc.Bytes()[0]) + } + tag := enc.Bytes()[1:9] + if !bytes.Equal(tag, types.EASTimestampTag[:]) { + t.Errorf("tag = %v, want %v", tag, types.EASTimestampTag[:]) + } +} + +func TestEncoderWriteTimestamp(t *testing.T) { + enc := NewEncoder() + ts := types.Timestamp{ + types.NewSHA256Step(nil), + types.NewAppendStep([]byte("test"), nil), + } + err := enc.WriteTimestamp(ts) + if err != nil { + t.Fatalf("WriteTimestamp error: %v", err) + } + + want := []byte{0x08, 0xf0, 0x04, 't', 'e', 's', 't'} + if !bytes.Equal(enc.Bytes(), want) { + t.Errorf("WriteTimestamp() = %v, want %v", enc.Bytes(), want) + } +} + +func TestEncodeDetachedTimestamp(t *testing.T) { + digest := make([]byte, 32) + for i := range digest { + digest[i] = byte(i) + } + header, err := types.NewDigestHeader(types.DigestSHA256, digest) + if err != nil { + t.Fatalf("NewDigestHeader() error = %v", err) + } + ts := types.Timestamp{ + types.NewAttestationStep(&types.PendingAttestation{URI: "https://example.com"}), + } + ots := types.NewDetachedTimestamp(header, ts) + + encoded, err := EncodeDetachedTimestamp(ots) + if err != nil { + t.Fatalf("EncodeDetachedTimestamp error: %v", err) + } + + if !bytes.HasPrefix(encoded, MagicBytes) { + t.Error("encoded timestamp should start with magic bytes") + } + if encoded[len(MagicBytes)] != 0x01 { + t.Error("encoded timestamp should have version 0x01") + } +} + +func TestEncoderCapacityGrowth(t *testing.T) { + enc := NewEncoderWithSize(10) + for i := 0; i < 100; i++ { + enc.WriteByte(byte(i)) + } + if len(enc.Bytes()) != 100 { + t.Errorf("expected 100 bytes, got %d", len(enc.Bytes())) + } +} + +func TestEncoderChaining(t *testing.T) { + enc := NewEncoder() + enc.WriteByte(0x01).WriteByte(0x02).WriteByte(0x03) + + want := []byte{0x01, 0x02, 0x03} + if !bytes.Equal(enc.Bytes(), want) { + t.Errorf("chaining = %v, want %v", enc.Bytes(), want) + } +} diff --git a/packages/sdk-go/codec/leb128.go b/packages/sdk-go/codec/leb128.go new file mode 100644 index 0000000..62f381f --- /dev/null +++ b/packages/sdk-go/codec/leb128.go @@ -0,0 +1,103 @@ +package codec + +import ( + "io" + + "github.com/lightsing/uts/packages/sdk-go/errors" +) + +func EncodeU32(n uint32) []byte { + buf := make([]byte, 0, 5) + for { + b := byte(n & 0x7f) + n >>= 7 + if n != 0 { + b |= 0x80 + } + buf = append(buf, b) + if n == 0 { + break + } + } + return buf +} + +func DecodeU32(r io.Reader) (uint32, error) { + var result uint32 + var shift uint + + for { + var b [1]byte + if _, err := io.ReadFull(r, b[:]); err != nil { + return 0, err + } + + val := uint32(b[0] & 0x7f) + if shift >= 28 { + if shift == 28 && val > 0x0f { + return 0, errors.ErrLEB128Overflow(32) + } + if shift > 28 { + return 0, errors.ErrLEB128Overflow(32) + } + } + + result |= val << shift + + if b[0]&0x80 == 0 { + break + } + + shift += 7 + } + + return result, nil +} + +func EncodeU64(n uint64) []byte { + buf := make([]byte, 0, 10) + for { + b := byte(n & 0x7f) + n >>= 7 + if n != 0 { + b |= 0x80 + } + buf = append(buf, b) + if n == 0 { + break + } + } + return buf +} + +func DecodeU64(r io.Reader) (uint64, error) { + var result uint64 + var shift uint + + for { + var b [1]byte + if _, err := io.ReadFull(r, b[:]); err != nil { + return 0, err + } + + val := uint64(b[0] & 0x7f) + if shift >= 63 { + if shift == 63 && val > 0x01 { + return 0, errors.ErrLEB128Overflow(64) + } + if shift > 63 { + return 0, errors.ErrLEB128Overflow(64) + } + } + + result |= val << shift + + if b[0]&0x80 == 0 { + break + } + + shift += 7 + } + + return result, nil +} diff --git a/packages/sdk-go/codec/leb128_test.go b/packages/sdk-go/codec/leb128_test.go new file mode 100644 index 0000000..d9cf256 --- /dev/null +++ b/packages/sdk-go/codec/leb128_test.go @@ -0,0 +1,212 @@ +package codec + +import ( + "bytes" + "testing" +) + +func TestEncodeU32(t *testing.T) { + tests := []struct { + name string + input uint32 + want []byte + }{ + {"zero", 0, []byte{0x00}}, + {"one", 1, []byte{0x01}}, + {"127", 127, []byte{0x7f}}, + {"128", 128, []byte{0x80, 0x01}}, + {"300", 300, []byte{0xac, 0x02}}, + {"16383", 16383, []byte{0xff, 0x7f}}, + {"16384", 16384, []byte{0x80, 0x80, 0x01}}, + {"max uint32", 0xffffffff, []byte{0xff, 0xff, 0xff, 0xff, 0x0f}}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := EncodeU32(tt.input) + if !bytes.Equal(got, tt.want) { + t.Errorf("EncodeU32(%d) = %v, want %v", tt.input, got, tt.want) + } + }) + } +} + +func TestDecodeU32(t *testing.T) { + tests := []struct { + name string + input []byte + want uint32 + }{ + {"zero", []byte{0x00}, 0}, + {"one", []byte{0x01}, 1}, + {"127", []byte{0x7f}, 127}, + {"128", []byte{0x80, 0x01}, 128}, + {"300", []byte{0xac, 0x02}, 300}, + {"16383", []byte{0xff, 0x7f}, 16383}, + {"16384", []byte{0x80, 0x80, 0x01}, 16384}, + {"max uint32", []byte{0xff, 0xff, 0xff, 0xff, 0x0f}, 0xffffffff}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := DecodeU32(bytes.NewReader(tt.input)) + if err != nil { + t.Errorf("DecodeU32(%v) error: %v", tt.input, err) + return + } + if got != tt.want { + t.Errorf("DecodeU32(%v) = %d, want %d", tt.input, got, tt.want) + } + }) + } +} + +func TestEncodeDecodeU32RoundTrip(t *testing.T) { + values := []uint32{ + 0, + 1, + 127, + 128, + 300, + 16383, + 16384, + 624352, + 0x00ffffff, + 0xffffffff, + } + + for _, v := range values { + t.Run("", func(t *testing.T) { + encoded := EncodeU32(v) + decoded, err := DecodeU32(bytes.NewReader(encoded)) + if err != nil { + t.Errorf("DecodeU32 error: %v", err) + return + } + if decoded != v { + t.Errorf("round trip: got %d, want %d", decoded, v) + } + }) + } +} + +func TestEncodeU64(t *testing.T) { + tests := []struct { + name string + input uint64 + want []byte + }{ + {"zero", 0, []byte{0x00}}, + {"one", 1, []byte{0x01}}, + {"127", 127, []byte{0x7f}}, + {"128", 128, []byte{0x80, 0x01}}, + {"300", 300, []byte{0xac, 0x02}}, + {"16383", 16383, []byte{0xff, 0x7f}}, + {"16384", 16384, []byte{0x80, 0x80, 0x01}}, + {"max uint32", 0xffffffff, []byte{0xff, 0xff, 0xff, 0xff, 0x0f}}, + {"large", 0x0fffffffffffffff, []byte{0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x0f}}, + {"max uint64", 0xffffffffffffffff, []byte{0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x01}}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := EncodeU64(tt.input) + if !bytes.Equal(got, tt.want) { + t.Errorf("EncodeU64(%d) = %v, want %v", tt.input, got, tt.want) + } + }) + } +} + +func TestDecodeU64(t *testing.T) { + tests := []struct { + name string + input []byte + want uint64 + }{ + {"zero", []byte{0x00}, 0}, + {"one", []byte{0x01}, 1}, + {"127", []byte{0x7f}, 127}, + {"128", []byte{0x80, 0x01}, 128}, + {"300", []byte{0xac, 0x02}, 300}, + {"16383", []byte{0xff, 0x7f}, 16383}, + {"16384", []byte{0x80, 0x80, 0x01}, 16384}, + {"max uint32", []byte{0xff, 0xff, 0xff, 0xff, 0x0f}, 0xffffffff}, + {"large", []byte{0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x0f}, 0x0fffffffffffffff}, + {"max uint64", []byte{0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x01}, 0xffffffffffffffff}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := DecodeU64(bytes.NewReader(tt.input)) + if err != nil { + t.Errorf("DecodeU64(%v) error: %v", tt.input, err) + return + } + if got != tt.want { + t.Errorf("DecodeU64(%v) = %d, want %d", tt.input, got, tt.want) + } + }) + } +} + +func TestEncodeDecodeU64RoundTrip(t *testing.T) { + values := []uint64{ + 0, + 1, + 127, + 128, + 300, + 16383, + 16384, + 0xffffffff, + 0x0fffffffffffffff, + 0xffffffffffffffff, + } + + for _, v := range values { + t.Run("", func(t *testing.T) { + encoded := EncodeU64(v) + decoded, err := DecodeU64(bytes.NewReader(encoded)) + if err != nil { + t.Errorf("DecodeU64 error: %v", err) + return + } + if decoded != v { + t.Errorf("round trip: got %d, want %d", decoded, v) + } + }) + } +} + +func TestDecodeU32Overflow(t *testing.T) { + input := []byte{0xff, 0xff, 0xff, 0xff, 0x1f} + _, err := DecodeU32(bytes.NewReader(input)) + if err == nil { + t.Error("expected overflow error, got nil") + } +} + +func TestDecodeU64Overflow(t *testing.T) { + input := []byte{0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x7f} + _, err := DecodeU64(bytes.NewReader(input)) + if err == nil { + t.Error("expected overflow error, got nil") + } +} + +func TestDecodeU32UnexpectedEOF(t *testing.T) { + input := []byte{0x80} + _, err := DecodeU32(bytes.NewReader(input)) + if err == nil { + t.Error("expected error for truncated input, got nil") + } +} + +func TestDecodeU64UnexpectedEOF(t *testing.T) { + input := []byte{0x80} + _, err := DecodeU64(bytes.NewReader(input)) + if err == nil { + t.Error("expected error for truncated input, got nil") + } +} diff --git a/packages/sdk-go/crypto/bmt.go b/packages/sdk-go/crypto/bmt.go new file mode 100644 index 0000000..3331e66 --- /dev/null +++ b/packages/sdk-go/crypto/bmt.go @@ -0,0 +1,187 @@ +package crypto + +import ( + "context" + "errors" + + "github.com/lightsing/uts/packages/sdk-go/logging" +) + +const InnerNodePrefix = 0x01 + +type NodePosition int + +const ( + PositionLeft NodePosition = iota + PositionRight +) + +type ProofStep struct { + Position NodePosition + Sibling [HashSize]byte +} + +type MerkleTree struct { + nodes [][HashSize]byte + len int +} + +func NewMerkleTree(leaves [][HashSize]byte) (*MerkleTree, error) { + logger := logging.Default() + logger.Trace(context.Background(), "NewMerkleTree: creating", "leaves", len(leaves)) + if len(leaves) == 0 { + return nil, errors.New("cannot create Merkle tree with zero leaves") + } + + rawLen := len(leaves) + treeLen := nextPowerOfTwo(rawLen) + nodes := make([][HashSize]byte, 2*treeLen) + + for i := 0; i < rawLen; i++ { + nodes[treeLen+i] = leaves[i] + } + + for i := treeLen - 1; i >= 1; i-- { + left := nodes[2*i] + right := nodes[2*i+1] + nodes[i] = hashInnerNode(left, right) + } + + logger.Trace(context.Background(), "NewMerkleTree: created", "tree_len", treeLen) + return &MerkleTree{ + nodes: nodes, + len: treeLen, + }, nil +} + +func hashInnerNode(left, right [HashSize]byte) [HashSize]byte { + data := make([]byte, 0, 1+HashSize*2) + data = append(data, InnerNodePrefix) + data = append(data, left[:]...) + data = append(data, right[:]...) + return SHA256(data) +} + +func (t *MerkleTree) Root() [HashSize]byte { + return t.nodes[1] +} + +func (t *MerkleTree) Leaves() [][HashSize]byte { + start := t.len + end := t.len + t.len + return t.nodes[start:end] +} + +func (t *MerkleTree) Contains(leaf [HashSize]byte) bool { + for _, l := range t.Leaves() { + if l == leaf { + return true + } + } + return false +} + +func (t *MerkleTree) GetProof(leaf [HashSize]byte) ([]ProofStep, error) { + logger := logging.Default() + logger.Trace(context.Background(), "MerkleTree: GetProof") + leaves := t.Leaves() + leafIndex := -1 + for i, l := range leaves { + if l == leaf { + leafIndex = i + break + } + } + if leafIndex == -1 { + return nil, errors.New("leaf not found in tree") + } + + current := t.len + leafIndex + var proof []ProofStep + + for current > 1 { + var position NodePosition + var siblingIndex int + + if current%2 == 0 { + position = PositionLeft + siblingIndex = current + 1 + } else { + position = PositionRight + siblingIndex = current - 1 + } + + proof = append(proof, ProofStep{ + Position: position, + Sibling: t.nodes[siblingIndex], + }) + + current = current / 2 + } + + logger.Trace(context.Background(), "MerkleTree: GetProof complete", "steps", len(proof)) + return proof, nil +} + +func VerifyProof(leaf [HashSize]byte, proof []ProofStep, root [HashSize]byte) bool { + logger := logging.Default() + logger.Trace(context.Background(), "VerifyProof", "steps", len(proof)) + current := leaf + + for _, step := range proof { + var left, right [HashSize]byte + switch step.Position { + case PositionLeft: + left = current + right = step.Sibling + case PositionRight: + left = step.Sibling + right = current + } + current = hashInnerNode(left, right) + } + + result := current == root + logger.Trace(context.Background(), "VerifyProof: complete", "valid", result) + return result +} + +func nextPowerOfTwo(n int) int { + if n <= 0 { + return 1 + } + n-- + n |= n >> 1 + n |= n >> 2 + n |= n >> 4 + n |= n >> 8 + n |= n >> 16 + n |= n >> 32 + return n + 1 +} + +func (t *MerkleTree) AsRawBytes() []byte { + result := make([]byte, len(t.nodes)*HashSize) + for i, node := range t.nodes { + copy(result[i*HashSize:], node[:]) + } + return result +} + +func MerkleTreeFromRawBytes(data []byte) *MerkleTree { + if len(data)%HashSize != 0 { + panic("data length must be a multiple of hash size") + } + numNodes := len(data) / HashSize + if numNodes%2 != 0 { + panic("number of nodes must be even") + } + nodes := make([][HashSize]byte, numNodes) + for i := 0; i < numNodes; i++ { + copy(nodes[i][:], data[i*HashSize:(i+1)*HashSize]) + } + return &MerkleTree{ + nodes: nodes, + len: numNodes / 2, + } +} diff --git a/packages/sdk-go/crypto/bmt_test.go b/packages/sdk-go/crypto/bmt_test.go new file mode 100644 index 0000000..9a87ede --- /dev/null +++ b/packages/sdk-go/crypto/bmt_test.go @@ -0,0 +1,375 @@ +package crypto + +import ( + "testing" +) + +func TestNewMerkleTree(t *testing.T) { + leaves := [][HashSize]byte{ + SHA256([]byte("leaf1")), + SHA256([]byte("leaf2")), + SHA256([]byte("leaf3")), + SHA256([]byte("leaf4")), + } + + tree, err := NewMerkleTree(leaves) + if err != nil { + t.Fatalf("failed to create Merkle tree: %v", err) + } + + if tree.len != 4 { + t.Errorf("expected len 4, got %d", tree.len) + } + + if len(tree.nodes) != 8 { + t.Errorf("expected 8 nodes, got %d", len(tree.nodes)) + } + + leavesSlice := tree.Leaves() + if len(leavesSlice) != 4 { + t.Errorf("expected 4 leaves, got %d", len(leavesSlice)) + } +} + +func TestNewMerkleTreeNonPowerOfTwo(t *testing.T) { + leaves := [][HashSize]byte{ + SHA256([]byte("leaf1")), + SHA256([]byte("leaf2")), + SHA256([]byte("leaf3")), + } + + tree, err := NewMerkleTree(leaves) + if err != nil { + t.Fatalf("failed to create Merkle tree: %v", err) + } + + if tree.len != 4 { + t.Errorf("expected len 4 (next power of two), got %d", tree.len) + } + + if len(tree.nodes) != 8 { + t.Errorf("expected 8 nodes, got %d", len(tree.nodes)) + } +} + +func TestMerkleTreeRoot(t *testing.T) { + leaves := [][HashSize]byte{ + SHA256([]byte("leaf1")), + SHA256([]byte("leaf2")), + SHA256([]byte("leaf3")), + SHA256([]byte("leaf4")), + } + + tree, err := NewMerkleTree(leaves) + if err != nil { + t.Fatalf("failed to create Merkle tree: %v", err) + } + + leftHash := hashInnerNode(leaves[0], leaves[1]) + rightHash := hashInnerNode(leaves[2], leaves[3]) + expectedRoot := hashInnerNode(leftHash, rightHash) + + root := tree.Root() + if root != expectedRoot { + t.Errorf("root mismatch: expected %x, got %x", expectedRoot, root) + } +} + +func TestMerkleTreeContains(t *testing.T) { + leaves := [][HashSize]byte{ + SHA256([]byte("leaf1")), + SHA256([]byte("leaf2")), + SHA256([]byte("leaf3")), + SHA256([]byte("leaf4")), + } + + tree, err := NewMerkleTree(leaves) + if err != nil { + t.Fatalf("failed to create Merkle tree: %v", err) + } + + if !tree.Contains(leaves[0]) { + t.Error("expected tree to contain leaves[0]") + } + + if !tree.Contains(leaves[3]) { + t.Error("expected tree to contain leaves[3]") + } + + notInTree := SHA256([]byte("not_in_tree")) + if tree.Contains(notInTree) { + t.Error("expected tree to not contain notInTree") + } +} + +func TestMerkleTreeGetProof(t *testing.T) { + leaves := [][HashSize]byte{ + SHA256([]byte("apple")), + SHA256([]byte("banana")), + SHA256([]byte("cherry")), + SHA256([]byte("date")), + } + + tree, err := NewMerkleTree(leaves) + if err != nil { + t.Fatalf("failed to create Merkle tree: %v", err) + } + + for _, leaf := range leaves { + proof, err := tree.GetProof(leaf) + if err != nil { + t.Errorf("failed to get proof: %v", err) + continue + } + + if !VerifyProof(leaf, proof, tree.Root()) { + t.Errorf("proof verification failed for leaf") + } + } +} + +func TestMerkleTreeGetProofNotFound(t *testing.T) { + leaves := [][HashSize]byte{ + SHA256([]byte("leaf1")), + SHA256([]byte("leaf2")), + } + + tree, err := NewMerkleTree(leaves) + if err != nil { + t.Fatalf("failed to create Merkle tree: %v", err) + } + + notInTree := SHA256([]byte("not_in_tree")) + _, err = tree.GetProof(notInTree) + if err == nil { + t.Error("expected error for leaf not in tree") + } +} + +func TestVerifyProof(t *testing.T) { + leaves := [][HashSize]byte{ + SHA256([]byte("apple")), + SHA256([]byte("banana")), + SHA256([]byte("cherry")), + SHA256([]byte("date")), + } + + tree, err := NewMerkleTree(leaves) + if err != nil { + t.Fatalf("failed to create Merkle tree: %v", err) + } + + for _, leaf := range leaves { + proof, _ := tree.GetProof(leaf) + if !VerifyProof(leaf, proof, tree.Root()) { + t.Errorf("proof verification failed for leaf") + } + } + + wrongRoot := SHA256([]byte("wrong_root")) + for _, leaf := range leaves { + proof, _ := tree.GetProof(leaf) + if VerifyProof(leaf, proof, wrongRoot) { + t.Error("proof should not verify against wrong root") + } + } +} + +func TestProofStructure(t *testing.T) { + leaves := [][HashSize]byte{ + SHA256([]byte("leaf0")), + SHA256([]byte("leaf1")), + SHA256([]byte("leaf2")), + SHA256([]byte("leaf3")), + } + + tree, err := NewMerkleTree(leaves) + if err != nil { + t.Fatalf("failed to create Merkle tree: %v", err) + } + + proof, _ := tree.GetProof(leaves[0]) + if len(proof) != 2 { + t.Errorf("expected 2 proof steps for 4 leaves, got %d", len(proof)) + } + + proof, _ = tree.GetProof(leaves[3]) + if len(proof) != 2 { + t.Errorf("expected 2 proof steps for 4 leaves, got %d", len(proof)) + } +} + +func TestSingleNodeTree(t *testing.T) { + leaves := [][HashSize]byte{ + SHA256([]byte("single")), + } + + tree, err := NewMerkleTree(leaves) + if err != nil { + t.Fatalf("failed to create Merkle tree: %v", err) + } + + if tree.len != 1 { + t.Errorf("expected len 1, got %d", tree.len) + } + + if tree.Root() != leaves[0] { + t.Error("root should equal the single leaf") + } + + proof, err := tree.GetProof(leaves[0]) + if err != nil { + t.Errorf("failed to get proof: %v", err) + } + + if len(proof) != 0 { + t.Errorf("expected 0 proof steps for single node, got %d", len(proof)) + } + + if !VerifyProof(leaves[0], proof, tree.Root()) { + t.Error("proof verification failed for single node") + } +} + +func TestNextPowerOfTwo(t *testing.T) { + tests := []struct { + input int + expected int + }{ + {1, 1}, + {2, 2}, + {3, 4}, + {4, 4}, + {5, 8}, + {7, 8}, + {8, 8}, + {9, 16}, + {15, 16}, + {16, 16}, + {17, 32}, + {1023, 1024}, + {1024, 1024}, + } + + for _, tt := range tests { + result := nextPowerOfTwo(tt.input) + if result != tt.expected { + t.Errorf("nextPowerOfTwo(%d) = %d, expected %d", tt.input, result, tt.expected) + } + } +} + +func TestAsRawBytes(t *testing.T) { + leaves := [][HashSize]byte{ + SHA256([]byte("leaf1")), + SHA256([]byte("leaf2")), + SHA256([]byte("leaf3")), + SHA256([]byte("leaf4")), + } + + tree, err := NewMerkleTree(leaves) + if err != nil { + t.Fatalf("failed to create Merkle tree: %v", err) + } + raw := tree.AsRawBytes() + + expectedLen := len(tree.nodes) * HashSize + if len(raw) != expectedLen { + t.Errorf("expected raw bytes length %d, got %d", expectedLen, len(raw)) + } +} + +func TestMerkleTreeFromRawBytes(t *testing.T) { + leaves := [][HashSize]byte{ + SHA256([]byte("leaf1")), + SHA256([]byte("leaf2")), + SHA256([]byte("leaf3")), + SHA256([]byte("leaf4")), + } + + original, err := NewMerkleTree(leaves) + if err != nil { + t.Fatalf("failed to create Merkle tree: %v", err) + } + raw := original.AsRawBytes() + + reconstructed := MerkleTreeFromRawBytes(raw) + + if reconstructed.Root() != original.Root() { + t.Error("reconstructed tree has different root") + } + + if reconstructed.len != original.len { + t.Errorf("reconstructed tree has different len: %d vs %d", reconstructed.len, original.len) + } + + for i, leaf := range leaves { + reconstructedProof, _ := reconstructed.GetProof(leaf) + + if !VerifyProof(leaf, reconstructedProof, reconstructed.Root()) { + t.Errorf("proof verification failed for leaf %d in reconstructed tree", i) + } + } +} + +func TestLargeTree(t *testing.T) { + numLeaves := 1024 + leaves := make([][HashSize]byte, numLeaves) + for i := 0; i < numLeaves; i++ { + leaves[i] = SHA256([]byte{byte(i >> 8), byte(i)}) + } + + tree, err := NewMerkleTree(leaves) + if err != nil { + t.Fatalf("failed to create Merkle tree: %v", err) + } + + if tree.len != 1024 { + t.Errorf("expected len 1024, got %d", tree.len) + } + + for i := 0; i < numLeaves; i++ { + proof, err := tree.GetProof(leaves[i]) + if err != nil { + t.Errorf("failed to get proof for leaf %d: %v", i, err) + continue + } + if !VerifyProof(leaves[i], proof, tree.Root()) { + t.Errorf("proof verification failed for leaf %d", i) + } + } +} + +func TestNonPowerOfTwoPadding(t *testing.T) { + leaves := [][HashSize]byte{ + SHA256([]byte("leaf1")), + SHA256([]byte("leaf2")), + SHA256([]byte("leaf3")), + } + + tree, err := NewMerkleTree(leaves) + if err != nil { + t.Fatalf("failed to create Merkle tree: %v", err) + } + + if tree.len != 4 { + t.Errorf("expected len 4 (padded), got %d", tree.len) + } + + treeLeaves := tree.Leaves() + if len(treeLeaves) != 4 { + t.Errorf("expected 4 leaves (padded), got %d", len(treeLeaves)) + } + + var zeroHash [HashSize]byte + if treeLeaves[3] != zeroHash { + t.Error("expected padding leaf to be zero hash") + } + + for _, leaf := range leaves { + proof, _ := tree.GetProof(leaf) + if !VerifyProof(leaf, proof, tree.Root()) { + t.Error("proof verification failed with non-power-of-two tree") + } + } +} diff --git a/packages/sdk-go/crypto/doc.go b/packages/sdk-go/crypto/doc.go new file mode 100644 index 0000000..4b6a8f1 --- /dev/null +++ b/packages/sdk-go/crypto/doc.go @@ -0,0 +1,12 @@ +// Package crypto provides cryptographic hash functions for the UTS protocol. +// +// This package wraps standard and Ethereum-compatible hash functions used +// throughout the UTS protocol for: +// - Timestamp operations (SHA256, Keccak256 steps) +// - Merkle tree construction +// - Digest computation +// +// Supported hash functions: +// - SHA256: Standard SHA-256 hash function (32 bytes output) +// - Keccak256: Ethereum's Keccak-256 hash function (32 bytes output) +package crypto diff --git a/packages/sdk-go/crypto/hash.go b/packages/sdk-go/crypto/hash.go new file mode 100644 index 0000000..9f81108 --- /dev/null +++ b/packages/sdk-go/crypto/hash.go @@ -0,0 +1,35 @@ +package crypto + +import ( + "crypto/sha256" + + "github.com/ethereum/go-ethereum/crypto" +) + +const ( + HashSize = 32 +) + +func SHA256(data []byte) [HashSize]byte { + var hash [HashSize]byte + sum := sha256.Sum256(data) + copy(hash[:], sum[:]) + return hash +} + +func Keccak256(data []byte) [HashSize]byte { + var hash [HashSize]byte + sum := crypto.Keccak256(data) + copy(hash[:], sum) + return hash +} + +func SHA256Hash(data []byte) []byte { + hash := SHA256(data) + return hash[:] +} + +func Keccak256Hash(data []byte) []byte { + hash := Keccak256(data) + return hash[:] +} diff --git a/packages/sdk-go/crypto/hash_test.go b/packages/sdk-go/crypto/hash_test.go new file mode 100644 index 0000000..b6926f3 --- /dev/null +++ b/packages/sdk-go/crypto/hash_test.go @@ -0,0 +1,155 @@ +package crypto + +import ( + "bytes" + "encoding/hex" + "testing" +) + +func TestSHA256(t *testing.T) { + tests := []struct { + name string + input string + expected string + }{ + { + name: "empty string", + input: "", + expected: "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + }, + { + name: "hello world", + input: "hello world", + expected: "b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9", + }, + { + name: "abc", + input: "abc", + expected: "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad", + }, + { + name: "long input", + input: "The quick brown fox jumps over the lazy dog", + expected: "d7a8fbb307d7809469ca9abcb0082e4f8d5651e46d3cdb762d02d0bf37c9e592", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + input := []byte(tt.input) + hash := SHA256(input) + + if len(hash) != HashSize { + t.Errorf("SHA256: expected hash length %d, got %d", HashSize, len(hash)) + } + + expectedBytes, err := hex.DecodeString(tt.expected) + if err != nil { + t.Fatalf("failed to decode expected hex: %v", err) + } + + if !bytes.Equal(hash[:], expectedBytes) { + t.Errorf("SHA256: expected %x, got %x", expectedBytes, hash) + } + }) + } +} + +func TestKeccak256(t *testing.T) { + tests := []struct { + name string + input string + expected string + }{ + { + name: "empty string", + input: "", + expected: "c5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470", + }, + { + name: "hello world", + input: "hello world", + expected: "47173285a8d7341e5e972fc677286384f802f8ef42a5ec5f03bbfa254cb01fad", + }, + { + name: "abc", + input: "abc", + expected: "4e03657aea45a94fc7d47ba826c8d667c0d1e6e33a64a036ec44f58fa12d6c45", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + input := []byte(tt.input) + hash := Keccak256(input) + + if len(hash) != HashSize { + t.Errorf("Keccak256: expected hash length %d, got %d", HashSize, len(hash)) + } + + expectedBytes, err := hex.DecodeString(tt.expected) + if err != nil { + t.Fatalf("failed to decode expected hex: %v", err) + } + + if !bytes.Equal(hash[:], expectedBytes) { + t.Errorf("Keccak256: expected %x, got %x", expectedBytes, hash) + } + }) + } +} + +func TestSHA256Hash(t *testing.T) { + input := []byte("test") + hash := SHA256Hash(input) + + if len(hash) != HashSize { + t.Errorf("SHA256Hash: expected length %d, got %d", HashSize, len(hash)) + } + + expected := SHA256(input) + if !bytes.Equal(hash, expected[:]) { + t.Errorf("SHA256Hash: result doesn't match SHA256") + } +} + +func TestKeccak256Hash(t *testing.T) { + input := []byte("test") + hash := Keccak256Hash(input) + + if len(hash) != HashSize { + t.Errorf("Keccak256Hash: expected length %d, got %d", HashSize, len(hash)) + } + + expected := Keccak256(input) + if !bytes.Equal(hash, expected[:]) { + t.Errorf("Keccak256Hash: result doesn't match Keccak256") + } +} + +func TestHashConsistency(t *testing.T) { + data := []byte("consistency test") + + hash1 := SHA256(data) + hash2 := SHA256(data) + if !bytes.Equal(hash1[:], hash2[:]) { + t.Error("SHA256: same input should produce same hash") + } + + khash1 := Keccak256(data) + khash2 := Keccak256(data) + if !bytes.Equal(khash1[:], khash2[:]) { + t.Error("Keccak256: same input should produce same hash") + } +} + +func TestDifferentHashes(t *testing.T) { + data := []byte("test data") + + shaHash := SHA256(data) + keccakHash := Keccak256(data) + + if bytes.Equal(shaHash[:], keccakHash[:]) { + t.Error("SHA256 and Keccak256 should produce different results") + } +} diff --git a/packages/sdk-go/doc.go b/packages/sdk-go/doc.go new file mode 100644 index 0000000..56c7d1a --- /dev/null +++ b/packages/sdk-go/doc.go @@ -0,0 +1,14 @@ +// Package uts provides a Go SDK for the Universal Timestamps (UTS) protocol. +// +// UTS is a superset of OpenTimestamps that batches user-submitted digests into +// Merkle trees and anchors the roots on-chain via EAS attestations, providing +// trustless, verifiable timestamps without relying on a single trusted calendar server. +// +// The SDK provides client-side interaction with the UTS protocol, including: +// - Timestamp creation and verification +// - EAS attestation handling +// - Binary Merkle Tree operations +// - Calendar server communication +// +// For more information, see https://book.timestamps.now/ +package uts diff --git a/packages/sdk-go/errors.go b/packages/sdk-go/errors.go new file mode 100644 index 0000000..8f1214b --- /dev/null +++ b/packages/sdk-go/errors.go @@ -0,0 +1,73 @@ +package uts + +import ( + "github.com/lightsing/uts/packages/sdk-go/errors" +) + +type ErrorCode = errors.ErrorCode + +const ( + ErrCodeBadMagic = errors.ErrCodeBadMagic + ErrCodeBadVersion = errors.ErrCodeBadVersion + ErrCodeBadAttestationTag = errors.ErrCodeBadAttestationTag + ErrCodeLEB128Overflow = errors.ErrCodeLEB128Overflow + ErrCodeBadOpCode = errors.ErrCodeBadOpCode + ErrCodeExpectedDigestOp = errors.ErrCodeExpectedDigestOp + ErrCodeOutOfRange = errors.ErrCodeOutOfRange + ErrCodeInvalidUriChar = errors.ErrCodeInvalidUriChar + ErrCodeUriTooLong = errors.ErrCodeUriTooLong + ErrCodeRecursionLimit = errors.ErrCodeRecursionLimit + ErrCodeUnexpectedEof = errors.ErrCodeUnexpectedEof + ErrCodeUsizeOverflow = errors.ErrCodeUsizeOverflow + ErrCodeNoValue = errors.ErrCodeNoValue + ErrCodePending = errors.ErrCodePending + ErrCodeInvalidLength = errors.ErrCodeInvalidLength + ErrCodeInvalidData = errors.ErrCodeInvalidData + ErrCodeInvalidSchema = errors.ErrCodeInvalidSchema + ErrCodeRevocableAttest = errors.ErrCodeRevocableAttest + ErrCodeMismatched = errors.ErrCodeMismatched + ErrCodeNotFound = errors.ErrCodeNotFound + ErrCodeRpc = errors.ErrCodeRpc + ErrCodeRemote = errors.ErrCodeRemote + ErrCodeDecode = errors.ErrCodeDecode +) + +type SDKError = errors.SDKError +type DecodeError = errors.DecodeError +type EncodeError = errors.EncodeError +type VerifyError = errors.VerifyError +type EASVerifierError = errors.EASVerifierError +type RemoteError = errors.RemoteError + +var ( + NewDecodeError = errors.NewDecodeError + ErrBadMagic = errors.ErrBadMagic + ErrBadVersion = errors.ErrBadVersion + ErrBadAttestationTag = errors.ErrBadAttestationTag + ErrLEB128Overflow = errors.ErrLEB128Overflow + ErrBadOpCode = errors.ErrBadOpCode + ErrExpectedDigestOp = errors.ErrExpectedDigestOp + ErrOutOfRange = errors.ErrOutOfRange + ErrInvalidUriChar = errors.ErrInvalidUriChar + ErrUriTooLong = errors.ErrUriTooLong + ErrRecursionLimit = errors.ErrRecursionLimit + ErrUnexpectedEof = errors.ErrUnexpectedEof + NewEncodeError = errors.NewEncodeError + ErrUsizeOverflow = errors.ErrUsizeOverflow + ErrEncodeInvalidUriChar = errors.ErrEncodeInvalidUriChar + ErrEncodeUriTooLong = errors.ErrEncodeUriTooLong + NewVerifyError = errors.NewVerifyError + ErrNoValue = errors.ErrNoValue + ErrPending = errors.ErrPending + ErrVerifyBadAttestationTag = errors.ErrVerifyBadAttestationTag + ErrVerifyDecode = errors.ErrVerifyDecode + NewEASVerifierError = errors.NewEASVerifierError + ErrInvalidLength = errors.ErrInvalidLength + ErrInvalidData = errors.ErrInvalidData + ErrInvalidSchema = errors.ErrInvalidSchema + ErrRevocableAttestation = errors.ErrRevocableAttestation + ErrMismatched = errors.ErrMismatched + ErrNotFound = errors.ErrNotFound + ErrRpc = errors.ErrRpc + NewRemoteError = errors.NewRemoteError +) diff --git a/packages/sdk-go/errors/errors.go b/packages/sdk-go/errors/errors.go new file mode 100644 index 0000000..c634372 --- /dev/null +++ b/packages/sdk-go/errors/errors.go @@ -0,0 +1,229 @@ +package errors + +import "fmt" + +type ErrorCode string + +const ( + ErrCodeBadMagic ErrorCode = "BadMagic" + ErrCodeBadVersion ErrorCode = "BadVersion" + ErrCodeBadAttestationTag ErrorCode = "BadAttestationTag" + ErrCodeLEB128Overflow ErrorCode = "LEB128Overflow" + ErrCodeBadOpCode ErrorCode = "BadOpCode" + ErrCodeExpectedDigestOp ErrorCode = "ExpectedDigestOp" + ErrCodeOutOfRange ErrorCode = "OutOfRange" + ErrCodeInvalidUriChar ErrorCode = "InvalidUriChar" + ErrCodeUriTooLong ErrorCode = "UriTooLong" + ErrCodeRecursionLimit ErrorCode = "RecursionLimit" + ErrCodeUnexpectedEof ErrorCode = "UnexpectedEof" + ErrCodeUsizeOverflow ErrorCode = "UsizeOverflow" + ErrCodeNoValue ErrorCode = "NoValue" + ErrCodePending ErrorCode = "Pending" + ErrCodeInvalidLength ErrorCode = "InvalidLength" + ErrCodeInvalidData ErrorCode = "InvalidData" + ErrCodeInvalidSchema ErrorCode = "InvalidSchema" + ErrCodeRevocableAttest ErrorCode = "RevocableAttestation" + ErrCodeMismatched ErrorCode = "Mismatched" + ErrCodeNotFound ErrorCode = "NotFound" + ErrCodeRpc ErrorCode = "Rpc" + ErrCodeRemote ErrorCode = "Remote" + ErrCodeDecode ErrorCode = "Decode" + ErrCodeEmptyRequests ErrorCode = "EmptyRequests" + ErrCodeGeneric ErrorCode = "Generic" + ErrCodeUnsupported ErrorCode = "Unsupported" +) + +type SDKError struct { + Code ErrorCode + Message string + Context map[string]interface{} +} + +func (e *SDKError) Error() string { + if e.Context != nil { + return fmt.Sprintf("%s: %s (%v)", e.Code, e.Message, e.Context) + } + return fmt.Sprintf("%s: %s", e.Code, e.Message) +} + +func NewSDKError(code ErrorCode, msg string, ctx map[string]interface{}) *SDKError { + return &SDKError{Code: code, Message: msg, Context: ctx} +} + +type DecodeError struct { + *SDKError +} + +func NewDecodeError(code ErrorCode, msg string, ctx map[string]interface{}) *DecodeError { + return &DecodeError{SDKError: NewSDKError(code, msg, ctx)} +} + +func ErrBadMagic() *DecodeError { + return &DecodeError{SDKError: NewSDKError(ErrCodeBadMagic, "bad magic bytes", nil)} +} + +func ErrBadVersion() *DecodeError { + return &DecodeError{SDKError: NewSDKError(ErrCodeBadVersion, "bad version", nil)} +} + +func ErrBadAttestationTag() *DecodeError { + return &DecodeError{SDKError: NewSDKError(ErrCodeBadAttestationTag, "bad attestation tag", nil)} +} + +func ErrLEB128Overflow(bits uint32) *DecodeError { + return &DecodeError{SDKError: NewSDKError(ErrCodeLEB128Overflow, + fmt.Sprintf("read a LEB128 value overflows %d bits", bits), + map[string]interface{}{"bits": bits})} +} + +func ErrBadOpCode(code byte) *DecodeError { + return &DecodeError{SDKError: NewSDKError(ErrCodeBadOpCode, + fmt.Sprintf("unrecognized opcode: 0x%02x", code), + map[string]interface{}{"code": code})} +} + +func ErrExpectedDigestOp(op string) *DecodeError { + return &DecodeError{SDKError: NewSDKError(ErrCodeExpectedDigestOp, + fmt.Sprintf("expected digest opcode but got: %s", op), + map[string]interface{}{"op": op})} +} + +func ErrOutOfRange() *DecodeError { + return &DecodeError{SDKError: NewSDKError(ErrCodeOutOfRange, "read value out of range", nil)} +} + +func ErrInvalidUriChar() *DecodeError { + return &DecodeError{SDKError: NewSDKError(ErrCodeInvalidUriChar, "invalid character in URI", nil)} +} + +func ErrUriTooLong() *DecodeError { + return &DecodeError{SDKError: NewSDKError(ErrCodeUriTooLong, "URI too long", nil)} +} + +func ErrRecursionLimit() *DecodeError { + return &DecodeError{SDKError: NewSDKError(ErrCodeRecursionLimit, "recursion limit reached", nil)} +} + +func ErrUnexpectedEof() *DecodeError { + return &DecodeError{SDKError: NewSDKError(ErrCodeUnexpectedEof, "unexpected end of file", nil)} +} + +type EncodeError struct { + *SDKError +} + +func NewEncodeError(code ErrorCode, msg string, ctx map[string]interface{}) *EncodeError { + return &EncodeError{SDKError: NewSDKError(code, msg, ctx)} +} + +func ErrUsizeOverflow() *EncodeError { + return &EncodeError{SDKError: NewSDKError(ErrCodeUsizeOverflow, + "tried to encode a usize exceeding u32::MAX", nil)} +} + +func ErrEncodeInvalidUriChar() *EncodeError { + return &EncodeError{SDKError: NewSDKError(ErrCodeInvalidUriChar, "invalid character in URI", nil)} +} + +func ErrEncodeUriTooLong() *EncodeError { + return &EncodeError{SDKError: NewSDKError(ErrCodeUriTooLong, "URI too long", nil)} +} + +type VerifyError struct { + *SDKError + Inner error +} + +func NewVerifyError(code ErrorCode, msg string, inner error) *VerifyError { + return &VerifyError{SDKError: NewSDKError(code, msg, nil), Inner: inner} +} + +func ErrNoValue() *VerifyError { + return &VerifyError{SDKError: NewSDKError(ErrCodeNoValue, "raw attestation lacks a value", nil)} +} + +func ErrPending() *VerifyError { + return &VerifyError{SDKError: NewSDKError(ErrCodePending, + "attestation is still pending and cannot be verified yet", nil)} +} + +func ErrVerifyBadAttestationTag() *VerifyError { + return &VerifyError{SDKError: NewSDKError(ErrCodeBadAttestationTag, + "attestation is not the expected type", nil)} +} + +func ErrVerifyDecode(inner *DecodeError) *VerifyError { + return &VerifyError{ + SDKError: NewSDKError(ErrCodeDecode, + fmt.Sprintf("error decoding attestation: %s", inner.Message), nil), + Inner: inner, + } +} + +type EASVerifierError struct { + *SDKError + Inner error +} + +func NewEASVerifierError(code ErrorCode, msg string, inner error) *EASVerifierError { + return &EASVerifierError{SDKError: NewSDKError(code, msg, nil), Inner: inner} +} + +func ErrInvalidLength() *EASVerifierError { + return &EASVerifierError{SDKError: NewSDKError(ErrCodeInvalidLength, + "invalid value length for EAS attestation", nil)} +} + +func ErrInvalidData(inner error) *EASVerifierError { + return &EASVerifierError{ + SDKError: NewSDKError(ErrCodeInvalidData, "invalid attestation data", nil), + Inner: inner, + } +} + +func ErrInvalidSchema() *EASVerifierError { + return &EASVerifierError{SDKError: NewSDKError(ErrCodeInvalidSchema, + "unexpected schema used for attestation", nil)} +} + +func ErrRevocableAttestation() *EASVerifierError { + return &EASVerifierError{SDKError: NewSDKError(ErrCodeRevocableAttest, + "attestation cannot be revocable", nil)} +} + +func ErrMismatched(expected, actual [32]byte) *EASVerifierError { + return &EASVerifierError{SDKError: NewSDKError(ErrCodeMismatched, + "attested hash is not equal to the expected hash", + map[string]interface{}{"expected": expected, "actual": actual})} +} + +func ErrNotFound() *EASVerifierError { + return &EASVerifierError{SDKError: NewSDKError(ErrCodeNotFound, "not found", nil)} +} + +func ErrRpc(inner error) *EASVerifierError { + return &EASVerifierError{ + SDKError: NewSDKError(ErrCodeRpc, "RPC error", nil), + Inner: inner, + } +} + +func (e *EASVerifierError) IsFatal() bool { + return e.Code != ErrCodeRpc +} + +func (e *EASVerifierError) ShouldRetry() bool { + return e.Code == ErrCodeRpc +} + +type RemoteError struct { + *SDKError + Inner error +} + +func NewRemoteError(msg string, inner error) *RemoteError { + return &RemoteError{ + SDKError: NewSDKError(ErrCodeRemote, msg, nil), + Inner: inner, + } +} diff --git a/packages/sdk-go/examples/stamp/main.go b/packages/sdk-go/examples/stamp/main.go new file mode 100644 index 0000000..819b752 --- /dev/null +++ b/packages/sdk-go/examples/stamp/main.go @@ -0,0 +1,100 @@ +package main + +import ( + "context" + "encoding/hex" + "fmt" + "log" + "os" + "time" + + uts "github.com/lightsing/uts/packages/sdk-go" + "github.com/lightsing/uts/packages/sdk-go/codec" + "github.com/lightsing/uts/packages/sdk-go/crypto" + "github.com/lightsing/uts/packages/sdk-go/types" +) + +func main() { + if len(os.Args) < 2 { + fmt.Println("Usage: stamp [output.ots]") + fmt.Println(" stamp --sample [output.ots]") + os.Exit(1) + } + + var data []byte + var outputFile string + var useSample bool + + if os.Args[1] == "--sample" { + useSample = true + data = []byte("Hello, Universal Timestamps!") + if len(os.Args) > 2 { + outputFile = os.Args[2] + } else { + outputFile = "sample.ots" + } + } else { + inputFile := os.Args[1] + if len(os.Args) > 2 { + outputFile = os.Args[2] + } else { + outputFile = inputFile + ".ots" + } + + var err error + data, err = os.ReadFile(inputFile) + if err != nil { + log.Fatalf("Failed to read file: %v", err) + } + } + + hash := crypto.Keccak256(data) + if useSample { + fmt.Printf("Sample data: %q\n", string(data)) + } else { + fmt.Printf("File: %s (%d bytes)\n", os.Args[1], len(data)) + } + fmt.Printf("Hash (keccak256): %s\n", hex.EncodeToString(hash[:])) + + sdk, err := uts.NewSDK( + uts.WithCalendars("https://lgm1.calendar.test.timestamps.now/"), + uts.WithTimeout(30*time.Second), + ) + if err != nil { + log.Fatalf("Failed to create SDK: %v", err) + } + + header, err := types.NewDigestHeader(types.DigestKECCAK256, hash[:]) + if err != nil { + log.Fatalf("Failed to create digest header: %v", err) + } + + ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second) + defer cancel() + + stamps, err := sdk.Stamp(ctx, []*types.DigestHeader{header}) + if err != nil { + log.Fatalf("Failed to stamp: %v", err) + } + + if len(stamps) == 0 { + log.Fatal("No stamps returned") + } + + stamp := stamps[0] + fmt.Printf("\nTimestamp created with %d steps\n", len(stamp.Timestamp)) + for i, step := range stamp.Timestamp { + fmt.Printf(" %d: %s\n", i+1, step) + } + + encoded, err := codec.EncodeDetachedTimestamp(stamp) + if err != nil { + log.Fatalf("Failed to encode timestamp: %v", err) + } + + if err := os.WriteFile(outputFile, encoded, 0644); err != nil { + log.Fatalf("Failed to write output file: %v", err) + } + + fmt.Printf("\nTimestamp saved to: %s (%d bytes)\n", outputFile, len(encoded)) +} diff --git a/packages/sdk-go/examples/upgrade/main.go b/packages/sdk-go/examples/upgrade/main.go new file mode 100644 index 0000000..019d4f3 --- /dev/null +++ b/packages/sdk-go/examples/upgrade/main.go @@ -0,0 +1,116 @@ +package main + +import ( + "context" + "fmt" + "log" + "os" + "time" + + uts "github.com/lightsing/uts/packages/sdk-go" + "github.com/lightsing/uts/packages/sdk-go/codec" + "github.com/lightsing/uts/packages/sdk-go/types" +) + +func main() { + if len(os.Args) < 2 { + fmt.Println("Usage: upgrade [output.ots]") + fmt.Println("\nUpgrades pending attestations in a timestamp by fetching") + fmt.Println("completed attestations from calendar servers.") + os.Exit(1) + } + + inputFile := os.Args[1] + var outputFile string + if len(os.Args) > 2 { + outputFile = os.Args[2] + } else { + outputFile = inputFile + } + + data, err := os.ReadFile(inputFile) + if err != nil { + log.Fatalf("Failed to read timestamp file: %v", err) + } + + stamp, err := codec.DecodeDetachedTimestamp(data) + if err != nil { + log.Fatalf("Failed to decode timestamp: %v", err) + } + + fmt.Printf("Loaded timestamp from: %s\n", inputFile) + fmt.Printf("Digest: %s\n", stamp.Header) + fmt.Printf("Steps: %d\n\n", len(stamp.Timestamp)) + + pendingCount := countPendingAttestations(stamp.Timestamp) + fmt.Printf("Pending attestations found: %d\n", pendingCount) + + if pendingCount == 0 { + fmt.Println("No pending attestations to upgrade.") + return + } + + sdk, err := uts.NewSDK( + uts.WithTimeout(30 * time.Second), + ) + if err != nil { + log.Fatalf("Failed to create SDK: %v", err) + } + + ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second) + defer cancel() + + results, err := sdk.Upgrade(ctx, stamp, true) + if err != nil { + log.Fatalf("Failed to upgrade: %v", err) + } + + fmt.Printf("\nUpgrade Results:\n") + upgraded := 0 + stillPending := 0 + failed := 0 + + for _, result := range results { + switch result.Status { + case types.UpgradeUpgraded: + upgraded++ + fmt.Printf(" - Upgraded successfully\n") + case types.UpgradePending: + stillPending++ + fmt.Printf(" - Still pending (not yet available)\n") + case types.UpgradeFailed: + failed++ + fmt.Printf(" - Failed: %v\n", result.Error) + } + } + + fmt.Printf("\nSummary: %d upgraded, %d pending, %d failed\n", upgraded, stillPending, failed) + + encoded, err := codec.EncodeDetachedTimestamp(stamp) + if err != nil { + log.Fatalf("Failed to encode timestamp: %v", err) + } + + if err := os.WriteFile(outputFile, encoded, 0644); err != nil { + log.Fatalf("Failed to write output file: %v", err) + } + + fmt.Printf("Updated timestamp saved to: %s\n", outputFile) +} + +func countPendingAttestations(ts types.Timestamp) int { + count := 0 + for _, step := range ts { + switch s := step.(type) { + case *types.AttestationStep: + if _, ok := s.Attestation.(*types.PendingAttestation); ok { + count++ + } + case *types.ForkStep: + for _, branch := range s.Branches { + count += countPendingAttestations(branch) + } + } + } + return count +} diff --git a/packages/sdk-go/examples/verify/main.go b/packages/sdk-go/examples/verify/main.go new file mode 100644 index 0000000..f8d023b --- /dev/null +++ b/packages/sdk-go/examples/verify/main.go @@ -0,0 +1,75 @@ +package main + +import ( + "context" + "fmt" + "log" + "os" + "time" + + uts "github.com/lightsing/uts/packages/sdk-go" + "github.com/lightsing/uts/packages/sdk-go/codec" + "github.com/lightsing/uts/packages/sdk-go/types" +) + +func main() { + if len(os.Args) < 2 { + fmt.Println("Usage: verify ") + os.Exit(1) + } + + inputFile := os.Args[1] + + data, err := os.ReadFile(inputFile) + if err != nil { + log.Fatalf("Failed to read timestamp file: %v", err) + } + + stamp, err := codec.DecodeDetachedTimestamp(data) + if err != nil { + log.Fatalf("Failed to decode timestamp: %v", err) + } + + fmt.Printf("Loaded timestamp from: %s\n", inputFile) + fmt.Printf("Digest: %s\n", stamp.Header) + fmt.Printf("Steps: %d\n", len(stamp.Timestamp)) + + sdk, err := uts.NewSDK( + uts.WithTimeout(30 * time.Second), + ) + if err != nil { + log.Fatalf("Failed to create SDK: %v", err) + } + + ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second) + defer cancel() + + result, err := sdk.Verify(ctx, stamp) + if err != nil { + log.Fatalf("Failed to verify: %v", err) + } + + fmt.Printf("\nVerification Result: %s\n", result.Status) + + if len(result.Attestations) > 0 { + fmt.Printf("\nAttestations (%d):\n", len(result.Attestations)) + for i, att := range result.Attestations { + fmt.Printf(" %d. %s\n", i+1, att) + } + } + + switch result.Status { + case types.VerifyValid: + fmt.Println("\nTimestamp is valid and confirmed on-chain.") + os.Exit(0) + case types.VerifyPartialValid: + fmt.Println("\nTimestamp has partial confirmation (some attestations valid).") + os.Exit(0) + case types.VerifyPending: + fmt.Println("\nTimestamp is pending confirmation.") + os.Exit(0) + case types.VerifyInvalid: + fmt.Println("\nTimestamp verification failed.") + os.Exit(1) + } +} diff --git a/packages/sdk-go/logging/logger.go b/packages/sdk-go/logging/logger.go new file mode 100644 index 0000000..1ab06df --- /dev/null +++ b/packages/sdk-go/logging/logger.go @@ -0,0 +1,164 @@ +package logging + +import ( + "context" + "io" + "log/slog" + "os" +) + +type Level int + +const ( + LevelTrace Level = -8 + LevelDebug Level = -4 + LevelInfo Level = 0 + LevelWarn Level = 4 + LevelError Level = 8 +) + +func (l Level) String() string { + switch l { + case LevelTrace: + return "TRACE" + case LevelDebug: + return "DEBUG" + case LevelInfo: + return "INFO" + case LevelWarn: + return "WARN" + case LevelError: + return "ERROR" + default: + return "UNKNOWN" + } +} + +func (l Level) slogLevel() slog.Level { + switch l { + case LevelTrace: + return slog.LevelDebug - 4 + case LevelDebug: + return slog.LevelDebug + case LevelInfo: + return slog.LevelInfo + case LevelWarn: + return slog.LevelWarn + case LevelError: + return slog.LevelError + default: + return slog.LevelInfo + } +} + +type Logger struct { + *slog.Logger + level Level +} + +func NewLogger(w io.Writer, level Level) *Logger { + opts := &slog.HandlerOptions{ + Level: level.slogLevel(), + ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr { + if a.Key == slog.LevelKey { + level := a.Value.Any().(slog.Level) + switch { + case level < slog.LevelDebug: + a.Value = slog.StringValue("TRACE") + } + } + return a + }, + } + handler := slog.NewTextHandler(w, opts) + logger := slog.New(handler) + return &Logger{Logger: logger, level: level} +} + +func NewJSONLogger(w io.Writer, level Level) *Logger { + opts := &slog.HandlerOptions{ + Level: level.slogLevel(), + } + handler := slog.NewJSONHandler(w, opts) + logger := slog.New(handler) + return &Logger{Logger: logger, level: level} +} + +func NewDefaultLogger(level Level) *Logger { + return NewLogger(os.Stderr, level) +} + +func (l *Logger) With(fields ...any) *Logger { + return &Logger{ + Logger: l.Logger.With(fields...), + level: l.level, + } +} + +func (l *Logger) Trace(ctx context.Context, msg string, args ...any) { + if l.level <= LevelTrace { + l.Log(ctx, LevelTrace.slogLevel(), msg, args...) + } +} + +func (l *Logger) Debug(ctx context.Context, msg string, args ...any) { + l.Logger.DebugContext(ctx, msg, args...) +} + +func (l *Logger) Info(ctx context.Context, msg string, args ...any) { + l.Logger.InfoContext(ctx, msg, args...) +} + +func (l *Logger) Warn(ctx context.Context, msg string, args ...any) { + l.Logger.WarnContext(ctx, msg, args...) +} + +func (l *Logger) Error(ctx context.Context, msg string, args ...any) { + l.Logger.ErrorContext(ctx, msg, args...) +} + +func (l *Logger) IsTraceEnabled() bool { + return l.level <= LevelTrace +} + +func (l *Logger) IsDebugEnabled() bool { + return l.level <= LevelDebug +} + +func SetDefault(l *Logger) { + slog.SetDefault(l.Logger) +} + +var defaultLogger = NewDefaultLogger(LevelInfo) + +func Default() *Logger { + return defaultLogger +} + +func SetLevel(level Level) { + defaultLogger = NewDefaultLogger(level) +} + +func Trace(ctx context.Context, msg string, args ...any) { + defaultLogger.Trace(ctx, msg, args...) +} + +func Debug(ctx context.Context, msg string, args ...any) { + defaultLogger.Debug(ctx, msg, args...) +} + +func Info(ctx context.Context, msg string, args ...any) { + defaultLogger.Info(ctx, msg, args...) +} + +func Warn(ctx context.Context, msg string, args ...any) { + defaultLogger.Warn(ctx, msg, args...) +} + +func Error(ctx context.Context, msg string, args ...any) { + defaultLogger.Error(ctx, msg, args...) +} + +func With(fields ...any) *Logger { + return defaultLogger.With(fields...) +} diff --git a/packages/sdk-go/rpc/bitcoin.go b/packages/sdk-go/rpc/bitcoin.go new file mode 100644 index 0000000..88b317a --- /dev/null +++ b/packages/sdk-go/rpc/bitcoin.go @@ -0,0 +1,151 @@ +package rpc + +import ( + "bytes" + "context" + "encoding/hex" + "encoding/json" + "fmt" + "io" + "net/http" + "time" + + "github.com/lightsing/uts/packages/sdk-go/logging" +) + +type BitcoinRPC struct { + url string + httpClient *http.Client + logger *logging.Logger +} + +type BlockHeader struct { + Hash string `json:"hash"` + MerkleRoot string `json:"merkleroot"` + Time int64 `json:"time"` + Height int64 `json:"height,omitempty"` + Confirmations int64 `json:"confirmations,omitempty"` +} + +type jsonRPCRequest struct { + Jsonrpc string `json:"jsonrpc"` + Method string `json:"method"` + Params []interface{} `json:"params"` + ID int `json:"id"` +} + +type jsonRPCResponse struct { + Jsonrpc string `json:"jsonrpc"` + Result json.RawMessage `json:"result"` + Error *jsonRPCError `json:"error"` + ID int `json:"id"` +} + +type jsonRPCError struct { + Code int `json:"code"` + Message string `json:"message"` +} + +func NewBitcoinRPC(url string) *BitcoinRPC { + return &BitcoinRPC{ + url: url, + httpClient: &http.Client{ + Timeout: 30 * time.Second, + }, + logger: logging.Default(), + } +} + +func (c *BitcoinRPC) SetLogger(logger *logging.Logger) { + c.logger = logger +} + +func (c *BitcoinRPC) GetBlockHash(height int64) (string, error) { + c.logger.Debug(context.Background(), "BitcoinRPC: GetBlockHash", "height", height) + var result string + if err := c.call("getblockhash", []interface{}{height}, &result); err != nil { + c.logger.Warn(context.Background(), "BitcoinRPC: GetBlockHash failed", "height", height, "error", err) + return "", fmt.Errorf("getblockhash: %w", err) + } + c.logger.Trace(context.Background(), "BitcoinRPC: GetBlockHash success", "height", height, "hash", result) + return result, nil +} + +func (c *BitcoinRPC) GetBlockHeader(hash string) (*BlockHeader, error) { + c.logger.Debug(context.Background(), "BitcoinRPC: GetBlockHeader", "hash", hash) + var result BlockHeader + if err := c.call("getblockheader", []interface{}{hash}, &result); err != nil { + c.logger.Warn(context.Background(), "BitcoinRPC: GetBlockHeader failed", "hash", hash, "error", err) + return nil, fmt.Errorf("getblockheader: %w", err) + } + c.logger.Trace(context.Background(), "BitcoinRPC: GetBlockHeader success", "hash", hash, "height", result.Height, "time", result.Time) + return &result, nil +} + +func (c *BitcoinRPC) call(method string, params []interface{}, result interface{}) error { + c.logger.Trace(context.Background(), "BitcoinRPC: calling", "method", method) + req := jsonRPCRequest{ + Jsonrpc: "2.0", + Method: method, + Params: params, + ID: 1, + } + + body, err := json.Marshal(req) + if err != nil { + return fmt.Errorf("marshal request: %w", err) + } + + httpReq, err := http.NewRequest("POST", c.url, bytes.NewReader(body)) + if err != nil { + return fmt.Errorf("create request: %w", err) + } + httpReq.Header.Set("Content-Type", "application/json") + + resp, err := c.httpClient.Do(httpReq) + if err != nil { + return fmt.Errorf("http request: %w", err) + } + defer resp.Body.Close() + + respBody, err := io.ReadAll(resp.Body) + if err != nil { + return fmt.Errorf("read response: %w", err) + } + + var rpcResp jsonRPCResponse + if err := json.Unmarshal(respBody, &rpcResp); err != nil { + return fmt.Errorf("unmarshal response: %w", err) + } + + if rpcResp.Error != nil { + c.logger.Warn(context.Background(), "BitcoinRPC: RPC error", "method", method, "code", rpcResp.Error.Code, "message", rpcResp.Error.Message) + return fmt.Errorf("rpc error %d: %s", rpcResp.Error.Code, rpcResp.Error.Message) + } + + if err := json.Unmarshal(rpcResp.Result, result); err != nil { + return fmt.Errorf("unmarshal result: %w", err) + } + + return nil +} + +func ReverseHexBytes(hexStr string) ([]byte, error) { + data, err := hex.DecodeString(hexStr) + if err != nil { + return nil, fmt.Errorf("decode hex: %w", err) + } + + for i, j := 0, len(data)-1; i < j; i, j = i+1, j-1 { + data[i], data[j] = data[j], data[i] + } + return data, nil +} + +func ReverseBytes(data []byte) []byte { + result := make([]byte, len(data)) + for i, b := range data { + result[len(data)-1-i] = b + } + return result +} diff --git a/packages/sdk-go/rpc/doc.go b/packages/sdk-go/rpc/doc.go new file mode 100644 index 0000000..c0caad2 --- /dev/null +++ b/packages/sdk-go/rpc/doc.go @@ -0,0 +1,9 @@ +// Package rpc provides clients for interacting with blockchain RPC endpoints. +// +// This package implements JSON-RPC clients for various blockchain networks +// used by the UTS protocol for timestamp verification: +// - Bitcoin RPC client for block header and merkle root verification +// +// The Bitcoin client handles the byte reversal required when working with +// Bitcoin's display format (hashes are shown in reversed byte order). +package rpc diff --git a/packages/sdk-go/rpc/ethereum.go b/packages/sdk-go/rpc/ethereum.go new file mode 100644 index 0000000..a7a08d8 --- /dev/null +++ b/packages/sdk-go/rpc/ethereum.go @@ -0,0 +1,255 @@ +package rpc + +import ( + "context" + "fmt" + "math/big" + "strings" + "sync" + + "github.com/ethereum/go-ethereum" + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/ethclient" + "github.com/lightsing/uts/packages/sdk-go/logging" +) + +var ( + SchemaID = [32]byte{ + 0x5c, 0x5b, 0x8b, 0x29, 0x5f, 0xf4, 0x3c, 0x8e, + 0x44, 0x2b, 0xe1, 0x1d, 0x56, 0x9e, 0x94, 0xa4, + 0xcd, 0x54, 0x76, 0xf5, 0xe2, 0x3d, 0xf0, 0xf7, + 0x1b, 0xdd, 0x40, 0x8d, 0xf6, 0xb9, 0x64, 0x9c, + } + + EASAddresses = map[uint64]common.Address{ + 1: common.HexToAddress("0xA1207F3BBa224E2c9c3c6D5aF63D0eb1582Ce587"), + 534351: common.HexToAddress("0xaEF4103A04090071165F78D45D83A0C0782c2B2a"), + 534352: common.HexToAddress("0xC47300428b6AD2c7D03BB76D05A176058b47E6B0"), + 11155111: common.HexToAddress("0xC47300428b6AD2c7D03BB76D05A176058b47E6B0"), + } + + DefaultRpcURLs = map[uint64]string{ + 1: "https://0xrpc.io/eth", + 534351: "https://sepolia-rpc.scroll.io", + 534352: "https://rpc.scroll.io", + 11155111: "https://0xrpc.io/sepolia", + } +) + +type Attestation struct { + UID [32]byte + Schema [32]byte + Time uint64 + ExpirationTime uint64 + RevocationTime uint64 + RefUID [32]byte + Recipient common.Address + Attester common.Address + Revocable bool + Data []byte +} + +type EthereumClient struct { + mu sync.RWMutex + clients map[uint64]*ethclient.Client + logger *logging.Logger +} + +func NewEthereumClient() *EthereumClient { + client := &EthereumClient{ + clients: make(map[uint64]*ethclient.Client), + logger: logging.Default(), + } + return client +} + +func (c *EthereumClient) SetLogger(logger *logging.Logger) { + c.mu.Lock() + defer c.mu.Unlock() + c.logger = logger +} + +func (c *EthereumClient) AddChain(chainID uint64, rpcURL string) error { + c.logger.Debug(context.Background(), "EthereumClient: AddChain", "chain_id", chainID, "url", rpcURL) + client, err := ethclient.Dial(rpcURL) + if err != nil { + c.logger.Warn(context.Background(), "EthereumClient: AddChain failed", "chain_id", chainID, "error", err) + return fmt.Errorf("failed to connect to RPC: %w", err) + } + + c.mu.Lock() + defer c.mu.Unlock() + c.clients[chainID] = client + return nil +} + +func (c *EthereumClient) GetClient(chainID uint64) (*ethclient.Client, bool) { + c.mu.RLock() + defer c.mu.RUnlock() + client, ok := c.clients[chainID] + return client, ok +} + +func (c *EthereumClient) GetEASAttestation(ctx context.Context, chainID uint64, uid [32]byte) (*Attestation, error) { + c.logger.Debug(ctx, "EthereumClient: GetEASAttestation", "chain_id", chainID, "uid", common.Bytes2Hex(uid[:])) + client, ok := c.GetClient(chainID) + if !ok { + return nil, fmt.Errorf("no client configured for chain %d", chainID) + } + + addr, ok := EASAddresses[chainID] + if !ok { + return nil, fmt.Errorf("no EAS address configured for chain %d", chainID) + } + + parsedABI, err := EASABI() + if err != nil { + return nil, fmt.Errorf("failed to parse ABI: %w", err) + } + + callData, err := parsedABI.Pack("getAttestation", uid) + if err != nil { + return nil, fmt.Errorf("failed to pack call data: %w", err) + } + + result, err := client.CallContract(ctx, ethereum.CallMsg{ + To: &addr, + Data: callData, + }, nil) + if err != nil { + c.logger.Warn(ctx, "EthereumClient: GetEASAttestation contract call failed", "chain_id", chainID, "error", err) + return nil, fmt.Errorf("contract call failed: %w", err) + } + + var att Attestation + var rawAtt struct { + UID [32]byte + Schema [32]byte + Time *big.Int + ExpirationTime *big.Int + RevocationTime *big.Int + RefUID [32]byte + Recipient common.Address + Attester common.Address + Revocable bool + Data []byte + } + + err = parsedABI.UnpackIntoInterface(&rawAtt, "getAttestation", result) + if err != nil { + return nil, fmt.Errorf("failed to unpack result: %w", err) + } + + att.UID = rawAtt.UID + att.Schema = rawAtt.Schema + att.Time = rawAtt.Time.Uint64() + att.ExpirationTime = rawAtt.ExpirationTime.Uint64() + att.RevocationTime = rawAtt.RevocationTime.Uint64() + att.RefUID = rawAtt.RefUID + att.Recipient = rawAtt.Recipient + att.Attester = rawAtt.Attester + att.Revocable = rawAtt.Revocable + att.Data = rawAtt.Data + + c.logger.Trace(ctx, "EthereumClient: GetEASAttestation success", "chain_id", chainID, "time", att.Time, "attester", att.Attester.Hex()) + return &att, nil +} + +func (c *EthereumClient) GetTimestamp(ctx context.Context, chainID uint64, data [32]byte) (uint64, error) { + c.logger.Debug(ctx, "EthereumClient: GetTimestamp", "chain_id", chainID, "data", common.Bytes2Hex(data[:])) + client, ok := c.GetClient(chainID) + if !ok { + return 0, fmt.Errorf("no client configured for chain %d", chainID) + } + + addr, ok := EASAddresses[chainID] + if !ok { + return 0, fmt.Errorf("no EAS address configured for chain %d", chainID) + } + + parsedABI, err := EASABI() + if err != nil { + return 0, fmt.Errorf("failed to parse ABI: %w", err) + } + + callData, err := parsedABI.Pack("getTimestamp", data) + if err != nil { + return 0, fmt.Errorf("failed to pack call data: %w", err) + } + + result, err := client.CallContract(ctx, ethereum.CallMsg{ + To: &addr, + Data: callData, + }, nil) + if err != nil { + c.logger.Warn(ctx, "EthereumClient: GetTimestamp contract call failed", "chain_id", chainID, "error", err) + return 0, fmt.Errorf("contract call failed: %w", err) + } + + var timestamp uint64 + err = parsedABI.UnpackIntoInterface(×tamp, "getTimestamp", result) + if err != nil { + return 0, fmt.Errorf("failed to unpack result: %w", err) + } + + c.logger.Trace(ctx, "EthereumClient: GetTimestamp success", "chain_id", chainID, "timestamp", timestamp) + return timestamp, nil +} + +func (c *EthereumClient) Close() { + c.mu.Lock() + defer c.mu.Unlock() + for _, client := range c.clients { + client.Close() + } + c.clients = make(map[uint64]*ethclient.Client) +} + +func (c *EthereumClient) GetBlockNumber(ctx context.Context, chainID uint64) (uint64, error) { + client, ok := c.GetClient(chainID) + if !ok { + return 0, fmt.Errorf("no client configured for chain %d", chainID) + } + + header, err := client.HeaderByNumber(ctx, nil) + if err != nil { + return 0, fmt.Errorf("failed to get block number: %w", err) + } + return header.Number.Uint64(), nil +} + +func (c *EthereumClient) GetTransactionReceipt(ctx context.Context, chainID uint64, txHash common.Hash) (*types.Receipt, error) { + client, ok := c.GetClient(chainID) + if !ok { + return nil, fmt.Errorf("no client configured for chain %d", chainID) + } + + return client.TransactionReceipt(ctx, txHash) +} + +type EASContract struct { + *bind.BoundContract + address common.Address + client *ethclient.Client +} + +func NewEASContract(client *ethclient.Client, address common.Address) (*EASContract, error) { + parsedABI, err := EASABI() + if err != nil { + return nil, fmt.Errorf("failed to parse ABI: %w", err) + } + return &EASContract{ + BoundContract: bind.NewBoundContract(address, parsedABI, client, nil, nil), + address: address, + client: client, + }, nil +} + +func EASABI() (abi.ABI, error) { + const easABI = `[{"inputs":[{"internalType":"bytes32","name":"uid","type":"bytes32"}],"name":"getAttestation","outputs":[{"components":[{"internalType":"bytes32","name":"uid","type":"bytes32"},{"internalType":"bytes32","name":"schema","type":"bytes32"},{"internalType":"uint64","name":"time","type":"uint64"},{"internalType":"uint64","name":"expirationTime","type":"uint64"},{"internalType":"uint64","name":"revocationTime","type":"uint64"},{"internalType":"bytes32","name":"refUID","type":"bytes32"},{"internalType":"address","name":"recipient","type":"address"},{"internalType":"address","name":"attester","type":"address"},{"internalType":"bool","name":"revocable","type":"bool"},{"internalType":"bytes","name":"data","type":"bytes"}],"internalType":"struct Attestation","name":"","type":"tuple"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"bytes32","name":"data","type":"bytes32"}],"name":"getTimestamp","outputs":[{"internalType":"uint64","name":"","type":"uint64"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"bytes32","name":"data","type":"bytes32"}],"name":"timestamp","outputs":[{"internalType":"uint64","name":"","type":"uint64"}],"stateMutability":"nonpayable","type":"function"}]` + + return abi.JSON(strings.NewReader(easABI)) +} diff --git a/packages/sdk-go/sdk.go b/packages/sdk-go/sdk.go new file mode 100644 index 0000000..3bbb9ab --- /dev/null +++ b/packages/sdk-go/sdk.go @@ -0,0 +1,844 @@ +// MIT License +// +// Copyright (c) 2025 UTS Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// +// Apache License, Version 2.0 +// +// Copyright (c) 2025 UTS Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +package uts + +import ( + "bytes" + "context" + "crypto/rand" + "encoding/hex" + "fmt" + "io" + "math" + "net/http" + "sync" + "time" + + "github.com/lightsing/uts/packages/sdk-go/attestation" + "github.com/lightsing/uts/packages/sdk-go/codec" + "github.com/lightsing/uts/packages/sdk-go/crypto" + "github.com/lightsing/uts/packages/sdk-go/errors" + "github.com/lightsing/uts/packages/sdk-go/logging" + "github.com/lightsing/uts/packages/sdk-go/rpc" + "github.com/lightsing/uts/packages/sdk-go/types" +) + +var DefaultCalendars = []string{ + "https://lgm1.calendar.test.timestamps.now/", + // Run by Peter Todd + "https://a.pool.opentimestamps.org/", + "https://b.pool.opentimestamps.org/", + // Run by Riccardo Casatta + "https://a.pool.eternitywall.com/", + // Run by Bull Bitcoin + "https://ots.btc.catallaxy.com/", +} + +const ( + DefaultTimeout = 10 * time.Second + DefaultNonceSize = 32 + DefaultHashAlgorithm = "keccak256" +) + +type HashAlgorithm string + +const ( + HashSHA256 HashAlgorithm = "sha256" + HashKeccak256 HashAlgorithm = "keccak256" +) + +const DefaultMaxHttpResponseBytes = 1024 * 1024 + +type SDK struct { + calendars []string + btcRPC attestation.BitcoinRPCClient + ethRPC *rpc.EthereumClient + timeout time.Duration + quorum int + nonceSize int + hashAlgorithm HashAlgorithm + httpClient *http.Client + maxHttpResponseBytes int64 + logger *logging.Logger +} + +type Option func(*SDK) error + +func WithCalendars(urls ...string) Option { + return func(s *SDK) error { + s.calendars = urls + return nil + } +} + +func WithBitcoinRPC(client attestation.BitcoinRPCClient) Option { + return func(s *SDK) error { + s.btcRPC = client + return nil + } +} + +func WithEthereumRPC(chainID uint64, rpcURL string) Option { + return func(s *SDK) error { + if s.ethRPC == nil { + s.ethRPC = rpc.NewEthereumClient() + } + return s.ethRPC.AddChain(chainID, rpcURL) + } +} + +func WithTimeout(d time.Duration) Option { + return func(s *SDK) error { + s.timeout = d + return nil + } +} + +func WithQuorum(n int) Option { + return func(s *SDK) error { + s.quorum = n + return nil + } +} + +func WithNonceSize(n int) Option { + return func(s *SDK) error { + s.nonceSize = n + return nil + } +} + +func WithHashAlgorithm(alg HashAlgorithm) Option { + return func(s *SDK) error { + s.hashAlgorithm = alg + return nil + } +} + +func WithLogger(logger *logging.Logger) Option { + return func(s *SDK) error { + s.logger = logger + return nil + } +} + +func WithMaxHttpResponseBytes(n int64) Option { + return func(s *SDK) error { + s.maxHttpResponseBytes = n + return nil + } +} + +func NewSDK(opts ...Option) (*SDK, error) { + calendars := make([]string, len(DefaultCalendars)) + copy(calendars, DefaultCalendars) + + s := &SDK{ + calendars: calendars, + timeout: DefaultTimeout, + nonceSize: DefaultNonceSize, + hashAlgorithm: DefaultHashAlgorithm, + httpClient: &http.Client{ + Timeout: DefaultTimeout, + }, + logger: logging.NewDefaultLogger(logging.LevelInfo), + maxHttpResponseBytes: DefaultMaxHttpResponseBytes, + } + + for _, opt := range opts { + if err := opt(s); err != nil { + return nil, err + } + } + + for i, url := range s.calendars { + s.calendars[i] = string(bytes.TrimRight([]byte(url), "/")) + } + + s.httpClient.Timeout = s.timeout + + if s.quorum == 0 { + s.quorum = int(math.Ceil(float64(len(s.calendars)) * 0.66)) + } + + if s.ethRPC == nil { + s.ethRPC = rpc.NewEthereumClient() + for chainID, rpcURL := range rpc.DefaultRpcURLs { + err := s.ethRPC.AddChain(chainID, rpcURL) + if err != nil { + s.logger.Warn(context.Background(), "Failed to add default Ethereum chain", "chain_id", chainID, "url", rpcURL, "error", err) + } + } + } + + s.logger.Debug(context.Background(), "SDK initialized", + "calendars", len(s.calendars), + "timeout", s.timeout, + "quorum", s.quorum, + "hash_algorithm", s.hashAlgorithm, + ) + + return s, nil +} + +func (s *SDK) Calendars() []string { + calendars := make([]string, len(s.calendars)) + copy(calendars, s.calendars) + return calendars +} + +func (s *SDK) Timeout() time.Duration { + return s.timeout +} + +func (s *SDK) Quorum() int { + return s.quorum +} + +func (s *SDK) SetBitcoinRPC(client attestation.BitcoinRPCClient) { + s.btcRPC = client +} + +func (s *SDK) SetEthereumRPC(client *rpc.EthereumClient) { + s.ethRPC = client +} + +type calendarResponse struct { + timestamp types.Timestamp + err error + url string +} + +func (s *SDK) requestAttestation(ctx context.Context, calendarURL string, root []byte) (types.Timestamp, error) { + url := calendarURL + "/digest" + + req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewReader(root)) + if err != nil { + return nil, errors.NewRemoteError(fmt.Sprintf("failed to create request for %s", calendarURL), err) + } + req.Header.Set("Accept", "application/vnd.opentimestamps.v1") + + resp, err := s.httpClient.Do(req) + if err != nil { + return nil, errors.NewRemoteError(fmt.Sprintf("failed to submit to calendar %s", calendarURL), err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return nil, errors.NewRemoteError( + fmt.Sprintf("calendar %s responded with status %d", calendarURL, resp.StatusCode), + nil, + ) + } + + reader := io.LimitReader(resp.Body, s.maxHttpResponseBytes+1) + data, err := io.ReadAll(reader) + if int64(len(data)) > s.maxHttpResponseBytes { + return nil, errors.NewRemoteError(fmt.Sprintf("response from %s is too large", calendarURL), nil) + } + if err != nil { + return nil, errors.NewRemoteError(fmt.Sprintf("failed to read response from %s", calendarURL), err) + } + + dec := codec.NewDecoder(data) + return dec.ReadTimestamp() +} + +func (s *SDK) Stamp(ctx context.Context, headers []*types.DigestHeader) ([]*types.DetachedTimestamp, error) { + s.logger.Trace(ctx, "Stamp: enter", "digest_count", len(headers)) + + if len(headers) == 0 { + return nil, errors.NewSDKError(errors.ErrCodeEmptyRequests, "at least one digest header is required", nil) + } + + s.logger.Debug(ctx, "Stamp: generating nonces", "count", len(headers)) + nonces := make([][]byte, len(headers)) + nonceDigests := make([][crypto.HashSize]byte, len(headers)) + + for i, header := range headers { + nonce := make([]byte, s.nonceSize) + if _, err := rand.Read(nonce); err != nil { + s.logger.Error(ctx, "Stamp: failed to generate nonce", "index", i, "error", err) + return nil, errors.NewSDKError(errors.ErrCodeGeneric, "failed to generate nonce", map[string]interface{}{"error": err.Error()}) + } + nonces[i] = nonce + + digest := header.DigestBytes() + var nonceDigest [crypto.HashSize]byte + switch s.hashAlgorithm { + case HashSHA256: + data := make([]byte, len(digest)+len(nonce)) + copy(data, digest) + copy(data[len(digest):], nonce) + nonceDigest = crypto.SHA256(data) + case HashKeccak256: + data := make([]byte, len(digest)+len(nonce)) + copy(data, digest) + copy(data[len(digest):], nonce) + nonceDigest = crypto.Keccak256(data) + default: + return nil, errors.NewSDKError(errors.ErrCodeUnsupported, fmt.Sprintf("unsupported hash algorithm: %s", s.hashAlgorithm), nil) + } + nonceDigests[i] = nonceDigest + } + + s.logger.Debug(ctx, "Stamp: building merkle tree") + tree, err := crypto.NewMerkleTree(nonceDigests) + if err != nil { + s.logger.Error(ctx, "Stamp: failed to build merkle tree", "error", err) + return nil, errors.NewSDKError(errors.ErrCodeGeneric, "failed to build merkle tree", map[string]interface{}{"error": err.Error()}) + } + root := tree.Root() + s.logger.Trace(ctx, "Stamp: merkle tree built", "root", hex.EncodeToString(root[:])) + + ctx, cancel := context.WithTimeout(ctx, s.timeout) + defer cancel() + + s.logger.Debug(ctx, "Stamp: submitting to calendars", "count", len(s.calendars), "quorum", s.quorum) + respChan := make(chan calendarResponse, len(s.calendars)) + var wg sync.WaitGroup + + for _, calURL := range s.calendars { + wg.Add(1) + go func(url string) { + defer wg.Done() + s.logger.Trace(ctx, "Stamp: requesting attestation", "calendar", url) + ts, err := s.requestAttestation(ctx, url, root[:]) + if err != nil { + s.logger.Debug(ctx, "Stamp: calendar error", "calendar", url, "error", err) + } else { + s.logger.Debug(ctx, "Stamp: calendar success", "calendar", url) + } + respChan <- calendarResponse{timestamp: ts, err: err, url: url} + }(calURL) + } + + go func() { + wg.Wait() + close(respChan) + }() + + var successfulResponses []types.Timestamp + for resp := range respChan { + if resp.err == nil { + successfulResponses = append(successfulResponses, resp.timestamp) + } + } + + s.logger.Debug(ctx, "Stamp: responses received", "success", len(successfulResponses), "quorum", s.quorum) + if len(successfulResponses) < s.quorum { + s.logger.Warn(ctx, "Stamp: quorum not reached", "success", len(successfulResponses), "quorum", s.quorum) + return nil, errors.NewRemoteError( + fmt.Sprintf("only received %d valid responses from calendars, which does not meet the quorum of %d", + len(successfulResponses), s.quorum), + nil, + ) + } + + var mergedTimestamp types.Timestamp + if len(successfulResponses) == 1 { + mergedTimestamp = successfulResponses[0] + } else { + s.logger.Trace(ctx, "Stamp: merging multiple responses into fork", "count", len(successfulResponses)) + mergedTimestamp = types.Timestamp{ + types.NewForkStep(successfulResponses), + } + } + + results := make([]*types.DetachedTimestamp, len(headers)) + for i, header := range headers { + ts := make(types.Timestamp, 0, 2) + + ts = append(ts, types.NewAppendStep(nonces[i], nil)) + + switch s.hashAlgorithm { + case HashSHA256: + ts = append(ts, types.NewSHA256Step(nil)) + case HashKeccak256: + ts = append(ts, types.NewKeccak256Step(nil)) + } + + proof, err := tree.GetProof(nonceDigests[i]) + if err != nil { + return nil, errors.NewSDKError(errors.ErrCodeGeneric, "failed to generate proof for digest", map[string]interface{}{"digest": header.DigestBytes(), "error": err.Error()}) + } + + for _, step := range proof { + prefix := []byte{crypto.InnerNodePrefix} + if step.Position == crypto.PositionLeft { + ts = append(ts, + types.NewPrependStep(prefix, nil), + types.NewAppendStep(step.Sibling[:], nil), + ) + switch s.hashAlgorithm { + case HashSHA256: + ts = append(ts, types.NewSHA256Step(nil)) + case HashKeccak256: + ts = append(ts, types.NewKeccak256Step(nil)) + } + } else { + ts = append(ts, + types.NewPrependStep(step.Sibling[:], nil), + types.NewPrependStep(prefix, nil), + ) + switch s.hashAlgorithm { + case HashSHA256: + ts = append(ts, types.NewSHA256Step(nil)) + case HashKeccak256: + ts = append(ts, types.NewKeccak256Step(nil)) + } + } + } + + ts = append(ts, mergedTimestamp...) + + results[i] = types.NewDetachedTimestamp(header, ts) + } + + return results, nil +} + +func (s *SDK) executeStep(input []byte, step types.Step) ([]byte, error) { + switch st := step.(type) { + case *types.AppendStep: + data := make([]byte, len(input)+len(st.Data)) + copy(data, input) + copy(data[len(input):], st.Data) + return data, nil + case *types.PrependStep: + data := make([]byte, len(input)+len(st.Data)) + copy(data, st.Data) + copy(data[len(st.Data):], input) + return data, nil + case *types.ReverseStep: + result := make([]byte, len(input)) + for i, b := range input { + result[len(input)-1-i] = b + } + return result, nil + case *types.HexlifyStep: + hexStr := hex.EncodeToString(input) + return []byte(hexStr), nil + case *types.SHA256Step: + hash := crypto.SHA256(input) + return hash[:], nil + case *types.Keccak256Step: + hash := crypto.Keccak256(input) + return hash[:], nil + case *types.SHA1Step: + return nil, errors.NewSDKError(errors.ErrCodeUnsupported, "SHA1 not supported", nil) + case *types.RIPEMD160Step: + return nil, errors.NewSDKError(errors.ErrCodeUnsupported, "RIPEMD160 not supported", nil) + default: + return nil, errors.NewSDKError(errors.ErrCodeUnsupported, fmt.Sprintf("unsupported step type: %T", step), nil) + } +} + +func (s *SDK) verifyTimestamp(ctx context.Context, input []byte, ts types.Timestamp) ([]*types.AttestationStatus, error) { + s.logger.Trace(ctx, "verifyTimestamp: enter", "input_len", len(input), "steps", len(ts)) + var attestations []*types.AttestationStatus + current := input + + for i, step := range ts { + s.logger.Trace(ctx, "verifyTimestamp: processing step", "index", i, "step_type", fmt.Sprintf("%T", step)) + switch st := step.(type) { + case *types.AppendStep: + var err error + current, err = s.executeStep(current, step) + if err != nil { + return nil, err + } + case *types.PrependStep: + var err error + current, err = s.executeStep(current, step) + if err != nil { + return nil, err + } + case *types.ReverseStep: + var err error + current, err = s.executeStep(current, step) + if err != nil { + return nil, err + } + case *types.HexlifyStep: + var err error + current, err = s.executeStep(current, step) + if err != nil { + return nil, err + } + case *types.SHA256Step: + var err error + current, err = s.executeStep(current, step) + if err != nil { + return nil, err + } + case *types.Keccak256Step: + var err error + current, err = s.executeStep(current, step) + if err != nil { + return nil, err + } + case *types.SHA1Step: + return nil, fmt.Errorf("SHA1 not supported") + case *types.RIPEMD160Step: + return nil, fmt.Errorf("RIPEMD160 not supported") + case *types.ForkStep: + s.logger.Trace(ctx, "verifyTimestamp: entering fork", "branches", len(st.Branches)) + for _, branch := range st.Branches { + results, err := s.verifyTimestamp(ctx, current, branch) + if err != nil { + return nil, err + } + attestations = append(attestations, results...) + } + case *types.AttestationStep: + s.logger.Debug(ctx, "verifyTimestamp: verifying attestation", "type", fmt.Sprintf("%T", st.Attestation)) + status := attestation.Verify(ctx, s.btcRPC, s.ethRPC, current, st.Attestation) + s.logger.Debug(ctx, "verifyTimestamp: attestation result", "status", status.Status) + attestations = append(attestations, status) + default: + return nil, fmt.Errorf("unsupported step type: %T", step) + } + } + + s.logger.Trace(ctx, "verifyTimestamp: complete", "attestations", len(attestations)) + return attestations, nil +} + +func (s *SDK) Verify(ctx context.Context, stamp *types.DetachedTimestamp) (*types.VerificationResult, error) { + s.logger.Trace(ctx, "Verify: enter", "digest_len", len(stamp.Header.DigestBytes())) + input := stamp.Header.DigestBytes() + + attestations, err := s.verifyTimestamp(ctx, input, stamp.Timestamp) + if err != nil { + s.logger.Warn(ctx, "Verify: failed", "error", err) + return nil, err + } + + status := s.aggregateResult(attestations) + s.logger.Debug(ctx, "Verify: complete", "status", status, "attestations", len(attestations)) + return types.NewVerificationResult(status, attestations), nil +} + +func (s *SDK) aggregateResult(attestations []*types.AttestationStatus) types.VerifyStatus { + counts := map[types.AttestationStatusKind]int{ + types.StatusValid: 0, + types.StatusInvalid: 0, + types.StatusPending: 0, + types.StatusUnknown: 0, + } + + for _, att := range attestations { + counts[att.Status]++ + } + + if counts[types.StatusValid] > 0 { + if counts[types.StatusInvalid] > 0 || counts[types.StatusUnknown] > 0 { + return types.VerifyPartialValid + } + return types.VerifyValid + } + + if counts[types.StatusPending] > 0 { + return types.VerifyPending + } + + return types.VerifyInvalid +} + +func (s *SDK) upgradeAttestation(ctx context.Context, commitment []byte, att *types.PendingAttestation) (types.Timestamp, error) { + url := att.URI + "/timestamp/" + hex.EncodeToString(commitment) + s.logger.Trace(ctx, "upgradeAttestation: fetching", "url", url) + + req, err := http.NewRequestWithContext(ctx, "GET", url, nil) + if err != nil { + return nil, errors.NewRemoteError(fmt.Sprintf("failed to create request for %s", att.URI), err) + } + req.Header.Set("Accept", "application/vnd.opentimestamps.v1") + + resp, err := s.httpClient.Do(req) + if err != nil { + return nil, errors.NewRemoteError(fmt.Sprintf("failed to fetch from calendar %s", att.URI), err) + } + defer resp.Body.Close() + + if resp.StatusCode == http.StatusNotFound { + s.logger.Debug(ctx, "upgradeAttestation: not found", "uri", att.URI) + return nil, nil + } + + if resp.StatusCode != http.StatusOK { + return nil, errors.NewRemoteError( + fmt.Sprintf("calendar %s responded with status %d", att.URI, resp.StatusCode), + nil, + ) + } + + reader := io.LimitReader(resp.Body, s.maxHttpResponseBytes+1) + data, err := io.ReadAll(reader) + if int64(len(data)) > s.maxHttpResponseBytes { + return nil, errors.NewRemoteError(fmt.Sprintf("response from %s is too large", att.URI), nil) + } + if err != nil { + return nil, errors.NewRemoteError(fmt.Sprintf("failed to read response from %s", att.URI), err) + } + + s.logger.Debug(ctx, "upgradeAttestation: received timestamp", "uri", att.URI, "bytes", len(data)) + dec := codec.NewDecoder(data) + return dec.ReadTimestamp() +} + +func (s *SDK) upgradeTimestamp(ctx context.Context, input []byte, ts types.Timestamp, keepPending bool) ([]*types.UpgradeResult, error) { + s.logger.Trace(ctx, "upgradeTimestamp: enter", "input_len", len(input), "steps", len(ts), "keep_pending", keepPending) + current := input + var results []*types.UpgradeResult + + for i := 0; i < len(ts); i++ { + step := ts[i] + + switch st := step.(type) { + case *types.AppendStep, *types.PrependStep, *types.ReverseStep, *types.HexlifyStep, + *types.SHA256Step, *types.Keccak256Step, *types.SHA1Step, *types.RIPEMD160Step: + var err error + current, err = s.executeStep(current, step) + if err != nil { + return nil, err + } + + case *types.ForkStep: + s.logger.Trace(ctx, "upgradeTimestamp: entering fork", "branches", len(st.Branches)) + for _, branch := range st.Branches { + branchResults, err := s.upgradeTimestamp(ctx, current, branch, keepPending) + if err != nil { + return nil, err + } + results = append(results, branchResults...) + } + + case *types.AttestationStep: + pendingAtt, ok := st.Attestation.(*types.PendingAttestation) + if !ok { + continue + } + + s.logger.Debug(ctx, "upgradeTimestamp: upgrading pending attestation", "uri", pendingAtt.URI) + upgraded, err := s.upgradeAttestation(ctx, current, pendingAtt) + if err != nil { + s.logger.Warn(ctx, "upgradeTimestamp: upgrade failed", "uri", pendingAtt.URI, "error", err) + results = append(results, &types.UpgradeResult{ + Status: types.UpgradeFailed, + Error: err, + }) + continue + } + + if upgraded == nil { + s.logger.Debug(ctx, "upgradeTimestamp: still pending", "uri", pendingAtt.URI) + results = append(results, &types.UpgradeResult{ + Status: types.UpgradePending, + }) + continue + } + + s.logger.Debug(ctx, "upgradeTimestamp: upgraded", "uri", pendingAtt.URI, "steps", len(upgraded)) + if keepPending { + ts[i] = types.NewForkStep([]types.Timestamp{ + {st}, + upgraded, + }) + } else { + newSteps := make(types.Timestamp, 0, len(ts)-1+len(upgraded)) + newSteps = append(newSteps, ts[:i]...) + newSteps = append(newSteps, upgraded...) + newSteps = append(newSteps, ts[i+1:]...) + ts = newSteps + i += len(upgraded) - 1 + } + + results = append(results, &types.UpgradeResult{ + Status: types.UpgradeUpgraded, + }) + } + } + + s.logger.Trace(ctx, "upgradeTimestamp: complete", "results", len(results)) + return results, nil +} + +func (s *SDK) Upgrade(ctx context.Context, stamp *types.DetachedTimestamp, keepPending bool) ([]*types.UpgradeResult, error) { + s.logger.Trace(ctx, "Upgrade: enter", "keep_pending", keepPending) + input := stamp.Header.DigestBytes() + results, err := s.upgradeTimestamp(ctx, input, stamp.Timestamp, keepPending) + if err != nil { + s.logger.Warn(ctx, "Upgrade: failed", "error", err) + return nil, err + } + s.logger.Debug(ctx, "Upgrade: complete", "results", len(results)) + return results, nil +} + +// ListPending returns all pending attestation URIs in the given detached timestamp. +func (s *SDK) ListPending(stamp *types.DetachedTimestamp) []string { + return collectPendingAttestations(stamp.Timestamp) +} + +func collectPendingAttestations(ts types.Timestamp) []string { + var uris []string + for _, step := range ts { + switch st := step.(type) { + case *types.AttestationStep: + if pending, ok := st.Attestation.(*types.PendingAttestation); ok { + uris = append(uris, pending.URI) + } + case *types.ForkStep: + for _, branch := range st.Branches { + uris = append(uris, collectPendingAttestations(branch)...) + } + } + } + return uris +} + +// PurgeResult contains the result of a purge operation. +type PurgeResult struct { + // Purged contains the URIs of the pending attestations that were removed. + Purged []string + // HasRemaining is true if the timestamp still has non-pending attestations. + HasRemaining bool +} + +// RetainAttestations retains only the attestations for which the predicate returns true, +// removing all others from the timestamp tree. This is analogous to Go's slices.DeleteFunc +// but operates on the attestation leaves of the timestamp tree. +// FORK nodes left with a single branch after filtering are collapsed. +// Returns true if the timestamp still has attestations, false if all were removed. +func (s *SDK) RetainAttestations(stamp *types.DetachedTimestamp, shouldRetain func(types.Attestation) bool) bool { + return retainAttestations(&stamp.Timestamp, shouldRetain) +} + +// PurgePending removes all pending attestations from the given detached timestamp. +// This is a convenience wrapper around RetainAttestations. +// It returns a PurgeResult with the purged URIs and whether any attestations remain. +func (s *SDK) PurgePending(stamp *types.DetachedTimestamp) *PurgeResult { + return s.PurgePendingByURIs(stamp, nil) +} + +// PurgePendingByURIs removes selected pending attestations from the given detached timestamp. +// If urisToPurge is nil, all pending attestations are removed. +// If urisToPurge is non-nil, only pending attestations whose URI is in the set are removed. +// This is a convenience wrapper around RetainAttestations. +func (s *SDK) PurgePendingByURIs(stamp *types.DetachedTimestamp, urisToPurge map[string]bool) *PurgeResult { + allPending := s.ListPending(stamp) + if len(allPending) == 0 { + return &PurgeResult{Purged: nil, HasRemaining: true} + } + + var purgedURIs []string + if urisToPurge != nil { + for _, uri := range allPending { + if urisToPurge[uri] { + purgedURIs = append(purgedURIs, uri) + } + } + } else { + purgedURIs = allPending + } + + if len(purgedURIs) == 0 { + return &PurgeResult{Purged: nil, HasRemaining: true} + } + + hasRemaining := s.RetainAttestations(stamp, func(att types.Attestation) bool { + pending, ok := att.(*types.PendingAttestation) + if !ok { + return true // keep non-pending + } + if urisToPurge == nil { + return false // purge all pending + } + return !urisToPurge[pending.URI] // keep if NOT in purge set + }) + + return &PurgeResult{ + Purged: purgedURIs, + HasRemaining: hasRemaining, + } +} + +// retainAttestations recursively retains only attestations matching the predicate. +// It modifies the timestamp slice in place. +// Returns true if the timestamp still has content, false if it should be removed. +func retainAttestations(ts *types.Timestamp, shouldRetain func(types.Attestation) bool) bool { + i := 0 + for i < len(*ts) { + step := (*ts)[i] + switch st := step.(type) { + case *types.AttestationStep: + if !shouldRetain(st.Attestation) { + *ts = append((*ts)[:i], (*ts)[i+1:]...) + continue + } + i++ + case *types.ForkStep: + j := 0 + for j < len(st.Branches) { + if !retainAttestations(&st.Branches[j], shouldRetain) { + st.Branches = append(st.Branches[:j], st.Branches[j+1:]...) + } else { + j++ + } + } + if len(st.Branches) == 0 { + *ts = append((*ts)[:i], (*ts)[i+1:]...) + continue + } else if len(st.Branches) == 1 { + remaining := st.Branches[0] + newTs := make(types.Timestamp, 0, len(*ts)-1+len(remaining)) + newTs = append(newTs, (*ts)[:i]...) + newTs = append(newTs, remaining...) + newTs = append(newTs, (*ts)[i+1:]...) + *ts = newTs + continue + } + i++ + default: + i++ + } + } + return len(*ts) > 0 +} diff --git a/packages/sdk-go/sdk_test.go b/packages/sdk-go/sdk_test.go new file mode 100644 index 0000000..f51f43f --- /dev/null +++ b/packages/sdk-go/sdk_test.go @@ -0,0 +1,1074 @@ +package uts + +import ( + "bytes" + "context" + "crypto/rand" + "encoding/hex" + "io" + "net/http" + "net/http/httptest" + "strings" + "testing" + "time" + + "github.com/lightsing/uts/packages/sdk-go/codec" + "github.com/lightsing/uts/packages/sdk-go/crypto" + "github.com/lightsing/uts/packages/sdk-go/rpc" + "github.com/lightsing/uts/packages/sdk-go/types" +) + +type mockHTTPClient struct { + doFunc func(req *http.Request) (*http.Response, error) +} + +func (m *mockHTTPClient) Do(req *http.Request) (*http.Response, error) { + return m.doFunc(req) +} + +type mockBitcoinRPC struct { + getBlockHashFunc func(height int64) (string, error) + getBlockHeaderFunc func(hash string) (*rpc.BlockHeader, error) +} + +func (m *mockBitcoinRPC) GetBlockHash(height int64) (string, error) { + return m.getBlockHashFunc(height) +} + +func (m *mockBitcoinRPC) GetBlockHeader(hash string) (*rpc.BlockHeader, error) { + return m.getBlockHeaderFunc(hash) +} + +type mockEASClient struct { + getAttestationFunc func(ctx context.Context, chainID uint64, uid [32]byte) (*rpc.Attestation, error) + getTimestampFunc func(ctx context.Context, chainID uint64, data [32]byte) (uint64, error) +} + +func (m *mockEASClient) GetEASAttestation(ctx context.Context, chainID uint64, uid [32]byte) (*rpc.Attestation, error) { + return m.getAttestationFunc(ctx, chainID, uid) +} + +func (m *mockEASClient) GetTimestamp(ctx context.Context, chainID uint64, data [32]byte) (uint64, error) { + return m.getTimestampFunc(ctx, chainID, data) +} + +func TestNewSDK(t *testing.T) { + tests := []struct { + name string + opts []Option + wantCalendars int + wantTimeout time.Duration + wantQuorum int + wantNonceSize int + wantHashAlg HashAlgorithm + }{ + { + name: "default values", + opts: nil, + wantCalendars: 5, + wantTimeout: DefaultTimeout, + wantQuorum: 4, + wantNonceSize: DefaultNonceSize, + wantHashAlg: DefaultHashAlgorithm, + }, + { + name: "custom calendars", + opts: []Option{ + WithCalendars("https://cal1.example.com", "https://cal2.example.com", "https://cal3.example.com"), + }, + wantCalendars: 3, + wantTimeout: DefaultTimeout, + wantQuorum: 2, + wantNonceSize: DefaultNonceSize, + wantHashAlg: DefaultHashAlgorithm, + }, + { + name: "custom timeout", + opts: []Option{ + WithTimeout(30 * time.Second), + }, + wantCalendars: 5, + wantTimeout: 30 * time.Second, + wantQuorum: 4, + wantNonceSize: DefaultNonceSize, + wantHashAlg: DefaultHashAlgorithm, + }, + { + name: "custom quorum", + opts: []Option{ + WithCalendars("https://cal1.example.com", "https://cal2.example.com"), + WithQuorum(2), + }, + wantCalendars: 2, + wantTimeout: DefaultTimeout, + wantQuorum: 2, + wantNonceSize: DefaultNonceSize, + wantHashAlg: DefaultHashAlgorithm, + }, + { + name: "custom nonce size", + opts: []Option{ + WithNonceSize(16), + }, + wantCalendars: 5, + wantTimeout: DefaultTimeout, + wantQuorum: 4, + wantNonceSize: 16, + wantHashAlg: DefaultHashAlgorithm, + }, + { + name: "custom hash algorithm SHA256", + opts: []Option{ + WithHashAlgorithm(HashSHA256), + }, + wantCalendars: 5, + wantTimeout: DefaultTimeout, + wantQuorum: 4, + wantNonceSize: DefaultNonceSize, + wantHashAlg: HashSHA256, + }, + { + name: "multiple options", + opts: []Option{ + WithCalendars("https://cal1.example.com", "https://cal2.example.com"), + WithTimeout(5 * time.Second), + WithQuorum(2), + WithNonceSize(64), + WithHashAlgorithm(HashSHA256), + }, + wantCalendars: 2, + wantTimeout: 5 * time.Second, + wantQuorum: 2, + wantNonceSize: 64, + wantHashAlg: HashSHA256, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + sdk, err := NewSDK(tt.opts...) + if err != nil { + t.Fatalf("NewSDK() error = %v", err) + } + + if got := len(sdk.Calendars()); got != tt.wantCalendars { + t.Errorf("NewSDK() calendars = %v, want %v", got, tt.wantCalendars) + } + if got := sdk.Timeout(); got != tt.wantTimeout { + t.Errorf("NewSDK() timeout = %v, want %v", got, tt.wantTimeout) + } + if got := sdk.Quorum(); got != tt.wantQuorum { + t.Errorf("NewSDK() quorum = %v, want %v", got, tt.wantQuorum) + } + if sdk.nonceSize != tt.wantNonceSize { + t.Errorf("NewSDK() nonceSize = %v, want %v", sdk.nonceSize, tt.wantNonceSize) + } + if sdk.hashAlgorithm != tt.wantHashAlg { + t.Errorf("NewSDK() hashAlgorithm = %v, want %v", sdk.hashAlgorithm, tt.wantHashAlg) + } + }) + } +} + +func TestWithBitcoinRPC(t *testing.T) { + mockBTC := &mockBitcoinRPC{ + getBlockHashFunc: func(height int64) (string, error) { + return "test-hash", nil + }, + } + + sdk, err := NewSDK(WithBitcoinRPC(mockBTC)) + if err != nil { + t.Fatalf("NewSDK() error = %v", err) + } + if sdk.btcRPC == nil { + t.Error("WithBitcoinRPC() did not set btcRPC") + } + + hash, err := sdk.btcRPC.GetBlockHash(123) + if err != nil || hash != "test-hash" { + t.Errorf("WithBitcoinRPC() mock not working correctly") + } +} + +func TestWithEthereumRPC(t *testing.T) { + sdk, err := NewSDK() + if err != nil { + t.Fatalf("NewSDK() error = %v", err) + } + if sdk.ethRPC == nil { + t.Error("NewSDK() should initialize ethRPC") + } +} + +func TestSDK_SetBitcoinRPC(t *testing.T) { + sdk, err := NewSDK() + if err != nil { + t.Fatalf("NewSDK() error = %v", err) + } + mockBTC := &mockBitcoinRPC{} + + sdk.SetBitcoinRPC(mockBTC) + if sdk.btcRPC != mockBTC { + t.Error("SetBitcoinRPC() did not set btcRPC correctly") + } +} + +func TestSDK_SetEthereumRPC(t *testing.T) { + sdk, err := NewSDK() + if err != nil { + t.Fatalf("NewSDK() error = %v", err) + } + mockEth := rpc.NewEthereumClient() + + sdk.SetEthereumRPC(mockEth) + if sdk.ethRPC != mockEth { + t.Error("SetEthereumRPC() did not set ethRPC correctly") + } +} + +func TestExecuteStep(t *testing.T) { + sdk, err := NewSDK() + if err != nil { + t.Fatalf("NewSDK() error = %v", err) + } + + tests := []struct { + name string + input []byte + step types.Step + wantOutput []byte + wantErr bool + errContain string + }{ + { + name: "AppendStep", + input: []byte("hello"), + step: types.NewAppendStep([]byte(" world"), nil), + wantOutput: []byte("hello world"), + }, + { + name: "PrependStep", + input: []byte("world"), + step: types.NewPrependStep([]byte("hello "), nil), + wantOutput: []byte("hello world"), + }, + { + name: "ReverseStep", + input: []byte("hello"), + step: types.NewReverseStep(nil), + wantOutput: []byte("olleh"), + }, + { + name: "HexlifyStep", + input: []byte{0x01, 0x02, 0xff}, + step: types.NewHexlifyStep(nil), + wantOutput: []byte("0102ff"), + }, + { + name: "SHA256Step", + input: []byte("test"), + step: types.NewSHA256Step(nil), + wantOutput: func() []byte { h := crypto.SHA256([]byte("test")); return h[:] }(), + }, + { + name: "Keccak256Step", + input: []byte("test"), + step: types.NewKeccak256Step(nil), + wantOutput: func() []byte { h := crypto.Keccak256([]byte("test")); return h[:] }(), + }, + { + name: "SHA1Step not supported", + input: []byte("test"), + step: types.NewSHA1Step(nil), + wantErr: true, + errContain: "SHA1 not supported", + }, + { + name: "RIPEMD160Step not supported", + input: []byte("test"), + step: types.NewRIPEMD160Step(nil), + wantErr: true, + errContain: "RIPEMD160 not supported", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + output, err := sdk.executeStep(tt.input, tt.step) + if tt.wantErr { + if err == nil { + t.Errorf("executeStep() expected error, got nil") + } else if !strings.Contains(err.Error(), tt.errContain) { + t.Errorf("executeStep() error = %v, want containing %v", err, tt.errContain) + } + return + } + if err != nil { + t.Errorf("executeStep() unexpected error = %v", err) + return + } + if !bytes.Equal(output, tt.wantOutput) { + t.Errorf("executeStep() = %x, want %x", output, tt.wantOutput) + } + }) + } +} + +func TestAggregateResult(t *testing.T) { + sdk, err := NewSDK() + if err != nil { + t.Fatalf("NewSDK() error = %v", err) + } + + tests := []struct { + name string + attestations []*types.AttestationStatus + wantStatus types.VerifyStatus + }{ + { + name: "empty attestations", + attestations: []*types.AttestationStatus{}, + wantStatus: types.VerifyInvalid, + }, + { + name: "single valid", + attestations: []*types.AttestationStatus{ + types.NewAttestationStatus(&types.BitcoinAttestation{Height: 100}, types.StatusValid, nil), + }, + wantStatus: types.VerifyValid, + }, + { + name: "single pending", + attestations: []*types.AttestationStatus{ + types.NewAttestationStatus(&types.PendingAttestation{URI: "https://example.com"}, types.StatusPending, nil), + }, + wantStatus: types.VerifyPending, + }, + { + name: "single invalid", + attestations: []*types.AttestationStatus{ + types.NewAttestationStatus(&types.BitcoinAttestation{Height: 100}, types.StatusInvalid, nil), + }, + wantStatus: types.VerifyInvalid, + }, + { + name: "single unknown", + attestations: []*types.AttestationStatus{ + types.NewAttestationStatus(&types.BitcoinAttestation{Height: 100}, types.StatusUnknown, nil), + }, + wantStatus: types.VerifyInvalid, + }, + { + name: "multiple valid", + attestations: []*types.AttestationStatus{ + types.NewAttestationStatus(&types.BitcoinAttestation{Height: 100}, types.StatusValid, nil), + types.NewAttestationStatus(&types.EASAttestation{ChainID: 1}, types.StatusValid, nil), + }, + wantStatus: types.VerifyValid, + }, + { + name: "valid and invalid gives partial valid", + attestations: []*types.AttestationStatus{ + types.NewAttestationStatus(&types.BitcoinAttestation{Height: 100}, types.StatusValid, nil), + types.NewAttestationStatus(&types.EASAttestation{ChainID: 1}, types.StatusInvalid, nil), + }, + wantStatus: types.VerifyPartialValid, + }, + { + name: "valid and unknown gives partial valid", + attestations: []*types.AttestationStatus{ + types.NewAttestationStatus(&types.BitcoinAttestation{Height: 100}, types.StatusValid, nil), + types.NewAttestationStatus(&types.EASAttestation{ChainID: 1}, types.StatusUnknown, nil), + }, + wantStatus: types.VerifyPartialValid, + }, + { + name: "pending takes precedence over invalid", + attestations: []*types.AttestationStatus{ + types.NewAttestationStatus(&types.PendingAttestation{URI: "https://example.com"}, types.StatusPending, nil), + types.NewAttestationStatus(&types.BitcoinAttestation{Height: 100}, types.StatusInvalid, nil), + }, + wantStatus: types.VerifyPending, + }, + { + name: "only invalid", + attestations: []*types.AttestationStatus{ + types.NewAttestationStatus(&types.BitcoinAttestation{Height: 100}, types.StatusInvalid, nil), + types.NewAttestationStatus(&types.EASAttestation{ChainID: 1}, types.StatusInvalid, nil), + }, + wantStatus: types.VerifyInvalid, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := sdk.aggregateResult(tt.attestations) + if got != tt.wantStatus { + t.Errorf("aggregateResult() = %v, want %v", got, tt.wantStatus) + } + }) + } +} + +func createMockCalendarServer(t *testing.T, responseTimestamp types.Timestamp) *httptest.Server { + return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path == "/digest" && r.Method == "POST" { + enc := codec.NewEncoder() + if err := enc.WriteTimestamp(responseTimestamp); err != nil { + t.Errorf("failed to encode timestamp: %v", err) + http.Error(w, "internal error", http.StatusInternalServerError) + return + } + w.Header().Set("Content-Type", "application/vnd.opentimestamps.v1") + w.WriteHeader(http.StatusOK) + w.Write(enc.Bytes()) + return + } + http.Error(w, "not found", http.StatusNotFound) + })) +} + +func createMockUpgradeServer(t *testing.T, commitment []byte, responseTimestamp types.Timestamp, statusCode int) *httptest.Server { + return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + expectedPath := "/timestamp/" + hex.EncodeToString(commitment) + if r.URL.Path == expectedPath && r.Method == "GET" { + if statusCode == http.StatusNotFound { + w.WriteHeader(http.StatusNotFound) + return + } + if statusCode != http.StatusOK { + w.WriteHeader(statusCode) + return + } + enc := codec.NewEncoder() + if err := enc.WriteTimestamp(responseTimestamp); err != nil { + t.Errorf("failed to encode timestamp: %v", err) + http.Error(w, "internal error", http.StatusInternalServerError) + return + } + w.Header().Set("Content-Type", "application/vnd.opentimestamps.v1") + w.WriteHeader(http.StatusOK) + w.Write(enc.Bytes()) + return + } + http.Error(w, "not found", http.StatusNotFound) + })) +} + +func TestStamp(t *testing.T) { + pendingAtt := &types.PendingAttestation{URI: "https://example.com"} + ts := types.Timestamp{ + types.NewAttestationStep(pendingAtt), + } + + server := createMockCalendarServer(t, ts) + defer server.Close() + + tests := []struct { + name string + opts []Option + headers []*types.DigestHeader + wantErr bool + errContain string + validateFn func(t *testing.T, results []*types.DetachedTimestamp) + }{ + { + name: "single digest", + opts: []Option{ + WithCalendars(server.URL), + WithTimeout(5 * time.Second), + }, + headers: func() []*types.DigestHeader { + h, _ := types.NewDigestHeader(types.DigestSHA256, make([]byte, 32)) + return []*types.DigestHeader{h} + }(), + wantErr: false, + validateFn: func(t *testing.T, results []*types.DetachedTimestamp) { + if len(results) != 1 { + t.Errorf("expected 1 result, got %d", len(results)) + return + } + if results[0] == nil { + t.Error("result should not be nil") + return + } + if len(results[0].Timestamp) == 0 { + t.Error("timestamp should not be empty") + } + }, + }, + { + name: "multiple digests", + opts: []Option{ + WithCalendars(server.URL), + WithTimeout(5 * time.Second), + }, + headers: func() []*types.DigestHeader { + h1, _ := types.NewDigestHeader(types.DigestSHA256, make([]byte, 32)) + h2, _ := types.NewDigestHeader(types.DigestSHA256, make([]byte, 32)) + h3, _ := types.NewDigestHeader(types.DigestSHA256, make([]byte, 32)) + return []*types.DigestHeader{h1, h2, h3} + }(), + wantErr: false, + validateFn: func(t *testing.T, results []*types.DetachedTimestamp) { + if len(results) != 3 { + t.Errorf("expected 3 results, got %d", len(results)) + return + } + for i, r := range results { + if r == nil { + t.Errorf("result %d should not be nil", i) + continue + } + if len(r.Timestamp) == 0 { + t.Errorf("timestamp %d should not be empty", i) + } + } + }, + }, + { + name: "with SHA256 hash algorithm", + opts: []Option{ + WithCalendars(server.URL), + WithTimeout(5 * time.Second), + WithHashAlgorithm(HashSHA256), + }, + headers: func() []*types.DigestHeader { + h, _ := types.NewDigestHeader(types.DigestSHA256, make([]byte, 32)) + return []*types.DigestHeader{h} + }(), + wantErr: false, + validateFn: func(t *testing.T, results []*types.DetachedTimestamp) { + if len(results) != 1 { + t.Errorf("expected 1 result, got %d", len(results)) + return + } + hasSHA256 := false + for _, step := range results[0].Timestamp { + if _, ok := step.(*types.SHA256Step); ok { + hasSHA256 = true + break + } + } + if !hasSHA256 { + t.Error("expected SHA256 step in timestamp") + } + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + sdk, err := NewSDK(tt.opts...) + if err != nil { + t.Fatalf("NewSDK() error = %v", err) + } + + results, err := sdk.Stamp(context.Background(), tt.headers) + if tt.wantErr { + if err == nil { + t.Errorf("Stamp() expected error, got nil") + } + return + } + if err != nil { + t.Errorf("Stamp() unexpected error = %v", err) + return + } + + if tt.validateFn != nil { + tt.validateFn(t, results) + } + }) + } +} + +func TestRequestAttestation_MalformedResponse(t *testing.T) { + malformedServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/vnd.opentimestamps.v1") + w.WriteHeader(http.StatusOK) + w.Write([]byte("invalid data")) + })) + defer malformedServer.Close() + + sdk, err := NewSDK( + WithCalendars(malformedServer.URL), + WithTimeout(5*time.Second), + ) + if err != nil { + t.Fatalf("NewSDK() error = %v", err) + } + + header, err := types.NewDigestHeader(types.DigestSHA256, make([]byte, 32)) + if err != nil { + t.Fatalf("NewDigestHeader() error = %v", err) + } + headers := []*types.DigestHeader{ + header, + } + + _, err = sdk.Stamp(context.Background(), headers) + if err == nil { + t.Error("Stamp() expected error for malformed response, got nil") + } +} + +func TestUpgradeAttestation_CommitmentCalculation(t *testing.T) { + digest := make([]byte, 32) + + appendData := []byte{0x01, 0x02, 0x03, 0x04} + + pendingAtt := &types.PendingAttestation{URI: "https://cal.example.com"} + + var requestReceived bool + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + requestReceived = true + w.WriteHeader(http.StatusNotFound) + })) + defer server.Close() + + pendingAtt.URI = server.URL + + header, err := types.NewDigestHeader(types.DigestSHA256, digest) + if err != nil { + t.Fatalf("NewDigestHeader() error = %v", err) + } + stamp := types.NewDetachedTimestamp( + header, + types.Timestamp{ + types.NewAppendStep(appendData, nil), + types.NewSHA256Step(nil), + types.NewAttestationStep(pendingAtt), + }, + ) + + sdk, err := NewSDK(WithTimeout(5 * time.Second)) + if err != nil { + t.Fatalf("NewSDK() error = %v", err) + } + + _, _ = sdk.Upgrade(context.Background(), stamp, false) + + if !requestReceived { + t.Error("expected request to be sent to calendar server") + } +} + +func TestUpgrade_ConcurrentCalendars(t *testing.T) { + if testing.Short() { + t.Skip("skipping concurrent test in short mode") + } + + digest := make([]byte, 32) + for i := range digest { + digest[i] = byte(i) + } + + callCount := 0 + upgraded := types.Timestamp{ + types.NewAttestationStep(&types.BitcoinAttestation{Height: 800000}), + } + + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + enc := codec.NewEncoder() + enc.WriteTimestamp(upgraded) + w.Header().Set("Content-Type", "application/vnd.opentimestamps.v1") + w.WriteHeader(http.StatusOK) + w.Write(enc.Bytes()) + })) + defer server.Close() + + header, err := types.NewDigestHeader(types.DigestSHA256, digest) + if err != nil { + t.Fatalf("NewDigestHeader() error = %v", err) + } + + stamp := types.NewDetachedTimestamp( + header, + types.Timestamp{ + types.NewAttestationStep(&types.PendingAttestation{URI: server.URL}), + }, + ) + + sdk, err := NewSDK(WithTimeout(5 * time.Second)) + if err != nil { + t.Fatalf("NewSDK() error = %v", err) + } + + for i := 0; i < 10; i++ { + results, err := sdk.Upgrade(context.Background(), stamp, false) + if err != nil { + t.Errorf("Upgrade() iteration %d error = %v", i, err) + continue + } + if len(results) > 0 && results[0].Status == types.UpgradeUpgraded { + callCount++ + } + } + + if callCount != 10 { + t.Errorf("expected 10 successful upgrades, got %d", callCount) + } +} + +func TestStamp_NonceGeneration(t *testing.T) { + ts := types.Timestamp{ + types.NewAttestationStep(&types.PendingAttestation{URI: "https://example.com"}), + } + + server := createMockCalendarServer(t, ts) + defer server.Close() + + sdk, err := NewSDK( + WithCalendars(server.URL), + WithTimeout(5*time.Second), + WithNonceSize(16), + ) + if err != nil { + t.Fatalf("NewSDK() error = %v", err) + } + + header1, err := types.NewDigestHeader(types.DigestSHA256, make([]byte, 32)) + if err != nil { + t.Fatalf("NewDigestHeader() error = %v", err) + } + header2, err := types.NewDigestHeader(types.DigestSHA256, make([]byte, 32)) + if err != nil { + t.Fatalf("NewDigestHeader() error = %v", err) + } + + headers := []*types.DigestHeader{ + header1, + header2, + } + + results, err := sdk.Stamp(context.Background(), headers) + if err != nil { + t.Fatalf("Stamp() error = %v", err) + } + + if len(results) != 2 { + t.Fatalf("expected 2 results, got %d", len(results)) + } + + appendSteps1 := extractAppendSteps(results[0].Timestamp) + appendSteps2 := extractAppendSteps(results[1].Timestamp) + + if len(appendSteps1) == 0 || len(appendSteps2) == 0 { + t.Fatal("expected at least one append step for nonce") + } + + if bytes.Equal(appendSteps1[0], appendSteps2[0]) { + t.Error("nonces should be different for different digests") + } +} + +func extractAppendSteps(ts types.Timestamp) [][]byte { + var result [][]byte + for _, step := range ts { + if appendStep, ok := step.(*types.AppendStep); ok { + result = append(result, appendStep.Data) + } + } + return result +} + +func TestVerify_WithHexlifyStep(t *testing.T) { + digest := make([]byte, 32) + + header, err := types.NewDigestHeader(types.DigestSHA256, digest) + if err != nil { + t.Fatalf("NewDigestHeader() error = %v", err) + } + + stamp := types.NewDetachedTimestamp( + header, + types.Timestamp{ + types.NewHexlifyStep(nil), + types.NewAttestationStep(&types.PendingAttestation{URI: "https://example.com"}), + }, + ) + + sdk, err := NewSDK() + if err != nil { + t.Fatalf("NewSDK() error = %v", err) + } + + result, err := sdk.Verify(context.Background(), stamp) + if err != nil { + t.Errorf("Verify() unexpected error = %v", err) + return + } + + if result.Status != types.VerifyPending { + t.Errorf("Verify() status = %v, want %v", result.Status, types.VerifyPending) + } +} + +func TestVerify_WithReverseStep(t *testing.T) { + digest := make([]byte, 32) + + header, err := types.NewDigestHeader(types.DigestSHA256, digest) + if err != nil { + t.Fatalf("NewDigestHeader() error = %v", err) + } + + stamp := types.NewDetachedTimestamp( + header, + types.Timestamp{ + types.NewReverseStep(nil), + types.NewAttestationStep(&types.PendingAttestation{URI: "https://example.com"}), + }, + ) + + sdk, err := NewSDK() + if err != nil { + t.Fatalf("NewSDK() error = %v", err) + } + + result, err := sdk.Verify(context.Background(), stamp) + if err != nil { + t.Errorf("Verify() unexpected error = %v", err) + return + } + + if result.Status != types.VerifyPending { + t.Errorf("Verify() status = %v, want %v", result.Status, types.VerifyPending) + } +} + +func TestUpgrade_EmptyTimestamp(t *testing.T) { + header, err := types.NewDigestHeader(types.DigestSHA256, make([]byte, 32)) + if err != nil { + t.Fatalf("NewDigestHeader() error = %v", err) + } + stamp := types.NewDetachedTimestamp( + header, + types.Timestamp{}, + ) + + sdk, err := NewSDK() + if err != nil { + t.Fatalf("NewSDK() error = %v", err) + } + + results, err := sdk.Upgrade(context.Background(), stamp, false) + if err != nil { + t.Errorf("Upgrade() unexpected error = %v", err) + return + } + + if len(results) != 0 { + t.Errorf("Upgrade() expected 0 results for empty timestamp, got %d", len(results)) + } +} + +func TestStamp_Keccak256Hash(t *testing.T) { + ts := types.Timestamp{ + types.NewAttestationStep(&types.PendingAttestation{URI: "https://example.com"}), + } + + server := createMockCalendarServer(t, ts) + defer server.Close() + + sdk, err := NewSDK( + WithCalendars(server.URL), + WithTimeout(5*time.Second), + WithHashAlgorithm(HashKeccak256), + ) + if err != nil { + t.Fatalf("NewSDK() error = %v", err) + } + + header, err := types.NewDigestHeader(types.DigestKECCAK256, make([]byte, 32)) + if err != nil { + t.Fatalf("NewDigestHeader() error = %v", err) + } + + headers := []*types.DigestHeader{ + header, + } + + results, err := sdk.Stamp(context.Background(), headers) + if err != nil { + t.Fatalf("Stamp() error = %v", err) + } + + if len(results) != 1 { + t.Fatalf("expected 1 result, got %d", len(results)) + } + + hasKeccak := false + for _, step := range results[0].Timestamp { + if _, ok := step.(*types.Keccak256Step); ok { + hasKeccak = true + break + } + } + + if !hasKeccak { + t.Error("expected Keccak256 step in timestamp") + } +} + +func TestRequestAttestation_HTTPErrorStatus(t *testing.T) { + tests := []struct { + name string + statusCode int + wantErr bool + }{ + {"bad request", http.StatusBadRequest, true}, + {"not found", http.StatusNotFound, true}, + {"internal error", http.StatusInternalServerError, true}, + {"service unavailable", http.StatusServiceUnavailable, true}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(tt.statusCode) + })) + defer server.Close() + + sdk, err := NewSDK( + WithCalendars(server.URL), + WithTimeout(5*time.Second), + ) + if err != nil { + t.Fatalf("NewSDK() error = %v", err) + } + + header, err := types.NewDigestHeader(types.DigestSHA256, make([]byte, 32)) + if err != nil { + t.Fatalf("NewDigestHeader() error = %v", err) + } + + headers := []*types.DigestHeader{ + header, + } + + _, err = sdk.Stamp(context.Background(), headers) + if !tt.wantErr && err != nil { + t.Errorf("Stamp() unexpected error = %v", err) + } + if tt.wantErr && err == nil { + t.Error("Stamp() expected error, got nil") + } + }) + } +} + +func TestVerify_NilStamp(t *testing.T) { + sdk, err := NewSDK() + if err != nil { + t.Fatalf("NewSDK() error = %v", err) + } + + defer func() { + if r := recover(); r == nil { + t.Error("Verify() expected panic for nil stamp, got nil") + } + }() + + _, _ = sdk.Verify(context.Background(), nil) +} + +func TestStamp_LargeDigestCount(t *testing.T) { + if testing.Short() { + t.Skip("skipping large digest count test in short mode") + } + + ts := types.Timestamp{ + types.NewAttestationStep(&types.PendingAttestation{URI: "https://example.com"}), + } + + server := createMockCalendarServer(t, ts) + defer server.Close() + + sdk, err := NewSDK( + WithCalendars(server.URL), + WithTimeout(30*time.Second), + ) + if err != nil { + t.Fatalf("NewSDK() error = %v", err) + } + + numDigests := 100 + headers := make([]*types.DigestHeader, numDigests) + for i := range headers { + digest := make([]byte, 32) + rand.Read(digest) + header, err := types.NewDigestHeader(types.DigestSHA256, digest) + if err != nil { + t.Fatalf("NewDigestHeader() error = %v", err) + } + headers[i] = header + } + + results, err := sdk.Stamp(context.Background(), headers) + if err != nil { + t.Fatalf("Stamp() error = %v", err) + } + + if len(results) != numDigests { + t.Errorf("expected %d results, got %d", numDigests, len(results)) + } +} + +func TestUpgrade_MultiplePendingAttestations(t *testing.T) { + digest := make([]byte, 32) + + upgraded := types.Timestamp{ + types.NewAttestationStep(&types.BitcoinAttestation{Height: 800000}), + } + + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + enc := codec.NewEncoder() + enc.WriteTimestamp(upgraded) + w.Header().Set("Content-Type", "application/vnd.opentimestamps.v1") + w.WriteHeader(http.StatusOK) + w.Write(enc.Bytes()) + })) + defer server.Close() + + header, err := types.NewDigestHeader(types.DigestSHA256, digest) + if err != nil { + t.Fatalf("NewDigestHeader() error = %v", err) + } + + stamp := types.NewDetachedTimestamp( + header, + types.Timestamp{ + types.NewSHA256Step(nil), + types.NewAttestationStep(&types.PendingAttestation{URI: server.URL}), + types.NewSHA256Step(nil), + types.NewAttestationStep(&types.PendingAttestation{URI: server.URL}), + }, + ) + + sdk, err := NewSDK(WithTimeout(5 * time.Second)) + if err != nil { + t.Fatalf("NewSDK() error = %v", err) + } + + results, err := sdk.Upgrade(context.Background(), stamp, false) + if err != nil { + t.Errorf("Upgrade() unexpected error = %v", err) + return + } + + if len(results) != 2 { + t.Errorf("expected 2 upgrade results, got %d", len(results)) + } + + for i, result := range results { + if result.Status != types.UpgradeUpgraded { + t.Errorf("result[%d] status = %v, want %v", i, result.Status, types.UpgradeUpgraded) + } + } +} + +func readAllWithLimit(r io.Reader, limit int64) ([]byte, error) { + return io.ReadAll(io.LimitReader(r, limit)) +} diff --git a/packages/sdk-go/types/attestation.go b/packages/sdk-go/types/attestation.go new file mode 100644 index 0000000..0c023cc --- /dev/null +++ b/packages/sdk-go/types/attestation.go @@ -0,0 +1,156 @@ +package types + +import ( + "encoding/hex" + "fmt" + "strings" +) + +const TagSize = 8 + +var ( + BitcoinTag = [TagSize]byte{0x05, 0x88, 0x96, 0x0d, 0x73, 0xd7, 0x19, 0x01} + PendingTag = [TagSize]byte{0x83, 0xdf, 0xe3, 0x0d, 0x2e, 0xf9, 0x0c, 0x8e} + EASAttestTag = [TagSize]byte{0x8b, 0xf4, 0x6b, 0xf4, 0xcf, 0xd6, 0x74, 0xfa} + EASTimestampTag = [TagSize]byte{0x5a, 0xaf, 0xce, 0xeb, 0x1c, 0x7a, 0xd5, 0x8e} +) + +type AttestationKind int + +const ( + KindUnknown AttestationKind = iota + KindBitcoin + KindPending + KindEASAttestation + KindEASTimestamped +) + +func (k AttestationKind) String() string { + switch k { + case KindBitcoin: + return "bitcoin" + case KindPending: + return "pending" + case KindEASAttestation: + return "eas-attestation" + case KindEASTimestamped: + return "eas-timestamped" + default: + return "unknown" + } +} + +type BitcoinAttestation struct { + Height uint32 +} + +func (a *BitcoinAttestation) Kind() AttestationKind { return KindBitcoin } +func (a *BitcoinAttestation) Tag() [TagSize]byte { return BitcoinTag } + +func (a *BitcoinAttestation) String() string { + return fmt.Sprintf("Bitcoin at height %d", a.Height) +} + +type EASAttestation struct { + ChainID uint64 + UID [32]byte +} + +func (a *EASAttestation) Kind() AttestationKind { return KindEASAttestation } +func (a *EASAttestation) Tag() [TagSize]byte { return EASAttestTag } + +func (a *EASAttestation) String() string { + return fmt.Sprintf("EAS attestation %s on chain %d", hex.EncodeToString(a.UID[:]), a.ChainID) +} + +type EASTimestamped struct { + ChainID uint64 +} + +func (a *EASTimestamped) Kind() AttestationKind { return KindEASTimestamped } +func (a *EASTimestamped) Tag() [TagSize]byte { return EASTimestampTag } + +func (a *EASTimestamped) String() string { + return fmt.Sprintf("EAS timestamped on chain %d", a.ChainID) +} + +type PendingAttestation struct { + URI string +} + +const MaxURILen = 1000 + +func ValidateURI(uri string) bool { + if len(uri) > MaxURILen { + return false + } + for _, ch := range uri { + if !((ch >= 'a' && ch <= 'z') || + (ch >= 'A' && ch <= 'Z') || + (ch >= '0' && ch <= '9') || + ch == '.' || ch == '-' || ch == '_' || ch == '/' || ch == ':') { + return false + } + } + return true +} + +func (a *PendingAttestation) Kind() AttestationKind { return KindPending } +func (a *PendingAttestation) Tag() [TagSize]byte { return PendingTag } + +func (a *PendingAttestation) String() string { + return fmt.Sprintf("Pending at %s", a.URI) +} + +func (a *PendingAttestation) Valid() bool { + return ValidateURI(a.URI) +} + +type UnknownAttestation struct { + tag [TagSize]byte + Data []byte +} + +func (a *UnknownAttestation) Kind() AttestationKind { return KindUnknown } +func (a *UnknownAttestation) Tag() [TagSize]byte { return a.tag } + +func NewUnknownAttestation(tag [TagSize]byte, data []byte) *UnknownAttestation { + return &UnknownAttestation{tag: tag, Data: data} +} + +func (a *UnknownAttestation) String() string { + tag := a.Tag() + return fmt.Sprintf("Unknown Attestation with tag %s", hex.EncodeToString(tag[:])) +} + +func AttestationKindFromTag(tag [TagSize]byte) AttestationKind { + switch tag { + case BitcoinTag: + return KindBitcoin + case PendingTag: + return KindPending + case EASAttestTag: + return KindEASAttestation + case EASTimestampTag: + return KindEASTimestamped + default: + return KindUnknown + } +} + +func ParseAttestationKind(s string) (AttestationKind, bool) { + switch strings.ToLower(s) { + case "bitcoin": + return KindBitcoin, true + case "pending": + return KindPending, true + case "eas-attestation": + return KindEASAttestation, true + case "eas-timestamped": + return KindEASTimestamped, true + case "unknown": + return KindUnknown, true + default: + return KindUnknown, false + } +} diff --git a/packages/sdk-go/types/attestation_test.go b/packages/sdk-go/types/attestation_test.go new file mode 100644 index 0000000..31f5a1c --- /dev/null +++ b/packages/sdk-go/types/attestation_test.go @@ -0,0 +1,251 @@ +package types + +import ( + "testing" +) + +func TestAttestationKindString(t *testing.T) { + tests := []struct { + kind AttestationKind + want string + }{ + {KindBitcoin, "bitcoin"}, + {KindPending, "pending"}, + {KindEASAttestation, "eas-attestation"}, + {KindEASTimestamped, "eas-timestamped"}, + {KindUnknown, "unknown"}, + {AttestationKind(99), "unknown"}, + } + + for _, tt := range tests { + t.Run(tt.want, func(t *testing.T) { + if got := tt.kind.String(); got != tt.want { + t.Errorf("AttestationKind.String() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestBitcoinAttestation(t *testing.T) { + att := &BitcoinAttestation{Height: 800000} + + if att.Kind() != KindBitcoin { + t.Errorf("BitcoinAttestation.Kind() = %v, want %v", att.Kind(), KindBitcoin) + } + + if att.Tag() != BitcoinTag { + t.Errorf("BitcoinAttestation.Tag() = %v, want %v", att.Tag(), BitcoinTag) + } + + expectedStr := "Bitcoin at height 800000" + if att.String() != expectedStr { + t.Errorf("BitcoinAttestation.String() = %v, want %v", att.String(), expectedStr) + } + + var _ Attestation = att +} + +func TestEASAttestation(t *testing.T) { + var uid [32]byte + for i := range uid { + uid[i] = byte(i) + } + + att := &EASAttestation{ + ChainID: 1, + UID: uid, + } + + if att.Kind() != KindEASAttestation { + t.Errorf("EASAttestation.Kind() = %v, want %v", att.Kind(), KindEASAttestation) + } + + if att.Tag() != EASAttestTag { + t.Errorf("EASAttestation.Tag() = %v, want %v", att.Tag(), EASAttestTag) + } + + var _ Attestation = att +} + +func TestEASTimestamped(t *testing.T) { + att := &EASTimestamped{ChainID: 534352} + + if att.Kind() != KindEASTimestamped { + t.Errorf("EASTimestamped.Kind() = %v, want %v", att.Kind(), KindEASTimestamped) + } + + if att.Tag() != EASTimestampTag { + t.Errorf("EASTimestamped.Tag() = %v, want %v", att.Tag(), EASTimestampTag) + } + + expectedStr := "EAS timestamped on chain 534352" + if att.String() != expectedStr { + t.Errorf("EASTimestamped.String() = %v, want %v", att.String(), expectedStr) + } + + var _ Attestation = att +} + +func TestPendingAttestation(t *testing.T) { + att := &PendingAttestation{URI: "https://example.com/calendar"} + + if att.Kind() != KindPending { + t.Errorf("PendingAttestation.Kind() = %v, want %v", att.Kind(), KindPending) + } + + if att.Tag() != PendingTag { + t.Errorf("PendingAttestation.Tag() = %v, want %v", att.Tag(), PendingTag) + } + + expectedStr := "Pending at https://example.com/calendar" + if att.String() != expectedStr { + t.Errorf("PendingAttestation.String() = %v, want %v", att.String(), expectedStr) + } + + var _ Attestation = att +} + +func TestValidateURI(t *testing.T) { + maxURIStr := make([]byte, MaxURILen) + for i := range maxURIStr { + maxURIStr[i] = 'a' + } + + tests := []struct { + name string + uri string + want bool + }{ + {"valid simple", "https://example.com", true}, + {"valid with path", "https://example.com/calendar", true}, + {"valid with port", "https://example.com:8080/path", true}, + {"alphanumeric", "abc123DEF", true}, + {"with special chars", "a-b_c.d/e:f", true}, + {"empty string", "", true}, + {"space invalid", "has space", false}, + {"at sign invalid", "user@host", false}, + {"percent invalid", "percent%20encoded", false}, + {"unicode invalid", "unicode\u0000null", false}, + {"too long", string(make([]byte, MaxURILen+1)), false}, + {"max length", string(maxURIStr), true}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := ValidateURI(tt.uri); got != tt.want { + t.Errorf("ValidateURI(%q) = %v, want %v", tt.uri, got, tt.want) + } + }) + } +} + +func TestPendingAttestationValid(t *testing.T) { + tests := []struct { + name string + uri string + want bool + }{ + {"valid URI", "https://example.com", true}, + {"invalid URI", "has space", false}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + att := &PendingAttestation{URI: tt.uri} + if got := att.Valid(); got != tt.want { + t.Errorf("PendingAttestation.Valid() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestUnknownAttestation(t *testing.T) { + tag := [TagSize]byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08} + data := []byte("somedata") + + att := NewUnknownAttestation(tag, data) + + if att.Kind() != KindUnknown { + t.Errorf("UnknownAttestation.Kind() = %v, want %v", att.Kind(), KindUnknown) + } + + if att.Tag() != tag { + t.Errorf("UnknownAttestation.Tag() = %v, want %v", att.Tag(), tag) + } + + if string(att.Data) != string(data) { + t.Errorf("UnknownAttestation.Data = %v, want %v", att.Data, data) + } +} + +func TestAttestationKindFromTag(t *testing.T) { + tests := []struct { + name string + tag [TagSize]byte + want AttestationKind + }{ + {"bitcoin", BitcoinTag, KindBitcoin}, + {"pending", PendingTag, KindPending}, + {"eas attestation", EASAttestTag, KindEASAttestation}, + {"eas timestamped", EASTimestampTag, KindEASTimestamped}, + {"unknown", [TagSize]byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, KindUnknown}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := AttestationKindFromTag(tt.tag); got != tt.want { + t.Errorf("AttestationKindFromTag() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestParseAttestationKind(t *testing.T) { + tests := []struct { + input string + want AttestationKind + wantOk bool + }{ + {"bitcoin", KindBitcoin, true}, + {"BITCOIN", KindBitcoin, true}, + {"pending", KindPending, true}, + {"Pending", KindPending, true}, + {"eas-attestation", KindEASAttestation, true}, + {"EAS-ATTESTATION", KindEASAttestation, true}, + {"eas-timestamped", KindEASTimestamped, true}, + {"unknown", KindUnknown, true}, + {"invalid", KindUnknown, false}, + {"", KindUnknown, false}, + } + + for _, tt := range tests { + t.Run(tt.input, func(t *testing.T) { + got, gotOk := ParseAttestationKind(tt.input) + if got != tt.want || gotOk != tt.wantOk { + t.Errorf("ParseAttestationKind(%q) = (%v, %v), want (%v, %v)", tt.input, got, gotOk, tt.want, tt.wantOk) + } + }) + } +} + +func TestTagConstants(t *testing.T) { + if BitcoinTag == [TagSize]byte{} { + t.Error("BitcoinTag should not be zero value") + } + if PendingTag == [TagSize]byte{} { + t.Error("PendingTag should not be zero value") + } + if EASAttestTag == [TagSize]byte{} { + t.Error("EASAttestTag should not be zero value") + } + if EASTimestampTag == [TagSize]byte{} { + t.Error("EASTimestampTag should not be zero value") + } + + if BitcoinTag == PendingTag { + t.Error("BitcoinTag and PendingTag should be different") + } + if EASAttestTag == EASTimestampTag { + t.Error("EASAttestTag and EASTimestampTag should be different") + } +} diff --git a/packages/sdk-go/types/doc.go b/packages/sdk-go/types/doc.go new file mode 100644 index 0000000..894faab --- /dev/null +++ b/packages/sdk-go/types/doc.go @@ -0,0 +1,11 @@ +// Package types defines core timestamp types for the UTS protocol. +// +// A Timestamp represents a sequence of operations (steps) that transform an input +// digest, ultimately leading to an attestation. Each step either transforms the +// current value or branches (FORK). +// +// Operation types: +// - Transform ops: APPEND, PREPEND, REVERSE, HEXLIFY +// - Hash ops: SHA1, RIPEMD160, SHA256, KECCAK256 +// - Control ops: ATTESTATION, FORK +package types diff --git a/packages/sdk-go/types/header.go b/packages/sdk-go/types/header.go new file mode 100644 index 0000000..0841a05 --- /dev/null +++ b/packages/sdk-go/types/header.go @@ -0,0 +1,100 @@ +package types + +import "fmt" + +type DigestOp byte + +const ( + DigestSHA1 DigestOp = 0x02 + DigestRIPEMD160 DigestOp = 0x03 + DigestSHA256 DigestOp = 0x08 + DigestKECCAK256 DigestOp = 0x67 +) + +func (op DigestOp) String() string { + switch op { + case DigestSHA1: + return "SHA1" + case DigestRIPEMD160: + return "RIPEMD160" + case DigestSHA256: + return "SHA256" + case DigestKECCAK256: + return "KECCAK256" + default: + return "UNKNOWN" + } +} + +func (op DigestOp) Valid() bool { + switch op { + case DigestSHA1, DigestRIPEMD160, DigestSHA256, DigestKECCAK256: + return true + default: + return false + } +} + +func (op DigestOp) OutputSize() int { + switch op { + case DigestSHA1: + return 20 + case DigestRIPEMD160: + return 20 + case DigestSHA256: + return 32 + case DigestKECCAK256: + return 32 + default: + return 0 + } +} + +func NewDigestOp(b byte) (DigestOp, bool) { + op := DigestOp(b) + return op, op.Valid() +} + +type DigestHeader struct { + kind DigestOp + digest []byte +} + +func NewDigestHeader(kind DigestOp, digest []byte) (*DigestHeader, error) { + if len(digest) != kind.OutputSize() { + return nil, fmt.Errorf("invalid digest length: expected %d, got %d", kind.OutputSize(), len(digest)) + } + h := &DigestHeader{kind: kind, digest: make([]byte, len(digest))} + copy(h.digest, digest) + return h, nil +} + +func (h *DigestHeader) Kind() DigestOp { + return h.kind +} + +func (h *DigestHeader) DigestBytes() []byte { + d := make([]byte, len(h.digest)) + copy(d, h.digest) + return d +} + +func (h *DigestHeader) String() string { + return fmt.Sprintf("%s %x", h.kind, h.DigestBytes()) +} + +type DetachedTimestamp struct { + Header *DigestHeader + Timestamp Timestamp +} + +func NewDetachedTimestamp(header *DigestHeader, ts Timestamp) *DetachedTimestamp { + return &DetachedTimestamp{ + Header: header, + Timestamp: ts, + } +} + +func (dt *DetachedTimestamp) String() string { + return fmt.Sprintf("digest of %s\n%s", dt.Header, dt.Timestamp) +} diff --git a/packages/sdk-go/types/header_test.go b/packages/sdk-go/types/header_test.go new file mode 100644 index 0000000..f434f56 --- /dev/null +++ b/packages/sdk-go/types/header_test.go @@ -0,0 +1,226 @@ +package types + +import ( + "bytes" + "testing" +) + +func TestDigestOpString(t *testing.T) { + tests := []struct { + op DigestOp + want string + }{ + {DigestSHA1, "SHA1"}, + {DigestRIPEMD160, "RIPEMD160"}, + {DigestSHA256, "SHA256"}, + {DigestKECCAK256, "KECCAK256"}, + {DigestOp(0x99), "UNKNOWN"}, + } + + for _, tt := range tests { + t.Run(tt.want, func(t *testing.T) { + if got := tt.op.String(); got != tt.want { + t.Errorf("DigestOp.String() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestDigestOpValid(t *testing.T) { + tests := []struct { + op DigestOp + want bool + }{ + {DigestSHA1, true}, + {DigestRIPEMD160, true}, + {DigestSHA256, true}, + {DigestKECCAK256, true}, + {DigestOp(0x00), false}, + {DigestOp(0x99), false}, + } + + for _, tt := range tests { + name := tt.op.String() + t.Run(name, func(t *testing.T) { + if got := tt.op.Valid(); got != tt.want { + t.Errorf("DigestOp.Valid() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestDigestOpOutputSize(t *testing.T) { + tests := []struct { + op DigestOp + want int + }{ + {DigestSHA1, 20}, + {DigestRIPEMD160, 20}, + {DigestSHA256, 32}, + {DigestKECCAK256, 32}, + {DigestOp(0x99), 0}, + } + + for _, tt := range tests { + name := tt.op.String() + t.Run(name, func(t *testing.T) { + if got := tt.op.OutputSize(); got != tt.want { + t.Errorf("DigestOp.OutputSize() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestNewDigestOp(t *testing.T) { + tests := []struct { + b byte + want DigestOp + wantOk bool + }{ + {0x02, DigestSHA1, true}, + {0x03, DigestRIPEMD160, true}, + {0x08, DigestSHA256, true}, + {0x67, DigestKECCAK256, true}, + {0x99, DigestOp(0x99), false}, + } + + for _, tt := range tests { + t.Run(string(rune(tt.b)), func(t *testing.T) { + got, gotOk := NewDigestOp(tt.b) + if got != tt.want || gotOk != tt.wantOk { + t.Errorf("NewDigestOp() = (%v, %v), want (%v, %v)", got, gotOk, tt.want, tt.wantOk) + } + }) + } +} + +func TestNewDigestHeader(t *testing.T) { + digest := make([]byte, 32) + for i := range digest { + digest[i] = byte(i) + } + + header, err := NewDigestHeader(DigestSHA256, digest) + if err != nil { + t.Fatalf("NewDigestHeader() error = %v", err) + } + + if header.Kind() != DigestSHA256 { + t.Errorf("DigestHeader.Kind = %v, want %v", header.Kind(), DigestSHA256) + } + + if !bytes.Equal(header.DigestBytes(), digest) { + t.Errorf("DigestHeader.Digest not set correctly") + } +} + +func TestDigestHeaderDigestBytes(t *testing.T) { + tests := []struct { + name string + kind DigestOp + wantLen int + }{ + {"SHA256", DigestSHA256, 32}, + {"SHA1", DigestSHA1, 20}, + {"RIPEMD160", DigestRIPEMD160, 20}, + {"KECCAK256", DigestKECCAK256, 32}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + digest := make([]byte, tt.wantLen) + for i := range digest { + digest[i] = byte(i) + } + header, err := NewDigestHeader(tt.kind, digest) + if err != nil { + t.Fatalf("NewDigestHeader() error = %v", err) + } + got := header.DigestBytes() + if len(got) != tt.wantLen { + t.Errorf("DigestBytes() length = %v, want %v", len(got), tt.wantLen) + } + if !bytes.Equal(got, digest) { + t.Errorf("DigestBytes() content mismatch") + } + }) + } +} + +func TestDigestHeaderString(t *testing.T) { + digest := make([]byte, 32) + for i := range digest { + digest[i] = byte(i) + } + + header, err := NewDigestHeader(DigestSHA256, digest) + if err != nil { + t.Fatalf("NewDigestHeader() error = %v", err) + } + str := header.String() + + if str == "" { + t.Error("DigestHeader.String() should not be empty") + } +} + +func TestDetachedTimestamp(t *testing.T) { + digest := make([]byte, 32) + for i := range digest { + digest[i] = byte(i) + } + + header, err := NewDigestHeader(DigestSHA256, digest) + if err != nil { + t.Fatalf("NewDigestHeader() error = %v", err) + } + ts := Timestamp{ + NewSHA256Step(Timestamp{ + NewAttestationStep(&BitcoinAttestation{Height: 123}), + }), + } + + dt := NewDetachedTimestamp(header, ts) + + if dt.Header != header { + t.Error("DetachedTimestamp.Header not set correctly") + } + + if len(dt.Timestamp) != 1 { + t.Error("DetachedTimestamp.Timestamp not set correctly") + } +} + +func TestDetachedTimestampString(t *testing.T) { + digest := make([]byte, 32) + for i := range digest { + digest[i] = byte(i) + } + + header, err := NewDigestHeader(DigestSHA256, digest) + if err != nil { + t.Fatalf("NewDigestHeader() error = %v", err) + } + ts := Timestamp{} + dt := NewDetachedTimestamp(header, ts) + + str := dt.String() + if str == "" { + t.Error("DetachedTimestamp.String() should not be empty") + } +} + +func TestDigestOpConstants(t *testing.T) { + if DigestSHA1 != DigestOp(0x02) { + t.Errorf("DigestSHA1 = %v, want 0x02", DigestSHA1) + } + if DigestRIPEMD160 != DigestOp(0x03) { + t.Errorf("DigestRIPEMD160 = %v, want 0x03", DigestRIPEMD160) + } + if DigestSHA256 != DigestOp(0x08) { + t.Errorf("DigestSHA256 = %v, want 0x08", DigestSHA256) + } + if DigestKECCAK256 != DigestOp(0x67) { + t.Errorf("DigestKECCAK256 = %v, want 0x67", DigestKECCAK256) + } +} diff --git a/packages/sdk-go/types/status.go b/packages/sdk-go/types/status.go new file mode 100644 index 0000000..0a1d3c2 --- /dev/null +++ b/packages/sdk-go/types/status.go @@ -0,0 +1,138 @@ +package types + +import "fmt" + +type AttestationStatusKind int + +const ( + StatusValid AttestationStatusKind = iota + StatusInvalid + StatusPending + StatusUnknown +) + +func (k AttestationStatusKind) String() string { + switch k { + case StatusValid: + return "valid" + case StatusInvalid: + return "invalid" + case StatusPending: + return "pending" + case StatusUnknown: + return "unknown" + default: + return "unknown" + } +} + +type AttestationStatus struct { + Attestation Attestation + Status AttestationStatusKind + Error error + Info map[string]interface{} +} + +func NewAttestationStatus(att Attestation, status AttestationStatusKind, err error) *AttestationStatus { + return &AttestationStatus{ + Attestation: att, + Status: status, + Error: err, + } +} + +func (s *AttestationStatus) String() string { + if s.Error != nil { + return fmt.Sprintf("%s: %s (%s)", s.Attestation, s.Status, s.Error) + } + return fmt.Sprintf("%s: %s", s.Attestation, s.Status) +} + +type VerifyStatus int + +const ( + VerifyValid VerifyStatus = iota + VerifyPartialValid + VerifyInvalid + VerifyPending +) + +func (s VerifyStatus) String() string { + switch s { + case VerifyValid: + return "valid" + case VerifyPartialValid: + return "partial_valid" + case VerifyInvalid: + return "invalid" + case VerifyPending: + return "pending" + default: + return "unknown" + } +} + +type UpgradeStatus int + +const ( + UpgradeUpgraded UpgradeStatus = iota + UpgradePending + UpgradeFailed +) + +func (s UpgradeStatus) String() string { + switch s { + case UpgradeUpgraded: + return "upgraded" + case UpgradePending: + return "pending" + case UpgradeFailed: + return "failed" + default: + return "unknown" + } +} + +type UpgradeResult struct { + Status UpgradeStatus + Timestamp *DetachedTimestamp + Error error + Attestations []Attestation +} + +func NewUpgradeResult(status UpgradeStatus, ts *DetachedTimestamp, err error) *UpgradeResult { + return &UpgradeResult{ + Status: status, + Timestamp: ts, + Error: err, + } +} + +type VerificationResult struct { + Status VerifyStatus + Attestations []*AttestationStatus + Error error +} + +func NewVerificationResult(status VerifyStatus, atts []*AttestationStatus) *VerificationResult { + return &VerificationResult{ + Status: status, + Attestations: atts, + } +} + +func (r *VerificationResult) HasValidAttestation() bool { + for _, att := range r.Attestations { + if att.Status == StatusValid { + return true + } + } + return false +} + +func (r *VerificationResult) String() string { + if r.Error != nil { + return fmt.Sprintf("%s (error: %s)", r.Status, r.Error) + } + return fmt.Sprintf("%s (%d attestations)", r.Status, len(r.Attestations)) +} diff --git a/packages/sdk-go/types/timestamp.go b/packages/sdk-go/types/timestamp.go new file mode 100644 index 0000000..d2059de --- /dev/null +++ b/packages/sdk-go/types/timestamp.go @@ -0,0 +1,280 @@ +package types + +import "encoding/hex" + +type Op byte + +const ( + OpAttestation Op = 0x00 + OpSHA1 Op = 0x02 + OpRIPEMD160 Op = 0x03 + OpSHA256 Op = 0x08 + OpKECCAK256 Op = 0x67 + OpAPPEND Op = 0xf0 + OpPREPEND Op = 0xf1 + OpREVERSE Op = 0xf2 + OpHEXLIFY Op = 0xf3 + OpFORK Op = 0xff +) + +func (op Op) String() string { + switch op { + case OpAttestation: + return "ATTESTATION" + case OpSHA1: + return "SHA1" + case OpRIPEMD160: + return "RIPEMD160" + case OpSHA256: + return "SHA256" + case OpKECCAK256: + return "KECCAK256" + case OpAPPEND: + return "APPEND" + case OpPREPEND: + return "PREPEND" + case OpREVERSE: + return "REVERSE" + case OpHEXLIFY: + return "HEXLIFY" + case OpFORK: + return "FORK" + default: + return "UNKNOWN" + } +} + +func (op Op) HasImmediate() bool { + return op == OpAPPEND || op == OpPREPEND +} + +func (op Op) IsControl() bool { + return op == OpAttestation || op == OpFORK +} + +func (op Op) IsDigest() bool { + return op == OpSHA1 || op == OpRIPEMD160 || op == OpSHA256 || op == OpKECCAK256 +} + +func (op Op) Valid() bool { + switch op { + case OpAttestation, OpSHA1, OpRIPEMD160, OpSHA256, OpKECCAK256, + OpAPPEND, OpPREPEND, OpREVERSE, OpHEXLIFY, OpFORK: + return true + default: + return false + } +} + +func NewOp(b byte) (Op, bool) { + op := Op(b) + return op, op.Valid() +} + +type Step interface { + Op() Op +} + +type baseStep struct { + op Op +} + +func (s *baseStep) Op() Op { + return s.op +} + +type AppendStep struct { + baseStep + Data []byte + Next Timestamp +} + +func NewAppendStep(data []byte, next Timestamp) *AppendStep { + return &AppendStep{ + baseStep: baseStep{op: OpAPPEND}, + Data: data, + Next: next, + } +} + +type PrependStep struct { + baseStep + Data []byte + Next Timestamp +} + +func NewPrependStep(data []byte, next Timestamp) *PrependStep { + return &PrependStep{ + baseStep: baseStep{op: OpPREPEND}, + Data: data, + Next: next, + } +} + +type ReverseStep struct { + baseStep + Next Timestamp +} + +func NewReverseStep(next Timestamp) *ReverseStep { + return &ReverseStep{ + baseStep: baseStep{op: OpREVERSE}, + Next: next, + } +} + +type HexlifyStep struct { + baseStep + Next Timestamp +} + +func NewHexlifyStep(next Timestamp) *HexlifyStep { + return &HexlifyStep{ + baseStep: baseStep{op: OpHEXLIFY}, + Next: next, + } +} + +type SHA256Step struct { + baseStep + Next Timestamp +} + +func NewSHA256Step(next Timestamp) *SHA256Step { + return &SHA256Step{ + baseStep: baseStep{op: OpSHA256}, + Next: next, + } +} + +type Keccak256Step struct { + baseStep + Next Timestamp +} + +func NewKeccak256Step(next Timestamp) *Keccak256Step { + return &Keccak256Step{ + baseStep: baseStep{op: OpKECCAK256}, + Next: next, + } +} + +type SHA1Step struct { + baseStep + Next Timestamp +} + +func NewSHA1Step(next Timestamp) *SHA1Step { + return &SHA1Step{ + baseStep: baseStep{op: OpSHA1}, + Next: next, + } +} + +type RIPEMD160Step struct { + baseStep + Next Timestamp +} + +func NewRIPEMD160Step(next Timestamp) *RIPEMD160Step { + return &RIPEMD160Step{ + baseStep: baseStep{op: OpRIPEMD160}, + Next: next, + } +} + +type ForkStep struct { + baseStep + Branches []Timestamp +} + +func NewForkStep(branches []Timestamp) *ForkStep { + return &ForkStep{ + baseStep: baseStep{op: OpFORK}, + Branches: branches, + } +} + +type Attestation interface { + Tag() [8]byte +} + +type AttestationStep struct { + baseStep + Attestation Attestation +} + +func NewAttestationStep(att Attestation) *AttestationStep { + return &AttestationStep{ + baseStep: baseStep{op: OpAttestation}, + Attestation: att, + } +} + +type Timestamp []Step + +type rawAttestationStep struct { + baseStep + tag [8]byte + data []byte +} + +func (s *rawAttestationStep) Tag() [8]byte { + return s.tag +} + +func (s *rawAttestationStep) Data() []byte { + return s.data +} + +func newRawAttestationStep(tag [8]byte, data []byte) *rawAttestationStep { + return &rawAttestationStep{ + baseStep: baseStep{op: OpAttestation}, + tag: tag, + data: data, + } +} + +func (s *baseStep) String() string { + return s.op.String() +} + +func (s *AppendStep) String() string { + return "APPEND " + hex.EncodeToString(s.Data) +} + +func (s *PrependStep) String() string { + return "PREPEND " + hex.EncodeToString(s.Data) +} + +func (s *ReverseStep) String() string { + return "REVERSE" +} + +func (s *HexlifyStep) String() string { + return "HEXLIFY" +} + +func (s *SHA256Step) String() string { + return "SHA256" +} + +func (s *Keccak256Step) String() string { + return "KECCAK256" +} + +func (s *SHA1Step) String() string { + return "SHA1" +} + +func (s *RIPEMD160Step) String() string { + return "RIPEMD160" +} + +func (s *ForkStep) String() string { + return "FORK" +} + +func (s *AttestationStep) String() string { + return "ATTESTATION" +} diff --git a/packages/sdk-go/types/timestamp_test.go b/packages/sdk-go/types/timestamp_test.go new file mode 100644 index 0000000..5dcd47e --- /dev/null +++ b/packages/sdk-go/types/timestamp_test.go @@ -0,0 +1,260 @@ +package types + +import ( + "testing" +) + +func TestOpString(t *testing.T) { + tests := []struct { + op Op + want string + }{ + {OpAttestation, "ATTESTATION"}, + {OpSHA1, "SHA1"}, + {OpRIPEMD160, "RIPEMD160"}, + {OpSHA256, "SHA256"}, + {OpKECCAK256, "KECCAK256"}, + {OpAPPEND, "APPEND"}, + {OpPREPEND, "PREPEND"}, + {OpREVERSE, "REVERSE"}, + {OpHEXLIFY, "HEXLIFY"}, + {OpFORK, "FORK"}, + {Op(0x99), "UNKNOWN"}, + } + + for _, tt := range tests { + t.Run(tt.want, func(t *testing.T) { + if got := tt.op.String(); got != tt.want { + t.Errorf("Op.String() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestOpHasImmediate(t *testing.T) { + tests := []struct { + op Op + want bool + }{ + {OpAPPEND, true}, + {OpPREPEND, true}, + {OpSHA256, false}, + {OpAttestation, false}, + {OpFORK, false}, + {OpREVERSE, false}, + } + + for _, tt := range tests { + name := tt.op.String() + t.Run(name, func(t *testing.T) { + if got := tt.op.HasImmediate(); got != tt.want { + t.Errorf("Op.HasImmediate() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestOpIsControl(t *testing.T) { + tests := []struct { + op Op + want bool + }{ + {OpAttestation, true}, + {OpFORK, true}, + {OpSHA256, false}, + {OpAPPEND, false}, + {OpPREPEND, false}, + {OpREVERSE, false}, + } + + for _, tt := range tests { + name := tt.op.String() + t.Run(name, func(t *testing.T) { + if got := tt.op.IsControl(); got != tt.want { + t.Errorf("Op.IsControl() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestOpIsDigest(t *testing.T) { + tests := []struct { + op Op + want bool + }{ + {OpSHA1, true}, + {OpRIPEMD160, true}, + {OpSHA256, true}, + {OpKECCAK256, true}, + {OpAPPEND, false}, + {OpPREPEND, false}, + {OpAttestation, false}, + {OpFORK, false}, + } + + for _, tt := range tests { + name := tt.op.String() + t.Run(name, func(t *testing.T) { + if got := tt.op.IsDigest(); got != tt.want { + t.Errorf("Op.IsDigest() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestOpValid(t *testing.T) { + tests := []struct { + op Op + want bool + }{ + {OpAttestation, true}, + {OpSHA1, true}, + {OpRIPEMD160, true}, + {OpSHA256, true}, + {OpKECCAK256, true}, + {OpAPPEND, true}, + {OpPREPEND, true}, + {OpREVERSE, true}, + {OpHEXLIFY, true}, + {OpFORK, true}, + {Op(0x00), true}, + {Op(0x99), false}, + {Op(0xfe), false}, + } + + for _, tt := range tests { + name := tt.op.String() + t.Run(name, func(t *testing.T) { + if got := tt.op.Valid(); got != tt.want { + t.Errorf("Op.Valid() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestNewOp(t *testing.T) { + tests := []struct { + b byte + want Op + wantOk bool + }{ + {0x00, OpAttestation, true}, + {0x02, OpSHA1, true}, + {0x08, OpSHA256, true}, + {0x67, OpKECCAK256, true}, + {0xf0, OpAPPEND, true}, + {0xff, OpFORK, true}, + {0x99, Op(0x99), false}, + } + + for _, tt := range tests { + name := string(rune(tt.b)) + t.Run(name, func(t *testing.T) { + got, gotOk := NewOp(tt.b) + if got != tt.want || gotOk != tt.wantOk { + t.Errorf("NewOp() = (%v, %v), want (%v, %v)", got, gotOk, tt.want, tt.wantOk) + } + }) + } +} + +func TestStepConstructors(t *testing.T) { + dummyTS := Timestamp{} + + t.Run("AppendStep", func(t *testing.T) { + data := []byte("test") + step := NewAppendStep(data, dummyTS) + if step.Op() != OpAPPEND { + t.Errorf("AppendStep.Op() = %v, want %v", step.Op(), OpAPPEND) + } + if string(step.Data) != string(data) { + t.Errorf("AppendStep.Data = %v, want %v", step.Data, data) + } + }) + + t.Run("PrependStep", func(t *testing.T) { + data := []byte("test") + step := NewPrependStep(data, dummyTS) + if step.Op() != OpPREPEND { + t.Errorf("PrependStep.Op() = %v, want %v", step.Op(), OpPREPEND) + } + if string(step.Data) != string(data) { + t.Errorf("PrependStep.Data = %v, want %v", step.Data, data) + } + }) + + t.Run("ReverseStep", func(t *testing.T) { + step := NewReverseStep(dummyTS) + if step.Op() != OpREVERSE { + t.Errorf("ReverseStep.Op() = %v, want %v", step.Op(), OpREVERSE) + } + }) + + t.Run("HexlifyStep", func(t *testing.T) { + step := NewHexlifyStep(dummyTS) + if step.Op() != OpHEXLIFY { + t.Errorf("HexlifyStep.Op() = %v, want %v", step.Op(), OpHEXLIFY) + } + }) + + t.Run("SHA256Step", func(t *testing.T) { + step := NewSHA256Step(dummyTS) + if step.Op() != OpSHA256 { + t.Errorf("SHA256Step.Op() = %v, want %v", step.Op(), OpSHA256) + } + }) + + t.Run("Keccak256Step", func(t *testing.T) { + step := NewKeccak256Step(dummyTS) + if step.Op() != OpKECCAK256 { + t.Errorf("Keccak256Step.Op() = %v, want %v", step.Op(), OpKECCAK256) + } + }) + + t.Run("SHA1Step", func(t *testing.T) { + step := NewSHA1Step(dummyTS) + if step.Op() != OpSHA1 { + t.Errorf("SHA1Step.Op() = %v, want %v", step.Op(), OpSHA1) + } + }) + + t.Run("RIPEMD160Step", func(t *testing.T) { + step := NewRIPEMD160Step(dummyTS) + if step.Op() != OpRIPEMD160 { + t.Errorf("RIPEMD160Step.Op() = %v, want %v", step.Op(), OpRIPEMD160) + } + }) + + t.Run("ForkStep", func(t *testing.T) { + branches := []Timestamp{dummyTS, dummyTS} + step := NewForkStep(branches) + if step.Op() != OpFORK { + t.Errorf("ForkStep.Op() = %v, want %v", step.Op(), OpFORK) + } + if len(step.Branches) != 2 { + t.Errorf("ForkStep.Branches length = %v, want 2", len(step.Branches)) + } + }) + + t.Run("AttestationStep", func(t *testing.T) { + att := &BitcoinAttestation{Height: 123} + step := NewAttestationStep(att) + if step.Op() != OpAttestation { + t.Errorf("AttestationStep.Op() = %v, want %v", step.Op(), OpAttestation) + } + }) +} + +func TestStepInterface(t *testing.T) { + dummyTS := Timestamp{} + var _ Step = NewAppendStep([]byte("test"), dummyTS) + var _ Step = NewPrependStep([]byte("test"), dummyTS) + var _ Step = NewReverseStep(dummyTS) + var _ Step = NewHexlifyStep(dummyTS) + var _ Step = NewSHA256Step(dummyTS) + var _ Step = NewKeccak256Step(dummyTS) + var _ Step = NewSHA1Step(dummyTS) + var _ Step = NewRIPEMD160Step(dummyTS) + var _ Step = NewForkStep(nil) + var _ Step = NewAttestationStep(&BitcoinAttestation{}) +} diff --git a/packages/sdk-py/.gitignore b/packages/sdk-py/.gitignore new file mode 100644 index 0000000..b0eafc4 --- /dev/null +++ b/packages/sdk-py/.gitignore @@ -0,0 +1,58 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[codz] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py.cover +.hypothesis/ +.pytest_cache/ +cover/ +.mypy_cache/ +.ruff_cache/ + +# pixi +# Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control. +# pixi.lock +# Pixi creates a virtual environment in the .pixi directory, just like venv module creates one +# in the .venv directory. It is recommended not to include this directory in version control. +.pixi + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ diff --git a/packages/sdk-py/AGENTS.md b/packages/sdk-py/AGENTS.md new file mode 100644 index 0000000..c7778c3 --- /dev/null +++ b/packages/sdk-py/AGENTS.md @@ -0,0 +1,17 @@ +# Python SDK Development Commands + +## Code Quality + +- **Type check**: `cd packages/sdk-py && poetry run mypy src/` +- **Lint**: `cd packages/sdk-py && poetry run ruff check src/ tests/` +- **Format**: `cd packages/sdk-py && poetry run ruff format src/ tests/` + +## Testing + +- **Run all tests**: `cd packages/sdk-py && poetry run pytest` +- **Run tests with coverage**: `cd packages/sdk-py && poetry run pytest --cov=uts_sdk --cov-report=term-missing` +- **Run specific test**: `cd packages/sdk-py && poetry run pytest tests/test_sdk.py -v` + +## Build + +- **Build package**: `cd packages/sdk-py && poetry build` diff --git a/packages/sdk-py/LICENSE-APACHE b/packages/sdk-py/LICENSE-APACHE new file mode 120000 index 0000000..1cd601d --- /dev/null +++ b/packages/sdk-py/LICENSE-APACHE @@ -0,0 +1 @@ +../../LICENSE-APACHE \ No newline at end of file diff --git a/packages/sdk-py/LICENSE-MIT b/packages/sdk-py/LICENSE-MIT new file mode 120000 index 0000000..b2cfbdc --- /dev/null +++ b/packages/sdk-py/LICENSE-MIT @@ -0,0 +1 @@ +../../LICENSE-MIT \ No newline at end of file diff --git a/packages/sdk-py/README.md b/packages/sdk-py/README.md new file mode 100644 index 0000000..df64ac3 --- /dev/null +++ b/packages/sdk-py/README.md @@ -0,0 +1,19 @@ +# UTS Python SDK + +Python SDK for Universal Timestamps (UTS) - a decentralized timestamping protocol with EAS integration. + +## Installation + +```bash +pip install uts-python-sdk +``` + +## Usage + +```python +from uts_sdk import SDK + +async with SDK() as client: + stamp = await client.stamp(b"Hello, World!") + result = await client.verify(stamp[0]) +``` diff --git a/packages/sdk-py/poetry.lock b/packages/sdk-py/poetry.lock new file mode 100644 index 0000000..812f71c --- /dev/null +++ b/packages/sdk-py/poetry.lock @@ -0,0 +1,2947 @@ +# This file is automatically @generated by Poetry 2.3.2 and should not be changed by hand. + +[[package]] +name = "aiohappyeyeballs" +version = "2.6.1" +description = "Happy Eyeballs for asyncio" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "aiohappyeyeballs-2.6.1-py3-none-any.whl", hash = "sha256:f349ba8f4b75cb25c99c5c2d84e997e485204d2902a9597802b0371f09331fb8"}, + {file = "aiohappyeyeballs-2.6.1.tar.gz", hash = "sha256:c3f9d0113123803ccadfdf3f0faa505bc78e6a72d1cc4806cbd719826e943558"}, +] + +[[package]] +name = "aiohttp" +version = "3.13.3" +description = "Async http client/server framework (asyncio)" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "aiohttp-3.13.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d5a372fd5afd301b3a89582817fdcdb6c34124787c70dbcc616f259013e7eef7"}, + {file = "aiohttp-3.13.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:147e422fd1223005c22b4fe080f5d93ced44460f5f9c105406b753612b587821"}, + {file = "aiohttp-3.13.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:859bd3f2156e81dd01432f5849fc73e2243d4a487c4fd26609b1299534ee1845"}, + {file = "aiohttp-3.13.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dca68018bf48c251ba17c72ed479f4dafe9dbd5a73707ad8d28a38d11f3d42af"}, + {file = "aiohttp-3.13.3-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:fee0c6bc7db1de362252affec009707a17478a00ec69f797d23ca256e36d5940"}, + {file = "aiohttp-3.13.3-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c048058117fd649334d81b4b526e94bde3ccaddb20463a815ced6ecbb7d11160"}, + {file = "aiohttp-3.13.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:215a685b6fbbfcf71dfe96e3eba7a6f58f10da1dfdf4889c7dd856abe430dca7"}, + {file = "aiohttp-3.13.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:de2c184bb1fe2cbd2cefba613e9db29a5ab559323f994b6737e370d3da0ac455"}, + {file = "aiohttp-3.13.3-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:75ca857eba4e20ce9f546cd59c7007b33906a4cd48f2ff6ccf1ccfc3b646f279"}, + {file = "aiohttp-3.13.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:81e97251d9298386c2b7dbeb490d3d1badbdc69107fb8c9299dd04eb39bddc0e"}, + {file = "aiohttp-3.13.3-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:c0e2d366af265797506f0283487223146af57815b388623f0357ef7eac9b209d"}, + {file = "aiohttp-3.13.3-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:4e239d501f73d6db1522599e14b9b321a7e3b1de66ce33d53a765d975e9f4808"}, + {file = "aiohttp-3.13.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:0db318f7a6f065d84cb1e02662c526294450b314a02bd9e2a8e67f0d8564ce40"}, + {file = "aiohttp-3.13.3-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:bfc1cc2fe31a6026a8a88e4ecfb98d7f6b1fec150cfd708adbfd1d2f42257c29"}, + {file = "aiohttp-3.13.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:af71fff7bac6bb7508956696dce8f6eec2bbb045eceb40343944b1ae62b5ef11"}, + {file = "aiohttp-3.13.3-cp310-cp310-win32.whl", hash = "sha256:37da61e244d1749798c151421602884db5270faf479cf0ef03af0ff68954c9dd"}, + {file = "aiohttp-3.13.3-cp310-cp310-win_amd64.whl", hash = "sha256:7e63f210bc1b57ef699035f2b4b6d9ce096b5914414a49b0997c839b2bd2223c"}, + {file = "aiohttp-3.13.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:5b6073099fb654e0a068ae678b10feff95c5cae95bbfcbfa7af669d361a8aa6b"}, + {file = "aiohttp-3.13.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1cb93e166e6c28716c8c6aeb5f99dfb6d5ccf482d29fe9bf9a794110e6d0ab64"}, + {file = "aiohttp-3.13.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:28e027cf2f6b641693a09f631759b4d9ce9165099d2b5d92af9bd4e197690eea"}, + {file = "aiohttp-3.13.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3b61b7169ababd7802f9568ed96142616a9118dd2be0d1866e920e77ec8fa92a"}, + {file = "aiohttp-3.13.3-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:80dd4c21b0f6237676449c6baaa1039abae86b91636b6c91a7f8e61c87f89540"}, + {file = "aiohttp-3.13.3-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:65d2ccb7eabee90ce0503c17716fc77226be026dcc3e65cce859a30db715025b"}, + {file = "aiohttp-3.13.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5b179331a481cb5529fca8b432d8d3c7001cb217513c94cd72d668d1248688a3"}, + {file = "aiohttp-3.13.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d4c940f02f49483b18b079d1c27ab948721852b281f8b015c058100e9421dd1"}, + {file = "aiohttp-3.13.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f9444f105664c4ce47a2a7171a2418bce5b7bae45fb610f4e2c36045d85911d3"}, + {file = "aiohttp-3.13.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:694976222c711d1d00ba131904beb60534f93966562f64440d0c9d41b8cdb440"}, + {file = "aiohttp-3.13.3-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:f33ed1a2bf1997a36661874b017f5c4b760f41266341af36febaf271d179f6d7"}, + {file = "aiohttp-3.13.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e636b3c5f61da31a92bf0d91da83e58fdfa96f178ba682f11d24f31944cdd28c"}, + {file = "aiohttp-3.13.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:5d2d94f1f5fcbe40838ac51a6ab5704a6f9ea42e72ceda48de5e6b898521da51"}, + {file = "aiohttp-3.13.3-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:2be0e9ccf23e8a94f6f0650ce06042cefc6ac703d0d7ab6c7a917289f2539ad4"}, + {file = "aiohttp-3.13.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9af5e68ee47d6534d36791bbe9b646d2a7c7deb6fc24d7943628edfbb3581f29"}, + {file = "aiohttp-3.13.3-cp311-cp311-win32.whl", hash = "sha256:a2212ad43c0833a873d0fb3c63fa1bacedd4cf6af2fee62bf4b739ceec3ab239"}, + {file = "aiohttp-3.13.3-cp311-cp311-win_amd64.whl", hash = "sha256:642f752c3eb117b105acbd87e2c143de710987e09860d674e068c4c2c441034f"}, + {file = "aiohttp-3.13.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:b903a4dfee7d347e2d87697d0713be59e0b87925be030c9178c5faa58ea58d5c"}, + {file = "aiohttp-3.13.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a45530014d7a1e09f4a55f4f43097ba0fd155089372e105e4bff4ca76cb1b168"}, + {file = "aiohttp-3.13.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:27234ef6d85c914f9efeb77ff616dbf4ad2380be0cda40b4db086ffc7ddd1b7d"}, + {file = "aiohttp-3.13.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d32764c6c9aafb7fb55366a224756387cd50bfa720f32b88e0e6fa45b27dcf29"}, + {file = "aiohttp-3.13.3-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b1a6102b4d3ebc07dad44fbf07b45bb600300f15b552ddf1851b5390202ea2e3"}, + {file = "aiohttp-3.13.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c014c7ea7fb775dd015b2d3137378b7be0249a448a1612268b5a90c2d81de04d"}, + {file = "aiohttp-3.13.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2b8d8ddba8f95ba17582226f80e2de99c7a7948e66490ef8d947e272a93e9463"}, + {file = "aiohttp-3.13.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9ae8dd55c8e6c4257eae3a20fd2c8f41edaea5992ed67156642493b8daf3cecc"}, + {file = "aiohttp-3.13.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:01ad2529d4b5035578f5081606a465f3b814c542882804e2e8cda61adf5c71bf"}, + {file = "aiohttp-3.13.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:bb4f7475e359992b580559e008c598091c45b5088f28614e855e42d39c2f1033"}, + {file = "aiohttp-3.13.3-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:c19b90316ad3b24c69cd78d5c9b4f3aa4497643685901185b65166293d36a00f"}, + {file = "aiohttp-3.13.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:96d604498a7c782cb15a51c406acaea70d8c027ee6b90c569baa6e7b93073679"}, + {file = "aiohttp-3.13.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:084911a532763e9d3dd95adf78a78f4096cd5f58cdc18e6fdbc1b58417a45423"}, + {file = "aiohttp-3.13.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:7a4a94eb787e606d0a09404b9c38c113d3b099d508021faa615d70a0131907ce"}, + {file = "aiohttp-3.13.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:87797e645d9d8e222e04160ee32aa06bc5c163e8499f24db719e7852ec23093a"}, + {file = "aiohttp-3.13.3-cp312-cp312-win32.whl", hash = "sha256:b04be762396457bef43f3597c991e192ee7da460a4953d7e647ee4b1c28e7046"}, + {file = "aiohttp-3.13.3-cp312-cp312-win_amd64.whl", hash = "sha256:e3531d63d3bdfa7e3ac5e9b27b2dd7ec9df3206a98e0b3445fa906f233264c57"}, + {file = "aiohttp-3.13.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:5dff64413671b0d3e7d5918ea490bdccb97a4ad29b3f311ed423200b2203e01c"}, + {file = "aiohttp-3.13.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:87b9aab6d6ed88235aa2970294f496ff1a1f9adcd724d800e9b952395a80ffd9"}, + {file = "aiohttp-3.13.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:425c126c0dc43861e22cb1c14ba4c8e45d09516d0a3ae0a3f7494b79f5f233a3"}, + {file = "aiohttp-3.13.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7f9120f7093c2a32d9647abcaf21e6ad275b4fbec5b55969f978b1a97c7c86bf"}, + {file = "aiohttp-3.13.3-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:697753042d57f4bf7122cab985bf15d0cef23c770864580f5af4f52023a56bd6"}, + {file = "aiohttp-3.13.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6de499a1a44e7de70735d0b39f67c8f25eb3d91eb3103be99ca0fa882cdd987d"}, + {file = "aiohttp-3.13.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:37239e9f9a7ea9ac5bf6b92b0260b01f8a22281996da609206a84df860bc1261"}, + {file = "aiohttp-3.13.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f76c1e3fe7d7c8afad7ed193f89a292e1999608170dcc9751a7462a87dfd5bc0"}, + {file = "aiohttp-3.13.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fc290605db2a917f6e81b0e1e0796469871f5af381ce15c604a3c5c7e51cb730"}, + {file = "aiohttp-3.13.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4021b51936308aeea0367b8f006dc999ca02bc118a0cc78c303f50a2ff6afb91"}, + {file = "aiohttp-3.13.3-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:49a03727c1bba9a97d3e93c9f93ca03a57300f484b6e935463099841261195d3"}, + {file = "aiohttp-3.13.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:3d9908a48eb7416dc1f4524e69f1d32e5d90e3981e4e37eb0aa1cd18f9cfa2a4"}, + {file = "aiohttp-3.13.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:2712039939ec963c237286113c68dbad80a82a4281543f3abf766d9d73228998"}, + {file = "aiohttp-3.13.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:7bfdc049127717581866fa4708791220970ce291c23e28ccf3922c700740fdc0"}, + {file = "aiohttp-3.13.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8057c98e0c8472d8846b9c79f56766bcc57e3e8ac7bfd510482332366c56c591"}, + {file = "aiohttp-3.13.3-cp313-cp313-win32.whl", hash = "sha256:1449ceddcdbcf2e0446957863af03ebaaa03f94c090f945411b61269e2cb5daf"}, + {file = "aiohttp-3.13.3-cp313-cp313-win_amd64.whl", hash = "sha256:693781c45a4033d31d4187d2436f5ac701e7bbfe5df40d917736108c1cc7436e"}, + {file = "aiohttp-3.13.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:ea37047c6b367fd4bd632bff8077449b8fa034b69e812a18e0132a00fae6e808"}, + {file = "aiohttp-3.13.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:6fc0e2337d1a4c3e6acafda6a78a39d4c14caea625124817420abceed36e2415"}, + {file = "aiohttp-3.13.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c685f2d80bb67ca8c3837823ad76196b3694b0159d232206d1e461d3d434666f"}, + {file = "aiohttp-3.13.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:48e377758516d262bde50c2584fc6c578af272559c409eecbdd2bae1601184d6"}, + {file = "aiohttp-3.13.3-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:34749271508078b261c4abb1767d42b8d0c0cc9449c73a4df494777dc55f0687"}, + {file = "aiohttp-3.13.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:82611aeec80eb144416956ec85b6ca45a64d76429c1ed46ae1b5f86c6e0c9a26"}, + {file = "aiohttp-3.13.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2fff83cfc93f18f215896e3a190e8e5cb413ce01553901aca925176e7568963a"}, + {file = "aiohttp-3.13.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bbe7d4cecacb439e2e2a8a1a7b935c25b812af7a5fd26503a66dadf428e79ec1"}, + {file = "aiohttp-3.13.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b928f30fe49574253644b1ca44b1b8adbd903aa0da4b9054a6c20fc7f4092a25"}, + {file = "aiohttp-3.13.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7b5e8fe4de30df199155baaf64f2fcd604f4c678ed20910db8e2c66dc4b11603"}, + {file = "aiohttp-3.13.3-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:8542f41a62bcc58fc7f11cf7c90e0ec324ce44950003feb70640fc2a9092c32a"}, + {file = "aiohttp-3.13.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:5e1d8c8b8f1d91cd08d8f4a3c2b067bfca6ec043d3ff36de0f3a715feeedf926"}, + {file = "aiohttp-3.13.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:90455115e5da1c3c51ab619ac57f877da8fd6d73c05aacd125c5ae9819582aba"}, + {file = "aiohttp-3.13.3-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:042e9e0bcb5fba81886c8b4fbb9a09d6b8a00245fd8d88e4d989c1f96c74164c"}, + {file = "aiohttp-3.13.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2eb752b102b12a76ca02dff751a801f028b4ffbbc478840b473597fc91a9ed43"}, + {file = "aiohttp-3.13.3-cp314-cp314-win32.whl", hash = "sha256:b556c85915d8efaed322bf1bdae9486aa0f3f764195a0fb6ee962e5c71ef5ce1"}, + {file = "aiohttp-3.13.3-cp314-cp314-win_amd64.whl", hash = "sha256:9bf9f7a65e7aa20dd764151fb3d616c81088f91f8df39c3893a536e279b4b984"}, + {file = "aiohttp-3.13.3-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:05861afbbec40650d8a07ea324367cb93e9e8cc7762e04dd4405df99fa65159c"}, + {file = "aiohttp-3.13.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2fc82186fadc4a8316768d61f3722c230e2c1dcab4200d52d2ebdf2482e47592"}, + {file = "aiohttp-3.13.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0add0900ff220d1d5c5ebbf99ed88b0c1bbf87aa7e4262300ed1376a6b13414f"}, + {file = "aiohttp-3.13.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:568f416a4072fbfae453dcf9a99194bbb8bdeab718e08ee13dfa2ba0e4bebf29"}, + {file = "aiohttp-3.13.3-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:add1da70de90a2569c5e15249ff76a631ccacfe198375eead4aadf3b8dc849dc"}, + {file = "aiohttp-3.13.3-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:10b47b7ba335d2e9b1239fa571131a87e2d8ec96b333e68b2a305e7a98b0bae2"}, + {file = "aiohttp-3.13.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3dd4dce1c718e38081c8f35f323209d4c1df7d4db4bab1b5c88a6b4d12b74587"}, + {file = "aiohttp-3.13.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:34bac00a67a812570d4a460447e1e9e06fae622946955f939051e7cc895cfab8"}, + {file = "aiohttp-3.13.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a19884d2ee70b06d9204b2727a7b9f983d0c684c650254679e716b0b77920632"}, + {file = "aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5f8ca7f2bb6ba8348a3614c7918cc4bb73268c5ac2a207576b7afea19d3d9f64"}, + {file = "aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:b0d95340658b9d2f11d9697f59b3814a9d3bb4b7a7c20b131df4bcef464037c0"}, + {file = "aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:a1e53262fd202e4b40b70c3aff944a8155059beedc8a89bba9dc1f9ef06a1b56"}, + {file = "aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:d60ac9663f44168038586cab2157e122e46bdef09e9368b37f2d82d354c23f72"}, + {file = "aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:90751b8eed69435bac9ff4e3d2f6b3af1f57e37ecb0fbeee59c0174c9e2d41df"}, + {file = "aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:fc353029f176fd2b3ec6cfc71be166aba1936fe5d73dd1992ce289ca6647a9aa"}, + {file = "aiohttp-3.13.3-cp314-cp314t-win32.whl", hash = "sha256:2e41b18a58da1e474a057b3d35248d8320029f61d70a37629535b16a0c8f3767"}, + {file = "aiohttp-3.13.3-cp314-cp314t-win_amd64.whl", hash = "sha256:44531a36aa2264a1860089ffd4dce7baf875ee5a6079d5fb42e261c704ef7344"}, + {file = "aiohttp-3.13.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:31a83ea4aead760dfcb6962efb1d861db48c34379f2ff72db9ddddd4cda9ea2e"}, + {file = "aiohttp-3.13.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:988a8c5e317544fdf0d39871559e67b6341065b87fceac641108c2096d5506b7"}, + {file = "aiohttp-3.13.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:9b174f267b5cfb9a7dba9ee6859cecd234e9a681841eb85068059bc867fb8f02"}, + {file = "aiohttp-3.13.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:947c26539750deeaee933b000fb6517cc770bbd064bad6033f1cff4803881e43"}, + {file = "aiohttp-3.13.3-cp39-cp39-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:9ebf57d09e131f5323464bd347135a88622d1c0976e88ce15b670e7ad57e4bd6"}, + {file = "aiohttp-3.13.3-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4ae5b5a0e1926e504c81c5b84353e7a5516d8778fbbff00429fe7b05bb25cbce"}, + {file = "aiohttp-3.13.3-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2ba0eea45eb5cc3172dbfc497c066f19c41bac70963ea1a67d51fc92e4cf9a80"}, + {file = "aiohttp-3.13.3-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bae5c2ed2eae26cc382020edad80d01f36cb8e746da40b292e68fec40421dc6a"}, + {file = "aiohttp-3.13.3-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8a60e60746623925eab7d25823329941aee7242d559baa119ca2b253c88a7bd6"}, + {file = "aiohttp-3.13.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:e50a2e1404f063427c9d027378472316201a2290959a295169bcf25992d04558"}, + {file = "aiohttp-3.13.3-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:9a9dc347e5a3dc7dfdbc1f82da0ef29e388ddb2ed281bfce9dd8248a313e62b7"}, + {file = "aiohttp-3.13.3-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:b46020d11d23fe16551466c77823df9cc2f2c1e63cc965daf67fa5eec6ca1877"}, + {file = "aiohttp-3.13.3-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:69c56fbc1993fa17043e24a546959c0178fe2b5782405ad4559e6c13975c15e3"}, + {file = "aiohttp-3.13.3-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:b99281b0704c103d4e11e72a76f1b543d4946fea7dd10767e7e1b5f00d4e5704"}, + {file = "aiohttp-3.13.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:40c5e40ecc29ba010656c18052b877a1c28f84344825efa106705e835c28530f"}, + {file = "aiohttp-3.13.3-cp39-cp39-win32.whl", hash = "sha256:56339a36b9f1fc708260c76c87e593e2afb30d26de9ae1eb445b5e051b98a7a1"}, + {file = "aiohttp-3.13.3-cp39-cp39-win_amd64.whl", hash = "sha256:c6b8568a3bb5819a0ad087f16d40e5a3fb6099f39ea1d5625a3edc1e923fc538"}, + {file = "aiohttp-3.13.3.tar.gz", hash = "sha256:a949eee43d3782f2daae4f4a2819b2cb9b0c5d3b7f7a927067cc84dafdbb9f88"}, +] + +[package.dependencies] +aiohappyeyeballs = ">=2.5.0" +aiosignal = ">=1.4.0" +async-timeout = {version = ">=4.0,<6.0", markers = "python_version < \"3.11\""} +attrs = ">=17.3.0" +frozenlist = ">=1.1.1" +multidict = ">=4.5,<7.0" +propcache = ">=0.2.0" +yarl = ">=1.17.0,<2.0" + +[package.extras] +speedups = ["Brotli (>=1.2) ; platform_python_implementation == \"CPython\"", "aiodns (>=3.3.0)", "backports.zstd ; platform_python_implementation == \"CPython\" and python_version < \"3.14\"", "brotlicffi (>=1.2) ; platform_python_implementation != \"CPython\""] + +[[package]] +name = "aiosignal" +version = "1.4.0" +description = "aiosignal: a list of registered asynchronous callbacks" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "aiosignal-1.4.0-py3-none-any.whl", hash = "sha256:053243f8b92b990551949e63930a839ff0cf0b0ebbe0597b0f3fb19e1a0fe82e"}, + {file = "aiosignal-1.4.0.tar.gz", hash = "sha256:f47eecd9468083c2029cc99945502cb7708b082c232f9aca65da147157b251c7"}, +] + +[package.dependencies] +frozenlist = ">=1.1.0" +typing-extensions = {version = ">=4.2", markers = "python_version < \"3.13\""} + +[[package]] +name = "annotated-types" +version = "0.7.0" +description = "Reusable constraint types to use with typing.Annotated" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53"}, + {file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"}, +] + +[[package]] +name = "anyio" +version = "4.12.1" +description = "High-level concurrency and networking framework on top of asyncio or Trio" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "anyio-4.12.1-py3-none-any.whl", hash = "sha256:d405828884fc140aa80a3c667b8beed277f1dfedec42ba031bd6ac3db606ab6c"}, + {file = "anyio-4.12.1.tar.gz", hash = "sha256:41cfcc3a4c85d3f05c932da7c26d0201ac36f72abd4435ba90d0464a3ffed703"}, +] + +[package.dependencies] +exceptiongroup = {version = ">=1.0.2", markers = "python_version < \"3.11\""} +idna = ">=2.8" +typing_extensions = {version = ">=4.5", markers = "python_version < \"3.13\""} + +[package.extras] +trio = ["trio (>=0.31.0) ; python_version < \"3.10\"", "trio (>=0.32.0) ; python_version >= \"3.10\""] + +[[package]] +name = "async-timeout" +version = "5.0.1" +description = "Timeout context manager for asyncio programs" +optional = false +python-versions = ">=3.8" +groups = ["main"] +markers = "python_version == \"3.10\"" +files = [ + {file = "async_timeout-5.0.1-py3-none-any.whl", hash = "sha256:39e3809566ff85354557ec2398b55e096c8364bacac9405a7a1fa429e77fe76c"}, + {file = "async_timeout-5.0.1.tar.gz", hash = "sha256:d9321a7a3d5a6a5e187e824d2fa0793ce379a202935782d555d6e9d2735677d3"}, +] + +[[package]] +name = "attrs" +version = "25.4.0" +description = "Classes Without Boilerplate" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "attrs-25.4.0-py3-none-any.whl", hash = "sha256:adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373"}, + {file = "attrs-25.4.0.tar.gz", hash = "sha256:16d5969b87f0859ef33a48b35d55ac1be6e42ae49d5e853b597db70c35c57e11"}, +] + +[[package]] +name = "backports-asyncio-runner" +version = "1.2.0" +description = "Backport of asyncio.Runner, a context manager that controls event loop life cycle." +optional = false +python-versions = "<3.11,>=3.8" +groups = ["dev"] +markers = "python_version == \"3.10\"" +files = [ + {file = "backports_asyncio_runner-1.2.0-py3-none-any.whl", hash = "sha256:0da0a936a8aeb554eccb426dc55af3ba63bcdc69fa1a600b5bb305413a4477b5"}, + {file = "backports_asyncio_runner-1.2.0.tar.gz", hash = "sha256:a5aa7b2b7d8f8bfcaa2b57313f70792df84e32a2a746f585213373f900b42162"}, +] + +[[package]] +name = "bitarray" +version = "3.8.0" +description = "efficient arrays of booleans -- C extension" +optional = false +python-versions = "*" +groups = ["main"] +files = [ + {file = "bitarray-3.8.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f08342dc8d19214faa7ef99574dea6c37a2790d6d04a9793ef8fa76c188dc08d"}, + {file = "bitarray-3.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:792462abfeeca6cc8c6c1e6d27e14319682f0182f6b0ba37befe911af794db70"}, + {file = "bitarray-3.8.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0df69d26f21a9d2f1b20266f6737fa43f08aa5015c99900fb69f255fbe4dabb4"}, + {file = "bitarray-3.8.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b4f10d3f304be7183fac79bf2cd997f82e16aa9a9f37343d76c026c6e435a8a8"}, + {file = "bitarray-3.8.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fc98ff43abad61f00515ad9a06213b7716699146e46eabd256cdfe7cb522bd97"}, + {file = "bitarray-3.8.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:81c6b4a6c1af800d52a6fa32389ef8f4281583f4f99dc1a40f2bb47667281541"}, + {file = "bitarray-3.8.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f3fd8df63c41ff6a676d031956aebf68ebbc687b47c507da25501eb22eec341f"}, + {file = "bitarray-3.8.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:f0ce9d9e07c75da8027c62b4c9f45771d1d8aae7dc9ad7fb606c6a5aedbe9741"}, + {file = "bitarray-3.8.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:8a9c962c64a4c08def58b9799333e33af94ec53038cf151d36edacdb41f81646"}, + {file = "bitarray-3.8.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:1a54d7e7999735faacdcbe8128e30207abc2caf9f9fd7102d180b32f1b78bfce"}, + {file = "bitarray-3.8.0-cp310-cp310-win32.whl", hash = "sha256:3ea52df96566457735314794422274bd1962066bfb609e7eea9113d70cf04ffe"}, + {file = "bitarray-3.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:82a07de83dce09b4fa1bccbdc8bde8f188b131666af0dc9048ba0a0e448d8a3b"}, + {file = "bitarray-3.8.0-cp310-cp310-win_arm64.whl", hash = "sha256:c5ba07e58fd98c9782201e79eb8dd4225733d212a5a3700f9a84d329bd0463a6"}, + {file = "bitarray-3.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:25b9cff6c9856bc396232e2f609ea0c5ec1a8a24c500cee4cca96ba8a3cd50b6"}, + {file = "bitarray-3.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4d9984017314da772f5f7460add7a0301a4ffc06c72c2998bb16c300a6253607"}, + {file = "bitarray-3.8.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bbbbfbb7d039b20d289ce56b1beb46138d65769d04af50c199c6ac4cb6054d52"}, + {file = "bitarray-3.8.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f1f723e260c35e1c7c57a09d3a6ebe681bd56c83e1208ae3ce1869b7c0d10d4f"}, + {file = "bitarray-3.8.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:cbd1660fb48827381ce3a621a4fdc237959e1cd4e98b098952a8f624a0726425"}, + {file = "bitarray-3.8.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:df6d7bf3e15b7e6e202a16ff4948a51759354016026deb04ab9b5acbbe35e096"}, + {file = "bitarray-3.8.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d5c931ec1c03111718cabf85f6012bb2815fa0ce578175567fa8d6f2cc15d3b4"}, + {file = "bitarray-3.8.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:41b53711f89008ba2de62e4c2d2260a8b357072fd4f18e1351b28955db2719dc"}, + {file = "bitarray-3.8.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:4f298daaaea58d45e245a132d6d2bdfb6f856da50dc03d75ebb761439fb626cf"}, + {file = "bitarray-3.8.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:30989a2451b693c3f9359d91098a744992b5431a0be4858f1fdf0ec76b457125"}, + {file = "bitarray-3.8.0-cp311-cp311-win32.whl", hash = "sha256:e5aed4754895942ae15ffa48c52d181e1c1463236fda68d2dba29c03aa61786b"}, + {file = "bitarray-3.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:22c540ed20167d3dbb1e2d868ca935180247d620c40eace90efa774504a40e3b"}, + {file = "bitarray-3.8.0-cp311-cp311-win_arm64.whl", hash = "sha256:84b52b2cf77bb7f703d16c4007b021078dbbe6cf8ffb57abe81a7bacfc175ef2"}, + {file = "bitarray-3.8.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f2fcbe9b3a5996b417e030aa33a562e7e20dfc86271e53d7e841fc5df16268b8"}, + {file = "bitarray-3.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:cd761d158f67e288fd0ebe00c3b158095ce80a4bc7c32b60c7121224003ba70d"}, + {file = "bitarray-3.8.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c394a3f055b49f92626f83c1a0b6d6cd2c628f1ccd72481c3e3c6aa4695f3b20"}, + {file = "bitarray-3.8.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:969fd67de8c42affdb47b38b80f1eaa79ac0ef17d65407cdd931db1675315af1"}, + {file = "bitarray-3.8.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:99d25aff3745c54e61ab340b98400c52ebec04290a62078155e0d7eb30380220"}, + {file = "bitarray-3.8.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e645b4c365d6f1f9e0799380ad6395268f3c3b898244a650aaeb8d9d27b74c35"}, + {file = "bitarray-3.8.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2fa23fdb3beab313950bbb49674e8a161e61449332d3997089fe3944953f1b77"}, + {file = "bitarray-3.8.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:165052a0e61c880f7093808a0c524ce1b3555bfa114c0dfb5c809cd07918a60d"}, + {file = "bitarray-3.8.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:337c8cd46a4c6568d367ed676cbf2d7de16f890bb31dbb54c44c1d6bb6d4a1de"}, + {file = "bitarray-3.8.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:21ca6a47bf20db9e7ad74ca04b3d479e4d76109b68333eb23535553d2705339e"}, + {file = "bitarray-3.8.0-cp312-cp312-win32.whl", hash = "sha256:178c5a4c7fdfb5cd79e372ae7f675390e670f3732e5bc68d327e01a5b3ff8d55"}, + {file = "bitarray-3.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:75a3b6e9c695a6570ea488db75b84bb592ff70a944957efa1c655867c575018b"}, + {file = "bitarray-3.8.0-cp312-cp312-win_arm64.whl", hash = "sha256:5591daf81313096909d973fb2612fccd87528fdfdd39f6478bdce54543178954"}, + {file = "bitarray-3.8.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:18214bac86341f1cc413772e66447d6cca10981e2880b70ecaf4e826c04f95e9"}, + {file = "bitarray-3.8.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:01c5f0dc080b0ebb432f7a68ee1e88a76bd34f6d89c9568fcec65fb16ed71f0e"}, + {file = "bitarray-3.8.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:86685fa04067f7175f9718489ae755f6acde03593a1a9ca89305554af40e14fd"}, + {file = "bitarray-3.8.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:56896ceeffe25946c4010320629e2d858ca763cd8ded273c81672a5edbcb1e0a"}, + {file = "bitarray-3.8.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:9858dcbc23ba7eaadcd319786b982278a1a2b2020720b19db43e309579ff76fb"}, + {file = "bitarray-3.8.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:aa7dec53c25f1949513457ef8b0ea1fb40e76c672cc4d2daa8ad3c8d6b73491a"}, + {file = "bitarray-3.8.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:15a2eff91f54d2b1f573cca8ca6fb58763ce8fea80e7899ab028f3987ef71cd5"}, + {file = "bitarray-3.8.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:b1572ee0eb1967e71787af636bb7d1eb9c6735d5337762c450650e7f51844594"}, + {file = "bitarray-3.8.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:5bfac7f236ba1a4d402644bdce47fb9db02a7cf3214a1f637d3a88390f9e5428"}, + {file = "bitarray-3.8.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f0a55cf02d2cdd739b40ce10c09bbdd520e141217696add7a48b56e67bdfdfe6"}, + {file = "bitarray-3.8.0-cp313-cp313-win32.whl", hash = "sha256:a2ba92f59e30ce915e9e79af37649432e3a212ddddf416d4d686b1b4825bcdb2"}, + {file = "bitarray-3.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:1c8f2a5d8006db5a555e06f9437e76bf52537d3dfd130cb8ae2b30866aca32c9"}, + {file = "bitarray-3.8.0-cp313-cp313-win_arm64.whl", hash = "sha256:50ddbe3a7b4b6ab96812f5a4d570f401a2cdb95642fd04c062f98939610bbeee"}, + {file = "bitarray-3.8.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8cbd4bfc933b33b85c43ef4c1f4d5e3e9d91975ea6368acf5fbac02bac06ea89"}, + {file = "bitarray-3.8.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9d35d8f8a1c9ed4e2b08187b513f8a3c71958600129db3aa26d85ea3abfd1310"}, + {file = "bitarray-3.8.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:99f55e14e7c56f4fafe1343480c32b110ef03836c21ff7c48bae7add6818f77c"}, + {file = "bitarray-3.8.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:dfbe2aa45b273f49e715c5345d94874cb65a28482bf231af408891c260601b8d"}, + {file = "bitarray-3.8.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:64af877116edf051375b45f0bda648143176a017b13803ec7b3a3111dc05f4c5"}, + {file = "bitarray-3.8.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cdfbb27f2c46bb5bbdcee147530cbc5ca8ab858d7693924e88e30ada21b2c5e2"}, + {file = "bitarray-3.8.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4d73d4948dcc5591d880db8933004e01f1dd2296df9de815354d53469beb26fe"}, + {file = "bitarray-3.8.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:28a85b056c0eb7f5d864c0ceef07034117e8ebfca756f50648c71950a568ba11"}, + {file = "bitarray-3.8.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:79ec4498a545733ecace48d780d22407411b07403a2e08b9a4d7596c0b97ebd7"}, + {file = "bitarray-3.8.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:33af25c4ff7723363cb8404dfc2eefeab4110b654f6c98d26aba8a08c745d860"}, + {file = "bitarray-3.8.0-cp314-cp314-win32.whl", hash = "sha256:2c3bb96b6026643ce24677650889b09073f60b9860a71765f843c99f9ab38b25"}, + {file = "bitarray-3.8.0-cp314-cp314-win_amd64.whl", hash = "sha256:847c7f61964225fc489fe1d49eda7e0e0d253e98862c012cecf845f9ad45cdf4"}, + {file = "bitarray-3.8.0-cp314-cp314-win_arm64.whl", hash = "sha256:a2cb35a6efaa0e3623d8272471371a12c7e07b51a33e5efce9b58f655d864b4e"}, + {file = "bitarray-3.8.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:15e8d0597cc6e8496de6f4dea2a6880c57e1251502a7072f5631108a1aa28521"}, + {file = "bitarray-3.8.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:8ffe660e963ae711cb9e2b8d8461c9b1ad6167823837fc17d59d5e539fb898fa"}, + {file = "bitarray-3.8.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4779f356083c62e29b4198d290b7b17a39a69702d150678b7efff0fdddf494a8"}, + {file = "bitarray-3.8.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:025d133bf4ca8cf75f904eeb8ea946228d7c043231866143f31946a6f4dd0bf3"}, + {file = "bitarray-3.8.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:451f9958850ea98440d542278368c8d1e1ea821e2494b204570ba34a340759df"}, + {file = "bitarray-3.8.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6d79f659965290af60d6acc8e2716341865fe74609a7ede2a33c2f86ad893b8f"}, + {file = "bitarray-3.8.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:fbf05678c2ae0064fb1b8de7e9e8f0fc30621b73c8477786dd0fb3868044a8c8"}, + {file = "bitarray-3.8.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:c396358023b876cff547ce87f4e8ff8a2280598873a137e8cc69e115262260b8"}, + {file = "bitarray-3.8.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:ed3493a369fe849cce98542d7405c88030b355e4d2e113887cb7ecc86c205773"}, + {file = "bitarray-3.8.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:c764fb167411d5afaef88138542a4bfa28bd5e5ded5e8e42df87cef965efd6e9"}, + {file = "bitarray-3.8.0-cp314-cp314t-win32.whl", hash = "sha256:e12769d3adcc419e65860de946df8d2ed274932177ac1cdb05186e498aaa9149"}, + {file = "bitarray-3.8.0-cp314-cp314t-win_amd64.whl", hash = "sha256:0ca70ccf789446a6dfde40b482ec21d28067172cd1f8efd50d5548159fccad9e"}, + {file = "bitarray-3.8.0-cp314-cp314t-win_arm64.whl", hash = "sha256:2a3d1b05ffdd3e95687942ae7b13c63689f85d3f15c39b33329e3cb9ce6c015f"}, + {file = "bitarray-3.8.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:f8d3417db5e14a6789073b21ae44439a755289477901901bae378a57b905e148"}, + {file = "bitarray-3.8.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7f65bd5d4cdb396295b6aa07f84ca659ac65c5c68b53956a6d95219e304b0ada"}, + {file = "bitarray-3.8.0-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7f14d6b303e55bd7d19b28309ef8014370e84a3806c5e452e078e7df7344d97a"}, + {file = "bitarray-3.8.0-cp38-cp38-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5c5a8a83df95e51f7a7c2b083eaea134cbed39fc42c6aeb2e764ddb7ccccd43e"}, + {file = "bitarray-3.8.0-cp38-cp38-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6d70fa9c6d2e955bde8cd327ffc11f2cc34bc21944e5571a46ca501e7eadef24"}, + {file = "bitarray-3.8.0-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f8069a807a3e6e3c361ce302ece4bf1c3b49962c1726d1d56587e8f48682861"}, + {file = "bitarray-3.8.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:a358277122456666a8b2a0b9aa04f1b89d34e8aa41d08a6557d693e6abb6667c"}, + {file = "bitarray-3.8.0-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:014df8a9430276862392ac5d471697de042367996c49f32d0008585d2c60755a"}, + {file = "bitarray-3.8.0-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:720963fee259291a88348ae9735d9deb5d334e84a016244f61c89f5a49aa400a"}, + {file = "bitarray-3.8.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:239578587b9c29469ab61149dda40a2fe714a6a4eca0f8ff9ea9439ec4b7bc30"}, + {file = "bitarray-3.8.0-cp38-cp38-win32.whl", hash = "sha256:004d518fa410e6da43386d20e07b576a41eb417ac67abf9f30fa75e125697199"}, + {file = "bitarray-3.8.0-cp38-cp38-win_amd64.whl", hash = "sha256:5338a313f998e1be7267191b7caaae82563b4a2b42b393561055412a34042caa"}, + {file = "bitarray-3.8.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d2dbe8a3baf2d842e342e8acb06ae3844765d38df67687c144cdeb71f1bcb5d7"}, + {file = "bitarray-3.8.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ff1863f037dad765ef5963efc2e37d399ac023e192a6f2bb394e2377d023cefe"}, + {file = "bitarray-3.8.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:26714898eb0d847aac8af94c4441c9cb50387847d0fe6b9fc4217c086cd68b80"}, + {file = "bitarray-3.8.0-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5f2fb10518f6b365f5b720e43a529c3b2324ca02932f609631a44edb347d8d54"}, + {file = "bitarray-3.8.0-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:1a926fa554870642607fd10e66ee25b75fdd9a7ca4bbffa93d424e4ae2bf734a"}, + {file = "bitarray-3.8.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4902f4ecd5fcb6a5f482d7b0ae1c16c21f26fc5279b3b6127363d13ad8e7a9d9"}, + {file = "bitarray-3.8.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:94652da1a4ca7cfb69c15dd6986b205e0bd9c63a05029c3b48b4201085f527bd"}, + {file = "bitarray-3.8.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:31a4ad2b730128e273f1c22300da3e3631f125703e4fee0ac44d385abfb15671"}, + {file = "bitarray-3.8.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:cbba763d99de0255a3e4938f25a8579930ac8aa089233cb2fb2ed7d04d4aff02"}, + {file = "bitarray-3.8.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:46cf239856b87fe1c86dfbb3d459d840a8b1649e7922b1e0bfb6b6464692644a"}, + {file = "bitarray-3.8.0-cp39-cp39-win32.whl", hash = "sha256:2fe8c54b15a9cd4f93bc2aaceab354ec65af93370aa1496ba2f9c537a4855ee0"}, + {file = "bitarray-3.8.0-cp39-cp39-win_amd64.whl", hash = "sha256:58a01ea34057463f7a98a4d6ff40160f65f945e924fec08a5b39e327e372875d"}, + {file = "bitarray-3.8.0-cp39-cp39-win_arm64.whl", hash = "sha256:a60da2f9efbed355edb35a1fb6829148676786c829fad708bb6bb47211b3593a"}, + {file = "bitarray-3.8.0.tar.gz", hash = "sha256:3eae38daffd77c9621ae80c16932eea3fb3a4af141fb7cc724d4ad93eff9210d"}, +] + +[[package]] +name = "black" +version = "26.3.0" +description = "The uncompromising code formatter." +optional = false +python-versions = ">=3.10" +groups = ["dev"] +files = [ + {file = "black-26.3.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:135bf8a352e35b3bfba4999c256063d8d86514654599eca7635e914a55d60ec3"}, + {file = "black-26.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6024a2959b6c62c311c564ce23ce0eaa977a50ed52a53f7abc83d2c9eb62b8d8"}, + {file = "black-26.3.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:264144203ea3374542a1591b6fb317561662d074bce5d91ad6afa8d8d3e4ec3d"}, + {file = "black-26.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:1a15d1386dce3af3993bf9baeb68d3e492cbb003dae05c3ecf8530a9b75edf85"}, + {file = "black-26.3.0-cp310-cp310-win_arm64.whl", hash = "sha256:d86a70bf048235aff62a79e229fe5d9e7809c7a05a3dd12982e7ccdc2678e096"}, + {file = "black-26.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3da07abe65732483e915ab7f9c7c50332c293056436e9519373775d62539607c"}, + {file = "black-26.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:fc9fd683ccabc3dc9791b93db494d93b5c6c03b105453b76d71e5474e9dfa6e7"}, + {file = "black-26.3.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8e2c7e2c5ee09ff575869258b2c07064c952637918fc5e15f6ebd45e45eae0aa"}, + {file = "black-26.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:a849286bfc3054eaeb233b6df9056fcf969ee18bf7ecb71b0257e838a0f05e6d"}, + {file = "black-26.3.0-cp311-cp311-win_arm64.whl", hash = "sha256:c93c83af43cda73ed8265d001214779ab245fa7a861a75b3e43828f4fb1f5657"}, + {file = "black-26.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c2b1e5eec220b419e3591a0aaa6351bd3a9c01fe6291fbaf76d84308eb7a2ede"}, + {file = "black-26.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1bab64de70bccc992432bee56cdffbe004ceeaa07352127c386faa87e81f9261"}, + {file = "black-26.3.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5b6c5f734290803b7b26493ffd734b02b72e6c90d82d45ac4d5b862b9bdf7720"}, + {file = "black-26.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:7c767396af15b54e1a6aae99ddf241ae97e589f666b1d22c4b6618282a04e4ca"}, + {file = "black-26.3.0-cp312-cp312-win_arm64.whl", hash = "sha256:765fd6ddd00f35c55250fdc6b790c272d54ac3f44da719cc42df428269b45980"}, + {file = "black-26.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:59754fd8f43ef457be190594c07a52c999e22cb1534dc5344bff1d46fdf1027d"}, + {file = "black-26.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1fd94cfee67b8d336761a0b08629a25938e4a491c440951ce517a7209c99b5ff"}, + {file = "black-26.3.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6f7b3e653a90ca1ef4e821c20f8edaee80b649c38d2532ed2e9073a9534b14a7"}, + {file = "black-26.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:f8fb9d7c2496adc83614856e1f6e55a9ce4b7ae7fc7f45b46af9189ddb493464"}, + {file = "black-26.3.0-cp313-cp313-win_arm64.whl", hash = "sha256:e8618c1d06838f56afbcb3ffa1aa16436cec62b86b38c7b32ca86f53948ffb91"}, + {file = "black-26.3.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:d0c6f64ead44f4369c66f1339ecf68e99b40f2e44253c257f7807c5a3ef0ca32"}, + {file = "black-26.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ed6f0809134e51ec4a7509e069cdfa42bf996bd0fd1df6d3146b907f36e28893"}, + {file = "black-26.3.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cc6ac0ea5dd5fa6311ca82edfa3620cba0ed0426022d10d2d5d39aedbf3e1958"}, + {file = "black-26.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:884bc0aefa96adabcba0b77b10e9775fd52d4b766e88c44dc6f41f7c82787fc8"}, + {file = "black-26.3.0-cp314-cp314-win_arm64.whl", hash = "sha256:be3bd02aab5c4ab03703172f5530ddc8fc8b5b7bb8786230e84c9e011cee9ca1"}, + {file = "black-26.3.0-py3-none-any.whl", hash = "sha256:e825d6b121910dff6f04d7691f826d2449327e8e71c26254c030c4f3d2311985"}, + {file = "black-26.3.0.tar.gz", hash = "sha256:4d438dfdba1c807c6c7c63c4f15794dda0820d2222e7c4105042ac9ddfc5dd0b"}, +] + +[package.dependencies] +click = ">=8.0.0" +mypy-extensions = ">=0.4.3" +packaging = ">=22.0" +pathspec = ">=1.0.0" +platformdirs = ">=2" +pytokens = ">=0.4.0,<0.5.0" +tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} +typing-extensions = {version = ">=4.0.1", markers = "python_version < \"3.11\""} + +[package.extras] +colorama = ["colorama (>=0.4.3)"] +d = ["aiohttp (>=3.10)"] +jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] +uvloop = ["uvloop (>=0.15.2) ; sys_platform != \"win32\"", "winloop (>=0.5.0) ; sys_platform == \"win32\""] + +[[package]] +name = "certifi" +version = "2026.2.25" +description = "Python package for providing Mozilla's CA Bundle." +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "certifi-2026.2.25-py3-none-any.whl", hash = "sha256:027692e4402ad994f1c42e52a4997a9763c646b73e4096e4d5d6db8af1d6f0fa"}, + {file = "certifi-2026.2.25.tar.gz", hash = "sha256:e887ab5cee78ea814d3472169153c2d12cd43b14bd03329a39a9c6e2e80bfba7"}, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.5" +description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "charset_normalizer-3.4.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:4167a621a9a1a986c73777dbc15d4b5eac8ac5c10393374109a343d4013ec765"}, + {file = "charset_normalizer-3.4.5-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3f64c6bf8f32f9133b668c7f7a7cbdbc453412bc95ecdbd157f3b1e377a92990"}, + {file = "charset_normalizer-3.4.5-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:568e3c34b58422075a1b49575a6abc616d9751b4d61b23f712e12ebb78fe47b2"}, + {file = "charset_normalizer-3.4.5-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:036c079aa08a6a592b82487f97c60b439428320ed1b2ea0b3912e99d30c77765"}, + {file = "charset_normalizer-3.4.5-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:340810d34ef83af92148e96e3e44cb2d3f910d2bf95e5618a5c467d9f102231d"}, + {file = "charset_normalizer-3.4.5-cp310-cp310-manylinux_2_31_armv7l.whl", hash = "sha256:cd2d0f0ec9aa977a27731a3209ebbcacebebaf41f902bd453a928bfd281cf7f8"}, + {file = "charset_normalizer-3.4.5-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0b362bcd27819f9c07cbf23db4e0e8cd4b44c5ecd900c2ff907b2b92274a7412"}, + {file = "charset_normalizer-3.4.5-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:77be992288f720306ab4108fe5c74797de327f3248368dfc7e1a916d6ed9e5a2"}, + {file = "charset_normalizer-3.4.5-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:8b78d8a609a4b82c273257ee9d631ded7fac0d875bdcdccc109f3ee8328cfcb1"}, + {file = "charset_normalizer-3.4.5-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:ba20bdf69bd127f66d0174d6f2a93e69045e0b4036dc1ca78e091bcc765830c4"}, + {file = "charset_normalizer-3.4.5-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:76a9d0de4d0eab387822e7b35d8f89367dd237c72e82ab42b9f7bf5e15ada00f"}, + {file = "charset_normalizer-3.4.5-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:8fff79bf5978c693c9b1a4d71e4a94fddfb5fe744eb062a318e15f4a2f63a550"}, + {file = "charset_normalizer-3.4.5-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:c7e84e0c0005e3bdc1a9211cd4e62c78ba80bc37b2365ef4410cd2007a9047f2"}, + {file = "charset_normalizer-3.4.5-cp310-cp310-win32.whl", hash = "sha256:58ad8270cfa5d4bef1bc85bd387217e14ff154d6630e976c6f56f9a040757475"}, + {file = "charset_normalizer-3.4.5-cp310-cp310-win_amd64.whl", hash = "sha256:02a9d1b01c1e12c27883b0c9349e0bcd9ae92e727ff1a277207e1a262b1cbf05"}, + {file = "charset_normalizer-3.4.5-cp310-cp310-win_arm64.whl", hash = "sha256:039215608ac7b358c4da0191d10fc76868567fbf276d54c14721bdedeb6de064"}, + {file = "charset_normalizer-3.4.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:610f72c0ee565dfb8ae1241b666119582fdbfe7c0975c175be719f940e110694"}, + {file = "charset_normalizer-3.4.5-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:60d68e820af339df4ae8358c7a2e7596badeb61e544438e489035f9fbf3246a5"}, + {file = "charset_normalizer-3.4.5-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:10b473fc8dca1c3ad8559985794815f06ca3fc71942c969129070f2c3cdf7281"}, + {file = "charset_normalizer-3.4.5-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d4eb8ac7469b2a5d64b5b8c04f84d8bf3ad340f4514b98523805cbf46e3b3923"}, + {file = "charset_normalizer-3.4.5-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5bcb3227c3d9aaf73eaaab1db7ccd80a8995c509ee9941e2aae060ca6e4e5d81"}, + {file = "charset_normalizer-3.4.5-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:75ee9c1cce2911581a70a3c0919d8bccf5b1cbc9b0e5171400ec736b4b569497"}, + {file = "charset_normalizer-3.4.5-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1d1401945cb77787dbd3af2446ff2d75912327c4c3a1526ab7955ecf8600687c"}, + {file = "charset_normalizer-3.4.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0a45e504f5e1be0bd385935a8e1507c442349ca36f511a47057a71c9d1d6ea9e"}, + {file = "charset_normalizer-3.4.5-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:e09f671a54ce70b79a1fc1dc6da3072b7ef7251fadb894ed92d9aa8218465a5f"}, + {file = "charset_normalizer-3.4.5-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:d01de5e768328646e6a3fa9e562706f8f6641708c115c62588aef2b941a4f88e"}, + {file = "charset_normalizer-3.4.5-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:131716d6786ad5e3dc542f5cc6f397ba3339dc0fb87f87ac30e550e8987756af"}, + {file = "charset_normalizer-3.4.5-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:1a374cc0b88aa710e8865dc1bd6edb3743c59f27830f0293ab101e4cf3ce9f85"}, + {file = "charset_normalizer-3.4.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d31f0d1671e1534e395f9eb84a68e0fb670e1edb1fe819a9d7f564ae3bc4e53f"}, + {file = "charset_normalizer-3.4.5-cp311-cp311-win32.whl", hash = "sha256:cace89841c0599d736d3d74a27bc5821288bb47c5441923277afc6059d7fbcb4"}, + {file = "charset_normalizer-3.4.5-cp311-cp311-win_amd64.whl", hash = "sha256:f8102ae93c0bc863b1d41ea0f4499c20a83229f52ed870850892df555187154a"}, + {file = "charset_normalizer-3.4.5-cp311-cp311-win_arm64.whl", hash = "sha256:ed98364e1c262cf5f9363c3eca8c2df37024f52a8fa1180a3610014f26eac51c"}, + {file = "charset_normalizer-3.4.5-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:ed97c282ee4f994ef814042423a529df9497e3c666dca19be1d4cd1129dc7ade"}, + {file = "charset_normalizer-3.4.5-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0294916d6ccf2d069727d65973c3a1ca477d68708db25fd758dd28b0827cff54"}, + {file = "charset_normalizer-3.4.5-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:dc57a0baa3eeedd99fafaef7511b5a6ef4581494e8168ee086031744e2679467"}, + {file = "charset_normalizer-3.4.5-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ed1a9a204f317ef879b32f9af507d47e49cd5e7f8e8d5d96358c98373314fc60"}, + {file = "charset_normalizer-3.4.5-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7ad83b8f9379176c841f8865884f3514d905bcd2a9a3b210eaa446e7d2223e4d"}, + {file = "charset_normalizer-3.4.5-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:a118e2e0b5ae6b0120d5efa5f866e58f2bb826067a646431da4d6a2bdae7950e"}, + {file = "charset_normalizer-3.4.5-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:754f96058e61a5e22e91483f823e07df16416ce76afa4ebf306f8e1d1296d43f"}, + {file = "charset_normalizer-3.4.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0c300cefd9b0970381a46394902cd18eaf2aa00163f999590ace991989dcd0fc"}, + {file = "charset_normalizer-3.4.5-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:c108f8619e504140569ee7de3f97d234f0fbae338a7f9f360455071ef9855a95"}, + {file = "charset_normalizer-3.4.5-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:d1028de43596a315e2720a9849ee79007ab742c06ad8b45a50db8cdb7ed4a82a"}, + {file = "charset_normalizer-3.4.5-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:19092dde50335accf365cce21998a1c6dd8eafd42c7b226eb54b2747cdce2fac"}, + {file = "charset_normalizer-3.4.5-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:4354e401eb6dab9aed3c7b4030514328a6c748d05e1c3e19175008ca7de84fb1"}, + {file = "charset_normalizer-3.4.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a68766a3c58fde7f9aaa22b3786276f62ab2f594efb02d0a1421b6282e852e98"}, + {file = "charset_normalizer-3.4.5-cp312-cp312-win32.whl", hash = "sha256:1827734a5b308b65ac54e86a618de66f935a4f63a8a462ff1e19a6788d6c2262"}, + {file = "charset_normalizer-3.4.5-cp312-cp312-win_amd64.whl", hash = "sha256:728c6a963dfab66ef865f49286e45239384249672cd598576765acc2a640a636"}, + {file = "charset_normalizer-3.4.5-cp312-cp312-win_arm64.whl", hash = "sha256:75dfd1afe0b1647449e852f4fb428195a7ed0588947218f7ba929f6538487f02"}, + {file = "charset_normalizer-3.4.5-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ac59c15e3f1465f722607800c68713f9fbc2f672b9eb649fe831da4019ae9b23"}, + {file = "charset_normalizer-3.4.5-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:165c7b21d19365464e8f70e5ce5e12524c58b48c78c1f5a57524603c1ab003f8"}, + {file = "charset_normalizer-3.4.5-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:28269983f25a4da0425743d0d257a2d6921ea7d9b83599d4039486ec5b9f911d"}, + {file = "charset_normalizer-3.4.5-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d27ce22ec453564770d29d03a9506d449efbb9fa13c00842262b2f6801c48cce"}, + {file = "charset_normalizer-3.4.5-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0625665e4ebdddb553ab185de5db7054393af8879fb0c87bd5690d14379d6819"}, + {file = "charset_normalizer-3.4.5-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:c23eb3263356d94858655b3e63f85ac5d50970c6e8febcdde7830209139cc37d"}, + {file = "charset_normalizer-3.4.5-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e6302ca4ae283deb0af68d2fbf467474b8b6aedcd3dab4db187e07f94c109763"}, + {file = "charset_normalizer-3.4.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e51ae7d81c825761d941962450f50d041db028b7278e7b08930b4541b3e45cb9"}, + {file = "charset_normalizer-3.4.5-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:597d10dec876923e5c59e48dbd366e852eacb2b806029491d307daea6b917d7c"}, + {file = "charset_normalizer-3.4.5-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:5cffde4032a197bd3b42fd0b9509ec60fb70918d6970e4cc773f20fc9180ca67"}, + {file = "charset_normalizer-3.4.5-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:2da4eedcb6338e2321e831a0165759c0c620e37f8cd044a263ff67493be8ffb3"}, + {file = "charset_normalizer-3.4.5-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:65a126fb4b070d05340a84fc709dd9e7c75d9b063b610ece8a60197a291d0adf"}, + {file = "charset_normalizer-3.4.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c7a80a9242963416bd81f99349d5f3fce1843c303bd404f204918b6d75a75fd6"}, + {file = "charset_normalizer-3.4.5-cp313-cp313-win32.whl", hash = "sha256:f1d725b754e967e648046f00c4facc42d414840f5ccc670c5670f59f83693e4f"}, + {file = "charset_normalizer-3.4.5-cp313-cp313-win_amd64.whl", hash = "sha256:e37bd100d2c5d3ba35db9c7c5ba5a9228cbcffe5c4778dc824b164e5257813d7"}, + {file = "charset_normalizer-3.4.5-cp313-cp313-win_arm64.whl", hash = "sha256:93b3b2cc5cf1b8743660ce77a4f45f3f6d1172068207c1defc779a36eea6bb36"}, + {file = "charset_normalizer-3.4.5-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8197abe5ca1ffb7d91e78360f915eef5addff270f8a71c1fc5be24a56f3e4873"}, + {file = "charset_normalizer-3.4.5-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a2aecdb364b8a1802afdc7f9327d55dad5366bc97d8502d0f5854e50712dbc5f"}, + {file = "charset_normalizer-3.4.5-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a66aa5022bf81ab4b1bebfb009db4fd68e0c6d4307a1ce5ef6a26e5878dfc9e4"}, + {file = "charset_normalizer-3.4.5-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d77f97e515688bd615c1d1f795d540f32542d514242067adcb8ef532504cb9ee"}, + {file = "charset_normalizer-3.4.5-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01a1ed54b953303ca7e310fafe0fe347aab348bd81834a0bcd602eb538f89d66"}, + {file = "charset_normalizer-3.4.5-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:b2d37d78297b39a9eb9eb92c0f6df98c706467282055419df141389b23f93362"}, + {file = "charset_normalizer-3.4.5-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e71bbb595973622b817c042bd943c3f3667e9c9983ce3d205f973f486fec98a7"}, + {file = "charset_normalizer-3.4.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4cd966c2559f501c6fd69294d082c2934c8dd4719deb32c22961a5ac6db0df1d"}, + {file = "charset_normalizer-3.4.5-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:d5e52d127045d6ae01a1e821acfad2f3a1866c54d0e837828538fabe8d9d1bd6"}, + {file = "charset_normalizer-3.4.5-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:30a2b1a48478c3428d047ed9690d57c23038dac838a87ad624c85c0a78ebeb39"}, + {file = "charset_normalizer-3.4.5-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:d8ed79b8f6372ca4254955005830fd61c1ccdd8c0fac6603e2c145c61dd95db6"}, + {file = "charset_normalizer-3.4.5-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:c5af897b45fa606b12464ccbe0014bbf8c09191e0a66aab6aa9d5cf6e77e0c94"}, + {file = "charset_normalizer-3.4.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:1088345bcc93c58d8d8f3d783eca4a6e7a7752bbff26c3eee7e73c597c191c2e"}, + {file = "charset_normalizer-3.4.5-cp314-cp314-win32.whl", hash = "sha256:ee57b926940ba00bca7ba7041e665cc956e55ef482f851b9b65acb20d867e7a2"}, + {file = "charset_normalizer-3.4.5-cp314-cp314-win_amd64.whl", hash = "sha256:4481e6da1830c8a1cc0b746b47f603b653dadb690bcd851d039ffaefe70533aa"}, + {file = "charset_normalizer-3.4.5-cp314-cp314-win_arm64.whl", hash = "sha256:97ab7787092eb9b50fb47fa04f24c75b768a606af1bcba1957f07f128a7219e4"}, + {file = "charset_normalizer-3.4.5-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:e22d1059b951e7ae7c20ef6b06afd10fb95e3c41bf3c4fbc874dba113321c193"}, + {file = "charset_normalizer-3.4.5-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:afca7f78067dd27c2b848f1b234623d26b87529296c6c5652168cc1954f2f3b2"}, + {file = "charset_normalizer-3.4.5-cp38-cp38-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ec56a2266f32bc06ed3c3e2a8f58417ce02f7e0356edc89786e52db13c593c98"}, + {file = "charset_normalizer-3.4.5-cp38-cp38-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2b970382e4a36bed897c19f310f31d7d13489c11b4f468ddfba42d41cddfb918"}, + {file = "charset_normalizer-3.4.5-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:573ef5814c4b7c0d59a7710aa920eaaaef383bd71626aa420fba27b5cab92e8d"}, + {file = "charset_normalizer-3.4.5-cp38-cp38-manylinux_2_31_armv7l.whl", hash = "sha256:50bcbca6603c06a1dcc7b056ed45c37715fb5d2768feb3bcd37d2313c587a5b9"}, + {file = "charset_normalizer-3.4.5-cp38-cp38-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1f2da5cbb9becfcd607757a169e38fb82aa5fd86fae6653dea716e7b613fe2cf"}, + {file = "charset_normalizer-3.4.5-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:fc1c64934b8faf7584924143eb9db4770bbdb16659626e1a1a4d9efbcb68d947"}, + {file = "charset_normalizer-3.4.5-cp38-cp38-musllinux_1_2_armv7l.whl", hash = "sha256:ae8b03427410731469c4033934cf473426faff3e04b69d2dfb64a4281a3719f8"}, + {file = "charset_normalizer-3.4.5-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:b3e71afc578b98512bfe7bdb822dd6bc57d4b0093b4b6e5487c1e96ad4ace242"}, + {file = "charset_normalizer-3.4.5-cp38-cp38-musllinux_1_2_riscv64.whl", hash = "sha256:4b8551b6e6531e156db71193771c93bda78ffc4d1e6372517fe58ad3b91e4659"}, + {file = "charset_normalizer-3.4.5-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:65b3c403a5b6b8034b655e7385de4f72b7b244869a22b32d4030b99a60593eca"}, + {file = "charset_normalizer-3.4.5-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:8ce11cd4d62d11166f2b441e30ace226c19a3899a7cf0796f668fba49a9fb123"}, + {file = "charset_normalizer-3.4.5-cp38-cp38-win32.whl", hash = "sha256:66dee73039277eb35380d1b82cccc69cc82b13a66f9f4a18da32d573acf02b7c"}, + {file = "charset_normalizer-3.4.5-cp38-cp38-win_amd64.whl", hash = "sha256:d29dd9c016f2078b43d0c357511e87eee5b05108f3dd603423cb389b89813969"}, + {file = "charset_normalizer-3.4.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:259cd1ca995ad525f638e131dbcc2353a586564c038fc548a3fe450a91882139"}, + {file = "charset_normalizer-3.4.5-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8a28afb04baa55abf26df544e3e5c6534245d3daa5178bc4a8eeb48202060d0e"}, + {file = "charset_normalizer-3.4.5-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ff95a9283de8a457e6b12989de3f9f5193430f375d64297d323a615ea52cbdb3"}, + {file = "charset_normalizer-3.4.5-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:708c7acde173eedd4bfa4028484426ba689d2103b28588c513b9db2cd5ecde9c"}, + {file = "charset_normalizer-3.4.5-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:aa92ec1102eaff840ccd1021478af176a831f1bccb08e526ce844b7ddda85c22"}, + {file = "charset_normalizer-3.4.5-cp39-cp39-manylinux_2_31_armv7l.whl", hash = "sha256:5fea359734b140d0d6741189fea5478c6091b54ffc69d7ce119e0a05637d8c99"}, + {file = "charset_normalizer-3.4.5-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e545b51da9f9af5c67815ca0eb40676c0f016d0b0381c86f20451e35696c5f95"}, + {file = "charset_normalizer-3.4.5-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:30987f4a8ed169983f93e1be8ffeea5214a779e27ed0b059835c7afe96550ad7"}, + {file = "charset_normalizer-3.4.5-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:149ec69866c3d6c2fb6f758dbc014ecb09f30b35a5ca90b6a8a2d4e54e18fdfe"}, + {file = "charset_normalizer-3.4.5-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:530beedcec9b6e027e7a4b6ce26eed36678aa39e17da85e6e03d7bd9e8e9d7c9"}, + {file = "charset_normalizer-3.4.5-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:14498a429321de554b140013142abe7608f9d8ccc04d7baf2ad60498374aefa2"}, + {file = "charset_normalizer-3.4.5-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:2820a98460c83663dd8ec015d9ddfd1e4879f12e06bb7d0500f044fb477d2770"}, + {file = "charset_normalizer-3.4.5-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:aa2f963b4da26daf46231d9b9e0e2c9408a751f8f0d0f44d2de56d3caf51d294"}, + {file = "charset_normalizer-3.4.5-cp39-cp39-win32.whl", hash = "sha256:82cc7c2ad42faec8b574351f8bc2a0c049043893853317bd9bb309f5aba6cb5a"}, + {file = "charset_normalizer-3.4.5-cp39-cp39-win_amd64.whl", hash = "sha256:92263f7eca2f4af326cd20de8d16728d2602f7cfea02e790dcde9d83c365d7cc"}, + {file = "charset_normalizer-3.4.5-cp39-cp39-win_arm64.whl", hash = "sha256:014837af6fabf57121b6254fa8ade10dceabc3528b27b721a64bbc7b8b1d4eb4"}, + {file = "charset_normalizer-3.4.5-py3-none-any.whl", hash = "sha256:9db5e3fcdcee89a78c04dffb3fe33c79f77bd741a624946db2591c81b2fc85b0"}, + {file = "charset_normalizer-3.4.5.tar.gz", hash = "sha256:95adae7b6c42a6c5b5b559b1a99149f090a57128155daeea91732c8d970d8644"}, +] + +[[package]] +name = "ckzg" +version = "2.1.6" +description = "Python bindings for C-KZG-4844" +optional = false +python-versions = "*" +groups = ["main"] +files = [ + {file = "ckzg-2.1.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2e181778e627f5d4502e85a9615e751841ff14ed57a76b79ad87856fcf74c227"}, + {file = "ckzg-2.1.6-cp310-cp310-manylinux2010_i686.manylinux_2_12_i686.manylinux_2_28_i686.whl", hash = "sha256:98b8eae4fbe89e99fd5a1282c5fa8b593dab178e6779df9973b7b4cfc4159c03"}, + {file = "ckzg-2.1.6-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e922b8cc960257fb18965bbf4666b4e07dda4f0c4292a58e8745c6c9efc2af44"}, + {file = "ckzg-2.1.6-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:94d847c752e512731f4b333cff5da41be3324d04831fbd87c018135a8f6d9f28"}, + {file = "ckzg-2.1.6-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d44bd2944ce37068ad1468726b44a54791bd272cfc6ca2e544b18cedd7192e48"}, + {file = "ckzg-2.1.6-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:48cd36886b68b768ddc0c50743e69ef3317bd5df614f1f3639fa26b6c7bb822b"}, + {file = "ckzg-2.1.6-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:860f5b3cc9ebc7758da0e4722250010e39df8a75094bf4d360f5de29614c6775"}, + {file = "ckzg-2.1.6-cp310-cp310-win_amd64.whl", hash = "sha256:9339ed55b765fe1b7c020dbfd01531d4d774b70beff8cffdcd2a516756935455"}, + {file = "ckzg-2.1.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a6d20c3d6256352aa1ce811486e926e41a7a600e2c88df4c951fe396bd2344d1"}, + {file = "ckzg-2.1.6-cp311-cp311-manylinux2010_i686.manylinux_2_12_i686.manylinux_2_28_i686.whl", hash = "sha256:60d5bcf2c17c7eeb099f48980adaa58933db86cbc4acdbe4ead59ae0ac152754"}, + {file = "ckzg-2.1.6-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10acb6e9ae534783f372aea5cf8d62dfb7008fbe0b626092e19449a7b399ca49"}, + {file = "ckzg-2.1.6-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:074c736ca325b536e2eeea68bca03688d1a9698ffd45103845b7490254fbb420"}, + {file = "ckzg-2.1.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:cb74d4fbaed8e034839e193d85cb003dd7b5da673adfb3bb5e57b80f71d18736"}, + {file = "ckzg-2.1.6-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:6d33408ecd986c2d370c523312e549c1f54aa95d6f3d5564dab603946c6f7fbd"}, + {file = "ckzg-2.1.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d86808f032a87ea7cf26446e8a89039d09b259d8d5fbc62e7e1c28a903e1ef2f"}, + {file = "ckzg-2.1.6-cp311-cp311-win_amd64.whl", hash = "sha256:15ba55ded3118ab942c5cb9eea0d306f4ab3995a26331f629509fd80678c0063"}, + {file = "ckzg-2.1.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:93b350b0f3d074df84f8836df0db2fb0978403565477b6e25415c48251c5c7a1"}, + {file = "ckzg-2.1.6-cp312-cp312-manylinux2010_i686.manylinux_2_12_i686.manylinux_2_28_i686.whl", hash = "sha256:c1e3cf33671cd35d86d7a7f68ef1f40381a3315a61db8861858247cfda46ca6d"}, + {file = "ckzg-2.1.6-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cfe71caa4f667ded6c87f496ac1783f004c3f5ab29f695f8d3163c75df51398f"}, + {file = "ckzg-2.1.6-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bf835249b20d58de28b097da7c06c3a6b3b5f184120b0ace55373d6b044c9445"}, + {file = "ckzg-2.1.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a82f4bfd4fb1d3b378af859a1d0dc1febb83634981d8d50635afec0c7d10a372"}, + {file = "ckzg-2.1.6-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:36fd682e34c47befb7f28324793a92bb7fb14f8f2845d0b39abbcb6444e9565f"}, + {file = "ckzg-2.1.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:9efcf3359bf12b128b4e0d86ed663946699fecaeb2d1298594c14a7cf14a7feb"}, + {file = "ckzg-2.1.6-cp312-cp312-win_amd64.whl", hash = "sha256:e1c705a96c0ac99669f3691613b6eecd1d36c75fe433322b12293c906f8d8ae2"}, + {file = "ckzg-2.1.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:616cd69938d0d79b13e128f4706ea48c21866c3f7c52547d4f185837d5568d69"}, + {file = "ckzg-2.1.6-cp313-cp313-manylinux2010_i686.manylinux_2_12_i686.manylinux_2_28_i686.whl", hash = "sha256:8d3056cd48f97041f98b73404f397c29aebd04b7f8f3bbc012180680d295a464"}, + {file = "ckzg-2.1.6-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c732e429b50dee04cd51fb601fc9cb4ba4d853e2e29a9914b3fdd36b576b0211"}, + {file = "ckzg-2.1.6-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b0f9933b6e06e6560b4b8980e2385ec4d639cfdebb03bffaadde75a5c61edb45"}, + {file = "ckzg-2.1.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:be65a7c00d445cf07adea7679842df469989e6790df1d846944f9885a4a788be"}, + {file = "ckzg-2.1.6-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:36e2e198c9e0a94498db32b760b446a1c29ba7e01aaec17404237ef6ae1705df"}, + {file = "ckzg-2.1.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5ce6aaac6ad4d70cc6e8ef61b430957150e1eb3370fd898cebd074db85cde987"}, + {file = "ckzg-2.1.6-cp313-cp313-win_amd64.whl", hash = "sha256:e897650e650fd090b97136103963a0bd338ff8582442b6e4b2bd660b0b81ff2e"}, + {file = "ckzg-2.1.6-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:b10f2b50369d95c2d3707293f958a73cc4a505f53d1dfeadb9534aad4dd33ec9"}, + {file = "ckzg-2.1.6-cp314-cp314-manylinux2010_i686.manylinux_2_12_i686.manylinux_2_28_i686.whl", hash = "sha256:c1642c7c1fd9225155660ee5bf96117b1d94a639a7f495c3b655ad7640bbb5c1"}, + {file = "ckzg-2.1.6-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eb3d119e5008385ec3d47e81965bf1c644f50077fa9aa890d49ee1a0963fbfb3"}, + {file = "ckzg-2.1.6-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3e81244ae23f27a6f85dc69838adcd3c5618acef57aec7ed87db8070cd6995bf"}, + {file = "ckzg-2.1.6-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:92b60f5f9eb880c595680af52d609e06dedee2bcdd109597ce58bb5422639b1a"}, + {file = "ckzg-2.1.6-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:c1f9c9b2fd7d5f303eb2420130c1a1ee44a071308e227a8f9e238aeb4e2194ae"}, + {file = "ckzg-2.1.6-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:eaf30b4719199f1d243bd761caaec3582bdad70a6797475c6cd5c03c5ce3cd1d"}, + {file = "ckzg-2.1.6-cp314-cp314-win_amd64.whl", hash = "sha256:30964b9fac452746db7e60c9c324957c8dc7bc815b72bb09eea88409decc33ed"}, + {file = "ckzg-2.1.6-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:4fd1c8e20c52ce77f9ad7b004440b0ba46d22328af07a5eb095ea4f252d22644"}, + {file = "ckzg-2.1.6-cp314-cp314t-manylinux2010_i686.manylinux_2_12_i686.manylinux_2_28_i686.whl", hash = "sha256:502bb5e5bbbf1bc14b324d8e012c06fc30c24840d35a7933b80b839869280491"}, + {file = "ckzg-2.1.6-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c859c8b93e82b9839a5bb443511a0b0631e93cb9275e755f54781693a3afc246"}, + {file = "ckzg-2.1.6-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0330b7a7e0aca5622a31089c1d56a1a7040a52075803d31983fa9101fc45dddc"}, + {file = "ckzg-2.1.6-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:646078c085edc4c92361f6277cb8b6aac978287306e664e3c29de2f26ad206d2"}, + {file = "ckzg-2.1.6-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:1224f2477fc794f7719bbe7650f735188120351b9511a7dd928b2fe8d74911c3"}, + {file = "ckzg-2.1.6-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:3b33131a9674d9dd509eb9fbb59f65c66dc14bfe85bc3dc93af5140274741c12"}, + {file = "ckzg-2.1.6-cp314-cp314t-win_amd64.whl", hash = "sha256:73301ca29c29255960ebcee8bf52151cd3ac8de214c31a4e29dbcde8c44e0571"}, + {file = "ckzg-2.1.6-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:dec64aaffd0c0699e4d148c29ab887ead2535f5c634085645cc739cb1415333a"}, + {file = "ckzg-2.1.6-cp38-cp38-manylinux2010_i686.manylinux_2_12_i686.manylinux_2_28_i686.whl", hash = "sha256:9474bc1eb2232233dd69c9818b23dd52ff82f985e2b3d1429f73566d04ad7f13"}, + {file = "ckzg-2.1.6-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a16902ab068d4cf74ba64a74fba8bed67a90040445affb24d33c9433fe543f0f"}, + {file = "ckzg-2.1.6-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8c54e86489879ffca3c6d5e56d72984d8a2fb10facf70eecc55b075d263aab03"}, + {file = "ckzg-2.1.6-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:74f89e4c24824c3f86a979c87180d60f07b152412e9579b7b9eb54c8124bf139"}, + {file = "ckzg-2.1.6-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:11f80fc1f7bc214e4b6e34c7e832c36a2c6f2920a9e122f8ef7a58095e0f6b4f"}, + {file = "ckzg-2.1.6-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:7e7070fe17996dcfa571bc807a36f28a8cf253a44656e23e5038930dbe01b2aa"}, + {file = "ckzg-2.1.6-cp38-cp38-win_amd64.whl", hash = "sha256:c4f057e98eb7792737e20a90e65ba6b383243190f2fba72b70ee95ed53cf1836"}, + {file = "ckzg-2.1.6-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:7a44f84f014b955f6d5ce58dfba2e0910fc7c1941df979328f88e979a304000d"}, + {file = "ckzg-2.1.6-cp39-cp39-manylinux2010_i686.manylinux_2_12_i686.manylinux_2_28_i686.whl", hash = "sha256:617ee440355694d1a953ca42a1c1cb90bba43119b095f1f9154ecae805e6b83f"}, + {file = "ckzg-2.1.6-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:db08718952312c5df6c1ede1619f4e62870b59ee3ea28357799f36d89da63bb0"}, + {file = "ckzg-2.1.6-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1a172e5ba99e8dad4f1847f0ca15fcbfcb4a78fefa09a52cd249de14d98a7c55"}, + {file = "ckzg-2.1.6-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:7148258ed01c7be6ced6effaae36baaefe4a86dd6c283db32cae0b19e2617798"}, + {file = "ckzg-2.1.6-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:e3e3d830386a6b1d7aaf1aa6027e8cad8fca78df062f9ea1eff0b499ef4b5f9e"}, + {file = "ckzg-2.1.6-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:93c9135ca57181f445ba75344c1c9be563a9ccd33b058aa8dfa1b1d6240380b0"}, + {file = "ckzg-2.1.6-cp39-cp39-win_amd64.whl", hash = "sha256:2d79614f279def0cdaa323ad22ed1792c1c7db2805677fe3864bf057b0c13dfe"}, + {file = "ckzg-2.1.6.tar.gz", hash = "sha256:49df31684283dfcfd1eeca638d84c03788ebdd48e8afc0643bf5188ec023dc8d"}, +] + +[[package]] +name = "click" +version = "8.3.1" +description = "Composable command line interface toolkit" +optional = false +python-versions = ">=3.10" +groups = ["dev"] +files = [ + {file = "click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6"}, + {file = "click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} + +[[package]] +name = "colorama" +version = "0.4.6" +description = "Cross-platform colored terminal text." +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +groups = ["dev"] +markers = "sys_platform == \"win32\" or platform_system == \"Windows\"" +files = [ + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, +] + +[[package]] +name = "coverage" +version = "7.13.4" +description = "Code coverage measurement for Python" +optional = false +python-versions = ">=3.10" +groups = ["dev"] +files = [ + {file = "coverage-7.13.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0fc31c787a84f8cd6027eba44010517020e0d18487064cd3d8968941856d1415"}, + {file = "coverage-7.13.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a32ebc02a1805adf637fc8dec324b5cdacd2e493515424f70ee33799573d661b"}, + {file = "coverage-7.13.4-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:e24f9156097ff9dc286f2f913df3a7f63c0e333dcafa3c196f2c18b4175ca09a"}, + {file = "coverage-7.13.4-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8041b6c5bfdc03257666e9881d33b1abc88daccaf73f7b6340fb7946655cd10f"}, + {file = "coverage-7.13.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2a09cfa6a5862bc2fc6ca7c3def5b2926194a56b8ab78ffcf617d28911123012"}, + {file = "coverage-7.13.4-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:296f8b0af861d3970c2a4d8c91d48eb4dd4771bcef9baedec6a9b515d7de3def"}, + {file = "coverage-7.13.4-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e101609bcbbfb04605ea1027b10dc3735c094d12d40826a60f897b98b1c30256"}, + {file = "coverage-7.13.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:aa3feb8db2e87ff5e6d00d7e1480ae241876286691265657b500886c98f38bda"}, + {file = "coverage-7.13.4-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:4fc7fa81bbaf5a02801b65346c8b3e657f1d93763e58c0abdf7c992addd81a92"}, + {file = "coverage-7.13.4-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:33901f604424145c6e9c2398684b92e176c0b12df77d52db81c20abd48c3794c"}, + {file = "coverage-7.13.4-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:bb28c0f2cf2782508a40cec377935829d5fcc3ad9a3681375af4e84eb34b6b58"}, + {file = "coverage-7.13.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:9d107aff57a83222ddbd8d9ee705ede2af2cc926608b57abed8ef96b50b7e8f9"}, + {file = "coverage-7.13.4-cp310-cp310-win32.whl", hash = "sha256:a6f94a7d00eb18f1b6d403c91a88fd58cfc92d4b16080dfdb774afc8294469bf"}, + {file = "coverage-7.13.4-cp310-cp310-win_amd64.whl", hash = "sha256:2cb0f1e000ebc419632bbe04366a8990b6e32c4e0b51543a6484ffe15eaeda95"}, + {file = "coverage-7.13.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d490ba50c3f35dd7c17953c68f3270e7ccd1c6642e2d2afe2d8e720b98f5a053"}, + {file = "coverage-7.13.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:19bc3c88078789f8ef36acb014d7241961dbf883fd2533d18cb1e7a5b4e28b11"}, + {file = "coverage-7.13.4-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3998e5a32e62fdf410c0dbd3115df86297995d6e3429af80b8798aad894ca7aa"}, + {file = "coverage-7.13.4-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8e264226ec98e01a8e1054314af91ee6cde0eacac4f465cc93b03dbe0bce2fd7"}, + {file = "coverage-7.13.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a3aa4e7b9e416774b21797365b358a6e827ffadaaca81b69ee02946852449f00"}, + {file = "coverage-7.13.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:71ca20079dd8f27fcf808817e281e90220475cd75115162218d0e27549f95fef"}, + {file = "coverage-7.13.4-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e2f25215f1a359ab17320b47bcdaca3e6e6356652e8256f2441e4ef972052903"}, + {file = "coverage-7.13.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d65b2d373032411e86960604dc4edac91fdfb5dca539461cf2cbe78327d1e64f"}, + {file = "coverage-7.13.4-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:94eb63f9b363180aff17de3e7c8760c3ba94664ea2695c52f10111244d16a299"}, + {file = "coverage-7.13.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e856bf6616714c3a9fbc270ab54103f4e685ba236fa98c054e8f87f266c93505"}, + {file = "coverage-7.13.4-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:65dfcbe305c3dfe658492df2d85259e0d79ead4177f9ae724b6fb245198f55d6"}, + {file = "coverage-7.13.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b507778ae8a4c915436ed5c2e05b4a6cecfa70f734e19c22a005152a11c7b6a9"}, + {file = "coverage-7.13.4-cp311-cp311-win32.whl", hash = "sha256:784fc3cf8be001197b652d51d3fd259b1e2262888693a4636e18879f613a62a9"}, + {file = "coverage-7.13.4-cp311-cp311-win_amd64.whl", hash = "sha256:2421d591f8ca05b308cf0092807308b2facbefe54af7c02ac22548b88b95c98f"}, + {file = "coverage-7.13.4-cp311-cp311-win_arm64.whl", hash = "sha256:79e73a76b854d9c6088fe5d8b2ebe745f8681c55f7397c3c0a016192d681045f"}, + {file = "coverage-7.13.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:02231499b08dabbe2b96612993e5fc34217cdae907a51b906ac7fca8027a4459"}, + {file = "coverage-7.13.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40aa8808140e55dc022b15d8aa7f651b6b3d68b365ea0398f1441e0b04d859c3"}, + {file = "coverage-7.13.4-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5b856a8ccf749480024ff3bd7310adaef57bf31fd17e1bfc404b7940b6986634"}, + {file = "coverage-7.13.4-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2c048ea43875fbf8b45d476ad79f179809c590ec7b79e2035c662e7afa3192e3"}, + {file = "coverage-7.13.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b7b38448866e83176e28086674fe7368ab8590e4610fb662b44e345b86d63ffa"}, + {file = "coverage-7.13.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:de6defc1c9badbf8b9e67ae90fd00519186d6ab64e5cc5f3d21359c2a9b2c1d3"}, + {file = "coverage-7.13.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7eda778067ad7ffccd23ecffce537dface96212576a07924cbf0d8799d2ded5a"}, + {file = "coverage-7.13.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e87f6c587c3f34356c3759f0420693e35e7eb0e2e41e4c011cb6ec6ecbbf1db7"}, + {file = "coverage-7.13.4-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:8248977c2e33aecb2ced42fef99f2d319e9904a36e55a8a68b69207fb7e43edc"}, + {file = "coverage-7.13.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:25381386e80ae727608e662474db537d4df1ecd42379b5ba33c84633a2b36d47"}, + {file = "coverage-7.13.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:ee756f00726693e5ba94d6df2bdfd64d4852d23b09bb0bc700e3b30e6f333985"}, + {file = "coverage-7.13.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fdfc1e28e7c7cdce44985b3043bc13bbd9c747520f94a4d7164af8260b3d91f0"}, + {file = "coverage-7.13.4-cp312-cp312-win32.whl", hash = "sha256:01d4cbc3c283a17fc1e42d614a119f7f438eabb593391283adca8dc86eff1246"}, + {file = "coverage-7.13.4-cp312-cp312-win_amd64.whl", hash = "sha256:9401ebc7ef522f01d01d45532c68c5ac40fb27113019b6b7d8b208f6e9baa126"}, + {file = "coverage-7.13.4-cp312-cp312-win_arm64.whl", hash = "sha256:b1ec7b6b6e93255f952e27ab58fbc68dcc468844b16ecbee881aeb29b6ab4d8d"}, + {file = "coverage-7.13.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b66a2da594b6068b48b2692f043f35d4d3693fb639d5ea8b39533c2ad9ac3ab9"}, + {file = "coverage-7.13.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3599eb3992d814d23b35c536c28df1a882caa950f8f507cef23d1cbf334995ac"}, + {file = "coverage-7.13.4-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:93550784d9281e374fb5a12bf1324cc8a963fd63b2d2f223503ef0fd4aa339ea"}, + {file = "coverage-7.13.4-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b720ce6a88a2755f7c697c23268ddc47a571b88052e6b155224347389fdf6a3b"}, + {file = "coverage-7.13.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7b322db1284a2ed3aa28ffd8ebe3db91c929b7a333c0820abec3d838ef5b3525"}, + {file = "coverage-7.13.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f4594c67d8a7c89cf922d9df0438c7c7bb022ad506eddb0fdb2863359ff78242"}, + {file = "coverage-7.13.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:53d133df809c743eb8bce33b24bcababb371f4441340578cd406e084d94a6148"}, + {file = "coverage-7.13.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:76451d1978b95ba6507a039090ba076105c87cc76fc3efd5d35d72093964d49a"}, + {file = "coverage-7.13.4-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:7f57b33491e281e962021de110b451ab8a24182589be17e12a22c79047935e23"}, + {file = "coverage-7.13.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:1731dc33dc276dafc410a885cbf5992f1ff171393e48a21453b78727d090de80"}, + {file = "coverage-7.13.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:bd60d4fe2f6fa7dff9223ca1bbc9f05d2b6697bc5961072e5d3b952d46e1b1ea"}, + {file = "coverage-7.13.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9181a3ccead280b828fae232df12b16652702b49d41e99d657f46cc7b1f6ec7a"}, + {file = "coverage-7.13.4-cp313-cp313-win32.whl", hash = "sha256:f53d492307962561ac7de4cd1de3e363589b000ab69617c6156a16ba7237998d"}, + {file = "coverage-7.13.4-cp313-cp313-win_amd64.whl", hash = "sha256:e6f70dec1cc557e52df5306d051ef56003f74d56e9c4dd7ddb07e07ef32a84dd"}, + {file = "coverage-7.13.4-cp313-cp313-win_arm64.whl", hash = "sha256:fb07dc5da7e849e2ad31a5d74e9bece81f30ecf5a42909d0a695f8bd1874d6af"}, + {file = "coverage-7.13.4-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:40d74da8e6c4b9ac18b15331c4b5ebc35a17069410cad462ad4f40dcd2d50c0d"}, + {file = "coverage-7.13.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4223b4230a376138939a9173f1bdd6521994f2aff8047fae100d6d94d50c5a12"}, + {file = "coverage-7.13.4-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:1d4be36a5114c499f9f1f9195e95ebf979460dbe2d88e6816ea202010ba1c34b"}, + {file = "coverage-7.13.4-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:200dea7d1e8095cc6e98cdabe3fd1d21ab17d3cee6dab00cadbb2fe35d9c15b9"}, + {file = "coverage-7.13.4-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b8eb931ee8e6d8243e253e5ed7336deea6904369d2fd8ae6e43f68abbf167092"}, + {file = "coverage-7.13.4-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:75eab1ebe4f2f64d9509b984f9314d4aa788540368218b858dad56dc8f3e5eb9"}, + {file = "coverage-7.13.4-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c35eb28c1d085eb7d8c9b3296567a1bebe03ce72962e932431b9a61f28facf26"}, + {file = "coverage-7.13.4-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:eb88b316ec33760714a4720feb2816a3a59180fd58c1985012054fa7aebee4c2"}, + {file = "coverage-7.13.4-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:7d41eead3cc673cbd38a4417deb7fd0b4ca26954ff7dc6078e33f6ff97bed940"}, + {file = "coverage-7.13.4-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:fb26a934946a6afe0e326aebe0730cdff393a8bc0bbb65a2f41e30feddca399c"}, + {file = "coverage-7.13.4-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:dae88bc0fc77edaa65c14be099bd57ee140cf507e6bfdeea7938457ab387efb0"}, + {file = "coverage-7.13.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:845f352911777a8e722bfce168958214951e07e47e5d5d9744109fa5fe77f79b"}, + {file = "coverage-7.13.4-cp313-cp313t-win32.whl", hash = "sha256:2fa8d5f8de70688a28240de9e139fa16b153cc3cbb01c5f16d88d6505ebdadf9"}, + {file = "coverage-7.13.4-cp313-cp313t-win_amd64.whl", hash = "sha256:9351229c8c8407645840edcc277f4a2d44814d1bc34a2128c11c2a031d45a5dd"}, + {file = "coverage-7.13.4-cp313-cp313t-win_arm64.whl", hash = "sha256:30b8d0512f2dc8c8747557e8fb459d6176a2c9e5731e2b74d311c03b78451997"}, + {file = "coverage-7.13.4-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:300deaee342f90696ed186e3a00c71b5b3d27bffe9e827677954f4ee56969601"}, + {file = "coverage-7.13.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:29e3220258d682b6226a9b0925bc563ed9a1ebcff3cad30f043eceea7eaf2689"}, + {file = "coverage-7.13.4-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:391ee8f19bef69210978363ca930f7328081c6a0152f1166c91f0b5fdd2a773c"}, + {file = "coverage-7.13.4-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0dd7ab8278f0d58a0128ba2fca25824321f05d059c1441800e934ff2efa52129"}, + {file = "coverage-7.13.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:78cdf0d578b15148b009ccf18c686aa4f719d887e76e6b40c38ffb61d264a552"}, + {file = "coverage-7.13.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:48685fee12c2eb3b27c62f2658e7ea21e9c3239cba5a8a242801a0a3f6a8c62a"}, + {file = "coverage-7.13.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4e83efc079eb39480e6346a15a1bcb3e9b04759c5202d157e1dd4303cd619356"}, + {file = "coverage-7.13.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ecae9737b72408d6a950f7e525f30aca12d4bd8dd95e37342e5beb3a2a8c4f71"}, + {file = "coverage-7.13.4-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ae4578f8528569d3cf303fef2ea569c7f4c4059a38c8667ccef15c6e1f118aa5"}, + {file = "coverage-7.13.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:6fdef321fdfbb30a197efa02d48fcd9981f0d8ad2ae8903ac318adc653f5df98"}, + {file = "coverage-7.13.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b0f6ccf3dbe577170bebfce1318707d0e8c3650003cb4b3a9dd744575daa8b5"}, + {file = "coverage-7.13.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:75fcd519f2a5765db3f0e391eb3b7d150cce1a771bf4c9f861aeab86c767a3c0"}, + {file = "coverage-7.13.4-cp314-cp314-win32.whl", hash = "sha256:8e798c266c378da2bd819b0677df41ab46d78065fb2a399558f3f6cae78b2fbb"}, + {file = "coverage-7.13.4-cp314-cp314-win_amd64.whl", hash = "sha256:245e37f664d89861cf2329c9afa2c1fe9e6d4e1a09d872c947e70718aeeac505"}, + {file = "coverage-7.13.4-cp314-cp314-win_arm64.whl", hash = "sha256:ad27098a189e5838900ce4c2a99f2fe42a0bf0c2093c17c69b45a71579e8d4a2"}, + {file = "coverage-7.13.4-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:85480adfb35ffc32d40918aad81b89c69c9cc5661a9b8a81476d3e645321a056"}, + {file = "coverage-7.13.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:79be69cf7f3bf9b0deeeb062eab7ac7f36cd4cc4c4dd694bd28921ba4d8596cc"}, + {file = "coverage-7.13.4-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:caa421e2684e382c5d8973ac55e4f36bed6821a9bad5c953494de960c74595c9"}, + {file = "coverage-7.13.4-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:14375934243ee05f56c45393fe2ce81fe5cc503c07cee2bdf1725fb8bef3ffaf"}, + {file = "coverage-7.13.4-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:25a41c3104d08edb094d9db0d905ca54d0cd41c928bb6be3c4c799a54753af55"}, + {file = "coverage-7.13.4-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6f01afcff62bf9a08fb32b2c1d6e924236c0383c02c790732b6537269e466a72"}, + {file = "coverage-7.13.4-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:eb9078108fbf0bcdde37c3f4779303673c2fa1fe8f7956e68d447d0dd426d38a"}, + {file = "coverage-7.13.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:0e086334e8537ddd17e5f16a344777c1ab8194986ec533711cbe6c41cde841b6"}, + {file = "coverage-7.13.4-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:725d985c5ab621268b2edb8e50dfe57633dc69bda071abc470fed55a14935fd3"}, + {file = "coverage-7.13.4-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:3c06f0f1337c667b971ca2f975523347e63ec5e500b9aa5882d91931cd3ef750"}, + {file = "coverage-7.13.4-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:590c0ed4bf8e85f745e6b805b2e1c457b2e33d5255dd9729743165253bc9ad39"}, + {file = "coverage-7.13.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:eb30bf180de3f632cd043322dad5751390e5385108b2807368997d1a92a509d0"}, + {file = "coverage-7.13.4-cp314-cp314t-win32.whl", hash = "sha256:c4240e7eded42d131a2d2c4dec70374b781b043ddc79a9de4d55ca71f8e98aea"}, + {file = "coverage-7.13.4-cp314-cp314t-win_amd64.whl", hash = "sha256:4c7d3cc01e7350f2f0f6f7036caaf5673fb56b6998889ccfe9e1c1fe75a9c932"}, + {file = "coverage-7.13.4-cp314-cp314t-win_arm64.whl", hash = "sha256:23e3f687cf945070d1c90f85db66d11e3025665d8dafa831301a0e0038f3db9b"}, + {file = "coverage-7.13.4-py3-none-any.whl", hash = "sha256:1af1641e57cf7ba1bd67d677c9abdbcd6cc2ab7da3bca7fa1e2b7e50e65f2ad0"}, + {file = "coverage-7.13.4.tar.gz", hash = "sha256:e5c8f6ed1e61a8b2dcdf31eb0b9bbf0130750ca79c1c49eb898e2ad86f5ccc91"}, +] + +[package.dependencies] +tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.11.0a6\" and extra == \"toml\""} + +[package.extras] +toml = ["tomli ; python_full_version <= \"3.11.0a6\""] + +[[package]] +name = "cytoolz" +version = "1.1.0" +description = "Cython implementation of Toolz: High performance functional utilities" +optional = false +python-versions = ">=3.9" +groups = ["main"] +markers = "implementation_name == \"cpython\"" +files = [ + {file = "cytoolz-1.1.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:72d7043a88ea5e61ba9d17ea0d1c1eff10f645d7edfcc4e56a31ef78be287644"}, + {file = "cytoolz-1.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d759e9ed421bacfeb456d47af8d734c057b9912b5f2441f95b27ca35e5efab07"}, + {file = "cytoolz-1.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fdb5be8fbcc0396141189022724155a4c1c93712ac4aef8c03829af0c2a816d7"}, + {file = "cytoolz-1.1.0-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:c8c0a513dc89bc05cc72893609118815bced5ef201f1a317b4cc3423b3a0e750"}, + {file = "cytoolz-1.1.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ce94db4f8ebe842c30c0ece42ff5de977c47859088c2c363dede5a68f6906484"}, + {file = "cytoolz-1.1.0-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b622d4f54e370c853ded94a668f94fe72c6d70e06ac102f17a2746661c27ab52"}, + {file = "cytoolz-1.1.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:375a65baa5a5b4ff6a0c5ff17e170cf23312e4c710755771ca966144c24216b5"}, + {file = "cytoolz-1.1.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c0d51bcdb3203a062a78f66bbe33db5e3123048e24a5f0e1402422d79df8ee2d"}, + {file = "cytoolz-1.1.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1010869529bb05dc9802b6d776a34ca1b6d48b9deec70ad5e2918ae175be5c2f"}, + {file = "cytoolz-1.1.0-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:11a8f2e83295bdb33f35454d6bafcb7845b03b5881dcaed66ecbd726c7f16772"}, + {file = "cytoolz-1.1.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0499c5e0a8e688ed367a2e51cc13792ae8f08226c15f7d168589fc44b9b9cada"}, + {file = "cytoolz-1.1.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:87d44e6033d4c5e95a7d39ba59b8e105ba1c29b1ccd1d215f26477cc1d64be39"}, + {file = "cytoolz-1.1.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:a68cef396a7de237f7b97422a6a450dfb111722296ba217ba5b34551832f1f6e"}, + {file = "cytoolz-1.1.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:06ad4c95b258141f138a93ebfdc1d76ac087afc1a82f1401100a1f44b44ba656"}, + {file = "cytoolz-1.1.0-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:ada59a4b3c59d4ac7162e0ed08667ffa78abf48e975c8a9f9d5b9bc50720f4fd"}, + {file = "cytoolz-1.1.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:a8957bcaea1ba01327a9b219d2adb84144377684f51444253890dab500ca171f"}, + {file = "cytoolz-1.1.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:6d8cdc299d67eb0f3b9ecdafeeb55eb3b7b7470e2d950ac34b05ed4c7a5572b8"}, + {file = "cytoolz-1.1.0-cp310-cp310-win32.whl", hash = "sha256:d8e08464c5cdea4f6df31e84b11ed6bfd79cedb99fbcbfdc15eb9361a6053c5a"}, + {file = "cytoolz-1.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:7e49922a7ed54262d41960bf3b835a7700327bf79cff1e9bfc73d79021132ff8"}, + {file = "cytoolz-1.1.0-cp310-cp310-win_arm64.whl", hash = "sha256:943a662d2e72ffc4438d43ab5a1de8d852237775a423236594a3b3e381b8032c"}, + {file = "cytoolz-1.1.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:dba8e5a8c6e3c789d27b0eb5e7ce5ed7d032a7a9aae17ca4ba5147b871f6e327"}, + {file = "cytoolz-1.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:44b31c05addb0889167a720123b3b497b28dd86f8a0aeaf3ae4ffa11e2c85d55"}, + {file = "cytoolz-1.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:653cb18c4fc5d8a8cfce2bce650aabcbe82957cd0536827367d10810566d5294"}, + {file = "cytoolz-1.1.0-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:853a5b4806915020c890e1ce70cc056bbc1dd8bc44f2d74d555cccfd7aefba7d"}, + {file = "cytoolz-1.1.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c7b44e9de86bea013fe84fd8c399d6016bbb96c37c5290769e5c99460b9c53e5"}, + {file = "cytoolz-1.1.0-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:098d628a801dc142e9740126be5624eb7aef1d732bc7a5719f60a2095547b485"}, + {file = "cytoolz-1.1.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:779ee4096ed7a82cffab89372ffc339631c285079dbf33dbe7aff1f6174985df"}, + {file = "cytoolz-1.1.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f2ce18dd99533d077e9712f9faa852f389f560351b1efd2f2bdb193a95eddde2"}, + {file = "cytoolz-1.1.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ac266a34437812cf841cecbfe19f355ab9c3dd1ef231afc60415d40ff12a76e4"}, + {file = "cytoolz-1.1.0-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1920b9b9c13d60d0bb6cd14594b3bce0870022eccb430618c37156da5f2b7a55"}, + {file = "cytoolz-1.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47caa376dafd2bdc29f8a250acf59c810ec9105cd6f7680b9a9d070aae8490ec"}, + {file = "cytoolz-1.1.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:5ab2c97d8aaa522b038cca9187b1153347af22309e7c998b14750c6fdec7b1cb"}, + {file = "cytoolz-1.1.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:4bce006121b120e8b359244ee140bb0b1093908efc8b739db8dbaa3f8fb42139"}, + {file = "cytoolz-1.1.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:7fc0f1e4e9bb384d26e73c6657bbc26abdae4ff66a95933c00f3d578be89181b"}, + {file = "cytoolz-1.1.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:dd3f894ff972da1994d06ac6157d74e40dda19eb31fe5e9b7863ca4278c3a167"}, + {file = "cytoolz-1.1.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:0846f49cf8a4496bd42659040e68bd0484ce6af819709cae234938e039203ba0"}, + {file = "cytoolz-1.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:16a3af394ade1973226d64bb2f9eb3336adbdea03ed5b134c1bbec5a3b20028e"}, + {file = "cytoolz-1.1.0-cp311-cp311-win32.whl", hash = "sha256:b786c9c8aeab76cc2f76011e986f7321a23a56d985b77d14f155d5e5514ea781"}, + {file = "cytoolz-1.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:ebf06d1c5344fb22fee71bf664234733e55db72d74988f2ecb7294b05e4db30c"}, + {file = "cytoolz-1.1.0-cp311-cp311-win_arm64.whl", hash = "sha256:b63f5f025fac893393b186e132e3e242de8ee7265d0cd3f5bdd4dda93f6616c9"}, + {file = "cytoolz-1.1.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:99f8e134c9be11649342853ec8c90837af4089fc8ff1e8f9a024a57d1fa08514"}, + {file = "cytoolz-1.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:0a6f44cf9319c30feb9a50aa513d777ef51efec16f31c404409e7deb8063df64"}, + {file = "cytoolz-1.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:945580dc158c557172fca899a35a99a16fbcebf6db0c77cb6621084bc82189f9"}, + {file = "cytoolz-1.1.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:257905ec050d04f2f856854620d1e25556fd735064cebd81b460f54939b9f9d5"}, + {file = "cytoolz-1.1.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:82779049f352fb3ab5e8c993ab45edbb6e02efb1f17f0b50f4972c706cc51d76"}, + {file = "cytoolz-1.1.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7d3e405e435320e08c5a1633afaf285a392e2d9cef35c925d91e2a31dfd7a688"}, + {file = "cytoolz-1.1.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:923df8f5591e0d20543060c29909c149ab1963a7267037b39eee03a83dbc50a8"}, + {file = "cytoolz-1.1.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:25db9e4862f22ea0ae2e56c8bec9fc9fd756b655ae13e8c7b5625d7ed1c582d4"}, + {file = "cytoolz-1.1.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c7a98deb11ccd8e5d9f9441ef2ff3352aab52226a2b7d04756caaa53cd612363"}, + {file = "cytoolz-1.1.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:dce4ee9fc99104bc77efdea80f32ca5a650cd653bcc8a1d984a931153d3d9b58"}, + {file = "cytoolz-1.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:80d6da158f7d20c15819701bbda1c041f0944ede2f564f5c739b1bc80a9ffb8b"}, + {file = "cytoolz-1.1.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:3b5c5a192abda123ad45ef716ec9082b4cf7d95e9ada8291c5c2cc5558be858b"}, + {file = "cytoolz-1.1.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5b399ce7d967b1cb6280250818b786be652aa8ddffd3c0bb5c48c6220d945ab5"}, + {file = "cytoolz-1.1.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:e7e29a1a03f00b4322196cfe8e2c38da9a6c8d573566052c586df83aacc5663c"}, + {file = "cytoolz-1.1.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:5291b117d71652a817ec164e7011f18e6a51f8a352cc9a70ed5b976c51102fda"}, + {file = "cytoolz-1.1.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:8caef62f846a9011676c51bda9189ae394cdd6bb17f2946ecaedc23243268320"}, + {file = "cytoolz-1.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:de425c5a8e3be7bb3a195e19191d28d9eb3c2038046064a92edc4505033ec9cb"}, + {file = "cytoolz-1.1.0-cp312-cp312-win32.whl", hash = "sha256:296440a870e8d1f2e1d1edf98f60f1532b9d3ab8dfbd4b25ec08cd76311e79e5"}, + {file = "cytoolz-1.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:07156987f224c6dac59aa18fb8bf91e1412f5463961862716a3381bf429c8699"}, + {file = "cytoolz-1.1.0-cp312-cp312-win_arm64.whl", hash = "sha256:23e616b38f5b3160c7bb45b0f84a8f3deb4bd26b29fb2dfc716f241c738e27b8"}, + {file = "cytoolz-1.1.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:76c9b58555300be6dde87a41faf1f97966d79b9a678b7a526fcff75d28ef4945"}, + {file = "cytoolz-1.1.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:d1d638b10d3144795655e9395566ce35807df09219fd7cacd9e6acbdef67946a"}, + {file = "cytoolz-1.1.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:26801c1a165e84786a99e03c9c9973356caaca002d66727b761fb1042878ef06"}, + {file = "cytoolz-1.1.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:2a9a464542912d3272f6dccc5142df057c71c6a5cbd30439389a732df401afb7"}, + {file = "cytoolz-1.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ed6104fa942aa5784bf54f339563de637557e3443b105760bc4de8f16a7fc79b"}, + {file = "cytoolz-1.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:56161f0ab60dc4159ec343509abaf809dc88e85c7e420e354442c62e3e7cbb77"}, + {file = "cytoolz-1.1.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:832bd36cc9123535f1945acf6921f8a2a15acc19cfe4065b1c9b985a28671886"}, + {file = "cytoolz-1.1.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1842636b6e034f229bf084c2bcdcfd36c8437e752eefd2c74ce9e2f10415cb6e"}, + {file = "cytoolz-1.1.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:823df012ab90d2f2a0f92fea453528539bf71ac1879e518524cd0c86aa6df7b9"}, + {file = "cytoolz-1.1.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2f1fcf9e7e7b3487883ff3f815abc35b89dcc45c4cf81c72b7ee457aa72d197b"}, + {file = "cytoolz-1.1.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4cdb3fa1772116827f263f25b0cdd44c663b6701346a56411960534a06c082de"}, + {file = "cytoolz-1.1.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d1b5c95041741b81430454db65183e133976f45ac3c03454cfa8147952568529"}, + {file = "cytoolz-1.1.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b2079fd9f1a65f4c61e6278c8a6d4f85edf30c606df8d5b32f1add88cbbe2286"}, + {file = "cytoolz-1.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a92a320d72bef1c7e2d4c6d875125cf57fc38be45feb3fac1bfa64ea401f54a4"}, + {file = "cytoolz-1.1.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:06d1c79aa51e6a92a90b0e456ebce2288f03dd6a76c7f582bfaa3eda7692e8a5"}, + {file = "cytoolz-1.1.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:e1d7be25f6971e986a52b6d3a0da28e1941850985417c35528f6823aef2cfec5"}, + {file = "cytoolz-1.1.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:964b248edc31efc50a65e9eaa0c845718503823439d2fa5f8d2c7e974c2b5409"}, + {file = "cytoolz-1.1.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c9ff2b3c57c79b65cb5be14a18c6fd4a06d5036fb3f33e973a9f70e9ac13ca28"}, + {file = "cytoolz-1.1.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:22290b73086af600042d99f5ce52a43d4ad9872c382610413176e19fc1d4fd2d"}, + {file = "cytoolz-1.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a2ade74fccd080ea793382968913ee38d7a35c921df435bbf0a6aeecf0d17574"}, + {file = "cytoolz-1.1.0-cp313-cp313-win32.whl", hash = "sha256:db5dbcfda1c00e937426cbf9bdc63c24ebbc358c3263bfcbc1ab4a88dc52aa8e"}, + {file = "cytoolz-1.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:9e2d3fe3b45c3eb7233746f7aca37789be3dceec3e07dcc406d3e045ea0f7bdc"}, + {file = "cytoolz-1.1.0-cp313-cp313-win_arm64.whl", hash = "sha256:32c559f95ff44a9ebcbd934acaa1e6dc8f3e6ffce4762a79a88528064873d6d5"}, + {file = "cytoolz-1.1.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:9e2cd93b28f667c5870a070ab2b8bb4397470a85c4b204f2454b0ad001cd1ca3"}, + {file = "cytoolz-1.1.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:f494124e141a9361f31d79875fe7ea459a3be2b9dadd90480427c0c52a0943d4"}, + {file = "cytoolz-1.1.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:53a3262bf221f19437ed544bf8c0e1980c81ac8e2a53d87a9bc075dba943d36f"}, + {file = "cytoolz-1.1.0-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:47663e57d3f3f124921f38055e86a1022d0844c444ede2e8f090d3bbf80deb65"}, + {file = "cytoolz-1.1.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a5a8755c4104ee4e3d5ba434c543b5f85fdee6a1f1df33d93f518294da793a60"}, + {file = "cytoolz-1.1.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:4d96ff3d381423af1b105295f97de86d1db51732c9566eb37378bab6670c5010"}, + {file = "cytoolz-1.1.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0ec96b3d537cdf47d4e76ded199f7440715f4c71029b45445cff92c1248808c2"}, + {file = "cytoolz-1.1.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:208e2f2ef90a32b0acbff3303d90d89b13570a228d491d2e622a7883a3c68148"}, + {file = "cytoolz-1.1.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0d416a81bb0bd517558668e49d30a7475b5445f9bbafaab7dcf066f1e9adba36"}, + {file = "cytoolz-1.1.0-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f32e94c91ffe49af04835ee713ebd8e005c85ebe83e7e1fdcc00f27164c2d636"}, + {file = "cytoolz-1.1.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:15d0c6405efc040499c46df44056a5c382f551a7624a41cf3e4c84a96b988a15"}, + {file = "cytoolz-1.1.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:bf069c5381d757debae891401b88b3a346ba3a28ca45ba9251103b282463fad8"}, + {file = "cytoolz-1.1.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:7d5cf15892e63411ec1bd67deff0e84317d974e6ab2cdfefdd4a7cea2989df66"}, + {file = "cytoolz-1.1.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:3e3872c21170f8341656f8692f8939e8800dcee6549ad2474d4c817bdefd62cd"}, + {file = "cytoolz-1.1.0-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:b9ddeff8e8fd65eb1fcefa61018100b2b627e759ea6ad275d2e2a93ffac147bf"}, + {file = "cytoolz-1.1.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:02feeeda93e1fa3b33414eb57c2b0aefd1db8f558dd33fdfcce664a0f86056e4"}, + {file = "cytoolz-1.1.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:d08154ad45349162b6c37f12d5d1b2e6eef338e657b85e1621e4e6a4a69d64cb"}, + {file = "cytoolz-1.1.0-cp313-cp313t-win32.whl", hash = "sha256:10ae4718a056948d73ca3e1bb9ab1f95f897ec1e362f829b9d37cc29ab566c60"}, + {file = "cytoolz-1.1.0-cp313-cp313t-win_amd64.whl", hash = "sha256:1bb77bc6197e5cb19784b6a42bb0f8427e81737a630d9d7dda62ed31733f9e6c"}, + {file = "cytoolz-1.1.0-cp313-cp313t-win_arm64.whl", hash = "sha256:563dda652c6ff52d215704fbe6b491879b78d7bbbb3a9524ec8e763483cb459f"}, + {file = "cytoolz-1.1.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:d542cee7c7882d2a914a33dec4d3600416fb336734df979473249d4c53d207a1"}, + {file = "cytoolz-1.1.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:31922849b701b0f24bb62e56eb2488dcd3aa6ae3057694bd6b3b7c4c2bc27c2f"}, + {file = "cytoolz-1.1.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:e68308d32afd31943314735c1335e4ab5696110e96b405f6bdb8f2a8dc771a16"}, + {file = "cytoolz-1.1.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:fc4bb48b3b866e1867f7c6411a4229e5b44be3989060663713e10efc24c9bd5f"}, + {file = "cytoolz-1.1.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:456f77207d1445025d7ef262b8370a05492dcb1490cb428b0f3bf1bd744a89b0"}, + {file = "cytoolz-1.1.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:174ebc71ebb20a9baeffce6ee07ee2cd913754325c93f99d767380d8317930f7"}, + {file = "cytoolz-1.1.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:8b3604fef602bcd53415055a4f68468339192fd17be39e687ae24f476d23d56e"}, + {file = "cytoolz-1.1.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3604b959a01f64c366e7d10ec7634d5f5cfe10301e27a8f090f6eb3b2a628a18"}, + {file = "cytoolz-1.1.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6db2127a3c1bc2f59f08010d2ae53a760771a9de2f67423ad8d400e9ba4276e8"}, + {file = "cytoolz-1.1.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:56584745ac647993a016a21bc76399113b7595e312f8d0a1b140c9fcf9b58a27"}, + {file = "cytoolz-1.1.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:db2c4c3a7f7bd7e03bb1a236a125c8feb86c75802f4ecda6ecfaf946610b2930"}, + {file = "cytoolz-1.1.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:48cb8a692111a285d2b9acd16d185428176bfbffa8a7c274308525fccd01dd42"}, + {file = "cytoolz-1.1.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d2f344ba5eb17dcf38ee37fdde726f69053f54927db8f8a1bed6ac61e5b1890d"}, + {file = "cytoolz-1.1.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:abf76b1c1abd031f098f293b6d90ee08bdaa45f8b5678430e331d991b82684b1"}, + {file = "cytoolz-1.1.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:ddf9a38a5b686091265ff45b53d142e44a538cd6c2e70610d3bc6be094219032"}, + {file = "cytoolz-1.1.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:946786755274f07bb2be0400f28adb31d7d85a7c7001873c0a8e24a503428fb3"}, + {file = "cytoolz-1.1.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:d5b8f78b9fed79cf185ad4ddec099abeef45951bdcb416c5835ba05f0a1242c7"}, + {file = "cytoolz-1.1.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:fccde6efefdbc02e676ccb352a2ccc8a8e929f59a1c6d3d60bb78e923a49ca44"}, + {file = "cytoolz-1.1.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:717b7775313da5f51b0fbf50d865aa9c39cb241bd4cb605df3cf2246d6567397"}, + {file = "cytoolz-1.1.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5158744a09d0e0e4a4f82225e3a3c4ebf38f9ae74467aaa905467270e52f2794"}, + {file = "cytoolz-1.1.0-cp314-cp314-win32.whl", hash = "sha256:1ed534bdbbf063b2bb28fca7d0f6723a3e5a72b086e7c7fe6d74ae8c3e4d00e2"}, + {file = "cytoolz-1.1.0-cp314-cp314-win_amd64.whl", hash = "sha256:472c1c9a085f5ad973ec0ad7f0b9ba0969faea6f96c9e397f6293d386f3a25ec"}, + {file = "cytoolz-1.1.0-cp314-cp314-win_arm64.whl", hash = "sha256:a7ad7ca3386fa86bd301be3fa36e7f0acb024f412f665937955acfc8eb42deff"}, + {file = "cytoolz-1.1.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:64b63ed4b71b1ba813300ad0f06b8aff19a12cf51116e0e4f1ed837cea4debcf"}, + {file = "cytoolz-1.1.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:a60ba6f2ed9eb0003a737e1ee1e9fa2258e749da6477946008d4324efa25149f"}, + {file = "cytoolz-1.1.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1aa58e2434d732241f7f051e6f17657e969a89971025e24578b5cbc6f1346485"}, + {file = "cytoolz-1.1.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:6965af3fc7214645970e312deb9bd35a213a1eaabcfef4f39115e60bf2f76867"}, + {file = "cytoolz-1.1.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ddd2863f321d67527d3b67a93000a378ad6f967056f68c06467fe011278a6d0e"}, + {file = "cytoolz-1.1.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:4e6b428e9eb5126053c2ae0efa62512ff4b38ed3951f4d0888ca7005d63e56f5"}, + {file = "cytoolz-1.1.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d758e5ef311d2671e0ae8c214c52e44617cf1e58bef8f022b547b9802a5a7f30"}, + {file = "cytoolz-1.1.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a95416eca473e6c1179b48d86adcf528b59c63ce78f4cb9934f2e413afa9b56b"}, + {file = "cytoolz-1.1.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:36c8ede93525cf11e2cc787b7156e5cecd7340193ef800b816a16f1404a8dc6d"}, + {file = "cytoolz-1.1.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c0c949755b6d8a649c5fbc888bc30915926f1b09fe42fea9f289e297c2f6ddd3"}, + {file = "cytoolz-1.1.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e1b6d37545816905a76d9ed59fa4e332f929e879f062a39ea0f6f620405cdc27"}, + {file = "cytoolz-1.1.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:05332112d4087904842b36954cd1d3fc0e463a2f4a7ef9477bd241427c593c3b"}, + {file = "cytoolz-1.1.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:31538ca2fad2d688cbd962ccc3f1da847329e2258a52940f10a2ac0719e526be"}, + {file = "cytoolz-1.1.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:747562aa70abf219ea16f07d50ac0157db856d447f7f498f592e097cbc77df0b"}, + {file = "cytoolz-1.1.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:3dc15c48b20c0f467e15e341e102896c8422dccf8efc6322def5c1b02f074629"}, + {file = "cytoolz-1.1.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:3c03137ee6103ba92d5d6ad6a510e86fded69cd67050bd8a1843f15283be17ac"}, + {file = "cytoolz-1.1.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:be8e298d88f88bd172b59912240558be3b7a04959375646e7fd4996401452941"}, + {file = "cytoolz-1.1.0-cp314-cp314t-win32.whl", hash = "sha256:3d407140f5604a89578285d4aac7b18b8eafa055cf776e781aabb89c48738fad"}, + {file = "cytoolz-1.1.0-cp314-cp314t-win_amd64.whl", hash = "sha256:56e5afb69eb6e1b3ffc34716ee5f92ffbdb5cb003b3a5ca4d4b0fe700e217162"}, + {file = "cytoolz-1.1.0-cp314-cp314t-win_arm64.whl", hash = "sha256:27b19b4a286b3ff52040efa42dbe403730aebe5fdfd2def704eb285e2125c63e"}, + {file = "cytoolz-1.1.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:08a63935c66488511b7b29b06233be0be5f4123622fc8fd488f28dc1b7e4c164"}, + {file = "cytoolz-1.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:93bd0afcc4cc05794507084afaefb161c3639f283ee629bd0e8654b5c0327ba8"}, + {file = "cytoolz-1.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:8f3d4da470cfd5cf44f6d682c6eb01363066e0af53ebe111225e44a618f9453d"}, + {file = "cytoolz-1.1.0-cp39-cp39-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:ba6c12d0e6a67399f4102b4980f4f1bebdbf226ed0a68e84617709d4009b4e71"}, + {file = "cytoolz-1.1.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b557071405b4aeeaa7cbec1a95d15d6c8f37622fe3f4b595311e0e226ce772c"}, + {file = "cytoolz-1.1.0-cp39-cp39-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:cdb406001474726a47fbe903f3aba0de86f5c0b9c9861f55c09c366368225ae0"}, + {file = "cytoolz-1.1.0-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b6072876ba56446d9ac29d349983677d6f44c6d1c6c1c6be44e66e377c57c767"}, + {file = "cytoolz-1.1.0-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8c3784c965c9a6822d315d099c3a85b0884ac648952815891c667b469116f1d0"}, + {file = "cytoolz-1.1.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0cc537ad78981df1a827773069fd3b7774f4478db43f518b1616efaf87d7d8f9"}, + {file = "cytoolz-1.1.0-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:574ee9dfdc632db8bf9237f27f2a687d1a0b90d29d5e96cab2b21fd2b419c17d"}, + {file = "cytoolz-1.1.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:6594efbaea72dc58b368b53e745ad902c8d8cc41286f00b3743ceac464d5ef3f"}, + {file = "cytoolz-1.1.0-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:7c849f9ddaf3c7faba938440f9c849235a2908b303063d49da3092a93acd695b"}, + {file = "cytoolz-1.1.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:1fef0296fb3577d0a08ad9b70344ee418f728f1ec21a768ffe774437d67ac859"}, + {file = "cytoolz-1.1.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:1dce1e66fdf72cc474367bd7a7f2b90ec67bb8197dc3fe8ecd08f4ce3ab950a1"}, + {file = "cytoolz-1.1.0-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:202fe9975efaec0085cab14a6a6050418bc041f5316f2cf098c0cd2aced4c50e"}, + {file = "cytoolz-1.1.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:528349434601b9d55e65c6a495494de0001c9a06b431547fea4c60b5edc7d5b3"}, + {file = "cytoolz-1.1.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:3e248cdbf2a54bafdadf4486ddd32e8352f816d3caa2014e44de99f8c525d4a8"}, + {file = "cytoolz-1.1.0-cp39-cp39-win32.whl", hash = "sha256:e63f2b70f4654648a5c6a176ae80897c0de6401f385540dce8e365019e800cfe"}, + {file = "cytoolz-1.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:f731c53ed29959f105ae622b62e39603c207ed8e8cb2a40cd4accb63d9f92901"}, + {file = "cytoolz-1.1.0-cp39-cp39-win_arm64.whl", hash = "sha256:5a2120bf9e6e8f25e1b32748424a5571e319ef03a995a8fde663fd2feec1a696"}, + {file = "cytoolz-1.1.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:f32e93a55681d782fc6af939f6df36509d65122423cbc930be39b141064adff8"}, + {file = "cytoolz-1.1.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:5d9bc596751cbda8073e65be02ca11706f00029768fbbbc81e11a8c290bb41aa"}, + {file = "cytoolz-1.1.0-pp311-pypy311_pp73-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:9b16660d01c3931951fab49db422c627897c38c1a1f0393a97582004019a4887"}, + {file = "cytoolz-1.1.0-pp311-pypy311_pp73-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b7de5718e2113d4efccea3f06055758cdbc17388ecc3341ba4d1d812837d7c1a"}, + {file = "cytoolz-1.1.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a12a2a1a6bc44099491c05a12039efa08cc33a3d0f8c7b0566185e085e139283"}, + {file = "cytoolz-1.1.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:047defa7f5f9a32f82373dbc3957289562e8a3fa58ae02ec8e4dca4f43a33a21"}, + {file = "cytoolz-1.1.0.tar.gz", hash = "sha256:13a7bf254c3c0d28b12e2290b82aed0f0977a4c2a2bf84854fcdc7796a29f3b0"}, +] + +[package.dependencies] +toolz = ">=0.8.0" + +[package.extras] +cython = ["cython (>=0.29)"] +test = ["pytest"] + +[[package]] +name = "eth-abi" +version = "5.2.0" +description = "eth_abi: Python utilities for working with Ethereum ABI definitions, especially encoding and decoding" +optional = false +python-versions = "<4,>=3.8" +groups = ["main"] +files = [ + {file = "eth_abi-5.2.0-py3-none-any.whl", hash = "sha256:17abe47560ad753f18054f5b3089fcb588f3e3a092136a416b6c1502cb7e8877"}, + {file = "eth_abi-5.2.0.tar.gz", hash = "sha256:178703fa98c07d8eecd5ae569e7e8d159e493ebb6eeb534a8fe973fbc4e40ef0"}, +] + +[package.dependencies] +eth-typing = ">=3.0.0" +eth-utils = ">=2.0.0" +parsimonious = ">=0.10.0,<0.11.0" + +[package.extras] +dev = ["build (>=0.9.0)", "bump_my_version (>=0.19.0)", "eth-hash[pycryptodome]", "hypothesis (>=6.22.0,<6.108.7)", "ipython", "mypy (==1.10.0)", "pre-commit (>=3.4.0)", "pytest (>=7.0.0)", "pytest-pythonpath (>=0.7.1)", "pytest-timeout (>=2.0.0)", "pytest-xdist (>=2.4.0)", "sphinx (>=6.0.0)", "sphinx-autobuild (>=2021.3.14)", "sphinx_rtd_theme (>=1.0.0)", "towncrier (>=24,<25)", "tox (>=4.0.0)", "twine", "wheel"] +docs = ["sphinx (>=6.0.0)", "sphinx-autobuild (>=2021.3.14)", "sphinx_rtd_theme (>=1.0.0)", "towncrier (>=24,<25)"] +test = ["eth-hash[pycryptodome]", "hypothesis (>=6.22.0,<6.108.7)", "pytest (>=7.0.0)", "pytest-pythonpath (>=0.7.1)", "pytest-timeout (>=2.0.0)", "pytest-xdist (>=2.4.0)"] +tools = ["hypothesis (>=6.22.0,<6.108.7)"] + +[[package]] +name = "eth-account" +version = "0.13.7" +description = "eth-account: Sign Ethereum transactions and messages with local private keys" +optional = false +python-versions = "<4,>=3.8" +groups = ["main"] +files = [ + {file = "eth_account-0.13.7-py3-none-any.whl", hash = "sha256:39727de8c94d004ff61d10da7587509c04d2dc7eac71e04830135300bdfc6d24"}, + {file = "eth_account-0.13.7.tar.gz", hash = "sha256:5853ecbcbb22e65411176f121f5f24b8afeeaf13492359d254b16d8b18c77a46"}, +] + +[package.dependencies] +bitarray = ">=2.4.0" +ckzg = ">=2.0.0" +eth-abi = ">=4.0.0b2" +eth-keyfile = ">=0.7.0,<0.9.0" +eth-keys = ">=0.4.0" +eth-rlp = ">=2.1.0" +eth-utils = ">=2.0.0" +hexbytes = ">=1.2.0" +pydantic = ">=2.0.0" +rlp = ">=1.0.0" + +[package.extras] +dev = ["build (>=0.9.0)", "bump_my_version (>=0.19.0)", "coverage", "hypothesis (>=6.22.0,<6.108.7)", "ipython", "mypy (==1.10.0)", "pre-commit (>=3.4.0)", "pytest (>=7.0.0)", "pytest-xdist (>=2.4.0)", "sphinx (>=6.0.0)", "sphinx-autobuild (>=2021.3.14)", "sphinx_rtd_theme (>=1.0.0)", "towncrier (>=24,<25)", "tox (>=4.0.0)", "twine", "wheel"] +docs = ["sphinx (>=6.0.0)", "sphinx-autobuild (>=2021.3.14)", "sphinx_rtd_theme (>=1.0.0)", "towncrier (>=24,<25)"] +test = ["coverage", "hypothesis (>=6.22.0,<6.108.7)", "pytest (>=7.0.0)", "pytest-xdist (>=2.4.0)"] + +[[package]] +name = "eth-hash" +version = "0.7.1" +description = "eth-hash: The Ethereum hashing function, keccak256, sometimes (erroneously) called sha3" +optional = false +python-versions = "<4,>=3.8" +groups = ["main"] +files = [ + {file = "eth_hash-0.7.1-py3-none-any.whl", hash = "sha256:0fb1add2adf99ef28883fd6228eb447ef519ea72933535ad1a0b28c6f65f868a"}, + {file = "eth_hash-0.7.1.tar.gz", hash = "sha256:d2411a403a0b0a62e8247b4117932d900ffb4c8c64b15f92620547ca5ce46be5"}, +] + +[package.dependencies] +pycryptodome = {version = ">=3.6.6,<4", optional = true, markers = "extra == \"pycryptodome\""} + +[package.extras] +dev = ["build (>=0.9.0)", "bump_my_version (>=0.19.0)", "ipython", "mypy (==1.10.0)", "pre-commit (>=3.4.0)", "pytest (>=7.0.0)", "pytest-xdist (>=2.4.0)", "sphinx (>=6.0.0)", "sphinx-autobuild (>=2021.3.14)", "sphinx_rtd_theme (>=1.0.0)", "towncrier (>=24,<25)", "tox (>=4.0.0)", "twine", "wheel"] +docs = ["sphinx (>=6.0.0)", "sphinx-autobuild (>=2021.3.14)", "sphinx_rtd_theme (>=1.0.0)", "towncrier (>=24,<25)"] +pycryptodome = ["pycryptodome (>=3.6.6,<4)"] +pysha3 = ["pysha3 (>=1.0.0,<2.0.0) ; python_version < \"3.9\"", "safe-pysha3 (>=1.0.0) ; python_version >= \"3.9\""] +test = ["pytest (>=7.0.0)", "pytest-xdist (>=2.4.0)"] + +[[package]] +name = "eth-keyfile" +version = "0.8.1" +description = "eth-keyfile: A library for handling the encrypted keyfiles used to store ethereum private keys" +optional = false +python-versions = "<4,>=3.8" +groups = ["main"] +files = [ + {file = "eth_keyfile-0.8.1-py3-none-any.whl", hash = "sha256:65387378b82fe7e86d7cb9f8d98e6d639142661b2f6f490629da09fddbef6d64"}, + {file = "eth_keyfile-0.8.1.tar.gz", hash = "sha256:9708bc31f386b52cca0969238ff35b1ac72bd7a7186f2a84b86110d3c973bec1"}, +] + +[package.dependencies] +eth-keys = ">=0.4.0" +eth-utils = ">=2" +pycryptodome = ">=3.6.6,<4" + +[package.extras] +dev = ["build (>=0.9.0)", "bumpversion (>=0.5.3)", "ipython", "pre-commit (>=3.4.0)", "pytest (>=7.0.0)", "pytest-xdist (>=2.4.0)", "towncrier (>=21,<22)", "tox (>=4.0.0)", "twine", "wheel"] +docs = ["towncrier (>=21,<22)"] +test = ["pytest (>=7.0.0)", "pytest-xdist (>=2.4.0)"] + +[[package]] +name = "eth-keys" +version = "0.7.0" +description = "eth-keys: Common API for Ethereum key operations" +optional = false +python-versions = "<4,>=3.8" +groups = ["main"] +files = [ + {file = "eth_keys-0.7.0-py3-none-any.whl", hash = "sha256:b0cdda8ffe8e5ba69c7c5ca33f153828edcace844f67aabd4542d7de38b159cf"}, + {file = "eth_keys-0.7.0.tar.gz", hash = "sha256:79d24fd876201df67741de3e3fefb3f4dbcbb6ace66e47e6fe662851a4547814"}, +] + +[package.dependencies] +eth-typing = ">=3" +eth-utils = ">=2" + +[package.extras] +coincurve = ["coincurve (>=17.0.0)"] +dev = ["asn1tools (>=0.146.2)", "build (>=0.9.0)", "bump_my_version (>=0.19.0)", "coincurve (>=17.0.0)", "eth-hash[pysha3]", "factory-boy (>=3.0.1)", "hypothesis (>=5.10.3)", "ipython", "mypy (==1.10.0)", "pre-commit (>=3.4.0)", "pyasn1 (>=0.4.5)", "pytest (>=7.0.0)", "towncrier (>=24,<25)", "tox (>=4.0.0)", "twine", "wheel"] +docs = ["towncrier (>=24,<25)"] +test = ["asn1tools (>=0.146.2)", "eth-hash[pysha3]", "factory-boy (>=3.0.1)", "hypothesis (>=5.10.3)", "pyasn1 (>=0.4.5)", "pytest (>=7.0.0)"] + +[[package]] +name = "eth-rlp" +version = "2.2.0" +description = "eth-rlp: RLP definitions for common Ethereum objects in Python" +optional = false +python-versions = "<4,>=3.8" +groups = ["main"] +files = [ + {file = "eth_rlp-2.2.0-py3-none-any.whl", hash = "sha256:5692d595a741fbaef1203db6a2fedffbd2506d31455a6ad378c8449ee5985c47"}, + {file = "eth_rlp-2.2.0.tar.gz", hash = "sha256:5e4b2eb1b8213e303d6a232dfe35ab8c29e2d3051b86e8d359def80cd21db83d"}, +] + +[package.dependencies] +eth-utils = ">=2.0.0" +hexbytes = ">=1.2.0" +rlp = ">=0.6.0" +typing_extensions = {version = ">=4.0.1", markers = "python_version <= \"3.10\""} + +[package.extras] +dev = ["build (>=0.9.0)", "bump_my_version (>=0.19.0)", "eth-hash[pycryptodome]", "ipython", "mypy (==1.10.0)", "pre-commit (>=3.4.0)", "pytest (>=7.0.0)", "pytest-xdist (>=2.4.0)", "sphinx (>=6.0.0)", "sphinx-autobuild (>=2021.3.14)", "sphinx_rtd_theme (>=1.0.0)", "towncrier (>=24,<25)", "tox (>=4.0.0)", "twine", "wheel"] +docs = ["sphinx (>=6.0.0)", "sphinx-autobuild (>=2021.3.14)", "sphinx_rtd_theme (>=1.0.0)", "towncrier (>=24,<25)"] +test = ["eth-hash[pycryptodome]", "pytest (>=7.0.0)", "pytest-xdist (>=2.4.0)"] + +[[package]] +name = "eth-typing" +version = "5.2.1" +description = "eth-typing: Common type annotations for ethereum python packages" +optional = false +python-versions = "<4,>=3.8" +groups = ["main"] +files = [ + {file = "eth_typing-5.2.1-py3-none-any.whl", hash = "sha256:b0c2812ff978267563b80e9d701f487dd926f1d376d674f3b535cfe28b665d3d"}, + {file = "eth_typing-5.2.1.tar.gz", hash = "sha256:7557300dbf02a93c70fa44af352b5c4a58f94e997a0fd6797fb7d1c29d9538ee"}, +] + +[package.dependencies] +typing_extensions = ">=4.5.0" + +[package.extras] +dev = ["build (>=0.9.0)", "bump_my_version (>=0.19.0)", "ipython", "mypy (==1.10.0)", "pre-commit (>=3.4.0)", "pytest (>=7.0.0)", "pytest-xdist (>=2.4.0)", "sphinx (>=6.0.0)", "sphinx-autobuild (>=2021.3.14)", "sphinx_rtd_theme (>=1.0.0)", "towncrier (>=24,<25)", "tox (>=4.0.0)", "twine", "wheel"] +docs = ["sphinx (>=6.0.0)", "sphinx-autobuild (>=2021.3.14)", "sphinx_rtd_theme (>=1.0.0)", "towncrier (>=24,<25)"] +test = ["pytest (>=7.0.0)", "pytest-xdist (>=2.4.0)"] + +[[package]] +name = "eth-utils" +version = "5.3.1" +description = "eth-utils: Common utility functions for python code that interacts with Ethereum" +optional = false +python-versions = "<4,>=3.8" +groups = ["main"] +files = [ + {file = "eth_utils-5.3.1-py3-none-any.whl", hash = "sha256:1f5476d8f29588d25b8ae4987e1ffdfae6d4c09026e476c4aad13b32dda3ead0"}, + {file = "eth_utils-5.3.1.tar.gz", hash = "sha256:c94e2d2abd024a9a42023b4ddc1c645814ff3d6a737b33d5cfd890ebf159c2d1"}, +] + +[package.dependencies] +cytoolz = {version = ">=0.10.1", markers = "implementation_name == \"cpython\""} +eth-hash = ">=0.3.1" +eth-typing = ">=5.0.0" +pydantic = ">=2.0.0,<3" +toolz = {version = ">0.8.2", markers = "implementation_name == \"pypy\""} + +[package.extras] +dev = ["build (>=0.9.0)", "bump_my_version (>=0.19.0)", "eth-hash[pycryptodome]", "hypothesis (>=4.43.0)", "ipython", "mypy (==1.10.0)", "mypy (==1.10.0)", "pre-commit (>=3.4.0)", "pytest (>=7.0.0)", "pytest-xdist (>=2.4.0)", "sphinx (>=6.0.0)", "sphinx-autobuild (>=2021.3.14)", "sphinx_rtd_theme (>=1.0.0)", "towncrier (>=24,<25)", "tox (>=4.0.0)", "twine", "wheel"] +docs = ["sphinx (>=6.0.0)", "sphinx-autobuild (>=2021.3.14)", "sphinx_rtd_theme (>=1.0.0)", "towncrier (>=24,<25)"] +test = ["hypothesis (>=4.43.0)", "mypy (==1.10.0)", "pytest (>=7.0.0)", "pytest-xdist (>=2.4.0)"] + +[[package]] +name = "exceptiongroup" +version = "1.3.1" +description = "Backport of PEP 654 (exception groups)" +optional = false +python-versions = ">=3.7" +groups = ["main", "dev"] +markers = "python_version == \"3.10\"" +files = [ + {file = "exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598"}, + {file = "exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219"}, +] + +[package.dependencies] +typing-extensions = {version = ">=4.6.0", markers = "python_version < \"3.13\""} + +[package.extras] +test = ["pytest (>=6)"] + +[[package]] +name = "frozenlist" +version = "1.8.0" +description = "A list-like structure which implements collections.abc.MutableSequence" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "frozenlist-1.8.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:b37f6d31b3dcea7deb5e9696e529a6aa4a898adc33db82da12e4c60a7c4d2011"}, + {file = "frozenlist-1.8.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ef2b7b394f208233e471abc541cc6991f907ffd47dc72584acee3147899d6565"}, + {file = "frozenlist-1.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a88f062f072d1589b7b46e951698950e7da00442fc1cacbe17e19e025dc327ad"}, + {file = "frozenlist-1.8.0-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f57fb59d9f385710aa7060e89410aeb5058b99e62f4d16b08b91986b9a2140c2"}, + {file = "frozenlist-1.8.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:799345ab092bee59f01a915620b5d014698547afd011e691a208637312db9186"}, + {file = "frozenlist-1.8.0-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c23c3ff005322a6e16f71bf8692fcf4d5a304aaafe1e262c98c6d4adc7be863e"}, + {file = "frozenlist-1.8.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8a76ea0f0b9dfa06f254ee06053d93a600865b3274358ca48a352ce4f0798450"}, + {file = "frozenlist-1.8.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c7366fe1418a6133d5aa824ee53d406550110984de7637d65a178010f759c6ef"}, + {file = "frozenlist-1.8.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:13d23a45c4cebade99340c4165bd90eeb4a56c6d8a9d8aa49568cac19a6d0dc4"}, + {file = "frozenlist-1.8.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:e4a3408834f65da56c83528fb52ce7911484f0d1eaf7b761fc66001db1646eff"}, + {file = "frozenlist-1.8.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:42145cd2748ca39f32801dad54aeea10039da6f86e303659db90db1c4b614c8c"}, + {file = "frozenlist-1.8.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:e2de870d16a7a53901e41b64ffdf26f2fbb8917b3e6ebf398098d72c5b20bd7f"}, + {file = "frozenlist-1.8.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:20e63c9493d33ee48536600d1a5c95eefc870cd71e7ab037763d1fbb89cc51e7"}, + {file = "frozenlist-1.8.0-cp310-cp310-win32.whl", hash = "sha256:adbeebaebae3526afc3c96fad434367cafbfd1b25d72369a9e5858453b1bb71a"}, + {file = "frozenlist-1.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:667c3777ca571e5dbeb76f331562ff98b957431df140b54c85fd4d52eea8d8f6"}, + {file = "frozenlist-1.8.0-cp310-cp310-win_arm64.whl", hash = "sha256:80f85f0a7cc86e7a54c46d99c9e1318ff01f4687c172ede30fd52d19d1da1c8e"}, + {file = "frozenlist-1.8.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:09474e9831bc2b2199fad6da3c14c7b0fbdd377cce9d3d77131be28906cb7d84"}, + {file = "frozenlist-1.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:17c883ab0ab67200b5f964d2b9ed6b00971917d5d8a92df149dc2c9779208ee9"}, + {file = "frozenlist-1.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:fa47e444b8ba08fffd1c18e8cdb9a75db1b6a27f17507522834ad13ed5922b93"}, + {file = "frozenlist-1.8.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2552f44204b744fba866e573be4c1f9048d6a324dfe14475103fd51613eb1d1f"}, + {file = "frozenlist-1.8.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:957e7c38f250991e48a9a73e6423db1bb9dd14e722a10f6b8bb8e16a0f55f695"}, + {file = "frozenlist-1.8.0-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:8585e3bb2cdea02fc88ffa245069c36555557ad3609e83be0ec71f54fd4abb52"}, + {file = "frozenlist-1.8.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:edee74874ce20a373d62dc28b0b18b93f645633c2943fd90ee9d898550770581"}, + {file = "frozenlist-1.8.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c9a63152fe95756b85f31186bddf42e4c02c6321207fd6601a1c89ebac4fe567"}, + {file = "frozenlist-1.8.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b6db2185db9be0a04fecf2f241c70b63b1a242e2805be291855078f2b404dd6b"}, + {file = "frozenlist-1.8.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:f4be2e3d8bc8aabd566f8d5b8ba7ecc09249d74ba3c9ed52e54dc23a293f0b92"}, + {file = "frozenlist-1.8.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:c8d1634419f39ea6f5c427ea2f90ca85126b54b50837f31497f3bf38266e853d"}, + {file = "frozenlist-1.8.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:1a7fa382a4a223773ed64242dbe1c9c326ec09457e6b8428efb4118c685c3dfd"}, + {file = "frozenlist-1.8.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:11847b53d722050808926e785df837353bd4d75f1d494377e59b23594d834967"}, + {file = "frozenlist-1.8.0-cp311-cp311-win32.whl", hash = "sha256:27c6e8077956cf73eadd514be8fb04d77fc946a7fe9f7fe167648b0b9085cc25"}, + {file = "frozenlist-1.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:ac913f8403b36a2c8610bbfd25b8013488533e71e62b4b4adce9c86c8cea905b"}, + {file = "frozenlist-1.8.0-cp311-cp311-win_arm64.whl", hash = "sha256:d4d3214a0f8394edfa3e303136d0575eece0745ff2b47bd2cb2e66dd92d4351a"}, + {file = "frozenlist-1.8.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:78f7b9e5d6f2fdb88cdde9440dc147259b62b9d3b019924def9f6478be254ac1"}, + {file = "frozenlist-1.8.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:229bf37d2e4acdaf808fd3f06e854a4a7a3661e871b10dc1f8f1896a3b05f18b"}, + {file = "frozenlist-1.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f833670942247a14eafbb675458b4e61c82e002a148f49e68257b79296e865c4"}, + {file = "frozenlist-1.8.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:494a5952b1c597ba44e0e78113a7266e656b9794eec897b19ead706bd7074383"}, + {file = "frozenlist-1.8.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96f423a119f4777a4a056b66ce11527366a8bb92f54e541ade21f2374433f6d4"}, + {file = "frozenlist-1.8.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3462dd9475af2025c31cc61be6652dfa25cbfb56cbbf52f4ccfe029f38decaf8"}, + {file = "frozenlist-1.8.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c4c800524c9cd9bac5166cd6f55285957fcfc907db323e193f2afcd4d9abd69b"}, + {file = "frozenlist-1.8.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d6a5df73acd3399d893dafc71663ad22534b5aa4f94e8a2fabfe856c3c1b6a52"}, + {file = "frozenlist-1.8.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:405e8fe955c2280ce66428b3ca55e12b3c4e9c336fb2103a4937e891c69a4a29"}, + {file = "frozenlist-1.8.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:908bd3f6439f2fef9e85031b59fd4f1297af54415fb60e4254a95f75b3cab3f3"}, + {file = "frozenlist-1.8.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:294e487f9ec720bd8ffcebc99d575f7eff3568a08a253d1ee1a0378754b74143"}, + {file = "frozenlist-1.8.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:74c51543498289c0c43656701be6b077f4b265868fa7f8a8859c197006efb608"}, + {file = "frozenlist-1.8.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:776f352e8329135506a1d6bf16ac3f87bc25b28e765949282dcc627af36123aa"}, + {file = "frozenlist-1.8.0-cp312-cp312-win32.whl", hash = "sha256:433403ae80709741ce34038da08511d4a77062aa924baf411ef73d1146e74faf"}, + {file = "frozenlist-1.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:34187385b08f866104f0c0617404c8eb08165ab1272e884abc89c112e9c00746"}, + {file = "frozenlist-1.8.0-cp312-cp312-win_arm64.whl", hash = "sha256:fe3c58d2f5db5fbd18c2987cba06d51b0529f52bc3a6cdc33d3f4eab725104bd"}, + {file = "frozenlist-1.8.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8d92f1a84bb12d9e56f818b3a746f3efba93c1b63c8387a73dde655e1e42282a"}, + {file = "frozenlist-1.8.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:96153e77a591c8adc2ee805756c61f59fef4cf4073a9275ee86fe8cba41241f7"}, + {file = "frozenlist-1.8.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f21f00a91358803399890ab167098c131ec2ddd5f8f5fd5fe9c9f2c6fcd91e40"}, + {file = "frozenlist-1.8.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fb30f9626572a76dfe4293c7194a09fb1fe93ba94c7d4f720dfae3b646b45027"}, + {file = "frozenlist-1.8.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eaa352d7047a31d87dafcacbabe89df0aa506abb5b1b85a2fb91bc3faa02d822"}, + {file = "frozenlist-1.8.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:03ae967b4e297f58f8c774c7eabcce57fe3c2434817d4385c50661845a058121"}, + {file = "frozenlist-1.8.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f6292f1de555ffcc675941d65fffffb0a5bcd992905015f85d0592201793e0e5"}, + {file = "frozenlist-1.8.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:29548f9b5b5e3460ce7378144c3010363d8035cea44bc0bf02d57f5a685e084e"}, + {file = "frozenlist-1.8.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ec3cc8c5d4084591b4237c0a272cc4f50a5b03396a47d9caaf76f5d7b38a4f11"}, + {file = "frozenlist-1.8.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:517279f58009d0b1f2e7c1b130b377a349405da3f7621ed6bfae50b10adf20c1"}, + {file = "frozenlist-1.8.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:db1e72ede2d0d7ccb213f218df6a078a9c09a7de257c2fe8fcef16d5925230b1"}, + {file = "frozenlist-1.8.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:b4dec9482a65c54a5044486847b8a66bf10c9cb4926d42927ec4e8fd5db7fed8"}, + {file = "frozenlist-1.8.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:21900c48ae04d13d416f0e1e0c4d81f7931f73a9dfa0b7a8746fb2fe7dd970ed"}, + {file = "frozenlist-1.8.0-cp313-cp313-win32.whl", hash = "sha256:8b7b94a067d1c504ee0b16def57ad5738701e4ba10cec90529f13fa03c833496"}, + {file = "frozenlist-1.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:878be833caa6a3821caf85eb39c5ba92d28e85df26d57afb06b35b2efd937231"}, + {file = "frozenlist-1.8.0-cp313-cp313-win_arm64.whl", hash = "sha256:44389d135b3ff43ba8cc89ff7f51f5a0bb6b63d829c8300f79a2fe4fe61bcc62"}, + {file = "frozenlist-1.8.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:e25ac20a2ef37e91c1b39938b591457666a0fa835c7783c3a8f33ea42870db94"}, + {file = "frozenlist-1.8.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:07cdca25a91a4386d2e76ad992916a85038a9b97561bf7a3fd12d5d9ce31870c"}, + {file = "frozenlist-1.8.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4e0c11f2cc6717e0a741f84a527c52616140741cd812a50422f83dc31749fb52"}, + {file = "frozenlist-1.8.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b3210649ee28062ea6099cfda39e147fa1bc039583c8ee4481cb7811e2448c51"}, + {file = "frozenlist-1.8.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:581ef5194c48035a7de2aefc72ac6539823bb71508189e5de01d60c9dcd5fa65"}, + {file = "frozenlist-1.8.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3ef2d026f16a2b1866e1d86fc4e1291e1ed8a387b2c333809419a2f8b3a77b82"}, + {file = "frozenlist-1.8.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5500ef82073f599ac84d888e3a8c1f77ac831183244bfd7f11eaa0289fb30714"}, + {file = "frozenlist-1.8.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:50066c3997d0091c411a66e710f4e11752251e6d2d73d70d8d5d4c76442a199d"}, + {file = "frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:5c1c8e78426e59b3f8005e9b19f6ff46e5845895adbde20ece9218319eca6506"}, + {file = "frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:eefdba20de0d938cec6a89bd4d70f346a03108a19b9df4248d3cf0d88f1b0f51"}, + {file = "frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:cf253e0e1c3ceb4aaff6df637ce033ff6535fb8c70a764a8f46aafd3d6ab798e"}, + {file = "frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:032efa2674356903cd0261c4317a561a6850f3ac864a63fc1583147fb05a79b0"}, + {file = "frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6da155091429aeba16851ecb10a9104a108bcd32f6c1642867eadaee401c1c41"}, + {file = "frozenlist-1.8.0-cp313-cp313t-win32.whl", hash = "sha256:0f96534f8bfebc1a394209427d0f8a63d343c9779cda6fc25e8e121b5fd8555b"}, + {file = "frozenlist-1.8.0-cp313-cp313t-win_amd64.whl", hash = "sha256:5d63a068f978fc69421fb0e6eb91a9603187527c86b7cd3f534a5b77a592b888"}, + {file = "frozenlist-1.8.0-cp313-cp313t-win_arm64.whl", hash = "sha256:bf0a7e10b077bf5fb9380ad3ae8ce20ef919a6ad93b4552896419ac7e1d8e042"}, + {file = "frozenlist-1.8.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:cee686f1f4cadeb2136007ddedd0aaf928ab95216e7691c63e50a8ec066336d0"}, + {file = "frozenlist-1.8.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:119fb2a1bd47307e899c2fac7f28e85b9a543864df47aa7ec9d3c1b4545f096f"}, + {file = "frozenlist-1.8.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4970ece02dbc8c3a92fcc5228e36a3e933a01a999f7094ff7c23fbd2beeaa67c"}, + {file = "frozenlist-1.8.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:cba69cb73723c3f329622e34bdbf5ce1f80c21c290ff04256cff1cd3c2036ed2"}, + {file = "frozenlist-1.8.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:778a11b15673f6f1df23d9586f83c4846c471a8af693a22e066508b77d201ec8"}, + {file = "frozenlist-1.8.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0325024fe97f94c41c08872db482cf8ac4800d80e79222c6b0b7b162d5b13686"}, + {file = "frozenlist-1.8.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:97260ff46b207a82a7567b581ab4190bd4dfa09f4db8a8b49d1a958f6aa4940e"}, + {file = "frozenlist-1.8.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:54b2077180eb7f83dd52c40b2750d0a9f175e06a42e3213ce047219de902717a"}, + {file = "frozenlist-1.8.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:2f05983daecab868a31e1da44462873306d3cbfd76d1f0b5b69c473d21dbb128"}, + {file = "frozenlist-1.8.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:33f48f51a446114bc5d251fb2954ab0164d5be02ad3382abcbfe07e2531d650f"}, + {file = "frozenlist-1.8.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:154e55ec0655291b5dd1b8731c637ecdb50975a2ae70c606d100750a540082f7"}, + {file = "frozenlist-1.8.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:4314debad13beb564b708b4a496020e5306c7333fa9a3ab90374169a20ffab30"}, + {file = "frozenlist-1.8.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:073f8bf8becba60aa931eb3bc420b217bb7d5b8f4750e6f8b3be7f3da85d38b7"}, + {file = "frozenlist-1.8.0-cp314-cp314-win32.whl", hash = "sha256:bac9c42ba2ac65ddc115d930c78d24ab8d4f465fd3fc473cdedfccadb9429806"}, + {file = "frozenlist-1.8.0-cp314-cp314-win_amd64.whl", hash = "sha256:3e0761f4d1a44f1d1a47996511752cf3dcec5bbdd9cc2b4fe595caf97754b7a0"}, + {file = "frozenlist-1.8.0-cp314-cp314-win_arm64.whl", hash = "sha256:d1eaff1d00c7751b7c6662e9c5ba6eb2c17a2306ba5e2a37f24ddf3cc953402b"}, + {file = "frozenlist-1.8.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:d3bb933317c52d7ea5004a1c442eef86f426886fba134ef8cf4226ea6ee1821d"}, + {file = "frozenlist-1.8.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:8009897cdef112072f93a0efdce29cd819e717fd2f649ee3016efd3cd885a7ed"}, + {file = "frozenlist-1.8.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2c5dcbbc55383e5883246d11fd179782a9d07a986c40f49abe89ddf865913930"}, + {file = "frozenlist-1.8.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:39ecbc32f1390387d2aa4f5a995e465e9e2f79ba3adcac92d68e3e0afae6657c"}, + {file = "frozenlist-1.8.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92db2bf818d5cc8d9c1f1fc56b897662e24ea5adb36ad1f1d82875bd64e03c24"}, + {file = "frozenlist-1.8.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2dc43a022e555de94c3b68a4ef0b11c4f747d12c024a520c7101709a2144fb37"}, + {file = "frozenlist-1.8.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cb89a7f2de3602cfed448095bab3f178399646ab7c61454315089787df07733a"}, + {file = "frozenlist-1.8.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:33139dc858c580ea50e7e60a1b0ea003efa1fd42e6ec7fdbad78fff65fad2fd2"}, + {file = "frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:168c0969a329b416119507ba30b9ea13688fafffac1b7822802537569a1cb0ef"}, + {file = "frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:28bd570e8e189d7f7b001966435f9dac6718324b5be2990ac496cf1ea9ddb7fe"}, + {file = "frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:b2a095d45c5d46e5e79ba1e5b9cb787f541a8dee0433836cea4b96a2c439dcd8"}, + {file = "frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:eab8145831a0d56ec9c4139b6c3e594c7a83c2c8be25d5bcf2d86136a532287a"}, + {file = "frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:974b28cf63cc99dfb2188d8d222bc6843656188164848c4f679e63dae4b0708e"}, + {file = "frozenlist-1.8.0-cp314-cp314t-win32.whl", hash = "sha256:342c97bf697ac5480c0a7ec73cd700ecfa5a8a40ac923bd035484616efecc2df"}, + {file = "frozenlist-1.8.0-cp314-cp314t-win_amd64.whl", hash = "sha256:06be8f67f39c8b1dc671f5d83aaefd3358ae5cdcf8314552c57e7ed3e6475bdd"}, + {file = "frozenlist-1.8.0-cp314-cp314t-win_arm64.whl", hash = "sha256:102e6314ca4da683dca92e3b1355490fed5f313b768500084fbe6371fddfdb79"}, + {file = "frozenlist-1.8.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:d8b7138e5cd0647e4523d6685b0eac5d4be9a184ae9634492f25c6eb38c12a47"}, + {file = "frozenlist-1.8.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a6483e309ca809f1efd154b4d37dc6d9f61037d6c6a81c2dc7a15cb22c8c5dca"}, + {file = "frozenlist-1.8.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1b9290cf81e95e93fdf90548ce9d3c1211cf574b8e3f4b3b7cb0537cf2227068"}, + {file = "frozenlist-1.8.0-cp39-cp39-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:59a6a5876ca59d1b63af8cd5e7ffffb024c3dc1e9cf9301b21a2e76286505c95"}, + {file = "frozenlist-1.8.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6dc4126390929823e2d2d9dc79ab4046ed74680360fc5f38b585c12c66cdf459"}, + {file = "frozenlist-1.8.0-cp39-cp39-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:332db6b2563333c5671fecacd085141b5800cb866be16d5e3eb15a2086476675"}, + {file = "frozenlist-1.8.0-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9ff15928d62a0b80bb875655c39bf517938c7d589554cbd2669be42d97c2cb61"}, + {file = "frozenlist-1.8.0-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7bf6cdf8e07c8151fba6fe85735441240ec7f619f935a5205953d58009aef8c6"}, + {file = "frozenlist-1.8.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:48e6d3f4ec5c7273dfe83ff27c91083c6c9065af655dc2684d2c200c94308bb5"}, + {file = "frozenlist-1.8.0-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:1a7607e17ad33361677adcd1443edf6f5da0ce5e5377b798fba20fae194825f3"}, + {file = "frozenlist-1.8.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:5a3a935c3a4e89c733303a2d5a7c257ea44af3a56c8202df486b7f5de40f37e1"}, + {file = "frozenlist-1.8.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:940d4a017dbfed9daf46a3b086e1d2167e7012ee297fef9e1c545c4d022f5178"}, + {file = "frozenlist-1.8.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:b9be22a69a014bc47e78072d0ecae716f5eb56c15238acca0f43d6eb8e4a5bda"}, + {file = "frozenlist-1.8.0-cp39-cp39-win32.whl", hash = "sha256:1aa77cb5697069af47472e39612976ed05343ff2e84a3dcf15437b232cbfd087"}, + {file = "frozenlist-1.8.0-cp39-cp39-win_amd64.whl", hash = "sha256:7398c222d1d405e796970320036b1b563892b65809d9e5261487bb2c7f7b5c6a"}, + {file = "frozenlist-1.8.0-cp39-cp39-win_arm64.whl", hash = "sha256:b4f3b365f31c6cd4af24545ca0a244a53688cad8834e32f56831c4923b50a103"}, + {file = "frozenlist-1.8.0-py3-none-any.whl", hash = "sha256:0c18a16eab41e82c295618a77502e17b195883241c563b00f0aa5106fc4eaa0d"}, + {file = "frozenlist-1.8.0.tar.gz", hash = "sha256:3ede829ed8d842f6cd48fc7081d7a41001a56f1f38603f9d49bf3020d59a31ad"}, +] + +[[package]] +name = "h11" +version = "0.16.0" +description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86"}, + {file = "h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1"}, +] + +[[package]] +name = "hexbytes" +version = "1.3.1" +description = "hexbytes: Python `bytes` subclass that decodes hex, with a readable console output" +optional = false +python-versions = "<4,>=3.8" +groups = ["main"] +files = [ + {file = "hexbytes-1.3.1-py3-none-any.whl", hash = "sha256:da01ff24a1a9a2b1881c4b85f0e9f9b0f51b526b379ffa23832ae7899d29c2c7"}, + {file = "hexbytes-1.3.1.tar.gz", hash = "sha256:a657eebebdfe27254336f98d8af6e2236f3f83aed164b87466b6cf6c5f5a4765"}, +] + +[package.extras] +dev = ["build (>=0.9.0)", "bump_my_version (>=0.19.0)", "eth_utils (>=2.0.0)", "hypothesis (>=3.44.24)", "ipython", "mypy (==1.10.0)", "pre-commit (>=3.4.0)", "pytest (>=7.0.0)", "pytest-xdist (>=2.4.0)", "sphinx (>=6.0.0)", "sphinx-autobuild (>=2021.3.14)", "sphinx_rtd_theme (>=1.0.0)", "towncrier (>=24,<25)", "tox (>=4.0.0)", "twine", "wheel"] +docs = ["sphinx (>=6.0.0)", "sphinx-autobuild (>=2021.3.14)", "sphinx_rtd_theme (>=1.0.0)", "towncrier (>=24,<25)"] +test = ["eth_utils (>=2.0.0)", "hypothesis (>=3.44.24)", "pytest (>=7.0.0)", "pytest-xdist (>=2.4.0)"] + +[[package]] +name = "httpcore" +version = "1.0.9" +description = "A minimal low-level HTTP client." +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55"}, + {file = "httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8"}, +] + +[package.dependencies] +certifi = "*" +h11 = ">=0.16" + +[package.extras] +asyncio = ["anyio (>=4.0,<5.0)"] +http2 = ["h2 (>=3,<5)"] +socks = ["socksio (==1.*)"] +trio = ["trio (>=0.22.0,<1.0)"] + +[[package]] +name = "httpx" +version = "0.28.1" +description = "The next generation HTTP client." +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad"}, + {file = "httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc"}, +] + +[package.dependencies] +anyio = "*" +certifi = "*" +httpcore = "==1.*" +idna = "*" + +[package.extras] +brotli = ["brotli ; platform_python_implementation == \"CPython\"", "brotlicffi ; platform_python_implementation != \"CPython\""] +cli = ["click (==8.*)", "pygments (==2.*)", "rich (>=10,<14)"] +http2 = ["h2 (>=3,<5)"] +socks = ["socksio (==1.*)"] +zstd = ["zstandard (>=0.18.0)"] + +[[package]] +name = "idna" +version = "3.11" +description = "Internationalized Domain Names in Applications (IDNA)" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea"}, + {file = "idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902"}, +] + +[package.extras] +all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"] + +[[package]] +name = "iniconfig" +version = "2.3.0" +description = "brain-dead simple config-ini parsing" +optional = false +python-versions = ">=3.10" +groups = ["dev"] +files = [ + {file = "iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12"}, + {file = "iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730"}, +] + +[[package]] +name = "librt" +version = "0.8.1" +description = "Mypyc runtime library" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +markers = "platform_python_implementation != \"PyPy\"" +files = [ + {file = "librt-0.8.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:81fd938344fecb9373ba1b155968c8a329491d2ce38e7ddb76f30ffb938f12dc"}, + {file = "librt-0.8.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5db05697c82b3a2ec53f6e72b2ed373132b0c2e05135f0696784e97d7f5d48e7"}, + {file = "librt-0.8.1-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d56bc4011975f7460bea7b33e1ff425d2f1adf419935ff6707273c77f8a4ada6"}, + {file = "librt-0.8.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5cdc0f588ff4b663ea96c26d2a230c525c6fc62b28314edaaaca8ed5af931ad0"}, + {file = "librt-0.8.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:97c2b54ff6717a7a563b72627990bec60d8029df17df423f0ed37d56a17a176b"}, + {file = "librt-0.8.1-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8f1125e6bbf2f1657d9a2f3ccc4a2c9b0c8b176965bb565dd4d86be67eddb4b6"}, + {file = "librt-0.8.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:8f4bb453f408137d7581be309b2fbc6868a80e7ef60c88e689078ee3a296ae71"}, + {file = "librt-0.8.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:c336d61d2fe74a3195edc1646d53ff1cddd3a9600b09fa6ab75e5514ba4862a7"}, + {file = "librt-0.8.1-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:eb5656019db7c4deacf0c1a55a898c5bb8f989be904597fcb5232a2f4828fa05"}, + {file = "librt-0.8.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:c25d9e338d5bed46c1632f851babf3d13c78f49a225462017cf5e11e845c5891"}, + {file = "librt-0.8.1-cp310-cp310-win32.whl", hash = "sha256:aaab0e307e344cb28d800957ef3ec16605146ef0e59e059a60a176d19543d1b7"}, + {file = "librt-0.8.1-cp310-cp310-win_amd64.whl", hash = "sha256:56e04c14b696300d47b3bc5f1d10a00e86ae978886d0cee14e5714fafb5df5d2"}, + {file = "librt-0.8.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:681dc2451d6d846794a828c16c22dc452d924e9f700a485b7ecb887a30aad1fd"}, + {file = "librt-0.8.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a3b4350b13cc0e6f5bec8fa7caf29a8fb8cdc051a3bae45cfbfd7ce64f009965"}, + {file = "librt-0.8.1-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:ac1e7817fd0ed3d14fd7c5df91daed84c48e4c2a11ee99c0547f9f62fdae13da"}, + {file = "librt-0.8.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:747328be0c5b7075cde86a0e09d7a9196029800ba75a1689332348e998fb85c0"}, + {file = "librt-0.8.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f0af2bd2bc204fa27f3d6711d0f360e6b8c684a035206257a81673ab924aa11e"}, + {file = "librt-0.8.1-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d480de377f5b687b6b1bc0c0407426da556e2a757633cc7e4d2e1a057aa688f3"}, + {file = "librt-0.8.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d0ee06b5b5291f609ddb37b9750985b27bc567791bc87c76a569b3feed8481ac"}, + {file = "librt-0.8.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:9e2c6f77b9ad48ce5603b83b7da9ee3e36b3ab425353f695cba13200c5d96596"}, + {file = "librt-0.8.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:439352ba9373f11cb8e1933da194dcc6206daf779ff8df0ed69c5e39113e6a99"}, + {file = "librt-0.8.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:82210adabbc331dbb65d7868b105185464ef13f56f7f76688565ad79f648b0fe"}, + {file = "librt-0.8.1-cp311-cp311-win32.whl", hash = "sha256:52c224e14614b750c0a6d97368e16804a98c684657c7518752c356834fff83bb"}, + {file = "librt-0.8.1-cp311-cp311-win_amd64.whl", hash = "sha256:c00e5c884f528c9932d278d5c9cbbea38a6b81eb62c02e06ae53751a83a4d52b"}, + {file = "librt-0.8.1-cp311-cp311-win_arm64.whl", hash = "sha256:f7cdf7f26c2286ffb02e46d7bac56c94655540b26347673bea15fa52a6af17e9"}, + {file = "librt-0.8.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a28f2612ab566b17f3698b0da021ff9960610301607c9a5e8eaca62f5e1c350a"}, + {file = "librt-0.8.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:60a78b694c9aee2a0f1aaeaa7d101cf713e92e8423a941d2897f4fa37908dab9"}, + {file = "librt-0.8.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:758509ea3f1eba2a57558e7e98f4659d0ea7670bff49673b0dde18a3c7e6c0eb"}, + {file = "librt-0.8.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:039b9f2c506bd0ab0f8725aa5ba339c6f0cd19d3b514b50d134789809c24285d"}, + {file = "librt-0.8.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5bb54f1205a3a6ab41a6fd71dfcdcbd278670d3a90ca502a30d9da583105b6f7"}, + {file = "librt-0.8.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:05bd41cdee35b0c59c259f870f6da532a2c5ca57db95b5f23689fcb5c9e42440"}, + {file = "librt-0.8.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:adfab487facf03f0d0857b8710cf82d0704a309d8ffc33b03d9302b4c64e91a9"}, + {file = "librt-0.8.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:153188fe98a72f206042be10a2c6026139852805215ed9539186312d50a8e972"}, + {file = "librt-0.8.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:dd3c41254ee98604b08bd5b3af5bf0a89740d4ee0711de95b65166bf44091921"}, + {file = "librt-0.8.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e0d138c7ae532908cbb342162b2611dbd4d90c941cd25ab82084aaf71d2c0bd0"}, + {file = "librt-0.8.1-cp312-cp312-win32.whl", hash = "sha256:43353b943613c5d9c49a25aaffdba46f888ec354e71e3529a00cca3f04d66a7a"}, + {file = "librt-0.8.1-cp312-cp312-win_amd64.whl", hash = "sha256:ff8baf1f8d3f4b6b7257fcb75a501f2a5499d0dda57645baa09d4d0d34b19444"}, + {file = "librt-0.8.1-cp312-cp312-win_arm64.whl", hash = "sha256:0f2ae3725904f7377e11cc37722d5d401e8b3d5851fb9273d7f4fe04f6b3d37d"}, + {file = "librt-0.8.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7e6bad1cd94f6764e1e21950542f818a09316645337fd5ab9a7acc45d99a8f35"}, + {file = "librt-0.8.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cf450f498c30af55551ba4f66b9123b7185362ec8b625a773b3d39aa1a717583"}, + {file = "librt-0.8.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:eca45e982fa074090057132e30585a7e8674e9e885d402eae85633e9f449ce6c"}, + {file = "librt-0.8.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0c3811485fccfda840861905b8c70bba5ec094e02825598bb9d4ca3936857a04"}, + {file = "librt-0.8.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e4af413908f77294605e28cfd98063f54b2c790561383971d2f52d113d9c363"}, + {file = "librt-0.8.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5212a5bd7fae98dae95710032902edcd2ec4dc994e883294f75c857b83f9aba0"}, + {file = "librt-0.8.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e692aa2d1d604e6ca12d35e51fdc36f4cda6345e28e36374579f7ef3611b3012"}, + {file = "librt-0.8.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:4be2a5c926b9770c9e08e717f05737a269b9d0ebc5d2f0060f0fe3fe9ce47acb"}, + {file = "librt-0.8.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:fd1a720332ea335ceb544cf0a03f81df92abd4bb887679fd1e460976b0e6214b"}, + {file = "librt-0.8.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:93c2af9e01e0ef80d95ae3c720be101227edae5f2fe7e3dc63d8857fadfc5a1d"}, + {file = "librt-0.8.1-cp313-cp313-win32.whl", hash = "sha256:086a32dbb71336627e78cc1d6ee305a68d038ef7d4c39aaff41ae8c9aa46e91a"}, + {file = "librt-0.8.1-cp313-cp313-win_amd64.whl", hash = "sha256:e11769a1dbda4da7b00a76cfffa67aa47cfa66921d2724539eee4b9ede780b79"}, + {file = "librt-0.8.1-cp313-cp313-win_arm64.whl", hash = "sha256:924817ab3141aca17893386ee13261f1d100d1ef410d70afe4389f2359fea4f0"}, + {file = "librt-0.8.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:6cfa7fe54fd4d1f47130017351a959fe5804bda7a0bc7e07a2cdbc3fdd28d34f"}, + {file = "librt-0.8.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:228c2409c079f8c11fb2e5d7b277077f694cb93443eb760e00b3b83cb8b3176c"}, + {file = "librt-0.8.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:7aae78ab5e3206181780e56912d1b9bb9f90a7249ce12f0e8bf531d0462dd0fc"}, + {file = "librt-0.8.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:172d57ec04346b047ca6af181e1ea4858086c80bdf455f61994c4aa6fc3f866c"}, + {file = "librt-0.8.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6b1977c4ea97ce5eb7755a78fae68d87e4102e4aaf54985e8b56806849cc06a3"}, + {file = "librt-0.8.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:10c42e1f6fd06733ef65ae7bebce2872bcafd8d6e6b0a08fe0a05a23b044fb14"}, + {file = "librt-0.8.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4c8dfa264b9193c4ee19113c985c95f876fae5e51f731494fc4e0cf594990ba7"}, + {file = "librt-0.8.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:01170b6729a438f0dedc4a26ed342e3dc4f02d1000b4b19f980e1877f0c297e6"}, + {file = "librt-0.8.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:7b02679a0d783bdae30d443025b94465d8c3dc512f32f5b5031f93f57ac32071"}, + {file = "librt-0.8.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:190b109bb69592a3401fe1ffdea41a2e73370ace2ffdc4a0e8e2b39cdea81b78"}, + {file = "librt-0.8.1-cp314-cp314-win32.whl", hash = "sha256:e70a57ecf89a0f64c24e37f38d3fe217a58169d2fe6ed6d70554964042474023"}, + {file = "librt-0.8.1-cp314-cp314-win_amd64.whl", hash = "sha256:7e2f3edca35664499fbb36e4770650c4bd4a08abc1f4458eab9df4ec56389730"}, + {file = "librt-0.8.1-cp314-cp314-win_arm64.whl", hash = "sha256:0d2f82168e55ddefd27c01c654ce52379c0750ddc31ee86b4b266bcf4d65f2a3"}, + {file = "librt-0.8.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2c74a2da57a094bd48d03fa5d196da83d2815678385d2978657499063709abe1"}, + {file = "librt-0.8.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a355d99c4c0d8e5b770313b8b247411ed40949ca44e33e46a4789b9293a907ee"}, + {file = "librt-0.8.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:2eb345e8b33fb748227409c9f1233d4df354d6e54091f0e8fc53acdb2ffedeb7"}, + {file = "librt-0.8.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9be2f15e53ce4e83cc08adc29b26fb5978db62ef2a366fbdf716c8a6c8901040"}, + {file = "librt-0.8.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:785ae29c1f5c6e7c2cde2c7c0e148147f4503da3abc5d44d482068da5322fd9e"}, + {file = "librt-0.8.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1d3a7da44baf692f0c6aeb5b2a09c5e6fc7a703bca9ffa337ddd2e2da53f7732"}, + {file = "librt-0.8.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5fc48998000cbc39ec0d5311312dda93ecf92b39aaf184c5e817d5d440b29624"}, + {file = "librt-0.8.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:e96baa6820280077a78244b2e06e416480ed859bbd8e5d641cf5742919d8beb4"}, + {file = "librt-0.8.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:31362dbfe297b23590530007062c32c6f6176f6099646bb2c95ab1b00a57c382"}, + {file = "librt-0.8.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cc3656283d11540ab0ea01978378e73e10002145117055e03722417aeab30994"}, + {file = "librt-0.8.1-cp314-cp314t-win32.whl", hash = "sha256:738f08021b3142c2918c03692608baed43bc51144c29e35807682f8070ee2a3a"}, + {file = "librt-0.8.1-cp314-cp314t-win_amd64.whl", hash = "sha256:89815a22daf9c51884fb5dbe4f1ef65ee6a146e0b6a8df05f753e2e4a9359bf4"}, + {file = "librt-0.8.1-cp314-cp314t-win_arm64.whl", hash = "sha256:bf512a71a23504ed08103a13c941f763db13fb11177beb3d9244c98c29fb4a61"}, + {file = "librt-0.8.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3dff3d3ca8db20e783b1bc7de49c0a2ab0b8387f31236d6a026597d07fcd68ac"}, + {file = "librt-0.8.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:08eec3a1fc435f0d09c87b6bf1ec798986a3544f446b864e4099633a56fcd9ed"}, + {file = "librt-0.8.1-cp39-cp39-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:e3f0a41487fd5fad7e760b9e8a90e251e27c2816fbc2cff36a22a0e6bcbbd9dd"}, + {file = "librt-0.8.1-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bacdb58d9939d95cc557b4dbaa86527c9db2ac1ed76a18bc8d26f6dc8647d851"}, + {file = "librt-0.8.1-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b6d7ab1f01aa753188605b09a51faa44a3327400b00b8cce424c71910fc0a128"}, + {file = "librt-0.8.1-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4998009e7cb9e896569f4be7004f09d0ed70d386fa99d42b6d363f6d200501ac"}, + {file = "librt-0.8.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:2cc68eeeef5e906839c7bb0815748b5b0a974ec27125beefc0f942715785b551"}, + {file = "librt-0.8.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:0bf69d79a23f4f40b8673a947a234baeeb133b5078b483b7297c5916539cf5d5"}, + {file = "librt-0.8.1-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:22b46eabd76c1986ee7d231b0765ad387d7673bbd996aa0d0d054b38ac65d8f6"}, + {file = "librt-0.8.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:237796479f4d0637d6b9cbcb926ff424a97735e68ade6facf402df4ec93375ed"}, + {file = "librt-0.8.1-cp39-cp39-win32.whl", hash = "sha256:4beb04b8c66c6ae62f8c1e0b2f097c1ebad9295c929a8d5286c05eae7c2fc7dc"}, + {file = "librt-0.8.1-cp39-cp39-win_amd64.whl", hash = "sha256:64548cde61b692dc0dc379f4b5f59a2f582c2ebe7890d09c1ae3b9e66fa015b7"}, + {file = "librt-0.8.1.tar.gz", hash = "sha256:be46a14693955b3bd96014ccbdb8339ee8c9346fbe11c1b78901b55125f14c73"}, +] + +[[package]] +name = "multidict" +version = "6.7.1" +description = "multidict implementation" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "multidict-6.7.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:c93c3db7ea657dd4637d57e74ab73de31bccefe144d3d4ce370052035bc85fb5"}, + {file = "multidict-6.7.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:974e72a2474600827abaeda71af0c53d9ebbc3c2eb7da37b37d7829ae31232d8"}, + {file = "multidict-6.7.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cdea2e7b2456cfb6694fb113066fd0ec7ea4d67e3a35e1f4cbeea0b448bf5872"}, + {file = "multidict-6.7.1-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:17207077e29342fdc2c9a82e4b306f1127bf1ea91f8b71e02d4798a70bb99991"}, + {file = "multidict-6.7.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d4f49cb5661344764e4c7c7973e92a47a59b8fc19b6523649ec9dc4960e58a03"}, + {file = "multidict-6.7.1-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a9fc4caa29e2e6ae408d1c450ac8bf19892c5fca83ee634ecd88a53332c59981"}, + {file = "multidict-6.7.1-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c5f0c21549ab432b57dcc82130f388d84ad8179824cc3f223d5e7cfbfd4143f6"}, + {file = "multidict-6.7.1-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7dfb78d966b2c906ae1d28ccf6e6712a3cd04407ee5088cd276fe8cb42186190"}, + {file = "multidict-6.7.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9b0d9b91d1aa44db9c1f1ecd0d9d2ae610b2f4f856448664e01a3b35899f3f92"}, + {file = "multidict-6.7.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:dd96c01a9dcd4889dcfcf9eb5544ca0c77603f239e3ffab0524ec17aea9a93ee"}, + {file = "multidict-6.7.1-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:067343c68cd6612d375710f895337b3a98a033c94f14b9a99eff902f205424e2"}, + {file = "multidict-6.7.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:5884a04f4ff56c6120f6ccf703bdeb8b5079d808ba604d4d53aec0d55dc33568"}, + {file = "multidict-6.7.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:8affcf1c98b82bc901702eb73b6947a1bfa170823c153fe8a47b5f5f02e48e40"}, + {file = "multidict-6.7.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:0d17522c37d03e85c8098ec8431636309b2682cf12e58f4dbc76121fb50e4962"}, + {file = "multidict-6.7.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:24c0cf81544ca5e17cfcb6e482e7a82cd475925242b308b890c9452a074d4505"}, + {file = "multidict-6.7.1-cp310-cp310-win32.whl", hash = "sha256:d82dd730a95e6643802f4454b8fdecdf08667881a9c5670db85bc5a56693f122"}, + {file = "multidict-6.7.1-cp310-cp310-win_amd64.whl", hash = "sha256:cf37cbe5ced48d417ba045aca1b21bafca67489452debcde94778a576666a1df"}, + {file = "multidict-6.7.1-cp310-cp310-win_arm64.whl", hash = "sha256:59bc83d3f66b41dac1e7460aac1d196edc70c9ba3094965c467715a70ecb46db"}, + {file = "multidict-6.7.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7ff981b266af91d7b4b3793ca3382e53229088d193a85dfad6f5f4c27fc73e5d"}, + {file = "multidict-6.7.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:844c5bca0b5444adb44a623fb0a1310c2f4cd41f402126bb269cd44c9b3f3e1e"}, + {file = "multidict-6.7.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f2a0a924d4c2e9afcd7ec64f9de35fcd96915149b2216e1cb2c10a56df483855"}, + {file = "multidict-6.7.1-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:8be1802715a8e892c784c0197c2ace276ea52702a0ede98b6310c8f255a5afb3"}, + {file = "multidict-6.7.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2e2d2ed645ea29f31c4c7ea1552fcfd7cb7ba656e1eafd4134a6620c9f5fdd9e"}, + {file = "multidict-6.7.1-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:95922cee9a778659e91db6497596435777bd25ed116701a4c034f8e46544955a"}, + {file = "multidict-6.7.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6b83cabdc375ffaaa15edd97eb7c0c672ad788e2687004990074d7d6c9b140c8"}, + {file = "multidict-6.7.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:38fb49540705369bab8484db0689d86c0a33a0a9f2c1b197f506b71b4b6c19b0"}, + {file = "multidict-6.7.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:439cbebd499f92e9aa6793016a8acaa161dfa749ae86d20960189f5398a19144"}, + {file = "multidict-6.7.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6d3bc717b6fe763b8be3f2bee2701d3c8eb1b2a8ae9f60910f1b2860c82b6c49"}, + {file = "multidict-6.7.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:619e5a1ac57986dbfec9f0b301d865dddf763696435e2962f6d9cf2fdff2bb71"}, + {file = "multidict-6.7.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:0b38ebffd9be37c1170d33bc0f36f4f262e0a09bc1aac1c34c7aa51a7293f0b3"}, + {file = "multidict-6.7.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:10ae39c9cfe6adedcdb764f5e8411d4a92b055e35573a2eaa88d3323289ef93c"}, + {file = "multidict-6.7.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:25167cc263257660290fba06b9318d2026e3c910be240a146e1f66dd114af2b0"}, + {file = "multidict-6.7.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:128441d052254f42989ef98b7b6a6ecb1e6f708aa962c7984235316db59f50fa"}, + {file = "multidict-6.7.1-cp311-cp311-win32.whl", hash = "sha256:d62b7f64ffde3b99d06b707a280db04fb3855b55f5a06df387236051d0668f4a"}, + {file = "multidict-6.7.1-cp311-cp311-win_amd64.whl", hash = "sha256:bdbf9f3b332abd0cdb306e7c2113818ab1e922dc84b8f8fd06ec89ed2a19ab8b"}, + {file = "multidict-6.7.1-cp311-cp311-win_arm64.whl", hash = "sha256:b8c990b037d2fff2f4e33d3f21b9b531c5745b33a49a7d6dbe7a177266af44f6"}, + {file = "multidict-6.7.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:a90f75c956e32891a4eda3639ce6dd86e87105271f43d43442a3aedf3cddf172"}, + {file = "multidict-6.7.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3fccb473e87eaa1382689053e4a4618e7ba7b9b9b8d6adf2027ee474597128cd"}, + {file = "multidict-6.7.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b0fa96985700739c4c7853a43c0b3e169360d6855780021bfc6d0f1ce7c123e7"}, + {file = "multidict-6.7.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:cb2a55f408c3043e42b40cc8eecd575afa27b7e0b956dfb190de0f8499a57a53"}, + {file = "multidict-6.7.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eb0ce7b2a32d09892b3dd6cc44877a0d02a33241fafca5f25c8b6b62374f8b75"}, + {file = "multidict-6.7.1-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c3a32d23520ee37bf327d1e1a656fec76a2edd5c038bf43eddfa0572ec49c60b"}, + {file = "multidict-6.7.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9c90fed18bffc0189ba814749fdcc102b536e83a9f738a9003e569acd540a733"}, + {file = "multidict-6.7.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:da62917e6076f512daccfbbde27f46fed1c98fee202f0559adec8ee0de67f71a"}, + {file = "multidict-6.7.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bfde23ef6ed9db7eaee6c37dcec08524cb43903c60b285b172b6c094711b3961"}, + {file = "multidict-6.7.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3758692429e4e32f1ba0df23219cd0b4fc0a52f476726fff9337d1a57676a582"}, + {file = "multidict-6.7.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:398c1478926eca669f2fd6a5856b6de9c0acf23a2cb59a14c0ba5844fa38077e"}, + {file = "multidict-6.7.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:c102791b1c4f3ab36ce4101154549105a53dc828f016356b3e3bcae2e3a039d3"}, + {file = "multidict-6.7.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:a088b62bd733e2ad12c50dad01b7d0166c30287c166e137433d3b410add807a6"}, + {file = "multidict-6.7.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:3d51ff4785d58d3f6c91bdbffcb5e1f7ddfda557727043aa20d20ec4f65e324a"}, + {file = "multidict-6.7.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fc5907494fccf3e7d3f94f95c91d6336b092b5fc83811720fae5e2765890dfba"}, + {file = "multidict-6.7.1-cp312-cp312-win32.whl", hash = "sha256:28ca5ce2fd9716631133d0e9a9b9a745ad7f60bac2bccafb56aa380fc0b6c511"}, + {file = "multidict-6.7.1-cp312-cp312-win_amd64.whl", hash = "sha256:fcee94dfbd638784645b066074b338bc9cc155d4b4bffa4adce1615c5a426c19"}, + {file = "multidict-6.7.1-cp312-cp312-win_arm64.whl", hash = "sha256:ba0a9fb644d0c1a2194cf7ffb043bd852cea63a57f66fbd33959f7dae18517bf"}, + {file = "multidict-6.7.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:2b41f5fed0ed563624f1c17630cb9941cf2309d4df00e494b551b5f3e3d67a23"}, + {file = "multidict-6.7.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:84e61e3af5463c19b67ced91f6c634effb89ef8bfc5ca0267f954451ed4bb6a2"}, + {file = "multidict-6.7.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:935434b9853c7c112eee7ac891bc4cb86455aa631269ae35442cb316790c1445"}, + {file = "multidict-6.7.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:432feb25a1cb67fe82a9680b4d65fb542e4635cb3166cd9c01560651ad60f177"}, + {file = "multidict-6.7.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e82d14e3c948952a1a85503817e038cba5905a3352de76b9a465075d072fba23"}, + {file = "multidict-6.7.1-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:4cfb48c6ea66c83bcaaf7e4dfa7ec1b6bbcf751b7db85a328902796dfde4c060"}, + {file = "multidict-6.7.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1d540e51b7e8e170174555edecddbd5538105443754539193e3e1061864d444d"}, + {file = "multidict-6.7.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:273d23f4b40f3dce4d6c8a821c741a86dec62cded82e1175ba3d99be128147ed"}, + {file = "multidict-6.7.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d624335fd4fa1c08a53f8b4be7676ebde19cd092b3895c421045ca87895b429"}, + {file = "multidict-6.7.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:12fad252f8b267cc75b66e8fc51b3079604e8d43a75428ffe193cd9e2195dfd6"}, + {file = "multidict-6.7.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:03ede2a6ffbe8ef936b92cb4529f27f42be7f56afcdab5ab739cd5f27fb1cbf9"}, + {file = "multidict-6.7.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:90efbcf47dbe33dcf643a1e400d67d59abeac5db07dc3f27d6bdeae497a2198c"}, + {file = "multidict-6.7.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:5c4b9bfc148f5a91be9244d6264c53035c8a0dcd2f51f1c3c6e30e30ebaa1c84"}, + {file = "multidict-6.7.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:401c5a650f3add2472d1d288c26deebc540f99e2fb83e9525007a74cd2116f1d"}, + {file = "multidict-6.7.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:97891f3b1b3ffbded884e2916cacf3c6fc87b66bb0dde46f7357404750559f33"}, + {file = "multidict-6.7.1-cp313-cp313-win32.whl", hash = "sha256:e1c5988359516095535c4301af38d8a8838534158f649c05dd1050222321bcb3"}, + {file = "multidict-6.7.1-cp313-cp313-win_amd64.whl", hash = "sha256:960c83bf01a95b12b08fd54324a4eb1d5b52c88932b5cba5d6e712bb3ed12eb5"}, + {file = "multidict-6.7.1-cp313-cp313-win_arm64.whl", hash = "sha256:563fe25c678aaba333d5399408f5ec3c383ca5b663e7f774dd179a520b8144df"}, + {file = "multidict-6.7.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:c76c4bec1538375dad9d452d246ca5368ad6e1c9039dadcf007ae59c70619ea1"}, + {file = "multidict-6.7.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:57b46b24b5d5ebcc978da4ec23a819a9402b4228b8a90d9c656422b4bdd8a963"}, + {file = "multidict-6.7.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e954b24433c768ce78ab7929e84ccf3422e46deb45a4dc9f93438f8217fa2d34"}, + {file = "multidict-6.7.1-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3bd231490fa7217cc832528e1cd8752a96f0125ddd2b5749390f7c3ec8721b65"}, + {file = "multidict-6.7.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:253282d70d67885a15c8a7716f3a73edf2d635793ceda8173b9ecc21f2fb8292"}, + {file = "multidict-6.7.1-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0b4c48648d7649c9335cf1927a8b87fa692de3dcb15faa676c6a6f1f1aabda43"}, + {file = "multidict-6.7.1-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:98bc624954ec4d2c7cb074b8eefc2b5d0ce7d482e410df446414355d158fe4ca"}, + {file = "multidict-6.7.1-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:1b99af4d9eec0b49927b4402bcbb58dea89d3e0db8806a4086117019939ad3dd"}, + {file = "multidict-6.7.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6aac4f16b472d5b7dc6f66a0d49dd57b0e0902090be16594dc9ebfd3d17c47e7"}, + {file = "multidict-6.7.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:21f830fe223215dffd51f538e78c172ed7c7f60c9b96a2bf05c4848ad49921c3"}, + {file = "multidict-6.7.1-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:f5dd81c45b05518b9aa4da4aa74e1c93d715efa234fd3e8a179df611cc85e5f4"}, + {file = "multidict-6.7.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:eb304767bca2bb92fb9c5bd33cedc95baee5bb5f6c88e63706533a1c06ad08c8"}, + {file = "multidict-6.7.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:c9035dde0f916702850ef66460bc4239d89d08df4d02023a5926e7446724212c"}, + {file = "multidict-6.7.1-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:af959b9beeb66c822380f222f0e0a1889331597e81f1ded7f374f3ecb0fd6c52"}, + {file = "multidict-6.7.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:41f2952231456154ee479651491e94118229844dd7226541788be783be2b5108"}, + {file = "multidict-6.7.1-cp313-cp313t-win32.whl", hash = "sha256:df9f19c28adcb40b6aae30bbaa1478c389efd50c28d541d76760199fc1037c32"}, + {file = "multidict-6.7.1-cp313-cp313t-win_amd64.whl", hash = "sha256:d54ecf9f301853f2c5e802da559604b3e95bb7a3b01a9c295c6ee591b9882de8"}, + {file = "multidict-6.7.1-cp313-cp313t-win_arm64.whl", hash = "sha256:5a37ca18e360377cfda1d62f5f382ff41f2b8c4ccb329ed974cc2e1643440118"}, + {file = "multidict-6.7.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8f333ec9c5eb1b7105e3b84b53141e66ca05a19a605368c55450b6ba208cb9ee"}, + {file = "multidict-6.7.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:a407f13c188f804c759fc6a9f88286a565c242a76b27626594c133b82883b5c2"}, + {file = "multidict-6.7.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0e161ddf326db5577c3a4cc2d8648f81456e8a20d40415541587a71620d7a7d1"}, + {file = "multidict-6.7.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:1e3a8bb24342a8201d178c3b4984c26ba81a577c80d4d525727427460a50c22d"}, + {file = "multidict-6.7.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97231140a50f5d447d3164f994b86a0bed7cd016e2682f8650d6a9158e14fd31"}, + {file = "multidict-6.7.1-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6b10359683bd8806a200fd2909e7c8ca3a7b24ec1d8132e483d58e791d881048"}, + {file = "multidict-6.7.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:283ddac99f7ac25a4acadbf004cb5ae34480bbeb063520f70ce397b281859362"}, + {file = "multidict-6.7.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:538cec1e18c067d0e6103aa9a74f9e832904c957adc260e61cd9d8cf0c3b3d37"}, + {file = "multidict-6.7.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7eee46ccb30ff48a1e35bb818cc90846c6be2b68240e42a78599166722cea709"}, + {file = "multidict-6.7.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fa263a02f4f2dd2d11a7b1bb4362aa7cb1049f84a9235d31adf63f30143469a0"}, + {file = "multidict-6.7.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:2e1425e2f99ec5bd36c15a01b690a1a2456209c5deed58f95469ffb46039ccbb"}, + {file = "multidict-6.7.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:497394b3239fc6f0e13a78a3e1b61296e72bf1c5f94b4c4eb80b265c37a131cd"}, + {file = "multidict-6.7.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:233b398c29d3f1b9676b4b6f75c518a06fcb2ea0b925119fb2c1bc35c05e1601"}, + {file = "multidict-6.7.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:93b1818e4a6e0930454f0f2af7dfce69307ca03cdcfb3739bf4d91241967b6c1"}, + {file = "multidict-6.7.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f33dc2a3abe9249ea5d8360f969ec7f4142e7ac45ee7014d8f8d5acddf178b7b"}, + {file = "multidict-6.7.1-cp314-cp314-win32.whl", hash = "sha256:3ab8b9d8b75aef9df299595d5388b14530839f6422333357af1339443cff777d"}, + {file = "multidict-6.7.1-cp314-cp314-win_amd64.whl", hash = "sha256:5e01429a929600e7dab7b166062d9bb54a5eed752384c7384c968c2afab8f50f"}, + {file = "multidict-6.7.1-cp314-cp314-win_arm64.whl", hash = "sha256:4885cb0e817aef5d00a2e8451d4665c1808378dc27c2705f1bf4ef8505c0d2e5"}, + {file = "multidict-6.7.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:0458c978acd8e6ea53c81eefaddbbee9c6c5e591f41b3f5e8e194780fe026581"}, + {file = "multidict-6.7.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:c0abd12629b0af3cf590982c0b413b1e7395cd4ec026f30986818ab95bfaa94a"}, + {file = "multidict-6.7.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:14525a5f61d7d0c94b368a42cff4c9a4e7ba2d52e2672a7b23d84dc86fb02b0c"}, + {file = "multidict-6.7.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:17307b22c217b4cf05033dabefe68255a534d637c6c9b0cc8382718f87be4262"}, + {file = "multidict-6.7.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7a7e590ff876a3eaf1c02a4dfe0724b6e69a9e9de6d8f556816f29c496046e59"}, + {file = "multidict-6.7.1-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:5fa6a95dfee63893d80a34758cd0e0c118a30b8dcb46372bf75106c591b77889"}, + {file = "multidict-6.7.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a0543217a6a017692aa6ae5cc39adb75e587af0f3a82288b1492eb73dd6cc2a4"}, + {file = "multidict-6.7.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f99fe611c312b3c1c0ace793f92464d8cd263cc3b26b5721950d977b006b6c4d"}, + {file = "multidict-6.7.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9004d8386d133b7e6135679424c91b0b854d2d164af6ea3f289f8f2761064609"}, + {file = "multidict-6.7.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e628ef0e6859ffd8273c69412a2465c4be4a9517d07261b33334b5ec6f3c7489"}, + {file = "multidict-6.7.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:841189848ba629c3552035a6a7f5bf3b02eb304e9fea7492ca220a8eda6b0e5c"}, + {file = "multidict-6.7.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:ce1bbd7d780bb5a0da032e095c951f7014d6b0a205f8318308140f1a6aba159e"}, + {file = "multidict-6.7.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:b26684587228afed0d50cf804cc71062cc9c1cdf55051c4c6345d372947b268c"}, + {file = "multidict-6.7.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:9f9af11306994335398293f9958071019e3ab95e9a707dc1383a35613f6abcb9"}, + {file = "multidict-6.7.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:b4938326284c4f1224178a560987b6cf8b4d38458b113d9b8c1db1a836e640a2"}, + {file = "multidict-6.7.1-cp314-cp314t-win32.whl", hash = "sha256:98655c737850c064a65e006a3df7c997cd3b220be4ec8fe26215760b9697d4d7"}, + {file = "multidict-6.7.1-cp314-cp314t-win_amd64.whl", hash = "sha256:497bde6223c212ba11d462853cfa4f0ae6ef97465033e7dc9940cdb3ab5b48e5"}, + {file = "multidict-6.7.1-cp314-cp314t-win_arm64.whl", hash = "sha256:2bbd113e0d4af5db41d5ebfe9ccaff89de2120578164f86a5d17d5a576d1e5b2"}, + {file = "multidict-6.7.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:65573858d27cdeaca41893185677dc82395159aa28875a8867af66532d413a8f"}, + {file = "multidict-6.7.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c524c6fb8fc342793708ab111c4dbc90ff9abd568de220432500e47e990c0358"}, + {file = "multidict-6.7.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:aa23b001d968faef416ff70dc0f1ab045517b9b42a90edd3e9bcdb06479e31d5"}, + {file = "multidict-6.7.1-cp39-cp39-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:6704fa2b7453b2fb121740555fa1ee20cd98c4d011120caf4d2b8d4e7c76eec0"}, + {file = "multidict-6.7.1-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:121a34e5bfa410cdf2c8c49716de160de3b1dbcd86b49656f5681e4543bcd1a8"}, + {file = "multidict-6.7.1-cp39-cp39-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:026d264228bcd637d4e060844e39cdc60f86c479e463d49075dedc21b18fbbe0"}, + {file = "multidict-6.7.1-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0e697826df7eb63418ee190fd06ce9f1803593bb4b9517d08c60d9b9a7f69d8f"}, + {file = "multidict-6.7.1-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:bb08271280173720e9fea9ede98e5231defcbad90f1624bea26f32ec8a956e2f"}, + {file = "multidict-6.7.1-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c6b3228e1d80af737b72925ce5fb4daf5a335e49cd7ab77ed7b9fdfbf58c526e"}, + {file = "multidict-6.7.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:3943debf0fbb57bdde5901695c11094a9a36723e5c03875f87718ee15ca2f4d2"}, + {file = "multidict-6.7.1-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:98c5787b0a0d9a41d9311eae44c3b76e6753def8d8870ab501320efe75a6a5f8"}, + {file = "multidict-6.7.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:08ccb2a6dc72009093ebe7f3f073e5ec5964cba9a706fa94b1a1484039b87941"}, + {file = "multidict-6.7.1-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:eb351f72c26dc9abe338ca7294661aa22969ad8ffe7ef7d5541d19f368dc854a"}, + {file = "multidict-6.7.1-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:ac1c665bad8b5d762f5f85ebe4d94130c26965f11de70c708c75671297c776de"}, + {file = "multidict-6.7.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:1fa6609d0364f4f6f58351b4659a1f3e0e898ba2a8c5cac04cb2c7bc556b0bc5"}, + {file = "multidict-6.7.1-cp39-cp39-win32.whl", hash = "sha256:6f77ce314a29263e67adadc7e7c1bc699fcb3a305059ab973d038f87caa42ed0"}, + {file = "multidict-6.7.1-cp39-cp39-win_amd64.whl", hash = "sha256:f537b55778cd3cbee430abe3131255d3a78202e0f9ea7ffc6ada893a4bcaeea4"}, + {file = "multidict-6.7.1-cp39-cp39-win_arm64.whl", hash = "sha256:749aa54f578f2e5f439538706a475aa844bfa8ef75854b1401e6e528e4937cf9"}, + {file = "multidict-6.7.1-py3-none-any.whl", hash = "sha256:55d97cc6dae627efa6a6e548885712d4864b81110ac76fa4e534c03819fa4a56"}, + {file = "multidict-6.7.1.tar.gz", hash = "sha256:ec6652a1bee61c53a3e5776b6049172c53b6aaba34f18c9ad04f82712bac623d"}, +] + +[package.dependencies] +typing-extensions = {version = ">=4.1.0", markers = "python_version < \"3.11\""} + +[[package]] +name = "mypy" +version = "1.19.1" +description = "Optional static typing for Python" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "mypy-1.19.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5f05aa3d375b385734388e844bc01733bd33c644ab48e9684faa54e5389775ec"}, + {file = "mypy-1.19.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:022ea7279374af1a5d78dfcab853fe6a536eebfda4b59deab53cd21f6cd9f00b"}, + {file = "mypy-1.19.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee4c11e460685c3e0c64a4c5de82ae143622410950d6be863303a1c4ba0e36d6"}, + {file = "mypy-1.19.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:de759aafbae8763283b2ee5869c7255391fbc4de3ff171f8f030b5ec48381b74"}, + {file = "mypy-1.19.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ab43590f9cd5108f41aacf9fca31841142c786827a74ab7cc8a2eacb634e09a1"}, + {file = "mypy-1.19.1-cp310-cp310-win_amd64.whl", hash = "sha256:2899753e2f61e571b3971747e302d5f420c3fd09650e1951e99f823bc3089dac"}, + {file = "mypy-1.19.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d8dfc6ab58ca7dda47d9237349157500468e404b17213d44fc1cb77bce532288"}, + {file = "mypy-1.19.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e3f276d8493c3c97930e354b2595a44a21348b320d859fb4a2b9f66da9ed27ab"}, + {file = "mypy-1.19.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2abb24cf3f17864770d18d673c85235ba52456b36a06b6afc1e07c1fdcd3d0e6"}, + {file = "mypy-1.19.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a009ffa5a621762d0c926a078c2d639104becab69e79538a494bcccb62cc0331"}, + {file = "mypy-1.19.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f7cee03c9a2e2ee26ec07479f38ea9c884e301d42c6d43a19d20fb014e3ba925"}, + {file = "mypy-1.19.1-cp311-cp311-win_amd64.whl", hash = "sha256:4b84a7a18f41e167f7995200a1d07a4a6810e89d29859df936f1c3923d263042"}, + {file = "mypy-1.19.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a8174a03289288c1f6c46d55cef02379b478bfbc8e358e02047487cad44c6ca1"}, + {file = "mypy-1.19.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ffcebe56eb09ff0c0885e750036a095e23793ba6c2e894e7e63f6d89ad51f22e"}, + {file = "mypy-1.19.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b64d987153888790bcdb03a6473d321820597ab8dd9243b27a92153c4fa50fd2"}, + {file = "mypy-1.19.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c35d298c2c4bba75feb2195655dfea8124d855dfd7343bf8b8c055421eaf0cf8"}, + {file = "mypy-1.19.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:34c81968774648ab5ac09c29a375fdede03ba253f8f8287847bd480782f73a6a"}, + {file = "mypy-1.19.1-cp312-cp312-win_amd64.whl", hash = "sha256:b10e7c2cd7870ba4ad9b2d8a6102eb5ffc1f16ca35e3de6bfa390c1113029d13"}, + {file = "mypy-1.19.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e3157c7594ff2ef1634ee058aafc56a82db665c9438fd41b390f3bde1ab12250"}, + {file = "mypy-1.19.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bdb12f69bcc02700c2b47e070238f42cb87f18c0bc1fc4cdb4fb2bc5fd7a3b8b"}, + {file = "mypy-1.19.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f859fb09d9583a985be9a493d5cfc5515b56b08f7447759a0c5deaf68d80506e"}, + {file = "mypy-1.19.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c9a6538e0415310aad77cb94004ca6482330fece18036b5f360b62c45814c4ef"}, + {file = "mypy-1.19.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:da4869fc5e7f62a88f3fe0b5c919d1d9f7ea3cef92d3689de2823fd27e40aa75"}, + {file = "mypy-1.19.1-cp313-cp313-win_amd64.whl", hash = "sha256:016f2246209095e8eda7538944daa1d60e1e8134d98983b9fc1e92c1fc0cb8dd"}, + {file = "mypy-1.19.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:06e6170bd5836770e8104c8fdd58e5e725cfeb309f0a6c681a811f557e97eac1"}, + {file = "mypy-1.19.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:804bd67b8054a85447c8954215a906d6eff9cabeabe493fb6334b24f4bfff718"}, + {file = "mypy-1.19.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:21761006a7f497cb0d4de3d8ef4ca70532256688b0523eee02baf9eec895e27b"}, + {file = "mypy-1.19.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:28902ee51f12e0f19e1e16fbe2f8f06b6637f482c459dd393efddd0ec7f82045"}, + {file = "mypy-1.19.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:481daf36a4c443332e2ae9c137dfee878fcea781a2e3f895d54bd3002a900957"}, + {file = "mypy-1.19.1-cp314-cp314-win_amd64.whl", hash = "sha256:8bb5c6f6d043655e055be9b542aa5f3bdd30e4f3589163e85f93f3640060509f"}, + {file = "mypy-1.19.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7bcfc336a03a1aaa26dfce9fff3e287a3ba99872a157561cbfcebe67c13308e3"}, + {file = "mypy-1.19.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b7951a701c07ea584c4fe327834b92a30825514c868b1f69c30445093fdd9d5a"}, + {file = "mypy-1.19.1-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b13cfdd6c87fc3efb69ea4ec18ef79c74c3f98b4e5498ca9b85ab3b2c2329a67"}, + {file = "mypy-1.19.1-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4f28f99c824ecebcdaa2e55d82953e38ff60ee5ec938476796636b86afa3956e"}, + {file = "mypy-1.19.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:c608937067d2fc5a4dd1a5ce92fd9e1398691b8c5d012d66e1ddd430e9244376"}, + {file = "mypy-1.19.1-cp39-cp39-win_amd64.whl", hash = "sha256:409088884802d511ee52ca067707b90c883426bd95514e8cfda8281dc2effe24"}, + {file = "mypy-1.19.1-py3-none-any.whl", hash = "sha256:f1235f5ea01b7db5468d53ece6aaddf1ad0b88d9e7462b86ef96fe04995d7247"}, + {file = "mypy-1.19.1.tar.gz", hash = "sha256:19d88bb05303fe63f71dd2c6270daca27cb9401c4ca8255fe50d1d920e0eb9ba"}, +] + +[package.dependencies] +librt = {version = ">=0.6.2", markers = "platform_python_implementation != \"PyPy\""} +mypy_extensions = ">=1.0.0" +pathspec = ">=0.9.0" +tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} +typing_extensions = ">=4.6.0" + +[package.extras] +dmypy = ["psutil (>=4.0)"] +faster-cache = ["orjson"] +install-types = ["pip"] +mypyc = ["setuptools (>=50)"] +reports = ["lxml"] + +[[package]] +name = "mypy-extensions" +version = "1.1.0" +description = "Type system extensions for programs checked with the mypy type checker." +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505"}, + {file = "mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558"}, +] + +[[package]] +name = "packaging" +version = "26.0" +description = "Core utilities for Python packages" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529"}, + {file = "packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4"}, +] + +[[package]] +name = "parsimonious" +version = "0.10.0" +description = "(Soon to be) the fastest pure-Python PEG parser I could muster" +optional = false +python-versions = "*" +groups = ["main"] +files = [ + {file = "parsimonious-0.10.0-py3-none-any.whl", hash = "sha256:982ab435fabe86519b57f6b35610aa4e4e977e9f02a14353edf4bbc75369fc0f"}, + {file = "parsimonious-0.10.0.tar.gz", hash = "sha256:8281600da180ec8ae35427a4ab4f7b82bfec1e3d1e52f80cb60ea82b9512501c"}, +] + +[package.dependencies] +regex = ">=2022.3.15" + +[[package]] +name = "pathspec" +version = "1.0.4" +description = "Utility library for gitignore style pattern matching of file paths." +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "pathspec-1.0.4-py3-none-any.whl", hash = "sha256:fb6ae2fd4e7c921a165808a552060e722767cfa526f99ca5156ed2ce45a5c723"}, + {file = "pathspec-1.0.4.tar.gz", hash = "sha256:0210e2ae8a21a9137c0d470578cb0e595af87edaa6ebf12ff176f14a02e0e645"}, +] + +[package.extras] +hyperscan = ["hyperscan (>=0.7)"] +optional = ["typing-extensions (>=4)"] +re2 = ["google-re2 (>=1.1)"] +tests = ["pytest (>=9)", "typing-extensions (>=4.15)"] + +[[package]] +name = "platformdirs" +version = "4.9.4" +description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." +optional = false +python-versions = ">=3.10" +groups = ["dev"] +files = [ + {file = "platformdirs-4.9.4-py3-none-any.whl", hash = "sha256:68a9a4619a666ea6439f2ff250c12a853cd1cbd5158d258bd824a7df6be2f868"}, + {file = "platformdirs-4.9.4.tar.gz", hash = "sha256:1ec356301b7dc906d83f371c8f487070e99d3ccf9e501686456394622a01a934"}, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +description = "plugin and hook calling mechanisms for python" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746"}, + {file = "pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3"}, +] + +[package.extras] +dev = ["pre-commit", "tox"] +testing = ["coverage", "pytest", "pytest-benchmark"] + +[[package]] +name = "propcache" +version = "0.4.1" +description = "Accelerated property cache" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "propcache-0.4.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7c2d1fa3201efaf55d730400d945b5b3ab6e672e100ba0f9a409d950ab25d7db"}, + {file = "propcache-0.4.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:1eb2994229cc8ce7fe9b3db88f5465f5fd8651672840b2e426b88cdb1a30aac8"}, + {file = "propcache-0.4.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:66c1f011f45a3b33d7bcb22daed4b29c0c9e2224758b6be00686731e1b46f925"}, + {file = "propcache-0.4.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9a52009f2adffe195d0b605c25ec929d26b36ef986ba85244891dee3b294df21"}, + {file = "propcache-0.4.1-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5d4e2366a9c7b837555cf02fb9be2e3167d333aff716332ef1b7c3a142ec40c5"}, + {file = "propcache-0.4.1-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:9d2b6caef873b4f09e26ea7e33d65f42b944837563a47a94719cc3544319a0db"}, + {file = "propcache-0.4.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2b16ec437a8c8a965ecf95739448dd938b5c7f56e67ea009f4300d8df05f32b7"}, + {file = "propcache-0.4.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:296f4c8ed03ca7476813fe666c9ea97869a8d7aec972618671b33a38a5182ef4"}, + {file = "propcache-0.4.1-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:1f0978529a418ebd1f49dad413a2b68af33f85d5c5ca5c6ca2a3bed375a7ac60"}, + {file = "propcache-0.4.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:fd138803047fb4c062b1c1dd95462f5209456bfab55c734458f15d11da288f8f"}, + {file = "propcache-0.4.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:8c9b3cbe4584636d72ff556d9036e0c9317fa27b3ac1f0f558e7e84d1c9c5900"}, + {file = "propcache-0.4.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f93243fdc5657247533273ac4f86ae106cc6445a0efacb9a1bfe982fcfefd90c"}, + {file = "propcache-0.4.1-cp310-cp310-win32.whl", hash = "sha256:a0ee98db9c5f80785b266eb805016e36058ac72c51a064040f2bc43b61101cdb"}, + {file = "propcache-0.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:1cdb7988c4e5ac7f6d175a28a9aa0c94cb6f2ebe52756a3c0cda98d2809a9e37"}, + {file = "propcache-0.4.1-cp310-cp310-win_arm64.whl", hash = "sha256:d82ad62b19645419fe79dd63b3f9253e15b30e955c0170e5cebc350c1844e581"}, + {file = "propcache-0.4.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:60a8fda9644b7dfd5dece8c61d8a85e271cb958075bfc4e01083c148b61a7caf"}, + {file = "propcache-0.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c30b53e7e6bda1d547cabb47c825f3843a0a1a42b0496087bb58d8fedf9f41b5"}, + {file = "propcache-0.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6918ecbd897443087a3b7cd978d56546a812517dcaaca51b49526720571fa93e"}, + {file = "propcache-0.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3d902a36df4e5989763425a8ab9e98cd8ad5c52c823b34ee7ef307fd50582566"}, + {file = "propcache-0.4.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a9695397f85973bb40427dedddf70d8dc4a44b22f1650dd4af9eedf443d45165"}, + {file = "propcache-0.4.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2bb07ffd7eaad486576430c89f9b215f9e4be68c4866a96e97db9e97fead85dc"}, + {file = "propcache-0.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fd6f30fdcf9ae2a70abd34da54f18da086160e4d7d9251f81f3da0ff84fc5a48"}, + {file = "propcache-0.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:fc38cba02d1acba4e2869eef1a57a43dfbd3d49a59bf90dda7444ec2be6a5570"}, + {file = "propcache-0.4.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:67fad6162281e80e882fb3ec355398cf72864a54069d060321f6cd0ade95fe85"}, + {file = "propcache-0.4.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f10207adf04d08bec185bae14d9606a1444715bc99180f9331c9c02093e1959e"}, + {file = "propcache-0.4.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:e9b0d8d0845bbc4cfcdcbcdbf5086886bc8157aa963c31c777ceff7846c77757"}, + {file = "propcache-0.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:981333cb2f4c1896a12f4ab92a9cc8f09ea664e9b7dbdc4eff74627af3a11c0f"}, + {file = "propcache-0.4.1-cp311-cp311-win32.whl", hash = "sha256:f1d2f90aeec838a52f1c1a32fe9a619fefd5e411721a9117fbf82aea638fe8a1"}, + {file = "propcache-0.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:364426a62660f3f699949ac8c621aad6977be7126c5807ce48c0aeb8e7333ea6"}, + {file = "propcache-0.4.1-cp311-cp311-win_arm64.whl", hash = "sha256:e53f3a38d3510c11953f3e6a33f205c6d1b001129f972805ca9b42fc308bc239"}, + {file = "propcache-0.4.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e153e9cd40cc8945138822807139367f256f89c6810c2634a4f6902b52d3b4e2"}, + {file = "propcache-0.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:cd547953428f7abb73c5ad82cbb32109566204260d98e41e5dfdc682eb7f8403"}, + {file = "propcache-0.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f048da1b4f243fc44f205dfd320933a951b8d89e0afd4c7cacc762a8b9165207"}, + {file = "propcache-0.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ec17c65562a827bba85e3872ead335f95405ea1674860d96483a02f5c698fa72"}, + {file = "propcache-0.4.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:405aac25c6394ef275dee4c709be43745d36674b223ba4eb7144bf4d691b7367"}, + {file = "propcache-0.4.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0013cb6f8dde4b2a2f66903b8ba740bdfe378c943c4377a200551ceb27f379e4"}, + {file = "propcache-0.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:15932ab57837c3368b024473a525e25d316d8353016e7cc0e5ba9eb343fbb1cf"}, + {file = "propcache-0.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:031dce78b9dc099f4c29785d9cf5577a3faf9ebf74ecbd3c856a7b92768c3df3"}, + {file = "propcache-0.4.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:ab08df6c9a035bee56e31af99be621526bd237bea9f32def431c656b29e41778"}, + {file = "propcache-0.4.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4d7af63f9f93fe593afbf104c21b3b15868efb2c21d07d8732c0c4287e66b6a6"}, + {file = "propcache-0.4.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:cfc27c945f422e8b5071b6e93169679e4eb5bf73bbcbf1ba3ae3a83d2f78ebd9"}, + {file = "propcache-0.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:35c3277624a080cc6ec6f847cbbbb5b49affa3598c4535a0a4682a697aaa5c75"}, + {file = "propcache-0.4.1-cp312-cp312-win32.whl", hash = "sha256:671538c2262dadb5ba6395e26c1731e1d52534bfe9ae56d0b5573ce539266aa8"}, + {file = "propcache-0.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:cb2d222e72399fcf5890d1d5cc1060857b9b236adff2792ff48ca2dfd46c81db"}, + {file = "propcache-0.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:204483131fb222bdaaeeea9f9e6c6ed0cac32731f75dfc1d4a567fc1926477c1"}, + {file = "propcache-0.4.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:43eedf29202c08550aac1d14e0ee619b0430aaef78f85864c1a892294fbc28cf"}, + {file = "propcache-0.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d62cdfcfd89ccb8de04e0eda998535c406bf5e060ffd56be6c586cbcc05b3311"}, + {file = "propcache-0.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cae65ad55793da34db5f54e4029b89d3b9b9490d8abe1b4c7ab5d4b8ec7ebf74"}, + {file = "propcache-0.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:333ddb9031d2704a301ee3e506dc46b1fe5f294ec198ed6435ad5b6a085facfe"}, + {file = "propcache-0.4.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:fd0858c20f078a32cf55f7e81473d96dcf3b93fd2ccdb3d40fdf54b8573df3af"}, + {file = "propcache-0.4.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:678ae89ebc632c5c204c794f8dab2837c5f159aeb59e6ed0539500400577298c"}, + {file = "propcache-0.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d472aeb4fbf9865e0c6d622d7f4d54a4e101a89715d8904282bb5f9a2f476c3f"}, + {file = "propcache-0.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4d3df5fa7e36b3225954fba85589da77a0fe6a53e3976de39caf04a0db4c36f1"}, + {file = "propcache-0.4.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:ee17f18d2498f2673e432faaa71698032b0127ebf23ae5974eeaf806c279df24"}, + {file = "propcache-0.4.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:580e97762b950f993ae618e167e7be9256b8353c2dcd8b99ec100eb50f5286aa"}, + {file = "propcache-0.4.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:501d20b891688eb8e7aa903021f0b72d5a55db40ffaab27edefd1027caaafa61"}, + {file = "propcache-0.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a0bd56e5b100aef69bd8562b74b46254e7c8812918d3baa700c8a8009b0af66"}, + {file = "propcache-0.4.1-cp313-cp313-win32.whl", hash = "sha256:bcc9aaa5d80322bc2fb24bb7accb4a30f81e90ab8d6ba187aec0744bc302ad81"}, + {file = "propcache-0.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:381914df18634f5494334d201e98245c0596067504b9372d8cf93f4bb23e025e"}, + {file = "propcache-0.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:8873eb4460fd55333ea49b7d189749ecf6e55bf85080f11b1c4530ed3034cba1"}, + {file = "propcache-0.4.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:92d1935ee1f8d7442da9c0c4fa7ac20d07e94064184811b685f5c4fada64553b"}, + {file = "propcache-0.4.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:473c61b39e1460d386479b9b2f337da492042447c9b685f28be4f74d3529e566"}, + {file = "propcache-0.4.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:c0ef0aaafc66fbd87842a3fe3902fd889825646bc21149eafe47be6072725835"}, + {file = "propcache-0.4.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f95393b4d66bfae908c3ca8d169d5f79cd65636ae15b5e7a4f6e67af675adb0e"}, + {file = "propcache-0.4.1-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c07fda85708bc48578467e85099645167a955ba093be0a2dcba962195676e859"}, + {file = "propcache-0.4.1-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:af223b406d6d000830c6f65f1e6431783fc3f713ba3e6cc8c024d5ee96170a4b"}, + {file = "propcache-0.4.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a78372c932c90ee474559c5ddfffd718238e8673c340dc21fe45c5b8b54559a0"}, + {file = "propcache-0.4.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:564d9f0d4d9509e1a870c920a89b2fec951b44bf5ba7d537a9e7c1ccec2c18af"}, + {file = "propcache-0.4.1-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:17612831fda0138059cc5546f4d12a2aacfb9e47068c06af35c400ba58ba7393"}, + {file = "propcache-0.4.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:41a89040cb10bd345b3c1a873b2bf36413d48da1def52f268a055f7398514874"}, + {file = "propcache-0.4.1-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:e35b88984e7fa64aacecea39236cee32dd9bd8c55f57ba8a75cf2399553f9bd7"}, + {file = "propcache-0.4.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6f8b465489f927b0df505cbe26ffbeed4d6d8a2bbc61ce90eb074ff129ef0ab1"}, + {file = "propcache-0.4.1-cp313-cp313t-win32.whl", hash = "sha256:2ad890caa1d928c7c2965b48f3a3815c853180831d0e5503d35cf00c472f4717"}, + {file = "propcache-0.4.1-cp313-cp313t-win_amd64.whl", hash = "sha256:f7ee0e597f495cf415bcbd3da3caa3bd7e816b74d0d52b8145954c5e6fd3ff37"}, + {file = "propcache-0.4.1-cp313-cp313t-win_arm64.whl", hash = "sha256:929d7cbe1f01bb7baffb33dc14eb5691c95831450a26354cd210a8155170c93a"}, + {file = "propcache-0.4.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3f7124c9d820ba5548d431afb4632301acf965db49e666aa21c305cbe8c6de12"}, + {file = "propcache-0.4.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:c0d4b719b7da33599dfe3b22d3db1ef789210a0597bc650b7cee9c77c2be8c5c"}, + {file = "propcache-0.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9f302f4783709a78240ebc311b793f123328716a60911d667e0c036bc5dcbded"}, + {file = "propcache-0.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c80ee5802e3fb9ea37938e7eecc307fb984837091d5fd262bb37238b1ae97641"}, + {file = "propcache-0.4.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ed5a841e8bb29a55fb8159ed526b26adc5bdd7e8bd7bf793ce647cb08656cdf4"}, + {file = "propcache-0.4.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:55c72fd6ea2da4c318e74ffdf93c4fe4e926051133657459131a95c846d16d44"}, + {file = "propcache-0.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8326e144341460402713f91df60ade3c999d601e7eb5ff8f6f7862d54de0610d"}, + {file = "propcache-0.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:060b16ae65bc098da7f6d25bf359f1f31f688384858204fe5d652979e0015e5b"}, + {file = "propcache-0.4.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:89eb3fa9524f7bec9de6e83cf3faed9d79bffa560672c118a96a171a6f55831e"}, + {file = "propcache-0.4.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:dee69d7015dc235f526fe80a9c90d65eb0039103fe565776250881731f06349f"}, + {file = "propcache-0.4.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:5558992a00dfd54ccbc64a32726a3357ec93825a418a401f5cc67df0ac5d9e49"}, + {file = "propcache-0.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c9b822a577f560fbd9554812526831712c1436d2c046cedee4c3796d3543b144"}, + {file = "propcache-0.4.1-cp314-cp314-win32.whl", hash = "sha256:ab4c29b49d560fe48b696cdcb127dd36e0bc2472548f3bf56cc5cb3da2b2984f"}, + {file = "propcache-0.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:5a103c3eb905fcea0ab98be99c3a9a5ab2de60228aa5aceedc614c0281cf6153"}, + {file = "propcache-0.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:74c1fb26515153e482e00177a1ad654721bf9207da8a494a0c05e797ad27b992"}, + {file = "propcache-0.4.1-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:824e908bce90fb2743bd6b59db36eb4f45cd350a39637c9f73b1c1ea66f5b75f"}, + {file = "propcache-0.4.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:c2b5e7db5328427c57c8e8831abda175421b709672f6cfc3d630c3b7e2146393"}, + {file = "propcache-0.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6f6ff873ed40292cd4969ef5310179afd5db59fdf055897e282485043fc80ad0"}, + {file = "propcache-0.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:49a2dc67c154db2c1463013594c458881a069fcf98940e61a0569016a583020a"}, + {file = "propcache-0.4.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:005f08e6a0529984491e37d8dbc3dd86f84bd78a8ceb5fa9a021f4c48d4984be"}, + {file = "propcache-0.4.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5c3310452e0d31390da9035c348633b43d7e7feb2e37be252be6da45abd1abcc"}, + {file = "propcache-0.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4c3c70630930447f9ef1caac7728c8ad1c56bc5015338b20fed0d08ea2480b3a"}, + {file = "propcache-0.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8e57061305815dfc910a3634dcf584f08168a8836e6999983569f51a8544cd89"}, + {file = "propcache-0.4.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:521a463429ef54143092c11a77e04056dd00636f72e8c45b70aaa3140d639726"}, + {file = "propcache-0.4.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:120c964da3fdc75e3731aa392527136d4ad35868cc556fd09bb6d09172d9a367"}, + {file = "propcache-0.4.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:d8f353eb14ee3441ee844ade4277d560cdd68288838673273b978e3d6d2c8f36"}, + {file = "propcache-0.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ab2943be7c652f09638800905ee1bab2c544e537edb57d527997a24c13dc1455"}, + {file = "propcache-0.4.1-cp314-cp314t-win32.whl", hash = "sha256:05674a162469f31358c30bcaa8883cb7829fa3110bf9c0991fe27d7896c42d85"}, + {file = "propcache-0.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:990f6b3e2a27d683cb7602ed6c86f15ee6b43b1194736f9baaeb93d0016633b1"}, + {file = "propcache-0.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:ecef2343af4cc68e05131e45024ba34f6095821988a9d0a02aa7c73fcc448aa9"}, + {file = "propcache-0.4.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:3d233076ccf9e450c8b3bc6720af226b898ef5d051a2d145f7d765e6e9f9bcff"}, + {file = "propcache-0.4.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:357f5bb5c377a82e105e44bd3d52ba22b616f7b9773714bff93573988ef0a5fb"}, + {file = "propcache-0.4.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:cbc3b6dfc728105b2a57c06791eb07a94229202ea75c59db644d7d496b698cac"}, + {file = "propcache-0.4.1-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:182b51b421f0501952d938dc0b0eb45246a5b5153c50d42b495ad5fb7517c888"}, + {file = "propcache-0.4.1-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4b536b39c5199b96fc6245eb5fb796c497381d3942f169e44e8e392b29c9ebcc"}, + {file = "propcache-0.4.1-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:db65d2af507bbfbdcedb254a11149f894169d90488dd3e7190f7cdcb2d6cd57a"}, + {file = "propcache-0.4.1-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fd2dbc472da1f772a4dae4fa24be938a6c544671a912e30529984dd80400cd88"}, + {file = "propcache-0.4.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:daede9cd44e0f8bdd9e6cc9a607fc81feb80fae7a5fc6cecaff0e0bb32e42d00"}, + {file = "propcache-0.4.1-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:71b749281b816793678ae7f3d0d84bd36e694953822eaad408d682efc5ca18e0"}, + {file = "propcache-0.4.1-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:0002004213ee1f36cfb3f9a42b5066100c44276b9b72b4e1504cddd3d692e86e"}, + {file = "propcache-0.4.1-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:fe49d0a85038f36ba9e3ffafa1103e61170b28e95b16622e11be0a0ea07c6781"}, + {file = "propcache-0.4.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:99d43339c83aaf4d32bda60928231848eee470c6bda8d02599cc4cebe872d183"}, + {file = "propcache-0.4.1-cp39-cp39-win32.whl", hash = "sha256:a129e76735bc792794d5177069691c3217898b9f5cee2b2661471e52ffe13f19"}, + {file = "propcache-0.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:948dab269721ae9a87fd16c514a0a2c2a1bdb23a9a61b969b0f9d9ee2968546f"}, + {file = "propcache-0.4.1-cp39-cp39-win_arm64.whl", hash = "sha256:5fd37c406dd6dc85aa743e214cef35dc54bbdd1419baac4f6ae5e5b1a2976938"}, + {file = "propcache-0.4.1-py3-none-any.whl", hash = "sha256:af2a6052aeb6cf17d3e46ee169099044fd8224cbaf75c76a2ef596e8163e2237"}, + {file = "propcache-0.4.1.tar.gz", hash = "sha256:f48107a8c637e80362555f37ecf49abe20370e557cc4ab374f04ec4423c97c3d"}, +] + +[[package]] +name = "pycryptodome" +version = "3.23.0" +description = "Cryptographic library for Python" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +groups = ["main"] +files = [ + {file = "pycryptodome-3.23.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:a176b79c49af27d7f6c12e4b178b0824626f40a7b9fed08f712291b6d54bf566"}, + {file = "pycryptodome-3.23.0-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:573a0b3017e06f2cffd27d92ef22e46aa3be87a2d317a5abf7cc0e84e321bd75"}, + {file = "pycryptodome-3.23.0-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:63dad881b99ca653302b2c7191998dd677226222a3f2ea79999aa51ce695f720"}, + {file = "pycryptodome-3.23.0-cp27-cp27m-win32.whl", hash = "sha256:b34e8e11d97889df57166eda1e1ddd7676da5fcd4d71a0062a760e75060514b4"}, + {file = "pycryptodome-3.23.0-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:7ac1080a8da569bde76c0a104589c4f414b8ba296c0b3738cf39a466a9fb1818"}, + {file = "pycryptodome-3.23.0-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:6fe8258e2039eceb74dfec66b3672552b6b7d2c235b2dfecc05d16b8921649a8"}, + {file = "pycryptodome-3.23.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:0011f7f00cdb74879142011f95133274741778abba114ceca229adbf8e62c3e4"}, + {file = "pycryptodome-3.23.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:90460fc9e088ce095f9ee8356722d4f10f86e5be06e2354230a9880b9c549aae"}, + {file = "pycryptodome-3.23.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4764e64b269fc83b00f682c47443c2e6e85b18273712b98aa43bcb77f8570477"}, + {file = "pycryptodome-3.23.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eb8f24adb74984aa0e5d07a2368ad95276cf38051fe2dc6605cbcf482e04f2a7"}, + {file = "pycryptodome-3.23.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d97618c9c6684a97ef7637ba43bdf6663a2e2e77efe0f863cce97a76af396446"}, + {file = "pycryptodome-3.23.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9a53a4fe5cb075075d515797d6ce2f56772ea7e6a1e5e4b96cf78a14bac3d265"}, + {file = "pycryptodome-3.23.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:763d1d74f56f031788e5d307029caef067febf890cd1f8bf61183ae142f1a77b"}, + {file = "pycryptodome-3.23.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:954af0e2bd7cea83ce72243b14e4fb518b18f0c1649b576d114973e2073b273d"}, + {file = "pycryptodome-3.23.0-cp313-cp313t-win32.whl", hash = "sha256:257bb3572c63ad8ba40b89f6fc9d63a2a628e9f9708d31ee26560925ebe0210a"}, + {file = "pycryptodome-3.23.0-cp313-cp313t-win_amd64.whl", hash = "sha256:6501790c5b62a29fcb227bd6b62012181d886a767ce9ed03b303d1f22eb5c625"}, + {file = "pycryptodome-3.23.0-cp313-cp313t-win_arm64.whl", hash = "sha256:9a77627a330ab23ca43b48b130e202582e91cc69619947840ea4d2d1be21eb39"}, + {file = "pycryptodome-3.23.0-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:187058ab80b3281b1de11c2e6842a357a1f71b42cb1e15bce373f3d238135c27"}, + {file = "pycryptodome-3.23.0-cp37-abi3-macosx_10_9_x86_64.whl", hash = "sha256:cfb5cd445280c5b0a4e6187a7ce8de5a07b5f3f897f235caa11f1f435f182843"}, + {file = "pycryptodome-3.23.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:67bd81fcbe34f43ad9422ee8fd4843c8e7198dd88dd3d40e6de42ee65fbe1490"}, + {file = "pycryptodome-3.23.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c8987bd3307a39bc03df5c8e0e3d8be0c4c3518b7f044b0f4c15d1aa78f52575"}, + {file = "pycryptodome-3.23.0-cp37-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aa0698f65e5b570426fc31b8162ed4603b0c2841cbb9088e2b01641e3065915b"}, + {file = "pycryptodome-3.23.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:53ecbafc2b55353edcebd64bf5da94a2a2cdf5090a6915bcca6eca6cc452585a"}, + {file = "pycryptodome-3.23.0-cp37-abi3-musllinux_1_2_i686.whl", hash = "sha256:156df9667ad9f2ad26255926524e1c136d6664b741547deb0a86a9acf5ea631f"}, + {file = "pycryptodome-3.23.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:dea827b4d55ee390dc89b2afe5927d4308a8b538ae91d9c6f7a5090f397af1aa"}, + {file = "pycryptodome-3.23.0-cp37-abi3-win32.whl", hash = "sha256:507dbead45474b62b2bbe318eb1c4c8ee641077532067fec9c1aa82c31f84886"}, + {file = "pycryptodome-3.23.0-cp37-abi3-win_amd64.whl", hash = "sha256:c75b52aacc6c0c260f204cbdd834f76edc9fb0d8e0da9fbf8352ef58202564e2"}, + {file = "pycryptodome-3.23.0-cp37-abi3-win_arm64.whl", hash = "sha256:11eeeb6917903876f134b56ba11abe95c0b0fd5e3330def218083c7d98bbcb3c"}, + {file = "pycryptodome-3.23.0-pp27-pypy_73-manylinux2010_x86_64.whl", hash = "sha256:350ebc1eba1da729b35ab7627a833a1a355ee4e852d8ba0447fafe7b14504d56"}, + {file = "pycryptodome-3.23.0-pp27-pypy_73-win32.whl", hash = "sha256:93837e379a3e5fd2bb00302a47aee9fdf7940d83595be3915752c74033d17ca7"}, + {file = "pycryptodome-3.23.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:ddb95b49df036ddd264a0ad246d1be5b672000f12d6961ea2c267083a5e19379"}, + {file = "pycryptodome-3.23.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8e95564beb8782abfd9e431c974e14563a794a4944c29d6d3b7b5ea042110b4"}, + {file = "pycryptodome-3.23.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:14e15c081e912c4b0d75632acd8382dfce45b258667aa3c67caf7a4d4c13f630"}, + {file = "pycryptodome-3.23.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a7fc76bf273353dc7e5207d172b83f569540fc9a28d63171061c42e361d22353"}, + {file = "pycryptodome-3.23.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:45c69ad715ca1a94f778215a11e66b7ff989d792a4d63b68dc586a1da1392ff5"}, + {file = "pycryptodome-3.23.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:865d83c906b0fc6a59b510deceee656b6bc1c4fa0d82176e2b77e97a420a996a"}, + {file = "pycryptodome-3.23.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:89d4d56153efc4d81defe8b65fd0821ef8b2d5ddf8ed19df31ba2f00872b8002"}, + {file = "pycryptodome-3.23.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e3f2d0aaf8080bda0587d58fc9fe4766e012441e2eed4269a77de6aea981c8be"}, + {file = "pycryptodome-3.23.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:64093fc334c1eccfd3933c134c4457c34eaca235eeae49d69449dc4728079339"}, + {file = "pycryptodome-3.23.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:ce64e84a962b63a47a592690bdc16a7eaf709d2c2697ababf24a0def566899a6"}, + {file = "pycryptodome-3.23.0.tar.gz", hash = "sha256:447700a657182d60338bab09fdb27518f8856aecd80ae4c6bdddb67ff5da44ef"}, +] + +[[package]] +name = "pydantic" +version = "2.12.5" +description = "Data validation using Python type hints" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "pydantic-2.12.5-py3-none-any.whl", hash = "sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d"}, + {file = "pydantic-2.12.5.tar.gz", hash = "sha256:4d351024c75c0f085a9febbb665ce8c0c6ec5d30e903bdb6394b7ede26aebb49"}, +] + +[package.dependencies] +annotated-types = ">=0.6.0" +pydantic-core = "2.41.5" +typing-extensions = ">=4.14.1" +typing-inspection = ">=0.4.2" + +[package.extras] +email = ["email-validator (>=2.0.0)"] +timezone = ["tzdata ; python_version >= \"3.9\" and platform_system == \"Windows\""] + +[[package]] +name = "pydantic-core" +version = "2.41.5" +description = "Core functionality for Pydantic validation and serialization" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "pydantic_core-2.41.5-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:77b63866ca88d804225eaa4af3e664c5faf3568cea95360d21f4725ab6e07146"}, + {file = "pydantic_core-2.41.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:dfa8a0c812ac681395907e71e1274819dec685fec28273a28905df579ef137e2"}, + {file = "pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5921a4d3ca3aee735d9fd163808f5e8dd6c6972101e4adbda9a4667908849b97"}, + {file = "pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e25c479382d26a2a41b7ebea1043564a937db462816ea07afa8a44c0866d52f9"}, + {file = "pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f547144f2966e1e16ae626d8ce72b4cfa0caedc7fa28052001c94fb2fcaa1c52"}, + {file = "pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6f52298fbd394f9ed112d56f3d11aabd0d5bd27beb3084cc3d8ad069483b8941"}, + {file = "pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:100baa204bb412b74fe285fb0f3a385256dad1d1879f0a5cb1499ed2e83d132a"}, + {file = "pydantic_core-2.41.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:05a2c8852530ad2812cb7914dc61a1125dc4e06252ee98e5638a12da6cc6fb6c"}, + {file = "pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:29452c56df2ed968d18d7e21f4ab0ac55e71dc59524872f6fc57dcf4a3249ed2"}, + {file = "pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:d5160812ea7a8a2ffbe233d8da666880cad0cbaf5d4de74ae15c313213d62556"}, + {file = "pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:df3959765b553b9440adfd3c795617c352154e497a4eaf3752555cfb5da8fc49"}, + {file = "pydantic_core-2.41.5-cp310-cp310-win32.whl", hash = "sha256:1f8d33a7f4d5a7889e60dc39856d76d09333d8a6ed0f5f1190635cbec70ec4ba"}, + {file = "pydantic_core-2.41.5-cp310-cp310-win_amd64.whl", hash = "sha256:62de39db01b8d593e45871af2af9e497295db8d73b085f6bfd0b18c83c70a8f9"}, + {file = "pydantic_core-2.41.5-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a3a52f6156e73e7ccb0f8cced536adccb7042be67cb45f9562e12b319c119da6"}, + {file = "pydantic_core-2.41.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7f3bf998340c6d4b0c9a2f02d6a400e51f123b59565d74dc60d252ce888c260b"}, + {file = "pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:378bec5c66998815d224c9ca994f1e14c0c21cb95d2f52b6021cc0b2a58f2a5a"}, + {file = "pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e7b576130c69225432866fe2f4a469a85a54ade141d96fd396dffcf607b558f8"}, + {file = "pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6cb58b9c66f7e4179a2d5e0f849c48eff5c1fca560994d6eb6543abf955a149e"}, + {file = "pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:88942d3a3dff3afc8288c21e565e476fc278902ae4d6d134f1eeda118cc830b1"}, + {file = "pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f31d95a179f8d64d90f6831d71fa93290893a33148d890ba15de25642c5d075b"}, + {file = "pydantic_core-2.41.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c1df3d34aced70add6f867a8cf413e299177e0c22660cc767218373d0779487b"}, + {file = "pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:4009935984bd36bd2c774e13f9a09563ce8de4abaa7226f5108262fa3e637284"}, + {file = "pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:34a64bc3441dc1213096a20fe27e8e128bd3ff89921706e83c0b1ac971276594"}, + {file = "pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c9e19dd6e28fdcaa5a1de679aec4141f691023916427ef9bae8584f9c2fb3b0e"}, + {file = "pydantic_core-2.41.5-cp311-cp311-win32.whl", hash = "sha256:2c010c6ded393148374c0f6f0bf89d206bf3217f201faa0635dcd56bd1520f6b"}, + {file = "pydantic_core-2.41.5-cp311-cp311-win_amd64.whl", hash = "sha256:76ee27c6e9c7f16f47db7a94157112a2f3a00e958bc626e2f4ee8bec5c328fbe"}, + {file = "pydantic_core-2.41.5-cp311-cp311-win_arm64.whl", hash = "sha256:4bc36bbc0b7584de96561184ad7f012478987882ebf9f9c389b23f432ea3d90f"}, + {file = "pydantic_core-2.41.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f41a7489d32336dbf2199c8c0a215390a751c5b014c2c1c5366e817202e9cdf7"}, + {file = "pydantic_core-2.41.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:070259a8818988b9a84a449a2a7337c7f430a22acc0859c6b110aa7212a6d9c0"}, + {file = "pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e96cea19e34778f8d59fe40775a7a574d95816eb150850a85a7a4c8f4b94ac69"}, + {file = "pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed2e99c456e3fadd05c991f8f437ef902e00eedf34320ba2b0842bd1c3ca3a75"}, + {file = "pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:65840751b72fbfd82c3c640cff9284545342a4f1eb1586ad0636955b261b0b05"}, + {file = "pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e536c98a7626a98feb2d3eaf75944ef6f3dbee447e1f841eae16f2f0a72d8ddc"}, + {file = "pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eceb81a8d74f9267ef4081e246ffd6d129da5d87e37a77c9bde550cb04870c1c"}, + {file = "pydantic_core-2.41.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d38548150c39b74aeeb0ce8ee1d8e82696f4a4e16ddc6de7b1d8823f7de4b9b5"}, + {file = "pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c23e27686783f60290e36827f9c626e63154b82b116d7fe9adba1fda36da706c"}, + {file = "pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:482c982f814460eabe1d3bb0adfdc583387bd4691ef00b90575ca0d2b6fe2294"}, + {file = "pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:bfea2a5f0b4d8d43adf9d7b8bf019fb46fdd10a2e5cde477fbcb9d1fa08c68e1"}, + {file = "pydantic_core-2.41.5-cp312-cp312-win32.whl", hash = "sha256:b74557b16e390ec12dca509bce9264c3bbd128f8a2c376eaa68003d7f327276d"}, + {file = "pydantic_core-2.41.5-cp312-cp312-win_amd64.whl", hash = "sha256:1962293292865bca8e54702b08a4f26da73adc83dd1fcf26fbc875b35d81c815"}, + {file = "pydantic_core-2.41.5-cp312-cp312-win_arm64.whl", hash = "sha256:1746d4a3d9a794cacae06a5eaaccb4b8643a131d45fbc9af23e353dc0a5ba5c3"}, + {file = "pydantic_core-2.41.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:941103c9be18ac8daf7b7adca8228f8ed6bb7a1849020f643b3a14d15b1924d9"}, + {file = "pydantic_core-2.41.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:112e305c3314f40c93998e567879e887a3160bb8689ef3d2c04b6cc62c33ac34"}, + {file = "pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cbaad15cb0c90aa221d43c00e77bb33c93e8d36e0bf74760cd00e732d10a6a0"}, + {file = "pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:03ca43e12fab6023fc79d28ca6b39b05f794ad08ec2feccc59a339b02f2b3d33"}, + {file = "pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc799088c08fa04e43144b164feb0c13f9a0bc40503f8df3e9fde58a3c0c101e"}, + {file = "pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:97aeba56665b4c3235a0e52b2c2f5ae9cd071b8a8310ad27bddb3f7fb30e9aa2"}, + {file = "pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:406bf18d345822d6c21366031003612b9c77b3e29ffdb0f612367352aab7d586"}, + {file = "pydantic_core-2.41.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b93590ae81f7010dbe380cdeab6f515902ebcbefe0b9327cc4804d74e93ae69d"}, + {file = "pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:01a3d0ab748ee531f4ea6c3e48ad9dac84ddba4b0d82291f87248f2f9de8d740"}, + {file = "pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:6561e94ba9dacc9c61bce40e2d6bdc3bfaa0259d3ff36ace3b1e6901936d2e3e"}, + {file = "pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:915c3d10f81bec3a74fbd4faebe8391013ba61e5a1a8d48c4455b923bdda7858"}, + {file = "pydantic_core-2.41.5-cp313-cp313-win32.whl", hash = "sha256:650ae77860b45cfa6e2cdafc42618ceafab3a2d9a3811fcfbd3bbf8ac3c40d36"}, + {file = "pydantic_core-2.41.5-cp313-cp313-win_amd64.whl", hash = "sha256:79ec52ec461e99e13791ec6508c722742ad745571f234ea6255bed38c6480f11"}, + {file = "pydantic_core-2.41.5-cp313-cp313-win_arm64.whl", hash = "sha256:3f84d5c1b4ab906093bdc1ff10484838aca54ef08de4afa9de0f5f14d69639cd"}, + {file = "pydantic_core-2.41.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:3f37a19d7ebcdd20b96485056ba9e8b304e27d9904d233d7b1015db320e51f0a"}, + {file = "pydantic_core-2.41.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1d1d9764366c73f996edd17abb6d9d7649a7eb690006ab6adbda117717099b14"}, + {file = "pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25e1c2af0fce638d5f1988b686f3b3ea8cd7de5f244ca147c777769e798a9cd1"}, + {file = "pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:506d766a8727beef16b7adaeb8ee6217c64fc813646b424d0804d67c16eddb66"}, + {file = "pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4819fa52133c9aa3c387b3328f25c1facc356491e6135b459f1de698ff64d869"}, + {file = "pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b761d210c9ea91feda40d25b4efe82a1707da2ef62901466a42492c028553a2"}, + {file = "pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22f0fb8c1c583a3b6f24df2470833b40207e907b90c928cc8d3594b76f874375"}, + {file = "pydantic_core-2.41.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2782c870e99878c634505236d81e5443092fba820f0373997ff75f90f68cd553"}, + {file = "pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:0177272f88ab8312479336e1d777f6b124537d47f2123f89cb37e0accea97f90"}, + {file = "pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:63510af5e38f8955b8ee5687740d6ebf7c2a0886d15a6d65c32814613681bc07"}, + {file = "pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:e56ba91f47764cc14f1daacd723e3e82d1a89d783f0f5afe9c364b8bb491ccdb"}, + {file = "pydantic_core-2.41.5-cp314-cp314-win32.whl", hash = "sha256:aec5cf2fd867b4ff45b9959f8b20ea3993fc93e63c7363fe6851424c8a7e7c23"}, + {file = "pydantic_core-2.41.5-cp314-cp314-win_amd64.whl", hash = "sha256:8e7c86f27c585ef37c35e56a96363ab8de4e549a95512445b85c96d3e2f7c1bf"}, + {file = "pydantic_core-2.41.5-cp314-cp314-win_arm64.whl", hash = "sha256:e672ba74fbc2dc8eea59fb6d4aed6845e6905fc2a8afe93175d94a83ba2a01a0"}, + {file = "pydantic_core-2.41.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8566def80554c3faa0e65ac30ab0932b9e3a5cd7f8323764303d468e5c37595a"}, + {file = "pydantic_core-2.41.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b80aa5095cd3109962a298ce14110ae16b8c1aece8b72f9dafe81cf597ad80b3"}, + {file = "pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3006c3dd9ba34b0c094c544c6006cc79e87d8612999f1a5d43b769b89181f23c"}, + {file = "pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72f6c8b11857a856bcfa48c86f5368439f74453563f951e473514579d44aa612"}, + {file = "pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cb1b2f9742240e4bb26b652a5aeb840aa4b417c7748b6f8387927bc6e45e40d"}, + {file = "pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd3d54f38609ff308209bd43acea66061494157703364ae40c951f83ba99a1a9"}, + {file = "pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ff4321e56e879ee8d2a879501c8e469414d948f4aba74a2d4593184eb326660"}, + {file = "pydantic_core-2.41.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0d2568a8c11bf8225044aa94409e21da0cb09dcdafe9ecd10250b2baad531a9"}, + {file = "pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:a39455728aabd58ceabb03c90e12f71fd30fa69615760a075b9fec596456ccc3"}, + {file = "pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:239edca560d05757817c13dc17c50766136d21f7cd0fac50295499ae24f90fdf"}, + {file = "pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:2a5e06546e19f24c6a96a129142a75cee553cc018ffee48a460059b1185f4470"}, + {file = "pydantic_core-2.41.5-cp314-cp314t-win32.whl", hash = "sha256:b4ececa40ac28afa90871c2cc2b9ffd2ff0bf749380fbdf57d165fd23da353aa"}, + {file = "pydantic_core-2.41.5-cp314-cp314t-win_amd64.whl", hash = "sha256:80aa89cad80b32a912a65332f64a4450ed00966111b6615ca6816153d3585a8c"}, + {file = "pydantic_core-2.41.5-cp314-cp314t-win_arm64.whl", hash = "sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008"}, + {file = "pydantic_core-2.41.5-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:8bfeaf8735be79f225f3fefab7f941c712aaca36f1128c9d7e2352ee1aa87bdf"}, + {file = "pydantic_core-2.41.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:346285d28e4c8017da95144c7f3acd42740d637ff41946af5ce6e5e420502dd5"}, + {file = "pydantic_core-2.41.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a75dafbf87d6276ddc5b2bf6fae5254e3d0876b626eb24969a574fff9149ee5d"}, + {file = "pydantic_core-2.41.5-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7b93a4d08587e2b7e7882de461e82b6ed76d9026ce91ca7915e740ecc7855f60"}, + {file = "pydantic_core-2.41.5-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e8465ab91a4bd96d36dde3263f06caa6a8a6019e4113f24dc753d79a8b3a3f82"}, + {file = "pydantic_core-2.41.5-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:299e0a22e7ae2b85c1a57f104538b2656e8ab1873511fd718a1c1c6f149b77b5"}, + {file = "pydantic_core-2.41.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:707625ef0983fcfb461acfaf14de2067c5942c6bb0f3b4c99158bed6fedd3cf3"}, + {file = "pydantic_core-2.41.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f41eb9797986d6ebac5e8edff36d5cef9de40def462311b3eb3eeded1431e425"}, + {file = "pydantic_core-2.41.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0384e2e1021894b1ff5a786dbf94771e2986ebe2869533874d7e43bc79c6f504"}, + {file = "pydantic_core-2.41.5-cp39-cp39-musllinux_1_1_armv7l.whl", hash = "sha256:f0cd744688278965817fd0839c4a4116add48d23890d468bc436f78beb28abf5"}, + {file = "pydantic_core-2.41.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:753e230374206729bf0a807954bcc6c150d3743928a73faffee51ac6557a03c3"}, + {file = "pydantic_core-2.41.5-cp39-cp39-win32.whl", hash = "sha256:873e0d5b4fb9b89ef7c2d2a963ea7d02879d9da0da8d9d4933dee8ee86a8b460"}, + {file = "pydantic_core-2.41.5-cp39-cp39-win_amd64.whl", hash = "sha256:e4f4a984405e91527a0d62649ee21138f8e3d0ef103be488c1dc11a80d7f184b"}, + {file = "pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:b96d5f26b05d03cc60f11a7761a5ded1741da411e7fe0909e27a5e6a0cb7b034"}, + {file = "pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:634e8609e89ceecea15e2d61bc9ac3718caaaa71963717bf3c8f38bfde64242c"}, + {file = "pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:93e8740d7503eb008aa2df04d3b9735f845d43ae845e6dcd2be0b55a2da43cd2"}, + {file = "pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f15489ba13d61f670dcc96772e733aad1a6f9c429cc27574c6cdaed82d0146ad"}, + {file = "pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:7da7087d756b19037bc2c06edc6c170eeef3c3bafcb8f532ff17d64dc427adfd"}, + {file = "pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:aabf5777b5c8ca26f7824cb4a120a740c9588ed58df9b2d196ce92fba42ff8dc"}, + {file = "pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c007fe8a43d43b3969e8469004e9845944f1a80e6acd47c150856bb87f230c56"}, + {file = "pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76d0819de158cd855d1cbb8fcafdf6f5cf1eb8e470abe056d5d161106e38062b"}, + {file = "pydantic_core-2.41.5-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b5819cd790dbf0c5eb9f82c73c16b39a65dd6dd4d1439dcdea7816ec9adddab8"}, + {file = "pydantic_core-2.41.5-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:5a4e67afbc95fa5c34cf27d9089bca7fcab4e51e57278d710320a70b956d1b9a"}, + {file = "pydantic_core-2.41.5-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ece5c59f0ce7d001e017643d8d24da587ea1f74f6993467d85ae8a5ef9d4f42b"}, + {file = "pydantic_core-2.41.5-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:16f80f7abe3351f8ea6858914ddc8c77e02578544a0ebc15b4c2e1a0e813b0b2"}, + {file = "pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:33cb885e759a705b426baada1fe68cbb0a2e68e34c5d0d0289a364cf01709093"}, + {file = "pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:c8d8b4eb992936023be7dee581270af5c6e0697a8559895f527f5b7105ecd36a"}, + {file = "pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:242a206cd0318f95cd21bdacff3fcc3aab23e79bba5cac3db5a841c9ef9c6963"}, + {file = "pydantic_core-2.41.5-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d3a978c4f57a597908b7e697229d996d77a6d3c94901e9edee593adada95ce1a"}, + {file = "pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b2379fa7ed44ddecb5bfe4e48577d752db9fc10be00a6b7446e9663ba143de26"}, + {file = "pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:266fb4cbf5e3cbd0b53669a6d1b039c45e3ce651fd5442eff4d07c2cc8d66808"}, + {file = "pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58133647260ea01e4d0500089a8c4f07bd7aa6ce109682b1426394988d8aaacc"}, + {file = "pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:287dad91cfb551c363dc62899a80e9e14da1f0e2b6ebde82c806612ca2a13ef1"}, + {file = "pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:03b77d184b9eb40240ae9fd676ca364ce1085f203e1b1256f8ab9984dca80a84"}, + {file = "pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:a668ce24de96165bb239160b3d854943128f4334822900534f2fe947930e5770"}, + {file = "pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f14f8f046c14563f8eb3f45f499cc658ab8d10072961e07225e507adb700e93f"}, + {file = "pydantic_core-2.41.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:56121965f7a4dc965bff783d70b907ddf3d57f6eba29b6d2e5dabfaf07799c51"}, + {file = "pydantic_core-2.41.5.tar.gz", hash = "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e"}, +] + +[package.dependencies] +typing-extensions = ">=4.14.1" + +[[package]] +name = "pygments" +version = "2.19.2" +description = "Pygments is a syntax highlighting package written in Python." +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b"}, + {file = "pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887"}, +] + +[package.extras] +windows-terminal = ["colorama (>=0.4.6)"] + +[[package]] +name = "pytest" +version = "9.0.2" +description = "pytest: simple powerful testing with Python" +optional = false +python-versions = ">=3.10" +groups = ["dev"] +files = [ + {file = "pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b"}, + {file = "pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11"}, +] + +[package.dependencies] +colorama = {version = ">=0.4", markers = "sys_platform == \"win32\""} +exceptiongroup = {version = ">=1", markers = "python_version < \"3.11\""} +iniconfig = ">=1.0.1" +packaging = ">=22" +pluggy = ">=1.5,<2" +pygments = ">=2.7.2" +tomli = {version = ">=1", markers = "python_version < \"3.11\""} + +[package.extras] +dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "requests", "setuptools", "xmlschema"] + +[[package]] +name = "pytest-asyncio" +version = "1.3.0" +description = "Pytest support for asyncio" +optional = false +python-versions = ">=3.10" +groups = ["dev"] +files = [ + {file = "pytest_asyncio-1.3.0-py3-none-any.whl", hash = "sha256:611e26147c7f77640e6d0a92a38ed17c3e9848063698d5c93d5aa7aa11cebff5"}, + {file = "pytest_asyncio-1.3.0.tar.gz", hash = "sha256:d7f52f36d231b80ee124cd216ffb19369aa168fc10095013c6b014a34d3ee9e5"}, +] + +[package.dependencies] +backports-asyncio-runner = {version = ">=1.1,<2", markers = "python_version < \"3.11\""} +pytest = ">=8.2,<10" +typing-extensions = {version = ">=4.12", markers = "python_version < \"3.13\""} + +[package.extras] +docs = ["sphinx (>=5.3)", "sphinx-rtd-theme (>=1)"] +testing = ["coverage (>=6.2)", "hypothesis (>=5.7.1)"] + +[[package]] +name = "pytest-cov" +version = "7.0.0" +description = "Pytest plugin for measuring coverage." +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "pytest_cov-7.0.0-py3-none-any.whl", hash = "sha256:3b8e9558b16cc1479da72058bdecf8073661c7f57f7d3c5f22a1c23507f2d861"}, + {file = "pytest_cov-7.0.0.tar.gz", hash = "sha256:33c97eda2e049a0c5298e91f519302a1334c26ac65c1a483d6206fd458361af1"}, +] + +[package.dependencies] +coverage = {version = ">=7.10.6", extras = ["toml"]} +pluggy = ">=1.2" +pytest = ">=7" + +[package.extras] +testing = ["process-tests", "pytest-xdist", "virtualenv"] + +[[package]] +name = "pytokens" +version = "0.4.1" +description = "A Fast, spec compliant Python 3.14+ tokenizer that runs on older Pythons." +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "pytokens-0.4.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2a44ed93ea23415c54f3face3b65ef2b844d96aeb3455b8a69b3df6beab6acc5"}, + {file = "pytokens-0.4.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:add8bf86b71a5d9fb5b89f023a80b791e04fba57960aa790cc6125f7f1d39dfe"}, + {file = "pytokens-0.4.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:670d286910b531c7b7e3c0b453fd8156f250adb140146d234a82219459b9640c"}, + {file = "pytokens-0.4.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:4e691d7f5186bd2842c14813f79f8884bb03f5995f0575272009982c5ac6c0f7"}, + {file = "pytokens-0.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:27b83ad28825978742beef057bfe406ad6ed524b2d28c252c5de7b4a6dd48fa2"}, + {file = "pytokens-0.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d70e77c55ae8380c91c0c18dea05951482e263982911fc7410b1ffd1dadd3440"}, + {file = "pytokens-0.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4a58d057208cb9075c144950d789511220b07636dd2e4708d5645d24de666bdc"}, + {file = "pytokens-0.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b49750419d300e2b5a3813cf229d4e5a4c728dae470bcc89867a9ad6f25a722d"}, + {file = "pytokens-0.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d9907d61f15bf7261d7e775bd5d7ee4d2930e04424bab1972591918497623a16"}, + {file = "pytokens-0.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:ee44d0f85b803321710f9239f335aafe16553b39106384cef8e6de40cb4ef2f6"}, + {file = "pytokens-0.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:140709331e846b728475786df8aeb27d24f48cbcf7bcd449f8de75cae7a45083"}, + {file = "pytokens-0.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6d6c4268598f762bc8e91f5dbf2ab2f61f7b95bdc07953b602db879b3c8c18e1"}, + {file = "pytokens-0.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:24afde1f53d95348b5a0eb19488661147285ca4dd7ed752bbc3e1c6242a304d1"}, + {file = "pytokens-0.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5ad948d085ed6c16413eb5fec6b3e02fa00dc29a2534f088d3302c47eb59adf9"}, + {file = "pytokens-0.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:3f901fe783e06e48e8cbdc82d631fca8f118333798193e026a50ce1b3757ea68"}, + {file = "pytokens-0.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8bdb9d0ce90cbf99c525e75a2fa415144fd570a1ba987380190e8b786bc6ef9b"}, + {file = "pytokens-0.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5502408cab1cb18e128570f8d598981c68a50d0cbd7c61312a90507cd3a1276f"}, + {file = "pytokens-0.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:29d1d8fb1030af4d231789959f21821ab6325e463f0503a61d204343c9b355d1"}, + {file = "pytokens-0.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:970b08dd6b86058b6dc07efe9e98414f5102974716232d10f32ff39701e841c4"}, + {file = "pytokens-0.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:9bd7d7f544d362576be74f9d5901a22f317efc20046efe2034dced238cbbfe78"}, + {file = "pytokens-0.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4a14d5f5fc78ce85e426aa159489e2d5961acf0e47575e08f35584009178e321"}, + {file = "pytokens-0.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97f50fd18543be72da51dd505e2ed20d2228c74e0464e4262e4899797803d7fa"}, + {file = "pytokens-0.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dc74c035f9bfca0255c1af77ddd2d6ae8419012805453e4b0e7513e17904545d"}, + {file = "pytokens-0.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f66a6bbe741bd431f6d741e617e0f39ec7257ca1f89089593479347cc4d13324"}, + {file = "pytokens-0.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:b35d7e5ad269804f6697727702da3c517bb8a5228afa450ab0fa787732055fc9"}, + {file = "pytokens-0.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:8fcb9ba3709ff77e77f1c7022ff11d13553f3c30299a9fe246a166903e9091eb"}, + {file = "pytokens-0.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:79fc6b8699564e1f9b521582c35435f1bd32dd06822322ec44afdeba666d8cb3"}, + {file = "pytokens-0.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d31b97b3de0f61571a124a00ffe9a81fb9939146c122c11060725bd5aea79975"}, + {file = "pytokens-0.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:967cf6e3fd4adf7de8fc73cd3043754ae79c36475c1c11d514fc72cf5490094a"}, + {file = "pytokens-0.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:584c80c24b078eec1e227079d56dc22ff755e0ba8654d8383b2c549107528918"}, + {file = "pytokens-0.4.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:da5baeaf7116dced9c6bb76dc31ba04a2dc3695f3d9f74741d7910122b456edc"}, + {file = "pytokens-0.4.1-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:11edda0942da80ff58c4408407616a310adecae1ddd22eef8c692fe266fa5009"}, + {file = "pytokens-0.4.1-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0fc71786e629cef478cbf29d7ea1923299181d0699dbe7c3c0f4a583811d9fc1"}, + {file = "pytokens-0.4.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:dcafc12c30dbaf1e2af0490978352e0c4041a7cde31f4f81435c2a5e8b9cabb6"}, + {file = "pytokens-0.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:42f144f3aafa5d92bad964d471a581651e28b24434d184871bd02e3a0d956037"}, + {file = "pytokens-0.4.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:34bcc734bd2f2d5fe3b34e7b3c0116bfb2397f2d9666139988e7a3eb5f7400e3"}, + {file = "pytokens-0.4.1-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:941d4343bf27b605e9213b26bfa1c4bf197c9c599a9627eb7305b0defcfe40c1"}, + {file = "pytokens-0.4.1-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3ad72b851e781478366288743198101e5eb34a414f1d5627cdd585ca3b25f1db"}, + {file = "pytokens-0.4.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:682fa37ff4d8e95f7df6fe6fe6a431e8ed8e788023c6bcc0f0880a12eab80ad1"}, + {file = "pytokens-0.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:30f51edd9bb7f85c748979384165601d028b84f7bd13fe14d3e065304093916a"}, + {file = "pytokens-0.4.1-py3-none-any.whl", hash = "sha256:26cef14744a8385f35d0e095dc8b3a7583f6c953c2e3d269c7f82484bf5ad2de"}, + {file = "pytokens-0.4.1.tar.gz", hash = "sha256:292052fe80923aae2260c073f822ceba21f3872ced9a68bb7953b348e561179a"}, +] + +[package.extras] +dev = ["black", "build", "mypy", "pytest", "pytest-cov", "setuptools", "tox", "twine", "wheel"] + +[[package]] +name = "pyunormalize" +version = "17.0.0" +description = "A library for Unicode normalization (NFC, NFD, NFKC, NFKD) independent of Python's core Unicode database." +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "pyunormalize-17.0.0-py3-none-any.whl", hash = "sha256:f0d93b076f938db2b26d319d04f2b58505d1cd7a80b5b72badbe7d1aa4d2a31c"}, + {file = "pyunormalize-17.0.0.tar.gz", hash = "sha256:0949a3e56817e287febcaf1b0cc4b5adf0bb107628d379335938040947eec792"}, +] + +[[package]] +name = "pywin32" +version = "311" +description = "Python for Window Extensions" +optional = false +python-versions = "*" +groups = ["main"] +markers = "platform_system == \"Windows\"" +files = [ + {file = "pywin32-311-cp310-cp310-win32.whl", hash = "sha256:d03ff496d2a0cd4a5893504789d4a15399133fe82517455e78bad62efbb7f0a3"}, + {file = "pywin32-311-cp310-cp310-win_amd64.whl", hash = "sha256:797c2772017851984b97180b0bebe4b620bb86328e8a884bb626156295a63b3b"}, + {file = "pywin32-311-cp310-cp310-win_arm64.whl", hash = "sha256:0502d1facf1fed4839a9a51ccbcc63d952cf318f78ffc00a7e78528ac27d7a2b"}, + {file = "pywin32-311-cp311-cp311-win32.whl", hash = "sha256:184eb5e436dea364dcd3d2316d577d625c0351bf237c4e9a5fabbcfa5a58b151"}, + {file = "pywin32-311-cp311-cp311-win_amd64.whl", hash = "sha256:3ce80b34b22b17ccbd937a6e78e7225d80c52f5ab9940fe0506a1a16f3dab503"}, + {file = "pywin32-311-cp311-cp311-win_arm64.whl", hash = "sha256:a733f1388e1a842abb67ffa8e7aad0e70ac519e09b0f6a784e65a136ec7cefd2"}, + {file = "pywin32-311-cp312-cp312-win32.whl", hash = "sha256:750ec6e621af2b948540032557b10a2d43b0cee2ae9758c54154d711cc852d31"}, + {file = "pywin32-311-cp312-cp312-win_amd64.whl", hash = "sha256:b8c095edad5c211ff31c05223658e71bf7116daa0ecf3ad85f3201ea3190d067"}, + {file = "pywin32-311-cp312-cp312-win_arm64.whl", hash = "sha256:e286f46a9a39c4a18b319c28f59b61de793654af2f395c102b4f819e584b5852"}, + {file = "pywin32-311-cp313-cp313-win32.whl", hash = "sha256:f95ba5a847cba10dd8c4d8fefa9f2a6cf283b8b88ed6178fa8a6c1ab16054d0d"}, + {file = "pywin32-311-cp313-cp313-win_amd64.whl", hash = "sha256:718a38f7e5b058e76aee1c56ddd06908116d35147e133427e59a3983f703a20d"}, + {file = "pywin32-311-cp313-cp313-win_arm64.whl", hash = "sha256:7b4075d959648406202d92a2310cb990fea19b535c7f4a78d3f5e10b926eeb8a"}, + {file = "pywin32-311-cp314-cp314-win32.whl", hash = "sha256:b7a2c10b93f8986666d0c803ee19b5990885872a7de910fc460f9b0c2fbf92ee"}, + {file = "pywin32-311-cp314-cp314-win_amd64.whl", hash = "sha256:3aca44c046bd2ed8c90de9cb8427f581c479e594e99b5c0bb19b29c10fd6cb87"}, + {file = "pywin32-311-cp314-cp314-win_arm64.whl", hash = "sha256:a508e2d9025764a8270f93111a970e1d0fbfc33f4153b388bb649b7eec4f9b42"}, + {file = "pywin32-311-cp38-cp38-win32.whl", hash = "sha256:6c6f2969607b5023b0d9ce2541f8d2cbb01c4f46bc87456017cf63b73f1e2d8c"}, + {file = "pywin32-311-cp38-cp38-win_amd64.whl", hash = "sha256:c8015b09fb9a5e188f83b7b04de91ddca4658cee2ae6f3bc483f0b21a77ef6cd"}, + {file = "pywin32-311-cp39-cp39-win32.whl", hash = "sha256:aba8f82d551a942cb20d4a83413ccbac30790b50efb89a75e4f586ac0bb8056b"}, + {file = "pywin32-311-cp39-cp39-win_amd64.whl", hash = "sha256:e0c4cfb0621281fe40387df582097fd796e80430597cb9944f0ae70447bacd91"}, + {file = "pywin32-311-cp39-cp39-win_arm64.whl", hash = "sha256:62ea666235135fee79bb154e695f3ff67370afefd71bd7fea7512fc70ef31e3d"}, +] + +[[package]] +name = "regex" +version = "2026.1.15" +description = "Alternative regular expression module, to replace re." +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "regex-2026.1.15-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:4e3dd93c8f9abe8aa4b6c652016da9a3afa190df5ad822907efe6b206c09896e"}, + {file = "regex-2026.1.15-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:97499ff7862e868b1977107873dd1a06e151467129159a6ffd07b66706ba3a9f"}, + {file = "regex-2026.1.15-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0bda75ebcac38d884240914c6c43d8ab5fb82e74cde6da94b43b17c411aa4c2b"}, + {file = "regex-2026.1.15-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7dcc02368585334f5bc81fc73a2a6a0bbade60e7d83da21cead622faf408f32c"}, + {file = "regex-2026.1.15-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:693b465171707bbe882a7a05de5e866f33c76aa449750bee94a8d90463533cc9"}, + {file = "regex-2026.1.15-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b0d190e6f013ea938623a58706d1469a62103fb2a241ce2873a9906e0386582c"}, + {file = "regex-2026.1.15-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5ff818702440a5878a81886f127b80127f5d50563753a28211482867f8318106"}, + {file = "regex-2026.1.15-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f052d1be37ef35a54e394de66136e30fa1191fab64f71fc06ac7bc98c9a84618"}, + {file = "regex-2026.1.15-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6bfc31a37fd1592f0c4fc4bfc674b5c42e52efe45b4b7a6a14f334cca4bcebe4"}, + {file = "regex-2026.1.15-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3d6ce5ae80066b319ae3bc62fd55a557c9491baa5efd0d355f0de08c4ba54e79"}, + {file = "regex-2026.1.15-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:1704d204bd42b6bb80167df0e4554f35c255b579ba99616def38f69e14a5ccb9"}, + {file = "regex-2026.1.15-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:e3174a5ed4171570dc8318afada56373aa9289eb6dc0d96cceb48e7358b0e220"}, + {file = "regex-2026.1.15-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:87adf5bd6d72e3e17c9cb59ac4096b1faaf84b7eb3037a5ffa61c4b4370f0f13"}, + {file = "regex-2026.1.15-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e85dc94595f4d766bd7d872a9de5ede1ca8d3063f3bdf1e2c725f5eb411159e3"}, + {file = "regex-2026.1.15-cp310-cp310-win32.whl", hash = "sha256:21ca32c28c30d5d65fc9886ff576fc9b59bbca08933e844fa2363e530f4c8218"}, + {file = "regex-2026.1.15-cp310-cp310-win_amd64.whl", hash = "sha256:3038a62fc7d6e5547b8915a3d927a0fbeef84cdbe0b1deb8c99bbd4a8961b52a"}, + {file = "regex-2026.1.15-cp310-cp310-win_arm64.whl", hash = "sha256:505831646c945e3e63552cc1b1b9b514f0e93232972a2d5bedbcc32f15bc82e3"}, + {file = "regex-2026.1.15-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:1ae6020fb311f68d753b7efa9d4b9a5d47a5d6466ea0d5e3b5a471a960ea6e4a"}, + {file = "regex-2026.1.15-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:eddf73f41225942c1f994914742afa53dc0d01a6e20fe14b878a1b1edc74151f"}, + {file = "regex-2026.1.15-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e8cd52557603f5c66a548f69421310886b28b7066853089e1a71ee710e1cdc1"}, + {file = "regex-2026.1.15-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5170907244b14303edc5978f522f16c974f32d3aa92109fabc2af52411c9433b"}, + {file = "regex-2026.1.15-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2748c1ec0663580b4510bd89941a31560b4b439a0b428b49472a3d9944d11cd8"}, + {file = "regex-2026.1.15-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2f2775843ca49360508d080eaa87f94fa248e2c946bbcd963bb3aae14f333413"}, + {file = "regex-2026.1.15-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d9ea2604370efc9a174c1b5dcc81784fb040044232150f7f33756049edfc9026"}, + {file = "regex-2026.1.15-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0dcd31594264029b57bf16f37fd7248a70b3b764ed9e0839a8f271b2d22c0785"}, + {file = "regex-2026.1.15-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c08c1f3e34338256732bd6938747daa3c0d5b251e04b6e43b5813e94d503076e"}, + {file = "regex-2026.1.15-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e43a55f378df1e7a4fa3547c88d9a5a9b7113f653a66821bcea4718fe6c58763"}, + {file = "regex-2026.1.15-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:f82110ab962a541737bd0ce87978d4c658f06e7591ba899192e2712a517badbb"}, + {file = "regex-2026.1.15-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:27618391db7bdaf87ac6c92b31e8f0dfb83a9de0075855152b720140bda177a2"}, + {file = "regex-2026.1.15-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bfb0d6be01fbae8d6655c8ca21b3b72458606c4aec9bbc932db758d47aba6db1"}, + {file = "regex-2026.1.15-cp311-cp311-win32.whl", hash = "sha256:b10e42a6de0e32559a92f2f8dc908478cc0fa02838d7dbe764c44dca3fa13569"}, + {file = "regex-2026.1.15-cp311-cp311-win_amd64.whl", hash = "sha256:e9bf3f0bbdb56633c07d7116ae60a576f846efdd86a8848f8d62b749e1209ca7"}, + {file = "regex-2026.1.15-cp311-cp311-win_arm64.whl", hash = "sha256:41aef6f953283291c4e4e6850607bd71502be67779586a61472beacb315c97ec"}, + {file = "regex-2026.1.15-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:4c8fcc5793dde01641a35905d6731ee1548f02b956815f8f1cab89e515a5bdf1"}, + {file = "regex-2026.1.15-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:bfd876041a956e6a90ad7cdb3f6a630c07d491280bfeed4544053cd434901681"}, + {file = "regex-2026.1.15-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9250d087bc92b7d4899ccd5539a1b2334e44eee85d848c4c1aef8e221d3f8c8f"}, + {file = "regex-2026.1.15-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c8a154cf6537ebbc110e24dabe53095e714245c272da9c1be05734bdad4a61aa"}, + {file = "regex-2026.1.15-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8050ba2e3ea1d8731a549e83c18d2f0999fbc99a5f6bd06b4c91449f55291804"}, + {file = "regex-2026.1.15-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0bf065240704cb8951cc04972cf107063917022511273e0969bdb34fc173456c"}, + {file = "regex-2026.1.15-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c32bef3e7aeee75746748643667668ef941d28b003bfc89994ecf09a10f7a1b5"}, + {file = "regex-2026.1.15-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d5eaa4a4c5b1906bd0d2508d68927f15b81821f85092e06f1a34a4254b0e1af3"}, + {file = "regex-2026.1.15-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:86c1077a3cc60d453d4084d5b9649065f3bf1184e22992bd322e1f081d3117fb"}, + {file = "regex-2026.1.15-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:2b091aefc05c78d286657cd4db95f2e6313375ff65dcf085e42e4c04d9c8d410"}, + {file = "regex-2026.1.15-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:57e7d17f59f9ebfa9667e6e5a1c0127b96b87cb9cede8335482451ed00788ba4"}, + {file = "regex-2026.1.15-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:c6c4dcdfff2c08509faa15d36ba7e5ef5fcfab25f1e8f85a0c8f45bc3a30725d"}, + {file = "regex-2026.1.15-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:cf8ff04c642716a7f2048713ddc6278c5fd41faa3b9cab12607c7abecd012c22"}, + {file = "regex-2026.1.15-cp312-cp312-win32.whl", hash = "sha256:82345326b1d8d56afbe41d881fdf62f1926d7264b2fc1537f99ae5da9aad7913"}, + {file = "regex-2026.1.15-cp312-cp312-win_amd64.whl", hash = "sha256:4def140aa6156bc64ee9912383d4038f3fdd18fee03a6f222abd4de6357ce42a"}, + {file = "regex-2026.1.15-cp312-cp312-win_arm64.whl", hash = "sha256:c6c565d9a6e1a8d783c1948937ffc377dd5771e83bd56de8317c450a954d2056"}, + {file = "regex-2026.1.15-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e69d0deeb977ffe7ed3d2e4439360089f9c3f217ada608f0f88ebd67afb6385e"}, + {file = "regex-2026.1.15-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:3601ffb5375de85a16f407854d11cca8fe3f5febbe3ac78fb2866bb220c74d10"}, + {file = "regex-2026.1.15-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4c5ef43b5c2d4114eb8ea424bb8c9cec01d5d17f242af88b2448f5ee81caadbc"}, + {file = "regex-2026.1.15-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:968c14d4f03e10b2fd960f1d5168c1f0ac969381d3c1fcc973bc45fb06346599"}, + {file = "regex-2026.1.15-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:56a5595d0f892f214609c9f76b41b7428bed439d98dc961efafdd1354d42baae"}, + {file = "regex-2026.1.15-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0bf650f26087363434c4e560011f8e4e738f6f3e029b85d4904c50135b86cfa5"}, + {file = "regex-2026.1.15-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:18388a62989c72ac24de75f1449d0fb0b04dfccd0a1a7c1c43af5eb503d890f6"}, + {file = "regex-2026.1.15-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6d220a2517f5893f55daac983bfa9fe998a7dbcaee4f5d27a88500f8b7873788"}, + {file = "regex-2026.1.15-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c9c08c2fbc6120e70abff5d7f28ffb4d969e14294fb2143b4b5c7d20e46d1714"}, + {file = "regex-2026.1.15-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:7ef7d5d4bd49ec7364315167a4134a015f61e8266c6d446fc116a9ac4456e10d"}, + {file = "regex-2026.1.15-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:6e42844ad64194fa08d5ccb75fe6a459b9b08e6d7296bd704460168d58a388f3"}, + {file = "regex-2026.1.15-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:cfecdaa4b19f9ca534746eb3b55a5195d5c95b88cac32a205e981ec0a22b7d31"}, + {file = "regex-2026.1.15-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:08df9722d9b87834a3d701f3fca570b2be115654dbfd30179f30ab2f39d606d3"}, + {file = "regex-2026.1.15-cp313-cp313-win32.whl", hash = "sha256:d426616dae0967ca225ab12c22274eb816558f2f99ccb4a1d52ca92e8baf180f"}, + {file = "regex-2026.1.15-cp313-cp313-win_amd64.whl", hash = "sha256:febd38857b09867d3ed3f4f1af7d241c5c50362e25ef43034995b77a50df494e"}, + {file = "regex-2026.1.15-cp313-cp313-win_arm64.whl", hash = "sha256:8e32f7896f83774f91499d239e24cebfadbc07639c1494bb7213983842348337"}, + {file = "regex-2026.1.15-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:ec94c04149b6a7b8120f9f44565722c7ae31b7a6d2275569d2eefa76b83da3be"}, + {file = "regex-2026.1.15-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:40c86d8046915bb9aeb15d3f3f15b6fd500b8ea4485b30e1bbc799dab3fe29f8"}, + {file = "regex-2026.1.15-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:726ea4e727aba21643205edad8f2187ec682d3305d790f73b7a51c7587b64bdd"}, + {file = "regex-2026.1.15-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1cb740d044aff31898804e7bf1181cc72c03d11dfd19932b9911ffc19a79070a"}, + {file = "regex-2026.1.15-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:05d75a668e9ea16f832390d22131fe1e8acc8389a694c8febc3e340b0f810b93"}, + {file = "regex-2026.1.15-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d991483606f3dbec93287b9f35596f41aa2e92b7c2ebbb935b63f409e243c9af"}, + {file = "regex-2026.1.15-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:194312a14819d3e44628a44ed6fea6898fdbecb0550089d84c403475138d0a09"}, + {file = "regex-2026.1.15-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fe2fda4110a3d0bc163c2e0664be44657431440722c5c5315c65155cab92f9e5"}, + {file = "regex-2026.1.15-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:124dc36c85d34ef2d9164da41a53c1c8c122cfb1f6e1ec377a1f27ee81deb794"}, + {file = "regex-2026.1.15-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:a1774cd1981cd212506a23a14dba7fdeaee259f5deba2df6229966d9911e767a"}, + {file = "regex-2026.1.15-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:b5f7d8d2867152cdb625e72a530d2ccb48a3d199159144cbdd63870882fb6f80"}, + {file = "regex-2026.1.15-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:492534a0ab925d1db998defc3c302dae3616a2fc3fe2e08db1472348f096ddf2"}, + {file = "regex-2026.1.15-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c661fc820cfb33e166bf2450d3dadbda47c8d8981898adb9b6fe24e5e582ba60"}, + {file = "regex-2026.1.15-cp313-cp313t-win32.whl", hash = "sha256:99ad739c3686085e614bf77a508e26954ff1b8f14da0e3765ff7abbf7799f952"}, + {file = "regex-2026.1.15-cp313-cp313t-win_amd64.whl", hash = "sha256:32655d17905e7ff8ba5c764c43cb124e34a9245e45b83c22e81041e1071aee10"}, + {file = "regex-2026.1.15-cp313-cp313t-win_arm64.whl", hash = "sha256:b2a13dd6a95e95a489ca242319d18fc02e07ceb28fa9ad146385194d95b3c829"}, + {file = "regex-2026.1.15-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:d920392a6b1f353f4aa54328c867fec3320fa50657e25f64abf17af054fc97ac"}, + {file = "regex-2026.1.15-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:b5a28980a926fa810dbbed059547b02783952e2efd9c636412345232ddb87ff6"}, + {file = "regex-2026.1.15-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:621f73a07595d83f28952d7bd1e91e9d1ed7625fb7af0064d3516674ec93a2a2"}, + {file = "regex-2026.1.15-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3d7d92495f47567a9b1669c51fc8d6d809821849063d168121ef801bbc213846"}, + {file = "regex-2026.1.15-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8dd16fba2758db7a3780a051f245539c4451ca20910f5a5e6ea1c08d06d4a76b"}, + {file = "regex-2026.1.15-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:1e1808471fbe44c1a63e5f577a1d5f02fe5d66031dcbdf12f093ffc1305a858e"}, + {file = "regex-2026.1.15-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0751a26ad39d4f2ade8fe16c59b2bf5cb19eb3d2cd543e709e583d559bd9efde"}, + {file = "regex-2026.1.15-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0f0c7684c7f9ca241344ff95a1de964f257a5251968484270e91c25a755532c5"}, + {file = "regex-2026.1.15-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:74f45d170a21df41508cb67165456538425185baaf686281fa210d7e729abc34"}, + {file = "regex-2026.1.15-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:f1862739a1ffb50615c0fde6bae6569b5efbe08d98e59ce009f68a336f64da75"}, + {file = "regex-2026.1.15-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:453078802f1b9e2b7303fb79222c054cb18e76f7bdc220f7530fdc85d319f99e"}, + {file = "regex-2026.1.15-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:a30a68e89e5a218b8b23a52292924c1f4b245cb0c68d1cce9aec9bbda6e2c160"}, + {file = "regex-2026.1.15-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:9479cae874c81bf610d72b85bb681a94c95722c127b55445285fb0e2c82db8e1"}, + {file = "regex-2026.1.15-cp314-cp314-win32.whl", hash = "sha256:d639a750223132afbfb8f429c60d9d318aeba03281a5f1ab49f877456448dcf1"}, + {file = "regex-2026.1.15-cp314-cp314-win_amd64.whl", hash = "sha256:4161d87f85fa831e31469bfd82c186923070fc970b9de75339b68f0c75b51903"}, + {file = "regex-2026.1.15-cp314-cp314-win_arm64.whl", hash = "sha256:91c5036ebb62663a6b3999bdd2e559fd8456d17e2b485bf509784cd31a8b1705"}, + {file = "regex-2026.1.15-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:ee6854c9000a10938c79238de2379bea30c82e4925a371711af45387df35cab8"}, + {file = "regex-2026.1.15-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2c2b80399a422348ce5de4fe40c418d6299a0fa2803dd61dc0b1a2f28e280fcf"}, + {file = "regex-2026.1.15-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:dca3582bca82596609959ac39e12b7dad98385b4fefccb1151b937383cec547d"}, + {file = "regex-2026.1.15-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ef71d476caa6692eea743ae5ea23cde3260677f70122c4d258ca952e5c2d4e84"}, + {file = "regex-2026.1.15-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c243da3436354f4af6c3058a3f81a97d47ea52c9bd874b52fd30274853a1d5df"}, + {file = "regex-2026.1.15-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8355ad842a7c7e9e5e55653eade3b7d1885ba86f124dd8ab1f722f9be6627434"}, + {file = "regex-2026.1.15-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f192a831d9575271a22d804ff1a5355355723f94f31d9eef25f0d45a152fdc1a"}, + {file = "regex-2026.1.15-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:166551807ec20d47ceaeec380081f843e88c8949780cd42c40f18d16168bed10"}, + {file = "regex-2026.1.15-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:f9ca1cbdc0fbfe5e6e6f8221ef2309988db5bcede52443aeaee9a4ad555e0dac"}, + {file = "regex-2026.1.15-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:b30bcbd1e1221783c721483953d9e4f3ab9c5d165aa709693d3f3946747b1aea"}, + {file = "regex-2026.1.15-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:2a8d7b50c34578d0d3bf7ad58cde9652b7d683691876f83aedc002862a35dc5e"}, + {file = "regex-2026.1.15-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:9d787e3310c6a6425eb346be4ff2ccf6eece63017916fd77fe8328c57be83521"}, + {file = "regex-2026.1.15-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:619843841e220adca114118533a574a9cd183ed8a28b85627d2844c500a2b0db"}, + {file = "regex-2026.1.15-cp314-cp314t-win32.whl", hash = "sha256:e90b8db97f6f2c97eb045b51a6b2c5ed69cedd8392459e0642d4199b94fabd7e"}, + {file = "regex-2026.1.15-cp314-cp314t-win_amd64.whl", hash = "sha256:5ef19071f4ac9f0834793af85bd04a920b4407715624e40cb7a0631a11137cdf"}, + {file = "regex-2026.1.15-cp314-cp314t-win_arm64.whl", hash = "sha256:ca89c5e596fc05b015f27561b3793dc2fa0917ea0d7507eebb448efd35274a70"}, + {file = "regex-2026.1.15-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:55b4ea996a8e4458dd7b584a2f89863b1655dd3d17b88b46cbb9becc495a0ec5"}, + {file = "regex-2026.1.15-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7e1e28be779884189cdd57735e997f282b64fd7ccf6e2eef3e16e57d7a34a815"}, + {file = "regex-2026.1.15-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0057de9eaef45783ff69fa94ae9f0fd906d629d0bd4c3217048f46d1daa32e9b"}, + {file = "regex-2026.1.15-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cc7cd0b2be0f0269283a45c0d8b2c35e149d1319dcb4a43c9c3689fa935c1ee6"}, + {file = "regex-2026.1.15-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8db052bbd981e1666f09e957f3790ed74080c2229007c1dd67afdbf0b469c48b"}, + {file = "regex-2026.1.15-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:343db82cb3712c31ddf720f097ef17c11dab2f67f7a3e7be976c4f82eba4e6df"}, + {file = "regex-2026.1.15-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:55e9d0118d97794367309635df398bdfd7c33b93e2fdfa0b239661cd74b4c14e"}, + {file = "regex-2026.1.15-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:008b185f235acd1e53787333e5690082e4f156c44c87d894f880056089e9bc7c"}, + {file = "regex-2026.1.15-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fd65af65e2aaf9474e468f9e571bd7b189e1df3a61caa59dcbabd0000e4ea839"}, + {file = "regex-2026.1.15-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:f42e68301ff4afee63e365a5fc302b81bb8ba31af625a671d7acb19d10168a8c"}, + {file = "regex-2026.1.15-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:f7792f27d3ee6e0244ea4697d92b825f9a329ab5230a78c1a68bd274e64b5077"}, + {file = "regex-2026.1.15-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:dbaf3c3c37ef190439981648ccbf0c02ed99ae066087dd117fcb616d80b010a4"}, + {file = "regex-2026.1.15-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:adc97a9077c2696501443d8ad3fa1b4fc6d131fc8fd7dfefd1a723f89071cf0a"}, + {file = "regex-2026.1.15-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:069f56a7bf71d286a6ff932a9e6fb878f151c998ebb2519a9f6d1cee4bffdba3"}, + {file = "regex-2026.1.15-cp39-cp39-win32.whl", hash = "sha256:ea4e6b3566127fda5e007e90a8fd5a4169f0cf0619506ed426db647f19c8454a"}, + {file = "regex-2026.1.15-cp39-cp39-win_amd64.whl", hash = "sha256:cda1ed70d2b264952e88adaa52eea653a33a1b98ac907ae2f86508eb44f65cdc"}, + {file = "regex-2026.1.15-cp39-cp39-win_arm64.whl", hash = "sha256:b325d4714c3c48277bfea1accd94e193ad6ed42b4bad79ad64f3b8f8a31260a5"}, + {file = "regex-2026.1.15.tar.gz", hash = "sha256:164759aa25575cbc0651bef59a0b18353e54300d79ace8084c818ad8ac72b7d5"}, +] + +[[package]] +name = "requests" +version = "2.32.5" +description = "Python HTTP for Humans." +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6"}, + {file = "requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf"}, +] + +[package.dependencies] +certifi = ">=2017.4.17" +charset_normalizer = ">=2,<4" +idna = ">=2.5,<4" +urllib3 = ">=1.21.1,<3" + +[package.extras] +socks = ["PySocks (>=1.5.6,!=1.5.7)"] +use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] + +[[package]] +name = "rlp" +version = "4.1.0" +description = "rlp: A package for Recursive Length Prefix encoding and decoding" +optional = false +python-versions = "<4,>=3.8" +groups = ["main"] +files = [ + {file = "rlp-4.1.0-py3-none-any.whl", hash = "sha256:8eca394c579bad34ee0b937aecb96a57052ff3716e19c7a578883e767bc5da6f"}, + {file = "rlp-4.1.0.tar.gz", hash = "sha256:be07564270a96f3e225e2c107db263de96b5bc1f27722d2855bd3459a08e95a9"}, +] + +[package.dependencies] +eth-utils = ">=2" + +[package.extras] +dev = ["build (>=0.9.0)", "bump_my_version (>=0.19.0)", "hypothesis (>=6.22.0,<6.108.7)", "ipython", "pre-commit (>=3.4.0)", "pytest (>=7.0.0)", "pytest-xdist (>=2.4.0)", "sphinx (>=6.0.0)", "sphinx-autobuild (>=2021.3.14)", "sphinx_rtd_theme (>=1.0.0)", "towncrier (>=24,<25)", "tox (>=4.0.0)", "twine", "wheel"] +docs = ["sphinx (>=6.0.0)", "sphinx-autobuild (>=2021.3.14)", "sphinx_rtd_theme (>=1.0.0)", "towncrier (>=24,<25)"] +rust-backend = ["rusty-rlp (>=0.2.1)"] +test = ["hypothesis (>=6.22.0,<6.108.7)", "pytest (>=7.0.0)", "pytest-xdist (>=2.4.0)"] + +[[package]] +name = "ruff" +version = "0.15.5" +description = "An extremely fast Python linter and code formatter, written in Rust." +optional = false +python-versions = ">=3.7" +groups = ["dev"] +files = [ + {file = "ruff-0.15.5-py3-none-linux_armv6l.whl", hash = "sha256:4ae44c42281f42e3b06b988e442d344a5b9b72450ff3c892e30d11b29a96a57c"}, + {file = "ruff-0.15.5-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:6edd3792d408ebcf61adabc01822da687579a1a023f297618ac27a5b51ef0080"}, + {file = "ruff-0.15.5-py3-none-macosx_11_0_arm64.whl", hash = "sha256:89f463f7c8205a9f8dea9d658d59eff49db05f88f89cc3047fb1a02d9f344010"}, + {file = "ruff-0.15.5-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ba786a8295c6574c1116704cf0b9e6563de3432ac888d8f83685654fe528fd65"}, + {file = "ruff-0.15.5-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fd4b801e57955fe9f02b31d20375ab3a5c4415f2e5105b79fb94cf2642c91440"}, + {file = "ruff-0.15.5-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:391f7c73388f3d8c11b794dbbc2959a5b5afe66642c142a6effa90b45f6f5204"}, + {file = "ruff-0.15.5-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8dc18f30302e379fe1e998548b0f5e9f4dff907f52f73ad6da419ea9c19d66c8"}, + {file = "ruff-0.15.5-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1cc6e7f90087e2d27f98dc34ed1b3ab7c8f0d273cc5431415454e22c0bd2a681"}, + {file = "ruff-0.15.5-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c1cb7169f53c1ddb06e71a9aebd7e98fc0fea936b39afb36d8e86d36ecc2636a"}, + {file = "ruff-0.15.5-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:9b037924500a31ee17389b5c8c4d88874cc6ea8e42f12e9c61a3d754ff72f1ca"}, + {file = "ruff-0.15.5-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:65bb414e5b4eadd95a8c1e4804f6772bbe8995889f203a01f77ddf2d790929dd"}, + {file = "ruff-0.15.5-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:d20aa469ae3b57033519c559e9bc9cd9e782842e39be05b50e852c7c981fa01d"}, + {file = "ruff-0.15.5-py3-none-musllinux_1_2_i686.whl", hash = "sha256:15388dd28c9161cdb8eda68993533acc870aa4e646a0a277aa166de9ad5a8752"}, + {file = "ruff-0.15.5-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:b30da330cbd03bed0c21420b6b953158f60c74c54c5f4c1dabbdf3a57bf355d2"}, + {file = "ruff-0.15.5-py3-none-win32.whl", hash = "sha256:732e5ee1f98ba5b3679029989a06ca39a950cced52143a0ea82a2102cb592b74"}, + {file = "ruff-0.15.5-py3-none-win_amd64.whl", hash = "sha256:821d41c5fa9e19117616c35eaa3f4b75046ec76c65e7ae20a333e9a8696bc7fe"}, + {file = "ruff-0.15.5-py3-none-win_arm64.whl", hash = "sha256:b498d1c60d2fe5c10c45ec3f698901065772730b411f164ae270bb6bfcc4740b"}, + {file = "ruff-0.15.5.tar.gz", hash = "sha256:7c3601d3b6d76dce18c5c824fc8d06f4eef33d6df0c21ec7799510cde0f159a2"}, +] + +[[package]] +name = "socksio" +version = "1.0.0" +description = "Sans-I/O implementation of SOCKS4, SOCKS4A, and SOCKS5." +optional = false +python-versions = ">=3.6" +groups = ["dev"] +files = [ + {file = "socksio-1.0.0-py3-none-any.whl", hash = "sha256:95dc1f15f9b34e8d7b16f06d74b8ccf48f609af32ab33c608d08761c5dcbb1f3"}, + {file = "socksio-1.0.0.tar.gz", hash = "sha256:f88beb3da5b5c38b9890469de67d0cb0f9d494b78b106ca1845f96c10b91c4ac"}, +] + +[[package]] +name = "tomli" +version = "2.4.0" +description = "A lil' TOML parser" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +markers = "python_full_version <= \"3.11.0a6\"" +files = [ + {file = "tomli-2.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b5ef256a3fd497d4973c11bf142e9ed78b150d36f5773f1ca6088c230ffc5867"}, + {file = "tomli-2.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5572e41282d5268eb09a697c89a7bee84fae66511f87533a6f88bd2f7b652da9"}, + {file = "tomli-2.4.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:551e321c6ba03b55676970b47cb1b73f14a0a4dce6a3e1a9458fd6d921d72e95"}, + {file = "tomli-2.4.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e3f639a7a8f10069d0e15408c0b96a2a828cfdec6fca05296ebcdcc28ca7c76"}, + {file = "tomli-2.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1b168f2731796b045128c45982d3a4874057626da0e2ef1fdd722848b741361d"}, + {file = "tomli-2.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:133e93646ec4300d651839d382d63edff11d8978be23da4cc106f5a18b7d0576"}, + {file = "tomli-2.4.0-cp311-cp311-win32.whl", hash = "sha256:b6c78bdf37764092d369722d9946cb65b8767bfa4110f902a1b2542d8d173c8a"}, + {file = "tomli-2.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:d3d1654e11d724760cdb37a3d7691f0be9db5fbdaef59c9f532aabf87006dbaa"}, + {file = "tomli-2.4.0-cp311-cp311-win_arm64.whl", hash = "sha256:cae9c19ed12d4e8f3ebf46d1a75090e4c0dc16271c5bce1c833ac168f08fb614"}, + {file = "tomli-2.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:920b1de295e72887bafa3ad9f7a792f811847d57ea6b1215154030cf131f16b1"}, + {file = "tomli-2.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7d6d9a4aee98fac3eab4952ad1d73aee87359452d1c086b5ceb43ed02ddb16b8"}, + {file = "tomli-2.4.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:36b9d05b51e65b254ea6c2585b59d2c4cb91c8a3d91d0ed0f17591a29aaea54a"}, + {file = "tomli-2.4.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1c8a885b370751837c029ef9bc014f27d80840e48bac415f3412e6593bbc18c1"}, + {file = "tomli-2.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8768715ffc41f0008abe25d808c20c3d990f42b6e2e58305d5da280ae7d1fa3b"}, + {file = "tomli-2.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7b438885858efd5be02a9a133caf5812b8776ee0c969fea02c45e8e3f296ba51"}, + {file = "tomli-2.4.0-cp312-cp312-win32.whl", hash = "sha256:0408e3de5ec77cc7f81960c362543cbbd91ef883e3138e81b729fc3eea5b9729"}, + {file = "tomli-2.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:685306e2cc7da35be4ee914fd34ab801a6acacb061b6a7abca922aaf9ad368da"}, + {file = "tomli-2.4.0-cp312-cp312-win_arm64.whl", hash = "sha256:5aa48d7c2356055feef06a43611fc401a07337d5b006be13a30f6c58f869e3c3"}, + {file = "tomli-2.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:84d081fbc252d1b6a982e1870660e7330fb8f90f676f6e78b052ad4e64714bf0"}, + {file = "tomli-2.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9a08144fa4cba33db5255f9b74f0b89888622109bd2776148f2597447f92a94e"}, + {file = "tomli-2.4.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c73add4bb52a206fd0c0723432db123c0c75c280cbd67174dd9d2db228ebb1b4"}, + {file = "tomli-2.4.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1fb2945cbe303b1419e2706e711b7113da57b7db31ee378d08712d678a34e51e"}, + {file = "tomli-2.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bbb1b10aa643d973366dc2cb1ad94f99c1726a02343d43cbc011edbfac579e7c"}, + {file = "tomli-2.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4cbcb367d44a1f0c2be408758b43e1ffb5308abe0ea222897d6bfc8e8281ef2f"}, + {file = "tomli-2.4.0-cp313-cp313-win32.whl", hash = "sha256:7d49c66a7d5e56ac959cb6fc583aff0651094ec071ba9ad43df785abc2320d86"}, + {file = "tomli-2.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:3cf226acb51d8f1c394c1b310e0e0e61fecdd7adcb78d01e294ac297dd2e7f87"}, + {file = "tomli-2.4.0-cp313-cp313-win_arm64.whl", hash = "sha256:d20b797a5c1ad80c516e41bc1fb0443ddb5006e9aaa7bda2d71978346aeb9132"}, + {file = "tomli-2.4.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:26ab906a1eb794cd4e103691daa23d95c6919cc2fa9160000ac02370cc9dd3f6"}, + {file = "tomli-2.4.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:20cedb4ee43278bc4f2fee6cb50daec836959aadaf948db5172e776dd3d993fc"}, + {file = "tomli-2.4.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:39b0b5d1b6dd03684b3fb276407ebed7090bbec989fa55838c98560c01113b66"}, + {file = "tomli-2.4.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a26d7ff68dfdb9f87a016ecfd1e1c2bacbe3108f4e0f8bcd2228ef9a766c787d"}, + {file = "tomli-2.4.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:20ffd184fb1df76a66e34bd1b36b4a4641bd2b82954befa32fe8163e79f1a702"}, + {file = "tomli-2.4.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:75c2f8bbddf170e8effc98f5e9084a8751f8174ea6ccf4fca5398436e0320bc8"}, + {file = "tomli-2.4.0-cp314-cp314-win32.whl", hash = "sha256:31d556d079d72db7c584c0627ff3a24c5d3fb4f730221d3444f3efb1b2514776"}, + {file = "tomli-2.4.0-cp314-cp314-win_amd64.whl", hash = "sha256:43e685b9b2341681907759cf3a04e14d7104b3580f808cfde1dfdb60ada85475"}, + {file = "tomli-2.4.0-cp314-cp314-win_arm64.whl", hash = "sha256:3d895d56bd3f82ddd6faaff993c275efc2ff38e52322ea264122d72729dca2b2"}, + {file = "tomli-2.4.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:5b5807f3999fb66776dbce568cc9a828544244a8eb84b84b9bafc080c99597b9"}, + {file = "tomli-2.4.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c084ad935abe686bd9c898e62a02a19abfc9760b5a79bc29644463eaf2840cb0"}, + {file = "tomli-2.4.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f2e3955efea4d1cfbcb87bc321e00dc08d2bcb737fd1d5e398af111d86db5df"}, + {file = "tomli-2.4.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e0fe8a0b8312acf3a88077a0802565cb09ee34107813bba1c7cd591fa6cfc8d"}, + {file = "tomli-2.4.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:413540dce94673591859c4c6f794dfeaa845e98bf35d72ed59636f869ef9f86f"}, + {file = "tomli-2.4.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0dc56fef0e2c1c470aeac5b6ca8cc7b640bb93e92d9803ddaf9ea03e198f5b0b"}, + {file = "tomli-2.4.0-cp314-cp314t-win32.whl", hash = "sha256:d878f2a6707cc9d53a1be1414bbb419e629c3d6e67f69230217bb663e76b5087"}, + {file = "tomli-2.4.0-cp314-cp314t-win_amd64.whl", hash = "sha256:2add28aacc7425117ff6364fe9e06a183bb0251b03f986df0e78e974047571fd"}, + {file = "tomli-2.4.0-cp314-cp314t-win_arm64.whl", hash = "sha256:2b1e3b80e1d5e52e40e9b924ec43d81570f0e7d09d11081b797bc4692765a3d4"}, + {file = "tomli-2.4.0-py3-none-any.whl", hash = "sha256:1f776e7d669ebceb01dee46484485f43a4048746235e683bcdffacdf1fb4785a"}, + {file = "tomli-2.4.0.tar.gz", hash = "sha256:aa89c3f6c277dd275d8e243ad24f3b5e701491a860d5121f2cdd399fbb31fc9c"}, +] + +[[package]] +name = "toolz" +version = "1.1.0" +description = "List processing tools and functional utilities" +optional = false +python-versions = ">=3.9" +groups = ["main"] +markers = "implementation_name == \"pypy\" or implementation_name == \"cpython\"" +files = [ + {file = "toolz-1.1.0-py3-none-any.whl", hash = "sha256:15ccc861ac51c53696de0a5d6d4607f99c210739caf987b5d2054f3efed429d8"}, + {file = "toolz-1.1.0.tar.gz", hash = "sha256:27a5c770d068c110d9ed9323f24f1543e83b2f300a687b7891c1a6d56b697b5b"}, +] + +[[package]] +name = "types-requests" +version = "2.32.4.20260107" +description = "Typing stubs for requests" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "types_requests-2.32.4.20260107-py3-none-any.whl", hash = "sha256:b703fe72f8ce5b31ef031264fe9395cac8f46a04661a79f7ed31a80fb308730d"}, + {file = "types_requests-2.32.4.20260107.tar.gz", hash = "sha256:018a11ac158f801bfa84857ddec1650750e393df8a004a8a9ae2a9bec6fcb24f"}, +] + +[package.dependencies] +urllib3 = ">=2" + +[[package]] +name = "typing-extensions" +version = "4.15.0" +description = "Backported and Experimental Type Hints for Python 3.9+" +optional = false +python-versions = ">=3.9" +groups = ["main", "dev"] +files = [ + {file = "typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548"}, + {file = "typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466"}, +] + +[[package]] +name = "typing-inspection" +version = "0.4.2" +description = "Runtime typing introspection tools" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7"}, + {file = "typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464"}, +] + +[package.dependencies] +typing-extensions = ">=4.12.0" + +[[package]] +name = "urllib3" +version = "2.6.3" +description = "HTTP library with thread-safe connection pooling, file post, and more." +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4"}, + {file = "urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed"}, +] + +[package.extras] +brotli = ["brotli (>=1.2.0) ; platform_python_implementation == \"CPython\"", "brotlicffi (>=1.2.0.0) ; platform_python_implementation != \"CPython\""] +h2 = ["h2 (>=4,<5)"] +socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] +zstd = ["backports-zstd (>=1.0.0) ; python_version < \"3.14\""] + +[[package]] +name = "web3" +version = "7.14.1" +description = "web3: A Python library for interacting with Ethereum" +optional = false +python-versions = "<4,>=3.8" +groups = ["main"] +files = [ + {file = "web3-7.14.1-py3-none-any.whl", hash = "sha256:bec367ba44261f874662aed9b5e138aa7bb907700a30a7580b2264534e88ce12"}, + {file = "web3-7.14.1.tar.gz", hash = "sha256:856dc8517f362aefa75fdc298d975894055565dc866f21279f27fe060b7fb2c3"}, +] + +[package.dependencies] +aiohttp = ">=3.7.4.post0" +eth-abi = ">=5.0.1" +eth-account = ">=0.13.6" +eth-hash = {version = ">=0.5.1", extras = ["pycryptodome"]} +eth-typing = ">=5.0.0" +eth-utils = ">=5.0.0" +hexbytes = ">=1.2.0" +pydantic = ">=2.4.0" +pyunormalize = ">=15.0.0" +pywin32 = {version = ">=223", markers = "platform_system == \"Windows\""} +requests = ">=2.23.0" +types-requests = ">=2.0.0" +typing-extensions = ">=4.0.1" +websockets = ">=10.0.0,<16.0.0" + +[package.extras] +dev = ["build (>=0.9.0)", "bump_my_version (>=0.19.0)", "eth-tester[py-evm] (>=0.13.0b1,<0.14.0b1)", "flaky (>=3.7.0)", "hypothesis (>=3.31.2)", "ipython", "mypy (==1.10.0)", "pre-commit (>=3.4.0)", "py-geth (>=6.4.0)", "pytest (>=7.0.0)", "pytest-asyncio (>=0.18.1,<0.23)", "pytest-mock (>=1.10)", "pytest-xdist (>=2.4.0)", "setuptools (>=38.6.0)", "sphinx (>=6.0.0)", "sphinx-autobuild (>=2021.3.14)", "sphinx_rtd_theme (>=1.0.0)", "towncrier (>=24,<25)", "tox (>=4.0.0)", "tqdm (>4.32)", "twine (>=1.13)", "wheel"] +docs = ["sphinx (>=6.0.0)", "sphinx-autobuild (>=2021.3.14)", "sphinx_rtd_theme (>=1.0.0)", "towncrier (>=24,<25)"] +test = ["eth-tester[py-evm] (>=0.13.0b1,<0.14.0b1)", "flaky (>=3.7.0)", "hypothesis (>=3.31.2)", "mypy (==1.10.0)", "pre-commit (>=3.4.0)", "py-geth (>=6.4.0)", "pytest (>=7.0.0)", "pytest-asyncio (>=0.18.1,<0.23)", "pytest-mock (>=1.10)", "pytest-xdist (>=2.4.0)", "tox (>=4.0.0)"] +tester = ["eth-tester[py-evm] (>=0.13.0b1,<0.14.0b1)", "py-geth (>=6.4.0)"] + +[[package]] +name = "websockets" +version = "15.0.1" +description = "An implementation of the WebSocket Protocol (RFC 6455 & 7692)" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "websockets-15.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d63efaa0cd96cf0c5fe4d581521d9fa87744540d4bc999ae6e08595a1014b45b"}, + {file = "websockets-15.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ac60e3b188ec7574cb761b08d50fcedf9d77f1530352db4eef1707fe9dee7205"}, + {file = "websockets-15.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5756779642579d902eed757b21b0164cd6fe338506a8083eb58af5c372e39d9a"}, + {file = "websockets-15.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0fdfe3e2a29e4db3659dbd5bbf04560cea53dd9610273917799f1cde46aa725e"}, + {file = "websockets-15.0.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4c2529b320eb9e35af0fa3016c187dffb84a3ecc572bcee7c3ce302bfeba52bf"}, + {file = "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"}, + {file = "websockets-15.0.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:5df592cd503496351d6dc14f7cdad49f268d8e618f80dce0cd5a36b93c3fc08d"}, + {file = "websockets-15.0.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:0a34631031a8f05657e8e90903e656959234f3a04552259458aac0b0f9ae6fd9"}, + {file = "websockets-15.0.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3d00075aa65772e7ce9e990cab3ff1de702aa09be3940d1dc88d5abf1ab8a09c"}, + {file = "websockets-15.0.1-cp310-cp310-win32.whl", hash = "sha256:1234d4ef35db82f5446dca8e35a7da7964d02c127b095e172e54397fb6a6c256"}, + {file = "websockets-15.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:39c1fec2c11dc8d89bba6b2bf1556af381611a173ac2b511cf7231622058af41"}, + {file = "websockets-15.0.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:823c248b690b2fd9303ba00c4f66cd5e2d8c3ba4aa968b2779be9532a4dad431"}, + {file = "websockets-15.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678999709e68425ae2593acf2e3ebcbcf2e69885a5ee78f9eb80e6e371f1bf57"}, + {file = "websockets-15.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d50fd1ee42388dcfb2b3676132c78116490976f1300da28eb629272d5d93e905"}, + {file = "websockets-15.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d99e5546bf73dbad5bf3547174cd6cb8ba7273062a23808ffea025ecb1cf8562"}, + {file = "websockets-15.0.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:66dd88c918e3287efc22409d426c8f729688d89a0c587c88971a0faa2c2f3792"}, + {file = "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"}, + {file = "websockets-15.0.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8fdc51055e6ff4adeb88d58a11042ec9a5eae317a0a53d12c062c8a8865909e8"}, + {file = "websockets-15.0.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:693f0192126df6c2327cce3baa7c06f2a117575e32ab2308f7f8216c29d9e2e3"}, + {file = "websockets-15.0.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:54479983bd5fb469c38f2f5c7e3a24f9a4e70594cd68cd1fa6b9340dadaff7cf"}, + {file = "websockets-15.0.1-cp311-cp311-win32.whl", hash = "sha256:16b6c1b3e57799b9d38427dda63edcbe4926352c47cf88588c0be4ace18dac85"}, + {file = "websockets-15.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:27ccee0071a0e75d22cb35849b1db43f2ecd3e161041ac1ee9d2352ddf72f065"}, + {file = "websockets-15.0.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:3e90baa811a5d73f3ca0bcbf32064d663ed81318ab225ee4f427ad4e26e5aff3"}, + {file = "websockets-15.0.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:592f1a9fe869c778694f0aa806ba0374e97648ab57936f092fd9d87f8bc03665"}, + {file = "websockets-15.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0701bc3cfcb9164d04a14b149fd74be7347a530ad3bbf15ab2c678a2cd3dd9a2"}, + {file = "websockets-15.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8b56bdcdb4505c8078cb6c7157d9811a85790f2f2b3632c7d1462ab5783d215"}, + {file = "websockets-15.0.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0af68c55afbd5f07986df82831c7bff04846928ea8d1fd7f30052638788bc9b5"}, + {file = "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"}, + {file = "websockets-15.0.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d5f6b181bb38171a8ad1d6aa58a67a6aa9d4b38d0f8c5f496b9e42561dfc62fe"}, + {file = "websockets-15.0.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5d54b09eba2bada6011aea5375542a157637b91029687eb4fdb2dab11059c1b4"}, + {file = "websockets-15.0.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3be571a8b5afed347da347bfcf27ba12b069d9d7f42cb8c7028b5e98bbb12597"}, + {file = "websockets-15.0.1-cp312-cp312-win32.whl", hash = "sha256:c338ffa0520bdb12fbc527265235639fb76e7bc7faafbb93f6ba80d9c06578a9"}, + {file = "websockets-15.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:fcd5cf9e305d7b8338754470cf69cf81f420459dbae8a3b40cee57417f4614a7"}, + {file = "websockets-15.0.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ee443ef070bb3b6ed74514f5efaa37a252af57c90eb33b956d35c8e9c10a1931"}, + {file = "websockets-15.0.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5a939de6b7b4e18ca683218320fc67ea886038265fd1ed30173f5ce3f8e85675"}, + {file = "websockets-15.0.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:746ee8dba912cd6fc889a8147168991d50ed70447bf18bcda7039f7d2e3d9151"}, + {file = "websockets-15.0.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:595b6c3969023ecf9041b2936ac3827e4623bfa3ccf007575f04c5a6aa318c22"}, + {file = "websockets-15.0.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c714d2fc58b5ca3e285461a4cc0c9a66bd0e24c5da9911e30158286c9b5be7f"}, + {file = "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"}, + {file = "websockets-15.0.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:229cf1d3ca6c1804400b0a9790dc66528e08a6a1feec0d5040e8b9eb14422375"}, + {file = "websockets-15.0.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:756c56e867a90fb00177d530dca4b097dd753cde348448a1012ed6c5131f8b7d"}, + {file = "websockets-15.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:558d023b3df0bffe50a04e710bc87742de35060580a293c2a984299ed83bc4e4"}, + {file = "websockets-15.0.1-cp313-cp313-win32.whl", hash = "sha256:ba9e56e8ceeeedb2e080147ba85ffcd5cd0711b89576b83784d8605a7df455fa"}, + {file = "websockets-15.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:e09473f095a819042ecb2ab9465aee615bd9c2028e4ef7d933600a8401c79561"}, + {file = "websockets-15.0.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:5f4c04ead5aed67c8a1a20491d54cdfba5884507a48dd798ecaf13c74c4489f5"}, + {file = "websockets-15.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:abdc0c6c8c648b4805c5eacd131910d2a7f6455dfd3becab248ef108e89ab16a"}, + {file = "websockets-15.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a625e06551975f4b7ea7102bc43895b90742746797e2e14b70ed61c43a90f09b"}, + {file = "websockets-15.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d591f8de75824cbb7acad4e05d2d710484f15f29d4a915092675ad3456f11770"}, + {file = "websockets-15.0.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:47819cea040f31d670cc8d324bb6435c6f133b8c7a19ec3d61634e62f8d8f9eb"}, + {file = "websockets-15.0.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ac017dd64572e5c3bd01939121e4d16cf30e5d7e110a119399cf3133b63ad054"}, + {file = "websockets-15.0.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:4a9fac8e469d04ce6c25bb2610dc535235bd4aa14996b4e6dbebf5e007eba5ee"}, + {file = "websockets-15.0.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:363c6f671b761efcb30608d24925a382497c12c506b51661883c3e22337265ed"}, + {file = "websockets-15.0.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:2034693ad3097d5355bfdacfffcbd3ef5694f9718ab7f29c29689a9eae841880"}, + {file = "websockets-15.0.1-cp39-cp39-win32.whl", hash = "sha256:3b1ac0d3e594bf121308112697cf4b32be538fb1444468fb0a6ae4feebc83411"}, + {file = "websockets-15.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:b7643a03db5c95c799b89b31c036d5f27eeb4d259c798e878d6937d71832b1e4"}, + {file = "websockets-15.0.1-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0c9e74d766f2818bb95f84c25be4dea09841ac0f734d1966f415e4edfc4ef1c3"}, + {file = "websockets-15.0.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:1009ee0c7739c08a0cd59de430d6de452a55e42d6b522de7aa15e6f67db0b8e1"}, + {file = "websockets-15.0.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:76d1f20b1c7a2fa82367e04982e708723ba0e7b8d43aa643d3dcd404d74f1475"}, + {file = "websockets-15.0.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f29d80eb9a9263b8d109135351caf568cc3f80b9928bccde535c235de55c22d9"}, + {file = "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"}, + {file = "websockets-15.0.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:cad21560da69f4ce7658ca2cb83138fb4cf695a2ba3e475e0559e05991aa8122"}, + {file = "websockets-15.0.1-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:7f493881579c90fc262d9cdbaa05a6b54b3811c2f300766748db79f098db9940"}, + {file = "websockets-15.0.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:47b099e1f4fbc95b701b6e85768e1fcdaf1630f3cbe4765fa216596f12310e2e"}, + {file = "websockets-15.0.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:67f2b6de947f8c757db2db9c71527933ad0019737ec374a8a6be9a956786aaf9"}, + {file = "websockets-15.0.1-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d08eb4c2b7d6c41da6ca0600c077e93f5adcfd979cd777d747e9ee624556da4b"}, + {file = "websockets-15.0.1-pp39-pypy39_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b826973a4a2ae47ba357e4e82fa44a463b8f168e1ca775ac64521442b19e87f"}, + {file = "websockets-15.0.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:21c1fa28a6a7e3cbdc171c694398b6df4744613ce9b36b1a498e816787e28123"}, + {file = "websockets-15.0.1-py3-none-any.whl", hash = "sha256:f7a866fbc1e97b5c617ee4116daaa09b722101d4a3c170c787450ba409f9736f"}, + {file = "websockets-15.0.1.tar.gz", hash = "sha256:82544de02076bafba038ce055ee6412d68da13ab47f0c60cab827346de828dee"}, +] + +[[package]] +name = "yarl" +version = "1.23.0" +description = "Yet another URL library" +optional = false +python-versions = ">=3.10" +groups = ["main"] +files = [ + {file = "yarl-1.23.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cff6d44cb13d39db2663a22b22305d10855efa0fa8015ddeacc40bc59b9d8107"}, + {file = "yarl-1.23.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e4c53f8347cd4200f0d70a48ad059cabaf24f5adc6ba08622a23423bc7efa10d"}, + {file = "yarl-1.23.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2a6940a074fb3c48356ed0158a3ca5699c955ee4185b4d7d619be3c327143e05"}, + {file = "yarl-1.23.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ed5f69ce7be7902e5c70ea19eb72d20abf7d725ab5d49777d696e32d4fc1811d"}, + {file = "yarl-1.23.0-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:389871e65468400d6283c0308e791a640b5ab5c83bcee02a2f51295f95e09748"}, + {file = "yarl-1.23.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:dda608c88cf709b1d406bdfcd84d8d63cff7c9e577a403c6108ce8ce9dcc8764"}, + {file = "yarl-1.23.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8c4fe09e0780c6c3bf2b7d4af02ee2394439d11a523bbcf095cf4747c2932007"}, + {file = "yarl-1.23.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:31c9921eb8bd12633b41ad27686bbb0b1a2a9b8452bfdf221e34f311e9942ed4"}, + {file = "yarl-1.23.0-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5f10fd85e4b75967468af655228fbfd212bdf66db1c0d135065ce288982eda26"}, + {file = "yarl-1.23.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:dbf507e9ef5688bada447a24d68b4b58dd389ba93b7afc065a2ba892bea54769"}, + {file = "yarl-1.23.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:85e9beda1f591bc73e77ea1c51965c68e98dafd0fec72cdd745f77d727466716"}, + {file = "yarl-1.23.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:0e1fdaa14ef51366d7757b45bde294e95f6c8c049194e793eedb8387c86d5993"}, + {file = "yarl-1.23.0-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:75e3026ab649bf48f9a10c0134512638725b521340293f202a69b567518d94e0"}, + {file = "yarl-1.23.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:80e6d33a3d42a7549b409f199857b4fb54e2103fc44fb87605b6663b7a7ff750"}, + {file = "yarl-1.23.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5ec2f42d41ccbd5df0270d7df31618a8ee267bfa50997f5d720ddba86c4a83a6"}, + {file = "yarl-1.23.0-cp310-cp310-win32.whl", hash = "sha256:debe9c4f41c32990771be5c22b56f810659f9ddf3d63f67abfdcaa2c6c9c5c1d"}, + {file = "yarl-1.23.0-cp310-cp310-win_amd64.whl", hash = "sha256:ab5f043cb8a2d71c981c09c510da013bc79fd661f5c60139f00dd3c3cc4f2ffb"}, + {file = "yarl-1.23.0-cp310-cp310-win_arm64.whl", hash = "sha256:263cd4f47159c09b8b685890af949195b51d1aa82ba451c5847ca9bc6413c220"}, + {file = "yarl-1.23.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:b35d13d549077713e4414f927cdc388d62e543987c572baee613bf82f11a4b99"}, + {file = "yarl-1.23.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cbb0fef01f0c6b38cb0f39b1f78fc90b807e0e3c86a7ff3ce74ad77ce5c7880c"}, + {file = "yarl-1.23.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dc52310451fc7c629e13c4e061cbe2dd01684d91f2f8ee2821b083c58bd72432"}, + {file = "yarl-1.23.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b2c6b50c7b0464165472b56b42d4c76a7b864597007d9c085e8b63e185cf4a7a"}, + {file = "yarl-1.23.0-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:aafe5dcfda86c8af00386d7781d4c2181b5011b7be3f2add5e99899ea925df05"}, + {file = "yarl-1.23.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9ee33b875f0b390564c1fb7bc528abf18c8ee6073b201c6ae8524aca778e2d83"}, + {file = "yarl-1.23.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4c41e021bc6d7affb3364dc1e1e5fa9582b470f283748784bd6ea0558f87f42c"}, + {file = "yarl-1.23.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:99c8a9ed30f4164bc4c14b37a90208836cbf50d4ce2a57c71d0f52c7fb4f7598"}, + {file = "yarl-1.23.0-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f2af5c81a1f124609d5f33507082fc3f739959d4719b56877ab1ee7e7b3d602b"}, + {file = "yarl-1.23.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6b41389c19b07c760c7e427a3462e8ab83c4bb087d127f0e854c706ce1b9215c"}, + {file = "yarl-1.23.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:1dc702e42d0684f42d6519c8d581e49c96cefaaab16691f03566d30658ee8788"}, + {file = "yarl-1.23.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:0e40111274f340d32ebcc0a5668d54d2b552a6cca84c9475859d364b380e3222"}, + {file = "yarl-1.23.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:4764a6a7588561a9aef92f65bda2c4fb58fe7c675c0883862e6df97559de0bfb"}, + {file = "yarl-1.23.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:03214408cfa590df47728b84c679ae4ef00be2428e11630277be0727eba2d7cc"}, + {file = "yarl-1.23.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:170e26584b060879e29fac213e4228ef063f39128723807a312e5c7fec28eff2"}, + {file = "yarl-1.23.0-cp311-cp311-win32.whl", hash = "sha256:51430653db848d258336cfa0244427b17d12db63d42603a55f0d4546f50f25b5"}, + {file = "yarl-1.23.0-cp311-cp311-win_amd64.whl", hash = "sha256:bf49a3ae946a87083ef3a34c8f677ae4243f5b824bfc4c69672e72b3d6719d46"}, + {file = "yarl-1.23.0-cp311-cp311-win_arm64.whl", hash = "sha256:b39cb32a6582750b6cc77bfb3c49c0f8760dc18dc96ec9fb55fbb0f04e08b928"}, + {file = "yarl-1.23.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:1932b6b8bba8d0160a9d1078aae5838a66039e8832d41d2992daa9a3a08f7860"}, + {file = "yarl-1.23.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:411225bae281f114067578891bc75534cfb3d92a3b4dfef7a6ca78ba354e6069"}, + {file = "yarl-1.23.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:13a563739ae600a631c36ce096615fe307f131344588b0bc0daec108cdb47b25"}, + {file = "yarl-1.23.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9cbf44c5cb4a7633d078788e1b56387e3d3cf2b8139a3be38040b22d6c3221c8"}, + {file = "yarl-1.23.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:53ad387048f6f09a8969631e4de3f1bf70c50e93545d64af4f751b2498755072"}, + {file = "yarl-1.23.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4a59ba56f340334766f3a4442e0efd0af895fae9e2b204741ef885c446b3a1a8"}, + {file = "yarl-1.23.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:803a3c3ce4acc62eaf01eaca1208dcf0783025ef27572c3336502b9c232005e7"}, + {file = "yarl-1.23.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a3d2bff8f37f8d0f96c7ec554d16945050d54462d6e95414babaa18bfafc7f51"}, + {file = "yarl-1.23.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c75eb09e8d55bceb4367e83496ff8ef2bc7ea6960efb38e978e8073ea59ecb67"}, + {file = "yarl-1.23.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:877b0738624280e34c55680d6054a307aa94f7d52fa0e3034a9cc6e790871da7"}, + {file = "yarl-1.23.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:b5405bb8f0e783a988172993cfc627e4d9d00432d6bbac65a923041edacf997d"}, + {file = "yarl-1.23.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:1c3a3598a832590c5a3ce56ab5576361b5688c12cb1d39429cf5dba30b510760"}, + {file = "yarl-1.23.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:8419ebd326430d1cbb7efb5292330a2cf39114e82df5cc3d83c9a0d5ebeaf2f2"}, + {file = "yarl-1.23.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:be61f6fff406ca40e3b1d84716fde398fc08bc63dd96d15f3a14230a0973ed86"}, + {file = "yarl-1.23.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3ceb13c5c858d01321b5d9bb65e4cf37a92169ea470b70fec6f236b2c9dd7e34"}, + {file = "yarl-1.23.0-cp312-cp312-win32.whl", hash = "sha256:fffc45637bcd6538de8b85f51e3df3223e4ad89bccbfca0481c08c7fc8b7ed7d"}, + {file = "yarl-1.23.0-cp312-cp312-win_amd64.whl", hash = "sha256:f69f57305656a4852f2a7203efc661d8c042e6cc67f7acd97d8667fb448a426e"}, + {file = "yarl-1.23.0-cp312-cp312-win_arm64.whl", hash = "sha256:6e87a6e8735b44816e7db0b2fbc9686932df473c826b0d9743148432e10bb9b9"}, + {file = "yarl-1.23.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:16c6994ac35c3e74fb0ae93323bf8b9c2a9088d55946109489667c510a7d010e"}, + {file = "yarl-1.23.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4a42e651629dafb64fd5b0286a3580613702b5809ad3f24934ea87595804f2c5"}, + {file = "yarl-1.23.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7c6b9461a2a8b47c65eef63bb1c76a4f1c119618ffa99ea79bc5bb1e46c5821b"}, + {file = "yarl-1.23.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2569b67d616eab450d262ca7cb9f9e19d2f718c70a8b88712859359d0ab17035"}, + {file = "yarl-1.23.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e9d9a4d06d3481eab79803beb4d9bd6f6a8e781ec078ac70d7ef2dcc29d1bea5"}, + {file = "yarl-1.23.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f514f6474e04179d3d33175ed3f3e31434d3130d42ec153540d5b157deefd735"}, + {file = "yarl-1.23.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fda207c815b253e34f7e1909840fd14299567b1c0eb4908f8c2ce01a41265401"}, + {file = "yarl-1.23.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:34b6cf500e61c90f305094911f9acc9c86da1a05a7a3f5be9f68817043f486e4"}, + {file = "yarl-1.23.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d7504f2b476d21653e4d143f44a175f7f751cd41233525312696c76aa3dbb23f"}, + {file = "yarl-1.23.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:578110dd426f0d209d1509244e6d4a3f1a3e9077655d98c5f22583d63252a08a"}, + {file = "yarl-1.23.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:609d3614d78d74ebe35f54953c5bbd2ac647a7ddb9c30a5d877580f5e86b22f2"}, + {file = "yarl-1.23.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4966242ec68afc74c122f8459abd597afd7d8a60dc93d695c1334c5fd25f762f"}, + {file = "yarl-1.23.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:e0fd068364a6759bc794459f0a735ab151d11304346332489c7972bacbe9e72b"}, + {file = "yarl-1.23.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:39004f0ad156da43e86aa71f44e033de68a44e5a31fc53507b36dd253970054a"}, + {file = "yarl-1.23.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e5723c01a56c5028c807c701aa66722916d2747ad737a046853f6c46f4875543"}, + {file = "yarl-1.23.0-cp313-cp313-win32.whl", hash = "sha256:1b6b572edd95b4fa8df75de10b04bc81acc87c1c7d16bcdd2035b09d30acc957"}, + {file = "yarl-1.23.0-cp313-cp313-win_amd64.whl", hash = "sha256:baaf55442359053c7d62f6f8413a62adba3205119bcb6f49594894d8be47e5e3"}, + {file = "yarl-1.23.0-cp313-cp313-win_arm64.whl", hash = "sha256:fb4948814a2a98e3912505f09c9e7493b1506226afb1f881825368d6fb776ee3"}, + {file = "yarl-1.23.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:aecfed0b41aa72b7881712c65cf764e39ce2ec352324f5e0837c7048d9e6daaa"}, + {file = "yarl-1.23.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:a41bcf68efd19073376eb8cf948b8d9be0af26256403e512bb18f3966f1f9120"}, + {file = "yarl-1.23.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:cde9a2ecd91668bcb7f077c4966d8ceddb60af01b52e6e3e2680e4cf00ad1a59"}, + {file = "yarl-1.23.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5023346c4ee7992febc0068e7593de5fa2bf611848c08404b35ebbb76b1b0512"}, + {file = "yarl-1.23.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d1009abedb49ae95b136a8904a3f71b342f849ffeced2d3747bf29caeda218c4"}, + {file = "yarl-1.23.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a8d00f29b42f534cc8aa3931cfe773b13b23e561e10d2b26f27a8d309b0e82a1"}, + {file = "yarl-1.23.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:95451e6ce06c3e104556d73b559f5da6c34a069b6b62946d3ad66afcd51642ea"}, + {file = "yarl-1.23.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:531ef597132086b6cf96faa7c6c1dcd0361dd5f1694e5cc30375907b9b7d3ea9"}, + {file = "yarl-1.23.0-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:88f9fb0116fbfcefcab70f85cf4b74a2b6ce5d199c41345296f49d974ddb4123"}, + {file = "yarl-1.23.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:e7b0460976dc75cb87ad9cc1f9899a4b97751e7d4e77ab840fc9b6d377b8fd24"}, + {file = "yarl-1.23.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:115136c4a426f9da976187d238e84139ff6b51a20839aa6e3720cd1026d768de"}, + {file = "yarl-1.23.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:ead11956716a940c1abc816b7df3fa2b84d06eaed8832ca32f5c5e058c65506b"}, + {file = "yarl-1.23.0-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:fe8f8f5e70e6dbdfca9882cd9deaac058729bcf323cf7a58660901e55c9c94f6"}, + {file = "yarl-1.23.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:a0e317df055958a0c1e79e5d2aa5a5eaa4a6d05a20d4b0c9c3f48918139c9fc6"}, + {file = "yarl-1.23.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6f0fd84de0c957b2d280143522c4f91a73aada1923caee763e24a2b3fda9f8a5"}, + {file = "yarl-1.23.0-cp313-cp313t-win32.whl", hash = "sha256:93a784271881035ab4406a172edb0faecb6e7d00f4b53dc2f55919d6c9688595"}, + {file = "yarl-1.23.0-cp313-cp313t-win_amd64.whl", hash = "sha256:dd00607bffbf30250fe108065f07453ec124dbf223420f57f5e749b04295e090"}, + {file = "yarl-1.23.0-cp313-cp313t-win_arm64.whl", hash = "sha256:ac09d42f48f80c9ee1635b2fcaa819496a44502737660d3c0f2ade7526d29144"}, + {file = "yarl-1.23.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:21d1b7305a71a15b4794b5ff22e8eef96ff4a6d7f9657155e5aa419444b28912"}, + {file = "yarl-1.23.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:85610b4f27f69984932a7abbe52703688de3724d9f72bceb1cca667deff27474"}, + {file = "yarl-1.23.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:23f371bd662cf44a7630d4d113101eafc0cfa7518a2760d20760b26021454719"}, + {file = "yarl-1.23.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c4a80f77dc1acaaa61f0934176fccca7096d9b1ff08c8ba9cddf5ae034a24319"}, + {file = "yarl-1.23.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:bd654fad46d8d9e823afbb4f87c79160b5a374ed1ff5bde24e542e6ba8f41434"}, + {file = "yarl-1.23.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:682bae25f0a0dd23a056739f23a134db9f52a63e2afd6bfb37ddc76292bbd723"}, + {file = "yarl-1.23.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a82836cab5f197a0514235aaf7ffccdc886ccdaa2324bc0aafdd4ae898103039"}, + {file = "yarl-1.23.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1c57676bdedc94cd3bc37724cf6f8cd2779f02f6aba48de45feca073e714fe52"}, + {file = "yarl-1.23.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c7f8dc16c498ff06497c015642333219871effba93e4a2e8604a06264aca5c5c"}, + {file = "yarl-1.23.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:5ee586fb17ff8f90c91cf73c6108a434b02d69925f44f5f8e0d7f2f260607eae"}, + {file = "yarl-1.23.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:17235362f580149742739cc3828b80e24029d08cbb9c4bda0242c7b5bc610a8e"}, + {file = "yarl-1.23.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:0793e2bd0cf14234983bbb371591e6bea9e876ddf6896cdcc93450996b0b5c85"}, + {file = "yarl-1.23.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:3650dc2480f94f7116c364096bc84b1d602f44224ef7d5c7208425915c0475dd"}, + {file = "yarl-1.23.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:f40e782d49630ad384db66d4d8b73ff4f1b8955dc12e26b09a3e3af064b3b9d6"}, + {file = "yarl-1.23.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:94f8575fbdf81749008d980c17796097e645574a3b8c28ee313931068dad14fe"}, + {file = "yarl-1.23.0-cp314-cp314-win32.whl", hash = "sha256:c8aa34a5c864db1087d911a0b902d60d203ea3607d91f615acd3f3108ac32169"}, + {file = "yarl-1.23.0-cp314-cp314-win_amd64.whl", hash = "sha256:63e92247f383c85ab00dd0091e8c3fa331a96e865459f5ee80353c70a4a42d70"}, + {file = "yarl-1.23.0-cp314-cp314-win_arm64.whl", hash = "sha256:70efd20be968c76ece7baa8dafe04c5be06abc57f754d6f36f3741f7aa7a208e"}, + {file = "yarl-1.23.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:9a18d6f9359e45722c064c97464ec883eb0e0366d33eda61cb19a244bf222679"}, + {file = "yarl-1.23.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:2803ed8b21ca47a43da80a6fd1ed3019d30061f7061daa35ac54f63933409412"}, + {file = "yarl-1.23.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:394906945aa8b19fc14a61cf69743a868bb8c465efe85eee687109cc540b98f4"}, + {file = "yarl-1.23.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:71d006bee8397a4a89f469b8deb22469fe7508132d3c17fa6ed871e79832691c"}, + {file = "yarl-1.23.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:62694e275c93d54f7ccedcfef57d42761b2aad5234b6be1f3e3026cae4001cd4"}, + {file = "yarl-1.23.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a31de1613658308efdb21ada98cbc86a97c181aa050ba22a808120bb5be3ab94"}, + {file = "yarl-1.23.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fb1e8b8d66c278b21d13b0a7ca22c41dd757a7c209c6b12c313e445c31dd3b28"}, + {file = "yarl-1.23.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:50f9d8d531dfb767c565f348f33dd5139a6c43f5cbdf3f67da40d54241df93f6"}, + {file = "yarl-1.23.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:575aa4405a656e61a540f4a80eaa5260f2a38fff7bfdc4b5f611840d76e9e277"}, + {file = "yarl-1.23.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:041b1a4cefacf65840b4e295c6985f334ba83c30607441ae3cf206a0eed1a2e4"}, + {file = "yarl-1.23.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:d38c1e8231722c4ce40d7593f28d92b5fc72f3e9774fe73d7e800ec32299f63a"}, + {file = "yarl-1.23.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:d53834e23c015ee83a99377db6e5e37d8484f333edb03bd15b4bc312cc7254fb"}, + {file = "yarl-1.23.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:2e27c8841126e017dd2a054a95771569e6070b9ee1b133366d8b31beb5018a41"}, + {file = "yarl-1.23.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:76855800ac56f878847a09ce6dba727c93ca2d89c9e9d63002d26b916810b0a2"}, + {file = "yarl-1.23.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e09fd068c2e169a7070d83d3bde728a4d48de0549f975290be3c108c02e499b4"}, + {file = "yarl-1.23.0-cp314-cp314t-win32.whl", hash = "sha256:73309162a6a571d4cbd3b6a1dcc703c7311843ae0d1578df6f09be4e98df38d4"}, + {file = "yarl-1.23.0-cp314-cp314t-win_amd64.whl", hash = "sha256:4503053d296bc6e4cbd1fad61cf3b6e33b939886c4f249ba7c78b602214fabe2"}, + {file = "yarl-1.23.0-cp314-cp314t-win_arm64.whl", hash = "sha256:44bb7bef4ea409384e3f8bc36c063d77ea1b8d4a5b2706956c0d6695f07dcc25"}, + {file = "yarl-1.23.0-py3-none-any.whl", hash = "sha256:a2df6afe50dea8ae15fa34c9f824a3ee958d785fd5d089063d960bae1daa0a3f"}, + {file = "yarl-1.23.0.tar.gz", hash = "sha256:53b1ea6ca88ebd4420379c330aea57e258408dd0df9af0992e5de2078dc9f5d5"}, +] + +[package.dependencies] +idna = ">=2.0" +multidict = ">=4.0" +propcache = ">=0.2.1" + +[metadata] +lock-version = "2.1" +python-versions = ">=3.10,<4" +content-hash = "04a6cd14d36aa22596c43bbf957e95aebfce2143b21f69f00e03062a843dcc90" diff --git a/packages/sdk-py/pyproject.toml b/packages/sdk-py/pyproject.toml new file mode 100644 index 0000000..454bbfb --- /dev/null +++ b/packages/sdk-py/pyproject.toml @@ -0,0 +1,57 @@ +[project] +authors = [ + { name = "Akase Haruka", email = "light.tsing@gmail.com" }, +] +dependencies = [ + "web3>=7.0.0", + "httpx>=0.27.0", + "yarl>=1.23.0", + "eth-hash[pycryptodome]>=0.7.0", + "pycryptodome>=3.20.0", + "typing-extensions>=4.0.0", +] +description = "Python SDK for Universal Timestamps" +license = { text = "MIT OR Apache-2.0" } +name = "uts-python-sdk" +readme = "README.md" +requires-python = ">=3.10,<4" +version = "0.1.0" + +[build-system] +build-backend = "poetry.core.masonry.api" +requires = ["poetry-core>=2.0.0,<3.0.0"] + +[tool.poetry] +packages = [ + { include = "uts_sdk", from = "src" }, +] + +[tool.ruff] +line-length = 100 +target-version = "py310" + +[tool.ruff.lint] +select = ["E", "F", "I", "W", "UP", "B", "C4", "SIM"] + +[tool.mypy] +python_version = "3.10" +strict = true +warn_return_any = true +warn_unused_ignores = true + +[tool.pytest.ini_options] +asyncio_mode = "auto" +testpaths = ["tests"] + +[dependency-groups] +dev = [ + "pytest>=9.0.0", + "pytest-asyncio>=1.3.0", + "pytest-cov>=7.0.0", + "black>=26.0.0", + "ruff>=0.15.0", + "mypy>=1.19.0", + "pytest-cov (>=7.0.0,<8.0.0)", + "pytest-asyncio (>=1.3.0,<2.0.0)", + "socksio (>=1.0.0,<2.0.0)", +] diff --git a/packages/sdk-py/src/uts_sdk/__init__.py b/packages/sdk-py/src/uts_sdk/__init__.py new file mode 100644 index 0000000..668c0ac --- /dev/null +++ b/packages/sdk-py/src/uts_sdk/__init__.py @@ -0,0 +1,135 @@ +# packages/sdk-py/src/uts_sdk/__init__.py + +# MIT License +# +# Copyright (c) 2025 UTS Contributors +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +# +# Apache License, Version 2.0 +# +# Copyright (c) 2025 UTS Contributors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Universal Timestamps Python SDK.""" + +from __future__ import annotations + +from uts_sdk._codec import Decoder, Encoder +from uts_sdk._crypto import ( + MerkleProof, + SiblingNode, + UnorderedMerkleTree, + keccak256, + sha256, +) +from uts_sdk._types import ( + AppendStep, + Attestation, + AttestationStatus, + AttestationStatusKind, + AttestationStep, + BitcoinAttestation, + DetachedTimestamp, + DigestHeader, + DigestOp, + EASAttestation, + EASTimestamped, + ForkStep, + HexlifyStep, + Keccak256Step, + NodePosition, + OpCode, + PendingAttestation, + PrependStep, + PurgeResult, + ReverseStep, + RIPEMD160Step, + SHA1Step, + SHA256Step, + StampPhase, + Step, + Timestamp, + UnknownAttestation, + UpgradeResult, + UpgradeStatus, + VerifyStatus, +) +from uts_sdk.errors import DecodeError, EncodeError, RemoteError, UTSError, VerifyError +from uts_sdk.sdk import SDK, VerificationResult + +__version__ = "0.1.0" + +__all__ = [ + "SDK", + "VerificationResult", + "OpCode", + "Attestation", + "PendingAttestation", + "BitcoinAttestation", + "EASAttestation", + "EASTimestamped", + "UnknownAttestation", + "Timestamp", + "Step", + "DetachedTimestamp", + "DigestHeader", + "DigestOp", + "VerifyStatus", + "AttestationStatus", + "AttestationStatusKind", + "UpgradeStatus", + "UpgradeResult", + "PurgeResult", + "StampPhase", + "NodePosition", + "AttestationStep", + "AppendStep", + "PrependStep", + "SHA256Step", + "SHA1Step", + "RIPEMD160Step", + "Keccak256Step", + "ReverseStep", + "HexlifyStep", + "ForkStep", + "Encoder", + "Decoder", + "UnorderedMerkleTree", + "MerkleProof", + "SiblingNode", + "sha256", + "keccak256", + "UTSError", + "EncodeError", + "DecodeError", + "RemoteError", + "VerifyError", +] diff --git a/packages/sdk-py/src/uts_sdk/_codec/__init__.py b/packages/sdk-py/src/uts_sdk/_codec/__init__.py new file mode 100644 index 0000000..25379aa --- /dev/null +++ b/packages/sdk-py/src/uts_sdk/_codec/__init__.py @@ -0,0 +1,30 @@ +# packages/sdk-py/src/uts_sdk/_codec/__init__.py +"""Binary codec for OpenTimestamps format.""" + +from __future__ import annotations + +from .constants import ( + BITCOIN_TAG, + DIGEST_LENGTHS, + EAS_ATTEST_TAG, + EAS_TIMESTAMPED_TAG, + MAGIC_BYTES, + MAX_URI_LEN, + PENDING_TAG, + TAG_SIZE, +) +from .decoder import Decoder +from .encoder import Encoder + +__all__ = [ + "BITCOIN_TAG", + "EAS_ATTEST_TAG", + "EAS_TIMESTAMPED_TAG", + "MAGIC_BYTES", + "MAX_URI_LEN", + "PENDING_TAG", + "TAG_SIZE", + "DIGEST_LENGTHS", + "Decoder", + "Encoder", +] diff --git a/packages/sdk-py/src/uts_sdk/_codec/constants.py b/packages/sdk-py/src/uts_sdk/_codec/constants.py new file mode 100644 index 0000000..db73c4c --- /dev/null +++ b/packages/sdk-py/src/uts_sdk/_codec/constants.py @@ -0,0 +1,26 @@ +# packages/sdk-py/src/uts_sdk/_codec/constants.py +"""Binary protocol constants for OpenTimestamps format.""" + +from __future__ import annotations + +from typing import Final + +MAGIC_BYTES: Final[bytes] = ( + b"\x00OpenTimestamps\x00\x00Proof\x00\xbf\x89\xe2\xe8\x84\xe8\x92\x94" +) + +TAG_SIZE: Final[int] = 8 + +BITCOIN_TAG: Final[bytes] = b"\x05\x88\x96\x0d\x73\xd7\x19\x01" +PENDING_TAG: Final[bytes] = b"\x83\xdf\xe3\x0d\x2e\xf9\x0c\x8e" +EAS_ATTEST_TAG: Final[bytes] = b"\x8b\xf4\x6b\xf4\xcf\xd6\x74\xfa" +EAS_TIMESTAMPED_TAG: Final[bytes] = b"\x5a\xaf\xce\xeb\x1c\x7a\xd5\x8e" + +DIGEST_LENGTHS: Final[dict[str, int]] = { + "SHA256": 32, + "SHA1": 20, + "RIPEMD160": 20, + "KECCAK256": 32, +} + +MAX_URI_LEN: Final[int] = 1000 diff --git a/packages/sdk-py/src/uts_sdk/_codec/decoder.py b/packages/sdk-py/src/uts_sdk/_codec/decoder.py new file mode 100644 index 0000000..e213119 --- /dev/null +++ b/packages/sdk-py/src/uts_sdk/_codec/decoder.py @@ -0,0 +1,424 @@ +# packages/sdk-py/src/uts_sdk/_codec/decoder.py +"""Binary decoder for OpenTimestamps format.""" + +from __future__ import annotations + +import re +from typing import TYPE_CHECKING + +from uts_sdk._types import ( + AppendStep, + AttestationStep, + BitcoinAttestation, + DetachedTimestamp, + DigestHeader, + DigestOp, + EASAttestation, + EASTimestamped, + ForkStep, + HexlifyStep, + Keccak256Step, + OpCode, + PendingAttestation, + PrependStep, + ReverseStep, + RIPEMD160Step, + SHA1Step, + SHA256Step, + Step, + Timestamp, + UnknownAttestation, +) +from uts_sdk.errors import DecodeError, ErrorCode + +from .constants import ( + BITCOIN_TAG, + EAS_ATTEST_TAG, + EAS_TIMESTAMPED_TAG, + MAGIC_BYTES, + MAX_URI_LEN, + PENDING_TAG, + TAG_SIZE, +) + +if TYPE_CHECKING: + pass + +_SAFE_URL_RE = re.compile(r"^[a-zA-Z0-9.\-_/:]+$") + + +def _op_from_code(code: int) -> OpCode | None: + """Convert opcode byte to OpCode enum.""" + try: + return OpCode(code) + except ValueError: + return None + + +class Decoder: + """Binary decoder for OpenTimestamps proof format. + + Example: + decoder = Decoder(data) + version = decoder.read_magic() + header = decoder.read_header() + timestamp = decoder.read_timestamp() + """ + + __slots__ = ("_data", "_offset", "_length") + + def __init__(self, data: bytes) -> None: + self._data = data + self._offset = 0 + self._length = len(data) + + @property + def remaining(self) -> int: + """Number of bytes remaining to read.""" + return self._length - self._offset + + def _check_bounds(self, required: int) -> None: + if self._offset + required > self._length: + raise DecodeError( + ErrorCode.UNEXPECTED_EOF, + f"Unexpected end of stream: needed {required} bytes " + f"but only {self.remaining} available", + offset=self._offset, + ) + + def check_eof(self) -> None: + """Verify all bytes have been consumed.""" + if self.remaining > 0: + raise DecodeError( + ErrorCode.INVALID_STRUCTURE, + f"Expected end of stream but {self.remaining} bytes remain", + offset=self._offset, + ) + + def read_byte(self) -> int: + """Read a single byte.""" + self._check_bounds(1) + value = self._data[self._offset] + self._offset += 1 + return value + + def read_bytes(self, length: int) -> bytes: + """Read a fixed number of bytes.""" + self._check_bounds(length) + result = self._data[self._offset : self._offset + length] + self._offset += length + return result + + def read_u32(self) -> int: + """Read unsigned 32-bit integer using LEB128 decoding.""" + result = 0 + shift = 0 + + while True: + byte = self.read_byte() + result |= (byte & 0x7F) << shift + + if (byte & 0x80) == 0: + break + + shift += 7 + if shift > 35: + raise DecodeError( + ErrorCode.OVERFLOW, + "LEB128 varint too long for u32", + offset=self._offset, + ) + + if result > 0xFFFFFFFF: + raise DecodeError( + ErrorCode.OVERFLOW, + f"Value exceeds maximum for u32: {result}", + offset=self._offset, + ) + + return result + + def read_u64(self) -> int: + """Read unsigned 64-bit integer using LEB128 decoding.""" + result = 0 + shift = 0 + + while True: + byte = self.read_byte() + result |= (byte & 0x7F) << shift + + if (byte & 0x80) == 0: + break + + shift += 7 + if shift > 70: + raise DecodeError( + ErrorCode.OVERFLOW, + "LEB128 varint too long for u64", + offset=self._offset, + ) + + return result + + def read_length_prefixed(self) -> bytes: + """Read length-prefixed bytes.""" + length = self.read_u32() + return self.read_bytes(length) + + def peek_op(self) -> OpCode | None: + """Peek at the next opcode without consuming it.""" + if self.remaining == 0: + return None + return _op_from_code(self._data[self._offset]) + + def read_op(self) -> OpCode: + """Read and return an opcode.""" + code = self.read_byte() + op = _op_from_code(code) + if op is None: + raise DecodeError( + ErrorCode.UNKNOWN_OP, + f"Unknown opcode: 0x{code:02x}", + offset=self._offset - 1, + ) + return op + + def read_magic(self) -> int: + """Read and verify magic bytes, return version.""" + magic = self.read_bytes(len(MAGIC_BYTES)) + if magic != MAGIC_BYTES: + raise DecodeError( + ErrorCode.BAD_MAGIC, + f"Invalid magic bytes: expected {MAGIC_BYTES.hex()}, got {magic.hex()}", + offset=self._offset - len(MAGIC_BYTES), + ) + return self.read_byte() + + def read_header(self) -> DigestHeader: + """Read a digest header.""" + op = self.read_op() + + if op not in (OpCode.SHA256, OpCode.SHA1, OpCode.RIPEMD160, OpCode.KECCAK256): + raise DecodeError( + ErrorCode.INVALID_STRUCTURE, + f"Expected digest op in header, got: {op.name}", + offset=self._offset - 1, + ) + + kind = { + OpCode.SHA256: DigestOp.SHA256, + OpCode.SHA1: DigestOp.SHA1, + OpCode.RIPEMD160: DigestOp.RIPEMD160, + OpCode.KECCAK256: DigestOp.KECCAK256, + }[op] + + lengths = { + DigestOp.SHA256: 32, + DigestOp.SHA1: 20, + DigestOp.RIPEMD160: 20, + DigestOp.KECCAK256: 32, + } + + digest = self.read_bytes(lengths[kind]) + return DigestHeader(kind=kind, digest=digest) + + def read_execution_step( + self, + ) -> ( + AppendStep + | PrependStep + | SHA256Step + | SHA1Step + | RIPEMD160Step + | Keccak256Step + | ReverseStep + | HexlifyStep + ): + """Read an execution step.""" + op = self.read_op() + + match op: + case OpCode.APPEND: + data = self.read_length_prefixed() + return AppendStep(data=data) + case OpCode.PREPEND: + data = self.read_length_prefixed() + return PrependStep(data=data) + case OpCode.SHA256: + return SHA256Step() + case OpCode.SHA1: + return SHA1Step() + case OpCode.RIPEMD160: + return RIPEMD160Step() + case OpCode.KECCAK256: + return Keccak256Step() + case OpCode.REVERSE: + return ReverseStep() + case OpCode.HEXLIFY: + return HexlifyStep() + case OpCode.FORK | OpCode.ATTESTATION: + raise DecodeError( + ErrorCode.INVALID_STRUCTURE, + f"Unexpected {op.name} step in execution step", + offset=self._offset - 1, + ) + + def read_fork_step(self) -> ForkStep: + """Read a FORK step.""" + steps: list[Timestamp] = [] + + if self.peek_op() != OpCode.FORK: + raise DecodeError( + ErrorCode.INVALID_STRUCTURE, + f"Expected FORK op at the beginning of fork step, got: {self.peek_op()}", + offset=self._offset, + ) + + while True: + match self.peek_op(): + case OpCode.FORK: + self.read_op() + steps.append(self.read_timestamp()) + case _: + steps.append(self.read_timestamp()) + break + + return ForkStep(steps=steps) + + def read_pending_attestation(self) -> PendingAttestation: + """Read a pending attestation.""" + url_bytes = self.read_length_prefixed() + + try: + url = url_bytes.decode("utf-8") + except UnicodeDecodeError as e: + raise DecodeError( + ErrorCode.INVALID_URI, + f"Invalid UTF-8 in URL: {e}", + offset=self._offset, + ) from e + + if len(url) > MAX_URI_LEN: + raise DecodeError( + ErrorCode.INVALID_URI, + f"URL exceeds maximum length of {MAX_URI_LEN}: {len(url)}", + offset=self._offset, + ) + + if not _SAFE_URL_RE.match(url): + raise DecodeError( + ErrorCode.INVALID_URI, + f"Invalid URL format: {url}", + offset=self._offset, + ) + + return PendingAttestation(url=url) + + def read_bitcoin_attestation(self) -> BitcoinAttestation: + """Read a Bitcoin attestation.""" + height = self.read_u32() + return BitcoinAttestation(height=height) + + def read_eas_attestation(self) -> EASAttestation: + """Read an EAS attestation.""" + chain_id = self.read_u64() + uid = self.read_bytes(32) + return EASAttestation(chain_id=chain_id, uid=uid) + + def read_eas_timestamped(self) -> EASTimestamped: + """Read an EAS timestamped attestation.""" + chain_id = self.read_u64() + return EASTimestamped(chain_id=chain_id) + + def read_attestation_step(self, *, strict: bool = False) -> AttestationStep: + """Read an attestation step.""" + op = self.read_op() + + if op != OpCode.ATTESTATION: + raise DecodeError( + ErrorCode.INVALID_STRUCTURE, + f"Expected ATTESTATION op, got: {op.name}", + offset=self._offset - 1, + ) + + tag = self.read_bytes(TAG_SIZE) + data = self.read_length_prefixed() + + att: ( + PendingAttestation + | BitcoinAttestation + | EASAttestation + | EASTimestamped + | UnknownAttestation + ) + + match tag: + case bytes(t) if t == PENDING_TAG: + inner = Decoder(data) + att = inner.read_pending_attestation() + if strict: + inner.check_eof() + case bytes(t) if t == BITCOIN_TAG: + inner = Decoder(data) + att = inner.read_bitcoin_attestation() + if strict: + inner.check_eof() + case bytes(t) if t == EAS_ATTEST_TAG: + inner = Decoder(data) + att = inner.read_eas_attestation() + if strict: + inner.check_eof() + case bytes(t) if t == EAS_TIMESTAMPED_TAG: + inner = Decoder(data) + att = inner.read_eas_timestamped() + if strict: + inner.check_eof() + case _: + att = UnknownAttestation(tag=tag, data=data) + + return AttestationStep(attestation=att) + + def read_step(self, *, strict: bool = False) -> Step: + """Read any step type.""" + match self.peek_op(): + case OpCode.FORK: + return self.read_fork_step() + case OpCode.ATTESTATION: + return self.read_attestation_step(strict=strict) + case _: + return self.read_execution_step() + + def read_timestamp(self, *, strict: bool = False) -> Timestamp: + """Read a timestamp (sequence of steps ending with FORK or ATTESTATION).""" + steps: list[Step] = [] + + while self.remaining > 0: + step = self.read_step(strict=strict) + steps.append(step) + + match step: + case ForkStep() | AttestationStep(): + break + + return steps + + @classmethod + def decode_detached(cls, data: bytes, *, strict: bool = False) -> DetachedTimestamp: + """Decode a complete detached timestamp file.""" + decoder = cls(data) + + version = decoder.read_magic() + if version != 0x01: + raise DecodeError( + ErrorCode.INVALID_STRUCTURE, + f"Unsupported detached timestamp version: 0x{version:02x}", + offset=decoder._offset - 1, + ) + + header = decoder.read_header() + timestamp = decoder.read_timestamp(strict=strict) + + if strict: + decoder.check_eof() + + return DetachedTimestamp(header=header, timestamp=timestamp) diff --git a/packages/sdk-py/src/uts_sdk/_codec/encoder.py b/packages/sdk-py/src/uts_sdk/_codec/encoder.py new file mode 100644 index 0000000..20b71c7 --- /dev/null +++ b/packages/sdk-py/src/uts_sdk/_codec/encoder.py @@ -0,0 +1,241 @@ +# packages/sdk-py/src/uts_sdk/_codec/encoder.py +"""Binary encoder for OpenTimestamps format.""" + +from __future__ import annotations + +import re +from typing import TYPE_CHECKING + +from typing_extensions import Self + +from uts_sdk.errors import EncodeError, ErrorCode + +from .constants import ( + BITCOIN_TAG, + EAS_ATTEST_TAG, + EAS_TIMESTAMPED_TAG, + MAGIC_BYTES, + MAX_URI_LEN, + PENDING_TAG, +) + +if TYPE_CHECKING: + from uts_sdk._types import ( + AttestationStep, + BitcoinAttestation, + DetachedTimestamp, + DigestHeader, + EASAttestation, + EASTimestamped, + ExecutionStep, + ForkStep, + PendingAttestation, + Step, + Timestamp, + ) + +_SAFE_URL_RE = re.compile(r"^[a-zA-Z0-9.\-_/:]+$") + + +class Encoder: + """Binary encoder for OpenTimestamps proof format. + + Example: + encoder = Encoder() + encoder.write_u32(42).write_bytes(b"hello") + data = encoder.to_bytes() + """ + + __slots__ = ("_buffer", "_offset") + + def __init__(self, initial_size: int = 1024) -> None: + self._buffer = bytearray(initial_size) + self._offset = 0 + + def _ensure_capacity(self, required: int) -> None: + if self._offset + required > len(self._buffer): + new_size = max(len(self._buffer) * 2, self._offset + required) + self._buffer.extend(b"\x00" * (new_size - len(self._buffer))) + + def to_bytes(self) -> bytes: + return bytes(self._buffer[: self._offset]) + + def write_byte(self, value: int) -> Self: + if not 0 <= value <= 255: + raise EncodeError( + ErrorCode.OVERFLOW, + f"Byte value must be 0-255, got {value}", + ) + self._ensure_capacity(1) + self._buffer[self._offset] = value + self._offset += 1 + return self + + def write_bytes(self, data: bytes) -> Self: + self._ensure_capacity(len(data)) + self._buffer[self._offset : self._offset + len(data)] = data + self._offset += len(data) + return self + + def write_u32(self, value: int) -> Self: + if value < 0: + raise EncodeError( + ErrorCode.NEGATIVE_LEB128_INPUT, + f"LEB128 only supports non-negative integers, got {value}", + ) + if value > 0xFFFFFFFF: + raise EncodeError( + ErrorCode.OVERFLOW, + f"Value exceeds maximum for u32: {value}", + ) + + remaining = value + while True: + byte = remaining & 0x7F + remaining >>= 7 + if remaining != 0: + byte |= 0x80 + self.write_byte(byte) + if remaining == 0: + break + return self + + def write_u64(self, value: int) -> Self: + if value < 0: + raise EncodeError( + ErrorCode.NEGATIVE_LEB128_INPUT, + f"LEB128 only supports non-negative integers, got {value}", + ) + + remaining = value + while True: + byte = remaining & 0x7F + remaining >>= 7 + if remaining != 0: + byte |= 0x80 + self.write_byte(byte) + if remaining == 0: + break + return self + + def write_length_prefixed(self, data: bytes) -> Self: + self.write_u32(len(data)) + self.write_bytes(data) + return self + + def write_magic(self, version: int = 0x01) -> Self: + self.write_bytes(MAGIC_BYTES) + self.write_byte(version) + return self + + def write_header(self, header: DigestHeader) -> Self: + self.write_byte(header.kind.to_op_code().value) + self.write_bytes(header.digest) + return self + + def write_execution_step(self, step: ExecutionStep) -> Self: + from uts_sdk._types import AppendStep, PrependStep + + self.write_byte(step.op.value) + if isinstance(step, (AppendStep, PrependStep)): + self.write_length_prefixed(step.data) + return self + + def write_fork_step(self, step: ForkStep) -> Self: + for branch in step.steps[:-1]: + self.write_byte(0xFF) + self.write_timestamp(branch) + self.write_timestamp(step.steps[-1]) + return self + + def write_pending_attestation(self, att: PendingAttestation) -> Self: + url = att.url.rstrip("/") + if len(url) > MAX_URI_LEN: + raise EncodeError( + ErrorCode.INVALID_URI, + f"URL exceeds maximum length of {MAX_URI_LEN}: {len(url)}", + ) + if not _SAFE_URL_RE.match(url): + raise EncodeError( + ErrorCode.INVALID_URI, + f"Invalid URL format: {url}", + ) + self.write_length_prefixed(url.encode("utf-8")) + return self + + def write_bitcoin_attestation(self, att: BitcoinAttestation) -> Self: + self.write_u32(att.height) + return self + + def write_eas_attestation(self, att: EASAttestation | EASTimestamped) -> Self: + from uts_sdk._types import EASAttestation + + self.write_u64(att.chain_id) + if isinstance(att, EASAttestation): + self.write_bytes(att.uid) + return self + + def write_attestation_step(self, step: AttestationStep) -> Self: + from uts_sdk._types import ( + BitcoinAttestation, + EASAttestation, + EASTimestamped, + PendingAttestation, + UnknownAttestation, + ) + + self.write_byte(0x00) + + att = step.attestation + if isinstance(att, PendingAttestation): + self.write_bytes(PENDING_TAG) + inner = Encoder() + inner.write_pending_attestation(att) + self.write_length_prefixed(inner.to_bytes()) + elif isinstance(att, BitcoinAttestation): + self.write_bytes(BITCOIN_TAG) + inner = Encoder() + inner.write_bitcoin_attestation(att) + self.write_length_prefixed(inner.to_bytes()) + elif isinstance(att, EASAttestation): + self.write_bytes(EAS_ATTEST_TAG) + inner = Encoder() + inner.write_eas_attestation(att) + self.write_length_prefixed(inner.to_bytes()) + elif isinstance(att, EASTimestamped): + self.write_bytes(EAS_TIMESTAMPED_TAG) + inner = Encoder() + inner.write_eas_attestation(att) + self.write_length_prefixed(inner.to_bytes()) + elif isinstance(att, UnknownAttestation): + self.write_bytes(att.tag) + self.write_length_prefixed(att.data) + else: + raise EncodeError( + ErrorCode.GENERAL_ERROR, + f"Unsupported attestation type: {type(att).__name__}", + ) + return self + + def write_step(self, step: Step) -> Self: + from uts_sdk._types import AttestationStep, ForkStep + + if isinstance(step, ForkStep): + return self.write_fork_step(step) + elif isinstance(step, AttestationStep): + return self.write_attestation_step(step) + else: + return self.write_execution_step(step) + + def write_timestamp(self, timestamp: Timestamp) -> Self: + for step in timestamp: + self.write_step(step) + return self + + @classmethod + def encode_detached(cls, ots: DetachedTimestamp) -> bytes: + encoder = cls() + encoder.write_magic() + encoder.write_header(ots.header) + encoder.write_timestamp(ots.timestamp) + return encoder.to_bytes() diff --git a/packages/sdk-py/src/uts_sdk/_crypto/__init__.py b/packages/sdk-py/src/uts_sdk/_crypto/__init__.py new file mode 100644 index 0000000..8c01810 --- /dev/null +++ b/packages/sdk-py/src/uts_sdk/_crypto/__init__.py @@ -0,0 +1,18 @@ +# packages/sdk-py/src/uts_sdk/_crypto/__init__.py +"""Cryptographic utilities for UTS SDK.""" + +from __future__ import annotations + +from .merkle import MerkleProof, SiblingNode, UnorderedMerkleTree +from .utils import HashFunction, keccak256, ripemd160, sha1, sha256 + +__all__ = [ + "UnorderedMerkleTree", + "MerkleProof", + "SiblingNode", + "sha256", + "keccak256", + "sha1", + "ripemd160", + "HashFunction", +] diff --git a/packages/sdk-py/src/uts_sdk/_crypto/merkle.py b/packages/sdk-py/src/uts_sdk/_crypto/merkle.py new file mode 100644 index 0000000..19349af --- /dev/null +++ b/packages/sdk-py/src/uts_sdk/_crypto/merkle.py @@ -0,0 +1,198 @@ +# packages/sdk-py/src/uts_sdk/_crypto/merkle.py +"""Unordered Merkle tree implementation. + +This follows the UTS binary Merkle tree format where: +- Internal nodes are prefixed with 0x01 +- Leaves are sorted lexicographically +- The tree is built as a flat array structure for efficient proof generation +""" + +from __future__ import annotations + +from collections.abc import Callable, Sequence +from dataclasses import dataclass +from typing import overload + +from typing_extensions import Self + +from uts_sdk._types.status import NodePosition + + +@dataclass(frozen=True, slots=True) +class SiblingNode: + """A sibling node in a Merkle proof. + + Attributes: + position: The position indicates how to combine with sibling: + - LEFT: sibling is right child, APPEND sibling when computing parent + - RIGHT: sibling is left child, PREPEND sibling when computing parent + sibling: The sibling hash bytes + """ + + position: NodePosition + sibling: bytes + + +class MerkleProof(Sequence[SiblingNode]): + """A Merkle proof as a sequence of sibling nodes.""" + + __slots__ = ("_siblings",) + + def __init__(self, siblings: list[SiblingNode]) -> None: + self._siblings = siblings + + def __len__(self) -> int: + return len(self._siblings) + + @overload + def __getitem__(self, index: int) -> SiblingNode: ... + + @overload + def __getitem__(self, index: slice) -> Sequence[SiblingNode]: ... + + def __getitem__(self, index: int | slice) -> SiblingNode | Sequence[SiblingNode]: + return self._siblings[index] + + +INTERNAL_PREFIX = b"\x01" + + +def _next_power_of_two(n: int) -> int: + """Return the next power of two >= n.""" + if n <= 1: + return 1 + p = 1 + while p < n: + p *= 2 + return p + + +class UnorderedMerkleTree: + """Flat, fixed-size Merkle tree with sorted leaves. + + This implementation uses a flat array structure where: + - Index 0 is unused + - Leaves start at index `len` (the tree size, which is a power of two) + - Internal nodes fill indices 1 to len-1 + + This allows O(log n) proof generation without recomputing the tree. + """ + + __slots__ = ("_nodes", "_len", "_leaf_indices", "_hash_func") + + def __init__( + self, + nodes: list[bytes], + tree_len: int, + leaf_indices: dict[bytes, int], + hash_func: Callable[[bytes], bytes], + ) -> None: + self._nodes = nodes + self._len = tree_len + self._leaf_indices = leaf_indices + self._hash_func = hash_func + + @classmethod + def from_leaves( + cls, + leaves: Sequence[bytes], + hash_func: Callable[[bytes], bytes], + ) -> Self: + """Build a Merkle tree from pre-hashed leaves. + + Leaves must already be hashed digests (e.g., output of SHA256/Keccak256). + This matches the Rust BMT API where leaves are `Output` (hash outputs). + """ + if len(leaves) == 0: + raise ValueError("Merkle tree must have at least one leaf") + + raw_len = len(leaves) + tree_len = _next_power_of_two(raw_len) + nodes = [b"\x00" * 32] * (2 * tree_len) + + # Sort leaves lexicographically (they're already hashed digests) + sorted_leaves = sorted(leaves) + + # Map leaf -> index in sorted leaves + leaf_indices: dict[bytes, int] = {} + for i, leaf in enumerate(sorted_leaves): + nodes[tree_len + i] = leaf + leaf_indices[leaf] = i + + # Pad remaining leaf slots with zeros + for i in range(raw_len, tree_len): + nodes[tree_len + i] = b"\x00" * 32 + + # Build internal nodes (from bottom to top) + for i in range(tree_len - 1, 0, -1): + left = nodes[2 * i] + right = nodes[2 * i + 1] + combined = INTERNAL_PREFIX + left + right + nodes[i] = hash_func(combined) + + return cls(nodes, tree_len, leaf_indices, hash_func) + + @property + def root(self) -> bytes: + return self._nodes[1] + + @property + def leaves(self) -> tuple[bytes, ...]: + return tuple(self._leaf_indices.keys()) + + def __contains__(self, leaf: bytes) -> bool: + return leaf in self._leaf_indices + + def proof_for(self, leaf: bytes) -> MerkleProof | None: + """Generate a Merkle proof for the given leaf. + + Returns None if the leaf is not in the tree. + """ + if leaf not in self._leaf_indices: + return None + + siblings: list[SiblingNode] = [] + current = self._len + self._leaf_indices[leaf] + + while current > 1: + is_left = (current & 1) == 0 + # When current is left child (even), position is LEFT (sibling is right) + # When current is right child (odd), position is RIGHT (sibling is left) + position = NodePosition.LEFT if is_left else NodePosition.RIGHT + sibling_index = current ^ 1 # XOR to get sibling + siblings.append( + SiblingNode(position=position, sibling=self._nodes[sibling_index]) + ) + current >>= 1 + + return MerkleProof(siblings) + + def __bytes__(self) -> bytes: + result = bytearray() + result.extend(len(self._leaf_indices).to_bytes(4, "big")) + for leaf in self._leaf_indices: + result.extend(len(leaf).to_bytes(4, "big")) + result.extend(leaf) + result.extend(self.root) + return bytes(result) + + @classmethod + def from_bytes( + cls, + data: bytes, + hash_func: Callable[[bytes], bytes], + ) -> Self: + """Reconstruct tree from serialized bytes.""" + offset = 0 + n = int.from_bytes(data[offset : offset + 4], "big") + offset += 4 + + leaves = [] + for _ in range(n): + leaf_len = int.from_bytes(data[offset : offset + 4], "big") + offset += 4 + leaf = data[offset : offset + leaf_len] + offset += leaf_len + leaves.append(leaf) + + return cls.from_leaves(leaves, hash_func) diff --git a/packages/sdk-py/src/uts_sdk/_crypto/utils.py b/packages/sdk-py/src/uts_sdk/_crypto/utils.py new file mode 100644 index 0000000..5f79a3a --- /dev/null +++ b/packages/sdk-py/src/uts_sdk/_crypto/utils.py @@ -0,0 +1,47 @@ +# packages/sdk-py/src/uts_sdk/_crypto/utils.py +"""Cryptographic hash utilities.""" + +from __future__ import annotations + +import hashlib +from collections.abc import Callable + +try: + from Crypto.Hash import keccak + + def keccak256(data: bytes) -> bytes: + """Compute Keccak-256 hash (used by Ethereum). + + Note: This is NOT SHA3-256. Keccak-256 uses the original Keccak + permutation before NIST added domain separation for SHA3. + """ + k = keccak.new(digest_bits=256) + k.update(data) + return k.digest() + +except ImportError: + from web3 import Web3 + + _w3 = Web3() + + def keccak256(data: bytes) -> bytes: + """Compute Keccak-256 hash (used by Ethereum).""" + return _w3.keccak(data) + + +def sha256(data: bytes) -> bytes: + """Compute SHA-256 hash.""" + return hashlib.sha256(data).digest() + + +def ripemd160(data: bytes) -> bytes: + """Compute RIPEMD-160 hash.""" + return hashlib.new("ripemd160", data).digest() + + +def sha1(data: bytes) -> bytes: + """Compute SHA-1 hash (deprecated, not secure).""" + return hashlib.sha1(data).digest() + + +HashFunction = Callable[[bytes], bytes] diff --git a/packages/sdk-py/src/uts_sdk/_ethereum/__init__.py b/packages/sdk-py/src/uts_sdk/_ethereum/__init__.py new file mode 100644 index 0000000..8e7a735 --- /dev/null +++ b/packages/sdk-py/src/uts_sdk/_ethereum/__init__.py @@ -0,0 +1,13 @@ +# packages/sdk-py/src/uts_sdk/_ethereum/__init__.py +"""Ethereum integration for UTS SDK.""" + +from __future__ import annotations + +from .eas import EAS_SCHEMA_ID, NO_EXPIRATION, EasContract, OnChainAttestation + +__all__ = [ + "EasContract", + "EAS_SCHEMA_ID", + "NO_EXPIRATION", + "OnChainAttestation", +] diff --git a/packages/sdk-py/src/uts_sdk/_ethereum/eas.py b/packages/sdk-py/src/uts_sdk/_ethereum/eas.py new file mode 100644 index 0000000..6aa80ec --- /dev/null +++ b/packages/sdk-py/src/uts_sdk/_ethereum/eas.py @@ -0,0 +1,115 @@ +# packages/sdk-py/src/uts_sdk/_ethereum/eas.py +"""Ethereum Attestation Service (EAS) integration.""" + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any, Generic, TypeVar + +from eth_typing import Address, ChecksumAddress +from web3 import AsyncBaseProvider, AsyncWeb3 + +NO_EXPIRATION = 0 + +EAS_SCHEMA_ID = "0x5c5b8b295ff43c8e442be11d569e94a4cd5476f5e23df0f71bdd408df6b9649c" + +_EAS_ABI: list[dict[str, Any]] = [ + { + "anonymous": False, + "inputs": [ + {"indexed": True, "name": "recipient", "type": "address"}, + {"indexed": True, "name": "attester", "type": "address"}, + {"indexed": False, "name": "uid", "type": "bytes32"}, + {"indexed": False, "name": "schema", "type": "bytes32"}, + ], + "name": "Attested", + "type": "event", + }, + { + "inputs": [{"name": "uid", "type": "bytes32"}], + "name": "getAttestation", + "outputs": [ + { + "components": [ + {"name": "uid", "type": "bytes32"}, + {"name": "schema", "type": "bytes32"}, + {"name": "time", "type": "uint64"}, + {"name": "expirationTime", "type": "uint64"}, + {"name": "revocationTime", "type": "uint64"}, + {"name": "refUID", "type": "bytes32"}, + {"name": "recipient", "type": "address"}, + {"name": "attester", "type": "address"}, + {"name": "revocable", "type": "bool"}, + {"name": "data", "type": "bytes"}, + ], + "name": "", + "type": "tuple", + } + ], + "stateMutability": "view", + "type": "function", + }, + { + "inputs": [{"name": "data", "type": "bytes32"}], + "name": "getTimestamp", + "outputs": [{"name": "", "type": "uint64"}], + "stateMutability": "view", + "type": "function", + }, +] + + +@dataclass(frozen=True, slots=True) +class OnChainAttestation: + """Attestation data from EAS contract.""" + + uid: str + schema: str + time: int + expiration_time: int + revocation_time: int + ref_uid: str + recipient: str + attester: str + revocable: bool + data: str + + +AsyncProviderT = TypeVar("AsyncProviderT", bound=AsyncBaseProvider) + + +class EasContract(Generic[AsyncProviderT]): + """Helper class for interacting with EAS contract.""" + + def __init__( + self, w3: AsyncWeb3[AsyncProviderT], eas_address: Address | ChecksumAddress + ) -> None: + self._w3 = w3 + self._contract = w3.eth.contract(address=eas_address, abi=_EAS_ABI) + + async def get_timestamp(self, data: bytes) -> int: + """Read timestamp from EAS contract using getTimestamp(data).""" + padded_data = data.ljust(32, b"\x00") if len(data) < 32 else data[:32] + + result = await self._contract.functions.getTimestamp(padded_data).call() + return int(result) + + async def get_attestation(self, uid: bytes) -> OnChainAttestation: + """Read attestation from EAS contract using getAttestation(uid).""" + if len(uid) != 32: + raise ValueError(f"UID must be 32 bytes, got {len(uid)}") + + result = await self._contract.functions.getAttestation(uid).call() + + return OnChainAttestation( + uid=result[0].hex(), + schema=result[1].hex(), + time=int(result[2]), + expiration_time=int(result[3]), + revocation_time=int(result[4]), + ref_uid=result[5].hex(), + recipient=result[6], + attester=result[7], + revocable=bool(result[8]), + data=result[9].hex() if isinstance(result[9], bytes) else result[9], + ) diff --git a/packages/sdk-py/src/uts_sdk/_rpc/__init__.py b/packages/sdk-py/src/uts_sdk/_rpc/__init__.py new file mode 100644 index 0000000..cf650fa --- /dev/null +++ b/packages/sdk-py/src/uts_sdk/_rpc/__init__.py @@ -0,0 +1,8 @@ +# packages/sdk-py/src/uts_sdk/_rpc/__init__.py +"""RPC clients for UTS SDK.""" + +from __future__ import annotations + +from .bitcoin import BitcoinBlockHeader, BitcoinRPC + +__all__ = ["BitcoinRPC", "BitcoinBlockHeader"] diff --git a/packages/sdk-py/src/uts_sdk/_rpc/bitcoin.py b/packages/sdk-py/src/uts_sdk/_rpc/bitcoin.py new file mode 100644 index 0000000..01bfb6e --- /dev/null +++ b/packages/sdk-py/src/uts_sdk/_rpc/bitcoin.py @@ -0,0 +1,134 @@ +# packages/sdk-py/src/uts_sdk/_rpc/bitcoin.py +"""Bitcoin RPC client for verifying Bitcoin attestations.""" + +from __future__ import annotations + +import json +from dataclasses import dataclass +from types import TracebackType +from typing import Any +from urllib.parse import urlparse + +import httpx + +from uts_sdk.errors import RemoteError + + +@dataclass(frozen=True, slots=True) +class BitcoinBlockHeader: + """Bitcoin block header data.""" + + hash: str + confirmations: int + height: int + version: int + versionHex: str + merkleroot: str + time: int + mediantime: int + nonce: int + bits: str + difficulty: float + chainwork: str + nTx: int + previousblockhash: str | None = None + nextblockhash: str | None = None + + +class BitcoinRPC: + """Async client for Bitcoin JSON-RPC.""" + + def __init__(self, url: str = "https://bitcoin-rpc.publicnode.com") -> None: + parsed = urlparse(url) + if not parsed.scheme or not parsed.netloc: + raise ValueError(f"Invalid RPC URL: {url}") + self._url = url + self._client = httpx.AsyncClient(timeout=30.0) + + async def call(self, method: str, params: list[Any] | None = None) -> Any: + if params is None: + params = [] + + request_body = { + "jsonrpc": "1.0", + "method": method, + "params": params, + "id": 1, + } + + try: + response = await self._client.post( + self._url, + json=request_body, + headers={"Content-Type": "application/json"}, + ) + except httpx.RequestError as e: + raise RemoteError( + f"Bitcoin RPC network error: {e}", + context={"method": method, "params": params}, + ) from e + + try: + response_data = response.json() + except json.JSONDecodeError as e: + raise RemoteError( + "Bitcoin RPC invalid JSON response", + context={"status_code": response.status_code, "error": str(e)}, + ) from e + + if response.status_code >= 400: + raise RemoteError( + f"Bitcoin RPC HTTP error: {response.status_code}", + context={ + "status_code": response.status_code, + "response": response_data, + }, + ) + + if response_data.get("error") is not None: + error = response_data["error"] + raise RemoteError( + f"Bitcoin RPC error: {error.get('message', 'Unknown error')}", + context={"error_code": error.get("code"), "error_details": error}, + ) + + return response_data.get("result") + + async def get_block_hash(self, height: int) -> str: + result = await self.call("getblockhash", [height]) + return str(result) + + async def get_block_header(self, block_hash: str) -> BitcoinBlockHeader: + result = await self.call("getblockheader", [block_hash]) + + return BitcoinBlockHeader( + hash=result["hash"], + confirmations=result["confirmations"], + height=result["height"], + version=result["version"], + versionHex=result["versionHex"], + merkleroot=result["merkleroot"], + time=result["time"], + mediantime=result["mediantime"], + nonce=result["nonce"], + bits=result["bits"], + difficulty=result.get("difficulty", 1.0), + chainwork=result["chainwork"], + nTx=result["nTx"], + previousblockhash=result.get("previousblockhash"), + nextblockhash=result.get("nextblockhash"), + ) + + async def close(self) -> None: + await self._client.aclose() + + async def __aenter__(self) -> BitcoinRPC: + return self + + async def __aexit__( + self, + exc_type: type[BaseException] | None, + exc_val: BaseException | None, + exc_tb: TracebackType | None, + ) -> None: + await self.close() diff --git a/packages/sdk-py/src/uts_sdk/_types/__init__.py b/packages/sdk-py/src/uts_sdk/_types/__init__.py new file mode 100644 index 0000000..9e1526a --- /dev/null +++ b/packages/sdk-py/src/uts_sdk/_types/__init__.py @@ -0,0 +1,87 @@ +# packages/sdk-py/src/uts_sdk/_types/__init__.py +"""Core types for UTS SDK.""" + +from __future__ import annotations + +from .attestations import ( + Attestation, + BitcoinAttestation, + EASAttestation, + EASTimestamped, + PendingAttestation, + UnknownAttestation, + attestation_kind, +) +from .digest import DetachedTimestamp, DigestHeader, DigestOp, SecureDigestOp +from .ops import DIGEST_OPS, SECURE_DIGEST_OPS, OpCode +from .status import ( + AttestationStatus, + AttestationStatusKind, + NodePosition, + PurgeResult, + StampPhase, + UpgradeResult, + UpgradeStatus, + VerifyStatus, +) +from .timestamp_steps import ( + AppendStep, + AttestationStep, + DataStep, + ExecutionStep, + ForkStep, + HexlifyStep, + Keccak256Step, + PrependStep, + ReverseStep, + RIPEMD160Step, + SHA1Step, + SHA256Step, + Step, + Timestamp, + UnaryOp, +) +from .timestamp_steps import ( + DigestOp as DigestOpStep, +) + +__all__ = [ + "AppendStep", + "Attestation", + "AttestationStatus", + "AttestationStatusKind", + "AttestationStep", + "BitcoinAttestation", + "DataStep", + "DigestHeader", + "DigestOp", + "DigestOpStep", + "DetachedTimestamp", + "EASAttestation", + "EASTimestamped", + "ExecutionStep", + "ForkStep", + "HexlifyStep", + "Keccak256Step", + "NodePosition", + "OpCode", + "PendingAttestation", + "PrependStep", + "PurgeResult", + "RIPEMD160Step", + "ReverseStep", + "SHA1Step", + "SHA256Step", + "SecureDigestOp", + "StampPhase", + "Step", + "Timestamp", + "UnaryOp", + "UnknownAttestation", + "UpgradeResult", + "UpgradeStatus", + "VerifyStatus", + "attestation_kind", + "DIGEST_OPS", + "SECURE_DIGEST_OPS", +] diff --git a/packages/sdk-py/src/uts_sdk/_types/attestations.py b/packages/sdk-py/src/uts_sdk/_types/attestations.py new file mode 100644 index 0000000..efe3fd9 --- /dev/null +++ b/packages/sdk-py/src/uts_sdk/_types/attestations.py @@ -0,0 +1,88 @@ +# packages/sdk-py/src/uts_sdk/_types/attestations.py +"""Attestation types representing proof that data existed at a given time.""" + +from __future__ import annotations + +from dataclasses import dataclass +from typing import TYPE_CHECKING, Literal + +if TYPE_CHECKING: + pass + + +@dataclass(frozen=True, slots=True, kw_only=True) +class PendingAttestation: + """Attestation that is not yet confirmed, pointing to a calendar server.""" + + url: str + kind: Literal["pending"] = "pending" + + def __post_init__(self) -> None: + if len(self.url) > 1000: + raise ValueError(f"URL exceeds maximum length of 1000: {len(self.url)}") + + +@dataclass(frozen=True, slots=True, kw_only=True) +class BitcoinAttestation: + """Attestation confirmed in a Bitcoin block.""" + + height: int + kind: Literal["bitcoin"] = "bitcoin" + + def __post_init__(self) -> None: + if self.height < 0: + raise ValueError(f"Block height must be non-negative: {self.height}") + + +@dataclass(frozen=True, slots=True, kw_only=True) +class EASAttestation: + """Attestation via Ethereum Attestation Service attestation.""" + + chain_id: int + uid: bytes + kind: Literal["eas-attestation"] = "eas-attestation" + + def __post_init__(self) -> None: + if len(self.uid) != 32: + raise ValueError(f"EAS UID must be 32 bytes, got {len(self.uid)}") + + +@dataclass(frozen=True, slots=True, kw_only=True) +class EASTimestamped: + """Attestation via EAS timestamp log.""" + + chain_id: int + kind: Literal["eas-timestamped"] = "eas-timestamped" + + +@dataclass(frozen=True, slots=True, kw_only=True) +class UnknownAttestation: + """Attestation with an unrecognized tag.""" + + tag: bytes + data: bytes + kind: Literal["unknown"] = "unknown" + + +Attestation = ( + PendingAttestation + | BitcoinAttestation + | EASAttestation + | EASTimestamped + | UnknownAttestation +) + + +def attestation_kind(att: Attestation) -> str: + """Get the kind string from an attestation.""" + match att: + case PendingAttestation(): + return "pending" + case BitcoinAttestation(): + return "bitcoin" + case EASAttestation(): + return "eas-attestation" + case EASTimestamped(): + return "eas-timestamped" + case UnknownAttestation(): + return "unknown" diff --git a/packages/sdk-py/src/uts_sdk/_types/digest.py b/packages/sdk-py/src/uts_sdk/_types/digest.py new file mode 100644 index 0000000..1f0b7ae --- /dev/null +++ b/packages/sdk-py/src/uts_sdk/_types/digest.py @@ -0,0 +1,65 @@ +# packages/sdk-py/src/uts_sdk/_types/digest.py +"""Digest types for timestamp headers.""" + +from __future__ import annotations + +from dataclasses import dataclass +from enum import Enum +from typing import TYPE_CHECKING, Literal + +from .ops import OpCode + +if TYPE_CHECKING: + from .timestamp_steps import Timestamp + + +class DigestOp(str, Enum): + """Supported digest operations.""" + + SHA256 = "SHA256" + SHA1 = "SHA1" + RIPEMD160 = "RIPEMD160" + KECCAK256 = "KECCAK256" + + def to_op_code(self) -> OpCode: + """Convert to binary operation code.""" + mapping = { + DigestOp.SHA256: OpCode.SHA256, + DigestOp.SHA1: OpCode.SHA1, + DigestOp.RIPEMD160: OpCode.RIPEMD160, + DigestOp.KECCAK256: OpCode.KECCAK256, + } + return mapping[self] + + +SecureDigestOp = Literal[DigestOp.SHA256, DigestOp.KECCAK256] + + +@dataclass(frozen=True, slots=True, kw_only=True) +class DigestHeader: + """Header containing digest algorithm and value.""" + + digest: bytes + kind: DigestOp + + def __post_init__(self) -> None: + expected_lengths = { + DigestOp.SHA256: 32, + DigestOp.SHA1: 20, + DigestOp.RIPEMD160: 20, + DigestOp.KECCAK256: 32, + } + expected = expected_lengths[self.kind] + if len(self.digest) != expected: + raise ValueError( + f"Digest length mismatch for {self.kind.value}: " + f"expected {expected}, got {len(self.digest)}" + ) + + +@dataclass(frozen=True, slots=True, kw_only=True) +class DetachedTimestamp: + """A detached timestamp file containing digest header and proof steps.""" + + header: DigestHeader + timestamp: Timestamp diff --git a/packages/sdk-py/src/uts_sdk/_types/ops.py b/packages/sdk-py/src/uts_sdk/_types/ops.py new file mode 100644 index 0000000..e50e6a7 --- /dev/null +++ b/packages/sdk-py/src/uts_sdk/_types/ops.py @@ -0,0 +1,40 @@ +# packages/sdk-py/src/uts_sdk/_types/ops.py +"""Operation codes for timestamp steps.""" + +from __future__ import annotations + +from enum import IntEnum, unique +from typing import Final + + +@unique +class OpCode(IntEnum): + """Binary operation codes for timestamp steps.""" + + ATTESTATION = 0x00 + SHA1 = 0x02 + RIPEMD160 = 0x03 + SHA256 = 0x08 + KECCAK256 = 0x67 + APPEND = 0xF0 + PREPEND = 0xF1 + REVERSE = 0xF2 + HEXLIFY = 0xF3 + FORK = 0xFF + + +DIGEST_OPS: Final[frozenset[OpCode]] = frozenset( + { + OpCode.SHA256, + OpCode.SHA1, + OpCode.RIPEMD160, + OpCode.KECCAK256, + } +) + +SECURE_DIGEST_OPS: Final[frozenset[OpCode]] = frozenset( + { + OpCode.SHA256, + OpCode.KECCAK256, + } +) diff --git a/packages/sdk-py/src/uts_sdk/_types/status.py b/packages/sdk-py/src/uts_sdk/_types/status.py new file mode 100644 index 0000000..984add2 --- /dev/null +++ b/packages/sdk-py/src/uts_sdk/_types/status.py @@ -0,0 +1,86 @@ +# packages/sdk-py/src/uts_sdk/_types/status.py +"""Status enums and result types for timestamp verification.""" + +from __future__ import annotations + +from dataclasses import dataclass +from enum import Enum +from typing import TYPE_CHECKING, Any + +from .attestations import Attestation, PendingAttestation + +if TYPE_CHECKING: + from .timestamp_steps import Timestamp + + +class VerifyStatus(str, Enum): + """Overall verification status for a timestamp.""" + + VALID = "VALID" + PARTIAL_VALID = "PARTIAL_VALID" + INVALID = "INVALID" + PENDING = "PENDING" + + +class UpgradeStatus(str, Enum): + """Status of upgrading a pending attestation.""" + + UPGRADED = "UPGRADED" + PENDING = "PENDING" + FAILED = "FAILED" + + +class AttestationStatusKind(str, Enum): + """Status of a single attestation verification.""" + + VALID = "VALID" + INVALID = "INVALID" + PENDING = "PENDING" + UNKNOWN = "UNKNOWN" + + +class StampPhase(str, Enum): + """Phase of a stamping operation.""" + + QUEUED = "QUEUED" + BATCHING = "BATCHING" + AGGREGATING = "AGGREGATING" + ATTESTING = "ATTESTING" + CONFIRMING = "CONFIRMING" + COMPLETED = "COMPLETED" + FAILED = "FAILED" + + +class NodePosition(str, Enum): + """Position of a node in a Merkle tree.""" + + LEFT = "LEFT" + RIGHT = "RIGHT" + + +@dataclass(frozen=True, slots=True, kw_only=True) +class AttestationStatus: + """Result of attestation verification.""" + + attestation: Attestation + status: AttestationStatusKind + error: Exception | None = None + additional_info: dict[str, Any] | None = None + + +@dataclass(frozen=True, slots=True, kw_only=True) +class UpgradeResult: + """Result of upgrading a pending attestation.""" + + status: UpgradeStatus + original: PendingAttestation + upgraded: Timestamp | None = None + error: Exception | None = None + + +@dataclass(frozen=True, slots=True, kw_only=True) +class PurgeResult: + """Result of purging pending attestations from a timestamp.""" + + purged: list[str] + has_remaining: bool diff --git a/packages/sdk-py/src/uts_sdk/_types/timestamp_steps.py b/packages/sdk-py/src/uts_sdk/_types/timestamp_steps.py new file mode 100644 index 0000000..f37ac66 --- /dev/null +++ b/packages/sdk-py/src/uts_sdk/_types/timestamp_steps.py @@ -0,0 +1,101 @@ +# packages/sdk-py/src/uts_sdk/_types/timestamp_steps.py +"""Timestamp step types for building timestamp proofs.""" + +from __future__ import annotations + +from dataclasses import dataclass +from typing import TYPE_CHECKING, Literal + +from .attestations import Attestation +from .ops import OpCode + +if TYPE_CHECKING: + pass + + +@dataclass(frozen=True, slots=True, kw_only=True) +class AppendStep: + """Append data to the current digest.""" + + data: bytes + op: Literal[OpCode.APPEND] = OpCode.APPEND + + +@dataclass(frozen=True, slots=True, kw_only=True) +class PrependStep: + """Prepend data to the current digest.""" + + data: bytes + op: Literal[OpCode.PREPEND] = OpCode.PREPEND + + +@dataclass(frozen=True, slots=True) +class SHA256Step: + """SHA-256 hash operation.""" + + op: Literal[OpCode.SHA256] = OpCode.SHA256 + + +@dataclass(frozen=True, slots=True) +class SHA1Step: + """SHA-1 hash operation (deprecated, not secure).""" + + op: Literal[OpCode.SHA1] = OpCode.SHA1 + + +@dataclass(frozen=True, slots=True) +class RIPEMD160Step: + """RIPEMD-160 hash operation.""" + + op: Literal[OpCode.RIPEMD160] = OpCode.RIPEMD160 + + +@dataclass(frozen=True, slots=True) +class Keccak256Step: + """Keccak-256 hash operation.""" + + op: Literal[OpCode.KECCAK256] = OpCode.KECCAK256 + + +@dataclass(frozen=True, slots=True) +class ReverseStep: + """Reverse the byte order of the digest.""" + + op: Literal[OpCode.REVERSE] = OpCode.REVERSE + + +@dataclass(frozen=True, slots=True) +class HexlifyStep: + """Convert digest to hexadecimal string.""" + + op: Literal[OpCode.HEXLIFY] = OpCode.HEXLIFY + + +@dataclass(frozen=True, slots=True, kw_only=True) +class AttestationStep: + """Attestation step containing proof from a time-stamping authority.""" + + attestation: Attestation + op: Literal[OpCode.ATTESTATION] = OpCode.ATTESTATION + + +@dataclass(frozen=True, slots=True) +class ForkStep: + """Fork into multiple parallel proof paths.""" + + steps: list[Timestamp] + op: Literal[OpCode.FORK] = OpCode.FORK + + def __post_init__(self) -> None: + if len(self.steps) < 2: + raise ValueError( + f"FORK step must have at least 2 branches, got {len(self.steps)}" + ) + + +DigestOp = SHA256Step | SHA1Step | RIPEMD160Step | Keccak256Step +DataStep = AppendStep | PrependStep +UnaryOp = DigestOp | ReverseStep | HexlifyStep +ExecutionStep = DataStep | UnaryOp +Step = ExecutionStep | AttestationStep | ForkStep +Timestamp = list[Step] diff --git a/packages/sdk-py/src/uts_sdk/errors.py b/packages/sdk-py/src/uts_sdk/errors.py new file mode 100644 index 0000000..37eaee6 --- /dev/null +++ b/packages/sdk-py/src/uts_sdk/errors.py @@ -0,0 +1,80 @@ +# packages/sdk-py/src/uts_sdk/errors.py +"""Error types for UTS SDK.""" + +from __future__ import annotations + +from enum import Enum, auto +from typing import Any + + +class ErrorCode(Enum): + """Error codes for UTS operations.""" + + GENERAL_ERROR = auto() + BAD_MAGIC = auto() + UNKNOWN_OP = auto() + INVALID_STRUCTURE = auto() + NEGATIVE_LEB128_INPUT = auto() + OVERFLOW = auto() + INVALID_URI = auto() + LENGTH_MISMATCH = auto() + UNEXPECTED_EOF = auto() + REMOTE_ERROR = auto() + UNSUPPORTED_ATTESTATION = auto() + ATTESTATION_MISMATCH = auto() + + +class UTSError(Exception): + """Base exception for all UTS errors.""" + + __slots__ = ("code", "offset", "context", "_message") + + def __init__( + self, + code: ErrorCode, + message: str, + *, + offset: int | None = None, + context: dict[str, Any] | None = None, + ) -> None: + super().__init__(message) + self._message = message + self.code = code + self.offset = offset + self.context = context or {} + + def __repr__(self) -> str: + parts = [f"{self.__class__.__name__}(code={self.code.name}"] + if self.offset is not None: + parts.append(f", offset={self.offset}") + parts.append(f", message={self._message!r})") + return "".join(parts) + + +class EncodeError(UTSError): + """Error during binary encoding.""" + + pass + + +class DecodeError(UTSError): + """Error during binary decoding.""" + + pass + + +class RemoteError(UTSError): + """Error from remote calendar server or RPC.""" + + def __init__( + self, + message: str, + context: dict[str, Any] | None = None, + ) -> None: + super().__init__(ErrorCode.REMOTE_ERROR, message, context=context) + + +class VerifyError(UTSError): + """Error during timestamp verification.""" + + pass diff --git a/packages/sdk-py/src/uts_sdk/sdk.py b/packages/sdk-py/src/uts_sdk/sdk.py new file mode 100644 index 0000000..9e96d0e --- /dev/null +++ b/packages/sdk-py/src/uts_sdk/sdk.py @@ -0,0 +1,748 @@ +# packages/sdk-py/src/uts_sdk/sdk.py +"""Universal Timestamps SDK for Python.""" + +from __future__ import annotations + +import secrets +from collections.abc import Awaitable, Callable, Mapping, Sequence +from dataclasses import dataclass +from types import TracebackType +from typing import Literal + +import httpx +from eth_typing import Address, HexStr +from web3 import Web3 +from yarl import URL + +from uts_sdk._codec import Decoder +from uts_sdk._crypto import UnorderedMerkleTree +from uts_sdk._crypto.merkle import INTERNAL_PREFIX +from uts_sdk._crypto.utils import keccak256, sha256 +from uts_sdk._ethereum import ( + EAS_SCHEMA_ID, + NO_EXPIRATION, + EasContract, +) +from uts_sdk._rpc import BitcoinRPC +from uts_sdk._types import ( + AppendStep, + Attestation, + AttestationStatus, + AttestationStatusKind, + AttestationStep, + BitcoinAttestation, + DetachedTimestamp, + DigestHeader, + DigestOp, + EASAttestation, + EASTimestamped, + ForkStep, + Keccak256Step, + NodePosition, + PendingAttestation, + PrependStep, + PurgeResult, + RIPEMD160Step, + SHA1Step, + SHA256Step, + StampPhase, + Step, + Timestamp, + UpgradeResult, + UpgradeStatus, + VerifyStatus, +) +from uts_sdk._types.timestamp_steps import HexlifyStep, ReverseStep +from uts_sdk.errors import DecodeError, ErrorCode, RemoteError, VerifyError + +DEFAULT_CALENDARS = [ + "https://lgm1.calendar.test.timestamps.now/", + # Run by Peter Todd + "https://a.pool.opentimestamps.org/", + "https://b.pool.opentimestamps.org/", + # Run by Riccardo Casatta + "https://a.pool.eternitywall.com/", + # Run by Bull Bitcoin + "https://ots.btc.catallaxy.com/", +] + +DEFAULT_EAS_ADDRESSES: dict[int, Address] = { + 1: Address( + Web3.to_bytes(hexstr=HexStr("0xA1207F3BBa224E2c9c3c6D5aF63D0eb1582Ce587")) + ), + 11155111: Address( + Web3.to_bytes(hexstr=HexStr("0xC47300428b6AD2c7D03BB76D05A176058b47E6B0")) + ), + 534352: Address( + Web3.to_bytes(hexstr=HexStr("0xC47300428b6AD2c7D03BB76D05A176058b47E6B0")) + ), + 534351: Address( + Web3.to_bytes(hexstr=HexStr("0xaEF4103A04090071165F78D45D83A0C0782c2B2a")) + ), +} + + +@dataclass(frozen=True, slots=True) +class CalendarError: + """Error from a calendar server submission.""" + + url: str + error: str + + +@dataclass(frozen=True, slots=True) +class VerificationResult: + """Result of verifying a detached timestamp.""" + + status: VerifyStatus + attestations: list[AttestationStatus] + + @property + def is_valid(self) -> bool: + return self.status in (VerifyStatus.VALID, VerifyStatus.PARTIAL_VALID) + + @property + def is_pending(self) -> bool: + return self.status == VerifyStatus.PENDING + + +class SDK: + """Universal Timestamps SDK for Python. + + Usage: + async with SDK() as sdk: + result = await sdk.stamp(digests) + status = await sdk.verify(result[0]) + """ + + def __init__( + self, + *, + calendars: Sequence[str] | None = None, + btc_rpc_url: str = "https://bitcoin-rpc.publicnode.com", + eth_rpc_urls: Mapping[int, str] | None = None, + timeout: float = 10.0, + quorum: int | None = None, + nonce_size: int = 32, + hash_algorithm: Literal["sha256", "keccak256"] = "keccak256", + ) -> None: + self._calendars = [ + URL(str(c).rstrip("/") + "/") for c in (calendars or DEFAULT_CALENDARS) + ] + self._btc_rpc = BitcoinRPC(btc_rpc_url) + self._eth_rpc_urls = dict(eth_rpc_urls) if eth_rpc_urls else {} + self._timeout = timeout + self._nonce_size = nonce_size + self._quorum = quorum or max(1, int(len(self._calendars) * 0.66)) + self._hash_algorithm = hash_algorithm + + if hash_algorithm not in ("sha256", "keccak256"): + raise ValueError(f"Unsupported hash algorithm: {hash_algorithm}") + + @property + def calendars(self) -> list[str]: + return [str(c) for c in self._calendars] + + @property + def timeout(self) -> float: + return self._timeout + + @property + def nonce_size(self) -> int: + return self._nonce_size + + def _hash(self, data: bytes) -> bytes: + if self._hash_algorithm == "sha256": + return sha256(data) + return keccak256(data) + + async def __aenter__(self) -> SDK: + return self + + async def __aexit__( + self, + exc_type: type[BaseException] | None, + exc_val: BaseException | None, + exc_tb: TracebackType | None, + ) -> None: + await self._btc_rpc.close() + + @classmethod + def from_env(cls) -> SDK: + """Create SDK from environment variables.""" + import os + + calendars = os.environ.get("UTS_CALENDARS") + calendars_list = ( + [c.strip() for c in calendars.split(",")] if calendars else None + ) + + eth_rpc_urls: dict[int, str] = {} + for key, value in os.environ.items(): + if key.startswith("UTS_ETH_RPC_URL_"): + try: + chain_id = int(key[len("UTS_ETH_RPC_URL_") :]) + eth_rpc_urls[chain_id] = value + except ValueError: + pass + + timeout_str = os.environ.get("UTS_TIMEOUT", "10.0") + quorum_str = os.environ.get("UTS_QUORUM") + + hash_algo_env = os.environ.get("UTS_HASH_ALGORITHM", "keccak256") + if hash_algo_env == "sha256": + hash_algorithm: Literal["sha256", "keccak256"] = "sha256" + else: + hash_algorithm = "keccak256" + + return cls( + calendars=calendars_list, + btc_rpc_url=os.environ.get( + "UTS_BTC_RPC_URL", "https://bitcoin-rpc.publicnode.com" + ), + eth_rpc_urls=eth_rpc_urls or None, + timeout=float(timeout_str), + quorum=int(quorum_str) if quorum_str else None, + hash_algorithm=hash_algorithm, + ) + + async def stamp( + self, + *digests: DigestHeader | bytes, + on_progress: Callable[[StampPhase, float], Awaitable[None]] | None = None, + ) -> list[DetachedTimestamp]: + """Stamp digests by submitting to calendar servers.""" + import asyncio + + digest_headers = [ + ( + d + if isinstance(d, DigestHeader) + else DigestHeader(kind=DigestOp.SHA256, digest=d) + ) + for d in digests + ] + + if on_progress: + await on_progress(StampPhase.QUEUED, 0.0) + + nonces = [secrets.token_bytes(self._nonce_size) for _ in digest_headers] + nonce_digests = [ + self._hash(h.digest + n) + for h, n in zip(digest_headers, nonces, strict=True) + ] + + if on_progress: + await on_progress(StampPhase.BATCHING, 0.5) + + tree = UnorderedMerkleTree.from_leaves(nonce_digests, self._hash) + root = tree.root + + if on_progress: + await on_progress(StampPhase.AGGREGATING, 0.5) + + calendar_errors: list[CalendarError] = [] + + async def submit_to_calendar( + calendar: URL, + ) -> tuple[Timestamp | None, CalendarError | None]: + """Submit root to a calendar server. + + Returns (timestamp, None) on success, (None, error) on failure. + """ + try: + async with httpx.AsyncClient(timeout=self._timeout) as client: + response = await client.post( + str(calendar / "digest"), + content=root, + headers={"Accept": "application/vnd.opentimestamps.v1"}, + ) + if response.is_success: + decoder = Decoder(response.content) + ts = decoder.read_timestamp() + return ts, None + return None, CalendarError( + url=str(calendar), + error=f"HTTP {response.status_code}: {response.text[:200]}", + ) + except httpx.TimeoutException as e: + return None, CalendarError(url=str(calendar), error=f"Timeout: {e}") + except httpx.RequestError as e: + return None, CalendarError( + url=str(calendar), error=f"Network error: {e}" + ) + except DecodeError as e: + return None, CalendarError( + url=str(calendar), error=f"Decode error: {e}" + ) + + results = await asyncio.gather( + *[submit_to_calendar(c) for c in self._calendars] + ) + + successful: list[Timestamp] = [] + for ts, err in results: + if ts is not None: + successful.append(ts) + elif err is not None: + calendar_errors.append(err) + + if len(successful) < self._quorum: + error_details = "; ".join(f"{e.url}: {e.error}" for e in calendar_errors) + raise RemoteError( + f"Quorum not reached: {len(successful)}/{self._quorum} calendars responded. " + f"Errors: {error_details}" + ) + + merged: Timestamp = ( + successful[0] if len(successful) == 1 else [ForkStep(steps=successful)] + ) + + if on_progress: + await on_progress(StampPhase.ATTESTING, 1.0) + + # Build timestamps with proper Merkle proof steps + result_timestamps: list[DetachedTimestamp] = [] + hash_step = ( + SHA256Step() if self._hash_algorithm == "sha256" else Keccak256Step() + ) + + for i, header in enumerate(digest_headers): + # Start with: APPEND nonce, then HASH + steps: list[Step] = [ + AppendStep(data=nonces[i]), + hash_step, + ] + + # Add Merkle proof steps with inner-node prefix + proof = tree.proof_for(nonce_digests[i]) + if proof: + for node in proof: + if node.position == NodePosition.LEFT: + # Sibling is right child: PREPEND prefix, APPEND sibling, HASH + steps.extend( + [ + PrependStep(data=INTERNAL_PREFIX), + AppendStep(data=node.sibling), + hash_step, + ] + ) + else: + # Sibling is left child: PREPEND sibling, PREPEND prefix, HASH + steps.extend( + [ + PrependStep(data=node.sibling), + PrependStep(data=INTERNAL_PREFIX), + hash_step, + ] + ) + + steps.extend(merged) + result_timestamps.append(DetachedTimestamp(header=header, timestamp=steps)) + + if on_progress: + await on_progress(StampPhase.COMPLETED, 1.0) + + return result_timestamps + + async def verify(self, stamp: DetachedTimestamp) -> VerificationResult: + """Verify a detached timestamp.""" + statuses = await self._verify_timestamp(stamp.header.digest, stamp.timestamp) + return self._aggregate_result(statuses) + + async def upgrade( + self, + stamp: DetachedTimestamp, + *, + keep_pending: bool = False, + ) -> list[UpgradeResult]: + """Upgrade pending attestations in a timestamp.""" + return await self._upgrade_timestamp( + stamp.header.digest, stamp.timestamp, keep_pending + ) + + def list_pending(self, stamp: DetachedTimestamp) -> list[str]: + """List all pending attestation URLs in the given detached timestamp.""" + return self._collect_pending_attestations(stamp.timestamp) + + @staticmethod + def _collect_pending_attestations(timestamp: Timestamp) -> list[str]: + """Collect all pending attestation URIs from a timestamp.""" + uris: list[str] = [] + for step in timestamp: + match step: + case AttestationStep(attestation=att): + if isinstance(att, PendingAttestation): + uris.append(att.url) + case ForkStep(steps=branches): + for branch in branches: + uris.extend(SDK._collect_pending_attestations(branch)) + return uris + + def retain_attestations( + self, + stamp: DetachedTimestamp, + should_retain: Callable[[Attestation], bool], + ) -> bool: + """Retain only attestations matching the predicate, removing all others. + + This is analogous to Python's ``filter`` but operates on the attestation + leaves of the timestamp tree. FORK nodes left with a single branch after + filtering are collapsed. + + Args: + stamp: The detached timestamp to filter. + should_retain: Predicate receiving each attestation; return True to keep. + + Returns True if the timestamp still has attestations, False if all were removed. + """ + return self._retain_attestations_in_timestamp(stamp.timestamp, should_retain) + + @staticmethod + def _retain_attestations_in_timestamp( + timestamp: Timestamp, + should_retain: Callable[[Attestation], bool], + ) -> bool: + """Recursively retain attestations matching the predicate. + + Modifies the timestamp list in place. + Returns True if the timestamp still has content. + """ + i = len(timestamp) - 1 + while i >= 0: + step = timestamp[i] + match step: + case AttestationStep(attestation=att): + if not should_retain(att): + del timestamp[i] + case ForkStep(steps=branches): + j = len(branches) - 1 + while j >= 0: + if not SDK._retain_attestations_in_timestamp( + branches[j], should_retain + ): + del branches[j] + j -= 1 + if len(branches) == 0: + del timestamp[i] + elif len(branches) == 1: + # Collapse: expand the single remaining branch in place + timestamp[i : i + 1] = branches[0] + i -= 1 + return len(timestamp) > 0 + + def purge_pending( + self, + stamp: DetachedTimestamp, + *, + uris_to_purge: set[str] | None = None, + ) -> PurgeResult: + """Purge pending attestations from the given detached timestamp. + + This is a convenience wrapper around :meth:`retain_attestations` that + removes pending attestation branches from the timestamp tree in place. + + Args: + stamp: The detached timestamp to purge. + uris_to_purge: Optional set of URIs to selectively purge. If None, + all pending attestations are purged. + + Returns a PurgeResult with purged URIs and whether any non-pending attestations + remain. If all attestations were pending, the timestamp will be empty. + """ + all_pending = self.list_pending(stamp) + if not all_pending: + return PurgeResult(purged=[], has_remaining=True) + + purged_uris = ( + [u for u in all_pending if u in uris_to_purge] + if uris_to_purge is not None + else all_pending + ) + + if not purged_uris: + return PurgeResult(purged=[], has_remaining=True) + + def should_retain(att: Attestation) -> bool: + if not isinstance(att, PendingAttestation): + return True + if uris_to_purge is None: + return False + return att.url not in uris_to_purge + + has_remaining = self.retain_attestations(stamp, should_retain) + return PurgeResult(purged=purged_uris, has_remaining=has_remaining) + + async def _upgrade_timestamp( + self, + current: bytes, + timestamp: Timestamp, + keep_pending: bool, + ) -> list[UpgradeResult]: + """Recursively upgrade pending attestations.""" + results: list[UpgradeResult] = [] + + for i, step in enumerate(timestamp): + match step: + case ForkStep(steps=branches): + for branch in branches: + branch_results = await self._upgrade_timestamp( + current, branch, keep_pending + ) + results.extend(branch_results) + case AttestationStep(attestation=att): + if isinstance(att, PendingAttestation): + result = await self._upgrade_pending(current, att) + results.append(result) + if result.status == UpgradeStatus.UPGRADED and result.upgraded: + if keep_pending: + timestamp[i] = ForkStep(steps=[[step], result.upgraded]) + else: + timestamp[i : i + 1] = result.upgraded + case _: + current = self._execute_step(current, step) + + return results + + async def _upgrade_pending( + self, commitment: bytes, pending: PendingAttestation + ) -> UpgradeResult: + """Upgrade a single pending attestation.""" + + commitment_hex = commitment.hex() + try: + async with httpx.AsyncClient(timeout=self._timeout) as client: + response = await client.get( + f"{pending.url.rstrip('/')}/timestamp/{commitment_hex}", + headers={"Accept": "application/vnd.opentimestamps.v1"}, + ) + if response.status_code == 202 or response.status_code == 404: + return UpgradeResult(status=UpgradeStatus.PENDING, original=pending) + if response.is_success: + decoder = Decoder(response.content) + ts = decoder.read_timestamp() + return UpgradeResult( + status=UpgradeStatus.UPGRADED, original=pending, upgraded=ts + ) + return UpgradeResult( + status=UpgradeStatus.FAILED, + original=pending, + error=RemoteError(f"Calendar returned {response.status_code}"), + ) + except Exception as e: + return UpgradeResult(status=UpgradeStatus.FAILED, original=pending, error=e) + + async def _verify_timestamp( + self, current: bytes, timestamp: Timestamp + ) -> list[AttestationStatus]: + """Verify timestamp steps and return attestation statuses.""" + attestations: list[AttestationStatus] = [] + + for step in timestamp: + match step: + case ForkStep(steps=branches): + for branch in branches: + branch_results = await self._verify_timestamp(current, branch) + attestations.extend(branch_results) + case AttestationStep(attestation=att): + status = await self._verify_attestation(current, att) + attestations.append(status) + case _: + current = self._execute_step(current, step) + + return attestations + + def _execute_step(self, current: bytes, step: Step) -> bytes: + """Execute a single timestamp step.""" + import hashlib + + match step: + case AppendStep(data=data): + return current + data + case PrependStep(data=data): + return data + current + case ReverseStep(): + return current[::-1] + case HexlifyStep(): + return current.hex().encode() + case SHA256Step(): + return sha256(current) + case Keccak256Step(): + return keccak256(current) + case SHA1Step(): + return hashlib.sha1(current).digest() + case RIPEMD160Step(): + return hashlib.new("ripemd160", current).digest() + case _: + raise VerifyError( + ErrorCode.INVALID_STRUCTURE, + f"Unsupported step type: {type(step).__name__}", + ) + + async def _verify_attestation( + self, digest: bytes, attestation: Attestation + ) -> AttestationStatus: + """Verify a single attestation.""" + if isinstance(attestation, PendingAttestation): + return AttestationStatus( + attestation=attestation, + status=AttestationStatusKind.PENDING, + ) + elif isinstance(attestation, BitcoinAttestation): + return await self._verify_bitcoin(digest, attestation) + elif isinstance(attestation, (EASAttestation, EASTimestamped)): + return await self._verify_eas(digest, attestation) + else: + return AttestationStatus( + attestation=attestation, + status=AttestationStatusKind.UNKNOWN, + error=VerifyError( + ErrorCode.UNSUPPORTED_ATTESTATION, "Unknown attestation type" + ), + ) + + async def _verify_bitcoin( + self, digest: bytes, att: BitcoinAttestation + ) -> AttestationStatus: + """Verify a Bitcoin attestation.""" + try: + block_hash = await self._btc_rpc.get_block_hash(att.height) + header = await self._btc_rpc.get_block_header(block_hash) + + # Bitcoin displays hashes in reversed byte order (little-endian for display) + # The merkleroot from RPC is in display format, so reverse it for comparison + merkleroot_bytes = bytes.fromhex(header.merkleroot)[::-1] + + if digest != merkleroot_bytes: + return AttestationStatus( + attestation=att, + status=AttestationStatusKind.INVALID, + error=VerifyError( + ErrorCode.ATTESTATION_MISMATCH, + f"Merkle root mismatch at height {att.height}", + ), + ) + + return AttestationStatus( + attestation=att, + status=AttestationStatusKind.VALID, + additional_info={"header": header}, + ) + except Exception as e: + return AttestationStatus( + attestation=att, + status=AttestationStatusKind.UNKNOWN, + error=VerifyError(ErrorCode.REMOTE_ERROR, str(e)), + ) + + async def _verify_eas( + self, digest: bytes, att: EASAttestation | EASTimestamped + ) -> AttestationStatus: + """Verify an EAS attestation.""" + from web3 import AsyncWeb3 + + rpc_url = self._eth_rpc_urls.get(att.chain_id) + if not rpc_url: + return AttestationStatus( + attestation=att, + status=AttestationStatusKind.UNKNOWN, + error=VerifyError( + ErrorCode.GENERAL_ERROR, f"No RPC URL for chain {att.chain_id}" + ), + ) + + eas_address = DEFAULT_EAS_ADDRESSES.get(att.chain_id) + if not eas_address: + return AttestationStatus( + attestation=att, + status=AttestationStatusKind.UNKNOWN, + error=VerifyError( + ErrorCode.GENERAL_ERROR, f"No EAS address for chain {att.chain_id}" + ), + ) + + try: + w3 = AsyncWeb3(AsyncWeb3.AsyncHTTPProvider(rpc_url)) + eas = EasContract(w3, eas_address) + + if isinstance(att, EASTimestamped): + ts = await eas.get_timestamp(digest) + if ts == 0: + return AttestationStatus( + attestation=att, + status=AttestationStatusKind.INVALID, + error=VerifyError( + ErrorCode.ATTESTATION_MISMATCH, "No EAS timestamp found" + ), + ) + return AttestationStatus( + attestation=att, + status=AttestationStatusKind.VALID, + additional_info={"time": ts}, + ) + else: + on_chain = await eas.get_attestation(att.uid) + + if on_chain.schema != EAS_SCHEMA_ID[2:]: + return AttestationStatus( + attestation=att, + status=AttestationStatusKind.INVALID, + error=VerifyError( + ErrorCode.ATTESTATION_MISMATCH, "Schema mismatch" + ), + ) + + if on_chain.expiration_time != NO_EXPIRATION: + return AttestationStatus( + attestation=att, + status=AttestationStatusKind.INVALID, + error=VerifyError( + ErrorCode.ATTESTATION_MISMATCH, "Has expiration" + ), + ) + + if on_chain.revocable: + return AttestationStatus( + attestation=att, + status=AttestationStatusKind.INVALID, + error=VerifyError( + ErrorCode.ATTESTATION_MISMATCH, "Is revocable" + ), + ) + + return AttestationStatus( + attestation=att, + status=AttestationStatusKind.VALID, + additional_info={"attestation": on_chain}, + ) + except Exception as e: + return AttestationStatus( + attestation=att, + status=AttestationStatusKind.UNKNOWN, + error=VerifyError(ErrorCode.REMOTE_ERROR, str(e)), + ) + + def _aggregate_result( + self, statuses: list[AttestationStatus] + ) -> VerificationResult: + """Aggregate attestation statuses into overall verification result.""" + counts = dict.fromkeys(AttestationStatusKind, 0) + for s in statuses: + counts[s.status] += 1 + + if counts[AttestationStatusKind.VALID] > 0: + if ( + counts[AttestationStatusKind.INVALID] > 0 + or counts[AttestationStatusKind.UNKNOWN] > 0 + ): + status = VerifyStatus.PARTIAL_VALID + else: + status = VerifyStatus.VALID + elif counts[AttestationStatusKind.PENDING] > 0: + status = VerifyStatus.PENDING + elif counts[AttestationStatusKind.INVALID] > 0: + status = VerifyStatus.INVALID + else: + status = VerifyStatus.INVALID + + return VerificationResult(status=status, attestations=statuses) diff --git a/packages/sdk-py/tests/__init__.py b/packages/sdk-py/tests/__init__.py new file mode 100644 index 0000000..5d3a9d7 --- /dev/null +++ b/packages/sdk-py/tests/__init__.py @@ -0,0 +1 @@ +# packages/sdk-py/tests/__init__.py diff --git a/packages/sdk-py/tests/conftest.py b/packages/sdk-py/tests/conftest.py new file mode 100644 index 0000000..5673421 --- /dev/null +++ b/packages/sdk-py/tests/conftest.py @@ -0,0 +1,20 @@ +# packages/sdk-py/tests/conftest.py +"""Common fixtures for tests.""" + +from __future__ import annotations + +import pytest + + +@pytest.fixture +def sample_digest() -> bytes: + """A sample SHA-256 digest for testing.""" + import hashlib + + return hashlib.sha256(b"test data").digest() + + +@pytest.fixture +def sample_pending_url() -> str: + """A sample calendar URL for testing.""" + return "https://calendar.example.com" diff --git a/packages/sdk-py/tests/test_calendar_integration.py b/packages/sdk-py/tests/test_calendar_integration.py new file mode 100644 index 0000000..ca80343 --- /dev/null +++ b/packages/sdk-py/tests/test_calendar_integration.py @@ -0,0 +1,146 @@ +"""Integration tests for calendar server operations. + +These tests make real network calls to calendar servers. +Set UTS_CALENDARS environment variable or use defaults. +""" + +from __future__ import annotations + +import hashlib +import os + +import pytest + +from uts_sdk import ( + SDK, + AttestationStatusKind, + DetachedTimestamp, + DigestHeader, + DigestOp, + PendingAttestation, + UpgradeStatus, + VerifyStatus, +) + +DEFAULT_TEST_CALENDAR = "https://alice.btc.calendar.opentimestamps.org" + + +@pytest.fixture +def sdk() -> SDK: + """Create an SDK instance with real calendar servers.""" + calendars = os.environ.get("UTS_CALENDARS", DEFAULT_TEST_CALENDAR) + return SDK(calendars=calendars.split(","), timeout=30.0) + + +@pytest.fixture +def test_digest() -> bytes: + """Create a test digest.""" + return hashlib.sha256(b"Python SDK integration test").digest() + + +class TestCalendarIntegration: + """Real integration tests for calendar server operations.""" + + @pytest.mark.asyncio + async def test_stamp_and_verify(self, sdk: SDK, test_digest: bytes) -> None: + """Test stamping a digest and verifying the result.""" + header = DigestHeader(kind=DigestOp.SHA256, digest=test_digest) + + results = await sdk.stamp(header) + + assert len(results) == 1 + stamp = results[0] + assert isinstance(stamp, DetachedTimestamp) + assert stamp.header.digest == test_digest + + result = await sdk.verify(stamp) + + assert result.status == VerifyStatus.PENDING + assert len(result.attestations) >= 1 + assert all( + a.status == AttestationStatusKind.PENDING for a in result.attestations + ) + + @pytest.mark.asyncio + async def test_stamp_multiple_digests(self, sdk: SDK) -> None: + """Test stamping multiple digests at once.""" + digests = [hashlib.sha256(f"test data {i}".encode()).digest() for i in range(3)] + + results = await sdk.stamp(*digests) + + assert len(results) == 3 + for i, stamp in enumerate(results): + assert stamp.header.digest == digests[i] + + @pytest.mark.asyncio + async def test_stamp_bytes_input(self, sdk: SDK) -> None: + """Test stamping with raw bytes input (not DigestHeader).""" + digest = hashlib.sha256(b"raw bytes test").digest() + + results = await sdk.stamp(digest) + + assert len(results) == 1 + assert results[0].header.digest == digest + + @pytest.mark.asyncio + async def test_upgrade_pending_attestation( + self, sdk: SDK, test_digest: bytes + ) -> None: + """Test upgrading a pending attestation (will likely stay pending).""" + header = DigestHeader(kind=DigestOp.SHA256, digest=test_digest) + + results = await sdk.stamp(header) + stamp = results[0] + + upgrade_results = await sdk.upgrade(stamp) + + assert len(upgrade_results) >= 1 + for result in upgrade_results: + assert result.status in ( + UpgradeStatus.PENDING, + UpgradeStatus.UPGRADED, + UpgradeStatus.FAILED, + ) + + @pytest.mark.asyncio + async def test_roundtrip_encoding(self, sdk: SDK, test_digest: bytes) -> None: + """Test that stamps can be encoded and decoded.""" + from uts_sdk._codec import Decoder, Encoder + + header = DigestHeader(kind=DigestOp.SHA256, digest=test_digest) + + results = await sdk.stamp(header) + stamp = results[0] + + encoded = Encoder.encode_detached(stamp) + assert len(encoded) > 0 + + decoded = Decoder.decode_detached(encoded, strict=True) + assert decoded.header.digest == test_digest + + @pytest.mark.asyncio + async def test_context_manager(self, test_digest: bytes) -> None: + """Test SDK as async context manager.""" + async with SDK() as sdk: + results = await sdk.stamp(test_digest) + assert len(results) == 1 + + @pytest.mark.asyncio + async def test_verify_pending_attestation(self, sdk: SDK) -> None: + """Test verifying a manually created pending attestation.""" + digest = hashlib.sha256(b"manual pending test").digest() + header = DigestHeader(kind=DigestOp.SHA256, digest=digest) + + pending = PendingAttestation( + url="https://alice.btc.calendar.opentimestamps.org" + ) + from uts_sdk import AttestationStep + + step = AttestationStep(attestation=pending) + stamp = DetachedTimestamp(header=header, timestamp=[step]) + + result = await sdk.verify(stamp) + + assert result.status == VerifyStatus.PENDING + assert result.is_pending + assert not result.is_valid diff --git a/packages/sdk-py/tests/test_codec.py b/packages/sdk-py/tests/test_codec.py new file mode 100644 index 0000000..e408ba8 --- /dev/null +++ b/packages/sdk-py/tests/test_codec.py @@ -0,0 +1,87 @@ +"""Tests for codec round-trip.""" + +from __future__ import annotations + +import hashlib + +from uts_sdk._codec import Decoder, Encoder +from uts_sdk._types import ( + AppendStep, + AttestationStep, + BitcoinAttestation, + DetachedTimestamp, + DigestHeader, + DigestOp, + PendingAttestation, + Timestamp, +) + + +def test_encode_decode_simple_timestamp() -> None: + digest = hashlib.sha256(b"hello world").digest() + header = DigestHeader(kind=DigestOp.SHA256, digest=digest) + + att = PendingAttestation(url="https://calendar.example.com") + step = AttestationStep(attestation=att) + timestamp: Timestamp = [step] + + ots = DetachedTimestamp(header=header, timestamp=timestamp) + + encoded = Encoder.encode_detached(ots) + decoded = Decoder.decode_detached(encoded, strict=True) + + assert decoded.header.kind == DigestOp.SHA256 + assert decoded.header.digest == digest + assert len(decoded.timestamp) == 1 + assert isinstance(decoded.timestamp[0], AttestationStep) + att_step = decoded.timestamp[0] + assert isinstance(att_step.attestation, PendingAttestation) + + +def test_encode_decode_append_step() -> None: + digest = hashlib.sha256(b"test").digest() + header = DigestHeader(kind=DigestOp.SHA256, digest=digest) + + append = AppendStep(data=b"\x00\x01\x02\x03") + att = BitcoinAttestation(height=800000) + step = AttestationStep(attestation=att) + timestamp: Timestamp = [append, step] + + ots = DetachedTimestamp(header=header, timestamp=timestamp) + + encoded = Encoder.encode_detached(ots) + decoded = Decoder.decode_detached(encoded, strict=True) + + assert len(decoded.timestamp) == 2 + assert isinstance(decoded.timestamp[0], AppendStep) + assert decoded.timestamp[0].data == b"\x00\x01\x02\x03" + + +def test_leb128_encoding() -> None: + encoder = Encoder() + encoder.write_u32(0) + encoder.write_u32(127) + encoder.write_u32(128) + encoder.write_u32(300) + + data = encoder.to_bytes() + decoder = Decoder(data) + + assert decoder.read_u32() == 0 + assert decoder.read_u32() == 127 + assert decoder.read_u32() == 128 + assert decoder.read_u32() == 300 + + +def test_leb64_encoding() -> None: + encoder = Encoder() + encoder.write_u64(0) + encoder.write_u64(2**32) + encoder.write_u64(2**50) + + data = encoder.to_bytes() + decoder = Decoder(data) + + assert decoder.read_u64() == 0 + assert decoder.read_u64() == 2**32 + assert decoder.read_u64() == 2**50 diff --git a/packages/sdk-py/tests/test_cross_sdk.py b/packages/sdk-py/tests/test_cross_sdk.py new file mode 100644 index 0000000..4653345 --- /dev/null +++ b/packages/sdk-py/tests/test_cross_sdk.py @@ -0,0 +1,67 @@ +"""Cross-SDK compatibility tests using Rust SDK test vectors. + +These tests verify that the Python SDK produces identical results to the Rust SDK +for encoding, decoding, and cryptographic operations. +""" + +from __future__ import annotations + +from uts_sdk._codec import Decoder, Encoder +from uts_sdk._types import DigestOp, ForkStep + +SMALL_DETACHED_TIMESTAMP = bytes( + b"\x00\x4f\x70\x65\x6e\x54\x69\x6d\x65\x73\x74\x61\x6d\x70\x73\x00\x00\x50\x72\x6f\x6f\x66\x00\xbf\x89\xe2\xe8\x84\xe8\x92" + b"\x94\x01\x08\xa7\x0d\xfe\x69\xc5\xa0\xd6\x28\x16\x78\x1a\xbb\x6e\x17\x77\x85\x47\x18\x62\x4a\x0d\x19\x42\x31\xad\xb1\x4c" + b"\x32\xee\x54\x38\xa4\xf0\x10\x7a\x46\x05\xde\x0a\x5b\x37\xcb\x21\x17\x59\xc6\x81\x2b\xfe\x2e\x08\xff\xf0\x10\x24\x4b\x79" + b"\xd5\x78\xaa\x38\xe3\x4f\x42\x7b\x0f\x3e\xd2\x55\xa5\x08\xf1\x04\x58\xa4\xc2\x57\xf0\x08\xa1\xa9\x2c\x61\xd5\x41\x72\x06" + b"\x00\x83\xdf\xe3\x0d\x2e\xf9\x0c\x8e\x2c\x2b\x68\x74\x74\x70\x73\x3a\x2f\x2f\x62\x6f\x62\x2e\x62\x74\x63\x2e\x63\x61\x6c" + b"\x65\x6e\x64\x61\x72\x2e\x6f\x70\x65\x6e\x74\x69\x6d\x65\x73\x74\x61\x6d\x70\x73\x2e\x6f\x72\x67\xf0\x10\xe0\x27\x85\x91" + b"\xe2\x88\x68\x19\xba\x7b\x3d\xdd\x63\x2e\xd3\xfe\x08\xf1\x04\x58\xa4\xc2\x56\xf0\x08\x38\xf2\xc7\xf4\xba\xf4\xbc\xd7\x00" + b"\x83\xdf\xe3\x0d\x2e\xf9\x0c\x8e\x2e\x2d\x68\x74\x74\x70\x73\x3a\x2f\x2f\x61\x6c\x69\x63\x65\x2e\x62\x74\x63\x2e\x63\x61" + b"\x6c\x65\x6e\x64\x61\x72\x2e\x6f\x70\x65\x6e\x74\x69\x6d\x65\x73\x74\x61\x6d\x70\x73\x2e\x6f\x72\x67" +) + + +class TestCrossSDKDecoding: + """Test decoding Rust SDK encoded timestamps.""" + + def test_decode_small_detached_timestamp(self) -> None: + """Decode a small detached timestamp from Rust SDK fixture.""" + decoded = Decoder.decode_detached(SMALL_DETACHED_TIMESTAMP, strict=True) + + assert decoded.header.kind == DigestOp.SHA256 + assert len(decoded.header.digest) == 32 + + assert len(decoded.timestamp) == 3 + assert isinstance(decoded.timestamp[2], ForkStep) + fork = decoded.timestamp[2] + assert len(fork.steps) == 2 + + def test_roundtrip_small_detached_timestamp(self) -> None: + """Encode and decode round-trip matches original.""" + decoded = Decoder.decode_detached(SMALL_DETACHED_TIMESTAMP, strict=True) + + encoded = Encoder.encode_detached(decoded) + + assert encoded == SMALL_DETACHED_TIMESTAMP + + def test_decode_multiple_pending_attestations(self) -> None: + """Timestamp has multiple pending attestations from different calendars.""" + from uts_sdk._types import AttestationStep, PendingAttestation + + decoded = Decoder.decode_detached(SMALL_DETACHED_TIMESTAMP, strict=True) + + fork = decoded.timestamp[2] + assert isinstance(fork, ForkStep) + + urls = [] + for branch in fork.steps: + for step in branch: + if isinstance(step, AttestationStep): + attestation = step.attestation + if isinstance(attestation, PendingAttestation): + urls.append(attestation.url) + + assert len(urls) == 2 + assert any("bob.btc.calendar" in u for u in urls) + assert any("alice.btc.calendar" in u for u in urls) diff --git a/packages/sdk-py/tests/test_crypto_utils.py b/packages/sdk-py/tests/test_crypto_utils.py new file mode 100644 index 0000000..3699265 --- /dev/null +++ b/packages/sdk-py/tests/test_crypto_utils.py @@ -0,0 +1,42 @@ +"""Tests for crypto utilities.""" + +from __future__ import annotations + +import hashlib + +from uts_sdk._crypto.utils import HashFunction, keccak256, ripemd160, sha1, sha256 + + +class TestHashFunctions: + def test_sha256(self) -> None: + result = sha256(b"hello") + expected = hashlib.sha256(b"hello").digest() + assert result == expected + assert len(result) == 32 + + def test_keccak256(self) -> None: + result = keccak256(b"hello") + assert len(result) == 32 + assert ( + result.hex() + == "1c8aff950685c2ed4bc3174f3472287b56d9517b9c948127319a09a7a36deac8" + ) + + def test_sha1(self) -> None: + result = sha1(b"hello") + expected = hashlib.sha1(b"hello").digest() + assert result == expected + assert len(result) == 20 + + def test_ripemd160(self) -> None: + result = ripemd160(b"hello") + expected = hashlib.new("ripemd160", b"hello").digest() + assert result == expected + assert len(result) == 20 + + def test_hash_function_protocol(self) -> None: + def custom_hash(data: bytes) -> bytes: + return data + b"\x00" + + h: HashFunction = custom_hash + assert h(b"test") == b"test\x00" diff --git a/packages/sdk-py/tests/test_decoder.py b/packages/sdk-py/tests/test_decoder.py new file mode 100644 index 0000000..b95e8da --- /dev/null +++ b/packages/sdk-py/tests/test_decoder.py @@ -0,0 +1,198 @@ +"""Tests for decoder module.""" + +from __future__ import annotations + +import hashlib + +import pytest + +from uts_sdk._codec.constants import MAGIC_BYTES +from uts_sdk._codec.decoder import Decoder +from uts_sdk._types import ( + AppendStep, + AttestationStep, + DigestOp, + HexlifyStep, + Keccak256Step, + PrependStep, + ReverseStep, + RIPEMD160Step, + SHA1Step, + SHA256Step, + UnknownAttestation, +) +from uts_sdk.errors import DecodeError + + +class TestDecoderBasics: + def test_read_byte(self) -> None: + decoder = Decoder(b"\xff\x00") + assert decoder.read_byte() == 0xFF + assert decoder.read_byte() == 0x00 + + def test_read_bytes(self) -> None: + decoder = Decoder(b"hello") + assert decoder.read_bytes(5) == b"hello" + + def test_remaining(self) -> None: + decoder = Decoder(b"\x00\x01\x02") + assert decoder.remaining == 3 + decoder.read_byte() + assert decoder.remaining == 2 + + def test_check_eof_success(self) -> None: + decoder = Decoder(b"\x00") + decoder.read_byte() + decoder.check_eof() + + def test_check_eof_failure(self) -> None: + decoder = Decoder(b"\x00\x01") + decoder.read_byte() + with pytest.raises(DecodeError, match="bytes remain"): + decoder.check_eof() + + +class TestDecoderLEB128: + def test_u32_zero(self) -> None: + decoder = Decoder(b"\x00") + assert decoder.read_u32() == 0 + + def test_u32_small(self) -> None: + decoder = Decoder(b"\x7f") + assert decoder.read_u32() == 127 + + def test_u32_128(self) -> None: + decoder = Decoder(b"\x80\x01") + assert decoder.read_u32() == 128 + + def test_u32_300(self) -> None: + decoder = Decoder(b"\xac\x02") + assert decoder.read_u32() == 300 + + def test_u64_large(self) -> None: + decoder = Decoder(b"\x80\x80\x80\x80\x10") + assert decoder.read_u64() == 2**32 + + +class TestDecoderMagic: + def test_valid_magic(self) -> None: + data = MAGIC_BYTES + b"\x01" + decoder = Decoder(data) + version = decoder.read_magic() + assert version == 0x01 + + def test_invalid_magic(self) -> None: + invalid_magic = b"\x00" * len(MAGIC_BYTES) + decoder = Decoder(invalid_magic) + with pytest.raises(DecodeError, match="Invalid magic"): + decoder.read_magic() + + +class TestDecoderHeader: + def test_sha256_header(self) -> None: + digest = hashlib.sha256(b"test").digest() + data = b"\x08" + digest + decoder = Decoder(data) + header = decoder.read_header() + assert header.kind == DigestOp.SHA256 + assert header.digest == digest + + def test_sha1_header(self) -> None: + digest = hashlib.sha1(b"test").digest() + data = b"\x02" + digest + decoder = Decoder(data) + header = decoder.read_header() + assert header.kind == DigestOp.SHA1 + + def test_ripemd160_header(self) -> None: + import hashlib + + digest = hashlib.new("ripemd160", b"test").digest() + data = b"\x03" + digest + decoder = Decoder(data) + header = decoder.read_header() + assert header.kind == DigestOp.RIPEMD160 + + def test_invalid_header_op(self) -> None: + decoder = Decoder(b"\x00" + b"\x00" * 32) + with pytest.raises(DecodeError, match="Expected digest op"): + decoder.read_header() + + +class TestDecoderExecutionSteps: + def test_append_step(self) -> None: + decoder = Decoder(b"\xf0\x03\x01\x02\x03") + step = decoder.read_execution_step() + assert isinstance(step, AppendStep) + assert step.data == b"\x01\x02\x03" + + def test_prepend_step(self) -> None: + decoder = Decoder(b"\xf1\x02AB") + step = decoder.read_execution_step() + assert isinstance(step, PrependStep) + assert step.data == b"AB" + + def test_sha256_step(self) -> None: + decoder = Decoder(b"\x08") + step = decoder.read_execution_step() + assert isinstance(step, SHA256Step) + + def test_sha1_step(self) -> None: + decoder = Decoder(b"\x02") + step = decoder.read_execution_step() + assert isinstance(step, SHA1Step) + + def test_ripemd160_step(self) -> None: + decoder = Decoder(b"\x03") + step = decoder.read_execution_step() + assert isinstance(step, RIPEMD160Step) + + def test_keccak256_step(self) -> None: + decoder = Decoder(b"\x67") + step = decoder.read_execution_step() + assert isinstance(step, Keccak256Step) + + def test_reverse_step(self) -> None: + decoder = Decoder(b"\xf2") + step = decoder.read_execution_step() + assert isinstance(step, ReverseStep) + + def test_hexlify_step(self) -> None: + decoder = Decoder(b"\xf3") + step = decoder.read_execution_step() + assert isinstance(step, HexlifyStep) + + +class TestDecoderAttestations: + def test_unknown_attestation(self) -> None: + unknown_tag = b"\xff" * 8 + data = b"\x00" + unknown_tag + b"\x04test" + decoder = Decoder(data) + step = decoder.read_attestation_step() + assert isinstance(step, AttestationStep) + assert isinstance(step.attestation, UnknownAttestation) + assert step.attestation.tag == unknown_tag + + +class TestDecoderErrors: + def test_unexpected_eof(self) -> None: + decoder = Decoder(b"\x01") + with pytest.raises(DecodeError, match="Unexpected end"): + decoder.read_bytes(10) + + def test_unknown_opcode(self) -> None: + decoder = Decoder(b"\x99") + with pytest.raises(DecodeError, match="Unknown opcode"): + decoder.read_op() + + +class TestDecoderLengthPrefixed: + def test_read_length_prefixed(self) -> None: + decoder = Decoder(b"\x05hello") + data = decoder.read_length_prefixed() + assert data == b"hello" + + def test_read_length_prefixed_empty(self) -> None: + decoder = Decoder(b"\x00") + data = decoder.read_length_prefixed() + assert data == b"" diff --git a/packages/sdk-py/tests/test_eas.py b/packages/sdk-py/tests/test_eas.py new file mode 100644 index 0000000..1a78ac1 --- /dev/null +++ b/packages/sdk-py/tests/test_eas.py @@ -0,0 +1,186 @@ +"""Tests for EAS (Ethereum Attestation Service) integration.""" + +from __future__ import annotations + +from dataclasses import FrozenInstanceError +from unittest.mock import AsyncMock, MagicMock + +import pytest + +from uts_sdk._ethereum.eas import EasContract, OnChainAttestation + + +@pytest.fixture +def mock_web3() -> MagicMock: + """Create a mock AsyncWeb3 instance.""" + w3 = MagicMock() + w3.eth = MagicMock() + return w3 + + +@pytest.fixture +def mock_contract() -> MagicMock: + """Create a mock EAS contract.""" + contract = MagicMock() + contract.functions = MagicMock() + return contract + + +class TestOnChainAttestation: + """Tests for OnChainAttestation dataclass.""" + + def test_on_chain_attestation_creation(self) -> None: + att = OnChainAttestation( + uid="0x" + "00" * 32, + schema="0x" + "aa" * 32, + time=1234567890, + expiration_time=0, + revocation_time=0, + ref_uid="0x" + "00" * 32, + recipient="0x" + "bb" * 20, + attester="0x" + "cc" * 20, + revocable=False, + data="0x" + "dd" * 32, + ) + + assert att.uid.startswith("0x") + assert att.time == 1234567890 + assert att.expiration_time == 0 + assert not att.revocable + + def test_on_chain_attestation_frozen(self) -> None: + att = OnChainAttestation( + uid="0x" + "00" * 32, + schema="0x" + "aa" * 32, + time=100, + expiration_time=0, + revocation_time=0, + ref_uid="0x" + "00" * 32, + recipient="0x" + "bb" * 20, + attester="0x" + "cc" * 20, + revocable=False, + data="0x", + ) + + with pytest.raises(FrozenInstanceError): + att.time = 200 # type: ignore[misc] + + +class TestEasContract: + """Tests for EasContract class.""" + + def test_eas_contract_initialization( + self, mock_web3: MagicMock, mock_contract: MagicMock + ) -> None: + mock_web3.eth.contract = MagicMock(return_value=mock_contract) + + eas = EasContract(mock_web3, "0xA1207F3BBa224E2c9c3c6D5aF63D0eb1582Ce587") # type: ignore[arg-type] + + assert eas._w3 == mock_web3 + assert eas._contract == mock_contract + + @pytest.mark.asyncio + async def test_get_timestamp( + self, mock_web3: MagicMock, mock_contract: MagicMock + ) -> None: + mock_get_timestamp = AsyncMock(return_value=1234567890) + mock_contract.functions.getTimestamp = MagicMock( + return_value=MagicMock(call=mock_get_timestamp) + ) + mock_web3.eth.contract = MagicMock(return_value=mock_contract) + + eas = EasContract(mock_web3, "0xA1207F3BBa224E2c9c3c6D5aF63D0eb1582Ce587") # type: ignore[arg-type] + data = b"\x00" * 32 + + result = await eas.get_timestamp(data) + + assert result == 1234567890 + mock_get_timestamp.assert_called_once() + + @pytest.mark.asyncio + async def test_get_timestamp_pads_short_data( + self, mock_web3: MagicMock, mock_contract: MagicMock + ) -> None: + called_data: bytes = b"" + mock_get_timestamp = AsyncMock(return_value=0) + + def capture_padded(data: bytes) -> MagicMock: + nonlocal called_data + called_data = data + return MagicMock(call=mock_get_timestamp) + + mock_contract.functions.getTimestamp = capture_padded + mock_web3.eth.contract = MagicMock(return_value=mock_contract) + + eas = EasContract(mock_web3, "0xA1207F3BBa224E2c9c3c6D5aF63D0eb1582Ce587") # type: ignore[arg-type] + short_data = b"\x01\x02\x03" + + await eas.get_timestamp(short_data) + + assert len(called_data) == 32 + assert called_data[:3] == b"\x01\x02\x03" + assert called_data[3:] == b"\x00" * 29 + + @pytest.mark.asyncio + async def test_get_timestamp_truncates_long_data( + self, mock_web3: MagicMock, mock_contract: MagicMock + ) -> None: + called_data: bytes = b"" + mock_get_timestamp = AsyncMock(return_value=0) + + def capture_padded(data: bytes) -> MagicMock: + nonlocal called_data + called_data = data + return MagicMock(call=mock_get_timestamp) + + mock_contract.functions.getTimestamp = capture_padded + mock_web3.eth.contract = MagicMock(return_value=mock_contract) + + eas = EasContract(mock_web3, "0xA1207F3BBa224E2c9c3c6D5aF63D0eb1582Ce587") # type: ignore[arg-type] + long_data = b"\xff" * 64 + + await eas.get_timestamp(long_data) + + assert len(called_data) == 32 + assert called_data == b"\xff" * 32 + + @pytest.mark.asyncio + async def test_get_attestation( + self, mock_web3: MagicMock, mock_contract: MagicMock + ) -> None: + uid = b"\x01" * 32 + mock_result = ( + uid, # uid + b"\x02" * 32, # schema + 1234567890, # time + 0, # expiration_time + 0, # revocation_time + b"\x00" * 32, # ref_uid + "0xRecipientAddress", # recipient + "0xAttesterAddress", # attester + False, # revocable + b"\x03" * 32, # data + ) + mock_get_attestation = AsyncMock(return_value=mock_result) + mock_contract.functions.getAttestation = MagicMock( + return_value=MagicMock(call=mock_get_attestation) + ) + mock_web3.eth.contract = MagicMock(return_value=mock_contract) + + eas = EasContract(mock_web3, "0xA1207F3BBa224E2c9c3c6D5aF63D0eb1582Ce587") # type: ignore[arg-type] + + result = await eas.get_attestation(uid) + + assert isinstance(result, OnChainAttestation) + assert result.time == 1234567890 + assert not result.revocable + + @pytest.mark.asyncio + async def test_get_attestation_invalid_uid_length( + self, mock_web3: MagicMock, mock_contract: MagicMock + ) -> None: + mock_web3.eth.contract = MagicMock(return_value=mock_contract) + eas = EasContract(mock_web3, "0xA1207F3BBa224E2c9c3c6D5aF63D0eb1582Ce587") # type: ignore[arg-type] + + with pytest.raises(ValueError, match="UID must be 32 bytes"): + await eas.get_attestation(b"\x01\x02\x03") diff --git a/packages/sdk-py/tests/test_eas_integration.py b/packages/sdk-py/tests/test_eas_integration.py new file mode 100644 index 0000000..5b0e10b --- /dev/null +++ b/packages/sdk-py/tests/test_eas_integration.py @@ -0,0 +1,88 @@ +"""Integration tests for EAS (Ethereum Attestation Service). + +These tests make real network calls to Ethereum RPC endpoints. +Set UTS_ETH_RPC_URL_11155111 environment variable to run them. +""" + +from __future__ import annotations + +import os + +import pytest + +from uts_sdk._ethereum.eas import EAS_SCHEMA_ID, EasContract, OnChainAttestation + +# Skip all tests in this module if no Sepolia RPC is configured +pytestmark = pytest.mark.skipif( + not os.environ.get("UTS_ETH_RPC_URL_11155111"), + reason="UTS_ETH_RPC_URL_11155111 not set", +) + +SEPOLIA_RPC = os.environ.get("UTS_ETH_RPC_URL_11155111", "") +SEPOLIA_EAS = "0xC2679fBD37d54388Ce493F1DB75320D236e1815e" + + +@pytest.fixture +async def eas_contract() -> EasContract: + """Create an EasContract instance connected to Sepolia.""" + from web3 import AsyncWeb3 + + w3 = AsyncWeb3(AsyncWeb3.AsyncHTTPProvider(SEPOLIA_RPC)) + return EasContract(w3, SEPOLIA_EAS) # type: ignore[arg-type] + + +class TestEasContractIntegration: + """Real integration tests for EAS contract on Sepolia.""" + + @pytest.mark.asyncio + async def test_get_timestamp_known_data(self, eas_contract: EasContract) -> None: + """Test getTimestamp with known timestamped data.""" + known_data = bytes.fromhex( + "D445CE83D2BC148E8DDDDBC0EC6602D29A5ACAC302CE3E222833D4E830976381" + ) + + result = await eas_contract.get_timestamp(known_data) + + assert result == 1773009984 + + @pytest.mark.asyncio + async def test_get_timestamp_pads_short_data( + self, eas_contract: EasContract + ) -> None: + """Test that short data is properly padded to 32 bytes.""" + short_data = b"\x01\x02\x03" + + result = await eas_contract.get_timestamp(short_data) + + assert isinstance(result, int) + assert result >= 0 + + @pytest.mark.asyncio + async def test_get_attestation_known_uid(self, eas_contract: EasContract) -> None: + """Test getAttestation with known attestation UID.""" + known_uid = bytes.fromhex( + "09b4943032820e002fdd690783e3e76e3c8af7df0f8e56fa953317dac8f1f5a8" + ) + + result = await eas_contract.get_attestation(known_uid) + + assert isinstance(result, OnChainAttestation) + assert result.data.startswith("0xcee") + + @pytest.mark.asyncio + async def test_get_attestation_invalid_uid_raises( + self, eas_contract: EasContract + ) -> None: + """Test that getAttestation raises for invalid UID length.""" + invalid_uid = b"\x01\x02\x03" + + with pytest.raises(ValueError, match="UID must be 32 bytes"): + await eas_contract.get_attestation(invalid_uid) + + @pytest.mark.asyncio + async def test_eas_schema_id_constant(self) -> None: + """Verify EAS_SCHEMA_ID is the expected value.""" + assert ( + EAS_SCHEMA_ID + == "0x5c5b8b295ff43c8e442be11d569e94a4cd5476f5e23df0f71bdd408df6b9649c" + ) diff --git a/packages/sdk-py/tests/test_encoder.py b/packages/sdk-py/tests/test_encoder.py new file mode 100644 index 0000000..0ce6473 --- /dev/null +++ b/packages/sdk-py/tests/test_encoder.py @@ -0,0 +1,232 @@ +"""Tests for encoder module.""" + +from __future__ import annotations + +import hashlib + +import pytest + +from uts_sdk._codec.constants import ( + BITCOIN_TAG, + EAS_ATTEST_TAG, + MAGIC_BYTES, + PENDING_TAG, +) +from uts_sdk._codec.encoder import Encoder +from uts_sdk._types import ( + AppendStep, + AttestationStep, + BitcoinAttestation, + DetachedTimestamp, + DigestHeader, + DigestOp, + EASAttestation, + EASTimestamped, + ForkStep, + HexlifyStep, + Keccak256Step, + PendingAttestation, + PrependStep, + ReverseStep, + RIPEMD160Step, + SHA1Step, + SHA256Step, + Timestamp, + UnknownAttestation, +) +from uts_sdk.errors import EncodeError + + +class TestEncoderBasics: + def test_write_byte(self) -> None: + encoder = Encoder() + encoder.write_byte(0xFF) + assert encoder.to_bytes() == b"\xff" + + def test_write_byte_out_of_range(self) -> None: + encoder = Encoder() + with pytest.raises(EncodeError, match="0-255"): + encoder.write_byte(256) + + def test_write_bytes(self) -> None: + encoder = Encoder() + encoder.write_bytes(b"hello") + assert encoder.to_bytes() == b"hello" + + def test_write_length_prefixed(self) -> None: + encoder = Encoder() + encoder.write_length_prefixed(b"test") + assert encoder.to_bytes() == b"\x04test" + + def test_write_magic(self) -> None: + encoder = Encoder() + encoder.write_magic() + data = encoder.to_bytes() + assert data.startswith(MAGIC_BYTES) + assert data[len(MAGIC_BYTES)] == 0x01 + + +class TestEncoderLEB128: + def test_u32_zero(self) -> None: + encoder = Encoder() + encoder.write_u32(0) + assert encoder.to_bytes() == b"\x00" + + def test_u32_small(self) -> None: + encoder = Encoder() + encoder.write_u32(127) + assert encoder.to_bytes() == b"\x7f" + + def test_u32_128(self) -> None: + encoder = Encoder() + encoder.write_u32(128) + assert encoder.to_bytes() == b"\x80\x01" + + def test_u32_negative(self) -> None: + encoder = Encoder() + with pytest.raises(EncodeError, match="non-negative"): + encoder.write_u32(-1) + + def test_u32_overflow(self) -> None: + encoder = Encoder() + with pytest.raises(EncodeError, match="exceeds maximum"): + encoder.write_u32(0xFFFFFFFF + 1) + + def test_u64_large(self) -> None: + encoder = Encoder() + encoder.write_u64(2**32) + data = encoder.to_bytes() + assert len(data) >= 5 + + +class TestEncoderSteps: + def test_write_sha256_step(self) -> None: + encoder = Encoder() + encoder.write_step(SHA256Step()) + assert encoder.to_bytes() == b"\x08" + + def test_write_sha1_step(self) -> None: + encoder = Encoder() + encoder.write_step(SHA1Step()) + assert encoder.to_bytes() == b"\x02" + + def test_write_ripemd160_step(self) -> None: + encoder = Encoder() + encoder.write_step(RIPEMD160Step()) + assert encoder.to_bytes() == b"\x03" + + def test_write_keccak256_step(self) -> None: + encoder = Encoder() + encoder.write_step(Keccak256Step()) + assert encoder.to_bytes() == b"\x67" + + def test_write_reverse_step(self) -> None: + encoder = Encoder() + encoder.write_step(ReverseStep()) + assert encoder.to_bytes() == b"\xf2" + + def test_write_hexlify_step(self) -> None: + encoder = Encoder() + encoder.write_step(HexlifyStep()) + assert encoder.to_bytes() == b"\xf3" + + def test_write_append_step(self) -> None: + encoder = Encoder() + encoder.write_step(AppendStep(data=b"\x01\x02")) + assert encoder.to_bytes() == b"\xf0\x02\x01\x02" + + def test_write_prepend_step(self) -> None: + encoder = Encoder() + encoder.write_step(PrependStep(data=b"\x01\x02")) + assert encoder.to_bytes() == b"\xf1\x02\x01\x02" + + +class TestEncoderAttestations: + def test_write_pending_attestation(self) -> None: + encoder = Encoder() + att = PendingAttestation(url="https://example.com") + step = AttestationStep(attestation=att) + encoder.write_step(step) + data = encoder.to_bytes() + assert data[0] == 0x00 + assert PENDING_TAG in data + + def test_write_bitcoin_attestation(self) -> None: + encoder = Encoder() + att = BitcoinAttestation(height=800000) + step = AttestationStep(attestation=att) + encoder.write_step(step) + data = encoder.to_bytes() + assert data[0] == 0x00 + assert BITCOIN_TAG in data + + def test_write_eas_attestation(self) -> None: + encoder = Encoder() + att = EASAttestation(chain_id=1, uid=b"\x00" * 32) + step = AttestationStep(attestation=att) + encoder.write_step(step) + data = encoder.to_bytes() + assert data[0] == 0x00 + assert EAS_ATTEST_TAG in data + + def test_write_eas_timestamped(self) -> None: + encoder = Encoder() + att = EASTimestamped(chain_id=1) + step = AttestationStep(attestation=att) + encoder.write_step(step) + data = encoder.to_bytes() + assert data[0] == 0x00 + + def test_write_unknown_attestation(self) -> None: + encoder = Encoder() + att = UnknownAttestation(tag=b"\xff" * 8, data=b"unknown data") + step = AttestationStep(attestation=att) + encoder.write_step(step) + data = encoder.to_bytes() + assert data[0] == 0x00 + assert b"\xff" * 8 in data + + +class TestEncoderFork: + def test_write_fork_step(self) -> None: + ts1: Timestamp = [SHA256Step()] + ts2: Timestamp = [SHA256Step()] + fork = ForkStep(steps=[ts1, ts2]) + encoder = Encoder() + encoder.write_step(fork) + data = encoder.to_bytes() + assert b"\xff" in data + + +class TestEncoderPendingAttestationErrors: + def test_invalid_url_format(self) -> None: + encoder = Encoder() + att = PendingAttestation(url="http://example.com?query=1&foo=bar") + with pytest.raises(EncodeError, match="Invalid URL format"): + encoder.write_pending_attestation(att) + + def test_url_exceeds_max_length(self) -> None: + long_url = "https://example.com/" + "a" * 1000 + with pytest.raises(ValueError, match="URL exceeds maximum"): + PendingAttestation(url=long_url) + + +class TestEncodeDetached: + def test_encode_detached_sha256(self) -> None: + digest = hashlib.sha256(b"test").digest() + header = DigestHeader(kind=DigestOp.SHA256, digest=digest) + att = PendingAttestation(url="https://example.com") + ts: Timestamp = [AttestationStep(attestation=att)] + ots = DetachedTimestamp(header=header, timestamp=ts) + + data = Encoder.encode_detached(ots) + assert data.startswith(MAGIC_BYTES) + + def test_encode_detached_ripemd160(self) -> None: + digest = b"\x00" * 20 + header = DigestHeader(kind=DigestOp.RIPEMD160, digest=digest) + ts: Timestamp = [SHA256Step()] + ots = DetachedTimestamp(header=header, timestamp=ts) + + data = Encoder.encode_detached(ots) + assert data.startswith(MAGIC_BYTES) diff --git a/packages/sdk-py/tests/test_errors.py b/packages/sdk-py/tests/test_errors.py new file mode 100644 index 0000000..e9092f0 --- /dev/null +++ b/packages/sdk-py/tests/test_errors.py @@ -0,0 +1,81 @@ +"""Tests for error classes.""" + +from __future__ import annotations + +from uts_sdk.errors import ( + DecodeError, + EncodeError, + ErrorCode, + RemoteError, + UTSError, + VerifyError, +) + + +class TestErrorCode: + def test_error_codes_exist(self) -> None: + assert ErrorCode.GENERAL_ERROR.value > 0 + assert ErrorCode.BAD_MAGIC.value > 0 + assert ErrorCode.UNKNOWN_OP.value > 0 + assert ErrorCode.INVALID_STRUCTURE.value > 0 + assert ErrorCode.REMOTE_ERROR.value > 0 + + +class TestUTSError: + def test_basic_error(self) -> None: + err = UTSError(ErrorCode.GENERAL_ERROR, "test error") + assert err.code == ErrorCode.GENERAL_ERROR + assert "test error" in str(err) + + def test_error_with_offset(self) -> None: + err = UTSError(ErrorCode.BAD_MAGIC, "bad magic", offset=10) + assert err.offset == 10 + + def test_error_with_context(self) -> None: + err = UTSError( + ErrorCode.REMOTE_ERROR, + "remote error", + context={"url": "https://example.com"}, + ) + assert err.context["url"] == "https://example.com" + + def test_error_repr(self) -> None: + err = UTSError(ErrorCode.GENERAL_ERROR, "test", offset=5) + repr_str = repr(err) + assert "UTSError" in repr_str + assert "GENERAL_ERROR" in repr_str + assert "offset=5" in repr_str + + +class TestEncodeError: + def test_encode_error(self) -> None: + err = EncodeError(ErrorCode.OVERFLOW, "value too large") + assert err.code == ErrorCode.OVERFLOW + assert isinstance(err, UTSError) + + +class TestDecodeError: + def test_decode_error(self) -> None: + err = DecodeError(ErrorCode.UNEXPECTED_EOF, "unexpected end", offset=100) + assert err.code == ErrorCode.UNEXPECTED_EOF + assert err.offset == 100 + assert isinstance(err, UTSError) + + +class TestRemoteError: + def test_remote_error(self) -> None: + err = RemoteError("connection failed", context={"host": "example.com"}) + assert err.code == ErrorCode.REMOTE_ERROR + assert "connection failed" in str(err) + assert err.context["host"] == "example.com" + + def test_remote_error_is_uts_error(self) -> None: + err = RemoteError("test") + assert isinstance(err, UTSError) + + +class TestVerifyError: + def test_verify_error(self) -> None: + err = VerifyError(ErrorCode.ATTESTATION_MISMATCH, "hash mismatch") + assert err.code == ErrorCode.ATTESTATION_MISMATCH + assert isinstance(err, UTSError) diff --git a/packages/sdk-py/tests/test_merkle.py b/packages/sdk-py/tests/test_merkle.py new file mode 100644 index 0000000..09ac99b --- /dev/null +++ b/packages/sdk-py/tests/test_merkle.py @@ -0,0 +1,115 @@ +"""Tests for Merkle tree implementation.""" + +from __future__ import annotations + +import hashlib + +import pytest + +from uts_sdk._crypto.merkle import INTERNAL_PREFIX, UnorderedMerkleTree +from uts_sdk._types.status import NodePosition + + +def sha256(data: bytes) -> bytes: + return hashlib.sha256(data).digest() + + +def test_single_leaf_tree() -> None: + leaf = sha256(b"leaf1") + tree = UnorderedMerkleTree.from_leaves([leaf], sha256) + + assert tree.root == leaf + assert leaf in tree + + +def test_two_leaf_tree() -> None: + a = sha256(b"a") + b = sha256(b"b") + tree = UnorderedMerkleTree.from_leaves([a, b], sha256) + + assert a in tree + assert b in tree + assert len(tree.leaves) == 2 + + +def test_four_leaf_tree() -> None: + leaves = [sha256(b"a"), sha256(b"b"), sha256(b"c"), sha256(b"d")] + tree = UnorderedMerkleTree.from_leaves(leaves, sha256) + + for leaf in leaves: + assert leaf in tree + + +def test_proof_for_leaf() -> None: + leaves = [sha256(b"a"), sha256(b"b"), sha256(b"c"), sha256(b"d")] + tree = UnorderedMerkleTree.from_leaves(leaves, sha256) + + leaf = sha256(b"a") + proof = tree.proof_for(leaf) + assert proof is not None + assert len(proof) == 2 + + +def test_verify_proof() -> None: + leaves = [sha256(b"a"), sha256(b"b"), sha256(b"c"), sha256(b"d")] + tree = UnorderedMerkleTree.from_leaves(leaves, sha256) + + for leaf in leaves: + proof = tree.proof_for(leaf) + assert proof is not None + + computed_root = leaf + for node in proof: + if node.position == NodePosition.LEFT: + computed_root = sha256(INTERNAL_PREFIX + computed_root + node.sibling) + else: + computed_root = sha256(INTERNAL_PREFIX + node.sibling + computed_root) + + assert computed_root == tree.root, f"Proof verification failed for leaf {leaf}" + + +def test_proof_nonexistent_leaf() -> None: + a = sha256(b"a") + b = sha256(b"b") + tree = UnorderedMerkleTree.from_leaves([a, b], sha256) + + c = sha256(b"c") + proof = tree.proof_for(c) + assert proof is None + + +def test_empty_tree_error() -> None: + with pytest.raises(ValueError, match="at least one leaf"): + UnorderedMerkleTree.from_leaves([], sha256) + + +def test_odd_leaf_count() -> None: + leaves = [sha256(b"a"), sha256(b"b"), sha256(b"c")] + tree = UnorderedMerkleTree.from_leaves(leaves, sha256) + + for leaf in leaves: + assert leaf in tree + + for leaf in leaves: + proof = tree.proof_for(leaf) + assert proof is not None + + computed_root = leaf + for node in proof: + if node.position == NodePosition.LEFT: + computed_root = sha256(INTERNAL_PREFIX + computed_root + node.sibling) + else: + computed_root = sha256(INTERNAL_PREFIX + node.sibling + computed_root) + + assert computed_root == tree.root + + +def test_serialization() -> None: + leaves = [sha256(b"a"), sha256(b"b"), sha256(b"c"), sha256(b"d")] + tree = UnorderedMerkleTree.from_leaves(leaves, sha256) + + serialized = bytes(tree) + restored = UnorderedMerkleTree.from_bytes(serialized, sha256) + + assert restored.root == tree.root + assert set(restored.leaves) == set(tree.leaves) diff --git a/packages/sdk-py/tests/test_rpc_bitcoin.py b/packages/sdk-py/tests/test_rpc_bitcoin.py new file mode 100644 index 0000000..517786e --- /dev/null +++ b/packages/sdk-py/tests/test_rpc_bitcoin.py @@ -0,0 +1,67 @@ +"""Tests for Bitcoin RPC client.""" + +from __future__ import annotations + +from unittest.mock import AsyncMock, MagicMock, patch + +import pytest + +from uts_sdk._rpc.bitcoin import BitcoinRPC + + +@pytest.mark.asyncio +async def test_bitcoin_rpc_client() -> None: + rpc = BitcoinRPC(url="https://bitcoin-rpc.publicnode.com") + + mock_response = MagicMock() + mock_response.json.return_value = {"result": "0" * 64, "error": None, "id": 1} + mock_response.status_code = 200 + + with patch.object(rpc._client, "post", new_callable=AsyncMock) as mock_post: + mock_post.return_value = mock_response + + result = await rpc.get_block_hash(123456) + assert result == "0" * 64 + + +@pytest.mark.asyncio +async def test_get_block_header() -> None: + rpc = BitcoinRPC(url="https://bitcoin-rpc.publicnode.com") + + expected_response = { + "hash": "abc123", + "confirmations": 10, + "height": 123456, + "version": 1, + "versionHex": "00000001", + "merkleroot": "def456", + "time": 1234567890, + "mediantime": 1234567800, + "nonce": 12345, + "bits": "1d00ffff", + "difficulty": 1.0, + "chainwork": "00" * 32, + "nTx": 5, + } + + mock_response = MagicMock() + mock_response.json.return_value = { + "result": expected_response, + "error": None, + "id": 1, + } + mock_response.status_code = 200 + + with patch.object(rpc._client, "post", new_callable=AsyncMock) as mock_post: + mock_post.return_value = mock_response + + result = await rpc.get_block_header("fake_block_hash") + assert result.hash == "abc123" + assert result.height == 123456 + assert result.merkleroot == "def456" + + +@pytest.mark.asyncio +async def test_rpc_context_manager() -> None: + async with BitcoinRPC(url="https://bitcoin-rpc.publicnode.com") as rpc: + assert rpc is not None diff --git a/packages/sdk-py/tests/test_sdk.py b/packages/sdk-py/tests/test_sdk.py new file mode 100644 index 0000000..80f9124 --- /dev/null +++ b/packages/sdk-py/tests/test_sdk.py @@ -0,0 +1,221 @@ +"""Tests for main SDK class.""" + +from __future__ import annotations + +import hashlib +from unittest.mock import AsyncMock, MagicMock, patch + +import httpx +import pytest + +from uts_sdk import ( + SDK, + AttestationStatusKind, + AttestationStep, + BitcoinAttestation, + DetachedTimestamp, + DigestHeader, + DigestOp, + PendingAttestation, + UpgradeStatus, + VerifyStatus, +) + + +def test_sdk_initialization() -> None: + sdk = SDK() + assert sdk is not None + assert len(sdk.calendars) > 0 + + +def test_sdk_options() -> None: + sdk = SDK( + calendars=["https://test.calendar.com"], + timeout=20.0, + nonce_size=64, + ) + assert sdk.timeout == 20.0 + assert sdk.nonce_size == 64 + + +def test_sdk_default_calendars() -> None: + sdk = SDK() + assert "https://lgm1.calendar.test.timestamps.now/" in sdk.calendars + + +def test_sdk_invalid_hash_algorithm() -> None: + with pytest.raises(ValueError, match="Unsupported hash algorithm"): + SDK(hash_algorithm="md5") # type: ignore[arg-type] + + +@pytest.mark.asyncio +async def test_sdk_context_manager() -> None: + async with SDK() as sdk: + assert sdk is not None + + +@pytest.mark.asyncio +async def test_sdk_verify_pending() -> None: + digest = hashlib.sha256(b"test data").digest() + header = DigestHeader(kind=DigestOp.SHA256, digest=digest) + + pending = PendingAttestation(url="https://calendar.example.com") + step = AttestationStep(attestation=pending) + stamp = DetachedTimestamp(header=header, timestamp=[step]) + + async with SDK() as sdk: + result = await sdk.verify(stamp) + + assert result.status == VerifyStatus.PENDING + assert len(result.attestations) == 1 + assert result.attestations[0].status == AttestationStatusKind.PENDING + assert not result.is_valid + assert result.is_pending + + +@pytest.mark.asyncio +async def test_sdk_upgrade_pending_still_pending() -> None: + """Test upgrade when attestation is still pending (202 response).""" + digest = hashlib.sha256(b"test data").digest() + header = DigestHeader(kind=DigestOp.SHA256, digest=digest) + + pending = PendingAttestation(url="https://calendar.example.com") + step = AttestationStep(attestation=pending) + stamp = DetachedTimestamp(header=header, timestamp=[step]) + + mock_response = MagicMock() + mock_response.status_code = 202 + + mock_client = AsyncMock() + mock_client.get = AsyncMock(return_value=mock_response) + mock_client.__aenter__ = AsyncMock(return_value=mock_client) + mock_client.__aexit__ = AsyncMock(return_value=None) + + with patch("httpx.AsyncClient", return_value=mock_client): + async with SDK() as sdk: + results = await sdk.upgrade(stamp) + + assert len(results) == 1 + assert results[0].status == UpgradeStatus.PENDING + assert results[0].original == pending + + +@pytest.mark.asyncio +async def test_sdk_upgrade_pending_not_found() -> None: + """Test upgrade when attestation not found (404 response).""" + digest = hashlib.sha256(b"test data").digest() + header = DigestHeader(kind=DigestOp.SHA256, digest=digest) + + pending = PendingAttestation(url="https://calendar.example.com") + step = AttestationStep(attestation=pending) + stamp = DetachedTimestamp(header=header, timestamp=[step]) + + mock_response = MagicMock() + mock_response.status_code = 404 + + mock_client = AsyncMock() + mock_client.get = AsyncMock(return_value=mock_response) + mock_client.__aenter__ = AsyncMock(return_value=mock_client) + mock_client.__aexit__ = AsyncMock(return_value=None) + + with patch("httpx.AsyncClient", return_value=mock_client): + async with SDK() as sdk: + results = await sdk.upgrade(stamp) + + assert len(results) == 1 + assert results[0].status == UpgradeStatus.PENDING + + +@pytest.mark.asyncio +async def test_sdk_upgrade_pending_failed() -> None: + """Test upgrade when calendar returns an error (500 response).""" + digest = hashlib.sha256(b"test data").digest() + header = DigestHeader(kind=DigestOp.SHA256, digest=digest) + + pending = PendingAttestation(url="https://calendar.example.com") + step = AttestationStep(attestation=pending) + stamp = DetachedTimestamp(header=header, timestamp=[step]) + + mock_response = MagicMock() + mock_response.status_code = 500 + mock_response.is_success = False + + mock_client = AsyncMock() + mock_client.get = AsyncMock(return_value=mock_response) + mock_client.__aenter__ = AsyncMock(return_value=mock_client) + mock_client.__aexit__ = AsyncMock(return_value=None) + + with patch("httpx.AsyncClient", return_value=mock_client): + async with SDK() as sdk: + results = await sdk.upgrade(stamp) + + assert len(results) == 1 + assert results[0].status == UpgradeStatus.FAILED + + +@pytest.mark.asyncio +async def test_sdk_upgrade_pending_network_error() -> None: + """Test upgrade when network error occurs.""" + digest = hashlib.sha256(b"test data").digest() + header = DigestHeader(kind=DigestOp.SHA256, digest=digest) + + pending = PendingAttestation(url="https://calendar.example.com") + step = AttestationStep(attestation=pending) + stamp = DetachedTimestamp(header=header, timestamp=[step]) + + mock_client = AsyncMock() + mock_client.get = AsyncMock(side_effect=httpx.RequestError("Connection refused")) + mock_client.__aenter__ = AsyncMock(return_value=mock_client) + mock_client.__aexit__ = AsyncMock(return_value=None) + + with patch("httpx.AsyncClient", return_value=mock_client): + async with SDK() as sdk: + results = await sdk.upgrade(stamp) + + assert len(results) == 1 + assert results[0].status == UpgradeStatus.FAILED + + +def test_sdk_from_env(monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setenv( + "UTS_CALENDARS", "https://cal1.example.com,https://cal2.example.com" + ) + monkeypatch.setenv("UTS_TIMEOUT", "30.0") + monkeypatch.setenv("UTS_QUORUM", "2") + monkeypatch.setenv("UTS_HASH_ALGORITHM", "sha256") + monkeypatch.setenv("UTS_ETH_RPC_URL_1", "https://eth.example.com") + + sdk = SDK.from_env() + + assert "https://cal1.example.com/" in sdk.calendars + assert "https://cal2.example.com/" in sdk.calendars + assert sdk.timeout == 30.0 + assert sdk.nonce_size == 32 + + +def test_sdk_from_env_defaults(monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.delenv("UTS_CALENDARS", raising=False) + monkeypatch.delenv("UTS_TIMEOUT", raising=False) + monkeypatch.delenv("UTS_QUORUM", raising=False) + + sdk = SDK.from_env() + + assert "https://lgm1.calendar.test.timestamps.now/" in sdk.calendars + assert sdk.timeout == 10.0 + + +@pytest.mark.asyncio +async def test_sdk_verify_bitcoin_attestation() -> None: + """Test verification of Bitcoin attestation with mocked RPC.""" + digest = hashlib.sha256(b"test data").digest() + header = DigestHeader(kind=DigestOp.SHA256, digest=digest) + + btc_att = BitcoinAttestation(height=800000) + step = AttestationStep(attestation=btc_att) + stamp = DetachedTimestamp(header=header, timestamp=[step]) + + async with SDK() as sdk: + result = await sdk.verify(stamp) + + assert len(result.attestations) == 1 + assert result.attestations[0].attestation == btc_att diff --git a/packages/sdk-py/tests/test_types.py b/packages/sdk-py/tests/test_types.py new file mode 100644 index 0000000..253f3a9 --- /dev/null +++ b/packages/sdk-py/tests/test_types.py @@ -0,0 +1,155 @@ +"""Tests for types module.""" + +from __future__ import annotations + +import pytest + +from uts_sdk import ( + AttestationStatusKind, + BitcoinAttestation, + EASAttestation, + EASTimestamped, + OpCode, + PendingAttestation, + UnknownAttestation, + UpgradeStatus, + VerifyStatus, +) +from uts_sdk._types.attestations import attestation_kind +from uts_sdk._types.digest import DigestHeader, DigestOp +from uts_sdk._types.ops import DIGEST_OPS, SECURE_DIGEST_OPS +from uts_sdk._types.timestamp_steps import ForkStep + + +class TestPendingAttestation: + def test_valid_url(self) -> None: + att = PendingAttestation(url="https://calendar.example.com") + assert att.url == "https://calendar.example.com" + assert att.kind == "pending" + + def test_url_exceeds_max_length(self) -> None: + with pytest.raises(ValueError, match="URL exceeds maximum"): + PendingAttestation(url="https://example.com/" + "a" * 1000) + + +class TestBitcoinAttestation: + def test_valid_height(self) -> None: + att = BitcoinAttestation(height=800000) + assert att.height == 800000 + assert att.kind == "bitcoin" + + def test_negative_height(self) -> None: + with pytest.raises(ValueError, match="non-negative"): + BitcoinAttestation(height=-1) + + +class TestEASAttestation: + def test_valid_uid(self) -> None: + uid = b"\x00" * 32 + att = EASAttestation(chain_id=1, uid=uid) + assert att.chain_id == 1 + assert att.uid == uid + assert att.kind == "eas-attestation" + + def test_invalid_uid_length(self) -> None: + with pytest.raises(ValueError, match="32 bytes"): + EASAttestation(chain_id=1, uid=b"\x00" * 16) + + +class TestEASTimestamped: + def test_creation(self) -> None: + att = EASTimestamped(chain_id=1) + assert att.chain_id == 1 + assert att.kind == "eas-timestamped" + + +class TestUnknownAttestation: + def test_creation(self) -> None: + att = UnknownAttestation(tag=b"\x00" * 8, data=b"test") + assert att.tag == b"\x00" * 8 + assert att.data == b"test" + assert att.kind == "unknown" + + +class TestAttestationKind: + def test_attestation_kind_pending(self) -> None: + att = PendingAttestation(url="https://example.com") + assert attestation_kind(att) == "pending" + + def test_attestation_kind_bitcoin(self) -> None: + att = BitcoinAttestation(height=100) + assert attestation_kind(att) == "bitcoin" + + def test_attestation_kind_eas_attestation(self) -> None: + att = EASAttestation(chain_id=1, uid=b"\x00" * 32) + assert attestation_kind(att) == "eas-attestation" + + def test_attestation_kind_eas_timestamped(self) -> None: + att = EASTimestamped(chain_id=1) + assert attestation_kind(att) == "eas-timestamped" + + def test_attestation_kind_unknown(self) -> None: + att = UnknownAttestation(tag=b"\x00" * 8, data=b"test") + assert attestation_kind(att) == "unknown" + + +class TestDigestHeader: + def test_sha256_valid(self) -> None: + digest = b"\x00" * 32 + header = DigestHeader(kind=DigestOp.SHA256, digest=digest) + assert header.digest == digest + + def test_sha256_invalid_length(self) -> None: + with pytest.raises(ValueError, match="Digest length mismatch"): + DigestHeader(kind=DigestOp.SHA256, digest=b"\x00" * 16) + + def test_ripemd160_valid(self) -> None: + digest = b"\x00" * 20 + header = DigestHeader(kind=DigestOp.RIPEMD160, digest=digest) + assert header.digest == digest + + +class TestForkStep: + def test_valid_fork(self) -> None: + from uts_sdk import AttestationStep, PendingAttestation, SHA256Step, Timestamp + + ts1: Timestamp = [SHA256Step()] + ts2: Timestamp = [ + AttestationStep(attestation=PendingAttestation(url="https://example.com")) + ] + + fork = ForkStep(steps=[ts1, ts2]) + assert len(fork.steps) == 2 + + def test_single_branch_fails(self) -> None: + from uts_sdk import SHA256Step, Timestamp + + ts: Timestamp = [SHA256Step()] + with pytest.raises(ValueError, match="at least 2 branches"): + ForkStep(steps=[ts]) + + +class TestOpCode: + def test_digest_ops(self) -> None: + assert OpCode.SHA256 in DIGEST_OPS + assert OpCode.KECCAK256 in DIGEST_OPS + assert OpCode.APPEND not in DIGEST_OPS + + def test_secure_digest_ops(self) -> None: + assert OpCode.SHA256 in SECURE_DIGEST_OPS + assert OpCode.KECCAK256 in SECURE_DIGEST_OPS + assert OpCode.SHA1 not in SECURE_DIGEST_OPS + + +class TestEnums: + def test_verify_status_values(self) -> None: + assert VerifyStatus.VALID.value == "VALID" + assert VerifyStatus.PENDING.value == "PENDING" + + def test_upgrade_status_values(self) -> None: + assert UpgradeStatus.UPGRADED.value == "UPGRADED" + assert UpgradeStatus.FAILED.value == "FAILED" + + def test_attestation_status_kind_values(self) -> None: + assert AttestationStatusKind.VALID.value == "VALID" + assert AttestationStatusKind.UNKNOWN.value == "UNKNOWN" diff --git a/packages/sdk-rs/Cargo.toml b/packages/sdk-rs/Cargo.toml new file mode 100644 index 0000000..d606de0 --- /dev/null +++ b/packages/sdk-rs/Cargo.toml @@ -0,0 +1,68 @@ +[package] +authors.workspace = true +description = "Universal Timestamping SDK in Rust" +edition.workspace = true +homepage.workspace = true +keywords = ["uts", "timestamping", "sdk"] +license = "MIT OR Apache-2.0" +name = "uts-sdk" +repository.workspace = true +version.workspace = true + +[dependencies] +alloy-primitives = { workspace = true } +alloy-provider = { workspace = true, default-features = false, optional = true, features = ["reqwest"] } +alloy-rpc-client = { workspace = true } +backon = { workspace = true } +bytes = { workspace = true } +digest = { workspace = true } +futures = { workspace = true } +http = { workspace = true } +http-body-util = { workspace = true } +jiff = { workspace = true } +rand = { workspace = true } +reqwest = { workspace = true, default-features = false, features = ["http2"] } +ripemd = { workspace = true } +sha1 = { workspace = true } +sha2 = { workspace = true } +sha3 = { workspace = true } +thiserror = { workspace = true } +tokio = { workspace = true, features = ["full"] } +tracing = { workspace = true } +url = { workspace = true } +uts-bmt = { workspace = true } +uts-contracts = { workspace = true, optional = true } +uts-core = { workspace = true, features = ["io-utils"] } + +[features] +full = [ + "eas-verifier", + "bitcoin-verifier", +] + +bitcoin-verifier = ["uts-core/bitcoin-verifier"] +eas-verifier = [ + "dep:alloy-provider", + "dep:uts-contracts", + "uts-core/eas-verifier", + "uts-contracts/provider-helper", +] + +reqwest-default-tls = [ + "reqwest/default-tls", + "alloy-provider?/reqwest-default-tls", + "uts-core/reqwest-default-tls", +] +reqwest-native-tls = [ + "reqwest/native-tls", + "alloy-provider?/reqwest-native-tls", + "uts-core/reqwest-native-tls", +] +reqwest-rustls = [ + "reqwest/rustls", + "alloy-provider?/reqwest-rustls-tls", + "uts-core/reqwest-rustls", +] + +[lints] +workspace = true diff --git a/packages/sdk-rs/LICENSE-APACHE b/packages/sdk-rs/LICENSE-APACHE new file mode 120000 index 0000000..1cd601d --- /dev/null +++ b/packages/sdk-rs/LICENSE-APACHE @@ -0,0 +1 @@ +../../LICENSE-APACHE \ No newline at end of file diff --git a/packages/sdk-rs/LICENSE-MIT b/packages/sdk-rs/LICENSE-MIT new file mode 120000 index 0000000..b2cfbdc --- /dev/null +++ b/packages/sdk-rs/LICENSE-MIT @@ -0,0 +1 @@ +../../LICENSE-MIT \ No newline at end of file diff --git a/packages/sdk-rs/src/builder.rs b/packages/sdk-rs/src/builder.rs new file mode 100644 index 0000000..2ad38bc --- /dev/null +++ b/packages/sdk-rs/src/builder.rs @@ -0,0 +1,298 @@ +use crate::{Sdk, SdkInner}; +use backon::ExponentialBuilder; +use reqwest::Client; +use std::{ + collections::HashSet, + sync::{Arc, LazyLock}, + time::Duration, +}; +use url::Url; +#[cfg(feature = "eas-verifier")] +use { + alloy_primitives::ChainId, + alloy_provider::{Provider, ProviderBuilder}, + std::collections::BTreeMap, + uts_contracts::provider_helper::{RetryBackoffArgs, ThrottleArgs}, +}; + +/// Default public calendars to use. +static DEFAULT_CALENDARS: LazyLock> = LazyLock::new(|| { + HashSet::from([ + Url::parse("https://lgm1.calendar.test.timestamps.now/").unwrap(), + // Run by Peter Todd + Url::parse("https://a.pool.opentimestamps.org/").unwrap(), + Url::parse("https://b.pool.opentimestamps.org/").unwrap(), + // Run by Riccardo Casatta + Url::parse("https://a.pool.eternitywall.com/").unwrap(), + // Run by Bull Bitcoin + Url::parse("https://ots.btc.catallaxy.com/").unwrap(), + ]) +}); + +#[cfg(feature = "eas-verifier")] +static DEFAULT_PROVIDERS: LazyLock> = LazyLock::new(|| { + BTreeMap::from([ + (1, Url::parse("https://0xrpc.io/eth").unwrap()), + (11155111, Url::parse("https://0xrpc.io/sep").unwrap()), + (534352, Url::parse("https://rpc.scroll.io").unwrap()), + (534351, Url::parse("https://sepolia-rpc.scroll.io").unwrap()), + ]) +}); + +#[derive(Debug, thiserror::Error)] +pub enum BuilderError { + #[error("At least one calendar must be specified")] + NoCalendars, + #[error("Quorum of {quorum} is too high for only {calendar_count} calendars")] + QuorumTooHigh { + quorum: usize, + calendar_count: usize, + }, +} + +type Result = std::result::Result; + +#[derive(Debug, Clone)] +pub struct SdkBuilder { + http_client: Option, + + calendars: HashSet, + quorum: usize, + timeout_seconds: u64, + retry: ExponentialBuilder, + + nonce_size: usize, + + keep_pending: bool, + + #[cfg(feature = "eas-verifier")] + eth_providers: BTreeMap, + #[cfg(feature = "eas-verifier")] + eth_compute_units_per_second: u64, + #[cfg(feature = "eas-verifier")] + eth_requests_per_second: u32, + + #[cfg(feature = "bitcoin-verifier")] + bitcoin_rpc: Url, +} + +impl Default for SdkBuilder { + fn default() -> Self { + Self::try_default_from_calendars(DEFAULT_CALENDARS.clone()) + .expect("Default calendars should be valid") + } +} + +impl SdkBuilder { + /// Create a builder with no calendars and default settings. + pub fn empty() -> Self { + Self { + http_client: None, + + calendars: HashSet::new(), + quorum: 0, + timeout_seconds: 5, + retry: ExponentialBuilder::default(), + + nonce_size: 32, + + keep_pending: false, + + #[cfg(feature = "eas-verifier")] + eth_providers: BTreeMap::new(), + #[cfg(feature = "eas-verifier")] + eth_compute_units_per_second: 20, + #[cfg(feature = "eas-verifier")] + eth_requests_per_second: 25, + + #[cfg(feature = "bitcoin-verifier")] + bitcoin_rpc: Url::parse("https://bitcoin-rpc.publicnode.com").unwrap(), + } + } + + /// Create a builder with the given calendars and default settings. + pub fn try_default_from_calendars(calendars: impl IntoIterator) -> Result { + let calendars = calendars.into_iter().collect::>(); + if calendars.is_empty() { + return Err(BuilderError::NoCalendars); + } + + let this = Self { + calendars, + + #[cfg(feature = "eas-verifier")] + eth_providers: DEFAULT_PROVIDERS.clone(), + ..Self::empty() + }; + + Ok(this.with_two_thirds_quorum()) + } + + /// Set the HTTP client to use for calendar requests. + /// + /// If not set, a default client with a user agent will be used. + pub fn with_http_client(mut self, http_client: Client) -> Self { + self.http_client = Some(http_client); + self + } + + /// Add a calendar to the builder. + pub fn add_calendar(mut self, calendar: Url) -> Self { + self.calendars.insert(calendar); + self + } + + /// Set the quorum for the builder. This is capped to at least 1. + pub fn with_quorum(mut self, quorum: usize) -> Self { + self.quorum = quorum.max(1); + self + } + + /// Set the quorum to 2/3 of the number of calendars, rounded up. This is capped to at least 1. + pub fn with_two_thirds_quorum(self) -> Self { + let two_thirds = (self.calendars.len() * 2).div_ceil(3); + self.with_quorum(two_thirds) + } + + /// Set the timeout for calendar requests in seconds. + pub fn with_timeout_seconds(mut self, timeout_seconds: u64) -> Self { + self.timeout_seconds = timeout_seconds; + self + } + + // Set the retry strategy for calendar requests. + + /// Enable jitter for the backoff. + pub fn with_jitter(mut self) -> Self { + self.retry = self.retry.with_jitter(); + self + } + + /// Set the backoff factor for the backoff. + pub fn with_backoff_factor(mut self, factor: f32) -> Self { + self.retry = self.retry.with_factor(factor); + self + } + + /// Set the minimum delay for the backoff. + pub fn with_min_backoff_delay(mut self, min_delay: Duration) -> Self { + self.retry = self.retry.with_min_delay(min_delay); + self + } + + /// Set the maximum delay for the backoff. + pub fn with_max_backoff_delay(mut self, max_delay: Duration) -> Self { + self.retry = self.retry.with_max_delay(max_delay); + self + } + + /// Set the maximum number of retry attempts for the backoff. + pub fn with_max_backoff_attempts(mut self, max_times: usize) -> Self { + self.retry = self.retry.with_max_times(max_times); + self + } + + /// Set the maximum total delay for the backoff. + pub fn with_max_backoff_total_delay(mut self, total_delay: Option) -> Self { + self.retry = self.retry.with_total_delay(total_delay); + self + } + + /// Set the size of the nonce to use when stamping digests. If 0, no nonce will be added. + pub fn with_nonce_size(mut self, nonce_size: usize) -> Self { + self.nonce_size = nonce_size; + self + } + + /// Keep pending attestations in the proof when upgrading. + pub fn keep_pending(mut self) -> Self { + self.keep_pending = true; + self + } + + /// Add an Ethereum provider for a given chain ID. The URL should point to an Ethereum node that supports the JSON-RPC API. + #[cfg(feature = "eas-verifier")] + pub fn add_eth_provider(mut self, chain_id: ChainId, url: Url) -> Self { + self.eth_providers.insert(chain_id, url); + self + } + + /// Set the compute units per second for Ethereum provider requests. This is used to rate limit requests to avoid overwhelming the provider. + #[cfg(feature = "eas-verifier")] + pub fn with_eth_compute_units_per_second(mut self, compute_units_per_second: u64) -> Self { + self.eth_compute_units_per_second = compute_units_per_second; + self + } + + /// Set the requests per second for Ethereum provider requests. This is used to rate limit requests to avoid overwhelming the provider. + #[cfg(feature = "eas-verifier")] + pub fn with_eth_requests_per_second(mut self, requests_per_second: u32) -> Self { + self.eth_requests_per_second = requests_per_second; + self + } + + /// Build the SDK from the builder, validating the configuration. Returns an error if the configuration is invalid. + pub fn build(self) -> Result { + if self.calendars.is_empty() { + return Err(BuilderError::NoCalendars); + } + if self.quorum > self.calendars.len() { + return Err(BuilderError::QuorumTooHigh { + quorum: self.quorum, + calendar_count: self.calendars.len(), + }); + } + + let http_client = if let Some(client) = self.http_client { + client + } else { + Client::builder() + .user_agent(concat!("uts/", env!("CARGO_PKG_VERSION"))) + .build() + .expect("default HTTP client should be valid") + }; + + #[cfg(feature = "eas-verifier")] + let eth_providers = { + let eth_retry = RetryBackoffArgs { + compute_units_per_second: self.eth_compute_units_per_second, + ..Default::default() + }; + let eth_throttle = ThrottleArgs { + requests_per_second: self.eth_requests_per_second, + }; + self.eth_providers + .into_iter() + .map(|(chain_id, url)| { + let provider = ProviderBuilder::new().connect_client( + alloy_rpc_client::ClientBuilder::default() + .layer(eth_retry.layer()) + .layer(eth_throttle.layer()) + .http(url), + ); + (chain_id, provider.erased()) + }) + .collect() + }; + + Ok(Sdk { + inner: Arc::new(SdkInner { + http_client, + + calendars: self.calendars, + timeout_seconds: self.timeout_seconds, + retry: self.retry, + quorum: self.quorum, + + nonce_size: self.nonce_size, + + keep_pending: self.keep_pending, + + #[cfg(feature = "eas-verifier")] + eth_providers, + #[cfg(feature = "bitcoin-verifier")] + bitcoin_rpc: self.bitcoin_rpc, + }), + }) + } +} diff --git a/packages/sdk-rs/src/error.rs b/packages/sdk-rs/src/error.rs new file mode 100644 index 0000000..48f668d --- /dev/null +++ b/packages/sdk-rs/src/error.rs @@ -0,0 +1,71 @@ +use alloy_primitives::{ChainId, private::serde::de::StdError}; + +pub(crate) type BoxError = Box; + +/// Error type for the SDK, encompassing various error scenarios that can occur during operations. +#[derive(Debug, thiserror::Error)] +pub enum Error { + /// Error during an I/O operation, such as reading or writing files. + #[error("I/O error: {0}")] + Io(#[from] std::io::Error), + /// Error during an HTTP request, such as a network failure or non-success status code. + #[error("HTTP error: {0}")] + Http(BoxError), + /// Error parsing a URL, such as a calendar endpoint. + #[error("URL parse error: {0}")] + Url(#[from] url::ParseError), + /// Error during time-related operations, such as calculating timestamps or durations. + #[error("Time error: {0}")] + Jiff(#[from] jiff::Error), + + /// Error happened during decoding of a proof. + #[error("uts decoding error: {0}")] + Decode(#[from] uts_core::error::DecodeError), + /// Error happened during encoding of a proof. + #[error("uts encoding error: {0}")] + Encode(#[from] uts_core::error::EncodeError), + /// Error indicating that finalization of a timestamp failed due to conflicting inputs. + #[error(transparent)] + Finalization(#[from] uts_core::codec::v1::FinalizationError), + + /// Error indicating that the input provided was empty when it was expected to contain data. + #[error("Input cannot be empty")] + EmptyInput, + + /// Error indicating that a quorum of responses was not reached from the calendars. + #[error("Quorum of {required} not reached, only {received} responses received")] + QuorumNotReached { + /// Number of responses required to reach quorum + required: usize, + /// Number of responses actually received + received: usize, + }, + + /// Error indicating that a digest mismatch occurred, with expected and actual digest values. + #[error("Digest mismatch: expected {expected:?}, got {actual:?}")] + DigestMismatch { + /// Expected digest value + expected: Box<[u8]>, + /// Actual digest value + actual: Box<[u8]>, + }, + + /// Error indicating that an unsupported feature was encountered, with a message describing the unsupported feature. + #[error("Unsupported feature: {0}")] + Unsupported(&'static str), + /// Error indicating that an unsupported chain ID was encountered. + #[error("Unsupported chain ID: {0}")] + UnsupportedChain(ChainId), +} + +impl From for Error { + fn from(value: reqwest::Error) -> Self { + Self::Http(Box::new(value)) + } +} + +impl From for Error { + fn from(value: http::Error) -> Self { + Self::Http(Box::new(value)) + } +} diff --git a/packages/sdk-rs/src/lib.rs b/packages/sdk-rs/src/lib.rs new file mode 100644 index 0000000..82ee28f --- /dev/null +++ b/packages/sdk-rs/src/lib.rs @@ -0,0 +1,181 @@ +//! Rust SDK for the Universal Timestamps protocol. + +// MIT License +// +// Copyright (c) 2025 UTS Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// +// Apache License, Version 2.0 +// +// Copyright (c) 2025 UTS Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use backon::{ExponentialBuilder, Retryable}; +use bytes::Bytes; +use http::StatusCode; +use reqwest::{Client, RequestBuilder}; +use std::{collections::HashSet, sync::Arc, time::Duration}; +use tracing::trace; +use url::Url; +#[cfg(feature = "eas-verifier")] +use {alloy_primitives::ChainId, alloy_provider::DynProvider, std::collections::BTreeMap}; + +mod builder; +mod error; +mod purge; +mod stamp; +mod upgrade; +mod verify; + +pub use error::Error; +pub use purge::PurgeResult; +pub use upgrade::UpgradeResult; + +/// Alias `Result` to use the crate's error type by default. +pub type Result = std::result::Result; + +/// SDK for interacting with Universal Timestamping protocol. +#[derive(Debug, Clone)] +pub struct Sdk { + inner: Arc, +} + +#[derive(Debug)] +struct SdkInner { + http_client: Client, + + // Stamp + calendars: HashSet, + quorum: usize, + timeout_seconds: u64, + retry: ExponentialBuilder, + + // Privacy + nonce_size: usize, + + // Upgrade + keep_pending: bool, + + // Verify + #[cfg(feature = "eas-verifier")] + eth_providers: BTreeMap, + #[cfg(feature = "bitcoin-verifier")] + bitcoin_rpc: Url, +} + +impl Default for Sdk { + fn default() -> Self { + Self::new() + } +} + +impl Sdk { + /// Create a new SDK with default settings. + pub fn new() -> Self { + Self::builder() + .build() + .expect("Default SDK should be valid") + } + + /// Create a new SDK builder with default settings. + pub fn builder() -> builder::SdkBuilder { + builder::SdkBuilder::default() + } + + /// Create a new SDK builder with given calendars and default settings. + pub fn try_builder_from_calendars( + calendars: impl IntoIterator, + ) -> Result { + builder::SdkBuilder::try_default_from_calendars(calendars) + } + + async fn http_request_with_retry( + &self, + method: http::Method, + url: Url, + response_size_limit: usize, + builder_fn: Builder, + ) -> Result<(http::response::Parts, Bytes)> + where + Builder: Fn(RequestBuilder) -> RequestBuilder + Send + Sync + 'static, + { + let client = self.inner.http_client.clone(); + let timeout_seconds = self.inner.timeout_seconds; + let res = { + move || { + let client = client.clone(); + let method = method.clone(); + let url = url.clone(); + let req = client + .request(method, url) + .timeout(Duration::from_secs(timeout_seconds)); + let req = builder_fn(req); + + async move { + let res = req.send().await?; + if res.status().is_server_error() + || ( + // specially treat 404 as non-error + res.status().is_client_error() && res.status() != StatusCode::NOT_FOUND + ) + { + res.error_for_status() + } else { + Ok::<_, reqwest::Error>(res) + } + } + } + } + .retry(self.inner.retry) + .when(|e| { + if e.is_connect() || e.is_timeout() { + return true; + } + if let Some(status) = e.status() { + return status.is_server_error() || status == StatusCode::TOO_MANY_REQUESTS; + } + false + }) + .notify(|e, duration| { + trace!("retrying error {e:?} after sleeping {duration:?}"); + }) + .await?; + + let res: http::Response = res.into(); + let (parts, body) = res.into_parts(); + let body = http_body_util::Limited::new(body, response_size_limit); + let bytes = http_body_util::BodyExt::collect(body) + .await + .map_err(Error::Http)? + .to_bytes(); + Ok((parts, bytes)) + } +} diff --git a/packages/sdk-rs/src/purge.rs b/packages/sdk-rs/src/purge.rs new file mode 100644 index 0000000..6f99a61 --- /dev/null +++ b/packages/sdk-rs/src/purge.rs @@ -0,0 +1,114 @@ +use std::collections::HashSet; +use tracing::info; +use uts_core::{ + alloc::Allocator, + codec::v1::{Attestation, DetachedTimestamp, PendingAttestation}, +}; + +use crate::Sdk; + +/// Result of a purge operation on a detached timestamp. +#[derive(Debug)] +pub struct PurgeResult { + /// URIs of the pending attestations that were purged. + pub purged: Vec, + /// Whether the timestamp still has any attestations remaining. + pub has_remaining: bool, +} + +impl Sdk { + /// Lists all pending attestation URIs in the given detached timestamp. + pub fn list_pending(stamp: &DetachedTimestamp) -> Vec { + stamp + .attestations() + .filter_map(|att| { + PendingAttestation::from_raw(att) + .ok() + .map(|p| p.uri.to_string()) + }) + .collect() + } + + /// Purges all pending attestations from the given detached timestamp. + /// + /// Returns a [`PurgeResult`] containing the URIs of purged attestations + /// and whether the timestamp still has remaining (non-pending) attestations. + /// + /// If all attestations were pending, the timestamp becomes invalid and + /// `has_remaining` will be `false` — callers should handle this case + /// (e.g., by not writing the file). + pub fn purge_pending(stamp: &mut DetachedTimestamp) -> PurgeResult { + Self::purge_pending_by_uris(stamp, None) + } + + /// Purges selected pending attestations from the given detached timestamp. + /// + /// If `uris_to_purge` is `None`, all pending attestations are purged. + /// If `uris_to_purge` is `Some(set)`, only pending attestations whose URI + /// is in the set are purged. + /// + /// This is implemented using [`Timestamp::retain_attestations`] under the hood. + /// + /// Returns a [`PurgeResult`] containing the URIs of purged attestations + /// and whether the timestamp still has remaining (non-pending) attestations. + pub fn purge_pending_by_uris( + stamp: &mut DetachedTimestamp, + uris_to_purge: Option<&HashSet>, + ) -> PurgeResult { + let pending_uris = Self::list_pending(stamp); + + if pending_uris.is_empty() { + info!("no pending attestations found"); + return PurgeResult { + purged: Vec::new(), + has_remaining: true, + }; + } + + let purged_uris: Vec = match &uris_to_purge { + Some(set) => pending_uris + .iter() + .filter(|u| set.contains(*u)) + .cloned() + .collect(), + None => pending_uris, + }; + + if purged_uris.is_empty() { + info!("no matching pending attestations to purge"); + return PurgeResult { + purged: Vec::new(), + has_remaining: true, + }; + } + + let result = stamp.retain_attestations(|att| { + if att.tag != PendingAttestation::TAG { + return true; // keep non-pending attestations + } + match &uris_to_purge { + None => false, // purge all pending + Some(set) => { + let uri = PendingAttestation::from_raw(att) + .map(|p| p.uri.to_string()) + .unwrap_or_default(); + !set.contains(&uri) // keep if NOT in the purge set + } + } + }); + + if let Some(purged) = result { + info!("purged {purged} pending attestation(s)"); + PurgeResult { + purged: purged_uris, + has_remaining: true, + } + } else { + info!("all attestations were pending, timestamp is now empty"); + PurgeResult { + purged: purged_uris, + has_remaining: false, + } + } + } +} diff --git a/packages/sdk-rs/src/stamp.rs b/packages/sdk-rs/src/stamp.rs new file mode 100644 index 0000000..ec9dd0c --- /dev/null +++ b/packages/sdk-rs/src/stamp.rs @@ -0,0 +1,196 @@ +use crate::{Result, Sdk, error::Error}; +use digest::{Digest, FixedOutputReset, Output}; +use http::Method; +use std::path::PathBuf; +use tokio::fs; +use tracing::{debug, instrument}; +use url::Url; +use uts_bmt::MerkleTree; +use uts_core::{ + alloc, + alloc::{Allocator, Global}, + codec::{ + DecodeIn, + v1::{DetachedTimestamp, DigestHeader, Timestamp, TimestampBuilder, opcode::DigestOpExt}, + }, + utils::{HashAsyncFsExt, Hexed}, +}; + +impl Sdk { + /// Creates a timestamp for the given files. + pub async fn stamp_files(&self, files: &[PathBuf]) -> Result> + where + D: Digest + FixedOutputReset + DigestOpExt + Send, + Output: Copy, + { + Ok(Vec::from_iter( + self.stamp_files_in::<_, D>(files, Global).await?, + )) + } + + /// Creates a timestamp for the given digests. + pub async fn stamp_digest(&self, digests: &[Output]) -> Result> + where + D: Digest + FixedOutputReset + DigestOpExt + Send, + Output: Copy, + { + Ok(Vec::from_iter( + self.stamp_digests_in::<_, D>(digests, Global).await?, + )) + } + + /// Creates a timestamp for the given files in the provided allocator. + /// + /// # Note + /// + /// This uses the `allocator_api2` crate for allocator api. + pub async fn stamp_files_in( + &self, + files: &[PathBuf], + allocator: A, + ) -> Result, A>> + where + A: Allocator + Clone, + D: Digest + FixedOutputReset + DigestOpExt + Send, + Output: Copy, + { + if files.is_empty() { + return Err(Error::EmptyInput); + } + + let digests = futures::future::join_all(files.iter().map(|f| hash_file::(f.clone()))) + .await + .into_iter() + .collect::, _>>()?; + + self.stamp_digests_in::<_, D>(&digests, allocator).await + } + + /// Creates a timestamp for the given digests in the provided allocator. + /// + /// # Note + /// + /// This uses the `allocator_api2` crate for allocator api. + pub async fn stamp_digests_in( + &self, + digests: &[Output], + allocator: A, + ) -> Result, A>> + where + A: Allocator + Clone, + D: Digest + FixedOutputReset + DigestOpExt + Send, + Output: Copy, + { + if digests.is_empty() { + return Err(Error::EmptyInput); + } + + let mut builders: alloc::vec::Vec, A> = alloc::vec![in allocator.clone(); Timestamp::builder_in(allocator.clone()); digests.len() ]; + + let mut nonced_digest = alloc::vec::Vec::with_capacity_in(digests.len(), allocator.clone()); + + for (builder, digest) in builders.iter_mut().zip(digests.iter()) { + if self.inner.nonce_size == 0 { + nonced_digest.push(*digest); + continue; + } + + let mut hasher = D::new(); + Digest::update(&mut hasher, digest); + + let mut nonce = + alloc::vec::Vec::with_capacity_in(self.inner.nonce_size, allocator.clone()); + nonce.resize(self.inner.nonce_size, 0); + rand::fill(&mut nonce[..]); + + Digest::update(&mut hasher, &nonce); + builder.append(nonce).digest::(); + + nonced_digest.push(hasher.finalize()) + } + + let root = if digests.len() > 1 { + let internal_tire = MerkleTree::::new(&nonced_digest); + let root = internal_tire.root(); + debug!(internal_tire_root = ?Hexed(root)); + + for (builder, leaf) in builders.iter_mut().zip(nonced_digest) { + let proof = internal_tire.get_proof_iter(&leaf).expect("infallible"); + builder.merkle_proof(proof); + } + *root + } else { + nonced_digest[0] + }; + + let stamps_futures = futures::future::join_all( + self.inner + .calendars + .iter() + .map(|calendar| self.request_calendar(calendar.clone(), &root, allocator.clone())), + ) + .await + .into_iter() + .filter_map(|res| res.ok()); + let mut results = + alloc::vec::Vec::with_capacity_in(self.inner.calendars.len(), allocator.clone()); + for stamp in stamps_futures { + results.push(stamp); + } + + if results.len() < self.inner.quorum { + return Err(Error::QuorumNotReached { + required: self.inner.quorum, + received: results.len(), + }); + } + + let merged = if results.len() == 1 { + results.into_iter().next().unwrap() + } else { + Timestamp::::merge_in(results, allocator.clone()) + }; + + let mut stamps = alloc::vec::Vec::with_capacity_in(builders.len(), allocator.clone()); + for (builder, digest) in builders.into_iter().zip(digests.iter()) { + let timestamp = builder.concat(merged.clone()); + let header = DigestHeader::new::(*digest); + let timestamp = DetachedTimestamp::from_parts(header, timestamp); + stamps.push(timestamp); + } + + Ok(stamps) + } + + #[instrument(skip(self, allocator), level = "debug", err)] + async fn request_calendar( + &self, + calendar: Url, + root: &[u8], + allocator: A, + ) -> Result> { + let url = calendar.join("digest")?; + + let root = root.to_vec(); + let (_, body) = self + .http_request_with_retry( + Method::POST, + url, + 10 * 1024, // 10 KB + move |req| { + req.header("Accept", "application/vnd.opentimestamps.v1") + .body(root.clone()) + }, + ) + .await?; + + Ok(Timestamp::::decode_in(&mut &*body, allocator)?) + } +} + +async fn hash_file(path: PathBuf) -> Result> { + let mut hasher = D::new(); + let file = fs::File::open(path).await?; + HashAsyncFsExt::update(&mut hasher, file).await?; + Ok(hasher.finalize()) +} diff --git a/packages/sdk-rs/src/upgrade.rs b/packages/sdk-rs/src/upgrade.rs new file mode 100644 index 0000000..0dedb8a --- /dev/null +++ b/packages/sdk-rs/src/upgrade.rs @@ -0,0 +1,90 @@ +use crate::{Result, Sdk, error::Error}; +use http::{Method, StatusCode}; +use std::collections::BTreeMap; +use tracing::{debug, warn}; +use url::Url; +use uts_core::{ + alloc::Allocator, + codec::{ + DecodeIn, + v1::{Attestation, DetachedTimestamp, PendingAttestation, Timestamp}, + }, + utils::Hexed, +}; + +/// Result of attempting to upgrade a pending attestation. +#[derive(Debug)] +pub enum UpgradeResult { + /// The attestation has been successfully upgraded. + Upgraded, + /// The attestation is still pending and not ready to be upgraded. + Pending, + /// The attestation upgrade failed due to an error. + Failed(Error), +} + +impl Sdk { + /// Upgrades all pending attestations in the given detached timestamp. + pub async fn upgrade( + &self, + stamp: &mut DetachedTimestamp, + ) -> Result> { + let mut results = BTreeMap::new(); + + let alloc = stamp.allocator().clone(); + for step in stamp.pending_attestations_mut() { + let (calendar_server, retrieve_uri) = { + let Timestamp::Attestation(attestation) = step else { + unreachable!("bug: PendingAttestationIterMut should only yield Attestations"); + }; + let commitment = attestation.value().expect("finalized when decode"); + let calendar_server = PendingAttestation::from_raw(&*attestation)?.uri; + + let retrieve_uri = Url::parse(&calendar_server)? + .join(&format!("timestamp/{}", Hexed(commitment)))?; + + (calendar_server.to_string(), retrieve_uri) + }; + + let result = self + .http_request_with_retry( + Method::GET, + retrieve_uri, + 10 * 1024 * 1024, // 10 MiB response size limit + |req| req.header("Accept", "application/vnd.opentimestamps.v1"), + ) + .await; + + let result = match result { + Ok((parts, _)) if parts.status == StatusCode::NOT_FOUND => { + debug!("attestation from {calendar_server} not ready yet, skipping"); + UpgradeResult::Pending + } + Ok((_, response)) => { + let attestation = Timestamp::decode_in(&mut &*response, alloc.clone())?; + + *step = if self.inner.keep_pending { + Timestamp::merge_in( + uts_core::alloc::vec![in alloc.clone(); attestation, step.clone()], + alloc.clone(), + ) + } else { + attestation + }; + UpgradeResult::Upgraded + } + Err(e) => { + warn!("failed to upgrade pending attestation from {calendar_server}: {e}"); + UpgradeResult::Failed(e) + } + }; + + if let Some(old) = results.insert(calendar_server.clone(), result) { + warn!( + "multiple pending attestations from {calendar_server}, previous result was {old:?}, you should only attest to a calendar once per timestamp" + ); + } + } + Ok(results) + } +} diff --git a/packages/sdk-rs/src/verify.rs b/packages/sdk-rs/src/verify.rs new file mode 100644 index 0000000..039ddca --- /dev/null +++ b/packages/sdk-rs/src/verify.rs @@ -0,0 +1,337 @@ +use crate::{Error, Result, Sdk}; +#[cfg(any(feature = "eas-verifier", feature = "bitcoin-verifier"))] +use backon::RetryableWithContext; +use digest::Digest; +use jiff::Timestamp; +use std::path::Path; +use tokio::{ + fs::File, + io::{AsyncReadExt, BufReader}, +}; +use uts_core::{ + alloc, + alloc::Allocator, + codec::v1::{ + Attestation, DetachedTimestamp, PendingAttestation, RawAttestation, + opcode::{KECCAK256, RIPEMD160, SHA1, SHA256}, + }, +}; +#[cfg(feature = "eas-verifier")] +use { + alloy_provider::DynProvider, + uts_contracts::eas::EAS_ADDRESSES, + uts_core::codec::v1::{EASAttestation, EASTimestamped}, + uts_core::verifier::EASVerifier, +}; +#[cfg(feature = "bitcoin-verifier")] +use {uts_core::codec::v1::BitcoinAttestation, uts_core::verifier::BitcoinVerifier}; + +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum AttestationStatusKind { + /// The attestation is valid. + Valid(Timestamp), + /// The attestation is invalid. + Invalid, + /// The attestation is pending and has not yet been verified. + Pending, + /// The attestation is unknown, either because it is of an unsupported type or because an error occurred during verification. + Unknown, +} + +#[derive(Debug, Clone)] +pub struct AttestationStatus { + pub attestation: RawAttestation, + pub status: AttestationStatusKind, +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum VerifyStatus { + /// The timestamp is valid and all attestations are valid. + Valid(Timestamp), + /// The timestamp is partially valid, at least one attestation is not valid. + PartiallyValid(Timestamp), + /// The timestamp is invalid, all attestations are invalid. + Invalid, + /// All attestations are pending. + Pending, + /// All attestations are unknown. + Unknown, +} + +impl Sdk { + /// Verifies the given file against the given detached timestamp, returning a list of attestation statuses. + pub async fn verify( + &self, + file: impl AsRef, + timestamp: &DetachedTimestamp, + ) -> Result> { + Ok(Vec::from_iter( + self.verify_in(file, timestamp, alloc::Global).await?, + )) + } + + /// Verifies the given file against the given detached timestamp, returning a list of attestation statuses. + /// + /// This is the same as `verify`, but allows specifying a custom allocator for the attestation statuses. + /// + /// # Note + /// + /// This uses the `allocator_api2` crate for allocator api. + pub async fn verify_in( + &self, + file: impl AsRef, + timestamp: &DetachedTimestamp, + allocator: A, + ) -> Result, A>> { + let digest_header = timestamp.header(); + match digest_header.kind().tag() { + SHA1 => { + self.verify_digest::(file, digest_header.digest()) + .await + } + RIPEMD160 => { + self.verify_digest::(file, digest_header.digest()) + .await + } + SHA256 => { + self.verify_digest::(file, digest_header.digest()) + .await + } + KECCAK256 => { + self.verify_digest::(file, digest_header.digest()) + .await + } + _ => return Err(Error::Unsupported("unknown digest algorithm")), + }?; + + timestamp.try_finalize()?; + let mut result = + alloc::vec::Vec::with_capacity_in(timestamp.attestations().count(), allocator); + for attestation in timestamp.attestations() { + let attestation = attestation.to_owned(); + + if attestation.tag == PendingAttestation::TAG { + result.push(AttestationStatus { + attestation, + status: AttestationStatusKind::Pending, + }); + continue; + } + + let status = self + .verify_attestation_inner(&attestation) + .await + .unwrap_or(AttestationStatusKind::Unknown); + + result.push(AttestationStatus { + attestation, + status, + }); + } + + Ok(result) + } + + /// Verifies the digest of the given file against the expected digest. + pub async fn verify_digest( + &self, + file: impl AsRef, + expected: &[u8], + ) -> Result<()> { + let mut file = BufReader::new(File::open(file.as_ref()).await?); + let mut hasher = D::new(); + let mut buffer = [0u8; 64 * 1024]; // 64KB buffer + loop { + let bytes_read = file.read(&mut buffer).await?; + if bytes_read == 0 { + break; + } + hasher.update(&buffer[..bytes_read]); + } + let actual = hasher.finalize(); + + if *actual != *expected { + return Err(Error::DigestMismatch { + expected: expected.to_vec().into_boxed_slice(), + actual: actual.to_vec().into_boxed_slice(), + }); + } + Ok(()) + } + + /// Aggregate the individual attestation statuses into an overall verification status for the timestamp. + /// + /// The earliest valid attestation timestamp is used as the timestamp for the overall status, if there is at least one valid attestation. + /// + /// The logic is as follows: + /// - If there is at least one VALID attestation: + /// - If there are also INVALID or UNKNOWN attestations, the overall status is PARTIAL_VALID + /// - Otherwise, the overall status is VALID + /// - If there are no VALID attestations, but at least one PENDING attestation, the overall status is PENDING + /// - If there are no VALID attestations, but at least one UNKNOWN attestation, the overall status is UNKNOWN + /// - If there are no VALID or PENDING attestations, the overall status is INVALID + pub fn aggregate_verify_results(&self, results: &[AttestationStatus]) -> VerifyStatus { + let mut valid_ts = None; + let mut has_invalid = false; + let mut has_unknown = false; + let mut has_pending = false; + + for status in results { + match status.status { + AttestationStatusKind::Valid(ts) => { + if valid_ts.is_none() || ts < valid_ts.unwrap() { + valid_ts = Some(ts); + } + } + AttestationStatusKind::Invalid => has_invalid = true, + AttestationStatusKind::Unknown => has_unknown = true, + AttestationStatusKind::Pending => has_pending = true, + } + } + + if let Some(ts) = valid_ts { + if has_invalid || has_unknown { + VerifyStatus::PartiallyValid(ts) + } else { + VerifyStatus::Valid(ts) + } + } else if has_pending { + VerifyStatus::Pending + } else if has_unknown { + VerifyStatus::Unknown + } else { + VerifyStatus::Invalid + } + } + + async fn verify_attestation_inner( + &self, + attestation: &RawAttestation, + ) -> Result { + let _expected = attestation + .value() + .expect("Attestation value should be finalized"); + + #[cfg(feature = "eas-verifier")] + if attestation.tag == EASAttestation::TAG { + let attestation = EASAttestation::from_raw(attestation)?; + return self.verify_eas_attestation(_expected, attestation).await; + } else if attestation.tag == EASTimestamped::TAG { + let attestation = EASTimestamped::from_raw(attestation)?; + return self.verify_eas_timestamped(_expected, attestation).await; + } + + #[cfg(feature = "bitcoin-verifier")] + if attestation.tag == BitcoinAttestation::TAG { + let attestation = BitcoinAttestation::from_raw(attestation)?; + return self.verify_bitcoin(_expected, attestation).await; + } + + Ok(AttestationStatusKind::Unknown) + } + + #[cfg(feature = "eas-verifier")] + async fn verify_eas_attestation( + &self, + expected: &[u8], + attestation: EASAttestation, + ) -> Result { + let chain = attestation.chain; + let provider = self + .inner + .eth_providers + .get(&chain.id()) + .ok_or_else(|| Error::UnsupportedChain(chain.id()))?; + let eas_address = EAS_ADDRESSES + .get(&chain.id()) + .ok_or_else(|| Error::UnsupportedChain(chain.id()))?; + + let (_, result) = { + |verifier: EASVerifier| async { + let res = verifier.verify_attestation(&attestation, expected).await; + (verifier, res) + } + } + .retry(self.inner.retry) + .when(|e| e.should_retry()) + .context(EASVerifier::new(*eas_address, provider.clone())) + .await; + + match result { + Ok(result) => { + let ts = Timestamp::from_second(result.time.try_into().expect("i64 overflow"))?; + Ok(AttestationStatusKind::Valid(ts)) + } + Err(e) if e.is_fatal() => Ok(AttestationStatusKind::Invalid), + Err(_) => Ok(AttestationStatusKind::Unknown), + } + } + + #[cfg(feature = "eas-verifier")] + async fn verify_eas_timestamped( + &self, + expected: &[u8], + attestation: EASTimestamped, + ) -> Result { + let chain = attestation.chain; + let provider = self + .inner + .eth_providers + .get(&chain.id()) + .ok_or_else(|| Error::UnsupportedChain(chain.id()))?; + let eas_address = EAS_ADDRESSES + .get(&chain.id()) + .ok_or_else(|| Error::UnsupportedChain(chain.id()))?; + + let (_, result) = { + |verifier: EASVerifier| async { + let res = verifier.verify_timestamped(&attestation, expected).await; + (verifier, res) + } + } + .retry(self.inner.retry) + .when(|e| e.should_retry()) + .context(EASVerifier::new(*eas_address, provider.clone())) + .await; + + match result { + Ok(time) => { + let ts = Timestamp::from_second(time.try_into().expect("i64 overflow"))?; + Ok(AttestationStatusKind::Valid(ts)) + } + Err(e) if e.is_fatal() => Ok(AttestationStatusKind::Invalid), + Err(_) => Ok(AttestationStatusKind::Unknown), + } + } + + #[cfg(feature = "bitcoin-verifier")] + async fn verify_bitcoin( + &self, + expected: &[u8], + attestation: BitcoinAttestation, + ) -> Result { + let (_, result) = { + |verifier: BitcoinVerifier| async { + let res = verifier.verify(&attestation, expected).await; + (verifier, res) + } + } + .retry(self.inner.retry) + .when(|e| e.should_retry()) + .context(BitcoinVerifier::from_parts( + self.inner.http_client.clone(), + self.inner.bitcoin_rpc.clone(), + self.inner.retry, + )) + .await; + + match result { + Ok(header) => { + let ts = Timestamp::from_second(header.time.try_into().expect("i64 overflow"))?; + Ok(AttestationStatusKind::Valid(ts)) + } + Err(e) if e.is_fatal() => Ok(AttestationStatusKind::Invalid), + Err(_) => Ok(AttestationStatusKind::Unknown), + } + } +} diff --git a/packages/sdk-ts/LICENSE-APACHE b/packages/sdk-ts/LICENSE-APACHE new file mode 120000 index 0000000..1cd601d --- /dev/null +++ b/packages/sdk-ts/LICENSE-APACHE @@ -0,0 +1 @@ +../../LICENSE-APACHE \ No newline at end of file diff --git a/packages/sdk-ts/LICENSE-MIT b/packages/sdk-ts/LICENSE-MIT new file mode 120000 index 0000000..b2cfbdc --- /dev/null +++ b/packages/sdk-ts/LICENSE-MIT @@ -0,0 +1 @@ +../../LICENSE-MIT \ No newline at end of file diff --git a/packages/sdk/package.json b/packages/sdk-ts/package.json similarity index 88% rename from packages/sdk/package.json rename to packages/sdk-ts/package.json index 0c459f6..ca16541 100644 --- a/packages/sdk/package.json +++ b/packages/sdk-ts/package.json @@ -1,6 +1,6 @@ { - "name": "@uts/sdk", - "version": "0.1.0", + "name": "@universal-timestamps/sdk", + "version": "0.1.0-alpha.1", "description": "", "type": "module", "main": "./dist/index.js", @@ -33,7 +33,8 @@ "license": "(MIT OR Apache-2.0)", "dependencies": { "@noble/hashes": "^2.0.1", - "ethers": "^6.16.0" + "@uts/contracts": "workspace:*", + "viem": "^2.47.0" }, "devDependencies": { "@rollup/plugin-node-resolve": "^16.0.3", diff --git a/packages/sdk-ts/rollup.config.js b/packages/sdk-ts/rollup.config.js new file mode 100644 index 0000000..b626898 --- /dev/null +++ b/packages/sdk-ts/rollup.config.js @@ -0,0 +1,25 @@ +import resolve from '@rollup/plugin-node-resolve' +import typescript from '@rollup/plugin-typescript' + +/** @type {import('rollup').RollupOptions[]} */ +export default [ + { + input: 'src/index.ts', + output: [ + { + file: 'dist/index.js', + format: 'es', + sourcemap: true, + }, + ], + external: [/node_modules/, '@noble/hashes', 'viem', '@uts/contracts'], + plugins: [ + resolve(), + typescript({ + tsconfig: './tsconfig.json', + declaration: false, + declarationMap: false, + }), + ], + }, +] diff --git a/packages/sdk/src/bmt.ts b/packages/sdk-ts/src/bmt.ts similarity index 98% rename from packages/sdk/src/bmt.ts rename to packages/sdk-ts/src/bmt.ts index c9e50ad..94316ea 100644 --- a/packages/sdk/src/bmt.ts +++ b/packages/sdk-ts/src/bmt.ts @@ -101,9 +101,6 @@ export class UnorderedMerkleTree { } } - // Sort leaves unstable (lexicographical) - leavesBlock.sort(compareBytes) - // Copy back to tree nodes for (let i = 0; i < len; i++) { nodes[len + i] = leavesBlock[i] diff --git a/packages/sdk/src/codec/constants.ts b/packages/sdk-ts/src/codec/constants.ts similarity index 81% rename from packages/sdk/src/codec/constants.ts rename to packages/sdk-ts/src/codec/constants.ts index 9d2a6d2..0c49a15 100644 --- a/packages/sdk/src/codec/constants.ts +++ b/packages/sdk-ts/src/codec/constants.ts @@ -38,9 +38,12 @@ export const BITCOIN_ATTESTATION_TAG = new Uint8Array([ export const PENDING_ATTESTATION_TAG = new Uint8Array([ 0x83, 0xdf, 0xe3, 0x0d, 0x2e, 0xf9, 0x0c, 0x8e, ]) -export const ETHEREUM_UTS_ATTESTATION_TAG = new Uint8Array([ - 0xea, 0xf2, 0xbc, 0x69, 0x3c, 0x93, 0x25, 0x1c, +export const EAS_ATTEST_TAG = new Uint8Array([ + 0x8b, 0xf4, 0x6b, 0xf4, 0xcf, 0xd6, 0x74, 0xfa, +]) +export const EAS_TIMESTAMPED_TAG = new Uint8Array([ + 0x5a, 0xaf, 0xce, 0xeb, 0x1c, 0x7a, 0xd5, 0x8e, ]) export const MAX_URI_LEN = 1000 -export const SAFE_URL_REGEX = /^http(s)?:\/\/[\s\w./:-]+$/ +export const SAFE_URL_REGEX = /^[a-zA-Z0-9.\-_/:]+$/ diff --git a/packages/sdk/src/codec/decode.ts b/packages/sdk-ts/src/codec/decode.ts similarity index 84% rename from packages/sdk/src/codec/decode.ts rename to packages/sdk-ts/src/codec/decode.ts index 147e679..4593e14 100644 --- a/packages/sdk/src/codec/decode.ts +++ b/packages/sdk-ts/src/codec/decode.ts @@ -1,4 +1,4 @@ -import { hexlify } from 'ethers/utils' +import { hexlify } from '../utils.ts' import { DecodeError, ErrorCode } from '../errors.ts' import { DIGEST_OPS, @@ -7,7 +7,8 @@ import { type DetachedTimestamp, type DigestHeader, type DigestOp, - type EthereumUTSAttestation, + type EASAttestation, + type EASTimestamped, type ExecutionStep, type ForkStep, type Op, @@ -19,7 +20,8 @@ import { ATTESTATION_TAG_LENGTH, BITCOIN_ATTESTATION_TAG, DIGEST_LENGTHS, - ETHEREUM_UTS_ATTESTATION_TAG, + EAS_ATTEST_TAG, + EAS_TIMESTAMPED_TAG, getOpName, MAGIC_BYTES, MAX_URI_LEN, @@ -256,50 +258,23 @@ export default class Decoder { } } - readEthereumUTSAttestation(): EthereumUTSAttestation { + readEASAttestation(): EASAttestation { const chain = this.readNumber() - const height = this.readNumber() + const uid = this.readBytes(32) - if (this.remaining === 0) { - return { - kind: 'ethereum-uts', - chain, - height, - } - } - - if (this.remaining > 0 && this.remaining < 20) { - throw new DecodeError( - ErrorCode.INVALID_STRUCTURE, - `Invalid extra metadata length for Ethereum UTS attestation: expected 0 or at least 20 bytes, got ${this.remaining}`, - { offset: this.offset, context: { remaining: this.remaining } }, - ) - } - // Extra metadata is optional, only read if there's remaining data - const contract = this.readBytes(20) - if (this.remaining === 0) { - return { - kind: 'ethereum-uts', - chain, - height, - metadata: { contract }, - } + return { + kind: 'eas-attestation', + chain, + uid, } + } - if (this.remaining > 0 && this.remaining < 32) { - throw new DecodeError( - ErrorCode.INVALID_STRUCTURE, - `Invalid extra metadata length for Ethereum UTS attestation with contract: expected 0 or 32 bytes, got ${this.remaining}`, - { offset: this.offset, context: { remaining: this.remaining } }, - ) - } + readEASTimestamped(): EASTimestamped { + const chain = this.readNumber() - const txHash = this.readBytes(32) return { - kind: 'ethereum-uts', + kind: 'eas-timestamped', chain, - height, - metadata: { contract, txHash }, } } @@ -326,10 +301,12 @@ export default class Decoder { const attestation = decoder.readPendingAttestation() if (strict) decoder.checkEOF() return { op: 'ATTESTATION', attestation } - } else if ( - tag.every((byte, idx) => byte === ETHEREUM_UTS_ATTESTATION_TAG[idx]) - ) { - const attestation = decoder.readEthereumUTSAttestation() + } else if (tag.every((byte, idx) => byte === EAS_ATTEST_TAG[idx])) { + const attestation = decoder.readEASAttestation() + if (strict) decoder.checkEOF() + return { op: 'ATTESTATION', attestation } + } else if (tag.every((byte, idx) => byte === EAS_TIMESTAMPED_TAG[idx])) { + const attestation = decoder.readEASTimestamped() if (strict) decoder.checkEOF() return { op: 'ATTESTATION', attestation } } else { diff --git a/packages/sdk/src/codec/encode.ts b/packages/sdk-ts/src/codec/encode.ts similarity index 82% rename from packages/sdk/src/codec/encode.ts rename to packages/sdk-ts/src/codec/encode.ts index d157987..9be8dc3 100644 --- a/packages/sdk/src/codec/encode.ts +++ b/packages/sdk-ts/src/codec/encode.ts @@ -3,7 +3,8 @@ import { MAGIC_BYTES, PENDING_ATTESTATION_TAG, BITCOIN_ATTESTATION_TAG, - ETHEREUM_UTS_ATTESTATION_TAG, + EAS_ATTEST_TAG, + EAS_TIMESTAMPED_TAG, MAX_URI_LEN, SAFE_URL_REGEX, DIGEST_LENGTHS, @@ -19,9 +20,10 @@ import type { Op, PendingAttestation, BitcoinAttestation, - EthereumUTSAttestation, + EASAttestation, + EASTimestamped, } from '../types.ts' -import { getBytes, hexlify } from 'ethers/utils' +import { getBytes } from '../utils.ts' import { EncodeError, ErrorCode } from '../errors.ts' export default class Encoder { @@ -216,40 +218,10 @@ export default class Encoder { return this } - writeEthereumUTSAttestation(attestation: EthereumUTSAttestation): this { - this.writeU32(attestation.chain) - this.writeU32(attestation.height) - if (attestation.metadata) { - // trailing optional - if (attestation.metadata.contract) { - const contractBytes = getBytes(attestation.metadata.contract) - if (contractBytes.length !== 20) { - throw new EncodeError( - ErrorCode.LENGTH_MISMATCH, - `Invalid contract address in Ethereum UTS attestation: ${attestation.metadata.contract}`, - { - offset: this.offset, - context: { contract: hexlify(attestation.metadata.contract) }, - }, - ) - } - this.writeBytes(contractBytes) - - if (attestation.metadata.txHash) { - const txHashBytes = getBytes(attestation.metadata.txHash) - if (txHashBytes.length !== 32) { - throw new EncodeError( - ErrorCode.LENGTH_MISMATCH, - `Invalid transaction hash in Ethereum UTS attestation: ${attestation.metadata.txHash}`, - { - offset: this.offset, - context: { txHash: hexlify(attestation.metadata.txHash) }, - }, - ) - } - this.writeBytes(txHashBytes) - } - } + writeEAS(attestation: EASAttestation | EASTimestamped): this { + this.writeBigUint(BigInt(attestation.chain)) + if ('uid' in attestation) { + this.writeBytes(getBytes(attestation.uid)) } return this } @@ -268,9 +240,14 @@ export default class Encoder { encoder.writeBitcoinAttestation(step.attestation) this.writeLengthPrefixedBytes(encoder.toUint8Array()) break - case 'ethereum-uts': - this.writeBytes(ETHEREUM_UTS_ATTESTATION_TAG) - encoder.writeEthereumUTSAttestation(step.attestation) + case 'eas-attestation': + this.writeBytes(EAS_ATTEST_TAG) + encoder.writeEAS(step.attestation) + this.writeLengthPrefixedBytes(encoder.toUint8Array()) + break + case 'eas-timestamped': + this.writeBytes(EAS_TIMESTAMPED_TAG) + encoder.writeEAS(step.attestation) this.writeLengthPrefixedBytes(encoder.toUint8Array()) break case 'unknown': diff --git a/packages/sdk-ts/src/eas.ts b/packages/sdk-ts/src/eas.ts new file mode 100644 index 0000000..b5253e3 --- /dev/null +++ b/packages/sdk-ts/src/eas.ts @@ -0,0 +1,52 @@ +import { decodeAbiParameters, type Hex, type PublicClient } from 'viem' +import { ieasAbi } from '@uts/contracts' + +export const NO_EXPIRATION = 0n + +export interface OnChainAttestation { + uid: Hex + schema: Hex + time: bigint + expirationTime: bigint + revocationTime: bigint + refUID: Hex + recipient: Hex + attester: Hex + revocable: boolean + data: Hex +} + +export async function readEASTimestamp( + client: PublicClient, + easAddress: Hex, + data: Hex, +): Promise { + return client.readContract({ + address: easAddress, + abi: ieasAbi, + functionName: 'getTimestamp', + args: [data], + }) +} + +export async function readEASAttestation( + client: PublicClient, + easAddress: Hex, + uid: Hex, +): Promise { + const result = await client.readContract({ + address: easAddress, + abi: ieasAbi, + functionName: 'getAttestation', + args: [uid], + }) + return result as unknown as OnChainAttestation +} + +export function decodeContentHash(data: Hex): Hex { + const [contentHash] = decodeAbiParameters( + [{ name: 'contentHash', type: 'bytes32' }], + data, + ) + return contentHash +} diff --git a/packages/sdk/src/errors.ts b/packages/sdk-ts/src/errors.ts similarity index 100% rename from packages/sdk/src/errors.ts rename to packages/sdk-ts/src/errors.ts diff --git a/packages/sdk-ts/src/index.ts b/packages/sdk-ts/src/index.ts new file mode 100644 index 0000000..a09b138 --- /dev/null +++ b/packages/sdk-ts/src/index.ts @@ -0,0 +1,100 @@ +// MIT License +// +// Copyright (c) 2025 UTS Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// +// Apache License, Version 2.0 +// +// Copyright (c) 2025 UTS Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { toHex } from 'viem' + +export type * from './types.ts' + +export type { BytesLike } from './utils.ts' + +export type { + Attestation, + PendingAttestation, + BitcoinAttestation, + EASAttestation, + EASTimestamped, + EthereumUTSAttestationExtraMetadata, +} from './types.ts' + +export { + DIGEST_OPS, + UpgradeStatus, + AttestationStatusKind, + VerifyStatus, +} from './types.ts' + +export { default as Encoder } from './codec/encode.ts' +export { default as Decoder } from './codec/decode.ts' + +export * from './errors.ts' + +export * from './codec/constants.ts' + +export * from './bmt.ts' + +export { default as BitcoinRPC } from './rpc/btc.ts' + +export { default as SDK, DEFAULT_CALENDARS, WELL_KNOWN_CHAINS } from './sdk.ts' +export type { + EIP1193Provider, + SDKOptions, + StampEvent, + StampEventCallback, +} from './sdk.ts' + +export const hexlify = (obj: any): any => { + if (obj instanceof URL) { + return obj + } + if (obj instanceof Uint8Array) { + return toHex(obj) + } + if (Array.isArray(obj)) { + return obj.map((item) => hexlify(item)) + } + if (typeof obj === 'object' && obj !== null) { + const result: any = {} + for (const key in obj) { + if (Object.hasOwn(obj, key)) { + result[key] = hexlify(obj[key]) + } + } + return result + } + return obj +} diff --git a/packages/sdk/src/rpc/btc.ts b/packages/sdk-ts/src/rpc/btc.ts similarity index 100% rename from packages/sdk/src/rpc/btc.ts rename to packages/sdk-ts/src/rpc/btc.ts diff --git a/packages/sdk/src/sdk.ts b/packages/sdk-ts/src/sdk.ts similarity index 72% rename from packages/sdk/src/sdk.ts rename to packages/sdk-ts/src/sdk.ts index 9cfed58..ca296d4 100644 --- a/packages/sdk/src/sdk.ts +++ b/packages/sdk-ts/src/sdk.ts @@ -1,13 +1,12 @@ import { - type AbstractProvider, - type Eip1193Provider, - BrowserProvider, - getBytes, - hexlify, - id, - Interface, - JsonRpcProvider, -} from 'ethers' + createPublicClient, + custom, + http, + toHex, + type Hex, + type PublicClient, +} from 'viem' +import { getBytes, hexlify } from './utils.ts' import { AttestationStatusKind, UpgradeStatus, @@ -17,7 +16,8 @@ import { type BitcoinAttestation, type DetachedTimestamp, type DigestHeader, - type EthereumUTSAttestation, + type EASAttestation, + type EASTimestamped, type ExecutionStep, type ForkStep, type PendingAttestation, @@ -33,6 +33,12 @@ import Decoder from './codec/decode.ts' import { EncodeError, ErrorCode, RemoteError, VerifyError } from './errors.ts' import { ripemd160, sha1 } from '@noble/hashes/legacy.js' import BitcoinRPC from './rpc/btc.ts' +import { + readEASTimestamp, + readEASAttestation, + decodeContentHash, + NO_EXPIRATION, +} from './eas.ts' export type StampEvent = | { phase: 'generating-nonce' } @@ -50,11 +56,16 @@ export type StampEvent = export type StampEventCallback = (event: StampEvent) => void +/** EIP-1193 compliant Ethereum provider interface (e.g., MetaMask, WalletConnect). */ +export interface EIP1193Provider { + request(args: { method: string; params?: unknown[] }): Promise +} + export interface SDKOptions { calendars?: URL[] btcRPC?: BitcoinRPC - ethRPCs?: Record - web3Provider?: Eip1193Provider | null + ethRPCs?: Record + web3Provider?: EIP1193Provider | null timeout?: number quorum?: number nonceSize?: number @@ -76,24 +87,31 @@ export const WELL_KNOWN_CHAINS: Record< } export const DEFAULT_CALENDARS = [ - // new URL('https://a.pool.opentimestamps.org/'), - // new URL('https://b.pool.opentimestamps.org/'), - // new URL('https://a.pool.eternitywall.com/'), - // new URL('https://ots.btc.catallaxy.com/'), - new URL('http://127.0.0.1:3000/'), + new URL('https://lgm1.calendar.test.timestamps.now/'), + // Run by Peter Todd + new URL('https://a.pool.opentimestamps.org/'), + new URL('https://b.pool.opentimestamps.org/'), + // Run by Riccardo Casatta + new URL('https://a.pool.eternitywall.com/'), + // Run by Bull Bitcoin + new URL('https://ots.btc.catallaxy.com/'), ] -export const UTS_ABI = [ - 'event Attested(bytes32 indexed root, address indexed sender, uint256 timestamp)', - 'function attest(bytes32 root) external', - 'function timestamp(bytes32 root) external view returns (uint256)', -] +export const DEFAULT_EAS_ADDRESSES: Record = { + 1: '0xA1207F3BBa224E2c9c3c6D5aF63D0eb1582Ce587', + 11155111: '0xC47300428b6AD2c7D03BB76D05A176058b47E6B0', + 534352: '0xC47300428b6AD2c7D03BB76D05A176058b47E6B0', + 534351: '0xaEF4103A04090071165F78D45D83A0C0782c2B2a', +} + +export const EAS_SCHEMA_ID: Hex = + '0x5c5b8b295ff43c8e442be11d569e94a4cd5476f5e23df0f71bdd408df6b9649c' export default class SDK { readonly calendars: URL[] btcRPC: BitcoinRPC - ethRPCs: Record - web3Provider: Eip1193Provider | null = null + ethRPCs: Record + web3Provider: EIP1193Provider | null = null /** * Maximum time to wait for calendar responses in milliseconds. @@ -123,17 +141,17 @@ export default class SDK { private static encoder = new TextEncoder() - // 0x61cae4201bb8c0117495b22a70f5202410666b349c27302dac280dc054b60f2a - static readonly utsLogTopic = id('Attested(bytes32,address,uint256)') - static readonly utsInterface = new Interface(UTS_ABI) - constructor(options: SDKOptions = {}) { const { calendars = DEFAULT_CALENDARS, btcRPC = new BitcoinRPC(), ethRPCs = { - 534352: new JsonRpcProvider('https://rpc.scroll.io'), - 534351: new JsonRpcProvider('https://sepolia-rpc.scroll.io'), + 534352: createPublicClient({ + transport: http('https://rpc.scroll.io'), + }), + 534351: createPublicClient({ + transport: http('https://sepolia-rpc.scroll.io'), + }), }, web3Provider = null, timeout = 10000, @@ -180,28 +198,38 @@ export default class SDK { } } - getEthProvider(chainId: number): AbstractProvider | null { + getEthProvider(chainId: number): PublicClient | null { if (Object.hasOwn(this.ethRPCs, chainId)) { return this.ethRPCs[chainId]! } return null } + getEthProviderOrThrow(chainId: number): PublicClient { + const client = this.getEthProvider(chainId) + if (!client) { + throw new Error( + `No RPC provider configured for Ethereum chain ${chainId}`, + ) + } + return client + } + /** * Try to get a provider for the given chain from the web3 wallet. * If the wallet is on a different chain, attempts to switch to the target chain if it's well-known. * Returns null if no web3Provider or if switching fails. */ - async getWeb3ProviderForChain( - chainId: number, - ): Promise { + async getWeb3ProviderForChain(chainId: number): Promise { if (!this.web3Provider) return null try { - const browser = new BrowserProvider(this.web3Provider) - const network = await browser.getNetwork() - if (Number(network.chainId) === chainId) { - return browser + const client = createPublicClient({ + transport: custom(this.web3Provider), + }) + const currentChainId = await client.getChainId() + if (currentChainId === chainId) { + return client } // Try switching to the target chain if it's well-known @@ -212,7 +240,9 @@ export default class SDK { method: 'wallet_switchEthereumChain', params: [{ chainId: knownChain.chainId }], }) - return new BrowserProvider(this.web3Provider) + return createPublicClient({ + transport: custom(this.web3Provider), + }) } catch { // Switch failed, fall through } @@ -543,6 +573,105 @@ export default class SDK { return decoder.readTimestamp() } + /** + * List all pending attestation URLs in the given detached timestamp. + * @param stamp The detached timestamp to inspect. + * @returns An array of pending attestation URLs found in the timestamp. + */ + listPending(stamp: DetachedTimestamp): URL[] { + return SDK.collectPendingAttestations(stamp.timestamp) + } + + private static collectPendingAttestations(timestamp: Timestamp): URL[] { + const pending: URL[] = [] + for (const step of timestamp) { + if (step.op === 'ATTESTATION' && step.attestation.kind === 'pending') { + pending.push(step.attestation.url) + } else if (step.op === 'FORK') { + for (const branch of step.steps) { + pending.push(...SDK.collectPendingAttestations(branch)) + } + } + } + return pending + } + + /** + * Retain only attestations matching the predicate, removing all others from the timestamp tree. + * This is analogous to `Array.filter` but operates on the attestation leaves of the timestamp tree. + * FORK nodes left with a single branch after filtering are collapsed. + * + * @param stamp The detached timestamp to filter. + * @param shouldRetain Predicate that receives each attestation and returns true to keep it. + * @returns true if the timestamp still has attestations, false if all were removed. + */ + retainAttestations( + stamp: DetachedTimestamp, + shouldRetain: (attestation: Attestation) => boolean, + ): boolean { + return SDK.retainAttestationsInTimestamp(stamp.timestamp, shouldRetain) + } + + private static retainAttestationsInTimestamp( + timestamp: Timestamp, + shouldRetain: (attestation: Attestation) => boolean, + ): boolean { + for (let i = timestamp.length - 1; i >= 0; i--) { + const step = timestamp[i] + if (step.op === 'ATTESTATION') { + if (!shouldRetain(step.attestation)) { + timestamp.splice(i, 1) + } + } else if (step.op === 'FORK') { + for (let j = step.steps.length - 1; j >= 0; j--) { + if (!SDK.retainAttestationsInTimestamp(step.steps[j], shouldRetain)) { + step.steps.splice(j, 1) + } + } + if (step.steps.length === 0) { + timestamp.splice(i, 1) + } else if (step.steps.length === 1) { + timestamp.splice(i, 1, ...step.steps[0]) + } + } + } + return timestamp.length > 0 + } + + /** + * Purge pending attestations from the given detached timestamp, modifying it in place. + * This is a convenience wrapper around {@link retainAttestations} that removes + * pending attestations. + * + * @param stamp The detached timestamp to purge pending attestations from. + * @param urlsToPurge Optional set of URL strings to selectively purge. If not provided, all pending attestations are purged. + * @returns An object containing the purged URLs and whether any non-pending attestations remain. + */ + purgePending( + stamp: DetachedTimestamp, + urlsToPurge?: Set, + ): { + purged: URL[] + hasRemaining: boolean + } { + const allPending = this.listPending(stamp) + if (allPending.length === 0) { + return { purged: [], hasRemaining: true } + } + const purged = urlsToPurge + ? allPending.filter((u) => urlsToPurge.has(u.toString())) + : allPending + if (purged.length === 0) { + return { purged: [], hasRemaining: true } + } + const hasRemaining = this.retainAttestations(stamp, (attestation) => { + if (attestation.kind !== 'pending') return true + if (!urlsToPurge) return false + return !urlsToPurge.has(attestation.url.toString()) + }) + return { purged, hasRemaining } + } + /** * Verify the provided detached timestamp by replaying the timestamp steps and validating the attestations. * @@ -616,8 +745,9 @@ export default class SDK { } case 'bitcoin': return this.verifyBitcoinAttestation(input, attestation) - case 'ethereum-uts': - return this.verifyEthereumUTSAttestation(input, attestation) + case 'eas-attestation': + case 'eas-timestamped': + return this.verifyEAS(input, attestation) case 'unknown': return { attestation, @@ -672,89 +802,108 @@ export default class SDK { } } - async verifyEthereumUTSAttestation( + async verifyEAS( input: Uint8Array, - attestation: EthereumUTSAttestation, + attestation: EASAttestation | EASTimestamped, ): Promise { - // Try web3Provider first (works in browser without CORS issues) - let provider: AbstractProvider | null = await this.getWeb3ProviderForChain( - attestation.chain, - ) - - // Fallback to ethRPCs - if (!provider) { - if (!Object.hasOwn(this.ethRPCs, attestation.chain)) { - return { - attestation, - status: AttestationStatusKind.UNKNOWN, - error: new VerifyError( - ErrorCode.UNSUPPORTED_ATTESTATION, - `No RPC provider configured for Ethereum chain ${attestation.chain}`, - ), - } - } - provider = this.ethRPCs[attestation.chain]! + const client = this.getEthProviderOrThrow(attestation.chain) + const easAddress = DEFAULT_EAS_ADDRESSES[attestation.chain] + if (!easAddress) { + throw new Error( + `No EAS address configured for Ethereum chain ${attestation.chain}`, + ) } - try { - const logs = await provider.getLogs({ - fromBlock: attestation.height, - toBlock: attestation.height, - topics: [ - SDK.utsLogTopic, // Topic 0: Attested(bytes32,address,uint256) - hexlify(input), // Topic 1: digest - ], - }) - - if (logs.length === 0) { + if (attestation.kind === 'eas-timestamped') { + const time = await readEASTimestamp( + client, + easAddress, + toHex(input, { size: 32 }), + ) + if (time === 0n) { return { attestation, status: AttestationStatusKind.INVALID, error: new VerifyError( ErrorCode.ATTESTATION_MISMATCH, - `No attestation log found for block ${attestation.height} on chain ${attestation.chain}`, + `No EAS timestamp found for the given input on chain ${attestation.chain}`, ), } } + return { + attestation, + status: AttestationStatusKind.VALID, + additionalInfo: { time }, + } + } - const log = SDK.utsInterface.parseLog(logs[0])! - const root = log.args[0] // root - const sender = log.args[1] // sender - const timestamp = log.args[2] // timestamp + const onChainAttestation = await readEASAttestation( + client, + easAddress, + hexlify(attestation.uid), + ) - // sanity check to ensure the root matches - const rootBytes = getBytes(root) - if ( - rootBytes.length !== input.length || - !rootBytes.every((byte: number, i: number) => byte === input[i]) - ) { - return { - attestation, - status: AttestationStatusKind.INVALID, - error: new VerifyError( - ErrorCode.ATTESTATION_MISMATCH, - `Attestation log found but root does not match expected digest for block ${attestation.height} on chain ${attestation.chain}`, - ), - } + if (onChainAttestation.schema !== EAS_SCHEMA_ID) { + return { + attestation, + status: AttestationStatusKind.INVALID, + error: new VerifyError( + ErrorCode.ATTESTATION_MISMATCH, + `EAS attestation schema mismatch for UID ${hexlify(attestation.uid)} on chain ${attestation.chain}`, + ), } + } + if (onChainAttestation.expirationTime !== NO_EXPIRATION) { return { attestation, - status: AttestationStatusKind.VALID, - additionalInfo: { root, sender, timestamp }, + status: AttestationStatusKind.INVALID, + error: new VerifyError( + ErrorCode.ATTESTATION_MISMATCH, + `EAS attestation for UID ${hexlify(attestation.uid)} on chain ${attestation.chain} has expirationTime`, + ), } + } + + if (onChainAttestation.revocable) { + return { + attestation, + status: AttestationStatusKind.INVALID, + error: new VerifyError( + ErrorCode.ATTESTATION_MISMATCH, + `EAS attestation for UID ${hexlify(attestation.uid)} on chain ${attestation.chain} is revocable`, + ), + } + } + + try { + const contentHash = decodeContentHash(onChainAttestation.data) + console.debug( + `Decoded EAS attestation data for UID ${hexlify(attestation.uid)}:`, + contentHash, + ) } catch (error) { - console.error(`Error verifying Ethereum attestation: ${error}`) + console.debug( + `Failed to decode EAS attestation data for UID ${hexlify(attestation.uid)}:`, + error, + ) return { attestation, - status: AttestationStatusKind.UNKNOWN, + status: AttestationStatusKind.INVALID, error: new VerifyError( - ErrorCode.REMOTE_ERROR, - `Failed to verify Ethereum attestation for block ${attestation.height} on chain ${attestation.chain}`, - { context: { source: error } }, + ErrorCode.ATTESTATION_MISMATCH, + `EAS attestation for UID ${hexlify(attestation.uid)} on chain ${attestation.chain} has invalid data format`, ), } } + + // TODO: verify the attestation is actually valid for the input + + return { + attestation, + status: AttestationStatusKind.VALID, + additionalInfo: { ...onChainAttestation }, + } } /** @@ -765,6 +914,7 @@ export default class SDK { * - If there are also INVALID or UNKNOWN attestations, the overall status is PARTIAL_VALID * - Otherwise, the overall status is VALID * - If there are no VALID attestations, but at least one PENDING attestation, the overall status is PENDING + * - If there are no VALID attestations, but at least one UNKNOWN attestation, the overall status is UNKNOWN * - If there are no VALID or PENDING attestations, the overall status is INVALID * @param attestations */ @@ -792,6 +942,8 @@ export default class SDK { } } else if (counts[AttestationStatusKind.PENDING] > 0) { status = VerifyStatus.PENDING + } else if (counts[AttestationStatusKind.UNKNOWN] > 0) { + status = VerifyStatus.UNKNOWN } return status } diff --git a/packages/sdk/src/types.ts b/packages/sdk-ts/src/types.ts similarity index 87% rename from packages/sdk/src/types.ts rename to packages/sdk-ts/src/types.ts index 30a9a6f..37c62ac 100644 --- a/packages/sdk/src/types.ts +++ b/packages/sdk-ts/src/types.ts @@ -1,4 +1,4 @@ -import { type BytesLike } from 'ethers/utils' +import type { BytesLike } from './utils.ts' export const DIGEST_OPS = ['SHA1', 'SHA256', 'RIPEMD160', 'KECCAK256'] as const @@ -37,15 +37,23 @@ export interface UnaryExecutionStep extends BaseExecutionStep { op: DigestOp | 'REVERSE' | 'HEXLIFY' } -export type AttestationKind = 'pending' | 'bitcoin' | 'ethereum-uts' | 'unknown' +export type AttestationKind = + | 'pending' + | 'bitcoin' + | 'eas-attestation' + | 'eas-timestamped' + | 'unknown' export type PendingAttestation = { kind: 'pending'; url: URL } export type BitcoinAttestation = { kind: 'bitcoin'; height: number } -export type EthereumUTSAttestation = { - kind: 'ethereum-uts' +export type EASAttestation = { + kind: 'eas-attestation' + chain: number + uid: BytesLike +} +export type EASTimestamped = { + kind: 'eas-timestamped' chain: number - height: number - metadata?: EthereumUTSAttestationExtraMetadata } export type UnknownAttestation = { kind: 'unknown' @@ -56,7 +64,8 @@ export type UnknownAttestation = { export type Attestation = | PendingAttestation | BitcoinAttestation - | EthereumUTSAttestation + | EASAttestation + | EASTimestamped | UnknownAttestation export type AttestationStep = { op: 'ATTESTATION'; attestation: Attestation } @@ -117,4 +126,5 @@ export enum VerifyStatus { PARTIAL_VALID = 'PARTIAL_VALID', INVALID = 'INVALID', PENDING = 'PENDING', + UNKNOWN = 'UNKNOWN', } diff --git a/packages/sdk-ts/src/types/v8.d.ts b/packages/sdk-ts/src/types/v8.d.ts new file mode 100644 index 0000000..9eea723 --- /dev/null +++ b/packages/sdk-ts/src/types/v8.d.ts @@ -0,0 +1,3 @@ +interface ErrorConstructor { + captureStackTrace?(object: object, constructor?: () => void): void +} diff --git a/packages/sdk-ts/src/utils.ts b/packages/sdk-ts/src/utils.ts new file mode 100644 index 0000000..ec0b877 --- /dev/null +++ b/packages/sdk-ts/src/utils.ts @@ -0,0 +1,27 @@ +import { toHex, hexToBytes, type Hex } from 'viem' + +export type BytesLike = Hex | Uint8Array + +/** + * Convert a BytesLike value to a Uint8Array. + * If the input is already a Uint8Array, it is returned as-is. + * If the input is a hex string, it is decoded. + */ +export function getBytes(value: BytesLike): Uint8Array { + if (value instanceof Uint8Array) return value + try { + return hexToBytes(value) + } catch (error) { + throw new Error(`Invalid hex string: ${value}`, { cause: error }) + } +} + +/** + * Convert a BytesLike value to a hex string. + * If the input is already a hex string, it is returned as-is. + * If the input is a Uint8Array, it is encoded. + */ +export function hexlify(value: BytesLike): Hex { + if (typeof value === 'string') return value as Hex + return toHex(value) +} diff --git a/packages/sdk/test/btc.test.ts b/packages/sdk-ts/test/btc.test.ts similarity index 100% rename from packages/sdk/test/btc.test.ts rename to packages/sdk-ts/test/btc.test.ts diff --git a/packages/sdk/test/codec.test.ts b/packages/sdk-ts/test/codec.test.ts similarity index 100% rename from packages/sdk/test/codec.test.ts rename to packages/sdk-ts/test/codec.test.ts diff --git a/packages/sdk/test/merkle.test.ts b/packages/sdk-ts/test/merkle.test.ts similarity index 100% rename from packages/sdk/test/merkle.test.ts rename to packages/sdk-ts/test/merkle.test.ts diff --git a/packages/sdk/test/sdk.test.ts b/packages/sdk-ts/test/sdk.test.ts similarity index 93% rename from packages/sdk/test/sdk.test.ts rename to packages/sdk-ts/test/sdk.test.ts index a6dc36b..4fd1a9b 100644 --- a/packages/sdk/test/sdk.test.ts +++ b/packages/sdk-ts/test/sdk.test.ts @@ -4,7 +4,7 @@ import path from 'node:path' import Decoder from '../src/codec/decode.ts' import type { DetachedTimestamp } from '../src/types.ts' import SDK from '../src/sdk.ts' -import { getBytes } from 'ethers' +import { hexToBytes } from 'viem' const __filename = import.meta.filename const __dirname = import.meta.dirname @@ -26,7 +26,9 @@ describe.skip('Stamp', () => { expect(results).toHaveLength(1) const result = results[0] - expect(result.header.digest).toEqual(getBytes(testDigest)) + expect(result.header.digest).toEqual( + hexToBytes(testDigest as `0x${string}`), + ) expect(result.timestamp).toBeDefined() console.debug('Timestamp:', JSON.stringify(result, null, 2)) }) @@ -48,7 +50,7 @@ describe('Verify', () => { const sdk = new SDK() const verified = await sdk.verifyAttestation( - getBytes( + hexToBytes( '0x7eb06fdbe20e402a8125775968899b4ab87b9af1c20a81d4af8d5bb0c96d7c64', ), { diff --git a/packages/sdk/test/tsconfig.json b/packages/sdk-ts/test/tsconfig.json similarity index 100% rename from packages/sdk/test/tsconfig.json rename to packages/sdk-ts/test/tsconfig.json diff --git a/packages/sdk-ts/tsconfig.json b/packages/sdk-ts/tsconfig.json new file mode 100644 index 0000000..082005d --- /dev/null +++ b/packages/sdk-ts/tsconfig.json @@ -0,0 +1,13 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "./dist", + "rootDir": "./src", + "tsBuildInfoFile": "./dist/.tsbuildinfo", + "emitDeclarationOnly": true, + "types": [] + }, + "include": ["src/**/*.ts", "src/**/*.d.ts"], + "exclude": ["node_modules", "dist"], + "references": [{ "path": "../contract-types" }] +} diff --git a/packages/sdk/vitest.config.ts b/packages/sdk-ts/vitest.config.ts similarity index 54% rename from packages/sdk/vitest.config.ts rename to packages/sdk-ts/vitest.config.ts index 04b71ae..e1f8125 100644 --- a/packages/sdk/vitest.config.ts +++ b/packages/sdk-ts/vitest.config.ts @@ -1,6 +1,15 @@ import { defineConfig } from 'vitest/config' +import path from 'node:path' export default defineConfig({ + resolve: { + alias: { + '@uts/contracts': path.resolve( + __dirname, + '../contract-types/src/index.ts', + ), + }, + }, test: { environment: 'node', diff --git a/packages/sdk/fixtures/test.ots b/packages/sdk/fixtures/test.ots deleted file mode 100644 index 0e706a0..0000000 Binary files a/packages/sdk/fixtures/test.ots and /dev/null differ diff --git a/packages/sdk/src/index.ts b/packages/sdk/src/index.ts deleted file mode 100644 index c909047..0000000 --- a/packages/sdk/src/index.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { hexlify as h } from 'ethers' - -export type * from './types.ts' - -export type { - Attestation, - PendingAttestation, - BitcoinAttestation, - EthereumUTSAttestation, - EthereumUTSAttestationExtraMetadata, -} from './types.ts' - -export { - DIGEST_OPS, - UpgradeStatus, - AttestationStatusKind, - VerifyStatus, -} from './types.ts' - -export { default as Encoder } from './codec/encode.ts' -export { default as Decoder } from './codec/decode.ts' - -export * from './errors.ts' - -export * from './codec/constants.ts' - -export * from './bmt.ts' - -export { default as BitcoinRPC } from './rpc/btc.ts' - -export { - default as SDK, - DEFAULT_CALENDARS, - UTS_ABI, - WELL_KNOWN_CHAINS, -} from './sdk.ts' -export type { SDKOptions, StampEvent, StampEventCallback } from './sdk.ts' - -export const hexlify = (obj: any): any => { - if (obj instanceof URL) { - return obj - } - if (obj instanceof Uint8Array) { - return h(obj) - } - if (Array.isArray(obj)) { - return obj.map((item) => hexlify(item)) - } - if (typeof obj === 'object' && obj !== null) { - const result: any = {} - for (const key in obj) { - if (Object.hasOwn(obj, key)) { - result[key] = hexlify(obj[key]) - } - } - return result - } - return obj -} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4c06003..b90db73 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -32,6 +32,33 @@ importers: typescript-eslint: specifier: ^8.56.1 version: 8.56.1(eslint@9.39.3(jiti@2.6.1))(typescript@5.9.3) + wrangler: + specifier: ^4.73.0 + version: 4.73.0 + + apps/docs: + devDependencies: + '@lunariajs/core': + specifier: ^0.1.1 + version: 0.1.1 + markdown-it-mathjax3: + specifier: ^4.3.2 + version: 4.3.2 + open-cli: + specifier: ^8.0.0 + version: 8.0.0 + postcss-rtlcss: + specifier: ^5.7.1 + version: 5.7.1(postcss@8.5.8) + vitepress: + specifier: 2.0.0-alpha.16 + version: 2.0.0-alpha.16(@types/node@24.10.4)(change-case@5.4.4)(fuse.js@7.1.0)(jiti@2.6.1)(lightningcss@1.31.1)(markdown-it-mathjax3@4.3.2)(postcss@8.5.8)(terser@5.46.0)(typescript@5.9.3)(yaml@2.8.2) + vitepress-plugin-group-icons: + specifier: ^1.7.1 + version: 1.7.1(vite@7.3.1(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(yaml@2.8.2)) + vitepress-plugin-llms: + specifier: ^1.11.0 + version: 1.11.0 apps/web: dependencies: @@ -46,16 +73,13 @@ importers: version: 2.0.1 '@uts/sdk': specifier: workspace:* - version: link:../../packages/sdk + version: link:../../packages/sdk-ts '@vueuse/core': specifier: ^14.2.1 version: 14.2.1(vue@3.5.26(typescript@5.9.3)) date-fns: specifier: ^4.1.0 version: 4.1.0 - ethers: - specifier: ^6.16.0 - version: 6.16.0 jszip: specifier: ^3.10.1 version: 3.10.1 @@ -65,6 +89,9 @@ importers: pinia: specifier: ^3.0.4 version: 3.0.4(typescript@5.9.3)(vue@3.5.26(typescript@5.9.3)) + viem: + specifier: ^2.47.0 + version: 2.47.0(typescript@5.9.3)(zod@4.3.6) vue: specifier: ^3.5.24 version: 3.5.26(typescript@5.9.3) @@ -77,10 +104,10 @@ importers: version: 5.9.2(typescript@5.9.3) '@lingui/vite-plugin': specifier: ^5.9.2 - version: 5.9.2(typescript@5.9.3)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.31.1)) + version: 5.9.2(typescript@5.9.3)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(yaml@2.8.2)) '@tailwindcss/vite': specifier: ^4.2.1 - version: 4.2.1(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.31.1)) + version: 4.2.1(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(yaml@2.8.2)) '@types/jszip': specifier: ^3.4.1 version: 3.4.1 @@ -89,7 +116,7 @@ importers: version: 24.10.4 '@vitejs/plugin-vue': specifier: ^6.0.1 - version: 6.0.3(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.31.1))(vue@3.5.26(typescript@5.9.3)) + version: 6.0.3(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(yaml@2.8.2))(vue@3.5.26(typescript@5.9.3)) '@vue/tsconfig': specifier: ^0.8.1 version: 0.8.1(typescript@5.9.3)(vue@3.5.26(typescript@5.9.3)) @@ -101,26 +128,50 @@ importers: version: 5.9.3 vite: specifier: ^7.2.4 - version: 7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.31.1) + version: 7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(yaml@2.8.2) vue-tsc: specifier: ^3.1.4 version: 3.2.1(typescript@5.9.3) - packages/sdk: + packages/contract-types: + devDependencies: + '@rollup/plugin-node-resolve': + specifier: ^16.0.3 + version: 16.0.3(rollup@4.59.0) + '@rollup/plugin-typescript': + specifier: ^12.3.0 + version: 12.3.0(rollup@4.59.0)(tslib@2.8.1)(typescript@5.9.3) + '@wagmi/cli': + specifier: ^2.10.0 + version: 2.10.0(typescript@5.9.3) + rimraf: + specifier: ^6.1.3 + version: 6.1.3 + rollup: + specifier: ^4.59.0 + version: 4.59.0 + typescript: + specifier: ^5.9.3 + version: 5.9.3 + + packages/sdk-ts: dependencies: '@noble/hashes': specifier: ^2.0.1 version: 2.0.1 - ethers: - specifier: ^6.16.0 - version: 6.16.0 + '@uts/contracts': + specifier: workspace:* + version: link:../contract-types + viem: + specifier: ^2.47.0 + version: 2.47.0(typescript@5.9.3)(zod@4.3.6) devDependencies: '@rollup/plugin-node-resolve': specifier: ^16.0.3 version: 16.0.3(rollup@4.59.0) '@rollup/plugin-typescript': specifier: ^12.3.0 - version: 12.3.0(rollup@4.59.0)(tslib@2.7.0)(typescript@5.9.3) + version: 12.3.0(rollup@4.59.0)(tslib@2.8.1)(typescript@5.9.3) rimraf: specifier: ^6.1.3 version: 6.1.3 @@ -132,12 +183,15 @@ importers: version: 5.9.3 vitest: specifier: ^4.0.18 - version: 4.0.18(@types/node@24.10.4)(@vitest/browser-playwright@4.0.18)(jiti@2.6.1)(lightningcss@1.31.1) + version: 4.0.18(@types/node@24.10.4)(@vitest/browser-playwright@4.0.18)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(yaml@2.8.2) packages: - '@adraffy/ens-normalize@1.10.1': - resolution: {integrity: sha512-96Z2IP3mYmF1Xg2cDm8f1gWGf/HUVedQ3FMifV4kG/PQ4yEP51xDtRAEfhVNt5f/uzpNkZHwWQuUcu6D6K+Ekw==} + '@adraffy/ens-normalize@1.11.1': + resolution: {integrity: sha512-nhCBV3quEgesuf7c7KYfperqSS14T8bYuvJ8PcLJp6znkZpFc0AuW4qBtr8eKVyPPe/8RSr7sglCWPU5eaxwKQ==} + + '@antfu/install-pkg@1.1.0': + resolution: {integrity: sha512-MGQsmw10ZyI+EJo45CdSER4zEb+p31LpDAFp2Z3gkSd1yqVZGi0Ebx++YTEMonJy4oChEMLsxZ64j8FH6sSqtQ==} '@babel/code-frame@7.29.0': resolution: {integrity: sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==} @@ -219,6 +273,65 @@ packages: resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==} engines: {node: '>=6.9.0'} + '@clack/core@0.3.5': + resolution: {integrity: sha512-5cfhQNH+1VQ2xLQlmzXMqUoiaH0lRBq9/CLW9lTyMbuKLC3+xEK01tHVvyut++mLOn5urSHmkm6I0Lg9MaJSTQ==} + + '@cloudflare/kv-asset-handler@0.4.2': + resolution: {integrity: sha512-SIOD2DxrRRwQ+jgzlXCqoEFiKOFqaPjhnNTGKXSRLvp1HiOvapLaFG2kEr9dYQTYe8rKrd9uvDUzmAITeNyaHQ==} + engines: {node: '>=18.0.0'} + + '@cloudflare/unenv-preset@2.15.0': + resolution: {integrity: sha512-EGYmJaGZKWl+X8tXxcnx4v2bOZSjQeNI5dWFeXivgX9+YCT69AkzHHwlNbVpqtEUTbew8eQurpyOpeN8fg00nw==} + peerDependencies: + unenv: 2.0.0-rc.24 + workerd: 1.20260301.1 || ~1.20260302.1 || ~1.20260303.1 || ~1.20260304.1 || >1.20260305.0 <2.0.0-0 + peerDependenciesMeta: + workerd: + optional: true + + '@cloudflare/workerd-darwin-64@1.20260312.1': + resolution: {integrity: sha512-HUAtDWaqUduS6yasV6+NgsK7qBpP1qGU49ow/Wb117IHjYp+PZPUGReDYocpB4GOMRoQlvdd4L487iFxzdARpw==} + engines: {node: '>=16'} + cpu: [x64] + os: [darwin] + + '@cloudflare/workerd-darwin-arm64@1.20260312.1': + resolution: {integrity: sha512-DOn7TPTHSxJYfi4m4NYga/j32wOTqvJf/pY4Txz5SDKWIZHSTXFyGz2K4B+thoPWLop/KZxGoyTv7db0mk/qyw==} + engines: {node: '>=16'} + cpu: [arm64] + os: [darwin] + + '@cloudflare/workerd-linux-64@1.20260312.1': + resolution: {integrity: sha512-TdkIh3WzPXYHuvz7phAtFEEvAxvFd30tHrm4gsgpw0R0F5b8PtoM3hfL2uY7EcBBWVYUBtkY2ahDYFfufnXw/g==} + engines: {node: '>=16'} + cpu: [x64] + os: [linux] + + '@cloudflare/workerd-linux-arm64@1.20260312.1': + resolution: {integrity: sha512-kNauZhL569Iy94t844OMwa1zP6zKFiL3xiJ4tGLS+TFTEfZ3pZsRH6lWWOtkXkjTyCmBEOog0HSEKjIV4oAffw==} + engines: {node: '>=16'} + cpu: [arm64] + os: [linux] + + '@cloudflare/workerd-windows-64@1.20260312.1': + resolution: {integrity: sha512-5dBrlSK+nMsZy5bYQpj8t9iiQNvCRlkm9GGvswJa9vVU/1BNO4BhJMlqOLWT24EmFyApZ+kaBiPJMV8847NDTg==} + engines: {node: '>=16'} + cpu: [x64] + os: [win32] + + '@cspotcode/source-map-support@0.8.1': + resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} + engines: {node: '>=12'} + + '@docsearch/css@4.6.0': + resolution: {integrity: sha512-YlcAimkXclvqta47g47efzCM5CFxDwv2ClkDfEs/fC/Ak0OxPH2b3czwa4o8O1TRBf+ujFF2RiUwszz2fPVNJQ==} + + '@docsearch/js@4.6.0': + resolution: {integrity: sha512-9/rbgkm/BgTq46cwxIohvSAz3koOFjnPpg0mwkJItAfzKbQIj+310PvwtgUY1YITDuGCag6yOL50GW2DBkaaBw==} + + '@docsearch/sidepanel-js@4.6.0': + resolution: {integrity: sha512-lFT5KLwlzUmpoGArCScNoK41l9a22JYsEPwBzMrz+/ILVR5Ax87UphCuiyDFQWEvEmbwzn/kJx5W/O5BUlN1Rw==} + '@emnapi/core@1.8.1': resolution: {integrity: sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg==} @@ -240,6 +353,12 @@ packages: cpu: [ppc64] os: [aix] + '@esbuild/aix-ppc64@0.27.3': + resolution: {integrity: sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + '@esbuild/android-arm64@0.25.12': resolution: {integrity: sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==} engines: {node: '>=18'} @@ -252,6 +371,12 @@ packages: cpu: [arm64] os: [android] + '@esbuild/android-arm64@0.27.3': + resolution: {integrity: sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + '@esbuild/android-arm@0.25.12': resolution: {integrity: sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==} engines: {node: '>=18'} @@ -264,6 +389,12 @@ packages: cpu: [arm] os: [android] + '@esbuild/android-arm@0.27.3': + resolution: {integrity: sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + '@esbuild/android-x64@0.25.12': resolution: {integrity: sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==} engines: {node: '>=18'} @@ -276,6 +407,12 @@ packages: cpu: [x64] os: [android] + '@esbuild/android-x64@0.27.3': + resolution: {integrity: sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + '@esbuild/darwin-arm64@0.25.12': resolution: {integrity: sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==} engines: {node: '>=18'} @@ -288,6 +425,12 @@ packages: cpu: [arm64] os: [darwin] + '@esbuild/darwin-arm64@0.27.3': + resolution: {integrity: sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + '@esbuild/darwin-x64@0.25.12': resolution: {integrity: sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==} engines: {node: '>=18'} @@ -300,6 +443,12 @@ packages: cpu: [x64] os: [darwin] + '@esbuild/darwin-x64@0.27.3': + resolution: {integrity: sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + '@esbuild/freebsd-arm64@0.25.12': resolution: {integrity: sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==} engines: {node: '>=18'} @@ -312,6 +461,12 @@ packages: cpu: [arm64] os: [freebsd] + '@esbuild/freebsd-arm64@0.27.3': + resolution: {integrity: sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + '@esbuild/freebsd-x64@0.25.12': resolution: {integrity: sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==} engines: {node: '>=18'} @@ -324,6 +479,12 @@ packages: cpu: [x64] os: [freebsd] + '@esbuild/freebsd-x64@0.27.3': + resolution: {integrity: sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + '@esbuild/linux-arm64@0.25.12': resolution: {integrity: sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==} engines: {node: '>=18'} @@ -336,6 +497,12 @@ packages: cpu: [arm64] os: [linux] + '@esbuild/linux-arm64@0.27.3': + resolution: {integrity: sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + '@esbuild/linux-arm@0.25.12': resolution: {integrity: sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==} engines: {node: '>=18'} @@ -348,6 +515,12 @@ packages: cpu: [arm] os: [linux] + '@esbuild/linux-arm@0.27.3': + resolution: {integrity: sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + '@esbuild/linux-ia32@0.25.12': resolution: {integrity: sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==} engines: {node: '>=18'} @@ -360,6 +533,12 @@ packages: cpu: [ia32] os: [linux] + '@esbuild/linux-ia32@0.27.3': + resolution: {integrity: sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + '@esbuild/linux-loong64@0.25.12': resolution: {integrity: sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==} engines: {node: '>=18'} @@ -372,6 +551,12 @@ packages: cpu: [loong64] os: [linux] + '@esbuild/linux-loong64@0.27.3': + resolution: {integrity: sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + '@esbuild/linux-mips64el@0.25.12': resolution: {integrity: sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==} engines: {node: '>=18'} @@ -384,6 +569,12 @@ packages: cpu: [mips64el] os: [linux] + '@esbuild/linux-mips64el@0.27.3': + resolution: {integrity: sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + '@esbuild/linux-ppc64@0.25.12': resolution: {integrity: sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==} engines: {node: '>=18'} @@ -396,6 +587,12 @@ packages: cpu: [ppc64] os: [linux] + '@esbuild/linux-ppc64@0.27.3': + resolution: {integrity: sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + '@esbuild/linux-riscv64@0.25.12': resolution: {integrity: sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==} engines: {node: '>=18'} @@ -408,6 +605,12 @@ packages: cpu: [riscv64] os: [linux] + '@esbuild/linux-riscv64@0.27.3': + resolution: {integrity: sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + '@esbuild/linux-s390x@0.25.12': resolution: {integrity: sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==} engines: {node: '>=18'} @@ -420,6 +623,12 @@ packages: cpu: [s390x] os: [linux] + '@esbuild/linux-s390x@0.27.3': + resolution: {integrity: sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + '@esbuild/linux-x64@0.25.12': resolution: {integrity: sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==} engines: {node: '>=18'} @@ -432,6 +641,12 @@ packages: cpu: [x64] os: [linux] + '@esbuild/linux-x64@0.27.3': + resolution: {integrity: sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + '@esbuild/netbsd-arm64@0.25.12': resolution: {integrity: sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==} engines: {node: '>=18'} @@ -444,6 +659,12 @@ packages: cpu: [arm64] os: [netbsd] + '@esbuild/netbsd-arm64@0.27.3': + resolution: {integrity: sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + '@esbuild/netbsd-x64@0.25.12': resolution: {integrity: sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==} engines: {node: '>=18'} @@ -456,6 +677,12 @@ packages: cpu: [x64] os: [netbsd] + '@esbuild/netbsd-x64@0.27.3': + resolution: {integrity: sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + '@esbuild/openbsd-arm64@0.25.12': resolution: {integrity: sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==} engines: {node: '>=18'} @@ -468,6 +695,12 @@ packages: cpu: [arm64] os: [openbsd] + '@esbuild/openbsd-arm64@0.27.3': + resolution: {integrity: sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + '@esbuild/openbsd-x64@0.25.12': resolution: {integrity: sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==} engines: {node: '>=18'} @@ -480,6 +713,12 @@ packages: cpu: [x64] os: [openbsd] + '@esbuild/openbsd-x64@0.27.3': + resolution: {integrity: sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + '@esbuild/openharmony-arm64@0.25.12': resolution: {integrity: sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==} engines: {node: '>=18'} @@ -492,6 +731,12 @@ packages: cpu: [arm64] os: [openharmony] + '@esbuild/openharmony-arm64@0.27.3': + resolution: {integrity: sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + '@esbuild/sunos-x64@0.25.12': resolution: {integrity: sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==} engines: {node: '>=18'} @@ -504,6 +749,12 @@ packages: cpu: [x64] os: [sunos] + '@esbuild/sunos-x64@0.27.3': + resolution: {integrity: sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + '@esbuild/win32-arm64@0.25.12': resolution: {integrity: sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==} engines: {node: '>=18'} @@ -516,6 +767,12 @@ packages: cpu: [arm64] os: [win32] + '@esbuild/win32-arm64@0.27.3': + resolution: {integrity: sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + '@esbuild/win32-ia32@0.25.12': resolution: {integrity: sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==} engines: {node: '>=18'} @@ -528,6 +785,12 @@ packages: cpu: [ia32] os: [win32] + '@esbuild/win32-ia32@0.27.3': + resolution: {integrity: sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + '@esbuild/win32-x64@0.25.12': resolution: {integrity: sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==} engines: {node: '>=18'} @@ -540,6 +803,12 @@ packages: cpu: [x64] os: [win32] + '@esbuild/win32-x64@0.27.3': + resolution: {integrity: sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + '@eslint-community/eslint-utils@4.9.1': resolution: {integrity: sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -594,6 +863,174 @@ packages: resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} engines: {node: '>=18.18'} + '@iconify-json/logos@1.2.10': + resolution: {integrity: sha512-qxaXKJ6fu8jzTMPQdHtNxlfx6tBQ0jXRbHZIYy5Ilh8Lx9US9FsAdzZWUR8MXV8PnWTKGDFO4ZZee9VwerCyMA==} + + '@iconify-json/simple-icons@1.2.73': + resolution: {integrity: sha512-nQZTwul4c2zBqH/aLP4zMOiElj93T6HawbrP+sFQKpxmBdS5x1duCK3cAnkj6dntHz84EYkzaQRM83V2pj4qxA==} + + '@iconify-json/vscode-icons@1.2.45': + resolution: {integrity: sha512-ow+ueibMIq79ueM1kv6cOWgHx8jfh1XJQi2RrqMHb4HLbvIBlxpy5PCMvOJXlA68R6fBAHpWQeh6uWx7VKEVsA==} + + '@iconify/types@2.0.0': + resolution: {integrity: sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==} + + '@iconify/utils@3.1.0': + resolution: {integrity: sha512-Zlzem1ZXhI1iHeeERabLNzBHdOa4VhQbqAcOQaMKuTuyZCpwKbC2R4Dd0Zo3g9EAc+Y4fiarO8HIHRAth7+skw==} + + '@img/colour@1.1.0': + resolution: {integrity: sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==} + engines: {node: '>=18'} + + '@img/sharp-darwin-arm64@0.34.5': + resolution: {integrity: sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [darwin] + + '@img/sharp-darwin-x64@0.34.5': + resolution: {integrity: sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [darwin] + + '@img/sharp-libvips-darwin-arm64@1.2.4': + resolution: {integrity: sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==} + cpu: [arm64] + os: [darwin] + + '@img/sharp-libvips-darwin-x64@1.2.4': + resolution: {integrity: sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==} + cpu: [x64] + os: [darwin] + + '@img/sharp-libvips-linux-arm64@1.2.4': + resolution: {integrity: sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-arm@1.2.4': + resolution: {integrity: sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-ppc64@1.2.4': + resolution: {integrity: sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-riscv64@1.2.4': + resolution: {integrity: sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-s390x@1.2.4': + resolution: {integrity: sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-x64@1.2.4': + resolution: {integrity: sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linuxmusl-arm64@1.2.4': + resolution: {integrity: sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@img/sharp-libvips-linuxmusl-x64@1.2.4': + resolution: {integrity: sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@img/sharp-linux-arm64@0.34.5': + resolution: {integrity: sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-arm@0.34.5': + resolution: {integrity: sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-ppc64@0.34.5': + resolution: {integrity: sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-riscv64@0.34.5': + resolution: {integrity: sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-s390x@0.34.5': + resolution: {integrity: sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-x64@0.34.5': + resolution: {integrity: sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@img/sharp-linuxmusl-arm64@0.34.5': + resolution: {integrity: sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@img/sharp-linuxmusl-x64@0.34.5': + resolution: {integrity: sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [linux] + libc: [musl] + + '@img/sharp-wasm32@0.34.5': + resolution: {integrity: sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [wasm32] + + '@img/sharp-win32-arm64@0.34.5': + resolution: {integrity: sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [win32] + + '@img/sharp-win32-ia32@0.34.5': + resolution: {integrity: sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [ia32] + os: [win32] + + '@img/sharp-win32-x64@0.34.5': + resolution: {integrity: sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [win32] + '@isaacs/cliui@9.0.0': resolution: {integrity: sha512-AokJm4tuBHillT+FpMtxQ60n8ObyXBatq7jD2/JA9dxbDDokKQm8KMht5ibGzLVU9IJDIKK4TPKgMHEYMn3lMg==} engines: {node: '>=18'} @@ -616,12 +1053,24 @@ packages: resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} engines: {node: '>=6.0.0'} + '@jridgewell/source-map@0.3.11': + resolution: {integrity: sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==} + '@jridgewell/sourcemap-codec@1.5.5': resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} '@jridgewell/trace-mapping@0.3.31': resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + '@jridgewell/trace-mapping@0.3.9': + resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} + + '@kwsites/file-exists@1.1.1': + resolution: {integrity: sha512-m9/5YGR18lIwxSFDwfE3oA7bWuq9kdau6ugN4H2rJeyhFQZcG9AgSHkQtSD15a8WvTgfz9aikZMrKPHvbpqFiw==} + + '@kwsites/promise-deferred@1.1.1': + resolution: {integrity: sha512-GaHYm+c0O9MjZRu0ongGBRbinu8gVAMd2UZjji6jVmqKtZluZnptXGWhz1E8j8D2HJ3f/yMxKAUC0b+57wncIw==} + '@lingui/babel-plugin-extract-messages@5.9.2': resolution: {integrity: sha512-x9HxUUZgnzF5nTsRwCyJxL/csll/psQCeiTiUPMqE1qC9Mq3ZFPygHDUgijIwOCSDnILHM581Fjar/Njvpnnow==} engines: {node: '>=20.0.0'} @@ -670,26 +1119,57 @@ packages: peerDependencies: vite: ^3 || ^4 || ^5.0.9 || ^6 || ^7 + '@lunariajs/core@0.1.1': + resolution: {integrity: sha512-sAqM9+DVsLe3xHM9wu2pEnKGYMs/bWS9qpR+CGHol3RihOELnOQTzHddXbdB1MtgesbI8dnQuG64Ocd8KkWsng==} + engines: {node: '>=18.17.0'} + hasBin: true + '@messageformat/parser@5.1.1': resolution: {integrity: sha512-3p0YRGCcTUCYvBKLIxtDDyrJ0YijGIwrTRu1DT8gIviIDZru8H23+FkY6MJBzM1n9n20CiM4VeDYuBsrrwnLjg==} '@napi-rs/wasm-runtime@0.2.12': resolution: {integrity: sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==} - '@noble/curves@1.2.0': - resolution: {integrity: sha512-oYclrNgRaM9SsBUBVbb8M6DTV7ZHRTKugureoYEncY5c65HOmRzvSiTE3y5CYaPYJA/GVkrhXEoF0M3Ya9PMnw==} + '@noble/ciphers@1.3.0': + resolution: {integrity: sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw==} + engines: {node: ^14.21.3 || >=16} + + '@noble/curves@1.9.1': + resolution: {integrity: sha512-k11yZxZg+t+gWvBbIswW0yoJlu8cHOC7dhunwOzoWH/mXGBiYyR4YY6hAEK/3EUs4UpB8la1RfdRpeGsFHkWsA==} + engines: {node: ^14.21.3 || >=16} - '@noble/hashes@1.3.2': - resolution: {integrity: sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ==} - engines: {node: '>= 16'} + '@noble/hashes@1.8.0': + resolution: {integrity: sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==} + engines: {node: ^14.21.3 || >=16} '@noble/hashes@2.0.1': resolution: {integrity: sha512-XlOlEbQcE9fmuXxrVTXCTlG2nlRXa9Rj3rr5Ue/+tX+nmkgbX720YHh0VR3hBF9xDvwnb8D2shVGOwNx+ulArw==} engines: {node: '>= 20.19.0'} + '@nodelib/fs.scandir@2.1.5': + resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} + engines: {node: '>= 8'} + + '@nodelib/fs.stat@2.0.5': + resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} + engines: {node: '>= 8'} + + '@nodelib/fs.walk@1.2.8': + resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} + engines: {node: '>= 8'} + '@polka/url@1.0.0-next.29': resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==} + '@poppinss/colors@4.1.6': + resolution: {integrity: sha512-H9xkIdFswbS8n1d6vmRd8+c10t2Qe+rZITbbDHHkQixH5+2x1FDGmi/0K+WgWiqQFKPSlIYB7jlH6Kpfn6Fleg==} + + '@poppinss/dumper@0.6.5': + resolution: {integrity: sha512-NBdYIb90J7LfOI32dOewKI1r7wnkiH6m920puQ3qHUeZkxNkQiFnXVWoE6YtFSv6QOiPPf7ys6i+HWWecDz7sw==} + + '@poppinss/exception@1.2.3': + resolution: {integrity: sha512-dCED+QRChTVatE9ibtoaxc+WkdzOSjYTKi/+uacHWIsfodVfpsueo3+DKpgU5Px8qXjgmXkSvhXvSCz3fnP9lw==} + '@rolldown/pluginutils@1.0.0-beta.53': resolution: {integrity: sha512-vENRlFU4YbrwVqNDZ7fLvy+JR1CRkyr01jhSiDpE1u6py3OMzQfztQU2jxykW3ALNxO4kSlqIDeYyD0Y9RcQeQ==} @@ -788,121 +1268,145 @@ packages: resolution: {integrity: sha512-EHMUcDwhtdRGlXZsGSIuXSYwD5kOT9NVnx9sqzYiwAc91wfYOE1g1djOEDseZJKKqtHAHGwnGPQu3kytmfaXLQ==} cpu: [arm] os: [linux] + libc: [glibc] '@rollup/rollup-linux-arm-gnueabihf@4.59.0': resolution: {integrity: sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==} cpu: [arm] os: [linux] + libc: [glibc] '@rollup/rollup-linux-arm-musleabihf@4.54.0': resolution: {integrity: sha512-+pBrqEjaakN2ySv5RVrj/qLytYhPKEUwk+e3SFU5jTLHIcAtqh2rLrd/OkbNuHJpsBgxsD8ccJt5ga/SeG0JmA==} cpu: [arm] os: [linux] + libc: [musl] '@rollup/rollup-linux-arm-musleabihf@4.59.0': resolution: {integrity: sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==} cpu: [arm] os: [linux] + libc: [musl] '@rollup/rollup-linux-arm64-gnu@4.54.0': resolution: {integrity: sha512-NSqc7rE9wuUaRBsBp5ckQ5CVz5aIRKCwsoa6WMF7G01sX3/qHUw/z4pv+D+ahL1EIKy6Enpcnz1RY8pf7bjwng==} cpu: [arm64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-arm64-gnu@4.59.0': resolution: {integrity: sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==} cpu: [arm64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-arm64-musl@4.54.0': resolution: {integrity: sha512-gr5vDbg3Bakga5kbdpqx81m2n9IX8M6gIMlQQIXiLTNeQW6CucvuInJ91EuCJ/JYvc+rcLLsDFcfAD1K7fMofg==} cpu: [arm64] os: [linux] + libc: [musl] '@rollup/rollup-linux-arm64-musl@4.59.0': resolution: {integrity: sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==} cpu: [arm64] os: [linux] + libc: [musl] '@rollup/rollup-linux-loong64-gnu@4.54.0': resolution: {integrity: sha512-gsrtB1NA3ZYj2vq0Rzkylo9ylCtW/PhpLEivlgWe0bpgtX5+9j9EZa0wtZiCjgu6zmSeZWyI/e2YRX1URozpIw==} cpu: [loong64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-loong64-gnu@4.59.0': resolution: {integrity: sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==} cpu: [loong64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-loong64-musl@4.59.0': resolution: {integrity: sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==} cpu: [loong64] os: [linux] + libc: [musl] '@rollup/rollup-linux-ppc64-gnu@4.54.0': resolution: {integrity: sha512-y3qNOfTBStmFNq+t4s7Tmc9hW2ENtPg8FeUD/VShI7rKxNW7O4fFeaYbMsd3tpFlIg1Q8IapFgy7Q9i2BqeBvA==} cpu: [ppc64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-ppc64-gnu@4.59.0': resolution: {integrity: sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==} cpu: [ppc64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-ppc64-musl@4.59.0': resolution: {integrity: sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==} cpu: [ppc64] os: [linux] + libc: [musl] '@rollup/rollup-linux-riscv64-gnu@4.54.0': resolution: {integrity: sha512-89sepv7h2lIVPsFma8iwmccN7Yjjtgz0Rj/Ou6fEqg3HDhpCa+Et+YSufy27i6b0Wav69Qv4WBNl3Rs6pwhebQ==} cpu: [riscv64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-riscv64-gnu@4.59.0': resolution: {integrity: sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==} cpu: [riscv64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-riscv64-musl@4.54.0': resolution: {integrity: sha512-ZcU77ieh0M2Q8Ur7D5X7KvK+UxbXeDHwiOt/CPSBTI1fBmeDMivW0dPkdqkT4rOgDjrDDBUed9x4EgraIKoR2A==} cpu: [riscv64] os: [linux] + libc: [musl] '@rollup/rollup-linux-riscv64-musl@4.59.0': resolution: {integrity: sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==} cpu: [riscv64] os: [linux] + libc: [musl] '@rollup/rollup-linux-s390x-gnu@4.54.0': resolution: {integrity: sha512-2AdWy5RdDF5+4YfG/YesGDDtbyJlC9LHmL6rZw6FurBJ5n4vFGupsOBGfwMRjBYH7qRQowT8D/U4LoSvVwOhSQ==} cpu: [s390x] os: [linux] + libc: [glibc] '@rollup/rollup-linux-s390x-gnu@4.59.0': resolution: {integrity: sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==} cpu: [s390x] os: [linux] + libc: [glibc] '@rollup/rollup-linux-x64-gnu@4.54.0': resolution: {integrity: sha512-WGt5J8Ij/rvyqpFexxk3ffKqqbLf9AqrTBbWDk7ApGUzaIs6V+s2s84kAxklFwmMF/vBNGrVdYgbblCOFFezMQ==} cpu: [x64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-x64-gnu@4.59.0': resolution: {integrity: sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==} cpu: [x64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-x64-musl@4.54.0': resolution: {integrity: sha512-JzQmb38ATzHjxlPHuTH6tE7ojnMKM2kYNzt44LO/jJi8BpceEC8QuXYA908n8r3CNuG/B3BV8VR3Hi1rYtmPiw==} cpu: [x64] os: [linux] + libc: [musl] '@rollup/rollup-linux-x64-musl@4.59.0': resolution: {integrity: sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==} cpu: [x64] os: [linux] + libc: [musl] '@rollup/rollup-openbsd-x64@4.59.0': resolution: {integrity: sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==} @@ -959,16 +1463,56 @@ packages: cpu: [x64] os: [win32] - '@sinclair/typebox@0.27.10': - resolution: {integrity: sha512-MTBk/3jGLNB2tVxv6uLlFh1iu64iYOQ2PbdOSK3NW8JZsmlaOh2q6sdtKowBhfw8QFLmYNzTW4/oK4uATIi6ZA==} + '@scure/base@1.2.6': + resolution: {integrity: sha512-g/nm5FgUa//MCj1gV09zTJTaM6KBAHqLN907YVQqf7zC49+DcO4B1so4ZX07Ef10Twr6nuqYEH9GEggFXA4Fmg==} - '@standard-schema/spec@1.1.0': - resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} + '@scure/bip32@1.7.0': + resolution: {integrity: sha512-E4FFX/N3f4B80AKWp5dP6ow+flD1LQZo/w8UnLGYZO674jS6YnYeepycOOksv+vLPSpgN35wgKgy+ybfTb2SMw==} - '@tailwindcss/node@4.2.1': - resolution: {integrity: sha512-jlx6sLk4EOwO6hHe1oCGm1Q4AN/s0rSrTTPBGPM0/RQ6Uylwq17FuU8IeJJKEjtc6K6O07zsvP+gDO6MMWo7pg==} + '@scure/bip39@1.6.0': + resolution: {integrity: sha512-+lF0BbLiJNwVlev4eKelw1WWLaiKXw7sSl8T6FvBlWkdX+94aGJ4o8XjUdlyhTCjd8c+B3KT3JfS8P0bLRNU6A==} - '@tailwindcss/oxide-android-arm64@4.2.1': + '@shikijs/core@3.23.0': + resolution: {integrity: sha512-NSWQz0riNb67xthdm5br6lAkvpDJRTgB36fxlo37ZzM2yq0PQFFzbd8psqC2XMPgCzo1fW6cVi18+ArJ44wqgA==} + + '@shikijs/engine-javascript@3.23.0': + resolution: {integrity: sha512-aHt9eiGFobmWR5uqJUViySI1bHMqrAgamWE1TYSUoftkAeCCAiGawPMwM+VCadylQtF4V3VNOZ5LmfItH5f3yA==} + + '@shikijs/engine-oniguruma@3.23.0': + resolution: {integrity: sha512-1nWINwKXxKKLqPibT5f4pAFLej9oZzQTsby8942OTlsJzOBZ0MWKiwzMsd+jhzu8YPCHAswGnnN1YtQfirL35g==} + + '@shikijs/langs@3.23.0': + resolution: {integrity: sha512-2Ep4W3Re5aB1/62RSYQInK9mM3HsLeB91cHqznAJMuylqjzNVAVCMnNWRHFtcNHXsoNRayP9z1qj4Sq3nMqYXg==} + + '@shikijs/themes@3.23.0': + resolution: {integrity: sha512-5qySYa1ZgAT18HR/ypENL9cUSGOeI2x+4IvYJu4JgVJdizn6kG4ia5Q1jDEOi7gTbN4RbuYtmHh0W3eccOrjMA==} + + '@shikijs/transformers@3.23.0': + resolution: {integrity: sha512-F9msZVxdF+krQNSdQ4V+Ja5QemeAoTQ2jxt7nJCwhDsdF1JWS3KxIQXA3lQbyKwS3J61oHRUSv4jYWv3CkaKTQ==} + + '@shikijs/types@3.23.0': + resolution: {integrity: sha512-3JZ5HXOZfYjsYSk0yPwBrkupyYSLpAE26Qc0HLghhZNGTZg/SKxXIIgoxOpmmeQP0RRSDJTk1/vPfw9tbw+jSQ==} + + '@shikijs/vscode-textmate@10.0.2': + resolution: {integrity: sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==} + + '@sinclair/typebox@0.27.10': + resolution: {integrity: sha512-MTBk/3jGLNB2tVxv6uLlFh1iu64iYOQ2PbdOSK3NW8JZsmlaOh2q6sdtKowBhfw8QFLmYNzTW4/oK4uATIi6ZA==} + + '@sindresorhus/is@7.2.0': + resolution: {integrity: sha512-P1Cz1dWaFfR4IR+U13mqqiGsLFf1KbayybWwdd2vfctdV6hDpUkgCY0nKOLLTMSoRd/jJNjtbqzf13K8DCCXQw==} + engines: {node: '>=18'} + + '@speed-highlight/core@1.2.14': + resolution: {integrity: sha512-G4ewlBNhUtlLvrJTb88d2mdy2KRijzs4UhnlrOSRT4bmjh/IqNElZa3zkrZ+TC47TwtlDWzVLFADljF1Ijp5hA==} + + '@standard-schema/spec@1.1.0': + resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} + + '@tailwindcss/node@4.2.1': + resolution: {integrity: sha512-jlx6sLk4EOwO6hHe1oCGm1Q4AN/s0rSrTTPBGPM0/RQ6Uylwq17FuU8IeJJKEjtc6K6O07zsvP+gDO6MMWo7pg==} + + '@tailwindcss/oxide-android-arm64@4.2.1': resolution: {integrity: sha512-eZ7G1Zm5EC8OOKaesIKuw77jw++QJ2lL9N+dDpdQiAB/c/B2wDh0QPFHbkBVrXnwNugvrbJFk1gK2SsVjwWReg==} engines: {node: '>= 20'} cpu: [arm64] @@ -1003,24 +1547,28 @@ packages: engines: {node: '>= 20'} cpu: [arm64] os: [linux] + libc: [glibc] '@tailwindcss/oxide-linux-arm64-musl@4.2.1': resolution: {integrity: sha512-WZA0CHRL/SP1TRbA5mp9htsppSEkWuQ4KsSUumYQnyl8ZdT39ntwqmz4IUHGN6p4XdSlYfJwM4rRzZLShHsGAQ==} engines: {node: '>= 20'} cpu: [arm64] os: [linux] + libc: [musl] '@tailwindcss/oxide-linux-x64-gnu@4.2.1': resolution: {integrity: sha512-qMFzxI2YlBOLW5PhblzuSWlWfwLHaneBE0xHzLrBgNtqN6mWfs+qYbhryGSXQjFYB1Dzf5w+LN5qbUTPhW7Y5g==} engines: {node: '>= 20'} cpu: [x64] os: [linux] + libc: [glibc] '@tailwindcss/oxide-linux-x64-musl@4.2.1': resolution: {integrity: sha512-5r1X2FKnCMUPlXTWRYpHdPYUY6a1Ar/t7P24OuiEdEOmms5lyqjDRvVY1yy9Rmioh+AunQ0rWiOTPE8F9A3v5g==} engines: {node: '>= 20'} cpu: [x64] os: [linux] + libc: [musl] '@tailwindcss/oxide-wasm32-wasi@4.2.1': resolution: {integrity: sha512-MGFB5cVPvshR85MTJkEvqDUnuNoysrsRxd6vnk1Lf2tbiqNlXpHYZqkqOQalydienEWOHHFyyuTSYRsLfxFJ2Q==} @@ -1055,18 +1603,27 @@ packages: peerDependencies: vite: ^5.2.0 || ^6 || ^7 + '@tokenizer/token@0.3.0': + resolution: {integrity: sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==} + '@tybys/wasm-util@0.10.1': resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==} '@types/chai@5.2.3': resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} + '@types/debug@4.1.12': + resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==} + '@types/deep-eql@4.0.2': resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} '@types/estree@1.0.8': resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + '@types/hast@3.0.4': + resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==} + '@types/istanbul-lib-coverage@2.0.6': resolution: {integrity: sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==} @@ -1083,8 +1640,20 @@ packages: resolution: {integrity: sha512-TezXjmf3lj+zQ651r6hPqvSScqBLvyPI9FxdXBqpEwBijNGQ2NXpaFW/7joGzveYkKQUil7iiDHLo6LV71Pc0A==} deprecated: This is a stub types definition. jszip provides its own type definitions, so you do not need this installed. - '@types/node@22.7.5': - resolution: {integrity: sha512-jML7s2NAzMWc//QSJ1a3prpk78cOPchGvXJsC3C6R6PSMoooztvRVQEz89gmBTBY1SPMaqo5teB4uNHPdetShQ==} + '@types/linkify-it@5.0.0': + resolution: {integrity: sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==} + + '@types/markdown-it@14.1.2': + resolution: {integrity: sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==} + + '@types/mdast@4.0.4': + resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==} + + '@types/mdurl@2.0.0': + resolution: {integrity: sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==} + + '@types/ms@2.1.0': + resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==} '@types/node@24.10.4': resolution: {integrity: sha512-vnDVpYPMzs4wunl27jHrfmwojOGKya0xyM3sH+UE5iv5uPS6vX7UIoh6m+vQc5LGBq52HBKPIn/zcSZVzeDEZg==} @@ -1092,6 +1661,9 @@ packages: '@types/resolve@1.20.2': resolution: {integrity: sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==} + '@types/unist@3.0.3': + resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} + '@types/web-bluetooth@0.0.21': resolution: {integrity: sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==} @@ -1160,6 +1732,9 @@ packages: resolution: {integrity: sha512-KiROIzYdEV85YygXw6BI/Dx4fnBlFQu6Mq4QE4MOH9fFnhohw6wX/OAvDY2/C+ut0I3RSPKenvZJIVYqJNkhEw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@ungap/structured-clone@1.3.0': + resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==} + '@unrs/resolver-binding-android-arm-eabi@1.11.1': resolution: {integrity: sha512-ppLRUgHVaGRWUx0R0Ut06Mjo9gBaBkg3v/8AxusGLhsIotbBLuRk51rAzqLC8gq6NyyAojEXglNjzf6R948DNw==} cpu: [arm] @@ -1199,41 +1774,49 @@ packages: resolution: {integrity: sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ==} cpu: [arm64] os: [linux] + libc: [glibc] '@unrs/resolver-binding-linux-arm64-musl@1.11.1': resolution: {integrity: sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w==} cpu: [arm64] os: [linux] + libc: [musl] '@unrs/resolver-binding-linux-ppc64-gnu@1.11.1': resolution: {integrity: sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA==} cpu: [ppc64] os: [linux] + libc: [glibc] '@unrs/resolver-binding-linux-riscv64-gnu@1.11.1': resolution: {integrity: sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ==} cpu: [riscv64] os: [linux] + libc: [glibc] '@unrs/resolver-binding-linux-riscv64-musl@1.11.1': resolution: {integrity: sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew==} cpu: [riscv64] os: [linux] + libc: [musl] '@unrs/resolver-binding-linux-s390x-gnu@1.11.1': resolution: {integrity: sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg==} cpu: [s390x] os: [linux] + libc: [glibc] '@unrs/resolver-binding-linux-x64-gnu@1.11.1': resolution: {integrity: sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w==} cpu: [x64] os: [linux] + libc: [glibc] '@unrs/resolver-binding-linux-x64-musl@1.11.1': resolution: {integrity: sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA==} cpu: [x64] os: [linux] + libc: [musl] '@unrs/resolver-binding-wasm32-wasi@1.11.1': resolution: {integrity: sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ==} @@ -1314,44 +1897,82 @@ packages: '@vue/compiler-core@3.5.26': resolution: {integrity: sha512-vXyI5GMfuoBCnv5ucIT7jhHKl55Y477yxP6fc4eUswjP8FG3FFVFd41eNDArR+Uk3QKn2Z85NavjaxLxOC19/w==} + '@vue/compiler-core@3.5.30': + resolution: {integrity: sha512-s3DfdZkcu/qExZ+td75015ljzHc6vE+30cFMGRPROYjqkroYI5NV2X1yAMX9UeyBNWB9MxCfPcsjpLS11nzkkw==} + '@vue/compiler-dom@3.5.26': resolution: {integrity: sha512-y1Tcd3eXs834QjswshSilCBnKGeQjQXB6PqFn/1nxcQw4pmG42G8lwz+FZPAZAby6gZeHSt/8LMPfZ4Rb+Bd/A==} + '@vue/compiler-dom@3.5.30': + resolution: {integrity: sha512-eCFYESUEVYHhiMuK4SQTldO3RYxyMR/UQL4KdGD1Yrkfdx4m/HYuZ9jSfPdA+nWJY34VWndiYdW/wZXyiPEB9g==} + '@vue/compiler-sfc@3.5.26': resolution: {integrity: sha512-egp69qDTSEZcf4bGOSsprUr4xI73wfrY5oRs6GSgXFTiHrWj4Y3X5Ydtip9QMqiCMCPVwLglB9GBxXtTadJ3mA==} + '@vue/compiler-sfc@3.5.30': + resolution: {integrity: sha512-LqmFPDn89dtU9vI3wHJnwaV6GfTRD87AjWpTWpyrdVOObVtjIuSeZr181z5C4PmVx/V3j2p+0f7edFKGRMpQ5A==} + '@vue/compiler-ssr@3.5.26': resolution: {integrity: sha512-lZT9/Y0nSIRUPVvapFJEVDbEXruZh2IYHMk2zTtEgJSlP5gVOqeWXH54xDKAaFS4rTnDeDBQUYDtxKyoW9FwDw==} + '@vue/compiler-ssr@3.5.30': + resolution: {integrity: sha512-NsYK6OMTnx109PSL2IAyf62JP6EUdk4Dmj6AkWcJGBvN0dQoMYtVekAmdqgTtWQgEJo+Okstbf/1p7qZr5H+bA==} + '@vue/devtools-api@7.7.9': resolution: {integrity: sha512-kIE8wvwlcZ6TJTbNeU2HQNtaxLx3a84aotTITUuL/4bzfPxzajGBOoqjMhwZJ8L9qFYDU/lAYMEEm11dnZOD6g==} + '@vue/devtools-api@8.1.0': + resolution: {integrity: sha512-O44X57jjkLKbLEc4OgL/6fEPOOanRJU8kYpCE8qfKlV96RQZcdzrcLI5mxMuVRUeXhHKIHGhCpHacyCk0HyO4w==} + '@vue/devtools-kit@7.7.9': resolution: {integrity: sha512-PyQ6odHSgiDVd4hnTP+aDk2X4gl2HmLDfiyEnn3/oV+ckFDuswRs4IbBT7vacMuGdwY/XemxBoh302ctbsptuA==} + '@vue/devtools-kit@8.1.0': + resolution: {integrity: sha512-/NZlS4WtGIB54DA/z10gzk+n/V7zaqSzYZOVlg2CfdnpIKdB61bd7JDIMxf/zrtX41zod8E2/bbEBoW/d7x70Q==} + '@vue/devtools-shared@7.7.9': resolution: {integrity: sha512-iWAb0v2WYf0QWmxCGy0seZNDPdO3Sp5+u78ORnyeonS6MT4PC7VPrryX2BpMJrwlDeaZ6BD4vP4XKjK0SZqaeA==} + '@vue/devtools-shared@8.1.0': + resolution: {integrity: sha512-h8uCb4Qs8UT8VdTT5yjY6tOJ//qH7EpxToixR0xqejR55t5OdISIg7AJ7eBkhBs8iu1qG5gY3QQNN1DF1EelAA==} + '@vue/language-core@3.2.1': resolution: {integrity: sha512-g6oSenpnGMtpxHGAwKuu7HJJkNZpemK/zg3vZzZbJ6cnnXq1ssxuNrXSsAHYM3NvH8p4IkTw+NLmuxyeYz4r8A==} '@vue/reactivity@3.5.26': resolution: {integrity: sha512-9EnYB1/DIiUYYnzlnUBgwU32NNvLp/nhxLXeWRhHUEeWNTn1ECxX8aGO7RTXeX6PPcxe3LLuNBFoJbV4QZ+CFQ==} + '@vue/reactivity@3.5.30': + resolution: {integrity: sha512-179YNgKATuwj9gB+66snskRDOitDiuOZqkYia7mHKJaidOMo/WJxHKF8DuGc4V4XbYTJANlfEKb0yxTQotnx4Q==} + '@vue/runtime-core@3.5.26': resolution: {integrity: sha512-xJWM9KH1kd201w5DvMDOwDHYhrdPTrAatn56oB/LRG4plEQeZRQLw0Bpwih9KYoqmzaxF0OKSn6swzYi84e1/Q==} + '@vue/runtime-core@3.5.30': + resolution: {integrity: sha512-e0Z+8PQsUTdwV8TtEsLzUM7SzC7lQwYKePydb7K2ZnmS6jjND+WJXkmmfh/swYzRyfP1EY3fpdesyYoymCzYfg==} + '@vue/runtime-dom@3.5.26': resolution: {integrity: sha512-XLLd/+4sPC2ZkN/6+V4O4gjJu6kSDbHAChvsyWgm1oGbdSO3efvGYnm25yCjtFm/K7rrSDvSfPDgN1pHgS4VNQ==} + '@vue/runtime-dom@3.5.30': + resolution: {integrity: sha512-2UIGakjU4WSQ0T4iwDEW0W7vQj6n7AFn7taqZ9Cvm0Q/RA2FFOziLESrDL4GmtI1wV3jXg5nMoJSYO66egDUBw==} + '@vue/server-renderer@3.5.26': resolution: {integrity: sha512-TYKLXmrwWKSodyVuO1WAubucd+1XlLg4set0YoV+Hu8Lo79mp/YMwWV5mC5FgtsDxX3qo1ONrxFaTP1OQgy1uA==} peerDependencies: vue: 3.5.26 + '@vue/server-renderer@3.5.30': + resolution: {integrity: sha512-v+R34icapydRwbZRD0sXwtHqrQJv38JuMB4JxbOxd8NEpGLny7cncMp53W9UH/zo4j8eDHjQ1dEJXwzFQknjtQ==} + peerDependencies: + vue: 3.5.30 + '@vue/shared@3.5.26': resolution: {integrity: sha512-7Z6/y3uFI5PRoKeorTOSXKcDj0MSasfNNltcslbFrPpcw6aXRUALq4IfJlaTRspiWIUOEZbrpM+iQGmCOiWe4A==} + '@vue/shared@3.5.30': + resolution: {integrity: sha512-YXgQ7JjaO18NeK2K9VTbDHaFy62WrObMa6XERNfNOkAhD1F1oDSf3ZJ7K6GqabZ0BvSDHajp8qfS5Sa2I9n8uQ==} + '@vue/tsconfig@0.8.1': resolution: {integrity: sha512-aK7feIWPXFSUhsCP9PFqPyFOcz4ENkb8hZ2pneL6m2UjCkccvaOhC/5KCKluuBufvp2KzkbdA2W2pk20vLzu3g==} peerDependencies: @@ -1368,6 +1989,48 @@ packages: peerDependencies: vue: ^3.5.0 + '@vueuse/integrations@14.2.1': + resolution: {integrity: sha512-2LIUpBi/67PoXJGqSDQUF0pgQWpNHh7beiA+KG2AbybcNm+pTGWT6oPGlBgUoDWmYwfeQqM/uzOHqcILpKL7nA==} + peerDependencies: + async-validator: ^4 + axios: ^1 + change-case: ^5 + drauu: ^0.4 + focus-trap: ^7 || ^8 + fuse.js: ^7 + idb-keyval: ^6 + jwt-decode: ^4 + nprogress: ^0.2 + qrcode: ^1.5 + sortablejs: ^1 + universal-cookie: ^7 || ^8 + vue: ^3.5.0 + peerDependenciesMeta: + async-validator: + optional: true + axios: + optional: true + change-case: + optional: true + drauu: + optional: true + focus-trap: + optional: true + fuse.js: + optional: true + idb-keyval: + optional: true + jwt-decode: + optional: true + nprogress: + optional: true + qrcode: + optional: true + sortablejs: + optional: true + universal-cookie: + optional: true + '@vueuse/metadata@14.2.1': resolution: {integrity: sha512-1ButlVtj5Sb/HDtIy1HFr1VqCP4G6Ypqt5MAo0lCgjokrk2mvQKsK2uuy0vqu/Ks+sHfuHo0B9Y9jn9xKdjZsw==} @@ -1376,6 +2039,34 @@ packages: peerDependencies: vue: ^3.5.0 + '@wagmi/cli@2.10.0': + resolution: {integrity: sha512-2tYt6Bp1q26mWexH+XE6dMpPB5/Gp/3OVtE2SeeJ/gNHKLZmVF/TuoZR75mpJKTpofyvpz/fnuMCkUxzbc/kRw==} + hasBin: true + peerDependencies: + typescript: '>=5.7.3' + peerDependenciesMeta: + typescript: + optional: true + + '@xmldom/xmldom@0.9.8': + resolution: {integrity: sha512-p96FSY54r+WJ50FIOsCOjyj/wavs8921hG5+kVMmZgKcvIKxMXHTrjNJvRgWa/zuX3B6t2lijLNFaOyuxUH+2A==} + engines: {node: '>=14.6'} + + abitype@1.2.3: + resolution: {integrity: sha512-Ofer5QUnuUdTFsBRwARMoWKOH1ND5ehwYhJ3OJ/BQO+StkwQjHw0XyVh4vDttzHB7QOFhPHa/o413PJ82gU/Tg==} + peerDependencies: + typescript: '>=5.0.4' + zod: ^3.22.0 || ^4.0.0 + peerDependenciesMeta: + typescript: + optional: true + zod: + optional: true + + abort-controller@3.0.0: + resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==} + engines: {node: '>=6.5'} + acorn-jsx@5.3.2: resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} peerDependencies: @@ -1386,15 +2077,16 @@ packages: engines: {node: '>=0.4.0'} hasBin: true - aes-js@4.0.0-beta.5: - resolution: {integrity: sha512-G965FqalsNyrPqgEGON7nIx1e/OVENSgiEIzyC63haUMuvNnwIgIjMs52hlTCKhkBny7A2ORNlfY9Zu+jmGk1Q==} - ajv@6.14.0: resolution: {integrity: sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==} alien-signals@3.1.2: resolution: {integrity: sha512-d9dYqZTS90WLiU0I5c6DHj/HcKkF8ZyGN3G5x8wSbslulz70KOxaqCT0hQCo9KOyhVqzqGojvNdJXoTumZOtcw==} + ansi-colors@4.1.3: + resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==} + engines: {node: '>=6'} + ansi-regex@5.0.1: resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} engines: {node: '>=8'} @@ -1411,6 +2103,9 @@ packages: resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} engines: {node: '>= 8'} + argparse@1.0.10: + resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} + argparse@2.0.1: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} @@ -1418,6 +2113,9 @@ packages: resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} engines: {node: '>=12'} + bail@2.0.2: + resolution: {integrity: sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==} + balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} @@ -1443,6 +2141,12 @@ packages: bl@4.1.0: resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} + blake3-wasm@2.1.5: + resolution: {integrity: sha512-F1+K8EbfOZE49dtoPtmxUQrpXaBIl3ICvasLh+nJta0xkz+9kF/7uet9fLnwKqhDrmj6g+6K3Tw9yQPUg2ka5g==} + + boolbase@1.0.0: + resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} + brace-expansion@1.1.12: resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==} @@ -1459,13 +2163,33 @@ packages: engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true + buffer-from@1.1.2: + resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} + buffer@5.7.1: resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} + buffer@6.0.3: + resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==} + builtin-modules@5.0.0: resolution: {integrity: sha512-bkXY9WsVpY7CvMhKSR6pZilZu9Ln5WDrKVBUXf2S443etkmEO4V58heTecXcUIsNsi4Rx8JUO4NfX1IcQl4deg==} engines: {node: '>=18.20'} + bundle-name@4.1.0: + resolution: {integrity: sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==} + engines: {node: '>=18'} + + bundle-require@5.1.0: + resolution: {integrity: sha512-3WrrOuZiyaaZPWiEt4G3+IffISVC9HYlWueJEBWED4ZH4aIAC2PnkdnuRrR94M+w6yGWn4AglWtJtBI8YqvgoA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + peerDependencies: + esbuild: '>=0.18' + + cac@6.7.14: + resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} + engines: {node: '>=8'} + callsites@3.1.0: resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} engines: {node: '>=6'} @@ -1477,6 +2201,9 @@ packages: caniuse-lite@1.0.30001774: resolution: {integrity: sha512-DDdwPGz99nmIEv216hKSgLD+D4ikHQHjBC/seF98N9CPqRX4M5mSxT9eTV6oyisnJcuzxtZy4n17yKKQYmYQOA==} + ccount@2.0.1: + resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} + chai@6.2.2: resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==} engines: {node: '>=18'} @@ -1488,10 +2215,30 @@ packages: change-case@5.4.4: resolution: {integrity: sha512-HRQyTk2/YPEkt9TnUPbOpr64Uw3KOicFWPVBb+xiHvd6eBx/qPr9xqfBFDT8P2vWsvvz4jbEkfDe71W3VyNu2w==} + character-entities-html4@2.1.0: + resolution: {integrity: sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==} + + character-entities-legacy@3.0.0: + resolution: {integrity: sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==} + + character-entities@2.0.2: + resolution: {integrity: sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==} + + cheerio-select@1.6.0: + resolution: {integrity: sha512-eq0GdBvxVFbqWgmCm7M3XGs1I8oLy/nExUnh6oLqmBditPO9AqQJrkslDpMun/hZ0yyTs8L0m85OHp4ho6Qm9g==} + + cheerio@1.0.0-rc.10: + resolution: {integrity: sha512-g0J0q/O6mW8z5zxQ3A8E8J1hUgp4SMOvEoW/x84OwyHKe/Zccz83PVT4y5Crcr530FV6NgmKI1qvGTKVl9XXVw==} + engines: {node: '>= 6'} + chokidar@3.5.1: resolution: {integrity: sha512-9+s+Od+W0VJJzawDma/gvBNQqkTiqYTWLuZoyAsivsI4AaWTCzHG06/TMjsf1cYe9Cb97UCEhjz7HvnPk2p/tw==} engines: {node: '>= 8.10.0'} + chokidar@4.0.1: + resolution: {integrity: sha512-n8enUVCED/KVRQlab1hr3MVpcVMvxtZjmEa956u+4YijlmQED223XMSYj2tLuKvr4jcCTzNNMpQDUer72MMmzA==} + engines: {node: '>= 14.16.0'} + ci-info@4.4.0: resolution: {integrity: sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg==} engines: {node: '>=8'} @@ -1512,6 +2259,10 @@ packages: resolution: {integrity: sha512-IqLQi4lO0nIB4tcdTpN4LCB9FI3uqrJZK7RC515EnhZ6qBaglkIgICb1wjeAqpdoOabm1+SuQtkXIPdYC93jhQ==} engines: {node: '>= 0.2.0'} + cliui@8.0.1: + resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} + engines: {node: '>=12'} + clone@1.0.4: resolution: {integrity: sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==} engines: {node: '>=0.8'} @@ -1527,10 +2278,24 @@ packages: resolution: {integrity: sha512-pFGrxThWcWQ2MsAz6RtgeWe4NK2kUE1WfsrvvlctdII745EW9I0yflqhe7++M5LEc7bV2c/9/5zc8sFcpL0Drw==} engines: {node: '>=0.1.90'} + comma-separated-tokens@2.0.3: + resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==} + commander@10.0.1: resolution: {integrity: sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==} engines: {node: '>=14'} + commander@13.1.0: + resolution: {integrity: sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==} + engines: {node: '>=18'} + + commander@2.20.3: + resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==} + + commander@6.2.1: + resolution: {integrity: sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==} + engines: {node: '>= 6'} + comment-parser@1.4.5: resolution: {integrity: sha512-aRDkn3uyIlCFfk5NUA+VdwMmMsh8JGhc4hapfV4yxymHGQ3BVskMQfoXGpCo5IoBuQ9tS5iiVKhCpTcB4pW4qw==} engines: {node: '>= 12.0.0'} @@ -1538,9 +2303,16 @@ packages: concat-map@0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + confbox@0.1.8: + resolution: {integrity: sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==} + convert-source-map@2.0.0: resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + cookie@1.1.1: + resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==} + engines: {node: '>=18'} + copy-anything@4.0.5: resolution: {integrity: sha512-7Vv6asjS4gMOuILabD3l739tsaxFQmC+a7pLZm02zyvs8p977bL3zEgq3yDk5rn9B0PbYgIv++jmHcuUab4RhA==} engines: {node: '>=18'} @@ -1564,6 +2336,17 @@ packages: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} + crypto-random-string@4.0.0: + resolution: {integrity: sha512-x8dy3RnvYdlUcPOjkEHqozhiwzKNSq7GcPuXFbnyMOCHxX8V3OgIg/pYuabl2sbUPfIJaeAQB7PMOK8DFIdoRA==} + engines: {node: '>=12'} + + css-select@4.3.0: + resolution: {integrity: sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ==} + + css-what@6.2.2: + resolution: {integrity: sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==} + engines: {node: '>= 6'} + csstype@3.2.3: resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} @@ -1582,6 +2365,12 @@ packages: supports-color: optional: true + decode-named-character-reference@1.3.0: + resolution: {integrity: sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q==} + + dedent@0.7.0: + resolution: {integrity: sha512-Q6fKUPqnAHAyhiUgFU7BUzLiv0kd8saH9al7tnu5Q/okj6dnupxyTgFIBjVzJATdfIAm9NAsvXNzjaKa+bxVyA==} + deep-is@0.1.4: resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} @@ -1589,27 +2378,88 @@ packages: resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} engines: {node: '>=0.10.0'} + default-browser-id@5.0.1: + resolution: {integrity: sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q==} + engines: {node: '>=18'} + + default-browser@5.5.0: + resolution: {integrity: sha512-H9LMLr5zwIbSxrmvikGuI/5KGhZ8E2zH3stkMgM5LpOWDutGM2JZaj460Udnf1a+946zc7YBgrqEWwbk7zHvGw==} + engines: {node: '>=18'} + defaults@1.0.4: resolution: {integrity: sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==} + define-lazy-prop@3.0.0: + resolution: {integrity: sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==} + engines: {node: '>=12'} + + dequal@2.0.3: + resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} + engines: {node: '>=6'} + detect-libc@2.1.2: resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} engines: {node: '>=8'} + devlop@1.1.0: + resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==} + + dom-serializer@1.4.1: + resolution: {integrity: sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==} + + domelementtype@2.3.0: + resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==} + + domhandler@3.3.0: + resolution: {integrity: sha512-J1C5rIANUbuYK+FuFL98650rihynUOEzRLxW+90bKZRWB6A1X1Tf82GxR1qAWLyfNPRvjqfip3Q5tdYlmAa9lA==} + engines: {node: '>= 4'} + + domhandler@4.3.1: + resolution: {integrity: sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==} + engines: {node: '>= 4'} + + domutils@2.8.0: + resolution: {integrity: sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==} + + dotenv-expand@10.0.0: + resolution: {integrity: sha512-GopVGCpVS1UKH75VKHGuQFqS1Gusej0z4FyQkPdwjil2gNIv+LNsqBlboOzpJFZKVT95GkCyWJbBSdFEFUWI2A==} + engines: {node: '>=12'} + + dotenv@16.6.1: + resolution: {integrity: sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==} + engines: {node: '>=12'} + electron-to-chromium@1.5.302: resolution: {integrity: sha512-sM6HAN2LyK82IyPBpznDRqlTQAtuSaO+ShzFiWTvoMJLHyZ+Y39r8VMfHzwbU8MVBzQ4Wdn85+wlZl2TLGIlwg==} + emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + enhanced-resolve@5.19.0: resolution: {integrity: sha512-phv3E1Xl4tQOShqSte26C7Fl84EwUdZsyOuSSk9qtAGyyQs2s3jJzComh+Abf4g187lUUAvH+H26omrqia2aGg==} engines: {node: '>=10.13.0'} + entities@2.2.0: + resolution: {integrity: sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==} + + entities@4.5.0: + resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} + engines: {node: '>=0.12'} + entities@7.0.0: resolution: {integrity: sha512-FDWG5cmEYf2Z00IkYRhbFrwIwvdFKH07uV8dvNy0omp/Qb1xcyCWp2UDtcwJF4QZZvk0sLudP6/hAu42TaqVhQ==} engines: {node: '>=0.12'} + entities@7.0.1: + resolution: {integrity: sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==} + engines: {node: '>=0.12'} + error-ex@1.3.4: resolution: {integrity: sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==} + error-stack-parser-es@1.0.5: + resolution: {integrity: sha512-5qucVt2XcuGMcEGgWI7i+yZpmpByQ8J1lHhcL7PwqCwu9FPP3VUXzT4ltHe5i2z9dePwEHcDVOAfSnHsOlCXRA==} + es-module-lexer@1.7.0: resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} @@ -1623,10 +2473,19 @@ packages: engines: {node: '>=18'} hasBin: true + esbuild@0.27.3: + resolution: {integrity: sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==} + engines: {node: '>=18'} + hasBin: true + escalade@3.2.0: resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} engines: {node: '>=6'} + escape-goat@3.0.0: + resolution: {integrity: sha512-w3PwNZJwRxlp47QGzhuEBldEqVHHhh8/tIPcl6ecf2Bou99cdAt0knihBV0Ecc7CGxYduXVBDheH1K2oADRlvw==} + engines: {node: '>=10'} + escape-string-regexp@1.0.5: resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==} engines: {node: '>=0.8.0'} @@ -1635,6 +2494,10 @@ packages: resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} engines: {node: '>=10'} + escape-string-regexp@5.0.0: + resolution: {integrity: sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==} + engines: {node: '>=12'} + eslint-import-context@0.1.9: resolution: {integrity: sha512-K9Hb+yRaGAGUbwjhFNHvSmmkZs9+zbuoe3kFQ4V1wYjrepUFYM2dZAfNtjbbj3qsPfUfsA68Bx/ICWQMi+C8Eg==} engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} @@ -1719,6 +2582,11 @@ packages: resolution: {integrity: sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + esprima@4.0.1: + resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} + engines: {node: '>=4'} + hasBin: true + esquery@1.7.0: resolution: {integrity: sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==} engines: {node: '>=0.10'} @@ -1741,23 +2609,47 @@ packages: resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} engines: {node: '>=0.10.0'} - ethers@6.16.0: - resolution: {integrity: sha512-U1wulmetNymijEhpSEQ7Ct/P/Jw9/e7R1j5XIbPRydgV2DjLVMsULDlNksq3RQnFgKoLlZf88ijYtWEXcPa07A==} - engines: {node: '>=14.0.0'} + event-target-shim@5.0.1: + resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==} + engines: {node: '>=6'} + + eventemitter3@5.0.1: + resolution: {integrity: sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==} + + events@3.3.0: + resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} + engines: {node: '>=0.8.x'} expect-type@1.3.0: resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==} engines: {node: '>=12.0.0'} + extend-shallow@2.0.1: + resolution: {integrity: sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==} + engines: {node: '>=0.10.0'} + + extend@3.0.2: + resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} + fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + fast-glob@3.3.3: + resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} + engines: {node: '>=8.6.0'} + fast-json-stable-stringify@2.1.0: resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} fast-levenshtein@2.0.6: resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + fastq@1.20.1: + resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==} + + fault@2.0.1: + resolution: {integrity: sha512-WtySTkS4OKev5JtpHXnib4Gxiurzh5NCGvWrFaZ34m6JehfTUhKZvn9njTfw48t6JumVQOmrKqpmGcdwxnhqBQ==} + fdir@6.5.0: resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} engines: {node: '>=12.0.0'} @@ -1771,6 +2663,10 @@ packages: resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} engines: {node: '>=16.0.0'} + file-type@18.7.0: + resolution: {integrity: sha512-ihHtXRzXEziMrQ56VSgU7wkxh55iNchFkosu7Y9/S+tXHdKyrGjVK0ujbqNnsxzea+78MaLhN6PGmfYSAv1ACw==} + engines: {node: '>=14.16'} + fill-range@7.1.1: resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} engines: {node: '>=8'} @@ -1790,10 +2686,17 @@ packages: flatted@3.3.3: resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==} + focus-trap@7.8.0: + resolution: {integrity: sha512-/yNdlIkpWbM0ptxno3ONTuf+2g318kh2ez3KSeZN5dZ8YC6AAmgeWz+GasYYiBJPFaYcSAPeu4GfhUaChzIJXA==} + foreground-child@3.3.1: resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} engines: {node: '>=14'} + format@0.2.2: + resolution: {integrity: sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww==} + engines: {node: '>=0.4.x'} + fsevents@2.3.2: resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} @@ -1807,10 +2710,26 @@ packages: function-bind@1.1.2: resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + fuse.js@7.1.0: + resolution: {integrity: sha512-trLf4SzuuUxfusZADLINj+dE8clK1frKdmqiJNb1Es75fmI5oY6X2mxLVUciLLjxqw/xr72Dhy+lER6dGd02FQ==} + engines: {node: '>=10'} + gensync@1.0.0-beta.2: resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} engines: {node: '>=6.9.0'} + get-caller-file@2.0.5: + resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} + engines: {node: 6.* || 8.* || >= 10.*} + + get-port@7.1.0: + resolution: {integrity: sha512-QB9NKEeDg3xxVwCCwJQ9+xycaz6pBB6iQ76wiWMl1927n0Kir6alPiP+yuiICLLU4jpMe08dXfpebuQppFA2zw==} + engines: {node: '>=16'} + + get-stdin@9.0.0: + resolution: {integrity: sha512-dVKBjfWisLAicarI2Sf+JuBE/DghV4UzNAVe9yhEJuzeREd3JhOTE9cUaJTeSa77fsbQUK3pcOpJfM59+VKZaA==} + engines: {node: '>=12'} + get-tsconfig@4.13.6: resolution: {integrity: sha512-shZT/QMiSHc/YBLxxOkMtgSid5HFoauqCE3/exfsEcwg1WkeqjG+V40yBbBrsD+jW2HDXcs28xOfcbm2jI8Ddw==} @@ -1843,6 +2762,10 @@ packages: graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + gray-matter@4.0.3: + resolution: {integrity: sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q==} + engines: {node: '>=6.0'} + has-flag@4.0.0: resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} engines: {node: '>=8'} @@ -1851,9 +2774,24 @@ packages: resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} engines: {node: '>= 0.4'} + hast-util-to-html@9.0.5: + resolution: {integrity: sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw==} + + hast-util-whitespace@3.0.0: + resolution: {integrity: sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==} + hookable@5.5.3: resolution: {integrity: sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==} + html-void-elements@3.0.0: + resolution: {integrity: sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==} + + htmlparser2@5.0.1: + resolution: {integrity: sha512-vKZZra6CSe9qsJzh0BjBGXo8dvzNsq/oGvsjfRdOrrryfeD9UOBEEQdeoqCRmKZchF5h2zOBMQ6YuQ0uRUmdbQ==} + + htmlparser2@6.1.0: + resolution: {integrity: sha512-gyyPk6rgonLFEDGoeRgQNaEUvdJ4ktTmmUh/h2t7s+M8oPpIPxgNACWa+6ESR57kXstwqPiCut0V8NRpcwgU7A==} + ieee754@1.2.1: resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} @@ -1901,14 +2839,32 @@ packages: resolution: {integrity: sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==} engines: {node: '>= 0.4'} + is-docker@3.0.0: + resolution: {integrity: sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + hasBin: true + + is-extendable@0.1.1: + resolution: {integrity: sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==} + engines: {node: '>=0.10.0'} + is-extglob@2.1.1: resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} engines: {node: '>=0.10.0'} + is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + is-glob@4.0.3: resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} engines: {node: '>=0.10.0'} + is-inside-container@1.0.0: + resolution: {integrity: sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==} + engines: {node: '>=14.16'} + hasBin: true + is-interactive@1.0.0: resolution: {integrity: sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==} engines: {node: '>=8'} @@ -1924,6 +2880,14 @@ packages: resolution: {integrity: sha512-DailKdLb0WU+xX8K5w7VsJhapwHLZ9jjmazqCJq4X12CTgqq73TKnbRcnSLuXYPOoLQgV5IrD7ePiX/h1vnkBw==} engines: {node: '>=8'} + is-plain-obj@4.1.0: + resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==} + engines: {node: '>=12'} + + is-stream@3.0.0: + resolution: {integrity: sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + is-unicode-supported@0.1.0: resolution: {integrity: sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==} engines: {node: '>=10'} @@ -1932,12 +2896,21 @@ packages: resolution: {integrity: sha512-oG7cgbmg5kLYae2N5IVd3jm2s+vldjxJzK1pcu9LfpGuQ93MQSzo0okvRna+7y5ifrD+20FE8FvjusyGaz14fw==} engines: {node: '>=18'} + is-wsl@3.1.1: + resolution: {integrity: sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw==} + engines: {node: '>=16'} + isarray@1.0.0: resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==} isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + isows@1.0.7: + resolution: {integrity: sha512-I1fSfDCZL5P0v33sVqeTDSpcstAg/N+wF5HS033mogOVIp4B+oHC7oOCsA3axAbBSGTJ8QubbNmnIRN/h8U7hg==} + peerDependencies: + ws: '*' + jackspeak@4.2.3: resolution: {integrity: sha512-ykkVRwrYvFm1nb2AJfKKYPr0emF6IiXDYUaFx4Zn9ZuIH7MrzEZ3sD5RlqGXNRpHtvUHJyOnCEFxOlNDtGo7wg==} engines: {node: 20 || >=22} @@ -1950,6 +2923,10 @@ packages: resolution: {integrity: sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + jiti@1.21.7: + resolution: {integrity: sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==} + hasBin: true + jiti@2.6.1: resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==} hasBin: true @@ -1960,6 +2937,10 @@ packages: js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + js-yaml@3.14.2: + resolution: {integrity: sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==} + hasBin: true + js-yaml@4.1.1: resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} hasBin: true @@ -1989,9 +2970,22 @@ packages: jszip@3.10.1: resolution: {integrity: sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==} + juice@8.1.0: + resolution: {integrity: sha512-FLzurJrx5Iv1e7CfBSZH68dC04EEvXvvVvPYB7Vx1WAuhCp1ZPIMtqxc+WTWxVkpTIC2Ach/GAv0rQbtGf6YMA==} + engines: {node: '>=10.0.0'} + hasBin: true + keyv@4.5.4: resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + kind-of@6.0.3: + resolution: {integrity: sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==} + engines: {node: '>=0.10.0'} + + kleur@4.1.5: + resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==} + engines: {node: '>=6'} + leven@3.1.0: resolution: {integrity: sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==} engines: {node: '>=6'} @@ -2038,24 +3032,28 @@ packages: engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] + libc: [glibc] lightningcss-linux-arm64-musl@1.31.1: resolution: {integrity: sha512-mVZ7Pg2zIbe3XlNbZJdjs86YViQFoJSpc41CbVmKBPiGmC4YrfeOyz65ms2qpAobVd7WQsbW4PdsSJEMymyIMg==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] + libc: [musl] lightningcss-linux-x64-gnu@1.31.1: resolution: {integrity: sha512-xGlFWRMl+0KvUhgySdIaReQdB4FNudfUTARn7q0hh/V67PVGCs3ADFjw+6++kG1RNd0zdGRlEKa+T13/tQjPMA==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] + libc: [glibc] lightningcss-linux-x64-musl@1.31.1: resolution: {integrity: sha512-eowF8PrKHw9LpoZii5tdZwnBcYDxRw2rRCyvAXLi34iyeYfqCQNA9rmUM0ce62NlPhCvof1+9ivRaTY6pSKDaA==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] + libc: [musl] lightningcss-win32-arm64-msvc@1.31.1: resolution: {integrity: sha512-aJReEbSEQzx1uBlQizAOBSjcmr9dCdL3XuC/6HLXAxmtErsj2ICo5yYggg1qOODQMtnjNQv2UHb9NpOuFtYe4w==} @@ -2076,6 +3074,13 @@ packages: lines-and-columns@1.2.4: resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} + linkify-it@5.0.0: + resolution: {integrity: sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==} + + load-tsconfig@0.2.5: + resolution: {integrity: sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + locate-path@6.0.0: resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} engines: {node: '>=10'} @@ -2087,6 +3092,9 @@ packages: resolution: {integrity: sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==} engines: {node: '>=10'} + longest-streak@3.1.0: + resolution: {integrity: sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==} + lru-cache@11.2.6: resolution: {integrity: sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ==} engines: {node: 20 || >=22} @@ -2102,18 +3110,155 @@ packages: magic-string@0.30.21: resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + mark.js@8.11.1: + resolution: {integrity: sha512-1I+1qpDt4idfgLQG+BNWmrqku+7/2bi5nLf4YwF8y8zXvmfiTBY3PV3ZibfrjBueCByROpuBjLLFCajqkgYoLQ==} + + markdown-it-mathjax3@4.3.2: + resolution: {integrity: sha512-TX3GW5NjmupgFtMJGRauioMbbkGsOXAAt1DZ/rzzYmTHqzkO1rNAdiMD4NiruurToPApn2kYy76x02QN26qr2w==} + + markdown-it@14.1.1: + resolution: {integrity: sha512-BuU2qnTti9YKgK5N+IeMubp14ZUKUUw7yeJbkjtosvHiP0AZ5c8IAgEMk79D0eC8F23r4Ac/q8cAIFdm2FtyoA==} + hasBin: true + + markdown-title@1.0.2: + resolution: {integrity: sha512-MqIQVVkz+uGEHi3TsHx/czcxxCbRIL7sv5K5DnYw/tI+apY54IbPefV/cmgxp6LoJSEx/TqcHdLs/298afG5QQ==} + engines: {node: '>=6'} + + mathjax-full@3.2.2: + resolution: {integrity: sha512-+LfG9Fik+OuI8SLwsiR02IVdjcnRCy5MufYLi0C3TdMT56L/pjB0alMVGgoWJF8pN9Rc7FESycZB9BMNWIid5w==} + deprecated: Version 4 replaces this package with the scoped package @mathjax/src + + mdast-util-from-markdown@2.0.3: + resolution: {integrity: sha512-W4mAWTvSlKvf8L6J+VN9yLSqQ9AOAAvHuoDAmPkz4dHf553m5gVj2ejadHJhoJmcmxEnOv6Pa8XJhpxE93kb8Q==} + + mdast-util-frontmatter@2.0.1: + resolution: {integrity: sha512-LRqI9+wdgC25P0URIJY9vwocIzCcksduHQ9OF2joxQoyTNVduwLAFUzjoopuRJbJAReaKrNQKAZKL3uCMugWJA==} + + mdast-util-phrasing@4.1.0: + resolution: {integrity: sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==} + + mdast-util-to-hast@13.2.1: + resolution: {integrity: sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==} + + mdast-util-to-markdown@2.1.2: + resolution: {integrity: sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==} + + mdast-util-to-string@4.0.0: + resolution: {integrity: sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==} + + mdurl@2.0.0: + resolution: {integrity: sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==} + + mensch@0.3.4: + resolution: {integrity: sha512-IAeFvcOnV9V0Yk+bFhYR07O3yNina9ANIN5MoXBKYJ/RLYPurd2d0yw14MDhpr9/momp0WofT1bPUh3hkzdi/g==} + + meow@12.1.1: + resolution: {integrity: sha512-BhXM0Au22RwUneMPwSCnyhTOizdWoIEPU9sp0Aqa1PnDMR5Wv2FGXYDjuzJEIX+Eo2Rb8xuYe5jrnm5QowQFkw==} + engines: {node: '>=16.10'} + + merge2@1.4.1: + resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} + engines: {node: '>= 8'} + + mhchemparser@4.2.1: + resolution: {integrity: sha512-kYmyrCirqJf3zZ9t/0wGgRZ4/ZJw//VwaRVGA75C4nhE60vtnIzhl9J9ndkX/h6hxSN7pjg/cE0VxbnNM+bnDQ==} + + micromark-core-commonmark@2.0.3: + resolution: {integrity: sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==} + + micromark-extension-frontmatter@2.0.0: + resolution: {integrity: sha512-C4AkuM3dA58cgZha7zVnuVxBhDsbttIMiytjgsM2XbHAB2faRVaHRle40558FBN+DJcrLNCoqG5mlrpdU4cRtg==} + + micromark-factory-destination@2.0.1: + resolution: {integrity: sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==} + + micromark-factory-label@2.0.1: + resolution: {integrity: sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==} + + micromark-factory-space@2.0.1: + resolution: {integrity: sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==} + + micromark-factory-title@2.0.1: + resolution: {integrity: sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==} + + micromark-factory-whitespace@2.0.1: + resolution: {integrity: sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==} + + micromark-util-character@2.1.1: + resolution: {integrity: sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==} + + micromark-util-chunked@2.0.1: + resolution: {integrity: sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==} + + micromark-util-classify-character@2.0.1: + resolution: {integrity: sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==} + + micromark-util-combine-extensions@2.0.1: + resolution: {integrity: sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==} + + micromark-util-decode-numeric-character-reference@2.0.2: + resolution: {integrity: sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==} + + micromark-util-decode-string@2.0.1: + resolution: {integrity: sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==} + + micromark-util-encode@2.0.1: + resolution: {integrity: sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==} + + micromark-util-html-tag-name@2.0.1: + resolution: {integrity: sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==} + + micromark-util-normalize-identifier@2.0.1: + resolution: {integrity: sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==} + + micromark-util-resolve-all@2.0.1: + resolution: {integrity: sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==} + + micromark-util-sanitize-uri@2.0.1: + resolution: {integrity: sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==} + + micromark-util-subtokenize@2.1.0: + resolution: {integrity: sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==} + + micromark-util-symbol@2.0.1: + resolution: {integrity: sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==} + + micromark-util-types@2.0.2: + resolution: {integrity: sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==} + + micromark@4.0.2: + resolution: {integrity: sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==} + micromatch@4.0.8: resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} engines: {node: '>=8.6'} + millify@6.1.0: + resolution: {integrity: sha512-H/E3J6t+DQs/F2YgfDhxUVZz/dF8JXPPKTLHL/yHCcLZLtCXJDUaqvhJXQwqOVBvbyNn4T0WjLpIHd7PAw7fBA==} + hasBin: true + + mime@2.6.0: + resolution: {integrity: sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==} + engines: {node: '>=4.0.0'} + hasBin: true + mimic-fn@2.1.0: resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} engines: {node: '>=6'} + miniflare@4.20260312.0: + resolution: {integrity: sha512-pieP2rfXynPT6VRINYaiHe/tfMJ4c5OIhqRlIdLF6iZ9g5xgpEmvimvIgMpgAdDJuFlrLcwDUi8MfAo2R6dt/w==} + engines: {node: '>=18.0.0'} + hasBin: true + minimatch@10.2.2: resolution: {integrity: sha512-+G4CpNBxa5MprY+04MbgOw1v7So6n5JY166pFi9KfYwT78fxScCeSNQSNzp6dpPSW2rONOps6Ocam1wFhCgoVw==} engines: {node: 18 || 20 || >=22} + minimatch@10.2.4: + resolution: {integrity: sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==} + engines: {node: 18 || 20 || >=22} + minimatch@3.1.4: resolution: {integrity: sha512-twmL+S8+7yIsE9wsqgzU3E8/LumN3M3QELrBZ20OdmQ9jB2JvW5oZtBEmft84k/Gs5CG9mqtWc6Y9vW+JEzGxw==} @@ -2121,9 +3266,18 @@ packages: resolution: {integrity: sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==} engines: {node: '>=16 || 14 >=14.17'} + minisearch@7.2.0: + resolution: {integrity: sha512-dqT2XBYUOZOiC5t2HRnwADjhNS2cecp9u+TJRiJ1Qp/f5qjkeT5APcGPjHw+bz89Ms8Jp+cG4AlE+QZ/QnDglg==} + mitt@3.0.1: resolution: {integrity: sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==} + mj-context-menu@0.6.1: + resolution: {integrity: sha512-7NO5s6n10TIV96d4g2uDpG7ZDpIhMh0QNfGdJw/W47JswFcosz457wqz/b5sAKvl12sxINGFCn80NZHKwxQEXA==} + + mlly@1.8.1: + resolution: {integrity: sha512-SnL6sNutTwRWWR/vcmCYHSADjiEesp5TGQQ0pXyLhW5IoeibRlF/CbSLailbB3CNqJUk9cVJ9dUDnbD7GrcHBQ==} + moo@0.5.2: resolution: {integrity: sha512-iSAJLHYKnX41mKcJKjqvnAN9sf0LMDTXDEvFv+ffuRR9a1MIuXLjMNL6EsnDHSkKLTWNqQQ5uo61P4EbU4NU+Q==} @@ -2142,6 +3296,9 @@ packages: engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true + nanospinner@1.2.2: + resolution: {integrity: sha512-Zt/AmG6qRU3e+WnzGGLuMCEAO/dAu45stNbHY223tUxldaDAeE+FxSPsd9Q+j+paejmm0ZbrNVs5Sraqy3dRxA==} + napi-postinstall@0.3.4: resolution: {integrity: sha512-PHI5f1O0EP5xJ9gQmFGMS6IZcrVvTjpXjz7Na41gTE7eE2hK11lg04CECCYEEjdc17EV4DO+fkGEtt7TpTaTiQ==} engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} @@ -2150,6 +3307,15 @@ packages: natural-compare@1.4.0: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + node-fetch@2.7.0: + resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==} + engines: {node: 4.x || >=6.0.0} + peerDependencies: + encoding: ^0.1.0 + peerDependenciesMeta: + encoding: + optional: true + node-releases@2.0.27: resolution: {integrity: sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==} @@ -2157,6 +3323,9 @@ packages: resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} engines: {node: '>=0.10.0'} + nth-check@2.1.1: + resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==} + observable-fns@0.6.1: resolution: {integrity: sha512-9gRK4+sRWzeN6AOewNBTLXir7Zl/i3GB6Yl26gK4flxz8BXVpD3kt8amREmWNb0mxYOGDotvE5a4N+PtGGKdkg==} @@ -2167,6 +3336,21 @@ packages: resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} engines: {node: '>=6'} + oniguruma-parser@0.12.1: + resolution: {integrity: sha512-8Unqkvk1RYc6yq2WBYRj4hdnsAxVze8i7iPfQr8e4uSP3tRv0rpZcbGUDvxfQQcdwHt/e9PrMvGCsa8OqG9X3w==} + + oniguruma-to-es@4.3.4: + resolution: {integrity: sha512-3VhUGN3w2eYxnTzHn+ikMI+fp/96KoRSVK9/kMTcFqj1NRDh2IhQCKvYxDnWePKRXY/AqH+Fuiyb7VHSzBjHfA==} + + open-cli@8.0.0: + resolution: {integrity: sha512-3muD3BbfLyzl+aMVSEfn2FfOqGdPYR0O4KNnxXsLEPE2q9OSjBfJAaB6XKbrUzLgymoSMejvb5jpXJfru/Ko2A==} + engines: {node: '>=18'} + hasBin: true + + open@10.2.0: + resolution: {integrity: sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA==} + engines: {node: '>=18'} + optionator@0.9.4: resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} engines: {node: '>= 0.8.0'} @@ -2175,6 +3359,14 @@ packages: resolution: {integrity: sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==} engines: {node: '>=10'} + ox@0.14.0: + resolution: {integrity: sha512-WLOB7IKnmI3Ol6RAqY7CJdZKl8QaI44LN91OGF1061YIeN6bL5IsFcdp7+oQShRyamE/8fW/CBRWhJAOzI35Dw==} + peerDependencies: + typescript: '>=5.4.0' + peerDependenciesMeta: + typescript: + optional: true + p-limit@3.1.0: resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} engines: {node: '>=10'} @@ -2186,6 +3378,9 @@ packages: package-json-from-dist@1.0.1: resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} + package-manager-detector@1.6.0: + resolution: {integrity: sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA==} + pako@1.0.11: resolution: {integrity: sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==} @@ -2197,6 +3392,12 @@ packages: resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} engines: {node: '>=8'} + parse5-htmlparser2-tree-adapter@6.0.1: + resolution: {integrity: sha512-qPuWvbLgvDGilKc5BoicRovlT4MtYT6JfJyBOMDsKoiT+GiuP5qyrPCnR9HcPECIJJmZh5jRndyNThnhhb/vlA==} + + parse5@6.0.1: + resolution: {integrity: sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==} + path-browserify@1.0.1: resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==} @@ -2215,16 +3416,29 @@ packages: resolution: {integrity: sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==} engines: {node: 18 || 20 || >=22} + path-to-regexp@6.3.0: + resolution: {integrity: sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==} + path-type@4.0.0: resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} engines: {node: '>=8'} + pathe@1.1.2: + resolution: {integrity: sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==} + pathe@2.0.3: resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + peek-readable@5.4.2: + resolution: {integrity: sha512-peBp3qZyuS6cNIJ2akRNG1uo1WJ1d0wTxg/fxMdZ0BqCVhx242bSFHM9eNqflfJVS9SsgkzgT/1UgnsurBOTMg==} + engines: {node: '>=14.16'} + perfect-debounce@1.0.0: resolution: {integrity: sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==} + perfect-debounce@2.1.0: + resolution: {integrity: sha512-LjgdTytVFXeUgtHZr9WYViYSM/g8MkcTPYDlPa3cDqMirHjKiSZPYd6DoL7pK8AJQr+uWkQvCjHNdiMqsrJs+g==} + picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} @@ -2232,6 +3446,10 @@ packages: resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} engines: {node: '>=8.6'} + picomatch@3.0.1: + resolution: {integrity: sha512-I3EurrIQMlRc9IaAZnqRR044Phh2DXY+55o7uJ0V+hYZAcQYSuFWsc9q5PvyDHUSCe1Qxn/iBz+78s86zWnGag==} + engines: {node: '>=10'} + picomatch@4.0.3: resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} engines: {node: '>=12'} @@ -2249,6 +3467,9 @@ packages: resolution: {integrity: sha512-1wrVzJ2STrpmONHKBy228LM1b84msXDUoAzVEl0R8Mz4Ce6EPr+IVtxm8+yvrqLYMHswREkjYFaMxnyGnaY3Ng==} hasBin: true + pkg-types@1.3.1: + resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==} + playwright-core@1.58.2: resolution: {integrity: sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg==} engines: {node: '>=18'} @@ -2275,10 +3496,20 @@ packages: pofile@1.1.4: resolution: {integrity: sha512-r6Q21sKsY1AjTVVjOuU02VYKVNQGJNQHjTIvs4dEbeuuYfxgYk/DGD2mqqq4RDaVkwdSq0VEtmQUOPe/wH8X3g==} + postcss-rtlcss@5.7.1: + resolution: {integrity: sha512-zE68CuARv5StOG/UQLa0W1Y/raUTzgJlfjtas43yh3/G1BFmoPEaHxPRHgeowXRFFhW33FehrNgsljxRLmPVWw==} + engines: {node: '>=18.0.0'} + peerDependencies: + postcss: ^8.4.21 + postcss@8.5.6: resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} engines: {node: ^10 || ^12 || >=14} + postcss@8.5.8: + resolution: {integrity: sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==} + engines: {node: ^10 || ^12 || >=14} + prelude-ls@1.2.1: resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} engines: {node: '>= 0.8.0'} @@ -2288,6 +3519,10 @@ packages: engines: {node: '>=14'} hasBin: true + pretty-bytes@7.1.0: + resolution: {integrity: sha512-nODzvTiYVRGRqAOvE84Vk5JDPyyxsVk0/fbA/bq7RqlnhksGpset09XTxbpvLTIjoaF7K8Z8DG8yHtKGTPSYRw==} + engines: {node: '>=20'} + pretty-format@29.7.0: resolution: {integrity: sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -2295,15 +3530,29 @@ packages: process-nextick-args@2.0.1: resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} + process@0.11.10: + resolution: {integrity: sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==} + engines: {node: '>= 0.6.0'} + + property-information@7.1.0: + resolution: {integrity: sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==} + pseudolocale@2.2.0: resolution: {integrity: sha512-O+D2eU7fO9wVLqrohvt9V/9fwMadnJQ4jxwiK+LeNEqhMx8JYx4xQHkArDCJFAdPPOp/pQq6z5L37eBvAoc8jw==} engines: {node: '>=16.0.0'} hasBin: true + punycode.js@2.3.1: + resolution: {integrity: sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==} + engines: {node: '>=6'} + punycode@2.3.1: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} + queue-microtask@1.2.3: + resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + react-is@18.3.1: resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==} @@ -2314,10 +3563,31 @@ packages: resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} engines: {node: '>= 6'} + readable-stream@4.7.0: + resolution: {integrity: sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + readable-web-to-node-stream@3.0.4: + resolution: {integrity: sha512-9nX56alTf5bwXQ3ZDipHJhusu9NTQJ/CVPtb/XHAJCXihZeitfJvIRS4GqQ/mfIoOE3IelHMrpayVrosdHBuLw==} + engines: {node: '>=8'} + readdirp@3.5.0: resolution: {integrity: sha512-cMhu7c/8rdhkHXWsY+osBhfSy0JikwpHK/5+imo+LpeasTF8ouErHrlYkwT0++njiyuDvc7OFY5T3ukvZ8qmFQ==} engines: {node: '>=8.10.0'} + readdirp@4.1.2: + resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} + engines: {node: '>= 14.18.0'} + + regex-recursion@6.0.2: + resolution: {integrity: sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg==} + + regex-utilities@2.3.0: + resolution: {integrity: sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng==} + + regex@6.1.0: + resolution: {integrity: sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg==} + regexp-tree@0.1.27: resolution: {integrity: sha512-iETxpjK6YoRWJG5o6hXLwvjYAoW+FEZn9os0PD/b6AP6xQwsa/Y7lCVgIixBbUPMfhu+i2LtdeAqVTgGlQarfA==} hasBin: true @@ -2326,6 +3596,22 @@ packages: resolution: {integrity: sha512-NZQZdC5wOE/H3UT28fVGL+ikOZcEzfMGk/c3iN9UGxzWHMa1op7274oyiUVrAG4B2EuFhus8SvkaYnhvW92p9Q==} hasBin: true + remark-frontmatter@5.0.0: + resolution: {integrity: sha512-XTFYvNASMe5iPN0719nPrdItC9aU0ssC4v14mH1BCi1u0n1gAocqcujWUrByftZTbLhRtiKRyjYTSIOcr69UVQ==} + + remark-parse@11.0.0: + resolution: {integrity: sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==} + + remark-stringify@11.0.0: + resolution: {integrity: sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==} + + remark@15.0.1: + resolution: {integrity: sha512-Eht5w30ruCXgFmxVUSlNWQ9iiimq07URKeFS3hNc8cUWy1llX4KDWfyEDZRycMc+znsN9Ux5/tJ/BFdgdOwA3A==} + + require-directory@2.1.1: + resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} + engines: {node: '>=0.10.0'} + resolve-from@4.0.0: resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} engines: {node: '>=4'} @@ -2342,6 +3628,10 @@ packages: resolution: {integrity: sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==} engines: {node: '>=8'} + reusify@1.1.0: + resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} + engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + rfdc@1.4.1: resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==} @@ -2360,9 +3650,28 @@ packages: engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true + rtlcss@4.3.0: + resolution: {integrity: sha512-FI+pHEn7Wc4NqKXMXFM+VAYKEj/mRIcW4h24YVwVtyjI+EqGrLc2Hx/Ny0lrZ21cBWU2goLy36eqMcNj3AQJig==} + engines: {node: '>=12.0.0'} + hasBin: true + + run-applescript@7.1.0: + resolution: {integrity: sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==} + engines: {node: '>=18'} + + run-parallel@1.2.0: + resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + safe-buffer@5.1.2: resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==} + safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + + section-matter@1.0.0: + resolution: {integrity: sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA==} + engines: {node: '>=4'} + semver@6.3.1: resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} hasBin: true @@ -2375,6 +3684,10 @@ packages: setimmediate@1.0.5: resolution: {integrity: sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==} + sharp@0.34.5: + resolution: {integrity: sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + shebang-command@2.0.0: resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} engines: {node: '>=8'} @@ -2383,6 +3696,9 @@ packages: resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} engines: {node: '>=8'} + shiki@3.23.0: + resolution: {integrity: sha512-55Dj73uq9ZXL5zyeRPzHQsK7Nbyt6Y10k5s7OjuFZGMhpp4r/rsLBH0o/0fstIzX1Lep9VxefWljK/SKCzygIA==} + siginfo@2.0.0: resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} @@ -2393,22 +3709,48 @@ packages: resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} engines: {node: '>=14'} + simple-git@3.33.0: + resolution: {integrity: sha512-D4V/tGC2sjsoNhoMybKyGoE+v8A60hRawKQ1iFRA1zwuDgGZCBJ4ByOzZ5J8joBbi4Oam0qiPH+GhzmSBwbJng==} + sirv@3.0.2: resolution: {integrity: sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==} engines: {node: '>=18'} + sisteransi@1.0.5: + resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} + + slick@1.12.2: + resolution: {integrity: sha512-4qdtOGcBjral6YIBCWJ0ljFSKNLz9KkhbWtuGvUyRowl1kxfuE1x/Z/aJcaiilpb3do9bl5K7/1h9XC5wWpY/A==} + source-map-js@1.2.1: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} + source-map-support@0.5.21: + resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} + + source-map@0.6.1: + resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} + engines: {node: '>=0.10.0'} + source-map@0.7.6: resolution: {integrity: sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==} engines: {node: '>= 12'} + space-separated-tokens@2.0.2: + resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==} + speakingurl@14.0.1: resolution: {integrity: sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ==} engines: {node: '>=0.10.0'} + speech-rule-engine@4.1.2: + resolution: {integrity: sha512-S6ji+flMEga+1QU79NDbwZ8Ivf0S/MpupQQiIC0rTpU/ZTKgcajijJJb1OcByBQDjrXCN1/DJtGz4ZJeBMPGJw==} + hasBin: true + + sprintf-js@1.0.3: + resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} + stable-hash-x@0.2.0: resolution: {integrity: sha512-o3yWv49B/o4QZk5ZcsALc6t0+eCelPc44zZsLtCQnZPDwFpDYSWcDnrv2TtMmMbQ7uKo3J0HTURCqckw23czNQ==} engines: {node: '>=12.0.0'} @@ -2419,13 +3761,27 @@ packages: std-env@3.10.0: resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} + string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + string_decoder@1.1.1: resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==} + string_decoder@1.3.0: + resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} + + stringify-entities@4.0.4: + resolution: {integrity: sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==} + strip-ansi@6.0.1: resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} engines: {node: '>=8'} + strip-bom-string@1.0.0: + resolution: {integrity: sha512-uCC2VHvQRYu+lMh4My/sFNmF2klFymLX1wHJeXnbEJERpV/ZsVuonzerjfrGpIGF7LBVa1O7i9kjiWvJiFck8g==} + engines: {node: '>=0.10.0'} + strip-indent@4.1.1: resolution: {integrity: sha512-SlyRoSkdh1dYP0PzclLE7r0M9sgbFKKMFXpFRUMNuKhQSbC6VQIGzq3E0qsfvGJaUFJPGv6Ws1NZ/haTAjfbMA==} engines: {node: '>=12'} @@ -2434,10 +3790,18 @@ packages: resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} engines: {node: '>=8'} + strtok3@7.1.1: + resolution: {integrity: sha512-mKX8HA/cdBqMKUr0MMZAFssCkIGoZeSCMXgnt79yKxNFguMLVFgRe6wB+fsL0NmoHDbeyZXczy7vEPSoo3rkzg==} + engines: {node: '>=16'} + superjson@2.2.6: resolution: {integrity: sha512-H+ue8Zo4vJmV2nRjpx86P35lzwDT3nItnIsocgumgr0hHMQ+ZGq5vrERg9kJBo5AWGmxZDhzDo+WVIJqkB0cGA==} engines: {node: '>=16'} + supports-color@10.2.2: + resolution: {integrity: sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==} + engines: {node: '>=18'} + supports-color@7.2.0: resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} engines: {node: '>=8'} @@ -2446,6 +3810,9 @@ packages: resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} engines: {node: '>= 0.4'} + tabbable@6.4.0: + resolution: {integrity: sha512-05PUHKSNE8ou2dwIxTngl4EzcnsCDZGJ/iCLtDflR/SHB/ny14rXc+qU5P4mG9JkusiV7EivzY9Mhm55AzAvCg==} + tailwindcss@4.2.1: resolution: {integrity: sha512-/tBrSQ36vCleJkAOsy9kbNTgaxvGbyOamC30PRePTQe/o1MFwEKHQk4Cn7BNGaPtjp+PuUrByJehM1hgxfq4sw==} @@ -2453,6 +3820,19 @@ packages: resolution: {integrity: sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==} engines: {node: '>=6'} + temp-dir@3.0.0: + resolution: {integrity: sha512-nHc6S/bwIilKHNRgK/3jlhDoIHcp45YgyiwcAk46Tr0LfEqGBVpmiAyuiuxeVE44m3mXnEeVhaipLOEWmH+Njw==} + engines: {node: '>=14.16'} + + tempy@3.2.0: + resolution: {integrity: sha512-d79HhZya5Djd7am0q+W4RTsSU+D/aJzM+4Y4AGJGuGlgM2L6sx5ZvOYTmZjqPhrDrV6xJTtRSm1JCLj6V6LHLQ==} + engines: {node: '>=14.16'} + + terser@5.46.0: + resolution: {integrity: sha512-jTwoImyr/QbOWFFso3YoU3ik0jBBDJ6JTOQiy/J2YxVJdZCc+5u7skhNwiOR3FQIygFqVUPHl7qbbxtjW2K3Qg==} + engines: {node: '>=10'} + hasBin: true + threads@1.7.0: resolution: {integrity: sha512-Mx5NBSHX3sQYR6iI9VYbgHKBLisyB+xROCBGjjWm1O9wb9vfLxdaGtmT/KCjUqMsSNW6nERzCW3T6H43LqjDZQ==} @@ -2478,10 +3858,26 @@ packages: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} + token-types@5.0.1: + resolution: {integrity: sha512-Y2fmSnZjQdDb9W4w4r1tswlMHylzWIeOKpx0aZH9BgGtACHhrk3OkT52AzwcuqTRBZtvvnTjDBh8eynMulu8Vg==} + engines: {node: '>=14.16'} + + tokenx@1.3.0: + resolution: {integrity: sha512-NLdXTEZkKiO0gZuLtMoZKjCXTREXeZZt8nnnNeyoXtNZAfG/GKGSbQtLU5STspc0rMSwcA+UJfWZkbNU01iKmQ==} + totalist@3.0.1: resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==} engines: {node: '>=6'} + tr46@0.0.3: + resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} + + trim-lines@3.0.1: + resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==} + + trough@2.2.0: + resolution: {integrity: sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==} + ts-api-utils@2.4.0: resolution: {integrity: sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==} engines: {node: '>=18.12'} @@ -2491,10 +3887,21 @@ packages: tslib@2.7.0: resolution: {integrity: sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==} + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + type-check@0.4.0: resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} engines: {node: '>= 0.8.0'} + type-fest@1.4.0: + resolution: {integrity: sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA==} + engines: {node: '>=10'} + + type-fest@2.19.0: + resolution: {integrity: sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==} + engines: {node: '>=12.20'} + typescript-eslint@8.56.1: resolution: {integrity: sha512-U4lM6pjmBX7J5wk4szltF7I1cGBHXZopnAXCMXb3+fZ3B/0Z3hq3wS/CCUB2NZBNAExK92mCU2tEohWuwVMsDQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -2507,12 +3914,50 @@ packages: engines: {node: '>=14.17'} hasBin: true - undici-types@6.19.8: - resolution: {integrity: sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==} + uc.micro@2.1.0: + resolution: {integrity: sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==} + + ufo@1.6.3: + resolution: {integrity: sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q==} + + ultramatter@0.0.4: + resolution: {integrity: sha512-1f/hO3mR+/Hgue4eInOF/Qm/wzDqwhYha4DxM0hre9YIUyso3fE2XtrAU6B4njLqTC8CM49EZaYgsVSa+dXHGw==} undici-types@7.16.0: resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==} + undici@7.18.2: + resolution: {integrity: sha512-y+8YjDFzWdQlSE9N5nzKMT3g4a5UBX1HKowfdXh0uvAnTaqqwqB92Jt4UXBAeKekDs5IaDKyJFR4X1gYVCgXcw==} + engines: {node: '>=20.18.1'} + + unenv@2.0.0-rc.24: + resolution: {integrity: sha512-i7qRCmY42zmCwnYlh9H2SvLEypEFGye5iRmEMKjcGi7zk9UquigRjFtTLz0TYqr0ZGLZhaMHl/foy1bZR+Cwlw==} + + unified@11.0.5: + resolution: {integrity: sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==} + + unique-string@3.0.0: + resolution: {integrity: sha512-VGXBUVwxKMBUznyffQweQABPRRW1vHZAbadFZud4pLFAqRGvv/96vafgjWFqzourzr8YonlQiPgH0YCJfawoGQ==} + engines: {node: '>=12'} + + unist-util-is@6.0.1: + resolution: {integrity: sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==} + + unist-util-position@5.0.0: + resolution: {integrity: sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==} + + unist-util-remove@4.0.0: + resolution: {integrity: sha512-b4gokeGId57UVRX/eVKej5gXqGlc9+trkORhFJpu9raqZkZhU0zm8Doi05+HaiBsMEIJowL+2WtQ5ItjsngPXg==} + + unist-util-stringify-position@4.0.0: + resolution: {integrity: sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==} + + unist-util-visit-parents@6.0.2: + resolution: {integrity: sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==} + + unist-util-visit@5.1.0: + resolution: {integrity: sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==} + unrs-resolver@1.11.1: resolution: {integrity: sha512-bSjt9pjaEBnNiGgc9rUiHGKv5l4/TGzDmYw3RhnkJGtLhbnnA/5qJj7x3dNDCRx/PJxu774LlH8lCOlB4hEfKg==} @@ -2528,6 +3973,24 @@ packages: util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + valid-data-url@3.0.1: + resolution: {integrity: sha512-jOWVmzVceKlVVdwjNSenT4PbGghU0SBIizAev8ofZVgivk/TVHXSbNL8LP6M3spZvkR9/QolkyJavGSX5Cs0UA==} + engines: {node: '>=10'} + + vfile-message@4.0.3: + resolution: {integrity: sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==} + + vfile@6.0.3: + resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==} + + viem@2.47.0: + resolution: {integrity: sha512-jU5e1E1s5E5M1y+YrELDnNar/34U8NXfVcRfxtVETigs2gS1vvW2ngnBoQUGBwLnNr0kNv+NUu4m10OqHByoFw==} + peerDependencies: + typescript: '>=5.0.4' + peerDependenciesMeta: + typescript: + optional: true + vite@7.3.0: resolution: {integrity: sha512-dZwN5L1VlUBewiP6H9s2+B3e3Jg96D0vzN+Ry73sOefebhYr9f94wwkMNN/9ouoU8pV1BqA1d1zGk8928cx0rg==} engines: {node: ^20.19.0 || >=22.12.0} @@ -2568,6 +4031,72 @@ packages: yaml: optional: true + vite@7.3.1: + resolution: {integrity: sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + '@types/node': ^20.19.0 || >=22.12.0 + jiti: '>=1.21.0' + less: ^4.0.0 + lightningcss: ^1.21.0 + sass: ^1.70.0 + sass-embedded: ^1.70.0 + stylus: '>=0.54.8' + sugarss: ^5.0.0 + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + jiti: + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + + vitepress-plugin-group-icons@1.7.1: + resolution: {integrity: sha512-3ZPcIqwHNBg1btrOOSecOqv8yJxHdu3W2ugxE5LusclDF005LAm60URMEmBQrkgl4JvM32AqJirqghK6lGIk8g==} + peerDependencies: + vite: '>=3' + peerDependenciesMeta: + vite: + optional: true + + vitepress-plugin-llms@1.11.0: + resolution: {integrity: sha512-n6fjWzBNKy40p8cij+d2cHiC2asNW1eQKdmc06gX9VAv7vWppIoVLH/f7Ht1bK0vSpGzzW2QimvNfbfv1oCdJw==} + + vitepress@2.0.0-alpha.16: + resolution: {integrity: sha512-w1nwsefDVIsje7BZr2tsKxkZutDGjG0YoQ2yxO7+a9tvYVqfljYbwj5LMYkPy8Tb7YbPwa22HtIhk62jbrvuEQ==} + hasBin: true + peerDependencies: + markdown-it-mathjax3: ^4 + oxc-minify: '*' + postcss: ^8 + peerDependenciesMeta: + markdown-it-mathjax3: + optional: true + oxc-minify: + optional: true + postcss: + optional: true + vitest@4.0.18: resolution: {integrity: sha512-hOQuK7h0FGKgBAas7v0mSAsnvrIgAvWmRFjmzpJ7SwFHH3g1k2u37JtYwOwmEKhK6ZO3v9ggDBBm0La1LCK4uQ==} engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} @@ -2619,9 +4148,27 @@ packages: typescript: optional: true + vue@3.5.30: + resolution: {integrity: sha512-hTHLc6VNZyzzEH/l7PFGjpcTvUgiaPK5mdLkbjrTeWSRcEfxFrv56g/XckIYlE9ckuobsdwqd5mk2g1sBkMewg==} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + wcwidth@1.0.1: resolution: {integrity: sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==} + web-resource-inliner@6.0.1: + resolution: {integrity: sha512-kfqDxt5dTB1JhqsCUQVFDj0rmY+4HLwGQIsLPbyrsN9y9WV/1oFDSx3BQ4GfCv9X+jVeQ7rouTqwK53rA/7t8A==} + engines: {node: '>=10.0.0'} + + webidl-conversions@3.0.1: + resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} + + whatwg-url@5.0.0: + resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} + which@2.0.2: resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} engines: {node: '>= 8'} @@ -2632,12 +4179,46 @@ packages: engines: {node: '>=8'} hasBin: true + wicked-good-xpath@1.3.0: + resolution: {integrity: sha512-Gd9+TUn5nXdwj/hFsPVx5cuHHiF5Bwuc30jZ4+ronF1qHK5O7HD0sgmXWSEgwKquT3ClLoKPVbO6qGwVwLzvAw==} + word-wrap@1.2.5: resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} engines: {node: '>=0.10.0'} - ws@8.17.1: - resolution: {integrity: sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==} + workerd@1.20260312.1: + resolution: {integrity: sha512-nNpPkw9jaqo79B+iBCOiksx+N62xC+ETIfyzofUEdY3cSOHJg6oNnVSHm7vHevzVblfV76c8Gr0cXHEapYMBEg==} + engines: {node: '>=16'} + hasBin: true + + wrangler@4.73.0: + resolution: {integrity: sha512-VJXsqKDFCp6OtFEHXITSOR5kh95JOknwPY8m7RyQuWJQguSybJy43m4vhoCSt42prutTef7eeuw7L4V4xiynGw==} + engines: {node: '>=20.0.0'} + hasBin: true + peerDependencies: + '@cloudflare/workers-types': ^4.20260312.1 + peerDependenciesMeta: + '@cloudflare/workers-types': + optional: true + + wrap-ansi@7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} + + ws@8.18.0: + resolution: {integrity: sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + + ws@8.18.3: + resolution: {integrity: sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==} engines: {node: '>=10.0.0'} peerDependencies: bufferutil: ^4.0.1 @@ -2660,16 +4241,57 @@ packages: utf-8-validate: optional: true + wsl-utils@0.1.0: + resolution: {integrity: sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw==} + engines: {node: '>=18'} + + y18n@5.0.8: + resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} + engines: {node: '>=10'} + yallist@3.1.1: resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + yaml@2.8.2: + resolution: {integrity: sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==} + engines: {node: '>= 14.6'} + hasBin: true + + yargs-parser@21.1.1: + resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} + engines: {node: '>=12'} + + yargs@17.7.2: + resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} + engines: {node: '>=12'} + yocto-queue@0.1.0: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} + youch-core@0.3.3: + resolution: {integrity: sha512-ho7XuGjLaJ2hWHoK8yFnsUGy2Y5uDpqSTq1FkHLK4/oqKtyUU1AFbOOxY4IpC9f0fTLjwYbslUz0Po5BpD1wrA==} + + youch@4.1.0-beta.10: + resolution: {integrity: sha512-rLfVLB4FgQneDr0dv1oddCVZmKjcJ6yX6mS4pU82Mq/Dt9a3cLZQ62pDBL4AUO+uVrCvtWz3ZFUL2HFAFJ/BXQ==} + + zod@3.25.76: + resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==} + + zod@4.3.6: + resolution: {integrity: sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==} + + zwitch@2.0.4: + resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} + snapshots: - '@adraffy/ens-normalize@1.10.1': {} + '@adraffy/ens-normalize@1.11.1': {} + + '@antfu/install-pkg@1.1.0': + dependencies: + package-manager-detector: 1.6.0 + tinyexec: 1.0.2 '@babel/code-frame@7.29.0': dependencies: @@ -2782,6 +4404,44 @@ snapshots: '@babel/helper-string-parser': 7.27.1 '@babel/helper-validator-identifier': 7.28.5 + '@clack/core@0.3.5': + dependencies: + picocolors: 1.1.1 + sisteransi: 1.0.5 + + '@cloudflare/kv-asset-handler@0.4.2': {} + + '@cloudflare/unenv-preset@2.15.0(unenv@2.0.0-rc.24)(workerd@1.20260312.1)': + dependencies: + unenv: 2.0.0-rc.24 + optionalDependencies: + workerd: 1.20260312.1 + + '@cloudflare/workerd-darwin-64@1.20260312.1': + optional: true + + '@cloudflare/workerd-darwin-arm64@1.20260312.1': + optional: true + + '@cloudflare/workerd-linux-64@1.20260312.1': + optional: true + + '@cloudflare/workerd-linux-arm64@1.20260312.1': + optional: true + + '@cloudflare/workerd-windows-64@1.20260312.1': + optional: true + + '@cspotcode/source-map-support@0.8.1': + dependencies: + '@jridgewell/trace-mapping': 0.3.9 + + '@docsearch/css@4.6.0': {} + + '@docsearch/js@4.6.0': {} + + '@docsearch/sidepanel-js@4.6.0': {} + '@emnapi/core@1.8.1': dependencies: '@emnapi/wasi-threads': 1.1.0 @@ -2804,156 +4464,234 @@ snapshots: '@esbuild/aix-ppc64@0.27.2': optional: true + '@esbuild/aix-ppc64@0.27.3': + optional: true + '@esbuild/android-arm64@0.25.12': optional: true '@esbuild/android-arm64@0.27.2': optional: true + '@esbuild/android-arm64@0.27.3': + optional: true + '@esbuild/android-arm@0.25.12': optional: true '@esbuild/android-arm@0.27.2': optional: true + '@esbuild/android-arm@0.27.3': + optional: true + '@esbuild/android-x64@0.25.12': optional: true '@esbuild/android-x64@0.27.2': optional: true + '@esbuild/android-x64@0.27.3': + optional: true + '@esbuild/darwin-arm64@0.25.12': optional: true '@esbuild/darwin-arm64@0.27.2': optional: true - '@esbuild/darwin-x64@0.25.12': + '@esbuild/darwin-arm64@0.27.3': + optional: true + + '@esbuild/darwin-x64@0.25.12': optional: true '@esbuild/darwin-x64@0.27.2': optional: true + '@esbuild/darwin-x64@0.27.3': + optional: true + '@esbuild/freebsd-arm64@0.25.12': optional: true '@esbuild/freebsd-arm64@0.27.2': optional: true + '@esbuild/freebsd-arm64@0.27.3': + optional: true + '@esbuild/freebsd-x64@0.25.12': optional: true '@esbuild/freebsd-x64@0.27.2': optional: true + '@esbuild/freebsd-x64@0.27.3': + optional: true + '@esbuild/linux-arm64@0.25.12': optional: true '@esbuild/linux-arm64@0.27.2': optional: true + '@esbuild/linux-arm64@0.27.3': + optional: true + '@esbuild/linux-arm@0.25.12': optional: true '@esbuild/linux-arm@0.27.2': optional: true + '@esbuild/linux-arm@0.27.3': + optional: true + '@esbuild/linux-ia32@0.25.12': optional: true '@esbuild/linux-ia32@0.27.2': optional: true + '@esbuild/linux-ia32@0.27.3': + optional: true + '@esbuild/linux-loong64@0.25.12': optional: true '@esbuild/linux-loong64@0.27.2': optional: true + '@esbuild/linux-loong64@0.27.3': + optional: true + '@esbuild/linux-mips64el@0.25.12': optional: true '@esbuild/linux-mips64el@0.27.2': optional: true + '@esbuild/linux-mips64el@0.27.3': + optional: true + '@esbuild/linux-ppc64@0.25.12': optional: true '@esbuild/linux-ppc64@0.27.2': optional: true + '@esbuild/linux-ppc64@0.27.3': + optional: true + '@esbuild/linux-riscv64@0.25.12': optional: true '@esbuild/linux-riscv64@0.27.2': optional: true + '@esbuild/linux-riscv64@0.27.3': + optional: true + '@esbuild/linux-s390x@0.25.12': optional: true '@esbuild/linux-s390x@0.27.2': optional: true + '@esbuild/linux-s390x@0.27.3': + optional: true + '@esbuild/linux-x64@0.25.12': optional: true '@esbuild/linux-x64@0.27.2': optional: true + '@esbuild/linux-x64@0.27.3': + optional: true + '@esbuild/netbsd-arm64@0.25.12': optional: true '@esbuild/netbsd-arm64@0.27.2': optional: true + '@esbuild/netbsd-arm64@0.27.3': + optional: true + '@esbuild/netbsd-x64@0.25.12': optional: true '@esbuild/netbsd-x64@0.27.2': optional: true + '@esbuild/netbsd-x64@0.27.3': + optional: true + '@esbuild/openbsd-arm64@0.25.12': optional: true '@esbuild/openbsd-arm64@0.27.2': optional: true + '@esbuild/openbsd-arm64@0.27.3': + optional: true + '@esbuild/openbsd-x64@0.25.12': optional: true '@esbuild/openbsd-x64@0.27.2': optional: true + '@esbuild/openbsd-x64@0.27.3': + optional: true + '@esbuild/openharmony-arm64@0.25.12': optional: true '@esbuild/openharmony-arm64@0.27.2': optional: true + '@esbuild/openharmony-arm64@0.27.3': + optional: true + '@esbuild/sunos-x64@0.25.12': optional: true '@esbuild/sunos-x64@0.27.2': optional: true + '@esbuild/sunos-x64@0.27.3': + optional: true + '@esbuild/win32-arm64@0.25.12': optional: true '@esbuild/win32-arm64@0.27.2': optional: true + '@esbuild/win32-arm64@0.27.3': + optional: true + '@esbuild/win32-ia32@0.25.12': optional: true '@esbuild/win32-ia32@0.27.2': optional: true + '@esbuild/win32-ia32@0.27.3': + optional: true + '@esbuild/win32-x64@0.25.12': optional: true '@esbuild/win32-x64@0.27.2': optional: true + '@esbuild/win32-x64@0.27.3': + optional: true + '@eslint-community/eslint-utils@4.9.1(eslint@9.39.3(jiti@2.6.1))': dependencies: eslint: 9.39.3(jiti@2.6.1) @@ -3011,6 +4749,122 @@ snapshots: '@humanwhocodes/retry@0.4.3': {} + '@iconify-json/logos@1.2.10': + dependencies: + '@iconify/types': 2.0.0 + + '@iconify-json/simple-icons@1.2.73': + dependencies: + '@iconify/types': 2.0.0 + + '@iconify-json/vscode-icons@1.2.45': + dependencies: + '@iconify/types': 2.0.0 + + '@iconify/types@2.0.0': {} + + '@iconify/utils@3.1.0': + dependencies: + '@antfu/install-pkg': 1.1.0 + '@iconify/types': 2.0.0 + mlly: 1.8.1 + + '@img/colour@1.1.0': {} + + '@img/sharp-darwin-arm64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-darwin-arm64': 1.2.4 + optional: true + + '@img/sharp-darwin-x64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-darwin-x64': 1.2.4 + optional: true + + '@img/sharp-libvips-darwin-arm64@1.2.4': + optional: true + + '@img/sharp-libvips-darwin-x64@1.2.4': + optional: true + + '@img/sharp-libvips-linux-arm64@1.2.4': + optional: true + + '@img/sharp-libvips-linux-arm@1.2.4': + optional: true + + '@img/sharp-libvips-linux-ppc64@1.2.4': + optional: true + + '@img/sharp-libvips-linux-riscv64@1.2.4': + optional: true + + '@img/sharp-libvips-linux-s390x@1.2.4': + optional: true + + '@img/sharp-libvips-linux-x64@1.2.4': + optional: true + + '@img/sharp-libvips-linuxmusl-arm64@1.2.4': + optional: true + + '@img/sharp-libvips-linuxmusl-x64@1.2.4': + optional: true + + '@img/sharp-linux-arm64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-arm64': 1.2.4 + optional: true + + '@img/sharp-linux-arm@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-arm': 1.2.4 + optional: true + + '@img/sharp-linux-ppc64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-ppc64': 1.2.4 + optional: true + + '@img/sharp-linux-riscv64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-riscv64': 1.2.4 + optional: true + + '@img/sharp-linux-s390x@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-s390x': 1.2.4 + optional: true + + '@img/sharp-linux-x64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-x64': 1.2.4 + optional: true + + '@img/sharp-linuxmusl-arm64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-arm64': 1.2.4 + optional: true + + '@img/sharp-linuxmusl-x64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-x64': 1.2.4 + optional: true + + '@img/sharp-wasm32@0.34.5': + dependencies: + '@emnapi/runtime': 1.8.1 + optional: true + + '@img/sharp-win32-arm64@0.34.5': + optional: true + + '@img/sharp-win32-ia32@0.34.5': + optional: true + + '@img/sharp-win32-x64@0.34.5': + optional: true + '@isaacs/cliui@9.0.0': {} '@jest/schemas@29.6.3': @@ -3038,6 +4892,12 @@ snapshots: '@jridgewell/resolve-uri@3.1.2': {} + '@jridgewell/source-map@0.3.11': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + optional: true + '@jridgewell/sourcemap-codec@1.5.5': {} '@jridgewell/trace-mapping@0.3.31': @@ -3045,6 +4905,19 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping@0.3.9': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + + '@kwsites/file-exists@1.1.1': + dependencies: + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + '@kwsites/promise-deferred@1.1.1': {} + '@lingui/babel-plugin-extract-messages@5.9.2': {} '@lingui/babel-plugin-lingui-macro@5.9.2(typescript@5.9.3)': @@ -3124,16 +4997,31 @@ snapshots: '@messageformat/parser': 5.1.1 js-sha256: 0.10.1 - '@lingui/vite-plugin@5.9.2(typescript@5.9.3)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.31.1))': + '@lingui/vite-plugin@5.9.2(typescript@5.9.3)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(yaml@2.8.2))': dependencies: '@lingui/cli': 5.9.2(typescript@5.9.3) '@lingui/conf': 5.9.2(typescript@5.9.3) - vite: 7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.31.1) + vite: 7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(yaml@2.8.2) transitivePeerDependencies: - babel-plugin-macros - supports-color - typescript + '@lunariajs/core@0.1.1': + dependencies: + '@clack/core': 0.3.5 + fast-glob: 3.3.3 + get-port: 7.1.0 + jiti: 1.21.7 + micromatch: 4.0.8 + path-to-regexp: 6.3.0 + picocolors: 1.1.1 + simple-git: 3.33.0 + ultramatter: 0.0.4 + zod: 3.25.76 + transitivePeerDependencies: + - supports-color + '@messageformat/parser@5.1.1': dependencies: moo: 0.5.2 @@ -3145,17 +5033,43 @@ snapshots: '@tybys/wasm-util': 0.10.1 optional: true - '@noble/curves@1.2.0': + '@noble/ciphers@1.3.0': {} + + '@noble/curves@1.9.1': dependencies: - '@noble/hashes': 1.3.2 + '@noble/hashes': 1.8.0 - '@noble/hashes@1.3.2': {} + '@noble/hashes@1.8.0': {} '@noble/hashes@2.0.1': {} + '@nodelib/fs.scandir@2.1.5': + dependencies: + '@nodelib/fs.stat': 2.0.5 + run-parallel: 1.2.0 + + '@nodelib/fs.stat@2.0.5': {} + + '@nodelib/fs.walk@1.2.8': + dependencies: + '@nodelib/fs.scandir': 2.1.5 + fastq: 1.20.1 + '@polka/url@1.0.0-next.29': optional: true + '@poppinss/colors@4.1.6': + dependencies: + kleur: 4.1.5 + + '@poppinss/dumper@0.6.5': + dependencies: + '@poppinss/colors': 4.1.6 + '@sindresorhus/is': 7.2.0 + supports-color: 10.2.2 + + '@poppinss/exception@1.2.3': {} + '@rolldown/pluginutils@1.0.0-beta.53': {} '@rollup/plugin-node-resolve@16.0.3(rollup@4.59.0)': @@ -3168,14 +5082,14 @@ snapshots: optionalDependencies: rollup: 4.59.0 - '@rollup/plugin-typescript@12.3.0(rollup@4.59.0)(tslib@2.7.0)(typescript@5.9.3)': + '@rollup/plugin-typescript@12.3.0(rollup@4.59.0)(tslib@2.8.1)(typescript@5.9.3)': dependencies: '@rollup/pluginutils': 5.3.0(rollup@4.59.0) resolve: 1.22.11 typescript: 5.9.3 optionalDependencies: rollup: 4.59.0 - tslib: 2.7.0 + tslib: 2.8.1 '@rollup/pluginutils@5.3.0(rollup@4.59.0)': dependencies: @@ -3326,8 +5240,63 @@ snapshots: '@rollup/rollup-win32-x64-msvc@4.59.0': optional: true + '@scure/base@1.2.6': {} + + '@scure/bip32@1.7.0': + dependencies: + '@noble/curves': 1.9.1 + '@noble/hashes': 1.8.0 + '@scure/base': 1.2.6 + + '@scure/bip39@1.6.0': + dependencies: + '@noble/hashes': 1.8.0 + '@scure/base': 1.2.6 + + '@shikijs/core@3.23.0': + dependencies: + '@shikijs/types': 3.23.0 + '@shikijs/vscode-textmate': 10.0.2 + '@types/hast': 3.0.4 + hast-util-to-html: 9.0.5 + + '@shikijs/engine-javascript@3.23.0': + dependencies: + '@shikijs/types': 3.23.0 + '@shikijs/vscode-textmate': 10.0.2 + oniguruma-to-es: 4.3.4 + + '@shikijs/engine-oniguruma@3.23.0': + dependencies: + '@shikijs/types': 3.23.0 + '@shikijs/vscode-textmate': 10.0.2 + + '@shikijs/langs@3.23.0': + dependencies: + '@shikijs/types': 3.23.0 + + '@shikijs/themes@3.23.0': + dependencies: + '@shikijs/types': 3.23.0 + + '@shikijs/transformers@3.23.0': + dependencies: + '@shikijs/core': 3.23.0 + '@shikijs/types': 3.23.0 + + '@shikijs/types@3.23.0': + dependencies: + '@shikijs/vscode-textmate': 10.0.2 + '@types/hast': 3.0.4 + + '@shikijs/vscode-textmate@10.0.2': {} + '@sinclair/typebox@0.27.10': {} + '@sindresorhus/is@7.2.0': {} + + '@speed-highlight/core@1.2.14': {} + '@standard-schema/spec@1.1.0': {} '@tailwindcss/node@4.2.1': @@ -3391,12 +5360,14 @@ snapshots: '@tailwindcss/oxide-win32-arm64-msvc': 4.2.1 '@tailwindcss/oxide-win32-x64-msvc': 4.2.1 - '@tailwindcss/vite@4.2.1(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.31.1))': + '@tailwindcss/vite@4.2.1(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(yaml@2.8.2))': dependencies: '@tailwindcss/node': 4.2.1 '@tailwindcss/oxide': 4.2.1 tailwindcss: 4.2.1 - vite: 7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.31.1) + vite: 7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(yaml@2.8.2) + + '@tokenizer/token@0.3.0': {} '@tybys/wasm-util@0.10.1': dependencies: @@ -3408,10 +5379,18 @@ snapshots: '@types/deep-eql': 4.0.2 assertion-error: 2.0.1 + '@types/debug@4.1.12': + dependencies: + '@types/ms': 2.1.0 + '@types/deep-eql@4.0.2': {} '@types/estree@1.0.8': {} + '@types/hast@3.0.4': + dependencies: + '@types/unist': 3.0.3 + '@types/istanbul-lib-coverage@2.0.6': {} '@types/istanbul-lib-report@3.0.3': @@ -3428,9 +5407,20 @@ snapshots: dependencies: jszip: 3.10.1 - '@types/node@22.7.5': + '@types/linkify-it@5.0.0': {} + + '@types/markdown-it@14.1.2': dependencies: - undici-types: 6.19.8 + '@types/linkify-it': 5.0.0 + '@types/mdurl': 2.0.0 + + '@types/mdast@4.0.4': + dependencies: + '@types/unist': 3.0.3 + + '@types/mdurl@2.0.0': {} + + '@types/ms@2.1.0': {} '@types/node@24.10.4': dependencies: @@ -3438,6 +5428,8 @@ snapshots: '@types/resolve@1.20.2': {} + '@types/unist@3.0.3': {} + '@types/web-bluetooth@0.0.21': {} '@types/yargs-parser@21.0.3': {} @@ -3537,6 +5529,8 @@ snapshots: '@typescript-eslint/types': 8.56.1 eslint-visitor-keys: 5.0.1 + '@ungap/structured-clone@1.3.0': {} + '@unrs/resolver-binding-android-arm-eabi@1.11.1': optional: true @@ -3596,19 +5590,25 @@ snapshots: '@unrs/resolver-binding-win32-x64-msvc@1.11.1': optional: true - '@vitejs/plugin-vue@6.0.3(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.31.1))(vue@3.5.26(typescript@5.9.3))': + '@vitejs/plugin-vue@6.0.3(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(yaml@2.8.2))(vue@3.5.26(typescript@5.9.3))': dependencies: '@rolldown/pluginutils': 1.0.0-beta.53 - vite: 7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.31.1) + vite: 7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(yaml@2.8.2) vue: 3.5.26(typescript@5.9.3) - '@vitest/browser-playwright@4.0.18(playwright@1.58.2)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.31.1))(vitest@4.0.18)': + '@vitejs/plugin-vue@6.0.3(vite@7.3.1(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(yaml@2.8.2))(vue@3.5.30(typescript@5.9.3))': + dependencies: + '@rolldown/pluginutils': 1.0.0-beta.53 + vite: 7.3.1(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(yaml@2.8.2) + vue: 3.5.30(typescript@5.9.3) + + '@vitest/browser-playwright@4.0.18(playwright@1.58.2)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(yaml@2.8.2))(vitest@4.0.18)': dependencies: - '@vitest/browser': 4.0.18(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.31.1))(vitest@4.0.18) - '@vitest/mocker': 4.0.18(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.31.1)) + '@vitest/browser': 4.0.18(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(yaml@2.8.2))(vitest@4.0.18) + '@vitest/mocker': 4.0.18(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(yaml@2.8.2)) playwright: 1.58.2 tinyrainbow: 3.0.3 - vitest: 4.0.18(@types/node@24.10.4)(@vitest/browser-playwright@4.0.18)(jiti@2.6.1)(lightningcss@1.31.1) + vitest: 4.0.18(@types/node@24.10.4)(@vitest/browser-playwright@4.0.18)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(yaml@2.8.2) transitivePeerDependencies: - bufferutil - msw @@ -3616,16 +5616,16 @@ snapshots: - vite optional: true - '@vitest/browser@4.0.18(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.31.1))(vitest@4.0.18)': + '@vitest/browser@4.0.18(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(yaml@2.8.2))(vitest@4.0.18)': dependencies: - '@vitest/mocker': 4.0.18(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.31.1)) + '@vitest/mocker': 4.0.18(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(yaml@2.8.2)) '@vitest/utils': 4.0.18 magic-string: 0.30.21 pixelmatch: 7.1.0 pngjs: 7.0.0 sirv: 3.0.2 tinyrainbow: 3.0.3 - vitest: 4.0.18(@types/node@24.10.4)(@vitest/browser-playwright@4.0.18)(jiti@2.6.1)(lightningcss@1.31.1) + vitest: 4.0.18(@types/node@24.10.4)(@vitest/browser-playwright@4.0.18)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(yaml@2.8.2) ws: 8.19.0 transitivePeerDependencies: - bufferutil @@ -3643,13 +5643,13 @@ snapshots: chai: 6.2.2 tinyrainbow: 3.0.3 - '@vitest/mocker@4.0.18(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.31.1))': + '@vitest/mocker@4.0.18(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(yaml@2.8.2))': dependencies: '@vitest/spy': 4.0.18 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - vite: 7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.31.1) + vite: 7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(yaml@2.8.2) '@vitest/pretty-format@4.0.18': dependencies: @@ -3693,11 +5693,24 @@ snapshots: estree-walker: 2.0.2 source-map-js: 1.2.1 + '@vue/compiler-core@3.5.30': + dependencies: + '@babel/parser': 7.29.0 + '@vue/shared': 3.5.30 + entities: 7.0.1 + estree-walker: 2.0.2 + source-map-js: 1.2.1 + '@vue/compiler-dom@3.5.26': dependencies: '@vue/compiler-core': 3.5.26 '@vue/shared': 3.5.26 + '@vue/compiler-dom@3.5.30': + dependencies: + '@vue/compiler-core': 3.5.30 + '@vue/shared': 3.5.30 + '@vue/compiler-sfc@3.5.26': dependencies: '@babel/parser': 7.28.5 @@ -3710,15 +5723,36 @@ snapshots: postcss: 8.5.6 source-map-js: 1.2.1 + '@vue/compiler-sfc@3.5.30': + dependencies: + '@babel/parser': 7.29.0 + '@vue/compiler-core': 3.5.30 + '@vue/compiler-dom': 3.5.30 + '@vue/compiler-ssr': 3.5.30 + '@vue/shared': 3.5.30 + estree-walker: 2.0.2 + magic-string: 0.30.21 + postcss: 8.5.8 + source-map-js: 1.2.1 + '@vue/compiler-ssr@3.5.26': dependencies: '@vue/compiler-dom': 3.5.26 '@vue/shared': 3.5.26 + '@vue/compiler-ssr@3.5.30': + dependencies: + '@vue/compiler-dom': 3.5.30 + '@vue/shared': 3.5.30 + '@vue/devtools-api@7.7.9': dependencies: '@vue/devtools-kit': 7.7.9 + '@vue/devtools-api@8.1.0': + dependencies: + '@vue/devtools-kit': 8.1.0 + '@vue/devtools-kit@7.7.9': dependencies: '@vue/devtools-shared': 7.7.9 @@ -3729,10 +5763,19 @@ snapshots: speakingurl: 14.0.1 superjson: 2.2.6 + '@vue/devtools-kit@8.1.0': + dependencies: + '@vue/devtools-shared': 8.1.0 + birpc: 2.9.0 + hookable: 5.5.3 + perfect-debounce: 2.1.0 + '@vue/devtools-shared@7.7.9': dependencies: rfdc: 1.4.1 + '@vue/devtools-shared@8.1.0': {} + '@vue/language-core@3.2.1': dependencies: '@volar/language-core': 2.4.27 @@ -3747,11 +5790,20 @@ snapshots: dependencies: '@vue/shared': 3.5.26 + '@vue/reactivity@3.5.30': + dependencies: + '@vue/shared': 3.5.30 + '@vue/runtime-core@3.5.26': dependencies: '@vue/reactivity': 3.5.26 '@vue/shared': 3.5.26 + '@vue/runtime-core@3.5.30': + dependencies: + '@vue/reactivity': 3.5.30 + '@vue/shared': 3.5.30 + '@vue/runtime-dom@3.5.26': dependencies: '@vue/reactivity': 3.5.26 @@ -3759,14 +5811,29 @@ snapshots: '@vue/shared': 3.5.26 csstype: 3.2.3 + '@vue/runtime-dom@3.5.30': + dependencies: + '@vue/reactivity': 3.5.30 + '@vue/runtime-core': 3.5.30 + '@vue/shared': 3.5.30 + csstype: 3.2.3 + '@vue/server-renderer@3.5.26(vue@3.5.26(typescript@5.9.3))': dependencies: '@vue/compiler-ssr': 3.5.26 '@vue/shared': 3.5.26 vue: 3.5.26(typescript@5.9.3) + '@vue/server-renderer@3.5.30(vue@3.5.30(typescript@5.9.3))': + dependencies: + '@vue/compiler-ssr': 3.5.30 + '@vue/shared': 3.5.30 + vue: 3.5.30(typescript@5.9.3) + '@vue/shared@3.5.26': {} + '@vue/shared@3.5.30': {} + '@vue/tsconfig@0.8.1(typescript@5.9.3)(vue@3.5.26(typescript@5.9.3))': optionalDependencies: typescript: 5.9.3 @@ -3779,20 +5846,76 @@ snapshots: '@vueuse/shared': 14.2.1(vue@3.5.26(typescript@5.9.3)) vue: 3.5.26(typescript@5.9.3) + '@vueuse/core@14.2.1(vue@3.5.30(typescript@5.9.3))': + dependencies: + '@types/web-bluetooth': 0.0.21 + '@vueuse/metadata': 14.2.1 + '@vueuse/shared': 14.2.1(vue@3.5.30(typescript@5.9.3)) + vue: 3.5.30(typescript@5.9.3) + + '@vueuse/integrations@14.2.1(change-case@5.4.4)(focus-trap@7.8.0)(fuse.js@7.1.0)(vue@3.5.30(typescript@5.9.3))': + dependencies: + '@vueuse/core': 14.2.1(vue@3.5.30(typescript@5.9.3)) + '@vueuse/shared': 14.2.1(vue@3.5.30(typescript@5.9.3)) + vue: 3.5.30(typescript@5.9.3) + optionalDependencies: + change-case: 5.4.4 + focus-trap: 7.8.0 + fuse.js: 7.1.0 + '@vueuse/metadata@14.2.1': {} '@vueuse/shared@14.2.1(vue@3.5.26(typescript@5.9.3))': dependencies: vue: 3.5.26(typescript@5.9.3) + '@vueuse/shared@14.2.1(vue@3.5.30(typescript@5.9.3))': + dependencies: + vue: 3.5.30(typescript@5.9.3) + + '@wagmi/cli@2.10.0(typescript@5.9.3)': + dependencies: + abitype: 1.2.3(typescript@5.9.3)(zod@4.3.6) + bundle-require: 5.1.0(esbuild@0.25.12) + cac: 6.7.14 + change-case: 5.4.4 + chokidar: 4.0.1 + dedent: 0.7.0 + dotenv: 16.6.1 + dotenv-expand: 10.0.0 + esbuild: 0.25.12 + escalade: 3.2.0 + fdir: 6.5.0(picomatch@3.0.1) + nanospinner: 1.2.2 + pathe: 1.1.2 + picocolors: 1.1.1 + picomatch: 3.0.1 + prettier: 3.8.1 + viem: 2.47.0(typescript@5.9.3)(zod@4.3.6) + zod: 4.3.6 + optionalDependencies: + typescript: 5.9.3 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + + '@xmldom/xmldom@0.9.8': {} + + abitype@1.2.3(typescript@5.9.3)(zod@4.3.6): + optionalDependencies: + typescript: 5.9.3 + zod: 4.3.6 + + abort-controller@3.0.0: + dependencies: + event-target-shim: 5.0.1 + acorn-jsx@5.3.2(acorn@8.16.0): dependencies: acorn: 8.16.0 acorn@8.16.0: {} - aes-js@4.0.0-beta.5: {} - ajv@6.14.0: dependencies: fast-deep-equal: 3.1.3 @@ -3802,6 +5925,8 @@ snapshots: alien-signals@3.1.2: {} + ansi-colors@4.1.3: {} + ansi-regex@5.0.1: {} ansi-styles@4.3.0: @@ -3815,10 +5940,16 @@ snapshots: normalize-path: 3.0.0 picomatch: 2.3.1 + argparse@1.0.10: + dependencies: + sprintf-js: 1.0.3 + argparse@2.0.1: {} assertion-error@2.0.1: {} + bail@2.0.2: {} + balanced-match@1.0.2: {} balanced-match@4.0.4: {} @@ -3837,6 +5968,10 @@ snapshots: inherits: 2.0.4 readable-stream: 3.6.2 + blake3-wasm@2.1.5: {} + + boolbase@1.0.0: {} + brace-expansion@1.1.12: dependencies: balanced-match: 1.0.2 @@ -3858,19 +5993,40 @@ snapshots: node-releases: 2.0.27 update-browserslist-db: 1.2.3(browserslist@4.28.1) + buffer-from@1.1.2: + optional: true + buffer@5.7.1: dependencies: base64-js: 1.5.1 ieee754: 1.2.1 - builtin-modules@5.0.0: {} - - callsites@3.1.0: {} + buffer@6.0.3: + dependencies: + base64-js: 1.5.1 + ieee754: 1.2.1 + + builtin-modules@5.0.0: {} + + bundle-name@4.1.0: + dependencies: + run-applescript: 7.1.0 + + bundle-require@5.1.0(esbuild@0.25.12): + dependencies: + esbuild: 0.25.12 + load-tsconfig: 0.2.5 + + cac@6.7.14: {} + + callsites@3.1.0: {} camelcase@6.3.0: {} caniuse-lite@1.0.30001774: {} + ccount@2.0.1: {} + chai@6.2.2: {} chalk@4.1.2: @@ -3880,6 +6036,30 @@ snapshots: change-case@5.4.4: {} + character-entities-html4@2.1.0: {} + + character-entities-legacy@3.0.0: {} + + character-entities@2.0.2: {} + + cheerio-select@1.6.0: + dependencies: + css-select: 4.3.0 + css-what: 6.2.2 + domelementtype: 2.3.0 + domhandler: 4.3.1 + domutils: 2.8.0 + + cheerio@1.0.0-rc.10: + dependencies: + cheerio-select: 1.6.0 + dom-serializer: 1.4.1 + domhandler: 4.3.1 + htmlparser2: 6.1.0 + parse5: 6.0.1 + parse5-htmlparser2-tree-adapter: 6.0.1 + tslib: 2.8.1 + chokidar@3.5.1: dependencies: anymatch: 3.1.3 @@ -3892,6 +6072,10 @@ snapshots: optionalDependencies: fsevents: 2.3.3 + chokidar@4.0.1: + dependencies: + readdirp: 4.1.2 + ci-info@4.4.0: {} clean-regexp@1.0.0: @@ -3908,6 +6092,12 @@ snapshots: dependencies: colors: 1.0.3 + cliui@8.0.1: + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 7.0.0 + clone@1.0.4: {} color-convert@2.0.1: @@ -3918,14 +6108,27 @@ snapshots: colors@1.0.3: {} + comma-separated-tokens@2.0.3: {} + commander@10.0.1: {} + commander@13.1.0: {} + + commander@2.20.3: + optional: true + + commander@6.2.1: {} + comment-parser@1.4.5: {} concat-map@0.0.1: {} + confbox@0.1.8: {} + convert-source-map@2.0.0: {} + cookie@1.1.1: {} + copy-anything@4.0.5: dependencies: is-what: 5.5.0 @@ -3951,6 +6154,20 @@ snapshots: shebang-command: 2.0.0 which: 2.0.2 + crypto-random-string@4.0.0: + dependencies: + type-fest: 1.4.0 + + css-select@4.3.0: + dependencies: + boolbase: 1.0.0 + css-what: 6.2.2 + domhandler: 4.3.1 + domutils: 2.8.0 + nth-check: 2.1.1 + + css-what@6.2.2: {} + csstype@3.2.3: {} date-fns@3.6.0: {} @@ -3961,29 +6178,86 @@ snapshots: dependencies: ms: 2.1.3 + decode-named-character-reference@1.3.0: + dependencies: + character-entities: 2.0.2 + + dedent@0.7.0: {} + deep-is@0.1.4: {} deepmerge@4.3.1: {} + default-browser-id@5.0.1: {} + + default-browser@5.5.0: + dependencies: + bundle-name: 4.1.0 + default-browser-id: 5.0.1 + defaults@1.0.4: dependencies: clone: 1.0.4 + define-lazy-prop@3.0.0: {} + + dequal@2.0.3: {} + detect-libc@2.1.2: {} + devlop@1.1.0: + dependencies: + dequal: 2.0.3 + + dom-serializer@1.4.1: + dependencies: + domelementtype: 2.3.0 + domhandler: 4.3.1 + entities: 2.2.0 + + domelementtype@2.3.0: {} + + domhandler@3.3.0: + dependencies: + domelementtype: 2.3.0 + + domhandler@4.3.1: + dependencies: + domelementtype: 2.3.0 + + domutils@2.8.0: + dependencies: + dom-serializer: 1.4.1 + domelementtype: 2.3.0 + domhandler: 4.3.1 + + dotenv-expand@10.0.0: {} + + dotenv@16.6.1: {} + electron-to-chromium@1.5.302: {} + emoji-regex@8.0.0: {} + enhanced-resolve@5.19.0: dependencies: graceful-fs: 4.2.11 tapable: 2.3.0 + entities@2.2.0: {} + + entities@4.5.0: {} + entities@7.0.0: {} + entities@7.0.1: {} + error-ex@1.3.4: dependencies: is-arrayish: 0.2.1 + error-stack-parser-es@1.0.5: {} + es-module-lexer@1.7.0: {} esbuild@0.25.12: @@ -4044,12 +6318,45 @@ snapshots: '@esbuild/win32-ia32': 0.27.2 '@esbuild/win32-x64': 0.27.2 + esbuild@0.27.3: + optionalDependencies: + '@esbuild/aix-ppc64': 0.27.3 + '@esbuild/android-arm': 0.27.3 + '@esbuild/android-arm64': 0.27.3 + '@esbuild/android-x64': 0.27.3 + '@esbuild/darwin-arm64': 0.27.3 + '@esbuild/darwin-x64': 0.27.3 + '@esbuild/freebsd-arm64': 0.27.3 + '@esbuild/freebsd-x64': 0.27.3 + '@esbuild/linux-arm': 0.27.3 + '@esbuild/linux-arm64': 0.27.3 + '@esbuild/linux-ia32': 0.27.3 + '@esbuild/linux-loong64': 0.27.3 + '@esbuild/linux-mips64el': 0.27.3 + '@esbuild/linux-ppc64': 0.27.3 + '@esbuild/linux-riscv64': 0.27.3 + '@esbuild/linux-s390x': 0.27.3 + '@esbuild/linux-x64': 0.27.3 + '@esbuild/netbsd-arm64': 0.27.3 + '@esbuild/netbsd-x64': 0.27.3 + '@esbuild/openbsd-arm64': 0.27.3 + '@esbuild/openbsd-x64': 0.27.3 + '@esbuild/openharmony-arm64': 0.27.3 + '@esbuild/sunos-x64': 0.27.3 + '@esbuild/win32-arm64': 0.27.3 + '@esbuild/win32-ia32': 0.27.3 + '@esbuild/win32-x64': 0.27.3 + escalade@3.2.0: {} + escape-goat@3.0.0: {} + escape-string-regexp@1.0.5: {} escape-string-regexp@4.0.0: {} + escape-string-regexp@5.0.0: {} + eslint-import-context@0.1.9(unrs-resolver@1.11.1): dependencies: get-tsconfig: 4.13.6 @@ -4167,8 +6474,7 @@ snapshots: transitivePeerDependencies: - supports-color - esm@3.2.25: - optional: true + esm@3.2.25: {} espree@10.4.0: dependencies: @@ -4176,6 +6482,8 @@ snapshots: acorn-jsx: 5.3.2(acorn@8.16.0) eslint-visitor-keys: 4.2.1 + esprima@4.0.1: {} + esquery@1.7.0: dependencies: estraverse: 5.3.0 @@ -4194,27 +6502,46 @@ snapshots: esutils@2.0.3: {} - ethers@6.16.0: - dependencies: - '@adraffy/ens-normalize': 1.10.1 - '@noble/curves': 1.2.0 - '@noble/hashes': 1.3.2 - '@types/node': 22.7.5 - aes-js: 4.0.0-beta.5 - tslib: 2.7.0 - ws: 8.17.1 - transitivePeerDependencies: - - bufferutil - - utf-8-validate + event-target-shim@5.0.1: {} + + eventemitter3@5.0.1: {} + + events@3.3.0: {} expect-type@1.3.0: {} + extend-shallow@2.0.1: + dependencies: + is-extendable: 0.1.1 + + extend@3.0.2: {} + fast-deep-equal@3.1.3: {} + fast-glob@3.3.3: + dependencies: + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.8 + fast-json-stable-stringify@2.1.0: {} fast-levenshtein@2.0.6: {} + fastq@1.20.1: + dependencies: + reusify: 1.1.0 + + fault@2.0.1: + dependencies: + format: 0.2.2 + + fdir@6.5.0(picomatch@3.0.1): + optionalDependencies: + picomatch: 3.0.1 + fdir@6.5.0(picomatch@4.0.3): optionalDependencies: picomatch: 4.0.3 @@ -4223,6 +6550,12 @@ snapshots: dependencies: flat-cache: 4.0.1 + file-type@18.7.0: + dependencies: + readable-web-to-node-stream: 3.0.4 + strtok3: 7.1.1 + token-types: 5.0.1 + fill-range@7.1.1: dependencies: to-regex-range: 5.0.1 @@ -4241,11 +6574,17 @@ snapshots: flatted@3.3.3: {} + focus-trap@7.8.0: + dependencies: + tabbable: 6.4.0 + foreground-child@3.3.1: dependencies: cross-spawn: 7.0.6 signal-exit: 4.1.0 + format@0.2.2: {} + fsevents@2.3.2: optional: true @@ -4254,8 +6593,17 @@ snapshots: function-bind@1.1.2: {} + fuse.js@7.1.0: + optional: true + gensync@1.0.0-beta.2: {} + get-caller-file@2.0.5: {} + + get-port@7.1.0: {} + + get-stdin@9.0.0: {} + get-tsconfig@4.13.6: dependencies: resolve-pkg-maps: 1.0.0 @@ -4289,14 +6637,55 @@ snapshots: graceful-fs@4.2.11: {} + gray-matter@4.0.3: + dependencies: + js-yaml: 3.14.2 + kind-of: 6.0.3 + section-matter: 1.0.0 + strip-bom-string: 1.0.0 + has-flag@4.0.0: {} hasown@2.0.2: dependencies: function-bind: 1.1.2 + hast-util-to-html@9.0.5: + dependencies: + '@types/hast': 3.0.4 + '@types/unist': 3.0.3 + ccount: 2.0.1 + comma-separated-tokens: 2.0.3 + hast-util-whitespace: 3.0.0 + html-void-elements: 3.0.0 + mdast-util-to-hast: 13.2.1 + property-information: 7.1.0 + space-separated-tokens: 2.0.2 + stringify-entities: 4.0.4 + zwitch: 2.0.4 + + hast-util-whitespace@3.0.0: + dependencies: + '@types/hast': 3.0.4 + hookable@5.5.3: {} + html-void-elements@3.0.0: {} + + htmlparser2@5.0.1: + dependencies: + domelementtype: 2.3.0 + domhandler: 3.3.0 + domutils: 2.8.0 + entities: 2.2.0 + + htmlparser2@6.1.0: + dependencies: + domelementtype: 2.3.0 + domhandler: 4.3.1 + domutils: 2.8.0 + entities: 2.2.0 + ieee754@1.2.1: {} ignore@5.3.2: {} @@ -4334,12 +6723,22 @@ snapshots: dependencies: hasown: 2.0.2 + is-docker@3.0.0: {} + + is-extendable@0.1.1: {} + is-extglob@2.1.1: {} + is-fullwidth-code-point@3.0.0: {} + is-glob@4.0.3: dependencies: is-extglob: 2.1.1 + is-inside-container@1.0.0: + dependencies: + is-docker: 3.0.0 + is-interactive@1.0.0: {} is-module@1.0.0: {} @@ -4348,14 +6747,26 @@ snapshots: is-observable@2.1.0: {} + is-plain-obj@4.1.0: {} + + is-stream@3.0.0: {} + is-unicode-supported@0.1.0: {} is-what@5.5.0: {} + is-wsl@3.1.1: + dependencies: + is-inside-container: 1.0.0 + isarray@1.0.0: {} isexe@2.0.0: {} + isows@1.0.7(ws@8.18.3): + dependencies: + ws: 8.18.3 + jackspeak@4.2.3: dependencies: '@isaacs/cliui': 9.0.0 @@ -4371,12 +6782,19 @@ snapshots: leven: 3.1.0 pretty-format: 29.7.0 + jiti@1.21.7: {} + jiti@2.6.1: {} js-sha256@0.10.1: {} js-tokens@4.0.0: {} + js-yaml@3.14.2: + dependencies: + argparse: 1.0.10 + esprima: 4.0.1 + js-yaml@4.1.1: dependencies: argparse: 2.0.1 @@ -4400,10 +6818,24 @@ snapshots: readable-stream: 2.3.8 setimmediate: 1.0.5 + juice@8.1.0: + dependencies: + cheerio: 1.0.0-rc.10 + commander: 6.2.1 + mensch: 0.3.4 + slick: 1.12.2 + web-resource-inliner: 6.0.1 + transitivePeerDependencies: + - encoding + keyv@4.5.4: dependencies: json-buffer: 3.0.1 + kind-of@6.0.3: {} + + kleur@4.1.5: {} + leven@3.1.0: {} levn@0.4.1: @@ -4466,6 +6898,12 @@ snapshots: lines-and-columns@1.2.4: {} + linkify-it@5.0.0: + dependencies: + uc.micro: 2.1.0 + + load-tsconfig@0.2.5: {} + locate-path@6.0.0: dependencies: p-locate: 5.0.0 @@ -4477,6 +6915,8 @@ snapshots: chalk: 4.1.2 is-unicode-supported: 0.1.0 + longest-streak@3.1.0: {} + lru-cache@11.2.6: {} lru-cache@5.1.1: @@ -4491,25 +6931,296 @@ snapshots: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 + mark.js@8.11.1: {} + + markdown-it-mathjax3@4.3.2: + dependencies: + juice: 8.1.0 + mathjax-full: 3.2.2 + transitivePeerDependencies: + - encoding + + markdown-it@14.1.1: + dependencies: + argparse: 2.0.1 + entities: 4.5.0 + linkify-it: 5.0.0 + mdurl: 2.0.0 + punycode.js: 2.3.1 + uc.micro: 2.1.0 + + markdown-title@1.0.2: {} + + mathjax-full@3.2.2: + dependencies: + esm: 3.2.25 + mhchemparser: 4.2.1 + mj-context-menu: 0.6.1 + speech-rule-engine: 4.1.2 + + mdast-util-from-markdown@2.0.3: + dependencies: + '@types/mdast': 4.0.4 + '@types/unist': 3.0.3 + decode-named-character-reference: 1.3.0 + devlop: 1.1.0 + mdast-util-to-string: 4.0.0 + micromark: 4.0.2 + micromark-util-decode-numeric-character-reference: 2.0.2 + micromark-util-decode-string: 2.0.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + unist-util-stringify-position: 4.0.0 + transitivePeerDependencies: + - supports-color + + mdast-util-frontmatter@2.0.1: + dependencies: + '@types/mdast': 4.0.4 + devlop: 1.1.0 + escape-string-regexp: 5.0.0 + mdast-util-from-markdown: 2.0.3 + mdast-util-to-markdown: 2.1.2 + micromark-extension-frontmatter: 2.0.0 + transitivePeerDependencies: + - supports-color + + mdast-util-phrasing@4.1.0: + dependencies: + '@types/mdast': 4.0.4 + unist-util-is: 6.0.1 + + mdast-util-to-hast@13.2.1: + dependencies: + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + '@ungap/structured-clone': 1.3.0 + devlop: 1.1.0 + micromark-util-sanitize-uri: 2.0.1 + trim-lines: 3.0.1 + unist-util-position: 5.0.0 + unist-util-visit: 5.1.0 + vfile: 6.0.3 + + mdast-util-to-markdown@2.1.2: + dependencies: + '@types/mdast': 4.0.4 + '@types/unist': 3.0.3 + longest-streak: 3.1.0 + mdast-util-phrasing: 4.1.0 + mdast-util-to-string: 4.0.0 + micromark-util-classify-character: 2.0.1 + micromark-util-decode-string: 2.0.1 + unist-util-visit: 5.1.0 + zwitch: 2.0.4 + + mdast-util-to-string@4.0.0: + dependencies: + '@types/mdast': 4.0.4 + + mdurl@2.0.0: {} + + mensch@0.3.4: {} + + meow@12.1.1: {} + + merge2@1.4.1: {} + + mhchemparser@4.2.1: {} + + micromark-core-commonmark@2.0.3: + dependencies: + decode-named-character-reference: 1.3.0 + devlop: 1.1.0 + micromark-factory-destination: 2.0.1 + micromark-factory-label: 2.0.1 + micromark-factory-space: 2.0.1 + micromark-factory-title: 2.0.1 + micromark-factory-whitespace: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-chunked: 2.0.1 + micromark-util-classify-character: 2.0.1 + micromark-util-html-tag-name: 2.0.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-resolve-all: 2.0.1 + micromark-util-subtokenize: 2.1.0 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-frontmatter@2.0.0: + dependencies: + fault: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-factory-destination@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-factory-label@2.0.1: + dependencies: + devlop: 1.1.0 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-factory-space@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-types: 2.0.2 + + micromark-factory-title@2.0.1: + dependencies: + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-factory-whitespace@2.0.1: + dependencies: + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-character@2.1.1: + dependencies: + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-chunked@2.0.1: + dependencies: + micromark-util-symbol: 2.0.1 + + micromark-util-classify-character@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-combine-extensions@2.0.1: + dependencies: + micromark-util-chunked: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-decode-numeric-character-reference@2.0.2: + dependencies: + micromark-util-symbol: 2.0.1 + + micromark-util-decode-string@2.0.1: + dependencies: + decode-named-character-reference: 1.3.0 + micromark-util-character: 2.1.1 + micromark-util-decode-numeric-character-reference: 2.0.2 + micromark-util-symbol: 2.0.1 + + micromark-util-encode@2.0.1: {} + + micromark-util-html-tag-name@2.0.1: {} + + micromark-util-normalize-identifier@2.0.1: + dependencies: + micromark-util-symbol: 2.0.1 + + micromark-util-resolve-all@2.0.1: + dependencies: + micromark-util-types: 2.0.2 + + micromark-util-sanitize-uri@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-encode: 2.0.1 + micromark-util-symbol: 2.0.1 + + micromark-util-subtokenize@2.1.0: + dependencies: + devlop: 1.1.0 + micromark-util-chunked: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-symbol@2.0.1: {} + + micromark-util-types@2.0.2: {} + + micromark@4.0.2: + dependencies: + '@types/debug': 4.1.12 + debug: 4.4.3 + decode-named-character-reference: 1.3.0 + devlop: 1.1.0 + micromark-core-commonmark: 2.0.3 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-chunked: 2.0.1 + micromark-util-combine-extensions: 2.0.1 + micromark-util-decode-numeric-character-reference: 2.0.2 + micromark-util-encode: 2.0.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-resolve-all: 2.0.1 + micromark-util-sanitize-uri: 2.0.1 + micromark-util-subtokenize: 2.1.0 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + transitivePeerDependencies: + - supports-color + micromatch@4.0.8: dependencies: braces: 3.0.3 picomatch: 2.3.1 + millify@6.1.0: + dependencies: + yargs: 17.7.2 + + mime@2.6.0: {} + mimic-fn@2.1.0: {} + miniflare@4.20260312.0: + dependencies: + '@cspotcode/source-map-support': 0.8.1 + sharp: 0.34.5 + undici: 7.18.2 + workerd: 1.20260312.1 + ws: 8.18.0 + youch: 4.1.0-beta.10 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + minimatch@10.2.2: dependencies: brace-expansion: 5.0.3 + minimatch@10.2.4: + dependencies: + brace-expansion: 5.0.3 + minimatch@3.1.4: dependencies: brace-expansion: 1.1.12 minipass@7.1.3: {} + minisearch@7.2.0: {} + mitt@3.0.1: {} + mj-context-menu@0.6.1: {} + + mlly@1.8.1: + dependencies: + acorn: 8.16.0 + pathe: 2.0.3 + pkg-types: 1.3.1 + ufo: 1.6.3 + moo@0.5.2: {} mrmime@2.0.1: @@ -4521,14 +7232,26 @@ snapshots: nanoid@3.3.11: {} + nanospinner@1.2.2: + dependencies: + picocolors: 1.1.1 + napi-postinstall@0.3.4: {} natural-compare@1.4.0: {} + node-fetch@2.7.0: + dependencies: + whatwg-url: 5.0.0 + node-releases@2.0.27: {} normalize-path@3.0.0: {} + nth-check@2.1.1: + dependencies: + boolbase: 1.0.0 + observable-fns@0.6.1: {} obug@2.1.1: {} @@ -4537,6 +7260,29 @@ snapshots: dependencies: mimic-fn: 2.1.0 + oniguruma-parser@0.12.1: {} + + oniguruma-to-es@4.3.4: + dependencies: + oniguruma-parser: 0.12.1 + regex: 6.1.0 + regex-recursion: 6.0.2 + + open-cli@8.0.0: + dependencies: + file-type: 18.7.0 + get-stdin: 9.0.0 + meow: 12.1.1 + open: 10.2.0 + tempy: 3.2.0 + + open@10.2.0: + dependencies: + default-browser: 5.5.0 + define-lazy-prop: 3.0.0 + is-inside-container: 1.0.0 + wsl-utils: 0.1.0 + optionator@0.9.4: dependencies: deep-is: 0.1.4 @@ -4558,6 +7304,21 @@ snapshots: strip-ansi: 6.0.1 wcwidth: 1.0.1 + ox@0.14.0(typescript@5.9.3)(zod@4.3.6): + dependencies: + '@adraffy/ens-normalize': 1.11.1 + '@noble/ciphers': 1.3.0 + '@noble/curves': 1.9.1 + '@noble/hashes': 1.8.0 + '@scure/bip32': 1.7.0 + '@scure/bip39': 1.6.0 + abitype: 1.2.3(typescript@5.9.3)(zod@4.3.6) + eventemitter3: 5.0.1 + optionalDependencies: + typescript: 5.9.3 + transitivePeerDependencies: + - zod + p-limit@3.1.0: dependencies: yocto-queue: 0.1.0 @@ -4568,6 +7329,8 @@ snapshots: package-json-from-dist@1.0.1: {} + package-manager-detector@1.6.0: {} + pako@1.0.11: {} parent-module@1.0.1: @@ -4581,6 +7344,12 @@ snapshots: json-parse-even-better-errors: 2.3.1 lines-and-columns: 1.2.4 + parse5-htmlparser2-tree-adapter@6.0.1: + dependencies: + parse5: 6.0.1 + + parse5@6.0.1: {} + path-browserify@1.0.1: {} path-exists@4.0.0: {} @@ -4594,16 +7363,26 @@ snapshots: lru-cache: 11.2.6 minipass: 7.1.3 + path-to-regexp@6.3.0: {} + path-type@4.0.0: {} + pathe@1.1.2: {} + pathe@2.0.3: {} + peek-readable@5.4.2: {} + perfect-debounce@1.0.0: {} + perfect-debounce@2.1.0: {} + picocolors@1.1.1: {} picomatch@2.3.1: {} + picomatch@3.0.1: {} + picomatch@4.0.3: {} pinia@3.0.4(typescript@5.9.3)(vue@3.5.26(typescript@5.9.3)): @@ -4618,6 +7397,12 @@ snapshots: pngjs: 7.0.0 optional: true + pkg-types@1.3.1: + dependencies: + confbox: 0.1.8 + mlly: 1.8.1 + pathe: 2.0.3 + playwright-core@1.58.2: optional: true @@ -4637,16 +7422,29 @@ snapshots: pofile@1.1.4: {} + postcss-rtlcss@5.7.1(postcss@8.5.8): + dependencies: + postcss: 8.5.8 + rtlcss: 4.3.0 + postcss@8.5.6: dependencies: nanoid: 3.3.11 picocolors: 1.1.1 source-map-js: 1.2.1 + postcss@8.5.8: + dependencies: + nanoid: 3.3.11 + picocolors: 1.1.1 + source-map-js: 1.2.1 + prelude-ls@1.2.1: {} prettier@3.8.1: {} + pretty-bytes@7.1.0: {} + pretty-format@29.7.0: dependencies: '@jest/schemas': 29.6.3 @@ -4655,12 +7453,20 @@ snapshots: process-nextick-args@2.0.1: {} + process@0.11.10: {} + + property-information@7.1.0: {} + pseudolocale@2.2.0: dependencies: commander: 10.0.1 + punycode.js@2.3.1: {} + punycode@2.3.1: {} + queue-microtask@1.2.3: {} + react-is@18.3.1: {} readable-stream@2.3.8: @@ -4679,16 +7485,75 @@ snapshots: string_decoder: 1.1.1 util-deprecate: 1.0.2 + readable-stream@4.7.0: + dependencies: + abort-controller: 3.0.0 + buffer: 6.0.3 + events: 3.3.0 + process: 0.11.10 + string_decoder: 1.3.0 + + readable-web-to-node-stream@3.0.4: + dependencies: + readable-stream: 4.7.0 + readdirp@3.5.0: dependencies: picomatch: 2.3.1 + readdirp@4.1.2: {} + + regex-recursion@6.0.2: + dependencies: + regex-utilities: 2.3.0 + + regex-utilities@2.3.0: {} + + regex@6.1.0: + dependencies: + regex-utilities: 2.3.0 + regexp-tree@0.1.27: {} regjsparser@0.13.0: dependencies: jsesc: 3.1.0 + remark-frontmatter@5.0.0: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-frontmatter: 2.0.1 + micromark-extension-frontmatter: 2.0.0 + unified: 11.0.5 + transitivePeerDependencies: + - supports-color + + remark-parse@11.0.0: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-from-markdown: 2.0.3 + micromark-util-types: 2.0.2 + unified: 11.0.5 + transitivePeerDependencies: + - supports-color + + remark-stringify@11.0.0: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-to-markdown: 2.1.2 + unified: 11.0.5 + + remark@15.0.1: + dependencies: + '@types/mdast': 4.0.4 + remark-parse: 11.0.0 + remark-stringify: 11.0.0 + unified: 11.0.5 + transitivePeerDependencies: + - supports-color + + require-directory@2.1.1: {} + resolve-from@4.0.0: {} resolve-pkg-maps@1.0.0: {} @@ -4704,6 +7569,8 @@ snapshots: onetime: 5.1.2 signal-exit: 3.0.7 + reusify@1.1.0: {} + rfdc@1.4.1: {} rimraf@6.1.3: @@ -4770,26 +7637,96 @@ snapshots: '@rollup/rollup-win32-x64-msvc': 4.59.0 fsevents: 2.3.3 + rtlcss@4.3.0: + dependencies: + escalade: 3.2.0 + picocolors: 1.1.1 + postcss: 8.5.8 + strip-json-comments: 3.1.1 + + run-applescript@7.1.0: {} + + run-parallel@1.2.0: + dependencies: + queue-microtask: 1.2.3 + safe-buffer@5.1.2: {} + safe-buffer@5.2.1: {} + + section-matter@1.0.0: + dependencies: + extend-shallow: 2.0.1 + kind-of: 6.0.3 + semver@6.3.1: {} semver@7.7.4: {} setimmediate@1.0.5: {} + sharp@0.34.5: + dependencies: + '@img/colour': 1.1.0 + detect-libc: 2.1.2 + semver: 7.7.4 + optionalDependencies: + '@img/sharp-darwin-arm64': 0.34.5 + '@img/sharp-darwin-x64': 0.34.5 + '@img/sharp-libvips-darwin-arm64': 1.2.4 + '@img/sharp-libvips-darwin-x64': 1.2.4 + '@img/sharp-libvips-linux-arm': 1.2.4 + '@img/sharp-libvips-linux-arm64': 1.2.4 + '@img/sharp-libvips-linux-ppc64': 1.2.4 + '@img/sharp-libvips-linux-riscv64': 1.2.4 + '@img/sharp-libvips-linux-s390x': 1.2.4 + '@img/sharp-libvips-linux-x64': 1.2.4 + '@img/sharp-libvips-linuxmusl-arm64': 1.2.4 + '@img/sharp-libvips-linuxmusl-x64': 1.2.4 + '@img/sharp-linux-arm': 0.34.5 + '@img/sharp-linux-arm64': 0.34.5 + '@img/sharp-linux-ppc64': 0.34.5 + '@img/sharp-linux-riscv64': 0.34.5 + '@img/sharp-linux-s390x': 0.34.5 + '@img/sharp-linux-x64': 0.34.5 + '@img/sharp-linuxmusl-arm64': 0.34.5 + '@img/sharp-linuxmusl-x64': 0.34.5 + '@img/sharp-wasm32': 0.34.5 + '@img/sharp-win32-arm64': 0.34.5 + '@img/sharp-win32-ia32': 0.34.5 + '@img/sharp-win32-x64': 0.34.5 + shebang-command@2.0.0: dependencies: shebang-regex: 3.0.0 shebang-regex@3.0.0: {} + shiki@3.23.0: + dependencies: + '@shikijs/core': 3.23.0 + '@shikijs/engine-javascript': 3.23.0 + '@shikijs/engine-oniguruma': 3.23.0 + '@shikijs/langs': 3.23.0 + '@shikijs/themes': 3.23.0 + '@shikijs/types': 3.23.0 + '@shikijs/vscode-textmate': 10.0.2 + '@types/hast': 3.0.4 + siginfo@2.0.0: {} signal-exit@3.0.7: {} signal-exit@4.1.0: {} + simple-git@3.33.0: + dependencies: + '@kwsites/file-exists': 1.1.1 + '@kwsites/promise-deferred': 1.1.1 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + sirv@3.0.2: dependencies: '@polka/url': 1.0.0-next.29 @@ -4797,44 +7734,110 @@ snapshots: totalist: 3.0.1 optional: true + sisteransi@1.0.5: {} + + slick@1.12.2: {} + source-map-js@1.2.1: {} + source-map-support@0.5.21: + dependencies: + buffer-from: 1.1.2 + source-map: 0.6.1 + optional: true + + source-map@0.6.1: + optional: true + source-map@0.7.6: {} + space-separated-tokens@2.0.2: {} + speakingurl@14.0.1: {} + speech-rule-engine@4.1.2: + dependencies: + '@xmldom/xmldom': 0.9.8 + commander: 13.1.0 + wicked-good-xpath: 1.3.0 + + sprintf-js@1.0.3: {} + stable-hash-x@0.2.0: {} stackback@0.0.2: {} std-env@3.10.0: {} + string-width@4.2.3: + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + string_decoder@1.1.1: dependencies: safe-buffer: 5.1.2 + string_decoder@1.3.0: + dependencies: + safe-buffer: 5.2.1 + + stringify-entities@4.0.4: + dependencies: + character-entities-html4: 2.1.0 + character-entities-legacy: 3.0.0 + strip-ansi@6.0.1: dependencies: ansi-regex: 5.0.1 + strip-bom-string@1.0.0: {} + strip-indent@4.1.1: {} strip-json-comments@3.1.1: {} + strtok3@7.1.1: + dependencies: + '@tokenizer/token': 0.3.0 + peek-readable: 5.4.2 + superjson@2.2.6: dependencies: copy-anything: 4.0.5 + supports-color@10.2.2: {} + supports-color@7.2.0: dependencies: has-flag: 4.0.0 supports-preserve-symlinks-flag@1.0.0: {} + tabbable@6.4.0: {} + tailwindcss@4.2.1: {} tapable@2.3.0: {} + temp-dir@3.0.0: {} + + tempy@3.2.0: + dependencies: + is-stream: 3.0.0 + temp-dir: 3.0.0 + type-fest: 2.19.0 + unique-string: 3.0.0 + + terser@5.46.0: + dependencies: + '@jridgewell/source-map': 0.3.11 + acorn: 8.16.0 + commander: 2.20.3 + source-map-support: 0.5.21 + optional: true + threads@1.7.0: dependencies: callsites: 3.1.0 @@ -4866,19 +7869,39 @@ snapshots: dependencies: is-number: 7.0.0 + token-types@5.0.1: + dependencies: + '@tokenizer/token': 0.3.0 + ieee754: 1.2.1 + + tokenx@1.3.0: {} + totalist@3.0.1: optional: true + tr46@0.0.3: {} + + trim-lines@3.0.1: {} + + trough@2.2.0: {} + ts-api-utils@2.4.0(typescript@5.9.3): dependencies: typescript: 5.9.3 - tslib@2.7.0: {} + tslib@2.7.0: + optional: true + + tslib@2.8.1: {} type-check@0.4.0: dependencies: prelude-ls: 1.2.1 + type-fest@1.4.0: {} + + type-fest@2.19.0: {} + typescript-eslint@8.56.1(eslint@9.39.3(jiti@2.6.1))(typescript@5.9.3): dependencies: '@typescript-eslint/eslint-plugin': 8.56.1(@typescript-eslint/parser@8.56.1(eslint@9.39.3(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.3(jiti@2.6.1))(typescript@5.9.3) @@ -4892,10 +7915,63 @@ snapshots: typescript@5.9.3: {} - undici-types@6.19.8: {} + uc.micro@2.1.0: {} + + ufo@1.6.3: {} + + ultramatter@0.0.4: {} undici-types@7.16.0: {} + undici@7.18.2: {} + + unenv@2.0.0-rc.24: + dependencies: + pathe: 2.0.3 + + unified@11.0.5: + dependencies: + '@types/unist': 3.0.3 + bail: 2.0.2 + devlop: 1.1.0 + extend: 3.0.2 + is-plain-obj: 4.1.0 + trough: 2.2.0 + vfile: 6.0.3 + + unique-string@3.0.0: + dependencies: + crypto-random-string: 4.0.0 + + unist-util-is@6.0.1: + dependencies: + '@types/unist': 3.0.3 + + unist-util-position@5.0.0: + dependencies: + '@types/unist': 3.0.3 + + unist-util-remove@4.0.0: + dependencies: + '@types/unist': 3.0.3 + unist-util-is: 6.0.1 + unist-util-visit-parents: 6.0.2 + + unist-util-stringify-position@4.0.0: + dependencies: + '@types/unist': 3.0.3 + + unist-util-visit-parents@6.0.2: + dependencies: + '@types/unist': 3.0.3 + unist-util-is: 6.0.1 + + unist-util-visit@5.1.0: + dependencies: + '@types/unist': 3.0.3 + unist-util-is: 6.0.1 + unist-util-visit-parents: 6.0.2 + unrs-resolver@1.11.1: dependencies: napi-postinstall: 0.3.4 @@ -4932,7 +8008,36 @@ snapshots: util-deprecate@1.0.2: {} - vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.31.1): + valid-data-url@3.0.1: {} + + vfile-message@4.0.3: + dependencies: + '@types/unist': 3.0.3 + unist-util-stringify-position: 4.0.0 + + vfile@6.0.3: + dependencies: + '@types/unist': 3.0.3 + vfile-message: 4.0.3 + + viem@2.47.0(typescript@5.9.3)(zod@4.3.6): + dependencies: + '@noble/curves': 1.9.1 + '@noble/hashes': 1.8.0 + '@scure/bip32': 1.7.0 + '@scure/bip39': 1.6.0 + abitype: 1.2.3(typescript@5.9.3)(zod@4.3.6) + isows: 1.0.7(ws@8.18.3) + ox: 0.14.0(typescript@5.9.3)(zod@4.3.6) + ws: 8.18.3 + optionalDependencies: + typescript: 5.9.3 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + - zod + + vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(yaml@2.8.2): dependencies: esbuild: 0.27.2 fdir: 6.5.0(picomatch@4.0.3) @@ -4945,11 +8050,105 @@ snapshots: fsevents: 2.3.3 jiti: 2.6.1 lightningcss: 1.31.1 + terser: 5.46.0 + yaml: 2.8.2 + + vite@7.3.1(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(yaml@2.8.2): + dependencies: + esbuild: 0.27.2 + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 + postcss: 8.5.8 + rollup: 4.59.0 + tinyglobby: 0.2.15 + optionalDependencies: + '@types/node': 24.10.4 + fsevents: 2.3.3 + jiti: 2.6.1 + lightningcss: 1.31.1 + terser: 5.46.0 + yaml: 2.8.2 + + vitepress-plugin-group-icons@1.7.1(vite@7.3.1(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(yaml@2.8.2)): + dependencies: + '@iconify-json/logos': 1.2.10 + '@iconify-json/vscode-icons': 1.2.45 + '@iconify/utils': 3.1.0 + optionalDependencies: + vite: 7.3.1(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(yaml@2.8.2) - vitest@4.0.18(@types/node@24.10.4)(@vitest/browser-playwright@4.0.18)(jiti@2.6.1)(lightningcss@1.31.1): + vitepress-plugin-llms@1.11.0: + dependencies: + gray-matter: 4.0.3 + markdown-it: 14.1.1 + markdown-title: 1.0.2 + mdast-util-from-markdown: 2.0.3 + millify: 6.1.0 + minimatch: 10.2.4 + path-to-regexp: 6.3.0 + picocolors: 1.1.1 + pretty-bytes: 7.1.0 + remark: 15.0.1 + remark-frontmatter: 5.0.0 + tokenx: 1.3.0 + unist-util-remove: 4.0.0 + unist-util-visit: 5.1.0 + transitivePeerDependencies: + - supports-color + + vitepress@2.0.0-alpha.16(@types/node@24.10.4)(change-case@5.4.4)(fuse.js@7.1.0)(jiti@2.6.1)(lightningcss@1.31.1)(markdown-it-mathjax3@4.3.2)(postcss@8.5.8)(terser@5.46.0)(typescript@5.9.3)(yaml@2.8.2): + dependencies: + '@docsearch/css': 4.6.0 + '@docsearch/js': 4.6.0 + '@docsearch/sidepanel-js': 4.6.0 + '@iconify-json/simple-icons': 1.2.73 + '@shikijs/core': 3.23.0 + '@shikijs/transformers': 3.23.0 + '@shikijs/types': 3.23.0 + '@types/markdown-it': 14.1.2 + '@vitejs/plugin-vue': 6.0.3(vite@7.3.1(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(yaml@2.8.2))(vue@3.5.30(typescript@5.9.3)) + '@vue/devtools-api': 8.1.0 + '@vue/shared': 3.5.30 + '@vueuse/core': 14.2.1(vue@3.5.30(typescript@5.9.3)) + '@vueuse/integrations': 14.2.1(change-case@5.4.4)(focus-trap@7.8.0)(fuse.js@7.1.0)(vue@3.5.30(typescript@5.9.3)) + focus-trap: 7.8.0 + mark.js: 8.11.1 + minisearch: 7.2.0 + shiki: 3.23.0 + vite: 7.3.1(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(yaml@2.8.2) + vue: 3.5.30(typescript@5.9.3) + optionalDependencies: + markdown-it-mathjax3: 4.3.2 + postcss: 8.5.8 + transitivePeerDependencies: + - '@types/node' + - async-validator + - axios + - change-case + - drauu + - fuse.js + - idb-keyval + - jiti + - jwt-decode + - less + - lightningcss + - nprogress + - qrcode + - sass + - sass-embedded + - sortablejs + - stylus + - sugarss + - terser + - tsx + - typescript + - universal-cookie + - yaml + + vitest@4.0.18(@types/node@24.10.4)(@vitest/browser-playwright@4.0.18)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(yaml@2.8.2): dependencies: '@vitest/expect': 4.0.18 - '@vitest/mocker': 4.0.18(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.31.1)) + '@vitest/mocker': 4.0.18(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(yaml@2.8.2)) '@vitest/pretty-format': 4.0.18 '@vitest/runner': 4.0.18 '@vitest/snapshot': 4.0.18 @@ -4966,11 +8165,11 @@ snapshots: tinyexec: 1.0.2 tinyglobby: 0.2.15 tinyrainbow: 3.0.3 - vite: 7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.31.1) + vite: 7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(yaml@2.8.2) why-is-node-running: 2.3.0 optionalDependencies: '@types/node': 24.10.4 - '@vitest/browser-playwright': 4.0.18(playwright@1.58.2)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.31.1))(vitest@4.0.18) + '@vitest/browser-playwright': 4.0.18(playwright@1.58.2)(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(yaml@2.8.2))(vitest@4.0.18) transitivePeerDependencies: - jiti - less @@ -5002,10 +8201,38 @@ snapshots: optionalDependencies: typescript: 5.9.3 + vue@3.5.30(typescript@5.9.3): + dependencies: + '@vue/compiler-dom': 3.5.30 + '@vue/compiler-sfc': 3.5.30 + '@vue/runtime-dom': 3.5.30 + '@vue/server-renderer': 3.5.30(vue@3.5.30(typescript@5.9.3)) + '@vue/shared': 3.5.30 + optionalDependencies: + typescript: 5.9.3 + wcwidth@1.0.1: dependencies: defaults: 1.0.4 + web-resource-inliner@6.0.1: + dependencies: + ansi-colors: 4.1.3 + escape-goat: 3.0.0 + htmlparser2: 5.0.1 + mime: 2.6.0 + node-fetch: 2.7.0 + valid-data-url: 3.0.1 + transitivePeerDependencies: + - encoding + + webidl-conversions@3.0.1: {} + + whatwg-url@5.0.0: + dependencies: + tr46: 0.0.3 + webidl-conversions: 3.0.1 + which@2.0.2: dependencies: isexe: 2.0.0 @@ -5015,13 +8242,87 @@ snapshots: siginfo: 2.0.0 stackback: 0.0.2 + wicked-good-xpath@1.3.0: {} + word-wrap@1.2.5: {} - ws@8.17.1: {} + workerd@1.20260312.1: + optionalDependencies: + '@cloudflare/workerd-darwin-64': 1.20260312.1 + '@cloudflare/workerd-darwin-arm64': 1.20260312.1 + '@cloudflare/workerd-linux-64': 1.20260312.1 + '@cloudflare/workerd-linux-arm64': 1.20260312.1 + '@cloudflare/workerd-windows-64': 1.20260312.1 + + wrangler@4.73.0: + dependencies: + '@cloudflare/kv-asset-handler': 0.4.2 + '@cloudflare/unenv-preset': 2.15.0(unenv@2.0.0-rc.24)(workerd@1.20260312.1) + blake3-wasm: 2.1.5 + esbuild: 0.27.3 + miniflare: 4.20260312.0 + path-to-regexp: 6.3.0 + unenv: 2.0.0-rc.24 + workerd: 1.20260312.1 + optionalDependencies: + fsevents: 2.3.3 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + + wrap-ansi@7.0.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + ws@8.18.0: {} + + ws@8.18.3: {} ws@8.19.0: optional: true + wsl-utils@0.1.0: + dependencies: + is-wsl: 3.1.1 + + y18n@5.0.8: {} + yallist@3.1.1: {} + yaml@2.8.2: + optional: true + + yargs-parser@21.1.1: {} + + yargs@17.7.2: + dependencies: + cliui: 8.0.1 + escalade: 3.2.0 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + string-width: 4.2.3 + y18n: 5.0.8 + yargs-parser: 21.1.1 + yocto-queue@0.1.0: {} + + youch-core@0.3.3: + dependencies: + '@poppinss/exception': 1.2.3 + error-stack-parser-es: 1.0.5 + + youch@4.1.0-beta.10: + dependencies: + '@poppinss/colors': 4.1.6 + '@poppinss/dumper': 0.6.5 + '@speed-highlight/core': 1.2.14 + cookie: 1.1.1 + youch-core: 0.3.3 + + zod@3.25.76: {} + + zod@4.3.6: {} + + zwitch@2.0.4: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 182def7..1d67993 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -4,5 +4,6 @@ packages: onlyBuiltDependencies: - esbuild + - sharp - unrs-resolver - - wasm-pack + - workerd diff --git a/remappings.txt b/remappings.txt index 9b42f71..c7ebc70 100644 --- a/remappings.txt +++ b/remappings.txt @@ -6,3 +6,6 @@ halmos-cheatcodes/=lib/openzeppelin-contracts-upgradeable/lib/halmos-cheatcodes/ openzeppelin-contracts-upgradeable/=lib/openzeppelin-contracts-upgradeable/ openzeppelin-contracts/=lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/ openzeppelin-foundry-upgrades/=lib/openzeppelin-foundry-upgrades/src/ +scroll-contracts/=lib/scroll-contracts/src/ +eas-contracts/=lib/eas-contracts/contracts +solady/=lib/solady/src diff --git a/rust-toolchain.toml b/rust-toolchain.toml index 5d56faf..76a06e6 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,2 +1,2 @@ [toolchain] -channel = "nightly" +channel = "1.94.0" diff --git a/script/Deploy.s.sol b/script/Deploy.s.sol new file mode 100644 index 0000000..8f26443 --- /dev/null +++ b/script/Deploy.s.sol @@ -0,0 +1,207 @@ +// SPDX-License-Identifier: MIT +pragma solidity =0.8.28; + +import {Script, console} from "forge-std/Script.sol"; +import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; +import {L2AnchoringManager} from "../contracts/L2/manager/L2AnchoringManager.sol"; +import {IL2AnchoringManager} from "../contracts/L2/manager/IL2AnchoringManager.sol"; +import {FeeOracle} from "../contracts/L2/oracle/FeeOracle.sol"; +import {IFeeOracle} from "../contracts/L2/oracle/IFeeOracle.sol"; +import {L1AnchoringGateway} from "../contracts/L1/L1AnchoringGateway.sol"; +import {NFTGenerator} from "../contracts/L2/nft/NFTGenerator.sol"; + +bytes32 constant SALT = keccak256("UniversalTimestamps"); + +// Deployment requires to run the scripts in the order they are defined in this file. + +// Verify Args: +// --verifier etherscan --verifier-url "https://api.etherscan.io/v2/api?chainid={chain_id}" --etherscan-api-key {key} --verify --retries 10 --delay 10 + +// forge script script/Deploy.s.sol:DeployFeeOracle --broadcast --rpc-url https://sepolia-rpc.scroll.io/ --account dev {verify_args} +contract DeployFeeOracle is Script { + function run() public { + address owner = vm.envAddress("OWNER_ADDRESS"); + + vm.startBroadcast(); + FeeOracle implementation = new FeeOracle(owner); + vm.stopBroadcast(); + + console.log("FeeOracle deployed at", address(implementation)); + } +} + +// forge script script/Deploy.s.sol:DeployNFTGenerator --broadcast --rpc-url https://sepolia-rpc.scroll.io/ --account dev {verify_args} +contract DeployNFTGenerator is Script { + function run() public { + vm.startBroadcast(); + NFTGenerator generator = new NFTGenerator(); + vm.stopBroadcast(); + + console.log("NFTGenerator deployed at", address(generator)); + } +} + +// This should deploy using master account for deterministic address. +// forge script script/Deploy.s.sol:DeployManager --broadcast --rpc-url https://sepolia-rpc.scroll.io/ --account master --sender $MASTER_ADDRESS {verify_args} +contract DeployManager is Script { + function run() public { + address master = vm.envAddress("MASTER_ADDRESS"); + address owner = vm.envAddress("OWNER_ADDRESS"); + address eas = vm.envAddress("EAS_SCROLL"); + address feeOracle = vm.envAddress("FEE_ORACLE"); + address generator = vm.envAddress("NFT_GENERATOR"); + address l2Messenger = vm.envAddress("L2_MESSENGER"); + + vm.startBroadcast(master); + L2AnchoringManager implementation = new L2AnchoringManager{salt: SALT}(); + console.log("Implementation deployed at:", address(implementation)); + + bytes memory initData = abi.encodeCall(L2AnchoringManager.initialize, (master)); + ERC1967Proxy proxy = new ERC1967Proxy{salt: SALT}(address(implementation), initData); + + L2AnchoringManager manager = L2AnchoringManager(payable(address(proxy))); + manager.lateInitialize("Scroll", owner, eas, feeOracle, l2Messenger, generator); + vm.stopBroadcast(); + + console.log("Proxy deployed at:", address(proxy)); + } +} + +// forge script script/Deploy.s.sol:AcceptManagerAdmin --broadcast --rpc-url https://sepolia-rpc.scroll.io/ +contract AcceptManagerAdmin is Script { + function run() public { + address anchoringManager = vm.envAddress("ANCHORING_MANAGER"); + + L2AnchoringManager manager = L2AnchoringManager(payable(anchoringManager)); + + vm.startBroadcast(); + manager.completeInitialization(); + vm.stopBroadcast(); + } +} + +// This should deploy using master account for deterministic address. +// forge script script/Deploy.s.sol:DeployGateway --broadcast --rpc-url https://0xrpc.io/sep --account master --sender $MASTER_ADDRESS {verify_args} +contract DeployGateway is Script { + function run() public { + address master = vm.envAddress("MASTER_ADDRESS"); + address owner = vm.envAddress("OWNER_ADDRESS"); + address eas = vm.envAddress("EAS_MAINNET"); + address l1Messenger = vm.envAddress("L1_MESSENGER"); + address anchoringManager = vm.envAddress("ANCHORING_MANAGER"); + + vm.startBroadcast(master); + L1AnchoringGateway implementation = new L1AnchoringGateway{salt: SALT}(); + console.log("Implementation deployed at:", address(implementation)); + + bytes memory initData = abi.encodeCall(L1AnchoringGateway.initialize, (master)); + ERC1967Proxy proxy = new ERC1967Proxy{salt: SALT}(address(implementation), initData); + + L1AnchoringGateway gateway = L1AnchoringGateway(payable(address(proxy))); + gateway.lateInitialize(owner, eas, l1Messenger, anchoringManager); + vm.stopBroadcast(); + + console.log("Proxy deployed at:", address(proxy)); + } +} + +// forge script script/Deploy.s.sol:AcceptGatewayAdmin --broadcast --rpc-url https://0xrpc.io/sep +contract AcceptGatewayAdmin is Script { + function run() public { + address anchoringGateway = vm.envAddress("ANCHORING_GATEWAY"); + + L1AnchoringGateway gateway = L1AnchoringGateway(payable(anchoringGateway)); + + vm.startBroadcast(); + gateway.completeInitialization(); + vm.stopBroadcast(); + } +} + +// forge script script/Deploy.s.sol:SetGateway --broadcast --rpc-url https://sepolia-rpc.scroll.io/ --account dev {verify_args} +contract SetGateway is Script { + function run() public { + address l1Gateway = vm.envAddress("ANCHORING_GATEWAY"); + address anchoringManager = vm.envAddress("ANCHORING_MANAGER"); + + IL2AnchoringManager manager = IL2AnchoringManager(anchoringManager); + vm.startBroadcast(); + manager.setL1Gateway(l1Gateway); + vm.stopBroadcast(); + } +} + +// --- Following scripts are utils, and should be run only when needed. --- + +contract GrantSubmitter is Script { + function run() public { + address anchoringGateway = vm.envAddress("ANCHORING_GATEWAY"); + address submitter = vm.envAddress("SUBMITTER"); + + L1AnchoringGateway gateway = L1AnchoringGateway(payable(anchoringGateway)); + + vm.startBroadcast(); + gateway.grantRole(gateway.SUBMITTER_ROLE(), submitter); + vm.stopBroadcast(); + + console.log("Granted SUBMITTER_ROLE to", submitter); + } +} + +contract SetManager is Script { + function run() public { + address gateway = vm.envAddress("ANCHORING_GATEWAY"); + address anchoringManager = vm.envAddress("ANCHORING_MANAGER"); + + L1AnchoringGateway l1Gateway = L1AnchoringGateway(payable(gateway)); + vm.startBroadcast(); + l1Gateway.setL2AnchoringManager(anchoringManager); + vm.stopBroadcast(); + } +} + +contract UpgradeManager is Script { + function run() public { + address anchoringManager = vm.envAddress("ANCHORING_MANAGER"); + + vm.startBroadcast(); + L2AnchoringManager newImplementation = new L2AnchoringManager(); + console.log("New implementation deployed at:", address(newImplementation)); + + L2AnchoringManager manager = L2AnchoringManager(payable(anchoringManager)); + + manager.upgradeToAndCall(address(newImplementation), ""); + vm.stopBroadcast(); + + console.log("Manager upgraded to new implementation at:", address(newImplementation)); + } +} + +contract UpgradeGateway is Script { + function run() public { + address l1Gateway = vm.envAddress("ANCHORING_GATEWAY"); + + vm.startBroadcast(); + L1AnchoringGateway newImplementation = new L1AnchoringGateway(); + console.log("New implementation deployed at:", address(newImplementation)); + + L1AnchoringGateway gateway = L1AnchoringGateway(payable(l1Gateway)); + + gateway.upgradeToAndCall(address(newImplementation), ""); + vm.stopBroadcast(); + + console.log("Gateway upgraded to new implementation at:", address(newImplementation)); + } +} + +contract ClearBatch is Script { + function run() public { + address anchoringManager = vm.envAddress("ANCHORING_MANAGER"); + + IL2AnchoringManager manager = IL2AnchoringManager(anchoringManager); + + vm.startBroadcast(); + manager.clearBatch(); + vm.stopBroadcast(); + } +} diff --git a/script/E2E.s.sol b/script/E2E.s.sol new file mode 100644 index 0000000..0db4f60 --- /dev/null +++ b/script/E2E.s.sol @@ -0,0 +1,111 @@ +// SPDX-License-Identifier: MIT +pragma solidity =0.8.28; + +import {Script, console} from "forge-std/Script.sol"; +import {IL2AnchoringManager} from "../contracts/L2/manager/IL2AnchoringManager.sol"; +import {IL1AnchoringGateway} from "../contracts/L1/IL1AnchoringGateway.sol"; +import {IFeeOracle} from "../contracts/L2/oracle/IFeeOracle.sol"; +import {MerkleTree} from "../contracts/core/MerkleTree.sol"; +import {IEAS, AttestationRequest, AttestationRequestData} from "eas-contracts/IEAS.sol"; +import {EASHelper} from "../contracts/core/EASHelper.sol"; +import {Strings} from "@openzeppelin/contracts/utils/Strings.sol"; + +contract SubmitEAS is Script { + function run() public { + address _eas = vm.envAddress("EAS_SCROLL"); + IEAS eas = IEAS(_eas); + + vm.startBroadcast(); + eas.attest( + AttestationRequest({ + schema: EASHelper.CONTENT_HASH_SCHEMA, + data: AttestationRequestData({ + recipient: address(0), // No specific recipient, as the attestation is about the content hash + expirationTime: 0, // No expiration + revocable: false, // Un-revokable + refUID: bytes32(0), // No reference to another attestation + data: abi.encode(0x0000000000000000000000000000000000000000000000000000000000000007), // Encode the root in the data field + value: 0 // No ETH value needed for this attestation + }) + }) + ); + vm.stopBroadcast(); + } +} + +contract SubmitAnchoring is Script { + IL2AnchoringManager manager; + IFeeOracle oracle; + + function run() public { + address anchoringManager = vm.envAddress("ANCHORING_MANAGER"); + address feeOracle = vm.envAddress("FEE_ORACLE"); + + manager = IL2AnchoringManager(anchoringManager); + oracle = IFeeOracle(feeOracle); + + vm.startBroadcast(); + // submit(0x45c9a61d29a852778fc2d3fd4f955136dd2a49d633d6431bbe379842102518d7); + // submit(0xcbca13c4d782ed3ee2306f4c0aa21112cea47e920f10ccac48ce1fd135917387); + submit(0x4168A6777EC5730C480A613661ED418790BF31A6B47ABC6602072DD2E74D6F36); + // submit(0x7cbb36e4a87098984ff38a1f07c8d493eaed57992d116322b7bc2354e1b9a608); + // submit(0xE47C7963CBD803028A83A00B33C27DC5ABF4790400385F3EE5268BA645C2F401); + vm.stopBroadcast(); + } + + function submit(bytes32 attestationId) internal virtual { + uint256 fee = oracle.getFloorFee() * 1.1e18 / 1e18; + console.log("Current floor fee:", fee); + + manager.submitForL1Anchoring{value: fee}(attestationId); + } +} + +contract SubmitBatch is Script { + function run() public { + address anchoringGateway = vm.envAddress("ANCHORING_GATEWAY"); + + IL1AnchoringGateway gateway = IL1AnchoringGateway(anchoringGateway); + + bytes32[] memory roots = new bytes32[](5); + roots[0] = 0x0000000000000000000000000000000000000000000000000000000000000002; + roots[1] = 0x0000000000000000000000000000000000000000000000000000000000000003; + roots[2] = 0x0000000000000000000000000000000000000000000000000000000000000001; + roots[3] = 0x0000000000000000000000000000000000000000000000000000000000000000; + roots[4] = 0x0000000000000000000000000000000000000000000000000000000000000004; + + bytes32 root = MerkleTree.computeRoot(roots); + + vm.startBroadcast(); + gateway.submitBatch{value: 0.1 ether}(root, 1, 5, 200_000); + vm.stopBroadcast(); + } +} + +contract FinalizeBatch is Script { + function run() public { + address anchoringManager = vm.envAddress("ANCHORING_MANAGER"); + + IL2AnchoringManager manager = IL2AnchoringManager(anchoringManager); + + vm.startBroadcast(); + manager.finalizeBatch(); + vm.stopBroadcast(); + } +} + +contract MintNFT is Script { + function run() public { + address anchoringManager = vm.envAddress("ANCHORING_MANAGER"); + + IL2AnchoringManager manager = IL2AnchoringManager(anchoringManager); + + vm.startBroadcast(); + manager.claimNFT(0x45c9a61d29a852778fc2d3fd4f955136dd2a49d633d6431bbe379842102518d7, 1); + manager.claimNFT(0xcbca13c4d782ed3ee2306f4c0aa21112cea47e920f10ccac48ce1fd135917387, 1); + manager.claimNFT(0xb2135365d636d3f9674c5425fdee7f0e3a1b09e2e37814100e7cded8d6d63301, 1); + manager.claimNFT(0x7cbb36e4a87098984ff38a1f07c8d493eaed57992d116322b7bc2354e1b9a608, 1); + manager.claimNFT(0xb3c7afbdcf6213aaf8aef4367a74199d6aa97a7c0aea5c302dd279e2a667c54d, 1); + vm.stopBroadcast(); + } +} diff --git a/tsconfig.json b/tsconfig.json index 9e7bf2a..72aeb5b 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -51,13 +51,15 @@ "skipLibCheck": true, // skip all type checks for .d.ts files "paths": { - "@uts/sdk": ["./packages/sdk/src/index.ts"] + "@uts/sdk": ["./packages/sdk/src/index.ts"], + "@uts/contracts": ["./packages/contract-types/src/index.ts"] }, "customConditions": ["uts-source"] }, "references": [ - { "path": "./packages/sdk" }, - { "path": "./packages/sdk/test/" }, - { "path": "./apps/web" } + { "path": "./packages/sdk-ts" }, + { "path": "./packages/sdk-ts/test/" }, + { "path": "./apps/web" }, + { "path": "./apps/docs" } ] }