Account Abstraction의 Paymaster API는 어떻게 설계될까?
viem과 호환가능한 JSON-RPC 2.0과 ERC-7677

Intro
요즘 이더리움의 표준(EIP, ERC)에서는 노드 또는 Provider와 클라이언트 간 통신을 위한 API 규격이 자주 정의되고 있습니다. 특히 이더리움 생태계에서는 JSON-RPC 기반 API의 활용도가 높기 때문에, 표준을 기반으로 통신 아키텍처를 설계하려면 원격 프로시저 호출(RPC) 프로토콜에 대한 이해와 정의 능력이 요구됩니다.
본 자료에서는 RPC 2.0 Specification을 기반으로 RPC의 개념을 살펴보고, Node.ts/Express.ts를 활용하여 ERC-7677 표준의 일부 기능을 직접 구현해 보고자 합니다.
REST와 RPC의 차이점
RPC는 잘 알려진 REST와 마찬가지로 API를 설계하는 아키텍처 스타일을 의미합니다. 두 방식 모두 HTTP를 기본 전송 프로토콜로 사용할 수 있지만, 설계 철학과 적용 목적에는 차이가 있으며 서비스의 요구사항에 따라 선택적으로 활용됩니다.
REST
REST는 자원(Resource) 중심의 아키텍처 스타일입니다. HTTP 메서드(GET, POST, PUT, DELETE 등)를 통해 자원에 대한 CRUD(Create, Read, Update, Delete) 수행을 명시하며, 이러한 메서드와 행위 간의 관계는 사전에 정의되어 있습니다.
예를 들어 GET은 데이터를 조회하고, POST는 새로운 데이터를 생성하는 의미로 사용됩니다. 이처럼 클라이언트는 서버에 어떤 함수가 구현되어 있는지 알지 못하더라도, 표준화된 방식을 통해 목적에 맞는 API를 호출할 수 있습니다.
RPC
RPC는 "함수 호출"이라는 개념을 네트워크로 확장한 아키텍처 스타일입니다. 클라이언트는 마치 로컬에서 함수를 호출하듯이 원격에 위치한 함수에 직접 접근하여 실행을 요청할 수 있습니다. 따라서 RPC에서는 요청 메시지에 “이 함수(method)를 호출해줘”라는 정보와 함께 필요한 “파라미터”를 담아 전송합니다.
즉, 원격 함수 실행을 위해 설계된 통신 방식으로, 비즈니스 로직에 따라 정의된 특정 함수를 목적에 맞게 호출할 수 있도록 합니다.
주로 HTTP POST 요청의 body에 호출할 함수의 이름과 전달할 파라미터를 JSON 형식으로 포함하여 서버에 전송하는 방식으로 사용됩니다.
JSON-RPC
현재 JSON-RPC는 1.0과 2.0 두 가지 버전이 존재하며, 본 개발에서는 2.0 버전을 중심으로 설명할 예정입니다. 1.0과 2.0 간의 상세한 차이점은 simple-is-better.org에 잘 정리되어 있으므로, 관심 있는 독자는 참고하면 도움이 될 것입니다.
JSON-RPC는 클라이언트가 JSON 형식의 요청 메시지를 서버로 전송하여 원격 프로시저 호출(RPC)을 수행하는 프로토콜입니다. 서버는 요청을 처리한 후 그 결과를 JSON 형식의 응답 메시지로 반환합니다. JSON은 가독성과 유연성이 높기 때문에 RPC API를 설계하는 데 주로 활용됩니다. 이때 클라이언트와 서버 간에 주고받는 요청과 응답은 JSON-RPC 스펙에서 정의한 구조를 준수해야 합니다.
요청(request)
jsonrpc
JSON-RPC 프로토콜의 버전을 지정하는 필드이며, 반드시 문자열 "2.0"으로 명시해야 합니다.
method
호출할 함수의 이름을 나타내는 문자열입니다.
params
함수 호출 시 사용할 매개변수를 지정하는 필드입니다. 배열 형태(순서를 따름) 또는 객체 형태(key-value 쌍)로 전달할 수 있으며, 이는 서비스의 설계 방식에 따라 달라집니다.
id
요청을 식별하기 위한 고유 값입니다. 클라이언트는 여러 개의 요청을 배치(batch) 형태로 동시에 전송할 수 있으며, 이때 id는 각각의 요청을 구분하기 위한 값으로 활용됩니다. 문자열, 숫자, null을 사용할 수 있지만, 소수점이 포함된 숫자는 사용할 수 없습니다.
응답(response)
jsonrpc
요청과 동일하게 "2.0"을 명시해야 합니다.
result
호출된 메서드의 실행 결과를 포함하는 필드입니다. 요청이 정상적으로 처리된 경우에만 포함되며, 오류가 발생한 경우에는 존재하지 않습니다.
id
요청 시 사용된 id 값과 반드시 동일해야 합니다.
code

오류 유형을 나타내는 정수 값입니다. JSON-RPC 사양에서는 -32768부터 -32000 사이의 오류 코드 범위를 정의하고 있으며, 이 중 일부는 프로토콜에 의해 고정된 의미를 가집니다.
참고로, 요청을 처리하는 과정에서 오류가 발생한 경우에는 응답 객체에 result 대신 error 필드가 포함됩니다.
message
오류의 원인에 대한 간략한 설명을 담는 문자열입니다.
data
오류에 대한 추가 정보를 담을 수 있는 선택적 필드이며, 상황에 따라 생략할 수 있습니다. 구조화된 객체 또는 기본 타입을 모두 사용할 수 있습니다.
ERC-7677: Paymaster JSON-RPC
ERC-7677은 지갑과 ERC-4337 Paymaster 간의 클라이언트–서버 통신을 표준화된 방식으로 정의하여, Paymaster 서비스와의 통신 기능을 제안합니다.
이를 위해 pm_getPaymasterStubData와 pm_getPaymasterData 메서드의 JSON-RPC 요청 및 응답 프로토콜을 정의하고 있으며, 클라이언트는 이 프로토콜을 통해 UserOperation의 paymasterAndData 필드에 필요한 값을 반환받아 번들러에 전달할 수 있습니다.
{
"method": string,
"id": number,
"jsonrpc": "2.0",
"params": [
{
"callData": `0x${string}`,
"factory": `0x${string}`,
"factoryData": `0x${string}`,
"maxFeePerGas": `0x${string}`,
"maxPriorityFeePerGas": `0x${string}`,
"nonce": `0x${string}`,
"sender": `0x${string}`,
"signature": `0x${string}`,
"callGasLimit": `0x${string}`,
"verificationGasLimit": `0x${string}`,
"preVerificationGas": `0x${string}`
}, // userOp
`0x${string}`, // EntryPoint
`0x${string}`, // Chain ID
Record<string, any> // Context
]
}ERC-7677은 JSON-RPC 통신 시 위와 같은 구조를 사용할 것을 권장합니다. 특히 params는 배열 형태로 전달되며, index 순서에 따라 userOp****, EntryPoint 주소, Chain ID, Context를 포함해야 합니다. 서버는 전달된 순서를 기준으로 각 파라미터를 해석하고, 이를 함수의 인자로 활용하게 됩니다.
응답(Response) 데이터의 result 또한 사전에 정의된 구조를 사용할 것을 권장합니다. pm_getPaymasterStubData와 pm_getPaymasterData는 params 구조는 동일하지만, 반환해야 하는 결과 값이 다르기 때문에 각각 서로 다른 result 구조를 갖도록 설계되어 있습니다.
{
"paymaster": "0x...",
"paymasterData": "0x...",
"paymasterVerificationGasLimit": "0x...",
"paymasterPostOpGasLimit": "0x..."
}먼저, pm_getPaymasterStubData 함수는 위와 같은 구조를 따릅니다. 이 함수는 사용자가 UserOperation을 번들러에게 전달하기 전에 가스비를 사전 측정(estimate) 하기 위해 사용되며, 대략적인 가스 소요량을 추정하기 위한 스텁(Stub) 데이터를 제공하는 역할을 합니다. 이러한 목적에 맞춰 가스 추정에 필요한 정보와 페이마스터 함수를 호출할 컨트랙트 주소 등을 반환하는 형태로 설계되어 있습니다.
{
"sponsor": {
"name": "My App",
"icon": "https://..."
},
"paymaster": "0x...",
"paymasterData": "0x..."
}pm_getPaymasterData 함수도 위와 동일한 params 구조를 사용합니다. 이 함수는 UserOperation을 번들러에게 실제로 제출하기 위한 데이터를 반환하는 데 목적이 있습니다.
반환된 데이터는 UserOperation.paymasterAndData 필드에 삽입되며, 이를 통해 EntryPoint는 해당 사용자 요청이 페이마스터로부터 가스비 지원(sponsorship)을 승인받았는지 검증하게 됩니다.
이 함수의 응답 데이터는 별도의 정제 과정 없이 즉시 번들러에게 전달할 수 있도록 설계되어 있으며, 실제 제출에 사용하기 위한 최종 데이터를 반환하는 것이 핵심입니다.
result 객체 내의 sponsor 필드는 서비스 제공자의 메타데이터를 담는 선택적 요소입니다. 지갑 서비스 등에서는 이를 활용하여 사용자에게 어떤 주체가 해당 트랜잭션을 지원하고 있는지 시각적으로 표시할 수 있습니다.
Simple Paymaster Server
지금까지 학습한 JSON-RPC와 ERC-7677 표준을 기반으로, 간단한 페이마스터 서버(Simple Paymaster Server)를 express.ts로 구현해보겠습니다. 본 리뷰에서는 페이마스터의 내부 로직 구현이 아닌, 통신 프로토콜 구조에만 초점을 맞출 예정입니다. 실제 페이마스터가 동작하는 로직에 대해서는 아래 GitHub 레포지토리를 참고하여 별도로 학습 및 리뷰하는 것이 좋습니다.
덧붙여, 해당 서버는 말 그대로 학습용으로 제작된 간단한 서버이므로 실서비스 환경에서 바로 사용하는 것은 바람직하지 않습니다. 페이마스터를 운영하기 위해서는 ERC-4337과 번들러(Bundler)에 대한 충분한 이해와 보안에 대한 고려가 반드시 필요합니다. 따라서 본 코드는 통신 구조와 흐름을 이해하기 위한 레퍼런스 용도로만 활용하기를 권장합니다.
1. 함수(method) 정의
const methodMap = {
pm_getPaymasterData: async (req: Request, res: Response) => {
# ...service...
},
pm_getPaymasterStubData: async (req: Request, res: Response) => {
# ...service...
},
pm_sponsorUserOperation: async (req: Request, res: Response) => {
return res.json(rpcError(-32000, 'Unsupported pm_sponsorUserOperation received', req.body.id));
},
pm_getERC20TokenQuotes: async (req: Request, res: Response) => {
return res.json(rpcError(-32000, 'Unsupported pm_getERC20TokenQuotes received', req.body.id));
},
};먼저, 해당 서버에서는 pm_getPaymasterData와 pm_getPaymasterStubData 메서드를 지원합니다. 내부 로직은 서비스 레이어로 분리하여 구성할 수도 있지만, 본 서버는 간단한 구조를 가지므로 라우터 레이어에서 직접 구현하였습니다.
한편, pm_sponsorUserOperation과 pm_getERC20TokenQuotes는 ERC-20 토큰으로 가스비를 청구하는 페이마스터 기능을 위한 API입니다. 그러나 본 서버는 가스비 대납(Sponsorship) 기능만 지원하는 페이마스터를 기반으로 하므로, 해당 API가 호출될 경우 미지원 기능에 대한 오류 응답을 반환하도록 구성되어 있습니다.
function rpcError(code: number, message: string, id: any = null, data?: any) {
return { jsonrpc: '2.0', error: { code, message, data }, id };
}이때 반환되는 오류 또한 JSON-RPC 프로토콜을 기반으로 작성되며, 위에서 설명한 응답 구조를 따르는 함수를 통해 일관된 형태로 반환되도록 구성하였습니다.
JSON-RPC 스펙에 따르면 오류 코드 -32000부터 -32099까지는 서버가 자체적으로 정의할 수 있는 범위이기 때문에, 본 서버에서는 지원하지 않는 함수 호출에 대해 해당 범위의 코드를 사용하여 자체 오류를 정의하고 반환하도록 설계하였습니다.
2. HTTP POST 설계
router.post('/', async (req: Request, res: Response) => {
const { method, id, jsonrpc, params } = req.body;
if (jsonrpc !== '2.0') {
return res.json(rpcError(-32600, 'Invalid JSON-RPC version', id));
}
if (!method || !(method in methodMap)) {
return res.json(rpcError(-32601, 'Method not found', id));
}
# ...service...
}JSON-RPC는 관례적으로 POST '/' 또는 '/rpc'와 같은 URL 경로를 사용하며, 요청 본문(Body)에 method, id, jsonrpc, params로 구성된 JSON-RPC 표준 파라미터를 포함하여 전달합니다.
서버가 JSON-RPC 2.0 버전에 맞춰 API를 설계하였다면, 클라이언트 또한 jsonrpc 필드에 "2.0"을 반드시 명시해야 합니다. 만약 해당 값이 누락되거나 "2.0"이 아닌 경우에는 JSON-RPC 오류 코드 표준에 따라 -32600 오류 코드와 함께 유효하지 않은 버전(Invalid JSON-RPC version)으로 처리됩니다.
또한 요청한 함수(method)가 누락되었거나 서버에서 지원하지 않는 경우에는 JSON-RPC 표준에 따라 -32601 오류 코드를 반환하여, 정의되지 않은 메서드(Method not found) 에 해당하는 오류로 응답하도록 구현하였습니다.
3. Param DTO
다음으로, ERC-7677에서 정의한 파라미터 표준에 따라 요청 본문(Body) 내 params 필드의 데이터를 구성하고자 합니다. 이때 전달받은 데이터가 표준에 명시된 타입과 구조를 정확히 따르고 있는지 확인하기 위해, params 배열의 순서(index)에 따라 userOp, EntryPoint 주소, Chain ID, Context가 정확히 포함되어 있는지를 검증하는 DTO(Data Transfer Object, 데이터 전송 객체) 를 구현하여 데이터 유효성을 검사합니다.
export class PaymasterDataRequestDTO {
userOp: UserOperation;
entryPoint: string;
chainId: string;
context: Record<string, any>;
constructor() {}
static of(params: any) {
const dto = new PaymasterDataRequestDTO();
const _params:PaymasterParams = {
userOp: params[0],
entryPoint: params[1],
chainId: params[2],
context: params[3],
};
const { userOp, entryPoint, chainId, context } = PaymasterDataRequestDTO.validation(_params);
dto.userOp = userOp;
dto.entryPoint = entryPoint;
dto.chainId = chainId;
dto.context = context;
return dto;
}
...
}ERC-7677의 params는 배열의 순서에 따라 각 데이터를 정의하도록 되어 있습니다. 이에 따라 0번부터 3번까지의 인덱스에서 데이터를 추출한 후, Zod로 구현한 데이터 검증 함수 validation()을 통해 각 항목을 파싱합니다.
if (error instanceof ZodError) {
return res.json(rpcError(-32602, 'Invalid params', id, error.message));
}만약 데이터가 올바르지 않은 형태로 전달되어 파싱에 실패할 경우에는 JSON-RPC 오류 코드 표준에 따라 -32602 오류 코드와 함께 유효하지 않은 파라미터(Invalid params)로 처리됩니다.
4. Result return
... generate paymaster data
if (estimate) {
return {
paymaster: userOp.paymaster,
paymasterData: paymasterAndData,
paymasterPostOpGasLimit: this.paymasterPostOpGasLimit,
paymasterVerificationGasLimit: this.paymasterVerificationGasLimit,
};
} else {
return {
paymaster,
paymasterData: paymasterAndData,
};
}
}마지막으로, ERC-7677에서 정의한 데이터 반환 결과(result)의 구조입니다. 반환에 필요한 값들은 표준에서 정의한 키(key)에 맞춰 설계된 구조로 응답되도록 구성하였습니다.
function rpcReturn(result: any, id: any = null, data?: any) {
return { jsonrpc: '2.0', result: result, id };
}또한 응답(Response) 데이터 역시 JSON-RPC 프로토콜에 따라 구조가 강제되도록 구현하였습니다. 해당 구조를 응답 함수 내부에 포함시킴으로써, 각각의 메서드에서 생성된 결과가 항상 일관된 JSON-RPC 응답 형태로 반환되도록 처리하였습니다.
API Logging

Simple Paymaster Server는 morgan과 winston을 활용하여 요청 및 응답 데이터를 파싱하고, 이를 로그로 출력하도록 구현하였습니다. 이를 통해 클라이언트가 어떤 형태로 데이터를 전송하는지와 서버가 어떤 값을 반환하는지를 명확하게 확인할 수 있습니다.
Request body
{
"method": "pm_getPaymasterStubData",
"id": 3,
"jsonrpc": "2.0",
"params": [
{
"callData": "0xb61d27f60000000000000000000000000000000000000000000000000000000000004337000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000",
"nonce": "0x1987e494d8d0000000000000000",
"sender": "0xa2CBe4Bc648C28bA6E29d191059E706a95751Ea9",
"signature": "0xfffffffffffffffffffffffffffffff0000000000000000000000000000000007aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa1c",
"callGasLimit": "0x0",
"verificationGasLimit": "0x0",
"preVerificationGas": "0x0"
},
"0x0000000071727De22E5E9d8BAf0edAc6f37da032",
"0x539",
{ "policy-id": "1234-5678-90" }
]
}Response body
{
"jsonrpc": "2.0",
"result": {
"sponsor": { "name": "sponsorName", "icon": "sponsorImage" },
"paymaster": "0x064Fbec1c03eC4004E7f9ADc5FAe2e2fB1857064",
"paymasterData": "0x00000000000000000000000000000000000000000000000000000000689306c60000000000000000000000000000000000000000000000000000000068930432625a05dfbfb68d9d6db2356809beb782f19b91552865932e429e0aa35cec9d652f103e11e6ebbc7667ddc459aa5de698e27dec6384dc637b9667ef8923cf56f41b"
},
"id": 4
}Viem 호환 가능한 인터페이스
console.log('Estimate UserOp to Bundler.... ');
const result = await bundlerClient.estimateUserOperationGas({
account,
paymasterContext: {
'policy-id': '1234-5678-90',
},
calls: [
{
to: to,
value: 0n,
},
],
});
console.log(result);viem은 Account Abstraction에 필요한 다양한 유틸리티를 제공하며, 본 프로젝트에서는 viem을 활용하여 API를 호출하는 스크립트를 작성하였습니다. 특히 ERC-7677 표준에 맞춰 클라이언트 API를 구현하였기 때문에, 해당 스크립트를 통해 서버를 호출했을 때 문제없이 정상적으로 동작할 수 있도록 서버를 설계하였습니다.
실제로 viem에서 제공하는 함수와도 정상적으로 호환되는 것을 확인하였으며, 이를 통해 본 서버가 JSON-RPC 및 ERC-7677 표준에 맞춰 API를 설계하고 있음을 확인할 수 있습니다.
Reference
- JSON-RPC 2.0 Specification : https://www.jsonrpc.org/specification
- [Wiki] 원격 프로시저 호출: https://en.wikipedia.org/wiki/Remote_procedure_call
- [simple is better] JSON-RPC : https://www.simple-is-better.org/rpc/#differences-between-1-0-and-2-0
- JSON-RPC API 개발: https://wikidocs.net/225140
- [AWS] RPC와 REST의 차이점은 무엇인가요?: https://aws.amazon.com/ko/compare/the-difference-between-rpc-and-rest/
- ERC-7677: Paymaster Web Service Capability: https://eips.ethereum.org/EIPS/eip-7677