use std::{sync::Arc, time::Duration};
use starknet::{
core::types::{ExecutionResult, Felt, TransactionReceipt, TransactionReceiptWithBlockInfo},
providers::Provider,
};
use tokio::time::sleep;
use tokio_stream::{StreamExt, wrappers::IntervalStream};
use tracing::{debug, warn};
use crate::error::{Result, StarkzapError};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum TxStatus {
Pending,
Accepted,
Reverted { reason: String },
Rejected { reason: String },
}
impl TxStatus {
pub fn is_final(&self) -> bool {
!matches!(self, TxStatus::Pending)
}
}
#[derive(Debug, Clone)]
pub struct Tx<P>
where
P: Provider + Send + Sync + Clone + 'static,
{
pub hash: Felt,
provider: Arc<P>,
}
impl<P> Tx<P>
where
P: Provider + Send + Sync + Clone + 'static,
{
pub(crate) fn new(hash: Felt, provider: Arc<P>) -> Self {
Self { hash, provider }
}
pub fn hash_hex(&self) -> String {
format!("{:#x}", self.hash)
}
pub async fn wait(&self) -> Result<TransactionReceiptWithBlockInfo> {
self.wait_with_options(30, Duration::from_secs(2)).await
}
pub async fn wait_with_options(
&self,
max_attempts: u32,
initial_interval: Duration,
) -> Result<TransactionReceiptWithBlockInfo> {
let mut interval = initial_interval;
let cap = Duration::from_secs(30);
for attempt in 0..max_attempts {
debug!(hash = %self.hash_hex(), attempt, "polling transaction receipt");
match self.provider.get_transaction_receipt(self.hash).await {
Ok(receipt) => {
return match execution_result(&receipt.receipt) {
ExecutionResult::Succeeded => Ok(receipt),
ExecutionResult::Reverted { reason } => {
Err(StarkzapError::TransactionReverted {
reason: reason.clone(),
})
}
};
}
Err(e) => {
warn!(hash = %self.hash_hex(), error = %e, "receipt fetch error, retrying");
}
}
sleep(interval).await;
interval = (interval * 2).min(cap);
}
Err(StarkzapError::WaitTimeout {
attempts: max_attempts,
})
}
pub async fn status(&self) -> Result<TxStatus> {
match self.provider.get_transaction_receipt(self.hash).await {
Ok(receipt) => Ok(match execution_result(&receipt.receipt) {
ExecutionResult::Succeeded => TxStatus::Accepted,
ExecutionResult::Reverted { reason } => TxStatus::Reverted {
reason: reason.clone(),
},
}),
Err(_) => Ok(TxStatus::Pending),
}
}
pub fn watch(&self, interval: Duration) -> impl tokio_stream::Stream<Item = TxStatus> + '_ {
IntervalStream::new(tokio::time::interval(interval))
.then(move |_| async move { self.status().await.unwrap_or(TxStatus::Pending) })
}
}
impl<P> std::fmt::Display for Tx<P>
where
P: Provider + Send + Sync + Clone + 'static,
{
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "Tx({})", self.hash_hex())
}
}
fn execution_result(receipt: &TransactionReceipt) -> &ExecutionResult {
match receipt {
TransactionReceipt::Invoke(r) => &r.execution_result,
TransactionReceipt::L1Handler(r) => &r.execution_result,
TransactionReceipt::Declare(r) => &r.execution_result,
TransactionReceipt::Deploy(r) => &r.execution_result,
TransactionReceipt::DeployAccount(r) => &r.execution_result,
}
}