From 31cff07fc5a946d638cf23d21abaf3f6a60857aa Mon Sep 17 00:00:00 2001 From: Danyal Prout Date: Thu, 12 Feb 2026 15:31:24 -0600 Subject: [PATCH] Remove non-UI code and move UI to top level --- .cursor/rules/bugsnag/fix-bugsnag-issues.mdc | 72 - .cursor/rules/coding-workflow.mdc | 19 - .cursor/rules/github/pr-template-format.mdc | 59 - .cursor/rules/golang/coding-rules.mdc | 27 - .../rules/golang/english-and-comments-std.mdc | 49 - .cursor/rules/golang/golang-naming-std.mdc | 69 - .cursor/rules/golang/golang-use-getters.mdc | 53 - .cursor/rules/golang/grpcclient.mdc | 17 - .../observability/logging-best-practices.mdc | 255 - .../observability/metrics-best-practices.mdc | 136 - .cursor/rules/python/api-design.mdc | 28 - .../rules/python/dependency-management.mdc | 21 - .../rules/python/distributed-computing.mdc | 25 - .cursor/rules/python/fastapi-standards.mdc | 41 - .cursor/rules/python/infrastructure.mdc | 25 - .../python/microservices-architecture.mdc | 36 - .cursor/rules/python/python-coding-rules.mdc | 32 - .cursor/rules/python/testing-standards.mdc | 28 - .cursor/rules/rego/rego-coding-patterns.mdc | 94 - .dockerignore | 29 - .env.example | 37 +- .github/workflows/docker.yml | 60 - .github/workflows/rust.yml | 81 - .github/workflows/stale.yml | 32 - .github/workflows/ui.yml | 23 +- .gitignore | 57 +- CLAUDE.md | 18 - Cargo.lock | 8468 ----------------- Cargo.toml | 101 - Dockerfile | 53 +- Justfile | 207 - README.md | 30 - bin/tips-audit/Cargo.toml | 21 - bin/tips-audit/src/main.rs | 137 - bin/tips-ingress-rpc/Cargo.toml | 23 - bin/tips-ingress-rpc/src/main.rs | 143 - ui/biome.json => biome.json | 0 crates/account-abstraction-core/Cargo.toml | 29 - crates/account-abstraction-core/README.md | 310 - .../src/domain/entrypoints/mod.rs | 3 - .../src/domain/entrypoints/v06.rs | 139 - .../src/domain/entrypoints/v07.rs | 180 - .../src/domain/entrypoints/version.rs | 31 - .../src/domain/events.rs | 17 - .../src/domain/mempool.rs | 18 - .../src/domain/mod.rs | 13 - .../src/domain/reputation.rs | 18 - .../src/domain/types.rs | 207 - .../src/factories/kafka_engine.rs | 33 - .../src/factories/mod.rs | 1 - .../src/infrastructure/base_node/mod.rs | 1 - .../src/infrastructure/base_node/validator.rs | 173 - .../src/infrastructure/in_memory/mempool.rs | 424 - .../src/infrastructure/in_memory/mod.rs | 3 - .../src/infrastructure/kafka/consumer.rs | 29 - .../src/infrastructure/kafka/mod.rs | 1 - .../src/infrastructure/mod.rs | 3 - crates/account-abstraction-core/src/lib.rs | 23 - .../src/services/interfaces/event_source.rs | 7 - .../src/services/interfaces/mod.rs | 2 - .../services/interfaces/user_op_validator.rs | 12 - .../src/services/mempool_engine.rs | 159 - .../src/services/mod.rs | 7 - .../src/services/reputations_service.rs | 27 - crates/audit/Cargo.toml | 33 - crates/audit/src/archiver.rs | 163 - crates/audit/src/lib.rs | 74 - crates/audit/src/metrics.rs | 55 - crates/audit/src/publisher.rs | 235 - crates/audit/src/reader.rs | 268 - crates/audit/src/storage.rs | 1017 -- crates/audit/src/types.rs | 331 - crates/audit/tests/common/mod.rs | 81 - crates/audit/tests/integration_tests.rs | 128 - crates/audit/tests/s3_test.rs | 483 - crates/core/Cargo.toml | 36 - crates/core/README.md | 3 - crates/core/src/kafka.rs | 22 - crates/core/src/lib.rs | 19 - crates/core/src/logger.rs | 73 - crates/core/src/metrics.rs | 9 - crates/core/src/test_utils.rs | 80 - crates/core/src/types.rs | 494 - crates/ingress-rpc/Cargo.toml | 42 - crates/ingress-rpc/src/health.rs | 31 - crates/ingress-rpc/src/lib.rs | 262 - crates/ingress-rpc/src/metrics.rs | 54 - crates/ingress-rpc/src/queue.rs | 167 - crates/ingress-rpc/src/service.rs | 953 -- crates/ingress-rpc/src/validation.rs | 373 - crates/system-tests/Cargo.toml | 49 - crates/system-tests/METRICS.md | 133 - crates/system-tests/README.md | 30 - crates/system-tests/src/bin/load-test.rs | 13 - crates/system-tests/src/client/mod.rs | 3 - crates/system-tests/src/client/tips_rpc.rs | 53 - crates/system-tests/src/fixtures/mod.rs | 3 - .../system-tests/src/fixtures/transactions.rs | 73 - crates/system-tests/src/lib.rs | 3 - crates/system-tests/src/load_test/config.rs | 76 - crates/system-tests/src/load_test/load.rs | 133 - crates/system-tests/src/load_test/metrics.rs | 78 - crates/system-tests/src/load_test/mod.rs | 9 - crates/system-tests/src/load_test/output.rs | 67 - crates/system-tests/src/load_test/poller.rs | 77 - crates/system-tests/src/load_test/sender.rs | 113 - crates/system-tests/src/load_test/setup.rs | 90 - crates/system-tests/src/load_test/tracker.rs | 115 - crates/system-tests/src/load_test/wallet.rs | 86 - crates/system-tests/tests/common/kafka.rs | 121 - crates/system-tests/tests/common/mod.rs | 1 - .../system-tests/tests/integration_tests.rs | 359 - docker-compose.tips.yml | 43 - docker-compose.yml | 75 - docker/audit-kafka-properties | 10 - docker/host-ingress-audit-kafka-properties | 4 - docker/host-ingress-bundles-kafka-properties | 4 - docker/ingress-audit-kafka-properties | 4 - docker/ingress-bundles-kafka-properties | 3 - ...s-user-operation-consumer-kafka-properties | 9 - docs/API.md | 75 - docs/AUDIT_S3_FORMAT.md | 134 - docs/BUNDLE_STATES.md | 51 - docs/ERC4337_BUNDLER.md | 104 - docs/PULL_REQUEST_GUIDELINES.md | 93 - docs/SETUP.md | 117 - docs/logo.png | Bin 127664 -> 0 bytes ui/next.config.ts => next.config.ts | 0 ui/package-lock.json => package-lock.json | 0 ui/package.json => package.json | 0 ui/postcss.config.mjs => postcss.config.mjs | 0 {ui/public => public}/logo.svg | 0 {ui/src => src}/app/api/block/[hash]/route.ts | 0 {ui/src => src}/app/api/blocks/route.ts | 0 .../app/api/bundle/[uuid]/route.ts | 0 {ui/src => src}/app/api/health/route.ts | 0 {ui/src => src}/app/api/txn/[hash]/route.ts | 0 {ui/src => src}/app/block/[hash]/page.tsx | 0 {ui/src => src}/app/bundles/[uuid]/page.tsx | 0 {ui/src => src}/app/globals.css | 0 {ui/src => src}/app/layout.tsx | 0 {ui/src => src}/app/page.tsx | 0 {ui/src => src}/app/txn/[hash]/page.tsx | 0 {ui/src => src}/lib/s3.ts | 0 ui/tsconfig.json => tsconfig.json | 0 ui/.gitignore | 41 - ui/Dockerfile | 40 - ui/yarn.lock => yarn.lock | 0 148 files changed, 74 insertions(+), 20380 deletions(-) delete mode 100644 .cursor/rules/bugsnag/fix-bugsnag-issues.mdc delete mode 100644 .cursor/rules/coding-workflow.mdc delete mode 100644 .cursor/rules/github/pr-template-format.mdc delete mode 100644 .cursor/rules/golang/coding-rules.mdc delete mode 100644 .cursor/rules/golang/english-and-comments-std.mdc delete mode 100644 .cursor/rules/golang/golang-naming-std.mdc delete mode 100644 .cursor/rules/golang/golang-use-getters.mdc delete mode 100644 .cursor/rules/golang/grpcclient.mdc delete mode 100644 .cursor/rules/observability/logging-best-practices.mdc delete mode 100644 .cursor/rules/observability/metrics-best-practices.mdc delete mode 100644 .cursor/rules/python/api-design.mdc delete mode 100644 .cursor/rules/python/dependency-management.mdc delete mode 100644 .cursor/rules/python/distributed-computing.mdc delete mode 100644 .cursor/rules/python/fastapi-standards.mdc delete mode 100644 .cursor/rules/python/infrastructure.mdc delete mode 100644 .cursor/rules/python/microservices-architecture.mdc delete mode 100644 .cursor/rules/python/python-coding-rules.mdc delete mode 100644 .cursor/rules/python/testing-standards.mdc delete mode 100644 .cursor/rules/rego/rego-coding-patterns.mdc delete mode 100644 .dockerignore delete mode 100644 .github/workflows/docker.yml delete mode 100644 .github/workflows/rust.yml delete mode 100644 .github/workflows/stale.yml delete mode 100644 CLAUDE.md delete mode 100644 Cargo.lock delete mode 100644 Cargo.toml delete mode 100644 Justfile delete mode 100644 README.md delete mode 100644 bin/tips-audit/Cargo.toml delete mode 100644 bin/tips-audit/src/main.rs delete mode 100644 bin/tips-ingress-rpc/Cargo.toml delete mode 100644 bin/tips-ingress-rpc/src/main.rs rename ui/biome.json => biome.json (100%) delete mode 100644 crates/account-abstraction-core/Cargo.toml delete mode 100644 crates/account-abstraction-core/README.md delete mode 100644 crates/account-abstraction-core/src/domain/entrypoints/mod.rs delete mode 100644 crates/account-abstraction-core/src/domain/entrypoints/v06.rs delete mode 100644 crates/account-abstraction-core/src/domain/entrypoints/v07.rs delete mode 100644 crates/account-abstraction-core/src/domain/entrypoints/version.rs delete mode 100644 crates/account-abstraction-core/src/domain/events.rs delete mode 100644 crates/account-abstraction-core/src/domain/mempool.rs delete mode 100644 crates/account-abstraction-core/src/domain/mod.rs delete mode 100644 crates/account-abstraction-core/src/domain/reputation.rs delete mode 100644 crates/account-abstraction-core/src/domain/types.rs delete mode 100644 crates/account-abstraction-core/src/factories/kafka_engine.rs delete mode 100644 crates/account-abstraction-core/src/factories/mod.rs delete mode 100644 crates/account-abstraction-core/src/infrastructure/base_node/mod.rs delete mode 100644 crates/account-abstraction-core/src/infrastructure/base_node/validator.rs delete mode 100644 crates/account-abstraction-core/src/infrastructure/in_memory/mempool.rs delete mode 100644 crates/account-abstraction-core/src/infrastructure/in_memory/mod.rs delete mode 100644 crates/account-abstraction-core/src/infrastructure/kafka/consumer.rs delete mode 100644 crates/account-abstraction-core/src/infrastructure/kafka/mod.rs delete mode 100644 crates/account-abstraction-core/src/infrastructure/mod.rs delete mode 100644 crates/account-abstraction-core/src/lib.rs delete mode 100644 crates/account-abstraction-core/src/services/interfaces/event_source.rs delete mode 100644 crates/account-abstraction-core/src/services/interfaces/mod.rs delete mode 100644 crates/account-abstraction-core/src/services/interfaces/user_op_validator.rs delete mode 100644 crates/account-abstraction-core/src/services/mempool_engine.rs delete mode 100644 crates/account-abstraction-core/src/services/mod.rs delete mode 100644 crates/account-abstraction-core/src/services/reputations_service.rs delete mode 100644 crates/audit/Cargo.toml delete mode 100644 crates/audit/src/archiver.rs delete mode 100644 crates/audit/src/lib.rs delete mode 100644 crates/audit/src/metrics.rs delete mode 100644 crates/audit/src/publisher.rs delete mode 100644 crates/audit/src/reader.rs delete mode 100644 crates/audit/src/storage.rs delete mode 100644 crates/audit/src/types.rs delete mode 100644 crates/audit/tests/common/mod.rs delete mode 100644 crates/audit/tests/integration_tests.rs delete mode 100644 crates/audit/tests/s3_test.rs delete mode 100644 crates/core/Cargo.toml delete mode 100644 crates/core/README.md delete mode 100644 crates/core/src/kafka.rs delete mode 100644 crates/core/src/lib.rs delete mode 100644 crates/core/src/logger.rs delete mode 100644 crates/core/src/metrics.rs delete mode 100644 crates/core/src/test_utils.rs delete mode 100644 crates/core/src/types.rs delete mode 100644 crates/ingress-rpc/Cargo.toml delete mode 100644 crates/ingress-rpc/src/health.rs delete mode 100644 crates/ingress-rpc/src/lib.rs delete mode 100644 crates/ingress-rpc/src/metrics.rs delete mode 100644 crates/ingress-rpc/src/queue.rs delete mode 100644 crates/ingress-rpc/src/service.rs delete mode 100644 crates/ingress-rpc/src/validation.rs delete mode 100644 crates/system-tests/Cargo.toml delete mode 100644 crates/system-tests/METRICS.md delete mode 100644 crates/system-tests/README.md delete mode 100644 crates/system-tests/src/bin/load-test.rs delete mode 100644 crates/system-tests/src/client/mod.rs delete mode 100644 crates/system-tests/src/client/tips_rpc.rs delete mode 100644 crates/system-tests/src/fixtures/mod.rs delete mode 100644 crates/system-tests/src/fixtures/transactions.rs delete mode 100644 crates/system-tests/src/lib.rs delete mode 100644 crates/system-tests/src/load_test/config.rs delete mode 100644 crates/system-tests/src/load_test/load.rs delete mode 100644 crates/system-tests/src/load_test/metrics.rs delete mode 100644 crates/system-tests/src/load_test/mod.rs delete mode 100644 crates/system-tests/src/load_test/output.rs delete mode 100644 crates/system-tests/src/load_test/poller.rs delete mode 100644 crates/system-tests/src/load_test/sender.rs delete mode 100644 crates/system-tests/src/load_test/setup.rs delete mode 100644 crates/system-tests/src/load_test/tracker.rs delete mode 100644 crates/system-tests/src/load_test/wallet.rs delete mode 100644 crates/system-tests/tests/common/kafka.rs delete mode 100644 crates/system-tests/tests/common/mod.rs delete mode 100644 crates/system-tests/tests/integration_tests.rs delete mode 100644 docker-compose.tips.yml delete mode 100644 docker-compose.yml delete mode 100644 docker/audit-kafka-properties delete mode 100644 docker/host-ingress-audit-kafka-properties delete mode 100644 docker/host-ingress-bundles-kafka-properties delete mode 100644 docker/ingress-audit-kafka-properties delete mode 100644 docker/ingress-bundles-kafka-properties delete mode 100644 docker/ingress-user-operation-consumer-kafka-properties delete mode 100644 docs/API.md delete mode 100644 docs/AUDIT_S3_FORMAT.md delete mode 100644 docs/BUNDLE_STATES.md delete mode 100644 docs/ERC4337_BUNDLER.md delete mode 100644 docs/PULL_REQUEST_GUIDELINES.md delete mode 100644 docs/SETUP.md delete mode 100644 docs/logo.png rename ui/next.config.ts => next.config.ts (100%) rename ui/package-lock.json => package-lock.json (100%) rename ui/package.json => package.json (100%) rename ui/postcss.config.mjs => postcss.config.mjs (100%) rename {ui/public => public}/logo.svg (100%) rename {ui/src => src}/app/api/block/[hash]/route.ts (100%) rename {ui/src => src}/app/api/blocks/route.ts (100%) rename {ui/src => src}/app/api/bundle/[uuid]/route.ts (100%) rename {ui/src => src}/app/api/health/route.ts (100%) rename {ui/src => src}/app/api/txn/[hash]/route.ts (100%) rename {ui/src => src}/app/block/[hash]/page.tsx (100%) rename {ui/src => src}/app/bundles/[uuid]/page.tsx (100%) rename {ui/src => src}/app/globals.css (100%) rename {ui/src => src}/app/layout.tsx (100%) rename {ui/src => src}/app/page.tsx (100%) rename {ui/src => src}/app/txn/[hash]/page.tsx (100%) rename {ui/src => src}/lib/s3.ts (100%) rename ui/tsconfig.json => tsconfig.json (100%) delete mode 100644 ui/.gitignore delete mode 100644 ui/Dockerfile rename ui/yarn.lock => yarn.lock (100%) diff --git a/.cursor/rules/bugsnag/fix-bugsnag-issues.mdc b/.cursor/rules/bugsnag/fix-bugsnag-issues.mdc deleted file mode 100644 index 630c982d..00000000 --- a/.cursor/rules/bugsnag/fix-bugsnag-issues.mdc +++ /dev/null @@ -1,72 +0,0 @@ ---- -description: Describes how to handle a request to fix a bugsnag issue -globs: -alwaysApply: false ---- -# Bugsnag Ticket Workflow - -When a user provides a Bugsnag error URL or asks to fix a Bugsnag issue, follow this systematic approach: - -## 1. **Root Cause Analysis** - -### Error Investigation -- Use `mcp_bugsnag-mcp_list-error-events` to get recent events for the error -- Use `mcp_bugsnag-mcp_get-stacktrace` with `include_code: true` and `show_all_frames: true` to get the complete stacktrace -- Analyze the stacktrace to identify: - - The exact line and file where the error occurs - - The call stack that leads to the error - - The error message and context - -### Codebase Investigation -- Read the relevant files identified in the stacktrace -- Search for related code patterns or similar implementations -- Identify the data flow that leads to the problematic state -- Look for edge cases or missing null/undefined checks - -## 2. **Suggest Fixes (No Code Changes)** - -### Analysis Summary -- Provide a clear explanation of what's causing the error -- Identify the specific conditions that trigger the issue -- Explain the impact and severity of the error -- If you can't figure out what is going on, just say so and ask the user for more context - -### Proposed Solutions -- Present 1-3 potential fix approaches with pros/cons -- Suggest relevant tests to prevent regression -- Try to be tactical with your suggestions, avoid large refactors when possible - -### Implementation Recommendations -- Specify which files need to be modified -- Outline the exact changes needed (but don't make them yet) -- Mention any dependencies or related changes required -- Highlight potential breaking changes or compatibility concerns - -## 3. **User Confirmation & Implementation** - -### Get Approval -- Wait for user confirmation on the proposed approach -- Allow for discussion and refinement of the solution -- Clarify any ambiguous requirements or edge cases - -### Implementation & PR Creation -Once the user confirms the approach: -- Follow the below process: - - Making code changes - - Adding tests if needed - ---- - -**Example Workflow:** - -- User: "Fix this Bugsnag issue: https://app.bugsnag.com/" -- Assistant: - 1. Fetches error events and stacktrace from Bugsnag - 2. Analyzes the code to identify root cause - 3. Proposes specific fix approach with explanation - 4. Waits for user confirmation - 5. Making code changes and tests if needed - ---- - -**Note:** This workflow ensures thorough analysis before making changes, reducing the risk of incomplete fixes or introducing new issues while maintaining the systematic approach for PR creation. \ No newline at end of file diff --git a/.cursor/rules/coding-workflow.mdc b/.cursor/rules/coding-workflow.mdc deleted file mode 100644 index a5b8a37a..00000000 --- a/.cursor/rules/coding-workflow.mdc +++ /dev/null @@ -1,19 +0,0 @@ ---- -description: Coding workflow cursor must follow -globs: -alwaysApply: false ---- -# Coding workflow preferences -- Focus on the areas of code relevant to the task -- Do not touch code that is unrelated to the task -- Avoid making major changes to the patterns and architecture of how a feature works, after it has shown to work well, unless explicitly instructed -- Always think about what other methods and areas of code might be affected by code changes -- Keep code simple and readable -- Write thorough tests for all functionalities you wrote - -Follow this sequential workflow: -1. Write or update existing code -2. Write the incremental unit-test to cover code logic you wrote -3. Test unit-test pass -4. Verify it passes all the tests by running `make test` command -5. Ensue your unit-test has good code coverage for the code you have written diff --git a/.cursor/rules/github/pr-template-format.mdc b/.cursor/rules/github/pr-template-format.mdc deleted file mode 100644 index bcdac6de..00000000 --- a/.cursor/rules/github/pr-template-format.mdc +++ /dev/null @@ -1,59 +0,0 @@ ---- -description: -globs: -alwaysApply: false ---- ---- -description: Follow PR template format from .github/pull_request_template.md when creating pull request descriptions -globs: "**/*" -alwaysApply: false ---- - -# GitHub Pull Request Template Format - -When creating pull request descriptions, you **must** follow the format specified in `.github/pull_request_template.md` if it exists in the repository. - -## Rule Requirements - -1. **Check for Template**: Always check if `.github/pull_request_template.md` exists in the repository root before creating PR descriptions. - -2. **Use Template Structure**: If the template exists: - - Follow the exact section structure defined in the template - - Include all required sections from the template - - Maintain the same heading levels and formatting style - - Preserve any placeholder text guidelines or instructions - - Fill in the template sections with relevant information for the specific PR - -3. **Template Sections**: Common sections that should be preserved if present in the template include: - - **Summary/Description**: Brief overview of the changes - - **Changes Made**: Detailed list of modifications - - **Testing**: How the changes were tested - - **Type of Change**: Bug fix, feature, documentation, etc. - - **Checklist**: Action items or verification steps - - **Breaking Changes**: Any backward compatibility concerns - - **Related Issues**: Links to related issues or tickets - -4. **Fallback Behavior**: If no template exists, create a well-structured PR description with: - - Clear summary of changes - - Bullet points for key modifications - - Testing information if applicable - - Any relevant context or notes - -## Implementation Guidelines - -- **Read Template First**: Use tools to read the `.github/pull_request_template.md` file content before generating PR descriptions -- **Preserve Formatting**: Maintain markdown formatting, comments, and structure from the template -- **Fill Appropriately**: Replace template placeholders with actual, relevant information -- **Be Comprehensive**: Ensure all template sections are addressed, even if briefly -- **Stay Consistent**: Use the same tone and style as indicated by the template - -## Example Usage - -When creating a PR: -1. Check for `.github/pull_request_template.md` -2. If found, read the template content -3. Generate PR description following the template structure -4. Fill in each section with relevant information -5. Ensure all required sections are included - -This rule ensures consistency across all pull requests in repositories that have established PR templates, while providing a sensible fallback for repositories without templates. diff --git a/.cursor/rules/golang/coding-rules.mdc b/.cursor/rules/golang/coding-rules.mdc deleted file mode 100644 index 125cceb3..00000000 --- a/.cursor/rules/golang/coding-rules.mdc +++ /dev/null @@ -1,27 +0,0 @@ ---- -description: Rules to follow when writing code -globs: *.go ---- -You are Staff Software Engineer expert in Golang, Protobuff, GRPC. You write clean and properly docummented code. You ensure code written works, and you always write corresponding Unit-tests. -You are an expert in writing Unit-test, and specialised in updating existing unit-test to fit missing use-cases. - -# Coding pattern preferences -- Always prefer simple solutions. -- Keep the codebase very clean and organized. -- Avoid duplication of code whenever possible, which means checking for other areas of the codebase that might already have similar code and functionality. -- Write code that takes into account the different environments: development, staging, and production. -- You are careful to only make changes that are requested or you are confident are well understood and related to the change being requested. -- When fixing an issue or bug, do not introduce a new pattern or technology without first exhausting all options for the existing implementation. And if you finally do this, make sure to remove the old ipmlementation afterwards so we don't have duplicate logic. -- Avoid having files over 200-300 lines of code. Refactor at that point. -- Mocking data is only needed for tests, never mock data for dev or prod. -- Avoid writing scripts in files if possible, especially if the script is likely only to be run once. -- Never add stubbing or fake data patterns to code that affects the dev or prod environments. -- Never overwrite config *.yml (Yaml) files without first asking and confirming. -- It is acceptable to say you do not know. -- Do not write your own mocks in testing. Follow this sequence: -1. Update Makefile to generate the mock needed. - Following the example for internal packages - `mockgen -source=internal/client/users_service.go -destination=internal/dao/mocks/mock_users_service.go -package=mockdao` - and for external packages follow this: - ` mockgen -destination=mocks/eth_client_mock.go -mock_names EthClient=MockEthClient -package=mocks github.cbhq.net/intl/rip7755-fulfiller/internal/client EthClientmockgen` -2. Update testing code to use the generated mock diff --git a/.cursor/rules/golang/english-and-comments-std.mdc b/.cursor/rules/golang/english-and-comments-std.mdc deleted file mode 100644 index c35cf56f..00000000 --- a/.cursor/rules/golang/english-and-comments-std.mdc +++ /dev/null @@ -1,49 +0,0 @@ ---- -description: Make sure to always add clear comments within the codebase -globs: *.go -alwaysApply: true ---- -# Standard: Language, Comments, and Documentation - -This rule defines the standards for language use and commenting within the codebase, prioritizing clarity and developer experience for engineers familiar with the project. - -**1. Language:** - -* **All** code artifacts, including variable names, function names, comments, documentation, commit messages, and generated rules, **must** be written in **simple, clear, friendly and idiomatic English**. - -**2. Comments:** - -* **Target Audience:** Assume the reader is an experienced developer familiar with Go and the general project context. -* **Prioritize Readability:** Code should be self-documenting whenever possible through clear naming and structure. -* **Avoid Redundant Comments:** Do **not** add comments that merely restate what the code clearly does. For example: - ```go - // Bad: Comment explains the obvious - // Increment count - count++ - - // Good: Comment starts with the function name - // GetUserByID finds a user by their ID - func GetUserByID(id string) (*User, error) { ... } - ``` -* **Focus on the "Why", Not Just the "What":** Prioritize comments that explain *why* a particular approach was taken, especially if it's non-obvious, involves trade-offs, or relates to external factors or historical context. - * Explain complex logic or algorithms briefly. - * Clarify the purpose of seemingly arbitrary values or constants. - * Document known limitations, potential issues, or future work (`TODO`, `FIXME`). - * Add comments when fixing subtle bugs to explain the reasoning. - ```go - // Good: Explains the rationale for a non-obvious choice - // Use FNV-1a hash for Redis hash tags to optimize for speed and key length, - // as cryptographic security is not required for slot assignment. - func hashUserIDForTag(userID string) string { ... } - - // Good: Explains a workaround or limitation - // TODO(GH-123): Refactor this when the upstream API supports batch requests. - for _, item := range items { ... } - ``` -* **Placement:** Place comments on the line *before* the code they refer to, or sometimes at the end of a line for very short clarifications. - -**3. Documentation (e.g., READMEs, Design Docs):** - -* Maintain clarity and conciseness. -* Keep documentation up-to-date with significant code changes. -* Use diagrams or examples where appropriate to illustrate complex concepts. diff --git a/.cursor/rules/golang/golang-naming-std.mdc b/.cursor/rules/golang/golang-naming-std.mdc deleted file mode 100644 index 25a1e963..00000000 --- a/.cursor/rules/golang/golang-naming-std.mdc +++ /dev/null @@ -1,69 +0,0 @@ ---- -description: Avoid package prefix redundancy when naming -globs: *.go -alwaysApply: false ---- -# Go Standard: Naming Conventions - Avoid Package Prefix Redundancy - -This rule outlines the standard Go practice for naming exported identifiers to avoid redundancy with the package name. - -**The Standard:** - -When naming exported identifiers (types, functions, variables, constants), **avoid repeating the package name** if the context provided by the package itself makes the identifier clear. - -**Rationale:** - -* **Readability:** Code that imports the package becomes cleaner and less verbose. For example, `store.New()` is more idiomatic and readable than `store.NewStore()`. -* **Conciseness:** Reduces unnecessary stuttering in code (e.g., `store.StoreType` vs. `store.Type`). -* **Idiomatic Go:** Follows the common practice seen in the Go standard library and many popular Go projects. - -**Examples:** - -```go -// --- Package: store --- - -// BAD: Repeats "store" -package store - -type StoreConfig struct { ... } -func NewStore(cfg StoreConfig) (*Store, error) { ... } -var DefaultStoreOptions StoreOptions - -// GOOD: Avoids repeating "store" -package store - -type Config struct { ... } // Type name is clear within package 'store' -func New(cfg Config) (*Store, error) { ... } // Function name 'New' is clear -var DefaultOptions Options // Variable name is clear - -type Store struct { ... } // OK: Identifier itself IS the package name conceptually -``` - -When importing and using the "good" example: - -```go -import "path/to/store" - -// ... -cfg := store.Config{ ... } -activityStore, err := store.New(cfg) -opts := store.DefaultOptions -var s store.Store -``` - -This reads much better than: - -```go -import "path/to/store" - -// ... -cfg := store.StoreConfig{ ... } -activityStore, err := store.NewStore(cfg) -opts := store.DefaultStoreOptions -var s store.Store -``` - -**Exceptions:** - -* It is acceptable if the identifier itself essentially *is* the package name (e.g., `package http; type Client`, `package store; type Store`). -* Sometimes repeating a part of the package name is necessary for clarity if the package has many distinct concepts. diff --git a/.cursor/rules/golang/golang-use-getters.mdc b/.cursor/rules/golang/golang-use-getters.mdc deleted file mode 100644 index 149f3802..00000000 --- a/.cursor/rules/golang/golang-use-getters.mdc +++ /dev/null @@ -1,53 +0,0 @@ ---- -description: Prefer getter methods over direct field access -globs: *.go -alwaysApply: false ---- -# Go Standard: Prefer Getter Methods (Especially for Protobuf) - -This rule encourages the use of getter methods over direct field access, particularly when working with structs generated from Protobuf definitions. - -**The Standard:** - -**Prefer using generated getter methods (e.g., `myProto.GetMyField()`) over direct field access (e.g., `myProto.MyField`) when such getters are available.** This is especially relevant for structs generated by the Protobuf compiler (`protoc-gen-go`). - -**Rationale:** - -* **Encapsulation and Nil Safety:** Getters often provide a layer of abstraction. For Protobuf messages, getters automatically handle `nil` checks for pointer fields (like optional message fields or fields within a `oneof`), returning a zero value instead of causing a panic. This significantly improves robustness. -* **Consistency:** Using getters consistently makes the code easier to read and maintain, aligning with common Go practices for Protobuf. -* **Future-Proofing:** Relying on the getter method decouples the calling code from the exact internal representation of the field. If the underlying field changes in a backward-compatible way (e.g., how a default value is handled), code using the getter is less likely to break. -* **Helper Logic:** Getters might potentially include minor logic (though less common in basic Protobuf getters beyond nil checks). - -**Example (Protobuf):** - -```protobuf -// -- Example.proto -- -message UserProfile { - optional string name = 1; - optional int32 age = 2; -} -``` - -```go -// -- Go code -- -import "path/to/gen/go/examplepb" - -func processProfile(profile *examplepb.UserProfile) { - // GOOD: Uses getter, safe even if profile or profile.Name is nil. - name := profile.GetName() - age := profile.GetAge() // Also handles potential nil receiver safely. - - // BAD: Direct access risks nil pointer dereference if profile is non-nil - // but profile.Name is nil (for optional fields). - // nameDirect := *profile.Name // PANICS if profile.Name is nil! - // ageDirect := *profile.Age // PANICS if profile.Age is nil! - - fmt.Printf("Name: %s, Age: %d\n", name, age) -} -``` - -**Exceptions:** - -* **No Getter Available:** If a struct field does not have a corresponding getter method, direct access is necessary. -* **Performance Critical Code:** In extremely rare, performance-critical sections where profiling has demonstrably shown the function call overhead of the getter to be a bottleneck, direct access *might* be considered cautiously. This should be well-documented and justified. -* **Setting Values:** This rule applies to *reading* values. Setting struct fields typically involves direct assignment. diff --git a/.cursor/rules/golang/grpcclient.mdc b/.cursor/rules/golang/grpcclient.mdc deleted file mode 100644 index 5dac8a63..00000000 --- a/.cursor/rules/golang/grpcclient.mdc +++ /dev/null @@ -1,17 +0,0 @@ ---- -description: Creating a new grpc connection should use csf's grpcclient, not golang/grpc -globs: *.go -alwaysApply: false ---- -Pull in the package from github.cbhq.net/engineering/csf/grpcclient - -Here is an example of how you should implement it -``` - manager := csf.New() - ctx := manager.ServiceContext() - conn, err := grpcclient.Dial( - ctx, - endpoint, - grpcclient.WithDialOpt(grpc.WithBlock()), - ) -``` \ No newline at end of file diff --git a/.cursor/rules/observability/logging-best-practices.mdc b/.cursor/rules/observability/logging-best-practices.mdc deleted file mode 100644 index 52e94b5a..00000000 --- a/.cursor/rules/observability/logging-best-practices.mdc +++ /dev/null @@ -1,255 +0,0 @@ ---- -description: Rules to follow when logging -globs: ["*.go", "*.py"] ---- -## Rule -Ensure all logging statements are correct, useful, performant, and secure. Logs must accurately reflect the code's logic, provide sufficient context for debugging, avoid performance degradation, and never expose sensitive information. Use appropriate log levels and avoid logging sensitive data, large objects, or high-frequency debug information. - -Refer to [logging best practices](https://docs.cbhq.net/infra/observability/logging) for more details. - -## Scope -This rule ONLY APPLIES when ALL the following conditions are met: -1. Code contains logging statements (logger.debug, log.info, console.log, etc.) -2. The logging exhibits one or more of these problematic patterns (tagged with Defect Patterns and scenarios): - - **(SS-1) Logging Sensitive Data:** Exposing credentials, tokens, PII, or financial data (passwords, API keys, email addresses). - - **(SS-2) Logging Unsanitized Objects:** Logging entire request/response bodies or complex objects without removing sensitive fields. - - **(PF-1) Logging in High-Frequency Loops:** Placing log statements inside tight loops or on other hot paths like function entry/exit logging for every function, causing excessive volume. - - **(LV-1) Improper Log Level:** Using `DEBUG` or `TRACE` in non-development environments. Using `WARN` or `ERROR` for informational messages. - - **(SM-1) Unit/Metric Mismatch:** The unit in the log message (e.g., "ms") does not match the variable's actual unit (e.g., nanoseconds). - - **(SM-2) Message Contradicts Logic:** The log message misrepresents the code's state (e.g., logging "Success" in an `if err!= nil` block). - - **(IS-1) Insufficient Context:** An error log is not actionable because it omits the `error` variable or critical identifiers (e.g., `transaction_id`). - - **(VR-2) Placeholder-Argument Mismatch:** The number of placeholders in a format string (e.g., `%s`) does not match the number of provided variables. - - **(RD-3) Vague or Ambiguous Message:** The log message is too generic to be useful (e.g., "Done", "Error"). - -| Defect Patterns | Scenario Name | -| --- | --- | -| **RD:** Readability Issues | RD-1: Complicated domain-specific terminology
RD-2: Non-standard language used
RD-3: Poorly formatted or unclear messages | -| **VR:** Variable Issues | VR-1: Incorrect variable value logging
VR-2: Placeholder–value mismatch | -| **LV:** Logging Level Issues | LV-1: Improper verbosity level | -| **SM:** Semantics Inconsistent | SM-1: Wrong unit or metric label
SM-2: Message text does not match the code
SM-3: Misused variables in the message | -| **SS:** Sensitive Information | SS-1: Credentials logged in plain text
SS-2: Dumping whole objects without scrubbing | -| **IS:** Insufficient Information | IS-1: Insufficient information | -| **PF:** Performance Issues | PF-1: Logging on hot path
PF-2: Costly string operations | - -## Out of Scope -This rule does NOT apply to: -- ERROR and WARN level logs for genuine issues -- INFO level logs for significant business events -- Structured logging that includes only relevant, non-sensitive fields. -- Logs that are properly sampled or rate-limited -- Test files or development-only code -- Logs that are conditionally enabled for debugging - -## Good Examples - -1. Appropriate log levels with structured data: -```go -logger.Info("User login successful", - "user_id", userID, - "login_method", "oauth", - "ip_address", clientIP, -) -``` - -2. ERROR logs with context but no sensitive data: -```go -logger.Error("Payment processing failed", - "error_code", "INSUFFICIENT_FUNDS", - "transaction_id", txnID, - "user_id", userID, - "retry_count", retryCount, -) -``` -```go -user, err := db.FindUser(userID) -if err!= nil { - logger.Error("Failed to find user", - "error", err, - "user_id", userID, - "trace_id", traceID, - ) - return -} -``` - - -3. Conditional debug logging: -```go -if config.DebugEnabled { - logger.Debug("Processing order for user", "user_id", userID) -} -``` - -4. Sampling high-frequency events: -```go -// Only log 1% of successful requests -if rand.Float32() < 0.01 { - logger.Info("Request processed successfully", - "endpoint", endpoint, - "duration_ms", duration.Milliseconds(), - ) -} -``` - -5. Structured logging with relevant fields: -```go -logger.Info("Order processed", - "order_id", order.ID, - "user_tier", user.Tier, - "payment_method", "card", - "processing_time_ms", processingTime, -) -``` - -6. Summarizing after a loop instead of logging within it: -```go -processedCount := 0 -for _, item := range items { - if err := process(item); err == nil { - processedCount++ - } -} -logger.Info("Batch processing complete", - "total_items", len(items), - "processed_count", processedCount, -) -``` - -7. Logging specific, safe fields instead of a whole object: -```go -logger.Info("User profile updated", - "user_id", user.ID, - "user_tier", user.Tier, - "updated_fields", updatedFields, -) -``` - -8. Ensuring unit consistency in logs -```go -//... operation... -duration := time.Since(startTime) -logger.Info("Request processed", - "duration_ms", duration.Milliseconds(), -) -``` - -## Bad Examples - -1. (LV-1) DEBUG logging in production code: -```go -// DEBUG logs create excessive volume -logger.Debug("Entering function processPayment") -logger.Debug("Validating payment request") -logger.Debug("Connecting to payment gateway") -logger.Debug("Exiting function processPayment") -``` - -2. (PF-1) Logging inside loops: -```go -// Creates massive log volume -for _, item := range items { - logger.Info("Processing item", "item_id", item.ID) - // ... process item -} -``` - -3. (SS-2) Logging entire objects or request bodies: -```go -// Logs potentially sensitive data and large objects -logger.Info("Received request", "request_body", fmt.Sprintf("%+v", requestBody)) -logger.Info("User object", "user", fmt.Sprintf("%+v", user)) -``` -```go -// BAD: Logs the entire user object, potentially exposing PII. -logger.Info("User object details", "user", fmt.Sprintf("%+v", user)) -``` - -4. (SS-1) Logging sensitive information: -```go -// Exposes sensitive data in logs -logger.Info("Authentication attempt", - "email", user.Email, - "password", password, - "api_key", apiKey, - "token", authToken, - "auth_token", authToken, - "bearer_token", bearerToken, - "credit_card", creditCard, -) -``` - -5. (PF-1) Function entry/exit logging everywhere: -```go -// Excessive noise for every function -func calculateTotal(items []Item) float64 { - logger.Debug("Entering calculateTotal") - total := 0.0 - for _, item := range items { - total += item.Price - } - logger.Debug("Exiting calculateTotal", "total", total) - return total -} -``` - -6. (SS-1) Logging URLs with sensitive information: -```go -// Exposes sensitive token in URL parameter -logger.Info("API request", "url", fmt.Sprintf("/api/users/%s/payments/%s?token=%s", userID, paymentID, token)) -``` - -7. (SM-2) Message contradicts code logic: -```go -// BAD: A success message is logged in the error-handling block. -err := processPayment(paymentDetails) -if err!= nil { - logger.Info("Payment processed successfully", "transaction_id", paymentDetails.ID) - //... handle error... -} -``` - -8. (IS-1) Insufficient context in an error log: -```go -// BAD: The log is not actionable because it omits the actual 'err' variable. -err := db.Save(user) -if err!= nil { - logger.Error("Failed to save user to database", "user_id", user.ID) -} -``` - -9. (SM-1) Unit mismatch in the log message: -```go -// BAD: The log text claims "ms" but the variable is in nanoseconds. -startTime := time.Now() -//... operation... -durationNanos := time.Since(startTime).Nanoseconds() -logger.Info(fmt.Sprintf("Task completed in %d ms", durationNanos)) -``` - -10. (VR-2) Placeholder-argument mismatch: -```go -// BAD: Two placeholders but only one argument provided. -logger.Error(fmt.Sprintf("Login for user %s from %s failed", userID)) -``` - - -## Evaluation Process -1. Identify all logging statements in the code -2. Check log levels - flag DEBUG logs that aren't conditionally enabled -3. Analyze control flow - for each logging statement, analyze its surrounding code to understand the logical context (e.g., is it inside an error-handling block like if err!= nil, a success path, or a loop?). Look for logging patterns inside loops or high-frequency operations -4. Extract semantic intent - use natural language understanding to determine the meaning of the static log message text (e.g., does it imply success, failure, or a status update?). -5. Correlate Logic and Intent - Compare the code's logical context with the message's semantic intent. Flag contradictions (e.g., a "success" message in a failure block - SM-2). -6. Analyze Log Content and Variables: - - Flag the logging of entire un-sanitized objects, request bodies, or other large data structures (SS-2). - - Scan for sensitive data patterns (passwords, keys, PII) in variable names, message text, and URL parameters (SS-1). - - URLs with parameters that might contain sensitive data - - For metric variables (e.g., duration), verify consistency between the variable's actual unit and the unit stated in the log message (SM-1). - - In error-handling blocks, verify that the `error` variable itself is being logged to prevent IS-1. - - Check for function entry/exit logging patterns -7. For each problematic pattern, suggest alternatives: - - Use appropriate log levels (ERROR, WARN, INFO) - - Sample high-frequency logs - - Log only relevant fields instead of entire objects - - Use structured logging with sanitized data - - Move detailed debugging to distributed tracing - - Rate-limit repetitive logs diff --git a/.cursor/rules/observability/metrics-best-practices.mdc b/.cursor/rules/observability/metrics-best-practices.mdc deleted file mode 100644 index d2230570..00000000 --- a/.cursor/rules/observability/metrics-best-practices.mdc +++ /dev/null @@ -1,136 +0,0 @@ ---- -description: Rules to follow when emitting metrics -globs: ["*.go", "*.py"] ---- -## Rule -Do not use high-cardinality values as tags when emitting metrics. High-cardinality tags can significantly increase observability costs and impact system performance. Every tag that contains an ID must be flagged. - -Refer to [tagging strategy](https://docs.cbhq.net/infra/observability/metrics#tagging-strategy) for more details. - -## Scope -This rule applies to ANY metric emission found ANYWHERE in the code changes, regardless of the primary purpose of the PR. ALL metric emission calls (statsd) must be checked for violations. - -Violation happens when ALL the following conditions are met: -1. Code is emitting metrics to a monitoring system (Datadog, StatsD, etc.) -2. Tags or labels are being added to the metric -3. The tag values contain AT LEAST ONE high-cardinality. - -### High-cardinality Guidelines: -A tag value is considered high-cardinality if it falls into any of these categories: -- **Looks unique** – anything that sounds like it will generate a one-off value (e.g., id, uuid, token, session, generated_x) -- **Continuous values** – non-discrete numbers that can vary infinitely, like current_price, latitude, longitude, sensor readings, etc. -- **High entropy values** – anything random or cryptographic such as random, hash, sha1, md5, encrypted, signature - -### Common High-cardinality Examples: -- **Identifiers**: User IDs, customer IDs, account identifiers (`user_id`, `customer_id`, `account_id`) -- **Request tracking**: Request IDs, trace IDs, transaction IDs (`message_id`, `request_id`, `trace_id`) -- **Pattern-based keys**: Any tag key ending with `_id`, `_uuid`, `_token` -- **Time-based values**: Timestamps or time-based values -- **Network data**: URLs with parameters, dynamic paths, IP addresses, specific hostnames -- **Unique identifiers**: UUIDs, hashes, or other unique identifiers -- **Personal data**: Email addresses or user-specific data - -## Good Examples - -1. Using low-cardinality status codes: -```go -statsdClient.CountWithTags("api.requests_total", 1, map[string]string{ - "method": "GET", - "status": "200", - "endpoint": "/api/users", -}) -``` - -2. Building tags separately with low-cardinality values: -```go -tags := map[string]string{ - "method": "GET", - "status": "200", - "endpoint": "/api/users", -} -statsdClient.CountWithTags("api.requests_total", 1, tags) -``` - -3. Using bounded categorical values: -```go -statsdClient.CountWithTags("payment.processed", 1, map[string]string{ - "payment_method": "card", // Limited values: card, bank, crypto - "region": "us-east-1", // Limited AWS regions - "user_tier": "premium", // Limited tiers: basic, premium, enterprise -}) -``` - -4. Aggregating instead of individual IDs: -```go -// Instead of user_id tag, use aggregated user tier -statsdClient.GaugeWithTags("user.active_sessions", sessionCount, map[string]string{ - "user_tier": getUserTier(userID), // Low cardinality - "region": "us-west-2", -}) -``` - -## Bad Examples - -1. Using user IDs or message IDs as tags: -```go -// VIOLATION: user_id and message_id are high-cardinality IDs -statsd.IncrWithTags(statsdUSTEventProcessingFailure, map[string]string{ - "message_id": messageID, // VIOLATION: message_id is high-cardinality - "error_type": "nil_body", -}) -``` - -2. Building tags separately with ID values: -```go -// VIOLATION: Still problematic when tags are built separately -tags := map[string]string{ - "user_id": userID, // VIOLATION: user_id is high-cardinality - "message_id": messageID, // VIOLATION: message_id is high-cardinality -} -statsd.HistogramWithTags(statsdUSTRepositoryOperationTime, value, tags) -``` - -3. Using request IDs or trace IDs: -```go -// VIOLATION: Request IDs are unique for every request -statsdClient.TimingWithTags("api.response_time", duration, map[string]string{ - "request_id": "req_abc123def456", // VIOLATION: request_id is high-cardinality - "trace_id": "7d5d747be160e280504c099d984bcfe0", // VIOLATION: trace_id is high-cardinality -}) -``` - -4. Using timestamps as tags: -```go -// VIOLATION: Timestamps create unlimited unique values -stats.CountWithTags("queue.length", 1, map[string]string{ - "timestamp": time.Now().Format("2006-01-02T15:04:05"), // VIOLATION: timestamp is high-cardinality - "queue_name": "payments", -}) -``` - -5. Using IP addresses: -```go -// VIOLATION: IP addresses have very high cardinality -statsd.GaugeWithTags("connections.active", 1, map[string]string{ - "client_ip": "192.168.1.100", // VIOLATION: IP address is high-cardinality - "hostname": "server-abc123-def456", // VIOLATION: Dynamic hostnames are high-cardinality -}) -``` - -## Evaluation Process -1. Search the entire code change for ANY calls to `statsd` function that ends with `WithTags` (`statsd.*WithTags`) or tags map building, regardless of the PR's stated purpose -2. For each metric emission found, examine ALL tag keys in the map -3. If ANY tag key contains high-cardinality patterns, flag as violation. Examples for violations: - - `user_id`, `message_id`, `request_id`, `trace_id`, `session_id`, `customer_id` - - Any key ending with `_id`, `_uuid`, `_token` - - Timestamps, IP addresses, URLs with parameters -4. Do not skip metric calls because they seem unrelated to the main PR purpose -5. Check ANY function that contains these strings in its name: `CountWithTags`, `IncrWithTags`, `GaugeWithTags`, `HistogramWithTags`, `TimingWithTags`, `DistributionWithTags`. This includes methods from any package or client (e.g., `statsd.CountWithTags()`, `client.IncrWithTags()`, `metrics.GaugeWithTags()`, etc.) - -**Example**: Even if a PR is titled "Add configuration options", still check ALL metric emissions like: -```go -statsd.IncrWithTags(metric, map[string]string{ - "message_id": messageID, // VIOLATION - Must be flagged - "error_type": "nil_body", -}) -``` diff --git a/.cursor/rules/python/api-design.mdc b/.cursor/rules/python/api-design.mdc deleted file mode 100644 index 58f975f1..00000000 --- a/.cursor/rules/python/api-design.mdc +++ /dev/null @@ -1,28 +0,0 @@ -# .cursor/rules/python/api-design.mdc ---- -description: API design principles for Python microservices -globs: ["*.py"] -alwaysApply: true ---- -# API Design Principles - -## REST API Design -- Use proper HTTP methods (GET, POST, PUT, DELETE) -- Implement proper status codes -- Use plural nouns for resource endpoints -- Version APIs in the URL (e.g., /v1/resources) -- Use query parameters for filtering and pagination - -## Request/Response -- Validate all input data -- Use Pydantic models for request/response schemas -- Include proper error responses -- Implement pagination for list endpoints -- Use consistent response formats - -## Security -- Implement proper authentication -- Use JWT for stateless authentication -- Implement rate limiting -- Validate and sanitize all inputs -- Use HTTPS only \ No newline at end of file diff --git a/.cursor/rules/python/dependency-management.mdc b/.cursor/rules/python/dependency-management.mdc deleted file mode 100644 index a02d227f..00000000 --- a/.cursor/rules/python/dependency-management.mdc +++ /dev/null @@ -1,21 +0,0 @@ -# .cursor/rules/python/dependency-management.mdc ---- -description: Package and dependency management guidelines -globs: ["pyproject.toml", "requirements.txt", "setup.py"] -alwaysApply: true ---- -# Dependency Management - -## Package Management -- Use Poetry for dependency management -- Pin all dependencies with exact versions -- Use separate dependency groups for dev and prod -- Regular security audits with safety -- Keep dependencies up to date - -## Virtual Environments -- Use virtual environments for all projects -- Document Python version requirements -- Use .env files for environment variables -- Keep production dependencies minimal -- Document all third-party integrations \ No newline at end of file diff --git a/.cursor/rules/python/distributed-computing.mdc b/.cursor/rules/python/distributed-computing.mdc deleted file mode 100644 index 0b727edd..00000000 --- a/.cursor/rules/python/distributed-computing.mdc +++ /dev/null @@ -1,25 +0,0 @@ -# .cursor/rules/python/distributed-computing.mdc ---- -description: Guidelines for distributed computing with Ray -globs: ["*.py"] -alwaysApply: true ---- -# Distributed Computing Standards - -## Ray Framework Usage -- Use Ray for distributed task processing -- Implement proper actor patterns -- Use Ray's object store effectively -- Handle distributed errors properly - -## Scaling Patterns -- Implement proper auto-scaling -- Use resource management effectively -- Handle node failures gracefully -- Implement proper checkpointing - -## Performance -- Use Ray's performance monitoring -- Implement proper batching -- Use Ray's memory management -- Profile distributed operations \ No newline at end of file diff --git a/.cursor/rules/python/fastapi-standards.mdc b/.cursor/rules/python/fastapi-standards.mdc deleted file mode 100644 index a83a666f..00000000 --- a/.cursor/rules/python/fastapi-standards.mdc +++ /dev/null @@ -1,41 +0,0 @@ -# .cursor/rules/python/fastapi-standards.mdc ---- -description: FastAPI-specific development standards and patterns -globs: ["*.py"] -alwaysApply: true ---- -# FastAPI Development Standards - -## Project Structure -- Use the following directory structure: - ``` - app/ - ├── api/ - │ └── v1/ - │ └── endpoints/ - ├── core/ - │ ├── config.py - │ └── security.py - ├── models/ - ├── schemas/ - └── services/ - ``` - -## FastAPI Best Practices -- Use dependency injection for service dependencies -- Implement proper exception handlers -- Use background tasks for async operations -- Implement proper middleware chain -- Use FastAPI's built-in OpenAPI support - -## Performance Optimization -- Use async/await properly -- Implement caching strategies -- Use connection pooling for databases -- Implement proper background tasks - -## Security -- Use FastAPI's security dependencies -- Implement proper CORS policies -- Use rate limiting -- Implement proper authentication middleware \ No newline at end of file diff --git a/.cursor/rules/python/infrastructure.mdc b/.cursor/rules/python/infrastructure.mdc deleted file mode 100644 index d9bd974d..00000000 --- a/.cursor/rules/python/infrastructure.mdc +++ /dev/null @@ -1,25 +0,0 @@ -# .cursor/rules/python/infrastructure.mdc ---- -description: Infrastructure and deployment standards -globs: ["*.py", "Dockerfile", "*.yaml"] -alwaysApply: true ---- -# Infrastructure Standards - -## Containerization -- Use multi-stage Docker builds -- Implement proper health checks -- Use non-root users -- Follow container security best practices - -## Kubernetes -- Use proper resource requests/limits -- Implement proper probes -- Use proper service mesh integration -- Follow GitOps practices - -## CI/CD -- Implement proper testing stages -- Use proper security scanning -- Implement proper deployment strategies -- Use proper environment separation \ No newline at end of file diff --git a/.cursor/rules/python/microservices-architecture.mdc b/.cursor/rules/python/microservices-architecture.mdc deleted file mode 100644 index e4352957..00000000 --- a/.cursor/rules/python/microservices-architecture.mdc +++ /dev/null @@ -1,36 +0,0 @@ -# .cursor/rules/python/microservices-architecture.mdc ---- -description: Guidelines for Python microservice architecture -globs: ["*.py"] -alwaysApply: true ---- -# Microservice Architecture Guidelines - -## Service Design -- Keep services small and focused on a single business capability -- Use FastAPI for HTTP APIs -- Use gRPC for internal service communication -- Implement health check endpoints -- Use OpenAPI/Swagger for API documentation -- Use proper service discovery -- Implement proper circuit breaking -- Use proper message queuing -- Implement proper retry policies - -## Configuration -- Use environment variables for service configuration -- Store secrets in a secure vault (e.g., HashiCorp Vault) -- Use configuration management for different environments -- Implement feature flags for gradual rollouts - -## Observability -- Implement structured logging using `structlog` -- Use OpenTelemetry for distributed tracing -- Implement metrics using Prometheus -- Set up proper monitoring dashboards - -## Resilience -- Implement circuit breakers for external calls -- Use retries with exponential backoff -- Implement rate limiting -- Handle partial failures gracefully \ No newline at end of file diff --git a/.cursor/rules/python/python-coding-rules.mdc b/.cursor/rules/python/python-coding-rules.mdc deleted file mode 100644 index 34e99a74..00000000 --- a/.cursor/rules/python/python-coding-rules.mdc +++ /dev/null @@ -1,32 +0,0 @@ -# .cursor/rules/python/python-coding-rules.mdc ---- -description: Python coding standards and best practices -globs: ["*.py"] -alwaysApply: true ---- -# Python Coding Standards - -## Code Style -- Follow PEP 8 style guide -- Use type hints for all function parameters and return values -- Maximum line length: 88 characters (Black formatter standard) -- Use descriptive variable names that reflect their purpose -- Use docstrings for all public modules, functions, classes, and methods - -## Project Structure -- Use src-layout pattern for all Python packages -- Separate business logic from API handlers -- Keep modules focused and single-responsibility -- Use absolute imports over relative imports - -## Error Handling -- Use custom exceptions for domain-specific errors -- Always include meaningful error messages -- Handle exceptions at appropriate levels -- Log errors with proper context - -## Best Practices -- Use dataclasses or Pydantic models for data structures -- Implement proper logging with structured data -- Use environment variables for configuration -- Follow the principle of least privilege \ No newline at end of file diff --git a/.cursor/rules/python/testing-standards.mdc b/.cursor/rules/python/testing-standards.mdc deleted file mode 100644 index 84638166..00000000 --- a/.cursor/rules/python/testing-standards.mdc +++ /dev/null @@ -1,28 +0,0 @@ -# .cursor/rules/python/testing-standards.mdc ---- -description: Testing requirements for Python microservices -globs: ["*_test.py", "test_*.py"] -alwaysApply: true ---- -# Testing Standards - -## Unit Tests -- Use pytest as the testing framework -- Maintain minimum 80% code coverage -- Mock external dependencies -- Use fixtures for test data -- Test both success and error cases - -## Integration Tests -- Test service integrations -- Use docker-compose for local testing -- Implement API tests using pytest-asyncio -- Test database interactions -- Verify message queue operations - -## Performance Tests -- Implement load tests using locust -- Test service scalability -- Measure response times -- Test rate limiting -- Verify resource usage \ No newline at end of file diff --git a/.cursor/rules/rego/rego-coding-patterns.mdc b/.cursor/rules/rego/rego-coding-patterns.mdc deleted file mode 100644 index 06695508..00000000 --- a/.cursor/rules/rego/rego-coding-patterns.mdc +++ /dev/null @@ -1,94 +0,0 @@ ---- -description: -globs: *.rego -alwaysApply: false ---- -# Common Rego Patterns and Idioms - -## Data Access Patterns -```rego -# Safe object access -value := object.get("key", "default") - -# Array iteration -result := [item | item := array[_]] - -# Set operations -combined := set_union(set1, set2) -``` - -## Control Flow -```rego -# Conditional logic -allow { - condition1 - condition2 -} else { - condition3 -} - -# Early returns -deny["reason"] { - not is_valid -} - -# Multiple conditions -allow { - count(violations) == 0 - is_authorized -} -``` - -## Common Functions -```rego -# Existence check -exists { - count(array) > 0 -} - -# Contains check -contains { - array[_] == value -} - -# All elements satisfy condition -all_valid { - count([x | x := array[_]; not is_valid(x)]) == 0 -} -``` - -## Testing Patterns -```rego -# Test case structure -test_allow_when_valid { - allow with input as {"valid": true} -} - -# Test helper functions -test_is_valid { - is_valid with input as {"value": "test"} -} -``` - -## Error Handling -```rego -# Error collection -errors[msg] { - not is_valid - msg := "Invalid input" -} - -# Multiple error messages -errors[msg] { - not is_authorized - msg := "Not authorized" -} -``` - -## Best Practices -1. Use helper functions for complex logic -2. Keep rules focused and single-purpose -3. Use meaningful variable names -4. Document complex logic with comments -5. Use consistent formatting -6. Break down complex conditions into smaller rules diff --git a/.dockerignore b/.dockerignore deleted file mode 100644 index 7b0473fd..00000000 --- a/.dockerignore +++ /dev/null @@ -1,29 +0,0 @@ -/target/ -**/target/ -/ui/node_modules -/ui/.next -.git -/data -*.log -.env -.env.docker -/ui/.env -.idea/ -.vscode/ -*.swp -*.swo -*~ -docs/ -README.md -*.md -!CLAUDE.md -.DS_Store -Thumbs.db -Dockerfile* -docker-compose*.yml -.dockerignore -.gitignore -*.tmp -*.temp -.claude/ -**/*.orig \ No newline at end of file diff --git a/.env.example b/.env.example index 316b3358..a18021b3 100644 --- a/.env.example +++ b/.env.example @@ -1,38 +1,3 @@ -# Ingress -TIPS_INGRESS_ADDRESS=0.0.0.0 -TIPS_INGRESS_PORT=8080 -TIPS_INGRESS_RPC_MEMPOOL=http://localhost:2222 -TIPS_INGRESS_TX_SUBMISSION_METHOD=mempool -TIPS_INGRESS_KAFKA_INGRESS_PROPERTIES_FILE=/app/docker/ingress-bundles-kafka-properties -TIPS_INGRESS_KAFKA_INGRESS_TOPIC=tips-ingress -TIPS_INGRESS_KAFKA_AUDIT_PROPERTIES_FILE=/app/docker/ingress-audit-kafka-properties -TIPS_INGRESS_KAFKA_AUDIT_TOPIC=tips-audit -TIPS_INGRESS_KAFKA_USER_OPERATION_CONSUMER_PROPERTIES_FILE=/app/docker/ingress-user-operation-consumer-kafka-properties -TIPS_INGRESS_LOG_LEVEL=info -TIPS_INGRESS_LOG_FORMAT=pretty -TIPS_INGRESS_SEND_TRANSACTION_DEFAULT_LIFETIME_SECONDS=10800 -TIPS_INGRESS_RPC_SIMULATION=http://localhost:8549 -TIPS_INGRESS_METRICS_ADDR=0.0.0.0:9002 -TIPS_INGRESS_HEALTH_CHECK_ADDR=0.0.0.0:8081 -TIPS_INGRESS_BLOCK_TIME_MILLISECONDS=2000 -TIPS_INGRESS_METER_BUNDLE_TIMEOUT_MS=2000 -TIPS_INGRESS_MAX_BUFFERED_METER_BUNDLE_RESPONSES=100 -TIPS_INGRESS_BUILDER_RPCS=http://localhost:2222,http://localhost:2222,http://localhost:2222 -TIPS_INGRESS_BACKRUN_ENABLED=true - -# Audit service configuration -TIPS_AUDIT_KAFKA_PROPERTIES_FILE=/app/docker/audit-kafka-properties -TIPS_AUDIT_KAFKA_TOPIC=tips-audit -TIPS_AUDIT_LOG_LEVEL=info -TIPS_AUDIT_LOG_FORMAT=pretty -TIPS_AUDIT_S3_BUCKET=tips -TIPS_AUDIT_S3_CONFIG_TYPE=manual -TIPS_AUDIT_S3_ENDPOINT=http://localhost:7000 -TIPS_AUDIT_S3_REGION=us-east-1 -TIPS_AUDIT_S3_ACCESS_KEY_ID=minioadmin -TIPS_AUDIT_S3_SECRET_ACCESS_KEY=minioadmin - -# TIPS UI NEXT_PUBLIC_BLOCK_EXPLORER_URL=https://base.blockscout.com TIPS_UI_RPC_URL=http://localhost:8549 TIPS_UI_AWS_REGION=us-east-1 @@ -40,4 +5,4 @@ TIPS_UI_S3_BUCKET_NAME=tips TIPS_UI_S3_CONFIG_TYPE=manual TIPS_UI_S3_ENDPOINT=http://localhost:7000 TIPS_UI_S3_ACCESS_KEY_ID=minioadmin -TIPS_UI_S3_SECRET_ACCESS_KEY=minioadmin \ No newline at end of file +TIPS_UI_S3_SECRET_ACCESS_KEY=minioadmin diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml deleted file mode 100644 index 849a3fd2..00000000 --- a/.github/workflows/docker.yml +++ /dev/null @@ -1,60 +0,0 @@ -name: Docker Build and Publish -permissions: - contents: read - packages: write - -on: - push: - branches: [master] - tags: ['v*'] - pull_request: - -env: - REGISTRY: ghcr.io - IMAGE_NAME: ${{ github.repository }} - -jobs: - docker-build: - name: Build and Publish Docker image - runs-on: ubuntu-latest - steps: - - name: Harden the runner (Audit all outbound calls) - uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1 - with: - egress-policy: audit - - - uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3.10.0 - - - name: Log in to GitHub Container Registry - if: github.event_name != 'pull_request' - uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0 - with: - registry: ${{ env.REGISTRY }} - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Extract metadata (tags, labels) - id: meta - uses: docker/metadata-action@902fa8ec7d6ecbf8d84d538b9b233a880e428804 # v5.7.0 - with: - images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} - tags: | - type=ref,event=branch - type=ref,event=pr - type=semver,pattern={{version}} - type=semver,pattern={{major}}.{{minor}} - type=sha - - - name: Build and push Docker image - uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0 - with: - context: . - platforms: linux/amd64 - push: ${{ github.event_name != 'pull_request' }} - tags: ${{ steps.meta.outputs.tags }} - labels: ${{ steps.meta.outputs.labels }} - cache-from: type=gha - cache-to: type=gha,mode=max diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml deleted file mode 100644 index 051baf62..00000000 --- a/.github/workflows/rust.yml +++ /dev/null @@ -1,81 +0,0 @@ -name: Rust -permissions: - contents: read - -on: - push: - branches: [ master ] - pull_request: - -env: - CARGO_TERM_COLOR: always - -jobs: - check: - name: Check - runs-on: ubuntu-latest - steps: - - name: Harden the runner (Audit all outbound calls) - uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1 - with: - egress-policy: audit - - - uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 - - uses: dtolnay/rust-toolchain@5d458579430fc14a04a08a1e7d3694f545e91ce6 # stable - - run: cargo check - - test: - name: Test - runs-on: ubuntu-latest - steps: - - name: Harden the runner (Audit all outbound calls) - uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1 - with: - egress-policy: audit - - - uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 - - uses: dtolnay/rust-toolchain@5d458579430fc14a04a08a1e7d3694f545e91ce6 # stable - - run: cargo test - - fmt: - name: Rustfmt - runs-on: ubuntu-latest - steps: - - name: Harden the runner (Audit all outbound calls) - uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1 - with: - egress-policy: audit - - - uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 - - uses: dtolnay/rust-toolchain@5d458579430fc14a04a08a1e7d3694f545e91ce6 # stable - with: - components: rustfmt - - run: cargo fmt --all -- --check - - clippy: - name: Clippy - runs-on: ubuntu-latest - steps: - - name: Harden the runner (Audit all outbound calls) - uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1 - with: - egress-policy: audit - - - uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 - - uses: dtolnay/rust-toolchain@5d458579430fc14a04a08a1e7d3694f545e91ce6 # stable - with: - components: clippy - - run: cargo clippy -- -D warnings -W clippy::uninlined_format_args - - build: - name: Build - runs-on: ubuntu-latest - steps: - - name: Harden the runner (Audit all outbound calls) - uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1 - with: - egress-policy: audit - - - uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0 - - uses: dtolnay/rust-toolchain@5d458579430fc14a04a08a1e7d3694f545e91ce6 # stable - - run: cargo build --release \ No newline at end of file diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml deleted file mode 100644 index bca290fb..00000000 --- a/.github/workflows/stale.yml +++ /dev/null @@ -1,32 +0,0 @@ -name: Mark stale issues and PRs - -on: - schedule: - - cron: '30 0 * * *' - workflow_dispatch: -permissions: - contents: read - -jobs: - stale: - runs-on: ubuntu-latest - permissions: - actions: write - issues: write - pull-requests: write - steps: - - name: Harden the runner (Audit all outbound calls) - uses: step-security/harden-runner@95d9a5deda9de15063e7595e9719c11c38c90ae2 # v2.13.2 - with: - egress-policy: audit - - - uses: actions/stale@5f858e3efba33a5ca4407a664cc011ad407f2008 # v10.1.0 - with: - days-before-stale: 14 - days-before-close: 5 - stale-issue-message: 'This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.' - stale-pr-message: 'This pull request has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.' - close-issue-message: 'This issue was closed because it has been inactive for 5 days since being marked as stale.' - close-pr-message: 'This pull request was closed because it has been inactive for 5 days since being marked as stale.' - exempt-issue-labels: keep-open - exempt-pr-labels: keep-open diff --git a/.github/workflows/ui.yml b/.github/workflows/ui.yml index f4559b98..765fc455 100644 --- a/.github/workflows/ui.yml +++ b/.github/workflows/ui.yml @@ -5,9 +5,7 @@ permissions: on: push: branches: [ master ] - paths: ['ui/**'] pull_request: - paths: ['ui/**'] jobs: lint: @@ -24,10 +22,9 @@ jobs: with: node-version: '20' cache: 'yarn' - cache-dependency-path: ui/yarn.lock - - run: cp .env.example ui/.env - - run: cd ui && yarn install - - run: cd ui && yarn lint + - run: cp .env.example .env + - run: yarn install + - run: yarn lint type-check: name: Type Check @@ -43,10 +40,9 @@ jobs: with: node-version: '20' cache: 'yarn' - cache-dependency-path: ui/yarn.lock - - run: cp .env.example ui/.env - - run: cd ui && yarn install - - run: cd ui && npx tsc --noEmit + - run: cp .env.example .env + - run: yarn install + - run: npx tsc --noEmit build: name: Build @@ -62,7 +58,6 @@ jobs: with: node-version: '20' cache: 'yarn' - cache-dependency-path: ui/yarn.lock - - run: cp .env.example ui/.env - - run: cd ui && yarn install - - run: cd ui && yarn build \ No newline at end of file + - run: cp .env.example .env + - run: yarn install + - run: yarn build diff --git a/.gitignore b/.gitignore index 509ea6b7..7b8da95f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,29 +1,42 @@ -# Rust -/target/ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. -# NextJS -/ui/.next -/ui/node_modules +# dependencies +/node_modules +/.pnp +.pnp.* +.yarn/* +!.yarn/patches +!.yarn/plugins +!.yarn/releases +!.yarn/versions -# Local Dev -/data/ +# testing +/coverage -# IDE & OS -.idea/ -.vscode/ -*.swp -*.swo +# next.js +/.next/ +/out/ + +# production +/build + +# misc .DS_Store -Thumbs.db +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnpm-debug.log* -# Environment variables -.env -.env.docker -/ui/.env +# env files (can opt-in for committing if needed) +.env* +!.env.example -# Claude -/.claude -/ui/.claude +# vercel +.vercel -# e2e / load tests -wallets.json +# typescript +*.tsbuildinfo +next-env.d.ts diff --git a/CLAUDE.md b/CLAUDE.md deleted file mode 100644 index 998ae7a4..00000000 --- a/CLAUDE.md +++ /dev/null @@ -1,18 +0,0 @@ -# TIPS - Transaction Inclusion Prioritization Stack - -## Overview -TIPS is an experimental project to replace the p2p mempool with a collection of stateless servies to enable: - -- Higher throughput -- Simulation of all transaction -- Cost savings on hardware -- Bundle support - -## Code Style & Standards -- Do not add comments unless instructed -- Put imports at the top of the file, never in functions -- Use `just fix` to fix formatting and warnings -- Run `just ci` to verify your changes -- Add dependencies to the Cargo.toml in the root and reference them in the crate cargo files -- Always use the latest dependency versions. Use https://crates.io/ to find dependency versions when adding new deps -- For logging use the tracing crate with appropriate levels and structured logging \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock deleted file mode 100644 index e08f1bec..00000000 --- a/Cargo.lock +++ /dev/null @@ -1,8468 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 4 - -[[package]] -name = "account-abstraction-core" -version = "0.1.0" -dependencies = [ - "alloy-primitives", - "alloy-provider", - "alloy-rpc-types", - "alloy-serde", - "alloy-sol-types", - "anyhow", - "async-trait", - "jsonrpsee", - "op-alloy-network", - "rdkafka", - "serde", - "serde_json", - "tips-core", - "tokio", - "tracing", - "wiremock", -] - -[[package]] -name = "adler2" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" - -[[package]] -name = "ahash" -version = "0.8.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" -dependencies = [ - "cfg-if", - "getrandom 0.3.4", - "once_cell", - "version_check", - "zerocopy", -] - -[[package]] -name = "aho-corasick" -version = "1.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" -dependencies = [ - "memchr", -] - -[[package]] -name = "allocator-api2" -version = "0.2.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" - -[[package]] -name = "alloy-chains" -version = "0.2.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35d744058a9daa51a8cf22a3009607498fcf82d3cf4c5444dd8056cdf651f471" -dependencies = [ - "alloy-primitives", - "alloy-rlp", - "num_enum", - "serde", - "strum", -] - -[[package]] -name = "alloy-consensus" -version = "1.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e318e25fb719e747a7e8db1654170fc185024f3ed5b10f86c08d448a912f6e2" -dependencies = [ - "alloy-eips", - "alloy-primitives", - "alloy-rlp", - "alloy-serde", - "alloy-trie", - "alloy-tx-macros", - "auto_impl", - "borsh", - "c-kzg", - "derive_more", - "either", - "k256", - "once_cell", - "rand 0.8.5", - "secp256k1 0.30.0", - "serde", - "serde_json", - "serde_with", - "thiserror 2.0.17", -] - -[[package]] -name = "alloy-consensus-any" -version = "1.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "364380a845193a317bcb7a5398fc86cdb66c47ebe010771dde05f6869bf9e64a" -dependencies = [ - "alloy-consensus", - "alloy-eips", - "alloy-primitives", - "alloy-rlp", - "alloy-serde", - "serde", -] - -[[package]] -name = "alloy-eip2124" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "741bdd7499908b3aa0b159bba11e71c8cddd009a2c2eb7a06e825f1ec87900a5" -dependencies = [ - "alloy-primitives", - "alloy-rlp", - "crc", - "serde", - "thiserror 2.0.17", -] - -[[package]] -name = "alloy-eip2930" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9441120fa82df73e8959ae0e4ab8ade03de2aaae61be313fbf5746277847ce25" -dependencies = [ - "alloy-primitives", - "alloy-rlp", - "borsh", - "serde", -] - -[[package]] -name = "alloy-eip7702" -version = "0.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2919c5a56a1007492da313e7a3b6d45ef5edc5d33416fdec63c0d7a2702a0d20" -dependencies = [ - "alloy-primitives", - "alloy-rlp", - "borsh", - "k256", - "serde", - "thiserror 2.0.17", -] - -[[package]] -name = "alloy-eips" -version = "1.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4c4d7c5839d9f3a467900c625416b24328450c65702eb3d8caff8813e4d1d33" -dependencies = [ - "alloy-eip2124", - "alloy-eip2930", - "alloy-eip7702", - "alloy-primitives", - "alloy-rlp", - "alloy-serde", - "auto_impl", - "borsh", - "c-kzg", - "derive_more", - "either", - "ethereum_ssz", - "ethereum_ssz_derive", - "serde", - "serde_with", - "sha2", - "thiserror 2.0.17", -] - -[[package]] -name = "alloy-evm" -version = "0.23.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "527b47dc39850c6168002ddc1f7a2063e15d26137c1bb5330f6065a7524c1aa9" -dependencies = [ - "alloy-consensus", - "alloy-eips", - "alloy-hardforks", - "alloy-op-hardforks", - "alloy-primitives", - "alloy-rpc-types-engine", - "alloy-rpc-types-eth", - "alloy-sol-types", - "auto_impl", - "derive_more", - "op-alloy-consensus", - "op-alloy-rpc-types-engine", - "op-revm", - "revm", - "thiserror 2.0.17", -] - -[[package]] -name = "alloy-genesis" -version = "1.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ba4b1be0988c11f0095a2380aa596e35533276b8fa6c9e06961bbfe0aebcac5" -dependencies = [ - "alloy-eips", - "alloy-primitives", - "alloy-serde", - "alloy-trie", - "borsh", - "serde", - "serde_with", -] - -[[package]] -name = "alloy-hardforks" -version = "0.4.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d9a33550fc21fd77a3f8b63e99969d17660eec8dcc50a95a80f7c9964f7680b" -dependencies = [ - "alloy-chains", - "alloy-eip2124", - "alloy-primitives", - "auto_impl", - "dyn-clone", -] - -[[package]] -name = "alloy-json-abi" -version = "1.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84e3cf01219c966f95a460c95f1d4c30e12f6c18150c21a30b768af2a2a29142" -dependencies = [ - "alloy-primitives", - "alloy-sol-type-parser", - "serde", - "serde_json", -] - -[[package]] -name = "alloy-json-rpc" -version = "1.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f72cf87cda808e593381fb9f005ffa4d2475552b7a6c5ac33d087bf77d82abd0" -dependencies = [ - "alloy-primitives", - "alloy-sol-types", - "http 1.4.0", - "serde", - "serde_json", - "thiserror 2.0.17", - "tracing", -] - -[[package]] -name = "alloy-network" -version = "1.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12aeb37b6f2e61b93b1c3d34d01ee720207c76fe447e2a2c217e433ac75b17f5" -dependencies = [ - "alloy-consensus", - "alloy-consensus-any", - "alloy-eips", - "alloy-json-rpc", - "alloy-network-primitives", - "alloy-primitives", - "alloy-rpc-types-any", - "alloy-rpc-types-eth", - "alloy-serde", - "alloy-signer", - "alloy-sol-types", - "async-trait", - "auto_impl", - "derive_more", - "futures-utils-wasm", - "serde", - "serde_json", - "thiserror 2.0.17", -] - -[[package]] -name = "alloy-network-primitives" -version = "1.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "abd29ace62872083e30929cd9b282d82723196d196db589f3ceda67edcc05552" -dependencies = [ - "alloy-consensus", - "alloy-eips", - "alloy-primitives", - "alloy-serde", - "serde", -] - -[[package]] -name = "alloy-op-evm" -version = "0.23.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6eea81517a852d9e3b03979c10febe00aacc3d50fbd34c5c30281051773285f7" -dependencies = [ - "alloy-consensus", - "alloy-eips", - "alloy-evm", - "alloy-op-hardforks", - "alloy-primitives", - "auto_impl", - "op-alloy-consensus", - "op-revm", - "revm", - "thiserror 2.0.17", -] - -[[package]] -name = "alloy-op-hardforks" -version = "0.4.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f96fb2fce4024ada5b2c11d4076acf778a0d3e4f011c6dfd2ffce6d0fcf84ee9" -dependencies = [ - "alloy-chains", - "alloy-hardforks", - "alloy-primitives", - "auto_impl", -] - -[[package]] -name = "alloy-primitives" -version = "1.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6a0fb18dd5fb43ec5f0f6a20be1ce0287c79825827de5744afaa6c957737c33" -dependencies = [ - "alloy-rlp", - "bytes", - "cfg-if", - "const-hex", - "derive_more", - "foldhash 0.2.0", - "getrandom 0.3.4", - "hashbrown 0.16.1", - "indexmap 2.12.1", - "itoa", - "k256", - "keccak-asm", - "paste", - "proptest", - "rand 0.9.2", - "rapidhash", - "ruint", - "rustc-hash", - "serde", - "sha3", - "tiny-keccak", -] - -[[package]] -name = "alloy-provider" -version = "1.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b710636d7126e08003b8217e24c09f0cca0b46d62f650a841736891b1ed1fc1" -dependencies = [ - "alloy-chains", - "alloy-consensus", - "alloy-eips", - "alloy-json-rpc", - "alloy-network", - "alloy-network-primitives", - "alloy-primitives", - "alloy-rpc-client", - "alloy-rpc-types-eth", - "alloy-signer", - "alloy-sol-types", - "alloy-transport", - "alloy-transport-http", - "async-stream", - "async-trait", - "auto_impl", - "dashmap", - "either", - "futures", - "futures-utils-wasm", - "lru 0.13.0", - "parking_lot", - "pin-project", - "reqwest", - "serde", - "serde_json", - "thiserror 2.0.17", - "tokio", - "tracing", - "url", - "wasmtimer", -] - -[[package]] -name = "alloy-rlp" -version = "0.3.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f70d83b765fdc080dbcd4f4db70d8d23fe4761f2f02ebfa9146b833900634b4" -dependencies = [ - "alloy-rlp-derive", - "arrayvec", - "bytes", -] - -[[package]] -name = "alloy-rlp-derive" -version = "0.3.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64b728d511962dda67c1bc7ea7c03736ec275ed2cf4c35d9585298ac9ccf3b73" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.111", -] - -[[package]] -name = "alloy-rpc-client" -version = "1.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0882e72d2c1c0c79dcf4ab60a67472d3f009a949f774d4c17d0bdb669cfde05" -dependencies = [ - "alloy-json-rpc", - "alloy-primitives", - "alloy-transport", - "alloy-transport-http", - "futures", - "pin-project", - "reqwest", - "serde", - "serde_json", - "tokio", - "tokio-stream", - "tower", - "tracing", - "url", - "wasmtimer", -] - -[[package]] -name = "alloy-rpc-types" -version = "1.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39cf1398cb33aacb139a960fa3d8cf8b1202079f320e77e952a0b95967bf7a9f" -dependencies = [ - "alloy-primitives", - "alloy-rpc-types-eth", - "alloy-serde", - "serde", -] - -[[package]] -name = "alloy-rpc-types-admin" -version = "1.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65a583d2029b171301f5dcf122aa2ef443a65a373778ec76540d999691ae867d" -dependencies = [ - "alloy-genesis", - "alloy-primitives", - "serde", - "serde_json", -] - -[[package]] -name = "alloy-rpc-types-any" -version = "1.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a63fb40ed24e4c92505f488f9dd256e2afaed17faa1b7a221086ebba74f4122" -dependencies = [ - "alloy-consensus-any", - "alloy-rpc-types-eth", - "alloy-serde", -] - -[[package]] -name = "alloy-rpc-types-engine" -version = "1.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c60bdce3be295924122732b7ecd0b2495ce4790bedc5370ca7019c08ad3f26e" -dependencies = [ - "alloy-consensus", - "alloy-eips", - "alloy-primitives", - "alloy-rlp", - "alloy-serde", - "derive_more", - "ethereum_ssz", - "ethereum_ssz_derive", - "serde", - "strum", -] - -[[package]] -name = "alloy-rpc-types-eth" -version = "1.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9eae0c7c40da20684548cbc8577b6b7447f7bf4ddbac363df95e3da220e41e72" -dependencies = [ - "alloy-consensus", - "alloy-consensus-any", - "alloy-eips", - "alloy-network-primitives", - "alloy-primitives", - "alloy-rlp", - "alloy-serde", - "alloy-sol-types", - "itertools 0.14.0", - "serde", - "serde_json", - "serde_with", - "thiserror 2.0.17", -] - -[[package]] -name = "alloy-rpc-types-trace" -version = "1.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef206a4b8d436fbb7cf2e6a61c692d11df78f9382becc3c9a283bd58e64f0583" -dependencies = [ - "alloy-primitives", - "alloy-rpc-types-eth", - "alloy-serde", - "serde", - "serde_json", - "thiserror 2.0.17", -] - -[[package]] -name = "alloy-serde" -version = "1.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0df1987ed0ff2d0159d76b52e7ddfc4e4fbddacc54d2fbee765e0d14d7c01b5" -dependencies = [ - "alloy-primitives", - "serde", - "serde_json", -] - -[[package]] -name = "alloy-signer" -version = "1.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ff69deedee7232d7ce5330259025b868c5e6a52fa8dffda2c861fb3a5889b24" -dependencies = [ - "alloy-primitives", - "async-trait", - "auto_impl", - "either", - "elliptic-curve", - "k256", - "thiserror 2.0.17", -] - -[[package]] -name = "alloy-signer-local" -version = "1.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72cfe0be3ec5a8c1a46b2e5a7047ed41121d360d97f4405bb7c1c784880c86cb" -dependencies = [ - "alloy-consensus", - "alloy-network", - "alloy-primitives", - "alloy-signer", - "async-trait", - "k256", - "rand 0.8.5", - "thiserror 2.0.17", -] - -[[package]] -name = "alloy-sol-macro" -version = "1.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09eb18ce0df92b4277291bbaa0ed70545d78b02948df756bbd3d6214bf39a218" -dependencies = [ - "alloy-sol-macro-expander", - "alloy-sol-macro-input", - "proc-macro-error2", - "proc-macro2", - "quote", - "syn 2.0.111", -] - -[[package]] -name = "alloy-sol-macro-expander" -version = "1.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95d9fa2daf21f59aa546d549943f10b5cce1ae59986774019fbedae834ffe01b" -dependencies = [ - "alloy-sol-macro-input", - "const-hex", - "heck", - "indexmap 2.12.1", - "proc-macro-error2", - "proc-macro2", - "quote", - "syn 2.0.111", - "syn-solidity", - "tiny-keccak", -] - -[[package]] -name = "alloy-sol-macro-input" -version = "1.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9396007fe69c26ee118a19f4dee1f5d1d6be186ea75b3881adf16d87f8444686" -dependencies = [ - "const-hex", - "dunce", - "heck", - "macro-string", - "proc-macro2", - "quote", - "syn 2.0.111", - "syn-solidity", -] - -[[package]] -name = "alloy-sol-type-parser" -version = "1.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af67a0b0dcebe14244fc92002cd8d96ecbf65db4639d479f5fcd5805755a4c27" -dependencies = [ - "serde", - "winnow", -] - -[[package]] -name = "alloy-sol-types" -version = "1.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09aeea64f09a7483bdcd4193634c7e5cf9fd7775ee767585270cd8ce2d69dc95" -dependencies = [ - "alloy-json-abi", - "alloy-primitives", - "alloy-sol-macro", - "serde", -] - -[[package]] -name = "alloy-transport" -version = "1.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be98b07210d24acf5b793c99b759e9a696e4a2e67593aec0487ae3b3e1a2478c" -dependencies = [ - "alloy-json-rpc", - "auto_impl", - "base64 0.22.1", - "derive_more", - "futures", - "futures-utils-wasm", - "parking_lot", - "serde", - "serde_json", - "thiserror 2.0.17", - "tokio", - "tower", - "tracing", - "url", - "wasmtimer", -] - -[[package]] -name = "alloy-transport-http" -version = "1.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4198a1ee82e562cab85e7f3d5921aab725d9bd154b6ad5017f82df1695877c97" -dependencies = [ - "alloy-json-rpc", - "alloy-transport", - "reqwest", - "serde_json", - "tower", - "tracing", - "url", -] - -[[package]] -name = "alloy-trie" -version = "0.9.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b77b56af09ead281337d06b1d036c88e2dc8a2e45da512a532476dbee94912b" -dependencies = [ - "alloy-primitives", - "alloy-rlp", - "arrayvec", - "derive_more", - "nybbles", - "serde", - "smallvec", - "tracing", -] - -[[package]] -name = "alloy-tx-macros" -version = "1.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "333544408503f42d7d3792bfc0f7218b643d968a03d2c0ed383ae558fb4a76d0" -dependencies = [ - "darling 0.21.3", - "proc-macro2", - "quote", - "syn 2.0.111", -] - -[[package]] -name = "android_system_properties" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" -dependencies = [ - "libc", -] - -[[package]] -name = "anstream" -version = "0.6.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" -dependencies = [ - "anstyle", - "anstyle-parse", - "anstyle-query", - "anstyle-wincon", - "colorchoice", - "is_terminal_polyfill", - "utf8parse", -] - -[[package]] -name = "anstyle" -version = "1.0.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" - -[[package]] -name = "anstyle-parse" -version = "0.2.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" -dependencies = [ - "utf8parse", -] - -[[package]] -name = "anstyle-query" -version = "1.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" -dependencies = [ - "windows-sys 0.61.2", -] - -[[package]] -name = "anstyle-wincon" -version = "3.0.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" -dependencies = [ - "anstyle", - "once_cell_polyfill", - "windows-sys 0.61.2", -] - -[[package]] -name = "anyhow" -version = "1.0.100" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" - -[[package]] -name = "aquamarine" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f50776554130342de4836ba542aa85a4ddb361690d7e8df13774d7284c3d5c2" -dependencies = [ - "include_dir", - "itertools 0.10.5", - "proc-macro-error2", - "proc-macro2", - "quote", - "syn 2.0.111", -] - -[[package]] -name = "ark-bls12-381" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3df4dcc01ff89867cd86b0da835f23c3f02738353aaee7dde7495af71363b8d5" -dependencies = [ - "ark-ec", - "ark-ff 0.5.0", - "ark-serialize 0.5.0", - "ark-std 0.5.0", -] - -[[package]] -name = "ark-bn254" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d69eab57e8d2663efa5c63135b2af4f396d66424f88954c21104125ab6b3e6bc" -dependencies = [ - "ark-ec", - "ark-ff 0.5.0", - "ark-r1cs-std", - "ark-std 0.5.0", -] - -[[package]] -name = "ark-ec" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43d68f2d516162846c1238e755a7c4d131b892b70cc70c471a8e3ca3ed818fce" -dependencies = [ - "ahash", - "ark-ff 0.5.0", - "ark-poly", - "ark-serialize 0.5.0", - "ark-std 0.5.0", - "educe", - "fnv", - "hashbrown 0.15.5", - "itertools 0.13.0", - "num-bigint", - "num-integer", - "num-traits", - "zeroize", -] - -[[package]] -name = "ark-ff" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b3235cc41ee7a12aaaf2c575a2ad7b46713a8a50bda2fc3b003a04845c05dd6" -dependencies = [ - "ark-ff-asm 0.3.0", - "ark-ff-macros 0.3.0", - "ark-serialize 0.3.0", - "ark-std 0.3.0", - "derivative", - "num-bigint", - "num-traits", - "paste", - "rustc_version 0.3.3", - "zeroize", -] - -[[package]] -name = "ark-ff" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec847af850f44ad29048935519032c33da8aa03340876d351dfab5660d2966ba" -dependencies = [ - "ark-ff-asm 0.4.2", - "ark-ff-macros 0.4.2", - "ark-serialize 0.4.2", - "ark-std 0.4.0", - "derivative", - "digest 0.10.7", - "itertools 0.10.5", - "num-bigint", - "num-traits", - "paste", - "rustc_version 0.4.1", - "zeroize", -] - -[[package]] -name = "ark-ff" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a177aba0ed1e0fbb62aa9f6d0502e9b46dad8c2eab04c14258a1212d2557ea70" -dependencies = [ - "ark-ff-asm 0.5.0", - "ark-ff-macros 0.5.0", - "ark-serialize 0.5.0", - "ark-std 0.5.0", - "arrayvec", - "digest 0.10.7", - "educe", - "itertools 0.13.0", - "num-bigint", - "num-traits", - "paste", - "zeroize", -] - -[[package]] -name = "ark-ff-asm" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db02d390bf6643fb404d3d22d31aee1c4bc4459600aef9113833d17e786c6e44" -dependencies = [ - "quote", - "syn 1.0.109", -] - -[[package]] -name = "ark-ff-asm" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ed4aa4fe255d0bc6d79373f7e31d2ea147bcf486cba1be5ba7ea85abdb92348" -dependencies = [ - "quote", - "syn 1.0.109", -] - -[[package]] -name = "ark-ff-asm" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62945a2f7e6de02a31fe400aa489f0e0f5b2502e69f95f853adb82a96c7a6b60" -dependencies = [ - "quote", - "syn 2.0.111", -] - -[[package]] -name = "ark-ff-macros" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db2fd794a08ccb318058009eefdf15bcaaaaf6f8161eb3345f907222bac38b20" -dependencies = [ - "num-bigint", - "num-traits", - "quote", - "syn 1.0.109", -] - -[[package]] -name = "ark-ff-macros" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7abe79b0e4288889c4574159ab790824d0033b9fdcb2a112a3182fac2e514565" -dependencies = [ - "num-bigint", - "num-traits", - "proc-macro2", - "quote", - "syn 1.0.109", -] - -[[package]] -name = "ark-ff-macros" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09be120733ee33f7693ceaa202ca41accd5653b779563608f1234f78ae07c4b3" -dependencies = [ - "num-bigint", - "num-traits", - "proc-macro2", - "quote", - "syn 2.0.111", -] - -[[package]] -name = "ark-poly" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "579305839da207f02b89cd1679e50e67b4331e2f9294a57693e5051b7703fe27" -dependencies = [ - "ahash", - "ark-ff 0.5.0", - "ark-serialize 0.5.0", - "ark-std 0.5.0", - "educe", - "fnv", - "hashbrown 0.15.5", -] - -[[package]] -name = "ark-r1cs-std" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "941551ef1df4c7a401de7068758db6503598e6f01850bdb2cfdb614a1f9dbea1" -dependencies = [ - "ark-ec", - "ark-ff 0.5.0", - "ark-relations", - "ark-std 0.5.0", - "educe", - "num-bigint", - "num-integer", - "num-traits", - "tracing", -] - -[[package]] -name = "ark-relations" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec46ddc93e7af44bcab5230937635b06fb5744464dd6a7e7b083e80ebd274384" -dependencies = [ - "ark-ff 0.5.0", - "ark-std 0.5.0", - "tracing", - "tracing-subscriber 0.2.25", -] - -[[package]] -name = "ark-serialize" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d6c2b318ee6e10f8c2853e73a83adc0ccb88995aa978d8a3408d492ab2ee671" -dependencies = [ - "ark-std 0.3.0", - "digest 0.9.0", -] - -[[package]] -name = "ark-serialize" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "adb7b85a02b83d2f22f89bd5cac66c9c89474240cb6207cb1efc16d098e822a5" -dependencies = [ - "ark-std 0.4.0", - "digest 0.10.7", - "num-bigint", -] - -[[package]] -name = "ark-serialize" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f4d068aaf107ebcd7dfb52bc748f8030e0fc930ac8e360146ca54c1203088f7" -dependencies = [ - "ark-serialize-derive", - "ark-std 0.5.0", - "arrayvec", - "digest 0.10.7", - "num-bigint", -] - -[[package]] -name = "ark-serialize-derive" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "213888f660fddcca0d257e88e54ac05bca01885f258ccdf695bafd77031bb69d" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.111", -] - -[[package]] -name = "ark-std" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1df2c09229cbc5a028b1d70e00fdb2acee28b1055dfb5ca73eea49c5a25c4e7c" -dependencies = [ - "num-traits", - "rand 0.8.5", -] - -[[package]] -name = "ark-std" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94893f1e0c6eeab764ade8dc4c0db24caf4fe7cbbaafc0eba0a9030f447b5185" -dependencies = [ - "num-traits", - "rand 0.8.5", -] - -[[package]] -name = "ark-std" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "246a225cc6131e9ee4f24619af0f19d67761fff15d7ccc22e42b80846e69449a" -dependencies = [ - "num-traits", - "rand 0.8.5", -] - -[[package]] -name = "arrayref" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" - -[[package]] -name = "arrayvec" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" -dependencies = [ - "serde", -] - -[[package]] -name = "assert-json-diff" -version = "2.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47e4f2b81832e72834d7518d8487a0396a28cc408186a2e8854c0f98011faf12" -dependencies = [ - "serde", - "serde_json", -] - -[[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" -version = "0.3.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b5a71a6f37880a80d1d7f19efd781e4b5de42c88f0722cc13bcb6cc2cfe8476" -dependencies = [ - "async-stream-impl", - "futures-core", - "pin-project-lite", -] - -[[package]] -name = "async-stream-impl" -version = "0.3.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.111", -] - -[[package]] -name = "async-trait" -version = "0.1.89" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.111", -] - -[[package]] -name = "atomic-waker" -version = "1.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" - -[[package]] -name = "aurora-engine-modexp" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "518bc5745a6264b5fd7b09dffb9667e400ee9e2bbe18555fac75e1fe9afa0df9" -dependencies = [ - "hex", - "num", -] - -[[package]] -name = "auto_impl" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffdcb70bdbc4d478427380519163274ac86e52916e10f0a8889adf0f96d3fee7" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.111", -] - -[[package]] -name = "autocfg" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" - -[[package]] -name = "aws-config" -version = "1.8.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96571e6996817bf3d58f6b569e4b9fd2e9d2fcf9f7424eed07b2ce9bb87535e5" -dependencies = [ - "aws-credential-types", - "aws-runtime", - "aws-sdk-sts", - "aws-smithy-async", - "aws-smithy-http", - "aws-smithy-json", - "aws-smithy-runtime", - "aws-smithy-runtime-api", - "aws-smithy-types", - "aws-types", - "bytes", - "fastrand", - "http 1.4.0", - "time", - "tokio", - "tracing", - "url", -] - -[[package]] -name = "aws-credential-types" -version = "1.2.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3cd362783681b15d136480ad555a099e82ecd8e2d10a841e14dfd0078d67fee3" -dependencies = [ - "aws-smithy-async", - "aws-smithy-runtime-api", - "aws-smithy-types", - "zeroize", -] - -[[package]] -name = "aws-lc-rs" -version = "1.15.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a88aab2464f1f25453baa7a07c84c5b7684e274054ba06817f382357f77a288" -dependencies = [ - "aws-lc-sys", - "zeroize", -] - -[[package]] -name = "aws-lc-sys" -version = "0.35.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b45afffdee1e7c9126814751f88dddc747f41d91da16c9551a0f1e8a11e788a1" -dependencies = [ - "cc", - "cmake", - "dunce", - "fs_extra", -] - -[[package]] -name = "aws-runtime" -version = "1.5.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d81b5b2898f6798ad58f484856768bca817e3cd9de0974c24ae0f1113fe88f1b" -dependencies = [ - "aws-credential-types", - "aws-sigv4", - "aws-smithy-async", - "aws-smithy-eventstream", - "aws-smithy-http", - "aws-smithy-runtime", - "aws-smithy-runtime-api", - "aws-smithy-types", - "aws-types", - "bytes", - "fastrand", - "http 0.2.12", - "http-body 0.4.6", - "percent-encoding", - "pin-project-lite", - "tracing", - "uuid", -] - -[[package]] -name = "aws-sdk-s3" -version = "1.118.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3e6b7079f85d9ea9a70643c9f89f50db70f5ada868fa9cfe08c1ffdf51abc13" -dependencies = [ - "aws-credential-types", - "aws-runtime", - "aws-sigv4", - "aws-smithy-async", - "aws-smithy-checksums", - "aws-smithy-eventstream", - "aws-smithy-http", - "aws-smithy-json", - "aws-smithy-runtime", - "aws-smithy-runtime-api", - "aws-smithy-types", - "aws-smithy-xml", - "aws-types", - "bytes", - "fastrand", - "hex", - "hmac", - "http 0.2.12", - "http 1.4.0", - "http-body 0.4.6", - "lru 0.12.5", - "percent-encoding", - "regex-lite", - "sha2", - "tracing", - "url", -] - -[[package]] -name = "aws-sdk-sts" -version = "1.95.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55542378e419558e6b1f398ca70adb0b2088077e79ad9f14eb09441f2f7b2164" -dependencies = [ - "aws-credential-types", - "aws-runtime", - "aws-smithy-async", - "aws-smithy-http", - "aws-smithy-json", - "aws-smithy-query", - "aws-smithy-runtime", - "aws-smithy-runtime-api", - "aws-smithy-types", - "aws-smithy-xml", - "aws-types", - "fastrand", - "http 0.2.12", - "regex-lite", - "tracing", -] - -[[package]] -name = "aws-sigv4" -version = "1.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69e523e1c4e8e7e8ff219d732988e22bfeae8a1cafdbe6d9eca1546fa080be7c" -dependencies = [ - "aws-credential-types", - "aws-smithy-eventstream", - "aws-smithy-http", - "aws-smithy-runtime-api", - "aws-smithy-types", - "bytes", - "form_urlencoded", - "hex", - "hmac", - "http 0.2.12", - "http 1.4.0", - "percent-encoding", - "sha2", - "time", - "tracing", -] - -[[package]] -name = "aws-smithy-async" -version = "1.2.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ee19095c7c4dda59f1697d028ce704c24b2d33c6718790c7f1d5a3015b4107c" -dependencies = [ - "futures-util", - "pin-project-lite", - "tokio", -] - -[[package]] -name = "aws-smithy-checksums" -version = "0.63.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87294a084b43d649d967efe58aa1f9e0adc260e13a6938eb904c0ae9b45824ae" -dependencies = [ - "aws-smithy-http", - "aws-smithy-types", - "bytes", - "crc-fast", - "hex", - "http 0.2.12", - "http-body 0.4.6", - "md-5", - "pin-project-lite", - "sha1", - "sha2", - "tracing", -] - -[[package]] -name = "aws-smithy-eventstream" -version = "0.60.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc12f8b310e38cad85cf3bef45ad236f470717393c613266ce0a89512286b650" -dependencies = [ - "aws-smithy-types", - "bytes", - "crc32fast", -] - -[[package]] -name = "aws-smithy-http" -version = "0.62.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "826141069295752372f8203c17f28e30c464d22899a43a0c9fd9c458d469c88b" -dependencies = [ - "aws-smithy-eventstream", - "aws-smithy-runtime-api", - "aws-smithy-types", - "bytes", - "bytes-utils", - "futures-core", - "futures-util", - "http 0.2.12", - "http 1.4.0", - "http-body 0.4.6", - "percent-encoding", - "pin-project-lite", - "pin-utils", - "tracing", -] - -[[package]] -name = "aws-smithy-http-client" -version = "1.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59e62db736db19c488966c8d787f52e6270be565727236fd5579eaa301e7bc4a" -dependencies = [ - "aws-smithy-async", - "aws-smithy-runtime-api", - "aws-smithy-types", - "h2 0.3.27", - "h2 0.4.12", - "http 0.2.12", - "http 1.4.0", - "http-body 0.4.6", - "hyper 0.14.32", - "hyper 1.8.1", - "hyper-rustls 0.24.2", - "hyper-rustls 0.27.7", - "hyper-util", - "pin-project-lite", - "rustls 0.21.12", - "rustls 0.23.35", - "rustls-native-certs", - "rustls-pki-types", - "tokio", - "tokio-rustls 0.26.4", - "tower", - "tracing", -] - -[[package]] -name = "aws-smithy-json" -version = "0.61.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49fa1213db31ac95288d981476f78d05d9cbb0353d22cdf3472cc05bb02f6551" -dependencies = [ - "aws-smithy-types", -] - -[[package]] -name = "aws-smithy-observability" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17f616c3f2260612fe44cede278bafa18e73e6479c4e393e2c4518cf2a9a228a" -dependencies = [ - "aws-smithy-runtime-api", -] - -[[package]] -name = "aws-smithy-query" -version = "0.60.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae5d689cf437eae90460e944a58b5668530d433b4ff85789e69d2f2a556e057d" -dependencies = [ - "aws-smithy-types", - "urlencoding", -] - -[[package]] -name = "aws-smithy-runtime" -version = "1.9.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65fda37911905ea4d3141a01364bc5509a0f32ae3f3b22d6e330c0abfb62d247" -dependencies = [ - "aws-smithy-async", - "aws-smithy-http", - "aws-smithy-http-client", - "aws-smithy-observability", - "aws-smithy-runtime-api", - "aws-smithy-types", - "bytes", - "fastrand", - "http 0.2.12", - "http 1.4.0", - "http-body 0.4.6", - "http-body 1.0.1", - "pin-project-lite", - "pin-utils", - "tokio", - "tracing", -] - -[[package]] -name = "aws-smithy-runtime-api" -version = "1.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab0d43d899f9e508300e587bf582ba54c27a452dd0a9ea294690669138ae14a2" -dependencies = [ - "aws-smithy-async", - "aws-smithy-types", - "bytes", - "http 0.2.12", - "http 1.4.0", - "pin-project-lite", - "tokio", - "tracing", - "zeroize", -] - -[[package]] -name = "aws-smithy-types" -version = "1.3.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "905cb13a9895626d49cf2ced759b062d913834c7482c38e49557eac4e6193f01" -dependencies = [ - "base64-simd", - "bytes", - "bytes-utils", - "futures-core", - "http 0.2.12", - "http 1.4.0", - "http-body 0.4.6", - "http-body 1.0.1", - "http-body-util", - "itoa", - "num-integer", - "pin-project-lite", - "pin-utils", - "ryu", - "serde", - "time", - "tokio", - "tokio-util", -] - -[[package]] -name = "aws-smithy-xml" -version = "0.60.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11b2f670422ff42bf7065031e72b45bc52a3508bd089f743ea90731ca2b6ea57" -dependencies = [ - "xmlparser", -] - -[[package]] -name = "aws-types" -version = "1.3.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d980627d2dd7bfc32a3c025685a033eeab8d365cc840c631ef59d1b8f428164" -dependencies = [ - "aws-credential-types", - "aws-smithy-async", - "aws-smithy-runtime-api", - "aws-smithy-types", - "rustc_version 0.4.1", - "tracing", -] - -[[package]] -name = "axum" -version = "0.8.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b52af3cb4058c895d37317bb27508dccc8e5f2d39454016b297bf4a400597b8" -dependencies = [ - "axum-core", - "bytes", - "futures-util", - "http 1.4.0", - "http-body 1.0.1", - "http-body-util", - "hyper 1.8.1", - "hyper-util", - "itoa", - "matchit", - "memchr", - "mime", - "percent-encoding", - "pin-project-lite", - "serde_core", - "serde_json", - "serde_path_to_error", - "sync_wrapper", - "tokio", - "tower", - "tower-layer", - "tower-service", -] - -[[package]] -name = "axum-core" -version = "0.5.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59446ce19cd142f8833f856eb31f3eb097812d1479ab224f54d72428ca21ea22" -dependencies = [ - "bytes", - "futures-core", - "http 1.4.0", - "http-body 1.0.1", - "http-body-util", - "mime", - "pin-project-lite", - "sync_wrapper", - "tower-layer", - "tower-service", -] - -[[package]] -name = "az" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b7e4c2464d97fe331d41de9d5db0def0a96f4d823b8b32a2efd503578988973" - -[[package]] -name = "backon" -version = "1.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cffb0e931875b666fc4fcb20fee52e9bbd1ef836fd9e9e04ec21555f9f85f7ef" -dependencies = [ - "fastrand", - "tokio", -] - -[[package]] -name = "base-reth-rpc-types" -version = "0.2.1" -source = "git+https://github.com/base/node-reth?branch=main#6eb0b53eb7ed6aa087058f898e8f965f423b9521" -dependencies = [ - "reth-optimism-evm", - "reth-rpc-eth-types", -] - -[[package]] -name = "base16ct" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" - -[[package]] -name = "base64" -version = "0.21.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" - -[[package]] -name = "base64" -version = "0.22.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" - -[[package]] -name = "base64-simd" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "339abbe78e73178762e23bea9dfd08e697eb3f3301cd4be981c0f78ba5859195" -dependencies = [ - "outref", - "vsimd", -] - -[[package]] -name = "base64ct" -version = "1.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e050f626429857a27ddccb31e0aca21356bfa709c04041aefddac081a8f068a" - -[[package]] -name = "bindgen" -version = "0.72.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895" -dependencies = [ - "bitflags 2.10.0", - "cexpr", - "clang-sys", - "itertools 0.13.0", - "proc-macro2", - "quote", - "regex", - "rustc-hash", - "shlex", - "syn 2.0.111", -] - -[[package]] -name = "bit-set" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" -dependencies = [ - "bit-vec", -] - -[[package]] -name = "bit-vec" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" - -[[package]] -name = "bitcoin-io" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dee39a0ee5b4095224a0cfc6bf4cc1baf0f9624b96b367e53b66d974e51d953" - -[[package]] -name = "bitcoin_hashes" -version = "0.14.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26ec84b80c482df901772e931a9a681e26a1b9ee2302edeff23cb30328745c8b" -dependencies = [ - "bitcoin-io", - "hex-conservative", -] - -[[package]] -name = "bitflags" -version = "1.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" - -[[package]] -name = "bitflags" -version = "2.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" -dependencies = [ - "serde_core", -] - -[[package]] -name = "bitvec" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" -dependencies = [ - "funty", - "radium", - "serde", - "tap", - "wyz", -] - -[[package]] -name = "block-buffer" -version = "0.10.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" -dependencies = [ - "generic-array", -] - -[[package]] -name = "blst" -version = "0.3.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcdb4c7013139a150f9fc55d123186dbfaba0d912817466282c73ac49e71fb45" -dependencies = [ - "cc", - "glob", - "threadpool", - "zeroize", -] - -[[package]] -name = "bollard" -version = "0.18.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97ccca1260af6a459d75994ad5acc1651bcabcbdbc41467cc9786519ab854c30" -dependencies = [ - "base64 0.22.1", - "bollard-stubs", - "bytes", - "futures-core", - "futures-util", - "hex", - "home", - "http 1.4.0", - "http-body-util", - "hyper 1.8.1", - "hyper-named-pipe", - "hyper-rustls 0.27.7", - "hyper-util", - "hyperlocal", - "log", - "pin-project-lite", - "rustls 0.23.35", - "rustls-native-certs", - "rustls-pemfile", - "rustls-pki-types", - "serde", - "serde_derive", - "serde_json", - "serde_repr", - "serde_urlencoded", - "thiserror 2.0.17", - "tokio", - "tokio-util", - "tower-service", - "url", - "winapi", -] - -[[package]] -name = "bollard-stubs" -version = "1.47.1-rc.27.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f179cfbddb6e77a5472703d4b30436bff32929c0aa8a9008ecf23d1d3cdd0da" -dependencies = [ - "serde", - "serde_repr", - "serde_with", -] - -[[package]] -name = "borsh" -version = "1.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1da5ab77c1437701eeff7c88d968729e7766172279eab0676857b3d63af7a6f" -dependencies = [ - "borsh-derive", - "cfg_aliases", -] - -[[package]] -name = "borsh-derive" -version = "1.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0686c856aa6aac0c4498f936d7d6a02df690f614c03e4d906d1018062b5c5e2c" -dependencies = [ - "once_cell", - "proc-macro-crate", - "proc-macro2", - "quote", - "syn 2.0.111", -] - -[[package]] -name = "bumpalo" -version = "3.19.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510" - -[[package]] -name = "byte-slice-cast" -version = "1.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7575182f7272186991736b70173b0ea045398f984bf5ebbb3804736ce1330c9d" - -[[package]] -name = "byteorder" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" - -[[package]] -name = "bytes" -version = "1.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" -dependencies = [ - "serde", -] - -[[package]] -name = "bytes-utils" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7dafe3a8757b027e2be6e4e5601ed563c55989fcf1546e933c66c8eb3a058d35" -dependencies = [ - "bytes", - "either", -] - -[[package]] -name = "c-kzg" -version = "2.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e00bf4b112b07b505472dbefd19e37e53307e2bfed5a79e0cc161d58ccd0e687" -dependencies = [ - "blst", - "cc", - "glob", - "hex", - "libc", - "once_cell", - "serde", -] - -[[package]] -name = "cc" -version = "1.2.50" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f50d563227a1c37cc0a263f64eca3334388c01c5e4c4861a9def205c614383c" -dependencies = [ - "find-msvc-tools", - "jobserver", - "libc", - "shlex", -] - -[[package]] -name = "cesu8" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" - -[[package]] -name = "cexpr" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" -dependencies = [ - "nom", -] - -[[package]] -name = "cfg-if" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" - -[[package]] -name = "cfg_aliases" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" - -[[package]] -name = "chrono" -version = "0.4.42" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2" -dependencies = [ - "iana-time-zone", - "num-traits", - "serde", - "windows-link", -] - -[[package]] -name = "clang-sys" -version = "1.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" -dependencies = [ - "glob", - "libc", - "libloading", -] - -[[package]] -name = "clap" -version = "4.5.53" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9e340e012a1bf4935f5282ed1436d1489548e8f72308207ea5df0e23d2d03f8" -dependencies = [ - "clap_builder", - "clap_derive", -] - -[[package]] -name = "clap_builder" -version = "4.5.53" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d76b5d13eaa18c901fd2f7fca939fefe3a0727a953561fefdf3b2922b8569d00" -dependencies = [ - "anstream", - "anstyle", - "clap_lex", - "strsim", -] - -[[package]] -name = "clap_derive" -version = "4.5.49" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a0b5487afeab2deb2ff4e03a807ad1a03ac532ff5a2cee5d86884440c7f7671" -dependencies = [ - "heck", - "proc-macro2", - "quote", - "syn 2.0.111", -] - -[[package]] -name = "clap_lex" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d" - -[[package]] -name = "cmake" -version = "0.1.57" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75443c44cd6b379beb8c5b45d85d0773baf31cce901fe7bb252f4eff3008ef7d" -dependencies = [ - "cc", -] - -[[package]] -name = "colorchoice" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" - -[[package]] -name = "combine" -version = "4.6.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" -dependencies = [ - "bytes", - "memchr", -] - -[[package]] -name = "concurrent-queue" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" -dependencies = [ - "crossbeam-utils", -] - -[[package]] -name = "console" -version = "0.15.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "054ccb5b10f9f2cbf51eb355ca1d05c2d279ce1804688d0db74b4733a5aeafd8" -dependencies = [ - "encode_unicode", - "libc", - "once_cell", - "unicode-width", - "windows-sys 0.59.0", -] - -[[package]] -name = "const-hex" -version = "1.17.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3bb320cac8a0750d7f25280aa97b09c26edfe161164238ecbbb31092b079e735" -dependencies = [ - "cfg-if", - "cpufeatures", - "proptest", - "serde_core", -] - -[[package]] -name = "const-oid" -version = "0.9.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" - -[[package]] -name = "const_format" -version = "0.2.35" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7faa7469a93a566e9ccc1c73fe783b4a65c274c5ace346038dca9c39fe0030ad" -dependencies = [ - "const_format_proc_macros", -] - -[[package]] -name = "const_format_proc_macros" -version = "0.2.34" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d57c2eccfb16dbac1f4e61e206105db5820c9d26c3c472bc17c774259ef7744" -dependencies = [ - "proc-macro2", - "quote", - "unicode-xid", -] - -[[package]] -name = "convert_case" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "633458d4ef8c78b72454de2d54fd6ab2e60f9e02be22f3c6104cdc8a4e0fceb9" -dependencies = [ - "unicode-segmentation", -] - -[[package]] -name = "core-foundation" -version = "0.9.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" -dependencies = [ - "core-foundation-sys", - "libc", -] - -[[package]] -name = "core-foundation" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" -dependencies = [ - "core-foundation-sys", - "libc", -] - -[[package]] -name = "core-foundation-sys" -version = "0.8.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" - -[[package]] -name = "cpufeatures" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" -dependencies = [ - "libc", -] - -[[package]] -name = "crc" -version = "3.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5eb8a2a1cd12ab0d987a5d5e825195d372001a4094a0376319d5a0ad71c1ba0d" -dependencies = [ - "crc-catalog", -] - -[[package]] -name = "crc-catalog" -version = "2.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" - -[[package]] -name = "crc-fast" -version = "1.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ddc2d09feefeee8bd78101665bd8645637828fa9317f9f292496dbbd8c65ff3" -dependencies = [ - "crc", - "digest 0.10.7", - "rand 0.9.2", - "regex", - "rustversion", -] - -[[package]] -name = "crc32fast" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "critical-section" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b" - -[[package]] -name = "crossbeam-channel" -version = "0.5.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" -dependencies = [ - "crossbeam-utils", -] - -[[package]] -name = "crossbeam-deque" -version = "0.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" -dependencies = [ - "crossbeam-epoch", - "crossbeam-utils", -] - -[[package]] -name = "crossbeam-epoch" -version = "0.9.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" -dependencies = [ - "crossbeam-utils", -] - -[[package]] -name = "crossbeam-utils" -version = "0.8.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" - -[[package]] -name = "crunchy" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" - -[[package]] -name = "crypto-bigint" -version = "0.5.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" -dependencies = [ - "generic-array", - "rand_core 0.6.4", - "subtle", - "zeroize", -] - -[[package]] -name = "crypto-common" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" -dependencies = [ - "generic-array", - "typenum", -] - -[[package]] -name = "darling" -version = "0.20.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" -dependencies = [ - "darling_core 0.20.11", - "darling_macro 0.20.11", -] - -[[package]] -name = "darling" -version = "0.21.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9cdf337090841a411e2a7f3deb9187445851f91b309c0c0a29e05f74a00a48c0" -dependencies = [ - "darling_core 0.21.3", - "darling_macro 0.21.3", -] - -[[package]] -name = "darling_core" -version = "0.20.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" -dependencies = [ - "fnv", - "ident_case", - "proc-macro2", - "quote", - "strsim", - "syn 2.0.111", -] - -[[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", - "quote", - "serde", - "strsim", - "syn 2.0.111", -] - -[[package]] -name = "darling_macro" -version = "0.20.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" -dependencies = [ - "darling_core 0.20.11", - "quote", - "syn 2.0.111", -] - -[[package]] -name = "darling_macro" -version = "0.21.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81" -dependencies = [ - "darling_core 0.21.3", - "quote", - "syn 2.0.111", -] - -[[package]] -name = "dashmap" -version = "6.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5041cc499144891f3790297212f32a74fb938e5136a14943f338ef9e0ae276cf" -dependencies = [ - "cfg-if", - "crossbeam-utils", - "hashbrown 0.14.5", - "lock_api", - "once_cell", - "parking_lot_core", -] - -[[package]] -name = "deadpool" -version = "0.12.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0be2b1d1d6ec8d846f05e137292d0b89133caf95ef33695424c09568bdd39b1b" -dependencies = [ - "deadpool-runtime", - "lazy_static", - "num_cpus", - "tokio", -] - -[[package]] -name = "deadpool-runtime" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "092966b41edc516079bdf31ec78a2e0588d1d0c08f78b91d8307215928642b2b" - -[[package]] -name = "der" -version = "0.7.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" -dependencies = [ - "const-oid", - "zeroize", -] - -[[package]] -name = "deranged" -version = "0.5.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ececcb659e7ba858fb4f10388c250a7252eb0a27373f1a72b8748afdd248e587" -dependencies = [ - "powerfmt", - "serde_core", -] - -[[package]] -name = "derivative" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", -] - -[[package]] -name = "derive-where" -version = "1.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef941ded77d15ca19b40374869ac6000af1c9f2a4c0f3d4c70926287e6364a8f" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.111", -] - -[[package]] -name = "derive_more" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" -dependencies = [ - "derive_more-impl", -] - -[[package]] -name = "derive_more-impl" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" -dependencies = [ - "convert_case", - "proc-macro2", - "quote", - "rustc_version 0.4.1", - "syn 2.0.111", - "unicode-xid", -] - -[[package]] -name = "digest" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066" -dependencies = [ - "generic-array", -] - -[[package]] -name = "digest" -version = "0.10.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" -dependencies = [ - "block-buffer", - "const-oid", - "crypto-common", - "subtle", -] - -[[package]] -name = "displaydoc" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.111", -] - -[[package]] -name = "docker_credential" -version = "1.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d89dfcba45b4afad7450a99b39e751590463e45c04728cf555d36bb66940de8" -dependencies = [ - "base64 0.21.7", - "serde", - "serde_json", -] - -[[package]] -name = "dotenvy" -version = "0.15.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" - -[[package]] -name = "downcast" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1435fa1053d8b2fbbe9be7e97eca7f33d37b28409959813daefc1446a14247f1" - -[[package]] -name = "dunce" -version = "1.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" - -[[package]] -name = "dyn-clone" -version = "1.0.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" - -[[package]] -name = "ecdsa" -version = "0.16.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" -dependencies = [ - "der", - "digest 0.10.7", - "elliptic-curve", - "rfc6979", - "serdect", - "signature", - "spki", -] - -[[package]] -name = "educe" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d7bc049e1bd8cdeb31b68bbd586a9464ecf9f3944af3958a7a9d0f8b9799417" -dependencies = [ - "enum-ordinalize", - "proc-macro2", - "quote", - "syn 2.0.111", -] - -[[package]] -name = "either" -version = "1.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" -dependencies = [ - "serde", -] - -[[package]] -name = "elliptic-curve" -version = "0.13.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" -dependencies = [ - "base16ct", - "crypto-bigint", - "digest 0.10.7", - "ff", - "generic-array", - "group", - "pkcs8", - "rand_core 0.6.4", - "sec1", - "serdect", - "subtle", - "zeroize", -] - -[[package]] -name = "encode_unicode" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" - -[[package]] -name = "encoding_rs" -version = "0.8.35" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "enr" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "851bd664a3d3a3c175cff92b2f0df02df3c541b4895d0ae307611827aae46152" -dependencies = [ - "alloy-rlp", - "base64 0.22.1", - "bytes", - "hex", - "log", - "rand 0.8.5", - "secp256k1 0.30.0", - "sha3", - "zeroize", -] - -[[package]] -name = "enum-ordinalize" -version = "4.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a1091a7bb1f8f2c4b28f1fe2cef4980ca2d410a3d727d67ecc3178c9b0800f0" -dependencies = [ - "enum-ordinalize-derive", -] - -[[package]] -name = "enum-ordinalize-derive" -version = "4.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ca9601fb2d62598ee17836250842873a413586e5d7ed88b356e38ddbb0ec631" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.111", -] - -[[package]] -name = "equivalent" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" - -[[package]] -name = "errno" -version = "0.3.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" -dependencies = [ - "libc", - "windows-sys 0.61.2", -] - -[[package]] -name = "etcetera" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "136d1b5283a1ab77bd9257427ffd09d8667ced0570b6f938942bc7568ed5b943" -dependencies = [ - "cfg-if", - "home", - "windows-sys 0.48.0", -] - -[[package]] -name = "ethereum_serde_utils" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3dc1355dbb41fbbd34ec28d4fb2a57d9a70c67ac3c19f6a5ca4d4a176b9e997a" -dependencies = [ - "alloy-primitives", - "hex", - "serde", - "serde_derive", - "serde_json", -] - -[[package]] -name = "ethereum_ssz" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0dcddb2554d19cde19b099fadddde576929d7a4d0c1cd3512d1fd95cf174375c" -dependencies = [ - "alloy-primitives", - "ethereum_serde_utils", - "itertools 0.13.0", - "serde", - "serde_derive", - "smallvec", - "typenum", -] - -[[package]] -name = "ethereum_ssz_derive" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a657b6b3b7e153637dc6bdc6566ad9279d9ee11a15b12cfb24a2e04360637e9f" -dependencies = [ - "darling 0.20.11", - "proc-macro2", - "quote", - "syn 2.0.111", -] - -[[package]] -name = "event-listener" -version = "5.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" -dependencies = [ - "concurrent-queue", - "parking", - "pin-project-lite", -] - -[[package]] -name = "event-listener-strategy" -version = "0.5.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" -dependencies = [ - "event-listener", - "pin-project-lite", -] - -[[package]] -name = "fastrand" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" - -[[package]] -name = "fastrlp" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "139834ddba373bbdd213dffe02c8d110508dcf1726c2be27e8d1f7d7e1856418" -dependencies = [ - "arrayvec", - "auto_impl", - "bytes", -] - -[[package]] -name = "fastrlp" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce8dba4714ef14b8274c371879b175aa55b16b30f269663f19d576f380018dc4" -dependencies = [ - "arrayvec", - "auto_impl", - "bytes", -] - -[[package]] -name = "ff" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393" -dependencies = [ - "rand_core 0.6.4", - "subtle", -] - -[[package]] -name = "filetime" -version = "0.2.26" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc0505cd1b6fa6580283f6bdf70a73fcf4aba1184038c90902b92b3dd0df63ed" -dependencies = [ - "cfg-if", - "libc", - "libredox", - "windows-sys 0.60.2", -] - -[[package]] -name = "find-msvc-tools" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a3076410a55c90011c298b04d0cfa770b00fa04e1e3c97d3f6c9de105a03844" - -[[package]] -name = "fixed-hash" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "835c052cb0c08c1acf6ffd71c022172e18723949c8282f2b9f27efbc51e64534" -dependencies = [ - "byteorder", - "rand 0.8.5", - "rustc-hex", - "static_assertions", -] - -[[package]] -name = "fnv" -version = "1.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" - -[[package]] -name = "foldhash" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" - -[[package]] -name = "foldhash" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" - -[[package]] -name = "foreign-types" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" -dependencies = [ - "foreign-types-shared", -] - -[[package]] -name = "foreign-types-shared" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" - -[[package]] -name = "form_urlencoded" -version = "1.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" -dependencies = [ - "percent-encoding", -] - -[[package]] -name = "fragile" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28dd6caf6059519a65843af8fe2a3ae298b14b80179855aeb4adc2c1934ee619" - -[[package]] -name = "fs_extra" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" - -[[package]] -name = "funty" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" - -[[package]] -name = "futures" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" -dependencies = [ - "futures-channel", - "futures-core", - "futures-executor", - "futures-io", - "futures-sink", - "futures-task", - "futures-util", -] - -[[package]] -name = "futures-channel" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" -dependencies = [ - "futures-core", - "futures-sink", -] - -[[package]] -name = "futures-core" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" - -[[package]] -name = "futures-executor" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" -dependencies = [ - "futures-core", - "futures-task", - "futures-util", -] - -[[package]] -name = "futures-io" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" - -[[package]] -name = "futures-macro" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.111", -] - -[[package]] -name = "futures-sink" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" - -[[package]] -name = "futures-task" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" - -[[package]] -name = "futures-util" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" -dependencies = [ - "futures-channel", - "futures-core", - "futures-io", - "futures-macro", - "futures-sink", - "futures-task", - "memchr", - "pin-project-lite", - "pin-utils", - "slab", -] - -[[package]] -name = "futures-utils-wasm" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42012b0f064e01aa58b545fe3727f90f7dd4020f4a3ea735b50344965f5a57e9" - -[[package]] -name = "generic-array" -version = "0.14.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4bb6743198531e02858aeaea5398fcc883e71851fcbcb5a2f773e2fb6cb1edf2" -dependencies = [ - "typenum", - "version_check", - "zeroize", -] - -[[package]] -name = "getrandom" -version = "0.2.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" -dependencies = [ - "cfg-if", - "js-sys", - "libc", - "wasi", - "wasm-bindgen", -] - -[[package]] -name = "getrandom" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" -dependencies = [ - "cfg-if", - "js-sys", - "libc", - "r-efi", - "wasip2", - "wasm-bindgen", -] - -[[package]] -name = "glob" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" - -[[package]] -name = "gmp-mpfr-sys" -version = "1.6.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60f8970a75c006bb2f8ae79c6768a116dd215fa8346a87aed99bf9d82ca43394" -dependencies = [ - "libc", - "windows-sys 0.60.2", -] - -[[package]] -name = "group" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" -dependencies = [ - "ff", - "rand_core 0.6.4", - "subtle", -] - -[[package]] -name = "h2" -version = "0.3.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0beca50380b1fc32983fc1cb4587bfa4bb9e78fc259aad4a0032d2080309222d" -dependencies = [ - "bytes", - "fnv", - "futures-core", - "futures-sink", - "futures-util", - "http 0.2.12", - "indexmap 2.12.1", - "slab", - "tokio", - "tokio-util", - "tracing", -] - -[[package]] -name = "h2" -version = "0.4.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3c0b69cfcb4e1b9f1bf2f53f95f766e4661169728ec61cd3fe5a0166f2d1386" -dependencies = [ - "atomic-waker", - "bytes", - "fnv", - "futures-core", - "futures-sink", - "http 1.4.0", - "indexmap 2.12.1", - "slab", - "tokio", - "tokio-util", - "tracing", -] - -[[package]] -name = "hashbrown" -version = "0.12.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" - -[[package]] -name = "hashbrown" -version = "0.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43a3c133739dddd0d2990f9a4bdf8eb4b21ef50e4851ca85ab661199821d510e" - -[[package]] -name = "hashbrown" -version = "0.14.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" - -[[package]] -name = "hashbrown" -version = "0.15.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" -dependencies = [ - "allocator-api2", - "equivalent", - "foldhash 0.1.5", -] - -[[package]] -name = "hashbrown" -version = "0.16.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" -dependencies = [ - "foldhash 0.2.0", - "serde", - "serde_core", -] - -[[package]] -name = "heck" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" - -[[package]] -name = "hermit-abi" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" - -[[package]] -name = "hex" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" - -[[package]] -name = "hex-conservative" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fda06d18ac606267c40c04e41b9947729bf8b9efe74bd4e82b61a5f26a510b9f" -dependencies = [ - "arrayvec", -] - -[[package]] -name = "hmac" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" -dependencies = [ - "digest 0.10.7", -] - -[[package]] -name = "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 = "0.2.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" -dependencies = [ - "bytes", - "fnv", - "itoa", -] - -[[package]] -name = "http" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" -dependencies = [ - "bytes", - "itoa", -] - -[[package]] -name = "http-body" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" -dependencies = [ - "bytes", - "http 0.2.12", - "pin-project-lite", -] - -[[package]] -name = "http-body" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" -dependencies = [ - "bytes", - "http 1.4.0", -] - -[[package]] -name = "http-body-util" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" -dependencies = [ - "bytes", - "futures-core", - "http 1.4.0", - "http-body 1.0.1", - "pin-project-lite", -] - -[[package]] -name = "httparse" -version = "1.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" - -[[package]] -name = "httpdate" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" - -[[package]] -name = "hyper" -version = "0.14.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41dfc780fdec9373c01bae43289ea34c972e40ee3c9f6b3c8801a35f35586ce7" -dependencies = [ - "bytes", - "futures-channel", - "futures-core", - "futures-util", - "h2 0.3.27", - "http 0.2.12", - "http-body 0.4.6", - "httparse", - "httpdate", - "itoa", - "pin-project-lite", - "socket2 0.5.10", - "tokio", - "tower-service", - "tracing", - "want", -] - -[[package]] -name = "hyper" -version = "1.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11" -dependencies = [ - "atomic-waker", - "bytes", - "futures-channel", - "futures-core", - "h2 0.4.12", - "http 1.4.0", - "http-body 1.0.1", - "httparse", - "httpdate", - "itoa", - "pin-project-lite", - "pin-utils", - "smallvec", - "tokio", - "want", -] - -[[package]] -name = "hyper-named-pipe" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73b7d8abf35697b81a825e386fc151e0d503e8cb5fcb93cc8669c376dfd6f278" -dependencies = [ - "hex", - "hyper 1.8.1", - "hyper-util", - "pin-project-lite", - "tokio", - "tower-service", - "winapi", -] - -[[package]] -name = "hyper-rustls" -version = "0.24.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec3efd23720e2049821a693cbc7e65ea87c72f1c58ff2f9522ff332b1491e590" -dependencies = [ - "futures-util", - "http 0.2.12", - "hyper 0.14.32", - "log", - "rustls 0.21.12", - "tokio", - "tokio-rustls 0.24.1", -] - -[[package]] -name = "hyper-rustls" -version = "0.27.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" -dependencies = [ - "http 1.4.0", - "hyper 1.8.1", - "hyper-util", - "log", - "rustls 0.23.35", - "rustls-native-certs", - "rustls-pki-types", - "tokio", - "tokio-rustls 0.26.4", - "tower-service", -] - -[[package]] -name = "hyper-tls" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" -dependencies = [ - "bytes", - "http-body-util", - "hyper 1.8.1", - "hyper-util", - "native-tls", - "tokio", - "tokio-native-tls", - "tower-service", -] - -[[package]] -name = "hyper-util" -version = "0.1.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "727805d60e7938b76b826a6ef209eb70eaa1812794f9424d4a4e2d740662df5f" -dependencies = [ - "base64 0.22.1", - "bytes", - "futures-channel", - "futures-core", - "futures-util", - "http 1.4.0", - "http-body 1.0.1", - "hyper 1.8.1", - "ipnet", - "libc", - "percent-encoding", - "pin-project-lite", - "socket2 0.6.1", - "system-configuration", - "tokio", - "tower-service", - "tracing", - "windows-registry", -] - -[[package]] -name = "hyperlocal" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "986c5ce3b994526b3cd75578e62554abd09f0899d6206de48b3e96ab34ccc8c7" -dependencies = [ - "hex", - "http-body-util", - "hyper 1.8.1", - "hyper-util", - "pin-project-lite", - "tokio", - "tower-service", -] - -[[package]] -name = "iana-time-zone" -version = "0.1.64" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb" -dependencies = [ - "android_system_properties", - "core-foundation-sys", - "iana-time-zone-haiku", - "js-sys", - "log", - "wasm-bindgen", - "windows-core", -] - -[[package]] -name = "iana-time-zone-haiku" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" -dependencies = [ - "cc", -] - -[[package]] -name = "icu_collections" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" -dependencies = [ - "displaydoc", - "potential_utf", - "yoke", - "zerofrom", - "zerovec", -] - -[[package]] -name = "icu_locale_core" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" -dependencies = [ - "displaydoc", - "litemap", - "tinystr", - "writeable", - "zerovec", -] - -[[package]] -name = "icu_normalizer" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" -dependencies = [ - "icu_collections", - "icu_normalizer_data", - "icu_properties", - "icu_provider", - "smallvec", - "zerovec", -] - -[[package]] -name = "icu_normalizer_data" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" - -[[package]] -name = "icu_properties" -version = "2.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" -dependencies = [ - "icu_collections", - "icu_locale_core", - "icu_properties_data", - "icu_provider", - "zerotrie", - "zerovec", -] - -[[package]] -name = "icu_properties_data" -version = "2.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" - -[[package]] -name = "icu_provider" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" -dependencies = [ - "displaydoc", - "icu_locale_core", - "writeable", - "yoke", - "zerofrom", - "zerotrie", - "zerovec", -] - -[[package]] -name = "ident_case" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" - -[[package]] -name = "idna" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" -dependencies = [ - "idna_adapter", - "smallvec", - "utf8_iter", -] - -[[package]] -name = "idna_adapter" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" -dependencies = [ - "icu_normalizer", - "icu_properties", -] - -[[package]] -name = "impl-codec" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba6a270039626615617f3f36d15fc827041df3b78c439da2cadfa47455a77f2f" -dependencies = [ - "parity-scale-codec", -] - -[[package]] -name = "impl-trait-for-tuples" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0eb5a3343abf848c0984fe4604b2b105da9539376e24fc0a3b0007411ae4fd9" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.111", -] - -[[package]] -name = "include_dir" -version = "0.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "923d117408f1e49d914f1a379a309cffe4f18c05cf4e3d12e613a15fc81bd0dd" -dependencies = [ - "include_dir_macros", -] - -[[package]] -name = "include_dir_macros" -version = "0.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7cab85a7ed0bd5f0e76d93846e0147172bed2e2d3f859bcc33a8d9699cad1a75" -dependencies = [ - "proc-macro2", - "quote", -] - -[[package]] -name = "indexmap" -version = "1.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" -dependencies = [ - "autocfg", - "hashbrown 0.12.3", - "serde", -] - -[[package]] -name = "indexmap" -version = "2.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ad4bb2b565bca0645f4d68c5c9af97fba094e9791da685bf83cb5f3ce74acf2" -dependencies = [ - "equivalent", - "hashbrown 0.16.1", - "serde", - "serde_core", -] - -[[package]] -name = "indicatif" -version = "0.17.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "183b3088984b400f4cfac3620d5e076c84da5364016b4f49473de574b2586235" -dependencies = [ - "console", - "number_prefix", - "portable-atomic", - "unicode-width", - "web-time", -] - -[[package]] -name = "ipnet" -version = "2.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" - -[[package]] -name = "iri-string" -version = "0.7.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f867b9d1d896b67beb18518eda36fdb77a32ea590de864f1325b294a6d14397" -dependencies = [ - "memchr", - "serde", -] - -[[package]] -name = "is_terminal_polyfill" -version = "1.70.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" - -[[package]] -name = "itertools" -version = "0.10.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" -dependencies = [ - "either", -] - -[[package]] -name = "itertools" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" -dependencies = [ - "either", -] - -[[package]] -name = "itertools" -version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" -dependencies = [ - "either", -] - -[[package]] -name = "itoa" -version = "1.0.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ee5b5339afb4c41626dde77b7a611bd4f2c202b897852b4bcf5d03eddc61010" - -[[package]] -name = "jni" -version = "0.21.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" -dependencies = [ - "cesu8", - "cfg-if", - "combine", - "jni-sys", - "log", - "thiserror 1.0.69", - "walkdir", - "windows-sys 0.45.0", -] - -[[package]] -name = "jni-sys" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" - -[[package]] -name = "jobserver" -version = "0.1.34" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" -dependencies = [ - "getrandom 0.3.4", - "libc", -] - -[[package]] -name = "js-sys" -version = "0.3.83" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "464a3709c7f55f1f721e5389aa6ea4e3bc6aba669353300af094b29ffbdde1d8" -dependencies = [ - "once_cell", - "wasm-bindgen", -] - -[[package]] -name = "jsonrpsee" -version = "0.26.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f3f48dc3e6b8bd21e15436c1ddd0bc22a6a54e8ec46fedd6adf3425f396ec6a" -dependencies = [ - "jsonrpsee-core", - "jsonrpsee-http-client", - "jsonrpsee-proc-macros", - "jsonrpsee-server", - "jsonrpsee-types", - "tokio", - "tracing", -] - -[[package]] -name = "jsonrpsee-core" -version = "0.26.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "316c96719901f05d1137f19ba598b5fe9c9bc39f4335f67f6be8613921946480" -dependencies = [ - "async-trait", - "bytes", - "futures-util", - "http 1.4.0", - "http-body 1.0.1", - "http-body-util", - "jsonrpsee-types", - "parking_lot", - "pin-project", - "rand 0.9.2", - "rustc-hash", - "serde", - "serde_json", - "thiserror 2.0.17", - "tokio", - "tower", - "tracing", -] - -[[package]] -name = "jsonrpsee-http-client" -version = "0.26.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "790bedefcec85321e007ff3af84b4e417540d5c87b3c9779b9e247d1bcc3dab8" -dependencies = [ - "base64 0.22.1", - "http-body 1.0.1", - "hyper 1.8.1", - "hyper-rustls 0.27.7", - "hyper-util", - "jsonrpsee-core", - "jsonrpsee-types", - "rustls 0.23.35", - "rustls-platform-verifier", - "serde", - "serde_json", - "thiserror 2.0.17", - "tokio", - "tower", - "url", -] - -[[package]] -name = "jsonrpsee-proc-macros" -version = "0.26.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2da3f8ab5ce1bb124b6d082e62dffe997578ceaf0aeb9f3174a214589dc00f07" -dependencies = [ - "heck", - "proc-macro-crate", - "proc-macro2", - "quote", - "syn 2.0.111", -] - -[[package]] -name = "jsonrpsee-server" -version = "0.26.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c51b7c290bb68ce3af2d029648148403863b982f138484a73f02a9dd52dbd7f" -dependencies = [ - "futures-util", - "http 1.4.0", - "http-body 1.0.1", - "http-body-util", - "hyper 1.8.1", - "hyper-util", - "jsonrpsee-core", - "jsonrpsee-types", - "pin-project", - "route-recognizer", - "serde", - "serde_json", - "soketto", - "thiserror 2.0.17", - "tokio", - "tokio-stream", - "tokio-util", - "tower", - "tracing", -] - -[[package]] -name = "jsonrpsee-types" -version = "0.26.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc88ff4688e43cc3fa9883a8a95c6fa27aa2e76c96e610b737b6554d650d7fd5" -dependencies = [ - "http 1.4.0", - "serde", - "serde_json", - "thiserror 2.0.17", -] - -[[package]] -name = "k256" -version = "0.13.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6e3919bbaa2945715f0bb6d3934a173d1e9a59ac23767fbaaef277265a7411b" -dependencies = [ - "cfg-if", - "ecdsa", - "elliptic-curve", - "once_cell", - "serdect", - "sha2", -] - -[[package]] -name = "keccak" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ecc2af9a1119c51f12a14607e783cb977bde58bc069ff0c3da1095e635d70654" -dependencies = [ - "cpufeatures", -] - -[[package]] -name = "keccak-asm" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "505d1856a39b200489082f90d897c3f07c455563880bc5952e38eabf731c83b6" -dependencies = [ - "digest 0.10.7", - "sha3-asm", -] - -[[package]] -name = "lazy_static" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" - -[[package]] -name = "libc" -version = "0.2.178" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37c93d8daa9d8a012fd8ab92f088405fb202ea0b6ab73ee2482ae66af4f42091" - -[[package]] -name = "libloading" -version = "0.8.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" -dependencies = [ - "cfg-if", - "windows-link", -] - -[[package]] -name = "libm" -version = "0.2.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" - -[[package]] -name = "libredox" -version = "0.1.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df15f6eac291ed1cf25865b1ee60399f57e7c227e7f51bdbd4c5270396a9ed50" -dependencies = [ - "bitflags 2.10.0", - "libc", - "redox_syscall 0.6.0", -] - -[[package]] -name = "libz-sys" -version = "1.1.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15d118bbf3771060e7311cc7bb0545b01d08a8b4a7de949198dec1fa0ca1c0f7" -dependencies = [ - "cc", - "libc", - "pkg-config", - "vcpkg", -] - -[[package]] -name = "linux-raw-sys" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" - -[[package]] -name = "litemap" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" - -[[package]] -name = "lock_api" -version = "0.4.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" -dependencies = [ - "scopeguard", -] - -[[package]] -name = "log" -version = "0.4.29" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" - -[[package]] -name = "lru" -version = "0.12.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38" -dependencies = [ - "hashbrown 0.15.5", -] - -[[package]] -name = "lru" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "227748d55f2f0ab4735d87fd623798cb6b664512fe979705f829c9f81c934465" -dependencies = [ - "hashbrown 0.15.5", -] - -[[package]] -name = "lru-slab" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" - -[[package]] -name = "macro-string" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b27834086c65ec3f9387b096d66e99f221cf081c2b738042aa252bcd41204e3" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.111", -] - -[[package]] -name = "matchers" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" -dependencies = [ - "regex-automata", -] - -[[package]] -name = "matchit" -version = "0.8.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" - -[[package]] -name = "md-5" -version = "0.10.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" -dependencies = [ - "cfg-if", - "digest 0.10.7", -] - -[[package]] -name = "memchr" -version = "2.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" - -[[package]] -name = "metrics" -version = "0.24.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d5312e9ba3771cfa961b585728215e3d972c950a3eed9252aa093d6301277e8" -dependencies = [ - "ahash", - "portable-atomic", -] - -[[package]] -name = "metrics-derive" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3dbdd96ed57d565ec744cba02862d707acf373c5772d152abae6ec5c4e24f6c" -dependencies = [ - "proc-macro2", - "quote", - "regex", - "syn 2.0.111", -] - -[[package]] -name = "metrics-exporter-prometheus" -version = "0.17.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b166dea96003ee2531cf14833efedced545751d800f03535801d833313f8c15" -dependencies = [ - "base64 0.22.1", - "http-body-util", - "hyper 1.8.1", - "hyper-util", - "indexmap 2.12.1", - "ipnet", - "metrics", - "metrics-util", - "quanta", - "thiserror 2.0.17", - "tokio", - "tracing", -] - -[[package]] -name = "metrics-util" -version = "0.20.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cdfb1365fea27e6dd9dc1dbc19f570198bc86914533ad639dae939635f096be4" -dependencies = [ - "crossbeam-epoch", - "crossbeam-utils", - "hashbrown 0.16.1", - "metrics", - "quanta", - "rand 0.9.2", - "rand_xoshiro", - "sketches-ddsketch", -] - -[[package]] -name = "mime" -version = "0.3.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" - -[[package]] -name = "minimal-lexical" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" - -[[package]] -name = "miniz_oxide" -version = "0.8.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" -dependencies = [ - "adler2", -] - -[[package]] -name = "mio" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" -dependencies = [ - "libc", - "wasi", - "windows-sys 0.61.2", -] - -[[package]] -name = "mockall" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39a6bfcc6c8c7eed5ee98b9c3e33adc726054389233e201c95dab2d41a3839d2" -dependencies = [ - "cfg-if", - "downcast", - "fragile", - "mockall_derive", - "predicates", - "predicates-tree", -] - -[[package]] -name = "mockall_derive" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25ca3004c2efe9011bd4e461bd8256445052b9615405b4f7ea43fc8ca5c20898" -dependencies = [ - "cfg-if", - "proc-macro2", - "quote", - "syn 2.0.111", -] - -[[package]] -name = "modular-bitfield" -version = "0.11.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a53d79ba8304ac1c4f9eb3b9d281f21f7be9d4626f72ce7df4ad8fbde4f38a74" -dependencies = [ - "modular-bitfield-impl", - "static_assertions", -] - -[[package]] -name = "modular-bitfield-impl" -version = "0.11.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a7d5f7076603ebc68de2dc6a650ec331a062a13abaa346975be747bbfa4b789" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", -] - -[[package]] -name = "moka" -version = "0.12.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3dec6bd31b08944e08b58fd99373893a6c17054d6f3ea5006cc894f4f4eee2a" -dependencies = [ - "async-lock", - "crossbeam-channel", - "crossbeam-epoch", - "crossbeam-utils", - "equivalent", - "event-listener", - "futures-util", - "parking_lot", - "portable-atomic", - "smallvec", - "tagptr", - "uuid", -] - -[[package]] -name = "native-tls" -version = "0.2.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e" -dependencies = [ - "libc", - "log", - "openssl", - "openssl-probe", - "openssl-sys", - "schannel", - "security-framework 2.11.1", - "security-framework-sys", - "tempfile", -] - -[[package]] -name = "nom" -version = "7.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" -dependencies = [ - "memchr", - "minimal-lexical", -] - -[[package]] -name = "nu-ansi-term" -version = "0.50.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" -dependencies = [ - "windows-sys 0.61.2", -] - -[[package]] -name = "num" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35bd024e8b2ff75562e5f34e7f4905839deb4b22955ef5e73d2fea1b9813cb23" -dependencies = [ - "num-bigint", - "num-complex", - "num-integer", - "num-iter", - "num-rational", - "num-traits", -] - -[[package]] -name = "num-bigint" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" -dependencies = [ - "num-integer", - "num-traits", -] - -[[package]] -name = "num-complex" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" -dependencies = [ - "num-traits", -] - -[[package]] -name = "num-conv" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" - -[[package]] -name = "num-integer" -version = "0.1.46" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" -dependencies = [ - "num-traits", -] - -[[package]] -name = "num-iter" -version = "0.1.45" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" -dependencies = [ - "autocfg", - "num-integer", - "num-traits", -] - -[[package]] -name = "num-rational" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" -dependencies = [ - "num-bigint", - "num-integer", - "num-traits", -] - -[[package]] -name = "num-traits" -version = "0.2.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" -dependencies = [ - "autocfg", - "libm", -] - -[[package]] -name = "num_cpus" -version = "1.17.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b" -dependencies = [ - "hermit-abi", - "libc", -] - -[[package]] -name = "num_enum" -version = "0.7.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1207a7e20ad57b847bbddc6776b968420d38292bbfe2089accff5e19e82454c" -dependencies = [ - "num_enum_derive", - "rustversion", -] - -[[package]] -name = "num_enum_derive" -version = "0.7.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff32365de1b6743cb203b710788263c44a03de03802daf96092f2da4fe6ba4d7" -dependencies = [ - "proc-macro-crate", - "proc-macro2", - "quote", - "syn 2.0.111", -] - -[[package]] -name = "number_prefix" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3" - -[[package]] -name = "nybbles" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c4b5ecbd0beec843101bffe848217f770e8b8da81d8355b7d6e226f2199b3dc" -dependencies = [ - "alloy-rlp", - "cfg-if", - "proptest", - "ruint", - "serde", - "smallvec", -] - -[[package]] -name = "once_cell" -version = "1.21.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" -dependencies = [ - "critical-section", - "portable-atomic", -] - -[[package]] -name = "once_cell_polyfill" -version = "1.70.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" - -[[package]] -name = "op-alloy-consensus" -version = "0.22.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "726da827358a547be9f1e37c2a756b9e3729cb0350f43408164794b370cad8ae" -dependencies = [ - "alloy-consensus", - "alloy-eips", - "alloy-network", - "alloy-primitives", - "alloy-rlp", - "alloy-rpc-types-eth", - "alloy-serde", - "derive_more", - "serde", - "thiserror 2.0.17", -] - -[[package]] -name = "op-alloy-flz" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a79f352fc3893dcd670172e615afef993a41798a1d3fc0db88a3e60ef2e70ecc" - -[[package]] -name = "op-alloy-network" -version = "0.22.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f63f27e65be273ec8fcb0b6af0fd850b550979465ab93423705ceb3dfddbd2ab" -dependencies = [ - "alloy-consensus", - "alloy-network", - "alloy-primitives", - "alloy-provider", - "alloy-rpc-types-eth", - "alloy-signer", - "op-alloy-consensus", - "op-alloy-rpc-types", -] - -[[package]] -name = "op-alloy-rpc-types" -version = "0.22.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "562dd4462562c41f9fdc4d860858c40e14a25df7f983ae82047f15f08fce4d19" -dependencies = [ - "alloy-consensus", - "alloy-eips", - "alloy-network-primitives", - "alloy-primitives", - "alloy-rpc-types-eth", - "alloy-serde", - "derive_more", - "op-alloy-consensus", - "serde", - "serde_json", - "thiserror 2.0.17", -] - -[[package]] -name = "op-alloy-rpc-types-engine" -version = "0.22.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8f24b8cb66e4b33e6c9e508bf46b8ecafc92eadd0b93fedd306c0accb477657" -dependencies = [ - "alloy-consensus", - "alloy-eips", - "alloy-primitives", - "alloy-rlp", - "alloy-rpc-types-engine", - "derive_more", - "ethereum_ssz", - "ethereum_ssz_derive", - "op-alloy-consensus", - "snap", - "thiserror 2.0.17", -] - -[[package]] -name = "op-revm" -version = "12.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e31622d03b29c826e48800f4c8f389c8a9c440eb796a3e35203561a288f12985" -dependencies = [ - "auto_impl", - "revm", - "serde", -] - -[[package]] -name = "openssl" -version = "0.10.75" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08838db121398ad17ab8531ce9de97b244589089e290a384c900cb9ff7434328" -dependencies = [ - "bitflags 2.10.0", - "cfg-if", - "foreign-types", - "libc", - "once_cell", - "openssl-macros", - "openssl-sys", -] - -[[package]] -name = "openssl-macros" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.111", -] - -[[package]] -name = "openssl-probe" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" - -[[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" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82cab2d520aa75e3c58898289429321eb788c3106963d0dc886ec7a5f4adc321" -dependencies = [ - "cc", - "libc", - "openssl-src", - "pkg-config", - "vcpkg", -] - -[[package]] -name = "outref" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a80800c0488c3a21695ea981a54918fbb37abf04f4d0720c453632255e2ff0e" - -[[package]] -name = "p256" -version = "0.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b" -dependencies = [ - "ecdsa", - "elliptic-curve", - "primeorder", - "sha2", -] - -[[package]] -name = "parity-scale-codec" -version = "3.7.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "799781ae679d79a948e13d4824a40970bfa500058d245760dd857301059810fa" -dependencies = [ - "arrayvec", - "bitvec", - "byte-slice-cast", - "const_format", - "impl-trait-for-tuples", - "parity-scale-codec-derive", - "rustversion", - "serde", -] - -[[package]] -name = "parity-scale-codec-derive" -version = "3.7.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34b4653168b563151153c9e4c08ebed57fb8262bebfa79711552fa983c623e7a" -dependencies = [ - "proc-macro-crate", - "proc-macro2", - "quote", - "syn 2.0.111", -] - -[[package]] -name = "parking" -version = "2.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" - -[[package]] -name = "parking_lot" -version = "0.12.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" -dependencies = [ - "lock_api", - "parking_lot_core", -] - -[[package]] -name = "parking_lot_core" -version = "0.9.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" -dependencies = [ - "cfg-if", - "libc", - "redox_syscall 0.5.18", - "smallvec", - "windows-link", -] - -[[package]] -name = "parse-display" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "914a1c2265c98e2446911282c6ac86d8524f495792c38c5bd884f80499c7538a" -dependencies = [ - "parse-display-derive", - "regex", - "regex-syntax", -] - -[[package]] -name = "parse-display-derive" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ae7800a4c974efd12df917266338e79a7a74415173caf7e70aa0a0707345281" -dependencies = [ - "proc-macro2", - "quote", - "regex", - "regex-syntax", - "structmeta", - "syn 2.0.111", -] - -[[package]] -name = "paste" -version = "1.0.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" - -[[package]] -name = "percent-encoding" -version = "2.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" - -[[package]] -name = "pest" -version = "2.8.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cbcfd20a6d4eeba40179f05735784ad32bdaef05ce8e8af05f180d45bb3e7e22" -dependencies = [ - "memchr", - "ucd-trie", -] - -[[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.111", -] - -[[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" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "677f1add503faace112b9f1373e43e9e054bfdd22ff1a63c1bc485eaec6a6a8a" -dependencies = [ - "pin-project-internal", -] - -[[package]] -name = "pin-project-internal" -version = "1.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.111", -] - -[[package]] -name = "pin-project-lite" -version = "0.2.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" - -[[package]] -name = "pin-utils" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" - -[[package]] -name = "pkcs8" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" -dependencies = [ - "der", - "spki", -] - -[[package]] -name = "pkg-config" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" - -[[package]] -name = "portable-atomic" -version = "1.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f59e70c4aef1e55797c2e8fd94a4f2a973fc972cfde0e0b05f683667b0cd39dd" - -[[package]] -name = "potential_utf" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" -dependencies = [ - "zerovec", -] - -[[package]] -name = "powerfmt" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" - -[[package]] -name = "ppv-lite86" -version = "0.2.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" -dependencies = [ - "zerocopy", -] - -[[package]] -name = "predicates" -version = "3.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5d19ee57562043d37e82899fade9a22ebab7be9cef5026b07fda9cdd4293573" -dependencies = [ - "anstyle", - "predicates-core", -] - -[[package]] -name = "predicates-core" -version = "1.0.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "727e462b119fe9c93fd0eb1429a5f7647394014cf3c04ab2c0350eeb09095ffa" - -[[package]] -name = "predicates-tree" -version = "1.0.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72dd2d6d381dfb73a193c7fca536518d7caee39fc8503f74e7dc0be0531b425c" -dependencies = [ - "predicates-core", - "termtree", -] - -[[package]] -name = "primeorder" -version = "0.13.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6" -dependencies = [ - "elliptic-curve", -] - -[[package]] -name = "primitive-types" -version = "0.12.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b34d9fd68ae0b74a41b21c03c2f62847aa0ffea044eee893b4c140b37e244e2" -dependencies = [ - "fixed-hash", - "impl-codec", - "uint", -] - -[[package]] -name = "proc-macro-crate" -version = "3.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983" -dependencies = [ - "toml_edit", -] - -[[package]] -name = "proc-macro-error-attr2" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5" -dependencies = [ - "proc-macro2", - "quote", -] - -[[package]] -name = "proc-macro-error2" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802" -dependencies = [ - "proc-macro-error-attr2", - "proc-macro2", - "quote", - "syn 2.0.111", -] - -[[package]] -name = "proc-macro2" -version = "1.0.103" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "proptest" -version = "1.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bee689443a2bd0a16ab0348b52ee43e3b2d1b1f931c8aa5c9f8de4c86fbe8c40" -dependencies = [ - "bit-set", - "bit-vec", - "bitflags 2.10.0", - "num-traits", - "rand 0.9.2", - "rand_chacha 0.9.0", - "rand_xorshift", - "regex-syntax", - "rusty-fork", - "tempfile", - "unarray", -] - -[[package]] -name = "quanta" -version = "0.12.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3ab5a9d756f0d97bdc89019bd2e4ea098cf9cde50ee7564dde6b81ccc8f06c7" -dependencies = [ - "crossbeam-utils", - "libc", - "once_cell", - "raw-cpuid", - "wasi", - "web-sys", - "winapi", -] - -[[package]] -name = "quick-error" -version = "1.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" - -[[package]] -name = "quinn" -version = "0.11.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" -dependencies = [ - "bytes", - "cfg_aliases", - "pin-project-lite", - "quinn-proto", - "quinn-udp", - "rustc-hash", - "rustls 0.23.35", - "socket2 0.6.1", - "thiserror 2.0.17", - "tokio", - "tracing", - "web-time", -] - -[[package]] -name = "quinn-proto" -version = "0.11.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1906b49b0c3bc04b5fe5d86a77925ae6524a19b816ae38ce1e426255f1d8a31" -dependencies = [ - "bytes", - "getrandom 0.3.4", - "lru-slab", - "rand 0.9.2", - "ring", - "rustc-hash", - "rustls 0.23.35", - "rustls-pki-types", - "slab", - "thiserror 2.0.17", - "tinyvec", - "tracing", - "web-time", -] - -[[package]] -name = "quinn-udp" -version = "0.5.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" -dependencies = [ - "cfg_aliases", - "libc", - "once_cell", - "socket2 0.6.1", - "tracing", - "windows-sys 0.60.2", -] - -[[package]] -name = "quote" -version = "1.0.42" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" -dependencies = [ - "proc-macro2", -] - -[[package]] -name = "r-efi" -version = "5.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" - -[[package]] -name = "radium" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" - -[[package]] -name = "rand" -version = "0.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" -dependencies = [ - "libc", - "rand_chacha 0.3.1", - "rand_core 0.6.4", - "serde", -] - -[[package]] -name = "rand" -version = "0.9.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" -dependencies = [ - "rand_chacha 0.9.0", - "rand_core 0.9.3", - "serde", -] - -[[package]] -name = "rand_chacha" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" -dependencies = [ - "ppv-lite86", - "rand_core 0.6.4", -] - -[[package]] -name = "rand_chacha" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" -dependencies = [ - "ppv-lite86", - "rand_core 0.9.3", -] - -[[package]] -name = "rand_core" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" -dependencies = [ - "getrandom 0.2.16", -] - -[[package]] -name = "rand_core" -version = "0.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" -dependencies = [ - "getrandom 0.3.4", - "serde", -] - -[[package]] -name = "rand_xorshift" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "513962919efc330f829edb2535844d1b912b0fbe2ca165d613e4e8788bb05a5a" -dependencies = [ - "rand_core 0.9.3", -] - -[[package]] -name = "rand_xoshiro" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f703f4665700daf5512dcca5f43afa6af89f09db47fb56be587f80636bda2d41" -dependencies = [ - "rand_core 0.9.3", -] - -[[package]] -name = "rapidhash" -version = "4.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2988730ee014541157f48ce4dcc603940e00915edc3c7f9a8d78092256bb2493" -dependencies = [ - "rand 0.9.2", - "rustversion", -] - -[[package]] -name = "raw-cpuid" -version = "11.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "498cd0dc59d73224351ee52a95fee0f1a617a2eae0e7d9d720cc622c73a54186" -dependencies = [ - "bitflags 2.10.0", -] - -[[package]] -name = "rayon" -version = "1.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f" -dependencies = [ - "either", - "rayon-core", -] - -[[package]] -name = "rayon-core" -version = "1.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" -dependencies = [ - "crossbeam-deque", - "crossbeam-utils", -] - -[[package]] -name = "rdkafka" -version = "0.37.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14b52c81ac3cac39c9639b95c20452076e74b8d9a71bc6fc4d83407af2ea6fff" -dependencies = [ - "futures-channel", - "futures-util", - "libc", - "log", - "rdkafka-sys", - "serde", - "serde_derive", - "serde_json", - "slab", - "tokio", -] - -[[package]] -name = "rdkafka-sys" -version = "4.9.0+2.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5230dca48bc354d718269f3e4353280e188b610f7af7e2fcf54b7a79d5802872" -dependencies = [ - "libc", - "libz-sys", - "num_enum", - "openssl-sys", - "pkg-config", - "zstd-sys", -] - -[[package]] -name = "redox_syscall" -version = "0.3.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29" -dependencies = [ - "bitflags 1.3.2", -] - -[[package]] -name = "redox_syscall" -version = "0.5.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" -dependencies = [ - "bitflags 2.10.0", -] - -[[package]] -name = "redox_syscall" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec96166dafa0886eb81fe1c0a388bece180fbef2135f97c1e2cf8302e74b43b5" -dependencies = [ - "bitflags 2.10.0", -] - -[[package]] -name = "ref-cast" -version = "1.0.25" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f354300ae66f76f1c85c5f84693f0ce81d747e2c3f21a45fef496d89c960bf7d" -dependencies = [ - "ref-cast-impl", -] - -[[package]] -name = "ref-cast-impl" -version = "1.0.25" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.111", -] - -[[package]] -name = "regex" -version = "1.12.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" -dependencies = [ - "aho-corasick", - "memchr", - "regex-automata", - "regex-syntax", -] - -[[package]] -name = "regex-automata" -version = "0.4.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" -dependencies = [ - "aho-corasick", - "memchr", - "regex-syntax", -] - -[[package]] -name = "regex-lite" -version = "0.1.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d942b98df5e658f56f20d592c7f868833fe38115e65c33003d8cd224b0155da" - -[[package]] -name = "regex-syntax" -version = "0.8.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" - -[[package]] -name = "reqwest" -version = "0.12.26" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b4c14b2d9afca6a60277086b0cc6a6ae0b568f6f7916c943a8cdc79f8be240f" -dependencies = [ - "base64 0.22.1", - "bytes", - "encoding_rs", - "futures-core", - "h2 0.4.12", - "http 1.4.0", - "http-body 1.0.1", - "http-body-util", - "hyper 1.8.1", - "hyper-rustls 0.27.7", - "hyper-tls", - "hyper-util", - "js-sys", - "log", - "mime", - "native-tls", - "percent-encoding", - "pin-project-lite", - "quinn", - "rustls 0.23.35", - "rustls-native-certs", - "rustls-pki-types", - "serde", - "serde_json", - "serde_urlencoded", - "sync_wrapper", - "tokio", - "tokio-native-tls", - "tokio-rustls 0.26.4", - "tower", - "tower-http", - "tower-service", - "url", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", -] - -[[package]] -name = "reth-chain-state" -version = "1.9.3" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.9.3#27a8c0f5a6dfb27dea84c5751776ecabdd069646" -dependencies = [ - "alloy-consensus", - "alloy-eips", - "alloy-primitives", - "derive_more", - "metrics", - "parking_lot", - "pin-project", - "reth-chainspec", - "reth-errors", - "reth-ethereum-primitives", - "reth-execution-types", - "reth-metrics", - "reth-primitives-traits", - "reth-storage-api", - "reth-trie", - "revm-database", - "tokio", - "tokio-stream", - "tracing", -] - -[[package]] -name = "reth-chainspec" -version = "1.9.3" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.9.3#27a8c0f5a6dfb27dea84c5751776ecabdd069646" -dependencies = [ - "alloy-chains", - "alloy-consensus", - "alloy-eips", - "alloy-evm", - "alloy-genesis", - "alloy-primitives", - "alloy-trie", - "auto_impl", - "derive_more", - "reth-ethereum-forks", - "reth-network-peers", - "reth-primitives-traits", - "serde_json", -] - -[[package]] -name = "reth-codecs" -version = "1.9.3" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.9.3#27a8c0f5a6dfb27dea84c5751776ecabdd069646" -dependencies = [ - "alloy-consensus", - "alloy-eips", - "alloy-genesis", - "alloy-primitives", - "alloy-trie", - "bytes", - "modular-bitfield", - "op-alloy-consensus", - "reth-codecs-derive", - "reth-zstd-compressors", - "serde", -] - -[[package]] -name = "reth-codecs-derive" -version = "1.9.3" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.9.3#27a8c0f5a6dfb27dea84c5751776ecabdd069646" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.111", -] - -[[package]] -name = "reth-consensus" -version = "1.9.3" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.9.3#27a8c0f5a6dfb27dea84c5751776ecabdd069646" -dependencies = [ - "alloy-consensus", - "alloy-primitives", - "auto_impl", - "reth-execution-types", - "reth-primitives-traits", - "thiserror 2.0.17", -] - -[[package]] -name = "reth-consensus-common" -version = "1.9.3" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.9.3#27a8c0f5a6dfb27dea84c5751776ecabdd069646" -dependencies = [ - "alloy-consensus", - "alloy-eips", - "reth-chainspec", - "reth-consensus", - "reth-primitives-traits", -] - -[[package]] -name = "reth-db-models" -version = "1.9.3" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.9.3#27a8c0f5a6dfb27dea84c5751776ecabdd069646" -dependencies = [ - "alloy-eips", - "alloy-primitives", - "bytes", - "reth-primitives-traits", - "serde", -] - -[[package]] -name = "reth-errors" -version = "1.9.3" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.9.3#27a8c0f5a6dfb27dea84c5751776ecabdd069646" -dependencies = [ - "reth-consensus", - "reth-execution-errors", - "reth-storage-errors", - "thiserror 2.0.17", -] - -[[package]] -name = "reth-eth-wire-types" -version = "1.9.3" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.9.3#27a8c0f5a6dfb27dea84c5751776ecabdd069646" -dependencies = [ - "alloy-chains", - "alloy-consensus", - "alloy-eips", - "alloy-hardforks", - "alloy-primitives", - "alloy-rlp", - "bytes", - "derive_more", - "reth-chainspec", - "reth-codecs-derive", - "reth-ethereum-primitives", - "reth-primitives-traits", - "serde", - "thiserror 2.0.17", -] - -[[package]] -name = "reth-ethereum-forks" -version = "1.9.3" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.9.3#27a8c0f5a6dfb27dea84c5751776ecabdd069646" -dependencies = [ - "alloy-eip2124", - "alloy-hardforks", - "alloy-primitives", - "auto_impl", - "once_cell", - "rustc-hash", -] - -[[package]] -name = "reth-ethereum-primitives" -version = "1.9.3" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.9.3#27a8c0f5a6dfb27dea84c5751776ecabdd069646" -dependencies = [ - "alloy-consensus", - "alloy-eips", - "alloy-primitives", - "alloy-rlp", - "alloy-rpc-types-eth", - "alloy-serde", - "reth-primitives-traits", - "reth-zstd-compressors", - "serde", - "serde_with", -] - -[[package]] -name = "reth-evm" -version = "1.9.3" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.9.3#27a8c0f5a6dfb27dea84c5751776ecabdd069646" -dependencies = [ - "alloy-consensus", - "alloy-eips", - "alloy-evm", - "alloy-primitives", - "auto_impl", - "derive_more", - "futures-util", - "reth-execution-errors", - "reth-execution-types", - "reth-primitives-traits", - "reth-storage-api", - "reth-storage-errors", - "reth-trie-common", - "revm", -] - -[[package]] -name = "reth-execution-errors" -version = "1.9.3" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.9.3#27a8c0f5a6dfb27dea84c5751776ecabdd069646" -dependencies = [ - "alloy-evm", - "alloy-primitives", - "alloy-rlp", - "nybbles", - "reth-storage-errors", - "thiserror 2.0.17", -] - -[[package]] -name = "reth-execution-types" -version = "1.9.3" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.9.3#27a8c0f5a6dfb27dea84c5751776ecabdd069646" -dependencies = [ - "alloy-consensus", - "alloy-eips", - "alloy-evm", - "alloy-primitives", - "derive_more", - "reth-ethereum-primitives", - "reth-primitives-traits", - "reth-trie-common", - "revm", - "serde", - "serde_with", -] - -[[package]] -name = "reth-fs-util" -version = "1.9.3" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.9.3#27a8c0f5a6dfb27dea84c5751776ecabdd069646" -dependencies = [ - "serde", - "serde_json", - "thiserror 2.0.17", -] - -[[package]] -name = "reth-metrics" -version = "1.9.3" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.9.3#27a8c0f5a6dfb27dea84c5751776ecabdd069646" -dependencies = [ - "metrics", - "metrics-derive", -] - -[[package]] -name = "reth-net-banlist" -version = "1.9.3" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.9.3#27a8c0f5a6dfb27dea84c5751776ecabdd069646" -dependencies = [ - "alloy-primitives", -] - -[[package]] -name = "reth-network-api" -version = "1.9.3" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.9.3#27a8c0f5a6dfb27dea84c5751776ecabdd069646" -dependencies = [ - "alloy-consensus", - "alloy-primitives", - "alloy-rpc-types-admin", - "alloy-rpc-types-eth", - "auto_impl", - "derive_more", - "enr", - "futures", - "reth-eth-wire-types", - "reth-ethereum-forks", - "reth-network-p2p", - "reth-network-peers", - "reth-network-types", - "reth-tokio-util", - "thiserror 2.0.17", - "tokio", - "tokio-stream", -] - -[[package]] -name = "reth-network-p2p" -version = "1.9.3" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.9.3#27a8c0f5a6dfb27dea84c5751776ecabdd069646" -dependencies = [ - "alloy-consensus", - "alloy-eips", - "alloy-primitives", - "auto_impl", - "derive_more", - "futures", - "reth-consensus", - "reth-eth-wire-types", - "reth-ethereum-primitives", - "reth-network-peers", - "reth-network-types", - "reth-primitives-traits", - "reth-storage-errors", - "tokio", - "tracing", -] - -[[package]] -name = "reth-network-peers" -version = "1.9.3" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.9.3#27a8c0f5a6dfb27dea84c5751776ecabdd069646" -dependencies = [ - "alloy-primitives", - "alloy-rlp", - "secp256k1 0.30.0", - "serde_with", - "thiserror 2.0.17", - "url", -] - -[[package]] -name = "reth-network-types" -version = "1.9.3" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.9.3#27a8c0f5a6dfb27dea84c5751776ecabdd069646" -dependencies = [ - "alloy-eip2124", - "reth-net-banlist", - "reth-network-peers", - "serde_json", - "tracing", -] - -[[package]] -name = "reth-optimism-chainspec" -version = "1.9.3" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.9.3#27a8c0f5a6dfb27dea84c5751776ecabdd069646" -dependencies = [ - "alloy-chains", - "alloy-consensus", - "alloy-eips", - "alloy-genesis", - "alloy-hardforks", - "alloy-primitives", - "derive_more", - "miniz_oxide", - "op-alloy-consensus", - "op-alloy-rpc-types", - "reth-chainspec", - "reth-ethereum-forks", - "reth-network-peers", - "reth-optimism-forks", - "reth-optimism-primitives", - "reth-primitives-traits", - "serde", - "serde_json", - "thiserror 2.0.17", -] - -[[package]] -name = "reth-optimism-consensus" -version = "1.9.3" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.9.3#27a8c0f5a6dfb27dea84c5751776ecabdd069646" -dependencies = [ - "alloy-consensus", - "alloy-eips", - "alloy-primitives", - "alloy-trie", - "reth-chainspec", - "reth-consensus", - "reth-consensus-common", - "reth-execution-types", - "reth-optimism-chainspec", - "reth-optimism-forks", - "reth-optimism-primitives", - "reth-primitives-traits", - "reth-storage-api", - "reth-storage-errors", - "reth-trie-common", - "revm", - "thiserror 2.0.17", - "tracing", -] - -[[package]] -name = "reth-optimism-evm" -version = "1.9.3" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.9.3#27a8c0f5a6dfb27dea84c5751776ecabdd069646" -dependencies = [ - "alloy-consensus", - "alloy-eips", - "alloy-evm", - "alloy-op-evm", - "alloy-primitives", - "op-alloy-consensus", - "op-alloy-rpc-types-engine", - "op-revm", - "reth-chainspec", - "reth-evm", - "reth-execution-errors", - "reth-execution-types", - "reth-optimism-chainspec", - "reth-optimism-consensus", - "reth-optimism-forks", - "reth-optimism-primitives", - "reth-primitives-traits", - "reth-storage-errors", - "revm", - "thiserror 2.0.17", -] - -[[package]] -name = "reth-optimism-forks" -version = "1.9.3" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.9.3#27a8c0f5a6dfb27dea84c5751776ecabdd069646" -dependencies = [ - "alloy-op-hardforks", - "alloy-primitives", - "once_cell", - "reth-ethereum-forks", -] - -[[package]] -name = "reth-optimism-primitives" -version = "1.9.3" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.9.3#27a8c0f5a6dfb27dea84c5751776ecabdd069646" -dependencies = [ - "alloy-consensus", - "alloy-eips", - "alloy-primitives", - "alloy-rlp", - "bytes", - "op-alloy-consensus", - "reth-codecs", - "reth-primitives-traits", - "reth-zstd-compressors", - "serde", - "serde_with", -] - -[[package]] -name = "reth-primitives-traits" -version = "1.9.3" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.9.3#27a8c0f5a6dfb27dea84c5751776ecabdd069646" -dependencies = [ - "alloy-consensus", - "alloy-eips", - "alloy-genesis", - "alloy-primitives", - "alloy-rlp", - "alloy-rpc-types-eth", - "alloy-trie", - "auto_impl", - "bytes", - "derive_more", - "once_cell", - "op-alloy-consensus", - "reth-codecs", - "revm-bytecode", - "revm-primitives", - "revm-state", - "secp256k1 0.30.0", - "serde", - "serde_with", - "thiserror 2.0.17", -] - -[[package]] -name = "reth-prune-types" -version = "1.9.3" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.9.3#27a8c0f5a6dfb27dea84c5751776ecabdd069646" -dependencies = [ - "alloy-primitives", - "derive_more", - "serde", - "strum", - "thiserror 2.0.17", -] - -[[package]] -name = "reth-revm" -version = "1.9.3" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.9.3#27a8c0f5a6dfb27dea84c5751776ecabdd069646" -dependencies = [ - "alloy-primitives", - "reth-primitives-traits", - "reth-storage-api", - "reth-storage-errors", - "revm", -] - -[[package]] -name = "reth-rpc-convert" -version = "1.9.3" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.9.3#27a8c0f5a6dfb27dea84c5751776ecabdd069646" -dependencies = [ - "alloy-consensus", - "alloy-json-rpc", - "alloy-network", - "alloy-primitives", - "alloy-rpc-types-eth", - "alloy-signer", - "auto_impl", - "dyn-clone", - "jsonrpsee-types", - "reth-ethereum-primitives", - "reth-evm", - "reth-primitives-traits", - "revm-context", - "thiserror 2.0.17", -] - -[[package]] -name = "reth-rpc-eth-types" -version = "1.9.3" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.9.3#27a8c0f5a6dfb27dea84c5751776ecabdd069646" -dependencies = [ - "alloy-consensus", - "alloy-eips", - "alloy-evm", - "alloy-network", - "alloy-primitives", - "alloy-rpc-client", - "alloy-rpc-types-eth", - "alloy-sol-types", - "alloy-transport", - "derive_more", - "futures", - "itertools 0.14.0", - "jsonrpsee-core", - "jsonrpsee-types", - "metrics", - "rand 0.9.2", - "reqwest", - "reth-chain-state", - "reth-chainspec", - "reth-errors", - "reth-ethereum-primitives", - "reth-evm", - "reth-execution-types", - "reth-metrics", - "reth-primitives-traits", - "reth-revm", - "reth-rpc-convert", - "reth-rpc-server-types", - "reth-storage-api", - "reth-tasks", - "reth-transaction-pool", - "reth-trie", - "revm", - "revm-inspectors", - "schnellru", - "serde", - "thiserror 2.0.17", - "tokio", - "tokio-stream", - "tracing", -] - -[[package]] -name = "reth-rpc-server-types" -version = "1.9.3" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.9.3#27a8c0f5a6dfb27dea84c5751776ecabdd069646" -dependencies = [ - "alloy-eips", - "alloy-primitives", - "alloy-rpc-types-engine", - "jsonrpsee-core", - "jsonrpsee-types", - "reth-errors", - "reth-network-api", - "serde", - "strum", -] - -[[package]] -name = "reth-stages-types" -version = "1.9.3" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.9.3#27a8c0f5a6dfb27dea84c5751776ecabdd069646" -dependencies = [ - "alloy-primitives", - "bytes", - "reth-trie-common", - "serde", -] - -[[package]] -name = "reth-static-file-types" -version = "1.9.3" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.9.3#27a8c0f5a6dfb27dea84c5751776ecabdd069646" -dependencies = [ - "alloy-primitives", - "derive_more", - "serde", - "strum", -] - -[[package]] -name = "reth-storage-api" -version = "1.9.3" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.9.3#27a8c0f5a6dfb27dea84c5751776ecabdd069646" -dependencies = [ - "alloy-consensus", - "alloy-eips", - "alloy-primitives", - "alloy-rpc-types-engine", - "auto_impl", - "reth-chainspec", - "reth-db-models", - "reth-ethereum-primitives", - "reth-execution-types", - "reth-primitives-traits", - "reth-prune-types", - "reth-stages-types", - "reth-storage-errors", - "reth-trie-common", - "revm-database", -] - -[[package]] -name = "reth-storage-errors" -version = "1.9.3" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.9.3#27a8c0f5a6dfb27dea84c5751776ecabdd069646" -dependencies = [ - "alloy-eips", - "alloy-primitives", - "alloy-rlp", - "derive_more", - "reth-primitives-traits", - "reth-prune-types", - "reth-static-file-types", - "revm-database-interface", - "thiserror 2.0.17", -] - -[[package]] -name = "reth-tasks" -version = "1.9.3" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.9.3#27a8c0f5a6dfb27dea84c5751776ecabdd069646" -dependencies = [ - "auto_impl", - "dyn-clone", - "futures-util", - "metrics", - "reth-metrics", - "thiserror 2.0.17", - "tokio", - "tracing", - "tracing-futures", -] - -[[package]] -name = "reth-tokio-util" -version = "1.9.3" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.9.3#27a8c0f5a6dfb27dea84c5751776ecabdd069646" -dependencies = [ - "tokio", - "tokio-stream", - "tracing", -] - -[[package]] -name = "reth-transaction-pool" -version = "1.9.3" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.9.3#27a8c0f5a6dfb27dea84c5751776ecabdd069646" -dependencies = [ - "alloy-consensus", - "alloy-eips", - "alloy-primitives", - "alloy-rlp", - "aquamarine", - "auto_impl", - "bitflags 2.10.0", - "futures-util", - "metrics", - "parking_lot", - "pin-project", - "reth-chain-state", - "reth-chainspec", - "reth-eth-wire-types", - "reth-ethereum-primitives", - "reth-execution-types", - "reth-fs-util", - "reth-metrics", - "reth-primitives-traits", - "reth-storage-api", - "reth-tasks", - "revm-interpreter", - "revm-primitives", - "rustc-hash", - "schnellru", - "serde", - "serde_json", - "smallvec", - "thiserror 2.0.17", - "tokio", - "tokio-stream", - "tracing", -] - -[[package]] -name = "reth-trie" -version = "1.9.3" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.9.3#27a8c0f5a6dfb27dea84c5751776ecabdd069646" -dependencies = [ - "alloy-consensus", - "alloy-eips", - "alloy-primitives", - "alloy-rlp", - "alloy-trie", - "auto_impl", - "itertools 0.14.0", - "reth-execution-errors", - "reth-primitives-traits", - "reth-stages-types", - "reth-storage-errors", - "reth-trie-common", - "reth-trie-sparse", - "revm-database", - "tracing", -] - -[[package]] -name = "reth-trie-common" -version = "1.9.3" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.9.3#27a8c0f5a6dfb27dea84c5751776ecabdd069646" -dependencies = [ - "alloy-consensus", - "alloy-primitives", - "alloy-rlp", - "alloy-rpc-types-eth", - "alloy-serde", - "alloy-trie", - "arrayvec", - "bytes", - "derive_more", - "itertools 0.14.0", - "nybbles", - "rayon", - "reth-primitives-traits", - "revm-database", - "serde", - "serde_with", -] - -[[package]] -name = "reth-trie-sparse" -version = "1.9.3" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.9.3#27a8c0f5a6dfb27dea84c5751776ecabdd069646" -dependencies = [ - "alloy-primitives", - "alloy-rlp", - "alloy-trie", - "auto_impl", - "reth-execution-errors", - "reth-primitives-traits", - "reth-trie-common", - "smallvec", - "tracing", -] - -[[package]] -name = "reth-zstd-compressors" -version = "1.9.3" -source = "git+https://github.com/paradigmxyz/reth?tag=v1.9.3#27a8c0f5a6dfb27dea84c5751776ecabdd069646" -dependencies = [ - "zstd", -] - -[[package]] -name = "revm" -version = "31.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb67a5223602113cae59a305acde2d9936bc18f2478dda879a6124b267cebfb6" -dependencies = [ - "revm-bytecode", - "revm-context", - "revm-context-interface", - "revm-database", - "revm-database-interface", - "revm-handler", - "revm-inspector", - "revm-interpreter", - "revm-precompile", - "revm-primitives", - "revm-state", -] - -[[package]] -name = "revm-bytecode" -version = "7.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2c6b5e6e8dd1e28a4a60e5f46615d4ef0809111c9e63208e55b5c7058200fb0" -dependencies = [ - "bitvec", - "phf", - "revm-primitives", - "serde", -] - -[[package]] -name = "revm-context" -version = "11.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92850e150f4f99d46c05a20ad0cd09286a7ad4ee21866fffb87101de6e602231" -dependencies = [ - "bitvec", - "cfg-if", - "derive-where", - "revm-bytecode", - "revm-context-interface", - "revm-database-interface", - "revm-primitives", - "revm-state", - "serde", -] - -[[package]] -name = "revm-context-interface" -version = "12.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6d701e2c2347d65216b066489ab22a0a8e1f7b2568256110d73a7d5eff3385c" -dependencies = [ - "alloy-eip2930", - "alloy-eip7702", - "auto_impl", - "either", - "revm-database-interface", - "revm-primitives", - "revm-state", - "serde", -] - -[[package]] -name = "revm-database" -version = "9.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "980d8d6bba78c5dd35b83abbb6585b0b902eb25ea4448ed7bfba6283b0337191" -dependencies = [ - "alloy-eips", - "revm-bytecode", - "revm-database-interface", - "revm-primitives", - "revm-state", - "serde", -] - -[[package]] -name = "revm-database-interface" -version = "8.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8cce03e3780287b07abe58faf4a7f5d8be7e81321f93ccf3343c8f7755602bae" -dependencies = [ - "auto_impl", - "either", - "revm-primitives", - "revm-state", - "serde", -] - -[[package]] -name = "revm-handler" -version = "12.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b45418ed95cfdf0cb19effdbb7633cf2144cab7fb0e6ffd6b0eb9117a50adff6" -dependencies = [ - "auto_impl", - "derive-where", - "revm-bytecode", - "revm-context", - "revm-context-interface", - "revm-database-interface", - "revm-interpreter", - "revm-precompile", - "revm-primitives", - "revm-state", - "serde", -] - -[[package]] -name = "revm-inspector" -version = "12.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c99801eac7da06cc112df2244bd5a64024f4ef21240e923b26e73c4b4a0e5da6" -dependencies = [ - "auto_impl", - "either", - "revm-context", - "revm-database-interface", - "revm-handler", - "revm-interpreter", - "revm-primitives", - "revm-state", - "serde", - "serde_json", -] - -[[package]] -name = "revm-inspectors" -version = "0.32.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21caa99f22184a6818946362778cccd3ff02f743c1e085bee87700671570ecb7" -dependencies = [ - "alloy-primitives", - "alloy-rpc-types-eth", - "alloy-rpc-types-trace", - "alloy-sol-types", - "anstyle", - "colorchoice", - "revm", - "serde", - "serde_json", - "thiserror 2.0.17", -] - -[[package]] -name = "revm-interpreter" -version = "29.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22789ce92c5808c70185e3bc49732f987dc6fd907f77828c8d3470b2299c9c65" -dependencies = [ - "revm-bytecode", - "revm-context-interface", - "revm-primitives", - "revm-state", - "serde", -] - -[[package]] -name = "revm-precompile" -version = "29.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "968b124028960201abf6d6bf8e223f15fadebb4307df6b7dc9244a0aab5d2d05" -dependencies = [ - "ark-bls12-381", - "ark-bn254", - "ark-ec", - "ark-ff 0.5.0", - "ark-serialize 0.5.0", - "arrayref", - "aurora-engine-modexp", - "c-kzg", - "cfg-if", - "k256", - "p256", - "revm-primitives", - "ripemd", - "rug", - "secp256k1 0.31.1", - "sha2", -] - -[[package]] -name = "revm-primitives" -version = "21.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29e161db429d465c09ba9cbff0df49e31049fe6b549e28eb0b7bd642fcbd4412" -dependencies = [ - "alloy-primitives", - "num_enum", - "once_cell", - "serde", -] - -[[package]] -name = "revm-state" -version = "8.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d8be953b7e374dbdea0773cf360debed8df394ea8d82a8b240a6b5da37592fc" -dependencies = [ - "bitflags 2.10.0", - "revm-bytecode", - "revm-primitives", - "serde", -] - -[[package]] -name = "rfc6979" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" -dependencies = [ - "hmac", - "subtle", -] - -[[package]] -name = "ring" -version = "0.17.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" -dependencies = [ - "cc", - "cfg-if", - "getrandom 0.2.16", - "libc", - "untrusted", - "windows-sys 0.52.0", -] - -[[package]] -name = "ripemd" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd124222d17ad93a644ed9d011a40f4fb64aa54275c08cc216524a9ea82fb09f" -dependencies = [ - "digest 0.10.7", -] - -[[package]] -name = "rlp" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb919243f34364b6bd2fc10ef797edbfa75f33c252e7998527479c6d6b47e1ec" -dependencies = [ - "bytes", - "rustc-hex", -] - -[[package]] -name = "route-recognizer" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "afab94fb28594581f62d981211a9a4d53cc8130bbcbbb89a0440d9b8e81a7746" - -[[package]] -name = "rug" -version = "1.28.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "58ad2e973fe3c3214251a840a621812a4f40468da814b1a3d6947d433c2af11f" -dependencies = [ - "az", - "gmp-mpfr-sys", - "libc", - "libm", -] - -[[package]] -name = "ruint" -version = "1.17.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a68df0380e5c9d20ce49534f292a36a7514ae21350726efe1865bdb1fa91d278" -dependencies = [ - "alloy-rlp", - "ark-ff 0.3.0", - "ark-ff 0.4.2", - "ark-ff 0.5.0", - "bytes", - "fastrlp 0.3.1", - "fastrlp 0.4.0", - "num-bigint", - "num-integer", - "num-traits", - "parity-scale-codec", - "primitive-types", - "proptest", - "rand 0.8.5", - "rand 0.9.2", - "rlp", - "ruint-macro", - "serde_core", - "valuable", - "zeroize", -] - -[[package]] -name = "ruint-macro" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48fd7bd8a6377e15ad9d42a8ec25371b94ddc67abe7c8b9127bec79bebaaae18" - -[[package]] -name = "rustc-hash" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" -dependencies = [ - "rand 0.8.5", -] - -[[package]] -name = "rustc-hex" -version = "2.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e75f6a532d0fd9f7f13144f392b6ad56a32696bfcd9c78f797f16bbb6f072d6" - -[[package]] -name = "rustc_version" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0dfe2087c51c460008730de8b57e6a320782fbfb312e1f4d520e6c6fae155ee" -dependencies = [ - "semver 0.11.0", -] - -[[package]] -name = "rustc_version" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" -dependencies = [ - "semver 1.0.27", -] - -[[package]] -name = "rustix" -version = "1.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" -dependencies = [ - "bitflags 2.10.0", - "errno", - "libc", - "linux-raw-sys", - "windows-sys 0.61.2", -] - -[[package]] -name = "rustls" -version = "0.21.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f56a14d1f48b391359b22f731fd4bd7e43c97f3c50eee276f3aa09c94784d3e" -dependencies = [ - "log", - "ring", - "rustls-webpki 0.101.7", - "sct", -] - -[[package]] -name = "rustls" -version = "0.23.35" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "533f54bc6a7d4f647e46ad909549eda97bf5afc1585190ef692b4286b198bd8f" -dependencies = [ - "aws-lc-rs", - "log", - "once_cell", - "ring", - "rustls-pki-types", - "rustls-webpki 0.103.8", - "subtle", - "zeroize", -] - -[[package]] -name = "rustls-native-certs" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9980d917ebb0c0536119ba501e90834767bffc3d60641457fd84a1f3fd337923" -dependencies = [ - "openssl-probe", - "rustls-pki-types", - "schannel", - "security-framework 3.5.1", -] - -[[package]] -name = "rustls-pemfile" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50" -dependencies = [ - "rustls-pki-types", -] - -[[package]] -name = "rustls-pki-types" -version = "1.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21e6f2ab2928ca4291b86736a8bd920a277a399bba1589409d72154ff87c1282" -dependencies = [ - "web-time", - "zeroize", -] - -[[package]] -name = "rustls-platform-verifier" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19787cda76408ec5404443dc8b31795c87cd8fec49762dc75fa727740d34acc1" -dependencies = [ - "core-foundation 0.10.1", - "core-foundation-sys", - "jni", - "log", - "once_cell", - "rustls 0.23.35", - "rustls-native-certs", - "rustls-platform-verifier-android", - "rustls-webpki 0.103.8", - "security-framework 3.5.1", - "security-framework-sys", - "webpki-root-certs 0.26.11", - "windows-sys 0.59.0", -] - -[[package]] -name = "rustls-platform-verifier-android" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" - -[[package]] -name = "rustls-webpki" -version = "0.101.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765" -dependencies = [ - "ring", - "untrusted", -] - -[[package]] -name = "rustls-webpki" -version = "0.103.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ffdfa2f5286e2247234e03f680868ac2815974dc39e00ea15adc445d0aafe52" -dependencies = [ - "aws-lc-rs", - "ring", - "rustls-pki-types", - "untrusted", -] - -[[package]] -name = "rustversion" -version = "1.0.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" - -[[package]] -name = "rusty-fork" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc6bf79ff24e648f6da1f8d1f011e9cac26491b619e6b9280f2b47f1774e6ee2" -dependencies = [ - "fnv", - "quick-error", - "tempfile", - "wait-timeout", -] - -[[package]] -name = "ryu" -version = "1.0.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62049b2877bf12821e8f9ad256ee38fdc31db7387ec2d3b3f403024de2034aea" - -[[package]] -name = "same-file" -version = "1.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" -dependencies = [ - "winapi-util", -] - -[[package]] -name = "scc" -version = "2.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46e6f046b7fef48e2660c57ed794263155d713de679057f2d0c169bfc6e756cc" -dependencies = [ - "sdd", -] - -[[package]] -name = "schannel" -version = "0.1.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1" -dependencies = [ - "windows-sys 0.61.2", -] - -[[package]] -name = "schemars" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4cd191f9397d57d581cddd31014772520aa448f65ef991055d7f61582c65165f" -dependencies = [ - "dyn-clone", - "ref-cast", - "serde", - "serde_json", -] - -[[package]] -name = "schemars" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9558e172d4e8533736ba97870c4b2cd63f84b382a3d6eb063da41b91cce17289" -dependencies = [ - "dyn-clone", - "ref-cast", - "serde", - "serde_json", -] - -[[package]] -name = "schnellru" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "356285bbf17bea63d9e52e96bd18f039672ac92b55b8cb997d6162a2a37d1649" -dependencies = [ - "ahash", - "cfg-if", - "hashbrown 0.13.2", -] - -[[package]] -name = "scopeguard" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" - -[[package]] -name = "sct" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414" -dependencies = [ - "ring", - "untrusted", -] - -[[package]] -name = "sdd" -version = "3.0.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "490dcfcbfef26be6800d11870ff2df8774fa6e86d047e3e8c8a76b25655e41ca" - -[[package]] -name = "sec1" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" -dependencies = [ - "base16ct", - "der", - "generic-array", - "pkcs8", - "serdect", - "subtle", - "zeroize", -] - -[[package]] -name = "secp256k1" -version = "0.30.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b50c5943d326858130af85e049f2661ba3c78b26589b8ab98e65e80ae44a1252" -dependencies = [ - "bitcoin_hashes", - "rand 0.8.5", - "secp256k1-sys 0.10.1", - "serde", -] - -[[package]] -name = "secp256k1" -version = "0.31.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c3c81b43dc2d8877c216a3fccf76677ee1ebccd429566d3e67447290d0c42b2" -dependencies = [ - "bitcoin_hashes", - "rand 0.9.2", - "secp256k1-sys 0.11.0", -] - -[[package]] -name = "secp256k1-sys" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4387882333d3aa8cb20530a17c69a3752e97837832f34f6dccc760e715001d9" -dependencies = [ - "cc", -] - -[[package]] -name = "secp256k1-sys" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcb913707158fadaf0d8702c2db0e857de66eb003ccfdda5924b5f5ac98efb38" -dependencies = [ - "cc", -] - -[[package]] -name = "security-framework" -version = "2.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" -dependencies = [ - "bitflags 2.10.0", - "core-foundation 0.9.4", - "core-foundation-sys", - "libc", - "security-framework-sys", -] - -[[package]] -name = "security-framework" -version = "3.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3297343eaf830f66ede390ea39da1d462b6b0c1b000f420d0a83f898bbbe6ef" -dependencies = [ - "bitflags 2.10.0", - "core-foundation 0.10.1", - "core-foundation-sys", - "libc", - "security-framework-sys", -] - -[[package]] -name = "security-framework-sys" -version = "2.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc1f0cbffaac4852523ce30d8bd3c5cdc873501d96ff467ca09b6767bb8cd5c0" -dependencies = [ - "core-foundation-sys", - "libc", -] - -[[package]] -name = "semver" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f301af10236f6df4160f7c3f04eec6dbc70ace82d23326abad5edee88801c6b6" -dependencies = [ - "semver-parser", -] - -[[package]] -name = "semver" -version = "1.0.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" - -[[package]] -name = "semver-parser" -version = "0.10.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9900206b54a3527fdc7b8a938bffd94a568bac4f4aa8113b209df75a09c0dec2" -dependencies = [ - "pest", -] - -[[package]] -name = "serde" -version = "1.0.228" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" -dependencies = [ - "serde_core", - "serde_derive", -] - -[[package]] -name = "serde_core" -version = "1.0.228" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" -dependencies = [ - "serde_derive", -] - -[[package]] -name = "serde_derive" -version = "1.0.228" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.111", -] - -[[package]] -name = "serde_json" -version = "1.0.146" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "217ca874ae0207aac254aa02c957ded05585a90892cc8d87f9e5fa49669dadd8" -dependencies = [ - "indexmap 2.12.1", - "itoa", - "memchr", - "ryu", - "serde", - "serde_core", -] - -[[package]] -name = "serde_path_to_error" -version = "0.1.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457" -dependencies = [ - "itoa", - "serde", - "serde_core", -] - -[[package]] -name = "serde_repr" -version = "0.1.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.111", -] - -[[package]] -name = "serde_urlencoded" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" -dependencies = [ - "form_urlencoded", - "itoa", - "ryu", - "serde", -] - -[[package]] -name = "serde_with" -version = "3.16.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fa237f2807440d238e0364a218270b98f767a00d3dada77b1c53ae88940e2e7" -dependencies = [ - "base64 0.22.1", - "chrono", - "hex", - "indexmap 1.9.3", - "indexmap 2.12.1", - "schemars 0.9.0", - "schemars 1.1.0", - "serde_core", - "serde_json", - "serde_with_macros", - "time", -] - -[[package]] -name = "serde_with_macros" -version = "3.16.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52a8e3ca0ca629121f70ab50f95249e5a6f925cc0f6ffe8256c45b728875706c" -dependencies = [ - "darling 0.21.3", - "proc-macro2", - "quote", - "syn 2.0.111", -] - -[[package]] -name = "serdect" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a84f14a19e9a014bb9f4512488d9829a68e04ecabffb0f9904cd1ace94598177" -dependencies = [ - "base16ct", - "serde", -] - -[[package]] -name = "serial_test" -version = "3.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b258109f244e1d6891bf1053a55d63a5cd4f8f4c30cf9a1280989f80e7a1fa9" -dependencies = [ - "futures", - "log", - "once_cell", - "parking_lot", - "scc", - "serial_test_derive", -] - -[[package]] -name = "serial_test_derive" -version = "3.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d69265a08751de7844521fd15003ae0a888e035773ba05695c5c759a6f89eef" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.111", -] - -[[package]] -name = "sha1" -version = "0.10.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" -dependencies = [ - "cfg-if", - "cpufeatures", - "digest 0.10.7", -] - -[[package]] -name = "sha1_smol" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbfa15b3dddfee50a0fff136974b3e1bde555604ba463834a7eb7deb6417705d" - -[[package]] -name = "sha2" -version = "0.10.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" -dependencies = [ - "cfg-if", - "cpufeatures", - "digest 0.10.7", -] - -[[package]] -name = "sha3" -version = "0.10.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75872d278a8f37ef87fa0ddbda7802605cb18344497949862c0d4dcb291eba60" -dependencies = [ - "digest 0.10.7", - "keccak", -] - -[[package]] -name = "sha3-asm" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c28efc5e327c837aa837c59eae585fc250715ef939ac32881bcc11677cd02d46" -dependencies = [ - "cc", - "cfg-if", -] - -[[package]] -name = "sharded-slab" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" -dependencies = [ - "lazy_static", -] - -[[package]] -name = "shlex" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" - -[[package]] -name = "signal-hook-registry" -version = "1.4.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7664a098b8e616bdfcc2dc0e9ac44eb231eedf41db4e9fe95d8d32ec728dedad" -dependencies = [ - "libc", -] - -[[package]] -name = "signature" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" -dependencies = [ - "digest 0.10.7", - "rand_core 0.6.4", -] - -[[package]] -name = "siphasher" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" - -[[package]] -name = "sketches-ddsketch" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1e9a774a6c28142ac54bb25d25562e6bcf957493a184f15ad4eebccb23e410a" - -[[package]] -name = "slab" -version = "0.4.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" - -[[package]] -name = "smallvec" -version = "1.15.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" -dependencies = [ - "serde", -] - -[[package]] -name = "snap" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b6b67fb9a61334225b5b790716f609cd58395f895b3fe8b328786812a40bc3b" - -[[package]] -name = "socket2" -version = "0.5.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" -dependencies = [ - "libc", - "windows-sys 0.52.0", -] - -[[package]] -name = "socket2" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17129e116933cf371d018bb80ae557e889637989d8638274fb25622827b03881" -dependencies = [ - "libc", - "windows-sys 0.60.2", -] - -[[package]] -name = "soketto" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e859df029d160cb88608f5d7df7fb4753fd20fdfb4de5644f3d8b8440841721" -dependencies = [ - "base64 0.22.1", - "bytes", - "futures", - "http 1.4.0", - "httparse", - "log", - "rand 0.8.5", - "sha1", -] - -[[package]] -name = "spki" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" -dependencies = [ - "base64ct", - "der", -] - -[[package]] -name = "stable_deref_trait" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" - -[[package]] -name = "static_assertions" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" - -[[package]] -name = "strsim" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" - -[[package]] -name = "structmeta" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e1575d8d40908d70f6fd05537266b90ae71b15dbbe7a8b7dffa2b759306d329" -dependencies = [ - "proc-macro2", - "quote", - "structmeta-derive", - "syn 2.0.111", -] - -[[package]] -name = "structmeta-derive" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "152a0b65a590ff6c3da95cabe2353ee04e6167c896b28e3b14478c2636c922fc" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.111", -] - -[[package]] -name = "strum" -version = "0.27.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf" -dependencies = [ - "strum_macros", -] - -[[package]] -name = "strum_macros" -version = "0.27.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7695ce3845ea4b33927c055a39dc438a45b059f7c1b3d91d38d10355fb8cbca7" -dependencies = [ - "heck", - "proc-macro2", - "quote", - "syn 2.0.111", -] - -[[package]] -name = "subtle" -version = "2.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" - -[[package]] -name = "syn" -version = "1.0.109" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "syn" -version = "2.0.111" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "syn-solidity" -version = "1.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f92d01b5de07eaf324f7fca61cc6bd3d82bbc1de5b6c963e6fe79e86f36580d" -dependencies = [ - "paste", - "proc-macro2", - "quote", - "syn 2.0.111", -] - -[[package]] -name = "sync_wrapper" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" -dependencies = [ - "futures-core", -] - -[[package]] -name = "synstructure" -version = "0.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.111", -] - -[[package]] -name = "system-configuration" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" -dependencies = [ - "bitflags 2.10.0", - "core-foundation 0.9.4", - "system-configuration-sys", -] - -[[package]] -name = "system-configuration-sys" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" -dependencies = [ - "core-foundation-sys", - "libc", -] - -[[package]] -name = "tagptr" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b2093cf4c8eb1e67749a6762251bc9cd836b6fc171623bd0a9d324d37af2417" - -[[package]] -name = "tap" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" - -[[package]] -name = "tempfile" -version = "3.23.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16" -dependencies = [ - "fastrand", - "getrandom 0.3.4", - "once_cell", - "rustix", - "windows-sys 0.61.2", -] - -[[package]] -name = "termtree" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f50febec83f5ee1df3015341d8bd429f2d1cc62bcba7ea2076759d315084683" - -[[package]] -name = "testcontainers" -version = "0.23.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59a4f01f39bb10fc2a5ab23eb0d888b1e2bb168c157f61a1b98e6c501c639c74" -dependencies = [ - "async-trait", - "bollard", - "bollard-stubs", - "bytes", - "docker_credential", - "either", - "etcetera", - "futures", - "log", - "memchr", - "parse-display", - "pin-project-lite", - "serde", - "serde_json", - "serde_with", - "thiserror 2.0.17", - "tokio", - "tokio-stream", - "tokio-tar", - "tokio-util", - "url", -] - -[[package]] -name = "testcontainers-modules" -version = "0.11.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d43ed4e8f58424c3a2c6c56dbea6643c3c23e8666a34df13c54f0a184e6c707" -dependencies = [ - "testcontainers", -] - -[[package]] -name = "thiserror" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" -dependencies = [ - "thiserror-impl 1.0.69", -] - -[[package]] -name = "thiserror" -version = "2.0.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" -dependencies = [ - "thiserror-impl 2.0.17", -] - -[[package]] -name = "thiserror-impl" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.111", -] - -[[package]] -name = "thiserror-impl" -version = "2.0.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.111", -] - -[[package]] -name = "thread_local" -version = "1.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "threadpool" -version = "1.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d050e60b33d41c19108b32cea32164033a9013fe3b46cbd4457559bfbf77afaa" -dependencies = [ - "num_cpus", -] - -[[package]] -name = "time" -version = "0.3.44" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91e7d9e3bb61134e77bde20dd4825b97c010155709965fedf0f49bb138e52a9d" -dependencies = [ - "deranged", - "itoa", - "num-conv", - "powerfmt", - "serde", - "time-core", - "time-macros", -] - -[[package]] -name = "time-core" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40868e7c1d2f0b8d73e4a8c7f0ff63af4f6d19be117e90bd73eb1d62cf831c6b" - -[[package]] -name = "time-macros" -version = "0.2.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30cfb0125f12d9c277f35663a0a33f8c30190f4e4574868a330595412d34ebf3" -dependencies = [ - "num-conv", - "time-core", -] - -[[package]] -name = "tiny-keccak" -version = "2.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" -dependencies = [ - "crunchy", -] - -[[package]] -name = "tinystr" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" -dependencies = [ - "displaydoc", - "zerovec", -] - -[[package]] -name = "tinyvec" -version = "1.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" -dependencies = [ - "tinyvec_macros", -] - -[[package]] -name = "tinyvec_macros" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" - -[[package]] -name = "tips-audit" -version = "0.1.0" -dependencies = [ - "anyhow", - "aws-config", - "aws-credential-types", - "aws-sdk-s3", - "clap", - "dotenvy", - "rdkafka", - "tips-audit-lib", - "tips-core", - "tokio", - "tracing", -] - -[[package]] -name = "tips-audit-lib" -version = "0.1.0" -dependencies = [ - "alloy-consensus", - "alloy-primitives", - "anyhow", - "async-trait", - "aws-sdk-s3", - "bytes", - "futures", - "metrics", - "metrics-derive", - "rdkafka", - "serde", - "serde_json", - "testcontainers", - "testcontainers-modules", - "tips-core", - "tokio", - "tracing", - "uuid", -] - -[[package]] -name = "tips-core" -version = "0.1.0" -dependencies = [ - "alloy-consensus", - "alloy-primitives", - "alloy-provider", - "alloy-rpc-types", - "alloy-serde", - "alloy-signer-local", - "metrics-exporter-prometheus", - "op-alloy-consensus", - "op-alloy-flz", - "op-alloy-rpc-types", - "serde", - "serde_json", - "tracing", - "tracing-subscriber 0.3.22", - "uuid", -] - -[[package]] -name = "tips-ingress-rpc" -version = "0.1.0" -dependencies = [ - "account-abstraction-core", - "alloy-provider", - "anyhow", - "clap", - "dotenvy", - "jsonrpsee", - "op-alloy-network", - "rdkafka", - "tips-audit-lib", - "tips-core", - "tips-ingress-rpc-lib", - "tokio", - "tracing", -] - -[[package]] -name = "tips-ingress-rpc-lib" -version = "0.1.0" -dependencies = [ - "account-abstraction-core", - "alloy-consensus", - "alloy-primitives", - "alloy-provider", - "alloy-signer-local", - "anyhow", - "async-trait", - "axum", - "backon", - "base-reth-rpc-types", - "clap", - "dotenvy", - "jsonrpsee", - "metrics", - "metrics-derive", - "mockall", - "moka", - "op-alloy-consensus", - "op-alloy-network", - "op-revm", - "rdkafka", - "serde_json", - "tips-audit-lib", - "tips-core", - "tokio", - "tracing", - "url", - "uuid", - "wiremock", -] - -[[package]] -name = "tips-system-tests" -version = "0.1.0" -dependencies = [ - "alloy-consensus", - "alloy-network", - "alloy-primitives", - "alloy-provider", - "alloy-signer-local", - "anyhow", - "async-trait", - "aws-config", - "aws-credential-types", - "aws-sdk-s3", - "bytes", - "clap", - "dashmap", - "hex", - "indicatif", - "jsonrpsee", - "op-alloy-consensus", - "op-alloy-network", - "rand 0.8.5", - "rand_chacha 0.3.1", - "rdkafka", - "reqwest", - "serde", - "serde_json", - "serial_test", - "testcontainers", - "testcontainers-modules", - "tips-audit-lib", - "tips-core", - "tips-ingress-rpc-lib", - "tokio", - "tracing", - "tracing-subscriber 0.3.22", - "url", - "uuid", -] - -[[package]] -name = "tokio" -version = "1.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff360e02eab121e0bc37a2d3b4d4dc622e6eda3a8e5253d5435ecf5bd4c68408" -dependencies = [ - "bytes", - "libc", - "mio", - "parking_lot", - "pin-project-lite", - "signal-hook-registry", - "socket2 0.6.1", - "tokio-macros", - "windows-sys 0.61.2", -] - -[[package]] -name = "tokio-macros" -version = "2.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.111", -] - -[[package]] -name = "tokio-native-tls" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" -dependencies = [ - "native-tls", - "tokio", -] - -[[package]] -name = "tokio-rustls" -version = "0.24.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081" -dependencies = [ - "rustls 0.21.12", - "tokio", -] - -[[package]] -name = "tokio-rustls" -version = "0.26.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" -dependencies = [ - "rustls 0.23.35", - "tokio", -] - -[[package]] -name = "tokio-stream" -version = "0.1.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047" -dependencies = [ - "futures-core", - "pin-project-lite", - "tokio", - "tokio-util", -] - -[[package]] -name = "tokio-tar" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d5714c010ca3e5c27114c1cdeb9d14641ace49874aa5626d7149e47aedace75" -dependencies = [ - "filetime", - "futures-core", - "libc", - "redox_syscall 0.3.5", - "tokio", - "tokio-stream", - "xattr", -] - -[[package]] -name = "tokio-util" -version = "0.7.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2efa149fe76073d6e8fd97ef4f4eca7b67f599660115591483572e406e165594" -dependencies = [ - "bytes", - "futures-core", - "futures-io", - "futures-sink", - "pin-project-lite", - "tokio", -] - -[[package]] -name = "toml_datetime" -version = "0.7.5+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" -dependencies = [ - "serde_core", -] - -[[package]] -name = "toml_edit" -version = "0.23.10+spec-1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84c8b9f757e028cee9fa244aea147aab2a9ec09d5325a9b01e0a49730c2b5269" -dependencies = [ - "indexmap 2.12.1", - "toml_datetime", - "toml_parser", - "winnow", -] - -[[package]] -name = "toml_parser" -version = "1.0.6+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3198b4b0a8e11f09dd03e133c0280504d0801269e9afa46362ffde1cbeebf44" -dependencies = [ - "winnow", -] - -[[package]] -name = "tower" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" -dependencies = [ - "futures-core", - "futures-util", - "pin-project-lite", - "sync_wrapper", - "tokio", - "tower-layer", - "tower-service", -] - -[[package]] -name = "tower-http" -version = "0.6.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" -dependencies = [ - "bitflags 2.10.0", - "bytes", - "futures-util", - "http 1.4.0", - "http-body 1.0.1", - "iri-string", - "pin-project-lite", - "tower", - "tower-layer", - "tower-service", -] - -[[package]] -name = "tower-layer" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" - -[[package]] -name = "tower-service" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" - -[[package]] -name = "tracing" -version = "0.1.44" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" -dependencies = [ - "pin-project-lite", - "tracing-attributes", - "tracing-core", -] - -[[package]] -name = "tracing-attributes" -version = "0.1.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.111", -] - -[[package]] -name = "tracing-core" -version = "0.1.36" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" -dependencies = [ - "once_cell", - "valuable", -] - -[[package]] -name = "tracing-futures" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97d095ae15e245a057c8e8451bab9b3ee1e1f68e9ba2b4fbc18d0ac5237835f2" -dependencies = [ - "pin-project", - "tracing", -] - -[[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.2.25" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e0d2eaa99c3c2e41547cfa109e910a68ea03823cccad4a0525dcbc9b01e8c71" -dependencies = [ - "tracing-core", -] - -[[package]] -name = "tracing-subscriber" -version = "0.3.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e" -dependencies = [ - "matchers", - "nu-ansi-term", - "once_cell", - "regex-automata", - "serde", - "serde_json", - "sharded-slab", - "thread_local", - "tracing", - "tracing-core", - "tracing-serde", -] - -[[package]] -name = "try-lock" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" - -[[package]] -name = "typenum" -version = "1.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" - -[[package]] -name = "ucd-trie" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" - -[[package]] -name = "uint" -version = "0.9.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76f64bba2c53b04fcab63c01a7d7427eadc821e3bc48c34dc9ba29c501164b52" -dependencies = [ - "byteorder", - "crunchy", - "hex", - "static_assertions", -] - -[[package]] -name = "unarray" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94" - -[[package]] -name = "unicode-ident" -version = "1.0.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" - -[[package]] -name = "unicode-segmentation" -version = "1.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" - -[[package]] -name = "unicode-width" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" - -[[package]] -name = "unicode-xid" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" - -[[package]] -name = "untrusted" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" - -[[package]] -name = "url" -version = "2.5.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b" -dependencies = [ - "form_urlencoded", - "idna", - "percent-encoding", - "serde", -] - -[[package]] -name = "urlencoding" -version = "2.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" - -[[package]] -name = "utf8_iter" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" - -[[package]] -name = "utf8parse" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" - -[[package]] -name = "uuid" -version = "1.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2e054861b4bd027cd373e18e8d8d8e6548085000e41290d95ce0c373a654b4a" -dependencies = [ - "getrandom 0.3.4", - "js-sys", - "serde_core", - "sha1_smol", - "wasm-bindgen", -] - -[[package]] -name = "valuable" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" - -[[package]] -name = "vcpkg" -version = "0.2.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" - -[[package]] -name = "version_check" -version = "0.9.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" - -[[package]] -name = "vsimd" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c3082ca00d5a5ef149bb8b555a72ae84c9c59f7250f013ac822ac2e49b19c64" - -[[package]] -name = "wait-timeout" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09ac3b126d3914f9849036f826e054cbabdc8519970b8998ddaf3b5bd3c65f11" -dependencies = [ - "libc", -] - -[[package]] -name = "walkdir" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" -dependencies = [ - "same-file", - "winapi-util", -] - -[[package]] -name = "want" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" -dependencies = [ - "try-lock", -] - -[[package]] -name = "wasi" -version = "0.11.1+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" - -[[package]] -name = "wasip2" -version = "1.0.1+wasi-0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" -dependencies = [ - "wit-bindgen", -] - -[[package]] -name = "wasm-bindgen" -version = "0.2.106" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d759f433fa64a2d763d1340820e46e111a7a5ab75f993d1852d70b03dbb80fd" -dependencies = [ - "cfg-if", - "once_cell", - "rustversion", - "wasm-bindgen-macro", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-futures" -version = "0.4.56" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "836d9622d604feee9e5de25ac10e3ea5f2d65b41eac0d9ce72eb5deae707ce7c" -dependencies = [ - "cfg-if", - "js-sys", - "once_cell", - "wasm-bindgen", - "web-sys", -] - -[[package]] -name = "wasm-bindgen-macro" -version = "0.2.106" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48cb0d2638f8baedbc542ed444afc0644a29166f1595371af4fecf8ce1e7eeb3" -dependencies = [ - "quote", - "wasm-bindgen-macro-support", -] - -[[package]] -name = "wasm-bindgen-macro-support" -version = "0.2.106" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cefb59d5cd5f92d9dcf80e4683949f15ca4b511f4ac0a6e14d4e1ac60c6ecd40" -dependencies = [ - "bumpalo", - "proc-macro2", - "quote", - "syn 2.0.111", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-shared" -version = "0.2.106" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cbc538057e648b67f72a982e708d485b2efa771e1ac05fec311f9f63e5800db4" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "wasmtimer" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c598d6b99ea013e35844697fc4670d08339d5cda15588f193c6beedd12f644b" -dependencies = [ - "futures", - "js-sys", - "parking_lot", - "pin-utils", - "slab", - "wasm-bindgen", -] - -[[package]] -name = "web-sys" -version = "0.3.83" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b32828d774c412041098d182a8b38b16ea816958e07cf40eec2bc080ae137ac" -dependencies = [ - "js-sys", - "wasm-bindgen", -] - -[[package]] -name = "web-time" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" -dependencies = [ - "js-sys", - "wasm-bindgen", -] - -[[package]] -name = "webpki-root-certs" -version = "0.26.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75c7f0ef91146ebfb530314f5f1d24528d7f0767efbfd31dce919275413e393e" -dependencies = [ - "webpki-root-certs 1.0.4", -] - -[[package]] -name = "webpki-root-certs" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee3e3b5f5e80bc89f30ce8d0343bf4e5f12341c51f3e26cbeecbc7c85443e85b" -dependencies = [ - "rustls-pki-types", -] - -[[package]] -name = "winapi" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" -dependencies = [ - "winapi-i686-pc-windows-gnu", - "winapi-x86_64-pc-windows-gnu", -] - -[[package]] -name = "winapi-i686-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" - -[[package]] -name = "winapi-util" -version = "0.1.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" -dependencies = [ - "windows-sys 0.61.2", -] - -[[package]] -name = "winapi-x86_64-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" - -[[package]] -name = "windows-core" -version = "0.62.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" -dependencies = [ - "windows-implement", - "windows-interface", - "windows-link", - "windows-result", - "windows-strings", -] - -[[package]] -name = "windows-implement" -version = "0.60.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.111", -] - -[[package]] -name = "windows-interface" -version = "0.59.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.111", -] - -[[package]] -name = "windows-link" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" - -[[package]] -name = "windows-registry" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720" -dependencies = [ - "windows-link", - "windows-result", - "windows-strings", -] - -[[package]] -name = "windows-result" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" -dependencies = [ - "windows-link", -] - -[[package]] -name = "windows-strings" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" -dependencies = [ - "windows-link", -] - -[[package]] -name = "windows-sys" -version = "0.45.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" -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" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" -dependencies = [ - "windows-targets 0.52.6", -] - -[[package]] -name = "windows-sys" -version = "0.59.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" -dependencies = [ - "windows-targets 0.52.6", -] - -[[package]] -name = "windows-sys" -version = "0.60.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" -dependencies = [ - "windows-targets 0.53.5", -] - -[[package]] -name = "windows-sys" -version = "0.61.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" -dependencies = [ - "windows-link", -] - -[[package]] -name = "windows-targets" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" -dependencies = [ - "windows_aarch64_gnullvm 0.42.2", - "windows_aarch64_msvc 0.42.2", - "windows_i686_gnu 0.42.2", - "windows_i686_msvc 0.42.2", - "windows_x86_64_gnu 0.42.2", - "windows_x86_64_gnullvm 0.42.2", - "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" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" -dependencies = [ - "windows_aarch64_gnullvm 0.52.6", - "windows_aarch64_msvc 0.52.6", - "windows_i686_gnu 0.52.6", - "windows_i686_gnullvm 0.52.6", - "windows_i686_msvc 0.52.6", - "windows_x86_64_gnu 0.52.6", - "windows_x86_64_gnullvm 0.52.6", - "windows_x86_64_msvc 0.52.6", -] - -[[package]] -name = "windows-targets" -version = "0.53.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" -dependencies = [ - "windows-link", - "windows_aarch64_gnullvm 0.53.1", - "windows_aarch64_msvc 0.53.1", - "windows_i686_gnu 0.53.1", - "windows_i686_gnullvm 0.53.1", - "windows_i686_msvc 0.53.1", - "windows_x86_64_gnu 0.53.1", - "windows_x86_64_gnullvm 0.53.1", - "windows_x86_64_msvc 0.53.1", -] - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.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" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.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" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" - -[[package]] -name = "windows_i686_gnu" -version = "0.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" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" - -[[package]] -name = "windows_i686_gnu" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" - -[[package]] -name = "windows_i686_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" - -[[package]] -name = "windows_i686_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" - -[[package]] -name = "windows_i686_msvc" -version = "0.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" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" - -[[package]] -name = "windows_i686_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.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" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.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" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.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" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" - -[[package]] -name = "winnow" -version = "0.7.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829" -dependencies = [ - "memchr", -] - -[[package]] -name = "wiremock" -version = "0.6.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08db1edfb05d9b3c1542e521aea074442088292f00b5f28e435c714a98f85031" -dependencies = [ - "assert-json-diff", - "base64 0.22.1", - "deadpool", - "futures", - "http 1.4.0", - "http-body-util", - "hyper 1.8.1", - "hyper-util", - "log", - "once_cell", - "regex", - "serde", - "serde_json", - "tokio", - "url", -] - -[[package]] -name = "wit-bindgen" -version = "0.46.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" - -[[package]] -name = "writeable" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" - -[[package]] -name = "wyz" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" -dependencies = [ - "tap", -] - -[[package]] -name = "xattr" -version = "1.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156" -dependencies = [ - "libc", - "rustix", -] - -[[package]] -name = "xmlparser" -version = "0.13.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "66fee0b777b0f5ac1c69bb06d361268faafa61cd4682ae064a171c16c433e9e4" - -[[package]] -name = "yoke" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" -dependencies = [ - "stable_deref_trait", - "yoke-derive", - "zerofrom", -] - -[[package]] -name = "yoke-derive" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.111", - "synstructure", -] - -[[package]] -name = "zerocopy" -version = "0.8.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd74ec98b9250adb3ca554bdde269adf631549f51d8a8f8f0a10b50f1cb298c3" -dependencies = [ - "zerocopy-derive", -] - -[[package]] -name = "zerocopy-derive" -version = "0.8.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8a8d209fdf45cf5138cbb5a506f6b52522a25afccc534d1475dad8e31105c6a" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.111", -] - -[[package]] -name = "zerofrom" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" -dependencies = [ - "zerofrom-derive", -] - -[[package]] -name = "zerofrom-derive" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.111", - "synstructure", -] - -[[package]] -name = "zeroize" -version = "1.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" -dependencies = [ - "zeroize_derive", -] - -[[package]] -name = "zeroize_derive" -version = "1.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.111", -] - -[[package]] -name = "zerotrie" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" -dependencies = [ - "displaydoc", - "yoke", - "zerofrom", -] - -[[package]] -name = "zerovec" -version = "0.11.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" -dependencies = [ - "yoke", - "zerofrom", - "zerovec-derive", -] - -[[package]] -name = "zerovec-derive" -version = "0.11.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.111", -] - -[[package]] -name = "zstd" -version = "0.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" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91e19ebc2adc8f83e43039e79776e3fda8ca919132d68a1fed6a5faca2683748" -dependencies = [ - "bindgen", - "cc", - "pkg-config", -] diff --git a/Cargo.toml b/Cargo.toml deleted file mode 100644 index 623dedc7..00000000 --- a/Cargo.toml +++ /dev/null @@ -1,101 +0,0 @@ -[workspace.package] -version = "0.1.0" -edition = "2024" -rust-version = "1.88" -license = "MIT" -homepage = "https://github.com/base/tips" -repository = "https://github.com/base/tips" - -[workspace] -resolver = "2" -members = ["crates/*", "bin/*"] -default-members = ["bin/tips-ingress-rpc"] - -[workspace.lints.rust] -missing-debug-implementations = "warn" -missing-docs = "warn" -unreachable-pub = "warn" -unused-must-use = "deny" -rust-2018-idioms = "deny" -unnameable-types = "warn" - -[workspace.lints.rustdoc] -all = "warn" - -[workspace.lints.clippy] -all = { level = "warn", priority = -1 } -missing-const-for-fn = "warn" -use-self = "warn" -option-if-let-else = "warn" -redundant-clone = "warn" - -[workspace.dependencies] -# local -tips-core = { path = "crates/core" } -tips-audit = { path = "bin/tips-audit" } -tips-audit-lib = { path = "crates/audit" } -tips-ingress-rpc-lib = { path = "crates/ingress-rpc" } -tips-system-tests = { path = "crates/system-tests" } -account-abstraction-core = { path = "crates/account-abstraction-core" } - -# base-reth -base-reth-rpc-types = { git = "https://github.com/base/node-reth", branch = "main" } - -# revm -op-revm = { version = "12.0.0", default-features = false } -revm-context-interface = { version = "12.0.0", default-features = false } - -# alloy -alloy-serde = { version = "1.0.41", default-features = false } -alloy-signer = { version = "1.0.41", default-features = false } -alloy-network = { version = "1.0.41", default-features = false } -alloy-provider = { version = "1.0.41", default-features = false } -alloy-consensus = { version = "1.0.41", default-features = false } -alloy-sol-types = { version = "1.4.1", default-features = false } -alloy-rpc-types = { version = "1.1.2", default-features = false } -alloy-primitives = { version = "1.4.1", default-features = false } -alloy-signer-local = { version = "1.0.41", default-features = false } - -# op-alloy -op-alloy-flz = { version = "0.13.1", default-features = false } -op-alloy-network = { version = "0.22.0", default-features = false } -op-alloy-rpc-types = { version = "0.22.0", default-features = false } -op-alloy-consensus = { version = "0.22.0", default-features = false } - -# tokio -tokio = { version = "1.47.1", default-features = false } - -# async -async-trait = "0.1.89" - -# rpc -jsonrpsee = { version = "0.26.0", default-features = false } - -# kafka and s3 -bytes = { version = "1.8.0", default-features = false } -rdkafka = { version = "0.37.0", default-features = false } -aws-config = { version = "1.1.7", default-features = false } -aws-sdk-s3 = { version = "1.106.0", default-features = false } -aws-credential-types = { version = "1.1.7", default-features = false } - -# misc -url = { version = "2.5.7", default-features = false } -axum = { version = "0.8.3", default-features = false } -serde = { version = "1.0.219", default-features = false } -uuid = { version = "1.18.1", default-features = false } -clap = { version = "4.5.47", default-features = false } -backon = { version = "1.5.2", default-features = false } -chrono = { version = "0.4.42", default-features = false } -anyhow = { version = "1.0.99", default-features = false } -tracing = { version = "0.1.41", default-features = false } -wiremock = { version = "0.6.2", default-features = false } -dotenvy = { version = "0.15.7", default-features = false } -metrics = { version = "0.24.1", default-features = false } -metrics-derive = { version = "0.1", default-features = false } -serde_json = { version = "1.0.143", default-features = false } -testcontainers = { version = "0.23.1", default-features = false } -tracing-subscriber = { version = "0.3.20", default-features = false } -testcontainers-modules = { version = "0.11.2", default-features = false } -metrics-exporter-prometheus = { version = "0.17.0", default-features = false } -futures = { version = "0.3.31", default-features = false } -moka = { version = "0.12.12", default-features = false } diff --git a/Dockerfile b/Dockerfile index 9442acbf..8615be83 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,35 +1,40 @@ -FROM rust:1-bookworm AS base +FROM node:20-alpine AS deps +WORKDIR /app +COPY ui/package.json ui/yarn.lock ./ +RUN --mount=type=cache,target=/root/.yarn \ + yarn install --frozen-lockfile + +FROM node:20-alpine AS builder +WORKDIR /app +COPY --from=deps /app/node_modules ./node_modules +COPY ./ui . -RUN apt-get update && apt-get -y upgrade && apt-get install -y libclang-dev pkg-config libsasl2-dev libssl-dev +ENV NEXT_TELEMETRY_DISABLED=1 -RUN cargo install cargo-chef --locked +RUN --mount=type=cache,target=/app/.next/cache \ + yarn build + +FROM node:20-alpine AS runner WORKDIR /app -FROM base AS planner -COPY . . -RUN cargo chef prepare --recipe-path recipe.json +ENV NODE_ENV=production +ENV NEXT_TELEMETRY_DISABLED=1 -FROM base AS builder -COPY --from=planner /app/recipe.json recipe.json +RUN addgroup --system --gid 1001 nodejs +RUN adduser --system --uid 1001 nextjs -RUN --mount=type=cache,target=/usr/local/cargo/registry \ - --mount=type=cache,target=/usr/local/cargo/git \ - --mount=type=cache,target=/app/target \ - cargo chef cook --recipe-path recipe.json +RUN mkdir .next +RUN chown nextjs:nodejs .next -COPY . . -RUN --mount=type=cache,target=/usr/local/cargo/registry \ - --mount=type=cache,target=/usr/local/cargo/git \ - --mount=type=cache,target=/app/target \ - cargo build -p tips-ingress-rpc -p tips-audit && \ - cp target/debug/tips-ingress-rpc /tmp/tips-ingress-rpc && \ - cp target/debug/tips-audit /tmp/tips-audit +COPY --from=builder /app/public ./public +COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./ +COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static -FROM debian:bookworm +USER nextjs -RUN apt-get update && apt-get install -y libssl3 ca-certificates && rm -rf /var/lib/apt/lists/* +EXPOSE 3000 -WORKDIR /app +ENV PORT=3000 +ENV HOSTNAME="0.0.0.0" -COPY --from=builder /tmp/tips-audit /app/tips-audit -COPY --from=builder /tmp/tips-ingress-rpc /app/tips-ingress-rpc \ No newline at end of file +CMD ["node", "server.js"] \ No newline at end of file diff --git a/Justfile b/Justfile deleted file mode 100644 index c6a014a2..00000000 --- a/Justfile +++ /dev/null @@ -1,207 +0,0 @@ -set positional-arguments - -alias f := fix -alias c := ci - -# Default to display help menu -default: - @just --list - -# Runs all ci checks -ci: - cargo fmt --all -- --check - cargo clippy -- -D warnings - cargo build - cargo test - cd ui && npx --yes @biomejs/biome check . - cd ui && npm run build - -# Fixes formatting and clippy issues -fix: - cargo fmt --all - cargo clippy --fix --allow-dirty --allow-staged - cd ui && npx --yes @biomejs/biome check --write --unsafe . - -# Resets dependencies and reformats code -sync: deps-reset sync-env fix - -# Copies environment templates and adapts for docker -sync-env: - cp .env.example .env - cp .env.example ./ui/.env - cp .env.example .env.docker - sed -i '' 's/localhost:9092/host.docker.internal:9094/g' ./.env.docker - sed -i '' 's/localhost/host.docker.internal/g' ./.env.docker - -# Stops and removes all docker containers and data -stop-all: - export COMPOSE_FILE=docker-compose.yml:docker-compose.tips.yml && docker compose down && docker compose rm && rm -rf data/ - -# Starts all services in docker, useful for demos -start-all: stop-all - export COMPOSE_FILE=docker-compose.yml:docker-compose.tips.yml && mkdir -p data/kafka data/minio && docker compose build && docker compose up -d - -# Starts docker services except specified ones, e.g. just start-except ui ingress-rpc -start-except programs: stop-all - #!/bin/bash - all_services=(kafka kafka-setup minio minio-setup ingress-rpc audit ui) - exclude_services=({{ programs }}) - - # Create result array with services not in exclude list - result_services=() - for service in "${all_services[@]}"; do - skip=false - for exclude in "${exclude_services[@]}"; do - if [[ "$service" == "$exclude" ]]; then - skip=true - break - fi - done - if [[ "$skip" == false ]]; then - result_services+=("$service") - fi - done - - export COMPOSE_FILE=docker-compose.yml:docker-compose.tips.yml && mkdir -p data/kafka data/minio && docker compose build && docker compose up -d ${result_services[@]} - -# Resets docker dependencies with clean data -deps-reset: - COMPOSE_FILE=docker-compose.yml:docker-compose.tips.yml docker compose down && docker compose rm && rm -rf data/ && mkdir -p data/kafka data/minio && docker compose up -d - -# Restarts docker dependencies without data reset -deps: - COMPOSE_FILE=docker-compose.yml:docker-compose.tips.yml docker compose down && docker compose rm && docker compose up -d - -# Runs the tips-audit service -audit: - cargo run --bin tips-audit - -# Runs the tips-ingress-rpc service -ingress-rpc: - cargo run --bin tips-ingress-rpc - -# Runs the tips-maintenance service -maintenance: - cargo run --bin tips-maintenance - -# Runs the tips-ingress-writer service -ingress-writer: - cargo run --bin tips-ingress-writer - -# Starts the UI development server -ui: - cd ui && yarn dev - -sequencer_url := "http://localhost:8547" -validator_url := "http://localhost:8549" -builder_url := "http://localhost:2222" -ingress_url := "http://localhost:8080" - -sender := "0x70997970C51812dc3A010C7d01b50e0d17dc79C8" -sender_key := "0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d" - -backrunner := "0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC" -backrunner_key := "0x5de4111afa1a4b94908f83103eb1f1706367c2e68ca870fc3fb9a804cdab365a" - -# Queries block numbers from sequencer, validator, and builder -get-blocks: - echo "Sequencer" - cast bn -r {{ sequencer_url }} - echo "Validator" - cast bn -r {{ validator_url }} - echo "Builder" - cast bn -r {{ builder_url }} - -# Sends a test transaction through the ingress endpoint -send-txn: - #!/usr/bin/env bash - set -euxo pipefail - echo "sending txn" - nonce=$(cast nonce {{ sender }} -r {{ builder_url }}) - txn=$(cast mktx --private-key {{ sender_key }} 0x0000000000000000000000000000000000000000 --value 0.01ether --nonce $nonce --chain-id 13 -r {{ builder_url }}) - hash=$(curl -s {{ ingress_url }} -X POST -H "Content-Type: application/json" --data "{\"method\":\"eth_sendRawTransaction\",\"params\":[\"$txn\"],\"id\":1,\"jsonrpc\":\"2.0\"}" | jq -r ".result") - cast receipt $hash -r {{ sequencer_url }} | grep status - cast receipt $hash -r {{ builder_url }} | grep status - -# Sends a transaction with a backrun bundle -send-txn-with-backrun: - #!/usr/bin/env bash - set -euxo pipefail - - # 1. Get nonce and send target transaction from sender account - nonce=$(cast nonce {{ sender }} -r {{ builder_url }}) - echo "Sending target transaction from sender (nonce=$nonce)..." - target_txn=$(cast mktx --private-key {{ sender_key }} \ - 0x0000000000000000000000000000000000000000 \ - --value 0.01ether \ - --nonce $nonce \ - --chain-id 13 \ - -r {{ builder_url }}) - - target_hash=$(curl -s {{ ingress_url }} -X POST \ - -H "Content-Type: application/json" \ - --data "{\"method\":\"eth_sendRawTransaction\",\"params\":[\"$target_txn\"],\"id\":1,\"jsonrpc\":\"2.0\"}" \ - | jq -r ".result") - echo "Target tx sent: $target_hash" - - # 2. Build backrun transaction from backrunner account (different account!) - backrun_nonce=$(cast nonce {{ backrunner }} -r {{ builder_url }}) - echo "Building backrun transaction from backrunner (nonce=$backrun_nonce)..." - backrun_txn=$(cast mktx --private-key {{ backrunner_key }} \ - 0x0000000000000000000000000000000000000001 \ - --value 0.001ether \ - --nonce $backrun_nonce \ - --chain-id 13 \ - -r {{ builder_url }}) - - # 3. Compute tx hashes for reverting_tx_hashes - backrun_hash_computed=$(cast keccak $backrun_txn) - echo "Target tx hash: $target_hash" - echo "Backrun tx hash: $backrun_hash_computed" - - # 4. Construct and send bundle with reverting_tx_hashes - echo "Sending backrun bundle..." - bundle_json=$(jq -n \ - --arg target "$target_txn" \ - --arg backrun "$backrun_txn" \ - --arg target_hash "$target_hash" \ - --arg backrun_hash "$backrun_hash_computed" \ - '{ - txs: [$target, $backrun], - blockNumber: 0, - revertingTxHashes: [$target_hash, $backrun_hash] - }') - - bundle_hash=$(curl -s {{ ingress_url }} -X POST \ - -H "Content-Type: application/json" \ - --data "{\"method\":\"eth_sendBackrunBundle\",\"params\":[$bundle_json],\"id\":1,\"jsonrpc\":\"2.0\"}" \ - | jq -r ".result") - echo "Bundle sent: $bundle_hash" - - # 5. Wait and verify both transactions - echo "Waiting for transactions to land..." - sleep 5 - - echo "=== Target transaction (from sender) ===" - cast receipt $target_hash -r {{ sequencer_url }} | grep -E "(status|blockNumber|transactionIndex)" - - echo "=== Backrun transaction (from backrunner) ===" - cast receipt $backrun_hash_computed -r {{ sequencer_url }} | grep -E "(status|blockNumber|transactionIndex)" || echo "Backrun tx not found yet" - -# Runs integration tests with infrastructure checks -e2e: - #!/bin/bash - if ! INTEGRATION_TESTS=1 cargo test --package tips-system-tests --test integration_tests; then - echo "" - echo "═══════════════════════════════════════════════════════════════════" - echo " ⚠️ Integration tests failed!" - echo " Make sure the infrastructure is running locally (see SETUP.md for full instructions): " - echo " just start-all" - echo " start builder-playground" - echo " start op-rbuilder" - echo "═══════════════════════════════════════════════════════════════════" - exit 1 - fi - echo "═══════════════════════════════════════════════════════════════════" - echo " ✅ Integration tests passed!" - echo "═══════════════════════════════════════════════════════════════════" diff --git a/README.md b/README.md deleted file mode 100644 index 6fbb622e..00000000 --- a/README.md +++ /dev/null @@ -1,30 +0,0 @@ -![Base](./docs/logo.png) - -# TIPS - Transaction Inclusion & Prioritization Stack - -> [!WARNING] -> This repository is an experiment to enable bundles, transaction simulation and transaction tracing for Base. -> It's being used to explore ideas and experiment. It is currently not production ready. - -## Architecture Overview - -The project consists of several components: - -### 🗄️ Datastore (`crates/datastore`) -Postgres storage layer that provides API's to persist and retrieve bundles. - -### 📊 Audit (`bin/tips-audit`) -Event streaming and archival system that: -- Provides an API to publish bundle events to Kafka -- Archives bundle history to S3 for long-term storage -- See [S3 Storage Format](docs/AUDIT_S3_FORMAT.md) for data structure details - -### 🔌 Ingress RPC (`bin/tips-ingress-rpc`) -The main entry point that provides a JSON-RPC API for receiving transactions and bundles. - -### 🖥️ UI (`ui`) -A debug UI for viewing the state of the bundle store and S3. - -## Local Development - -See the [setup instructions](./docs/SETUP.md) for how to run TIPS locally. diff --git a/bin/tips-audit/Cargo.toml b/bin/tips-audit/Cargo.toml deleted file mode 100644 index 7d432445..00000000 --- a/bin/tips-audit/Cargo.toml +++ /dev/null @@ -1,21 +0,0 @@ -[package] -name = "tips-audit" -version.workspace = true -rust-version.workspace = true -license.workspace = true -homepage.workspace = true -repository.workspace = true -edition.workspace = true - -[dependencies] -tips-core.workspace = true -tips-audit-lib.workspace = true -clap.workspace = true -tokio.workspace = true -anyhow.workspace = true -tracing.workspace = true -dotenvy.workspace = true -rdkafka.workspace = true -aws-config.workspace = true -aws-sdk-s3.workspace = true -aws-credential-types.workspace = true diff --git a/bin/tips-audit/src/main.rs b/bin/tips-audit/src/main.rs deleted file mode 100644 index f29f6d19..00000000 --- a/bin/tips-audit/src/main.rs +++ /dev/null @@ -1,137 +0,0 @@ -use anyhow::Result; -use aws_config::{BehaviorVersion, Region}; -use aws_credential_types::Credentials; -use aws_sdk_s3::{Client as S3Client, config::Builder as S3ConfigBuilder}; -use clap::{Parser, ValueEnum}; -use rdkafka::consumer::Consumer; -use std::net::SocketAddr; -use tips_audit_lib::{ - KafkaAuditArchiver, KafkaAuditLogReader, S3EventReaderWriter, create_kafka_consumer, -}; -use tips_core::logger::init_logger_with_format; -use tips_core::metrics::init_prometheus_exporter; -use tracing::info; - -#[derive(Debug, Clone, ValueEnum)] -enum S3ConfigType { - Aws, - Manual, -} - -#[derive(Parser, Debug)] -#[command(author, version, about, long_about = None)] -struct Args { - #[arg(long, env = "TIPS_AUDIT_KAFKA_PROPERTIES_FILE")] - kafka_properties_file: String, - - #[arg(long, env = "TIPS_AUDIT_KAFKA_TOPIC")] - kafka_topic: String, - - #[arg(long, env = "TIPS_AUDIT_S3_BUCKET")] - s3_bucket: String, - - #[arg(long, env = "TIPS_AUDIT_LOG_LEVEL", default_value = "info")] - log_level: String, - - #[arg(long, env = "TIPS_AUDIT_LOG_FORMAT", default_value = "pretty")] - log_format: tips_core::logger::LogFormat, - - #[arg(long, env = "TIPS_AUDIT_S3_CONFIG_TYPE", default_value = "aws")] - s3_config_type: S3ConfigType, - - #[arg(long, env = "TIPS_AUDIT_S3_ENDPOINT")] - s3_endpoint: Option, - - #[arg(long, env = "TIPS_AUDIT_S3_REGION", default_value = "us-east-1")] - s3_region: String, - - #[arg(long, env = "TIPS_AUDIT_S3_ACCESS_KEY_ID")] - s3_access_key_id: Option, - - #[arg(long, env = "TIPS_AUDIT_S3_SECRET_ACCESS_KEY")] - s3_secret_access_key: Option, - - #[arg(long, env = "TIPS_AUDIT_METRICS_ADDR", default_value = "0.0.0.0:9002")] - metrics_addr: SocketAddr, - - #[arg(long, env = "TIPS_AUDIT_WORKER_POOL_SIZE", default_value = "80")] - worker_pool_size: usize, - - #[arg(long, env = "TIPS_AUDIT_CHANNEL_BUFFER_SIZE", default_value = "500")] - channel_buffer_size: usize, - - #[arg(long, env = "TIPS_AUDIT_NOOP_ARCHIVE", default_value = "false")] - noop_archive: bool, -} - -#[tokio::main] -async fn main() -> Result<()> { - dotenvy::dotenv().ok(); - - let args = Args::parse(); - - init_logger_with_format(&args.log_level, args.log_format); - - init_prometheus_exporter(args.metrics_addr).expect("Failed to install Prometheus exporter"); - - info!( - kafka_properties_file = %args.kafka_properties_file, - kafka_topic = %args.kafka_topic, - s3_bucket = %args.s3_bucket, - metrics_addr = %args.metrics_addr, - "Starting audit archiver" - ); - - let consumer = create_kafka_consumer(&args.kafka_properties_file)?; - consumer.subscribe(&[&args.kafka_topic])?; - - let reader = KafkaAuditLogReader::new(consumer, args.kafka_topic.clone())?; - - let s3_client = create_s3_client(&args).await?; - let s3_bucket = args.s3_bucket.clone(); - let writer = S3EventReaderWriter::new(s3_client, s3_bucket); - - let mut archiver = KafkaAuditArchiver::new( - reader, - writer, - args.worker_pool_size, - args.channel_buffer_size, - args.noop_archive, - ); - - info!("Audit archiver initialized, starting main loop"); - - archiver.run().await -} - -async fn create_s3_client(args: &Args) -> Result { - match args.s3_config_type { - S3ConfigType::Manual => { - let region = args.s3_region.clone(); - let mut config_builder = - aws_config::defaults(BehaviorVersion::latest()).region(Region::new(region)); - - if let Some(endpoint) = &args.s3_endpoint { - config_builder = config_builder.endpoint_url(endpoint); - } - - if let (Some(access_key), Some(secret_key)) = - (&args.s3_access_key_id, &args.s3_secret_access_key) - { - let credentials = Credentials::new(access_key, secret_key, None, None, "manual"); - config_builder = config_builder.credentials_provider(credentials); - } - - let config = config_builder.load().await; - let s3_config_builder = S3ConfigBuilder::from(&config).force_path_style(true); - - info!(message = "manually configuring s3 client"); - Ok(S3Client::from_conf(s3_config_builder.build())) - } - S3ConfigType::Aws => { - info!(message = "using aws s3 client"); - let config = aws_config::load_defaults(BehaviorVersion::latest()).await; - Ok(S3Client::new(&config)) - } - } -} diff --git a/bin/tips-ingress-rpc/Cargo.toml b/bin/tips-ingress-rpc/Cargo.toml deleted file mode 100644 index f3b26979..00000000 --- a/bin/tips-ingress-rpc/Cargo.toml +++ /dev/null @@ -1,23 +0,0 @@ -[package] -name = "tips-ingress-rpc" -version.workspace = true -rust-version.workspace = true -license.workspace = true -homepage.workspace = true -repository.workspace = true -edition.workspace = true - -[dependencies] -tips-core.workspace = true -tips-audit-lib.workspace = true -tips-ingress-rpc-lib.workspace = true -account-abstraction-core.workspace = true -clap.workspace = true -tokio.workspace = true -anyhow.workspace = true -tracing.workspace = true -dotenvy.workspace = true -rdkafka.workspace = true -jsonrpsee.workspace = true -op-alloy-network.workspace = true -alloy-provider.workspace = true diff --git a/bin/tips-ingress-rpc/src/main.rs b/bin/tips-ingress-rpc/src/main.rs deleted file mode 100644 index dcb22c2d..00000000 --- a/bin/tips-ingress-rpc/src/main.rs +++ /dev/null @@ -1,143 +0,0 @@ -use account_abstraction_core::create_mempool_engine; -use alloy_provider::ProviderBuilder; -use clap::Parser; -use jsonrpsee::server::Server; -use op_alloy_network::Optimism; -use rdkafka::ClientConfig; -use rdkafka::producer::FutureProducer; -use tips_audit_lib::{BundleEvent, KafkaBundleEventPublisher, connect_audit_to_publisher}; -use tips_core::kafka::load_kafka_config_from_file; -use tips_core::logger::init_logger_with_format; -use tips_core::metrics::init_prometheus_exporter; -use tips_core::{AcceptedBundle, MeterBundleResponse}; -use tips_ingress_rpc_lib::Config; -use tips_ingress_rpc_lib::connect_ingress_to_builder; -use tips_ingress_rpc_lib::health::bind_health_server; -use tips_ingress_rpc_lib::queue::KafkaMessageQueue; -use tips_ingress_rpc_lib::service::{IngressApiServer, IngressService, Providers}; -use tokio::sync::{broadcast, mpsc}; -use tracing::info; - -#[tokio::main] -async fn main() -> anyhow::Result<()> { - dotenvy::dotenv().ok(); - - let config = Config::parse(); - let cfg = config.clone(); - - init_logger_with_format(&config.log_level, config.log_format); - - init_prometheus_exporter(config.metrics_addr).expect("Failed to install Prometheus exporter"); - - info!( - message = "Starting ingress service", - address = %config.address, - port = config.port, - mempool_url = %config.mempool_url, - simulation_rpc = %config.simulation_rpc, - metrics_address = %config.metrics_addr, - health_check_address = %config.health_check_addr, - ); - - let providers = Providers { - mempool: ProviderBuilder::new() - .disable_recommended_fillers() - .network::() - .connect_http(config.mempool_url), - simulation: ProviderBuilder::new() - .disable_recommended_fillers() - .network::() - .connect_http(config.simulation_rpc), - raw_tx_forward: config.raw_tx_forward_rpc.clone().map(|url| { - ProviderBuilder::new() - .disable_recommended_fillers() - .network::() - .connect_http(url) - }), - }; - - let ingress_client_config = ClientConfig::from_iter(load_kafka_config_from_file( - &config.ingress_kafka_properties, - )?); - - let queue_producer: FutureProducer = ingress_client_config.create()?; - - let queue = KafkaMessageQueue::new(queue_producer); - - let audit_client_config = - ClientConfig::from_iter(load_kafka_config_from_file(&config.audit_kafka_properties)?); - - let audit_producer: FutureProducer = audit_client_config.create()?; - - let audit_publisher = KafkaBundleEventPublisher::new(audit_producer, config.audit_topic); - let (audit_tx, audit_rx) = mpsc::unbounded_channel::(); - connect_audit_to_publisher(audit_rx, audit_publisher); - - let (mempool_engine, mempool_engine_handle) = if let Some(user_op_properties_file) = - &config.user_operation_consumer_properties - { - let engine = create_mempool_engine( - user_op_properties_file, - &config.user_operation_topic, - &config.user_operation_consumer_group_id, - None, - )?; - - let handle = { - let engine_clone = engine.clone(); - tokio::spawn(async move { engine_clone.run().await }) - }; - - (Some(engine), Some(handle)) - } else { - info!( - "User operation consumer properties not provided, skipping mempool engine initialization" - ); - (None, None) - }; - - let (builder_tx, _) = - broadcast::channel::(config.max_buffered_meter_bundle_responses); - let (builder_backrun_tx, _) = - broadcast::channel::(config.max_buffered_backrun_bundles); - config.builder_rpcs.iter().for_each(|builder_rpc| { - let metering_rx = builder_tx.subscribe(); - let backrun_rx = builder_backrun_tx.subscribe(); - connect_ingress_to_builder(metering_rx, backrun_rx, builder_rpc.clone()); - }); - - let health_check_addr = config.health_check_addr; - let (bound_health_addr, health_handle) = bind_health_server(health_check_addr).await?; - info!( - message = "Health check server started", - address = %bound_health_addr - ); - - let service = IngressService::new( - providers, - queue, - audit_tx, - builder_tx, - builder_backrun_tx, - mempool_engine.clone(), - cfg, - ); - let bind_addr = format!("{}:{}", config.address, config.port); - - let server = Server::builder().build(&bind_addr).await?; - let addr = server.local_addr()?; - let handle = server.start(service.into_rpc()); - - info!( - message = "Ingress RPC server started", - address = %addr - ); - - handle.stopped().await; - health_handle.abort(); - if let Some(engine_handle) = mempool_engine_handle { - engine_handle.abort(); - } - - Ok(()) -} diff --git a/ui/biome.json b/biome.json similarity index 100% rename from ui/biome.json rename to biome.json diff --git a/crates/account-abstraction-core/Cargo.toml b/crates/account-abstraction-core/Cargo.toml deleted file mode 100644 index 53b843dc..00000000 --- a/crates/account-abstraction-core/Cargo.toml +++ /dev/null @@ -1,29 +0,0 @@ -[package] -name = "account-abstraction-core" -version.workspace = true -rust-version.workspace = true -license.workspace = true -homepage.workspace = true -repository.workspace = true -edition.workspace = true - -[dependencies] -tips-core.workspace = true -alloy-serde.workspace = true -async-trait.workspace = true -alloy-sol-types.workspace = true -op-alloy-network.workspace = true -serde = { workspace = true, features = ["std", "derive"] } -tokio = { workspace = true, features = ["full"] } -tracing = { workspace = true, features = ["std"] } -anyhow = { workspace = true, features = ["std"] } -serde_json = { workspace = true, features = ["std"] } -rdkafka = { workspace = true, features = ["tokio", "libz", "zstd", "ssl-vendored"] } -alloy-rpc-types = { workspace = true, features = ["eth"] } -jsonrpsee = { workspace = true, features = ["server", "macros"] } -alloy-provider = { workspace = true, features = ["reqwest"] } -alloy-primitives = { workspace = true, features = ["map-foldhash", "serde"] } - -[dev-dependencies] -wiremock.workspace = true -alloy-primitives = { workspace = true, features = ["map-foldhash", "serde"] } diff --git a/crates/account-abstraction-core/README.md b/crates/account-abstraction-core/README.md deleted file mode 100644 index 0abcaba8..00000000 --- a/crates/account-abstraction-core/README.md +++ /dev/null @@ -1,310 +0,0 @@ -# account-abstraction-core - -Clean architecture implementation for ERC-4337 account abstraction mempool and validation. - -## Architecture Overview - -This crate follows **Clean Architecture** (also known as Hexagonal Architecture or Ports & Adapters). The goal is to keep business logic independent of external concerns like databases, message queues, or RPC providers. - -**Note**: We use the term "interfaces" for what Hexagonal Architecture traditionally calls "ports" - both refer to the same concept of defining contracts between layers. - -``` -┌─────────────────────────────────────────────────────────────┐ -│ Factories │ -│ (Wiring/Dependency Injection) │ -└───────────────────────┬─────────────────────────────────────┘ - │ -┌───────────────────────▼─────────────────────────────────────┐ -│ Infrastructure │ -│ (Kafka, RPC providers, external systems) │ -│ implements ▼ │ -└───────────────────────┬─────────────────────────────────────┘ - │ -┌───────────────────────▼─────────────────────────────────────┐ -│ Services │ -│ (Orchestration & use cases) │ -│ defines ▼ interfaces │ -└───────────────────────┬─────────────────────────────────────┘ - │ -┌───────────────────────▼─────────────────────────────────────┐ -│ Domain │ -│ (Pure business logic - no dependencies) │ -└─────────────────────────────────────────────────────────────┘ -``` - -### Dependency Direction - -**Critical Rule**: Dependencies always point inward. -- ✅ `infrastructure/` depends on `services/` and `domain/` -- ✅ `services/` depends on `domain/` -- ✅ `domain/` depends on nothing -- ❌ Never reverse these dependencies - -## Layer Descriptions - -### 📦 Domain (`src/domain/`) - -**What it is**: Pure business logic with zero external dependencies. - -**Contains**: -- Core types (`UserOperation`, `ValidationResult`, `WrappedUserOperation`) -- Business events (`MempoolEvent` - what happened in our system) -- Business rules (`Mempool` trait, entrypoint validation logic) -- Domain services (in-memory mempool implementation) - -**Rules**: -- No imports from `infrastructure/`, `services/`, or external crates like `rdkafka` -- Should be reusable in any context (CLI tools, web servers, tests) -- Changes here affect the entire system - -**Example**: -```rust -// domain/events.rs - describes what happens in our system -pub enum MempoolEvent { - UserOpAdded { user_op: WrappedUserOperation }, - UserOpIncluded { user_op: WrappedUserOperation }, - UserOpDropped { user_op: WrappedUserOperation, reason: String }, -} -``` - -### 🎯 Services (`src/services/`) - -**What it is**: High-level orchestration that coordinates domain logic to accomplish specific goals. - -**Contains**: -- Use case implementations (`MempoolEngine` - handles mempool events) -- **Interfaces** (contracts that infrastructure must implement) - -**Purpose**: -- Reusable across different binaries (ingress-rpc, batch-processor, CLI tools) -- Defines "what we need" without specifying "how we get it" -- Orchestrates domain objects to perform complex operations - -**Example**: -```rust -// services/mempool_engine.rs -pub struct MempoolEngine { - mempool: Arc>, - event_source: Arc, // ← uses an interface, not Kafka directly -} - -impl MempoolEngine { - pub async fn run(&self) { - loop { - let event = self.event_source.receive().await?; // ← generic! - self.handle_event(event).await?; - } - } -} -``` - -### 🔌 Interfaces (`src/services/interfaces/`) - -**What they are**: Traits that define what the services layer needs from the outside world. - -**Why they exist**: -- **Dependency Inversion**: Services define what they need; infrastructure provides it -- **Testability**: Easy to mock interfaces with fake implementations -- **Flexibility**: Swap Kafka for Redis without touching service code - -**Interfaces in this crate**: -- `EventSource` - "I need a stream of MempoolEvents" (Kafka? Redis? In-memory? Don't care!) -- `UserOperationValidator` - "I need to validate user operations" (RPC? Mock? Don't care!) - -**Example**: -```rust -// services/interfaces/event_source.rs -#[async_trait] -pub trait EventSource: Send + Sync { - async fn receive(&self) -> anyhow::Result; -} - -// Now we can implement this for ANY event source: -// - KafkaEventSource -// - RedisEventSource -// - MockEventSource (for tests) -// - FileEventSource -``` - -### 🏗️ Infrastructure (`src/infrastructure/`) - -**What it is**: Adapters that connect our system to external services. - -**Contains**: -- Kafka consumer (`KafkaEventSource` implements `EventSource`) -- RPC validators (`BaseNodeValidator` implements `UserOperationValidator`) -- Database clients (when needed) -- External API clients - -**Purpose**: -- Translate between external systems and our domain -- Handle external concerns (serialization, retries, connection pooling) -- Implement the interfaces defined by services - -**Example**: -```rust -// infrastructure/kafka/consumer.rs -pub struct KafkaEventSource { - consumer: Arc, // ← Kafka-specific! -} - -#[async_trait] -impl EventSource for KafkaEventSource { // ← Implements the interface - async fn receive(&self) -> anyhow::Result { - let msg = self.consumer.recv().await?.detach(); - let payload = msg.payload().ok_or(...)?; - let event: MempoolEvent = serde_json::from_slice(payload)?; - Ok(event) - } -} -``` - -### 🏭 Factories (`src/factories/`) - -**What they are**: Convenience functions that wire everything together. - -**Contains**: -- `create_mempool_engine()` - creates a fully-wired MempoolEngine with Kafka consumer - -**Purpose**: -- Reduce boilerplate in main.rs -- Provide sensible defaults -- Make it easy to get started - -**When to use**: -- Quick setup in binaries -- Standard configurations - -**When NOT to use**: -- Custom wiring needed -- Testing (inject mocks directly) -- Non-standard configurations - -**Example**: -```rust -// factories/kafka_engine.rs -pub fn create_mempool_engine( - properties_file: &str, - topic: &str, - consumer_group_id: &str, - pool_config: Option, -) -> anyhow::Result> { - // 1. Create Kafka consumer (infrastructure) - let consumer: StreamConsumer = create_kafka_consumer(...)?; - - // 2. Wrap in interface adapter - let event_source = Arc::new(KafkaEventSource::new(Arc::new(consumer))); - - // 3. Create service with interface - let engine = MempoolEngine::with_event_source(event_source, pool_config); - - Ok(Arc::new(engine)) -} -``` - -## Why This Architecture? - -### ✅ Benefits - -1. **Testability**: Mock interfaces instead of real Kafka/RPC - ```rust - // Test with fake event source - let mock_source = Arc::new(MockEventSource::new(vec![event1, event2])); - let engine = MempoolEngine::with_event_source(mock_source, None); - ``` - -2. **Flexibility**: Swap infrastructure without touching business logic - ```rust - // Production: Kafka - let source = KafkaEventSource::new(kafka_consumer); - - // Development: In-memory - let source = InMemoryEventSource::new(vec![...]); - - // Same engine works with both! - let engine = MempoolEngine::with_event_source(source, config); - ``` - -3. **Reusability**: Services can be used in multiple binaries - ```rust - // ingress-rpc binary - use account_abstraction_core::MempoolEngine; - - // batch-processor binary - use account_abstraction_core::MempoolEngine; - - // Same code, different contexts! - ``` - -4. **Clear boundaries**: Each layer has a single responsibility - - Domain: Business rules - - Services: Orchestration - - Infrastructure: External systems - - Factories: Wiring - -5. **Independent evolution**: Change infrastructure without affecting domain - - Migrate Kafka → Redis: Only touch `infrastructure/` - - Add new validation rule: Only touch `domain/` - - Change orchestration: Only touch `services/` - -## Usage Examples - -### Basic Usage (with Factory) - -```rust -use account_abstraction_core::create_mempool_engine; - -let engine = create_mempool_engine( - "kafka.properties", - "user-operations", - "mempool-consumer", - None, -)?; - -tokio::spawn(async move { - engine.run().await; -}); -``` - -### Custom Setup (without Factory) - -```rust -use account_abstraction_core::{ - MempoolEngine, - infrastructure::kafka::consumer::KafkaEventSource, -}; - -let kafka_consumer = create_kafka_consumer(...)?; -let event_source = Arc::new(KafkaEventSource::new(Arc::new(kafka_consumer))); -let engine = MempoolEngine::with_event_source(event_source, Some(custom_config)); -``` - -### Testing - -```rust -use account_abstraction_core::{ - MempoolEngine, MempoolEvent, - services::interfaces::event_source::EventSource, -}; - -struct MockEventSource { - events: Vec, -} - -#[async_trait] -impl EventSource for MockEventSource { - async fn receive(&self) -> anyhow::Result { - // Return test events - } -} - -let mock = Arc::new(MockEventSource { events: test_events }); -let engine = MempoolEngine::with_event_source(mock, None); -// Test without any real Kafka! -``` - -## Further Reading - -- [Clean Architecture (Robert C. Martin)](https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html) -- [Hexagonal Architecture](https://alistair.cockburn.us/hexagonal-architecture/) -- [Ports and Adapters Pattern](https://jmgarridopaz.github.io/content/hexagonalarchitecture.html) diff --git a/crates/account-abstraction-core/src/domain/entrypoints/mod.rs b/crates/account-abstraction-core/src/domain/entrypoints/mod.rs deleted file mode 100644 index 4946ba2e..00000000 --- a/crates/account-abstraction-core/src/domain/entrypoints/mod.rs +++ /dev/null @@ -1,3 +0,0 @@ -pub mod v06; -pub mod v07; -pub mod version; diff --git a/crates/account-abstraction-core/src/domain/entrypoints/v06.rs b/crates/account-abstraction-core/src/domain/entrypoints/v06.rs deleted file mode 100644 index 4883305d..00000000 --- a/crates/account-abstraction-core/src/domain/entrypoints/v06.rs +++ /dev/null @@ -1,139 +0,0 @@ -/* - * ERC-4337 v0.6 UserOperation Hash Calculation - * - * 1. Hash variable-length fields: initCode, callData, paymasterAndData - * 2. Pack all fields into struct (using hashes from step 1, gas values as uint256) - * 3. encodedHash = keccak256(abi.encode(packed struct)) - * 4. final hash = keccak256(abi.encode(encodedHash, entryPoint, chainId)) - * - * Reference: rundler/crates/types/src/user_operation/v0_6.rs:927-934 - */ -use alloy_primitives::{ChainId, U256}; -use alloy_rpc_types::erc4337; -use alloy_sol_types::{SolValue, sol}; -sol! { - #[allow(missing_docs)] - #[derive(Default, Debug, PartialEq, Eq)] - struct UserOperationHashEncoded { - bytes32 encodedHash; - address entryPoint; - uint256 chainId; - } - - #[allow(missing_docs)] - #[derive(Default, Debug, PartialEq, Eq)] - struct UserOperationPackedForHash { - address sender; - uint256 nonce; - bytes32 hashInitCode; - bytes32 hashCallData; - uint256 callGasLimit; - uint256 verificationGasLimit; - uint256 preVerificationGas; - uint256 maxFeePerGas; - uint256 maxPriorityFeePerGas; - bytes32 hashPaymasterAndData; - } -} - -impl From for UserOperationPackedForHash { - fn from(op: erc4337::UserOperation) -> UserOperationPackedForHash { - UserOperationPackedForHash { - sender: op.sender, - nonce: op.nonce, - hashInitCode: alloy_primitives::keccak256(op.init_code), - hashCallData: alloy_primitives::keccak256(op.call_data), - callGasLimit: U256::from(op.call_gas_limit), - verificationGasLimit: U256::from(op.verification_gas_limit), - preVerificationGas: U256::from(op.pre_verification_gas), - maxFeePerGas: U256::from(op.max_fee_per_gas), - maxPriorityFeePerGas: U256::from(op.max_priority_fee_per_gas), - hashPaymasterAndData: alloy_primitives::keccak256(op.paymaster_and_data), - } - } -} - -pub fn hash_user_operation( - user_operation: &erc4337::UserOperation, - entry_point: alloy_primitives::Address, - chain_id: ChainId, -) -> alloy_primitives::B256 { - let packed = UserOperationPackedForHash::from(user_operation.clone()); - let encoded = UserOperationHashEncoded { - encodedHash: alloy_primitives::keccak256(packed.abi_encode()), - entryPoint: entry_point, - chainId: U256::from(chain_id), - }; - alloy_primitives::keccak256(encoded.abi_encode()) -} - -#[cfg(test)] -mod tests { - use super::*; - use alloy_primitives::{Bytes, U256, address, b256, bytes}; - use alloy_rpc_types::erc4337; - - #[test] - fn test_hash_zeroed() { - let entry_point_address_v0_6 = address!("66a15edcc3b50a663e72f1457ffd49b9ae284ddc"); - let chain_id = 1337; - let user_op_with_zeroed_init_code = erc4337::UserOperation { - sender: address!("0x0000000000000000000000000000000000000000"), - nonce: U256::ZERO, - init_code: Bytes::default(), - call_data: Bytes::default(), - call_gas_limit: U256::from(0), - verification_gas_limit: U256::from(0), - pre_verification_gas: U256::from(0), - max_fee_per_gas: U256::from(0), - max_priority_fee_per_gas: U256::from(0), - paymaster_and_data: Bytes::default(), - signature: Bytes::default(), - }; - - let hash = hash_user_operation( - &user_op_with_zeroed_init_code, - entry_point_address_v0_6, - chain_id, - ); - assert_eq!( - hash, - b256!("dca97c3b49558ab360659f6ead939773be8bf26631e61bb17045bb70dc983b2d") - ); - } - - #[test] - fn test_hash_non_zeroed() { - let entry_point_address_v0_6 = address!("66a15edcc3b50a663e72f1457ffd49b9ae284ddc"); - let chain_id = 1337; - let user_op_with_non_zeroed_init_code = erc4337::UserOperation { - sender: address!("0x1306b01bc3e4ad202612d3843387e94737673f53"), - nonce: U256::from(8942), - init_code: "0x6942069420694206942069420694206942069420" - .parse() - .unwrap(), - call_data: "0x0000000000000000000000000000000000000000080085" - .parse() - .unwrap(), - call_gas_limit: U256::from(10_000), - verification_gas_limit: U256::from(100_000), - pre_verification_gas: U256::from(100), - max_fee_per_gas: U256::from(99_999), - max_priority_fee_per_gas: U256::from(9_999_999), - paymaster_and_data: bytes!( - "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" - ), - signature: bytes!("da0929f527cded8d0a1eaf2e8861d7f7e2d8160b7b13942f99dd367df4473a"), - }; - - let hash = hash_user_operation( - &user_op_with_non_zeroed_init_code, - entry_point_address_v0_6, - chain_id, - ); - assert_eq!( - hash, - b256!("484add9e4d8c3172d11b5feb6a3cc712280e176d278027cfa02ee396eb28afa1") - ); - } -} diff --git a/crates/account-abstraction-core/src/domain/entrypoints/v07.rs b/crates/account-abstraction-core/src/domain/entrypoints/v07.rs deleted file mode 100644 index 60d8fd46..00000000 --- a/crates/account-abstraction-core/src/domain/entrypoints/v07.rs +++ /dev/null @@ -1,180 +0,0 @@ -/* - * ERC-4337 v0.7 UserOperation Hash Calculation - * - * 1. Hash variable-length fields: initCode, callData, paymasterAndData - * 2. Pack all fields into struct (using hashes from step 1, gas values as bytes32) - * 3. encodedHash = keccak256(abi.encode(packed struct)) - * 4. final hash = keccak256(abi.encode(encodedHash, entryPoint, chainId)) - * - * Reference: rundler/crates/types/src/user_operation/v0_7.rs:1094-1123 - */ -use alloy_primitives::{Address, ChainId, FixedBytes, U256}; -use alloy_primitives::{Bytes, keccak256}; -use alloy_rpc_types::erc4337; -use alloy_sol_types::{SolValue, sol}; - -sol!( - #[allow(missing_docs)] - #[derive(Default, Debug, PartialEq, Eq)] - struct PackedUserOperation { - address sender; - uint256 nonce; - bytes initCode; - bytes callData; - bytes32 accountGasLimits; - uint256 preVerificationGas; - bytes32 gasFees; - bytes paymasterAndData; - bytes signature; - } - - #[derive(Default, Debug, PartialEq, Eq)] - struct UserOperationHashEncoded { - bytes32 encodedHash; - address entryPoint; - uint256 chainId; - } - - #[derive(Default, Debug, PartialEq, Eq)] - struct UserOperationPackedForHash { - address sender; - uint256 nonce; - bytes32 hashInitCode; - bytes32 hashCallData; - bytes32 accountGasLimits; - uint256 preVerificationGas; - bytes32 gasFees; - bytes32 hashPaymasterAndData; - } -); - -impl From for PackedUserOperation { - fn from(uo: erc4337::PackedUserOperation) -> Self { - let init_code = if let Some(factory) = uo.factory { - let mut init_code = factory.to_vec(); - init_code.extend_from_slice(&uo.factory_data.clone().unwrap_or_default()); - Bytes::from(init_code) - } else { - Bytes::new() - }; - let account_gas_limits = - pack_u256_pair_to_bytes32(uo.verification_gas_limit, uo.call_gas_limit); - let gas_fees = pack_u256_pair_to_bytes32(uo.max_priority_fee_per_gas, uo.max_fee_per_gas); - let pvgl: [u8; 16] = uo - .paymaster_verification_gas_limit - .unwrap_or_default() - .to::() - .to_be_bytes(); - let pogl: [u8; 16] = uo - .paymaster_post_op_gas_limit - .unwrap_or_default() - .to::() - .to_be_bytes(); - let paymaster_and_data = if let Some(paymaster) = uo.paymaster { - let mut paymaster_and_data = paymaster.to_vec(); - paymaster_and_data.extend_from_slice(&pvgl); - paymaster_and_data.extend_from_slice(&pogl); - paymaster_and_data.extend_from_slice(&uo.paymaster_data.unwrap()); - Bytes::from(paymaster_and_data) - } else { - Bytes::new() - }; - PackedUserOperation { - sender: uo.sender, - nonce: uo.nonce, - initCode: init_code, - callData: uo.call_data.clone(), - accountGasLimits: account_gas_limits, - preVerificationGas: U256::from(uo.pre_verification_gas), - gasFees: gas_fees, - paymasterAndData: paymaster_and_data, - signature: uo.signature.clone(), - } - } -} -fn pack_u256_pair_to_bytes32(high: U256, low: U256) -> FixedBytes<32> { - let mask = (U256::from(1u64) << 128) - U256::from(1u64); - let hi = high & mask; - let lo = low & mask; - let combined: U256 = (hi << 128) | lo; - FixedBytes::from(combined.to_be_bytes::<32>()) -} - -fn hash_packed_user_operation( - puo: &PackedUserOperation, - entry_point: Address, - chain_id: u64, -) -> FixedBytes<32> { - let hash_init_code = alloy_primitives::keccak256(&puo.initCode); - let hash_call_data = alloy_primitives::keccak256(&puo.callData); - let hash_paymaster_and_data = alloy_primitives::keccak256(&puo.paymasterAndData); - - let packed_for_hash = UserOperationPackedForHash { - sender: puo.sender, - nonce: puo.nonce, - hashInitCode: hash_init_code, - hashCallData: hash_call_data, - accountGasLimits: puo.accountGasLimits, - preVerificationGas: puo.preVerificationGas, - gasFees: puo.gasFees, - hashPaymasterAndData: hash_paymaster_and_data, - }; - - let hashed = alloy_primitives::keccak256(packed_for_hash.abi_encode()); - - let encoded = UserOperationHashEncoded { - encodedHash: hashed, - entryPoint: entry_point, - chainId: U256::from(chain_id), - }; - - keccak256(encoded.abi_encode()) -} - -pub fn hash_user_operation( - user_operation: &erc4337::PackedUserOperation, - entry_point: Address, - chain_id: ChainId, -) -> FixedBytes<32> { - let packed = PackedUserOperation::from(user_operation.clone()); - hash_packed_user_operation(&packed, entry_point, chain_id) -} - -#[cfg(test)] -mod test { - use super::*; - use alloy_primitives::{Bytes, U256}; - use alloy_primitives::{address, b256, bytes, uint}; - - #[test] - fn test_hash() { - let puo = PackedUserOperation { - sender: address!("b292Cf4a8E1fF21Ac27C4f94071Cd02C022C414b"), - nonce: uint!(0xF83D07238A7C8814A48535035602123AD6DBFA63000000000000000000000001_U256), - initCode: Bytes::default(), // Empty - callData: - bytes!("e9ae5c53000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000 - 0000000000000000000000000000000000001d8b292cf4a8e1ff21ac27c4f94071cd02c022c414b00000000000000000000000000000000000000000000000000000000000000009517e29f000000000000000000 - 0000000000000000000000000000000000000000000002000000000000000000000000ad6330089d9a1fe89f4020292e1afe9969a5a2fc00000000000000000000000000000000000000000000000000000000000 - 0006000000000000000000000000000000000000000000000000000000000000001200000000000000000000000000000000000000000000000000000000000015180000000000000000000000000000000000000 - 00000000000000000000000000000000000000000000000000000000000000000000000000000000018e2fbe898000000000000000000000000000000000000000000000000000000000000000800000000000000 - 0000000000000000000000000000000000000000000000000800000000000000000000000002372912728f93ab3daaaebea4f87e6e28476d987000000000000000000000000000000000000000000000000002386 - f26fc10000000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000000000000000000000"), - accountGasLimits: b256!("000000000000000000000000000114fc0000000000000000000000000012c9b5"), - preVerificationGas: U256::from(48916), - gasFees: b256!("000000000000000000000000524121000000000000000000000000109a4a441a"), - paymasterAndData: Bytes::default(), // Empty - signature: bytes!("3c7bfe22c9c2ef8994a9637bcc4df1741c5dc0c25b209545a7aeb20f7770f351479b683bd17c4d55bc32e2a649c8d2dff49dcfcc1f3fd837bcd88d1e69a434cf1c"), - }; - - let expected_hash = - b256!("e486401370d145766c3cf7ba089553214a1230d38662ae532c9b62eb6dadcf7e"); - let uo = hash_packed_user_operation( - &puo, - address!("0x0000000071727De22E5E9d8BAf0edAc6f37da032"), - 11155111, - ); - - assert_eq!(uo, expected_hash); - } -} diff --git a/crates/account-abstraction-core/src/domain/entrypoints/version.rs b/crates/account-abstraction-core/src/domain/entrypoints/version.rs deleted file mode 100644 index ae37e5d4..00000000 --- a/crates/account-abstraction-core/src/domain/entrypoints/version.rs +++ /dev/null @@ -1,31 +0,0 @@ -use alloy_primitives::{Address, address}; - -#[derive(Debug, Clone)] -pub enum EntryPointVersion { - V06, - V07, -} - -impl EntryPointVersion { - pub const V06_ADDRESS: Address = address!("0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789"); - pub const V07_ADDRESS: Address = address!("0x0000000071727De22E5E9d8BAf0edAc6f37da032"); -} - -#[derive(Debug)] -pub struct UnknownEntryPointAddress { - pub address: Address, -} - -impl TryFrom
for EntryPointVersion { - type Error = UnknownEntryPointAddress; - - fn try_from(addr: Address) -> Result { - if addr == Self::V06_ADDRESS { - Ok(EntryPointVersion::V06) - } else if addr == Self::V07_ADDRESS { - Ok(EntryPointVersion::V07) - } else { - Err(UnknownEntryPointAddress { address: addr }) - } - } -} diff --git a/crates/account-abstraction-core/src/domain/events.rs b/crates/account-abstraction-core/src/domain/events.rs deleted file mode 100644 index 9b25c0ce..00000000 --- a/crates/account-abstraction-core/src/domain/events.rs +++ /dev/null @@ -1,17 +0,0 @@ -use crate::domain::types::WrappedUserOperation; -use serde::{Deserialize, Serialize}; - -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(tag = "event", content = "data")] -pub enum MempoolEvent { - UserOpAdded { - user_op: WrappedUserOperation, - }, - UserOpIncluded { - user_op: WrappedUserOperation, - }, - UserOpDropped { - user_op: WrappedUserOperation, - reason: String, - }, -} diff --git a/crates/account-abstraction-core/src/domain/mempool.rs b/crates/account-abstraction-core/src/domain/mempool.rs deleted file mode 100644 index 701a2344..00000000 --- a/crates/account-abstraction-core/src/domain/mempool.rs +++ /dev/null @@ -1,18 +0,0 @@ -use crate::domain::types::{UserOpHash, WrappedUserOperation}; -use std::sync::Arc; - -#[derive(Default)] -pub struct PoolConfig { - pub minimum_max_fee_per_gas: u128, -} - -pub trait Mempool: Send + Sync { - fn add_operation(&mut self, operation: &WrappedUserOperation) -> Result<(), anyhow::Error>; - - fn get_top_operations(&self, n: usize) -> impl Iterator>; - - fn remove_operation( - &mut self, - operation_hash: &UserOpHash, - ) -> Result, anyhow::Error>; -} diff --git a/crates/account-abstraction-core/src/domain/mod.rs b/crates/account-abstraction-core/src/domain/mod.rs deleted file mode 100644 index 751d4e84..00000000 --- a/crates/account-abstraction-core/src/domain/mod.rs +++ /dev/null @@ -1,13 +0,0 @@ -pub mod entrypoints; -pub mod events; -pub mod mempool; -pub mod reputation; -pub mod types; - -pub use events::MempoolEvent; -pub use mempool::{Mempool, PoolConfig}; -pub use reputation::{ReputationService, ReputationStatus}; -pub use types::{ - UserOpHash, UserOperationRequest, ValidationResult, VersionedUserOperation, - WrappedUserOperation, -}; diff --git a/crates/account-abstraction-core/src/domain/reputation.rs b/crates/account-abstraction-core/src/domain/reputation.rs deleted file mode 100644 index 0f0fed42..00000000 --- a/crates/account-abstraction-core/src/domain/reputation.rs +++ /dev/null @@ -1,18 +0,0 @@ -use alloy_primitives::Address; -use async_trait::async_trait; - -/// Reputation status for an entity -#[derive(Debug, Copy, Clone, PartialEq, Eq)] -pub enum ReputationStatus { - /// Entity is not throttled or banned - Ok, - /// Entity is throttled - Throttled, - /// Entity is banned - Banned, -} - -#[async_trait] -pub trait ReputationService: Send + Sync { - async fn get_reputation(&self, entity: &Address) -> ReputationStatus; -} diff --git a/crates/account-abstraction-core/src/domain/types.rs b/crates/account-abstraction-core/src/domain/types.rs deleted file mode 100644 index 837cd52e..00000000 --- a/crates/account-abstraction-core/src/domain/types.rs +++ /dev/null @@ -1,207 +0,0 @@ -use super::entrypoints::{v06, v07, version::EntryPointVersion}; -use alloy_primitives::{Address, B256, ChainId, FixedBytes, U256}; -use alloy_rpc_types::erc4337; -pub use alloy_rpc_types::erc4337::SendUserOperationResponse; -use anyhow::Result; -use serde::{Deserialize, Serialize}; - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -#[serde(untagged)] -pub enum VersionedUserOperation { - UserOperation(erc4337::UserOperation), - PackedUserOperation(erc4337::PackedUserOperation), -} - -impl VersionedUserOperation { - pub fn max_fee_per_gas(&self) -> U256 { - match self { - VersionedUserOperation::UserOperation(op) => op.max_fee_per_gas, - VersionedUserOperation::PackedUserOperation(op) => op.max_fee_per_gas, - } - } - - pub fn max_priority_fee_per_gas(&self) -> U256 { - match self { - VersionedUserOperation::UserOperation(op) => op.max_priority_fee_per_gas, - VersionedUserOperation::PackedUserOperation(op) => op.max_priority_fee_per_gas, - } - } - pub fn nonce(&self) -> U256 { - match self { - VersionedUserOperation::UserOperation(op) => op.nonce, - VersionedUserOperation::PackedUserOperation(op) => op.nonce, - } - } - - pub fn sender(&self) -> Address { - match self { - VersionedUserOperation::UserOperation(op) => op.sender, - VersionedUserOperation::PackedUserOperation(op) => op.sender, - } - } -} -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -pub struct UserOperationRequest { - pub user_operation: VersionedUserOperation, - pub entry_point: Address, - pub chain_id: ChainId, -} - -impl UserOperationRequest { - pub fn hash(&self) -> Result { - let entry_point_version = EntryPointVersion::try_from(self.entry_point) - .map_err(|_| anyhow::anyhow!("Unknown entry point version: {:#x}", self.entry_point))?; - - match (&self.user_operation, entry_point_version) { - (VersionedUserOperation::UserOperation(op), EntryPointVersion::V06) => Ok( - v06::hash_user_operation(op, self.entry_point, self.chain_id), - ), - (VersionedUserOperation::PackedUserOperation(op), EntryPointVersion::V07) => Ok( - v07::hash_user_operation(op, self.entry_point, self.chain_id), - ), - _ => Err(anyhow::anyhow!( - "Mismatched operation type and entry point version" - )), - } - } -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct UserOperationRequestValidationResult { - pub expiration_timestamp: u64, - pub gas_used: U256, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct ValidationResult { - pub valid: bool, - #[serde(skip_serializing_if = "Option::is_none")] - pub reason: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub valid_until: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub valid_after: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub context: Option, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct ValidationContext { - pub sender_info: EntityStakeInfo, - #[serde(skip_serializing_if = "Option::is_none")] - pub factory_info: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub paymaster_info: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub aggregator_info: Option, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct EntityStakeInfo { - pub address: Address, - pub stake: U256, - pub unstake_delay_sec: u64, - pub deposit: U256, - pub is_staked: bool, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct AggregatorInfo { - pub aggregator: Address, - pub stake_info: EntityStakeInfo, -} - -pub type UserOpHash = FixedBytes<32>; - -#[derive(Eq, PartialEq, Clone, Debug, Serialize, Deserialize)] -pub struct WrappedUserOperation { - pub operation: VersionedUserOperation, - pub hash: UserOpHash, -} - -impl WrappedUserOperation { - pub fn has_higher_max_fee(&self, other: &WrappedUserOperation) -> bool { - self.operation.max_fee_per_gas() > other.operation.max_fee_per_gas() - } -} - -#[cfg(test)] -mod tests { - use std::str::FromStr; - - use super::*; - use alloy_primitives::{Address, Uint}; - - #[test] - fn deser_untagged_user_operation_without_type_field() { - let json = r#" - { - "sender": "0x1111111111111111111111111111111111111111", - "nonce": "0x0", - "initCode": "0x", - "callData": "0x", - "callGasLimit": "0x5208", - "verificationGasLimit": "0x100000", - "preVerificationGas": "0x10000", - "maxFeePerGas": "0x59682f10", - "maxPriorityFeePerGas": "0x3b9aca00", - "paymasterAndData": "0x", - "signature": "0x01" - } - "#; - - let parsed: VersionedUserOperation = - serde_json::from_str(json).expect("should deserialize as v0.6"); - match parsed { - VersionedUserOperation::UserOperation(op) => { - assert_eq!( - op.sender, - Address::from_str("0x1111111111111111111111111111111111111111").unwrap() - ); - assert_eq!(op.nonce, Uint::from(0)); - } - other => panic!("expected UserOperation, got {:?}", other), - } - } - - #[test] - fn deser_untagged_packed_user_operation_without_type_field() { - let json = r#" - { - "sender": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", - "nonce": "0x1", - "factory": "0x2222222222222222222222222222222222222222", - "factoryData": "0xabcdef1234560000000000000000000000000000000000000000000000000000", - "callData": "0xb61d27f600000000000000000000000000000000000000000000000000000000000000c8", - "callGasLimit": "0x2dc6c0", - "verificationGasLimit": "0x1e8480", - "preVerificationGas": "0x186a0", - "maxFeePerGas": "0x77359400", - "maxPriorityFeePerGas": "0x3b9aca00", - "paymaster": "0x3333333333333333333333333333333333333333", - "paymasterVerificationGasLimit": "0x186a0", - "paymasterPostOpGasLimit": "0x27100", - "paymasterData": "0xfafb00000000000000000000000000000000000000000000000000000000000064", - "signature": "0xa3c5f1b90014e68abbbdc42e4b77b9accc0b7e1c5d0b5bcde1a47ba8faba00ff55c9a7de12e98b731766e35f6c51ab25c9b58cc0e7c4a33f25e75c51c6ad3c3a" - } - "#; - - let parsed: VersionedUserOperation = - serde_json::from_str(json).expect("should deserialize as v0.7 packed"); - match parsed { - VersionedUserOperation::PackedUserOperation(op) => { - assert_eq!( - op.sender, - Address::from_str("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48").unwrap() - ); - assert_eq!(op.nonce, Uint::from(1)); - } - other => panic!("expected PackedUserOperation, got {:?}", other), - } - } -} diff --git a/crates/account-abstraction-core/src/factories/kafka_engine.rs b/crates/account-abstraction-core/src/factories/kafka_engine.rs deleted file mode 100644 index a7d972b0..00000000 --- a/crates/account-abstraction-core/src/factories/kafka_engine.rs +++ /dev/null @@ -1,33 +0,0 @@ -use crate::domain::mempool::PoolConfig; -use crate::infrastructure::in_memory::mempool::InMemoryMempool; -use crate::infrastructure::kafka::consumer::KafkaEventSource; -use crate::services::mempool_engine::MempoolEngine; -use rdkafka::{ - ClientConfig, - consumer::{Consumer, StreamConsumer}, -}; -use std::sync::Arc; -use tips_core::kafka::load_kafka_config_from_file; -use tokio::sync::RwLock; - -pub fn create_mempool_engine( - properties_file: &str, - topic: &str, - consumer_group_id: &str, - pool_config: Option, -) -> anyhow::Result>> { - let mut client_config = ClientConfig::from_iter(load_kafka_config_from_file(properties_file)?); - client_config.set("group.id", consumer_group_id); - client_config.set("enable.auto.commit", "true"); - - let consumer: StreamConsumer = client_config.create()?; - consumer.subscribe(&[topic])?; - - let event_source = Arc::new(KafkaEventSource::new(Arc::new(consumer))); - let mempool = Arc::new(RwLock::new(InMemoryMempool::new( - pool_config.unwrap_or_default(), - ))); - let engine = MempoolEngine::::new(mempool, event_source); - - Ok(Arc::new(engine)) -} diff --git a/crates/account-abstraction-core/src/factories/mod.rs b/crates/account-abstraction-core/src/factories/mod.rs deleted file mode 100644 index 16ba50dc..00000000 --- a/crates/account-abstraction-core/src/factories/mod.rs +++ /dev/null @@ -1 +0,0 @@ -pub mod kafka_engine; diff --git a/crates/account-abstraction-core/src/infrastructure/base_node/mod.rs b/crates/account-abstraction-core/src/infrastructure/base_node/mod.rs deleted file mode 100644 index fa199f24..00000000 --- a/crates/account-abstraction-core/src/infrastructure/base_node/mod.rs +++ /dev/null @@ -1 +0,0 @@ -pub mod validator; diff --git a/crates/account-abstraction-core/src/infrastructure/base_node/validator.rs b/crates/account-abstraction-core/src/infrastructure/base_node/validator.rs deleted file mode 100644 index e0577f59..00000000 --- a/crates/account-abstraction-core/src/infrastructure/base_node/validator.rs +++ /dev/null @@ -1,173 +0,0 @@ -use crate::domain::types::{ValidationResult, VersionedUserOperation}; -use crate::services::interfaces::user_op_validator::UserOperationValidator; -use alloy_primitives::Address; -use alloy_provider::{Provider, RootProvider}; -use async_trait::async_trait; -use op_alloy_network::Optimism; -use std::sync::Arc; -use tokio::time::{Duration, timeout}; - -#[derive(Debug, Clone)] -pub struct BaseNodeValidator { - simulation_provider: Arc>, - validate_user_operation_timeout: u64, -} - -impl BaseNodeValidator { - pub fn new( - simulation_provider: Arc>, - validate_user_operation_timeout: u64, - ) -> Self { - Self { - simulation_provider, - validate_user_operation_timeout, - } - } -} - -#[async_trait] -impl UserOperationValidator for BaseNodeValidator { - async fn validate_user_operation( - &self, - user_operation: &VersionedUserOperation, - entry_point: &Address, - ) -> anyhow::Result { - let result = timeout( - Duration::from_secs(self.validate_user_operation_timeout), - self.simulation_provider - .client() - .request("base_validateUserOperation", (user_operation, entry_point)), - ) - .await; - - let validation_result: ValidationResult = match result { - Err(_) => { - return Err(anyhow::anyhow!("Timeout on requesting validation")); - } - Ok(Err(e)) => { - return Err(anyhow::anyhow!("RPC error: {e}")); - } - Ok(Ok(v)) => v, - }; - - Ok(validation_result) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - use alloy_primitives::{Address, Bytes, U256}; - use alloy_rpc_types::erc4337::UserOperation; - use tokio::time::Duration; - use wiremock::{Mock, MockServer, ResponseTemplate, matchers::method}; - - const VALIDATION_TIMEOUT_SECS: u64 = 1; - const LONG_DELAY_SECS: u64 = 3; - - async fn setup_mock_server() -> MockServer { - MockServer::start().await - } - - fn new_test_user_operation_v06() -> VersionedUserOperation { - VersionedUserOperation::UserOperation(UserOperation { - sender: Address::ZERO, - nonce: U256::from(0), - init_code: Bytes::default(), - call_data: Bytes::default(), - call_gas_limit: U256::from(21_000), - verification_gas_limit: U256::from(100_000), - pre_verification_gas: U256::from(21_000), - max_fee_per_gas: U256::from(1_000_000_000), - max_priority_fee_per_gas: U256::from(1_000_000_000), - paymaster_and_data: Bytes::default(), - signature: Bytes::default(), - }) - } - - fn new_validator(mock_server: &MockServer) -> BaseNodeValidator { - let provider: RootProvider = - RootProvider::new_http(mock_server.uri().parse().unwrap()); - let simulation_provider = Arc::new(provider); - BaseNodeValidator::new(simulation_provider, VALIDATION_TIMEOUT_SECS) - } - - #[tokio::test] - async fn base_node_validate_user_operation_times_out() { - let mock_server = setup_mock_server().await; - - Mock::given(method("POST")) - .respond_with( - ResponseTemplate::new(200).set_delay(Duration::from_secs(LONG_DELAY_SECS)), - ) - .mount(&mock_server) - .await; - - let validator = new_validator(&mock_server); - let user_operation = new_test_user_operation_v06(); - - let result = validator - .validate_user_operation(&user_operation, &Address::ZERO) - .await; - - assert!(result.is_err()); - assert!(result.unwrap_err().to_string().contains("Timeout")); - } - - #[tokio::test] - async fn should_propagate_error_from_base_node() { - let mock_server = setup_mock_server().await; - - Mock::given(method("POST")) - .respond_with(ResponseTemplate::new(500).set_body_json(serde_json::json!({ - "jsonrpc": "2.0", - "id": 1, - "error": { - "code": -32000, - "message": "Internal error" - } - }))) - .mount(&mock_server) - .await; - - let validator = new_validator(&mock_server); - let user_operation = new_test_user_operation_v06(); - - let result = validator - .validate_user_operation(&user_operation, &Address::ZERO) - .await; - assert!(result.is_err()); - assert!(result.unwrap_err().to_string().contains("Internal error")); - } - - #[tokio::test] - async fn base_node_validate_user_operation_succeeds() { - let mock_server = setup_mock_server().await; - - Mock::given(method("POST")) - .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({ - "jsonrpc": "2.0", - "id": 1, - "result": { - "valid": true, - "reason": null, - "valid_until": null, - "valid_after": null, - "context": null - } - }))) - .mount(&mock_server) - .await; - - let validator = new_validator(&mock_server); - let user_operation = new_test_user_operation_v06(); - - let result = validator - .validate_user_operation(&user_operation, &Address::ZERO) - .await - .unwrap(); - - assert_eq!(result.valid, true); - } -} diff --git a/crates/account-abstraction-core/src/infrastructure/in_memory/mempool.rs b/crates/account-abstraction-core/src/infrastructure/in_memory/mempool.rs deleted file mode 100644 index 5aa71661..00000000 --- a/crates/account-abstraction-core/src/infrastructure/in_memory/mempool.rs +++ /dev/null @@ -1,424 +0,0 @@ -use crate::domain::mempool::{Mempool, PoolConfig}; -use crate::domain::types::{UserOpHash, WrappedUserOperation}; -use alloy_primitives::Address; -use std::cmp::Ordering; -use std::collections::{BTreeSet, HashMap}; -use std::sync::Arc; -use std::sync::atomic::{AtomicU64, Ordering as AtomicOrdering}; -use tracing::warn; - -#[derive(Eq, PartialEq, Clone, Debug)] -struct OrderedPoolOperation { - pool_operation: WrappedUserOperation, - submission_id: u64, -} - -impl OrderedPoolOperation { - fn from_wrapped(operation: &WrappedUserOperation, submission_id: u64) -> Self { - Self { - pool_operation: operation.clone(), - submission_id, - } - } - - fn sender(&self) -> Address { - self.pool_operation.operation.sender() - } -} - -#[derive(Clone, Debug)] -struct ByMaxFeeAndSubmissionId(OrderedPoolOperation); - -impl PartialEq for ByMaxFeeAndSubmissionId { - fn eq(&self, other: &Self) -> bool { - self.0.pool_operation.hash == other.0.pool_operation.hash - } -} -impl Eq for ByMaxFeeAndSubmissionId {} - -impl PartialOrd for ByMaxFeeAndSubmissionId { - fn partial_cmp(&self, other: &Self) -> Option { - Some(self.cmp(other)) - } -} - -impl Ord for ByMaxFeeAndSubmissionId { - fn cmp(&self, other: &Self) -> Ordering { - other - .0 - .pool_operation - .operation - .max_priority_fee_per_gas() - .cmp(&self.0.pool_operation.operation.max_priority_fee_per_gas()) - .then_with(|| self.0.submission_id.cmp(&other.0.submission_id)) - } -} - -#[derive(Clone, Debug)] -struct ByNonce(OrderedPoolOperation); - -impl PartialEq for ByNonce { - fn eq(&self, other: &Self) -> bool { - self.0.pool_operation.hash == other.0.pool_operation.hash - } -} -impl Eq for ByNonce {} - -impl PartialOrd for ByNonce { - fn partial_cmp(&self, other: &Self) -> Option { - Some(self.cmp(other)) - } -} - -impl Ord for ByNonce { - fn cmp(&self, other: &Self) -> Ordering { - self.0 - .pool_operation - .operation - .nonce() - .cmp(&other.0.pool_operation.operation.nonce()) - .then_with(|| self.0.submission_id.cmp(&other.0.submission_id)) - .then_with(|| self.0.pool_operation.hash.cmp(&other.0.pool_operation.hash)) - } -} - -pub struct InMemoryMempool { - config: PoolConfig, - best: BTreeSet, - hash_to_operation: HashMap, - operations_by_account: HashMap>, - submission_id_counter: AtomicU64, -} - -impl Mempool for InMemoryMempool { - fn add_operation(&mut self, operation: &WrappedUserOperation) -> Result<(), anyhow::Error> { - if operation.operation.max_fee_per_gas() < self.config.minimum_max_fee_per_gas { - return Err(anyhow::anyhow!( - "Gas price is below the minimum required PVG gas" - )); - } - self.handle_add_operation(operation)?; - Ok(()) - } - - fn get_top_operations(&self, n: usize) -> impl Iterator> { - self.best - .iter() - .filter_map(|op_by_fee| { - let lowest = self - .operations_by_account - .get(&op_by_fee.0.sender()) - .and_then(|set| set.first()); - - match lowest { - Some(lowest) - if lowest.0.pool_operation.hash == op_by_fee.0.pool_operation.hash => - { - Some(Arc::new(op_by_fee.0.pool_operation.clone())) - } - Some(_) => None, - None => { - warn!( - account = %op_by_fee.0.sender(), - "Inconsistent state: operation in best set but not in account index" - ); - None - } - } - }) - .take(n) - } - - fn remove_operation( - &mut self, - operation_hash: &UserOpHash, - ) -> Result, anyhow::Error> { - if let Some(ordered_operation) = self.hash_to_operation.remove(operation_hash) { - self.best - .remove(&ByMaxFeeAndSubmissionId(ordered_operation.clone())); - self.operations_by_account - .get_mut(&ordered_operation.sender()) - .map(|set| set.remove(&ByNonce(ordered_operation.clone()))); - Ok(Some(ordered_operation.pool_operation)) - } else { - Ok(None) - } - } -} - -impl InMemoryMempool { - fn handle_add_operation( - &mut self, - operation: &WrappedUserOperation, - ) -> Result<(), anyhow::Error> { - if self.hash_to_operation.contains_key(&operation.hash) { - return Ok(()); - } - - let order = self.get_next_order_id(); - let ordered_operation = OrderedPoolOperation::from_wrapped(operation, order); - - self.best - .insert(ByMaxFeeAndSubmissionId(ordered_operation.clone())); - self.operations_by_account - .entry(ordered_operation.sender()) - .or_default() - .insert(ByNonce(ordered_operation.clone())); - self.hash_to_operation - .insert(operation.hash, ordered_operation.clone()); - Ok(()) - } - - fn get_next_order_id(&self) -> u64 { - self.submission_id_counter - .fetch_add(1, AtomicOrdering::SeqCst) - } - - pub fn new(config: PoolConfig) -> Self { - Self { - config, - best: BTreeSet::new(), - hash_to_operation: HashMap::new(), - operations_by_account: HashMap::new(), - submission_id_counter: AtomicU64::new(0), - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::domain::types::VersionedUserOperation; - use alloy_primitives::{Address, FixedBytes, Uint}; - use alloy_rpc_types::erc4337; - - fn create_test_user_operation(max_priority_fee_per_gas: u128) -> VersionedUserOperation { - VersionedUserOperation::UserOperation(erc4337::UserOperation { - sender: Address::random(), - nonce: Uint::from(0), - init_code: Default::default(), - call_data: Default::default(), - call_gas_limit: Uint::from(100000), - verification_gas_limit: Uint::from(100000), - pre_verification_gas: Uint::from(21000), - max_fee_per_gas: Uint::from(max_priority_fee_per_gas), - max_priority_fee_per_gas: Uint::from(max_priority_fee_per_gas), - paymaster_and_data: Default::default(), - signature: Default::default(), - }) - } - - fn create_wrapped_operation( - max_priority_fee_per_gas: u128, - hash: UserOpHash, - ) -> WrappedUserOperation { - WrappedUserOperation { - operation: create_test_user_operation(max_priority_fee_per_gas), - hash, - } - } - - fn create_test_mempool(minimum_required_pvg_gas: u128) -> InMemoryMempool { - InMemoryMempool::new(PoolConfig { - minimum_max_fee_per_gas: minimum_required_pvg_gas, - }) - } - - #[test] - fn test_add_operation_success() { - let mut mempool = create_test_mempool(1000); - let hash = FixedBytes::from([1u8; 32]); - let operation = create_wrapped_operation(2000, hash); - - let result = mempool.add_operation(&operation); - - assert!(result.is_ok()); - } - - #[test] - fn test_add_operation_below_minimum_gas() { - let mut mempool = create_test_mempool(2000); - let hash = FixedBytes::from([1u8; 32]); - let operation = create_wrapped_operation(1000, hash); - - let result = mempool.add_operation(&operation); - - assert!(result.is_err()); - assert!( - result - .unwrap_err() - .to_string() - .contains("Gas price is below the minimum required PVG gas") - ); - } - - #[test] - fn test_add_multiple_operations_with_different_hashes() { - let mut mempool = create_test_mempool(1000); - - let hash1 = FixedBytes::from([1u8; 32]); - let operation1 = create_wrapped_operation(2000, hash1); - let result1 = mempool.add_operation(&operation1); - assert!(result1.is_ok()); - - let hash2 = FixedBytes::from([2u8; 32]); - let operation2 = create_wrapped_operation(3000, hash2); - let result2 = mempool.add_operation(&operation2); - assert!(result2.is_ok()); - - let hash3 = FixedBytes::from([3u8; 32]); - let operation3 = create_wrapped_operation(1500, hash3); - let result3 = mempool.add_operation(&operation3); - assert!(result3.is_ok()); - - assert_eq!(mempool.hash_to_operation.len(), 3); - assert_eq!(mempool.best.len(), 3); - } - - #[test] - fn test_remove_operation_not_in_mempool() { - let mut mempool = create_test_mempool(1000); - let hash = FixedBytes::from([1u8; 32]); - - let result = mempool.remove_operation(&hash); - assert!(result.is_ok()); - assert!(result.unwrap().is_none()); - } - - #[test] - fn test_remove_operation_exists() { - let mut mempool = create_test_mempool(1000); - let hash = FixedBytes::from([1u8; 32]); - let operation = create_wrapped_operation(2000, hash); - - mempool.add_operation(&operation).unwrap(); - - let result = mempool.remove_operation(&hash); - assert!(result.is_ok()); - let removed = result.unwrap(); - assert!(removed.is_some()); - let removed_op = removed.unwrap(); - assert_eq!(removed_op.hash, hash); - assert_eq!(removed_op.operation.max_fee_per_gas(), Uint::from(2000)); - } - - #[test] - fn test_remove_operation_and_check_best() { - let mut mempool = create_test_mempool(1000); - let hash = FixedBytes::from([1u8; 32]); - let operation = create_wrapped_operation(2000, hash); - - mempool.add_operation(&operation).unwrap(); - - let best_before: Vec<_> = mempool.get_top_operations(10).collect(); - assert_eq!(best_before.len(), 1); - assert_eq!(best_before[0].hash, hash); - - let result = mempool.remove_operation(&hash); - assert!(result.is_ok()); - assert!(result.unwrap().is_some()); - - let best_after: Vec<_> = mempool.get_top_operations(10).collect(); - assert_eq!(best_after.len(), 0); - } - - #[test] - fn test_get_top_operations_ordering() { - let mut mempool = create_test_mempool(1000); - - let hash1 = FixedBytes::from([1u8; 32]); - let operation1 = create_wrapped_operation(2000, hash1); - mempool.add_operation(&operation1).unwrap(); - - let hash2 = FixedBytes::from([2u8; 32]); - let operation2 = create_wrapped_operation(3000, hash2); - mempool.add_operation(&operation2).unwrap(); - - let hash3 = FixedBytes::from([3u8; 32]); - let operation3 = create_wrapped_operation(1500, hash3); - mempool.add_operation(&operation3).unwrap(); - - let best: Vec<_> = mempool.get_top_operations(10).collect(); - assert_eq!(best.len(), 3); - assert_eq!(best[0].operation.max_fee_per_gas(), Uint::from(3000)); - assert_eq!(best[1].operation.max_fee_per_gas(), Uint::from(2000)); - assert_eq!(best[2].operation.max_fee_per_gas(), Uint::from(1500)); - } - - #[test] - fn test_get_top_operations_limit() { - let mut mempool = create_test_mempool(1000); - - let hash1 = FixedBytes::from([1u8; 32]); - let operation1 = create_wrapped_operation(2000, hash1); - mempool.add_operation(&operation1).unwrap(); - - let hash2 = FixedBytes::from([2u8; 32]); - let operation2 = create_wrapped_operation(3000, hash2); - mempool.add_operation(&operation2).unwrap(); - - let hash3 = FixedBytes::from([3u8; 32]); - let operation3 = create_wrapped_operation(1500, hash3); - mempool.add_operation(&operation3).unwrap(); - - let best: Vec<_> = mempool.get_top_operations(2).collect(); - assert_eq!(best.len(), 2); - assert_eq!(best[0].operation.max_fee_per_gas(), Uint::from(3000)); - assert_eq!(best[1].operation.max_fee_per_gas(), Uint::from(2000)); - } - - #[test] - fn test_get_top_operations_submission_id_tie_breaker() { - let mut mempool = create_test_mempool(1000); - - let hash1 = FixedBytes::from([1u8; 32]); - let operation1 = create_wrapped_operation(2000, hash1); - mempool.add_operation(&operation1).unwrap(); - - let hash2 = FixedBytes::from([2u8; 32]); - let operation2 = create_wrapped_operation(2000, hash2); - mempool.add_operation(&operation2).unwrap(); - - let best: Vec<_> = mempool.get_top_operations(2).collect(); - assert_eq!(best.len(), 2); - assert_eq!(best[0].hash, hash1); - assert_eq!(best[1].hash, hash2); - } - - #[test] - fn test_get_top_operations_should_return_the_lowest_nonce_operation_for_each_account() { - let mut mempool = create_test_mempool(1000); - let hash1 = FixedBytes::from([1u8; 32]); - let test_user_operation = create_test_user_operation(2000); - - let base_op = match test_user_operation.clone() { - VersionedUserOperation::UserOperation(op) => op, - _ => panic!("expected UserOperation variant"), - }; - - let operation1 = WrappedUserOperation { - operation: VersionedUserOperation::UserOperation(erc4337::UserOperation { - nonce: Uint::from(0), - max_fee_per_gas: Uint::from(2000), - ..base_op.clone() - }), - hash: hash1, - }; - - mempool.add_operation(&operation1).unwrap(); - let hash2 = FixedBytes::from([2u8; 32]); - let operation2 = WrappedUserOperation { - operation: VersionedUserOperation::UserOperation(erc4337::UserOperation { - nonce: Uint::from(1), - max_fee_per_gas: Uint::from(10_000), - ..base_op.clone() - }), - hash: hash2, - }; - mempool.add_operation(&operation2).unwrap(); - - let best: Vec<_> = mempool.get_top_operations(2).collect(); - assert_eq!(best.len(), 1); - assert_eq!(best[0].operation.nonce(), Uint::from(0)); - } -} diff --git a/crates/account-abstraction-core/src/infrastructure/in_memory/mod.rs b/crates/account-abstraction-core/src/infrastructure/in_memory/mod.rs deleted file mode 100644 index c6c321bf..00000000 --- a/crates/account-abstraction-core/src/infrastructure/in_memory/mod.rs +++ /dev/null @@ -1,3 +0,0 @@ -pub mod mempool; - -pub use mempool::InMemoryMempool; diff --git a/crates/account-abstraction-core/src/infrastructure/kafka/consumer.rs b/crates/account-abstraction-core/src/infrastructure/kafka/consumer.rs deleted file mode 100644 index 708266ba..00000000 --- a/crates/account-abstraction-core/src/infrastructure/kafka/consumer.rs +++ /dev/null @@ -1,29 +0,0 @@ -use crate::domain::events::MempoolEvent; -use crate::services::interfaces::event_source::EventSource; -use async_trait::async_trait; -use rdkafka::{Message, consumer::StreamConsumer}; -use serde_json; -use std::sync::Arc; - -pub struct KafkaEventSource { - consumer: Arc, -} - -impl KafkaEventSource { - pub fn new(consumer: Arc) -> Self { - Self { consumer } - } -} - -#[async_trait] -impl EventSource for KafkaEventSource { - async fn receive(&self) -> anyhow::Result { - let msg = self.consumer.recv().await?.detach(); - let payload = msg - .payload() - .ok_or_else(|| anyhow::anyhow!("Kafka message missing payload"))?; - let event: MempoolEvent = serde_json::from_slice(payload) - .map_err(|e| anyhow::anyhow!("Failed to parse Mempool event: {e}"))?; - Ok(event) - } -} diff --git a/crates/account-abstraction-core/src/infrastructure/kafka/mod.rs b/crates/account-abstraction-core/src/infrastructure/kafka/mod.rs deleted file mode 100644 index dca723bf..00000000 --- a/crates/account-abstraction-core/src/infrastructure/kafka/mod.rs +++ /dev/null @@ -1 +0,0 @@ -pub mod consumer; diff --git a/crates/account-abstraction-core/src/infrastructure/mod.rs b/crates/account-abstraction-core/src/infrastructure/mod.rs deleted file mode 100644 index 4b0d4ce0..00000000 --- a/crates/account-abstraction-core/src/infrastructure/mod.rs +++ /dev/null @@ -1,3 +0,0 @@ -pub mod base_node; -pub mod in_memory; -pub mod kafka; diff --git a/crates/account-abstraction-core/src/lib.rs b/crates/account-abstraction-core/src/lib.rs deleted file mode 100644 index 0b88b37f..00000000 --- a/crates/account-abstraction-core/src/lib.rs +++ /dev/null @@ -1,23 +0,0 @@ -//! High-level services that orchestrate domain logic. -//! Designed to be reused by other binaries (ingress-rpc, workers, etc.) - -pub mod domain; -pub mod factories; -pub mod infrastructure; -pub mod services; - -// Convenient re-exports for common imports -pub use domain::{ - events::MempoolEvent, - mempool::{Mempool, PoolConfig}, - types::{ValidationResult, VersionedUserOperation, WrappedUserOperation}, -}; - -pub use infrastructure::in_memory::InMemoryMempool; - -pub use services::{ - interfaces::{event_source::EventSource, user_op_validator::UserOperationValidator}, - mempool_engine::MempoolEngine, -}; - -pub use factories::kafka_engine::create_mempool_engine; diff --git a/crates/account-abstraction-core/src/services/interfaces/event_source.rs b/crates/account-abstraction-core/src/services/interfaces/event_source.rs deleted file mode 100644 index 913bda0b..00000000 --- a/crates/account-abstraction-core/src/services/interfaces/event_source.rs +++ /dev/null @@ -1,7 +0,0 @@ -use crate::domain::events::MempoolEvent; -use async_trait::async_trait; - -#[async_trait] -pub trait EventSource: Send + Sync { - async fn receive(&self) -> anyhow::Result; -} diff --git a/crates/account-abstraction-core/src/services/interfaces/mod.rs b/crates/account-abstraction-core/src/services/interfaces/mod.rs deleted file mode 100644 index 7c192948..00000000 --- a/crates/account-abstraction-core/src/services/interfaces/mod.rs +++ /dev/null @@ -1,2 +0,0 @@ -pub mod event_source; -pub mod user_op_validator; diff --git a/crates/account-abstraction-core/src/services/interfaces/user_op_validator.rs b/crates/account-abstraction-core/src/services/interfaces/user_op_validator.rs deleted file mode 100644 index 45bc6d98..00000000 --- a/crates/account-abstraction-core/src/services/interfaces/user_op_validator.rs +++ /dev/null @@ -1,12 +0,0 @@ -use crate::domain::types::{ValidationResult, VersionedUserOperation}; -use alloy_primitives::Address; -use async_trait::async_trait; - -#[async_trait] -pub trait UserOperationValidator: Send + Sync { - async fn validate_user_operation( - &self, - user_operation: &VersionedUserOperation, - entry_point: &Address, - ) -> anyhow::Result; -} diff --git a/crates/account-abstraction-core/src/services/mempool_engine.rs b/crates/account-abstraction-core/src/services/mempool_engine.rs deleted file mode 100644 index a4a8f665..00000000 --- a/crates/account-abstraction-core/src/services/mempool_engine.rs +++ /dev/null @@ -1,159 +0,0 @@ -use super::interfaces::event_source::EventSource; -use crate::domain::{events::MempoolEvent, mempool::Mempool}; -use std::sync::Arc; -use tokio::sync::RwLock; -use tracing::{info, warn}; - -pub struct MempoolEngine { - mempool: Arc>, - event_source: Arc, -} - -impl MempoolEngine { - pub fn new(mempool: Arc>, event_source: Arc) -> MempoolEngine { - Self { - mempool, - event_source, - } - } - - pub fn get_mempool(&self) -> Arc> { - Arc::clone(&self.mempool) - } - - pub async fn run(&self) { - loop { - if let Err(err) = self.process_next().await { - warn!(error = %err, "Mempool engine error, continuing"); - } - } - } - - pub async fn process_next(&self) -> anyhow::Result<()> { - let event = self.event_source.receive().await?; - self.handle_event(event).await - } - - async fn handle_event(&self, event: MempoolEvent) -> anyhow::Result<()> { - info!( - event = ?event, - "Mempool engine handling event" - ); - match event { - MempoolEvent::UserOpAdded { user_op } => { - self.mempool.write().await.add_operation(&user_op)?; - } - MempoolEvent::UserOpIncluded { user_op } => { - self.mempool.write().await.remove_operation(&user_op.hash)?; - } - MempoolEvent::UserOpDropped { user_op, reason: _ } => { - self.mempool.write().await.remove_operation(&user_op.hash)?; - } - } - Ok(()) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::domain::{ - mempool::PoolConfig, - types::{VersionedUserOperation, WrappedUserOperation}, - }; - use crate::infrastructure::in_memory::mempool::InMemoryMempool; - use crate::services::interfaces::event_source::EventSource; - use alloy_primitives::{Address, FixedBytes, Uint}; - use alloy_rpc_types::erc4337; - use async_trait::async_trait; - use tokio::sync::Mutex; - - fn make_wrapped_op(max_fee: u128, hash: [u8; 32]) -> WrappedUserOperation { - let op = VersionedUserOperation::UserOperation(erc4337::UserOperation { - sender: Address::ZERO, - nonce: Uint::from(0u64), - init_code: Default::default(), - call_data: Default::default(), - call_gas_limit: Uint::from(100_000u64), - verification_gas_limit: Uint::from(100_000u64), - pre_verification_gas: Uint::from(21_000u64), - max_fee_per_gas: Uint::from(max_fee), - max_priority_fee_per_gas: Uint::from(max_fee), - paymaster_and_data: Default::default(), - signature: Default::default(), - }); - - WrappedUserOperation { - operation: op, - hash: FixedBytes::from(hash), - } - } - - struct MockEventSource { - events: Mutex>, - } - - impl MockEventSource { - fn new(events: Vec) -> Self { - Self { - events: Mutex::new(events), - } - } - } - - #[async_trait] - impl EventSource for MockEventSource { - async fn receive(&self) -> anyhow::Result { - let mut guard = self.events.lock().await; - if guard.is_empty() { - Err(anyhow::anyhow!("no more events")) - } else { - Ok(guard.remove(0)) - } - } - } - - #[tokio::test] - async fn handle_add_operation() { - let mempool = Arc::new(RwLock::new(InMemoryMempool::new(PoolConfig::default()))); - - let op_hash = [1u8; 32]; - let wrapped = make_wrapped_op(1_000, op_hash); - - let add_event = MempoolEvent::UserOpAdded { - user_op: wrapped.clone(), - }; - let mock_source = Arc::new(MockEventSource::new(vec![add_event])); - - let engine = MempoolEngine::new(mempool.clone(), mock_source); - - engine.process_next().await.unwrap(); - let items: Vec<_> = mempool.read().await.get_top_operations(10).collect(); - assert_eq!(items.len(), 1); - assert_eq!(items[0].hash, FixedBytes::from(op_hash)); - } - - #[tokio::test] - async fn remove_operation_should_remove_from_mempool() { - let mempool = Arc::new(RwLock::new(InMemoryMempool::new(PoolConfig::default()))); - let op_hash = [1u8; 32]; - let wrapped = make_wrapped_op(1_000, op_hash); - let add_event = MempoolEvent::UserOpAdded { - user_op: wrapped.clone(), - }; - let remove_event = MempoolEvent::UserOpDropped { - user_op: wrapped.clone(), - reason: "test".to_string(), - }; - let mock_source = Arc::new(MockEventSource::new(vec![add_event, remove_event])); - - let engine = MempoolEngine::new(mempool.clone(), mock_source); - engine.process_next().await.unwrap(); - let items: Vec<_> = mempool.read().await.get_top_operations(10).collect(); - assert_eq!(items.len(), 1); - assert_eq!(items[0].hash, FixedBytes::from(op_hash)); - engine.process_next().await.unwrap(); - let items: Vec<_> = mempool.read().await.get_top_operations(10).collect(); - assert_eq!(items.len(), 0); - } -} diff --git a/crates/account-abstraction-core/src/services/mod.rs b/crates/account-abstraction-core/src/services/mod.rs deleted file mode 100644 index fe032dae..00000000 --- a/crates/account-abstraction-core/src/services/mod.rs +++ /dev/null @@ -1,7 +0,0 @@ -pub mod interfaces; -pub mod mempool_engine; -pub mod reputations_service; - -pub use interfaces::{event_source::EventSource, user_op_validator::UserOperationValidator}; -pub use mempool_engine::MempoolEngine; -pub use reputations_service::ReputationServiceImpl; diff --git a/crates/account-abstraction-core/src/services/reputations_service.rs b/crates/account-abstraction-core/src/services/reputations_service.rs deleted file mode 100644 index df15ff1f..00000000 --- a/crates/account-abstraction-core/src/services/reputations_service.rs +++ /dev/null @@ -1,27 +0,0 @@ -use crate::{ - Mempool, - domain::{ReputationService, ReputationStatus}, -}; -use alloy_primitives::Address; -use async_trait::async_trait; -use std::sync::Arc; -use tokio::sync::RwLock; - -pub struct ReputationServiceImpl { - mempool: Arc>, -} - -impl ReputationServiceImpl { - pub fn new(mempool: Arc>) -> Self { - Self { mempool } - } -} - -#[async_trait] -impl ReputationService for ReputationServiceImpl { - async fn get_reputation(&self, _entity: &Address) -> ReputationStatus { - // DO something with the mempool for compiling reasons, as this is scafolding - let _ = self.mempool.read().await.get_top_operations(1); - ReputationStatus::Ok - } -} diff --git a/crates/audit/Cargo.toml b/crates/audit/Cargo.toml deleted file mode 100644 index 159f2aa5..00000000 --- a/crates/audit/Cargo.toml +++ /dev/null @@ -1,33 +0,0 @@ -[package] -name = "tips-audit-lib" -version.workspace = true -rust-version.workspace = true -license.workspace = true -homepage.workspace = true -repository.workspace = true -edition.workspace = true - -[lints] -workspace = true - -[dependencies] -bytes.workspace = true -metrics.workspace = true -async-trait.workspace = true -metrics-derive.workspace = true -tips-core = { workspace = true, features = ["test-utils"] } -serde = { workspace = true, features = ["std", "derive"] } -tokio = { workspace = true, features = ["full"] } -uuid = { workspace = true, features = ["v5", "serde"] } -tracing = { workspace = true, features = ["std"] } -anyhow = { workspace = true, features = ["std"] } -serde_json = { workspace = true, features = ["std"] } -rdkafka = { workspace = true, features = ["tokio", "libz", "zstd", "ssl-vendored"] } -alloy-consensus = { workspace = true, features = ["std"] } -alloy-primitives = { workspace = true, features = ["map-foldhash", "serde"] } -aws-sdk-s3 = { workspace = true, features = ["rustls", "default-https-client", "rt-tokio"] } -futures = { workspace = true } - -[dev-dependencies] -testcontainers = { workspace = true, features = ["blocking"] } -testcontainers-modules = { workspace = true, features = ["postgres", "kafka", "minio"] } diff --git a/crates/audit/src/archiver.rs b/crates/audit/src/archiver.rs deleted file mode 100644 index 415782e5..00000000 --- a/crates/audit/src/archiver.rs +++ /dev/null @@ -1,163 +0,0 @@ -use crate::metrics::Metrics; -use crate::reader::{Event, EventReader}; -use crate::storage::EventWriter; -use anyhow::Result; -use std::fmt; -use std::marker::PhantomData; -use std::sync::Arc; -use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH}; -use tokio::sync::{Mutex, mpsc}; -use tokio::time::sleep; -use tracing::{error, info}; - -/// Archives audit events from Kafka to S3 storage. -pub struct KafkaAuditArchiver -where - R: EventReader, - W: EventWriter + Clone + Send + 'static, -{ - reader: R, - event_tx: mpsc::Sender, - metrics: Metrics, - _phantom: PhantomData, -} - -impl fmt::Debug for KafkaAuditArchiver -where - R: EventReader, - W: EventWriter + Clone + Send + 'static, -{ - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("KafkaAuditArchiver").finish_non_exhaustive() - } -} - -impl KafkaAuditArchiver -where - R: EventReader, - W: EventWriter + Clone + Send + 'static, -{ - /// Creates a new archiver with the given reader and writer. - pub fn new( - reader: R, - writer: W, - worker_pool_size: usize, - channel_buffer_size: usize, - noop_archive: bool, - ) -> Self { - let (event_tx, event_rx) = mpsc::channel(channel_buffer_size); - let metrics = Metrics::default(); - - Self::spawn_workers( - writer, - event_rx, - metrics.clone(), - worker_pool_size, - noop_archive, - ); - - Self { - reader, - event_tx, - metrics, - _phantom: PhantomData, - } - } - - fn spawn_workers( - writer: W, - event_rx: mpsc::Receiver, - metrics: Metrics, - worker_pool_size: usize, - noop_archive: bool, - ) { - let event_rx = Arc::new(Mutex::new(event_rx)); - - for worker_id in 0..worker_pool_size { - let writer = writer.clone(); - let metrics = metrics.clone(); - let event_rx = event_rx.clone(); - - tokio::spawn(async move { - loop { - let event = { - let mut rx = event_rx.lock().await; - rx.recv().await - }; - - match event { - Some(event) => { - let archive_start = Instant::now(); - // tmp: only use this to clear kafka consumer offset - // TODO: use debug! later - if noop_archive { - info!( - worker_id, - bundle_id = %event.event.bundle_id(), - tx_ids = ?event.event.transaction_ids(), - timestamp = event.timestamp, - "Noop archive - skipping event" - ); - metrics.events_processed.increment(1); - metrics.in_flight_archive_tasks.decrement(1.0); - continue; - } - if let Err(e) = writer.archive_event(event).await { - error!(worker_id, error = %e, "Failed to write event"); - } else { - metrics - .archive_event_duration - .record(archive_start.elapsed().as_secs_f64()); - metrics.events_processed.increment(1); - } - metrics.in_flight_archive_tasks.decrement(1.0); - } - None => { - info!(worker_id, "Worker stopped - channel closed"); - break; - } - } - } - }); - } - } - - /// Runs the archiver loop, reading events and writing them to storage. - pub async fn run(&mut self) -> Result<()> { - loop { - let read_start = Instant::now(); - match self.reader.read_event().await { - Ok(event) => { - self.metrics - .kafka_read_duration - .record(read_start.elapsed().as_secs_f64()); - - let now_ms = SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap_or_default() - .as_millis() as i64; - let event_age_ms = now_ms.saturating_sub(event.timestamp); - self.metrics.event_age.record(event_age_ms as f64); - - self.metrics.in_flight_archive_tasks.increment(1.0); - if let Err(e) = self.event_tx.send(event).await { - error!(error = %e, "Failed to send event to worker pool"); - self.metrics.in_flight_archive_tasks.decrement(1.0); - } - - let commit_start = Instant::now(); - if let Err(e) = self.reader.commit().await { - error!(error = %e, "Failed to commit message"); - } - self.metrics - .kafka_commit_duration - .record(commit_start.elapsed().as_secs_f64()); - } - Err(e) => { - error!(error = %e, "Error reading events"); - sleep(Duration::from_secs(1)).await; - } - } - } - } -} diff --git a/crates/audit/src/lib.rs b/crates/audit/src/lib.rs deleted file mode 100644 index 82919362..00000000 --- a/crates/audit/src/lib.rs +++ /dev/null @@ -1,74 +0,0 @@ -//! Audit library for tracking and archiving bundle and user operation events. -//! -//! This crate provides functionality for publishing events to Kafka, -//! archiving them to S3, and reading event history. - -#![doc(issue_tracker_base_url = "https://github.com/base/tips/issues/")] -#![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))] -#![cfg_attr(not(test), warn(unused_crate_dependencies))] - -mod archiver; -pub use archiver::KafkaAuditArchiver; - -mod metrics; -pub use metrics::Metrics; - -mod publisher; -pub use publisher::{ - BundleEventPublisher, KafkaBundleEventPublisher, KafkaUserOpEventPublisher, - LoggingBundleEventPublisher, LoggingUserOpEventPublisher, UserOpEventPublisher, -}; - -mod reader; -pub use reader::{ - Event, EventReader, KafkaAuditLogReader, KafkaUserOpAuditLogReader, UserOpEventReader, - UserOpEventWrapper, assign_topic_partition, create_kafka_consumer, -}; - -mod storage; -pub use storage::{ - BundleEventS3Reader, BundleHistory, BundleHistoryEvent, EventWriter, S3EventReaderWriter, - S3Key, TransactionMetadata, UserOpEventS3Reader, UserOpEventWriter, UserOpHistory, - UserOpHistoryEvent, -}; - -mod types; -pub use types::{ - BundleEvent, BundleId, DropReason, Transaction, TransactionId, UserOpDropReason, UserOpEvent, - UserOpHash, -}; - -use tokio::sync::mpsc; -use tracing::error; - -/// Connects a bundle event receiver to a publisher, spawning a task to forward events. -pub fn connect_audit_to_publisher

(event_rx: mpsc::UnboundedReceiver, publisher: P) -where - P: BundleEventPublisher + 'static, -{ - tokio::spawn(async move { - let mut event_rx = event_rx; - while let Some(event) = event_rx.recv().await { - if let Err(e) = publisher.publish(event).await { - error!(error = %e, "failed to publish bundle event"); - } - } - }); -} - -/// Connects a user operation event receiver to a publisher, spawning a task to forward events. -pub fn connect_userop_audit_to_publisher

( - event_rx: mpsc::UnboundedReceiver, - publisher: P, -) where - P: UserOpEventPublisher + 'static, -{ - tokio::spawn(async move { - let mut event_rx = event_rx; - while let Some(event) = event_rx.recv().await { - if let Err(e) = publisher.publish(event).await { - error!(error = %e, "Failed to publish user op event"); - } - } - }); -} diff --git a/crates/audit/src/metrics.rs b/crates/audit/src/metrics.rs deleted file mode 100644 index 906de8e3..00000000 --- a/crates/audit/src/metrics.rs +++ /dev/null @@ -1,55 +0,0 @@ -use metrics::{Counter, Gauge, Histogram}; -use metrics_derive::Metrics; - -/// Metrics for audit operations including Kafka reads, S3 writes, and event processing. -#[derive(Metrics, Clone)] -#[metrics(scope = "tips_audit")] -pub struct Metrics { - /// Duration of archive_event operations. - #[metric(describe = "Duration of archive_event")] - pub archive_event_duration: Histogram, - - /// Age of event when processed (now - event timestamp). - #[metric(describe = "Age of event when processed (now - event timestamp)")] - pub event_age: Histogram, - - /// Duration of Kafka read_event operations. - #[metric(describe = "Duration of Kafka read_event")] - pub kafka_read_duration: Histogram, - - /// Duration of Kafka commit operations. - #[metric(describe = "Duration of Kafka commit")] - pub kafka_commit_duration: Histogram, - - /// Duration of update_bundle_history operations. - #[metric(describe = "Duration of update_bundle_history")] - pub update_bundle_history_duration: Histogram, - - /// Duration of updating all transaction indexes. - #[metric(describe = "Duration of update all transaction indexes")] - pub update_tx_indexes_duration: Histogram, - - /// Duration of S3 get_object operations. - #[metric(describe = "Duration of S3 get_object")] - pub s3_get_duration: Histogram, - - /// Duration of S3 put_object operations. - #[metric(describe = "Duration of S3 put_object")] - pub s3_put_duration: Histogram, - - /// Total events processed. - #[metric(describe = "Total events processed")] - pub events_processed: Counter, - - /// Total S3 writes skipped due to deduplication. - #[metric(describe = "Total S3 writes skipped due to dedup")] - pub s3_writes_skipped: Counter, - - /// Number of in-flight archive tasks. - #[metric(describe = "Number of in-flight archive tasks")] - pub in_flight_archive_tasks: Gauge, - - /// Number of failed archive tasks. - #[metric(describe = "Number of failed archive tasks")] - pub failed_archive_tasks: Counter, -} diff --git a/crates/audit/src/publisher.rs b/crates/audit/src/publisher.rs deleted file mode 100644 index 30585436..00000000 --- a/crates/audit/src/publisher.rs +++ /dev/null @@ -1,235 +0,0 @@ -use crate::types::{BundleEvent, UserOpEvent}; -use anyhow::Result; -use async_trait::async_trait; -use rdkafka::producer::{FutureProducer, FutureRecord}; -use tracing::{debug, error, info}; - -/// Trait for publishing bundle events. -#[async_trait] -pub trait BundleEventPublisher: Send + Sync { - /// Publishes a single bundle event. - async fn publish(&self, event: BundleEvent) -> Result<()>; - - /// Publishes multiple bundle events. - async fn publish_all(&self, events: Vec) -> Result<()>; -} - -/// Publishes bundle events to Kafka. -#[derive(Clone)] -pub struct KafkaBundleEventPublisher { - producer: FutureProducer, - topic: String, -} - -impl std::fmt::Debug for KafkaBundleEventPublisher { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("KafkaBundleEventPublisher") - .field("topic", &self.topic) - .finish_non_exhaustive() - } -} - -impl KafkaBundleEventPublisher { - /// Creates a new Kafka bundle event publisher. - pub const fn new(producer: FutureProducer, topic: String) -> Self { - Self { producer, topic } - } - - async fn send_event(&self, event: &BundleEvent) -> Result<()> { - let bundle_id = event.bundle_id(); - let key = event.generate_event_key(); - let payload = serde_json::to_vec(event)?; - - let record = FutureRecord::to(&self.topic).key(&key).payload(&payload); - - match self - .producer - .send(record, tokio::time::Duration::from_secs(5)) - .await - { - Ok(_) => { - debug!( - bundle_id = %bundle_id, - topic = %self.topic, - payload_size = payload.len(), - "successfully published event" - ); - Ok(()) - } - Err((err, _)) => { - error!( - bundle_id = %bundle_id, - topic = %self.topic, - error = %err, - "failed to publish event" - ); - Err(anyhow::anyhow!("Failed to publish event: {err}")) - } - } - } -} - -#[async_trait] -impl BundleEventPublisher for KafkaBundleEventPublisher { - async fn publish(&self, event: BundleEvent) -> Result<()> { - self.send_event(&event).await - } - - async fn publish_all(&self, events: Vec) -> Result<()> { - for event in events { - self.send_event(&event).await?; - } - Ok(()) - } -} - -/// Publishes bundle events to logs (for testing/debugging). -#[derive(Clone, Debug)] -pub struct LoggingBundleEventPublisher; - -impl LoggingBundleEventPublisher { - /// Creates a new logging bundle event publisher. - pub const fn new() -> Self { - Self - } -} - -impl Default for LoggingBundleEventPublisher { - fn default() -> Self { - Self::new() - } -} - -#[async_trait] -impl BundleEventPublisher for LoggingBundleEventPublisher { - async fn publish(&self, event: BundleEvent) -> Result<()> { - info!( - bundle_id = %event.bundle_id(), - event = ?event, - "Received bundle event" - ); - Ok(()) - } - - async fn publish_all(&self, events: Vec) -> Result<()> { - for event in events { - self.publish(event).await?; - } - Ok(()) - } -} - -/// Trait for publishing user operation events. -#[async_trait] -pub trait UserOpEventPublisher: Send + Sync { - /// Publishes a single user operation event. - async fn publish(&self, event: UserOpEvent) -> Result<()>; - - /// Publishes multiple user operation events. - async fn publish_all(&self, events: Vec) -> Result<()>; -} - -/// Publishes user operation events to Kafka. -#[derive(Clone)] -pub struct KafkaUserOpEventPublisher { - producer: FutureProducer, - topic: String, -} - -impl std::fmt::Debug for KafkaUserOpEventPublisher { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("KafkaUserOpEventPublisher") - .field("topic", &self.topic) - .finish_non_exhaustive() - } -} - -impl KafkaUserOpEventPublisher { - /// Creates a new Kafka user operation event publisher. - pub const fn new(producer: FutureProducer, topic: String) -> Self { - Self { producer, topic } - } - - async fn send_event(&self, event: &UserOpEvent) -> Result<()> { - let user_op_hash = event.user_op_hash(); - let key = event.generate_event_key(); - let payload = serde_json::to_vec(event)?; - - let record = FutureRecord::to(&self.topic).key(&key).payload(&payload); - - match self - .producer - .send(record, tokio::time::Duration::from_secs(5)) - .await - { - Ok(_) => { - debug!( - user_op_hash = %user_op_hash, - topic = %self.topic, - payload_size = payload.len(), - "Successfully published user op event" - ); - Ok(()) - } - Err((err, _)) => { - error!( - user_op_hash = %user_op_hash, - topic = %self.topic, - error = %err, - "Failed to publish user op event" - ); - Err(anyhow::anyhow!("Failed to publish user op event: {err}")) - } - } - } -} - -#[async_trait] -impl UserOpEventPublisher for KafkaUserOpEventPublisher { - async fn publish(&self, event: UserOpEvent) -> Result<()> { - self.send_event(&event).await - } - - async fn publish_all(&self, events: Vec) -> Result<()> { - for event in events { - self.send_event(&event).await?; - } - Ok(()) - } -} - -/// Publishes user operation events to logs (for testing/debugging). -#[derive(Clone, Debug)] -pub struct LoggingUserOpEventPublisher; - -impl LoggingUserOpEventPublisher { - /// Creates a new logging user operation event publisher. - pub const fn new() -> Self { - Self - } -} - -impl Default for LoggingUserOpEventPublisher { - fn default() -> Self { - Self::new() - } -} - -#[async_trait] -impl UserOpEventPublisher for LoggingUserOpEventPublisher { - async fn publish(&self, event: UserOpEvent) -> Result<()> { - info!( - user_op_hash = %event.user_op_hash(), - event = ?event, - "Received user op event" - ); - Ok(()) - } - - async fn publish_all(&self, events: Vec) -> Result<()> { - for event in events { - self.publish(event).await?; - } - Ok(()) - } -} diff --git a/crates/audit/src/reader.rs b/crates/audit/src/reader.rs deleted file mode 100644 index be1c3c50..00000000 --- a/crates/audit/src/reader.rs +++ /dev/null @@ -1,268 +0,0 @@ -use crate::types::{BundleEvent, UserOpEvent}; -use anyhow::Result; -use async_trait::async_trait; -use rdkafka::{ - Timestamp, TopicPartitionList, - config::ClientConfig, - consumer::{Consumer, StreamConsumer}, - message::Message, -}; -use std::time::{Duration, SystemTime, UNIX_EPOCH}; -use tips_core::kafka::load_kafka_config_from_file; -use tokio::time::sleep; -use tracing::{debug, error, info}; - -/// Creates a Kafka consumer from a properties file. -pub fn create_kafka_consumer(kafka_properties_file: &str) -> Result { - let client_config: ClientConfig = - ClientConfig::from_iter(load_kafka_config_from_file(kafka_properties_file)?); - let consumer: StreamConsumer = client_config.create()?; - Ok(consumer) -} - -/// Assigns a topic partition to a consumer. -pub fn assign_topic_partition(consumer: &StreamConsumer, topic: &str) -> Result<()> { - let mut tpl = TopicPartitionList::new(); - tpl.add_partition(topic, 0); - consumer.assign(&tpl)?; - Ok(()) -} - -/// A bundle event with metadata from Kafka. -#[derive(Debug, Clone)] -pub struct Event { - /// The event key. - pub key: String, - /// The bundle event. - pub event: BundleEvent, - /// The event timestamp in milliseconds. - pub timestamp: i64, -} - -/// Trait for reading bundle events. -#[async_trait] -pub trait EventReader { - /// Reads the next event. - async fn read_event(&mut self) -> Result; - /// Commits the last read message. - async fn commit(&mut self) -> Result<()>; -} - -/// Reads bundle audit events from Kafka. -pub struct KafkaAuditLogReader { - consumer: StreamConsumer, - topic: String, - last_message_offset: Option, - last_message_partition: Option, -} - -impl std::fmt::Debug for KafkaAuditLogReader { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("KafkaAuditLogReader") - .field("topic", &self.topic) - .field("last_message_offset", &self.last_message_offset) - .field("last_message_partition", &self.last_message_partition) - .finish_non_exhaustive() - } -} - -impl KafkaAuditLogReader { - /// Creates a new Kafka audit log reader. - pub fn new(consumer: StreamConsumer, topic: String) -> Result { - consumer.subscribe(&[&topic])?; - Ok(Self { - consumer, - topic, - last_message_offset: None, - last_message_partition: None, - }) - } -} - -#[async_trait] -impl EventReader for KafkaAuditLogReader { - async fn read_event(&mut self) -> Result { - match self.consumer.recv().await { - Ok(message) => { - let payload = message - .payload() - .ok_or_else(|| anyhow::anyhow!("Message has no payload"))?; - - // Extract Kafka timestamp, use current time as fallback - let timestamp = match message.timestamp() { - Timestamp::CreateTime(millis) => millis, - Timestamp::LogAppendTime(millis) => millis, - Timestamp::NotAvailable => SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap_or_default() - .as_millis() as i64, - }; - - let event: BundleEvent = serde_json::from_slice(payload)?; - - info!( - bundle_id = %event.bundle_id(), - tx_ids = ?event.transaction_ids(), - timestamp = timestamp, - offset = message.offset(), - partition = message.partition(), - "Received event with timestamp" - ); - - self.last_message_offset = Some(message.offset()); - self.last_message_partition = Some(message.partition()); - - let key = message - .key() - .map(|k| String::from_utf8_lossy(k).to_string()) - .ok_or_else(|| anyhow::anyhow!("Message missing required key"))?; - - let event_result = Event { - key, - event, - timestamp, - }; - - Ok(event_result) - } - Err(e) => { - error!(error = %e, "Error receiving message from Kafka"); - sleep(Duration::from_secs(1)).await; - Err(e.into()) - } - } - } - - async fn commit(&mut self) -> Result<()> { - if let (Some(offset), Some(partition)) = - (self.last_message_offset, self.last_message_partition) - { - let mut tpl = TopicPartitionList::new(); - tpl.add_partition_offset(&self.topic, partition, rdkafka::Offset::Offset(offset + 1))?; - self.consumer - .commit(&tpl, rdkafka::consumer::CommitMode::Async)?; - } - Ok(()) - } -} - -impl KafkaAuditLogReader { - /// Returns the topic this reader is subscribed to. - pub fn topic(&self) -> &str { - &self.topic - } -} - -/// A user operation event with metadata from Kafka. -#[derive(Debug, Clone)] -pub struct UserOpEventWrapper { - /// The event key. - pub key: String, - /// The user operation event. - pub event: UserOpEvent, - /// The event timestamp in milliseconds. - pub timestamp: i64, -} - -/// Trait for reading user operation events. -#[async_trait] -pub trait UserOpEventReader { - /// Reads the next user operation event. - async fn read_event(&mut self) -> Result; - /// Commits the last read message. - async fn commit(&mut self) -> Result<()>; -} - -/// Reads user operation audit events from Kafka. -pub struct KafkaUserOpAuditLogReader { - consumer: StreamConsumer, - topic: String, - last_message_offset: Option, - last_message_partition: Option, -} - -impl std::fmt::Debug for KafkaUserOpAuditLogReader { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("KafkaUserOpAuditLogReader") - .field("topic", &self.topic) - .field("last_message_offset", &self.last_message_offset) - .field("last_message_partition", &self.last_message_partition) - .finish_non_exhaustive() - } -} - -impl KafkaUserOpAuditLogReader { - /// Creates a new Kafka user operation audit log reader. - pub fn new(consumer: StreamConsumer, topic: String) -> Result { - consumer.subscribe(&[&topic])?; - Ok(Self { - consumer, - topic, - last_message_offset: None, - last_message_partition: None, - }) - } -} - -#[async_trait] -impl UserOpEventReader for KafkaUserOpAuditLogReader { - async fn read_event(&mut self) -> Result { - match self.consumer.recv().await { - Ok(message) => { - let payload = message - .payload() - .ok_or_else(|| anyhow::anyhow!("Message has no payload"))?; - - let timestamp = match message.timestamp() { - Timestamp::CreateTime(millis) => millis, - Timestamp::LogAppendTime(millis) => millis, - Timestamp::NotAvailable => SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap_or_default() - .as_millis() as i64, - }; - - let event: UserOpEvent = serde_json::from_slice(payload)?; - - debug!( - user_op_hash = %event.user_op_hash(), - timestamp = timestamp, - offset = message.offset(), - partition = message.partition(), - "Received UserOp event" - ); - - self.last_message_offset = Some(message.offset()); - self.last_message_partition = Some(message.partition()); - - let key = message - .key() - .map(|k| String::from_utf8_lossy(k).to_string()) - .ok_or_else(|| anyhow::anyhow!("Message missing required key"))?; - - Ok(UserOpEventWrapper { - key, - event, - timestamp, - }) - } - Err(e) => { - error!(error = %e, "Error receiving UserOp message from Kafka"); - sleep(Duration::from_secs(1)).await; - Err(e.into()) - } - } - } - - async fn commit(&mut self) -> Result<()> { - if let (Some(offset), Some(partition)) = - (self.last_message_offset, self.last_message_partition) - { - let mut tpl = TopicPartitionList::new(); - tpl.add_partition_offset(&self.topic, partition, rdkafka::Offset::Offset(offset + 1))?; - self.consumer - .commit(&tpl, rdkafka::consumer::CommitMode::Async)?; - } - Ok(()) - } -} diff --git a/crates/audit/src/storage.rs b/crates/audit/src/storage.rs deleted file mode 100644 index ba9d1143..00000000 --- a/crates/audit/src/storage.rs +++ /dev/null @@ -1,1017 +0,0 @@ -use crate::metrics::Metrics; -use crate::reader::Event; -use crate::types::{ - BundleEvent, BundleId, DropReason, TransactionId, UserOpDropReason, UserOpEvent, UserOpHash, -}; -use alloy_primitives::{Address, TxHash, U256}; -use anyhow::Result; -use async_trait::async_trait; -use aws_sdk_s3::Client as S3Client; -use aws_sdk_s3::error::SdkError; -use aws_sdk_s3::operation::get_object::GetObjectError; -use aws_sdk_s3::primitives::ByteStream; -use futures::future; -use serde::{Deserialize, Serialize}; -use std::fmt; -use std::fmt::Debug; -use std::time::Instant; -use tips_core::AcceptedBundle; -use tracing::info; - -/// S3 key types for storing different event types. -#[derive(Debug)] -pub enum S3Key { - /// Key for bundle events. - Bundle(BundleId), - /// Key for transaction lookups by hash. - TransactionByHash(TxHash), - /// Key for user operation events. - UserOp(UserOpHash), -} - -impl fmt::Display for S3Key { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::Bundle(bundle_id) => write!(f, "bundles/{bundle_id}"), - Self::TransactionByHash(hash) => write!(f, "transactions/by_hash/{hash}"), - Self::UserOp(user_op_hash) => write!(f, "userops/{user_op_hash}"), - } - } -} - -/// Metadata for a transaction, tracking which bundles it belongs to. -#[derive(Debug, Clone, Serialize, Deserialize, Default)] -pub struct TransactionMetadata { - /// Bundle IDs that contain this transaction. - pub bundle_ids: Vec, -} - -/// History event for a bundle. -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(tag = "event", content = "data")] -pub enum BundleHistoryEvent { - /// Bundle was received. - Received { - /// Event key. - key: String, - /// Event timestamp. - timestamp: i64, - /// The accepted bundle. - bundle: Box, - }, - /// Bundle was cancelled. - Cancelled { - /// Event key. - key: String, - /// Event timestamp. - timestamp: i64, - }, - /// Bundle was included by a builder. - BuilderIncluded { - /// Event key. - key: String, - /// Event timestamp. - timestamp: i64, - /// Builder identifier. - builder: String, - /// Block number. - block_number: u64, - /// Flashblock index. - flashblock_index: u64, - }, - /// Bundle was included in a block. - BlockIncluded { - /// Event key. - key: String, - /// Event timestamp. - timestamp: i64, - /// Block number. - block_number: u64, - /// Block hash. - block_hash: TxHash, - }, - /// Bundle was dropped. - Dropped { - /// Event key. - key: String, - /// Event timestamp. - timestamp: i64, - /// Drop reason. - reason: DropReason, - }, -} - -impl BundleHistoryEvent { - /// Returns the event key. - pub fn key(&self) -> &str { - match self { - Self::Received { key, .. } => key, - Self::Cancelled { key, .. } => key, - Self::BuilderIncluded { key, .. } => key, - Self::BlockIncluded { key, .. } => key, - Self::Dropped { key, .. } => key, - } - } -} - -/// History of events for a bundle. -#[derive(Debug, Clone, Serialize, Deserialize, Default)] -pub struct BundleHistory { - /// List of history events. - pub history: Vec, -} - -/// History event for a user operation. -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(tag = "event", content = "data")] -pub enum UserOpHistoryEvent { - /// User operation was added to the mempool. - AddedToMempool { - /// Event key. - key: String, - /// Event timestamp. - timestamp: i64, - /// Sender address. - sender: Address, - /// Entry point address. - entry_point: Address, - /// Nonce. - nonce: U256, - }, - /// User operation was dropped. - Dropped { - /// Event key. - key: String, - /// Event timestamp. - timestamp: i64, - /// Drop reason. - reason: UserOpDropReason, - }, - /// User operation was included in a block. - Included { - /// Event key. - key: String, - /// Event timestamp. - timestamp: i64, - /// Block number. - block_number: u64, - /// Transaction hash. - tx_hash: TxHash, - }, -} - -impl UserOpHistoryEvent { - /// Returns the event key. - pub fn key(&self) -> &str { - match self { - Self::AddedToMempool { key, .. } => key, - Self::Dropped { key, .. } => key, - Self::Included { key, .. } => key, - } - } -} - -/// History of events for a user operation. -#[derive(Debug, Clone, Serialize, Deserialize, Default)] -pub struct UserOpHistory { - /// List of history events. - pub history: Vec, -} - -pub(crate) use crate::reader::UserOpEventWrapper; - -fn update_bundle_history_transform( - bundle_history: BundleHistory, - event: &Event, -) -> Option { - let mut history = bundle_history.history; - let bundle_id = event.event.bundle_id(); - - // Check for deduplication - if event with same key already exists, skip - if history.iter().any(|h| h.key() == event.key) { - info!( - bundle_id = %bundle_id, - event_key = %event.key, - "Event already exists, skipping due to deduplication" - ); - return None; - } - - let history_event = match &event.event { - BundleEvent::Received { bundle, .. } => BundleHistoryEvent::Received { - key: event.key.clone(), - timestamp: event.timestamp, - bundle: bundle.clone(), - }, - BundleEvent::Cancelled { .. } => BundleHistoryEvent::Cancelled { - key: event.key.clone(), - timestamp: event.timestamp, - }, - BundleEvent::BuilderIncluded { - builder, - block_number, - flashblock_index, - .. - } => BundleHistoryEvent::BuilderIncluded { - key: event.key.clone(), - timestamp: event.timestamp, - builder: builder.clone(), - block_number: *block_number, - flashblock_index: *flashblock_index, - }, - BundleEvent::BlockIncluded { - block_number, - block_hash, - .. - } => BundleHistoryEvent::BlockIncluded { - key: event.key.clone(), - timestamp: event.timestamp, - block_number: *block_number, - block_hash: *block_hash, - }, - BundleEvent::Dropped { reason, .. } => BundleHistoryEvent::Dropped { - key: event.key.clone(), - timestamp: event.timestamp, - reason: reason.clone(), - }, - }; - - history.push(history_event); - let bundle_history = BundleHistory { history }; - - info!( - bundle_id = %bundle_id, - event_count = bundle_history.history.len(), - "Updated bundle history" - ); - - Some(bundle_history) -} - -fn update_transaction_metadata_transform( - transaction_metadata: TransactionMetadata, - bundle_id: BundleId, -) -> Option { - let mut bundle_ids = transaction_metadata.bundle_ids; - - if bundle_ids.contains(&bundle_id) { - return None; - } - - bundle_ids.push(bundle_id); - Some(TransactionMetadata { bundle_ids }) -} - -fn update_userop_history_transform( - userop_history: UserOpHistory, - event: &UserOpEventWrapper, -) -> Option { - let mut history = userop_history.history; - let user_op_hash = event.event.user_op_hash(); - - if history.iter().any(|h| h.key() == event.key) { - info!( - user_op_hash = %user_op_hash, - event_key = %event.key, - "UserOp event already exists, skipping due to deduplication" - ); - return None; - } - - let history_event = match &event.event { - UserOpEvent::AddedToMempool { - sender, - entry_point, - nonce, - .. - } => UserOpHistoryEvent::AddedToMempool { - key: event.key.clone(), - timestamp: event.timestamp, - sender: *sender, - entry_point: *entry_point, - nonce: *nonce, - }, - UserOpEvent::Dropped { reason, .. } => UserOpHistoryEvent::Dropped { - key: event.key.clone(), - timestamp: event.timestamp, - reason: reason.clone(), - }, - UserOpEvent::Included { - block_number, - tx_hash, - .. - } => UserOpHistoryEvent::Included { - key: event.key.clone(), - timestamp: event.timestamp, - block_number: *block_number, - tx_hash: *tx_hash, - }, - }; - - history.push(history_event); - let userop_history = UserOpHistory { history }; - - info!( - user_op_hash = %user_op_hash, - event_count = userop_history.history.len(), - "Updated user op history" - ); - - Some(userop_history) -} - -/// Trait for writing bundle events to storage. -#[async_trait] -pub trait EventWriter { - /// Archives a bundle event. - async fn archive_event(&self, event: Event) -> Result<()>; -} - -/// Trait for writing user operation events to storage. -#[async_trait] -pub trait UserOpEventWriter { - /// Archives a user operation event. - async fn archive_userop_event(&self, event: UserOpEventWrapper) -> Result<()>; -} - -/// Trait for reading bundle events from S3. -#[async_trait] -pub trait BundleEventS3Reader { - /// Gets the bundle history for a given bundle ID. - async fn get_bundle_history(&self, bundle_id: BundleId) -> Result>; - /// Gets transaction metadata for a given transaction hash. - async fn get_transaction_metadata( - &self, - tx_hash: TxHash, - ) -> Result>; -} - -/// Trait for reading user operation events from S3. -#[async_trait] -pub trait UserOpEventS3Reader { - /// Gets the user operation history for a given hash. - async fn get_userop_history(&self, user_op_hash: UserOpHash) -> Result>; -} - -/// S3-backed event reader and writer. -#[derive(Clone, Debug)] -pub struct S3EventReaderWriter { - s3_client: S3Client, - bucket: String, - metrics: Metrics, -} - -impl S3EventReaderWriter { - /// Creates a new S3 event reader/writer. - pub fn new(s3_client: S3Client, bucket: String) -> Self { - Self { - s3_client, - bucket, - metrics: Metrics::default(), - } - } - - async fn update_bundle_history(&self, event: Event) -> Result<()> { - let s3_key = S3Key::Bundle(event.event.bundle_id()).to_string(); - - self.idempotent_write::(&s3_key, |current_history| { - update_bundle_history_transform(current_history, &event) - }) - .await - } - - async fn update_transaction_by_hash_index( - &self, - tx_id: &TransactionId, - bundle_id: BundleId, - ) -> Result<()> { - let s3_key = S3Key::TransactionByHash(tx_id.hash); - let key = s3_key.to_string(); - - self.idempotent_write::(&key, |current_metadata| { - update_transaction_metadata_transform(current_metadata, bundle_id) - }) - .await - } - - async fn update_userop_history(&self, event: UserOpEventWrapper) -> Result<()> { - let s3_key = S3Key::UserOp(event.event.user_op_hash()).to_string(); - - self.idempotent_write::(&s3_key, |current_history| { - update_userop_history_transform(current_history, &event) - }) - .await - } - - async fn idempotent_write(&self, key: &str, mut transform_fn: F) -> Result<()> - where - T: for<'de> Deserialize<'de> + Serialize + Clone + Default + Debug, - F: FnMut(T) -> Option, - { - const MAX_RETRIES: usize = 5; - const BASE_DELAY_MS: u64 = 100; - - for attempt in 0..MAX_RETRIES { - let get_start = Instant::now(); - let (current_value, etag) = self.get_object_with_etag::(key).await?; - self.metrics - .s3_get_duration - .record(get_start.elapsed().as_secs_f64()); - - let value = current_value.unwrap_or_default(); - - match transform_fn(value.clone()) { - Some(new_value) => { - let content = serde_json::to_string(&new_value)?; - - let mut put_request = self - .s3_client - .put_object() - .bucket(&self.bucket) - .key(key) - .body(ByteStream::from(content.into_bytes())); - - if let Some(etag) = etag { - put_request = put_request.if_match(etag); - } else { - put_request = put_request.if_none_match("*"); - } - - let put_start = Instant::now(); - match put_request.send().await { - Ok(_) => { - self.metrics - .s3_put_duration - .record(put_start.elapsed().as_secs_f64()); - info!( - s3_key = %key, - attempt = attempt + 1, - "Successfully wrote object with idempotent write" - ); - return Ok(()); - } - Err(e) => { - self.metrics - .s3_put_duration - .record(put_start.elapsed().as_secs_f64()); - - if attempt < MAX_RETRIES - 1 { - let delay = BASE_DELAY_MS * 2_u64.pow(attempt as u32); - info!( - s3_key = %key, - attempt = attempt + 1, - delay_ms = delay, - error = %e, - "Conflict detected, retrying with backoff" - ); - tokio::time::sleep(tokio::time::Duration::from_millis(delay)).await; - } else { - return Err(anyhow::anyhow!( - "Failed to write after {MAX_RETRIES} attempts: {e}" - )); - } - } - } - } - None => { - self.metrics.s3_writes_skipped.increment(1); - info!( - s3_key = %key, - "Transform function returned None, no write required" - ); - return Ok(()); - } - } - } - - Err(anyhow::anyhow!("Exceeded maximum retry attempts")) - } - - async fn get_object_with_etag(&self, key: &str) -> Result<(Option, Option)> - where - T: for<'de> Deserialize<'de>, - { - match self - .s3_client - .get_object() - .bucket(&self.bucket) - .key(key) - .send() - .await - { - Ok(response) => { - let etag = response.e_tag().map(|s| s.to_string()); - let body = response.body.collect().await?; - let content = String::from_utf8(body.into_bytes().to_vec())?; - let value: T = serde_json::from_str(&content)?; - Ok((Some(value), etag)) - } - Err(e) => match &e { - SdkError::ServiceError(service_err) => match service_err.err() { - GetObjectError::NoSuchKey(_) => Ok((None, None)), - _ => Err(anyhow::anyhow!("Failed to get object: {e}")), - }, - _ => { - let error_string = e.to_string(); - if error_string.contains("NoSuchKey") - || error_string.contains("NotFound") - || error_string.contains("404") - { - Ok((None, None)) - } else { - Err(anyhow::anyhow!("Failed to get object: {e}")) - } - } - }, - } - } -} - -#[async_trait] -impl EventWriter for S3EventReaderWriter { - async fn archive_event(&self, event: Event) -> Result<()> { - let bundle_id = event.event.bundle_id(); - let transaction_ids = event.event.transaction_ids(); - - let bundle_start = Instant::now(); - let bundle_future = self.update_bundle_history(event); - - let tx_start = Instant::now(); - let tx_futures: Vec<_> = transaction_ids - .into_iter() - .map(|tx_id| async move { - self.update_transaction_by_hash_index(&tx_id, bundle_id) - .await - }) - .collect(); - - // Run the bundle and transaction futures concurrently and wait for them to complete - tokio::try_join!(bundle_future, future::try_join_all(tx_futures))?; - - self.metrics - .update_bundle_history_duration - .record(bundle_start.elapsed().as_secs_f64()); - self.metrics - .update_tx_indexes_duration - .record(tx_start.elapsed().as_secs_f64()); - - Ok(()) - } -} - -#[async_trait] -impl BundleEventS3Reader for S3EventReaderWriter { - async fn get_bundle_history(&self, bundle_id: BundleId) -> Result> { - let s3_key = S3Key::Bundle(bundle_id).to_string(); - let (bundle_history, _) = self.get_object_with_etag::(&s3_key).await?; - Ok(bundle_history) - } - - async fn get_transaction_metadata( - &self, - tx_hash: TxHash, - ) -> Result> { - let s3_key = S3Key::TransactionByHash(tx_hash).to_string(); - let (transaction_metadata, _) = self - .get_object_with_etag::(&s3_key) - .await?; - Ok(transaction_metadata) - } -} - -#[async_trait] -impl UserOpEventWriter for S3EventReaderWriter { - async fn archive_userop_event(&self, event: UserOpEventWrapper) -> Result<()> { - self.update_userop_history(event).await - } -} - -#[async_trait] -impl UserOpEventS3Reader for S3EventReaderWriter { - async fn get_userop_history(&self, user_op_hash: UserOpHash) -> Result> { - let s3_key = S3Key::UserOp(user_op_hash).to_string(); - let (userop_history, _) = self.get_object_with_etag::(&s3_key).await?; - Ok(userop_history) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::reader::Event; - use crate::types::{BundleEvent, DropReason, UserOpDropReason, UserOpEvent}; - use alloy_primitives::{Address, B256, TxHash, U256}; - use tips_core::{BundleExtensions, test_utils::create_bundle_from_txn_data}; - use uuid::Uuid; - - fn create_test_event(key: &str, timestamp: i64, bundle_event: BundleEvent) -> Event { - Event { - key: key.to_string(), - timestamp, - event: bundle_event, - } - } - - #[test] - fn test_update_bundle_history_transform_adds_new_event() { - let bundle_history = BundleHistory { history: vec![] }; - let bundle = create_bundle_from_txn_data(); - let bundle_id = Uuid::new_v5(&Uuid::NAMESPACE_OID, bundle.bundle_hash().as_slice()); - let bundle_event = BundleEvent::Received { - bundle_id, - bundle: Box::new(bundle.clone()), - }; - let event = create_test_event("test-key", 1234567890, bundle_event); - - let result = update_bundle_history_transform(bundle_history, &event); - - assert!(result.is_some()); - let bundle_history = result.unwrap(); - assert_eq!(bundle_history.history.len(), 1); - - match &bundle_history.history[0] { - BundleHistoryEvent::Received { - key, - timestamp: ts, - bundle: b, - } => { - assert_eq!(key, "test-key"); - assert_eq!(*ts, 1234567890); - assert_eq!(b.block_number, bundle.block_number); - } - _ => panic!("Expected Created event"), - } - } - - #[test] - fn test_update_bundle_history_transform_skips_duplicate_key() { - let existing_event = BundleHistoryEvent::Received { - key: "duplicate-key".to_string(), - timestamp: 1111111111, - bundle: Box::new(create_bundle_from_txn_data()), - }; - let bundle_history = BundleHistory { - history: vec![existing_event], - }; - - let bundle = create_bundle_from_txn_data(); - let bundle_id = Uuid::new_v5(&Uuid::NAMESPACE_OID, bundle.bundle_hash().as_slice()); - let bundle_event = BundleEvent::Received { - bundle_id, - bundle: Box::new(bundle), - }; - let event = create_test_event("duplicate-key", 1234567890, bundle_event); - - let result = update_bundle_history_transform(bundle_history, &event); - - assert!(result.is_none()); - } - - #[test] - fn test_update_bundle_history_transform_handles_all_event_types() { - let bundle_history = BundleHistory { history: vec![] }; - let bundle = create_bundle_from_txn_data(); - let bundle_id = Uuid::new_v5(&Uuid::NAMESPACE_OID, bundle.bundle_hash().as_slice()); - - let bundle_event = BundleEvent::Received { - bundle_id, - bundle: Box::new(bundle), - }; - let event = create_test_event("test-key", 1234567890, bundle_event); - let result = update_bundle_history_transform(bundle_history.clone(), &event); - assert!(result.is_some()); - - let bundle_event = BundleEvent::Cancelled { bundle_id }; - let event = create_test_event("test-key-2", 1234567890, bundle_event); - let result = update_bundle_history_transform(bundle_history.clone(), &event); - assert!(result.is_some()); - - let bundle_event = BundleEvent::BuilderIncluded { - bundle_id, - builder: "test-builder".to_string(), - block_number: 12345, - flashblock_index: 1, - }; - let event = create_test_event("test-key-3", 1234567890, bundle_event); - let result = update_bundle_history_transform(bundle_history.clone(), &event); - assert!(result.is_some()); - - let bundle_event = BundleEvent::BlockIncluded { - bundle_id, - block_number: 12345, - block_hash: TxHash::from([1u8; 32]), - }; - let event = create_test_event("test-key-4", 1234567890, bundle_event); - let result = update_bundle_history_transform(bundle_history.clone(), &event); - assert!(result.is_some()); - - let bundle_event = BundleEvent::Dropped { - bundle_id, - reason: DropReason::TimedOut, - }; - let event = create_test_event("test-key-5", 1234567890, bundle_event); - let result = update_bundle_history_transform(bundle_history, &event); - assert!(result.is_some()); - } - - #[test] - fn test_update_transaction_metadata_transform_adds_new_bundle() { - let metadata = TransactionMetadata { bundle_ids: vec![] }; - let bundle = create_bundle_from_txn_data(); - let bundle_id = Uuid::new_v5(&Uuid::NAMESPACE_OID, bundle.bundle_hash().as_slice()); - - let result = update_transaction_metadata_transform(metadata, bundle_id); - - assert!(result.is_some()); - let metadata = result.unwrap(); - assert_eq!(metadata.bundle_ids.len(), 1); - assert_eq!(metadata.bundle_ids[0], bundle_id); - } - - #[test] - fn test_update_transaction_metadata_transform_skips_existing_bundle() { - let bundle = create_bundle_from_txn_data(); - let bundle_id = Uuid::new_v5(&Uuid::NAMESPACE_OID, bundle.bundle_hash().as_slice()); - let metadata = TransactionMetadata { - bundle_ids: vec![bundle_id], - }; - - let result = update_transaction_metadata_transform(metadata, bundle_id); - - assert!(result.is_none()); - } - - #[test] - fn test_update_transaction_metadata_transform_adds_to_existing_bundles() { - // Some different, dummy bundle IDs since create_bundle_from_txn_data() returns the same bundle ID - // Even if the same txn is contained across multiple bundles, the bundle ID will be different since the - // UUID is based on the bundle hash. - let existing_bundle_id = Uuid::parse_str("550e8400-e29b-41d4-a716-446655440000").unwrap(); - let new_bundle_id = Uuid::parse_str("6ba7b810-9dad-11d1-80b4-00c04fd430c8").unwrap(); - - let metadata = TransactionMetadata { - bundle_ids: vec![existing_bundle_id], - }; - - let result = update_transaction_metadata_transform(metadata, new_bundle_id); - - assert!(result.is_some()); - let metadata = result.unwrap(); - assert_eq!(metadata.bundle_ids.len(), 2); - assert!(metadata.bundle_ids.contains(&existing_bundle_id)); - assert!(metadata.bundle_ids.contains(&new_bundle_id)); - } - - fn create_test_userop_event( - key: &str, - timestamp: i64, - userop_event: UserOpEvent, - ) -> UserOpEventWrapper { - UserOpEventWrapper { - key: key.to_string(), - timestamp, - event: userop_event, - } - } - - #[test] - fn test_s3_key_userop_display() { - let hash = B256::from([1u8; 32]); - let key = S3Key::UserOp(hash); - let key_str = key.to_string(); - assert!(key_str.starts_with("userops/")); - assert!(key_str.contains(&format!("{hash}"))); - } - - #[test] - fn test_update_userop_history_transform_adds_new_event() { - let userop_history = UserOpHistory { history: vec![] }; - let user_op_hash = B256::from([1u8; 32]); - let sender = Address::from([2u8; 20]); - let entry_point = Address::from([3u8; 20]); - let nonce = U256::from(1); - - let userop_event = UserOpEvent::AddedToMempool { - user_op_hash, - sender, - entry_point, - nonce, - }; - let event = create_test_userop_event("test-key", 1234567890, userop_event); - - let result = update_userop_history_transform(userop_history, &event); - - assert!(result.is_some()); - let history = result.unwrap(); - assert_eq!(history.history.len(), 1); - - match &history.history[0] { - UserOpHistoryEvent::AddedToMempool { - key, - timestamp: ts, - sender: s, - entry_point: ep, - nonce: n, - } => { - assert_eq!(key, "test-key"); - assert_eq!(*ts, 1234567890); - assert_eq!(*s, sender); - assert_eq!(*ep, entry_point); - assert_eq!(*n, nonce); - } - _ => panic!("Expected AddedToMempool event"), - } - } - - #[test] - fn test_update_userop_history_transform_skips_duplicate_key() { - let user_op_hash = B256::from([1u8; 32]); - let sender = Address::from([2u8; 20]); - let entry_point = Address::from([3u8; 20]); - let nonce = U256::from(1); - - let existing_event = UserOpHistoryEvent::AddedToMempool { - key: "duplicate-key".to_string(), - timestamp: 1111111111, - sender, - entry_point, - nonce, - }; - let userop_history = UserOpHistory { - history: vec![existing_event], - }; - - let userop_event = UserOpEvent::AddedToMempool { - user_op_hash, - sender, - entry_point, - nonce, - }; - let event = create_test_userop_event("duplicate-key", 1234567890, userop_event); - - let result = update_userop_history_transform(userop_history, &event); - - assert!(result.is_none()); - } - - #[test] - fn test_update_userop_history_transform_handles_dropped_event() { - let userop_history = UserOpHistory { history: vec![] }; - let user_op_hash = B256::from([1u8; 32]); - let reason = UserOpDropReason::Expired; - - let userop_event = UserOpEvent::Dropped { - user_op_hash, - reason: reason.clone(), - }; - let event = create_test_userop_event("dropped-key", 1234567890, userop_event); - - let result = update_userop_history_transform(userop_history, &event); - - assert!(result.is_some()); - let history = result.unwrap(); - assert_eq!(history.history.len(), 1); - - match &history.history[0] { - UserOpHistoryEvent::Dropped { - key, - timestamp, - reason: r, - } => { - assert_eq!(key, "dropped-key"); - assert_eq!(*timestamp, 1234567890); - match r { - UserOpDropReason::Expired => {} - _ => panic!("Expected Expired reason"), - } - } - _ => panic!("Expected Dropped event"), - } - } - - #[test] - fn test_update_userop_history_transform_handles_included_event() { - let userop_history = UserOpHistory { history: vec![] }; - let user_op_hash = B256::from([1u8; 32]); - let tx_hash = TxHash::from([4u8; 32]); - let block_number = 12345u64; - - let userop_event = UserOpEvent::Included { - user_op_hash, - block_number, - tx_hash, - }; - let event = create_test_userop_event("included-key", 1234567890, userop_event); - - let result = update_userop_history_transform(userop_history, &event); - - assert!(result.is_some()); - let history = result.unwrap(); - assert_eq!(history.history.len(), 1); - - match &history.history[0] { - UserOpHistoryEvent::Included { - key, - timestamp, - block_number: bn, - tx_hash: th, - } => { - assert_eq!(key, "included-key"); - assert_eq!(*timestamp, 1234567890); - assert_eq!(*bn, 12345); - assert_eq!(*th, tx_hash); - } - _ => panic!("Expected Included event"), - } - } - - #[test] - fn test_update_userop_history_transform_handles_all_event_types() { - let userop_history = UserOpHistory { history: vec![] }; - let user_op_hash = B256::from([1u8; 32]); - let sender = Address::from([2u8; 20]); - let entry_point = Address::from([3u8; 20]); - let nonce = U256::from(1); - - let userop_event = UserOpEvent::AddedToMempool { - user_op_hash, - sender, - entry_point, - nonce, - }; - let event = create_test_userop_event("key-1", 1234567890, userop_event); - let result = update_userop_history_transform(userop_history.clone(), &event); - assert!(result.is_some()); - - let userop_event = UserOpEvent::Dropped { - user_op_hash, - reason: UserOpDropReason::Invalid("test error".to_string()), - }; - let event = create_test_userop_event("key-2", 1234567891, userop_event); - let result = update_userop_history_transform(userop_history.clone(), &event); - assert!(result.is_some()); - - let userop_event = UserOpEvent::Included { - user_op_hash, - block_number: 12345, - tx_hash: TxHash::from([4u8; 32]), - }; - let event = create_test_userop_event("key-3", 1234567892, userop_event); - let result = update_userop_history_transform(userop_history, &event); - assert!(result.is_some()); - } - - #[test] - fn test_userop_history_event_key_accessor() { - let sender = Address::from([2u8; 20]); - let entry_point = Address::from([3u8; 20]); - let nonce = U256::from(1); - - let event1 = UserOpHistoryEvent::AddedToMempool { - key: "key-1".to_string(), - timestamp: 1234567890, - sender, - entry_point, - nonce, - }; - assert_eq!(event1.key(), "key-1"); - - let event2 = UserOpHistoryEvent::Dropped { - key: "key-2".to_string(), - timestamp: 1234567890, - reason: UserOpDropReason::Expired, - }; - assert_eq!(event2.key(), "key-2"); - - let event3 = UserOpHistoryEvent::Included { - key: "key-3".to_string(), - timestamp: 1234567890, - block_number: 12345, - tx_hash: TxHash::from([4u8; 32]), - }; - assert_eq!(event3.key(), "key-3"); - } - - #[test] - fn test_userop_history_serialization() { - let sender = Address::from([2u8; 20]); - let entry_point = Address::from([3u8; 20]); - let nonce = U256::from(1); - - let history = UserOpHistory { - history: vec![UserOpHistoryEvent::AddedToMempool { - key: "test-key".to_string(), - timestamp: 1234567890, - sender, - entry_point, - nonce, - }], - }; - - let json = serde_json::to_string(&history).unwrap(); - let deserialized: UserOpHistory = serde_json::from_str(&json).unwrap(); - - assert_eq!(deserialized.history.len(), 1); - assert_eq!(deserialized.history[0].key(), "test-key"); - } -} diff --git a/crates/audit/src/types.rs b/crates/audit/src/types.rs deleted file mode 100644 index 2dd0291d..00000000 --- a/crates/audit/src/types.rs +++ /dev/null @@ -1,331 +0,0 @@ -use alloy_consensus::transaction::{SignerRecoverable, Transaction as ConsensusTransaction}; -use alloy_primitives::{Address, B256, TxHash, U256}; -use bytes::Bytes; -use serde::{Deserialize, Serialize}; -use tips_core::AcceptedBundle; -use uuid::Uuid; - -/// Unique identifier for a transaction. -#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] -pub struct TransactionId { - /// The sender address. - pub sender: Address, - /// The transaction nonce. - pub nonce: U256, - /// The transaction hash. - pub hash: TxHash, -} - -/// Unique identifier for a bundle. -pub type BundleId = Uuid; - -/// Reason a bundle was dropped. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub enum DropReason { - /// Bundle timed out. - TimedOut, - /// Bundle transaction reverted. - Reverted, -} - -/// A transaction with its data. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct Transaction { - /// Transaction identifier. - pub id: TransactionId, - /// Raw transaction data. - pub data: Bytes, -} - -/// Hash of a user operation. -pub type UserOpHash = B256; - -/// Reason a user operation was dropped. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub enum UserOpDropReason { - /// User operation was invalid. - Invalid(String), - /// User operation expired. - Expired, - /// Replaced by a higher fee user operation. - ReplacedByHigherFee, -} - -/// Bundle lifecycle event. -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(tag = "event", content = "data")] -pub enum BundleEvent { - /// Bundle was received. - Received { - /// Bundle identifier. - bundle_id: BundleId, - /// The accepted bundle. - bundle: Box, - }, - /// Bundle was cancelled. - Cancelled { - /// Bundle identifier. - bundle_id: BundleId, - }, - /// Bundle was included by a builder. - BuilderIncluded { - /// Bundle identifier. - bundle_id: BundleId, - /// Builder identifier. - builder: String, - /// Block number. - block_number: u64, - /// Flashblock index. - flashblock_index: u64, - }, - /// Bundle was included in a block. - BlockIncluded { - /// Bundle identifier. - bundle_id: BundleId, - /// Block number. - block_number: u64, - /// Block hash. - block_hash: TxHash, - }, - /// Bundle was dropped. - Dropped { - /// Bundle identifier. - bundle_id: BundleId, - /// Drop reason. - reason: DropReason, - }, -} - -impl BundleEvent { - /// Returns the bundle ID for this event. - pub const fn bundle_id(&self) -> BundleId { - match self { - Self::Received { bundle_id, .. } => *bundle_id, - Self::Cancelled { bundle_id, .. } => *bundle_id, - Self::BuilderIncluded { bundle_id, .. } => *bundle_id, - Self::BlockIncluded { bundle_id, .. } => *bundle_id, - Self::Dropped { bundle_id, .. } => *bundle_id, - } - } - - /// Returns transaction IDs from this event (only for Received events). - pub fn transaction_ids(&self) -> Vec { - match self { - Self::Received { bundle, .. } => bundle - .txs - .iter() - .filter_map(|envelope| { - envelope.recover_signer().ok().map(|sender| TransactionId { - sender, - nonce: U256::from(envelope.nonce()), - hash: *envelope.hash(), - }) - }) - .collect(), - Self::Cancelled { .. } => vec![], - Self::BuilderIncluded { .. } => vec![], - Self::BlockIncluded { .. } => vec![], - Self::Dropped { .. } => vec![], - } - } - - /// Generates a unique event key for this event. - pub fn generate_event_key(&self) -> String { - match self { - Self::BlockIncluded { - bundle_id, - block_hash, - .. - } => { - format!("{bundle_id}-{block_hash}") - } - _ => { - format!( - "{}-{}", - self.bundle_id(), - Uuid::new_v5(&Uuid::NAMESPACE_OID, self.bundle_id().as_bytes()) - ) - } - } - } -} - -/// User operation lifecycle event. -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(tag = "event", content = "data")] -pub enum UserOpEvent { - /// User operation was added to the mempool. - AddedToMempool { - /// Hash of the user operation. - user_op_hash: UserOpHash, - /// Sender address. - sender: Address, - /// Entry point address. - entry_point: Address, - /// Nonce. - nonce: U256, - }, - /// User operation was dropped. - Dropped { - /// Hash of the user operation. - user_op_hash: UserOpHash, - /// Reason for dropping. - reason: UserOpDropReason, - }, - /// User operation was included in a block. - Included { - /// Hash of the user operation. - user_op_hash: UserOpHash, - /// Block number. - block_number: u64, - /// Transaction hash. - tx_hash: TxHash, - }, -} - -impl UserOpEvent { - /// Returns the user operation hash for this event. - pub const fn user_op_hash(&self) -> UserOpHash { - match self { - Self::AddedToMempool { user_op_hash, .. } => *user_op_hash, - Self::Dropped { user_op_hash, .. } => *user_op_hash, - Self::Included { user_op_hash, .. } => *user_op_hash, - } - } - - /// Generates a unique event key for this event. - pub fn generate_event_key(&self) -> String { - match self { - Self::Included { - user_op_hash, - tx_hash, - .. - } => { - format!("{user_op_hash}-{tx_hash}") - } - _ => { - format!( - "{}-{}", - self.user_op_hash(), - Uuid::new_v5(&Uuid::NAMESPACE_OID, self.user_op_hash().as_slice()) - ) - } - } - } -} - -#[cfg(test)] -mod user_op_event_tests { - use super::*; - use alloy_primitives::{address, b256}; - - fn create_test_user_op_hash() -> UserOpHash { - b256!("1111111111111111111111111111111111111111111111111111111111111111") - } - - #[test] - fn test_user_op_event_added_to_mempool_serialization() { - let event = UserOpEvent::AddedToMempool { - user_op_hash: create_test_user_op_hash(), - sender: address!("2222222222222222222222222222222222222222"), - entry_point: address!("0000000071727De22E5E9d8BAf0edAc6f37da032"), - nonce: U256::from(1), - }; - - let json = serde_json::to_string(&event).unwrap(); - assert!(json.contains("\"event\":\"AddedToMempool\"")); - - let deserialized: UserOpEvent = serde_json::from_str(&json).unwrap(); - assert_eq!(event.user_op_hash(), deserialized.user_op_hash()); - } - - #[test] - fn test_user_op_event_dropped_serialization() { - let event = UserOpEvent::Dropped { - user_op_hash: create_test_user_op_hash(), - reason: UserOpDropReason::Invalid("gas too low".to_string()), - }; - - let json = serde_json::to_string(&event).unwrap(); - assert!(json.contains("\"event\":\"Dropped\"")); - assert!(json.contains("gas too low")); - - let deserialized: UserOpEvent = serde_json::from_str(&json).unwrap(); - assert_eq!(event.user_op_hash(), deserialized.user_op_hash()); - } - - #[test] - fn test_user_op_event_included_serialization() { - let event = UserOpEvent::Included { - user_op_hash: create_test_user_op_hash(), - block_number: 12345, - tx_hash: b256!("3333333333333333333333333333333333333333333333333333333333333333"), - }; - - let json = serde_json::to_string(&event).unwrap(); - assert!(json.contains("\"event\":\"Included\"")); - assert!(json.contains("\"block_number\":12345")); - - let deserialized: UserOpEvent = serde_json::from_str(&json).unwrap(); - assert_eq!(event.user_op_hash(), deserialized.user_op_hash()); - } - - #[test] - fn test_user_op_hash_accessor() { - let hash = create_test_user_op_hash(); - - let added = UserOpEvent::AddedToMempool { - user_op_hash: hash, - sender: address!("2222222222222222222222222222222222222222"), - entry_point: address!("0000000071727De22E5E9d8BAf0edAc6f37da032"), - nonce: U256::from(1), - }; - assert_eq!(added.user_op_hash(), hash); - - let dropped = UserOpEvent::Dropped { - user_op_hash: hash, - reason: UserOpDropReason::Expired, - }; - assert_eq!(dropped.user_op_hash(), hash); - - let included = UserOpEvent::Included { - user_op_hash: hash, - block_number: 100, - tx_hash: b256!("4444444444444444444444444444444444444444444444444444444444444444"), - }; - assert_eq!(included.user_op_hash(), hash); - } - - #[test] - fn test_generate_event_key_included() { - let user_op_hash = - b256!("1111111111111111111111111111111111111111111111111111111111111111"); - let tx_hash = b256!("2222222222222222222222222222222222222222222222222222222222222222"); - - let event = UserOpEvent::Included { - user_op_hash, - block_number: 100, - tx_hash, - }; - - let key = event.generate_event_key(); - assert!(key.contains(&format!("{user_op_hash}"))); - assert!(key.contains(&format!("{tx_hash}"))); - } - - #[test] - fn test_user_op_drop_reason_variants() { - let invalid = UserOpDropReason::Invalid("test reason".to_string()); - let json = serde_json::to_string(&invalid).unwrap(); - assert!(json.contains("Invalid")); - assert!(json.contains("test reason")); - - let expired = UserOpDropReason::Expired; - let json = serde_json::to_string(&expired).unwrap(); - assert!(json.contains("Expired")); - - let replaced = UserOpDropReason::ReplacedByHigherFee; - let json = serde_json::to_string(&replaced).unwrap(); - assert!(json.contains("ReplacedByHigherFee")); - } -} diff --git a/crates/audit/tests/common/mod.rs b/crates/audit/tests/common/mod.rs deleted file mode 100644 index 6db10f25..00000000 --- a/crates/audit/tests/common/mod.rs +++ /dev/null @@ -1,81 +0,0 @@ -use rdkafka::producer::FutureProducer; -use rdkafka::{ClientConfig, consumer::StreamConsumer}; -use testcontainers::runners::AsyncRunner; -use testcontainers_modules::{kafka, kafka::Kafka, minio::MinIO}; -use uuid::Uuid; - -pub struct TestHarness { - pub s3_client: aws_sdk_s3::Client, - pub bucket_name: String, - #[allow(dead_code)] // TODO is read - pub kafka_producer: FutureProducer, - #[allow(dead_code)] // TODO is read - pub kafka_consumer: StreamConsumer, - _minio_container: testcontainers::ContainerAsync, - _kafka_container: testcontainers::ContainerAsync, -} - -impl TestHarness { - pub async fn new() -> Result> { - let minio_container = MinIO::default().start().await?; - let s3_port = minio_container.get_host_port_ipv4(9000).await?; - let s3_endpoint = format!("http://127.0.0.1:{s3_port}"); - - let config = aws_config::defaults(aws_config::BehaviorVersion::latest()) - .region("us-east-1") - .endpoint_url(&s3_endpoint) - .credentials_provider(aws_sdk_s3::config::Credentials::new( - "minioadmin", - "minioadmin", - None, - None, - "test", - )) - .load() - .await; - - let s3_client = aws_sdk_s3::Client::new(&config); - let bucket_name = format!( - "test-bucket-{}", - Uuid::new_v5(&Uuid::NAMESPACE_OID, bundle.bundle_hash().as_slice()) - ); - - s3_client - .create_bucket() - .bucket(&bucket_name) - .send() - .await?; - - let kafka_container = Kafka::default().start().await?; - let bootstrap_servers = format!( - "127.0.0.1:{}", - kafka_container - .get_host_port_ipv4(kafka::KAFKA_PORT) - .await? - ); - - let kafka_producer = ClientConfig::new() - .set("bootstrap.servers", &bootstrap_servers) - .set("message.timeout.ms", "5000") - .create::() - .expect("Failed to create Kafka FutureProducer"); - - let kafka_consumer = ClientConfig::new() - .set("group.id", "testcontainer-rs") - .set("bootstrap.servers", &bootstrap_servers) - .set("session.timeout.ms", "6000") - .set("enable.auto.commit", "false") - .set("auto.offset.reset", "earliest") - .create::() - .expect("Failed to create Kafka StreamConsumer"); - - Ok(TestHarness { - s3_client, - bucket_name, - kafka_producer, - kafka_consumer, - _minio_container: minio_container, - _kafka_container: kafka_container, - }) - } -} diff --git a/crates/audit/tests/integration_tests.rs b/crates/audit/tests/integration_tests.rs deleted file mode 100644 index 5b23fd02..00000000 --- a/crates/audit/tests/integration_tests.rs +++ /dev/null @@ -1,128 +0,0 @@ -use alloy_primitives::{Address, B256, U256}; -use std::time::Duration; -use tips_audit_lib::{ - KafkaAuditArchiver, KafkaAuditLogReader, KafkaUserOpAuditLogReader, UserOpEventReader, - publisher::{ - BundleEventPublisher, KafkaBundleEventPublisher, KafkaUserOpEventPublisher, - UserOpEventPublisher, - }, - storage::{BundleEventS3Reader, S3EventReaderWriter}, - types::{BundleEvent, DropReason, UserOpEvent}, -}; -use tips_core::{BundleExtensions, test_utils::create_bundle_from_txn_data}; -use uuid::Uuid; -mod common; -use common::TestHarness; - -#[tokio::test] -#[ignore = "TODO doesn't appear to work with minio, should test against a real S3 bucket"] -async fn test_kafka_publisher_s3_archiver_integration() --> Result<(), Box> { - let harness = TestHarness::new().await?; - let topic = "test-mempool-events"; - - let s3_writer = - S3EventReaderWriter::new(harness.s3_client.clone(), harness.bucket_name.clone()); - - let bundle = create_bundle_from_txn_data(); - let test_bundle_id = Uuid::new_v5(&Uuid::NAMESPACE_OID, bundle.bundle_hash().as_slice()); - let test_events = [ - BundleEvent::Received { - bundle_id: test_bundle_id, - bundle: Box::new(bundle.clone()), - }, - BundleEvent::Dropped { - bundle_id: test_bundle_id, - reason: DropReason::TimedOut, - }, - ]; - - let publisher = KafkaBundleEventPublisher::new(harness.kafka_producer, topic.to_string()); - - for event in test_events.iter() { - publisher.publish(event.clone()).await?; - } - - let mut consumer = KafkaAuditArchiver::new( - KafkaAuditLogReader::new(harness.kafka_consumer, topic.to_string())?, - s3_writer.clone(), - 1, - 100, - ); - - tokio::spawn(async move { - consumer.run().await.expect("error running consumer"); - }); - - // Wait for the messages to be received - let mut counter = 0; - loop { - counter += 1; - if counter > 10 { - assert!(false, "unable to complete archiving within the deadline"); - } - - tokio::time::sleep(Duration::from_secs(1)).await; - let bundle_history = s3_writer.get_bundle_history(test_bundle_id).await?; - - if bundle_history.is_some() { - let history = bundle_history.unwrap(); - if history.history.len() != test_events.len() { - continue; - } else { - break; - } - } else { - continue; - } - } - - Ok(()) -} - -#[tokio::test] -async fn test_userop_kafka_publisher_reader_integration() --> Result<(), Box> { - let harness = TestHarness::new().await?; - let topic = "test-userop-events"; - - let test_user_op_hash = B256::from_slice(&[1u8; 32]); - let test_sender = Address::from_slice(&[2u8; 20]); - let test_entry_point = Address::from_slice(&[3u8; 20]); - let test_nonce = U256::from(42); - - let test_event = UserOpEvent::AddedToMempool { - user_op_hash: test_user_op_hash, - sender: test_sender, - entry_point: test_entry_point, - nonce: test_nonce, - }; - - let publisher = KafkaUserOpEventPublisher::new(harness.kafka_producer, topic.to_string()); - publisher.publish(test_event.clone()).await?; - - let mut reader = KafkaUserOpAuditLogReader::new(harness.kafka_consumer, topic.to_string())?; - - let received = tokio::time::timeout(Duration::from_secs(10), reader.read_event()).await??; - - assert_eq!(received.event.user_op_hash(), test_user_op_hash); - - match received.event { - UserOpEvent::AddedToMempool { - user_op_hash, - sender, - entry_point, - nonce, - } => { - assert_eq!(user_op_hash, test_user_op_hash); - assert_eq!(sender, test_sender); - assert_eq!(entry_point, test_entry_point); - assert_eq!(nonce, test_nonce); - } - _ => panic!("Expected AddedToMempool event"), - } - - reader.commit().await?; - - Ok(()) -} diff --git a/crates/audit/tests/s3_test.rs b/crates/audit/tests/s3_test.rs deleted file mode 100644 index d555bc0b..00000000 --- a/crates/audit/tests/s3_test.rs +++ /dev/null @@ -1,483 +0,0 @@ -use alloy_primitives::{Address, B256, TxHash, U256}; -use std::sync::Arc; -use tips_audit_lib::{ - reader::Event, - storage::{ - BundleEventS3Reader, EventWriter, S3EventReaderWriter, UserOpEventS3Reader, - UserOpEventWrapper, UserOpEventWriter, - }, - types::{BundleEvent, UserOpDropReason, UserOpEvent}, -}; -use tokio::task::JoinSet; -use uuid::Uuid; - -mod common; -use common::TestHarness; -use tips_core::{ - BundleExtensions, - test_utils::{TXN_HASH, create_bundle_from_txn_data}, -}; - -fn create_test_event(key: &str, timestamp: i64, bundle_event: BundleEvent) -> Event { - Event { - key: key.to_string(), - timestamp, - event: bundle_event, - } -} - -#[tokio::test] -async fn test_event_write_and_read() -> Result<(), Box> { - let harness = TestHarness::new().await?; - let writer = S3EventReaderWriter::new(harness.s3_client.clone(), harness.bucket_name.clone()); - - let bundle = create_bundle_from_txn_data(); - let bundle_id = Uuid::new_v5(&Uuid::NAMESPACE_OID, bundle.bundle_hash().as_slice()); - let event = create_test_event( - "test-key-1", - 1234567890, - BundleEvent::Received { - bundle_id, - bundle: Box::new(bundle.clone()), - }, - ); - - writer.archive_event(event).await?; - - let bundle_history = writer.get_bundle_history(bundle_id).await?; - assert!(bundle_history.is_some()); - - let history = bundle_history.unwrap(); - assert_eq!(history.history.len(), 1); - assert_eq!(history.history[0].key(), "test-key-1"); - - let metadata = writer.get_transaction_metadata(TXN_HASH).await?; - assert!(metadata.is_some()); - - if let Some(metadata) = metadata { - assert!(metadata.bundle_ids.contains(&bundle_id)); - } - - let bundle_id_two = Uuid::new_v5(&Uuid::NAMESPACE_OID, bundle.bundle_hash().as_slice()); - let bundle = create_bundle_from_txn_data(); - let event = create_test_event( - "test-key-2", - 1234567890, - BundleEvent::Received { - bundle_id: bundle_id_two, - bundle: Box::new(bundle.clone()), - }, - ); - - writer.archive_event(event).await?; - - let metadata = writer.get_transaction_metadata(TXN_HASH).await?; - assert!(metadata.is_some()); - - if let Some(metadata) = metadata { - assert!(metadata.bundle_ids.contains(&bundle_id)); - assert!(metadata.bundle_ids.contains(&bundle_id_two)); - } - - Ok(()) -} - -#[tokio::test] -async fn test_events_appended() -> Result<(), Box> { - let harness = TestHarness::new().await?; - let writer = S3EventReaderWriter::new(harness.s3_client.clone(), harness.bucket_name.clone()); - - let bundle = create_bundle_from_txn_data(); - let bundle_id = Uuid::new_v5(&Uuid::NAMESPACE_OID, bundle.bundle_hash().as_slice()); - - let events = [ - create_test_event( - "test-key-1", - 1234567890, - BundleEvent::Received { - bundle_id, - bundle: Box::new(bundle.clone()), - }, - ), - create_test_event( - "test-key-2", - 1234567891, - BundleEvent::Cancelled { bundle_id }, - ), - ]; - - for (idx, event) in events.iter().enumerate() { - writer.archive_event(event.clone()).await?; - - let bundle_history = writer.get_bundle_history(bundle_id).await?; - assert!(bundle_history.is_some()); - - let history = bundle_history.unwrap(); - assert_eq!(history.history.len(), idx + 1); - - let keys: Vec = history - .history - .iter() - .map(|e| e.key().to_string()) - .collect(); - assert_eq!( - keys, - events - .iter() - .map(|e| e.key.clone()) - .take(idx + 1) - .collect::>() - ); - } - - Ok(()) -} - -#[tokio::test] -async fn test_event_deduplication() -> Result<(), Box> { - let harness = TestHarness::new().await?; - let writer = S3EventReaderWriter::new(harness.s3_client.clone(), harness.bucket_name.clone()); - - let bundle = create_bundle_from_txn_data(); - let bundle_id = Uuid::new_v5(&Uuid::NAMESPACE_OID, bundle.bundle_hash().as_slice()); - let bundle = create_bundle_from_txn_data(); - let event = create_test_event( - "duplicate-key", - 1234567890, - BundleEvent::Received { - bundle_id, - bundle: Box::new(bundle.clone()), - }, - ); - - writer.archive_event(event.clone()).await?; - writer.archive_event(event).await?; - - let bundle_history = writer.get_bundle_history(bundle_id).await?; - assert!(bundle_history.is_some()); - - let history = bundle_history.unwrap(); - assert_eq!(history.history.len(), 1); - assert_eq!(history.history[0].key(), "duplicate-key"); - - Ok(()) -} - -#[tokio::test] -async fn test_nonexistent_data() -> Result<(), Box> { - let harness = TestHarness::new().await?; - let writer = S3EventReaderWriter::new(harness.s3_client.clone(), harness.bucket_name.clone()); - - let bundle = create_bundle_from_txn_data(); - let nonexistent_bundle_id = Uuid::new_v5(&Uuid::NAMESPACE_OID, bundle.bundle_hash().as_slice()); - let bundle_history = writer.get_bundle_history(nonexistent_bundle_id).await?; - assert!(bundle_history.is_none()); - - let nonexistent_tx_hash = TxHash::from([255u8; 32]); - let metadata = writer.get_transaction_metadata(nonexistent_tx_hash).await?; - assert!(metadata.is_none()); - - Ok(()) -} - -#[tokio::test] -#[ignore = "TODO doesn't appear to work with minio, should test against a real S3 bucket"] -async fn test_concurrent_writes_for_bundle() -> Result<(), Box> -{ - let harness = TestHarness::new().await?; - let writer = Arc::new(S3EventReaderWriter::new( - harness.s3_client.clone(), - harness.bucket_name.clone(), - )); - - let bundle = create_bundle_from_txn_data(); - let bundle_id = Uuid::new_v5(&Uuid::NAMESPACE_OID, bundle.bundle_hash().as_slice()); - - let event = create_test_event( - "hello-dan", - 1234567889i64, - BundleEvent::Received { - bundle_id, - bundle: Box::new(bundle.clone()), - }, - ); - - writer.archive_event(event.clone()).await?; - - let mut join_set: JoinSet>> = - JoinSet::new(); - - for i in 0..4 { - let writer_clone = writer.clone(); - let key = if i % 4 == 0 { - "shared-key".to_string() - } else { - format!("unique-key-{i}") - }; - - let event = create_test_event( - &key, - 1234567890 + i as i64, - BundleEvent::Received { - bundle_id, - bundle: Box::new(bundle.clone()), - }, - ); - - join_set.spawn(async move { writer_clone.archive_event(event.clone()).await }); - } - - let tasks = join_set.join_all().await; - assert_eq!(tasks.len(), 4); - for t in tasks.iter() { - assert!(t.is_ok()); - } - - let bundle_history = writer.get_bundle_history(bundle_id).await?; - assert!(bundle_history.is_some()); - - let history = bundle_history.unwrap(); - - let shared_count = history - .history - .iter() - .filter(|e| e.key() == "shared-key") - .count(); - assert_eq!(shared_count, 1); - - let unique_count = history - .history - .iter() - .filter(|e| e.key().starts_with("unique-key-")) - .count(); - assert_eq!(unique_count, 3); - - assert_eq!(history.history.len(), 4); - - Ok(()) -} - -fn create_test_userop_event( - key: &str, - timestamp: i64, - userop_event: UserOpEvent, -) -> UserOpEventWrapper { - UserOpEventWrapper { - key: key.to_string(), - timestamp, - event: userop_event, - } -} - -#[tokio::test] -async fn test_userop_event_write_and_read() -> Result<(), Box> -{ - let harness = TestHarness::new().await?; - let writer = S3EventReaderWriter::new(harness.s3_client.clone(), harness.bucket_name.clone()); - - let user_op_hash = B256::from([1u8; 32]); - let sender = Address::from([2u8; 20]); - let entry_point = Address::from([3u8; 20]); - let nonce = U256::from(1); - - let event = create_test_userop_event( - "test-userop-key-1", - 1234567890, - UserOpEvent::AddedToMempool { - user_op_hash, - sender, - entry_point, - nonce, - }, - ); - - writer.archive_userop_event(event).await?; - - let userop_history = writer.get_userop_history(user_op_hash).await?; - assert!(userop_history.is_some()); - - let history = userop_history.unwrap(); - assert_eq!(history.history.len(), 1); - assert_eq!(history.history[0].key(), "test-userop-key-1"); - - Ok(()) -} - -#[tokio::test] -async fn test_userop_events_appended() -> Result<(), Box> { - let harness = TestHarness::new().await?; - let writer = S3EventReaderWriter::new(harness.s3_client.clone(), harness.bucket_name.clone()); - - let user_op_hash = B256::from([10u8; 32]); - let sender = Address::from([11u8; 20]); - let entry_point = Address::from([12u8; 20]); - let nonce = U256::from(1); - let tx_hash = TxHash::from([13u8; 32]); - - let events = [ - create_test_userop_event( - "userop-key-1", - 1234567890, - UserOpEvent::AddedToMempool { - user_op_hash, - sender, - entry_point, - nonce, - }, - ), - create_test_userop_event( - "userop-key-2", - 1234567891, - UserOpEvent::Included { - user_op_hash, - block_number: 12345, - tx_hash, - }, - ), - ]; - - for (idx, event) in events.iter().enumerate() { - writer.archive_userop_event(event.clone()).await?; - - let userop_history = writer.get_userop_history(user_op_hash).await?; - assert!(userop_history.is_some()); - - let history = userop_history.unwrap(); - assert_eq!(history.history.len(), idx + 1); - - let keys: Vec = history - .history - .iter() - .map(|e| e.key().to_string()) - .collect(); - assert_eq!( - keys, - events - .iter() - .map(|e| e.key.clone()) - .take(idx + 1) - .collect::>() - ); - } - - Ok(()) -} - -#[tokio::test] -async fn test_userop_event_deduplication() -> Result<(), Box> { - let harness = TestHarness::new().await?; - let writer = S3EventReaderWriter::new(harness.s3_client.clone(), harness.bucket_name.clone()); - - let user_op_hash = B256::from([20u8; 32]); - let sender = Address::from([21u8; 20]); - let entry_point = Address::from([22u8; 20]); - let nonce = U256::from(1); - - let event = create_test_userop_event( - "duplicate-userop-key", - 1234567890, - UserOpEvent::AddedToMempool { - user_op_hash, - sender, - entry_point, - nonce, - }, - ); - - writer.archive_userop_event(event.clone()).await?; - writer.archive_userop_event(event).await?; - - let userop_history = writer.get_userop_history(user_op_hash).await?; - assert!(userop_history.is_some()); - - let history = userop_history.unwrap(); - assert_eq!(history.history.len(), 1); - assert_eq!(history.history[0].key(), "duplicate-userop-key"); - - Ok(()) -} - -#[tokio::test] -async fn test_userop_nonexistent_returns_none() --> Result<(), Box> { - let harness = TestHarness::new().await?; - let writer = S3EventReaderWriter::new(harness.s3_client.clone(), harness.bucket_name.clone()); - - let nonexistent_hash = B256::from([255u8; 32]); - let userop_history = writer.get_userop_history(nonexistent_hash).await?; - assert!(userop_history.is_none()); - - Ok(()) -} - -#[tokio::test] -async fn test_userop_all_event_types() -> Result<(), Box> { - let harness = TestHarness::new().await?; - let writer = S3EventReaderWriter::new(harness.s3_client.clone(), harness.bucket_name.clone()); - - let user_op_hash = B256::from([30u8; 32]); - let sender = Address::from([31u8; 20]); - let entry_point = Address::from([32u8; 20]); - let nonce = U256::from(1); - let tx_hash = TxHash::from([33u8; 32]); - - let event1 = create_test_userop_event( - "event-added", - 1234567890, - UserOpEvent::AddedToMempool { - user_op_hash, - sender, - entry_point, - nonce, - }, - ); - writer.archive_userop_event(event1).await?; - - let event2 = create_test_userop_event( - "event-included", - 1234567891, - UserOpEvent::Included { - user_op_hash, - block_number: 12345, - tx_hash, - }, - ); - writer.archive_userop_event(event2).await?; - - let userop_history = writer.get_userop_history(user_op_hash).await?; - assert!(userop_history.is_some()); - - let history = userop_history.unwrap(); - assert_eq!(history.history.len(), 2); - assert_eq!(history.history[0].key(), "event-added"); - assert_eq!(history.history[1].key(), "event-included"); - - Ok(()) -} - -#[tokio::test] -async fn test_userop_dropped_event() -> Result<(), Box> { - let harness = TestHarness::new().await?; - let writer = S3EventReaderWriter::new(harness.s3_client.clone(), harness.bucket_name.clone()); - - let user_op_hash = B256::from([40u8; 32]); - - let event = create_test_userop_event( - "event-dropped", - 1234567890, - UserOpEvent::Dropped { - user_op_hash, - reason: UserOpDropReason::Invalid("AA21 didn't pay prefund".to_string()), - }, - ); - writer.archive_userop_event(event).await?; - - let userop_history = writer.get_userop_history(user_op_hash).await?; - assert!(userop_history.is_some()); - - let history = userop_history.unwrap(); - assert_eq!(history.history.len(), 1); - assert_eq!(history.history[0].key(), "event-dropped"); - - Ok(()) -} diff --git a/crates/core/Cargo.toml b/crates/core/Cargo.toml deleted file mode 100644 index 0eef255d..00000000 --- a/crates/core/Cargo.toml +++ /dev/null @@ -1,36 +0,0 @@ -[package] -name = "tips-core" -description = "Core primitives and utilities for TIPS" -version.workspace = true -edition.workspace = true -rust-version.workspace = true -license.workspace = true -homepage.workspace = true -repository.workspace = true - -[lints] -workspace = true - -[features] -test-utils = ["dep:alloy-signer-local", "dep:op-alloy-rpc-types"] - -[dependencies] -op-alloy-flz.workspace = true -alloy-serde.workspace = true -serde = { workspace = true, features = ["std", "derive"] } -uuid = { workspace = true, features = ["v5", "serde"] } -tracing = { workspace = true, features = ["std"] } -alloy-consensus = { workspace = true, features = ["std"] } -alloy-rpc-types = { workspace = true, features = ["eth"] } -alloy-provider = { workspace = true, features = ["reqwest"] } -alloy-signer-local = { workspace = true, optional = true } -op-alloy-consensus = { workspace = true, features = ["std", "k256", "serde"] } -alloy-primitives = { workspace = true, features = ["map-foldhash", "serde"] } -op-alloy-rpc-types = { workspace = true, features = ["std"], optional = true } -metrics-exporter-prometheus = { workspace = true, features = ["http-listener"] } -tracing-subscriber = { workspace = true, features = ["std", "fmt", "ansi", "env-filter", "json"] } - -[dev-dependencies] -alloy-signer-local.workspace = true -serde_json = { workspace = true, features = ["std"] } -op-alloy-rpc-types = { workspace = true, features = ["std"] } diff --git a/crates/core/README.md b/crates/core/README.md deleted file mode 100644 index 6dd1d3df..00000000 --- a/crates/core/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# `tips-core` - -Core primitives and utilities for TIPS. diff --git a/crates/core/src/kafka.rs b/crates/core/src/kafka.rs deleted file mode 100644 index a5230eee..00000000 --- a/crates/core/src/kafka.rs +++ /dev/null @@ -1,22 +0,0 @@ -use std::collections::HashMap; -use std::fs; - -pub fn load_kafka_config_from_file( - properties_file_path: &str, -) -> Result, std::io::Error> { - let kafka_properties = fs::read_to_string(properties_file_path)?; - - let mut config = HashMap::new(); - - for line in kafka_properties.lines() { - let line = line.trim(); - if line.is_empty() || line.starts_with('#') { - continue; - } - if let Some((key, value)) = line.split_once('=') { - config.insert(key.trim().to_string(), value.trim().to_string()); - } - } - - Ok(config) -} diff --git a/crates/core/src/lib.rs b/crates/core/src/lib.rs deleted file mode 100644 index c2a045ce..00000000 --- a/crates/core/src/lib.rs +++ /dev/null @@ -1,19 +0,0 @@ -#![doc = include_str!("../README.md")] -#![doc(issue_tracker_base_url = "https://github.com/base/tips/issues/")] -#![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))] -#![cfg_attr(not(test), warn(unused_crate_dependencies))] -#![allow(missing_docs)] - -use alloy_rpc_types as _; - -pub mod kafka; -pub mod logger; -pub mod metrics; -#[cfg(any(test, feature = "test-utils"))] -pub mod test_utils; -pub mod types; - -pub use types::{ - AcceptedBundle, Bundle, BundleExtensions, BundleHash, BundleTxs, CancelBundle, - MeterBundleResponse, -}; diff --git a/crates/core/src/logger.rs b/crates/core/src/logger.rs deleted file mode 100644 index ca4ef1bb..00000000 --- a/crates/core/src/logger.rs +++ /dev/null @@ -1,73 +0,0 @@ -use std::str::FromStr; -use tracing::warn; -use tracing_subscriber::{fmt, layer::SubscriberExt, util::SubscriberInitExt}; - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum LogFormat { - Pretty, - Json, - Compact, -} - -impl FromStr for LogFormat { - type Err = String; - - fn from_str(s: &str) -> Result { - match s.to_lowercase().as_str() { - "json" => Ok(Self::Json), - "compact" => Ok(Self::Compact), - "pretty" => Ok(Self::Pretty), - _ => { - warn!("Invalid log format '{}', defaulting to 'pretty'", s); - Ok(Self::Pretty) - } - } - } -} - -pub fn init_logger(log_level: &str) { - init_logger_with_format(log_level, LogFormat::Pretty); -} - -pub fn init_logger_with_format(log_level: &str, format: LogFormat) { - let level = match log_level.to_lowercase().as_str() { - "trace" => tracing::Level::TRACE, - "debug" => tracing::Level::DEBUG, - "info" => tracing::Level::INFO, - "warn" => tracing::Level::WARN, - "error" => tracing::Level::ERROR, - _ => { - warn!("Invalid log level '{}', defaulting to 'info'", log_level); - tracing::Level::INFO - } - }; - - let env_filter = tracing_subscriber::EnvFilter::try_from_default_env() - .unwrap_or_else(|_| tracing_subscriber::EnvFilter::new(level.to_string())); - - match format { - LogFormat::Json => { - tracing_subscriber::registry() - .with(env_filter) - .with( - fmt::layer() - .json() - .flatten_event(true) - .with_current_span(true), - ) - .init(); - } - LogFormat::Compact => { - tracing_subscriber::registry() - .with(env_filter) - .with(fmt::layer().compact()) - .init(); - } - LogFormat::Pretty => { - tracing_subscriber::registry() - .with(env_filter) - .with(fmt::layer().pretty()) - .init(); - } - } -} diff --git a/crates/core/src/metrics.rs b/crates/core/src/metrics.rs deleted file mode 100644 index de365ce2..00000000 --- a/crates/core/src/metrics.rs +++ /dev/null @@ -1,9 +0,0 @@ -use metrics_exporter_prometheus::PrometheusBuilder; -use std::net::SocketAddr; - -pub fn init_prometheus_exporter(addr: SocketAddr) -> Result<(), Box> { - PrometheusBuilder::new() - .with_http_listener(addr) - .install() - .map_err(|e| Box::new(e) as Box) -} diff --git a/crates/core/src/test_utils.rs b/crates/core/src/test_utils.rs deleted file mode 100644 index b4c89b30..00000000 --- a/crates/core/src/test_utils.rs +++ /dev/null @@ -1,80 +0,0 @@ -use crate::{AcceptedBundle, Bundle, MeterBundleResponse}; -use alloy_consensus::SignableTransaction; -use alloy_primitives::{Address, B256, Bytes, TxHash, U256, b256, bytes}; -use alloy_provider::network::TxSignerSync; -use alloy_provider::network::eip2718::Encodable2718; -use alloy_signer_local::PrivateKeySigner; -use op_alloy_consensus::OpTxEnvelope; -use op_alloy_rpc_types::OpTransactionRequest; - -// https://basescan.org/tx/0x4f7ddfc911f5cf85dd15a413f4cbb2a0abe4f1ff275ed13581958c0bcf043c5e -pub const TXN_DATA: Bytes = bytes!( - "0x02f88f8221058304b6b3018315fb3883124f80948ff2f0a8d017c79454aa28509a19ab9753c2dd1480a476d58e1a0182426068c9ea5b00000000000000000002f84f00000000083e4fda54950000c080a086fbc7bbee41f441fb0f32f7aa274d2188c460fe6ac95095fa6331fa08ec4ce7a01aee3bcc3c28f7ba4e0c24da9ae85e9e0166c73cabb42c25ff7b5ecd424f3105" -); - -pub const TXN_HASH: TxHash = - b256!("0x4f7ddfc911f5cf85dd15a413f4cbb2a0abe4f1ff275ed13581958c0bcf043c5e"); - -pub fn create_bundle_from_txn_data() -> AcceptedBundle { - AcceptedBundle::new( - Bundle { - txs: vec![TXN_DATA], - ..Default::default() - } - .try_into() - .unwrap(), - create_test_meter_bundle_response(), - ) -} - -pub fn create_transaction(from: PrivateKeySigner, nonce: u64, to: Address) -> OpTxEnvelope { - let mut txn = OpTransactionRequest::default() - .value(U256::from(10_000)) - .gas_limit(21_000) - .max_fee_per_gas(200) - .max_priority_fee_per_gas(100) - .from(from.address()) - .to(to) - .nonce(nonce) - .build_typed_tx() - .unwrap(); - - let sig = from.sign_transaction_sync(&mut txn).unwrap(); - OpTxEnvelope::Eip1559(txn.eip1559().cloned().unwrap().into_signed(sig)) -} - -pub fn create_test_bundle( - txns: Vec, - block_number: Option, - min_timestamp: Option, - max_timestamp: Option, -) -> AcceptedBundle { - let txs = txns.iter().map(|t| t.encoded_2718().into()).collect(); - - let bundle = Bundle { - txs, - block_number: block_number.unwrap_or(0), - min_timestamp, - max_timestamp, - ..Default::default() - }; - let meter_bundle_response = create_test_meter_bundle_response(); - - AcceptedBundle::new(bundle.try_into().unwrap(), meter_bundle_response) -} - -pub fn create_test_meter_bundle_response() -> MeterBundleResponse { - MeterBundleResponse { - bundle_gas_price: U256::from(0), - bundle_hash: B256::default(), - coinbase_diff: U256::from(0), - eth_sent_to_coinbase: U256::from(0), - gas_fees: U256::from(0), - results: vec![], - state_block_number: 0, - state_flashblock_index: None, - total_gas_used: 0, - total_execution_time_us: 0, - state_root_time_us: 0, - } -} diff --git a/crates/core/src/types.rs b/crates/core/src/types.rs deleted file mode 100644 index 5f1c88f9..00000000 --- a/crates/core/src/types.rs +++ /dev/null @@ -1,494 +0,0 @@ -use alloy_consensus::Transaction; -use alloy_consensus::transaction::Recovered; -use alloy_consensus::transaction::SignerRecoverable; -use alloy_primitives::{Address, B256, Bytes, TxHash, U256, keccak256}; -use alloy_provider::network::eip2718::{Decodable2718, Encodable2718}; -use op_alloy_consensus::OpTxEnvelope; -use op_alloy_flz::tx_estimated_size_fjord_bytes; -use serde::{Deserialize, Serialize}; -use uuid::Uuid; - -/// `Bundle` is the type that mirrors `EthSendBundle` and is used for the API. -#[derive(Default, Debug, Serialize, Deserialize, Clone, PartialEq, Eq)] -#[serde(rename_all = "camelCase")] -pub struct Bundle { - pub txs: Vec, - - #[serde(with = "alloy_serde::quantity")] - pub block_number: u64, - - #[serde( - default, - deserialize_with = "alloy_serde::quantity::opt::deserialize", - skip_serializing_if = "Option::is_none" - )] - pub flashblock_number_min: Option, - - #[serde( - default, - deserialize_with = "alloy_serde::quantity::opt::deserialize", - skip_serializing_if = "Option::is_none" - )] - pub flashblock_number_max: Option, - - #[serde( - default, - deserialize_with = "alloy_serde::quantity::opt::deserialize", - skip_serializing_if = "Option::is_none" - )] - pub min_timestamp: Option, - - #[serde( - default, - deserialize_with = "alloy_serde::quantity::opt::deserialize", - skip_serializing_if = "Option::is_none" - )] - pub max_timestamp: Option, - - #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub reverting_tx_hashes: Vec, - - #[serde(default, skip_serializing_if = "Option::is_none")] - pub replacement_uuid: Option, - - #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub dropping_tx_hashes: Vec, -} - -/// `ParsedBundle` is the type that contains utility methods for the `Bundle` type. -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -pub struct ParsedBundle { - pub txs: Vec>, - pub block_number: u64, - pub flashblock_number_min: Option, - pub flashblock_number_max: Option, - pub min_timestamp: Option, - pub max_timestamp: Option, - pub reverting_tx_hashes: Vec, - pub replacement_uuid: Option, - pub dropping_tx_hashes: Vec, -} - -impl TryFrom for ParsedBundle { - type Error = String; - fn try_from(bundle: Bundle) -> Result { - let txs: Vec> = bundle - .txs - .into_iter() - .map(|tx| { - OpTxEnvelope::decode_2718_exact(tx.iter().as_slice()) - .map_err(|e| format!("Failed to decode transaction: {e:?}")) - .and_then(|tx| { - tx.try_into_recovered().map_err(|e| { - format!("Failed to convert transaction to recovered: {e:?}") - }) - }) - }) - .collect::>, String>>()?; - - let uuid = bundle - .replacement_uuid - .map(|x| Uuid::parse_str(x.as_ref())) - .transpose() - .map_err(|e| format!("Invalid UUID: {e:?}"))?; - - Ok(Self { - txs, - block_number: bundle.block_number, - flashblock_number_min: bundle.flashblock_number_min, - flashblock_number_max: bundle.flashblock_number_max, - min_timestamp: bundle.min_timestamp, - max_timestamp: bundle.max_timestamp, - reverting_tx_hashes: bundle.reverting_tx_hashes, - replacement_uuid: uuid, - dropping_tx_hashes: bundle.dropping_tx_hashes, - }) - } -} - -impl From for ParsedBundle { - fn from(accepted_bundle: AcceptedBundle) -> Self { - Self { - txs: accepted_bundle.txs, - block_number: accepted_bundle.block_number, - flashblock_number_min: accepted_bundle.flashblock_number_min, - flashblock_number_max: accepted_bundle.flashblock_number_max, - min_timestamp: accepted_bundle.min_timestamp, - max_timestamp: accepted_bundle.max_timestamp, - reverting_tx_hashes: accepted_bundle.reverting_tx_hashes, - replacement_uuid: accepted_bundle.replacement_uuid, - dropping_tx_hashes: accepted_bundle.dropping_tx_hashes, - } - } -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct BundleHash { - pub bundle_hash: B256, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct CancelBundle { - pub replacement_uuid: String, -} - -/// `AcceptedBundle` is the type that is sent over the wire. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct AcceptedBundle { - pub uuid: Uuid, - - pub txs: Vec>, - - #[serde(with = "alloy_serde::quantity")] - pub block_number: u64, - - #[serde( - default, - deserialize_with = "alloy_serde::quantity::opt::deserialize", - skip_serializing_if = "Option::is_none" - )] - pub flashblock_number_min: Option, - - #[serde( - default, - deserialize_with = "alloy_serde::quantity::opt::deserialize", - skip_serializing_if = "Option::is_none" - )] - pub flashblock_number_max: Option, - - #[serde( - default, - deserialize_with = "alloy_serde::quantity::opt::deserialize", - skip_serializing_if = "Option::is_none" - )] - pub min_timestamp: Option, - - #[serde( - default, - deserialize_with = "alloy_serde::quantity::opt::deserialize", - skip_serializing_if = "Option::is_none" - )] - pub max_timestamp: Option, - - #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub reverting_tx_hashes: Vec, - - #[serde(default, skip_serializing_if = "Option::is_none")] - pub replacement_uuid: Option, - - #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub dropping_tx_hashes: Vec, - - pub meter_bundle_response: MeterBundleResponse, -} - -pub trait BundleTxs { - fn transactions(&self) -> &Vec>; -} - -pub trait BundleExtensions { - fn bundle_hash(&self) -> B256; - fn txn_hashes(&self) -> Vec; - fn senders(&self) -> Vec

; - fn gas_limit(&self) -> u64; - fn da_size(&self) -> u64; -} - -impl BundleExtensions for T { - fn bundle_hash(&self) -> B256 { - let parsed = self.transactions(); - let mut concatenated = Vec::new(); - for tx in parsed { - concatenated.extend_from_slice(tx.tx_hash().as_slice()); - } - keccak256(&concatenated) - } - - fn txn_hashes(&self) -> Vec { - self.transactions().iter().map(|t| t.tx_hash()).collect() - } - - fn senders(&self) -> Vec
{ - self.transactions() - .iter() - .map(|t| t.recover_signer().unwrap()) - .collect() - } - - fn gas_limit(&self) -> u64 { - self.transactions().iter().map(|t| t.gas_limit()).sum() - } - - fn da_size(&self) -> u64 { - self.transactions() - .iter() - .map(|t| tx_estimated_size_fjord_bytes(&t.encoded_2718())) - .sum() - } -} - -impl BundleTxs for ParsedBundle { - fn transactions(&self) -> &Vec> { - &self.txs - } -} - -impl BundleTxs for AcceptedBundle { - fn transactions(&self) -> &Vec> { - &self.txs - } -} - -impl AcceptedBundle { - pub fn new(bundle: ParsedBundle, meter_bundle_response: MeterBundleResponse) -> Self { - Self { - uuid: bundle.replacement_uuid.unwrap_or_else(|| { - Uuid::new_v5(&Uuid::NAMESPACE_OID, bundle.bundle_hash().as_slice()) - }), - txs: bundle.txs, - block_number: bundle.block_number, - flashblock_number_min: bundle.flashblock_number_min, - flashblock_number_max: bundle.flashblock_number_max, - min_timestamp: bundle.min_timestamp, - max_timestamp: bundle.max_timestamp, - reverting_tx_hashes: bundle.reverting_tx_hashes, - replacement_uuid: bundle.replacement_uuid, - dropping_tx_hashes: bundle.dropping_tx_hashes, - meter_bundle_response, - } - } - - pub const fn uuid(&self) -> &Uuid { - &self.uuid - } -} - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -#[serde(rename_all = "camelCase")] -pub struct TransactionResult { - pub coinbase_diff: U256, - pub eth_sent_to_coinbase: U256, - pub from_address: Address, - pub gas_fees: U256, - pub gas_price: U256, - pub gas_used: u64, - pub to_address: Option
, - pub tx_hash: TxHash, - pub value: U256, - pub execution_time_us: u128, -} - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)] -#[serde(rename_all = "camelCase")] -pub struct MeterBundleResponse { - pub bundle_gas_price: U256, - pub bundle_hash: B256, - pub coinbase_diff: U256, - pub eth_sent_to_coinbase: U256, - pub gas_fees: U256, - pub results: Vec, - pub state_block_number: u64, - #[serde( - default, - deserialize_with = "alloy_serde::quantity::opt::deserialize", - skip_serializing_if = "Option::is_none" - )] - pub state_flashblock_index: Option, - pub total_gas_used: u64, - pub total_execution_time_us: u128, - #[serde(default)] - pub state_root_time_us: u128, -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::test_utils::{create_test_meter_bundle_response, create_transaction}; - use alloy_primitives::Keccak256; - use alloy_provider::network::eip2718::Encodable2718; - use alloy_signer_local::PrivateKeySigner; - - #[test] - fn test_bundle_types() { - let alice = PrivateKeySigner::random(); - let bob = PrivateKeySigner::random(); - - let tx1 = create_transaction(alice.clone(), 1, bob.address()); - let tx2 = create_transaction(alice.clone(), 2, bob.address()); - - let tx1_bytes = tx1.encoded_2718(); - let tx2_bytes = tx2.encoded_2718(); - - let bundle = AcceptedBundle::new( - Bundle { - txs: vec![tx1_bytes.clone().into()], - block_number: 1, - replacement_uuid: None, - ..Default::default() - } - .try_into() - .unwrap(), - create_test_meter_bundle_response(), - ); - - assert!(!bundle.uuid().is_nil()); - assert_eq!(bundle.replacement_uuid, None); // we're fine with bundles that don't have a replacement UUID - assert_eq!(bundle.txn_hashes().len(), 1); - assert_eq!(bundle.txn_hashes()[0], tx1.tx_hash()); - assert_eq!(bundle.senders().len(), 1); - assert_eq!(bundle.senders()[0], alice.address()); - - // Bundle hashes are keccack256(...txnHashes) - let expected_bundle_hash_single = { - let mut hasher = Keccak256::default(); - hasher.update(keccak256(&tx1_bytes)); - hasher.finalize() - }; - - assert_eq!(bundle.bundle_hash(), expected_bundle_hash_single); - - let uuid = Uuid::new_v5(&Uuid::NAMESPACE_OID, bundle.bundle_hash().as_slice()); - let bundle = AcceptedBundle::new( - Bundle { - txs: vec![tx1_bytes.clone().into(), tx2_bytes.clone().into()], - block_number: 1, - replacement_uuid: Some(uuid.to_string()), - ..Default::default() - } - .try_into() - .unwrap(), - create_test_meter_bundle_response(), - ); - - assert_eq!(*bundle.uuid(), uuid); - assert_eq!(bundle.replacement_uuid, Some(uuid)); - assert_eq!(bundle.txn_hashes().len(), 2); - assert_eq!(bundle.txn_hashes()[0], tx1.tx_hash()); - assert_eq!(bundle.txn_hashes()[1], tx2.tx_hash()); - assert_eq!(bundle.senders().len(), 2); - assert_eq!(bundle.senders()[0], alice.address()); - assert_eq!(bundle.senders()[1], alice.address()); - - let expected_bundle_hash_double = { - let mut hasher = Keccak256::default(); - hasher.update(keccak256(&tx1_bytes)); - hasher.update(keccak256(&tx2_bytes)); - hasher.finalize() - }; - - assert_eq!(bundle.bundle_hash(), expected_bundle_hash_double); - } - - #[test] - fn test_meter_bundle_response_serialization() { - let response = MeterBundleResponse { - bundle_gas_price: U256::from(1000000000), - bundle_hash: B256::default(), - coinbase_diff: U256::from(100), - eth_sent_to_coinbase: U256::from(0), - gas_fees: U256::from(100), - results: vec![], - state_block_number: 12345, - state_flashblock_index: Some(42), - total_gas_used: 21000, - total_execution_time_us: 1000, - }; - - let json = serde_json::to_string(&response).unwrap(); - assert!(json.contains("\"stateFlashblockIndex\":42")); - assert!(json.contains("\"stateBlockNumber\":12345")); - - let deserialized: MeterBundleResponse = serde_json::from_str(&json).unwrap(); - assert_eq!(deserialized.state_flashblock_index, Some(42)); - assert_eq!(deserialized.state_block_number, 12345); - } - - #[test] - fn test_meter_bundle_response_without_flashblock_index() { - let response = MeterBundleResponse { - bundle_gas_price: U256::from(1000000000), - bundle_hash: B256::default(), - coinbase_diff: U256::from(100), - eth_sent_to_coinbase: U256::from(0), - gas_fees: U256::from(100), - results: vec![], - state_block_number: 12345, - state_flashblock_index: None, - total_gas_used: 21000, - total_execution_time_us: 1000, - }; - - let json = serde_json::to_string(&response).unwrap(); - assert!(!json.contains("stateFlashblockIndex")); - assert!(json.contains("\"stateBlockNumber\":12345")); - - let deserialized: MeterBundleResponse = serde_json::from_str(&json).unwrap(); - assert_eq!(deserialized.state_flashblock_index, None); - assert_eq!(deserialized.state_block_number, 12345); - } - - #[test] - fn test_meter_bundle_response_deserialization_without_flashblock() { - let json = r#"{ - "bundleGasPrice": "1000000000", - "bundleHash": "0x0000000000000000000000000000000000000000000000000000000000000000", - "coinbaseDiff": "100", - "ethSentToCoinbase": "0", - "gasFees": "100", - "results": [], - "stateBlockNumber": 12345, - "totalGasUsed": 21000, - "totalExecutionTimeUs": 1000, - "stateRootTimeUs": 500 - }"#; - - let deserialized: MeterBundleResponse = serde_json::from_str(json).unwrap(); - assert_eq!(deserialized.bundle_gas_price, U256::from(1000000000)); - assert_eq!(deserialized.coinbase_diff, U256::from(100)); - assert_eq!(deserialized.eth_sent_to_coinbase, U256::from(0)); - assert_eq!(deserialized.state_flashblock_index, None); - assert_eq!(deserialized.state_block_number, 12345); - assert_eq!(deserialized.total_gas_used, 21000); - } - - #[test] - fn test_same_uuid_for_same_bundle_hash() { - let alice = PrivateKeySigner::random(); - let bob = PrivateKeySigner::random(); - - // suppose this is a spam tx - let tx1 = create_transaction(alice.clone(), 1, bob.address()); - let tx1_bytes = tx1.encoded_2718(); - - // we receive it the first time - let bundle1 = AcceptedBundle::new( - Bundle { - txs: vec![tx1_bytes.clone().into()], - block_number: 1, - replacement_uuid: None, - ..Default::default() - } - .try_into() - .unwrap(), - create_test_meter_bundle_response(), - ); - - // but we may receive it more than once - let bundle2 = AcceptedBundle::new( - Bundle { - txs: vec![tx1_bytes.clone().into()], - block_number: 1, - replacement_uuid: None, - ..Default::default() - } - .try_into() - .unwrap(), - create_test_meter_bundle_response(), - ); - - // however, the UUID should be the same - assert_eq!(bundle1.uuid(), bundle2.uuid()); - } -} diff --git a/crates/ingress-rpc/Cargo.toml b/crates/ingress-rpc/Cargo.toml deleted file mode 100644 index 75e30e00..00000000 --- a/crates/ingress-rpc/Cargo.toml +++ /dev/null @@ -1,42 +0,0 @@ -[package] -name = "tips-ingress-rpc-lib" -version.workspace = true -rust-version.workspace = true -license.workspace = true -homepage.workspace = true -repository.workspace = true -edition.workspace = true - -[dependencies] -url.workspace = true -tips-core.workspace = true -op-revm.workspace = true -metrics.workspace = true -dotenvy.workspace = true -tips-audit-lib.workspace = true -async-trait.workspace = true -metrics-derive.workspace = true -op-alloy-network.workspace = true -alloy-signer-local.workspace = true -base-reth-rpc-types.workspace = true -account-abstraction-core.workspace = true -alloy-primitives = { workspace = true, features = ["map-foldhash", "serde"] } -tokio = { workspace = true, features = ["full"] } -tracing = { workspace = true, features = ["std"] } -anyhow = { workspace = true, features = ["std"] } -serde_json = { workspace = true, features = ["std"] } -rdkafka = { workspace = true, features = ["tokio", "libz", "zstd", "ssl-vendored"] } -alloy-consensus = { workspace = true, features = ["std"] } -alloy-provider = { workspace = true, features = ["reqwest"] } -jsonrpsee = { workspace = true, features = ["server", "macros"] } -backon = { workspace = true, features = ["std", "tokio-sleep"] } -axum = { workspace = true, features = ["tokio", "http1", "json"] } -clap = { version = "4.5.47", features = ["std", "derive", "env"] } -op-alloy-consensus = { workspace = true, features = ["std", "k256", "serde"] } -moka = { workspace = true, features = ["future"] } -uuid = { workspace = true, features = ["v5"] } - -[dev-dependencies] -mockall = "0.13" -wiremock.workspace = true -jsonrpsee = { workspace = true, features = ["server", "http-client", "macros"] } diff --git a/crates/ingress-rpc/src/health.rs b/crates/ingress-rpc/src/health.rs deleted file mode 100644 index 314fc495..00000000 --- a/crates/ingress-rpc/src/health.rs +++ /dev/null @@ -1,31 +0,0 @@ -use axum::{Router, http::StatusCode, response::IntoResponse, routing::get}; -use std::net::SocketAddr; -use tracing::info; - -/// Health check handler that always returns 200 OK -async fn health() -> impl IntoResponse { - StatusCode::OK -} - -/// Bind and start the health check server on the specified address. -/// Returns a handle that can be awaited to run the server. -pub async fn bind_health_server( - addr: SocketAddr, -) -> anyhow::Result<(SocketAddr, tokio::task::JoinHandle>)> { - let app = Router::new().route("/health", get(health)); - - let listener = tokio::net::TcpListener::bind(addr).await?; - let bound_addr = listener.local_addr()?; - - info!( - message = "Health check server bound successfully", - address = %bound_addr - ); - - let handle = tokio::spawn(async move { - axum::serve(listener, app).await?; - Ok(()) - }); - - Ok((bound_addr, handle)) -} diff --git a/crates/ingress-rpc/src/lib.rs b/crates/ingress-rpc/src/lib.rs deleted file mode 100644 index ea304c64..00000000 --- a/crates/ingress-rpc/src/lib.rs +++ /dev/null @@ -1,262 +0,0 @@ -pub mod health; -pub mod metrics; -pub mod queue; -pub mod service; -pub mod validation; -use alloy_primitives::TxHash; -use alloy_provider::{Provider, ProviderBuilder, RootProvider}; -use clap::Parser; -use op_alloy_network::Optimism; -use std::net::{IpAddr, SocketAddr}; -use std::str::FromStr; -use tips_core::{AcceptedBundle, MeterBundleResponse}; -use tokio::sync::broadcast; -use tracing::{error, warn}; -use url::Url; - -#[derive(Debug, Clone, Copy)] -pub enum TxSubmissionMethod { - Mempool, - Kafka, - MempoolAndKafka, - None, -} - -impl FromStr for TxSubmissionMethod { - type Err = String; - - fn from_str(s: &str) -> Result { - match s { - "mempool" => Ok(TxSubmissionMethod::Mempool), - "kafka" => Ok(TxSubmissionMethod::Kafka), - "mempool,kafka" | "kafka,mempool" => Ok(TxSubmissionMethod::MempoolAndKafka), - "none" => Ok(TxSubmissionMethod::None), - _ => Err(format!( - "Invalid submission method: '{s}'. Valid options: mempool, kafka, mempool,kafka, kafka,mempool, none" - )), - } - } -} - -#[derive(Parser, Debug, Clone)] -#[command(author, version, about, long_about = None)] -pub struct Config { - /// Address to bind the RPC server to - #[arg(long, env = "TIPS_INGRESS_ADDRESS", default_value = "0.0.0.0")] - pub address: IpAddr, - - /// Port to bind the RPC server to - #[arg(long, env = "TIPS_INGRESS_PORT", default_value = "8080")] - pub port: u16, - - /// URL of the mempool service to proxy transactions to - #[arg(long, env = "TIPS_INGRESS_RPC_MEMPOOL")] - pub mempool_url: Url, - - /// Method to submit transactions to the mempool - #[arg( - long, - env = "TIPS_INGRESS_TX_SUBMISSION_METHOD", - default_value = "mempool" - )] - pub tx_submission_method: TxSubmissionMethod, - - /// Kafka brokers for publishing mempool events - #[arg(long, env = "TIPS_INGRESS_KAFKA_INGRESS_PROPERTIES_FILE")] - pub ingress_kafka_properties: String, - - /// Kafka topic for queuing transactions before the DB Writer - #[arg( - long, - env = "TIPS_INGRESS_KAFKA_INGRESS_TOPIC", - default_value = "tips-ingress" - )] - pub ingress_topic: String, - - /// Kafka properties file for audit events - #[arg(long, env = "TIPS_INGRESS_KAFKA_AUDIT_PROPERTIES_FILE")] - pub audit_kafka_properties: String, - - /// Kafka topic for audit events - #[arg( - long, - env = "TIPS_INGRESS_KAFKA_AUDIT_TOPIC", - default_value = "tips-audit" - )] - pub audit_topic: String, - - /// Kafka properties file for the user operation consumer - #[arg( - long, - env = "TIPS_INGRESS_KAFKA_USER_OPERATION_CONSUMER_PROPERTIES_FILE" - )] - pub user_operation_consumer_properties: Option, - - /// Consumer group id for user operation topic (set uniquely per deployment) - #[arg( - long, - env = "TIPS_INGRESS_KAFKA_USER_OPERATION_CONSUMER_GROUP_ID", - default_value = "tips-user-operation" - )] - pub user_operation_consumer_group_id: String, - - /// User operation topic for pushing valid user operations - #[arg( - long, - env = "TIPS_INGRESS_KAFKA_USER_OPERATION_TOPIC", - default_value = "tips-user-operation" - )] - pub user_operation_topic: String, - - #[arg(long, env = "TIPS_INGRESS_LOG_LEVEL", default_value = "info")] - pub log_level: String, - - #[arg(long, env = "TIPS_INGRESS_LOG_FORMAT", default_value = "pretty")] - pub log_format: tips_core::logger::LogFormat, - - /// Default lifetime for sent transactions in seconds (default: 3 hours) - #[arg( - long, - env = "TIPS_INGRESS_SEND_TRANSACTION_DEFAULT_LIFETIME_SECONDS", - default_value = "10800" - )] - pub send_transaction_default_lifetime_seconds: u64, - - /// URL of the simulation RPC service for bundle metering - #[arg(long, env = "TIPS_INGRESS_RPC_SIMULATION")] - pub simulation_rpc: Url, - - /// Port to bind the Prometheus metrics server to - #[arg( - long, - env = "TIPS_INGRESS_METRICS_ADDR", - default_value = "0.0.0.0:9002" - )] - pub metrics_addr: SocketAddr, - - /// Configurable block time in milliseconds (default: 2000 milliseconds) - #[arg( - long, - env = "TIPS_INGRESS_BLOCK_TIME_MILLISECONDS", - default_value = "2000" - )] - pub block_time_milliseconds: u64, - - /// Timeout for bundle metering in milliseconds (default: 2000 milliseconds) - #[arg( - long, - env = "TIPS_INGRESS_METER_BUNDLE_TIMEOUT_MS", - default_value = "2000" - )] - pub meter_bundle_timeout_ms: u64, - - #[arg( - long, - env = "TIPS_INGRESS_VALIDATE_USER_OPERATION_TIMEOUT_MS", - default_value = "2000" - )] - pub validate_user_operation_timeout_ms: u64, - - /// URLs of the builder RPC service for setting metering information - #[arg(long, env = "TIPS_INGRESS_BUILDER_RPCS", value_delimiter = ',')] - pub builder_rpcs: Vec, - - /// Maximum number of `MeterBundleResponse`s to buffer in memory - #[arg( - long, - env = "TIPS_INGRESS_MAX_BUFFERED_METER_BUNDLE_RESPONSES", - default_value = "100" - )] - pub max_buffered_meter_bundle_responses: usize, - - /// Maximum number of backrun bundles to buffer in memory - #[arg( - long, - env = "TIPS_INGRESS_MAX_BUFFERED_BACKRUN_BUNDLES", - default_value = "100" - )] - pub max_buffered_backrun_bundles: usize, - - /// Address to bind the health check server to - #[arg( - long, - env = "TIPS_INGRESS_HEALTH_CHECK_ADDR", - default_value = "0.0.0.0:8081" - )] - pub health_check_addr: SocketAddr, - - /// chain id - #[arg(long, env = "TIPS_INGRESS_CHAIN_ID", default_value = "11")] - pub chain_id: u64, - - /// Enable backrun bundle submission to op-rbuilder - #[arg(long, env = "TIPS_INGRESS_BACKRUN_ENABLED", default_value = "false")] - pub backrun_enabled: bool, - - /// Maximum number of transactions allowed in a backrun bundle (including target tx) - #[arg(long, env = "MAX_BACKRUN_TXS", default_value = "5")] - pub max_backrun_txs: usize, - - /// Maximum total gas limit for all transactions in a backrun bundle - #[arg(long, env = "MAX_BACKRUN_GAS_LIMIT", default_value = "5000000")] - pub max_backrun_gas_limit: u64, - - /// URL of third-party RPC endpoint to forward raw transactions to (enables forwarding if set) - #[arg(long, env = "TIPS_INGRESS_RAW_TX_FORWARD_RPC")] - pub raw_tx_forward_rpc: Option, - - /// TTL for bundle cache in seconds - #[arg(long, env = "TIPS_INGRESS_BUNDLE_CACHE_TTL", default_value = "20")] - pub bundle_cache_ttl: u64, - - /// Enable sending to builder - #[arg(long, env = "TIPS_INGRESS_SEND_TO_BUILDER", default_value = "false")] - pub send_to_builder: bool, -} - -pub fn connect_ingress_to_builder( - metering_rx: broadcast::Receiver, - backrun_rx: broadcast::Receiver, - builder_rpc: Url, -) { - let builder: RootProvider = ProviderBuilder::new() - .disable_recommended_fillers() - .network::() - .connect_http(builder_rpc); - - let metering_builder = builder.clone(); - tokio::spawn(async move { - let mut event_rx = metering_rx; - while let Ok(event) = event_rx.recv().await { - if event.results.is_empty() { - warn!(message = "received metering information with no transactions", hash=%event.bundle_hash); - continue; - } - - let tx_hash = event.results[0].tx_hash; - if let Err(e) = metering_builder - .client() - .request::<(TxHash, MeterBundleResponse), ()>( - "base_setMeteringInformation", - (tx_hash, event), - ) - .await - { - error!(error = %e, "Failed to set metering information for tx hash: {tx_hash}"); - } - } - }); - - tokio::spawn(async move { - let mut event_rx = backrun_rx; - while let Ok(accepted_bundle) = event_rx.recv().await { - if let Err(e) = builder - .client() - .request::<(AcceptedBundle,), ()>("base_sendBackrunBundle", (accepted_bundle,)) - .await - { - error!(error = ?e, "Failed to send backrun bundle to builder"); - } - } - }); -} diff --git a/crates/ingress-rpc/src/metrics.rs b/crates/ingress-rpc/src/metrics.rs deleted file mode 100644 index a62b3b87..00000000 --- a/crates/ingress-rpc/src/metrics.rs +++ /dev/null @@ -1,54 +0,0 @@ -use metrics::{Counter, Histogram}; -use metrics_derive::Metrics; -use tokio::time::Duration; - -pub fn record_histogram(rpc_latency: Duration, rpc: String) { - metrics::histogram!("tips_ingress_rpc_rpc_latency", "rpc" => rpc) - .record(rpc_latency.as_secs_f64()); -} - -#[derive(Metrics, Clone)] -#[metrics(scope = "tips_ingress_rpc")] -pub struct Metrics { - #[metric(describe = "Number of valid transactions received")] - pub transactions_received: Counter, - - #[metric(describe = "Number of valid bundles parsed")] - pub bundles_parsed: Counter, - - #[metric(describe = "Number of bundles simulated")] - pub successful_simulations: Counter, - - #[metric(describe = "Number of bundles simulated")] - pub failed_simulations: Counter, - - #[metric(describe = "Number of bundles sent to kafka")] - pub sent_to_kafka: Counter, - - #[metric(describe = "Number of transactions sent to mempool")] - pub sent_to_mempool: Counter, - - #[metric(describe = "Duration of validate_tx")] - pub validate_tx_duration: Histogram, - - #[metric(describe = "Duration of validate_bundle")] - pub validate_bundle_duration: Histogram, - - #[metric(describe = "Duration of meter_bundle")] - pub meter_bundle_duration: Histogram, - - #[metric(describe = "Duration of send_raw_transaction")] - pub send_raw_transaction_duration: Histogram, - - #[metric(describe = "Total backrun bundles received")] - pub backrun_bundles_received_total: Counter, - - #[metric(describe = "Duration to send backrun bundle to op-rbuilder")] - pub backrun_bundles_sent_duration: Histogram, - - #[metric(describe = "Total raw transactions forwarded to additional endpoint")] - pub raw_tx_forwards_total: Counter, - - #[metric(describe = "Number of bundles that exceeded the metering time")] - pub bundles_exceeded_metering_time: Counter, -} diff --git a/crates/ingress-rpc/src/queue.rs b/crates/ingress-rpc/src/queue.rs deleted file mode 100644 index cd227a0a..00000000 --- a/crates/ingress-rpc/src/queue.rs +++ /dev/null @@ -1,167 +0,0 @@ -use account_abstraction_core::{ - MempoolEvent, - domain::types::{VersionedUserOperation, WrappedUserOperation}, -}; -use alloy_primitives::B256; -use anyhow::Result; -use async_trait::async_trait; -use backon::{ExponentialBuilder, Retryable}; -use rdkafka::producer::{FutureProducer, FutureRecord}; -use std::sync::Arc; -use tips_core::AcceptedBundle; -use tokio::time::Duration; -use tracing::{error, info}; - -#[async_trait] -pub trait MessageQueue: Send + Sync { - async fn publish(&self, topic: &str, key: &str, payload: &[u8]) -> Result<()>; -} - -pub struct KafkaMessageQueue { - producer: FutureProducer, -} - -impl KafkaMessageQueue { - pub fn new(producer: FutureProducer) -> Self { - Self { producer } - } -} - -#[async_trait] -impl MessageQueue for KafkaMessageQueue { - async fn publish(&self, topic: &str, key: &str, payload: &[u8]) -> Result<()> { - let enqueue = || async { - let record = FutureRecord::to(topic).key(key).payload(payload); - - match self.producer.send(record, Duration::from_secs(5)).await { - Ok((partition, offset)) => { - info!( - key = %key, - partition = partition, - offset = offset, - topic = %topic, - "Successfully enqueued message" - ); - Ok(()) - } - Err((err, _)) => { - error!( - key = key, - error = %err, - topic = topic, - "Failed to enqueue message" - ); - Err(anyhow::anyhow!("Failed to enqueue bundle: {err}")) - } - } - }; - - enqueue - .retry( - &ExponentialBuilder::default() - .with_min_delay(Duration::from_millis(100)) - .with_max_delay(Duration::from_secs(5)) - .with_max_times(3), - ) - .notify(|err: &anyhow::Error, dur: Duration| { - info!("retrying to enqueue message {:?} after {:?}", err, dur); - }) - .await - } -} - -pub struct UserOpQueuePublisher { - queue: Arc, - topic: String, -} - -impl UserOpQueuePublisher { - pub fn new(queue: Arc, topic: String) -> Self { - Self { queue, topic } - } - - pub async fn publish(&self, user_op: &VersionedUserOperation, hash: &B256) -> Result<()> { - let key = hash.to_string(); - let event = self.create_user_op_added_event(user_op, hash); - let payload = serde_json::to_vec(&event)?; - self.queue.publish(&self.topic, &key, &payload).await - } - - fn create_user_op_added_event( - &self, - user_op: &VersionedUserOperation, - hash: &B256, - ) -> MempoolEvent { - let wrapped_user_op = WrappedUserOperation { - operation: user_op.clone(), - hash: *hash, - }; - - MempoolEvent::UserOpAdded { - user_op: wrapped_user_op, - } - } -} - -pub struct BundleQueuePublisher { - queue: Arc, - topic: String, -} - -impl BundleQueuePublisher { - pub fn new(queue: Arc, topic: String) -> Self { - Self { queue, topic } - } - - pub async fn publish(&self, bundle: &AcceptedBundle, hash: &B256) -> Result<()> { - let key = hash.to_string(); - let payload = serde_json::to_vec(bundle)?; - self.queue.publish(&self.topic, &key, &payload).await - } -} - -#[cfg(test)] -mod tests { - use super::*; - use rdkafka::config::ClientConfig; - use tips_core::{ - AcceptedBundle, Bundle, BundleExtensions, test_utils::create_test_meter_bundle_response, - }; - use tokio::time::{Duration, Instant}; - - fn create_test_bundle() -> Bundle { - Bundle::default() - } - - #[tokio::test] - async fn test_backoff_retry_logic() { - // use an invalid broker address to trigger the backoff logic - let producer = ClientConfig::new() - .set("bootstrap.servers", "localhost:9999") - .set("message.timeout.ms", "100") - .create() - .expect("Producer creation failed"); - - let publisher = KafkaMessageQueue::new(producer); - let bundle = create_test_bundle(); - let accepted_bundle = AcceptedBundle::new( - bundle.try_into().unwrap(), - create_test_meter_bundle_response(), - ); - let bundle_hash = &accepted_bundle.bundle_hash(); - - let start = Instant::now(); - let result = publisher - .publish( - "tips-ingress-rpc", - bundle_hash.to_string().as_str(), - &serde_json::to_vec(&accepted_bundle).unwrap(), - ) - .await; - let elapsed = start.elapsed(); - - // the backoff tries at minimum 100ms, so verify we tried at least once - assert!(result.is_err()); - assert!(elapsed >= Duration::from_millis(100)); - } -} diff --git a/crates/ingress-rpc/src/service.rs b/crates/ingress-rpc/src/service.rs deleted file mode 100644 index 1e276a48..00000000 --- a/crates/ingress-rpc/src/service.rs +++ /dev/null @@ -1,953 +0,0 @@ -use account_abstraction_core::domain::ReputationService; -use account_abstraction_core::infrastructure::base_node::validator::BaseNodeValidator; -use account_abstraction_core::services::ReputationServiceImpl; -use account_abstraction_core::services::interfaces::user_op_validator::UserOperationValidator; -use account_abstraction_core::{Mempool, MempoolEngine}; -use alloy_consensus::transaction::Recovered; -use alloy_consensus::{Transaction, transaction::SignerRecoverable}; -use alloy_primitives::{Address, B256, Bytes, FixedBytes}; -use alloy_provider::{Provider, RootProvider, network::eip2718::Decodable2718}; -use base_reth_rpc_types::EthApiError; -use jsonrpsee::{ - core::{RpcResult, async_trait}, - proc_macros::rpc, -}; -use moka::future::Cache; -use op_alloy_consensus::OpTxEnvelope; -use op_alloy_network::Optimism; -use std::time::{SystemTime, UNIX_EPOCH}; -use tips_audit_lib::BundleEvent; -use tips_core::types::ParsedBundle; -use tips_core::{ - AcceptedBundle, Bundle, BundleExtensions, BundleHash, CancelBundle, MeterBundleResponse, -}; -use tokio::sync::{broadcast, mpsc}; -use tokio::time::{Duration, Instant, timeout}; -use tracing::{debug, info, warn}; - -use crate::metrics::{Metrics, record_histogram}; -use crate::queue::{BundleQueuePublisher, MessageQueue, UserOpQueuePublisher}; -use crate::validation::validate_bundle; -use crate::{Config, TxSubmissionMethod}; -use account_abstraction_core::domain::entrypoints::version::EntryPointVersion; -use account_abstraction_core::domain::types::{UserOperationRequest, VersionedUserOperation}; -use std::sync::Arc; - -/// RPC providers for different endpoints -pub struct Providers { - pub mempool: RootProvider, - pub simulation: RootProvider, - pub raw_tx_forward: Option>, -} - -#[rpc(server, namespace = "eth")] -pub trait IngressApi { - /// `eth_sendBundle` can be used to send your bundles to the builder. - #[method(name = "sendBundle")] - async fn send_bundle(&self, bundle: Bundle) -> RpcResult; - - #[method(name = "sendBackrunBundle")] - async fn send_backrun_bundle(&self, bundle: Bundle) -> RpcResult; - - /// `eth_cancelBundle` is used to prevent a submitted bundle from being included on-chain. - #[method(name = "cancelBundle")] - async fn cancel_bundle(&self, request: CancelBundle) -> RpcResult<()>; - - /// Handler for: `eth_sendRawTransaction` - #[method(name = "sendRawTransaction")] - async fn send_raw_transaction(&self, tx: Bytes) -> RpcResult; - - /// Handler for: `eth_sendUserOperation` - #[method(name = "sendUserOperation")] - async fn send_user_operation( - &self, - user_operation: VersionedUserOperation, - entry_point: Address, - ) -> RpcResult>; -} - -pub struct IngressService { - mempool_provider: Arc>, - simulation_provider: Arc>, - raw_tx_forward_provider: Option>>, - user_op_validator: BaseNodeValidator, - tx_submission_method: TxSubmissionMethod, - bundle_queue_publisher: BundleQueuePublisher, - user_op_queue_publisher: UserOpQueuePublisher, - reputation_service: Option>>, - audit_channel: mpsc::UnboundedSender, - send_transaction_default_lifetime_seconds: u64, - metrics: Metrics, - block_time_milliseconds: u64, - meter_bundle_timeout_ms: u64, - builder_tx: broadcast::Sender, - backrun_enabled: bool, - builder_backrun_tx: broadcast::Sender, - max_backrun_txs: usize, - max_backrun_gas_limit: u64, - bundle_cache: Cache, - send_to_builder: bool, -} - -impl IngressService { - pub fn new( - providers: Providers, - queue: Q, - audit_channel: mpsc::UnboundedSender, - builder_tx: broadcast::Sender, - builder_backrun_tx: broadcast::Sender, - mempool_engine: impl Into>>>, - config: Config, - ) -> Self { - let mempool_engine = mempool_engine.into(); - let mempool_provider = Arc::new(providers.mempool); - let simulation_provider = Arc::new(providers.simulation); - let raw_tx_forward_provider = providers.raw_tx_forward.map(Arc::new); - let user_op_validator = BaseNodeValidator::new( - simulation_provider.clone(), - config.validate_user_operation_timeout_ms, - ); - let queue_connection = Arc::new(queue); - let reputation_service = mempool_engine - .as_ref() - .map(|engine| Arc::new(ReputationServiceImpl::new(engine.get_mempool()))); - - // A TTL cache to deduplicate bundles with the same Bundle ID - let bundle_cache = Cache::builder() - .time_to_live(Duration::from_secs(config.bundle_cache_ttl)) - .build(); - Self { - mempool_provider, - simulation_provider, - raw_tx_forward_provider, - user_op_validator, - tx_submission_method: config.tx_submission_method, - user_op_queue_publisher: UserOpQueuePublisher::new( - queue_connection.clone(), - config.user_operation_topic, - ), - bundle_queue_publisher: BundleQueuePublisher::new( - queue_connection.clone(), - config.ingress_topic, - ), - reputation_service, - audit_channel, - send_transaction_default_lifetime_seconds: config - .send_transaction_default_lifetime_seconds, - metrics: Metrics::default(), - block_time_milliseconds: config.block_time_milliseconds, - meter_bundle_timeout_ms: config.meter_bundle_timeout_ms, - builder_tx, - backrun_enabled: config.backrun_enabled, - builder_backrun_tx, - max_backrun_txs: config.max_backrun_txs, - max_backrun_gas_limit: config.max_backrun_gas_limit, - bundle_cache, - send_to_builder: config.send_to_builder, - } - } -} - -fn validate_backrun_bundle_limits( - txs_count: usize, - total_gas_limit: u64, - max_backrun_txs: usize, - max_backrun_gas_limit: u64, -) -> Result<(), String> { - if txs_count < 2 { - return Err( - "Backrun bundle must have at least 2 transactions (target + backrun)".to_string(), - ); - } - if txs_count > max_backrun_txs { - return Err(format!( - "Backrun bundle exceeds max transaction count: {txs_count} > {max_backrun_txs}", - )); - } - if total_gas_limit > max_backrun_gas_limit { - return Err(format!( - "Backrun bundle exceeds max gas limit: {total_gas_limit} > {max_backrun_gas_limit}", - )); - } - Ok(()) -} - -#[async_trait] -impl IngressApiServer for IngressService { - async fn send_backrun_bundle(&self, bundle: Bundle) -> RpcResult { - if !self.backrun_enabled { - return Err( - EthApiError::InvalidParams("Backrun bundle submission is disabled".into()) - .into_rpc_err(), - ); - } - - let start = Instant::now(); - let (accepted_bundle, bundle_hash) = - self.validate_parse_and_meter_bundle(&bundle, false).await?; - - let total_gas_limit: u64 = accepted_bundle.txs.iter().map(|tx| tx.gas_limit()).sum(); - validate_backrun_bundle_limits( - accepted_bundle.txs.len(), - total_gas_limit, - self.max_backrun_txs, - self.max_backrun_gas_limit, - ) - .map_err(|e| EthApiError::InvalidParams(e).into_rpc_err())?; - - self.metrics.backrun_bundles_received_total.increment(1); - - self.builder_backrun_tx - .send(accepted_bundle.clone()) - .map_err(|e| { - EthApiError::InvalidParams(format!("Failed to send backrun bundle: {e}")) - .into_rpc_err() - })?; - - self.send_audit_event(&accepted_bundle, bundle_hash); - - self.metrics - .backrun_bundles_sent_duration - .record(start.elapsed().as_secs_f64()); - - Ok(BundleHash { bundle_hash }) - } - - async fn send_bundle(&self, bundle: Bundle) -> RpcResult { - let (accepted_bundle, bundle_hash) = - self.validate_parse_and_meter_bundle(&bundle, true).await?; - - // Get meter_bundle_response for builder broadcast - let meter_bundle_response = accepted_bundle.meter_bundle_response.clone(); - - // asynchronously send the meter bundle response to the builder - self.builder_tx - .send(meter_bundle_response) - .map_err(|e| EthApiError::InvalidParams(e.to_string()).into_rpc_err())?; - - // publish the bundle to the queue - if let Err(e) = self - .bundle_queue_publisher - .publish(&accepted_bundle, &bundle_hash) - .await - { - warn!(message = "Failed to publish bundle to queue", bundle_hash = %bundle_hash, error = %e); - return Err(EthApiError::InvalidParams("Failed to queue bundle".into()).into_rpc_err()); - } - - info!( - message = "queued bundle", - bundle_hash = %bundle_hash, - ); - - // asynchronously send the audit event to the audit channel - self.send_audit_event(&accepted_bundle, bundle_hash); - - Ok(BundleHash { bundle_hash }) - } - - async fn cancel_bundle(&self, _request: CancelBundle) -> RpcResult<()> { - warn!( - message = "TODO: implement cancel_bundle", - method = "cancel_bundle" - ); - todo!("implement cancel_bundle") - } - - async fn send_raw_transaction(&self, data: Bytes) -> RpcResult { - let start = Instant::now(); - let transaction = self.get_tx(&data).await?; - - self.metrics.transactions_received.increment(1); - - let send_to_kafka = matches!( - self.tx_submission_method, - TxSubmissionMethod::Kafka | TxSubmissionMethod::MempoolAndKafka - ); - let send_to_mempool = matches!( - self.tx_submission_method, - TxSubmissionMethod::Mempool | TxSubmissionMethod::MempoolAndKafka - ); - - // Forward before metering - if let Some(forward_provider) = self.raw_tx_forward_provider.clone() { - self.metrics.raw_tx_forwards_total.increment(1); - let tx_data = data.clone(); - let tx_hash = transaction.tx_hash(); - tokio::spawn(async move { - match forward_provider - .send_raw_transaction(tx_data.iter().as_slice()) - .await - { - Ok(_) => { - debug!(message = "Forwarded raw tx", hash = %tx_hash); - } - Err(e) => { - warn!(message = "Failed to forward raw tx", hash = %tx_hash, error = %e); - } - } - }); - } - - let expiry_timestamp = SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap() - .as_secs() - + self.send_transaction_default_lifetime_seconds; - - let bundle = Bundle { - txs: vec![data.clone()], - max_timestamp: Some(expiry_timestamp), - reverting_tx_hashes: vec![transaction.tx_hash()], - ..Default::default() - }; - - let parsed_bundle: ParsedBundle = bundle - .clone() - .try_into() - .map_err(|e: String| EthApiError::InvalidParams(e).into_rpc_err())?; - - let bundle_hash = &parsed_bundle.bundle_hash(); - - if self.bundle_cache.get(bundle_hash).await.is_some() { - debug!( - message = "Duplicate bundle detected, skipping Kafka publish", - bundle_hash = %bundle_hash, - transaction_hash = %transaction.tx_hash(), - ); - } else { - self.bundle_cache.insert(*bundle_hash, ()).await; - self.metrics.bundles_parsed.increment(1); - - let meter_bundle_response = match self.meter_bundle(&bundle, bundle_hash).await { - Ok(response) => { - info!(message = "Metering succeeded for raw transaction", bundle_hash = %bundle_hash, response = ?response); - Some(response) - } - Err(e) => { - warn!( - bundle_hash = %bundle_hash, - error = %e, - "Metering failed for raw transaction" - ); - None - } - }; - - if let Some(meter_info) = meter_bundle_response.as_ref() { - self.metrics.successful_simulations.increment(1); - if self.send_to_builder { - _ = self.builder_tx.send(meter_info.clone()); - } - } else { - self.metrics.failed_simulations.increment(1); - } - - let accepted_bundle = - AcceptedBundle::new(parsed_bundle, meter_bundle_response.unwrap_or_default()); - - if send_to_kafka { - if let Err(e) = self - .bundle_queue_publisher - .publish(&accepted_bundle, bundle_hash) - .await - { - warn!(message = "Failed to publish Queue::enqueue_bundle", bundle_hash = %bundle_hash, error = %e); - } - - self.metrics.sent_to_kafka.increment(1); - info!(message="queued singleton bundle", txn_hash=%transaction.tx_hash()); - } - - if send_to_mempool { - let response = self - .mempool_provider - .send_raw_transaction(data.iter().as_slice()) - .await; - match response { - Ok(_) => { - self.metrics.sent_to_mempool.increment(1); - debug!(message = "sent transaction to the mempool", hash=%transaction.tx_hash()); - } - Err(e) => { - warn!(message = "Failed to send raw transaction to mempool", error = %e); - } - } - } - - info!( - message = "processed transaction", - bundle_hash = %bundle_hash, - transaction_hash = %transaction.tx_hash(), - ); - - self.send_audit_event(&accepted_bundle, accepted_bundle.bundle_hash()); - } - - self.metrics - .send_raw_transaction_duration - .record(start.elapsed().as_secs_f64()); - - Ok(transaction.tx_hash()) - } - - async fn send_user_operation( - &self, - rpc_user_operation: VersionedUserOperation, - entry_point: Address, - ) -> RpcResult> { - let entry_point_version = EntryPointVersion::try_from(entry_point).map_err(|_| { - EthApiError::InvalidParams("Unknown entry point version".into()).into_rpc_err() - })?; - - let versioned_user_operation = match (rpc_user_operation, entry_point_version) { - (VersionedUserOperation::UserOperation(op), EntryPointVersion::V06) => { - VersionedUserOperation::UserOperation(op) - } - (VersionedUserOperation::PackedUserOperation(op), EntryPointVersion::V07) => { - VersionedUserOperation::PackedUserOperation(op) - } - _ => { - return Err(EthApiError::InvalidParams( - "User operation type does not match entry point version".into(), - ) - .into_rpc_err()); - } - }; - - let request = UserOperationRequest { - user_operation: versioned_user_operation, - entry_point, - chain_id: 1, - }; - - if let Some(reputation_service) = &self.reputation_service { - let _ = reputation_service - .get_reputation(&request.user_operation.sender()) - .await; - } - - let user_op_hash = request.hash().map_err(|e| { - warn!(message = "Failed to hash user operation", error = %e); - EthApiError::InvalidParams(e.to_string()).into_rpc_err() - })?; - - let _ = self - .user_op_validator - .validate_user_operation(&request.user_operation, &entry_point) - .await - .map_err(|e| { - warn!(message = "Failed to validate user operation", error = %e); - EthApiError::InvalidParams(e.to_string()).into_rpc_err() - })?; - - if let Err(e) = self - .user_op_queue_publisher - .publish(&request.user_operation, &user_op_hash) - .await - { - warn!( - message = "Failed to publish user operation to queue", - user_operation_hash = %user_op_hash, - error = %e - ); - return Err( - EthApiError::InvalidParams("Failed to queue user operation".into()).into_rpc_err(), - ); - } - - Ok(user_op_hash) - } -} - -impl IngressService { - async fn get_tx(&self, data: &Bytes) -> RpcResult> { - if data.is_empty() { - return Err(EthApiError::EmptyRawTransactionData.into_rpc_err()); - } - - let envelope = OpTxEnvelope::decode_2718_exact(data.iter().as_slice()) - .map_err(|_| EthApiError::FailedToDecodeSignedTransaction.into_rpc_err())?; - - let transaction = envelope - .clone() - .try_into_recovered() - .map_err(|_| EthApiError::FailedToDecodeSignedTransaction.into_rpc_err())?; - Ok(transaction) - } - - async fn validate_bundle(&self, bundle: &Bundle) -> RpcResult<()> { - let start = Instant::now(); - if bundle.txs.is_empty() { - return Err( - EthApiError::InvalidParams("Bundle cannot have empty transactions".into()) - .into_rpc_err(), - ); - } - - let mut total_gas = 0u64; - let mut tx_hashes = Vec::new(); - for tx_data in &bundle.txs { - let transaction = self.get_tx(tx_data).await?; - total_gas = total_gas.saturating_add(transaction.gas_limit()); - tx_hashes.push(transaction.tx_hash()); - } - validate_bundle(bundle, total_gas, tx_hashes)?; - - self.metrics - .validate_bundle_duration - .record(start.elapsed().as_secs_f64()); - Ok(()) - } - - /// `meter_bundle` is used to determine how long a bundle will take to execute. A bundle that - /// is within `block_time_milliseconds` will return the `MeterBundleResponse` that can be passed along - /// to the builder. - async fn meter_bundle( - &self, - bundle: &Bundle, - bundle_hash: &B256, - ) -> RpcResult { - let start = Instant::now(); - let timeout_duration = Duration::from_millis(self.meter_bundle_timeout_ms); - - // The future we await has the nested type: - // Result< - // RpcResult, // 1. The inner operation's result - // tokio::time::error::Elapsed // 2. The outer timeout's result - // > - let res: MeterBundleResponse = timeout( - timeout_duration, - self.simulation_provider - .client() - .request("base_meterBundle", (bundle,)), - ) - .await - .map_err(|_| { - warn!(message = "Timed out on requesting metering", bundle_hash = %bundle_hash); - EthApiError::InvalidParams("Timeout on requesting metering".into()).into_rpc_err() - })? - .map_err(|e| EthApiError::InvalidParams(e.to_string()).into_rpc_err())?; - - record_histogram(start.elapsed(), "base_meterBundle".to_string()); - - // we can save some builder payload building computation by not including bundles - // that we know will take longer than the block time to execute - let total_execution_time = (res.total_execution_time_us / 1_000) as u64; - if total_execution_time > self.block_time_milliseconds { - self.metrics.bundles_exceeded_metering_time.increment(1); - return Err( - EthApiError::InvalidParams("Bundle simulation took too long".into()).into_rpc_err(), - ); - } - Ok(res) - } - - /// Helper method to validate, parse, and meter a bundle - async fn validate_parse_and_meter_bundle( - &self, - bundle: &Bundle, - to_meter: bool, - ) -> RpcResult<(AcceptedBundle, B256)> { - self.validate_bundle(bundle).await?; - let parsed_bundle: ParsedBundle = bundle - .clone() - .try_into() - .map_err(|e: String| EthApiError::InvalidParams(e).into_rpc_err())?; - let bundle_hash = parsed_bundle.bundle_hash(); - let meter_bundle_response = if to_meter { - self.meter_bundle(bundle, &bundle_hash).await? - } else { - MeterBundleResponse::default() - }; - let accepted_bundle = AcceptedBundle::new(parsed_bundle, meter_bundle_response.clone()); - Ok((accepted_bundle, bundle_hash)) - } - - /// Helper method to send audit event for a bundle - fn send_audit_event(&self, accepted_bundle: &AcceptedBundle, bundle_hash: B256) { - let audit_event = BundleEvent::Received { - bundle_id: *accepted_bundle.uuid(), - bundle: Box::new(accepted_bundle.clone()), - }; - if let Err(e) = self.audit_channel.send(audit_event) { - warn!( - message = "failed to send audit event", - bundle_hash = %bundle_hash, - error = %e - ); - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::{Config, TxSubmissionMethod, queue::MessageQueue}; - use account_abstraction_core::MempoolEvent; - use account_abstraction_core::domain::PoolConfig; - use account_abstraction_core::infrastructure::in_memory::mempool::InMemoryMempool; - use account_abstraction_core::services::interfaces::event_source::EventSource; - use alloy_provider::RootProvider; - use anyhow::Result; - use async_trait::async_trait; - use jsonrpsee::core::client::ClientT; - use jsonrpsee::http_client::{HttpClient, HttpClientBuilder}; - use jsonrpsee::server::{ServerBuilder, ServerHandle}; - use mockall::mock; - use serde_json::json; - use std::net::{IpAddr, SocketAddr}; - use std::str::FromStr; - use tips_core::test_utils::create_test_meter_bundle_response; - use tokio::sync::{RwLock, broadcast, mpsc}; - use url::Url; - use wiremock::{Mock, MockServer, ResponseTemplate, matchers::method}; - struct MockQueue; - - #[async_trait] - impl MessageQueue for MockQueue { - async fn publish(&self, _topic: &str, _key: &str, _payload: &[u8]) -> Result<()> { - Ok(()) - } - } - - struct NoopEventSource; - - #[async_trait] - impl EventSource for NoopEventSource { - async fn receive(&self) -> anyhow::Result { - Err(anyhow::anyhow!("no events")) - } - } - - fn create_test_config(mock_server: &MockServer) -> Config { - Config { - address: IpAddr::from([127, 0, 0, 1]), - port: 8080, - mempool_url: Url::parse("http://localhost:3000").unwrap(), - tx_submission_method: TxSubmissionMethod::Mempool, - ingress_kafka_properties: String::new(), - ingress_topic: String::new(), - audit_kafka_properties: String::new(), - audit_topic: String::new(), - user_operation_consumer_properties: Some(String::new()), - user_operation_consumer_group_id: "tips-user-operation".to_string(), - log_level: String::from("info"), - log_format: tips_core::logger::LogFormat::Pretty, - send_transaction_default_lifetime_seconds: 300, - simulation_rpc: mock_server.uri().parse().unwrap(), - metrics_addr: SocketAddr::from(([127, 0, 0, 1], 9002)), - block_time_milliseconds: 1000, - meter_bundle_timeout_ms: 5000, - validate_user_operation_timeout_ms: 2000, - builder_rpcs: vec![], - max_buffered_meter_bundle_responses: 100, - max_buffered_backrun_bundles: 100, - health_check_addr: SocketAddr::from(([127, 0, 0, 1], 8081)), - backrun_enabled: false, - raw_tx_forward_rpc: None, - chain_id: 11, - user_operation_topic: String::new(), - max_backrun_txs: 5, - max_backrun_gas_limit: 5000000, - bundle_cache_ttl: 20, - send_to_builder: false, - } - } - - async fn setup_rpc_server(mock: MockIngressApi) -> (HttpClient, ServerHandle) { - let server = ServerBuilder::default().build("127.0.0.1:0").await.unwrap(); - - let addr = server.local_addr().unwrap(); - let handle = server.start(mock.into_rpc()); - - let client = HttpClientBuilder::default() - .build(format!("http://{}", addr)) - .unwrap(); - - (client, handle) - } - - fn sample_user_operation_v06() -> serde_json::Value { - json!({ - "sender": "0x773d604960feccc5c2ce1e388595268187cf62bf", - "nonce": "0x19b0ffe729f0000000000000000", - "initCode": "0x9406cc6185a346906296840746125a0e449764545fbfb9cf000000000000000000000000f886bc0b4f161090096b82ac0c5eb7349add429d0000000000000000000000000000000000000000000000000000000000000000", - "callData": "0xb61d27f600000000000000000000000066519fcaee1ed65bc9e0acc25ccd900668d3ed490000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000443f84ac0e000000000000000000000000773d604960feccc5c2ce1e388595268187cf62bf000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000", - "callGasLimit": "0x5a3c", - "verificationGasLimit": "0x5b7c7", - "preVerificationGas": "0x1001744e6", - "maxFeePerGas": "0x889fca3c", - "maxPriorityFeePerGas": "0x1e8480", - "paymasterAndData": "0x", - "signature": "0x42eff6474dd0b7efd0ca3070e05ee0f3e3c6c665176b80c7768f59445d3415de30b65c4c6ae35c45822b726e8827a986765027e7e2d7d2a8d72c9cf0d23194b81c" - }) - } - - #[tokio::test] - async fn test_timeout_logic() { - let timeout_duration = Duration::from_millis(100); - - // Test a future that takes longer than the timeout - let slow_future = async { - tokio::time::sleep(Duration::from_millis(200)).await; - Ok::(create_test_meter_bundle_response()) - }; - - let result = timeout(timeout_duration, slow_future) - .await - .map_err(|_| { - EthApiError::InvalidParams("Timeout on requesting metering".into()).into_rpc_err() - }) - .map_err(|e| e.to_string()); - - assert!(result.is_err()); - let error_string = format!("{:?}", result.unwrap_err()); - assert!(error_string.contains("Timeout on requesting metering")); - } - - #[tokio::test] - async fn test_timeout_logic_success() { - let timeout_duration = Duration::from_millis(200); - - // Test a future that completes within the timeout - let fast_future = async { - tokio::time::sleep(Duration::from_millis(50)).await; - Ok::(create_test_meter_bundle_response()) - }; - - let result = timeout(timeout_duration, fast_future) - .await - .map_err(|_| { - EthApiError::InvalidParams("Timeout on requesting metering".into()).into_rpc_err() - }) - .map_err(|e| e.to_string()); - - assert!(result.is_ok()); - // we're assumging that `base_meterBundle` will not error hence the second unwrap - let res = result.unwrap().unwrap(); - assert_eq!(res, create_test_meter_bundle_response()); - } - - // Replicate a failed `meter_bundle` request and instead of returning an error, we return a default `MeterBundleResponse` - #[tokio::test] - async fn test_meter_bundle_success() { - let mock_server = MockServer::start().await; - - // Mock error response from base_meterBundle - Mock::given(method("POST")) - .respond_with(ResponseTemplate::new(500).set_body_json(serde_json::json!({ - "jsonrpc": "2.0", - "id": 1, - "error": { - "code": -32000, - "message": "Simulation failed" - } - }))) - .mount(&mock_server) - .await; - - let config = create_test_config(&mock_server); - - let provider: RootProvider = - RootProvider::new_http(mock_server.uri().parse().unwrap()); - - let providers = Providers { - mempool: provider.clone(), - simulation: provider.clone(), - raw_tx_forward: None, - }; - - let (audit_tx, _audit_rx) = mpsc::unbounded_channel(); - let (builder_tx, _builder_rx) = broadcast::channel(1); - let (backrun_tx, _backrun_rx) = broadcast::channel(1); - - let mempool_engine = Arc::new(MempoolEngine::::new( - Arc::new(RwLock::new(InMemoryMempool::new(PoolConfig::default()))), - Arc::new(NoopEventSource), - )); - - let service = IngressService::new( - providers, - MockQueue, - audit_tx, - builder_tx, - backrun_tx, - mempool_engine, - config, - ); - - let bundle = Bundle::default(); - let bundle_hash = B256::default(); - - let result = service.meter_bundle(&bundle, &bundle_hash).await; - - // Test that meter_bundle returns an error, but we handle it gracefully - assert!(result.is_err()); - let response = result.unwrap_or_else(|_| MeterBundleResponse::default()); - assert_eq!(response, MeterBundleResponse::default()); - } - - #[tokio::test] - async fn test_raw_tx_forward() { - let simulation_server = MockServer::start().await; - let forward_server = MockServer::start().await; - - // Mock error response from base_meterBundle - Mock::given(method("POST")) - .respond_with(ResponseTemplate::new(500).set_body_json(serde_json::json!({ - "jsonrpc": "2.0", - "id": 1, - "error": { - "code": -32000, - "message": "Simulation failed" - } - }))) - .mount(&simulation_server) - .await; - - // Mock forward endpoint - expect exactly 1 call - Mock::given(method("POST")) - .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({ - "jsonrpc": "2.0", - "id": 1, - "result": "0x0000000000000000000000000000000000000000000000000000000000000000" - }))) - .expect(1) - .mount(&forward_server) - .await; - - let mut config = create_test_config(&simulation_server); - config.tx_submission_method = TxSubmissionMethod::Kafka; // Skip mempool send - - let providers = Providers { - mempool: RootProvider::new_http(simulation_server.uri().parse().unwrap()), - simulation: RootProvider::new_http(simulation_server.uri().parse().unwrap()), - raw_tx_forward: Some(RootProvider::new_http( - forward_server.uri().parse().unwrap(), - )), - }; - - let (audit_tx, _audit_rx) = mpsc::unbounded_channel(); - let (builder_tx, _builder_rx) = broadcast::channel(1); - let (backrun_tx, _backrun_rx) = broadcast::channel(1); - - let mempool_engine = Arc::new(MempoolEngine::::new( - Arc::new(RwLock::new(InMemoryMempool::new(PoolConfig::default()))), - Arc::new(NoopEventSource), - )); - - let service = IngressService::new( - providers, - MockQueue, - audit_tx, - builder_tx, - backrun_tx, - mempool_engine, - config, - ); - - // Valid signed transaction bytes - let tx_bytes = Bytes::from_str("0x02f86c0d010183072335825208940000000000000000000000000000000000000000872386f26fc1000080c001a0cdb9e4f2f1ba53f9429077e7055e078cf599786e29059cd80c5e0e923bb2c114a01c90e29201e031baf1da66296c3a5c15c200bcb5e6c34da2f05f7d1778f8be07").unwrap(); - - let result = service.send_raw_transaction(tx_bytes).await; - assert!(result.is_ok()); - - // Wait for spawned forward task to complete - tokio::time::sleep(Duration::from_millis(100)).await; - - // wiremock automatically verifies expect(1) when forward_server is dropped - } - mock! { - pub IngressApi {} - - #[async_trait] - impl IngressApiServer for IngressApi { - async fn send_bundle(&self, bundle: Bundle) -> RpcResult; - async fn send_backrun_bundle(&self, bundle: Bundle) -> RpcResult; - async fn cancel_bundle(&self, request: CancelBundle) -> RpcResult<()>; - async fn send_raw_transaction(&self, tx: Bytes) -> RpcResult; - async fn send_user_operation( - &self, - user_operation: VersionedUserOperation, - entry_point: Address, - ) -> RpcResult>; - } - } - #[tokio::test] - async fn test_send_user_operation_accepts_valid_payload() { - let mut mock = MockIngressApi::new(); - mock.expect_send_user_operation() - .times(1) - .returning(|_, _| Ok(FixedBytes::ZERO)); - - let (client, _handle) = setup_rpc_server(mock).await; - - let user_op = sample_user_operation_v06(); - let entry_point = - account_abstraction_core::domain::entrypoints::version::EntryPointVersion::V06_ADDRESS; - - let result: Result, _> = client - .request("eth_sendUserOperation", (user_op, entry_point)) - .await; - - assert!(result.is_ok()); - } - - #[tokio::test] - async fn test_send_user_operation_rejects_invalid_payload() { - let mut mock = MockIngressApi::new(); - mock.expect_send_user_operation() - .times(0) - .returning(|_, _| Ok(FixedBytes::ZERO)); - - let (client, _handle) = setup_rpc_server(mock).await; - - let user_op = sample_user_operation_v06(); - - // Missing entry point argument should be rejected by the RPC layer - let result: Result, _> = - client.request("eth_sendUserOperation", (user_op,)).await; - - assert!(result.is_err()); - - let wrong_user_op = json!({ - "nonce": "0x19b0ffe729f0000000000000000", - "callGasLimit": "0x5a3c", - "verificationGasLimit": "0x5b7c7", - "preVerificationGas": "0x1001744e6", - "maxFeePerGas": "0x889fca3c", - "maxPriorityFeePerGas": "0x1e8480", - "paymasterAndData": "0x", - "signature": "0x42eff6474dd0b7efd0ca3070e05ee0f3e3c6c665176b80c7768f59445d3415de30b65c4c6ae35c45822b726e8827a986765027e7e2d7d2a8d72c9cf0d23194b81c" - }); - - let wrong_user_op_result: Result, _> = client - .request("eth_sendUserOperation", (wrong_user_op, Address::ZERO)) - .await; - - assert!(wrong_user_op_result.is_err()); - } - - #[test] - fn test_validate_backrun_bundle_rejects_invalid() { - // Too few transactions (need at least 2: target + backrun) - let result = validate_backrun_bundle_limits(1, 21000, 5, 5000000); - assert!(result.is_err()); - assert!(result.unwrap_err().contains("at least 2 transactions")); - - // Exceeds max tx count - let result = validate_backrun_bundle_limits(6, 21000, 5, 5000000); - assert!(result.is_err()); - assert!( - result - .unwrap_err() - .contains("exceeds max transaction count") - ); - - // Exceeds max gas limit - let result = validate_backrun_bundle_limits(2, 6000000, 5, 5000000); - assert!(result.is_err()); - assert!(result.unwrap_err().contains("exceeds max gas limit")); - } -} diff --git a/crates/ingress-rpc/src/validation.rs b/crates/ingress-rpc/src/validation.rs deleted file mode 100644 index 5ce10442..00000000 --- a/crates/ingress-rpc/src/validation.rs +++ /dev/null @@ -1,373 +0,0 @@ -use alloy_consensus::private::alloy_eips::{BlockId, BlockNumberOrTag}; -use alloy_primitives::{Address, B256, U256}; -use alloy_provider::{Provider, RootProvider}; -use async_trait::async_trait; -use base_reth_rpc_types::{EthApiError, SignError, extract_l1_info_from_tx}; -use jsonrpsee::core::RpcResult; -use op_alloy_network::Optimism; -use op_revm::l1block::L1BlockInfo; -use std::collections::HashSet; -use std::time::{Duration, SystemTime, UNIX_EPOCH}; -use tips_core::Bundle; -use tokio::time::Instant; -use tracing::warn; - -use crate::metrics::record_histogram; - -const MAX_BUNDLE_GAS: u64 = 25_000_000; - -/// Account info for a given address -pub struct AccountInfo { - pub balance: U256, - pub nonce: u64, - pub code_hash: B256, -} - -/// Interface for fetching account info for a given address -#[async_trait] -pub trait AccountInfoLookup: Send + Sync { - async fn fetch_account_info(&self, address: Address) -> RpcResult; -} - -/// Implementation of the `AccountInfoLookup` trait for the `RootProvider` -#[async_trait] -impl AccountInfoLookup for RootProvider { - async fn fetch_account_info(&self, address: Address) -> RpcResult { - let start = Instant::now(); - let account = self - .get_account(address) - .await - .map_err(|_| EthApiError::Signing(SignError::NoAccount))?; - record_histogram(start.elapsed(), "eth_getAccount".to_string()); - - Ok(AccountInfo { - balance: account.balance, - nonce: account.nonce, - code_hash: account.code_hash, - }) - } -} - -/// Interface for fetching L1 block info for a given block number -#[async_trait] -pub trait L1BlockInfoLookup: Send + Sync { - async fn fetch_l1_block_info(&self) -> RpcResult; -} - -/// Implementation of the `L1BlockInfoLookup` trait for the `RootProvider` -#[async_trait] -impl L1BlockInfoLookup for RootProvider { - async fn fetch_l1_block_info(&self) -> RpcResult { - let start = Instant::now(); - let block = self - .get_block(BlockId::Number(BlockNumberOrTag::Latest)) - .full() - .await - .map_err(|e| { - warn!(message = "failed to fetch latest block", err = %e); - EthApiError::InternalEthError.into_rpc_err() - })? - .ok_or_else(|| { - warn!(message = "empty latest block returned"); - EthApiError::InternalEthError.into_rpc_err() - })?; - record_histogram(start.elapsed(), "eth_getBlockByNumber".to_string()); - - let txs = block.transactions.clone(); - let first_tx = txs.first_transaction().ok_or_else(|| { - warn!(message = "block contains no transactions"); - EthApiError::InternalEthError.into_rpc_err() - })?; - - Ok(extract_l1_info_from_tx(&first_tx.clone()).map_err(|e| { - warn!(message = "failed to extract l1_info from tx", err = %e); - EthApiError::InternalEthError.into_rpc_err() - })?) - } -} - -/// Helper function to validate propeties of a bundle. A bundle is valid if it satisfies the following criteria: -/// - The bundle's max_timestamp is not more than 1 hour in the future -/// - The bundle's gas limit is not greater than the maximum allowed gas limit -/// - The bundle can only contain 3 transactions at once -/// - Partial transaction dropping is not supported, `dropping_tx_hashes` must be empty -/// - revert protection is not supported, all transaction hashes must be in `reverting_tx_hashes` -pub fn validate_bundle(bundle: &Bundle, bundle_gas: u64, tx_hashes: Vec) -> RpcResult<()> { - // Don't allow bundles to be submitted over 1 hour into the future - // TODO: make the window configurable - let valid_timestamp_window = SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap() - .as_secs() - + Duration::from_secs(3600).as_secs(); - if let Some(max_timestamp) = bundle.max_timestamp - && max_timestamp > valid_timestamp_window - { - return Err(EthApiError::InvalidParams( - "Bundle cannot be more than 1 hour in the future".into(), - ) - .into_rpc_err()); - } - - // Check max gas limit for the entire bundle - if bundle_gas > MAX_BUNDLE_GAS { - return Err( - EthApiError::InvalidParams("Bundle gas limit exceeds maximum allowed".into()) - .into_rpc_err(), - ); - } - - // Can only provide 3 transactions at once - if bundle.txs.len() > 3 { - return Err( - EthApiError::InvalidParams("Bundle can only contain 3 transactions".into()) - .into_rpc_err(), - ); - } - - // Partial transaction dropping is not supported, `dropping_tx_hashes` must be empty - if !bundle.dropping_tx_hashes.is_empty() { - return Err(EthApiError::InvalidParams( - "Partial transaction dropping is not supported".into(), - ) - .into_rpc_err()); - } - - // revert protection: all transaction hashes must be in `reverting_tx_hashes` - let reverting_tx_hashes_set: HashSet<_> = bundle.reverting_tx_hashes.iter().collect(); - let tx_hashes_set: HashSet<_> = tx_hashes.iter().collect(); - if reverting_tx_hashes_set != tx_hashes_set { - return Err(EthApiError::InvalidParams( - "Revert protection is not supported. reverting_tx_hashes must include all hashes" - .into(), - ) - .into_rpc_err()); - } - - Ok(()) -} - -#[cfg(test)] -mod tests { - use super::*; - use alloy_consensus::SignableTransaction; - use alloy_consensus::TxEip1559; - use alloy_consensus::transaction::SignerRecoverable; - use alloy_primitives::Bytes; - use alloy_primitives::bytes; - use alloy_signer_local::PrivateKeySigner; - use op_alloy_consensus::OpTxEnvelope; - use op_alloy_network::TxSignerSync; - use op_alloy_network::eip2718::Encodable2718; - use std::time::{SystemTime, UNIX_EPOCH}; - - #[tokio::test] - async fn test_err_bundle_max_timestamp_too_far_in_the_future() { - let current_time = SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap() - .as_secs(); - let too_far_in_the_future = current_time + 3601; - let bundle = Bundle { - txs: vec![], - max_timestamp: Some(too_far_in_the_future), - ..Default::default() - }; - assert_eq!( - validate_bundle(&bundle, 0, vec![]), - Err(EthApiError::InvalidParams( - "Bundle cannot be more than 1 hour in the future".into() - ) - .into_rpc_err()) - ); - } - - #[tokio::test] - async fn test_err_bundle_max_gas_limit_too_high() { - let signer = PrivateKeySigner::random(); - let mut encoded_txs = vec![]; - let mut tx_hashes = vec![]; - - // Create transactions that collectively exceed MAX_BUNDLE_GAS (25M) - // Each transaction uses 4M gas, so 8 transactions = 32M gas > 25M limit - let gas = 4_000_000; - let mut total_gas = 0u64; - for _ in 0..8 { - let mut tx = TxEip1559 { - chain_id: 1, - nonce: 0, - gas_limit: gas, - max_fee_per_gas: 200000u128, - max_priority_fee_per_gas: 100000u128, - to: Address::random().into(), - value: U256::from(1000000u128), - access_list: Default::default(), - input: bytes!("").clone(), - }; - total_gas = total_gas.saturating_add(gas); - - let signature = signer.sign_transaction_sync(&mut tx).unwrap(); - let envelope = OpTxEnvelope::Eip1559(tx.into_signed(signature)); - let tx_hash = envelope.clone().try_into_recovered().unwrap().tx_hash(); - tx_hashes.push(tx_hash); - - // Encode the transaction - let mut encoded = vec![]; - envelope.encode_2718(&mut encoded); - encoded_txs.push(Bytes::from(encoded)); - } - - let bundle = Bundle { - txs: encoded_txs, - block_number: 0, - min_timestamp: None, - max_timestamp: None, - reverting_tx_hashes: vec![], - ..Default::default() - }; - - // Test should fail due to exceeding gas limit - let result = validate_bundle(&bundle, total_gas, tx_hashes); - assert!(result.is_err()); - if let Err(e) = result { - let error_message = format!("{e:?}"); - assert!(error_message.contains("Bundle gas limit exceeds maximum allowed")); - } - } - - #[tokio::test] - async fn test_err_bundle_too_many_transactions() { - let signer = PrivateKeySigner::random(); - let mut encoded_txs = vec![]; - let mut tx_hashes = vec![]; - - let gas = 4_000_000; - let mut total_gas = 0u64; - for _ in 0..4 { - let mut tx = TxEip1559 { - chain_id: 1, - nonce: 0, - gas_limit: gas, - max_fee_per_gas: 200000u128, - max_priority_fee_per_gas: 100000u128, - to: Address::random().into(), - value: U256::from(1000000u128), - access_list: Default::default(), - input: bytes!("").clone(), - }; - total_gas = total_gas.saturating_add(gas); - - let signature = signer.sign_transaction_sync(&mut tx).unwrap(); - let envelope = OpTxEnvelope::Eip1559(tx.into_signed(signature)); - let tx_hash = envelope.clone().try_into_recovered().unwrap().tx_hash(); - tx_hashes.push(tx_hash); - - // Encode the transaction - let mut encoded = vec![]; - envelope.encode_2718(&mut encoded); - encoded_txs.push(Bytes::from(encoded)); - } - - let bundle = Bundle { - txs: encoded_txs, - block_number: 0, - min_timestamp: None, - max_timestamp: None, - reverting_tx_hashes: vec![], - ..Default::default() - }; - - // Test should fail due to exceeding gas limit - let result = validate_bundle(&bundle, total_gas, tx_hashes); - assert!(result.is_err()); - if let Err(e) = result { - let error_message = format!("{e:?}"); - assert!(error_message.contains("Bundle can only contain 3 transactions")); - } - } - - #[tokio::test] - async fn test_err_bundle_partial_transaction_dropping_not_supported() { - let bundle = Bundle { - txs: vec![], - dropping_tx_hashes: vec![B256::random()], - ..Default::default() - }; - assert_eq!( - validate_bundle(&bundle, 0, vec![]), - Err( - EthApiError::InvalidParams("Partial transaction dropping is not supported".into()) - .into_rpc_err() - ) - ); - } - - #[tokio::test] - async fn test_err_bundle_not_all_tx_hashes_in_reverting_tx_hashes() { - let signer = PrivateKeySigner::random(); - let mut encoded_txs = vec![]; - let mut tx_hashes = vec![]; - - let gas = 4_000_000; - let mut total_gas = 0u64; - for _ in 0..4 { - let mut tx = TxEip1559 { - chain_id: 1, - nonce: 0, - gas_limit: gas, - max_fee_per_gas: 200000u128, - max_priority_fee_per_gas: 100000u128, - to: Address::random().into(), - value: U256::from(1000000u128), - access_list: Default::default(), - input: bytes!("").clone(), - }; - total_gas = total_gas.saturating_add(gas); - - let signature = signer.sign_transaction_sync(&mut tx).unwrap(); - let envelope = OpTxEnvelope::Eip1559(tx.into_signed(signature)); - let tx_hash = envelope.clone().try_into_recovered().unwrap().tx_hash(); - tx_hashes.push(tx_hash); - - // Encode the transaction - let mut encoded = vec![]; - envelope.encode_2718(&mut encoded); - encoded_txs.push(Bytes::from(encoded)); - } - - let bundle = Bundle { - txs: encoded_txs, - block_number: 0, - min_timestamp: None, - max_timestamp: None, - reverting_tx_hashes: tx_hashes[..2].to_vec(), - ..Default::default() - }; - - // Test should fail due to exceeding gas limit - let result = validate_bundle(&bundle, total_gas, tx_hashes); - assert!(result.is_err()); - if let Err(e) = result { - let error_message = format!("{e:?}"); - assert!(error_message.contains("Bundle can only contain 3 transactions")); - } - } - - #[tokio::test] - async fn test_decode_tx_rejects_empty_bytes() { - // Test that empty bytes fail to decode - use op_alloy_network::eip2718::Decodable2718; - let empty_bytes = Bytes::new(); - let result = OpTxEnvelope::decode_2718(&mut empty_bytes.as_ref()); - assert!(result.is_err(), "Empty bytes should fail decoding"); - } - - #[tokio::test] - async fn test_decode_tx_rejects_invalid_bytes() { - // Test that malformed bytes fail to decode - use op_alloy_network::eip2718::Decodable2718; - let invalid_bytes = Bytes::from(vec![0x01, 0x02, 0x03]); - let result = OpTxEnvelope::decode_2718(&mut invalid_bytes.as_ref()); - assert!(result.is_err(), "Invalid bytes should fail decoding"); - } -} diff --git a/crates/system-tests/Cargo.toml b/crates/system-tests/Cargo.toml deleted file mode 100644 index 6163b29e..00000000 --- a/crates/system-tests/Cargo.toml +++ /dev/null @@ -1,49 +0,0 @@ -[package] -name = "tips-system-tests" -version.workspace = true -edition.workspace = true -rust-version.workspace = true -license.workspace = true - -[lib] -path = "src/lib.rs" - -[dependencies] -hex = "0.4.3" -rand = "0.8" -dashmap = "6.0" -indicatif = "0.17" -rand_chacha = "0.3" -url = { workspace = true } -tips-core = { workspace = true } -bytes = { workspace = true } -async-trait = { workspace = true } -tips-audit-lib = { workspace = true } -alloy-network = { workspace = true } -tips-ingress-rpc-lib.workspace = true -op-alloy-network = { workspace = true } -alloy-signer-local = { workspace = true } -aws-credential-types = { workspace = true } -serde = { workspace = true, features = ["std", "derive"] } -tokio = { workspace = true, features = ["full"] } -tracing = { workspace = true, features = ["std"] } -anyhow = { workspace = true, features = ["std"] } -uuid = { workspace = true, features = ["v5", "serde"] } -serde_json = { workspace = true, features = ["std"] } -reqwest = { version = "0.12.12", features = ["json"] } -rdkafka = { workspace = true, features = ["tokio", "libz", "zstd", "ssl-vendored"] } -alloy-consensus = { workspace = true, features = ["std"] } -alloy-provider = { workspace = true, features = ["reqwest"] } -jsonrpsee = { workspace = true, features = ["server", "macros"] } -clap = { version = "4.5", features = ["std", "derive", "env"] } -op-alloy-consensus = { workspace = true, features = ["std", "k256", "serde"] } -alloy-primitives = { workspace = true, features = ["map-foldhash", "serde"] } -aws-config = { workspace = true, features = ["default-https-client", "rt-tokio"] } -tracing-subscriber = { workspace = true, features = ["std", "fmt", "env-filter", "json"] } -aws-sdk-s3 = { workspace = true, features = ["rustls", "default-https-client", "rt-tokio"] } - -[dev-dependencies] -serial_test = "3" -tokio = { workspace = true, features = ["full", "test-util"] } -testcontainers = { workspace = true, features = ["blocking"] } -testcontainers-modules = { workspace = true, features = ["postgres", "kafka", "minio"] } diff --git a/crates/system-tests/METRICS.md b/crates/system-tests/METRICS.md deleted file mode 100644 index 49f9a0a7..00000000 --- a/crates/system-tests/METRICS.md +++ /dev/null @@ -1,133 +0,0 @@ -# TIPS Load Testing - -Multi-wallet concurrent load testing tool for measuring TIPS performance. - -## Quick Start - -```bash -# 1. Build -cargo build --release --bin load-test - -# 2. Setup wallets -./target/release/load-test setup \ - --master-key 0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d \ - --output wallets.json - -# 3. Run load test -./target/release/load-test load --wallets wallets.json -``` - ---- - -## Configuration Options - -### Setup Command - -Create and fund test wallets from a master wallet. Test wallets are saved to allow test reproducibility and avoid the need to create new wallets for every test run. - -**Usage:** -```bash -./target/release/load-test setup --master-key --output [OPTIONS] -``` - -**Options:** - -| Flag | Description | Default | Example | -|------|-------------|---------|---------| -| `--master-key` | Private key of funded wallet (required) | - | `0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d` | -| `--output` | Save wallets to JSON file (required) | - | `wallets.json` | -| `--sequencer` | L2 sequencer RPC URL | `http://localhost:8547` | `http://localhost:8547` | -| `--num-wallets` | Number of wallets to create | `10` | `100` | -| `--fund-amount` | ETH to fund each wallet | `0.1` | `0.5` | - -**Environment Variables:** -- `MASTER_KEY` - Alternative to `--master-key` flag -- `SEQUENCER_URL` - Alternative to `--sequencer` flag - -### Load Command - -Run load test with funded wallets. Use the `--seed` flag to set the RNG seed for test reproducibility. - -**Usage:** -```bash -./target/release/load-test load --wallets [OPTIONS] -``` - -**Options:** - -| Flag | Description | Default | Example | -|------|-------------|---------|---------| -| `--wallets` | Path to wallets JSON file (required) | - | `wallets.json` | -| `--target` | TIPS ingress RPC URL | `http://localhost:8080` | `http://localhost:8080` | -| `--sequencer` | L2 sequencer RPC URL | `http://localhost:8547` | `http://localhost:8547` | -| `--rate` | Target transaction rate (tx/s) | `100` | `500` | -| `--duration` | Test duration in seconds | `60` | `100` | -| `--tx-timeout` | Timeout for tx inclusion (seconds) | `60` | `120` | -| `--seed` | Random seed for reproducibility | (none) | `42` | -| `--output` | Save metrics to JSON file | (none) | `metrics.json` | - -**Environment Variables:** -- `INGRESS_URL` - Alternative to `--target` flag -- `SEQUENCER_URL` - Alternative to `--sequencer` flag - ---- ---- - -## Metrics Explained - -### Output Example - -``` -Load Test Results -━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ -Configuration: - Target: http://localhost:8080 - Sequencer: http://localhost:8547 - Wallets: 100 - Target Rate: 100 tx/s - Duration: 60s - TX Timeout: 60s - -Throughput: - Sent: 100.0 tx/s (6000 total) - Included: 98.5 tx/s (5910 total) - Success Rate: 98.5% - -Transaction Results: - Included: 5910 (98.5%) - Reverted: 10 (0.2%) - Timed Out: 70 (1.2%) - Send Errors: 10 (0.1%) -``` - -### Metrics Definitions - -**Throughput:** -- `Sent Rate` - Transactions sent to TIPS per second -- `Included Rate` - Transactions included in blocks per second -- `Success Rate` - Percentage of sent transactions that were included - -**Transaction Results:** -- `Included` - Successfully included in a block with status == true -- `Reverted` - Included in a block but transaction reverted (status == false) -- `Timed Out` - Not included within timeout period -- `Send Errors` - Failed to send to TIPS RPC - ---- - -## Architecture - -``` -Sender Tasks (1 per wallet) Receipt Poller - │ │ - ▼ ▼ - Send to TIPS ──► Tracker ◄── Poll sequencer every 2s - (retry 3x) (pending) │ - │ │ ├─ status=true → included - │ │ ├─ status=false → reverted - │ │ └─ timeout → timed_out - ▼ ▼ - rate/N tx/s Calculate Results → Print Summary -``` - ---- diff --git a/crates/system-tests/README.md b/crates/system-tests/README.md deleted file mode 100644 index d7f410a0..00000000 --- a/crates/system-tests/README.md +++ /dev/null @@ -1,30 +0,0 @@ -# System Tests (Integration Suite) - -Integration coverage for TIPS ingress RPC. Tests talk to the services started by `just start-all`. - -## What we test -- `test_client_can_connect_to_tips` – RPC connectivity. -- `test_send_raw_transaction_accepted` – `eth_sendRawTransaction` lands on-chain with success receipt. -- `test_send_bundle_accepted` – single‑tx bundle via `eth_sendBackrunBundle` returns the correct bundle hash, audit event, and on-chain inclusion. -- `test_send_bundle_with_two_transactions` – multi-tx bundle (2 txs) flows through audit and lands on-chain. - -Each test confirms: -1. The response hash equals `keccak256` of the tx hashes. -2. The bundle audit event is emitted to Kafka. -3. All transactions are included on-chain with successful receipts. - -## How to run -```bash -# Start infrastructure (see ../../SETUP.md for full instructions) -# - just sync && just start-all -# - builder-playground + op-rbuilder are running - -# Run the tests -INTEGRATION_TESTS=1 cargo test --package tips-system-tests --test integration_tests -``` - -**Note:** Tests that share the funded wallet use `#[serial]` to avoid nonce conflicts. - -Defaults: -- Kafka configs: `docker/host-*.properties` (override with the standard `TIPS_INGRESS_KAFKA_*` env vars if needed). -- URLs: `http://localhost:8080` ingress, `http://localhost:8547` sequencer (override via `INGRESS_URL` / `SEQUENCER_URL`). diff --git a/crates/system-tests/src/bin/load-test.rs b/crates/system-tests/src/bin/load-test.rs deleted file mode 100644 index 50a36d28..00000000 --- a/crates/system-tests/src/bin/load-test.rs +++ /dev/null @@ -1,13 +0,0 @@ -use anyhow::Result; -use clap::Parser; -use tips_system_tests::load_test::{config, load, setup}; - -#[tokio::main] -async fn main() -> Result<()> { - let cli = config::Cli::parse(); - - match cli.command { - config::Commands::Setup(args) => setup::run(args).await, - config::Commands::Load(args) => load::run(args).await, - } -} diff --git a/crates/system-tests/src/client/mod.rs b/crates/system-tests/src/client/mod.rs deleted file mode 100644 index 58dabd21..00000000 --- a/crates/system-tests/src/client/mod.rs +++ /dev/null @@ -1,3 +0,0 @@ -pub mod tips_rpc; - -pub use tips_rpc::TipsRpcClient; diff --git a/crates/system-tests/src/client/tips_rpc.rs b/crates/system-tests/src/client/tips_rpc.rs deleted file mode 100644 index cbe5a23a..00000000 --- a/crates/system-tests/src/client/tips_rpc.rs +++ /dev/null @@ -1,53 +0,0 @@ -use alloy_network::Network; -use alloy_primitives::{Bytes, TxHash}; -use alloy_provider::{Provider, RootProvider}; -use anyhow::Result; -use tips_core::{Bundle, BundleHash, CancelBundle}; - -/// Client for TIPS-specific RPC methods (eth_sendBundle, eth_cancelBundle) -/// -/// Wraps a RootProvider to add TIPS functionality while preserving access -/// to standard Ethereum JSON-RPC methods via provider(). -#[derive(Clone)] -pub struct TipsRpcClient { - provider: RootProvider, -} - -impl TipsRpcClient { - pub fn new(provider: RootProvider) -> Self { - Self { provider } - } - - pub async fn send_raw_transaction(&self, signed_tx: Bytes) -> Result { - let tx_hex = format!("0x{}", hex::encode(&signed_tx)); - self.provider - .raw_request("eth_sendRawTransaction".into(), [tx_hex]) - .await - .map_err(Into::into) - } - - pub async fn send_bundle(&self, bundle: Bundle) -> Result { - self.provider - .raw_request("eth_sendBundle".into(), [bundle]) - .await - .map_err(Into::into) - } - - pub async fn send_backrun_bundle(&self, bundle: Bundle) -> Result { - self.provider - .raw_request("eth_sendBackrunBundle".into(), [bundle]) - .await - .map_err(Into::into) - } - - pub async fn cancel_bundle(&self, request: CancelBundle) -> Result { - self.provider - .raw_request("eth_cancelBundle".into(), [request]) - .await - .map_err(Into::into) - } - - pub fn provider(&self) -> &RootProvider { - &self.provider - } -} diff --git a/crates/system-tests/src/fixtures/mod.rs b/crates/system-tests/src/fixtures/mod.rs deleted file mode 100644 index 3094ef9d..00000000 --- a/crates/system-tests/src/fixtures/mod.rs +++ /dev/null @@ -1,3 +0,0 @@ -pub mod transactions; - -pub use transactions::*; diff --git a/crates/system-tests/src/fixtures/transactions.rs b/crates/system-tests/src/fixtures/transactions.rs deleted file mode 100644 index 6fed6083..00000000 --- a/crates/system-tests/src/fixtures/transactions.rs +++ /dev/null @@ -1,73 +0,0 @@ -use alloy_consensus::{SignableTransaction, TxEip1559}; -use alloy_primitives::{Address, Bytes, U256}; -use alloy_provider::{ProviderBuilder, RootProvider}; -use alloy_signer_local::PrivateKeySigner; -use anyhow::Result; -use op_alloy_network::eip2718::Encodable2718; -use op_alloy_network::{Optimism, TxSignerSync}; - -/// Create an Optimism RPC provider from a URL string -/// -/// This is a convenience function to avoid repeating the provider setup -/// pattern across tests and runner code. -pub fn create_optimism_provider(url: &str) -> Result> { - Ok(ProviderBuilder::new() - .disable_recommended_fillers() - .network::() - .connect_http(url.parse()?)) -} - -pub fn create_test_signer() -> PrivateKeySigner { - "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80" - .parse() - .expect("Valid test private key") -} - -pub fn create_funded_signer() -> PrivateKeySigner { - // This is the same account used in justfile that has funds in builder-playground - "0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d" - .parse() - .expect("Valid funded private key") -} - -pub fn create_signed_transaction( - signer: &PrivateKeySigner, - to: Address, - value: U256, - nonce: u64, - gas_limit: u64, - max_fee_per_gas: u128, -) -> Result { - let mut tx = TxEip1559 { - chain_id: 13, // Local builder-playground chain ID - nonce, - gas_limit, - max_fee_per_gas, - max_priority_fee_per_gas: max_fee_per_gas / 10, // 10% of max fee as priority fee - to: to.into(), - value, - access_list: Default::default(), - input: Default::default(), - }; - - let signature = signer.sign_transaction_sync(&mut tx)?; - - let envelope = op_alloy_consensus::OpTxEnvelope::Eip1559(tx.into_signed(signature)); - - let mut buf = Vec::new(); - envelope.encode_2718(&mut buf); - - Ok(Bytes::from(buf)) -} - -/// Create a simple load test transaction with standard defaults: -/// - value: 1000 wei (small test amount) -/// - gas_limit: 21000 (standard transfer) -/// - max_fee_per_gas: 1 gwei -pub fn create_load_test_transaction( - signer: &PrivateKeySigner, - to: Address, - nonce: u64, -) -> Result { - create_signed_transaction(signer, to, U256::from(1000), nonce, 21000, 1_000_000_000) -} diff --git a/crates/system-tests/src/lib.rs b/crates/system-tests/src/lib.rs deleted file mode 100644 index c05f0a34..00000000 --- a/crates/system-tests/src/lib.rs +++ /dev/null @@ -1,3 +0,0 @@ -pub mod client; -pub mod fixtures; -pub mod load_test; diff --git a/crates/system-tests/src/load_test/config.rs b/crates/system-tests/src/load_test/config.rs deleted file mode 100644 index 02543e3e..00000000 --- a/crates/system-tests/src/load_test/config.rs +++ /dev/null @@ -1,76 +0,0 @@ -use clap::{Parser, Subcommand}; -use std::path::PathBuf; - -#[derive(Parser)] -#[command(name = "load-test")] -#[command(about = "Load testing tool for TIPS ingress service", long_about = None)] -pub struct Cli { - #[command(subcommand)] - pub command: Commands, -} - -#[derive(Subcommand)] -pub enum Commands { - /// Setup: Fund N wallets from a master wallet - Setup(SetupArgs), - /// Load: Run load test with funded wallets - Load(LoadArgs), -} - -#[derive(Parser)] -pub struct SetupArgs { - /// Master wallet private key (must have funds) - #[arg(long, env = "MASTER_KEY")] - pub master_key: String, - - /// Sequencer RPC URL - #[arg(long, env = "SEQUENCER_URL", default_value = "http://localhost:8547")] - pub sequencer: String, - - /// Number of wallets to create and fund - #[arg(long, default_value = "10")] - pub num_wallets: usize, - - /// Amount of ETH to fund each wallet - #[arg(long, default_value = "0.1")] - pub fund_amount: f64, - - /// Output file for wallet data (required) - #[arg(long)] - pub output: PathBuf, -} - -#[derive(Parser)] -pub struct LoadArgs { - /// TIPS ingress RPC URL - #[arg(long, env = "INGRESS_URL", default_value = "http://localhost:8080")] - pub target: String, - - /// Sequencer RPC URL (for nonce fetching and receipt polling) - #[arg(long, env = "SEQUENCER_URL", default_value = "http://localhost:8547")] - pub sequencer: String, - - /// Path to wallets JSON file (required) - #[arg(long)] - pub wallets: PathBuf, - - /// Target transaction rate (transactions per second) - #[arg(long, default_value = "100")] - pub rate: u64, - - /// Test duration in seconds - #[arg(long, default_value = "60")] - pub duration: u64, - - /// Timeout for transaction inclusion (seconds) - #[arg(long, default_value = "60")] - pub tx_timeout: u64, - - /// Random seed for reproducibility - #[arg(long)] - pub seed: Option, - - /// Output file for metrics (JSON) - #[arg(long)] - pub output: Option, -} diff --git a/crates/system-tests/src/load_test/load.rs b/crates/system-tests/src/load_test/load.rs deleted file mode 100644 index ca5045f6..00000000 --- a/crates/system-tests/src/load_test/load.rs +++ /dev/null @@ -1,133 +0,0 @@ -use super::config::LoadArgs; -use super::metrics::{TestConfig, calculate_results}; -use super::output::{print_results, save_results}; -use super::poller::ReceiptPoller; -use super::sender::SenderTask; -use super::tracker::TransactionTracker; -use super::wallet::load_wallets; -use crate::client::TipsRpcClient; -use crate::fixtures::create_optimism_provider; -use anyhow::{Context, Result}; -use indicatif::{ProgressBar, ProgressStyle}; -use rand::SeedableRng; -use rand_chacha::ChaCha8Rng; -use std::sync::Arc; -use std::time::Duration; - -pub async fn run(args: LoadArgs) -> Result<()> { - let wallets = load_wallets(&args.wallets).context("Failed to load wallets")?; - - if wallets.is_empty() { - anyhow::bail!("No wallets found in file. Run 'setup' command first."); - } - - let num_wallets = wallets.len(); - - let sequencer = create_optimism_provider(&args.sequencer)?; - - let tips_provider = create_optimism_provider(&args.target)?; - let tips_client = TipsRpcClient::new(tips_provider); - - let tracker = TransactionTracker::new(Duration::from_secs(args.duration)); - - let rate_per_wallet = args.rate as f64 / num_wallets as f64; - - let pb = ProgressBar::new(args.duration); - pb.set_style( - ProgressStyle::default_bar() - .template("[{elapsed_precise}] {bar:40.cyan/blue} {pos}/{len}s | Sent: {msg}") - .unwrap() - .progress_chars("##-"), - ); - - let poller = ReceiptPoller::new( - sequencer.clone(), - Arc::clone(&tracker), - Duration::from_secs(args.tx_timeout), - ); - let poller_handle = tokio::spawn(async move { poller.run().await }); - - let mut sender_handles = Vec::new(); - - for (i, wallet) in wallets.into_iter().enumerate() { - let rng = match args.seed { - Some(seed) => ChaCha8Rng::seed_from_u64(seed + i as u64), - None => ChaCha8Rng::from_entropy(), - }; - - let sender = SenderTask::new( - wallet, - tips_client.clone(), - sequencer.clone(), - rate_per_wallet, - Duration::from_secs(args.duration), - Arc::clone(&tracker), - rng, - ); - - let handle = tokio::spawn(async move { sender.run().await }); - - sender_handles.push(handle); - } - - let pb_tracker = Arc::clone(&tracker); - let pb_handle = tokio::spawn(async move { - let mut interval = tokio::time::interval(Duration::from_secs(1)); - loop { - interval.tick().await; - let elapsed = pb_tracker.elapsed().as_secs(); - let sent = pb_tracker.total_sent(); - pb.set_position(elapsed); - pb.set_message(format!("{sent}")); - - if pb_tracker.is_test_completed() { - break; - } - } - pb.finish_with_message("Complete"); - }); - - for handle in sender_handles { - handle.await??; - } - - tracker.mark_test_completed(); - - pb_handle.await?; - - let grace_period = Duration::from_secs(args.tx_timeout + 10); - match tokio::time::timeout(grace_period, poller_handle).await { - Ok(Ok(Ok(()))) => { - println!("✅ All transactions resolved"); - } - Ok(Ok(Err(e))) => { - println!("⚠️ Poller error: {e}"); - } - Ok(Err(e)) => { - println!("⚠️ Poller panicked: {e}"); - } - Err(_) => { - println!("⏱️ Grace period expired, some transactions may still be pending"); - } - } - - let config = TestConfig { - target: args.target.clone(), - sequencer: args.sequencer.clone(), - wallets: num_wallets, - target_rate: args.rate, - duration_secs: args.duration, - tx_timeout_secs: args.tx_timeout, - seed: args.seed, - }; - - let results = calculate_results(&tracker, config); - print_results(&results); - - // Save results if output file specified - if let Some(output_path) = args.output.as_ref() { - save_results(&results, output_path)?; - } - - Ok(()) -} diff --git a/crates/system-tests/src/load_test/metrics.rs b/crates/system-tests/src/load_test/metrics.rs deleted file mode 100644 index 53415c91..00000000 --- a/crates/system-tests/src/load_test/metrics.rs +++ /dev/null @@ -1,78 +0,0 @@ -use super::tracker::TransactionTracker; -use serde::{Deserialize, Serialize}; -use std::sync::Arc; - -#[derive(Debug, Serialize, Deserialize)] -pub struct TestResults { - pub config: TestConfig, - pub results: ThroughputResults, - pub errors: ErrorResults, -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct TestConfig { - pub target: String, - pub sequencer: String, - pub wallets: usize, - pub target_rate: u64, - pub duration_secs: u64, - pub tx_timeout_secs: u64, - pub seed: Option, -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct ThroughputResults { - pub sent_rate: f64, - pub included_rate: f64, - pub total_sent: u64, - pub total_included: u64, - pub total_reverted: u64, - pub total_pending: u64, - pub total_timed_out: u64, - pub success_rate: f64, - pub actual_duration_secs: f64, -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct ErrorResults { - pub send_errors: u64, - pub reverted: u64, - pub timed_out: u64, -} - -pub fn calculate_results(tracker: &Arc, config: TestConfig) -> TestResults { - let actual_duration = tracker.elapsed(); - let total_sent = tracker.total_sent(); - let total_included = tracker.total_included(); - let total_reverted = tracker.total_reverted(); - let total_timed_out = tracker.total_timed_out(); - let send_errors = tracker.total_send_errors(); - - let sent_rate = total_sent as f64 / actual_duration.as_secs_f64(); - let included_rate = total_included as f64 / actual_duration.as_secs_f64(); - let success_rate = if total_sent > 0 { - total_included as f64 / total_sent as f64 - } else { - 0.0 - }; - - TestResults { - config, - results: ThroughputResults { - sent_rate, - included_rate, - total_sent, - total_included, - total_reverted, - total_pending: tracker.total_pending(), - total_timed_out, - success_rate, - actual_duration_secs: actual_duration.as_secs_f64(), - }, - errors: ErrorResults { - send_errors, - reverted: total_reverted, - timed_out: total_timed_out, - }, - } -} diff --git a/crates/system-tests/src/load_test/mod.rs b/crates/system-tests/src/load_test/mod.rs deleted file mode 100644 index 896b4156..00000000 --- a/crates/system-tests/src/load_test/mod.rs +++ /dev/null @@ -1,9 +0,0 @@ -pub mod config; -pub mod load; -pub mod metrics; -pub mod output; -pub mod poller; -pub mod sender; -pub mod setup; -pub mod tracker; -pub mod wallet; diff --git a/crates/system-tests/src/load_test/output.rs b/crates/system-tests/src/load_test/output.rs deleted file mode 100644 index 60e75640..00000000 --- a/crates/system-tests/src/load_test/output.rs +++ /dev/null @@ -1,67 +0,0 @@ -use super::metrics::TestResults; -use anyhow::{Context, Result}; -use std::fs; -use std::path::Path; - -pub fn print_results(results: &TestResults) { - println!("\n"); - println!("Load Test Results"); - println!("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"); - - println!("Configuration:"); - println!(" Target: {}", results.config.target); - println!(" Sequencer: {}", results.config.sequencer); - println!(" Wallets: {}", results.config.wallets); - println!(" Target Rate: {} tx/s", results.config.target_rate); - println!(" Duration: {}s", results.config.duration_secs); - println!(" TX Timeout: {}s", results.config.tx_timeout_secs); - if let Some(seed) = results.config.seed { - println!(" Seed: {seed}"); - } - - println!("\nThroughput:"); - println!( - " Sent: {:.1} tx/s ({} total)", - results.results.sent_rate, results.results.total_sent - ); - println!( - " Included: {:.1} tx/s ({} total)", - results.results.included_rate, results.results.total_included - ); - println!( - " Success Rate: {:.1}%", - results.results.success_rate * 100.0 - ); - - println!("\nTransaction Results:"); - println!( - " Included: {} ({:.1}%)", - results.results.total_included, - (results.results.total_included as f64 / results.results.total_sent as f64) * 100.0 - ); - if results.results.total_reverted > 0 { - println!( - " Reverted: {} ({:.1}%)", - results.results.total_reverted, - (results.results.total_reverted as f64 / results.results.total_sent as f64) * 100.0 - ); - } - println!( - " Timed Out: {} ({:.1}%)", - results.results.total_timed_out, - (results.results.total_timed_out as f64 / results.results.total_sent as f64) * 100.0 - ); - println!(" Send Errors: {}", results.errors.send_errors); - if results.results.total_pending > 0 { - println!(" Still Pending: {}", results.results.total_pending); - } - - println!("\n"); -} - -pub fn save_results(results: &TestResults, path: &Path) -> Result<()> { - let json = serde_json::to_string_pretty(results).context("Failed to serialize results")?; - fs::write(path, json).context("Failed to write results file")?; - println!("💾 Metrics saved to: {}", path.display()); - Ok(()) -} diff --git a/crates/system-tests/src/load_test/poller.rs b/crates/system-tests/src/load_test/poller.rs deleted file mode 100644 index 43014a92..00000000 --- a/crates/system-tests/src/load_test/poller.rs +++ /dev/null @@ -1,77 +0,0 @@ -use super::tracker::TransactionTracker; -use alloy_network::ReceiptResponse; -use alloy_provider::{Provider, RootProvider}; -use anyhow::Result; -use op_alloy_network::Optimism; -use std::sync::Arc; -use std::time::Duration; -use tracing::debug; - -pub struct ReceiptPoller { - sequencer: RootProvider, - tracker: Arc, - timeout: Duration, -} - -impl ReceiptPoller { - pub fn new( - sequencer: RootProvider, - tracker: Arc, - timeout: Duration, - ) -> Self { - Self { - sequencer, - tracker, - timeout, - } - } - - pub async fn run(self) -> Result<()> { - let mut interval = tokio::time::interval(Duration::from_secs(2)); // Block time - - loop { - interval.tick().await; - - let pending_txs = self.tracker.get_pending(); - - for (tx_hash, send_time) in pending_txs { - let elapsed = send_time.elapsed(); - - if elapsed > self.timeout { - self.tracker.record_timeout(tx_hash); - debug!("Transaction timed out: {:?}", tx_hash); - continue; - } - - match self.sequencer.get_transaction_receipt(tx_hash).await { - Ok(Some(receipt)) => { - // Verify transaction succeeded (status == true) and is in a block - if receipt.status() && receipt.block_number().is_some() { - self.tracker.record_included(tx_hash); - debug!("Transaction included and succeeded: {:?}", tx_hash); - } else if receipt.block_number().is_some() { - // Transaction was included but reverted - self.tracker.record_reverted(tx_hash); - debug!("Transaction included but reverted: {:?}", tx_hash); - } - // If no block_number yet, keep polling - } - Ok(None) => { - // Transaction not yet included, continue polling - } - Err(e) => { - debug!("Error fetching receipt for {:?}: {}", tx_hash, e); - // Don't mark as timeout, might be temporary RPC error - } - } - } - - // Exit when all transactions resolved and test completed - if self.tracker.all_resolved() && self.tracker.is_test_completed() { - break; - } - } - - Ok(()) - } -} diff --git a/crates/system-tests/src/load_test/sender.rs b/crates/system-tests/src/load_test/sender.rs deleted file mode 100644 index 050363a4..00000000 --- a/crates/system-tests/src/load_test/sender.rs +++ /dev/null @@ -1,113 +0,0 @@ -use super::tracker::TransactionTracker; -use super::wallet::Wallet; -use crate::client::TipsRpcClient; -use crate::fixtures::create_load_test_transaction; -use alloy_network::Network; -use alloy_primitives::{Address, Bytes, keccak256}; -use alloy_provider::{Provider, RootProvider}; -use anyhow::{Context, Result}; -use op_alloy_network::Optimism; -use rand::Rng; -use rand_chacha::ChaCha8Rng; -use std::sync::Arc; -use std::time::{Duration, Instant}; -use tokio::time::sleep; - -const MAX_RETRIES: u32 = 3; -const INITIAL_BACKOFF_MS: u64 = 100; - -pub struct SenderTask { - wallet: Wallet, - client: TipsRpcClient, - sequencer: RootProvider, - rate_per_wallet: f64, - duration: Duration, - tracker: Arc, - rng: ChaCha8Rng, -} - -impl SenderTask { - pub fn new( - wallet: Wallet, - client: TipsRpcClient, - sequencer: RootProvider, - rate_per_wallet: f64, - duration: Duration, - tracker: Arc, - rng: ChaCha8Rng, - ) -> Self { - Self { - wallet, - client, - sequencer, - rate_per_wallet, - duration, - tracker, - rng, - } - } - - pub async fn run(mut self) -> Result<()> { - let mut nonce = self - .sequencer - .get_transaction_count(self.wallet.address) - .await - .context("Failed to get initial nonce")?; - - let interval_duration = Duration::from_secs_f64(1.0 / self.rate_per_wallet); - let mut ticker = tokio::time::interval(interval_duration); - ticker.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Delay); - - let deadline = Instant::now() + self.duration; - - while Instant::now() < deadline { - ticker.tick().await; - - let recipient = self.random_address(); - let tx_bytes = self.create_transaction(recipient, nonce)?; - let tx_hash = keccak256(&tx_bytes); - - // Retry loop with exponential backoff - let mut retries = 0; - let mut backoff_ms = INITIAL_BACKOFF_MS; - - loop { - let send_time = Instant::now(); - - match self.client.send_raw_transaction(tx_bytes.clone()).await { - Ok(_) => { - self.tracker.record_sent(tx_hash, send_time); - nonce += 1; - break; - } - Err(e) => { - retries += 1; - if retries > MAX_RETRIES { - println!( - "Error sending raw transaction after {MAX_RETRIES} retries: {e}" - ); - self.tracker.record_send_error(); - nonce += 1; // Move on to next nonce after max retries - break; - } - // Exponential backoff before retry - sleep(Duration::from_millis(backoff_ms)).await; - backoff_ms *= 2; // Double backoff each retry - } - } - } - } - - Ok(()) - } - - fn create_transaction(&self, to: Address, nonce: u64) -> Result { - create_load_test_transaction(&self.wallet.signer, to, nonce) - } - - fn random_address(&mut self) -> Address { - let mut bytes = [0u8; 20]; - self.rng.fill(&mut bytes); - Address::from(bytes) - } -} diff --git a/crates/system-tests/src/load_test/setup.rs b/crates/system-tests/src/load_test/setup.rs deleted file mode 100644 index 1603df6c..00000000 --- a/crates/system-tests/src/load_test/setup.rs +++ /dev/null @@ -1,90 +0,0 @@ -use super::config::SetupArgs; -use super::wallet::{Wallet, generate_wallets, save_wallets}; -use crate::fixtures::create_optimism_provider; -use alloy_consensus::{SignableTransaction, TxEip1559}; -use alloy_primitives::U256; -use alloy_provider::Provider; -use anyhow::{Context, Result}; -use indicatif::{ProgressBar, ProgressStyle}; -use op_alloy_network::TxSignerSync; -use op_alloy_network::eip2718::Encodable2718; - -const CHAIN_ID: u64 = 13; // builder-playground local chain ID - -pub async fn run(args: SetupArgs) -> Result<()> { - let master_wallet = Wallet::from_private_key(&args.master_key) - .context("Failed to parse master wallet private key")?; - - let provider = create_optimism_provider(&args.sequencer)?; - - let master_balance = provider - .get_balance(master_wallet.address) - .await - .context("Failed to get master wallet balance")?; - - let required_balance = - U256::from((args.fund_amount * 1e18) as u64) * U256::from(args.num_wallets); - - if master_balance < required_balance { - anyhow::bail!( - "Insufficient master wallet balance. Need {} ETH, have {} ETH", - required_balance.to::() as f64 / 1e18, - master_balance.to::() as f64 / 1e18 - ); - } - - let wallets = generate_wallets(args.num_wallets, None); - - let pb = ProgressBar::new(args.num_wallets as u64); - pb.set_style( - ProgressStyle::default_bar() - .template("[{elapsed_precise}] {bar:40.cyan/blue} {pos}/{len} {msg}") - .unwrap() - .progress_chars("##-"), - ); - - let mut nonce = provider - .get_transaction_count(master_wallet.address) - .await - .context("Failed to get master wallet nonce")?; - - let fund_amount_wei = U256::from((args.fund_amount * 1e18) as u64); - - // Send all funding transactions - let mut pending_txs = Vec::new(); - for (i, wallet) in wallets.iter().enumerate() { - let mut tx = TxEip1559 { - chain_id: CHAIN_ID, - nonce, - gas_limit: 21000, - max_fee_per_gas: 1_000_000_000, // 1 gwei - max_priority_fee_per_gas: 100_000_000, // 0.1 gwei - to: wallet.address.into(), - value: fund_amount_wei, - access_list: Default::default(), - input: Default::default(), - }; - - let signature = master_wallet.signer.sign_transaction_sync(&mut tx)?; - let envelope = op_alloy_consensus::OpTxEnvelope::Eip1559(tx.into_signed(signature)); - - let mut buf = Vec::new(); - envelope.encode_2718(&mut buf); - let pending = provider - .send_raw_transaction(buf.as_ref()) - .await - .with_context(|| format!("Failed to send funding tx for wallet {i}"))?; - - pending_txs.push(pending); - nonce += 1; - pb.set_message(format!("Sent funding tx {}", i + 1)); - pb.inc(1); - } - - pb.finish_with_message("All funding transactions sent!"); - - // Save wallets to file - save_wallets(&wallets, args.fund_amount, &args.output)?; - - Ok(()) -} diff --git a/crates/system-tests/src/load_test/tracker.rs b/crates/system-tests/src/load_test/tracker.rs deleted file mode 100644 index 18a84581..00000000 --- a/crates/system-tests/src/load_test/tracker.rs +++ /dev/null @@ -1,115 +0,0 @@ -use alloy_primitives::B256; -use dashmap::DashMap; -use std::sync::Arc; -use std::sync::atomic::{AtomicBool, AtomicU64, Ordering}; -use std::time::{Duration, Instant}; - -pub struct TransactionTracker { - // Pending transactions (tx_hash -> send_time) - pending: DashMap, - - // Included transactions (succeeded) - included: DashMap, - - // Reverted transactions (included but status == false) - reverted: DashMap, - - // Timed out transactions - timed_out: DashMap, - - // Send errors (not transaction-specific) - send_errors: AtomicU64, - - // Test metadata - test_start: Instant, - test_completed: AtomicBool, -} - -impl TransactionTracker { - pub fn new(_test_duration: Duration) -> Arc { - Arc::new(Self { - pending: DashMap::new(), - included: DashMap::new(), - reverted: DashMap::new(), - timed_out: DashMap::new(), - send_errors: AtomicU64::new(0), - test_start: Instant::now(), - test_completed: AtomicBool::new(false), - }) - } - - pub fn record_sent(&self, tx_hash: B256, send_time: Instant) { - self.pending.insert(tx_hash, send_time); - } - - pub fn record_send_error(&self) { - self.send_errors.fetch_add(1, Ordering::Relaxed); - } - - /// Record a transaction that was included and succeeded (status == true) - pub fn record_included(&self, tx_hash: B256) { - self.pending.remove(&tx_hash); - self.included.insert(tx_hash, ()); - } - - /// Record a transaction that was included but reverted (status == false) - pub fn record_reverted(&self, tx_hash: B256) { - self.pending.remove(&tx_hash); - self.reverted.insert(tx_hash, ()); - } - - pub fn record_timeout(&self, tx_hash: B256) { - if self.pending.remove(&tx_hash).is_some() { - self.timed_out.insert(tx_hash, ()); - } - } - - pub fn get_pending(&self) -> Vec<(B256, Instant)> { - self.pending - .iter() - .map(|entry| (*entry.key(), *entry.value())) - .collect() - } - - pub fn mark_test_completed(&self) { - self.test_completed.store(true, Ordering::Relaxed); - } - - pub fn is_test_completed(&self) -> bool { - self.test_completed.load(Ordering::Relaxed) - } - - pub fn all_resolved(&self) -> bool { - self.pending.is_empty() - } - - pub fn elapsed(&self) -> Duration { - self.test_start.elapsed() - } - - // Metrics getters - pub fn total_sent(&self) -> u64 { - (self.pending.len() + self.included.len() + self.reverted.len() + self.timed_out.len()) - as u64 - } - - pub fn total_included(&self) -> u64 { - self.included.len() as u64 - } - - pub fn total_reverted(&self) -> u64 { - self.reverted.len() as u64 - } - - pub fn total_pending(&self) -> u64 { - self.pending.len() as u64 - } - - pub fn total_timed_out(&self) -> u64 { - self.timed_out.len() as u64 - } - - pub fn total_send_errors(&self) -> u64 { - self.send_errors.load(Ordering::Relaxed) - } -} diff --git a/crates/system-tests/src/load_test/wallet.rs b/crates/system-tests/src/load_test/wallet.rs deleted file mode 100644 index e5d7a73d..00000000 --- a/crates/system-tests/src/load_test/wallet.rs +++ /dev/null @@ -1,86 +0,0 @@ -use alloy_primitives::Address; -use alloy_signer_local::PrivateKeySigner; -use anyhow::{Context, Result}; -use rand::SeedableRng; -use rand_chacha::ChaCha8Rng; -use serde::{Deserialize, Serialize}; -use std::fs; -use std::path::Path; - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct WalletData { - pub address: String, - pub private_key: String, - pub initial_balance: String, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct WalletsFile { - pub wallets: Vec, -} - -pub struct Wallet { - pub signer: PrivateKeySigner, - pub address: Address, -} - -impl Wallet { - pub fn from_private_key(private_key: &str) -> Result { - let signer: PrivateKeySigner = - private_key.parse().context("Failed to parse private key")?; - let address = signer.address(); - Ok(Self { signer, address }) - } - - pub fn new_random(rng: &mut ChaCha8Rng) -> Self { - let signer = PrivateKeySigner::random_with(rng); - let address = signer.address(); - Self { signer, address } - } -} - -pub fn generate_wallets(num_wallets: usize, seed: Option) -> Vec { - let mut rng = match seed { - Some(s) => ChaCha8Rng::seed_from_u64(s), - None => ChaCha8Rng::from_entropy(), - }; - - (0..num_wallets) - .map(|_| Wallet::new_random(&mut rng)) - .collect() -} - -pub fn save_wallets(wallets: &[Wallet], fund_amount: f64, path: &Path) -> Result<()> { - let wallet_data: Vec = wallets - .iter() - .map(|w| WalletData { - address: format!("{:?}", w.address), - private_key: format!("0x{}", hex::encode(w.signer.to_bytes())), - initial_balance: fund_amount.to_string(), - }) - .collect(); - - let wallets_file = WalletsFile { - wallets: wallet_data, - }; - - let json = - serde_json::to_string_pretty(&wallets_file).context("Failed to serialize wallets")?; - fs::write(path, json).context("Failed to write wallets file")?; - - Ok(()) -} - -pub fn load_wallets(path: &Path) -> Result> { - let json = fs::read_to_string(path).context("Failed to read wallets file")?; - let wallets_file: WalletsFile = - serde_json::from_str(&json).context("Failed to parse wallets file")?; - - let wallets: Result> = wallets_file - .wallets - .iter() - .map(|wd| Wallet::from_private_key(&wd.private_key)) - .collect(); - - wallets -} diff --git a/crates/system-tests/tests/common/kafka.rs b/crates/system-tests/tests/common/kafka.rs deleted file mode 100644 index 5acda1b8..00000000 --- a/crates/system-tests/tests/common/kafka.rs +++ /dev/null @@ -1,121 +0,0 @@ -use std::{path::Path, time::Duration}; - -use alloy_primitives::B256; -use anyhow::{Context, Result}; -use rdkafka::{ - Message, - config::ClientConfig, - consumer::{Consumer, StreamConsumer}, - message::BorrowedMessage, -}; -use tips_audit_lib::types::BundleEvent; -use tips_core::{BundleExtensions, kafka::load_kafka_config_from_file}; -use tokio::time::{Instant, timeout}; -use uuid::Uuid; - -const DEFAULT_AUDIT_TOPIC: &str = "tips-audit"; -const DEFAULT_AUDIT_PROPERTIES: &str = "../../docker/host-ingress-audit-kafka-properties"; -const KAFKA_WAIT_TIMEOUT: Duration = Duration::from_secs(60); - -fn resolve_properties_path(env_key: &str, default_path: &str) -> Result { - match std::env::var(env_key) { - Ok(value) => Ok(value), - Err(_) => { - if Path::new(default_path).exists() { - Ok(default_path.to_string()) - } else { - anyhow::bail!( - "Environment variable {env_key} must be set (default path '{default_path}' not found). \ - Run `just sync` or export {env_key} before running tests." - ); - } - } - } -} - -fn build_kafka_consumer(properties_env: &str, default_path: &str) -> Result { - let props_file = resolve_properties_path(properties_env, default_path)?; - - let mut client_config = ClientConfig::from_iter(load_kafka_config_from_file(&props_file)?); - - client_config - .set( - "group.id", - format!( - "tips-system-tests-{}", - Uuid::new_v5(&Uuid::NAMESPACE_OID, bundle.bundle_hash().as_slice()) - ), - ) - .set("enable.auto.commit", "false") - .set("auto.offset.reset", "earliest"); - - client_config - .create() - .context("Failed to create Kafka consumer") -} - -async fn wait_for_kafka_message( - properties_env: &str, - default_properties: &str, - topic_env: &str, - default_topic: &str, - timeout_duration: Duration, - mut matcher: impl FnMut(BorrowedMessage<'_>) -> Option, -) -> Result { - let consumer = build_kafka_consumer(properties_env, default_properties)?; - let topic = std::env::var(topic_env).unwrap_or_else(|_| default_topic.to_string()); - consumer.subscribe(&[&topic])?; - - let deadline = Instant::now() + timeout_duration; - - loop { - let now = Instant::now(); - if now >= deadline { - anyhow::bail!( - "Timed out waiting for Kafka message on topic {topic} after {:?}", - timeout_duration - ); - } - - let remaining = deadline - now; - match timeout(remaining, consumer.recv()).await { - Ok(Ok(message)) => { - if let Some(value) = matcher(message) { - return Ok(value); - } - } - Ok(Err(err)) => { - return Err(err.into()); - } - Err(_) => { - // Timeout for this iteration, continue looping - } - } - } -} - -pub async fn wait_for_audit_event_by_hash( - expected_bundle_hash: &B256, - mut matcher: impl FnMut(&BundleEvent) -> bool, -) -> Result { - let expected_hash = *expected_bundle_hash; - wait_for_kafka_message( - "TIPS_INGRESS_KAFKA_AUDIT_PROPERTIES_FILE", - DEFAULT_AUDIT_PROPERTIES, - "TIPS_INGRESS_KAFKA_AUDIT_TOPIC", - DEFAULT_AUDIT_TOPIC, - KAFKA_WAIT_TIMEOUT, - |message| { - let payload = message.payload()?; - let event: BundleEvent = serde_json::from_slice(payload).ok()?; - // Match by bundle hash from the Received event - if let BundleEvent::Received { bundle, .. } = &event { - if bundle.bundle_hash() == expected_hash && matcher(&event) { - return Some(event); - } - } - None - }, - ) - .await -} diff --git a/crates/system-tests/tests/common/mod.rs b/crates/system-tests/tests/common/mod.rs deleted file mode 100644 index b17877c5..00000000 --- a/crates/system-tests/tests/common/mod.rs +++ /dev/null @@ -1 +0,0 @@ -pub mod kafka; diff --git a/crates/system-tests/tests/integration_tests.rs b/crates/system-tests/tests/integration_tests.rs deleted file mode 100644 index 901bb41f..00000000 --- a/crates/system-tests/tests/integration_tests.rs +++ /dev/null @@ -1,359 +0,0 @@ -#[path = "common/mod.rs"] -mod common; - -use alloy_network::ReceiptResponse; -use alloy_primitives::{Address, TxHash, U256, keccak256}; -use alloy_provider::{Provider, RootProvider}; -use anyhow::{Context, Result, bail}; -use common::kafka::wait_for_audit_event_by_hash; -use op_alloy_network::Optimism; -use serial_test::serial; -use tips_audit_lib::types::BundleEvent; -use tips_core::BundleExtensions; -use tips_system_tests::client::TipsRpcClient; -use tips_system_tests::fixtures::{ - create_funded_signer, create_optimism_provider, create_signed_transaction, -}; -use tokio::time::{Duration, Instant, sleep}; - -/// Get the URL for integration tests against the TIPS ingress service -fn get_integration_test_url() -> String { - std::env::var("INGRESS_URL").unwrap_or_else(|_| "http://localhost:8080".to_string()) -} - -/// Get the URL for the sequencer (for fetching nonces) -fn get_sequencer_url() -> String { - std::env::var("SEQUENCER_URL").unwrap_or_else(|_| "http://localhost:8547".to_string()) -} - -async fn wait_for_transaction_seen( - provider: &RootProvider, - tx_hash: TxHash, - timeout_secs: u64, -) -> Result<()> { - let deadline = Instant::now() + Duration::from_secs(timeout_secs); - loop { - if Instant::now() >= deadline { - bail!( - "Timed out waiting for transaction {} to appear on the sequencer", - tx_hash - ); - } - - if provider - .get_transaction_by_hash(tx_hash.into()) - .await? - .is_some() - { - return Ok(()); - } - - sleep(Duration::from_millis(500)).await; - } -} - -#[tokio::test] -async fn test_client_can_connect_to_tips() -> Result<()> { - if std::env::var("INTEGRATION_TESTS").is_err() { - eprintln!( - "Skipping integration tests (set INTEGRATION_TESTS=1 and ensure TIPS infrastructure is running)" - ); - return Ok(()); - } - - let url = get_integration_test_url(); - let provider = create_optimism_provider(&url)?; - let _client = TipsRpcClient::new(provider); - Ok(()) -} - -#[tokio::test] -#[serial] -async fn test_send_raw_transaction_accepted() -> Result<()> { - if std::env::var("INTEGRATION_TESTS").is_err() { - eprintln!( - "Skipping integration tests (set INTEGRATION_TESTS=1 and ensure TIPS infrastructure is running)" - ); - return Ok(()); - } - - let url = get_integration_test_url(); - let provider = create_optimism_provider(&url)?; - let client = TipsRpcClient::new(provider); - let signer = create_funded_signer(); - - let sequencer_url = get_sequencer_url(); - let sequencer_provider = create_optimism_provider(&sequencer_url)?; - let nonce = sequencer_provider - .get_transaction_count(signer.address()) - .await?; - - let to = Address::from([0x11; 20]); - let value = U256::from(1000); - let gas_limit = 21000; - let gas_price = 1_000_000_000; - - let signed_tx = create_signed_transaction(&signer, to, value, nonce, gas_limit, gas_price)?; - - // Send transaction to TIPS - let tx_hash = client - .send_raw_transaction(signed_tx) - .await - .context("Failed to send transaction to TIPS")?; - - // Verify TIPS accepted the transaction and returned a hash - assert!(!tx_hash.is_zero(), "Transaction hash should not be zero"); - - // Verify transaction lands on-chain - wait_for_transaction_seen(&sequencer_provider, tx_hash, 30) - .await - .context("Transaction never appeared on sequencer")?; - - // Verify transaction receipt shows success - let receipt = sequencer_provider - .get_transaction_receipt(tx_hash) - .await - .context("Failed to fetch transaction receipt")? - .expect("Transaction receipt should exist after being seen on sequencer"); - assert!(receipt.status(), "Transaction should have succeeded"); - - Ok(()) -} - -#[tokio::test] -#[serial] -async fn test_send_bundle_accepted() -> Result<()> { - if std::env::var("INTEGRATION_TESTS").is_err() { - eprintln!( - "Skipping integration tests (set INTEGRATION_TESTS=1 and ensure TIPS infrastructure is running)" - ); - return Ok(()); - } - - use tips_core::Bundle; - - let url = get_integration_test_url(); - let provider = create_optimism_provider(&url)?; - let client = TipsRpcClient::new(provider); - let signer = create_funded_signer(); - - let sequencer_url = get_sequencer_url(); - let sequencer_provider = create_optimism_provider(&sequencer_url)?; - let nonce = sequencer_provider - .get_transaction_count(signer.address()) - .await?; - - let to = Address::from([0x11; 20]); - let value = U256::from(1000); - let gas_limit = 21000; - let gas_price = 1_000_000_000; - - let signed_tx = create_signed_transaction(&signer, to, value, nonce, gas_limit, gas_price)?; - let tx_hash = keccak256(&signed_tx); - - // First send the transaction to mempool - let _mempool_tx_hash = client - .send_raw_transaction(signed_tx.clone()) - .await - .context("Failed to send transaction to mempool")?; - - let bundle = Bundle { - txs: vec![signed_tx], - block_number: 0, - min_timestamp: None, - max_timestamp: None, - reverting_tx_hashes: vec![tx_hash], - replacement_uuid: None, - dropping_tx_hashes: vec![], - flashblock_number_min: None, - flashblock_number_max: None, - }; - - // Send backrun bundle to TIPS - let bundle_hash = client - .send_backrun_bundle(bundle) - .await - .context("Failed to send backrun bundle to TIPS")?; - - // Verify TIPS accepted the bundle and returned a hash - assert!( - !bundle_hash.bundle_hash.is_zero(), - "Bundle hash should not be zero" - ); - - // Verify bundle hash is calculated correctly: keccak256(concat(tx_hashes)) - let mut concatenated = Vec::new(); - concatenated.extend_from_slice(tx_hash.as_slice()); - let expected_bundle_hash = keccak256(&concatenated); - assert_eq!( - bundle_hash.bundle_hash, expected_bundle_hash, - "Bundle hash should match keccak256(tx_hash)" - ); - - // Verify audit channel emitted a Received event for this bundle - let audit_event = wait_for_audit_event_by_hash(&bundle_hash.bundle_hash, |event| { - matches!(event, BundleEvent::Received { .. }) - }) - .await - .context("Failed to read audit event from Kafka")?; - match audit_event { - BundleEvent::Received { bundle, .. } => { - assert_eq!( - bundle.bundle_hash(), - bundle_hash.bundle_hash, - "Audit event bundle hash should match response" - ); - } - other => panic!("Expected Received audit event, got {:?}", other), - } - - // Wait for transaction to appear on sequencer - wait_for_transaction_seen(&sequencer_provider, tx_hash.into(), 60) - .await - .context("Bundle transaction never appeared on sequencer")?; - - // Verify transaction receipt shows success - let receipt = sequencer_provider - .get_transaction_receipt(tx_hash.into()) - .await - .context("Failed to fetch transaction receipt")? - .expect("Transaction receipt should exist after being seen on sequencer"); - assert!(receipt.status(), "Transaction should have succeeded"); - assert!( - receipt.block_number().is_some(), - "Transaction should be included in a block" - ); - - Ok(()) -} - -#[tokio::test] -#[serial] -async fn test_send_bundle_with_two_transactions() -> Result<()> { - if std::env::var("INTEGRATION_TESTS").is_err() { - eprintln!( - "Skipping integration tests (set INTEGRATION_TESTS=1 and ensure TIPS infrastructure is running)" - ); - return Ok(()); - } - - use tips_core::Bundle; - - let url = get_integration_test_url(); - let provider = create_optimism_provider(&url)?; - let client = TipsRpcClient::new(provider); - let signer = create_funded_signer(); - - let sequencer_url = get_sequencer_url(); - let sequencer_provider = create_optimism_provider(&sequencer_url)?; - let nonce = sequencer_provider - .get_transaction_count(signer.address()) - .await?; - - // Create two transactions - let tx1 = create_signed_transaction( - &signer, - Address::from([0x33; 20]), - U256::from(1000), - nonce, - 21000, - 1_000_000_000, - )?; - - let tx2 = create_signed_transaction( - &signer, - Address::from([0x44; 20]), - U256::from(2000), - nonce + 1, - 21000, - 1_000_000_000, - )?; - - let tx1_hash = keccak256(&tx1); - let tx2_hash = keccak256(&tx2); - - // First send both transactions to mempool - client - .send_raw_transaction(tx1.clone()) - .await - .context("Failed to send tx1 to mempool")?; - client - .send_raw_transaction(tx2.clone()) - .await - .context("Failed to send tx2 to mempool")?; - - let bundle = Bundle { - txs: vec![tx1, tx2], - block_number: 0, - min_timestamp: None, - max_timestamp: None, - reverting_tx_hashes: vec![tx1_hash, tx2_hash], - replacement_uuid: None, - dropping_tx_hashes: vec![], - flashblock_number_min: None, - flashblock_number_max: None, - }; - - // Send backrun bundle with 2 transactions to TIPS - let bundle_hash = client - .send_backrun_bundle(bundle) - .await - .context("Failed to send multi-transaction backrun bundle to TIPS")?; - - // Verify TIPS accepted the bundle and returned a hash - assert!( - !bundle_hash.bundle_hash.is_zero(), - "Bundle hash should not be zero" - ); - - // Verify bundle hash is calculated correctly: keccak256(concat(all tx_hashes)) - let mut concatenated = Vec::new(); - concatenated.extend_from_slice(tx1_hash.as_slice()); - concatenated.extend_from_slice(tx2_hash.as_slice()); - let expected_bundle_hash = keccak256(&concatenated); - assert_eq!( - bundle_hash.bundle_hash, expected_bundle_hash, - "Bundle hash should match keccak256(concat(tx1_hash, tx2_hash))" - ); - - // Verify audit channel emitted a Received event - let audit_event = wait_for_audit_event_by_hash(&bundle_hash.bundle_hash, |event| { - matches!(event, BundleEvent::Received { .. }) - }) - .await - .context("Failed to read audit event for 2-tx bundle")?; - match audit_event { - BundleEvent::Received { bundle, .. } => { - assert_eq!( - bundle.bundle_hash(), - bundle_hash.bundle_hash, - "Audit event bundle hash should match response" - ); - } - other => panic!("Expected Received audit event, got {:?}", other), - } - - // Wait for both transactions to appear on sequencer - wait_for_transaction_seen(&sequencer_provider, tx1_hash.into(), 60) - .await - .context("Bundle tx1 never appeared on sequencer")?; - wait_for_transaction_seen(&sequencer_provider, tx2_hash.into(), 60) - .await - .context("Bundle tx2 never appeared on sequencer")?; - - // Verify both transaction receipts show success - for (tx_hash, name) in [(tx1_hash, "tx1"), (tx2_hash, "tx2")] { - let receipt = sequencer_provider - .get_transaction_receipt(tx_hash.into()) - .await - .context(format!("Failed to fetch {name} receipt"))? - .expect(&format!("{name} receipt should exist")); - assert!(receipt.status(), "{name} should have succeeded"); - assert!( - receipt.block_number().is_some(), - "{name} should be included in a block" - ); - } - - Ok(()) -} diff --git a/docker-compose.tips.yml b/docker-compose.tips.yml deleted file mode 100644 index 4054ccf8..00000000 --- a/docker-compose.tips.yml +++ /dev/null @@ -1,43 +0,0 @@ -services: - ingress-rpc: - build: - context: . - dockerfile: Dockerfile - command: - - "/app/tips-ingress-rpc" - container_name: tips-ingress-rpc - ports: - - "8080:8080" - - "8081:8081" - - "9002:9002" - env_file: - - .env.docker - volumes: - - ./docker/ingress-bundles-kafka-properties:/app/docker/ingress-bundles-kafka-properties:ro - - ./docker/ingress-audit-kafka-properties:/app/docker/ingress-audit-kafka-properties:ro - - ./docker/ingress-user-operation-consumer-kafka-properties:/app/docker/ingress-user-operation-consumer-kafka-properties:ro - restart: unless-stopped - - audit: - build: - context: . - dockerfile: Dockerfile - command: - - "/app/tips-audit" - container_name: tips-audit - env_file: - - .env.docker - volumes: - - ./docker/audit-kafka-properties:/app/docker/audit-kafka-properties:ro - restart: unless-stopped - - ui: - build: - context: . - dockerfile: ui/Dockerfile - ports: - - "3000:3000" - container_name: tips-ui - env_file: - - .env.docker - restart: unless-stopped \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml deleted file mode 100644 index 2cadfbf9..00000000 --- a/docker-compose.yml +++ /dev/null @@ -1,75 +0,0 @@ -services: - kafka: - image: confluentinc/cp-kafka:7.5.0 - container_name: tips-kafka - ports: - - "9092:9092" - - "9094:9094" - environment: - KAFKA_BROKER_ID: 1 - KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: PLAINTEXT:PLAINTEXT,PLAINTEXT_HOST:PLAINTEXT,PLAINTEXT_DOCKER:PLAINTEXT,CONTROLLER:PLAINTEXT - KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka:29092,PLAINTEXT_HOST://localhost:9092,PLAINTEXT_DOCKER://host.docker.internal:9094 - KAFKA_LISTENERS: PLAINTEXT://0.0.0.0:29092,CONTROLLER://0.0.0.0:9093,PLAINTEXT_HOST://0.0.0.0:9092,PLAINTEXT_DOCKER://0.0.0.0:9094 - KAFKA_INTER_BROKER_LISTENER_NAME: PLAINTEXT - KAFKA_CONTROLLER_LISTENER_NAMES: CONTROLLER - KAFKA_ZOOKEEPER_CONNECT: ' ' - KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1 - KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR: 1 - KAFKA_TRANSACTION_STATE_LOG_MIN_ISR: 1 - KAFKA_GROUP_INITIAL_REBALANCE_DELAY_MS: 0 - KAFKA_PROCESS_ROLES: broker,controller - KAFKA_NODE_ID: 1 - KAFKA_CONTROLLER_QUORUM_VOTERS: 1@kafka:9093 - KAFKA_LOG_DIRS: /var/lib/kafka/data - CLUSTER_ID: 4L6g3nShT-eMCtK--X86sw - volumes: - - ./data/kafka:/var/lib/kafka/data - healthcheck: - test: ["CMD", "kafka-broker-api-versions", "--bootstrap-server", "localhost:9092"] - interval: 10s - timeout: 10s - retries: 5 - kafka-setup: - image: confluentinc/cp-kafka:7.5.0 - container_name: tips-kafka-setup - depends_on: - kafka: - condition: service_healthy - command: | - sh -c " - kafka-topics --create --if-not-exists --topic tips-audit --bootstrap-server kafka:29092 --partitions 3 --replication-factor 1 - kafka-topics --create --if-not-exists --topic tips-ingress --bootstrap-server kafka:29092 --partitions 3 --replication-factor 1 - kafka-topics --create --if-not-exists --topic tips-user-operation --bootstrap-server kafka:29092 --partitions 3 --replication-factor 1 - kafka-topics --list --bootstrap-server kafka:29092 - " - - minio: - image: minio/minio:latest - container_name: tips-minio - environment: - MINIO_ROOT_USER: minioadmin - MINIO_ROOT_PASSWORD: minioadmin - ports: - - "7000:9000" - - "7001:9001" - command: server /data --console-address ":9001" - volumes: - - ./data/minio:/data - healthcheck: - test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"] - interval: 30s - timeout: 20s - retries: 3 - minio-setup: - image: minio/mc - container_name: tips-minio-setup - depends_on: - minio: - condition: service_healthy - entrypoint: > - /bin/sh -c " - /usr/bin/mc alias set minio http://minio:9000 minioadmin minioadmin; - /usr/bin/mc mb minio/tips; - /usr/bin/mc anonymous set public minio/tips; - exit 0; - " \ No newline at end of file diff --git a/docker/audit-kafka-properties b/docker/audit-kafka-properties deleted file mode 100644 index d0ede9bd..00000000 --- a/docker/audit-kafka-properties +++ /dev/null @@ -1,10 +0,0 @@ -# Kafka configuration properties for audit service -bootstrap.servers=host.docker.internal:9094 -message.timeout.ms=5000 -group.id=local-audit -enable.partition.eof=false -session.timeout.ms=6000 -enable.auto.commit=false -auto.offset.reset=earliest -fetch.wait.max.ms=100 -fetch.min.bytes=1 \ No newline at end of file diff --git a/docker/host-ingress-audit-kafka-properties b/docker/host-ingress-audit-kafka-properties deleted file mode 100644 index 7929f9b0..00000000 --- a/docker/host-ingress-audit-kafka-properties +++ /dev/null @@ -1,4 +0,0 @@ -# Kafka audit configuration for host-based integration tests -bootstrap.servers=localhost:9092 -message.timeout.ms=5000 - diff --git a/docker/host-ingress-bundles-kafka-properties b/docker/host-ingress-bundles-kafka-properties deleted file mode 100644 index 3462d1fe..00000000 --- a/docker/host-ingress-bundles-kafka-properties +++ /dev/null @@ -1,4 +0,0 @@ -# Kafka configuration properties for host-based integration tests -bootstrap.servers=localhost:9092 -message.timeout.ms=5000 - diff --git a/docker/ingress-audit-kafka-properties b/docker/ingress-audit-kafka-properties deleted file mode 100644 index 2f58fc90..00000000 --- a/docker/ingress-audit-kafka-properties +++ /dev/null @@ -1,4 +0,0 @@ -# Kafka configuration properties for ingress audit events -bootstrap.servers=host.docker.internal:9094 -message.timeout.ms=5000 -compression.type=zstd \ No newline at end of file diff --git a/docker/ingress-bundles-kafka-properties b/docker/ingress-bundles-kafka-properties deleted file mode 100644 index 6b7899a2..00000000 --- a/docker/ingress-bundles-kafka-properties +++ /dev/null @@ -1,3 +0,0 @@ -# Kafka configuration properties for ingress service -bootstrap.servers=host.docker.internal:9094 -message.timeout.ms=5000 \ No newline at end of file diff --git a/docker/ingress-user-operation-consumer-kafka-properties b/docker/ingress-user-operation-consumer-kafka-properties deleted file mode 100644 index 3bb02bfa..00000000 --- a/docker/ingress-user-operation-consumer-kafka-properties +++ /dev/null @@ -1,9 +0,0 @@ -# Kafka configuration properties for ingress user operation consumer -bootstrap.servers=host.docker.internal:9094 -message.timeout.ms=5000 -enable.partition.eof=false -session.timeout.ms=6000 -fetch.wait.max.ms=100 -fetch.min.bytes=1 -# Note: group.id and enable.auto.commit are set programmatically - diff --git a/docs/API.md b/docs/API.md deleted file mode 100644 index fc836135..00000000 --- a/docs/API.md +++ /dev/null @@ -1,75 +0,0 @@ -# API Reference - -TIPS processes all transactions as bundles. Transactions submitted via `eth_sendRawTransaction` are wrapped into a single-transaction bundle with sensible defaults. - -## Bundle Identifiers - -Bundles have two identifiers: -- **UUID**: Server-generated unique identifier assigned on submission -- **Bundle Hash**: `keccak(..bundle.txns)`, derived from the transaction set - -## Bundle Lifecycle - -### Creation - -Bundles are deduplicated by bundle hash. When multiple bundles share the same hash, the latest submission defines the bundle fields: - -``` -bundleA = (txA) → store: {bundleA} -bundleB = (txA, txB) → store: {bundleA, bundleB} -bundleC = (txA, txB) → store: {bundleA, bundleC} # replaces bundleB -bundleD = (txC, txA) → store: {bundleA, bundleC, bundleD} -``` - -### Updates - -Bundles can be updated via: -- `eth_sendRawTransaction`: matches by (address, nonce) -- `eth_sendBundle`: matches by UUID - -Updates are best-effort. If a bundle is already included in a flashblock before the update processes, the original bundle will be used. - -### Cancellation - -Cancel bundles with `eth_cancelBundle`. Cancellations are best-effort and may not take effect if the bundle is already included. - -## RPC Methods - -### eth_sendRawTransaction - -``` -eth_sendRawTransaction(bytes) → hash -``` - -Validates and wraps the transaction in a bundle. Replacement transactions (same address and nonce) replace the existing bundle. - -**Limits:** -- 25 million gas per transaction - -### eth_sendBundle - -``` -eth_sendBundle(EthSendBundle) → uuid -``` - -Submits a bundle directly. Without a replacement UUID, inserts a new bundle (merging with existing bundles sharing the same hash). With a UUID, updates the existing bundle if it still exists. - -**Limits:** -- 25 million gas per bundle -- Maximum 3 transactions per bundle -- All transaction hashes must be in `reverting_tx_hashes` (revert protection not supported) -- `dropping_tx_hashes` must be empty -- Refunds not supported (`refund_percent`, `refund_recipient`, `refund_tx_hashes` must be unset/empty) -- `extra_fields` must be empty - -**Reference:** [EthSendBundle](https://github.com/alloy-rs/alloy/blob/25019adf54272a3372d75c6c44a6185e4be9dfa2/crates/rpc-types-mev/src/eth_calls.rs#L252) - -### eth_cancelBundle - -``` -eth_cancelBundle(EthCancelBundle) -``` - -Cancels a bundle by UUID. Best-effort; may not succeed if already included by the builder. - -**Reference:** [EthCancelBundle](https://github.com/alloy-rs/alloy/blob/25019adf54272a3372d75c6c44a6185e4be9dfa2/crates/rpc-types-mev/src/eth_calls.rs#L216) diff --git a/docs/AUDIT_S3_FORMAT.md b/docs/AUDIT_S3_FORMAT.md deleted file mode 100644 index acb00d4f..00000000 --- a/docs/AUDIT_S3_FORMAT.md +++ /dev/null @@ -1,134 +0,0 @@ -# Audit S3 Storage Format - -The audit system archives bundle and UserOp lifecycle events to S3 for long-term storage and lookup. - -## Storage Paths - -| Path | Description | -|------|-------------| -| `/bundles/` | Bundle lifecycle history | -| `/transactions/by_hash/` | Transaction hash to bundle mapping | -| `/userops/` | UserOperation lifecycle history | - -## Bundle History - -**Path:** `/bundles/` - -Stores the complete lifecycle of a bundle as a series of events. - -```json -{ - "history": [ - { - "event": "Created", - "timestamp": 1234567890, - "key": "-", - "data": { - "bundle": { /* EthSendBundle object */ } - } - }, - { - "event": "BuilderIncluded", - "timestamp": 1234567893, - "key": "-", - "data": { - "blockNumber": 12345, - "flashblockIndex": 1, - "builderId": "builder-id" - } - }, - { - "event": "BlockIncluded", - "timestamp": 1234567895, - "key": "-", - "data": { - "blockNumber": 12345, - "blockHash": "0x..." - } - }, - { - "event": "Dropped", - "timestamp": 1234567896, - "key": "-", - "data": { - "reason": "TIMEOUT" - } - } - ] -} -``` - -See [Bundle States](./BUNDLE_STATES.md) for event type definitions. - -## Transaction Lookup - -**Path:** `/transactions/by_hash/` - -Maps transaction hashes to bundle UUIDs for efficient lookups. - -```json -{ - "bundle_ids": [ - "550e8400-e29b-41d4-a716-446655440000", - "6ba7b810-9dad-11d1-80b4-00c04fd430c8" - ] -} -``` - -## UserOperation History - -**Path:** `/userops/` - -Stores ERC-4337 UserOperation lifecycle events. Events are written after validation passes. - -```json -{ - "history": [ - { - "event": "AddedToMempool", - "data": { - "key": "-", - "timestamp": 1234567890, - "sender": "0x1234567890abcdef1234567890abcdef12345678", - "entry_point": "0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789", - "nonce": "0x1" - } - }, - { - "event": "Included", - "data": { - "key": "-", - "timestamp": 1234567895, - "block_number": 12345678, - "tx_hash": "0xabcdef..." - } - }, - { - "event": "Dropped", - "data": { - "key": "-", - "timestamp": 1234567896, - "reason": { - "Invalid": "AA21 didn't pay prefund" - } - } - } - ] -} -``` - -### UserOp Events - -| Event | Description | Key Fields | -|-------|-------------|------------| -| `AddedToMempool` | UserOp passed validation and entered mempool | sender, entry_point, nonce | -| `Included` | UserOp included in a block | block_number, tx_hash | -| `Dropped` | UserOp removed from mempool | reason | - -### Drop Reasons - -| Reason | Description | -|--------|-------------| -| `Invalid(String)` | Validation failed with error message | -| `Expired` | TTL exceeded | -| `ReplacedByHigherFee` | Replaced by another UserOp with higher fee | diff --git a/docs/BUNDLE_STATES.md b/docs/BUNDLE_STATES.md deleted file mode 100644 index d51e691f..00000000 --- a/docs/BUNDLE_STATES.md +++ /dev/null @@ -1,51 +0,0 @@ -# Bundle States - -Bundles transition through the following states during their lifecycle. - -## State Definitions - -| State | Description | Arguments | -|-------|-------------|-----------| -| **Created** | Bundle created with initial transaction list | bundle | -| **Updated** | Bundle modified (transactions added/removed) | bundle | -| **Cancelled** | Bundle explicitly cancelled | nonce \| uuid | -| **IncludedByBuilder** | Bundle included in flashblock by builder | flashblockNum, blockNum, builderId | -| **IncludedInBlock** | Bundle confirmed in blockchain | blockNum, blockHash | -| **Dropped** | Bundle dropped from processing | reason | - -## Drop Reasons - -| Reason | Description | -|--------|-------------| -| `TIMEOUT` | Bundle expired without inclusion | -| `INCLUDED_BY_OTHER` | Overlapping bundle caused this bundle's transactions to become non-includable | -| `REVERTED` | A non-revertible transaction reverted | - -## Mempool Limits - -Bundles may be dropped when limits are exceeded: - -### Bundle Limits -- Timeout (block or flashblock deadline) -- Target block number passed - -### Account Limits -- Fixed number of transactions per account in mempool -- Excess transactions dropped by descending nonce - -### Global Limits -- Mempool pruned at capacity based on: - - Bundle age - - Low base fee - -### Overlapping Bundles - -When bundles share transactions, inclusion of one may invalidate another: - -``` -bundleA = (txA, txB) -bundleB = (txA) - -If bundleB is included and txA in bundleA cannot be dropped, -bundleA is marked INCLUDED_BY_OTHER and removed. -``` diff --git a/docs/ERC4337_BUNDLER.md b/docs/ERC4337_BUNDLER.md deleted file mode 100644 index 58431913..00000000 --- a/docs/ERC4337_BUNDLER.md +++ /dev/null @@ -1,104 +0,0 @@ -# TIPS ERC-4337 Bundler - -## Overview - -ERC-4337 bundlers include user operations (smart account transactions) onchain. TIPS includes a native bundler that provides optimal speed and cost for user operations with full audit tracing via the TIPS UI. - -## Architecture - -### Ingress - -TIPS exposes `eth_sendUserOperation` and performs standard ERC-7562 validation checks while managing the user operation mempool. Supported entry points: v0.6 through v0.9. - -Base node reth includes `base_validateUserOperation` for validating user ops before adding them to the queue. - -### ERC-7562 Validation - -[ERC-7562](https://eips.ethereum.org/EIPS/eip-7562) protects bundlers from DoS attacks through unpaid computation and reverting transactions. The rules restrict: - -- Opcodes -- Reputation -- Storage access -- Bundle rules -- Staking for globally used contracts (paymasters, factories) - -These restrictions minimize cross-transactional dependencies that could allow one transaction to invalidate another. - -TIPS streams events from the block builder to the audit stream, updating ingress rules and reputation/mempool limits. A Redis cluster maintains reputation scores for user operation entities (sender, paymaster, factory) tracking `opsSeen` and `opsIncluded` over configured intervals. - -User operations from `BANNED` entities are filtered out before validation. - -### Block Building - -Native bundler integration enables: - -- **Larger bundles**: Reduces signatures from worst case 2N to best case N+1 (N = number of user ops), improving data availability on Base Chain -- **Priority fee ordering**: User operations ordered by priority fee within bundles - -Initial approach: One large bundle at the middle of each flashblock with priority fee ordering within that bundle. - -#### Bundle Construction - -1. Incrementally stack user op validation phases -2. Attempt to include the transaction -3. Prune and resubmit any reverting ops -4. Execute once the bundle is built (no revert risk in execution phase) - -#### Key Management - -The block builder requires a hot bundler key that accrues ETH. Balance is swept periodically (every N blocks) to the sequencer address. - -Future AA V2 phases will add a new transaction type to remove this hot key requirement. - -## RPC Methods - -### Base Node Methods - -| Method | Description | -|--------|-------------| -| `base_validateUserOperation` | Validates user operation conforms to ERC-7562 with successful validation phase | -| `eth_supportedEntrypoints` | Returns supported entry points | -| `eth_estimateUserOperationGas` | Returns PVG and gas limit estimates | -| `eth_sendUserOperation` | Sends user operation to TIPS pipeline after validation | -| `eth_getUserOperationByHash` | Gets user operation by hash (flashblock enabled) | -| `eth_getUserOperationReceipt` | Gets user operation receipt (flashblock enabled) | - -### Gas Estimation - -`eth_estimateUserOperationGas` returns: -- Verification gas limit -- Execution gas limit -- PreVerificationGas (PVG) - -PVG covers bundler tip, entrypoint overhead, and L1 data fee. - -### PreVerificationGas (PVG) - -Current issues: -1. Primary source of overcharging -2. User ops stuck in mempool if PVG too low - -Solutions: -- Future AA V2 will decouple L1 data fee from bundler tip + overhead via `l1GasFeeLimit` -- TIPS native bundler amortizes costs across user ops in bundles, enabling lower PVG values -- Clear error messages for PVG too low conditions - -## Call Flow - -``` -Wallet (EIP-5792) - ↓ -eth_sendUserOperation - ↓ -base_validateUserOperation (ERC-7562) - ↓ -User Operation Queue (Kafka) - ↓ -User Operation Mempool - ↓ -rBuilder (bundle construction) - ↓ -Block Inclusion - ↓ -Audit Pipeline -``` diff --git a/docs/PULL_REQUEST_GUIDELINES.md b/docs/PULL_REQUEST_GUIDELINES.md deleted file mode 100644 index 8fb41800..00000000 --- a/docs/PULL_REQUEST_GUIDELINES.md +++ /dev/null @@ -1,93 +0,0 @@ -# Pull Request Guidelines - -## Overview - -PRs are the lifeline of the team. They allow us to ship value, determine maintenance cost, and impact daily quality of life. Well-maintained code sustains velocity. - -## Why - -A quality pull request process enables sustained velocity and consistent delivery. - -Pull requests allow us to: - -- **Hold and improve quality**: Catch bugs and architectural issues early -- **Build team expertise**: Share knowledge through clear descriptions and thoughtful reviews -- **Stay customer focused**: Keep PRs tight and decoupled for incremental, reversible changes -- **Encourage ownership**: Clear domain ownership motivates high quality and reduces incidents - -## SLA - -| Metric | Target | Rationale | -|--------|--------|-----------| -| PR Review | Within half a working day | If reviews take longer than 1 working day, something needs improvement | - -## Success Metrics - -| Metric | Why | -|--------|-----| -| Time to PR Review | Fast reviews power the flywheel: shared context → quality code → maintainable codebase → iteration | -| Time from PR Open to Production | Deployed code reaches customers | -| Incidents | Quality authoring and review catches errors early | -| QA Regression Bugs | Quality authoring and review catches errors early | - -## Authoring Guidelines - -### Keep PRs Tight - -PRs should make either deep changes (few files, significant logic) or shallow changes (many files, simple refactors). - -- **< 500 LOC** (guideline; auto-generated or boilerplate may exceed this) -- **< 10 files** (guideline; renames may touch more files) - -### Write Clear Descriptions - -Enable reviewers to understand the problem and verify the solution. Good descriptions also serve as documentation. - -### Test Thoroughly - -Any code change impacts flows. Include: -- Manual testing -- Unit tests -- QA regression tests where appropriate - -### Choose Reviewers Carefully - -Select codebase owners and domain experts. Reach out early to allow reviewers to schedule time. - -### Budget Time for Reviews - -Allow time for comments, suggestions, and improvements. Code is written once but read many times. - -### Consider Live Reviews - -Synchronous reviews can resolve alignment faster. Document decisions in the PR for posterity. - -## Reviewing Guidelines - -### Review Within Half a Day - -Fast reviews generate a flywheel of velocity and knowledge-sharing. - -### Review in Detail - -No rubber stamping. Given good descriptions and self-review, PRs should be relatively easy to review thoroughly. - -## Codebase Ownership - -### Unit Tests - -Owners decide on unit test thresholds reflecting appropriate effort and business risk. - -### Conventions - -Team should have consensus on conventions. Ideally automated or linted; otherwise documented. - -## FAQ - -**Can we make an exception for tight timelines?** - -Yes, exceptions are always possible. For large PRs, hold a retro to identify what could be done differently. - -**When should authors seek reviewers?** - -As early as possible. Reviewers of design artifacts (PPS/TDD) should likely also review the PR. diff --git a/docs/SETUP.md b/docs/SETUP.md deleted file mode 100644 index 64892558..00000000 --- a/docs/SETUP.md +++ /dev/null @@ -1,117 +0,0 @@ -# Local Development Setup - -This guide covers setting up and running TIPS locally with all required dependencies. - -## Prerequisites - -- Docker and Docker Compose -- Rust (latest stable) -- Go (1.21+) -- Just command runner (`cargo install just`) -- Git - -## Clone Repositories - -Clone the three required repositories: - -```bash -# TIPS (this repository) -git clone https://github.com/base/tips.git - -# builder-playground (separate directory) -git clone https://github.com/flashbots/builder-playground.git -cd builder-playground -git remote add danyal git@github.com:danyalprout/builder-playground.git -git checkout danyal/base-overlay - -# op-rbuilder (separate directory) -git clone https://github.com/base/op-rbuilder.git -``` - -## Start Services - -### 1. TIPS Infrastructure - -```bash -cd tips -just sync # Sync and load environment variables -just start-all # Start all TIPS services -``` - -This starts: -- Docker containers (Kafka, MinIO, node-reth) -- Ingress RPC service -- Audit service -- Bundle pool service -- UI - -### 2. builder-playground - -Provides L1/L2 blockchain infrastructure: - -```bash -cd builder-playground -go run main.go cook opstack \ - --external-builder http://host.docker.internal:4444/ \ - --enable-latest-fork 0 \ - --flashblocks \ - --base-overlay \ - --flashblocks-builder ws://host.docker.internal:1111/ws -``` - -Keep this terminal running. - -### 3. op-rbuilder - -Handles L2 block building: - -```bash -cd op-rbuilder -just run-playground -``` - -Keep this terminal running. - -## Test the System - -```bash -cd tips -just send-txn -``` - -This submits a test transaction through the ingress → audit → bundle pool → builder pipeline. - -## Port Reference - -| Service | Port | Description | -|---------|------|-------------| -| TIPS Ingress RPC | 8080 | Bundle submission endpoint | -| TIPS UI | 3000 | Debug interface | -| MinIO Console | 7001 | Object storage UI | -| MinIO API | 7000 | Object storage API | -| Kafka | 9092 | Message broker | -| op-rbuilder | 4444 | Block builder API | - -## Development Workflow - -1. Keep builder-playground and op-rbuilder running -2. Run `just start-all` after code changes -3. Test with `just send-txn` -4. View logs with `docker logs -f ` -5. Access UI at http://localhost:3000 - -Query block information: - -```bash -just get-blocks -``` - -## Stop Services - -```bash -cd tips -just stop-all - -# Ctrl+C in op-rbuilder terminal -# Ctrl+C in builder-playground terminal -``` diff --git a/docs/logo.png b/docs/logo.png deleted file mode 100644 index 5dc999b136f37eb93277a7ae8e6c6f89f5f3e082..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 127664 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%0DZ1e%+iKGJNQ;S10@f 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 bT=$Q+4wsaj2{+p3;eIyP9V~FyZYTT~ME0hr diff --git a/ui/next.config.ts b/next.config.ts similarity index 100% rename from ui/next.config.ts rename to next.config.ts diff --git a/ui/package-lock.json b/package-lock.json similarity index 100% rename from ui/package-lock.json rename to package-lock.json diff --git a/ui/package.json b/package.json similarity index 100% rename from ui/package.json rename to package.json diff --git a/ui/postcss.config.mjs b/postcss.config.mjs similarity index 100% rename from ui/postcss.config.mjs rename to postcss.config.mjs diff --git a/ui/public/logo.svg b/public/logo.svg similarity index 100% rename from ui/public/logo.svg rename to public/logo.svg diff --git a/ui/src/app/api/block/[hash]/route.ts b/src/app/api/block/[hash]/route.ts similarity index 100% rename from ui/src/app/api/block/[hash]/route.ts rename to src/app/api/block/[hash]/route.ts diff --git a/ui/src/app/api/blocks/route.ts b/src/app/api/blocks/route.ts similarity index 100% rename from ui/src/app/api/blocks/route.ts rename to src/app/api/blocks/route.ts diff --git a/ui/src/app/api/bundle/[uuid]/route.ts b/src/app/api/bundle/[uuid]/route.ts similarity index 100% rename from ui/src/app/api/bundle/[uuid]/route.ts rename to src/app/api/bundle/[uuid]/route.ts diff --git a/ui/src/app/api/health/route.ts b/src/app/api/health/route.ts similarity index 100% rename from ui/src/app/api/health/route.ts rename to src/app/api/health/route.ts diff --git a/ui/src/app/api/txn/[hash]/route.ts b/src/app/api/txn/[hash]/route.ts similarity index 100% rename from ui/src/app/api/txn/[hash]/route.ts rename to src/app/api/txn/[hash]/route.ts diff --git a/ui/src/app/block/[hash]/page.tsx b/src/app/block/[hash]/page.tsx similarity index 100% rename from ui/src/app/block/[hash]/page.tsx rename to src/app/block/[hash]/page.tsx diff --git a/ui/src/app/bundles/[uuid]/page.tsx b/src/app/bundles/[uuid]/page.tsx similarity index 100% rename from ui/src/app/bundles/[uuid]/page.tsx rename to src/app/bundles/[uuid]/page.tsx diff --git a/ui/src/app/globals.css b/src/app/globals.css similarity index 100% rename from ui/src/app/globals.css rename to src/app/globals.css diff --git a/ui/src/app/layout.tsx b/src/app/layout.tsx similarity index 100% rename from ui/src/app/layout.tsx rename to src/app/layout.tsx diff --git a/ui/src/app/page.tsx b/src/app/page.tsx similarity index 100% rename from ui/src/app/page.tsx rename to src/app/page.tsx diff --git a/ui/src/app/txn/[hash]/page.tsx b/src/app/txn/[hash]/page.tsx similarity index 100% rename from ui/src/app/txn/[hash]/page.tsx rename to src/app/txn/[hash]/page.tsx diff --git a/ui/src/lib/s3.ts b/src/lib/s3.ts similarity index 100% rename from ui/src/lib/s3.ts rename to src/lib/s3.ts diff --git a/ui/tsconfig.json b/tsconfig.json similarity index 100% rename from ui/tsconfig.json rename to tsconfig.json diff --git a/ui/.gitignore b/ui/.gitignore deleted file mode 100644 index 5ef6a520..00000000 --- a/ui/.gitignore +++ /dev/null @@ -1,41 +0,0 @@ -# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. - -# dependencies -/node_modules -/.pnp -.pnp.* -.yarn/* -!.yarn/patches -!.yarn/plugins -!.yarn/releases -!.yarn/versions - -# testing -/coverage - -# next.js -/.next/ -/out/ - -# production -/build - -# misc -.DS_Store -*.pem - -# debug -npm-debug.log* -yarn-debug.log* -yarn-error.log* -.pnpm-debug.log* - -# env files (can opt-in for committing if needed) -.env* - -# vercel -.vercel - -# typescript -*.tsbuildinfo -next-env.d.ts diff --git a/ui/Dockerfile b/ui/Dockerfile deleted file mode 100644 index 8615be83..00000000 --- a/ui/Dockerfile +++ /dev/null @@ -1,40 +0,0 @@ -FROM node:20-alpine AS deps -WORKDIR /app -COPY ui/package.json ui/yarn.lock ./ -RUN --mount=type=cache,target=/root/.yarn \ - yarn install --frozen-lockfile - -FROM node:20-alpine AS builder -WORKDIR /app -COPY --from=deps /app/node_modules ./node_modules -COPY ./ui . - -ENV NEXT_TELEMETRY_DISABLED=1 - -RUN --mount=type=cache,target=/app/.next/cache \ - yarn build - -FROM node:20-alpine AS runner -WORKDIR /app - -ENV NODE_ENV=production -ENV NEXT_TELEMETRY_DISABLED=1 - -RUN addgroup --system --gid 1001 nodejs -RUN adduser --system --uid 1001 nextjs - -RUN mkdir .next -RUN chown nextjs:nodejs .next - -COPY --from=builder /app/public ./public -COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./ -COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static - -USER nextjs - -EXPOSE 3000 - -ENV PORT=3000 -ENV HOSTNAME="0.0.0.0" - -CMD ["node", "server.js"] \ No newline at end of file diff --git a/ui/yarn.lock b/yarn.lock similarity index 100% rename from ui/yarn.lock rename to yarn.lock