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.
#[owner] // only the entity where the component is installed#[private] // any component in the same contract#[public] // anyone#[view] // anyone, read-only
Access modifiers control who can call a function — by entity, by contract, or anywhere.
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.
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,}
Because we renamed component_1 → holder, 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, )
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 }}
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.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-tokengit stash # save any in-progress editsgit checkout v0.1.3
When you’re ready to compare, jump to the solution tag — or just reveal the
solution inline below.