본문으로 건너뛰기

배치 트랜잭션

Volr의 배치 트랜잭션 기능으로 단일 요청에 여러 온체인 액션을 실행하세요.

배치 트랜잭션이란?

배치 트랜잭션을 사용하면 여러 온체인 액션을 단일 요청으로 묶을 수 있습니다. 사용자는 한 번만 승인하면 되고, Volr가 모든 트랜잭션을 원자적으로 실행합니다 — 모두 성공하거나 모두 실패합니다.

장점

  • 원자적 실행: 모든 트랜잭션이 함께 성공하거나 실패합니다
  • 더 나은 UX: 사용자가 여러 번이 아닌 한 번만 승인합니다
  • 가스 효율적: 개별 트랜잭션보다 가스 비용이 감소합니다
  • 복잡한 작업에 완벽: 스왑, 브릿지, DeFi 작업에 이상적입니다

사용 사례

토큰 스왑

사용자가 다음을 수행하도록 요구하는 대신:

  1. 토큰 지출 승인
  2. 스왑 실행

두 액션을 묶을 수 있습니다:

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>;
}

제한사항

  • 가스 한도: 배치의 각 트랜잭션은 가스 한도를 준수해야 합니다
  • 원자적 실행: 모든 트랜잭션이 함께 성공하거나 실패합니다 — 부분 성공 없음
  • 복잡성: 더 복잡한 배치는 더 높은 가스 한도가 필요할 수 있습니다

다음 단계