Great work. We can now pack multiple values into a message. In this section, we will learn how to encode the function name and depending on that its parameters in the message. Let's imagine a more advanced calculator that not only has a single add
function but also a concatenate
function, that lets you concatenate two strings.
For the add function we need to encode two numbers. For the concatenate function we need to encode two strings. How can we go about this? The concept is easy: It's like packing an envelope into another envelope:
The first step is to create a CalculatorAction
enum that specifies the different functions that can be called on the calculator.
src/2b-invoking-functions/CalculatorActions.sol pragma solidity ^0.8.18 ;
enum CalculatorAction {
add ,
concatenate
}
In the next step we can add this to our encode helpers
in the sender contract:
src/2b-invoking-functions/CalculatorSenderOnCChain.sol pragma solidity ^0.8.18 ;
import "@teleporter/ITeleporterMessenger.sol" ;
import "./CalculatorActions.sol" ;
contract CalculatorSenderOnCChain {
ITeleporterMessenger public immutable teleporterMessenger =
ITeleporterMessenger ( 0x253b2784c75e510dD0fF1da844684a1aC0aa5fcf );
function sendAddMessage ( address destinationAddress , uint256 num1 , uint256 num2 ) external {
teleporterMessenger. sendCrossChainMessage (
TeleporterMessageInput ({
// Replace with chain id of your Avalanche L1 (see instructions in Readme)
destinationBlockchainID : 0x92756d698399805f0088fc07fc42af47c67e1d38c576667ac6c7031b8df05293 ,
destinationAddress : destinationAddress,
feeInfo : TeleporterFeeInfo ({feeTokenAddress : address ( 0 ), amount : 0 }),
requiredGasLimit : 100000 ,
allowedRelayerAddresses : new address []( 0 ),
message : encodeAddData (num1, num2)
})
);
}
function sendConcatenateMessage ( address destinationAddress , string memory text1 , string memory text2 ) external {
teleporterMessenger. sendCrossChainMessage (
TeleporterMessageInput ({
// Replace with chain id of your Avalanche L1 (see instructions in Readme)
destinationBlockchainID : 0x382d2a20c299b03b638dd4d42b96e7401f6c3e88209b764abce83cf71c0c30cd ,
destinationAddress : destinationAddress,
feeInfo : TeleporterFeeInfo ({feeTokenAddress : address ( 0 ), amount : 0 }),
requiredGasLimit : 100000 ,
allowedRelayerAddresses : new address []( 0 ),
message : encodeConcatenateData (text1, text2)
})
);
}
// Encode helpers
function encodeAddData ( uint256 a , uint256 b ) public pure returns ( bytes memory ) {
bytes memory paramsData = abi . encode (a, b);
return abi . encode (CalculatorAction.add, paramsData);
}
function encodeConcatenateData ( string memory a , string memory b ) public pure returns ( bytes memory ) {
bytes memory paramsData = abi . encode (a, b);
return abi . encode (CalculatorAction.concatenate, paramsData);
}
}
As you can see here we are calling abi.encode
twice in the encode helpers. The first time we encode the function paramters and the second time we encode the function name with the byte array containing paramters.
Let's now look at the receiver:
src/2b-invoking-functions/CalculatorReceiverOnSubnet.sol pragma solidity ^0.8.18 ;
import "@teleporter/ITeleporterMessenger.sol" ;
import "@teleporter/ITeleporterReceiver.sol" ;
import "./CalculatorActions.sol" ;
contract CalculatorReceiverOnSubnet is ITeleporterReceiver {
ITeleporterMessenger public immutable teleporterMessenger =
ITeleporterMessenger ( 0x253b2784c75e510dD0fF1da844684a1aC0aa5fcf );
uint256 public result_num;
string public result_string;
function receiveTeleporterMessage ( bytes32 , address , bytes calldata message ) external {
// Only the Interchain Messaging receiver can deliver a message.
require (
msg.sender == address (teleporterMessenger), "CalculatorReceiverOnSubnet: unauthorized TeleporterMessenger"
);
// Decoding the Action type:
(CalculatorAction actionType, bytes memory paramsData) = abi . decode (message, (CalculatorAction, bytes ));
// Route to the appropriate function.
if (actionType == CalculatorAction.add) {
( uint256 a, uint256 b) = abi . decode (paramsData, ( uint256 , uint256 ));
_calculatorAdd (a, b);
} else if (actionType == ...) {
( string memory text1, string memory text2) = abi . decode (paramsData, ( string , string ));
_calculatorConcatenateStrings (text1, text2);
} else {
revert ( "CalculatorReceiverOnSubnet: invalid action" );
}
}
function _calculatorAdd ( uint256 _num1, uint256 _num2) internal {
result_num = _num1 + _num2;
}
function _calculatorConcatenateStrings ( string memory str1 , string memory str2 ) internal {
bytes memory str1Bytes = bytes (str1);
bytes memory str2Bytes = bytes (str2);
bytes memory combined = new bytes (str1Bytes.length + str2Bytes.length + 1 );
for ( uint256 i = 0 ; i < str1Bytes.length; i ++ ) {
combined[i] = str1Bytes[i];
}
combined[str1Bytes.length] = " " ;
for ( uint256 i = 0 ; i < str2Bytes.length; i ++ ) {
combined[str1Bytes.length + i + 1 ] = str2Bytes[i];
}
result_string = string (combined);
}
}
You can see that we first decode the CalculatorAction
enum:
// Decoding the Action type:
(CalculatorAction actionType, bytes memory paramsData) = abi . decode (message, (CalculatorAction, bytes ));
Then based on the function name we decide how to unpack the paramters
// Route to the appropriate function.
if (actionType == CalculatorAction.add) {
( uint256 a, uint256 b) = abi . decode (paramsData, ( uint256 , uint256 ));
_calculatorAdd (a, b);
} else if (actionType == ...) {
( string memory text1, string memory text2) = abi . decode (paramsData, ( string , string ));
_calculatorConcatenateStrings (text1, text2);
} else {
revert ( "CalculatorReceiverOnSubnet: invalid action" );
}
For the add
function we decode two numbers and for the concatenate
function we decode two strings. After the decoding we call the appropriate internal function.
Deploy the sender and receiver contracts and try out the add
and concatenate
functions.
Don't forget to replace BOTH destinationBlockchainIDs in CalculatorSenderOnCChain with the Blockchain ID (HEX) from your Avalanche L1!
forge create --rpc-url local-c --private-key $PK src/2b-invoking-functions/CalculatorSenderOnCChain.sol:CalculatorSenderOnCChain
[⠃] Compiling...
[⠆] Compiling 2 files with Solc 0.8.18
[⠰] Solc 0.8.18 finished in 240.23ms
Compiler run successful!
Deployer: 0x8db97C7cEcE249c2b98bDC0226Cc4C2A57BF52FC
Deployed to: 0x8B3BC4270BE2abbB25BC04717830bd1Cc493a461
Transaction hash: 0xf9cce28a714764bb265bba7522bfd10d620fa0cb0f5dae26de2ac773b0a878ee
export SENDER_ADDRESS = 0x8B3BC4270BE2abbB25BC04717830bd1Cc493a461
forge create --rpc-url myblockchain --private-key $PK src/2b-invoking-functions/CalculatorReceiverOnSubnet.sol:CalculatorReceiverOnSubnet
[⠊] Compiling...
[⠢] Compiling 1 files with Solc 0.8.18
[⠆] Solc 0.8.18 finished in 148.40ms
Compiler run successful!
Deployer: 0x8db97C7cEcE249c2b98bDC0226Cc4C2A57BF52FC
Deployed to: 0x5DB9A7629912EBF95876228C24A848de0bfB43A9
Transaction hash: 0xa8efb88abfef486d2caba30cb4146b1dc56a0ee88c7fb4c46adccdf1414ae39e
export RECEIVER_ADDRESS = 0x5DB9A7629912EBF95876228C24A848de0bfB43A9
Now you can call the sendAddMessage
and sendConcatenateMessage
functions on the sender contract and see the results on the receiver contract.
cast send --rpc-url local-c --private-key $PK $SENDER_ADDRESS "sendAddMessage(address, uint, uint)" $RECEIVER_ADDRESS 1 2
cast call --rpc-url myblockchain $RECEIVER_ADDRESS "result_num()(uint)"