Skip to main content
Part 2 of composability. Picking up from Make It Compile: my-token now builds with a simple-token holder installed in each component. Now you make the cross-contract call actually run, then test it.
Prerequisites · Checkpoints 1–3 from the previous chapter pass — both crates build and each my-token component stores its own holder.

Phase 4 · Call across contracts with default routing

You can’t pass a Receiver straight to transfer() (type mismatch) and you can’t cast the component id directly (runtime function mismatch). The fix is default routing: register the receiver-side holder as the default handler for calls of its type.
#[install]
pub async fn install(simple_token_contract: GvmContract) -> Result<Self> {
    let receiver_simple_token_holder: Holder = SimpleTokenClient::installer()
        .holder.install(simple_token_contract).await?
        .try_into()?;
    Component::set_default_route(*receiver_simple_token_holder.component_id()).await?;
    Ok(Self { counter: 0, receiver_simple_token_holder })
}
Then in increment(), build a properly-typed component id and call transfer():
let receiver_holder_id = GvmComponentId::new(
    *to.component_entity_id(),
    GvmComponentHeader::builder()
        .version(*to.header().version())
        .index(*to.header().index())
        .contract(*self.incrementer_simple_token_holder.header().contract())
        .component_code_id(*self.incrementer_simple_token_holder.header().component_code_id())
        .build()
);

let transfer_future = self.incrementer_simple_token_holder.remote.transfer(
    AmountInSubunits::new(1),
    Holder::new(receiver_holder_id)?,
    with_confirmation,
);
transfer_future.spawn();
Challenge — Think about which error states are possible here and how you’d handle them differently between .spawn() and .await?. There’s a deep-dive on Composable Errors in the appendix.
Checkpoint 4increment() compiles and the transfer() call type-checks. Default routing is registered in each receiver’s install().

Phase 5 · Write and run the test

Housekeeping

Add view methods to inspect each holder’s balance:
#[view]
pub fn get_incrementer_balance(&self) -> Result<AmountInSubunits> {
    Ok(self.incrementer_simple_token_holder.local.get_balance()?)
}

#[view]
pub fn get_receiver_balance(&self) -> Result<AmountInSubunits> {
    Ok(self.receiver_simple_token_holder.local.get_balance()?)
}
Because deploy and install signatures changed, comment out your old tests and import simple-token’s ABI to generate its client:
#[generate_contract_client("../my-token/artifacts/my_token_modules.json")]
mod my_token_contract {}

#[generate_contract_client("../../simple-token/simple-token/artifacts/simple_token_modules.json")]
mod simple_token {}

The test

No Result return on the test fn, so every step uses unwrap_or_else(|e| panic!(... LOCAL_RUN_INSTRUCTIONS)) instead of ?. Imports + setup
#[cfg(test)]
mod tests {
    use super::env_utils::LOCAL_RUN_INSTRUCTIONS;
    use super::my_token_contract;
    use super::simple_token;
    use client_sdk::{ActivationOptions, ActivationTag};
    use gen_test_tools::{TestContext, test_context};
    use rstest::rstest;
    use crate::my_token_contract::self_refs::Receiver;
    use client_sdk::types::AmountInSubunits;

    #[rstest]
    #[tokio::test]
    async fn my_token_increment_confirmation_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 receiver_signer     = ctx.new_signer();
        let receiver_account    = receiver_signer.account();
        let incrementer_signer  = ctx.new_signer();
        let incrementer_account = incrementer_signer.account();

        ctx.create_accounts(&[deployer_account, receiver_account, incrementer_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 and deploy simple-token
        let simple_token_push_id = simple_token
            ::push_contract(deployer_account, tokio::fs::read).await
            .unwrap_or_else(|e| panic!("push contract failed: {e}{LOCAL_RUN_INSTRUCTIONS}"))
            .execute(&gen_client, &deployer_signer).await
            .unwrap_or_else(|e| panic!("push contract execution failed: {e}{LOCAL_RUN_INSTRUCTIONS}"));

        let simple_token_root = simple_token
            ::deploy_contract(deployer_account, simple_token_push_id)
            .unwrap_or_else(|e| panic!("deploy contract failed: {e}{LOCAL_RUN_INSTRUCTIONS}"))
            .execute(&gen_client, &deployer_signer).await
            .unwrap_or_else(|e| panic!("deploy contract execution failed: {e}{LOCAL_RUN_INSTRUCTIONS}"));

        let _simple_token_contract = simple_token_root.gvm_contract();
Push and deploy my-token — passing _simple_token_contract as the third deploy arg (this is what MyTokenRoot::deploy receives and stores):
        let push_id = my_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}"));

        let root = my_token_contract::deploy_contract(
            deployer_account,
            push_id,
            _simple_token_contract,
        )
        .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 three components on three different entities. The third one uses execute_with_options with a distinct ActivationTag so it doesn’t dedup against the first install_incrementer:
        let incrementer = my_token_contract::install_incrementer(deployer_account, _root_contract)
            .expect("incrementer installation payload build should succeed")
            .execute(&gen_client, &deployer_signer).await
            .expect("incrementer installation should succeed");

        let receiver = my_token_contract::install_receiver(receiver_account, _root_contract)
            .expect("receiver installation payload build should succeed")
            .execute(&gen_client, &receiver_signer).await
            .expect("receiver installation should succeed");

        let remote_incrementer = my_token_contract::install_incrementer(incrementer_account, _root_contract)
            .expect("remote incrementer installation payload build should succeed")
            .execute_with_options(
                &gen_client,
                &incrementer_signer,
                &ActivationOptions::builder().tag(ActivationTag::new(2)).build(),
            )
            .await
            .expect("remote incrementer installation should succeed");
Increment, then assert. Runs on remote_incrementer, so its holder shrinks, the receiver’s holder grows, and the counter ticks up.
        remote_incrementer
            .increment(Receiver::new(*receiver.component_id()), true)
            .expect("increment should succeed")
            .execute(&gen_client, &incrementer_signer).await
            .expect("increment should succeed");

        let incrementer_balance = incrementer
            .get_incrementer_balance(incrementer_account)
            .expect("get_incrementer_balance view payload build should succeed")
            .execute(&gen_client).await
            .expect("get_incrementer_balance should succeed");

        assert!(
            incrementer_balance == AmountInSubunits::new(11),
            "Incrementer balance mismatch: expected {}, got {}",
            AmountInSubunits::new(11), incrementer_balance
        );

        let counter: u128 = receiver
            .get_count(receiver_account)
            .expect("get_count view payload build should succeed")
            .execute(&gen_client).await
            .expect("get_count should succeed");

        assert!(
            counter == 2,
            "Receiver counter mismatch: expected {}, got {}",
            2, counter
        );
    }
}
Checkpoint 5 — the test compiles and runs. It may fail on the first run with a ContractRefError: Instance mismatch — that’s expected, and the next section fixes it.
Prerequisite — the test imports simple_token from ../../simple-token/simple-token/artifacts/simple_token_modules.json, which is the sibling simple-token/ directory already in your my-contracts-repo checkout from Project Setup. Bring it to the latest end-of-workshop state and build it so the ABI exists:
cd my-contracts-repo
git checkout v0.7.3    # latest tag at time of writing
gen genie build simple-token/simple-token
my-token/my-token/src/my_token_contract.rs
use genie::contract;

/// Implementation of the MyToken contract
#[contract]
pub mod my_token {
    /// Root component for MyToken
    pub mod root {
        use genie::{GvmContract, Result, deploy, installation_request, owner, view};

        /// Root implementation for MyToken
        pub struct MyTokenRoot {
            value: u64,
            simple_token_contract: GvmContract,
        }

        impl MyTokenRoot {

            /// Handles installation requests for MyToken components.

            #[deploy]
            pub async fn deploy(simple_token_contract: GvmContract) -> Result<Self> {
                Ok(Self { value: 0, simple_token_contract })
            }

            #[installation_request]
            pub async fn installation_request(
                &mut self,
                _component_type: MyTokenComponents,
            ) -> Result<GvmContract> {
                Ok(self.simple_token_contract)
            }

            /// 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)
            }
        }
    }

    /// incrementer component for MyToken
    pub mod incrementer {
        use super::self_refs::Receiver;

        use genie::{AmountInSubunits, GvmComponentHeader, GvmComponentId, GvmContract, view};
        use genie::{GvmError, Result, install, owner, private};
        use simple_token::simple_token_contract::instance::alpha::Holder;
        use simple_token::simple_token_contract::simple_token::SimpleTokenClient;

        /// Incrementer implementation
        pub struct CounterIncrementer {
            incrementer_simple_token_holder: Holder,
        }

        impl CounterIncrementer {
            #[view]
            pub fn get_incrementer_balance(&self) -> Result<AmountInSubunits> {
                Ok(self.incrementer_simple_token_holder.local.get_balance()?)
            }

            /// Install handler for Incrementer
            #[install]
            pub async fn install(installation_return_value: GvmContract) -> Result<Self> {
                let incrementer_simple_token_holder = SimpleTokenClient::installer()
                    .holder.install(installation_return_value).await?
                    .try_into()?;
                Ok(Self { incrementer_simple_token_holder })
            }

            /// Transfer 1 token to the receiver
            #[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(increment_result) = receive_future.await {
                        //Perform an action that reflects the action failed +
                        //Throw an error that accurately describes what happened
                        return self.handle_increment_failure(increment_result);
                    } else {
                        //Action succeeded so you can update something
                        let receiver_holder_id = GvmComponentId::new(
                            *to.component_entity_id(),
                            GvmComponentHeader::builder()
                                .version(*to.header().version())
                                .index(*to.header().index())
                                .contract(*self.incrementer_simple_token_holder.header().contract())
                                .component_code_id(*self.incrementer_simple_token_holder.header().component_code_id())
                                .build()
                        );

                        let transfer_future = self.incrementer_simple_token_holder.remote.transfer(
                            AmountInSubunits::new(1),
                            Holder::new(receiver_holder_id)?,
                            with_confirmation,
                        );
                        transfer_future.spawn();
                    };
                } else {
                    receive_future
                        .spawn()
                        .on_error(Self::callbacks().handle_increment_failure);
                }
                Ok(())
            }

            #[owner]
            pub async fn increment_same_entity(&mut self, to: Receiver) -> Result<()> {
                if let Err(increment_result) = to.local.receive() {
                    return self.handle_increment_failure(increment_result);
                }
                Ok(())
            }

            #[private]
            pub fn handle_increment_failure(&mut self, delivery_error: GvmError) -> Result<()> {
                //There's nothing meaningful to fix so throw an error
                Err(delivery_error.into())
            }
        }
    }

    /// Receiver component for MyToken
    pub mod receiver {
        use genie::{AmountInSubunits, Component, GvmContract};
        use genie::{Result, install, private, view};
        use simple_token::simple_token_contract::instance::alpha::Holder;
        use simple_token::simple_token_contract::simple_token::SimpleTokenClient;

        /// Receiver implementation
        pub struct CounterReceiver {
            counter: u128,
            receiver_simple_token_holder: Holder,
        }

        impl CounterReceiver {
            #[view]
            pub fn get_receiver_balance(&self) -> Result<AmountInSubunits> {
                Ok(self.receiver_simple_token_holder.local.get_balance()?)
            }

            /// Install handler for CounterReceiver
            #[install]
            pub async fn install(simple_token_contract: GvmContract) -> Result<Self> {
                let receiver_simple_token_holder: Holder = SimpleTokenClient::installer()
                    .holder.install(simple_token_contract).await?
                    .try_into()?;
                Component::set_default_route(*receiver_simple_token_holder.component_id()).await?;
                Ok(Self { counter: 0, receiver_simple_token_holder })
            }

            #[view]
            pub fn get_count(&self) -> Result<u128> {
                Ok(self.counter)
            }

            #[private]
            pub fn receive(&mut self) -> Result<()> {
                self.counter += 1;
                Ok(())
            }
        }
    }
}
my-token/my-token-tests/src/lib.rs
//! MyToken Contract Test Crate
//!
//! This crate contains integration tests for the MyToken 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("../my-token/artifacts/my_token_modules.json")]
mod my_token_contract {}

#[generate_contract_client("../../simple-token/simple-token/artifacts/simple_token_modules.json")]
mod simple_token {}

#[cfg(test)]
mod tests {
    use super::env_utils::LOCAL_RUN_INSTRUCTIONS;
    use super::my_token_contract;
    use super::simple_token;
    use client_sdk::{ActivationOptions, ActivationTag};
    use gen_test_tools::{TestContext, test_context};
    use rstest::rstest;
    use crate::my_token_contract::self_refs::Receiver;
    use client_sdk::types::AmountInSubunits;

    /// End-to-end test for the MyToken contract.
    #[rstest]
    #[tokio::test]
    async fn my_token_increment_confirmation_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 receiver_signer     = ctx.new_signer();
        let receiver_account    = receiver_signer.account();
        let incrementer_signer  = ctx.new_signer();
        let incrementer_account = incrementer_signer.account();

        ctx.create_accounts(&[deployer_account, receiver_account, incrementer_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}"));

        let simple_token_push_id = simple_token
            ::push_contract(deployer_account, tokio::fs::read).await
            .unwrap_or_else(|e| panic!("push contract failed: {e}{LOCAL_RUN_INSTRUCTIONS}"))
            .execute(&gen_client, &deployer_signer).await
            .unwrap_or_else(|e| panic!("push contract execution failed: {e}{LOCAL_RUN_INSTRUCTIONS}"));

        let simple_token_root = simple_token
            ::deploy_contract(deployer_account, simple_token_push_id)
            .unwrap_or_else(|e| panic!("deploy contract failed: {e}{LOCAL_RUN_INSTRUCTIONS}"))
            .execute(&gen_client, &deployer_signer).await
            .unwrap_or_else(|e| panic!("deploy contract execution failed: {e}{LOCAL_RUN_INSTRUCTIONS}"));

        let _simple_token_contract = simple_token_root.gvm_contract();

        // Push contract code
        let push_id = my_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 = my_token_contract::deploy_contract(
            deployer_account,
            push_id,
            _simple_token_contract,
        )
        .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 incrementer component
        let incrementer = my_token_contract::install_incrementer(
            deployer_account,
            _root_contract,
        )
        .expect("incrementer installation payload build should succeed")
        .execute(&gen_client, &deployer_signer)
        .await
        .expect("incrementer installation should succeed");

        // Install receiver component
        let receiver = my_token_contract::install_receiver(
            receiver_account,
            _root_contract,
        )
        .expect("receiver installation payload build should succeed")
        .execute(&gen_client, &receiver_signer)
        .await
        .expect("receiver installation should succeed");

        // Install remote incrementer component
        let remote_incrementer = my_token_contract::install_incrementer(
            incrementer_account,
            _root_contract,
        )
        .expect("remote incrementer installation payload build should succeed")
        .execute_with_options(
            &gen_client,
            &incrementer_signer,
            &ActivationOptions::builder()
                .tag(ActivationTag::new(2))
                .build(),
        )
        .await
        .expect("remote incrementer installation should succeed");

        // 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");

        // Increment the counter
        remote_incrementer
            .increment(Receiver::new(*receiver.component_id()), true)
            .expect("increment should succeed")
            .execute(&gen_client, &incrementer_signer)
            .await
            .expect("increment should succeed");

        let incrementer_balance = incrementer
            .get_incrementer_balance(incrementer_account)
            .expect("get_incrementer_balance view payload build should succeed")
            .execute(&gen_client)
            .await
            .expect("get_incrementer_balance should succeed");

        assert!(
            incrementer_balance == AmountInSubunits::new(11),
            "Incrementer balance mismatch: expected {}, got {}",
            AmountInSubunits::new(11), incrementer_balance
        );

        // Get the counter
        let counter: u128 = receiver
            .get_count(receiver_account)
            .expect("get_count view payload build should succeed")
            .execute(&gen_client)
            .await
            .expect("get_count should succeed");

        // Assert the counter is 2
        assert!(
            counter == 2,
            "Receiver counter mismatch: expected {}, got {}",
            2, counter
        );
    }
}

Troubleshooting · When the test fails with ContractRefError

Initial runs fail with a ContractRefError: Instance mismatch: the incrementer and receiver bake a reference to simple-token’s alpha instance (a deployer_entity::component_index address), but a freshly-deployed simple-token gets a random per-run deployer that never equals it. There are two ways to make the addresses line up:
Checkpoint 6 (final)my_token_transfer_when_increment_test passes — incrementer’s balance dropped by 1, receiver’s grew by 1, and the counter is 1.

What’s next

Final Exercises

Build a Safe Swap and a CLOB from scratch using everything you’ve learned.