Portal Community

The EVM2AnyMessage Tuple

Every CCIP message is, on-chain, a Solidity struct called Client.EVM2AnyMessage: receiver address, arbitrary data payload, an array of token transfers, a fee token, and extraArgs. message/build and router/getFee both assemble this same tuple from the same input fields — that's why their field lists overlap almost completely (see Message Operations and Router Operations).

Output shape — a JSON array of raw values, not a single encoded blob. message/build's encodedMessage output is a JSON array of the tuple's raw positional values (receiver/data/tokenAmounts/feeToken/extraArgs), not one pre-abi.encode()'d blob. This is deliberate: the Ethereum ExecutionNode's own contract-write step ABI-encodes a tuple-typed function argument from a nested JSON array of its component values — a single opaque blob wouldn't compose with that delegation path. This is what makes "Chainlink builds the message → Ethereum sends it" work end-to-end as two separate workflow steps.

extraArgs: v1 vs. genericV2

extraArgs is a 4-byte tag selector followed by ABI-encoded parameters. Two versions are supported, both using Chainlink's own published, fixed tag values:

VersionTagBody
v1 (EVMExtraArgsV1)0x97a657c9abi.encode(uint256 gasLimit)
genericV2 (GenericExtraArgsV2, default)0x181dcf10abi.encode(uint256 gasLimit, bool allowOutOfOrderExecution)

Use genericV2 (the default) unless a specific destination lane requires v1. allowOutOfOrderExecution only applies to genericV2 and is silently ignored for v1.

Fee Tokens: Empty Means Native Gas Token

Leave feeToken empty/omitted to pay the CCIP fee in the destination chain's native gas token — this node encodes that as the zero address (0x0000000000000000000000000000000000000000). router/getFee's output echoes back this resolved address, not your raw input, so you can always see exactly which token address was actually used for the fee calculation.

Addresses Are Validated Before They Reach the ABI Encoder

Every address this node encodes (receiverAddress, feeToken, each tokenAmounts[].token) is validated as exactly 0x + 40 hex characters before being passed to Nethereum's ABI encoder. This matters because Nethereum's own encoder does not reliably reject a malformed address — a too-short address like 0x123 is silently accepted with no error, which would otherwise produce a corrupted encoded message instead of a clear failure. See Troubleshooting.

Chain Selectors vs. Native Chain IDs

Covered in full on Networks: every chain has both a native EVM chain ID and a separate CCIP chain selector, and this node carries both as strings (never numbers) throughout its config and output contract to avoid JavaScript's safe-integer precision loss.

Token Amounts Are Strings Too

A tokenAmounts[].amount is a decimal string representing a uint256 wei-denominated value — e.g. "1000000000000000000" for 1 token at 18 decimals — never a JSON number, for the same precision reason as chain selectors.

This Node Never Sends a Message

message/build stops at producing the encoded tuple and extraArgs bytes. Actually calling ccipSend(destinationChainSelector, message) on the source chain's Router contract — the step that assigns the real messageId and moves funds/data — is performed by the Ethereum ExecutionNode's own contract-write operation, using this node's output as its input. This node holds no wallet and signs nothing.