diff --git a/.gitignore b/.gitignore
index 333840cbf..260141fbb 100644
--- a/.gitignore
+++ b/.gitignore
@@ -11,6 +11,7 @@ indexer.toml
.vscode/
# migrations/
.helix
+.claude/
# Node.js related files
crates/dips/node_modules/
diff --git a/Cargo.lock b/Cargo.lock
index 9833c195c..42e402119 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -2651,7 +2651,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8d162beedaa69905488a8da94f5ac3edb4dd4788b732fadb7bd120b2625c1976"
dependencies = [
"data-encoding",
- "syn 1.0.109",
+ "syn 2.0.108",
]
[[package]]
@@ -2989,7 +2989,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
dependencies = [
"libc",
- "windows-sys 0.59.0",
+ "windows-sys 0.61.2",
]
[[package]]
@@ -4021,7 +4021,7 @@ dependencies = [
"libc",
"percent-encoding",
"pin-project-lite",
- "socket2 0.5.10",
+ "socket2 0.6.1",
"system-configuration 0.6.1",
"tokio",
"tower-service",
@@ -4247,19 +4247,17 @@ version = "0.1.0"
dependencies = [
"anyhow",
"async-trait",
+ "bs58",
"build-info",
"bytes",
"derivative",
"futures",
"graph-networks-registry",
- "http 0.2.12",
"indexer-monitor",
- "indexer-watcher",
"ipfs-api-backend-hyper",
"prost 0.14.1",
- "rand 0.9.2",
+ "rand 0.8.5",
"serde",
- "serde_json",
"serde_yaml",
"sqlx",
"test-assets",
@@ -4601,7 +4599,7 @@ checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46"
dependencies = [
"hermit-abi",
"libc",
- "windows-sys 0.59.0",
+ "windows-sys 0.61.2",
]
[[package]]
@@ -5134,13 +5132,13 @@ dependencies = [
[[package]]
name = "match-lookup"
-version = "0.1.1"
+version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1265724d8cb29dbbc2b0f06fffb8bf1a8c0cf73a78eede9ba73a4a66c52a981e"
+checksum = "757aee279b8bdbb9f9e676796fd459e4207a1f986e87886700abf589f5abf771"
dependencies = [
"proc-macro2",
"quote",
- "syn 1.0.109",
+ "syn 2.0.108",
]
[[package]]
@@ -5438,7 +5436,7 @@ version = "0.50.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5"
dependencies = [
- "windows-sys 0.59.0",
+ "windows-sys 0.61.2",
]
[[package]]
@@ -5573,7 +5571,7 @@ version = "0.7.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ff32365de1b6743cb203b710788263c44a03de03802daf96092f2da4fe6ba4d7"
dependencies = [
- "proc-macro-crate 1.1.3",
+ "proc-macro-crate 3.4.0",
"proc-macro2",
"quote",
"syn 2.0.108",
@@ -5739,7 +5737,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7d8fae84b431384b68627d0f9b3b1245fcf9f46f6c0e3dc902e9dce64edd1967"
dependencies = [
"libc",
- "windows-sys 0.45.0",
+ "windows-sys 0.61.2",
]
[[package]]
@@ -6317,7 +6315,7 @@ version = "0.14.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac6c3320f9abac597dcbc668774ef006702672474aad53c6d596b62e487b40b1"
dependencies = [
- "heck 0.4.1",
+ "heck 0.5.0",
"itertools 0.14.0",
"log",
"multimap",
@@ -6543,7 +6541,7 @@ dependencies = [
"quinn-udp",
"rustc-hash",
"rustls 0.23.34",
- "socket2 0.5.10",
+ "socket2 0.6.1",
"thiserror 2.0.18",
"tokio",
"tracing",
@@ -6580,9 +6578,9 @@ dependencies = [
"cfg_aliases 0.2.1",
"libc",
"once_cell",
- "socket2 0.5.10",
+ "socket2 0.6.1",
"tracing",
- "windows-sys 0.59.0",
+ "windows-sys 0.60.2",
]
[[package]]
@@ -7224,7 +7222,7 @@ dependencies = [
"errno",
"libc",
"linux-raw-sys",
- "windows-sys 0.59.0",
+ "windows-sys 0.61.2",
]
[[package]]
@@ -8624,7 +8622,7 @@ dependencies = [
"getrandom 0.3.4",
"once_cell",
"rustix",
- "windows-sys 0.59.0",
+ "windows-sys 0.61.2",
]
[[package]]
@@ -9900,7 +9898,7 @@ version = "0.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
dependencies = [
- "windows-sys 0.48.0",
+ "windows-sys 0.61.2",
]
[[package]]
diff --git a/crates/config/maximal-config-example.toml b/crates/config/maximal-config-example.toml
index c3d850932..e0f417adb 100644
--- a/crates/config/maximal-config-example.toml
+++ b/crates/config/maximal-config-example.toml
@@ -187,19 +187,128 @@ max_receipts_per_request = 10000
# DIPS (Decentralized Indexing Payment System)
# NOTE: DIPS requires Horizon mode ([horizon].enabled = true)
+# Payer authorization is handled via escrow accounts (same trust model as TAP)
+#
+# Pricing uses human-readable GRT values (not wei), e.g. "100" = 100 GRT per 30 days.
[dips]
host = "0.0.0.0"
port = "7601"
-allowed_payers = ["0x3333333333333333333333333333333333333333"]
+recurring_collector = "0x4444444444444444444444444444444444444444"
+
+# Networks you explicitly support indexing.
+# Proposals from the dipper for you to index networks that are not in the list below are rejected.
+# See https://github.com/graphprotocol/networks-registry/blob/main/docs/networks-table.md
+# e.g. supported_networks = ["mainnet", "arbitrum-one"]
+supported_networks = []
-price_per_entity = "1000"
+# Minimum payment you are willing to accept in order to accept indexing agreements
+# (base price + entity-based price). Total payment = base price + (entities on sg * entity_rate)
+#
+# For reference: analysis of subgraphs indexed by the upgrade indexer in Q1 2025 found
+# the average entity size to be ~0.759 KiB. At this size, 1 billion entities ≈ 0.707 TiB.
+# Your own observations may differ - adjust pricing accordingly.
+min_grt_per_billion_entities_per_30_days = "200" # entity-based component (global)
-[dips.price_per_epoch]
-mainnet = "100"
-hardhat = "100"
+[dips.min_grt_per_30_days] # base rate component (per-network)
+# arbitrum-one = "450"
+# matic = "300"
+# fantom = "300"
+# avalanche = "225"
+# bsc = "200"
+# base = "80"
+# gnosis = "45"
+# near-mainnet = "45"
+# fuji = "45"
+# mainnet = "45"
+# optimism = "30"
+# xdai = "30"
+# polygon-zkevm = "30"
+# polygon-amoy = "30"
+# xlayer-mainnet = "30"
+# soneium = "30"
+# abstract = "30"
+# fantom-testnet = "30"
+# lens = "30"
+# rootstock-testnet = "30"
+# kaia = "30"
+# chiliz = "30"
+# linea-sepolia = "30"
+# joc-testnet = "30"
+# etherlink-mainnet = "30"
+# apechain = "30"
+# ink = "30"
+# unichain-testnet = "30"
+# blast-testnet = "30"
+# megaeth = "30"
+# sei-atlantic = "30"
+# zksync-era-sepolia = "30"
+# arbitrum-nova = "30"
+# hoodi = "30"
+# celo-sepolia = "30"
+# vana = "30"
+# joc = "30"
+# swellchain = "30"
+# soneium-testnet = "30"
+# zetachain = "30"
+# hemi-sepolia = "30"
+# megaeth-testnet = "30"
+# iotex = "30"
+# stable = "30"
+# cronos = "30"
+# ronin = "30"
+# fraxtal = "30"
+# kaia-testnet = "30"
+# abstract-testnet = "30"
+# neox-testnet = "30"
+# fuse-testnet = "30"
+# manta = "30"
+# viction = "30"
+# peaq = "30"
+# boba-testnet = "30"
+# hashkeychain = "30"
+# vana-moksha = "30"
+# botanix-testnet = "30"
+# corn = "30"
+# chiliz-testnet = "30"
+# apechain-curtis = "30"
+# megaeth-timothy = "30"
+# status-sepolia = "30"
+# etherlink-shadownet = "30"
+# etherlink-testnet = "30"
+# mint = "30"
+# ink-sepolia = "30"
+# iotex-testnet = "30"
+# neox = "30"
+# lumia = "30"
+# mint-sepolia = "30"
+# lens-testnet = "30"
+# berachain = "30"
+# sonic = "25"
+# katana = "25"
+# hemi = "20"
+# zksync-era = "20"
+# sei-mainnet = "20"
+# scroll = "15"
+# optimism-sepolia = "15"
+# celo = "15"
+# linea = "15"
+# base-sepolia = "15"
+# unichain = "15"
+# monad-testnet = "10"
+# monad = "10"
+# fuse = "10"
+# scroll-sepolia = "10"
+# rootstock = "10"
+# near-testnet = "10"
+# moonriver = "10"
+# chapel = "10"
+# moonbeam = "10"
+# blast-mainnet = "5"
+# arbitrum-sepolia = "5"
+# boba = "5"
+# sepolia = "5"
[dips.additional_networks]
-"eip155:1337" = "hardhat"
[horizon]
# Enable Horizon support and detection
diff --git a/crates/config/src/config.rs b/crates/config/src/config.rs
index 8b5b57a17..f313d3db0 100644
--- a/crates/config/src/config.rs
+++ b/crates/config/src/config.rs
@@ -19,13 +19,10 @@ use regex::Regex;
use serde::Deserialize;
use serde_repr::Deserialize_repr;
use serde_with::{serde_as, DurationSecondsWithFrac};
-use thegraph_core::{
- alloy::primitives::{Address, U256},
- DeploymentId,
-};
+use thegraph_core::{alloy::primitives::Address, DeploymentId};
use url::Url;
-use crate::NonZeroGRT;
+use crate::{NonZeroGRT, GRT};
const SHARED_PREFIX: &str = "INDEXER_";
@@ -631,16 +628,25 @@ fn default_allocation_reconciliation_interval_secs() -> Duration {
Duration::from_secs(300)
}
+/// DIPs configuration.
+///
+/// Validates RCA proposals (signature, IPFS manifest, network, pricing)
+/// before storing. The indexer agent queries pending proposals from the
+/// database and decides on-chain acceptance.
#[derive(Debug, Deserialize)]
+#[serde(default)]
#[cfg_attr(test, derive(PartialEq))]
pub struct DipsConfig {
pub host: String,
pub port: String,
- pub allowed_payers: Vec
,
-
- pub price_per_entity: U256,
- pub price_per_epoch: BTreeMap,
- pub additional_networks: HashMap,
+ pub recurring_collector: Address,
+ /// Networks this indexer explicitly supports. Proposals for other networks are rejected.
+ pub supported_networks: HashSet,
+ /// Minimum acceptable GRT per 30 days, per network. Converted to wei/second internally.
+ pub min_grt_per_30_days: BTreeMap,
+ /// Minimum acceptable GRT per billion entities per 30 days.
+ pub min_grt_per_billion_entities_per_30_days: GRT,
+ pub additional_networks: BTreeMap,
}
impl Default for DipsConfig {
@@ -648,10 +654,11 @@ impl Default for DipsConfig {
DipsConfig {
host: "0.0.0.0".to_string(),
port: "7601".to_string(),
- allowed_payers: vec![],
- price_per_entity: U256::from(100),
- price_per_epoch: BTreeMap::new(),
- additional_networks: HashMap::new(),
+ recurring_collector: Address::ZERO,
+ supported_networks: HashSet::new(),
+ min_grt_per_30_days: BTreeMap::new(),
+ min_grt_per_billion_entities_per_30_days: GRT::ZERO,
+ additional_networks: BTreeMap::new(),
}
}
}
@@ -738,17 +745,12 @@ pub struct HorizonConfig {
#[cfg(test)]
mod tests {
- use std::{
- collections::{BTreeMap, HashMap, HashSet},
- env, fs,
- path::PathBuf,
- str::FromStr,
- };
+ use std::{collections::HashSet, env, fs, path::PathBuf, str::FromStr};
use bip39::Mnemonic;
use figment::value::Uncased;
use sealed_test::prelude::*;
- use thegraph_core::alloy::primitives::{address, Address, FixedBytes, U256};
+ use thegraph_core::alloy::primitives::{address, Address, FixedBytes};
use tracing_test::traced_test;
use super::{DatabaseConfig, IndexerConfig, SHARED_PREFIX};
@@ -774,18 +776,10 @@ mod tests {
max_config.tap.trusted_senders =
HashSet::from([address!("deadbeefcafebabedeadbeefcafebabedeadbeef")]);
max_config.dips = Some(crate::DipsConfig {
- allowed_payers: vec![Address(
- FixedBytes::<20>::from_str("0x3333333333333333333333333333333333333333").unwrap(),
- )],
- price_per_entity: U256::from(1000),
- price_per_epoch: BTreeMap::from_iter(vec![
- ("mainnet".to_string(), U256::from(100)),
- ("hardhat".to_string(), U256::from(100)),
- ]),
- additional_networks: HashMap::from([(
- "eip155:1337".to_string(),
- "hardhat".to_string(),
- )]),
+ recurring_collector: Address(
+ FixedBytes::<20>::from_str("0x4444444444444444444444444444444444444444").unwrap(),
+ ),
+ min_grt_per_billion_entities_per_30_days: crate::GRT::from_grt("200"),
..Default::default()
});
@@ -1286,4 +1280,107 @@ mod tests {
.unwrap_err()
.contains("No operator mnemonic configured"));
}
+
+ // === DIPS Startup Validation Tests ===
+
+ /// Test that minimal config has no DIPS section (safe default for existing indexers).
+ #[test]
+ fn test_dips_absent_in_minimal_config() {
+ // Arrange & Act
+ let config = Config::parse(
+ ConfigPrefix::Service,
+ Some(PathBuf::from("minimal-config-example.toml")).as_ref(),
+ )
+ .unwrap();
+
+ // Assert
+ assert!(
+ config.dips.is_none(),
+ "Minimal config should not have DIPS enabled"
+ );
+ }
+
+ /// Test that DipsConfig defaults have recurring_collector as Address::ZERO.
+ /// This is important because the service startup validation checks for this
+ /// and fails with a clear error message if DIPS is enabled but recurring_collector
+ /// is not configured.
+ #[test]
+ fn test_dips_config_defaults_recurring_collector_zero() {
+ // Arrange & Act
+ let dips_config = crate::DipsConfig::default();
+
+ // Assert
+ assert_eq!(
+ dips_config.recurring_collector,
+ Address::ZERO,
+ "Default recurring_collector should be Address::ZERO to trigger startup validation"
+ );
+ }
+
+ /// Test that DipsConfig defaults have empty supported_networks.
+ /// This triggers a warning at startup that all proposals will be rejected.
+ #[test]
+ fn test_dips_config_defaults_empty_supported_networks() {
+ // Arrange & Act
+ let dips_config = crate::DipsConfig::default();
+
+ // Assert
+ assert!(
+ dips_config.supported_networks.is_empty(),
+ "Default supported_networks should be empty"
+ );
+ assert!(
+ dips_config.min_grt_per_30_days.is_empty(),
+ "Default min_grt_per_30_days should be empty"
+ );
+ }
+
+ /// Test that a DIPS config with only recurring_collector set uses defaults for other fields.
+ #[test]
+ fn test_dips_partial_config_uses_defaults() {
+ // Arrange - create a DipsConfig with just recurring_collector set
+ let dips_config = crate::DipsConfig {
+ recurring_collector: Address(
+ FixedBytes::<20>::from_str("0x1234567890123456789012345678901234567890").unwrap(),
+ ),
+ ..Default::default()
+ };
+
+ // Assert - recurring_collector is set, others use defaults
+ assert_ne!(
+ dips_config.recurring_collector,
+ Address::ZERO,
+ "recurring_collector should be set"
+ );
+ assert_eq!(dips_config.host, "0.0.0.0", "host should use default");
+ assert_eq!(dips_config.port, "7601", "port should use default");
+ assert!(
+ dips_config.supported_networks.is_empty(),
+ "supported_networks should default to empty"
+ );
+ assert!(
+ dips_config.min_grt_per_30_days.is_empty(),
+ "min_grt_per_30_days should default to empty"
+ );
+ }
+
+ /// Test that maximal config with DIPS section parses correctly.
+ #[test]
+ fn test_dips_maximal_config_parses() {
+ // Arrange & Act
+ let config: Config = toml::from_str(
+ fs::read_to_string("maximal-config-example.toml")
+ .unwrap()
+ .as_str(),
+ )
+ .unwrap();
+
+ // Assert
+ let dips = config.dips.expect("maximal config should have DIPS");
+ assert_ne!(
+ dips.recurring_collector,
+ Address::ZERO,
+ "recurring_collector should be set in maximal config"
+ );
+ }
}
diff --git a/crates/config/src/grt.rs b/crates/config/src/grt.rs
index 03f667712..b9facb1d5 100644
--- a/crates/config/src/grt.rs
+++ b/crates/config/src/grt.rs
@@ -4,6 +4,53 @@
use bigdecimal::{BigDecimal, ToPrimitive};
use serde::{de::Error, Deserialize};
+/// GRT value stored as wei (10^-18 GRT). Allows zero.
+///
+/// Deserializes from human-readable GRT strings like "1.5" or "0.001".
+#[derive(Debug, PartialEq, Default, Clone, Copy)]
+pub struct GRT(u128);
+
+impl GRT {
+ pub const ZERO: GRT = GRT(0);
+
+ /// Convert GRT string to wei for test construction.
+ /// Panics on invalid input - only use in tests.
+ #[cfg(test)]
+ pub fn from_grt(grt: &str) -> Self {
+ use bigdecimal::{BigDecimal, ToPrimitive};
+ use std::str::FromStr;
+ let v = BigDecimal::from_str(grt).expect("invalid GRT value");
+ let wei = (v * BigDecimal::from(10u64.pow(18)))
+ .to_u128()
+ .expect("GRT value too large");
+ GRT(wei)
+ }
+
+ pub fn wei(&self) -> u128 {
+ self.0
+ }
+}
+
+impl<'de> Deserialize<'de> for GRT {
+ fn deserialize(deserializer: D) -> Result
+ where
+ D: serde::Deserializer<'de>,
+ {
+ let v = BigDecimal::deserialize(deserializer)?;
+ if v < 0.into() {
+ return Err(Error::custom("GRT value cannot be negative"));
+ }
+ // Convert to wei
+ let v = v * BigDecimal::from(10u64.pow(18));
+ // Convert to u128
+ let wei = v.to_u128().ok_or_else(|| {
+ Error::custom("GRT value cannot be represented as a u128 GRT wei value")
+ })?;
+
+ Ok(Self(wei))
+ }
+}
+
#[derive(Debug, PartialEq, Default, Clone)]
pub struct NonZeroGRT(u128);
@@ -47,6 +94,35 @@ mod tests {
use super::*;
+ #[test]
+ fn test_grt_deserialize() {
+ // Arrange & Act & Assert
+ assert_de_tokens(&GRT(1_000_000_000_000_000_000), &[Token::Str("1")]);
+ assert_de_tokens(&GRT(1_100_000_000_000_000_000), &[Token::Str("1.1")]);
+ assert_de_tokens(&GRT(0), &[Token::Str("0")]);
+ }
+
+ #[test]
+ fn test_grt_negative_rejected() {
+ // Arrange & Act & Assert
+ assert_de_tokens_error::(&[Token::Str("-1")], "GRT value cannot be negative");
+ }
+
+ #[test]
+ fn test_grt_wei() {
+ // Arrange
+ let grt = GRT(1_500_000_000_000_000_000);
+
+ // Act & Assert
+ assert_eq!(grt.wei(), 1_500_000_000_000_000_000);
+ }
+
+ #[test]
+ fn test_grt_zero_constant() {
+ // Arrange & Act & Assert
+ assert_eq!(GRT::ZERO.wei(), 0);
+ }
+
#[test]
fn test_parse_grt_value_to_u128_deserialize() {
assert_de_tokens(&NonZeroGRT(1_000_000_000_000_000_000), &[Token::Str("1")]);
diff --git a/crates/dips/Cargo.toml b/crates/dips/Cargo.toml
index 1cb7bf2c5..c3d4edaf7 100644
--- a/crates/dips/Cargo.toml
+++ b/crates/dips/Cargo.toml
@@ -12,43 +12,52 @@ rpc = [
"dep:tonic-prost",
"dep:tonic-prost-build",
"dep:bytes",
+ "dep:graph-networks-registry",
+ "dep:serde",
+ "dep:serde_yaml",
+]
+db = [
+ "dep:sqlx",
+ "dep:build-info",
+ "dep:indexer-monitor",
+ "dep:graph-networks-registry",
+ "dep:serde",
+ "dep:serde_yaml",
]
-db = ["dep:sqlx"]
[dependencies]
-build-info.workspace = true
-thiserror.workspace = true
anyhow.workspace = true
thegraph-core.workspace = true
async-trait.workspace = true
uuid.workspace = true
tokio.workspace = true
-indexer-monitor = { path = "../monitor" }
tracing.workspace = true
-graph-networks-registry.workspace = true
+bs58 = "0.5"
+build-info = { workspace = true, optional = true }
+indexer-monitor = { path = "../monitor", optional = true }
+thiserror.workspace = true
+graph-networks-registry = { workspace = true, optional = true }
+serde = { workspace = true, optional = true }
+serde_yaml = { version = "0.9", optional = true }
-bytes = { version = "1.10.0", optional = true }
+# IPFS client dependencies
derivative = "2.2.0"
-
futures.workspace = true
-http = "0.2"
+ipfs-api-backend-hyper = { version = "0.6.0", features = ["with-send-sync", "with-hyper-tls"] }
+
+bytes = { version = "1.10.0", optional = true }
prost = { workspace = true, optional = true }
-ipfs-api-backend-hyper = { version = "0.6.0", features = [
- "with-send-sync",
- "with-hyper-tls",
-] }
-serde_yaml.workspace = true
-serde.workspace = true
sqlx = { workspace = true, optional = true }
tonic = { workspace = true, optional = true }
tonic-prost = { workspace = true, optional = true }
-serde_json.workspace = true
[dev-dependencies]
-rand.workspace = true
-indexer-watcher = { path = "../watcher" }
testcontainers-modules = { workspace = true, features = ["postgres"] }
test-assets = { path = "../test-assets" }
+indexer-monitor = { path = "../monitor" }
+graph-networks-registry.workspace = true
+build-info.workspace = true
+rand = "0.8"
[build-dependencies]
tonic-build = { workspace = true, optional = true }
diff --git a/crates/dips/proto/indexer.proto b/crates/dips/proto/indexer.proto
index dc97e82e8..0521f5a26 100644
--- a/crates/dips/proto/indexer.proto
+++ b/crates/dips/proto/indexer.proto
@@ -33,6 +33,7 @@ message SubmitAgreementProposalRequest {
*/
message SubmitAgreementProposalResponse {
ProposalResponse response = 1; /// The response to the agreement proposal.
+ RejectReason reject_reason = 2; /// Only set when response = REJECT.
}
/**
@@ -43,6 +44,23 @@ enum ProposalResponse {
REJECT = 1; /// The agreement proposal was rejected.
}
+/**
+ * The reason for rejecting an _indexing agreement_ proposal.
+ * Only meaningful when ProposalResponse = REJECT.
+ */
+enum RejectReason {
+ REJECT_REASON_UNSPECIFIED = 0; /// Default / not set (used for ACCEPT responses).
+ REJECT_REASON_PRICE_TOO_LOW = 1; /// The offered price is below the indexer's minimum.
+ REJECT_REASON_OTHER = 2; /// Any other reason (bad signature, etc.).
+ REJECT_REASON_SIGNER_NOT_AUTHORISED = 3; /// The proposal signer is not authorised on the escrow contract.
+ REJECT_REASON_DEADLINE_EXPIRED = 4; /// The proposal deadline has already passed.
+ REJECT_REASON_UNSUPPORTED_NETWORK = 5; /// The subgraph's network is not supported by this indexer.
+ REJECT_REASON_SUBGRAPH_MANIFEST_UNAVAILABLE = 6; /// The subgraph manifest could not be fetched from IPFS.
+ REJECT_REASON_UNEXPECTED_SERVICE_PROVIDER = 7; /// The RCA service provider does not match this indexer.
+ REJECT_REASON_AGREEMENT_EXPIRED = 8; /// The agreement end time has already passed.
+ REJECT_REASON_UNSUPPORTED_METADATA_VERSION = 9; /// The metadata version is not supported.
+}
+
/**
* A request to cancel an _indexing agreement_.
*
diff --git a/crates/dips/src/database.rs b/crates/dips/src/database.rs
index fdaf1f66e..dcff22331 100644
--- a/crates/dips/src/database.rs
+++ b/crates/dips/src/database.rs
@@ -1,366 +1,77 @@
// Copyright 2023-, Edge & Node, GraphOps, and Semiotic Labs.
// SPDX-License-Identifier: Apache-2.0
-use std::str::FromStr;
+//! PostgreSQL implementation of [`RcaStore`](crate::store::RcaStore).
+//!
+//! This module provides [`PsqlRcaStore`], which persists validated RCA proposals
+//! to the `pending_rca_proposals` table. The indexer-agent queries this table
+//! directly to find pending proposals and decide on-chain acceptance.
+//!
+//! # Shared Database
+//!
+//! indexer-rs (Rust) and indexer-agent (TypeScript) share the same PostgreSQL
+//! database. This module only writes; the agent reads and updates status:
+//!
+//! ```text
+//! indexer-rs ──INSERT──> pending_rca_proposals <──SELECT/UPDATE── indexer-agent
+//! ```
+//!
+//! # Status Lifecycle
+//!
+//! 1. indexer-rs inserts with status = "pending"
+//! 2. indexer-agent queries pending proposals
+//! 3. Agent validates allocation availability, accepts on-chain
+//! 4. Agent updates status to "accepted" or "rejected"
+//!
+//! # Idempotency
+//!
+//! The `store_rca` operation is idempotent: inserting the same agreement ID twice
+//! succeeds both times. This handles retry scenarios where Dipper re-sends an RCA
+//! after a timeout (network partition, crash after INSERT but before response, etc.).
+//!
+//! Without idempotency, the retry would fail with a duplicate key error, causing
+//! Dipper to mark the agreement as failed even though it was successfully stored.
+
+use std::any::Any;
use async_trait::async_trait;
-use build_info::chrono::{DateTime, Utc};
-use sqlx::{types::BigDecimal, PgPool, Row};
-use thegraph_core::alloy::{core::primitives::U256 as uint256, hex::ToHexExt, sol_types::SolType};
+use sqlx::PgPool;
use uuid::Uuid;
-use crate::{
- store::{AgreementStore, StoredIndexingAgreement},
- DipsError, SignedCancellationRequest, SignedIndexingAgreementVoucher,
- SubgraphIndexingVoucherMetadata,
-};
+use crate::{store::RcaStore, DipsError};
+/// PostgreSQL implementation of RcaStore for RecurringCollectionAgreement.
#[derive(Debug)]
-pub struct PsqlAgreementStore {
+pub struct PsqlRcaStore {
pub pool: PgPool,
}
-fn uint256_to_bigdecimal(value: &uint256, field: &str) -> Result {
- BigDecimal::from_str(&value.to_string())
- .map_err(|e| DipsError::InvalidVoucher(format!("{field}: {e}")))
-}
-
#[async_trait]
-impl AgreementStore for PsqlAgreementStore {
- async fn get_by_id(&self, id: Uuid) -> Result