panic! doesn’t save Alice,
and how Result::Err lets you compensate.
Why doesn’t panic help?
Definition —
panic! is a state where an activation cannot safely
continue. It must be aborted and its effects reverted on a best-effort basis
(gas cost not included).panic alone doesn’t solve cross-activation atomicity.
Result::Err to the rescue
Definition —
Result::Err is a state in which the activation did not
produce the desired result. Developers must handle it as they see fit. Nothing
is reverted automatically.token-order is expected to catch the error returned by the
NFT transfer, then either retry it or refund the USDTs.
Step 1 · Error handling in simple-token (Guided)
Custom errors
Insimple-token/simple-token/src/simple_token_errors.rs:
#[error_type]
pub enum SimpleTokenError {
#[error("Placeholder error - replace with your contract-specific errors")]
Placeholder,
#[error("Overflow error - the operation resulted in an overflow")]
Overflow,
#[error("Revert failed - the operation failed to revert")]
RevertFailed,
#[error("Failed to receive funds")]
FailedToReceiveFunds,
#[error("Insufficient funds")]
InsufficientFunds,
#[error("Amount must be greater than zero")]
AmountMustBeGreaterThanZero { amount: AmountInSubunits },
#[error("Transfer failed with refund error")]
TransferFailedWithRefundError {
delivery_error: String,
refund_error: String,
},
}
FailedToReceiveFunds in receive()
#[private]
pub fn receive(&mut self, amount: AmountInSubunits) -> Result<()> {
self.wallet_balance = self.wallet_balance
.checked_add(amount)
.ok_or(SimpleTokenError::FailedToReceiveFunds)?;
Ok(())
}
InsufficientFunds in transfer() and transfer_same_entity()
ensure!(amount <= self.wallet_balance, SimpleTokenError::InsufficientFunds);
self.wallet_balance = self.wallet_balance
.checked_sub(amount)
.ok_or(SimpleTokenError::InsufficientFunds)?;
AmountMustBeGreaterThanZero in transfer() and transfer_same_entity()
ensure!(amount > 0, SimpleTokenError::AmountMustBeGreaterThanZero { amount });
Transfer / delivery errors
Sync path: transfer_same_entity()
let result = recipient.local.receive(amount);
if let Err(delivery_error) = result {
return self.handle_transfer_failure(delivery_error, amount);
}
#[private]
pub fn handle_transfer_failure(
&mut self,
delivery_error: GvmError,
amount: AmountInSubunits,
) -> Result<()> {
if let Some(refund_error) = self.wallet_balance.checked_add(amount) {
return Err((SimpleTokenError::TransferFailedWithRefundError {
delivery_error: delivery_error.to_string(),
refund_error: refund_error.to_string(),
}).into());
}
Err(delivery_error.into())
}
Async path: transfer() — await or spawn
- .await — handle result inline
- .spawn — fire-and-forget
.await — wait for the result, handle it within the current activation’s
lifecycle.let receive_future = recipient.remote.receive(amount).await?;
if let Err(delivery_error) = receive_future.await {
return self.handle_receive_failure(delivery_error, amount);
}
.spawn() — fire-and-forget. Sent at end of activation; errors handled
via callbacks.let receive_future = recipient.remote.receive(amount);
receive_future
.spawn()
.on_error(Self::callbacks().handle_transfer_failure);
#[owner]
pub async fn transfer(
&mut self,
amount: AmountInSubunits,
recipient: Holder,
with_confirmation: bool,
) -> Result<()> {
ensure!(amount > 0, SimpleTokenError::AmountMustBeGreaterThanZero { amount });
ensure!(amount <= self.wallet_balance, SimpleTokenError::InsufficientFunds);
self.wallet_balance = self.wallet_balance
.checked_sub(amount)
.ok_or(SimpleTokenError::InsufficientFunds)?;
let receive_future = recipient.remote.receive(amount);
if with_confirmation {
if let Err(delivery_error) = receive_future.await {
return self.handle_transfer_failure(delivery_error, amount);
}
} else {
receive_future.spawn().on_error(Self::callbacks().handle_transfer_failure);
}
Ok(())
}
Testing
Add the newwith_confirmation argument:
holder_client
.transfer(
AmountInSubunits::new(1),
Holder::new(*holder2_client.component_id()),
true,
)
.expect("transfer payload build should succeed")
.execute(&gen_client, &holder_signer).await
.expect("transfer should succeed");
AmountMustBeGreaterThanZero:
ActivationResult(ExecutionFailed { error: ExecutionError(GvmErrorDetails {
code: 1005, name: "AmountMustBeGreaterThanZero", msg: "Amount must be greater than zero" ...
}) })
InsufficientFunds:
ActivationResult(ExecutionFailed { error: ExecutionError(GvmErrorDetails {
code: 1004, name: "InsufficientFunds", msg: "Insufficient funds" ...
}) })
Checkpoint — happy-path test passes; 0-amount and over-balance variants
fail with the expected named errors.
Full solution: simple-token at the end of Step 1
Full solution: simple-token at the end of Step 1
simple-token/src/simple_token_contracts.rsuse genie::contract;
/// Implementation of the SimpleToken contract
#[contract]
pub mod simple_token {
/// Root component for SimpleToken
pub mod root {
use genie::{deploy, installation_request, owner, view, Result};
/// Root implementation for SimpleToken
pub struct SimpleTokenRoot {
/// Stored value for testing state changes
value: u64,
}
impl SimpleTokenRoot {
/// Handles installation requests for SimpleToken components.
#[installation_request]
pub async fn installation_request(
&mut self,
_component_type: SimpleTokenComponents,
) -> Result<()> {
Ok(())
}
/// Deploys the SimpleToken contract
#[deploy]
pub async fn deploy() -> Result<Self> {
Ok(Self { value: 0 })
}
/// Sets the stored value (owner-only activation)
#[owner]
pub fn set_value(&mut self, new_value: u64) -> Result<()> {
self.value = new_value;
Ok(())
}
/// Gets the stored value (view function)
#[view]
pub fn get_value(&self) -> Result<u64> {
Ok(self.value)
}
}
}
/// Holder component for SimpleToken
pub mod holder {
use crate::simple_token_errors::SimpleTokenError;
use alloc::string::ToString;
use genie::GvmError;
use genie::{AmountInSubunits, Result, ensure, install, owner, private, view};
use super::self_refs::Holder;
pub struct SimpleTokenHolder {
pub wallet_balance: AmountInSubunits,
}
impl SimpleTokenHolder {
#[owner]
pub fn transfer_same_entity(
&mut self,
amount: AmountInSubunits,
recipient: Holder,
) -> Result<()> {
ensure!(
amount > 0,
SimpleTokenError::AmountMustBeGreaterThanZero { amount }
);
ensure!(
amount <= self.wallet_balance,
SimpleTokenError::InsufficientFunds
);
self.wallet_balance = self
.wallet_balance
.checked_sub(amount)
.ok_or(SimpleTokenError::InsufficientFunds)?;
let result = recipient.local.receive(amount);
if let Err(delivery_error) = result {
return self.handle_transfer_failure(delivery_error, amount);
}
Ok(())
}
#[owner]
pub async fn transfer(
&mut self,
amount: AmountInSubunits,
recipient: Holder,
with_confirmation: bool,
) -> Result<()> {
ensure!(
amount > 0,
SimpleTokenError::AmountMustBeGreaterThanZero { amount }
);
ensure!(
amount <= self.wallet_balance,
SimpleTokenError::InsufficientFunds
);
self.wallet_balance = self
.wallet_balance
.checked_sub(amount)
.ok_or(SimpleTokenError::InsufficientFunds)?;
let receive_future = recipient.remote.receive(amount);
if with_confirmation {
if let Err(delivery_error) = receive_future.await {
return self.handle_transfer_failure(delivery_error, amount);
}
} else {
receive_future.spawn().on_error(Self::callbacks().handle_transfer_failure);
}
Ok(())
}
#[private]
pub fn handle_transfer_failure(
&mut self,
delivery_error: GvmError,
amount: AmountInSubunits,
) -> Result<()> {
if let Some(refund_error) = self.wallet_balance.checked_add(amount) {
return Err(
(SimpleTokenError::TransferFailedWithRefundError {
delivery_error: delivery_error.to_string(),
refund_error: refund_error.to_string(),
})
.into(),
);
}
Err(delivery_error.into())
}
#[private]
pub fn receive(&mut self, amount: AmountInSubunits) -> Result<()> {
self.wallet_balance = self
.wallet_balance
.checked_add(amount)
.ok_or(SimpleTokenError::FailedToReceiveFunds)?;
Ok(())
}
#[view]
pub fn get_balance(&self) -> Result<AmountInSubunits> {
Ok(self.wallet_balance)
}
#[install]
pub async fn install(_installation_return_value: ()) -> Result<Self> {
Ok(SimpleTokenHolder {
wallet_balance: AmountInSubunits::new(12), // example starting balance
})
}
}
}
}
simple-token/src/simple_token_errors.rsuse alloc::string::{String, ToString};
use genie::{AmountInSubunits, error_type};
/// Possible errors for SimpleToken operations.
///
/// Extend this enum with your contract-specific errors.
#[error_type]
pub enum SimpleTokenError {
#[error("Placeholder error - replace with your contract-specific errors")]
Placeholder,
#[error("Overflow error - the operation resulted in an overflow")]
Overflow,
#[error("Revert failed - the operation failed to revert")]
RevertFailed,
#[error("Failed to receive funds")]
FailedToReceiveFunds,
#[error("Insufficient funds")]
InsufficientFunds,
#[error("Amount must be greater than zero")]
AmountMustBeGreaterThanZero { amount: AmountInSubunits },
#[error("Transfer failed with refund error")]
TransferFailedWithRefundError {
delivery_error: String,
refund_error: String,
},
}
simple-token-tests/src/lib.rs//! SimpleToken Contract Test Crate
//!
//! This crate contains integration tests for the SimpleToken contract.
use client_sdk_macros::generate_contract_client;
mod env_utils;
/// Contract client generated from the ABI.
/// This module is generated by the `#[generate_contract_client]` macro.
#[generate_contract_client("../simple-token/artifacts/simple_token_modules.json")]
mod simple_token_contract {}
#[cfg(test)]
mod tests {
use super::env_utils::LOCAL_RUN_INSTRUCTIONS;
use super::simple_token_contract;
use gen_test_tools::{TestContext, test_context};
use rstest::rstest;
use crate::simple_token_contract::self_refs::Holder;
use client_sdk::{ActivationOptions, ActivationTag, types::AmountInSubunits};
/// End-to-end test for the SimpleToken contract.
///
/// This test verifies:
/// - Contract code can be pushed to the validator
/// - Contract can be deployed successfully
/// - Root component is installed during deployment
/// - View function (get_value) returns initial value
/// - Activation function (set_value) changes state
/// - View function returns updated value after activation
#[rstest]
#[tokio::test]
async fn simple_token_e2e_test(#[future] test_context: TestContext) {
super::env_utils::assert_gen_cli_available();
let ctx = test_context.await;
let deployer_signer = ctx.new_signer();
let deployer_account = deployer_signer.account();
let holder_signer = ctx.new_signer();
let holder_account = holder_signer.account();
let holder2_signer = ctx.new_signer();
let holder2_account = holder2_signer.account();
ctx.create_accounts(&[deployer_account, holder_account, holder2_account])
.await.unwrap_or_else(|e| panic!("create_accounts failed: {e}{LOCAL_RUN_INSTRUCTIONS}"));
let gen_client = ctx
.gen_client()
.unwrap_or_else(|e| panic!("gen client creation failed: {e}{LOCAL_RUN_INSTRUCTIONS}"));
// Push contract code
let push_id = simple_token_contract::push_contract(
deployer_account,
tokio::fs::read,
)
.await
.unwrap_or_else(|e| panic!("contract push payload build failed: {e}{LOCAL_RUN_INSTRUCTIONS}"))
.execute(&gen_client, &deployer_signer)
.await
.unwrap_or_else(|e| panic!("contract push failed: {e}{LOCAL_RUN_INSTRUCTIONS}"));
// Deploy contract
let root = simple_token_contract::deploy_contract(
deployer_account,
push_id,
// TODO: Add your deploy parameters here
)
.unwrap_or_else(|e| panic!("contract deploy payload build failed: {e}{LOCAL_RUN_INSTRUCTIONS}"))
.execute(&gen_client, &deployer_signer)
.await
.unwrap_or_else(|e| panic!("contract deploy failed: {e}{LOCAL_RUN_INSTRUCTIONS}"));
let _root_contract = root.gvm_contract();
let holder_client = simple_token_contract
::install_holder(holder_account, _root_contract)
.expect("holder installation payload build should succeed")
.execute(&gen_client, &holder_signer).await
.expect("holder installation should succeed");
let holder2_client = simple_token_contract
::install_holder(holder2_account, _root_contract)
.expect("holder2 installation payload build should succeed")
.execute_with_options(
&gen_client,
&holder2_signer,
&ActivationOptions::builder().tag(ActivationTag::new(2)).build()
).await
.expect("holder2 installation should succeed");
holder_client
.transfer(
AmountInSubunits::new(1),
Holder::new(*holder2_client.component_id()),
true,
)
.expect("transfer payload build should succeed")
.execute(&gen_client, &holder_signer).await
.expect("transfer should succeed");
let balance = holder2_client
.get_balance(deployer_account)
.expect("get_balance view payload build should succeed")
.execute(&gen_client).await
.expect("get_balance view should succeed");
assert!(
balance == AmountInSubunits::new(13),
"Receiver balance mismatch: expected {}, got {}",
AmountInSubunits::new(13),
balance
);
// Test view function - should return initial value of 0
let value = root
.get_value(deployer_account)
.expect("get_value view payload build should succeed")
.execute(&gen_client)
.await
.expect("get_value view should succeed");
assert_eq!(value, 0, "Initial value should be 0");
// Test activation function - set a new value
root.set_value(42)
.expect("set_value activation payload build should succeed")
.execute(&gen_client, &deployer_signer)
.await
.expect("set_value activation should complete successfully");
// Verify the value was changed using view function
let value = root
.get_value(deployer_account)
.expect("get_value view payload build should succeed after set_value")
.execute(&gen_client)
.await
.expect("get_value view should succeed after set_value");
assert_eq!(value, 42, "Value should be updated to 42");
// TODO: Add your test assertions here
}
}
Step 2 · Error handling in my-token (Exercise)
Add the same error-handling logic to incrementer and receiver. The tests
in my-token-tests/src/lib.rs tell you which errors are expected for which
inputs.
Starter and solution — Check out the starting tag (stash first):Solution tag:
cd my-token
git stash
git checkout v0.5.3
git reset --hard HEAD
git checkout v0.6.2
- Starter (v0.5.3)
- Solution sketch (v0.6.2)
my-token/src/my_token_errors.rs// my-token @ v0.5.3
#[error_type]
pub enum MyTokenError {
#[error("Overflow")] Overflow,
// TODO: add error variants that match the expectations in the tests
}
my-token/src/my_token_contracts.rs// my-token @ v0.5.3
pub mod incrementer {
use super::self_refs::Receiver;
use crate::my_token_errors::MyTokenError;
use genie::{Result, install, owner};
pub struct Incrementer;
impl Incrementer {
#[owner]
pub fn increment_same_entity(&mut self, to: Receiver) -> Result<()> {
// TODO: call to.local.receive(); on failure, route the delivery
// error through a private handle_increment_failure() that
// returns the right MyTokenError variant.
to.local.receive()?;
Ok(())
}
#[owner]
pub async fn increment(&mut self, to: Receiver) -> Result<()> {
// TODO: add a `with_confirmation: bool` parameter, then either
// `.await` the result and recover, or `.spawn()` and
// attach `.on_error(...)` to a private callback.
to.remote.receive().await?;
Ok(())
}
#[install]
pub async fn install(_iv: ()) -> Result<Self> { Ok(Self) }
}
}
pub mod receiver {
use crate::my_token_errors::MyTokenError;
use genie::{Result, install, private, view};
pub struct Receiver { counter: u128 }
impl Receiver {
#[private]
pub fn receive(&mut self) -> Result<()> {
// TODO: guard the checked_add with the right MyTokenError variant.
self.counter = self.counter.checked_add(1).ok_or(MyTokenError::Overflow)?;
Ok(())
}
#[view]
pub fn get_counter(&self) -> Result<u128> { Ok(self.counter) }
#[install]
pub async fn install(_iv: ()) -> Result<Self> { Ok(Self { counter: 0 }) }
}
}
my-token/src/my_token_errors.rs// my-token @ v0.6.2
#[error_type]
pub enum MyTokenError {
#[error("Overflow")] Overflow,
#[error("Failed to receive increment")] FailedToReceive,
#[error("Receiver missing")] ReceiverMissing,
#[error("Increment failed with refund error")]
IncrementFailedWithRefund { delivery_error: String, refund_error: String },
}
my-token/src/my_token_contracts.rs// my-token @ v0.6.2
pub mod incrementer {
use super::self_refs::Receiver;
use crate::my_token_errors::MyTokenError;
use alloc::string::ToString;
use genie::GvmError;
use genie::{Result, install, owner, private};
pub struct Incrementer;
impl Incrementer {
#[owner]
pub fn increment_same_entity(&mut self, to: Receiver) -> Result<()> {
if let Err(delivery_error) = to.local.receive() {
return self.handle_increment_failure(delivery_error);
}
Ok(())
}
#[owner]
pub async fn increment(
&mut self,
to: Receiver,
with_confirmation: bool,
) -> Result<()> {
let receive_future = to.remote.receive();
if with_confirmation {
if let Err(delivery_error) = receive_future.await {
return self.handle_increment_failure(delivery_error);
}
} else {
receive_future
.spawn()
.on_error(Self::callbacks().handle_increment_failure);
}
Ok(())
}
#[private]
pub fn handle_increment_failure(
&mut self,
delivery_error: GvmError,
) -> Result<()> {
Err((MyTokenError::IncrementFailedWithRefund {
delivery_error: delivery_error.to_string(),
refund_error: "no compensating state to refund".to_string(),
})
.into())
}
#[install]
pub async fn install(_iv: ()) -> Result<Self> { Ok(Self) }
}
}
pub mod receiver {
use crate::my_token_errors::MyTokenError;
use genie::{Result, install, private, view};
pub struct Receiver { counter: u128 }
impl Receiver {
#[private]
pub fn receive(&mut self) -> Result<()> {
self.counter = self
.counter
.checked_add(1)
.ok_or(MyTokenError::Overflow)?;
Ok(())
}
#[view]
pub fn get_counter(&self) -> Result<u128> { Ok(self.counter) }
#[install]
pub async fn install(_iv: ()) -> Result<Self> { Ok(Self { counter: 0 }) }
}
}

