use alloy::network::ReceiptResponse;
use alloy::primitives::{Address, Bytes, B256};
use alloy::providers::Provider;
use tempo_alloy::TempoNetwork;
use super::chain::get_on_chain_channel;
use super::receipt::session_receipt;
use super::state::Opening;
use super::{normalize_channel_id, ChannelState, SessionMethod};
use crate::protocol::core::{PaymentCredential, Receipt};
use crate::protocol::methods::tempo::session::{
SessionCredentialPayload, TempoSessionMethodDetails,
};
use crate::protocol::methods::tempo::voucher::verify_voucher;
use crate::protocol::traits::VerificationError;
impl<P> SessionMethod<P>
where
P: Provider<TempoNetwork> + Clone + Send + Sync + 'static,
{
pub(super) fn verify_open_channel_id_binding(
tx_bytes: &[u8],
claimed_channel_id: B256,
escrow: Address,
chain_id: u64,
expected_payee: Address,
expected_token: Address,
) -> Result<(Address, u128), VerificationError> {
use alloy::consensus::transaction::SignerRecoverable;
use alloy::sol_types::SolCall;
alloy::sol! {
interface IEscrowOpen {
function open(address payee, address token, uint128 deposit, bytes32 salt, address authorizedSigner) external;
}
}
let (signed, input) = Self::decode_escrow_call(
tx_bytes,
escrow,
<IEscrowOpen::openCall as SolCall>::SELECTOR,
"open",
)?;
let sender = signed
.recover_signer()
.map_err(|e| VerificationError::new(format!("failed to recover sender: {e}")))?;
let decoded = IEscrowOpen::openCall::abi_decode(&input).map_err(|e| {
VerificationError::invalid_payload(format!(
"failed to decode escrow.open() calldata: {e}"
))
})?;
if decoded.payee != expected_payee {
return Err(VerificationError::credential_mismatch(
"open transaction payee does not match session recipient",
));
}
if decoded.token != expected_token {
return Err(VerificationError::credential_mismatch(
"open transaction token does not match session currency",
));
}
let derived = crate::protocol::methods::tempo::voucher::compute_channel_id(
sender,
decoded.payee,
decoded.token,
decoded.salt,
decoded.authorizedSigner,
escrow,
chain_id,
);
if derived != claimed_channel_id {
return Err(VerificationError::new(
"open transaction does not match claimed channelId",
));
}
let voucher_signer = if decoded.authorizedSigner == Address::ZERO {
sender
} else {
decoded.authorizedSigner
};
Ok((voucher_signer, decoded.deposit))
}
pub(super) async fn handle_open(
&self,
credential: &PaymentCredential,
payload: &SessionCredentialPayload,
details: &TempoSessionMethodDetails,
expected_payee: Address,
expected_token: Address,
amount: u128,
) -> Result<Receipt, VerificationError> {
let (
channel_id_str,
descriptor,
settlement_route,
cumulative_amount_str,
signature_str,
_authorized_signer_str,
transaction_str,
) = match payload {
SessionCredentialPayload::Open {
channel_id,
descriptor,
settlement_route,
cumulative_amount,
signature,
authorized_signer,
transaction,
..
} => (
channel_id,
descriptor.as_ref(),
settlement_route.as_ref(),
cumulative_amount,
signature,
authorized_signer,
transaction,
),
_ => unreachable!(),
};
let channel_id_str = &normalize_channel_id(channel_id_str);
let channel_id_b256 = Self::parse_channel_id(channel_id_str)?;
let escrow = self.resolve_escrow(details)?;
let chain_id = self.resolve_chain_id(details);
if details.machine_token_enabled == Some(true) {
let descriptor = descriptor.ok_or_else(|| {
VerificationError::invalid_payload(
"machine-token open credential is missing its channel descriptor",
)
})?;
let route = settlement_route.ok_or_else(|| {
VerificationError::invalid_payload(
"machine-token open credential is missing its settlement route",
)
})?;
let recipient = Self::parse_address(&route.recipient)?;
let target_token = Self::parse_address(&route.target_token)?;
let route_salt = route
.route_salt
.parse()
.map_err(|_| VerificationError::invalid_payload("invalid settlement routeSalt"))?;
let expected_salt =
crate::protocol::methods::tempo::machine_token::compute_session_salt(
recipient,
target_token,
route_salt,
);
if descriptor.salt != expected_salt.to_string()
|| details.settlement_adapter.as_deref() != Some(&route.adapter)
|| details.settlement_recipient.as_deref() != Some(&route.recipient)
|| details.settlement_token.as_deref() != Some(&route.target_token)
{
return Err(VerificationError::credential_mismatch(
"machine-token settlement route is not bound to the descriptor",
));
}
}
let accepted_settlement_route = settlement_route.cloned();
let tx_bytes: Bytes = transaction_str.parse().map_err(|e| {
VerificationError::invalid_payload(format!("invalid open transaction hex: {}", e))
})?;
let (voucher_signer, open_deposit) = Self::verify_open_channel_id_binding(
&tx_bytes,
channel_id_b256,
escrow,
chain_id,
expected_payee,
expected_token,
)?;
let cumulative_amount = Self::parse_amount(cumulative_amount_str, "cumulativeAmount")?;
if cumulative_amount > open_deposit {
return Err(VerificationError::amount_exceeds_deposit(
"voucher amount exceeds open deposit",
));
}
if open_deposit < amount {
return Err(VerificationError::insufficient_balance(
"open deposit is less than the session amount",
));
}
let sig_bytes = Self::parse_signature(signature_str)?;
if !verify_voucher(
escrow,
chain_id,
channel_id_b256,
cumulative_amount,
&sig_bytes,
voucher_signer,
) {
return Err(VerificationError::invalid_signature(
"invalid voucher signature",
));
}
let pending = self
.provider
.send_raw_transaction(&tx_bytes)
.await
.map_err(|e| {
VerificationError::network_error(format!("failed to broadcast open tx: {}", e))
})?;
let tx_receipt = pending
.get_receipt()
.await
.map_err(|e| VerificationError::network_error(format!("open tx failed: {}", e)))?;
if !tx_receipt.status() {
return Err(VerificationError::transaction_failed(format!(
"open transaction reverted (tx: {})",
tx_receipt.transaction_hash()
)));
}
let open_tx_hash = tx_receipt.transaction_hash().to_string();
let on_chain = get_on_chain_channel(&*self.provider, escrow, channel_id_b256).await?;
if on_chain.payee != expected_payee {
return Err(VerificationError::credential_mismatch(
"channel payee does not match session recipient",
));
}
if on_chain.token != expected_token {
return Err(VerificationError::credential_mismatch(
"channel token does not match session currency",
));
}
if on_chain.deposit == 0 {
return Err(VerificationError::channel_not_found(
"channel not funded on-chain",
));
}
if on_chain.finalized {
return Err(VerificationError::channel_closed(
"channel is finalized on-chain",
));
}
if on_chain.close_requested_at != 0 {
return Err(VerificationError::channel_closed(
"channel has a pending close request",
));
}
if on_chain.deposit.saturating_sub(on_chain.settled) < amount {
return Err(VerificationError::insufficient_balance(
"channel available balance is less than the session amount",
));
}
let authorized_signer = if on_chain.authorized_signer == Address::ZERO {
on_chain.payer
} else {
on_chain.authorized_signer
};
if cumulative_amount > on_chain.deposit {
return Err(VerificationError::amount_exceeds_deposit(
"voucher amount exceeds on-chain deposit",
));
}
if cumulative_amount < on_chain.settled {
return Err(VerificationError::new(
"voucher cumulativeAmount is below on-chain settled amount",
));
}
if authorized_signer != voucher_signer
&& !verify_voucher(
escrow,
chain_id,
channel_id_b256,
cumulative_amount,
&sig_bytes,
authorized_signer,
)
{
return Err(VerificationError::invalid_signature(
"invalid voucher signature",
));
}
let opening = Opening {
channel_id: channel_id_str.clone(),
chain_id,
escrow_contract: escrow,
authorized_signer,
settlement_route: accepted_settlement_route,
cumulative_amount,
signature: sig_bytes,
};
let updated = self
.store
.update_channel(
channel_id_str,
Box::new(move |existing| {
Ok(Some(ChannelState::open(existing, &on_chain, opening)))
}),
)
.await?;
let state =
updated.ok_or_else(|| VerificationError::internal("failed to create channel"))?;
Ok(session_receipt(
&credential.challenge.id,
&state,
Some(open_tx_hash),
))
}
}