Environment
The Environment is the core abstraction in Starkbiter, representing a sandboxed Starknet instance. It provides complete control over blockchain state, block production, and contract interaction.
Overview
An Environment wraps a Starknet Devnet instance, giving you:
- Full JSON-RPC capabilities
- Additional testing methods (cheating methods)
- State management and control
- Account creation and management
Think of it as your personal Starknet network that you have complete control over.
Creating an Environment
Basic Setup
use starkbiter_core::environment::Environment; use starknet::core::types::Felt; #[tokio::main] async fn main() -> anyhow::Result<()> { let env = Environment::builder() .with_chain_id(Felt::from_hex("0x534e5f5345504f4c4941")?) .build() .await?; println!("Environment ready!"); Ok(()) }
Builder Pattern
The EnvironmentBuilder provides a fluent API for configuration:
#![allow(unused)] fn main() { let env = Environment::builder() .with_chain_id(chain_id) .with_gas_price(100_000_000_000) // 100 gwei .with_block_time(10) // 10 seconds per block .build() .await?; }
Configuration Options
Chain ID
Specify which network to simulate:
#![allow(unused)] fn main() { // Starknet Mainnet let mainnet_id = Felt::from_hex("0x534e5f4d41494e")?; // Starknet Sepolia Testnet let sepolia_id = Felt::from_hex("0x534e5f5345504f4c4941")?; let env = Environment::builder() .with_chain_id(sepolia_id) .build() .await?; }
Block Time
Control block production:
#![allow(unused)] fn main() { // Automatic block production every 5 seconds let env = Environment::builder() .with_block_time(5) .build() .await?; // Manual block production let env = Environment::builder() .with_block_time(0) // 0 = manual mode .build() .await?; }
Gas Configuration
Set gas prices:
#![allow(unused)] fn main() { let env = Environment::builder() .with_gas_price(50_000_000_000) // 50 gwei .build() .await?; }
State Management
Block Production
Control when blocks are produced:
#![allow(unused)] fn main() { // Manual block production env.mine_block().await?; // Mine multiple blocks env.mine_blocks(10).await?; // Get current block number let block_num = env.get_block_number().await?; println!("Current block: {}", block_num); }
Time Manipulation
Control blockchain time:
#![allow(unused)] fn main() { // Increase time by 1 hour env.increase_time(3600).await?; // Set specific timestamp env.set_timestamp(1234567890).await?; // Get current timestamp let timestamp = env.get_timestamp().await?; }
State Snapshots
Save and restore state:
#![allow(unused)] fn main() { // Take a snapshot let snapshot_id = env.snapshot().await?; // Make some changes contract.do_something().await?; // Restore to snapshot env.restore(snapshot_id).await?; }
Account Management
Creating Accounts
#![allow(unused)] fn main() { use starknet::core::types::Felt; // Create with random keys let account = env.create_account().await?; // Create with specific keys let private_key = Felt::from_hex("0x123...")? ; let account_address = Felt::from_hex("0x456...")?; let account = env.create_single_owner_account( private_key, account_address ).await?; }
Predeployed Accounts
Devnet comes with predeployed accounts for testing:
#![allow(unused)] fn main() { // Get predeployed accounts let accounts = env.get_predeployed_accounts().await?; for account in accounts { println!("Address: {:#x}", account.address); println!("Private Key: {:#x}", account.private_key); } }
Contract Management
Declaring Contracts
Before deploying, contracts must be declared:
#![allow(unused)] fn main() { use std::fs; // Read contract JSON let contract_json = fs::read_to_string("path/to/contract.json")?; // Declare the contract let class_hash = env.declare_contract( &account, contract_json ).await?; println!("Contract declared: {:#x}", class_hash); }
Deploying Contracts
#![allow(unused)] fn main() { use starknet::core::types::Felt; // Deploy with constructor args let constructor_args = vec![ Felt::from(1000u64), // Initial supply Felt::from_hex("0x...")?, // Owner address ]; let contract_address = env.deploy_contract( &account, class_hash, constructor_args ).await?; println!("Contract deployed at: {:#x}", contract_address); }
Using Bindings
With cainome-generated bindings:
#![allow(unused)] fn main() { use starkbiter_bindings::erc_20_mintable_oz0::ERC20; // Deploy using binding let erc20 = ERC20::deploy( &account, name, symbol, decimals, initial_supply, recipient ).await?; // Interact with contract let balance = erc20.balance_of(address).await?; }
Querying State
Block Information
#![allow(unused)] fn main() { // Get latest block let block = env.get_block_latest().await?; println!("Block number: {}", block.block_number); println!("Timestamp: {}", block.timestamp); // Get specific block let block = env.get_block_by_number(100).await?; }
Transaction Information
#![allow(unused)] fn main() { // Get transaction by hash let tx = env.get_transaction(tx_hash).await?; // Get transaction receipt let receipt = env.get_transaction_receipt(tx_hash).await?; // Get transaction status let status = env.get_transaction_status(tx_hash).await?; }
Contract State
#![allow(unused)] fn main() { // Get contract storage let storage_value = env.get_storage_at( contract_address, storage_key ).await?; // Get contract nonce let nonce = env.get_nonce(contract_address).await?; // Get contract class let contract_class = env.get_class_at(contract_address).await?; }
Event Handling
Polling for Events
#![allow(unused)] fn main() { // Get events from latest block let events = env.get_events( from_block, to_block, contract_address, keys ).await?; for event in events { println!("Event: {:?}", event); } }
Event Filtering
#![allow(unused)] fn main() { use starknet::core::types::EventFilter; let filter = EventFilter { from_block: Some(0), to_block: Some(100), address: Some(contract_address), keys: Some(vec![event_key]), }; let events = env.get_events_filtered(filter).await?; }
Forking
Fork from live networks:
#![allow(unused)] fn main() { use url::Url; use std::str::FromStr; let env = Environment::builder() .with_chain_id(mainnet_id) .with_fork( Url::from_str("https://starknet-mainnet.public.blastapi.io")?, 12345, // Block number Some(Felt::from_hex("0xblock_hash")?), ) .build() .await?; }
See Forking for more details.
Cheating Methods
Starkbiter provides additional testing methods:
Impersonation
#![allow(unused)] fn main() { // Impersonate an address env.start_prank(target_address, impersonator_address).await?; // Make calls as the impersonator contract.privileged_function().await?; // Stop impersonating env.stop_prank(target_address).await?; }
Balance Manipulation
#![allow(unused)] fn main() { // Set ETH balance env.set_balance(address, amount).await?; // Get balance let balance = env.get_balance(address).await?; }
Storage Manipulation
#![allow(unused)] fn main() { // Write directly to storage env.store( contract_address, storage_key, value ).await?; // Load from storage let value = env.load(contract_address, storage_key).await?; }
Cleanup and Shutdown
Environments are automatically cleaned up when dropped:
#![allow(unused)] fn main() { { let env = Environment::builder().build().await?; // Use env } // env is dropped, resources cleaned up }
Explicit shutdown:
#![allow(unused)] fn main() { env.shutdown().await?; }
Best Practices
1. Use Builder Pattern
Always use the builder for consistent configuration:
#![allow(unused)] fn main() { let env = Environment::builder() .with_chain_id(chain_id) .build() .await?; }
2. Error Handling
Always handle environment errors:
#![allow(unused)] fn main() { match env.get_block_number().await { Ok(block) => println!("Block: {}", block), Err(e) => eprintln!("Error: {}", e), } }
3. Resource Management
Create environments in appropriate scopes:
#![allow(unused)] fn main() { #[tokio::test] async fn test_contract() -> Result<()> { let env = Environment::builder().build().await?; // Test code Ok(()) } // Environment cleaned up automatically }
4. Snapshots for Testing
Use snapshots to isolate test cases:
#![allow(unused)] fn main() { let snapshot = env.snapshot().await?; // Test case 1 run_test_1(&env).await?; env.restore(snapshot).await?; // Test case 2 run_test_2(&env).await?; env.restore(snapshot).await?; }
Common Patterns
Deploy and Initialize
#![allow(unused)] fn main() { async fn deploy_and_initialize(env: &Environment) -> Result<ContractAddress> { let account = env.create_account().await?; // Declare let class_hash = env.declare_contract(&account, contract_json).await?; // Deploy let address = env.deploy_contract(&account, class_hash, vec![]).await?; // Initialize let contract = MyContract::new(address, &account); contract.initialize().await?; Ok(address) } }
Time-Based Testing
#![allow(unused)] fn main() { async fn test_time_lock(env: &Environment) -> Result<()> { let contract = deploy_timelock(&env).await?; // Try before time lock expires (should fail) assert!(contract.withdraw().await.is_err()); // Fast forward env.increase_time(86400).await?; // +24 hours env.mine_block().await?; // Now should succeed contract.withdraw().await?; Ok(()) } }
Next Steps
- Middleware - Understanding the middleware layer
- Forking - State forking from live networks
- Usage Guide - Detailed API reference