Send a Cross-Chain Message
Quick start (Solidity)
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.28;
import {IConceroRouter} from "../interfaces/IConceroRouter.sol";
import {MessageCodec} from "../common/libraries/MessageCodec.sol";
contract ConceroQuickStartSender {
IConceroRouter internal immutable i_conceroRouter;
constructor(address conceroRouter) {
i_conceroRouter = IConceroRouter(conceroRouter);
}
function sendPing(
uint24 dstChainSelector,
address receiverOnDst,
uint32 gasLimitOnDst,
address relayerLib,
bytes calldata relayerConfig, // can be empty
address[] calldata validatorLibs,
bytes[] calldata validatorConfigs, // can be empty
string calldata text
) external payable returns (bytes32 messageId) {
// 1) Canonical packed EVM destination data: receiver (20 bytes) + gasLimit (uint32)
bytes memory dstChainData =
MessageCodec.encodeEvmDstChainData(receiverOnDst, gasLimitOnDst);
// 2) Build MessageRequest (fields aligned with your MessageCodec)
IConceroRouter.MessageRequest memory req = IConceroRouter.MessageRequest({
dstChainSelector: dstChainSelector,
dstChainData: dstChainData,
srcBlockConfirmations: 0, // set if you use it
feeToken: address(0), // native fees
validatorLibs: validatorLibs,
validatorConfigs: validatorConfigs,
relayerLib: relayerLib, // per-message relayer selection
relayerConfig: relayerConfig,
payload: bytes(text)
});
// 3) Quote total fee (relayer + validators), then send
uint256 fee = i_conceroRouter.getMessageFee(req);
messageId = i_conceroRouter.conceroSend{value: fee}(req);
}
}MessageRequest
Your request is the single source of truth for pricing, receipt packing, relayer selection, and validator setup.
At minimum (based on current packing logic) the router expects fields equivalent to:
struct MessageRequest {
/// @notice Which chain the message should be delivered to.
/// @dev Concero uses chain selectors (not chain IDs) to route delivery.
uint24 dstChainSelector;
/// @notice Destination-specific execution parameters.
/// @dev For EVM destinations this defines:
/// - who should be called on the destination (receiver)
/// - how much gas to give that call (gasLimit)
/// This is what makes the message actually executable on the target chain.
bytes dstChainData;
/// @notice How many source-chain block confirmations you want to wait for before the message
/// is considered safe enough to deliver (reorg protection).
/// @dev Set based on your security needs: higher confirmations = safer, but slower delivery.
uint64 srcBlockConfirmations;
/// @notice Which token you pay fees in.
/// @dev Currently supported: native only (address(0)).
address feeToken;
/// @notice Which validator modules you want to use to attest/verify this message.
/// @dev You choose the security model per message by choosing the validator libs.
address[] validatorLibs;
/// @notice Configuration for each validator module.
/// @dev One config per validator lib (must match validatorLibs length).
/// Typical examples: validator gas limits, thresholds, or other validator-specific params.
bytes[] validatorConfigs;
/// @notice The relayer module you want to deliver this message.
/// @dev This is a per-message choice. If the user selects a relayer, only that relayer
/// (per its own rules) is expected/allowed to submit the delivery.
address relayerLib;
/// @notice Relayer-specific configuration for this message.
/// @dev Optional opaque bytes interpreted only by the chosen relayer implementation.
/// Examples: service tier, delivery options, custom routing hints (depends on relayer).
bytes relayerConfig;
/// @notice The message data that will be delivered to the destination chain.
/// @dev This is arbitrary bytes. Concero does not impose a format here:
/// - it can be raw bytes (e.g. "hello")
/// - it can be an ABI-encoded struct
/// - it can be a custom serialization your app understands
/// The destination-side receiver decides how to interpret it.
bytes payload;
}Quote the fee
Call getMessageFee(req) before sending, then pass the returned value as msg.value when paying in native.
function getMessageFee(
MessageRequest calldata messageRequest
) external view returns (uint256 totalFee);What’s inside:
- relayer fee (computed by the chosen
relayerLib) - + validator fees (computed by validator libs)
Send the message
function conceroSend(
MessageRequest calldata messageRequest
) external payable returns (bytes32 messageId);The router builds a packed receipt via MessageCodec.toMessageReceiptBytes(...) and returns messageId = keccak256(packedReceipt).
Events to index
ConceroMessageSent
Emitted on the source chain; relayers watch this and pick messages where relayerLib == theirRelayerLib.
event ConceroMessageSent(
bytes32 indexed messageId,
bytes packedMessageReceipt,
address[] validatorLibs,
address relayerLib
);EVM destination chain data
Concero’s canonical EVM dstChainData format is:
[0:20] receiver (address)[20:24] dstGasLimit (uint32)
Use:
bytes memory dstChainData = MessageCodec.encodeEvmDstChainData(receiver, gasLimit);And to decode (off-chain or in contracts that need it):
(address r, uint32 gl) = MessageCodec.decodeEvmDstChainData(dstChainData);