> ## Documentation Index
> Fetch the complete documentation index at: https://docs.metalntwx.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Send messages from Metal

> Send a message from Metal and verify its execution on the destination chain

> [!NOTE]
>
> **What you'll be able to do**
>
> * Send a message from Metal to a contract on another chain.
> * Deliver the message and execute its destination call.

To call a contract on another blockchain, an application calls Metal's **outbox**
contract to record a message specifying the destination chain, receiving contract,
and function call. A **relayer** delivers the message by submitting it to the
**inbox** contract on the destination chain, along with proof that Metal recorded
and finalized it. The inbox checks the proof and calls the receiving contract using
the message's function and arguments. Record the message as complete after verifying
that the receiving contract performed the requested action and the destination block
is final.

## Prepare a configured route

Obtain the route provider's authenticated configuration. You need the destination
chain ID and RPC endpoint, its `MetalLightClient` and `MetalInbox` addresses and public
artifacts, the receiving contract's address and ABI, the authorized source
sender, and the destination's finality policy. Fund the signing accounts that
submit the source and destination transactions for each chain's transaction
fees.

Verify runtime bytecode against those artifacts and read the inbox's
`lightClient()` and `metalChainId()`. Check the light client's Metal chain ID,
genesis hash, verifier version, and validator configuration against the route
provider's authenticated values. The destination must support EIP-2537 and
transient storage. Read the
[light-client interfaces](https://docs.metalntwx.com/reference/interop/light-clients.md#metal-light-client)
for these checks. `MetalLightClient` accepts certificates only for epoch `0`,
so check the source network's current certificate epoch before sending.

The receiving contract must require both `msg.sender == inbox` and
`IMetalInbox(inbox).metalSender() == authorizedSourceSender` inside its
dispatched function. Its application state or event must identify the intended
action. `metalSender()` is zero outside execution.

## Record the message on Metal

Use the Metal outbox address in
[protocol contracts](https://docs.metalntwx.com/reference/contract-addresses.md) and the
[outbox ABI](https://docs.metalntwx.com/reference/interop/messages.md#send-from-metal). Encode the receiving
contract's function call with its ABI, then call
`send(destinationChainId, target, payload)`. The destination chain ID is a
`uint64`. `target` is the receiving contract, and `payload` contains its exact
function selector and arguments.

For a viem application, install the public dependency with `pnpm add viem`.
Encode the action using the receiving contract's published ABI:

```typescript theme={null}
import { encodeFunctionData, parseAbi } from 'viem'

const outboxAbi = parseAbi([
  'function send(uint64 destinationChainId, address target, bytes payload) returns (uint256 nonce)',
  'event MessageSent(uint256 indexed nonce, uint64 indexed destinationChainId, address indexed target, address sender, bytes payload)',
])

const payload = encodeFunctionData({
  abi: receiverAbi,
  functionName: receiverFunction,
  args: receiverArguments,
})
const data = encodeFunctionData({
  abi: outboxAbi,
  functionName: 'send',
  args: [destinationChainId, receiverAddress, payload],
})
```

`receiverAbi`, `receiverFunction`, and `receiverArguments` describe the
intended call. Submit `data` to the outbox from the authorized source sender. If
the receiving contract authorizes a source contract, that contract must call the
outbox. A direct call from an externally owned account authenticates that
account. When an application payment and message must succeed together, perform
both in the same source transaction.

Save the signed transaction and its hash before broadcast. Decode `MessageSent`
from the exact outbox address in the successful receipt and match its
`destinationChainId`, `target`, `sender` and `payload` to the intended
message. Save the emitted nonce, receipt block number and block hash, then check
[canonical Metal finality](https://docs.metalntwx.com/reference/network-fundamentals/settlement.md). Use the nonce from the
event, since concurrent sends can change a simulation's predicted return value.

## Select an accepted checkpoint

Read `latestBlockHash()` and then `checkpoint(hash)` on the destination light
client. An accepted checkpoint covers the message when its Metal block number is
at least the message's source block number. If no accepted checkpoint covers it,
read Metal's finalized head with `consensus_status` and fetch
`metal_getFinalityCertificate` for a covering finalized block using the
[certificate procedure](https://docs.metalntwx.com/reference/interop/finality-certificates.md#fetch-a-certificate).

Require a non-null certificate, epoch zero, and
`keccak256(headerRlp) == blockHash`. Submit `update` using the response's
`headerRlp`, `epoch`, `view`, `parentView`, `signersBitmap`, and
`signatureEipUncompressed`, in that order. Both block height and view must
advance the light client's latest values. Save the signed destination
transaction before broadcast, check successful execution and
`isAccepted(blockHash)`, then apply destination finality. One checkpoint can
cover many messages.

A null certificate can reflect delayed persistence or pruning. Retry for a
recent finalized block or another endpoint. If another relayer advances the
light client first, check whether the checkpoint hash was accepted. Otherwise,
select a later accepted checkpoint and build its proof. Preserve the chosen hash
and returned `interopMessageRoot` with the delivery record.

## Build the message proof

Retrieve every `MessageSent` event from the Metal outbox from nonce zero through
the checkpoint block, including messages for other destinations. Paginate block
ranges to respect RPC limits. Verify the canonical block hashes, sort by nonce,
and reject gaps or duplicates. The leaf count is the number of all messages at
that block. Obtain missing event history from an indexer or RPC endpoint before
attempting delivery.

Each leaf uses ABI encoding of the full message:

```typescript theme={null}
import {
  concatHex, encodeAbiParameters, keccak256, parseAbiParameters, toHex,
} from 'viem'

const leaf = keccak256(encodeAbiParameters(
  parseAbiParameters('uint256,uint256,uint256,address,address,bytes'),
  [metalChainId, destinationChainId, nonce, sender, target, payload],
))
const emptyLeaf = toHex(0n, { size: 32 })
const pair = (left: `0x${string}`, right: `0x${string}`) =>
  keccak256(concatHex([left, right]))
```

Build a depth-32 binary tree whose leaf at index `nonce` is that commitment.
Missing leaves are `emptyLeaf`. At each level, hash the left and right child in
that order. The parent of two empty subtrees is `pair(empty, empty)`. For a
sparse implementation, store occupied nodes and derive missing siblings from the
hashes of empty subtrees at each level.

For the requested nonce, record its sibling at each of the 32 levels, starting
at the leaf level. Divide the index by two after each level. To verify a path,
hash `pair(node, sibling)` for an even index and `pair(sibling, node)` for an
odd index. The message root binds the tree root and leaf count and must equal
the checkpoint's `interopMessageRoot`:

```typescript theme={null}
const messageRoot = keccak256(concatHex([
  treeRoot,
  toHex(leafCount, { size: 32 }),
]))
if (messageRoot !== checkpoint.interopMessageRoot) {
  throw new Error('Event history or proof does not match the checkpoint')
}
```

Here `treeRoot` is the depth-32 root and `leafCount` is a bigint. Require
`nonce < leafCount`. Changing the checkpoint requires a matching leaf count and
path, even when the source message is unchanged. An old accepted checkpoint and
its original proof remain usable while the nonce is unconsumed.

## Execute and verify the destination effect

Check `MetalInbox.consumed(nonce)`. If false, simulate and submit
[`execute`](https://docs.metalntwx.com/reference/interop/messages#execute-on-the-destination-chain) with
`checkpointBlockHash`, `nonce`, `sender`, `target`, `payload`, `leafCount`,
and the 32-element `path`, in that order. The relayer submits this
destination transaction from a signing account funded for that chain's
transaction fees. Save its signed bytes and hash before broadcast.

Require a successful receipt and the inbox's `MessageExecuted` with the expected
`nonce`, `target` and `sender`. Check `consumed(nonce) == true`, then read
the receiving contract's event or state to verify the intended application
effect. Confirm the receipt's canonical block and destination finality before
marking the delivery complete.

Key the delivery record by destination chain ID, inbox address and nonce. Retain
the source chain ID and outbox address with the original message fields,
checkpoint and transaction attempts. If a response is lost, look up the saved
hash and consumed state before rebroadcasting the same signed transaction.
Retrying delivery keeps the original message nonce, while a new `send` records a
separate instruction.

## Recover and test rejection

| Result | Recovery |
| - | - |
| Unknown checkpoint | Select or submit an accepted covering checkpoint and build its proof |
| `NotInTree` | Check both chain IDs, complete event history, fields, leaf count and path |
| `AlreadyConsumed` | Recover the existing execution and verify its application effect and finality |
| `TargetReverted` | Resolve the receiving contract's failure condition, then retry the same message |

A receiving contract's revert rolls back the application call and consumed flag.
Track retries per message because the inbox allows other nonces to execute
first. Define any ordering, expiry, cancellation and acknowledgement rules in
the application protocol.

Test the route with a disposable receiving contract. Alter the payload and a
proof sibling and confirm rejection leaves state unchanged. Cause the receiving
contract to revert, confirm `consumed` stays false, remove that condition, and
deliver the same message. After success, replay must fail without repeating its
effect. Record the final destination state and transaction hashes to test
recovery after a relayer restart.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.