Skip to main content
Appendix · Going the extra mile Typed references (self_refs::Holder, contract refs) make things ergonomic, but the generic path through GvmComponentId is more flexible for advanced use cases — anywhere you can’t statically pin the recipient’s type at the caller, or where you want to interact with components defined outside your own crate.

Understanding the issue

Take transfer() as it stands today — recipient is the typed self_refs::Holder:
#[owner]
pub async fn transfer(
    &mut self,
    amount: AmountInSubunits,
    recipient: Holder,
    with_confirmation: bool,
) -> Result<()> {
    ensure!(amount > 0, SimpleTokenError::AmountMustBeGreaterThanZero { amount });
    ensure!(amount <= self.wallet_balance, SimpleTokenError::InsufficientFunds);
    self.wallet_balance = self.wallet_balance
        .checked_sub(amount)
        .ok_or(SimpleTokenError::InsufficientFunds)?;

    let receive_future = recipient.remote.receive(amount);
    if with_confirmation {
        if let Err(delivery_error) = receive_future.await {
            return self.handle_transfer_failure(delivery_error, amount);
        }
    } else {
        receive_future
            .spawn()
            .on_error(Self::callbacks().handle_transfer_failure);
    }
    Ok(())
}

The change — bare GvmComponentId

Replace the typed parameter with a GvmComponentId and reach the holder via the contract client:
// recipient: Holder,
recipient_address: GvmComponentId,

let receive_future = Self::contract_client(recipient_address)
    .holder
    .remote
    .receive(amount);
The caller side (the increment() body in my-token) no longer wraps the id in Holder::new(...):
let transfer_future = self.incrementer_simple_token_holder.remote.transfer(
    AmountInSubunits(1),
    receiver_holder_id,                // bare GvmComponentId, no Holder wrapper
    with_confirmation,
);

Why this matters

  • Cross-contract compositionGvmComponentId is the universal address the network speaks. Any component on any contract can be a target.
  • Reduced coupling — the caller doesn’t have to import the recipient’s type at compile time. Useful for plugin-style architectures.
  • Symmetric with the runtime — what you’d construct from a CLI/SDK call is exactly the same GvmComponentId, so call shapes match across the client/contract boundary.

Exercise

Change the code in my-token so that it doesn’t reference Holder at all, and the same test as in Borrowed Code · Make It Run still passes — incrementer balance drops by 1, receiver’s grows by 1, counter is 1.
Checkpoint — the test passes without any Holder::new(...) calls or Holder-typed parameters anywhere in my_token_contracts.rs or its tests.

What’s next

Composable Errors

Conditioning a follow-up call on the success of a prior one — .await vs .spawn().