| name | mvx-cross-contract-calls |
| description | Make cross-contract calls in MultiversX smart contracts. Use when calling another contract, handling callbacks, managing back-transfers, using typed proxies, or sending tokens via the Tx builder API (.tx().to()). |
MultiversX Cross-Contract Calls — Tx Builder API Reference
Complete reference for cross-contract calls, callbacks, back-transfers, and proxies in MultiversX smart contracts (SDK v0.64+).
Tx Builder Chain
Every cross-contract interaction starts with self.tx() and chains builder methods:
self.tx()
.to(address) // recipient (ManagedAddress or &ManagedAddress)
.typed(ProxyType) // type-safe proxy (recommended)
// OR .raw_call("endpoint_name") // raw call without proxy
.egld(&amount) // payment: EGLD
// OR .single_esdt(&id, nonce, &amount) // payment: single ESDT
// OR .payment(payment) // payment: Payment / EgldOrEsdtTokenPayment
.gas(gas_limit) // explicit gas (required for promises)
.returns(ReturnsResult) // result handler(s)
.sync_call() // execution method
Builder Methods
| Category | Method | Description |
|---|
| Target | .to(address) | Set recipient address |
| Proxy | .typed(ProxyType) | Use generated proxy for type-safe calls |
| Raw | .raw_call("endpoint") | Manual endpoint call (no proxy) |
| Payment | .egld(&amount) | Attach EGLD |
| .single_esdt(&token_id, nonce, &amount) | Attach single ESDT |
| .esdt(esdt_payment) | Attach EsdtTokenPayment |
| .payment(payment) | Attach any payment type |
| .egld_or_single_esdt(&id, nonce, &amount) | Attach EGLD or ESDT |
| Gas | .gas(amount) | Set gas limit |
| .gas_for_callback(amount) | Reserve gas for callback |
| Args | .argument(&arg) | Add single argument (raw calls) |
| .arguments_raw(buffer) | Add pre-encoded arguments |
| Results | .returns(handler) | Add result handler (can chain multiple) |
| Callback | .callback(closure) | Set callback for async calls |
Execution Methods
| Method | Type | Description |
|---|
.sync_call() | Synchronous | Same-shard atomic. Panics if callee fails. |
.sync_call_fallible() | Synchronous | Same-shard. Returns Result on callee error (use with ReturnsHandledOrError). |
.sync_call_readonly() | Synchronous | Read-only call. Cannot modify state. |
.register_promise() | Async (v2) | Cross-shard. Requires .gas(). Multiple promises per tx OK. |
.async_call_and_exit() | Async (v1) | Legacy. Only 1 per tx. Exits immediately. Prefer register_promise(). |
.transfer() | Transfer | Send tokens without calling an endpoint. |
Simple Transfers (No Endpoint Call)
self.tx().to(&recipient).egld(&amount).transfer();
self.tx().to(&recipient).payment(payment.clone()).transfer();
self.tx().to(&recipient).single_esdt(&token_id, nonce, &amount).transfer();
Synchronous Calls (Same-Shard)
Typed Proxy Call
let result: BigUint = self.tx()
.to(&pool_address)
.typed(proxy_pool::LiquidityPoolProxy)
.get_reserve()
.returns(ReturnsResult)
.sync_call();
Sync Call with Payment
self.tx()
.to(&accumulator_address)
.typed(proxy_accumulator::AccumulatorProxy)
.deposit()
.payment(revenue)
.returns(ReturnsResult)
.sync_call();
Multiple Return Values
let (result, back_transfers) = self.tx()
.to(&pool_address)
.typed(proxy_pool::PoolProxy)
.withdraw(amount)
.returns(ReturnsResult)
.returns(ReturnsBackTransfersReset)
.sync_call();
Fallible Sync Call
let outcome: Result<BigUint, u32> = self.tx()
.to(&address)
.typed(SomeProxy)
.some_endpoint()
.returns(
ReturnsHandledOrError::new()
.returns(ReturnsResult)
)
.sync_call_fallible();
match outcome {
Ok(value) => { },
Err(error_code) => { },
}
Raw Call (No Proxy)
let result: ManagedBuffer = self.tx()
.to(&address)
.raw_call("getStatus")
.argument(&token_id)
.returns(ReturnsResult)
.sync_call();
Result Handlers
| Handler | Returns | Description |
|---|
ReturnsResult | Decoded return type | Decodes endpoint return value |
ReturnsBackTransfersReset | BackTransfers | Recommended. Resets back-transfers before call, returns them after. |
ReturnsBackTransfers | BackTransfers | Returns back-transfers without resetting (may include leftovers from prior calls). |
ReturnsBackTransfersEgld | BigUint | Only the EGLD portion of back-transfers |
ReturnsBackTransfersSingleEsdt | EsdtTokenPayment | Single ESDT back-transfer (panics if != 1) |
ReturnsNewManagedAddress | ManagedAddress | Address of newly deployed contract |
ReturnsHandledOrError | Result<T, u32> | Wraps other handlers; returns error code on failure |
Chain multiple handlers:
.returns(ReturnsResult)
.returns(ReturnsBackTransfersReset)
.sync_call()
Back-Transfers
Tokens sent back to the caller during a cross-contract call.
BackTransfers Struct
pub struct BackTransfers<A: ManagedTypeApi> {
pub payments: MultiEgldOrEsdtPayment<A>,
}
impl BackTransfers {
fn egld_sum(&self) -> BigUint
fn to_single_esdt(self) -> EsdtTokenPayment
fn into_payment_vec(self) -> PaymentVec
fn into_multi_value(self) -> MultiValueEncoded<EgldOrEsdtTokenPaymentMultiValue>
}
Using Back-Transfers with Result Handlers (Recommended)
let back_transfers = self.tx()
.to(&dex_address)
.typed(DexProxy)
.swap(token_out, min_amount)
.payment(input_payment)
.returns(ReturnsBackTransfersReset)
.sync_call();
let result_payments = back_transfers.into_payment_vec();
for payment in result_payments.iter() {
vault.deposit(&payment.token_identifier, &payment.amount);
}
Manual Back-Transfer Retrieval
self.tx().to(&addr).typed(Proxy).some_call().sync_call();
let bt = self.blockchain().get_back_transfers();
self.blockchain().reset_back_transfers();
Async Calls (Cross-Shard) — Promises
Register Promise with Callback
self.tx()
.to(&provider)
.typed(proxy_delegation::DelegationProxy)
.delegate()
.egld(&payment)
.gas(12_000_000u64)
.callback(
self.callbacks().delegation_callback(
provider.clone(),
&payment,
&caller,
),
)
.gas_for_callback(10_000_000u64)
.register_promise();
Callback Implementation
#[promises_callback]
fn delegation_callback(
&self,
contract_address: ManagedAddress,
staked_amount: &BigUint,
caller: &ManagedAddress,
#[call_result] result: ManagedAsyncCallResult<()>,
) {
match result {
ManagedAsyncCallResult::Ok(()) => {
let ls_amount = self.calculate_ls(staked_amount);
let user_payment = self.mint_ls_token(ls_amount);
self.tx().to(caller).esdt(user_payment).transfer();
},
ManagedAsyncCallResult::Err(_) => {
self.tx().to(caller).egld(staked_amount).transfer();
},
}
}
Key callback rules:
- Use
#[promises_callback] annotation (not #[callback] which is legacy)
#[call_result] parameter receives ManagedAsyncCallResult<T> where T matches callee return
- Callback arguments are passed via
self.callbacks().my_callback(arg1, arg2) — they're serialized into a CallbackClosure
- In callbacks,
self.call_value().all() gets tokens sent back
- Multiple
register_promise() calls allowed in a single transaction
Promise without Callback
self.tx()
.to(&address)
.typed(Proxy)
.some_endpoint()
.gas(5_000_000u64)
.register_promise();
Typed Proxy Pattern
Proxies are generated from contract interfaces. They provide type-safe endpoint calls.
Proxy Generation
Proxies are auto-generated by the framework from contract trait definitions. The generated proxy module is typically in a proxy/ directory or a proxy_*.rs file.
use crate::proxy_pool;
self.tx()
.to(&pool_address)
.typed(proxy_pool::LiquidityPoolProxy)
.deposit(token_id, amount)
.payment(payment)
.returns(ReturnsResult)
.sync_call();
Deploy via Proxy
let new_address: ManagedAddress = self.tx()
.typed(proxy_pool::LiquidityPoolProxy)
.init(base_asset, max_rate, threshold)
.from_source(self.template_address().get())
.code_metadata(CodeMetadata::UPGRADEABLE | CodeMetadata::READABLE)
.returns(ReturnsNewManagedAddress)
.sync_call();
Common Patterns
Swap and Forward Results
let back_transfers = self.tx()
.to(&dex)
.typed(DexProxy)
.swap(token_out, min_out)
.payment(input)
.returns(ReturnsBackTransfersReset)
.sync_call();
let output = back_transfers.into_payment_vec();
let caller = self.blockchain().get_caller();
for payment in output.iter() {
self.tx().to(&caller).payment(payment.clone()).transfer();
}
Multi-Step with Back-Transfer Tracking
let (withdrawn_amount, bt1) = self.tx()
.to(&pool)
.typed(PoolProxy)
.withdraw(shares)
.returns(ReturnsResult)
.returns(ReturnsBackTransfersReset)
.sync_call();
self.tx()
.to(&other_pool)
.typed(OtherPoolProxy)
.deposit()
.payment(bt1.into_payment_vec().get(0).clone())
.returns(ReturnsResult)
.sync_call();
Anti-Patterns
.returns(ReturnsBackTransfers).sync_call()
.returns(ReturnsBackTransfersReset).sync_call()
self.tx().to(&addr).typed(Proxy).call().register_promise();
self.tx().to(&addr).typed(Proxy).call().gas(10_000_000u64).register_promise();
self.send().direct_egld(&to, &amount);
self.tx().to(&to).egld(&amount).transfer();
let bt = self.blockchain().get_back_transfers();
let bt = self.blockchain().get_back_transfers();
self.blockchain().reset_back_transfers();
#[callback]
fn my_callback(&self) { }
#[promises_callback]
fn my_callback(&self) { }