배치 트랜잭션
Volr의 배치 트랜잭션 기능으로 단일 요청에 여러 온체인 액션을 실행하세요.
배치 트랜잭션이란?
배치 트랜잭션을 사용하면 여러 온체인 액션을 단일 요청으로 묶을 수 있습니다. 사용자는 한 번만 승인하면 되고, Volr가 모든 트랜잭션을 원자적으로 실행합니다 — 모두 성공하거나 모두 실패합니다.
장점
- 원자적 실행: 모든 트랜잭션이 함께 성공하거나 실패합니다
- 더 나은 UX: 사용자가 여러 번이 아닌 한 번만 승인합니다
- 가스 효율적: 개별 트랜잭션보다 가스 비용이 감소합니다
- 복잡한 작업에 완벽: 스왑, 브릿지, DeFi 작업에 이상적입니다
사용 사례
토큰 스왑
사용자가 다음을 수행하도록 요구하는 대신:
- 토큰 지출 승인
- 스왑 실행
두 액션을 묶을 수 있습니다:
import { useVolr } from '@volr/react';
function SwapButton() {
const { evm } = useVolr();
const handleSwap = async () => {
const client = evm.client(8453); // Base 체인
await client.sendBatch([
{
target: tokenAddress,
data: approveCalldata,
value: 0n,
gasLimit: 100000n,
},
{
target: swapRouterAddress,
data: swapCalldata,
value: 0n,
gasLimit: 200000n,
},
]);
};
return <button onClick={handleSwap}>토큰 스왑</button>;
}
크로스체인 브릿지
브릿징을 위한 여러 작업 묶기:
const { evm } = useVolr();
const client = evm.client(chainId);
await client.sendBatch([
{
target: sourceTokenAddress,
data: approveCalldata,
value: 0n,
gasLimit: 100000n,
},
{
target: bridgeContractAddress,
data: bridgeCalldata,
value: 0n,
gasLimit: 300000n,
},
]);
복잡한 DeFi 작업
여러 DeFi 액션을 원자적으로 실행:
const { evm } = useVolr();
const client = evm.client(chainId);
await client.sendBatch([
{
target: lendingProtocol,
data: depositCalldata,
value: 0n,
gasLimit: 150000n,
},
{
target: stakingContract,
data: stakeCalldata,
value: 0n,
gasLimit: 150000n,
},
]);
작동 방식
[사용자가 배치 시작]
↓
[사용자가 한 번 승인]
↓
[Volr가 트랜잭션 묶기]
↓
[모든 트랜잭션이 원자적으로 실행됨]
↓
[모두 성공하거나 모두 실패]
배치 트랜잭션과 가스 스폰서링
배치 트랜잭션은 가스 스폰서링과 원활하게 작동합니다:
- 개발자가 가스 스폰서링: 배치의 모든 트랜잭션에 대한 가스비를 지불합니다
- 사용자는 액션에 대해서만 지불: 사용자는 네이티브 토큰이 필요 없습니다
- 단일 승인: 사용자가 전체 배치에 대해 한 번만 승인합니다
모범 사례
1. 관련 액션 그룹화
함께 성공하거나 실패해야 하는 트랜잭션만 배치하세요:
// 좋음: 관련 액션
const swapBatch = [approveTx, swapTx];
// 나쁨: 관련 없는 액션
const unrelatedBatch = [swapTx, transferTx, stakeTx];
2. 가스 한도 고려
배치의 각 트랜잭션은 가스를 소비합니다. 가스 스폰서링 한도가 전체 배치를 수용할 수 있는지 확인하세요:
// 배치하기 전에 총 가스 추정치 확인
const totalGas = transactions.reduce((sum, tx) => sum + tx.gasLimit, 0n);
if (totalGas > maxGasLimit) {
// 더 작은 배치로 분할하거나 한도 증가
}
3. 오류 처리
배치 트랜잭션이 원자적이므로 실패를 적절히 처리하세요:
try {
await client.sendBatch(transactions);
} catch (error) {
if (error.message.includes('user rejected')) {
// 사용자가 취소함
} else {
// 트랜잭션 실패 - 배치의 모든 트랜잭션이 되돌려짐
console.error('배치 실행 실패:', error);
}
}
API 참조
sendBatch 메서드
useVolr().evm.client(chainId).sendBatch()를 사용하여 배치 트랜잭션을 실행합니다:
import { useVolr } from '@volr/react';
const { evm } = useVolr();
const client = evm.client(chainId);
await client.sendBatch(calls, options);
방법 1: 사전 빌드된 Call 객체
await client.sendBatch([
{
target: '0x...', // 컨트랙트 주소
data: '0x...', // 트랜잭션 calldata
value: 0n, // ETH 값 (선택, 기본값 0n)
gasLimit: 100000n, // 가스 한도 (선택)
},
]);
방법 2: BuildCallOptions (타입 안전)
await client.sendBatch([
{
target: tokenAddress,
abi: erc20Abi,
functionName: 'approve',
args: [spenderAddress, amount],
gasLimit: 100000n,
},
{
target: routerAddress,
abi: routerAbi,
functionName: 'swap',
args: [tokenIn, tokenOut, amount],
gasLimit: 200000n,
},
]);
매개변수
calls: 트랜잭션 객체 배열target: 컨트랙트 주소 (0x${string})data: 트랜잭션 calldata (사전 빌드된 호출용)abi,functionName,args: 타입 안전 호출용value: 선택적 ETH 값 (bigint, 기본값 0n)gasLimit: 선택적 가스 한도 (bigint)
options: 선택적 설정from: 발신자 주소 오버라이드expiresInSec: 트랜잭션 만료 시간
반환값
interface RelayResult {
txHash: string;
userOpHash?: string;
status: 'confirmed' | 'pending';
}
예제
완전한 스왑 예제
import { useVolr } from '@volr/react';
import { encodeFunctionData, parseUnits } from 'viem';
const erc20Abi = [
{
name: 'approve',
type: 'function',
inputs: [
{ name: 'spender', type: 'address' },
{ name: 'amount', type: 'uint256' },
],
outputs: [{ type: 'bool' }],
},
] as const;
function TokenSwap() {
const { evm } = useVolr();
const handleSwap = async () => {
const chainId = 8453; // Base
const tokenAddress = '0x...'; // 스왑할 토큰
const routerAddress = '0x...'; // DEX 라우터
const amount = parseUnits('100', 18);
const client = evm.client(chainId);
// 옵션 1: 타입 안전 BuildCallOptions 사용
try {
const result = await client.sendBatch([
{
target: tokenAddress,
abi: erc20Abi,
functionName: 'approve',
args: [routerAddress, amount],
gasLimit: 100000n,
},
{
target: routerAddress,
abi: swapRouterAbi,
functionName: 'swapExactTokensForTokens',
args: [amount, 0n, [tokenAddress, usdcAddress], userAddress, BigInt(Date.now() + 1000 * 60 * 20)],
gasLimit: 200000n,
},
]);
console.log('스왑 성공:', result.txHash);
} catch (error) {
console.error('스왑 실패:', error);
}
};
return <button onClick={handleSwap}>토큰 스왑</button>;
}
사전 인코딩된 Calldata 사용
import { useVolr } from '@volr/react';
import { encodeFunctionData } from 'viem';
function SwapWithCalldata() {
const { evm } = useVolr();
const handleSwap = async () => {
const client = evm.client(8453);
// 수동으로 calldata 인코딩
const approveData = encodeFunctionData({
abi: erc20Abi,
functionName: 'approve',
args: [routerAddress, amount],
});
const swapData = encodeFunctionData({
abi: swapRouterAbi,
functionName: 'swap',
args: [tokenIn, tokenOut, amount],
});
const result = await client.sendBatch([
{ target: tokenAddress, data: approveData, value: 0n, gasLimit: 100000n },
{ target: routerAddress, data: swapData, value: 0n, gasLimit: 200000n },
]);
console.log('TX Hash:', result.txHash);
};
return <button onClick={handleSwap}>스왑</button>;
}
제한사항
- 가스 한도: 배치의 각 트랜잭션은 가스 한도를 준수해야 합니다
- 원자적 실행: 모든 트랜잭션이 함께 성공하거나 실패합니다 — 부분 성공 없음
- 복잡성: 더 복잡한 배치는 더 높은 가스 한도가 필요할 수 있습니다