Skip to main content

Transaction data encoding

If you're sending transactions on the Immutable zkEVM using the Game SDKs or the Typescript SDK, you need to encode the data parameters correctly so the smart contract can execute the payload.

This guide will help you understand how to encode the data and execute smart contract interactions.
Transaction data encodingTransaction data encoding
💡WHO IS THIS FOR?
Developers sending transactions on the Immutable zkEVM Testnet using the Game SDKs or the Typescript SDK.

Encoding the data property

The data property in eth_sendTransaction is a hexadecimal string that represents the encoded input data for the transaction. Proper encoding is crucial for the transaction to be executed correctly. Here's how to encode the data property for different scenarios:

1. Function Signature

  • The first 4 bytes of the data property are typically the function signature.
  • This is calculated as the first 4 bytes of the Keccak-256 hash of the function name and its parameter types.
  • Example: For transfer(address,uint256), the signature would be 0xa9059cbb.

2. Parameter Encoding

  • Parameters are encoded according to the Ethereum ABI specification.
  • Each parameter is padded to 32 bytes (256 bits).
  • Address types are left-padded with zeros.
  • Integer types are big-endian encoded and left-padded with zeros.
  • Strings and bytes are right-padded with zeros.

This guide only covers the very basic encoding of function signatures and parameters. For more complex data structures, refer to the Solidity ABI Specification.

2a. Using ethers.js for Encoding

The ethers.js library provides utilities for encoding:

const ABI = ['function transfer(address to, uint256 amount)'];
const interface = new ethers.utils.Interface(ABI);
const data = interface.encodeFunctionData('transfer', [recipientAddress, amount]);

2b. Manual Encoding

For simple cases, you can concatenate the function signature and encoded parameters:

const functionSignature = '0xa9059cbb';

const encodedAddress = ethers.utils.defaultAbiCoder.encode(['address'], [recipientAddress]).slice(2);
// Might result in: '4a9045efa0ca935ff426c54a4b92de6d3bf4b0ff'

const encodedAmount = ethers.utils.defaultAbiCoder.encode(['uint256'], [amount]).slice(2); // amount = 123
// Results in: '000000000000000000000000000000000000000000000000000000000000007b'

const data = functionSignature + encodedAddress + encodedAmount;
// Results in: '0xeea987680000000000000000000000004a9045efa0ca935ff426c54a4b92de6d3bf4b0ff000000000000000000000000000000000000000000000000000000000000007b'

This example uses ethers.js to encode the data, but you can also encode values manually using Decimal to Hexadecimal converter.

💡Why slice(2)

The slice(2) function is used to remove the 0x prefix from the encoded data so they are not included when concatinating the strings for the final data result.

2c. Handling Dynamic Types

Dynamic types in Solidity, such as arrays and strings, require special handling during ABI encoding:

  • Offset: For dynamic types, the main data area contains a pointer (offset) to where the actual data is stored.
  • Length: The actual data is prefixed with its length.

This two-step process allows for efficient encoding of complex data structures.

Example: Encoding a String

Let's say we have a function setMessage(string message):

  1. The function signature would be encoded first.
  2. Then, a 32-byte offset pointing to the start of the string data.
  3. At that offset, we'd have:
    • 32 bytes representing the length of the string
    • The actual string data, padded to a multiple of 32 bytes
const ABI = ['function setMessage(string message)'];
const interface = new ethers.utils.Interface(ABI);
const message = "Hello, World!";
const data = interface.encodeFunctionData('setMessage', [message]);
console.log(data);
// 0x368b87720000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000d48656c6c6f2c20576f726c642100000000000000000000000000000000000000000000

// Breakdown:
// 0x368b8772 // Function signature
// 0000000000000000000000000000000000000000000000000000000000000020 // Offset to string data
// 000000000000000000000000000000000000000000000000000000000000000d // Length of string
// 48656c6c6f2c20576f726c642100000000000000000000000000000000000000000000 // Encoded string

3. Example Usage

The following examples are for demonstration purposes and may not work as-is in your project. Please adapt them to your specific requirements.

💡Transaction parameters

The send transaction methods only take 3 parameters, to, data, value. Other method properties typically found in a standard Ethereum transaction object are abstracted away from the user.

Also note, the from parameter is automatically sent with the users Passport wallet address.

Unity SDK

Please refer to the Unity SDK documentation for setting up the Unity SDK.

using Immutable.Passport.Model;

TransactionRequest request = new TransactionRequest()
{
to = "0x...", // The contract address
value = 0,
data = "0x0x368b8772..." // The encoded data
}
string transactionHash = await passport.ZkEvmSendTransaction(request);

Typescript SDK

This requires a connected provider. Please refer to the Immutable Passport docs for an example of conecting a wallet.

const transactionHash = await provider.request({
method: 'eth_sendTransaction',
params: [
{
to: '0x...', // The contract address
data: '0x0x368b8772...', // The encoded data
value: 0
}
]
});
console.log(transactionHash); // ['0x...']

Related content