use alloy::consensus::transaction::SignerRecoverable;
use alloy::eips::Decodable2718;
use alloy::providers::Provider;
use alloy::rpc::types::simulate::{SimBlock, SimCallResult, SimulatePayload};
use alloy::rpc::types::TransactionRequest;
use tempo_alloy::primitives::transaction::{PrimitiveSignature, TempoSignature};
use tempo_alloy::rpc::TempoTransactionRequest;
use tempo_alloy::TempoNetwork;
use crate::protocol::traits::VerificationError;
use super::ChargeMethod;
#[derive(Debug, Clone, serde::Deserialize)]
struct TempoSimulateResponse {
#[serde(default)]
blocks: Vec<TempoSimulateBlock>,
}
#[derive(Debug, Clone, serde::Deserialize)]
struct TempoSimulateBlock {
#[serde(default)]
calls: Vec<SimCallResult>,
}
impl<P> ChargeMethod<P>
where
P: Provider<TempoNetwork> + Clone + Send + Sync + 'static,
{
pub(super) fn build_simulate_payload(
final_tx_bytes: &[u8],
) -> Result<SimulatePayload<TempoTransactionRequest>, VerificationError> {
let signed = tempo_alloy::primitives::AASigned::decode_2718(&mut &final_tx_bytes[..])
.map_err(|e| {
VerificationError::new(format!("Failed to decode co-signed tx for simulation: {e}"))
})?;
let sender = signed.recover_signer().map_err(|e| {
VerificationError::new(format!("Failed to recover sender for simulation: {e}"))
})?;
let (key_id, key_type, key_data) = {
let (key_id, primitive_sig) =
if let TempoSignature::Keychain(keychain_sig) = signed.signature() {
let key_id = keychain_sig.key_id(&signed.signature_hash()).map_err(|e| {
VerificationError::new(format!(
"Failed to recover keychain access key for simulation: {e}"
))
})?;
(Some(key_id), Some(&keychain_sig.signature))
} else if let TempoSignature::Primitive(primitive_sig) = signed.signature() {
(None, Some(primitive_sig))
} else {
(None, None)
};
let (key_type, key_data) = if let Some(primitive_sig) = primitive_sig {
let key_data = match primitive_sig {
PrimitiveSignature::WebAuthn(webauthn) => Some(webauthn.webauthn_data.clone()),
_ => None,
};
(Some(primitive_sig.signature_type()), key_data)
} else {
(None, None)
};
(key_id, key_type, key_data)
};
let mut req: TempoTransactionRequest = signed.into();
req.inner.from = Some(sender);
let tail = req.calls.pop().ok_or_else(|| {
VerificationError::new("Cannot simulate Tempo AA transaction with no calls")
})?;
req.inner.to = Some(tail.to);
req.inner.value = Some(tail.value);
req.inner.input = tail.input.into();
req.key_type = key_type;
req.key_data = key_data;
if let Some(key_id) = key_id {
req.key_id = Some(key_id);
}
Ok(SimulatePayload {
block_state_calls: vec![SimBlock {
block_overrides: None,
state_overrides: None,
calls: vec![req],
}],
validation: false,
trace_transfers: false,
return_full_transactions: false,
})
}
pub(super) async fn simulate_before_broadcast(
&self,
final_tx_bytes: &[u8],
) -> Result<(), VerificationError> {
const JSONRPC_METHOD_NOT_FOUND: i64 = -32601;
let payload = Self::build_simulate_payload(final_tx_bytes)?;
let response: TempoSimulateResponse = match self
.provider
.raw_request("tempo_simulateV1".into(), (&payload,))
.await
{
Ok(response) => response,
Err(e) => {
if e.as_error_resp()
.is_some_and(|err| err.code == JSONRPC_METHOD_NOT_FOUND)
{
return self.simulate_with_eth_call(payload).await;
}
return Err(VerificationError::network_error(format!(
"Pre-broadcast simulation failed: {e}"
)));
}
};
let call: &SimCallResult = response
.blocks
.first()
.and_then(|block| block.calls.first())
.ok_or_else(|| {
VerificationError::internal("Pre-broadcast simulation returned no call results")
})?;
if !call.status {
let detail = match &call.error {
Some(err) => format!("{} (code {})", err.message, err.code),
None if !call.return_data.is_empty() => {
format!(
"revert data {}",
alloy::hex::encode_prefixed(&call.return_data)
)
}
None => "no revert reason returned".to_string(),
};
return Err(VerificationError::transaction_failed(format!(
"Sponsored transaction would revert in pre-broadcast simulation: {detail}"
)));
}
Ok(())
}
pub(super) fn sender_call_request(request: TempoTransactionRequest) -> TempoTransactionRequest {
TempoTransactionRequest {
inner: TransactionRequest {
from: request.inner.from,
to: request.inner.to,
value: request.inner.value,
input: request.inner.input,
..Default::default()
},
calls: request.calls,
..Default::default()
}
}
async fn simulate_with_eth_call(
&self,
payload: SimulatePayload<TempoTransactionRequest>,
) -> Result<(), VerificationError> {
const JSONRPC_EXECUTION_REVERTED: i64 = 3;
let requests = payload
.block_state_calls
.into_iter()
.flat_map(|block| block.calls);
for request in requests {
if let Err(e) = self.provider.call(Self::sender_call_request(request)).await {
return Err(match e.as_error_resp() {
Some(err) if err.code == JSONRPC_EXECUTION_REVERTED => {
VerificationError::transaction_failed(format!(
"Sponsored transaction would revert in pre-broadcast simulation: {} (code {})",
err.message, err.code
))
}
_ => VerificationError::network_error(format!(
"Pre-broadcast simulation failed: {e}"
)),
});
}
}
Ok(())
}
}