use alloy::network::ReceiptResponse;
use alloy::primitives::{Address, B256};
use alloy::providers::Provider;
use tempo_alloy::TempoNetwork;
use crate::protocol::core::{ChallengeEcho, Receipt};
use crate::protocol::intents::ChargeRequest;
use crate::protocol::traits::VerificationError;
use super::super::{proof, TempoChargeExt, METHOD_NAME};
use super::memo::assert_challenge_bound_memo;
use super::receipt_logs::{match_receipt_transfer_logs_with_settlement, ReceiptSenderPolicy};
use super::ChargeMethod;
pub(super) fn parse_hash_credential_source(
source: Option<&str>,
expected_chain_id: u64,
) -> Result<Option<Address>, VerificationError> {
let Some(source) = source else {
return Ok(None);
};
let invalid = || VerificationError::new("Hash credential source is invalid.");
let parsed = proof::parse_proof_source(source).map_err(|_| invalid())?;
if parsed.chain_id != expected_chain_id {
return Err(invalid());
}
Ok(Some(parsed.address))
}
fn request_settlement_senders(charge: &ChargeRequest, chain_id: u64) -> Vec<Address> {
if charge.machine_token_enabled() {
super::super::machine_token::settlement_sender(chain_id)
.into_iter()
.collect()
} else {
Vec::new()
}
}
impl<P> ChargeMethod<P>
where
P: Provider<TempoNetwork> + Clone + Send + Sync + 'static,
{
pub(super) async fn verify_hash(
&self,
tx_hash: &str,
charge: &ChargeRequest,
source: Option<&str>,
expected_chain_id: u64,
challenge: &ChallengeEcho,
reserve: bool,
) -> Result<Receipt, VerificationError> {
let source_address = parse_hash_credential_source(source, expected_chain_id)?;
let hash = tx_hash
.parse::<B256>()
.map_err(|e| VerificationError::new(format!("Invalid transaction hash: {}", e)))?;
let replay_key = format!("mpp:charge:{:#x}", hash);
let receipt = self
.provider
.get_transaction_receipt(hash)
.await
.map_err(|e| {
VerificationError::network_error(format!("Failed to fetch receipt: {}", e))
})?
.ok_or_else(|| {
VerificationError::pending(format!(
"Transaction {} not found or not yet mined",
tx_hash
))
})?;
if !receipt.status() {
return Err(VerificationError::transaction_failed(format!(
"Transaction {} reverted",
tx_hash
)));
}
let currency = charge.currency_address().map_err(|e| {
VerificationError::new(format!("Invalid currency address in request: {}", e))
})?;
let expected = Self::expected_transfers(charge)?;
let expected_sender = source_address.unwrap_or_else(|| receipt.from());
let matched_logs = match_receipt_transfer_logs_with_settlement(
receipt.logs(),
currency,
&expected,
ReceiptSenderPolicy {
expected_sender,
source,
validate_sender: self.validate_sender.as_deref(),
transaction_sender: receipt.from(),
settlement_senders: &request_settlement_senders(charge, expected_chain_id),
},
)?;
assert_challenge_bound_memo(&matched_logs, &challenge.id, &challenge.realm)?;
if let Some(store) = &self.store {
if reserve {
let claimed = store
.put_if_absent(&replay_key, serde_json::Value::Bool(true))
.await
.map_err(|e| {
VerificationError::internal(format!("Failed to record tx hash: {e}"))
})?;
if !claimed {
return Err(VerificationError::new(
"Transaction hash has already been used.",
));
}
} else if store
.get(&replay_key)
.await
.map_err(|e| VerificationError::internal(format!("Failed to check tx hash: {e}")))?
.is_some()
{
return Err(VerificationError::new(
"Transaction hash has already been used.",
));
}
}
Ok(Receipt::success(METHOD_NAME, format!("{hash:#x}")))
}
}