티스토리 뷰

개발 부스러기는 완결된 형식의 글이 아닌, 다양한 시행착오를 기록하는 글입니다.

1. 함수 시그니처란?

 다음과 같이 정의된 함수에서

function transfer(address to, uint256 amount) external {
    ...
}

 함수의 이름과 공백 없이 콤마(,)로 연결 파라미터의 타입들을 소괄호로 묶은 문자열을 연결한 것을 함수 시그니처(function signature)라고 한다.

transfer(address,uint256)

 함수 시그니처는 ABI(Application Binary Interface)를 파싱 할 때 사용하거나, 함수 선택자(function selector)를 계산하기 위해 사용된다. 함수 선택자는 함수 시그니처를 입력으로 한 keccack256 함수의 결괏값의 상위 4바이트를 가지는 값이며, 이 값은 스마트 컨트랙트의 실행 과정에서 실행할 함수를 식별하기 위해 사용된다.

bytes4(keccak256(bytes("transfer(address,uint256)")))

이번에는 함수 시그니처를 ABI로 파싱하는 방법을 살펴보자.


2. 함수 시그니처를 ABI로 파싱하기

 viem 라이브러리를 사용하면 손쉽게 진행할 수 있다. 함수 시그니처 앞에 'function 키워드를 붙여야 한다. 이벤트의 경우는 'event, 커스텀 에러의 경우는 'error'를 붙여서 파싱 할 수 있다.

import { parseAbi } from "viem";

const abi = parseAbi(["function transfer(address,uint256)"]);

console.log(abi[0]);
// {
//   name: 'transfer',
//   type: 'function',
//   stateMutability: 'nonpayable',
//   inputs: [ { type: 'address' }, { type: 'uint256' } ],
//   outputs: []
// }

 Solidity 코드 상에서 함수 선택자를 계산하기 위해 필요한 함수 선택자에 비해, 파라미터의 이름이나 statusMutability 등을 포함하여 파싱할 수도 있다.

import { parseAbi } from "viem";

const abi = parseAbi(["function transfer(address to, uint256 amount) payable"]);

console.log(abi[0]);

// {
//     name: 'transfer',
//     type: 'function',
//     stateMutability: 'payable',
//     inputs: [
//       { type: 'address', name: 'to' },
//       { type: 'uint256', name: 'amount' }
//     ],
//     outputs: []
//   }

 이를 활용하여 다음과 같이 함수 시그니처를 파싱하여 ABI를 구하고, 이를 Form 컴포넌트로 전달하여 동적으로 입력 필드를 구성할 수 있다.

const installDataAbi = useMemo(() => {
  if (!moduleMetadata) return [];

  let signature = moduleMetadata.installDataSignature;
  // check if signature has 'function' prefix
  if (!signature.startsWith("function")) {
    signature = `function ${signature}`;
  }

  try {
    const abi: Abi = parseAbi([signature]);
    return abi;
  } catch (error) {
    console.error("Error parsing install data signature", error);
    return [];
  }
}, [moduleMetadata]);
{installDataAbi.length > 0 && (
  <InstallDataForm abi={installDataAbi} />
)}

3. ABI를 기반으로 동적으로 입력 필드 구성하기

 컴포넌트로 전달된 ABI에서 AbiFunction 타입의 functionAbi를 추출한다.

if (abi.length === 0 || abi[0].type !== "function") {
  return <Text>No valid install data ABI found.</Text>;
}

const functionAbi = abi[0] as Extract<Abi[0], { type: "function" }>;

 functionAbi의 inputs 필드의 모든 값을 사용해 입력 필드를 구성한다.

{
  functionAbi.inputs.map((input, index) => (
    <FormControl key={index}>
      <FormLabel>{input.name || `Input ${index + 1}`}</FormLabel>
      <Input
        type="text"
        onChange={(e) => handleInputChange(input.name || "", e.target.value)}
        placeholder={input.type}
      />
    </FormControl>
  ));
}

 이 때, useState 훅을 사용해 상태를 저장한다.

const [formValues, setFormValues] = useState<Record<string, string>>({});

const handleInputChange = (name: string, value: string) => {
  setFormValues((prev) => ({ ...prev, [name]: value }));
};

 데이터를 인코딩하기 위해 버튼을 클릭하면 'handleSubmit' 함수가 실행된다. 우선 입력값들을 각 타입에 맞게 타입을 변환하여 배열 구조로 values 값에 저장한다. 그리고 AbiParameter 타입의 배열인 functionAbi.inputs를 첫 번째 인자로, 변환된 입력값의 배열 values를 두 번째 인자로 전달하여 encodeAbiParameters 함수를 실행한다. 그 결괏값을 상태 변수에 저장한다.

const [encodedData, setEncodedData] = useState<`0x${string}` | null>(null);

const handleSubmit = () => {
  try {
    const functionAbi = abi[0];
    if (functionAbi.type !== "function") {
      throw new Error("Invalid ABI: first element is not a function");
    }

    const values = functionAbi.inputs.map((input) => {
      const value = formValues[input.name || ""];
      return parseInputValue(input, value);
    });

    const encodedData = encodeAbiParameters(functionAbi.inputs, values);

    setEncodedData(encodedData);
  } catch (error) {
    console.log(error);
  }
};

const parseInputValue = (input: AbiParameter, value: string): any => {
  switch (input.type) {
    case "uint256":
    case "int256":
      return BigInt(value);
    case "bool":
      return value.toLowerCase() === "true";
    case "address":
      return value as `0x${string}`;
    // Add more cases for other types as needed
    default:
      return value;
  }
};

4. 결과물

 

Fuzion

 

zk-sync-fuzion.vercel.app


참고

 

Viem · TypeScript Interface for Ethereum

Build reliable Ethereum apps & libraries with lightweight, composable, & type-safe modules from viem.

viem.sh

 

Viem · TypeScript Interface for Ethereum

Build reliable Ethereum apps & libraries with lightweight, composable, & type-safe modules from viem.

viem.sh

 

최근에 올라온 글
최근에 달린 댓글
«   2025/01   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31
Total
Today
Yesterday
글 보관함