From 1c9fa5d342fb67ac52c66cfe36133b5502e2adad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Migone?= Date: Thu, 30 Apr 2026 17:55:41 -0300 Subject: [PATCH 1/4] refactor: move aggregator to bin subdir MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Tomás Migone --- .github/dependabot.yml | 2 +- .github/workflows/release.yml | 2 +- .release-please-manifest.json | 2 +- Cargo.toml | 2 +- README.md | 2 +- crates/{ => bin}/aggregator/CHANGELOG.md | 0 crates/{ => bin}/aggregator/Cargo.toml | 4 ++-- crates/{ => bin}/aggregator/README.md | 0 crates/{ => bin}/aggregator/build.rs | 0 crates/{ => bin}/aggregator/proto/graph_tally.proto | 0 crates/{ => bin}/aggregator/proto/tap_aggregator_legacy.proto | 0 crates/{ => bin}/aggregator/proto/uint128.proto | 0 crates/{ => bin}/aggregator/src/aggregator.rs | 0 crates/{ => bin}/aggregator/src/api_versioning.rs | 0 crates/{ => bin}/aggregator/src/error_codes.rs | 0 crates/{ => bin}/aggregator/src/grpc.rs | 0 crates/{ => bin}/aggregator/src/jsonrpsee_helpers.rs | 0 crates/{ => bin}/aggregator/src/lib.rs | 0 crates/{ => bin}/aggregator/src/main.rs | 0 crates/{ => bin}/aggregator/src/metrics.rs | 0 crates/{ => bin}/aggregator/src/server.rs | 0 crates/{ => bin}/aggregator/tests/aggregate_test.rs | 0 crates/integration_tests/Cargo.toml | 2 +- release-please-config.json | 2 +- 24 files changed, 9 insertions(+), 9 deletions(-) rename crates/{ => bin}/aggregator/CHANGELOG.md (100%) rename crates/{ => bin}/aggregator/Cargo.toml (93%) rename crates/{ => bin}/aggregator/README.md (100%) rename crates/{ => bin}/aggregator/build.rs (100%) rename crates/{ => bin}/aggregator/proto/graph_tally.proto (100%) rename crates/{ => bin}/aggregator/proto/tap_aggregator_legacy.proto (100%) rename crates/{ => bin}/aggregator/proto/uint128.proto (100%) rename crates/{ => bin}/aggregator/src/aggregator.rs (100%) rename crates/{ => bin}/aggregator/src/api_versioning.rs (100%) rename crates/{ => bin}/aggregator/src/error_codes.rs (100%) rename crates/{ => bin}/aggregator/src/grpc.rs (100%) rename crates/{ => bin}/aggregator/src/jsonrpsee_helpers.rs (100%) rename crates/{ => bin}/aggregator/src/lib.rs (100%) rename crates/{ => bin}/aggregator/src/main.rs (100%) rename crates/{ => bin}/aggregator/src/metrics.rs (100%) rename crates/{ => bin}/aggregator/src/server.rs (100%) rename crates/{ => bin}/aggregator/tests/aggregate_test.rs (100%) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 4b59a2a..0476a8c 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -1,7 +1,7 @@ version: 2 updates: - package-ecosystem: "cargo" - directory: "/crates/aggregator" + directory: "/crates/bin/aggregator" schedule: interval: "daily" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index fa49a7e..323b486 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -22,7 +22,7 @@ jobs: pull-requests: write runs-on: ubuntu-latest outputs: - graph_tally_aggregator: ${{ steps.release-please.outputs['crates/aggregator--tag_name'] }} + graph_tally_aggregator: ${{ steps.release-please.outputs['crates/bin/aggregator--tag_name'] }} steps: - name: Release please id: release-please diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 62a1906..cb71226 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,6 +1,6 @@ { ".": "0.0.0", - "crates/aggregator": "0.7.0", + "crates/bin/aggregator": "0.7.0", "crates/core": "7.0.0", "crates/integration_tests": "0.1.28", "crates/eip712_message": "1.0.0", diff --git a/Cargo.toml b/Cargo.toml index e4579d1..6673a6a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [workspace] resolver = "2" members = [ - "crates/aggregator", + "crates/bin/aggregator", "crates/core", "crates/eip712_message", "crates/graph", diff --git a/README.md b/README.md index 4b89704..9ab1ff6 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ simplifying the payment process. ## Documentation for Individual Components -- [graph_tally_aggregator](crates/aggregator/README.md) +- [graph_tally_aggregator](crates/bin/aggregator/README.md) ## Key Components diff --git a/crates/aggregator/CHANGELOG.md b/crates/bin/aggregator/CHANGELOG.md similarity index 100% rename from crates/aggregator/CHANGELOG.md rename to crates/bin/aggregator/CHANGELOG.md diff --git a/crates/aggregator/Cargo.toml b/crates/bin/aggregator/Cargo.toml similarity index 93% rename from crates/aggregator/Cargo.toml rename to crates/bin/aggregator/Cargo.toml index fd41802..8ec3612 100644 --- a/crates/aggregator/Cargo.toml +++ b/crates/bin/aggregator/Cargo.toml @@ -28,8 +28,8 @@ rdkafka.workspace = true serde.workspace = true serde_json.workspace = true strum.workspace = true -graph_tally_core = { path = "../core" } -graph_tally_graph = { path = "../graph" } +graph_tally_core = { path = "../../core" } +graph_tally_graph = { path = "../../graph" } thegraph-core = { workspace = true, features = ["alloy-eip712"] } tokio.workspace = true tonic.workspace = true diff --git a/crates/aggregator/README.md b/crates/bin/aggregator/README.md similarity index 100% rename from crates/aggregator/README.md rename to crates/bin/aggregator/README.md diff --git a/crates/aggregator/build.rs b/crates/bin/aggregator/build.rs similarity index 100% rename from crates/aggregator/build.rs rename to crates/bin/aggregator/build.rs diff --git a/crates/aggregator/proto/graph_tally.proto b/crates/bin/aggregator/proto/graph_tally.proto similarity index 100% rename from crates/aggregator/proto/graph_tally.proto rename to crates/bin/aggregator/proto/graph_tally.proto diff --git a/crates/aggregator/proto/tap_aggregator_legacy.proto b/crates/bin/aggregator/proto/tap_aggregator_legacy.proto similarity index 100% rename from crates/aggregator/proto/tap_aggregator_legacy.proto rename to crates/bin/aggregator/proto/tap_aggregator_legacy.proto diff --git a/crates/aggregator/proto/uint128.proto b/crates/bin/aggregator/proto/uint128.proto similarity index 100% rename from crates/aggregator/proto/uint128.proto rename to crates/bin/aggregator/proto/uint128.proto diff --git a/crates/aggregator/src/aggregator.rs b/crates/bin/aggregator/src/aggregator.rs similarity index 100% rename from crates/aggregator/src/aggregator.rs rename to crates/bin/aggregator/src/aggregator.rs diff --git a/crates/aggregator/src/api_versioning.rs b/crates/bin/aggregator/src/api_versioning.rs similarity index 100% rename from crates/aggregator/src/api_versioning.rs rename to crates/bin/aggregator/src/api_versioning.rs diff --git a/crates/aggregator/src/error_codes.rs b/crates/bin/aggregator/src/error_codes.rs similarity index 100% rename from crates/aggregator/src/error_codes.rs rename to crates/bin/aggregator/src/error_codes.rs diff --git a/crates/aggregator/src/grpc.rs b/crates/bin/aggregator/src/grpc.rs similarity index 100% rename from crates/aggregator/src/grpc.rs rename to crates/bin/aggregator/src/grpc.rs diff --git a/crates/aggregator/src/jsonrpsee_helpers.rs b/crates/bin/aggregator/src/jsonrpsee_helpers.rs similarity index 100% rename from crates/aggregator/src/jsonrpsee_helpers.rs rename to crates/bin/aggregator/src/jsonrpsee_helpers.rs diff --git a/crates/aggregator/src/lib.rs b/crates/bin/aggregator/src/lib.rs similarity index 100% rename from crates/aggregator/src/lib.rs rename to crates/bin/aggregator/src/lib.rs diff --git a/crates/aggregator/src/main.rs b/crates/bin/aggregator/src/main.rs similarity index 100% rename from crates/aggregator/src/main.rs rename to crates/bin/aggregator/src/main.rs diff --git a/crates/aggregator/src/metrics.rs b/crates/bin/aggregator/src/metrics.rs similarity index 100% rename from crates/aggregator/src/metrics.rs rename to crates/bin/aggregator/src/metrics.rs diff --git a/crates/aggregator/src/server.rs b/crates/bin/aggregator/src/server.rs similarity index 100% rename from crates/aggregator/src/server.rs rename to crates/bin/aggregator/src/server.rs diff --git a/crates/aggregator/tests/aggregate_test.rs b/crates/bin/aggregator/tests/aggregate_test.rs similarity index 100% rename from crates/aggregator/tests/aggregate_test.rs rename to crates/bin/aggregator/tests/aggregate_test.rs diff --git a/crates/integration_tests/Cargo.toml b/crates/integration_tests/Cargo.toml index 0ad563d..976204f 100644 --- a/crates/integration_tests/Cargo.toml +++ b/crates/integration_tests/Cargo.toml @@ -10,7 +10,7 @@ description = "Integration tests for Graph Tally." publish = false [dependencies] -graph_tally_aggregator = { path = "../aggregator" } +graph_tally_aggregator = { path = "../bin/aggregator" } graph_tally_core = { path = "../core" } graph_tally_graph = { path = "../graph" } diff --git a/release-please-config.json b/release-please-config.json index 9e6738b..30bdf62 100644 --- a/release-please-config.json +++ b/release-please-config.json @@ -7,7 +7,7 @@ "crates/graph": {}, "crates/eip712_message": {}, "crates/receipt": {}, - "crates/aggregator": { + "crates/bin/aggregator": { "bump-minor-pre-major": true, "bump-patch-for-minor-pre-major": true, "prerelease": true From ef679f142937990e24efc3bcf81c8549931c73b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Migone?= Date: Thu, 30 Apr 2026 18:20:17 -0300 Subject: [PATCH 2/4] feat!: add escrow manager from its own repo MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Tomás Migone --- .github/dependabot.yml | 5 + .github/workflows/release.yml | 5 +- .release-please-manifest.json | 1 + Cargo.toml | 1 + README.md | 93 +- crates/bin/escrow_manager/Cargo.toml | 34 + crates/bin/escrow_manager/README.md | 117 ++ .../bin/escrow_manager/src/abi/ERC20.abi.json | 303 ++++ .../src/abi/GraphTallyCollector.abi.json | 795 +++++++++ .../src/abi/PaymentsEscrow.abi.json | 585 +++++++ crates/bin/escrow_manager/src/config.rs | 60 + crates/bin/escrow_manager/src/contracts.rs | 189 ++ crates/bin/escrow_manager/src/kafka.rs | 376 ++++ crates/bin/escrow_manager/src/main.rs | 362 ++++ crates/bin/escrow_manager/src/metrics.rs | 105 ++ crates/bin/escrow_manager/src/subgraphs.rs | 130 ++ .../Dockerfile.graph_tally_aggregator | 0 docker/Dockerfile.graph_tally_escrow_manager | 28 + grafana/escrow_manager.json | 1520 +++++++++++++++++ release-please-config.json | 5 + 20 files changed, 4643 insertions(+), 71 deletions(-) create mode 100644 crates/bin/escrow_manager/Cargo.toml create mode 100644 crates/bin/escrow_manager/README.md create mode 100644 crates/bin/escrow_manager/src/abi/ERC20.abi.json create mode 100644 crates/bin/escrow_manager/src/abi/GraphTallyCollector.abi.json create mode 100644 crates/bin/escrow_manager/src/abi/PaymentsEscrow.abi.json create mode 100644 crates/bin/escrow_manager/src/config.rs create mode 100644 crates/bin/escrow_manager/src/contracts.rs create mode 100644 crates/bin/escrow_manager/src/kafka.rs create mode 100644 crates/bin/escrow_manager/src/main.rs create mode 100644 crates/bin/escrow_manager/src/metrics.rs create mode 100644 crates/bin/escrow_manager/src/subgraphs.rs rename Dockerfile.graph_tally_aggregator => docker/Dockerfile.graph_tally_aggregator (100%) create mode 100644 docker/Dockerfile.graph_tally_escrow_manager create mode 100644 grafana/escrow_manager.json diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 0476a8c..606fdc5 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -5,6 +5,11 @@ updates: schedule: interval: "daily" + - package-ecosystem: "cargo" + directory: "/crates/bin/escrow_manager" + schedule: + interval: "daily" + - package-ecosystem: "cargo" directory: "/crates/core" schedule: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 323b486..d2b14fd 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -23,6 +23,7 @@ jobs: runs-on: ubuntu-latest outputs: graph_tally_aggregator: ${{ steps.release-please.outputs['crates/bin/aggregator--tag_name'] }} + graph_tally_escrow_manager: ${{ steps.release-please.outputs['crates/bin/escrow_manager--tag_name'] }} steps: - name: Release please id: release-please @@ -36,7 +37,7 @@ jobs: if: always() && (needs.release-please.result == 'success' || needs.release-please.result == 'skipped') strategy: matrix: - target: [graph_tally_aggregator] + target: [graph_tally_aggregator, graph_tally_escrow_manager] permissions: packages: write steps: @@ -88,4 +89,4 @@ jobs: context: ./ push: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository }} tags: ${{ steps.meta.outputs.tags }} - file: Dockerfile.${{ matrix.target }} + file: docker/Dockerfile.${{ matrix.target }} diff --git a/.release-please-manifest.json b/.release-please-manifest.json index cb71226..e6024fc 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,6 +1,7 @@ { ".": "0.0.0", "crates/bin/aggregator": "0.7.0", + "crates/bin/escrow_manager": "1.0.0", "crates/core": "7.0.0", "crates/integration_tests": "0.1.28", "crates/eip712_message": "1.0.0", diff --git a/Cargo.toml b/Cargo.toml index 6673a6a..bbb15c1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,6 +2,7 @@ resolver = "2" members = [ "crates/bin/aggregator", + "crates/bin/escrow_manager", "crates/core", "crates/eip712_message", "crates/graph", diff --git a/README.md b/README.md index 9ab1ff6..54b2d01 100644 --- a/README.md +++ b/README.md @@ -1,83 +1,38 @@ # Graph Tally -| Crate | Latest Version | -| ------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| **graph_tally_aggregator** | [![GHCR](https://img.shields.io/github/v/release/graphprotocol/graph-tally?filter=graph_tally_aggregator-*&label=ghcr.io)](https://github.com/graphprotocol/graph-tally/pkgs/container/graph_tally_aggregator) | -| **graph_tally_core** | [![GitHub Release](https://img.shields.io/github/v/release/graphprotocol/graph-tally?filter=graph_tally_core-*)](https://github.com/graphprotocol/graph-tally/releases) | -| **graph_tally_eip712_message** | [![GitHub Release](https://img.shields.io/github/v/release/graphprotocol/graph-tally?filter=graph_tally_eip712_message-*)](https://github.com/graphprotocol/graph-tally/releases) | -| **graph_tally_graph** | [![GitHub Release](https://img.shields.io/github/v/release/graphprotocol/graph-tally?filter=graph_tally_graph-*)](https://github.com/graphprotocol/graph-tally/releases) | -| **graph_tally_receipt** | [![GitHub Release](https://img.shields.io/github/v/release/graphprotocol/graph-tally?filter=graph_tally_receipt-*)](https://github.com/graphprotocol/graph-tally/releases) | +Graph Tally (formerly TAP - Timeline Aggregation Protocol) is a trust-minimized payment system between Gateways and Indexers in [The Graph](https://thegraph.com) network. It supports arbitrary data services built on Graph Horizon. -## Overview +## How it works -Graph Tally (formerly TAP - Timeline Aggregation Protocol) facilitates a series of payments from a -sender to a receiver (Receipts), who aggregates these payments into a single -payment (a Receipt Aggregate Voucher, or RAV). This aggregate payment can then be -verified on-chain by a payment verifier, reducing the number of transactions and -simplifying the payment process. +1. **Gateways** send signed Receipts to Indexers alongside each query +2. **Indexers** collect Receipts and periodically request aggregation +3. **Aggregator** bundles Receipts into a signed Receipt Aggregate Voucher (RAV) +4. **Indexers** redeem RAVs on-chain to claim payment from the Gateway's escrow -## Documentation for Individual Components +This reduces on-chain transactions from one-per-query to one-per-aggregation-period, drastically lowering costs while maintaining cryptographic guarantees via EIP-712 signatures. -- [graph_tally_aggregator](crates/bin/aggregator/README.md) +A more detailed specification of the protocol can be found in the following GIPs: +- [GIP-0054](https://github.com/graphprotocol/graph-improvement-proposals/blob/main/gips/0054-timeline-aggregation-protocol.md) - Original TAP specification +- [GIP-0066](https://github.com/graphprotocol/graph-improvement-proposals/blob/main/gips/0066-graph-horizon.md) - Graph Horizon, introducing TAP v2 (renamed to Graph Tally) -## Key Components +## Binaries -- **Sender:** Initiates the payment. -- **Receiver:** Receives the payment. -- **Signers:** Multiple signers authorized by the sender to sign receipts. -- **State Channel:** A one-way channel opened by the sender with the receiver -for sending receipts. -- **Receipt:** A record of payment sent by the sender to the receiver. -- **ReceiptAggregateVoucher (RAV):** A signed message containing the aggregate -value of the receipts. -- **graph_tally_aggregator:** A service managed by the sender that aggregates receipts -on the receiver's request into a signed RAV. -- **EscrowAccount:** An account created in the blockchain to hold funds for -the sender-receiver pair. +These services are run by **Gateway operators**. See each component's README for configuration and deployment details. -## Security Measures +| Binary | Description | Docker Image | +|--------|-------------|--------------| +| [graph_tally_aggregator](crates/bin/aggregator/README.md) | Aggregates Receipts into signed RAVs | [![GHCR](https://img.shields.io/github/v/release/graphprotocol/graph-tally?filter=graph_tally_aggregator-*&label=ghcr.io)](https://github.com/graphprotocol/graph-tally/pkgs/container/graph_tally_aggregator) | +| [graph_tally_escrow_manager](crates/bin/escrow_manager/README.md) | Manages escrow balances for Indexer payments | [![GHCR](https://img.shields.io/github/v/release/graphprotocol/graph-tally?filter=graph_tally_escrow_manager-*&label=ghcr.io)](https://github.com/graphprotocol/graph-tally/pkgs/container/graph_tally_escrow_manager) | -- The protocol uses asymmetric cryptography (ECDSA secp256k1) to sign and -verify messages, ensuring the integrity of receipts and RAVs. +## Libraries -## Process - -1. **Opening a State Channel:** A state channel is opened via a blockchain -contract, creating an EscrowAccount for the sender-receiver pair. -2. **Sending Receipts:** The sender sends receipts to the receiver through the -state channel. -3. **Storing Receipts:** The receiver stores the receipts and tracks the -aggregate payment. -4. **Creating a RAV Request:** A RAV request consists of a list of receipts and, -optionally, the previous RAV. -5. **Signing the RAV:** The receiver sends the RAV request to the graph_tally_aggregator, -which signs it into a new RAV. -6. **Tracking Aggregate Value:** The receiver tracks the aggregate value and -new receipts since the last RAV. -7. **Requesting a New RAV:** The receiver sends new receipts and the last RAV -to the graph_tally_aggregator for a new RAV. -8. **Closing the State Channel:** When the allocation period ends, the receiver -can send the last RAV to the blockchain and receive payment from the EscrowAccount. - -## Performance Considerations - -- The primary performance limitations are the time required to verify receipts -and network limitations for sending requests to the graph_tally_aggregator. - -## Use Cases - -- Graph Tally is suitable for systems that need unidirectional, parallel -micro-payments that are too expensive to redeem individually on-chain. By -aggregating operations off-chain and redeeming them in one transaction, costs -are drastically reduced. - -## Compatibility - -- The current implementation is for EVM-compatible blockchains, with most of the -system being off-chain. +| Crate | Version | +|-------|---------| +| [graph_tally_core](crates/core) | [![crates.io](https://img.shields.io/crates/v/graph_tally_core)](https://crates.io/crates/graph_tally_core) | +| [graph_tally_receipt](crates/receipt) | [![crates.io](https://img.shields.io/crates/v/graph_tally_receipt)](https://crates.io/crates/graph_tally_receipt) | +| [graph_tally_graph](crates/graph) | [![crates.io](https://img.shields.io/crates/v/graph_tally_graph)](https://crates.io/crates/graph_tally_graph) | +| [graph_tally_eip712_message](crates/eip712_message) | [![crates.io](https://img.shields.io/crates/v/graph_tally_eip712_message)](https://crates.io/crates/graph_tally_eip712_message) | ## Contributing -Contributions are welcome! Please submit a pull request or open an issue to -discuss potential changes. -Also, make sure to follow the [Contributing Guide](https://github.com/graphprotocol/graph-tally/blob/main/CONTRIBUTING.md). +Contributions are welcome! Please submit a pull request or open an issue to discuss potential changes. See the [Contributing Guide](CONTRIBUTING.md) for details. diff --git a/crates/bin/escrow_manager/Cargo.toml b/crates/bin/escrow_manager/Cargo.toml new file mode 100644 index 0000000..ce98d0d --- /dev/null +++ b/crates/bin/escrow_manager/Cargo.toml @@ -0,0 +1,34 @@ +[package] +name = "graph_tally_escrow_manager" +version = "1.0.0" +edition.workspace = true +rust-version.workspace = true +license.workspace = true +repository.workspace = true +readme = "README.md" +description = "Manages Graph Tally escrow balances on behalf of a gateway sender." + +[[bin]] +name = "graph_tally_escrow_manager" +path = "src/main.rs" + +[dependencies] +alloy = { version = "1.0.3", features = ["contract", "signer-local"] } +anyhow.workspace = true +axum.workspace = true +chrono = { version = "0.4.34", default-features = false, features = ["clock"] } +futures-util.workspace = true +lazy_static.workspace = true +prometheus.workspace = true +prost.workspace = true +rdkafka = { workspace = true, features = ["cmake-build", "gssapi", "tracing"] } +reqwest = "0.12.5" +serde.workspace = true +serde_json.workspace = true +serde_with = "3.4.0" +snmalloc-rs = "0.3.4" +thegraph-client-subgraphs = "0.3.2" +titorelli = { git = "https://github.com/edgeandnode/titorelli.git", rev = "4c14fc1" } +tokio = { workspace = true, features = ["net", "rt-multi-thread", "time", "tracing"] } +tracing = "0.1.40" +tracing-subscriber.workspace = true diff --git a/crates/bin/escrow_manager/README.md b/crates/bin/escrow_manager/README.md new file mode 100644 index 0000000..1831e66 --- /dev/null +++ b/crates/bin/escrow_manager/README.md @@ -0,0 +1,117 @@ +# Graph Tally Escrow Manager + +This service maintains Graph Tally escrow balances on behalf of a gateway sender. + +The following data sources are monitored to guide the allocation of GRT into the [Graph Horizon Escrow contract](https://github.com/graphprotocol/contracts/blob/main/packages/horizon/contracts/payments/PaymentsEscrow.sol): + +- [Graph Network Subgraph](https://github.com/graphprotocol/graph-network-subgraph) - for active allocations, escrow accounts, and authorized signers +- Kafka topics for receipts and RAVs - to track outstanding debts from query fees + +# Configuration + +Configuration options are set via a single JSON file. The structure of the file is defined in [src/config.rs](src/config.rs). + +## Key Options + +| Field | Description | +|-------|-------------| +| `authorize_signers` | If `true`, automatically authorize signers on startup | +| `dry_run` | If `true`, skip contract calls (useful for testing) | +| `port_metrics` | Port for Prometheus metrics server (default: 9090) | +| `update_interval_seconds` | Polling interval for the main loop | + +## Sender and Signers + +The sender address used for graph_tally_escrow_manager expects authorizedSigners: + +- **Sender**: Requires ETH for transaction gas and GRT to allocate into Graph Tally escrow balances for paying indexers +- **Authorized signer**: Used by the gateway and graph_tally_aggregator to sign receipts and RAVs + +When `authorize_signers` is set to `true`, the graph_tally_escrow_manager will automatically setup authorized signers on startup. This requires the secret keys for the authorized signer wallets to be present in the `signers` config field. + +## Setting up Authorized Signers Manually + +To set up authorized signers for graph_tally_escrow_manager: + +1. [Find the `PaymentsEscrow` contract address](https://github.com/graphprotocol/contracts/blob/main/packages/horizon/addresses.json) for your network. +2. Navigate to the relevant blockchain explorer (e.g., https://arbiscan.io/address/0xf6Fcc27aAf1fcD8B254498c9794451d82afC673E). +3. Connect the sender address (the address graph_tally_escrow_manager is running with, or the address whose private key you provide in the `secret_key` field of `graph_tally_escrow_manager` config). +4. Go to the "Write Contract" tab and find the `authorizeSigner` function. +5. Generate proof and proofDeadline using the script below. + +```bash +mkdir proof-generator && cd proof-generator +npm init -y +npm install ethers +cat > generateProof.js << EOL +const ethers = require('ethers'); + +async function generateProof(signerPrivateKey, proofDeadline, senderAddress, chainId) { + const signer = new ethers.Wallet(signerPrivateKey); + + const messageHash = ethers.solidityPackedKeccak256( + ['uint256', 'uint256', 'address'], + [chainId, proofDeadline, senderAddress] + ); + + const digest = ethers.hashMessage(ethers.getBytes(messageHash)); + const signature = await signer.signMessage(ethers.getBytes(messageHash)); + + return signature; +} + +const signerPrivateKey = process.argv[2]; +const senderAddress = process.argv[3]; +const chainId = parseInt(process.argv[4]); +const proofDeadline = Math.floor(Date.now() / 1000) + 3600; // 1 hour from now + +if (!signerPrivateKey || !senderAddress || !chainId) { + console.error('Usage: node generateProof.js '); + process.exit(1); +} + +generateProof(signerPrivateKey, proofDeadline, senderAddress, chainId) + .then(proof => { + console.log('Proof:', proof); + console.log('ProofDeadline:', proofDeadline); + console.log('Human-readable date:', new Date(proofDeadline * 1000).toUTCString()); + console.log('Chain ID:', chainId); + }) + .catch(error => console.error('Error:', error)); +EOL + +echo "Setup complete. Run the script with:" +echo "node generateProof.js " +``` + +6. Pass signerAddress, proofDeadline, and proof to the contract and sign the transaction. Repeat if using multiple authorisedSigners + +# Logs + +Log levels are controlled by the `RUST_LOG` environment variable ([details](https://docs.rs/tracing-subscriber/latest/tracing_subscriber/filter/struct.EnvFilter.html)). + +Example: `RUST_LOG=info,graph_tally_escrow_manager=debug cargo run -- config.json` + +# Metrics + +Prometheus metrics are exposed on a separate HTTP server. Configure the port via `port_metrics` in the config file (default: 9090). + +```bash +curl http://localhost:9090/metrics +``` + +### Available Metrics + +| Metric | Type | Description | +|--------|------|-------------| +| `escrow_total_debt_grt` | Gauge | Total outstanding debt across all receivers | +| `escrow_total_balance_grt` | Gauge | Total escrow balance across all receivers | +| `escrow_total_adjustment_grt` | Gauge | Total GRT deposited in the last cycle | +| `escrow_receiver_count` | Gauge | Number of receivers being tracked | +| `escrow_loop_duration_seconds` | Histogram | Duration of each polling cycle | +| `escrow_debt_grt{receiver}` | Gauge | Outstanding debt per receiver | +| `escrow_balance_grt{receiver}` | Gauge | Escrow balance per receiver | +| `escrow_adjustment_grt{receiver}` | Gauge | Last adjustment per receiver | +| `escrow_deposit_ok` | Counter | Successful deposit transactions | +| `escrow_deposit_err` | Counter | Failed deposit transactions | +| `escrow_deposit_duration` | Histogram | Deposit transaction duration | diff --git a/crates/bin/escrow_manager/src/abi/ERC20.abi.json b/crates/bin/escrow_manager/src/abi/ERC20.abi.json new file mode 100644 index 0000000..694c759 --- /dev/null +++ b/crates/bin/escrow_manager/src/abi/ERC20.abi.json @@ -0,0 +1,303 @@ +[ + { + "inputs": [ + { + "internalType": "string", + "name": "name_", + "type": "string" + }, + { + "internalType": "string", + "name": "symbol_", + "type": "string" + }, + { + "internalType": "uint8", + "name": "decimals_", + "type": "uint8" + }, + { + "internalType": "uint256", + "name": "initialBalance_", + "type": "uint256" + }, + { + "internalType": "address payable", + "name": "feeReceiver_", + "type": "address" + } + ], + "stateMutability": "payable", + "type": "constructor" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "name": "Approval", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "name": "Transfer", + "type": "event" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "internalType": "address", + "name": "spender", + "type": "address" + } + ], + "name": "allowance", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "approve", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "balanceOf", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "decimals", + "outputs": [ + { + "internalType": "uint8", + "name": "", + "type": "uint8" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "internalType": "uint256", + "name": "subtractedValue", + "type": "uint256" + } + ], + "name": "decreaseAllowance", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "internalType": "uint256", + "name": "addedValue", + "type": "uint256" + } + ], + "name": "increaseAllowance", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "name", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "symbol", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "totalSupply", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "recipient", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "transfer", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "sender", + "type": "address" + }, + { + "internalType": "address", + "name": "recipient", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "transferFrom", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + } +] diff --git a/crates/bin/escrow_manager/src/abi/GraphTallyCollector.abi.json b/crates/bin/escrow_manager/src/abi/GraphTallyCollector.abi.json new file mode 100644 index 0000000..7d64182 --- /dev/null +++ b/crates/bin/escrow_manager/src/abi/GraphTallyCollector.abi.json @@ -0,0 +1,795 @@ +[ + { + "inputs": [ + { + "internalType": "string", + "name": "eip712Name", + "type": "string" + }, + { + "internalType": "string", + "name": "eip712Version", + "type": "string" + }, + { + "internalType": "address", + "name": "controller", + "type": "address" + }, + { + "internalType": "uint256", + "name": "revokeSignerThawingPeriod", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "inputs": [], + "name": "AuthorizableInvalidSignerProof", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "proofDeadline", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "currentTimestamp", + "type": "uint256" + } + ], + "name": "AuthorizableInvalidSignerProofDeadline", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "authorizer", + "type": "address" + }, + { + "internalType": "address", + "name": "signer", + "type": "address" + }, + { + "internalType": "bool", + "name": "revoked", + "type": "bool" + } + ], + "name": "AuthorizableSignerAlreadyAuthorized", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "authorizer", + "type": "address" + }, + { + "internalType": "address", + "name": "signer", + "type": "address" + } + ], + "name": "AuthorizableSignerNotAuthorized", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "signer", + "type": "address" + } + ], + "name": "AuthorizableSignerNotThawing", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "currentTimestamp", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "thawEndTimestamp", + "type": "uint256" + } + ], + "name": "AuthorizableSignerStillThawing", + "type": "error" + }, + { + "inputs": [], + "name": "ECDSAInvalidSignature", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "length", + "type": "uint256" + } + ], + "name": "ECDSAInvalidSignatureLength", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "s", + "type": "bytes32" + } + ], + "name": "ECDSAInvalidSignatureS", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "bytes", + "name": "contractName", + "type": "bytes" + } + ], + "name": "GraphDirectoryInvalidZeroAddress", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "caller", + "type": "address" + }, + { + "internalType": "address", + "name": "dataService", + "type": "address" + } + ], + "name": "GraphTallyCollectorCallerNotDataService", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "tokens", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "tokensCollected", + "type": "uint256" + } + ], + "name": "GraphTallyCollectorInconsistentRAVTokens", + "type": "error" + }, + { + "inputs": [], + "name": "GraphTallyCollectorInvalidRAVSigner", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "tokensToCollect", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "maxTokensToCollect", + "type": "uint256" + } + ], + "name": "GraphTallyCollectorInvalidTokensToCollectAmount", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "dataService", + "type": "address" + } + ], + "name": "GraphTallyCollectorUnauthorizedDataService", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidShortString", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "string", + "name": "str", + "type": "string" + } + ], + "name": "StringTooLong", + "type": "error" + }, + { + "anonymous": false, + "inputs": [], + "name": "EIP712DomainChanged", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "graphToken", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "graphStaking", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "graphPayments", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "graphEscrow", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "graphController", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "graphEpochManager", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "graphRewardsManager", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "graphTokenGateway", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "graphProxyAdmin", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "graphCuration", + "type": "address" + } + ], + "name": "GraphDirectoryInitialized", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "bytes32", + "name": "collectionId", + "type": "bytes32" + }, + { + "indexed": true, + "internalType": "address", + "name": "payer", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "serviceProvider", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "dataService", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint64", + "name": "timestampNs", + "type": "uint64" + }, + { + "indexed": false, + "internalType": "uint128", + "name": "valueAggregate", + "type": "uint128" + }, + { + "indexed": false, + "internalType": "bytes", + "name": "metadata", + "type": "bytes" + }, + { + "indexed": false, + "internalType": "bytes", + "name": "signature", + "type": "bytes" + } + ], + "name": "RAVCollected", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "authorizer", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "signer", + "type": "address" + } + ], + "name": "SignerAuthorized", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "authorizer", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "signer", + "type": "address" + } + ], + "name": "SignerRevoked", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "authorizer", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "signer", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "thawEndTimestamp", + "type": "uint256" + } + ], + "name": "SignerThawCanceled", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "authorizer", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "signer", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "thawEndTimestamp", + "type": "uint256" + } + ], + "name": "SignerThawing", + "type": "event" + }, + { + "inputs": [], + "name": "REVOKE_AUTHORIZATION_THAWING_PERIOD", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "signer", + "type": "address" + } + ], + "name": "authorizations", + "outputs": [ + { + "internalType": "address", + "name": "authorizer", + "type": "address" + }, + { + "internalType": "uint256", + "name": "thawEndTimestamp", + "type": "uint256" + }, + { + "internalType": "bool", + "name": "revoked", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "signer", + "type": "address" + }, + { + "internalType": "uint256", + "name": "proofDeadline", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "proof", + "type": "bytes" + } + ], + "name": "authorizeSigner", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "signer", + "type": "address" + } + ], + "name": "cancelThawSigner", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "eip712Domain", + "outputs": [ + { + "internalType": "bytes1", + "name": "fields", + "type": "bytes1" + }, + { + "internalType": "string", + "name": "name", + "type": "string" + }, + { + "internalType": "string", + "name": "version", + "type": "string" + }, + { + "internalType": "uint256", + "name": "chainId", + "type": "uint256" + }, + { + "internalType": "address", + "name": "verifyingContract", + "type": "address" + }, + { + "internalType": "bytes32", + "name": "salt", + "type": "bytes32" + }, + { + "internalType": "uint256[]", + "name": "extensions", + "type": "uint256[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "bytes32", + "name": "collectionId", + "type": "bytes32" + }, + { + "internalType": "address", + "name": "payer", + "type": "address" + }, + { + "internalType": "address", + "name": "serviceProvider", + "type": "address" + }, + { + "internalType": "address", + "name": "dataService", + "type": "address" + }, + { + "internalType": "uint64", + "name": "timestampNs", + "type": "uint64" + }, + { + "internalType": "uint128", + "name": "valueAggregate", + "type": "uint128" + }, + { + "internalType": "bytes", + "name": "metadata", + "type": "bytes" + } + ], + "internalType": "struct IGraphTallyCollector.ReceiptAggregateVoucher", + "name": "rav", + "type": "tuple" + } + ], + "name": "encodeRAV", + "outputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "signer", + "type": "address" + } + ], + "name": "getThawEnd", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "authorizer", + "type": "address" + }, + { + "internalType": "address", + "name": "signer", + "type": "address" + } + ], + "name": "isAuthorized", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "components": [ + { + "internalType": "bytes32", + "name": "collectionId", + "type": "bytes32" + }, + { + "internalType": "address", + "name": "payer", + "type": "address" + }, + { + "internalType": "address", + "name": "serviceProvider", + "type": "address" + }, + { + "internalType": "address", + "name": "dataService", + "type": "address" + }, + { + "internalType": "uint64", + "name": "timestampNs", + "type": "uint64" + }, + { + "internalType": "uint128", + "name": "valueAggregate", + "type": "uint128" + }, + { + "internalType": "bytes", + "name": "metadata", + "type": "bytes" + } + ], + "internalType": "struct IGraphTallyCollector.ReceiptAggregateVoucher", + "name": "rav", + "type": "tuple" + }, + { + "internalType": "bytes", + "name": "signature", + "type": "bytes" + } + ], + "internalType": "struct IGraphTallyCollector.SignedRAV", + "name": "signedRAV", + "type": "tuple" + } + ], + "name": "recoverRAVSigner", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "signer", + "type": "address" + } + ], + "name": "revokeAuthorizedSigner", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "signer", + "type": "address" + } + ], + "name": "thawSigner", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "dataService", + "type": "address" + }, + { + "internalType": "bytes32", + "name": "collectionId", + "type": "bytes32" + }, + { + "internalType": "address", + "name": "receiver", + "type": "address" + }, + { + "internalType": "address", + "name": "payer", + "type": "address" + } + ], + "name": "tokensCollected", + "outputs": [ + { + "internalType": "uint256", + "name": "tokens", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + } + ] \ No newline at end of file diff --git a/crates/bin/escrow_manager/src/abi/PaymentsEscrow.abi.json b/crates/bin/escrow_manager/src/abi/PaymentsEscrow.abi.json new file mode 100644 index 0000000..62002fe --- /dev/null +++ b/crates/bin/escrow_manager/src/abi/PaymentsEscrow.abi.json @@ -0,0 +1,585 @@ +[ + { + "inputs": [ + { + "internalType": "address", + "name": "controller", + "type": "address" + }, + { + "internalType": "uint256", + "name": "withdrawEscrowThawingPeriod", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "target", + "type": "address" + } + ], + "name": "AddressEmptyCode", + "type": "error" + }, + { + "inputs": [], + "name": "FailedCall", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "bytes", + "name": "contractName", + "type": "bytes" + } + ], + "name": "GraphDirectoryInvalidZeroAddress", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidInitialization", + "type": "error" + }, + { + "inputs": [], + "name": "NotInitializing", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "balanceBefore", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "balanceAfter", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "tokens", + "type": "uint256" + } + ], + "name": "PaymentsEscrowInconsistentCollection", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "balance", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "minBalance", + "type": "uint256" + } + ], + "name": "PaymentsEscrowInsufficientBalance", + "type": "error" + }, + { + "inputs": [], + "name": "PaymentsEscrowInvalidZeroTokens", + "type": "error" + }, + { + "inputs": [], + "name": "PaymentsEscrowIsPaused", + "type": "error" + }, + { + "inputs": [], + "name": "PaymentsEscrowNotThawing", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "currentTimestamp", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "thawEndTimestamp", + "type": "uint256" + } + ], + "name": "PaymentsEscrowStillThawing", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "thawingPeriod", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "maxWaitPeriod", + "type": "uint256" + } + ], + "name": "PaymentsEscrowThawingPeriodTooLong", + "type": "error" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "payer", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "collector", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "receiver", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "tokensThawing", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "thawEndTimestamp", + "type": "uint256" + } + ], + "name": "CancelThaw", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "payer", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "collector", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "receiver", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "tokens", + "type": "uint256" + } + ], + "name": "Deposit", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "graphToken", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "graphStaking", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "graphPayments", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "graphEscrow", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "graphController", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "graphEpochManager", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "graphRewardsManager", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "graphTokenGateway", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "graphProxyAdmin", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "graphCuration", + "type": "address" + } + ], + "name": "GraphDirectoryInitialized", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint64", + "name": "version", + "type": "uint64" + } + ], + "name": "Initialized", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "payer", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "collector", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "receiver", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "tokens", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "thawEndTimestamp", + "type": "uint256" + } + ], + "name": "Thaw", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "payer", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "collector", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "receiver", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "tokens", + "type": "uint256" + } + ], + "name": "Withdraw", + "type": "event" + }, + { + "inputs": [], + "name": "MAX_WAIT_PERIOD", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "WITHDRAW_ESCROW_THAWING_PERIOD", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "collector", + "type": "address" + }, + { + "internalType": "address", + "name": "receiver", + "type": "address" + } + ], + "name": "cancelThaw", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "collector", + "type": "address" + }, + { + "internalType": "address", + "name": "receiver", + "type": "address" + }, + { + "internalType": "uint256", + "name": "tokens", + "type": "uint256" + } + ], + "name": "deposit", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "payer", + "type": "address" + }, + { + "internalType": "address", + "name": "collector", + "type": "address" + }, + { + "internalType": "address", + "name": "receiver", + "type": "address" + }, + { + "internalType": "uint256", + "name": "tokens", + "type": "uint256" + } + ], + "name": "depositTo", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "payer", + "type": "address" + }, + { + "internalType": "address", + "name": "collector", + "type": "address" + }, + { + "internalType": "address", + "name": "receiver", + "type": "address" + } + ], + "name": "escrowAccounts", + "outputs": [ + { + "internalType": "uint256", + "name": "balance", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "tokensThawing", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "thawEndTimestamp", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "payer", + "type": "address" + }, + { + "internalType": "address", + "name": "collector", + "type": "address" + }, + { + "internalType": "address", + "name": "receiver", + "type": "address" + } + ], + "name": "getBalance", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "initialize", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes[]", + "name": "data", + "type": "bytes[]" + } + ], + "name": "multicall", + "outputs": [ + { + "internalType": "bytes[]", + "name": "results", + "type": "bytes[]" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "collector", + "type": "address" + }, + { + "internalType": "address", + "name": "receiver", + "type": "address" + }, + { + "internalType": "uint256", + "name": "tokens", + "type": "uint256" + } + ], + "name": "thaw", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "collector", + "type": "address" + }, + { + "internalType": "address", + "name": "receiver", + "type": "address" + } + ], + "name": "withdraw", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + } + ] \ No newline at end of file diff --git a/crates/bin/escrow_manager/src/config.rs b/crates/bin/escrow_manager/src/config.rs new file mode 100644 index 0000000..181adac --- /dev/null +++ b/crates/bin/escrow_manager/src/config.rs @@ -0,0 +1,60 @@ +use std::collections::BTreeMap; + +use alloy::primitives::{Address, B256}; +use reqwest::Url; +use serde::Deserialize; +use serde_with::serde_as; + +#[serde_as] +#[derive(Deserialize)] +pub struct Config { + /// Authorize signers on startup. + pub authorize_signers: bool, + /// Skip contract calls (for testing/debugging). + #[serde(default)] + pub dry_run: bool, + /// Table of minimum debts by indexer. This can be used, for example, to account for receipts + /// missing from the kafka topic. + pub debts: BTreeMap, + /// PaymentsEscrow contract address + pub payments_escrow_contract: Address, + /// GraphTallyCollector contract address + pub graph_tally_collector_contract: Address, + /// GRT contract for updating allowance + pub grt_contract: Address, + /// GRT allowance to set on startup + pub grt_allowance: u64, + /// Kafka configuration + pub kafka: Kafka, + /// Graph network subgraph URL + #[serde_as(as = "serde_with::DisplayFromStr")] + pub network_subgraph: Url, + /// API key for querying subgraphs + pub query_auth: String, + /// RPC for executing transactions + #[serde_as(as = "serde_with::DisplayFromStr")] + pub rpc_url: Url, + /// Secret key of the Graph Tally payer wallet + pub secret_key: B256, + /// Secret keys of the Graph Tally signer wallets, used to filter the indexer fees messages. + pub signers: Vec, + /// Period of the subgraph polling cycle + pub update_interval_seconds: u32, + /// Port for metrics server + #[serde(default = "default_port_metrics")] + pub port_metrics: u16, +} + +fn default_port_metrics() -> u16 { + 9090 +} + +#[derive(Debug, Deserialize)] +pub struct Kafka { + pub config: BTreeMap, + pub realtime_topic: String, + pub aggregated_topic: Option, + /// Cutoff timestamp (unix milliseconds) for aggregated topic data. + /// Aggregated records older than this are ignored. + pub aggregated_cutoff_timestamp: Option, +} diff --git a/crates/bin/escrow_manager/src/contracts.rs b/crates/bin/escrow_manager/src/contracts.rs new file mode 100644 index 0000000..52506e4 --- /dev/null +++ b/crates/bin/escrow_manager/src/contracts.rs @@ -0,0 +1,189 @@ +use std::time::{Duration, SystemTime, UNIX_EPOCH}; + +use alloy::{ + network::EthereumWallet, + primitives::{keccak256, Address, BlockNumber, Bytes, U256}, + providers::{DynProvider, Provider as _, ProviderBuilder, WalletProvider}, + signers::{local::PrivateKeySigner, SignerSync as _}, + sol, + sol_types::SolInterface, +}; +use anyhow::{anyhow, Context as _}; +use reqwest::Url; + +sol!( + #[allow(missing_docs)] + #[sol(rpc)] + ERC20, + "src/abi/ERC20.abi.json" +); +use ERC20::ERC20Instance; +sol!( + #[allow(missing_docs)] + #[sol(rpc)] + #[derive(Debug)] + PaymentsEscrow, + "src/abi/PaymentsEscrow.abi.json" +); +use PaymentsEscrow::{PaymentsEscrowErrors, PaymentsEscrowInstance}; +sol!( + #[allow(missing_docs)] + #[sol(rpc)] + #[derive(Debug)] + GraphTallyCollector, + "src/abi/GraphTallyCollector.abi.json" +); +use GraphTallyCollector::{GraphTallyCollectorErrors, GraphTallyCollectorInstance}; + +pub struct Contracts { + payments_escrow: PaymentsEscrowInstance, + graph_tally_collector: GraphTallyCollectorInstance, + token: ERC20Instance, + payer: Address, +} + +impl Contracts { + pub fn new( + payer: PrivateKeySigner, + chain_rpc: Url, + token: Address, + payments_escrow: Address, + graph_tally_collector: Address, + ) -> Self { + let provider = ProviderBuilder::new() + .with_simple_nonce_management() + .wallet(EthereumWallet::from(payer)) + .connect_http(chain_rpc); + let payer = provider.default_signer_address(); + let provider = provider.erased(); + let payments_escrow = PaymentsEscrowInstance::new(payments_escrow, provider.clone()); + let graph_tally_collector = + GraphTallyCollectorInstance::new(graph_tally_collector, provider.clone()); + let token = ERC20Instance::new(token, provider.clone()); + Self { + payments_escrow, + graph_tally_collector, + token, + payer, + } + } + + pub fn payer(&self) -> Address { + self.payer + } + + pub async fn allowance(&self) -> anyhow::Result { + self.token + .allowance(self.payer(), *self.payments_escrow.address()) + .call() + .await + .context("get allowance")? + .try_into() + .context("result out of bounds") + } + + pub async fn approve(&self, amount: u128) -> anyhow::Result<()> { + self.token + .approve(*self.payments_escrow.address(), U256::from(amount)) + .send() + .await? + .with_timeout(Some(Duration::from_secs(30))) + .with_required_confirmations(1) + .watch() + .await?; + Ok(()) + } + + pub async fn deposit_many( + &self, + deposits: impl IntoIterator, + ) -> anyhow::Result { + // Create individual deposit calls for multicall + let calls: Vec = deposits + .into_iter() + .map(|(receiver, amount)| { + self.payments_escrow + .deposit( + *self.graph_tally_collector.address(), + receiver, + U256::from(amount), + ) + .calldata() + .clone() + }) + .collect(); + + // Execute all deposits in a single multicall transaction + let receipt = self + .payments_escrow + .multicall(calls) + .send() + .await + .map_err(decoded_err::)? + .with_timeout(Some(Duration::from_secs(30))) + .with_required_confirmations(1) + .get_receipt() + .await?; + + let block_number = receipt + .block_number + .ok_or_else(|| anyhow!("invalid deposit receipt"))?; + Ok(block_number) + } + + pub async fn authorize_signer(&self, signer: &PrivateKeySigner) -> anyhow::Result<()> { + let chain_id = self + .graph_tally_collector + .provider() + .get_chain_id() + .await + .context("get chain ID")?; + let deadline_offset_s = 60; + let deadline = U256::from( + SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_secs() + + deadline_offset_s, + ); + // Build the message according to the contract's expectation: + // abi.encodePacked(block.chainid, address(this), "authorizeSignerProof", _proofDeadline, msg.sender) + let mut message = Vec::new(); + message.extend_from_slice(&U256::from(chain_id).to_be_bytes::<32>()); + message.extend_from_slice(&self.graph_tally_collector.address().0 .0); + message.extend_from_slice(b"authorizeSignerProof"); + message.extend_from_slice(&deadline.to_be_bytes::<32>()); + message.extend_from_slice(&self.payer().0 .0); + + let hash = keccak256(&message); + + // Sign with Ethereum message prefix (matching toEthSignedMessageHash) + let signature = signer + .sign_message_sync(hash.as_slice()) + .context("sign authorization proof")?; + let proof: Bytes = signature.as_bytes().into(); + + self.graph_tally_collector + .authorizeSigner(signer.address(), deadline, proof) + .send() + .await + .map_err(decoded_err::)? + .with_timeout(Some(Duration::from_secs(60))) + .with_required_confirmations(1) + .watch() + .await?; + Ok(()) + } +} + +fn decoded_err(err: alloy::contract::Error) -> anyhow::Error { + match err { + alloy::contract::Error::TransportError(alloy::transports::RpcError::ErrorResp(err)) => { + match err.as_decoded_interface_error::() { + Some(decoded) => anyhow!("{:?}", decoded), + None => anyhow!(err), + } + } + _ => anyhow!(err), + } +} diff --git a/crates/bin/escrow_manager/src/kafka.rs b/crates/bin/escrow_manager/src/kafka.rs new file mode 100644 index 0000000..6b54472 --- /dev/null +++ b/crates/bin/escrow_manager/src/kafka.rs @@ -0,0 +1,376 @@ +pub use ravs::ravs; +use rdkafka::consumer::StreamConsumer; +pub use receipts::receipts; + +use crate::config; + +fn consumer(config: &config::Kafka) -> anyhow::Result { + let mut consumer_config = rdkafka::ClientConfig::from_iter(config.config.clone()); + let defaults = [ + ("group.id", "graph-tally-escrow-manager"), + ("enable.auto.commit", "true"), + ("enable.auto.offset.store", "true"), + ]; + for (key, value) in defaults { + if !consumer_config.config_map().contains_key(key) { + consumer_config.set(key, value); + } + } + Ok(consumer_config.create()?) +} + +mod receipts { + use std::collections::BTreeMap; + + use alloy::{hex::ToHexExt as _, primitives::Address}; + use anyhow::{anyhow, Context as _}; + use chrono::{DateTime, Duration, Utc}; + use futures_util::StreamExt as _; + use prost::Message as _; + use rdkafka::{ + consumer::{Consumer as _, StreamConsumer}, + Message as _, + }; + use titorelli::kafka::{assign_partitions, latest_messages}; + use tokio::sync::{mpsc, watch}; + + use super::consumer; + use crate::config; + + pub async fn receipts( + config: &config::Kafka, + signers: Vec
, + ) -> anyhow::Result>> { + let window = Duration::days(28); + let (tx, rx) = watch::channel(Default::default()); + let db = DB::spawn(window, tx); + let mut consumer = consumer(config)?; + + let start_timestamp = hourly_timestamp(Utc::now() - window); + if let Some(aggregated_topic) = &config.aggregated_topic { + let latest_aggregated_messages = + latest_messages(&consumer, &[aggregated_topic]).await?; + let mut latest_aggregated_offsets: BTreeMap = latest_aggregated_messages + .into_iter() + .map(|msg| (format!("{}/{}", msg.topic(), msg.partition()), msg.offset())) + .collect(); + assign_partitions(&consumer, &[aggregated_topic], start_timestamp).await?; + let mut latest_aggregated_timestamp = 0; + let mut stream = consumer.stream(); + while let Some(msg) = stream.next().await { + let msg = msg?; + let partition = format!("{}/{}", msg.topic(), msg.partition()); + let offset = msg.offset(); + let payload = msg + .payload() + .with_context(|| anyhow!("missing payload at {partition} {offset}"))?; + let msg = IndexerFeesHourlyProtobuf::decode(payload)?; + latest_aggregated_timestamp = latest_aggregated_timestamp.max(msg.timestamp); + if let Some(cutoff) = config.aggregated_cutoff_timestamp { + if msg.timestamp < cutoff { + continue; + } + } + for aggregation in &msg.aggregations { + if !signers.contains(&Address::from_slice(&aggregation.signer)) { + continue; + } + let update = Update { + timestamp: DateTime::from_timestamp_millis(msg.timestamp) + .context("timestamp out of range")?, + indexer: Address::from_slice(&aggregation.receiver), + fee: (aggregation.fee_grt * 1e18) as u128, + }; + db.send(update).await.unwrap(); + } + + if latest_aggregated_offsets.get(&partition).unwrap() == &offset { + latest_aggregated_offsets.remove(&partition); + if latest_aggregated_offsets.is_empty() { + break; + } + } + } + consumer.unassign()?; + let realtime_start = + latest_aggregated_timestamp + Duration::hours(1).num_milliseconds(); + assign_partitions(&consumer, &[&config.realtime_topic], realtime_start).await?; + } else { + assign_partitions(&consumer, &[&config.realtime_topic], start_timestamp).await?; + } + tokio::spawn(async move { + if let Err(kafka_consumer_err) = process_messages(&mut consumer, db, signers).await { + tracing::error!(%kafka_consumer_err); + } + }); + + Ok(rx) + } + + #[derive(prost::Message)] + struct IndexerFeesProtobuf { + /// 20 bytes (address) + #[prost(bytes, tag = "1")] + signer: Vec, + /// 20 bytes (address) + #[prost(bytes, tag = "2")] + receiver: Vec, + #[prost(double, tag = "3")] + fee_grt: f64, + } + + #[derive(prost::Message)] + struct IndexerFeesHourlyProtobuf { + /// start timestamp for aggregation, in unix milliseconds + #[prost(int64, tag = "1")] + timestamp: i64, + #[prost(message, repeated, tag = "2")] + aggregations: Vec, + } + + #[derive(prost::Message)] + struct ClientQueryProtobuf { + // 20 bytes (address) + #[prost(bytes, tag = "2")] + receipt_signer: Vec, + #[prost(message, repeated, tag = "10")] + indexer_queries: Vec, + } + #[derive(prost::Message)] + struct IndexerQueryProtobuf { + /// 20 bytes (address) + #[prost(bytes, tag = "1")] + indexer: Vec, + #[prost(double, tag = "6")] + fee_grt: f64, + } + + async fn process_messages( + consumer: &mut StreamConsumer, + db: mpsc::Sender, + signers: Vec
, + ) -> anyhow::Result<()> { + consumer + .stream() + .for_each_concurrent(16, |msg| async { + let msg = match msg { + Ok(msg) => msg, + Err(recv_error) => { + tracing::error!(%recv_error); + return; + } + }; + let payload = match msg.payload() { + Some(payload) => payload, + None => return, + }; + let timestamp = msg + .timestamp() + .to_millis() + .and_then(|t| DateTime::from_timestamp(t / 1_000, (t % 1_000) as u32 * 1_000)) + .unwrap_or_else(Utc::now); + let payload = match ClientQueryProtobuf::decode(payload) { + Ok(payload) => payload, + Err(payload_parse_err) => { + tracing::error!(%payload_parse_err, input = payload.encode_hex()); + return; + } + }; + if !signers.contains(&Address::from_slice(&payload.receipt_signer)) { + return; + } + for indexer_query in payload.indexer_queries { + let update = Update { + timestamp, + indexer: Address::from_slice(&indexer_query.indexer), + fee: (indexer_query.fee_grt * 1e18) as u128, + }; + let _ = db.send(update).await; + } + }) + .await; + Ok(()) + } + + pub struct Update { + pub timestamp: DateTime, + pub indexer: Address, + pub fee: u128, + } + + pub struct DB { + // indexer debts, aggregated per hour + data: BTreeMap>, + window: Duration, + tx: watch::Sender>, + } + + impl DB { + pub fn spawn( + window: Duration, + tx: watch::Sender>, + ) -> mpsc::Sender { + let mut db = Self { + data: Default::default(), + window, + tx, + }; + let (tx, mut rx) = mpsc::channel(128); + tokio::spawn(async move { + let mut last_snapshot = Utc::now(); + let buffer_size = 128; + let mut buffer: Vec = Vec::with_capacity(buffer_size); + loop { + rx.recv_many(&mut buffer, buffer_size).await; + let now = Utc::now(); + for update in buffer.drain(..) { + db.update(update, now); + } + + if (now - last_snapshot) >= Duration::seconds(1) { + db.prune(now); + let snapshot = db.snapshot(); + + let _ = db.tx.send(snapshot); + last_snapshot = now; + } + } + }); + tx + } + + fn update(&mut self, update: Update, now: DateTime) { + if update.timestamp < (now - self.window) { + return; + } + let entry = self + .data + .entry(update.indexer) + .or_default() + .entry(hourly_timestamp(update.timestamp)) + .or_default(); + *entry += update.fee; + } + + fn prune(&mut self, now: DateTime) { + let min_timestamp = hourly_timestamp(now - self.window); + self.data.retain(|_, entries| { + entries.retain(|t, _| *t > min_timestamp); + !entries.is_empty() + }); + } + + fn snapshot(&self) -> BTreeMap { + self.data + .iter() + .map(|(indexer, entries)| (*indexer, entries.values().sum())) + .collect() + } + } + + fn hourly_timestamp(t: DateTime) -> i64 { + let t = t.timestamp(); + t - (t % Duration::hours(1).num_seconds()) + } +} + +mod ravs { + use std::collections::BTreeMap; + + use alloy::primitives::Address; + use anyhow::Context as _; + use futures_util::StreamExt as _; + use rdkafka::{consumer::StreamConsumer, Message as _}; + use titorelli::kafka::assign_partitions; + use tokio::sync::watch; + + use super::consumer; + use crate::config; + + pub async fn ravs( + config: &config::Kafka, + signers: Vec
, + ) -> anyhow::Result>> { + let (tx, rx) = watch::channel(Default::default()); + let mut consumer = consumer(config)?; + assign_partitions(&consumer, &["gateway_ravs"], 0).await?; + tokio::spawn(async move { process_messages(&mut consumer, tx, signers).await }); + Ok(rx) + } + + async fn process_messages( + consumer: &mut StreamConsumer, + tx: watch::Sender>, + signers: Vec
, + ) { + consumer + .stream() + .for_each_concurrent(16, |msg| async { + let msg = match msg { + Ok(msg) => msg, + Err(recv_error) => { + tracing::error!(%recv_error); + return; + } + }; + let record = match parse_record(&msg) { + Ok(ParseResult::V2(record)) => record, + Ok(ParseResult::V1) => return, + Err(record_parse_err) => { + let key = msg.key().map(String::from_utf8_lossy); + let payload = msg.payload().map(String::from_utf8_lossy); + tracing::error!(%record_parse_err, ?key, ?payload); + return; + } + }; + if !signers.contains(&record.signer) { + return; + } + tx.send_if_modified(|map| { + match map.entry(record.allocation) { + std::collections::btree_map::Entry::Vacant(entry) => { + entry.insert(record.value); + } + std::collections::btree_map::Entry::Occupied(mut entry) + if *entry.get() < record.value => + { + entry.insert(record.value); + } + _ => return false, + }; + true + }); + }) + .await; + } + + struct Record { + signer: Address, + allocation: Address, + value: u128, + } + + enum ParseResult { + V2(Record), + V1, + } + + fn parse_record(msg: &rdkafka::message::BorrowedMessage) -> anyhow::Result { + let key = String::from_utf8_lossy(msg.key().context("missing key")?); + let payload = String::from_utf8_lossy(msg.payload().context("missing payload")?); + let (signer, id) = key.split_once(':').context("malformed key")?; + // V1: allocation ID is 20 bytes (42 chars with 0x prefix) + // V2: collection ID is 32 bytes (66 chars with 0x prefix) + if id.len() == 42 { + return Ok(ParseResult::V1); + } + anyhow::ensure!(id.len() == 66, "invalid id length: {}", id.len()); + // Allocation ID is the last 20 bytes of collection ID + let allocation = &id[26..]; // skip "0x" + 24 zero chars (12 bytes padding) + Ok(ParseResult::V2(Record { + signer: signer.parse()?, + allocation: allocation.parse()?, + value: payload.parse()?, + })) + } +} diff --git a/crates/bin/escrow_manager/src/main.rs b/crates/bin/escrow_manager/src/main.rs new file mode 100644 index 0000000..f2babbc --- /dev/null +++ b/crates/bin/escrow_manager/src/main.rs @@ -0,0 +1,362 @@ +mod config; +mod contracts; +mod kafka; +mod metrics; +mod subgraphs; + +use std::{ + collections::{BTreeMap, BTreeSet}, + io::Write as _, + net::{IpAddr, Ipv4Addr, SocketAddr}, + time::{Duration, Instant}, +}; + +use alloy::{primitives::Address, signers::local::PrivateKeySigner}; +use anyhow::{anyhow, Context as _}; +use axum::{http::StatusCode, routing, Router}; +use config::Config; +use contracts::Contracts; +use prometheus::Encoder as _; +use subgraphs::{active_allocations, authorized_signers, escrow_accounts}; +use thegraph_client_subgraphs::Client as SubgraphClient; +use tokio::{ + net::TcpListener, + select, + time::{interval, MissedTickBehavior}, +}; + +#[global_allocator] +static ALLOC: snmalloc_rs::SnMalloc = snmalloc_rs::SnMalloc; + +const GRT: u128 = 1_000_000_000_000_000_000; +const MIN_DEPOSIT: u128 = 2 * GRT; +const MAX_ADJUSTMENT: u128 = 10_000 * GRT; + +#[tokio::main] +async fn main() -> anyhow::Result<()> { + tracing_subscriber::fmt::init(); + + let config_file = std::env::args() + .nth(1) + .ok_or_else(|| anyhow!("missing config file argument"))?; + let config: Config = std::fs::read_to_string(config_file) + .map_err(anyhow::Error::from) + .and_then(|s| serde_json::from_str(&s).map_err(anyhow::Error::from)) + .context("failed to load config")?; + + if config.dry_run { + tracing::info!("dry run mode enabled, contract calls will be skipped"); + } + + let payer = PrivateKeySigner::from_bytes(&config.secret_key)?; + tracing::info!(payer = %payer.address()); + let contracts = Contracts::new( + payer, + config.rpc_url.clone(), + config.grt_contract, + config.payments_escrow_contract, + config.graph_tally_collector_contract, + ); + + let http = reqwest::Client::builder() + .timeout(Duration::from_secs(10)) + .build() + .unwrap(); + let mut network_subgraph = SubgraphClient::builder(http.clone(), config.network_subgraph) + .with_auth_token(Some(config.query_auth.clone())) + .build(); + + let mut signers: Vec = Default::default(); + for signer in config.signers { + let signer = PrivateKeySigner::from_slice(signer.as_slice()).context("load signer key")?; + signers.push(signer); + } + let signers = signers; + + if config.authorize_signers { + let authorized_signers = authorized_signers(&mut network_subgraph, &contracts.payer()) + .await + .context("fetch authorized signers")?; + for signer in &signers { + let authorized = authorized_signers.contains(&signer.address().0.into()); + tracing::info!(signer = %signer.address(), authorized); + if authorized { + continue; + } + if config.dry_run { + tracing::info!(signer = %signer.address(), "dry run: skipping authorize_signer"); + continue; + } + match contracts.authorize_signer(signer).await { + Ok(()) => tracing::info!(signer = %signer.address(), "authorized"), + Err(err) => tracing::error!("failed to authorize signer: {err:#}"), + }; + } + } + + let mut allowance = contracts.allowance().await?; + let expected_allowance = config.grt_allowance as u128 * GRT; + tracing::info!(allowance = allowance as f64 * 1e-18); + if allowance < expected_allowance { + if config.dry_run { + tracing::info!( + expected_allowance = expected_allowance as f64 * 1e-18, + "dry run: skipping approve" + ); + } else { + contracts + .approve(expected_allowance) + .await + .context("approve")?; + allowance = contracts.allowance().await?; + tracing::info!(allowance = allowance as f64 * 1e-18); + } + } + + let signers: Vec
= signers.into_iter().map(|s| s.address()).collect(); + let receipts = kafka::receipts(&config.kafka, signers.clone()) + .await + .context("failed to start receipts consumer")?; + let ravs = kafka::ravs(&config.kafka, signers) + .await + .context("failed to start RAVs consumer")?; + + // Host metrics on a separate server with a port that isn't open to public requests. + let port_metrics = config.port_metrics; + tokio::spawn(async move { + let router = Router::new().route("/metrics", routing::get(handle_metrics)); + let metrics_listener = TcpListener::bind(SocketAddr::new( + IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)), + port_metrics, + )) + .await + .expect("failed to bind metrics server"); + tracing::info!(port_metrics, "metrics server started"); + axum::serve(metrics_listener, router.into_make_service()) + .await + .expect("metrics server failed"); + }); + + let mut interval = interval(Duration::from_secs(config.update_interval_seconds as u64)); + interval.set_missed_tick_behavior(MissedTickBehavior::Skip); + let mut sigterm = tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate())?; + loop { + select! { + _ = interval.tick() => (), + _ = tokio::signal::ctrl_c() => anyhow::bail!("exit"), + _ = sigterm.recv() => anyhow::bail!("exit"), + }; + let loop_start = Instant::now(); + + let allocations = match active_allocations(&mut network_subgraph).await { + Ok(allocations) => allocations, + Err(active_allocations_err) => { + tracing::error!("{:#}", active_allocations_err.context("active allocations")); + continue; + } + }; + let mut receivers: BTreeSet
= allocations.iter().map(|a| a.indexer).collect(); + let escrow_accounts = match escrow_accounts(&mut network_subgraph, &contracts.payer()).await + { + Ok(escrow_accounts) => escrow_accounts, + Err(escrow_accounts_err) => { + if escrow_accounts_err.to_string().contains("missing block") { + tracing::warn!("{:#}", escrow_accounts_err.context("escrow accounts")); + } else { + tracing::error!("{:#}", escrow_accounts_err.context("escrow accounts")); + } + continue; + } + }; + receivers.extend(escrow_accounts.keys()); + tracing::debug!(receivers = receivers.len()); + + metrics::METRICS.receiver_count.set(receivers.len() as i64); + metrics::METRICS + .total_balance_grt + .set(escrow_accounts.values().sum::() as f64 / GRT as f64); + + let mut indexer_ravs: BTreeMap = Default::default(); + { + let allocation_ravs = ravs.borrow(); + for allocation in allocations { + if let Some(value) = allocation_ravs.get(&allocation.id) { + *indexer_ravs.entry(allocation.indexer).or_default() += *value; + } + } + } + + let mut debts: BTreeMap = Default::default(); + { + let receipts = receipts.borrow(); + for receiver in &receivers { + let receipts = *receipts.get(receiver).unwrap_or(&0); + let ravs = *indexer_ravs.get(receiver).unwrap_or(&0); + let debt = u128::max(receipts, ravs); + debts.insert(*receiver, debt); + tracing::info!( + %receiver, + receipts = %format!("{:.6}", receipts as f64 * 1e-18), + ravs = %format!("{:.6}", ravs as f64 * 1e-18), + ); + let receiver_str = format!("{receiver:?}"); + let balance = escrow_accounts.get(receiver).copied().unwrap_or(0); + metrics::METRICS + .balance_grt + .with_label_values(&[&receiver_str]) + .set(balance as f64 / GRT as f64); + metrics::METRICS + .debt_grt + .with_label_values(&[&receiver_str]) + .set(debt as f64 / GRT as f64); + } + }; + metrics::METRICS + .total_debt_grt + .set(debts.values().sum::() as f64 / GRT as f64); + + let adjustments: Vec<(Address, u128)> = receivers + .into_iter() + .filter_map(|receiver| { + let balance = escrow_accounts.get(&receiver).cloned().unwrap_or(0); + let debt = u128::max( + debts.get(&receiver).copied().unwrap_or(0), + config.debts.get(&receiver).copied().unwrap_or(0) as u128 * GRT, + ); + let next_balance = next_balance(debt); + let adjustment = next_balance.saturating_sub(balance); + if adjustment == 0 { + return None; + } + tracing::info!( + ?receiver, + balance_grt = (balance as f64) / (GRT as f64), + debt_grt = (debt as f64) / (GRT as f64), + adjustment_grt = (adjustment as f64) / (GRT as f64), + ); + let receiver_str = format!("{receiver:?}"); + metrics::METRICS + .adjustment_grt + .with_label_values(&[&receiver_str]) + .set(adjustment as f64 / GRT as f64); + Some((receiver, adjustment)) + }) + .collect(); + + let total_adjustment: u128 = adjustments.iter().map(|(_, a)| a).sum(); + tracing::info!(total_adjustment_grt = ((total_adjustment as f64) * 1e-18).ceil() as u64); + metrics::METRICS + .total_adjustment_grt + .set(total_adjustment as f64 / GRT as f64); + if total_adjustment > 0 { + let adjustments = if total_adjustment <= MAX_ADJUSTMENT { + adjustments + } else { + reduce_adjustments(adjustments) + }; + if config.dry_run { + for (receiver, adjustment) in &adjustments { + tracing::info!( + ?receiver, + adjustment_grt = (*adjustment as f64) / (GRT as f64), + "dry run: skipping deposit" + ); + } + continue; + } + let deposit_start = Instant::now(); + let deposit_result = contracts.deposit_many(adjustments).await; + metrics::METRICS + .deposit + .duration + .observe(deposit_start.elapsed().as_secs_f64()); + let tx_block = match deposit_result { + Ok(block) => { + metrics::METRICS.deposit.ok.inc(); + block + } + Err(deposit_err) => { + metrics::METRICS.deposit.err.inc(); + tracing::error!("{:#}", deposit_err.context("deposit")); + continue; + } + }; + network_subgraph = SubgraphClient::builder( + network_subgraph.http_client, + network_subgraph.subgraph_url, + ) + .with_auth_token(Some(config.query_auth.clone())) + .with_subgraph_latest_block(tx_block) + .build(); + + tracing::info!("adjustments complete"); + } + + metrics::METRICS + .loop_duration + .observe(loop_start.elapsed().as_secs_f64()); + } +} + +fn next_balance(debt: u128) -> u128 { + let mut next_round = (MIN_DEPOSIT / GRT) as u32; + while (debt as f64) >= ((next_round as u128 * GRT) as f64 * 0.6) { + next_round = next_round + .saturating_mul(2) + .min(next_round + (MAX_ADJUSTMENT / GRT) as u32); + } + next_round as u128 * GRT +} + +fn reduce_adjustments(adjustments: Vec<(Address, u128)>) -> Vec<(Address, u128)> { + let desired: BTreeMap = adjustments.into_iter().collect(); + assert!(desired.values().sum::() > MAX_ADJUSTMENT); + let mut adjustments: BTreeMap = + desired.keys().map(|r| (*r, MIN_DEPOSIT)).collect(); + loop { + for (receiver, desired_value) in &desired { + let adjustment_value = adjustments.entry(*receiver).or_default(); + if *adjustment_value < *desired_value { + *adjustment_value = (*desired_value).min(*adjustment_value + (100 * GRT)); + } + if adjustments.values().sum::() >= MAX_ADJUSTMENT { + return adjustments.into_iter().collect(); + } + } + } +} + +async fn handle_metrics() -> impl axum::response::IntoResponse { + let encoder = prometheus::TextEncoder::new(); + let metric_families = prometheus::gather(); + let mut buffer = Vec::new(); + if let Err(metrics_encode_err) = encoder.encode(&metric_families, &mut buffer) { + tracing::error!(%metrics_encode_err); + buffer.clear(); + write!(&mut buffer, "Failed to encode metrics").unwrap(); + return (StatusCode::INTERNAL_SERVER_ERROR, String::new()); + } + (StatusCode::OK, String::from_utf8(buffer).unwrap()) +} + +#[cfg(test)] +mod tests { + use super::{GRT, MIN_DEPOSIT}; + + #[test] + fn next_balance() { + let tests = [ + (0, MIN_DEPOSIT), + (GRT, MIN_DEPOSIT), + (MIN_DEPOSIT / 2, MIN_DEPOSIT), + (MIN_DEPOSIT, MIN_DEPOSIT * 2), + (MIN_DEPOSIT + 1, MIN_DEPOSIT * 2), + (30 * GRT, 64 * GRT), + (70 * GRT, 128 * GRT), + (100 * GRT, 256 * GRT), + ]; + for (debt, expected) in tests { + assert_eq!(super::next_balance(debt), expected); + } + } +} diff --git a/crates/bin/escrow_manager/src/metrics.rs b/crates/bin/escrow_manager/src/metrics.rs new file mode 100644 index 0000000..cc68e32 --- /dev/null +++ b/crates/bin/escrow_manager/src/metrics.rs @@ -0,0 +1,105 @@ +use lazy_static::lazy_static; +use prometheus::{ + register_gauge, register_gauge_vec, register_histogram, register_int_counter, + register_int_gauge, Gauge, GaugeVec, Histogram, IntCounter, IntGauge, +}; + +lazy_static! { + pub static ref METRICS: Metrics = Metrics::new(); +} + +pub struct Metrics { + pub total_debt_grt: Gauge, + pub total_balance_grt: Gauge, + pub total_adjustment_grt: Gauge, + pub receiver_count: IntGauge, + pub loop_duration: Histogram, + pub deposit: ResponseMetrics, + // Per-receiver metrics + pub debt_grt: GaugeVec, + pub balance_grt: GaugeVec, + pub adjustment_grt: GaugeVec, +} + +impl Metrics { + fn new() -> Self { + Self { + total_debt_grt: register_gauge!( + "escrow_total_debt_grt", + "total outstanding debt across all receivers in GRT" + ) + .unwrap(), + total_balance_grt: register_gauge!( + "escrow_total_balance_grt", + "total escrow balance across all receivers in GRT" + ) + .unwrap(), + total_adjustment_grt: register_gauge!( + "escrow_total_adjustment_grt", + "total GRT deposited in the last cycle" + ) + .unwrap(), + receiver_count: register_int_gauge!( + "escrow_receiver_count", + "number of receivers being tracked" + ) + .unwrap(), + loop_duration: register_histogram!( + "escrow_loop_duration_seconds", + "duration of each polling cycle in seconds" + ) + .unwrap(), + deposit: ResponseMetrics::new("escrow_deposit", "escrow deposit transaction"), + debt_grt: register_gauge_vec!( + "escrow_debt_grt", + "outstanding debt per receiver in GRT", + &["receiver"] + ) + .unwrap(), + balance_grt: register_gauge_vec!( + "escrow_balance_grt", + "escrow balance per receiver in GRT", + &["receiver"] + ) + .unwrap(), + adjustment_grt: register_gauge_vec!( + "escrow_adjustment_grt", + "last adjustment per receiver in GRT", + &["receiver"] + ) + .unwrap(), + } + } +} + +#[derive(Clone)] +pub struct ResponseMetrics { + pub ok: IntCounter, + pub err: IntCounter, + pub duration: Histogram, +} + +impl ResponseMetrics { + pub fn new(prefix: &str, description: &str) -> Self { + let metrics = Self { + ok: register_int_counter!( + &format!("{prefix}_ok"), + &format!("{description} success count"), + ) + .unwrap(), + err: register_int_counter!( + &format!("{prefix}_err"), + &format!("{description} error count"), + ) + .unwrap(), + duration: register_histogram!( + &format!("{prefix}_duration"), + &format!("{description} duration"), + ) + .unwrap(), + }; + metrics.ok.inc(); + metrics.err.inc(); + metrics + } +} diff --git a/crates/bin/escrow_manager/src/subgraphs.rs b/crates/bin/escrow_manager/src/subgraphs.rs new file mode 100644 index 0000000..7abd15a --- /dev/null +++ b/crates/bin/escrow_manager/src/subgraphs.rs @@ -0,0 +1,130 @@ +use std::collections::HashMap; + +use alloy::primitives::Address; +use anyhow::anyhow; +use serde_with::serde_as; +use thegraph_client_subgraphs::{Client as SubgraphClient, PaginatedQueryError}; + +pub async fn authorized_signers( + network_subgraph: &mut SubgraphClient, + payer: &Address, +) -> anyhow::Result> { + #[derive(serde::Deserialize)] + struct Data { + payer: Option, + } + #[derive(serde::Deserialize)] + struct Payer { + signers: Vec, + } + #[derive(serde::Deserialize)] + struct Signer { + id: Address, + } + let data = network_subgraph + .query::(format!( + r#"{{ payer(id:"{payer:?}") {{ signers {{ id }} }} }}"#, + )) + .await + .map_err(|err| anyhow!(err))?; + let signers = data + .payer + .into_iter() + .flat_map(|s| s.signers) + .map(|s| s.id) + .collect(); + Ok(signers) +} + +pub async fn escrow_accounts( + network_subgraph: &mut SubgraphClient, + payer: &Address, +) -> anyhow::Result> { + let query = format!( + r#" + paymentsEscrowAccounts( + block: $block + orderBy: id + orderDirection: asc + first: $first + where: {{ + id_gt: $last + payer: "{payer:?}" + }} + ) {{ + id + balance + receiver {{ + id + }} + }} + "# + ); + #[serde_as] + #[derive(serde::Deserialize)] + struct EscrowAccount { + #[serde_as(as = "serde_with::DisplayFromStr")] + balance: u128, + receiver: Receiver, + } + #[derive(serde::Deserialize)] + struct Receiver { + id: Address, + } + let response = network_subgraph + .paginated_query::(query, 500) + .await; + match response { + Ok(accounts) => Ok(accounts + .into_iter() + .map(|a| (a.receiver.id, a.balance)) + .collect()), + Err(PaginatedQueryError::EmptyResponse) => Ok(Default::default()), + Err(err) => Err(anyhow!(err)), + } +} + +pub struct Allocation { + pub id: Address, + pub indexer: Address, +} + +pub async fn active_allocations( + network_subgraph: &mut SubgraphClient, +) -> anyhow::Result> { + let query = r#" + allocations( + block: $block + orderBy: id + orderDirection: asc + first: $first + where: { + id_gt: $last + status: Active + isLegacy: false + } + ) { + id + indexer { id } + } + "#; + #[derive(serde::Deserialize)] + struct Allocation_ { + id: Address, + indexer: Indexer_, + } + #[derive(serde::Deserialize)] + struct Indexer_ { + id: Address, + } + Ok(network_subgraph + .paginated_query::(query, 500) + .await + .map_err(|err| anyhow!(err))? + .into_iter() + .map(|a| Allocation { + id: a.id, + indexer: a.indexer.id, + }) + .collect()) +} diff --git a/Dockerfile.graph_tally_aggregator b/docker/Dockerfile.graph_tally_aggregator similarity index 100% rename from Dockerfile.graph_tally_aggregator rename to docker/Dockerfile.graph_tally_aggregator diff --git a/docker/Dockerfile.graph_tally_escrow_manager b/docker/Dockerfile.graph_tally_escrow_manager new file mode 100644 index 0000000..49e3239 --- /dev/null +++ b/docker/Dockerfile.graph_tally_escrow_manager @@ -0,0 +1,28 @@ +FROM rust:1.91-bookworm AS build + +WORKDIR /root + +RUN apt-get update && apt-get install -y --no-install-recommends \ + clang \ + cmake \ + libcurl4-openssl-dev \ + librdkafka-dev \ + libsasl2-dev \ + libssl-dev \ + pkg-config \ + && rm -rf /var/lib/apt/lists/* + +COPY . . + +RUN cargo build --release --bin graph_tally_escrow_manager + +######################################################################################## + +FROM debian:bookworm-slim + +RUN apt-get update && apt-get install -y --no-install-recommends \ + ca-certificates libsasl2-dev libssl-dev \ + && rm -rf /var/lib/apt/lists/* +COPY --from=build /root/target/release/graph_tally_escrow_manager /usr/local/bin/graph_tally_escrow_manager + +ENTRYPOINT [ "/usr/local/bin/graph_tally_escrow_manager" ] diff --git a/grafana/escrow_manager.json b/grafana/escrow_manager.json new file mode 100644 index 0000000..11d9d8b --- /dev/null +++ b/grafana/escrow_manager.json @@ -0,0 +1,1520 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 1, + "links": [], + "panels": [ + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 0 + }, + "id": 100, + "panels": [], + "title": "Overview", + "type": "row" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "decimals": 2, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + } + ] + }, + "unit": "none" + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 6, + "x": 0, + "y": 1 + }, + "id": 1, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "13.0.0-22326976726", + "targets": [ + { + "expr": "escrow_total_debt_grt", + "refId": "A" + } + ], + "title": "Total Debt (GRT)", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "decimals": 2, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "blue", + "value": 0 + } + ] + }, + "unit": "none" + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 6, + "x": 6, + "y": 1 + }, + "id": 2, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "13.0.0-22326976726", + "targets": [ + { + "expr": "escrow_total_balance_grt", + "refId": "A" + } + ], + "title": "Total Balance (GRT)", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "decimals": 2, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "yellow", + "value": 0 + } + ] + }, + "unit": "none" + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 6, + "x": 12, + "y": 1 + }, + "id": 3, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "13.0.0-22326976726", + "targets": [ + { + "expr": "escrow_total_adjustment_grt", + "refId": "A" + } + ], + "title": "Last Adjustment (GRT)", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "decimals": 0, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "purple", + "value": 0 + } + ] + }, + "unit": "none" + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 6, + "x": 18, + "y": 1 + }, + "id": 4, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "13.0.0-22326976726", + "targets": [ + { + "expr": "escrow_receiver_count", + "refId": "A" + } + ], + "title": "Receivers", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "GRT", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "smooth", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "showValues": false, + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + } + ] + }, + "unit": "none" + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Debt" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "red", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Balance" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "green", + "mode": "fixed" + } + } + ] + } + ] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 5 + }, + "id": 5, + "options": { + "legend": { + "calcs": [ + "lastNotNull", + "mean" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "multi", + "sort": "desc" + } + }, + "pluginVersion": "13.0.0-22326976726", + "targets": [ + { + "expr": "escrow_total_debt_grt", + "legendFormat": "Debt", + "refId": "A" + }, + { + "expr": "escrow_total_balance_grt", + "legendFormat": "Balance", + "refId": "B" + } + ], + "title": "Debt vs Balance", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "decimals": 2, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "yellow", + "value": 150 + }, + { + "color": "red", + "value": 200 + } + ] + }, + "unit": "percent" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 6, + "x": 12, + "y": 5 + }, + "id": 6, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "13.0.0-22326976726", + "targets": [ + { + "expr": "(escrow_total_balance_grt / escrow_total_debt_grt) * 100", + "refId": "A" + } + ], + "title": "Balance/Debt Ratio", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "seconds", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "smooth", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "showValues": false, + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + } + ] + }, + "unit": "s" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 6, + "x": 18, + "y": 5 + }, + "id": 7, + "options": { + "legend": { + "calcs": [ + "mean", + "max" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "13.0.0-22326976726", + "targets": [ + { + "expr": "rate(escrow_loop_duration_seconds_sum[5m]) / rate(escrow_loop_duration_seconds_count[5m])", + "legendFormat": "Loop Duration", + "refId": "A" + } + ], + "title": "Loop Duration", + "type": "timeseries" + }, + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 13 + }, + "id": 101, + "panels": [], + "title": "Deposits", + "type": "row" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "decimals": 0, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + } + ] + }, + "unit": "none" + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 4, + "x": 0, + "y": 14 + }, + "id": 8, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "13.0.0-22326976726", + "targets": [ + { + "expr": "escrow_deposit_ok", + "refId": "A" + } + ], + "title": "Deposits OK", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "decimals": 0, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 1 + } + ] + }, + "unit": "none" + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 4, + "x": 4, + "y": 14 + }, + "id": 9, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "13.0.0-22326976726", + "targets": [ + { + "expr": "escrow_deposit_err", + "refId": "A" + } + ], + "title": "Deposits Failed", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "decimals": 2, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "red", + "value": 0 + }, + { + "color": "yellow", + "value": 90 + }, + { + "color": "green", + "value": 99 + } + ] + }, + "unit": "percent" + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 4, + "x": 8, + "y": 14 + }, + "id": 10, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "13.0.0-22326976726", + "targets": [ + { + "expr": "(escrow_deposit_ok / (escrow_deposit_ok + escrow_deposit_err)) * 100", + "refId": "A" + } + ], + "title": "Deposit Success Rate", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "smooth", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "showValues": false, + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + } + ] + }, + "unit": "s" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 14 + }, + "id": 11, + "options": { + "legend": { + "calcs": [ + "mean", + "max" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "13.0.0-22326976726", + "targets": [ + { + "expr": "rate(escrow_deposit_duration_sum[5m]) / rate(escrow_deposit_duration_count[5m])", + "legendFormat": "Avg Duration", + "refId": "A" + }, + { + "expr": "histogram_quantile(0.95, rate(escrow_deposit_duration_bucket[5m]))", + "legendFormat": "p95", + "refId": "B" + } + ], + "title": "Deposit Duration", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "bars", + "fillOpacity": 100, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "showValues": false, + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + } + ] + }, + "unit": "none" + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "OK" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "green", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Errors" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "red", + "mode": "fixed" + } + } + ] + } + ] + }, + "gridPos": { + "h": 4, + "w": 12, + "x": 0, + "y": 18 + }, + "id": 12, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "13.0.0-22326976726", + "targets": [ + { + "expr": "increase(escrow_deposit_ok[1h])", + "legendFormat": "OK", + "refId": "A" + }, + { + "expr": "increase(escrow_deposit_err[1h])", + "legendFormat": "Errors", + "refId": "B" + } + ], + "title": "Deposits Over Time", + "type": "timeseries" + }, + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 22 + }, + "id": 102, + "panels": [], + "title": "Per Receiver", + "type": "row" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "GRT", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "smooth", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "showValues": false, + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + } + ] + }, + "unit": "none" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 23 + }, + "id": 13, + "options": { + "legend": { + "calcs": [ + "lastNotNull" + ], + "displayMode": "table", + "placement": "right", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "multi", + "sort": "desc" + } + }, + "pluginVersion": "13.0.0-22326976726", + "targets": [ + { + "expr": "topk(10, escrow_debt_grt)", + "legendFormat": "{{receiver}}", + "refId": "A" + } + ], + "title": "Debt by Receiver (Top 10)", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "GRT", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "smooth", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "showValues": false, + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + } + ] + }, + "unit": "none" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 23 + }, + "id": 14, + "options": { + "legend": { + "calcs": [ + "lastNotNull" + ], + "displayMode": "table", + "placement": "right", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "multi", + "sort": "desc" + } + }, + "pluginVersion": "13.0.0-22326976726", + "targets": [ + { + "expr": "topk(10, escrow_balance_grt)", + "legendFormat": "{{receiver}}", + "refId": "A" + } + ], + "title": "Balance by Receiver (Top 10)", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "GRT", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "smooth", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "showValues": false, + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + } + ] + }, + "unit": "none" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 31 + }, + "id": 15, + "options": { + "legend": { + "calcs": [ + "lastNotNull" + ], + "displayMode": "table", + "placement": "right", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "multi", + "sort": "desc" + } + }, + "pluginVersion": "13.0.0-22326976726", + "targets": [ + { + "expr": "topk(10, escrow_adjustment_grt)", + "legendFormat": "{{receiver}}", + "refId": "A" + } + ], + "title": "Adjustments by Receiver (Top 10)", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "align": "auto", + "cellOptions": { + "type": "auto" + }, + "footer": { + "reducers": [] + }, + "inspect": false + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + } + ] + } + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Debt" + }, + "properties": [ + { + "id": "custom.cellOptions", + "value": { + "mode": "gradient", + "type": "gauge" + } + }, + { + "id": "color", + "value": { + "mode": "continuous-RdYlGn", + "reverse": true + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Balance" + }, + "properties": [ + { + "id": "custom.cellOptions", + "value": { + "mode": "gradient", + "type": "gauge" + } + }, + { + "id": "color", + "value": { + "mode": "continuous-GrYlRd" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Ratio %" + }, + "properties": [ + { + "id": "unit", + "value": "percent" + }, + { + "id": "decimals", + "value": 1 + }, + { + "id": "thresholds", + "value": { + "mode": "absolute", + "steps": [ + { + "color": "red", + "value": 0 + }, + { + "color": "yellow", + "value": 150 + }, + { + "color": "green", + "value": 167 + } + ] + } + }, + { + "id": "custom.cellOptions", + "value": { + "mode": "basic", + "type": "gauge" + } + } + ] + } + ] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 31 + }, + "id": 16, + "options": { + "cellHeight": "sm", + "showHeader": true, + "sortBy": [ + { + "desc": true, + "displayName": "Ratio %" + } + ] + }, + "pluginVersion": "13.0.0-22326976726", + "targets": [ + { + "expr": "escrow_debt_grt", + "format": "table", + "instant": true, + "refId": "A" + }, + { + "expr": "escrow_balance_grt", + "format": "table", + "instant": true, + "refId": "B" + }, + { + "expr": "(escrow_balance_grt / escrow_debt_grt) * 100", + "format": "table", + "instant": true, + "refId": "C" + } + ], + "title": "All Receivers", + "transformations": [ + { + "id": "seriesToColumns", + "options": { + "byField": "receiver" + } + }, + { + "id": "organize", + "options": { + "excludeByName": { + "Time": true, + "Time 1": true, + "Time 2": true, + "Time 3": true, + "__name__": true, + "__name__ 1": true, + "__name__ 2": true, + "__name__ 3": true, + "cluster 1": true, + "cluster 2": true, + "cluster 3": true, + "container 1": true, + "container 2": true, + "container 3": true, + "endpoint 1": true, + "endpoint 2": true, + "endpoint 3": true, + "instance": true, + "instance 1": true, + "instance 2": true, + "instance 3": true, + "job": true, + "job 1": true, + "job 2": true, + "job 3": true, + "namespace 1": true, + "namespace 2": true, + "namespace 3": true, + "pod 1": true, + "pod 2": true, + "pod 3": true, + "prometheus 1": true, + "prometheus 2": true, + "prometheus 3": true + }, + "includeByName": {}, + "indexByName": {}, + "renameByName": { + "Value #A": "Debt", + "Value #B": "Balance", + "Value #C": "Ratio %", + "receiver": "Receiver" + } + } + } + ], + "type": "table" + } + ], + "preload": false, + "refresh": "", + "schemaVersion": 42, + "tags": [ + "graph-tally", + "escrow" + ], + "templating": { + "list": [ + { + "current": {}, + "includeAll": false, + "label": "Datasource", + "name": "datasource", + "options": [], + "query": "prometheus", + "refresh": 1, + "regex": "", + "type": "datasource" + } + ] + }, + "time": { + "from": "now-30m", + "to": "now" + }, + "timepicker": {}, + "timezone": "browser", + "title": "Graph Tally Escrow Manager", + "uid": "graph-tally-escrow-manager", + "version": 2, + "weekStart": "" +} \ No newline at end of file diff --git a/release-please-config.json b/release-please-config.json index 30bdf62..928cd60 100644 --- a/release-please-config.json +++ b/release-please-config.json @@ -11,6 +11,11 @@ "bump-minor-pre-major": true, "bump-patch-for-minor-pre-major": true, "prerelease": true + }, + "crates/bin/escrow_manager": { + "bump-minor-pre-major": true, + "bump-patch-for-minor-pre-major": true, + "prerelease": true } }, "plugins": [ From f9720cb8981a0d0766fd9efa579e1bc6b8275fd4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Migone?= Date: Thu, 30 Apr 2026 18:27:09 -0300 Subject: [PATCH 3/4] ci: add cmake, required for escrow manager ci MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Tomás Migone --- .github/workflows/tests.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index cc618a8..a57b92c 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -29,7 +29,7 @@ jobs: steps: - uses: actions/checkout@v3 - name: Install protobuf compiler - run: apt-get update && apt-get install libsasl2-dev protobuf-compiler -y + run: apt-get update && apt-get install cmake libsasl2-dev protobuf-compiler -y - uses: actions/cache@v3 with: path: | @@ -55,7 +55,7 @@ jobs: steps: - uses: actions/checkout@v3 - name: Install protobuf compiler - run: apt-get update && apt-get install libsasl2-dev protobuf-compiler -y + run: apt-get update && apt-get install cmake libsasl2-dev protobuf-compiler -y - uses: actions/cache@v3 with: path: | @@ -82,7 +82,7 @@ jobs: steps: - uses: actions/checkout@v3 - name: Install protobuf compiler - run: apt-get update && apt-get install libsasl2-dev protobuf-compiler -y + run: apt-get update && apt-get install cmake libsasl2-dev protobuf-compiler -y - uses: actions/cache@v3 with: path: | From bdcdc8ca19f3cf47ec5fa4872fc538f658d21a07 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Migone?= Date: Thu, 30 Apr 2026 18:41:51 -0300 Subject: [PATCH 4/4] fix: dynamically link kafka lib in escrow manager to match aggregator MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Tomás Migone --- .github/workflows/tests.yml | 6 +++--- crates/bin/escrow_manager/Cargo.toml | 2 +- docker/Dockerfile.graph_tally_aggregator | 4 ++-- docker/Dockerfile.graph_tally_escrow_manager | 5 ++--- 4 files changed, 8 insertions(+), 9 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index a57b92c..4cf778a 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -29,7 +29,7 @@ jobs: steps: - uses: actions/checkout@v3 - name: Install protobuf compiler - run: apt-get update && apt-get install cmake libsasl2-dev protobuf-compiler -y + run: apt-get update && apt-get install librdkafka-dev libsasl2-dev pkg-config protobuf-compiler -y - uses: actions/cache@v3 with: path: | @@ -55,7 +55,7 @@ jobs: steps: - uses: actions/checkout@v3 - name: Install protobuf compiler - run: apt-get update && apt-get install cmake libsasl2-dev protobuf-compiler -y + run: apt-get update && apt-get install librdkafka-dev libsasl2-dev pkg-config protobuf-compiler -y - uses: actions/cache@v3 with: path: | @@ -82,7 +82,7 @@ jobs: steps: - uses: actions/checkout@v3 - name: Install protobuf compiler - run: apt-get update && apt-get install cmake libsasl2-dev protobuf-compiler -y + run: apt-get update && apt-get install librdkafka-dev libsasl2-dev pkg-config protobuf-compiler -y - uses: actions/cache@v3 with: path: | diff --git a/crates/bin/escrow_manager/Cargo.toml b/crates/bin/escrow_manager/Cargo.toml index ce98d0d..1766ea0 100644 --- a/crates/bin/escrow_manager/Cargo.toml +++ b/crates/bin/escrow_manager/Cargo.toml @@ -21,7 +21,7 @@ futures-util.workspace = true lazy_static.workspace = true prometheus.workspace = true prost.workspace = true -rdkafka = { workspace = true, features = ["cmake-build", "gssapi", "tracing"] } +rdkafka = { workspace = true, features = ["gssapi", "tracing"] } reqwest = "0.12.5" serde.workspace = true serde_json.workspace = true diff --git a/docker/Dockerfile.graph_tally_aggregator b/docker/Dockerfile.graph_tally_aggregator index e1f74fc..29f812f 100644 --- a/docker/Dockerfile.graph_tally_aggregator +++ b/docker/Dockerfile.graph_tally_aggregator @@ -3,7 +3,7 @@ FROM rust:1.91-bookworm AS build WORKDIR /root RUN apt-get update && apt-get install -y --no-install-recommends \ - libsasl2-dev protobuf-compiler \ + librdkafka-dev libsasl2-dev pkg-config protobuf-compiler \ && rm -rf /var/lib/apt/lists/* COPY . . @@ -15,7 +15,7 @@ RUN cargo build --release --bin graph_tally_aggregator FROM debian:bookworm-slim RUN apt-get update && apt-get install -y --no-install-recommends \ - ca-certificates libsasl2-dev openssl \ + ca-certificates librdkafka1 libsasl2-2 openssl \ && rm -rf /var/lib/apt/lists/* COPY --from=build /root/target/release/graph_tally_aggregator /usr/local/bin/graph_tally_aggregator diff --git a/docker/Dockerfile.graph_tally_escrow_manager b/docker/Dockerfile.graph_tally_escrow_manager index 49e3239..8250329 100644 --- a/docker/Dockerfile.graph_tally_escrow_manager +++ b/docker/Dockerfile.graph_tally_escrow_manager @@ -4,12 +4,11 @@ WORKDIR /root RUN apt-get update && apt-get install -y --no-install-recommends \ clang \ - cmake \ - libcurl4-openssl-dev \ librdkafka-dev \ libsasl2-dev \ libssl-dev \ pkg-config \ + protobuf-compiler \ && rm -rf /var/lib/apt/lists/* COPY . . @@ -21,7 +20,7 @@ RUN cargo build --release --bin graph_tally_escrow_manager FROM debian:bookworm-slim RUN apt-get update && apt-get install -y --no-install-recommends \ - ca-certificates libsasl2-dev libssl-dev \ + ca-certificates librdkafka1 libsasl2-2 openssl \ && rm -rf /var/lib/apt/lists/* COPY --from=build /root/target/release/graph_tally_escrow_manager /usr/local/bin/graph_tally_escrow_manager