React 컴포넌트에서 컨트랙트를 호출하는 다음과 같은 코드가 있다고 가정해봅시다.
function buildCreateTokenTransaction({
name,
symbol,
caipChainID,
creatorAddress,
}: {
caipChainID: CAIPChainID;
name: string;
symbol: string;
creatorAddress: ChecksumAddress;
}) {
const creationFee = getTokenCreationFee(caipChainID);
return {
to: getBondContractAddress(caipChainID),
value: creationFee,
data: encodeCreateTokenCallData(creatorAddress, {
name,
symbol,
}),
};
}
function Component() {
const sendTransaction = useSendTransaction(); // wagmi
return (
<form action={async (formData) => {
const { caipChainId, tokenContractAddress, amount, ... } = parseFormData(formData);
const transaction = buildMintTokenTransaction({
caipChainID: caipChainId,
tokenContractAddress,
seiAmount: amount,
mintMinimumAmount: minAmountOut,
receiverAddress: toAddress,
});
sendTransaction(transaction)
}}>
...
</form>
)
}
이런 코드 패턴에서 buildCreateTokenTransaction
는 컨트랙트의 ABI를 보장하지 못합니다.
해당 함수에서 정의한 타입 인터페이스는 그냥 TypeScript 레벨에서 정의한 것 뿐이고, ABI로부터 파생된 정보가 아니기 때문입니다.
대신 viem의 getContract
함수를 사용하면 ABI와 연동되는 타입안전한 컨트렉트 호출이 가능합니다.
다음과 같이 작업할 수 있습니다:
function useContract() {
const client = useWalletClient();
return getContract({
abi: [...ABI],
address: `0x....`,
client,
});
}
function Component() {
const contract = getContract();
return (
<form action={(formData) => contract.write.createToken(parseFormData(formData))}>
{...}
</form>
)
}
쉽습니다. 다만 위 패턴에서는 항상 wallet 클라이언트 주입을 가정합니다. wallet 클라이언트는 쓰기 컨트렉트에서만 필요합니다. 읽기 작업만 수행하는데 지갑 연결이 필요한 것은 UX를 해칠 수 있습니다. 따라서 우리는 문제를 다음과 같이 분리해볼 수 있습니다.
function useReadContract() {
const client = usePublicClient();
return getContract({
abi: [...ABI],
address: `0x....`,
client,
}).read;
}
function useWriteContract() {
const client = useWalletClient()
return () => {
if (client == null) {
return null;
}
return getContract({
abi: [...ABI],
address: `0x....`,
client: walletClient,
}).write;
}
}
function Component() {
const readContract = useReadContract()
const resolveWriteContract = useWriteContractResolver()
const account = useAccount()
return (
<form action={formData => {
const writeContract = resolveWriteContract();
if (writeContract == null) {
alert('connect wallet')
return;
}
writeContract.createToken(parseFormData(formData))
}}>
{...}
<Suspense fallback={<span>estimating ...</span>}>
{readContract.creationFee(...).then(([fee]) => <span>{fee} coin</span>)}
</Suspense>
</form>
)
}
위와 같은 패턴은 지갑 사용자의 로그인 유무를 신경쓰지 않고 각각 필요에 맞춰 옵티멀한 코드를 작성할 수 있게 도와줍니다.
정리해보자면 다음과 같습니다. encodeCallData 등 raw level API를 사용하여 ABI를 만족시키는 대신, getContract 함수를 사용하여 더 적은 코드로 ABI와 민감하게 반응하며 지갑상태와 무관하게 UI를 작성하는 코드를 작성할 수 있습니다.