본문으로 건너뛰기

트랜잭션 수수료 대납

ABC Paymaster를 사용하면 기존 EOA 지갑 주소 그대로 트랜잭션 수수료를 대납받을 수 있습니다.

지원 네트워크

현재 가스비 대납 서비스 지원 네트워크는 Ethereum, Kaia, Base 입니다.
테스트넷은 Ethereum Sepolia, Kaia Kairos, Base Sepolia 를 지원합니다.

Avalanche C-Chain 사용 시

Avalanche C-Chain은 이 방식을 지원하지 않습니다. SmartAccount 가이드를 참고하세요.

API 가이드

전체 흐름은 3단계입니다.

단계메서드엔드포인트
1. SponsorPOST/core/evm/v2/eoa/paymaster/sponsor
2. SendPOST/core/evm/v2/eoa/paymaster/send
3. ReceiptGET/core/evm/v2/eoa/paymaster/receipt

1. Sponsor { #1-sponsor }

가스대납 요청을 보내고, 서명할 데이터를 받습니다.

Request

POST /core/evm/v2/eoa/paymaster/sponsor
{
"network": "ethereum_sepolia",
"from": "0xAbC...",
"to": "0xDeF...",
"value": "0x0",
"data": "0x..."
}
필드설명
network대상 네트워크 식별자 (예: ethereum_sepolia, kaia_kairos, base_sepolia)
from송신자 EOA 주소
to수신자 주소 (컨트랙트 또는 EOA)
value전송할 ETH 양 (wei, hex)
data컨트랙트 호출 데이터. 단순 전송 시 "0x"

Response

sponsored_transaction은 ERC-4337 v0.7 UserOperation 페이로드(split format)이며, 클라이언트는 어떤 필드도 변경하지 않고 다음 단계(send)로 그대로 전달해야 합니다.

{
"sponsored_transaction": {
"sender": "0xAbC...",
"nonce": "0x1",
"callData": "0x...",
"callGasLimit": "0x...",
"verificationGasLimit": "0x...",
"preVerificationGas": "0x...",
"maxFeePerGas": "0x...",
"maxPriorityFeePerGas": "0x...",
"paymaster": "0xabc7777777641b746429c25961fe99ea48797cbe",
"paymasterVerificationGasLimit": "0x...",
"paymasterPostOpGasLimit": "0x0",
"paymasterData": "0x...",
"signature": "0x"
},
"sign_hash": "0xabcdef...",
"authorization": {
"chainId": 11155111,
"address": "0x...",
"nonce": 0
},
"authorization_hash": "0x..."
}
필드설명
sponsored_transaction2단계 Send에 그대로 전달할 v0.7 UserOperation 페이로드
sign_hashEOA가 서명해야 할 UserOperation 해시
authorization최초 트랜잭션에만 포함. EIP-7702 위임 권한 부여 객체. 아래 참고
authorization_hash최초 트랜잭션에만 포함. authorization 서명 대상 해시. 아래 참고
Delegation 자동 감지

Paymaster는 EOA의 on-chain 코드를 읽어 이미 SimpleAccount v0.7 implementation에 위임된 상태(EIP-7702 0xef0100… prefix)인지 자동으로 감지합니다.

  • 미위임 EOA: 응답에 authorizationauthorization_hash가 포함됩니다. 클라이언트는 EIP-7702 authorization을 서명하여 /send에 전달해야 합니다.
  • 이미 위임된 EOA: authorizationauthorization_hash가 모두 생략됩니다. 클라이언트는 sign_hash만 서명하고, /send 호출 시 authorization 필드 자체를 생략합니다.

2. Send { #2-send }

서명한 데이터를 전송합니다.

Request

POST /core/evm/v2/eoa/paymaster/send
{
"network": "ethereum_sepolia",
"sponsored_transaction": { "..." },
"signature": "0x...",
"authorization": {
"chainId": 11155111,
"address": "0x...",
"nonce": 0,
"v": 0,
"r": "0x...",
"s": "0x..."
}
}
필드설명
sponsored_transaction1. Sponsor 응답의 sponsored_transaction 그대로 전달
signaturesign_hash에 대한 서명값. 아래 참고
authorizationSponsor 응답에 authorization이 포함된 경우에만 전달. 이미 위임된 EOA에서는 이 필드를 생략하고 sponsored_transactionsignature만 보내면 충분합니다. 아래 참고
경고

sponsored_transaction의 필드를 임의로 수정하면 서명 검증에 실패합니다. 응답을 그대로 전달하세요.

Response

{
"tx_id": "0x..."
}

3. Receipt { #3-receipt }

tx_id로 트랜잭션 처리 결과를 조회합니다.

Request

GET /core/evm/v2/eoa/paymaster/receipt?tx_id={tx_id}&network={network}

Response

처리 완료:

{
"success": true,
"sender": "0xAbC...",
"receipt": {
"transactionHash": "0x...",
"blockNumber": "0x...",
"status": "0x1"
}
}

처리 중 (아직 인덱싱되지 않음):

null

트랜잭션이 아직 mempool에 있거나 receipt 인덱싱이 진행 중이면 응답이 null로 내려옵니다. receipt.transactionHash가 채워질 때까지 1~5초 간격으로 폴링하세요.

서명 방법 { #signing }

sign_hashauthorization_hash 모두 WaaS의 MPC Sign API를 사용하여 서명합니다.

POST /v3/wallet/sign
{
"curve": "secp256k1",
"encrypted_share": "지갑 생성/복구 시 반환된 암호화된 MPC 쉐어",
"key_id": "서명에 사용할 키의 고유 식별자",
"message": "서명할 해시 (hex string)",
"secret_store": "쉐어 복호화에 사용되는 키"
}

두 해시는 서명 전 처리 방식이 다릅니다.

해시EIP-191 prefixMPC message에 전달할 값
sign_hash적용 (personal_sign)EIP-191 prefix를 적용한 32바이트 해시
authorization_hash미적용authorization_hash 값 그대로

sign_hash 서명

sign_hash는 SimpleAccount v0.7의 toEthSignedMessageHash().recover() 검증을 통과해야 하므로, Ethereum personal_sign 프리픽스를 적용한 해시를 계산한 뒤 /v3/wallet/sign에 전달합니다.

import { keccak256, toBytes, concat } from 'viem';

// personal_sign 프리픽스 적용
const prefix = toBytes(`\x19Ethereum Signed Message:\n32`);
const messageHash = keccak256(concat([prefix, toBytes(sponsorResponse.sign_hash)]));

const signRes = await fetch('/v3/wallet/sign', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${accessToken}` },
body: JSON.stringify({
curve: 'secp256k1',
encrypted_share: encryptedShare,
key_id: keyId,
message: messageHash,
secret_store: secretStore,
}),
});
const { signature } = await signRes.json();

const sendSignature = `0x${signature}`;

authorization_hash 서명 (최초 트랜잭션에만 해당) { #authorization }

authorizationauthorization_hash는 EOA가 처음으로 수수료 대납 트랜잭션을 전송할 때만 Sponsor 응답에 포함됩니다. 이후 트랜잭션에서는 두 필드 모두 생략되며, sign_hash만 서명해 /send에 보내면 됩니다.

authorization_hash는 EIP-191 prefix 없이 raw 값 그대로 message에 전달하고, 응답의 signature(65바이트)를 r, s, v로 분해하여 authorization 객체에 채웁니다.

const authSignRes = await fetch('/v3/wallet/sign', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${accessToken}` },
body: JSON.stringify({
curve: 'secp256k1',
encrypted_share: encryptedShare,
key_id: keyId,
message: sponsorResponse.authorization_hash,
secret_store: secretStore,
}),
});
const { signature: authSig } = await authSignRes.json();

// 65바이트 서명(130 hex chars)을 r, s, v로 분해
const r = `0x${authSig.slice(0, 64)}`;
const s = `0x${authSig.slice(64, 128)}`;

// v는 EIP-7702 yParity (0 또는 1)로 정규화.
// MPC 응답은 legacy ECDSA 형식(27/28)이므로 27을 차감.
let v = parseInt(authSig.slice(128, 130), 16);
if (v >= 27) v -= 27;

// chainId/nonce는 sponsor 응답의 decimal integer를 그대로 사용.
const authorization = {
...sponsorResponse.authorization,
v,
r,
s,
};