The Problem
There is a critical conceptual gap in the current Stylus documentation regarding the execution context of Rust code. The documentation fails to distinguish between the On-chain Context (Contract Logic) and the Off-chain Context (Integration Tests and Tooling), leading to silent failures in test suites.
Senior Diagnostic: The Contrast
On-chain (Synchronous WASM): Within the #[entrypoint], execution is strictly synchronous and deterministic. The WASM runtime lacks an executor (like tokio), making async/await impossible.
Off-chain (Asynchronous Testing): When developers write integration tests or deployment scripts using the stylus-sdk, they are interacting with the blockchain via RPC (often through ethers-rs). This environment is asynchronous.
Irrefutable Proof (Code Inconsistency)
Developers following the current guide often fall into this trap where types appear correct but logic never executes:
// ✅ WHAT DOCS SHOW (Correct for on-chain contracts)
#[entrypoint]
fn set_number(&mut self, x: U256) -> Result<(), Vec> {
self.number.set(x); // Sync - no async/await
Ok(())
}
// ❌ WHAT DEVELOPERS TRY (Fails silently in tests)
#[test]
fn test_counter() {
let contract = Counter::new(addr, provider);
let value = contract.number().call(); // Returns a Future, NOT U256
assert_eq!(value, U256::from(42)); // ❌ Compilation error or invalid comparison
}
Real-World Impact
This ambiguity causes significant friction for teams onboarding to Stylus. A lack of clarity on the requirement for an async runtime in tests leads to hours of debugging "silent failures" where assertions are never polled. This represents a measurable productivity loss for protocol developers migrating from EVM-native tools.
Proposed Resolution (MVP)
I propose adding a dedicated section to /stylus/reference/rust-sdk-guide explaining the Sync/Async boundary and providing the standard boilerplate for integration tests:
// ✅ Correct Integration Test (Off-chain)
#[cfg(test)]
mod integration_tests {
use stylus_sdk::prelude::;
use ethers::prelude::;
#[tokio::test] // ⚠️ CRITICAL: Tokio runtime required for off-chain calls
async fn test_async_interaction() -> Result<(), Box<dyn std::error::Error>> {
let provider = Provider::<Http>::try_from("http://localhost:8547")?;
let client = std::sync::Arc::new(provider);
let contract = Counter::new(address, client);
let current = contract.number().call().await?; // Use .await here
assert!(current >= U256::zero());
Ok(())
}
}
The Rule of Thumb:
Contract methods: fn (Sync)
Test/Script methods: async fn + #[tokio::test] (Async)
The Problem
There is a critical conceptual gap in the current Stylus documentation regarding the execution context of Rust code. The documentation fails to distinguish between the On-chain Context (Contract Logic) and the Off-chain Context (Integration Tests and Tooling), leading to silent failures in test suites.
Senior Diagnostic: The Contrast
On-chain (Synchronous WASM): Within the #[entrypoint], execution is strictly synchronous and deterministic. The WASM runtime lacks an executor (like tokio), making async/await impossible.
Off-chain (Asynchronous Testing): When developers write integration tests or deployment scripts using the stylus-sdk, they are interacting with the blockchain via RPC (often through ethers-rs). This environment is asynchronous.
Irrefutable Proof (Code Inconsistency)
Developers following the current guide often fall into this trap where types appear correct but logic never executes:
// ✅ WHAT DOCS SHOW (Correct for on-chain contracts)
#[entrypoint]
fn set_number(&mut self, x: U256) -> Result<(), Vec> {
self.number.set(x); // Sync - no async/await
Ok(())
}
// ❌ WHAT DEVELOPERS TRY (Fails silently in tests)
#[test]
fn test_counter() {
let contract = Counter::new(addr, provider);
let value = contract.number().call(); // Returns a Future, NOT U256
assert_eq!(value, U256::from(42)); // ❌ Compilation error or invalid comparison
}
Real-World Impact
This ambiguity causes significant friction for teams onboarding to Stylus. A lack of clarity on the requirement for an async runtime in tests leads to hours of debugging "silent failures" where assertions are never polled. This represents a measurable productivity loss for protocol developers migrating from EVM-native tools.
Proposed Resolution (MVP)
I propose adding a dedicated section to /stylus/reference/rust-sdk-guide explaining the Sync/Async boundary and providing the standard boilerplate for integration tests:
// ✅ Correct Integration Test (Off-chain)
#[cfg(test)]
mod integration_tests {
use stylus_sdk::prelude::;
use ethers::prelude::;
}
The Rule of Thumb:
Contract methods: fn (Sync)
Test/Script methods: async fn + #[tokio::test] (Async)