diff --git a/crates/service/src/tap/checks/allocation_redeemed.rs b/crates/service/src/tap/checks/allocation_redeemed.rs index 7b6cfadbe..c883ec2c9 100644 --- a/crates/service/src/tap/checks/allocation_redeemed.rs +++ b/crates/service/src/tap/checks/allocation_redeemed.rs @@ -2,17 +2,14 @@ // SPDX-License-Identifier: Apache-2.0 use indexer_monitor::{SubgraphClient, SubgraphQueryError}; -use indexer_query::payments_escrow_transactions_redeem; +use indexer_query::closed_allocations::{self, ClosedAllocations}; use tap_core::receipt::checks::{Check, CheckError, CheckResult}; use thegraph_core::{ alloy::{hex::ToHexExt, primitives::Address}, CollectionId, }; -use crate::{ - middleware::Sender, - tap::{CheckingReceipt, TapReceipt}, -}; +use crate::tap::{CheckingReceipt, TapReceipt}; /// Errors that can occur during allocation redemption checks. #[derive(Debug, thiserror::Error)] @@ -40,9 +37,8 @@ impl AllocationRedeemedCheck { } } - async fn v2_allocation_redeemed( + async fn v2_allocation_closed( &self, - sender: Address, collection_id: CollectionId, ) -> Result { let network_subgraph = self @@ -51,19 +47,22 @@ impl AllocationRedeemedCheck { // Horizon network subgraph stores allocationId as the 20-byte address derived // from the 32-byte collection_id (rightmost 20 bytes). - let allocation_ids = vec![collection_id.as_address().encode_hex()]; - - let response = network_subgraph - .query::( - payments_escrow_transactions_redeem::Variables { - payer: sender.encode_hex(), - receiver: self.indexer_address.encode_hex(), - allocation_ids: Some(allocation_ids), - }, - ) + let allocation_id = collection_id.as_address().encode_hex(); + + // Only reject receipts if the allocation is actually closed on-chain. + // In the continuous collection model, active allocations get collected + // from periodically — redeem transactions existing doesn't mean the + // allocation is done. + let closed_response = network_subgraph + .query::(closed_allocations::Variables { + allocation_ids: vec![allocation_id], + block: None, + first: 1, + last: String::new(), + }) .await?; - Ok(!response.payments_escrow_transactions.is_empty()) + Ok(!closed_response.allocations.is_empty()) } } @@ -71,22 +70,18 @@ impl AllocationRedeemedCheck { impl Check for AllocationRedeemedCheck { async fn check( &self, - ctx: &tap_core::receipt::Context, + _ctx: &tap_core::receipt::Context, receipt: &CheckingReceipt, ) -> CheckResult { - let Sender(sender) = ctx - .get::() - .ok_or_else(|| CheckError::Failed(anyhow::anyhow!("Missing sender in context")))?; - let collection_id = CollectionId::from(receipt.signed_receipt().as_ref().message.collection_id); - let redeemed = self - .v2_allocation_redeemed(*sender, collection_id) + let closed = self + .v2_allocation_closed(collection_id) .await .map_err(|e| CheckError::Failed(anyhow::anyhow!(e)))?; - if redeemed { + if closed { return Err(CheckError::Failed(anyhow::anyhow!( - "Allocation already redeemed (v2): {}", + "Allocation is closed (v2): {}", collection_id.as_address() ))); } @@ -112,7 +107,7 @@ mod tests { use wiremock::{matchers::body_string_contains, Mock, MockServer, ResponseTemplate}; use super::AllocationRedeemedCheck; - use crate::{middleware::Sender, tap::TapReceipt}; + use crate::tap::TapReceipt; fn create_wallet() -> PrivateKeySigner { MnemonicBuilder::::default() @@ -140,13 +135,16 @@ mod tests { } #[tokio::test] - async fn v2_redeemed_rejects() { + async fn v2_closed_allocation_rejects() { let mock_server: MockServer = MockServer::start().await; mock_server .register( - Mock::given(body_string_contains("paymentsEscrowTransactions")).respond_with( + Mock::given(body_string_contains("allocations")).respond_with( ResponseTemplate::new(200).set_body_json(json!({ - "data": { "paymentsEscrowTransactions": [ { "id": "0x01", "allocationId": TAP_SIGNER.1.to_string(), "timestamp": "1" } ] } + "data": { + "meta": { "block": { "number": 1, "hash": "0x00", "timestamp": 1 } }, + "allocations": [ { "id": "0x01" } ] + } })), ), ) @@ -164,12 +162,47 @@ mod tests { let check = AllocationRedeemedCheck::new(Address::from([0x22u8; 20]), Some(network_subgraph)); - let mut ctx = Context::default(); - ctx.insert(Sender(TAP_SIGNER.1)); + let ctx = Context::default(); let receipt = create_v2_receipt(TAP_SIGNER.1); let checking = crate::tap::CheckingReceipt::new(receipt); let result = check.check(&ctx, &checking).await; assert!(result.is_err()); } + + #[tokio::test] + async fn v2_open_allocation_passes() { + let mock_server: MockServer = MockServer::start().await; + mock_server + .register( + Mock::given(body_string_contains("allocations")).respond_with( + ResponseTemplate::new(200).set_body_json(json!({ + "data": { + "meta": { "block": { "number": 1, "hash": "0x00", "timestamp": 1 } }, + "allocations": [] + } + })), + ), + ) + .await; + + let network_subgraph = Box::leak(Box::new( + SubgraphClient::new( + reqwest::Client::new(), + None, + DeploymentDetails::for_query_url(&mock_server.uri()).unwrap(), + ) + .await, + )); + + let check = + AllocationRedeemedCheck::new(Address::from([0x22u8; 20]), Some(network_subgraph)); + + let ctx = Context::default(); + let receipt = create_v2_receipt(TAP_SIGNER.1); + let checking = crate::tap::CheckingReceipt::new(receipt); + + let result = check.check(&ctx, &checking).await; + assert!(result.is_ok()); + } } diff --git a/crates/service/tests/router_test.rs b/crates/service/tests/router_test.rs index 936ddde0f..f496d6e13 100644 --- a/crates/service/tests/router_test.rs +++ b/crates/service/tests/router_test.rs @@ -127,7 +127,9 @@ async fn full_integration_test() { )); mock_server.register(mock).await; - // Mock escrow subgraph (v1) and network subgraph (v2) redemption queries. + // Mock escrow subgraph (v1) and network subgraph (v2) queries. + // The v2 allocation check queries for closed allocations; returning an + // empty list means the allocation is still open (receipts accepted). mock_server .register(Mock::given(method("POST")).and(path("/")).respond_with( ResponseTemplate::new(200).set_body_raw( @@ -135,7 +137,8 @@ async fn full_integration_test() { { "data": { "transactions": [], - "paymentsEscrowTransactions": [] + "meta": { "block": { "number": 1, "hash": "0x00", "timestamp": 1 } }, + "allocations": [] } } "#,