local vs. remote, and how entity boundaries change which
one you can use.
Step 1 · Add a sync transfer (Guided)
This is justtransfer(), but:
- Not
async - Uses
localinstead ofremote - No
.await?
#[owner]
pub fn transfer_same_entity(
&mut self,
amount: AmountInSubunits,
recipient: Holder,
) -> Result<()> {
self.wallet_balance = self.wallet_balance
.checked_sub(amount)
.ok_or(SimpleTokenError::Overflow)?;
recipient.local.receive(amount);
Ok(())
}
local vs. remote
.local
Groups all sync functions. Only works between components installed on the
same entity. Immediate result, no entity-lock yield.
.remote
Groups all async functions. Works for inter-component calls regardless
of entity. Yields entity lock and produces a deferred result — mainly useful
for cross-entity calls.
Testing a sync function
Single-account setup (what you had so far)
let deployer_signer = ctx.new_signer();
let deployer_account = deployer_signer.account();
ctx.create_accounts(&[deployer_account]).await.unwrap_or_else(|e|
panic!("create_accounts failed: {e}{LOCAL_RUN_INSTRUCTIONS}")
);
holder_client
.transfer_same_entity(
AmountInSubunits::new(1),
Holder::new(*holder2_client.component_id())
)
.expect("transfer should succeed")
.execute(&gen_client, &deployer_signer).await
.expect("transfer should succeed");
Force the failure: put holders on different entities
- Add a second and third signer/account:
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}"));
- Install each holder under its own account:
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");
local) calls between them fail.
Now the same test fails — different entities can’t talk over local:
thread 'tests::simple_token_e2e_test' panicked at simple-token-tests/src/lib.rs:111:9:
Receiver balance mismatch: expected 13, got 12
Checkpoint — you understand why
local works only within a single entity
and when to reach for remote.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::{Result, deploy, installation_request, owner, view};
/// 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)
}
}
}
/// Component1 component for SimpleToken
pub mod /*component_1*/ holder {
use crate::simple_token_errors::SimpleTokenError;
use genie::{AmountInSubunits, Result, install, owner, private, view};
use super::self_refs::Holder;
pub struct SimpleTokenHolder {
pub wallet_balance: AmountInSubunits,
}
impl /*Component1*/ SimpleTokenHolder {
#[owner]
pub fn transfer_same_entity(
&mut self,
amount: AmountInSubunits,
recipient: Holder,
) -> Result<()> {
self.wallet_balance = self.wallet_balance
.checked_sub(amount)
.ok_or(SimpleTokenError::Overflow)?;
recipient.local.receive(amount);
Ok(())
}
#[owner]
pub async fn transfer(
&mut self,
amount: AmountInSubunits,
recipient: Holder,
) -> Result<()> {
self.wallet_balance = self.wallet_balance
.checked_sub(amount)
.ok_or(SimpleTokenError::Overflow)?;
recipient.remote.receive(amount).await?;
Ok(())
}
#[private]
pub fn receive(&mut self, amount: AmountInSubunits) -> Result<()> {
self.wallet_balance = self.wallet_balance
.checked_add(amount)
.ok_or(SimpleTokenError::Overflow)?;
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-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_same_entity(
AmountInSubunits::new(1),
Holder::new(*holder2_client.component_id())
)
.expect("transfer should succeed")
.execute(&gen_client, &deployer_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 · Sync in my-token (Exercise)
Add increment_local() to the incrementer component, mirroring what we just
did for simple-token. Make the supplied tests in my-token-tests/src/lib.rs
pass.
Starter and solution — Check out the starting tag (stash first):Solution tag:
cd my-token
git stash
git checkout v0.3.2
git reset --hard HEAD
git checkout v0.4.2
- Starter (v0.3.2)
- Solution (v0.4.2)
// my-token @ v0.3.2 — my-token/src/my_token_contracts.rs
impl Incrementer {
// TODO: add increment_local(&mut self, to: Receiver) -> Result<()>
// - sync, uses .local
#[owner]
pub async fn increment(&mut self, to: Receiver) -> Result<()> {
to.remote.receive().await?;
Ok(())
}
}
// my-token @ v0.4.2 — my-token/src/my_token_contracts.rs
impl Incrementer {
#[owner]
pub fn increment_local(&mut self, to: Receiver) -> Result<()> {
to.local.receive()?;
Ok(())
}
#[owner]
pub async fn increment(&mut self, to: Receiver) -> Result<()> {
to.remote.receive().await?;
Ok(())
}
}

