Skip to main content
Get familiar with the anatomy of a GEN contract — the root component, the ABI, and what push and deploy actually do — then run your first test.

Contracts: the root component

Both my_token_contracts.rs (cloned) and the generated simple_token_contracts.rs have a root module:
/// Root component for XToken
pub mod root {
    /* a lot of stuff here */
}
With a matching struct and impl:
/// Root implementation for XToken
pub struct XTokenRoot;

impl XTokenRoot {
    /* functions here */
}

Two required functions on root

#[deploy]
pub async fn deploy() -> Result<Self> {
    Ok(Self)
}
Called when the contract is deployed and the root component is installed. Instantiate root members or store parameters that other components will need at install time.
#[installation_request]
pub async fn installation_request(
    &mut self,
    _component_type: XTokenComponents,
) -> Result<()> {
    Ok(())
}
Called when root receives a request to install a component. Enforce conditions (e.g., max-installed) or pass parameters that the component needs.

Non-root components

pub mod component_1 {
    pub struct Component1;

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

Tests: ABI-driven typed clients

Both test files include:
#[generate_contract_client("[PATH]_token_modules.json")]
mod x_token_contract {}
This macro generates typed clients from the ABI — a JSON description of:
  • What functions exist
  • What parameters they take
  • What they return
  • How data is encoded/decoded

The module manifest and a per-component ABI

The module manifest:
{
  "schema_version": "1.0",
  "package_name": "my-token",
  "contract_name": "my_token",
  "modules": [
    { "name": "root", "component_type_index": 0,
      "object_path": "my_token_0_root.o",
      "abi_path": "my_token_0_root.abi.json" },
    { "name": "component_1", "component_type_index": 1,
      "object_path": "my_token_1_component_1.o",
      "abi_path": "my_token_1_component_1.abi.json" }
  ]
}
A per-component ABI:
{
  "contract_name": "my_token",
  "encoding": "Borsh",
  "module_name": "component_1",
  "methods": [
    {
      "name": "install",
      "access_modifier": "runtime_only",
      "params": [{ "name": "_installation_return_value",
                   "type": { "tuple": [] } }],
      "returns": [],
      "is_async": true
    }
  ]
}

What’s in a test?

Set up context and accounts

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

Initialize the client

let gen_client = ctx.gen_client()
    .unwrap_or_else(|e| panic!("gen client creation failed: {e}{LOCAL_RUN_INSTRUCTIONS}"));
The client lets you interact with entities on the blockchain — execution spaces with storage that can install components onto themselves or other entities.

Push the 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}"));
An external push call lands on default_entity(0), which copies the contract code into the entity’s storage. An entity (default_entity(0)) gets an external push call 1. The entity (default_entity(0)) receives the external push call. Contract code is copied onto the entity's storage 2. The contract code is copied into the entity’s storage.

Deploy the code (install the root component)

let root = simple_token_contract
    ::deploy_contract(deployer_account, push_id)
    .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();
External call asks the entity to create an instance using pushId 1. The external call asks the entity to create an instance of the code that was pushed using pushId. Entity installs the contract root component on itself 2. The entity deploys the contract by installing the root component (in this case, onto itself).

Install a component

let _component_1 = simple_token_contract::install_component_1(
        deployer_account,
        _root_contract,
    )
    .expect("component_1 installation payload build should succeed")
    .execute(&gen_client, &deployer_signer).await
    .expect("component_1 installation should succeed");
Install component step 1 1. The external call requests installation of component_1 via the deployed root. Install component step 2 2. Root forwards the installation request to the target entity. Install component step 3 3. The component is installed and a typed handle is returned.

It’s showtime

Build

cd my-token
gen genie build my-token
cd ../simple-token
gen genie build simple-token

Test

cargo test -p my-token-tests
# if gen is not on your PATH:
GEN_CLI_PATH=~/bin/gen cargo test -p my-token-tests
# full backtrace:
RUST_BACKTRACE=1 cargo test -p my-token-tests
# verbose (your println!s):
cargo test -p my-token-tests -- --nocapture
# repeatable scenario (same entity IDs):
TEST_SEED=12345 cargo test -p my-token-tests
build.rs detects changes in your contract source files, runs gen genie build automatically, and regenerates the ABI files used by #[generate_contract_client]. That means you can edit code and just run cargo test — but the first test will take a few minutes as it builds the contract and all dependencies.

Choose a test workflow

The default cargo test runs each test against a fresh validator subprocess, which is fine but slow. The IDE and helper scripts the scaffolding ships with keep a single validator running across runs:
EnvironmentRecommended workflowNotes
VS Code / CursorClick Run Test or Debug on any #[test], or use the Run and Debug panel entriesThe .vscode/bin/cargo shim starts a local validator automatically via scripts/ensure-local-validator.sh
Terminal./scripts/test-with-local-validator.sh -p my-token-testsSame as the IDE flow: starts a background validator, sets GEN_VALIDATOR_MODE=external, runs cargo test
Docker (test-container)GEN_VALIDATOR_MODE=test-container cargo test -p my-token-testsRequires the gen-cli:latest image; install.sh builds it automatically when Docker is reachable
ManualRun gen service validator local --embedded-config in one terminal, then GEN_VALIDATOR_MODE=external cargo test -p my-token-tests in anotherFull control; no helper scripts needed
For the full reference on validator modes and test fixtures, see Contract test tools.
Checkpointcargo test -p my-token-tests passes.