Skip to main content
Sync vs. async, local vs. remote, and how entity boundaries change which one you can use. When synchronization is needed across entities When components live on different entities, calls become asynchronous — the entity yields its lock and waits for a reply.

Step 1 · Add a sync transfer (Guided)

This is just transfer(), but:
  • Not async
  • Uses local instead of remote
  • 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}")
);
Calling the new function (still both holders on the same entity → passes):
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");
Single account: both holders on the same entity Single-account setup: both holders are installed on the same entity — sync calls work.

Force the failure: put holders on different entities

  1. 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}"));
  1. 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");
Multi-account: holders on separate entities Multi-account setup: each holder lives on its own entity — sync (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.
simple-token/src/simple_token_contracts.rs
use 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):
cd my-token
git stash
git checkout v0.3.2
Solution tag:
git reset --hard HEAD
git checkout 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(())
    }
}