Skip to content

Commit ccb7858

Browse files
committed
fix(monitor): implement v2 escrow accounts
1 parent c425176 commit ccb7858

File tree

3 files changed

+171
-5
lines changed

3 files changed

+171
-5
lines changed

crates/monitor/src/escrow_accounts.rs

Lines changed: 121 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@ use std::{
99

1010
use anyhow::anyhow;
1111
use indexer_query::escrow_account::{self, EscrowAccountQuery};
12+
use indexer_query::escrow_account_v2::{
13+
self as escrow_account_v2, EscrowAccountQuery as EscrowAccountQueryV2,
14+
};
1215
use thegraph_core::alloy::primitives::{Address, U256};
1316
use thiserror::Error;
1417
use tokio::sync::watch::Receiver;
@@ -112,13 +115,76 @@ pub async fn escrow_accounts_v2(
112115
.await
113116
}
114117

115-
// TODO implement escrow accounts v2 query
116118
async fn get_escrow_accounts_v2(
117-
_escrow_subgraph: &'static SubgraphClient,
118-
_indexer_address: Address,
119-
_reject_thawing_signers: bool,
119+
escrow_subgraph: &'static SubgraphClient,
120+
indexer_address: Address,
121+
reject_thawing_signers: bool,
120122
) -> anyhow::Result<EscrowAccounts> {
121-
Ok(EscrowAccounts::new(HashMap::new(), HashMap::new()))
123+
// V2 TAP receipts use different field names (payer/service_provider) but the underlying
124+
// escrow account model is identical to V1. Both V1 and V2 receipts reference the same
125+
// sender addresses and the same escrow relationships.
126+
//
127+
// The separation of V1/V2 escrow account watchers allows for potential future differences
128+
// in escrow models, but currently both query the same subgraph data with identical logic.
129+
//
130+
// V2 receipt flow:
131+
// 1. V2 receipt contains payer address (equivalent to V1 sender)
132+
// 2. Receipt is signed by a signer authorized by the payer
133+
// 3. Escrow accounts map: signer -> payer (sender) -> balance
134+
// 4. Service provider (indexer) receives payments from payer's escrow
135+
136+
let response = escrow_subgraph
137+
.query::<EscrowAccountQueryV2, _>(escrow_account_v2::Variables {
138+
indexer: format!("{:x?}", indexer_address),
139+
thaw_end_timestamp: if reject_thawing_signers {
140+
U256::ZERO.to_string()
141+
} else {
142+
U256::MAX.to_string()
143+
},
144+
})
145+
.await?;
146+
147+
let response = response?;
148+
149+
tracing::trace!("V2 Escrow accounts response: {:?}", response);
150+
151+
let senders_balances: HashMap<Address, U256> = response
152+
.escrow_accounts
153+
.iter()
154+
.map(|account| {
155+
let balance = U256::checked_sub(
156+
U256::from_str(&account.balance)?,
157+
U256::from_str(&account.total_amount_thawing)?,
158+
)
159+
.unwrap_or_else(|| {
160+
tracing::warn!(
161+
"Balance minus total amount thawing underflowed for V2 account {}. \
162+
Setting balance to 0, no V2 queries will be served for this payer.",
163+
account.payer.id
164+
);
165+
U256::from(0)
166+
});
167+
168+
Ok((Address::from_str(&account.payer.id)?, balance))
169+
})
170+
.collect::<Result<HashMap<_, _>, anyhow::Error>>()?;
171+
172+
let senders_to_signers = response
173+
.escrow_accounts
174+
.into_iter()
175+
.map(|account| {
176+
let payer = Address::from_str(&account.payer.id)?;
177+
let signers = account
178+
.payer
179+
.signers
180+
.iter()
181+
.map(|signer| Address::from_str(&signer.id))
182+
.collect::<Result<Vec<_>, _>>()?;
183+
Ok((payer, signers))
184+
})
185+
.collect::<Result<HashMap<_, _>, anyhow::Error>>()?;
186+
187+
Ok(EscrowAccounts::new(senders_balances, senders_to_signers))
122188
}
123189

124190
async fn get_escrow_accounts_v1(
@@ -262,4 +328,54 @@ mod tests {
262328
)
263329
);
264330
}
331+
332+
#[test(tokio::test)]
333+
async fn test_current_accounts_v2() {
334+
// Set up a mock escrow subgraph for V2 with payer fields
335+
let mock_server = MockServer::start().await;
336+
let escrow_subgraph = Box::leak(Box::new(
337+
SubgraphClient::new(
338+
reqwest::Client::new(),
339+
None,
340+
DeploymentDetails::for_query_url(&format!(
341+
"{}/subgraphs/id/{}",
342+
&mock_server.uri(),
343+
test_assets::ESCROW_SUBGRAPH_DEPLOYMENT
344+
))
345+
.unwrap(),
346+
)
347+
.await,
348+
));
349+
350+
let mock = Mock::given(method("POST"))
351+
.and(path(format!(
352+
"/subgraphs/id/{}",
353+
test_assets::ESCROW_SUBGRAPH_DEPLOYMENT
354+
)))
355+
.respond_with(
356+
ResponseTemplate::new(200)
357+
.set_body_raw(test_assets::ESCROW_QUERY_RESPONSE_V2, "application/json"),
358+
);
359+
mock_server.register(mock).await;
360+
361+
// Test V2 escrow accounts watcher
362+
let mut accounts = escrow_accounts_v2(
363+
escrow_subgraph,
364+
test_assets::INDEXER_ADDRESS,
365+
Duration::from_secs(60),
366+
true,
367+
)
368+
.await
369+
.unwrap();
370+
accounts.changed().await.unwrap();
371+
372+
// V2 should produce identical results to V1 since they query the same data
373+
assert_eq!(
374+
accounts.borrow().clone(),
375+
EscrowAccounts::new(
376+
ESCROW_ACCOUNTS_BALANCES.to_owned(),
377+
ESCROW_ACCOUNTS_SENDERS_TO_SIGNERS.to_owned(),
378+
)
379+
);
380+
}
265381
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
// Copyright 2023-, Edge & Node, GraphOps, and Semiotic Labs.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
//! Horizon (V2) contract detection utilities
5+
//!
6+
//! This module provides functionality to detect if Horizon (V2) contracts are active
7+
//! in the network by querying the network subgraph for PaymentsEscrow accounts.
8+
9+
use anyhow::Result;
10+
use indexer_query::horizon_detection::{self, HorizonDetectionQuery};
11+
12+
use crate::client::SubgraphClient;
13+
14+
/// Detects if Horizon (V2) contracts are active in the network.
15+
///
16+
/// This function queries the network subgraph to check if any PaymentsEscrow accounts exist.
17+
/// If they do, it indicates that Horizon contracts are deployed and active.
18+
///
19+
/// # Arguments
20+
/// * `network_subgraph` - The network subgraph client to query
21+
///
22+
/// # Returns
23+
/// * `Ok(true)` if Horizon contracts are active
24+
/// * `Ok(false)` if only legacy (V1) contracts are active
25+
/// * `Err(...)` if there was an error querying the subgraph
26+
pub async fn is_horizon_active(network_subgraph: &SubgraphClient) -> Result<bool> {
27+
tracing::debug!("Checking if Horizon (V2) contracts are active in the network");
28+
29+
let response = network_subgraph
30+
.query::<HorizonDetectionQuery, _>(horizon_detection::Variables {})
31+
.await?;
32+
33+
let response = response?;
34+
35+
// If we find any PaymentsEscrow accounts, Horizon is active
36+
let horizon_active = !response.payments_escrow_accounts.is_empty();
37+
38+
if horizon_active {
39+
tracing::info!(
40+
"Horizon (V2) contracts detected - found {} PaymentsEscrow accounts",
41+
response.payments_escrow_accounts.len()
42+
);
43+
} else {
44+
tracing::info!("No Horizon (V2) contracts found - using legacy (V1) mode");
45+
}
46+
47+
Ok(horizon_active)
48+
}

crates/monitor/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ mod client;
77
mod deployment_to_allocation;
88
mod dispute_manager;
99
mod escrow_accounts;
10+
mod horizon_detection;
1011

1112
pub use crate::{
1213
allocations::{indexer_allocations, AllocationWatcher},
@@ -18,4 +19,5 @@ pub use crate::{
1819
escrow_accounts_v1, escrow_accounts_v2, EscrowAccounts, EscrowAccountsError,
1920
EscrowAccountsWatcher,
2021
},
22+
horizon_detection::is_horizon_active,
2123
};

0 commit comments

Comments
 (0)