はじめに
EVM互換ブロックチェーンを操作する場合、TypeScriptではethers(v6) と viem のどちらかを使用することが多いです。
本記事では、ethers と viem の使用方法の違いを比較し、ブロックチェーンへの接続からコントラクト操作までのサンプルコードを提供します。
サンプルコード
ethers と viem のサンプルコードを比較形式で紹介します。
以下に示すコントラクトアドレスやJSON-RPCエンドポイントなどの前提定数は、各環境に合わせて変更してください。
const MNEMONIC_PHRASE = "<ニーモニック>";
const RPC_URL = "<JSON-RPCノードエンドポイント>";
const CONTRACT_ADDRESS = "<ERC20 コントラクトアドレス>";
ブロックチェーン接続(Provider、PublicClient)
署名者なしでブロックチェーンに接続して、ブロック番号を取得するサンプルです。
ライブラリの提供するAPIを使用する方法と、JSON-RPCを直接呼び出す方法の両方を示します。
ethers
const provider = new ethers.JsonRpcProvider(RPC_URL);
// API呼び出し
const blockNumber = await provider.getBlockNumber();
console.log(`Current block number: ${blockNumber}`);
// JSON-RPC直接呼び出し
const hexBlockNumber = await provider.send("eth_blockNumber", []);
console.log(`Current block number (hex): ${hexBlockNumber}`);
viem
const publicClient = createPublicClient({ transport: http(RPC_URL) });
// API呼び出し
const blockNumber = await publicClient.getBlockNumber();
console.log(`Current block number: ${blockNumber}`);
// JSON-RPC直接呼び出し
const hexBlockNumber = await publicClient.request({
method: "eth_blockNumber",
args: [],
});
console.log(`Current block number (hex): ${hexBlockNumber}`);
ウォレット(Wallet、WalletClient)
署名者付きのブロックチェーン接続です。
ニーモニックからウォレットを作成し、ネイティブトークンの残高を取得するサンプルです。
ethers
const wallet = Wallet.fromPhrase(MNEMONIC_PHRASE).connect(provider);
console.log(`Wallet address: ${wallet.address}`);
// ネイディブトークンの残高を取得
const balance = await provider.getBalance(wallet.address);
console.log(`Wallet balance: ${formatEther(balance)} ETH`);
viem
const chainId = await publicClient.getChainId();
console.log(`Connected to chain ID: ${chainId}`);
const chain: Chain = {
id: chainId,
name: "Localhost",
nativeCurrency: {
name: "Ether",
symbol: "ETH",
decimals: 18,
},
rpcUrls: {
default: {
http: [RPC_URL],
},
},
};
const walletClient = createWalletClient({
transport: http(RPC_URL),
account: mnemonicToAccount(MNEMONIC_PHRASE),
chain: chain,
});
console.log(`Wallet address: ${walletClient.account.address}`);
// ネイディブトークンの残高を取得
const balance = await publicClient.getBalance(walletClient.account);
console.log(`Wallet balance: ${formatEther(balance)} ETH`);
コントラクト接続
ERC20のコントラクトに接続して view 関数を呼び出すサンプルです。
ethers
const ERC20_ABI = [
"function name() view returns (string)",
"function symbol() view returns (string)",
"function decimals() view returns (uint8)",
"function totalSupply() view returns (uint256)",
"function balanceOf(address) view returns (uint256)",
"function transfer(address, uint256) returns (bool)",
"event Transfer(address indexed from, address indexed to, uint256 value)",
];
const contract = new Contract(CONTRACT_ADDRESS, ERC20_ABI, wallet);
const name = await contract.name();
const symbol = await contract.symbol();
const decimals = await contract.decimals();
const totalSupply = await contract.totalSupply();
console.log(`Token Name: ${name}`);
console.log(`Token Symbol: ${symbol}`);
console.log(`Token Decimals: ${decimals}`);
console.log(`Total Supply: ${formatUnits(totalSupply, decimals)} ${symbol}`);
viem
const ERC20_ABI = parseAbi([
"function name() view returns (string)",
"function symbol() view returns (string)",
"function decimals() view returns (uint8)",
"function totalSupply() view returns (uint256)",
"function balanceOf(address) view returns (uint256)",
"function transfer(address, uint256) returns (bool)",
"event Transfer(address indexed from, address indexed to, uint256 value)",
]);
const name = await publicClient.readContract({
address: CONTRACT_ADDRESS,
abi: ERC20_ABI,
functionName: "name",
});
console.log(`Token Name: ${name}`);
const symbol = await publicClient.readContract({
address: CONTRACT_ADDRESS,
abi: ERC20_ABI,
functionName: "symbol",
});
console.log(`Token Symbol: ${symbol}`);
const decimals = await publicClient.readContract({
address: CONTRACT_ADDRESS,
abi: ERC20_ABI,
functionName: "decimals",
});
console.log(`Token Decimals: ${decimals}`);
const totalSupply = await publicClient.readContract({
address: CONTRACT_ADDRESS,
abi: ERC20_ABI,
functionName: "totalSupply",
});
console.log(`Token Name: ${name}`);
console.log(`Token Symbol: ${symbol}`);
console.log(`Token Decimals: ${decimals}`);
console.log(`Total Supply: ${formatUnits(totalSupply, decimals)} ${symbol}`);
ABI(Application Binary Interface)ですが、関数表記の配列を利用しています。
ethers では、文字列配列である Human-Readable ABI でも利用できます。
viem の場合、 JSON 形式の Solidity JSON ABI でなければなりません。
そこで、 parseAbi を利用して形式をパースしています。
コントラクト更新
transferを使用してERC20トークンを移転し、トランザクションから得られるイベントログを取得・解析するサンプルです。
ethers
// ランダムアカウント生成
const wallet2 = Wallet.createRandom();
// トークン移転
const transferAmount = parseUnits("10", decimals);
const tx = await contract.transfer(wallet2.address, transferAmount);
console.log(`Transfer transaction hash: ${tx.hash}`);
// レシート取得
const receipt = await tx.wait();
console.log(`Transfer transaction confirmed in block ${receipt.blockNumber}`);
// イベントログのデコード
const transfers = receipt.logs.map((log: Log) => {
try {
return contract.interface.parseLog(log);
} catch {
return null;
}
});
// デコード成功したTransferイベントのみ表示
for (const t of transfers) {
if (!t) continue;
console.log(t.name, t.args.toObject(true));
}
// トークン残高の確認
const balanceOf = await contract.balanceOf(wallet2.address);
console.log(`balance: ${formatUnits(balanceOf, decimals)} ${symbol}`);
viem
// ランダムアカウント生成
const account = privateKeyToAccount(generatePrivateKey());
// トークン移転
const transferAmount = parseUnits("10", decimals);
const hash = await walletClient.writeContract({
address: CONTRACT_ADDRESS,
abi: ERC20_ABI,
functionName: "transfer",
args: [account.address, transferAmount],
});
console.log(`Transfer transaction hash: ${hash}`);
// レシート取得
const receipt = await publicClient.waitForTransactionReceipt({ hash });
console.log(`Transaction confirmed in block ${receipt.blockNumber}`);
// イベントログのデコード
const transfers = receipt.logs.map((log) => {
try {
return decodeEventLog({
abi: ERC20_ABI,
data: log.data,
topics: log.topics,
});
} catch {
return null; // 他コントラクトのログなど
}
});
// デコード成功したTransferイベントのみ表示
for (const t of transfers) {
if (!t) continue;
console.log(t.eventName, t.args);
}
// トークン残高の確認
const balanceOf = await publicClient.readContract({
address: CONTRACT_ADDRESS,
abi: ERC20_ABI,
functionName: "balanceOf",
args: [account.address],
});
console.log(`balance: ${formatUnits(balanceOf, decimals)} ${symbol}`);
コントラクトのデプロイ
新しいERC20コントラクトをブロックチェーンにデプロイするサンプルです。
ethers ではコントラクトファクトリーを使用します。
viem ではウォレットクライアントで直接デプロイします。
ethers
// コントラクトのバイトコード
const BYTE_CODE = "0x608060405234801561000f575f5ffd5b50604051806040016040...";
// コントラクトファクトリーの作成
const contractFactory = new ContractFactory(ERC20_ABI, BYTE_CODE, wallet);
// コントラクトのデプロイ
const contract = await contractFactory.deploy(); // コントラクトのコンストラクター引数がある場合はここに渡す
await contract.waitForDeployment();
// デプロイされたコントラクトのアドレスを表示
console.log(`Contract deployed at address: ${contract.target}`);
// デプロイメントトランザクションのハッシュとレシートの取得
const tx = contract.deploymentTransaction();
console.log(`Deployment transaction hash: ${tx?.hash}`);
const receipt = await tx?.wait();
console.log(`Contract deployed at address: ${receipt?.contractAddress}`);
viem
// コントラクトのバイトコード
const BYTE_CODE = "0x608060405234801561000f575f5ffd5b50604051806040016040...";
// コントラクトのデプロイ
const hash = await walletClient.deployContract({
abi: ERC20_ABI,
bytecode: BYTE_CODE,
args: [],
});
console.log(`Deployment transaction hash: ${hash}`);
// デプロイ完了まで待機
const receipt = await publicClient.waitForTransactionReceipt({ hash });
console.log(`Contract deployed at address: ${receipt.contractAddress}`);
まとめ
ethers と viem の使用方法の違いを、サンプルコードを交えて紹介してきました。
ethers の特徴
- ABI形式の柔軟性: Human-Readable ABI(文字列配列)をそのまま使用でき、手軽に記述できます
- Contract インスタンス: コントラクトインスタンスを通じて関数を直接呼び出せるため、直感的です
- ContractFactory パターン: デプロイ時にファクトリーパターンを使用し、デプロイメント処理が統一されています
viem の特徴
- 厳密な型安全: TypeScriptの型サポートが強力で、コンパイル時にエラーを検出しやすいです
-
ABI要件: Solidity JSON AB形式が必須のため、
parseAbiで統一的に扱う必要があります -
明確なAPI:
readContract、writeContract、deployContractなど機能別のメソッドが明確に分かれており、意図が読みやすいです - Chain定義: チェーン情報を明示的に定義する必要があり、複数チェーン対応がしやすいとも言えます
どちらのライブラリを選んでも、基本的な流れはほぼ同じです。プロジェクトの要件や好みに応じて、適切なものを選択してください。