rialo-cdk 0.2.0-alpha.0

Rialo CDK - A comprehensive toolkit for building with the Rialo blockchain
Documentation
// Copyright (c) Subzero Labs, Inc.
// SPDX-License-Identifier: Apache-2.0

//! Transaction utility functions for the Rialo CDK.
//!
//! This module provides helper functions for common transaction operations
//! such as waiting for confirmation. These utilities are only available on
//! non-WASM targets as they require tokio's async timer functionality.

#[cfg(not(target_arch = "wasm32"))]
use tokio::time::{sleep, Duration};

use crate::{
    error::{Result, RialoError},
    rpc::{
        traits::RpcClient,
        types::{Signature, SignatureStatus},
    },
};

/// Waits for a transaction to be confirmed on the blockchain.
///
/// This function polls the RPC client to check the status of a transaction
/// until it is confirmed (executed) or the timeout is reached.
///
/// # Arguments
///
/// * `client` - The RPC client to use for status checks.
/// * `signature` - The transaction signature to check.
/// * `timeout_ms` - Optional timeout in milliseconds to wait for confirmation.
///   Defaults to 5000ms if not specified.
///
/// # Returns
///
/// * `Ok(SignatureStatus)` if the transaction is confirmed (executed).
///   Note: The transaction may have failed on-chain; check `status.err` for errors.
/// * `Err(RialoError::TransactionTimeout)` if the timeout is reached.
/// * `Err(...)` for other RPC errors.
///
/// # Availability
///
/// This function is only available on non-WASM targets as it requires
/// tokio's async timer functionality.
///
/// # Examples
///
/// ```no_run
/// # use rialo_cdk::{RpcClient, rpc::HttpRpcClient};
/// # use rialo_cdk::rpc::transaction_utils::wait_for_confirmation;
/// # use rialo_cdk::rpc::types::Signature;
/// # use std::str::FromStr;
/// # async fn example() -> rialo_cdk::Result<()> {
/// # let client = HttpRpcClient::new("http://localhost:8899".to_string());
/// # let signature = Signature::from_str("5VERv8NMvzbJMEkV8xnrLkEaWRtSz9CosKDYjCJjBRnbJLgp8uirBgmQpjKhoR4tjF3ZpRzrFmBV6UjKdiSZkQUW").unwrap();
/// // Wait for transaction confirmation with default timeout
/// let status = wait_for_confirmation(&client, &signature, None).await?;
/// if let Some(err) = status.err {
///     eprintln!("Transaction failed on-chain: {}", err);
/// }
/// # Ok(())
/// # }
/// ```
#[cfg(not(target_arch = "wasm32"))]
#[tracing::instrument(skip(client))]
pub async fn wait_for_confirmation(
    client: &impl RpcClient,
    signature: &Signature,
    timeout_ms: Option<u32>,
) -> Result<SignatureStatus> {
    // Time to wait between status check attempts (in ms)
    const CHECK_INTERVAL_MS: u32 = 50;

    // Default timeout: 5 seconds
    let timeout_ms = timeout_ms.unwrap_or(5000);

    // Calculate maximum number of attempts
    let max_attempts = timeout_ms.div_ceil(CHECK_INTERVAL_MS);

    for _ in 0..max_attempts {
        // Check the transaction status
        let statuses = client.get_signature_statuses(&[*signature]).await?;

        // Check if the transaction has been executed
        if let Some(status) = statuses.into_iter().next() {
            if status.executed {
                return Ok(status);
            }
        }

        // Wait before the next check
        sleep(Duration::from_millis(CHECK_INTERVAL_MS as u64)).await;
    }

    // Timeout reached
    Err(RialoError::TransactionTimeout)
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::rpc::mock_client::MockRpcClient;

    #[cfg(not(target_arch = "wasm32"))]
    #[tokio::test]
    async fn test_confirmation_success_immediate() {
        let mock_rpc = MockRpcClient::new().with_signature_statuses(vec![Some(SignatureStatus {
            slot: 100,
            executed: true,
            err: None,
        })]);

        let signature = Signature::new_unique();
        let result = wait_for_confirmation(&mock_rpc, &signature, None).await;

        assert!(result.is_ok(), "Should confirm immediately");
        let status = result.unwrap();
        assert!(status.err.is_none(), "Should have no error");
    }

    #[cfg(not(target_arch = "wasm32"))]
    #[tokio::test]
    async fn test_confirmation_with_onchain_error() {
        let mock_rpc = MockRpcClient::new().with_signature_statuses(vec![Some(SignatureStatus {
            slot: 100,
            executed: true,
            err: Some("InstructionError: RegistryAlreadyInitialized".to_string()),
        })]);

        let signature = Signature::new_unique();
        let result = wait_for_confirmation(&mock_rpc, &signature, None).await;

        assert!(result.is_ok(), "Should return Ok even with on-chain error");
        let status = result.unwrap();
        assert!(status.err.is_some(), "Should have on-chain error");
        assert!(status.err.unwrap().contains("RegistryAlreadyInitialized"));
    }

    #[cfg(not(target_arch = "wasm32"))]
    #[tokio::test]
    async fn test_confirmation_timeout() {
        let mock_rpc = MockRpcClient::new().with_signature_statuses(vec![Some(SignatureStatus {
            slot: 100,
            err: None,
            executed: false,
        })]);

        let signature = Signature::new_unique();
        // Use short timeout for test
        let result = wait_for_confirmation(&mock_rpc, &signature, Some(100)).await;

        assert!(result.is_err(), "Should timeout");
        assert!(
            matches!(result.unwrap_err(), RialoError::TransactionTimeout),
            "Should be TransactionTimeout error"
        );
    }

    #[cfg(not(target_arch = "wasm32"))]
    #[tokio::test]
    async fn test_confirmation_with_custom_timeout() {
        let mock_rpc = MockRpcClient::new().with_signature_statuses(vec![Some(SignatureStatus {
            slot: 100,
            err: None,
            executed: true,
        })]);

        let signature = Signature::new_unique();
        let result = wait_for_confirmation(&mock_rpc, &signature, Some(10000)).await;

        assert!(result.is_ok(), "Should confirm with custom timeout");
    }
}