Skip to main content
Build the simple-token holder: wallet balance, transfer, receive, and the access modifiers that make inter-component calls safe.

Step 1 · The simple-token Holder (Guided)

GEN ships a fungible-token reference example, but it’s heavy for an intro. We’ll build a stripped-down version: no global events, minting, burning, or inventory — just transfers between holders.

Rename component_1holder

pub mod /*component_1*/ holder {
    pub struct /*Component1*/ SimpleTokenHolder;

    impl /*Component1*/ SimpleTokenHolder {
        #[install]
        pub async fn install(_installation_return_value: ()) -> Result<Self> {
            Ok(Self)
        }
    }
}

Add the two functions

Observing a transaction, you see two parties — so you need two functions, both on the same holder:
  • A function that transfers an amount of tokens to a recipient
  • A function that handles receiving tokens from a sender
impl SimpleTokenHolder {
    pub async fn transfer(
        &mut self,
        amount: AmountInSubunits,
        recipient: Holder,
    ) -> Result<()> {
        Ok(())
    }

    pub fn receive(&mut self, amount: AmountInSubunits) -> Result<()> {
        Ok(())
    }

    #[install]
    pub async fn install(_installation_return_value: ()) -> Result<Self> {
        Ok(Self)
    }
}
A transfer involves two parties: sender and recipient A transfer has two parties — sender calls transfer(), recipient handles receive(). To use another holder as recipient, import the generated self-ref:
pub mod holder {
    use super::self_refs::Holder;
    // ...
}

Add storage

We need to persist the wallet balance. Add a member to the struct:
pub struct SimpleTokenHolder {
    pub wallet_balance: AmountInSubunits,
}
Then initialize it in install():
#[install]
pub async fn install(_installation_return_value: ()) -> Result<Self> {
    Ok(Self {
        wallet_balance: AmountInSubunits::new(12), // example starting balance
    })
}

Implement receive()

pub fn receive(&mut self, amount: AmountInSubunits) -> Result<()> {
    self.wallet_balance = self.wallet_balance
        .checked_add(amount)
        .ok_or(SimpleTokenError::Overflow)?;
    Ok(())
}

Access modifiers

#[owner]   // only the entity where the component is installed
#[private] // any component in the same contract
#[public]  // anyone
#[view]    // anyone, read-only
Visual matrix of access modifiers Access modifiers control who can call a function — by entity, by contract, or anywhere.

receive() should be #[private]

It mutates state (rules out #[view]), shouldn’t be callable by anyone (rules out #[public]), and shouldn’t let the owner mint themselves tokens (rules out #[owner]). Only other holders should call it — and they’re in the same contract.
#[private]
pub fn receive(&mut self, amount: AmountInSubunits) -> Result<()> {
    self.wallet_balance = self.wallet_balance
        .checked_add(amount)
        .ok_or(SimpleTokenError::Overflow)?;
    Ok(())
}

transfer() should be #[owner]

Only the holder’s owner should be able to send their funds.
#[owner]
pub async fn transfer(
    &mut self,
    amount: AmountInSubunits,
    recipient: Holder,
) -> Result<()> {
    Ok(())
}

Wire up transfer()

With receive() marked #[private], transfer can now call it on another holder via remote:
#[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(())
}
Imports you’ll need:
pub mod holder {
    use crate::simple_token_errors::SimpleTokenError;
    use genie::{AmountInSubunits, Result, install, owner, private, view};
    // ...
}
And in 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,
}
Why .remote? Coming up in the next chapter.

Test that it works

Add a get_balance()

#[view]
pub fn get_balance(&self) -> Result<AmountInSubunits> {
    Ok(self.wallet_balance)
}

Fix the generated client

Because we renamed component_1holder, the generated test references break. Update them:
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};

    let /*_component_1*/holder_client = simple_token_contract::install_holder(/*component_1(*/
            deployer_account,
            _root_contract,
        )

Install a second holder

let holder2_client = simple_token_contract
    ::install_holder(deployer_account, _root_contract)
    .expect("holder2 installation payload build should succeed")
    .execute_with_options(
        &gen_client,
        &deployer_signer,
        &ActivationOptions::builder().tag(ActivationTag::new(2)).build()
    ).await
    .expect("holder2 installation should succeed");

Transfer and verify

holder_client
    .transfer(AmountInSubunits::new(1), Holder::new(*holder2_client.component_id()))
    .expect("transfer payload build 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
);
Checkpointcargo test -p simple-token-tests passes.
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 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/src/simple_token_errors.rs
use alloc::string::ToString;
use genie::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,
}
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();

        ctx.create_accounts(&[deployer_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();

        // Install component_1 component
        let /*_component_1*/holder_client = simple_token_contract::install_holder(/*component_1(*/

            deployer_account,

            _root_contract,

        )
        .expect("holder installation payload build should succeed")
        .execute(&gen_client, &deployer_signer)
        .await
        .expect("holder installation should succeed");

        let holder2_client = simple_token_contract
            ::install_holder(deployer_account, _root_contract)
            .expect("holder2 installation payload build should succeed")
            .execute_with_options(
                &gen_client,
                &deployer_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()))
            .expect("transfer payload build 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 · Async Counter (Exercise)

Now do the same in my-token. The contract has two components: incrementer and receiver. Your task is to add:
  • An increment(to: Receiver) on incrementer that asks the receiver to bump its internal counter by 1.
  • A receive() on receiver that increments the counter — callable only by incrementers.
  • A get_counter() view that returns the current counter value.
Your implementation must pass the tests in my-token-tests/src/lib.rs. Starter contract: incrementer and receiver components What my_token_contracts.rs looks like at v0.1.3 — the components are in place; you fill in the functions.
Starter and solution — Get the starting point by checking out its tag in the my-token repo. Stash anything you don’t want to lose first.
cd my-token
git stash       # save any in-progress edits
git checkout v0.1.3
When you’re ready to compare, jump to the solution tag — or just reveal the solution inline below.
git reset --hard HEAD
git checkout v0.2.2
// my-token @ v0.1.3 — my-token/src/my_token_contracts.rs
pub mod incrementer {
    pub struct Incrementer;

    impl Incrementer {
        // TODO: implement async fn increment(&mut self, to: Receiver) -> Result<()>
        #[install]
        pub async fn install(_iv: ()) -> Result<Self> { Ok(Self) }
    }
}

pub mod receiver {
    pub struct Receiver { /* TODO: counter */ }

    impl Receiver {
        // TODO: fn receive(&mut self) -> Result<()>
        // TODO: fn get_counter(&self) -> Result<u128>
        #[install]
        pub async fn install(_iv: ()) -> Result<Self> { Ok(Self { /* counter: 0 */ }) }
    }
}