Middleware
The middleware layer in Starkbiter provides a familiar interface for interacting with Starknet contracts, compatible with the starknet-rs ecosystem. This chapter explains how middleware works and how to use it effectively.
Overview
Starkbiter's middleware implements the standard Provider trait from starknet-rs, extended with additional "cheating" methods for testing. This means:
- Familiar API: If you know
starknet-rs, you know Starkbiter - Seamless Integration: Works with
cainomebindings and other tooling - Extended Capabilities: Additional methods for testing scenarios
The CheatingProvider
The CheatingProvider is the main middleware implementation in Starkbiter.
#![allow(unused)] fn main() { use starkbiter_core::middleware::CheatingProvider; // Access through environment let provider = env.provider(); // Use standard Provider methods let block_number = provider.block_number().await?; let block = provider.get_block_with_tx_hashes(block_number).await?; }
Standard Provider Methods
Block Queries
#![allow(unused)] fn main() { use starknet::providers::Provider; // Get latest block number let block_num = provider.block_number().await?; // Get block by number let block = provider.get_block_with_tx_hashes(block_num).await?; // Get block by hash let block = provider.get_block_with_tx_hashes_by_hash(block_hash).await?; }
Transaction Queries
#![allow(unused)] fn main() { // Get transaction by hash let tx = provider.get_transaction_by_hash(tx_hash).await?; // Get transaction receipt let receipt = provider.get_transaction_receipt(tx_hash).await?; // Get transaction status let status = provider.get_transaction_status(tx_hash).await?; }
State Queries
#![allow(unused)] fn main() { // Get storage at address let value = provider.get_storage_at( contract_address, key, block_id ).await?; // Get nonce let nonce = provider.get_nonce( block_id, contract_address ).await?; // Get class hash at contract let class_hash = provider.get_class_hash_at( block_id, contract_address ).await?; }
Contract Class Queries
#![allow(unused)] fn main() { // Get class let class = provider.get_class( block_id, class_hash ).await?; // Get class at contract address let class = provider.get_class_at( block_id, contract_address ).await?; }
Event Queries
#![allow(unused)] fn main() { use starknet::core::types::{EventFilter, EventsPage}; let filter = EventFilter { from_block: Some(BlockId::Number(0)), to_block: Some(BlockId::Number(100)), address: Some(contract_address), keys: None, }; let events: EventsPage = provider.get_events( filter, None, // continuation_token 100, // chunk_size ).await?; }
Cheating Methods
Beyond standard Provider methods, CheatingProvider offers testing-specific capabilities.
Time Manipulation
#![allow(unused)] fn main() { // Get current timestamp let timestamp = provider.get_timestamp().await?; // Set specific timestamp provider.set_timestamp(new_timestamp).await?; // Increase time provider.increase_time(seconds).await?; }
Block Manipulation
#![allow(unused)] fn main() { // Mine a single block provider.mine_block().await?; // Mine multiple blocks provider.mine_blocks(count).await?; // Set block interval provider.set_block_interval(seconds).await?; }
Account Impersonation
#![allow(unused)] fn main() { // Start impersonating an address provider.start_prank(target_contract, impersonator).await?; // Make calls as the impersonator // All calls to target_contract will appear to come from impersonator // Stop impersonating provider.stop_prank(target_contract).await?; }
Balance Manipulation
#![allow(unused)] fn main() { // Set ETH balance provider.set_balance(address, amount).await?; // Deal tokens (if supported) provider.deal(token_address, recipient, amount).await?; }
Storage Manipulation
#![allow(unused)] fn main() { // Write to storage provider.store( contract_address, storage_key, value ).await?; // Read from storage let value = provider.load( contract_address, storage_key ).await?; }
Snapshots
#![allow(unused)] fn main() { // Create snapshot let snapshot_id = provider.snapshot().await?; // Make changes... contract.modify_state().await?; // Restore to snapshot provider.revert(snapshot_id).await?; }
Using with Accounts
The middleware integrates seamlessly with Starknet accounts.
Account Creation
#![allow(unused)] fn main() { use starknet::accounts::{Account, SingleOwnerAccount}; use starknet::signers::LocalWallet; // Create wallet let signer = LocalWallet::from(private_key); // Create account with the provider let account = SingleOwnerAccount::new( provider.clone(), signer, account_address, chain_id, ); }
Signing Transactions
#![allow(unused)] fn main() { use starknet::accounts::Call; // Prepare call let call = Call { to: contract_address, selector: get_selector_from_name("transfer")?, calldata: vec![recipient, amount_low, amount_high], }; // Execute through account let result = account.execute(vec![call]).send().await?; }
Using with Contract Bindings
The middleware works seamlessly with cainome-generated bindings.
Reading from Contracts
#![allow(unused)] fn main() { use starkbiter_bindings::erc_20_mintable_oz0::ERC20; // Create contract instance let token = ERC20::new(token_address, &account); // Read state (calls Provider methods) let balance = token.balance_of(address).await?; let total_supply = token.total_supply().await?; }
Writing to Contracts
#![allow(unused)] fn main() { // Prepare transaction let tx = token.transfer(recipient, amount); // Execute (uses Account methods) let result = tx.send().await?; // Wait for confirmation let receipt = result.wait_for_acceptance().await?; }
Connection Management
The middleware manages the underlying connection to Devnet.
Connection Configuration
#![allow(unused)] fn main() { use starkbiter_core::middleware::Connection; // Default configuration (automatic) let connection = Connection::new()?; // Custom port let connection = Connection::with_port(5050)?; // Custom URL let connection = Connection::with_url("http://localhost:5050")?; }
Health Checks
#![allow(unused)] fn main() { // Check if Devnet is responsive if provider.is_alive().await? { println!("Devnet is running"); } }
Error Handling
The middleware uses standard Starknet error types.
Common Errors
#![allow(unused)] fn main() { use starknet::providers::ProviderError; match provider.get_block_with_tx_hashes(1000000).await { Ok(block) => { // Process block } Err(ProviderError::StarknetError(e)) => { // Starknet-specific error eprintln!("Starknet error: {:?}", e); } Err(e) => { // Other errors eprintln!("Provider error: {:?}", e); } } }
Best Practices
#![allow(unused)] fn main() { // Use Result types async fn get_balance( provider: &CheatingProvider, address: Felt, ) -> Result<Felt> { let balance = provider.get_balance(address).await?; Ok(balance) } // Handle specific error cases async fn safe_get_block( provider: &CheatingProvider, block_num: u64, ) -> Option<Block> { match provider.get_block_with_tx_hashes(block_num).await { Ok(block) => Some(block), Err(e) => { log::warn!("Failed to get block {}: {}", block_num, e); None } } } }
Advanced Patterns
Concurrent Requests
#![allow(unused)] fn main() { use tokio::try_join; // Execute multiple queries in parallel let (balance, nonce, block) = try_join!( provider.get_balance(address), provider.get_nonce(BlockId::Pending, address), provider.block_number(), )?; }
Retry Logic
#![allow(unused)] fn main() { use tokio::time::{sleep, Duration}; async fn get_receipt_with_retry( provider: &CheatingProvider, tx_hash: Felt, max_retries: u32, ) -> Result<TransactionReceipt> { for attempt in 0..max_retries { match provider.get_transaction_receipt(tx_hash).await { Ok(receipt) => return Ok(receipt), Err(e) if attempt < max_retries - 1 => { sleep(Duration::from_millis(100)).await; continue; } Err(e) => return Err(e.into()), } } unreachable!() } }
Polling for Events
#![allow(unused)] fn main() { async fn watch_for_event( provider: &CheatingProvider, contract: Felt, event_key: Felt, ) -> Result<Event> { let mut last_block = provider.block_number().await?; loop { let current_block = provider.block_number().await?; if current_block > last_block { let events = provider.get_events( EventFilter { from_block: Some(BlockId::Number(last_block + 1)), to_block: Some(BlockId::Number(current_block)), address: Some(contract), keys: Some(vec![vec![event_key]]), }, None, 100, ).await?; if let Some(event) = events.events.first() { return Ok(event.clone()); } last_block = current_block; } sleep(Duration::from_millis(100)).await; } } }
Testing with Middleware
Unit Tests
#![allow(unused)] fn main() { #[cfg(test)] mod tests { use super::*; use starkbiter_core::environment::Environment; #[tokio::test] async fn test_provider_queries() { let env = Environment::builder().build().await.unwrap(); let provider = env.provider(); let block_num = provider.block_number().await.unwrap(); assert_eq!(block_num, 0); } } }
Integration Tests
#![allow(unused)] fn main() { #[tokio::test] async fn test_contract_interaction() { let env = Environment::builder().build().await?; let provider = env.provider(); let account = env.create_account().await?; // Deploy contract let contract = deploy_test_contract(&account).await?; // Interact through provider let call = Call { to: contract.address, selector: get_selector_from_name("set_value")?, calldata: vec![Felt::from(42u64)], }; account.execute(vec![call]).send().await?; // Verify let value = contract.get_value().await?; assert_eq!(value, Felt::from(42u64)); } }
Next Steps
- Environment - Deep dive into Environment
- Forking - State forking from live networks
- Usage Guide - Detailed middleware API