Skip to content

Commit 0e3d8b2

Browse files
committed
fix(monitor): implement v2 escrow accounts
1 parent faacf49 commit 0e3d8b2

File tree

3 files changed

+183
-7
lines changed

3 files changed

+183
-7
lines changed

crates/monitor/src/escrow_accounts.rs

Lines changed: 131 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,12 @@ impl EscrowAccounts {
8888

8989
pub type EscrowAccountsWatcher = Receiver<EscrowAccounts>;
9090

91+
pub fn empty_escrow_accounts_watcher() -> EscrowAccountsWatcher {
92+
let (_, receiver) =
93+
tokio::sync::watch::channel(EscrowAccounts::new(HashMap::new(), HashMap::new()));
94+
receiver
95+
}
96+
9197
pub async fn escrow_accounts_v1(
9298
escrow_subgraph: &'static SubgraphClient,
9399
indexer_address: Address,
@@ -112,13 +118,83 @@ pub async fn escrow_accounts_v2(
112118
.await
113119
}
114120

115-
// TODO implement escrow accounts v2 query
116121
async fn get_escrow_accounts_v2(
117-
_escrow_subgraph: &'static SubgraphClient,
118-
_indexer_address: Address,
119-
_reject_thawing_signers: bool,
122+
escrow_subgraph: &'static SubgraphClient,
123+
indexer_address: Address,
124+
reject_thawing_signers: bool,
120125
) -> anyhow::Result<EscrowAccounts> {
121-
Ok(EscrowAccounts::new(HashMap::new(), HashMap::new()))
126+
// Query V2 escrow accounts from the network subgraph which tracks PaymentsEscrow
127+
// and GraphTallyCollector contract events.
128+
129+
use indexer_query::network_escrow_account_v2::{
130+
self as network_escrow_account_v2, NetworkEscrowAccountQueryV2,
131+
};
132+
133+
let response = escrow_subgraph
134+
.query::<NetworkEscrowAccountQueryV2, _>(network_escrow_account_v2::Variables {
135+
receiver: format!("{indexer_address:x?}"),
136+
thaw_end_timestamp: if reject_thawing_signers {
137+
U256::ZERO.to_string()
138+
} else {
139+
U256::MAX.to_string()
140+
},
141+
})
142+
.await?;
143+
144+
let response = response?;
145+
146+
tracing::trace!("Network V2 Escrow accounts response: {:?}", response);
147+
148+
// V2 TAP receipts use different field names (payer/service_provider) but the underlying
149+
// escrow account model is identical to V1. Both V1 and V2 receipts reference the same
150+
// sender addresses and the same escrow relationships.
151+
//
152+
// V1 queries the TAP subgraph while V2 queries the network subgraph, but both return
153+
// the same escrow account structure for processing.
154+
//
155+
// V2 receipt flow:
156+
// 1. V2 receipt contains payer address (equivalent to V1 sender)
157+
// 2. Receipt is signed by a signer authorized by the payer
158+
// 3. Escrow accounts map: signer -> payer (sender) -> balance
159+
// 4. Service provider (indexer) receives payments from payer's escrow
160+
161+
let senders_balances: HashMap<Address, U256> = response
162+
.payments_escrow_accounts
163+
.iter()
164+
.map(|account| {
165+
let balance = U256::checked_sub(
166+
U256::from_str(&account.balance)?,
167+
U256::from_str(&account.total_amount_thawing)?,
168+
)
169+
.unwrap_or_else(|| {
170+
tracing::warn!(
171+
"Balance minus total amount thawing underflowed for V2 account {}. \
172+
Setting balance to 0, no V2 queries will be served for this payer.",
173+
account.payer.id
174+
);
175+
U256::from(0)
176+
});
177+
178+
Ok((Address::from_str(&account.payer.id)?, balance))
179+
})
180+
.collect::<Result<HashMap<_, _>, anyhow::Error>>()?;
181+
182+
let senders_to_signers = response
183+
.payments_escrow_accounts
184+
.into_iter()
185+
.map(|account| {
186+
let payer = Address::from_str(&account.payer.id)?;
187+
let signers = account
188+
.payer
189+
.signers
190+
.iter()
191+
.map(|signer| Address::from_str(&signer.id))
192+
.collect::<Result<Vec<_>, _>>()?;
193+
Ok((payer, signers))
194+
})
195+
.collect::<Result<HashMap<_, _>, anyhow::Error>>()?;
196+
197+
Ok(EscrowAccounts::new(senders_balances, senders_to_signers))
122198
}
123199

124200
async fn get_escrow_accounts_v1(
@@ -262,4 +338,54 @@ mod tests {
262338
)
263339
);
264340
}
341+
342+
#[test(tokio::test)]
343+
async fn test_current_accounts_v2() {
344+
// Set up a mock escrow subgraph for V2 with payer fields
345+
let mock_server = MockServer::start().await;
346+
let escrow_subgraph = Box::leak(Box::new(
347+
SubgraphClient::new(
348+
reqwest::Client::new(),
349+
None,
350+
DeploymentDetails::for_query_url(&format!(
351+
"{}/subgraphs/id/{}",
352+
&mock_server.uri(),
353+
test_assets::ESCROW_SUBGRAPH_DEPLOYMENT
354+
))
355+
.unwrap(),
356+
)
357+
.await,
358+
));
359+
360+
let mock = Mock::given(method("POST"))
361+
.and(path(format!(
362+
"/subgraphs/id/{}",
363+
test_assets::ESCROW_SUBGRAPH_DEPLOYMENT
364+
)))
365+
.respond_with(
366+
ResponseTemplate::new(200)
367+
.set_body_raw(test_assets::ESCROW_QUERY_RESPONSE_V2, "application/json"),
368+
);
369+
mock_server.register(mock).await;
370+
371+
// Test V2 escrow accounts watcher
372+
let mut accounts = escrow_accounts_v2(
373+
escrow_subgraph,
374+
test_assets::INDEXER_ADDRESS,
375+
Duration::from_secs(60),
376+
true,
377+
)
378+
.await
379+
.unwrap();
380+
accounts.changed().await.unwrap();
381+
382+
// V2 should produce identical results to V1 since they query the same data
383+
assert_eq!(
384+
accounts.borrow().clone(),
385+
EscrowAccounts::new(
386+
ESCROW_ACCOUNTS_BALANCES.to_owned(),
387+
ESCROW_ACCOUNTS_SENDERS_TO_SIGNERS.to_owned(),
388+
)
389+
);
390+
}
265391
}
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: 4 additions & 2 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},
@@ -15,7 +16,8 @@ pub use crate::{
1516
deployment_to_allocation::{deployment_to_allocation, DeploymentToAllocationWatcher},
1617
dispute_manager::{dispute_manager, DisputeManagerWatcher},
1718
escrow_accounts::{
18-
escrow_accounts_v1, escrow_accounts_v2, EscrowAccounts, EscrowAccountsError,
19-
EscrowAccountsWatcher,
19+
empty_escrow_accounts_watcher, escrow_accounts_v1, escrow_accounts_v2, EscrowAccounts,
20+
EscrowAccountsError, EscrowAccountsWatcher,
2021
},
22+
horizon_detection::is_horizon_active,
2123
};

0 commit comments

Comments
 (0)