Build
Tutorials
Calls from Sui

ZetaChain enables Sui-based applications to interact directly with universal smart contracts deployed on ZetaChain. Using ZetaChain’s universal interoperability layer, Sui apps can:

  • Deposit SUI and supported fungible tokens to ZetaChain
  • Make cross-chain calls to universal contracts
  • Receive cross-chain calls and token transfers from universal contracts

In this tutorial, you'll learn how to:

  • Set up a local development environment with both ZetaChain and Sui
  • Deploy a universal contract on ZetaChain
  • Deposit SUI tokens from a Sui address to ZetaChain
  • Make a cross-chain call to trigger a function on a universal contract

By the end, you’ll understand how to transfer assets and trigger logic on ZetaChain from Sui, both from a client wallet and from a Sui smart contract.

Make sure the following tools are installed before you begin:

ToolPurpose
Sui CLI (opens in a new tab)Run a local Sui validator, manage addresses and objects, deploy contracts
Foundry (opens in a new tab)Encode ABI payloads for cross-chain calls using cast
Node.js (opens in a new tab)Run the ZetaChain CLI and JavaScript-based tooling
Yarn (opens in a new tab)Install and manage project dependencies
jq (opens in a new tab)Parse JSON output in shell scripts

Start by generating a new project using the ZetaChain CLI:

npx zetachain@latest new --project call
cd call

This sets up a ready-to-use example with Sui and ZetaChain contracts.

Install dependencies:

yarn
forge soldeer update

The project is now ready for local development and testing.

Start a local development environment with both ZetaChain and Sui running side by side:

yarn zetachain localnet start --chains sui

This command boots up:

  • A local ZetaChain instance
  • A local Sui validator
  • Pre-deployed gateway contracts on both networks

Leave this terminal open. Once started, you’ll see a table like this:

SUI
┌──────────────────┬──────────────────────────────────────────────────────────────────────────────────┐
│ (index)          │ Values                                                                           │
├──────────────────┼──────────────────────────────────────────────────────────────────────────────────┤
│ gatewayPackageId │ '0x17360c15c10bbc4ebc57e9872f2993dc4376f7f0bb78920fe5fa9ad276ac7f86'             │
│ gatewayObjectId  │ '0x9a26d6b6f413228bb120446977a8d8003eceb490cb7afd8921494815adc0a497'             │
│ userMnemonic     │ 'grape subway rack mean march bubble carry avoid muffin consider thing street'   │
│ userAddress      │ '0x2fec3fafe08d2928a6b8d9a6a77590856c458d984ae090ccbd4177ac13729e65'             │
│ tokenUSDC        │ '6b0b8d1bbc40893a7f793f52c46aeea9db9f2f710c6a623c666bff712e26c94a::token::TOKEN' │
└──────────────────┴──────────────────────────────────────────────────────────────────────────────────┘

💡 Keep this output handy. You’ll reference the gatewayObjectId, gatewayObjectId, and userMnemonic in later steps.

You’ll now deploy a universal smart contract to ZetaChain.

First, grab the local Gateway address and a funded private key from your localnet setup:

GATEWAY_ZETACHAIN=$(jq -r '.["31337"].contracts[] | select(.contractType == "gateway") | .address' ~/.zetachain/localnet/registry.json) && echo $GATEWAY_ZETACHAIN
PRIVATE_KEY=$(jq -r '.private_keys[0]' ~/.zetachain/localnet/anvil.json) && echo $PRIVATE_KEY

Compile the contract:

forge build

Then deploy the Universal contract:

UNIVERSAL=$(forge create Universal \
  --rpc-url http://localhost:8545 \
  --private-key $PRIVATE_KEY \
  --broadcast \
  --json \
  --constructor-args $GATEWAY_ZETACHAIN | jq -r .deployedTo) && echo $UNIVERSAL

Now that your universal contract is deployed on ZetaChain, you can deposit SUI tokens from the Sui Gateway contract to that contract address.

Use the following command to deposit tokens from Sui to your universal contract on ZetaChain:

npx zetachain sui deposit \
  --mnemonic "grape subway rack mean march bubble carry avoid muffin consider thing street" \
  --receiver $UNIVERSAL \
  --gateway-package 0x17360c15c10bbc4ebc57e9872f2993dc4376f7f0bb78920fe5fa9ad276ac7f86 \
  --gateway-object 0x9a26d6b6f413228bb120446977a8d8003eceb490cb7afd8921494815adc0a497 \
  --amount 0.001 \
  --chain-id 104

🔎 Replace --gateway-package and --gateway-object with the values printed in your localnet output table.

This command calls the deposit function on the Sui Gateway contract. ZetaChain observes the deposit event and propagates the deposit to ZetaChain by minting ZRC-20 SUI tokens and transferring them to your universal contract.

In this step, you’ll deposit SUI tokens from Sui and simultaneously trigger a function call on the universal contract deployed on ZetaChain.

npx zetachain sui deposit-and-call \
  --mnemonic "grape subway rack mean march bubble carry avoid muffin consider thing street" \
  --receiver $UNIVERSAL \
  --gateway-package 0x17360c15c10bbc4ebc57e9872f2993dc4376f7f0bb78920fe5fa9ad276ac7f86 \
  --gateway-object 0x9a26d6b6f413228bb120446977a8d8003eceb490cb7afd8921494815adc0a497 \
  --amount 0.001 \
  --chain-id 104 \
  --types string \
  --values hello

This command calls the deposit_and_call function on the Sui Gateway contract, transferring the specified SUI amount, encoding the payload ("hello") using the provided --types and --values, and triggering the onCall function on the universal contract deployed on ZetaChain.

To complete the round trip, you can also deploy a Sui contract that initiates deposits to ZetaChain from on-chain logic.

Go to the Sui contract directory:

cd sui

Check the Move.toml:

sui/Move.toml
[dependencies]
gateway = { local = "/usr/local/share/localnet/protocol-contracts-sui" }

This example project already imports the Gateway module from Localnet. When using testnet or mainnet, the Gateway will be imported from a public source instead.

Build the Sui contract:

sui move build --force

Publish the package to your local Sui instance:

SUI_CONTRACT=$(sui client publish \
  --skip-dependency-verification \
  --json 2>/dev/null | jq -r '.objectChanges[] | select(.type == "published") | .packageId') && echo $SUI_CONTRACT

Your Sui account needs some SUI tokens to pay for transactions.

To request tokens from the local faucet, run:

sui client faucet

This sends a faucet request to the local validator and credits your active address with test SUI.

In Sui, calling a universal contract from a Move contract is done through a Programmable Transaction Block (PTB).

PTBs let you compose multiple actions into a single atomic transaction: you can call a Move function, capture its return value, and feed that value into the next call (such as Sui Gateway’s deposit_and_call). A client app (for example, a web app) builds and signs the PTB, submits it to Sui, and Sui executes the sequence as one transaction.

In the created project, there’s a TypeScript command that builds such a PTB: commands/suiDepositAndCall.ts. By default, it ABI‑encodes a payload and sends it directly to the Sui Gateway. We’ll modify it to:

  1. Call our Sui contract first and get its return value.
  2. Pass that returned value as the payload to the Sui Gateway.

Open commands/suiDepositAndCall.ts and replace the payload construction:

commands/suiDepositAndCall.ts
// const payload = tx.pure.vector("u8", utils.arrayify(payloadABI));
 
const payload = tx.moveCall({
  target: `${options.connected}::connected::hello`,
  arguments: [tx.pure.string(params.values[0] as string)],
});

This calls your Sui module function connected::hello with the first provided value (a string in this example). The returned value from the Move call becomes the payload argument that is then forwarded to the Sui Gateway’s deposit_and_call within the same PTB.

Because a Sui contract can’t easily ABI‑encode data for EVM, we’ll update the universal Solidity contract to accept raw bytes for the message payload.

Update the universal contract’s onCall signature and logic to accept bytes and interpret it as a UTF‑8 string:

function onCall(
    MessageContext calldata context,
    address zrc20,
    uint256 amount,
    bytes calldata message
) external override onlyGateway {
    // string memory name = abi.decode(message, (string));
    emit HelloEvent("Hello on ZetaChain", string(message));
}

Rebuild and deploy the contract on your local ZetaChain:

forge build
UNIVERSAL=$(forge create Universal \
  --rpc-url http://localhost:8545 \
  --private-key $PRIVATE_KEY \
  --broadcast \
  --json \
  --constructor-args $GATEWAY_ZETACHAIN | jq -r .deployedTo) && echo $UNIVERSAL

Now execute the PTB that calls your Sui contract, takes its return value, and passes it to the Sui Gateway, which then calls your universal contract on ZetaChain:

npx tsx commands deposit-and-call \
  --private-key suiprivkey1qrqtrevmd40vxlv3q6fcgmm09af5p8f8j67ezve0u3nhrs0psslgjnw3y5p \
  --receiver $UNIVERSAL \
  --gateway-package 0xc52d19fd2f0bcb94d0c285d480b6a8af515f5ec19d0cff8818fc9bf0d3731b85 \
  --gateway-object 0xa755a1106ae7039a25c10578b6f3c5b73963055e0d6fc40e8abcebea454a6389 \
  --amount 0.001 \
  --chain-id 104 \
  --types string \
  --values alice \
  --connected $SUI_CONTRACT

You should see an event emitted in the terminal similar to:

[ZetaChain] Event from onCall: {"_type":"log","address":"0xa6e99A4ED7498b3cdDCBB61a6A607a4925Faa1B7","blockHash":"0x694028003ddc35ab9da2a18c262f8df88f882f3a4c3db8d9935b6346ed9f9b7f","blockNumber":192,"data":"0x00000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000001248656c6c6f206f6e205a657461436861696e0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000b68656c6c6f20616c696365000000000000000000000000000000000000000000","index":2,"removed":false,"topics":["0x39f8c79736fed93bca390bb3d6ff7da07482edb61cd7dafcfba496821d6ab7a3"],"transactionHash":"0x82e7ed404b1c3bfb06677174b74d54d1bb5b1b6248433ead6bd48c0cda519777","transactionIndex":0}

To inspect it, you can decode the log data:

cast abi-decode "data()(string,string)" \
  0x00000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000001248656c6c6f206f6e205a657461436861696e0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000b68656c6c6f20616c696365000000000000000000000000000000000000000000

Expected output:

"Hello on ZetaChain"
"hello alice"