hashgraph / hedera-sdk-js

Hedera™ Hashgraph SDK for JavaScript/TypeScript
https://docs.hedera.com/guides/docs/sdks
Apache License 2.0
270 stars 144 forks source link

ContractFunctionParameters fails with complex types like boolean arrays #2541

Open MiguelLZPF opened 1 month ago

MiguelLZPF commented 1 month ago

Description

We encountered an issue using ContractFunctionParameters().add(). While it works for most basic types, it fails when dealing with complex types, such as an array of booleans, and doesn't generate the correct structure.

We were unable to find any documentation or examples dealing with complex types like arrays of booleans. If there is a way to correctly handle these complex types using ContractFunctionParameters, it is not clearly documented.

Expected Behavior

ContractFunctionParameters should correctly handle complex types such as boolean arrays.

Actual Behavior

ContractFunctionParameters fails to generate the correct structure for complex types.

Steps to reproduce

Example Code:

Here's the code we initially tried:

// Create ContractFunctionParameters and add the parameters
// Convert boolean array to byte array
const byteArray = actives.map((val) => (val ? 0x01 : 0x00));
const functionParameters = new ContractFunctionParameters()
  .addBytes32Array(
    roles.map((role) => new Uint8Array(Buffer.from(role, 'hex'))),
  )
  .addBytes(Buffer.from(byteArray))
  .addAddress(targetId.toString());
const transaction = new ContractExecuteTransaction()
  .setContractId(address.toContractId().toString())
  .setGas(gas)
  .setFunction(FUNCTION_NAME, functionParameters);

We had to switch to the following approach to make it work:

```typescript
const functionDataEncodedHex = factoryInstance.interface.encodeFunctionData(
  FUNCTION_NAME,
  [roles, actives, targetId.toString()],
);
const functionDataEncoded = new Uint8Array(
  Buffer.from(functionDataEncodedHex.slice(2), 'hex'),
);
const transaction = new ContractExecuteTransaction()
  .setContractId(address.toContractId().toString())
  .setGas(gas)
  .setFunctionParameters(functionDataEncoded);

Additional context

We checked the available documentation, but only found standard examples for basic contract calls.

Hedera network

testnet

Version

v2.33.0

Operating system

Linux

ivaylonikolov7 commented 1 week ago

ContractFunctionParameters but rather with how the bytes data is being generated. As you may be aware, Ethereum utilizes a structure called "calldata," which encodes the function parameters into bytes when they are transferred to the EVM. The first 4 bytes of calldata represent the function selector, followed by the function parameters, which are encoded into 32-byte "slots."

In the example you provided, when generating the byte array, you are producing only 1 byte for each true/false value without the necessary padding. For instance, you are generating 01 (1 byte) when, in practice, you should be generating 0000000000000000000000000000000000000000000000000000000000000001 (32 bytes).

To assist you further, I’ve created an example that demonstrates the correct way to encode boolean arrays. While the SDK provides several helper functions to generate EVM-compatible bytes, it doesn’t cover every possible scenario. In such cases, external npm packages like ethers can be very helpful. Below is an example of how you can use ethers to properly encode your boolean array:

import { Interface, ethers } from "ethers";

async function contractFunctions() {
  const actives = [true, false, true, true, false];

  const abiCoder = new ethers.AbiCoder();
  const params = abiCoder.encode(["bool[]"], [actives]);
  const paramsBytes = hexToBytes(params.slice(2)).toString("hex");

  console.log(paramsBytes);
}

void contractFunctions();

function hexToBytes(hex) {
  let bytes = [];
  for (let c = 0; c < hex.length; c += 2)
    bytes.push(parseInt(hex.substr(c, 2), 16));
  return bytes;
}

This method ensures that the bytes generated for the contract function parameters are correctly padded and aligned as required by the EVM.