가스 없는 트랜잭션 활성화
Gas Waiver는 Stable에서 가스 없는 트랜잭션을 가능하게 합니다. Gas Waiver를 사용하면 애플리케이션이 사용자를 대신해 가스 수수료를 부담하므로, 사용자는 가스용 USDT0을 보유하지 않고도 컨트랙트와 상호작용할 수 있습니다.
이 가이드는 Waiver Server API를 통한 통합을 다룹니다.
사전 준비 사항
- Stable 팀이 발급한 Waiver Server용 API 키
- 대상 컨트랙트 주소가 waiver의
AllowedTarget정책에 등록되어 있어야 합니다
Waiver Server
Base URL:- 메인넷: TBD
- 테스트넷:
https://waiver.testnet.stable.xyz
Authorization: Bearer <your-api-key>
개요
통합 흐름은 세 단계로 구성됩니다:
- InnerTx 빌드: 사용자가
gasPrice = 0으로 트랜잭션에 서명합니다. - Waiver Server에 제출: 서명된 트랜잭션을 Waiver Server API에 제출합니다.
- 응답 처리: waiver 서버가 트랜잭션을 래핑하고 브로드캐스트합니다. 스트리밍된 결과를 처리하고 트랜잭션 해시를 사용자에게 표시합니다.
1단계: 사용자의 InnerTx 생성
사용자는 gasPrice = 0으로 표준 트랜잭션에 서명합니다. to 주소와 메서드 셀렉터는 waiver의 AllowedTarget 정책에 의해 허용되어야 합니다.
// config.ts
export const CONFIG = {
RPC_URL: "https://rpc.testnet.stable.xyz",
CHAIN_ID: 2201, // 988 for mainnet
WAIVER_SERVER: "https://waiver.testnet.stable.xyz",
USDT0_ADDRESS: "0x78Cf24370174180738C5B8E352B6D14c83a6c9A9",
};import { ethers } from "ethers";
import { CONFIG } from "./config";
const provider = new ethers.JsonRpcProvider(CONFIG.RPC_URL);
const usdt0 = new ethers.Contract(CONFIG.USDT0_ADDRESS, [
"function transfer(address to, uint256 amount) returns (bool)"
], provider);
const callData = usdt0.interface.encodeFunctionData("transfer", [
recipientAddress,
ethers.parseUnits("0.01", 18)
]);
const gasEstimate = await provider.estimateGas({
from: userWallet.address,
to: CONFIG.USDT0_ADDRESS,
data: callData,
});
const nonce = await provider.getTransactionCount(userWallet.address);
const innerTx = {
to: CONFIG.USDT0_ADDRESS,
data: callData,
value: 0,
gasPrice: 0,
gasLimit: gasEstimate,
nonce: nonce,
chainId: CONFIG.CHAIN_ID,
};
const signedInnerTx = await userWallet.signTransaction(innerTx);2단계: Waiver Server에 제출
import { CONFIG } from "./config";
const API_KEY = process.env.WAIVER_API_KEY;
const response = await fetch(`${CONFIG.WAIVER_SERVER}/v1/submit`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${API_KEY}`,
},
body: JSON.stringify({
transactions: [signedInnerTx],
}),
});배치 제출
단일 요청으로 여러 개의 서명된 트랜잭션을 제출할 수 있습니다:
body: JSON.stringify({
transactions: [signedTx1, signedTx2, signedTx3],
})각 결과 라인에는 배열 내 트랜잭션의 위치에 해당하는 index 필드가 포함됩니다.
3단계: 응답 처리
응답은 NDJSON(개행으로 구분된 JSON)으로 스트리밍됩니다. 각 라인은 제출된 하나의 트랜잭션에 해당합니다.
const reader = response.body.getReader();
const decoder = new TextDecoder();
while (true) {
const { done, value } = await reader.read();
if (done) break;
const lines = decoder.decode(value).trim().split("\n");
for (const line of lines) {
const result = JSON.parse(line);
if (result.success) {
console.log(`tx ${result.index} confirmed: ${result.txHash}`);
} else {
console.error(`tx ${result.index} failed: ${result.error.message}`);
}
}
}{"index": 0, "id": "abc123", "success": true, "txHash": "0x..."}{"index": 1, "id": "def456", "success": false, "error": {"code": "VALIDATION_FAILED", "message": "invalid signature"}}오류 코드
| 코드 | 설명 |
|---|---|
PARSE_ERROR | 트랜잭션 파싱 실패 |
INVALID_REQUEST | 잘못된 형식의 요청 본문 |
BATCH_SIZE_EXCEEDED | 배치 크기가 허용된 최대치를 초과함 |
VALIDATION_FAILED | 트랜잭션 검증 실패 (예: 잘못된 서명, 허용되지 않은 대상) |
BROADCAST_FAILED | 체인으로의 브로드캐스트 실패 |
RATE_LIMITED | 속도 제한 초과 |
QUEUE_FULL | 서버 큐가 가득 참 |
TIMEOUT | 요청 시간 초과 |
API 레퍼런스
GET /v1/health
상태 확인 엔드포인트. 인증: 없음.
POST /v1/submit
서명된 inner 트랜잭션의 배치를 제출합니다. 인증: 필수 (Bearer).
요청 본문:{
"transactions": ["0x<signedInnerTx1>", "0x<signedInnerTx2>"]
}응답은 NDJSON으로 스트리밍됩니다. 각 라인은 제출된 트랜잭션 인덱스에 해당합니다.
GET /v1/submit
스트리밍 제출을 위한 WebSocket 인터페이스. 인증: 필수 (Bearer).
핵심 요약
- Gas Waiver는 서버 측 통합입니다. 백엔드가 서명된 사용자 트랜잭션을 Waiver Server에 제출합니다. 사용자는 Waiver Server와 직접 상호작용하지 않습니다.
- 사용자가 항상 InnerTx에 서명하므로 서명 무결성이 유지됩니다. waiver는 사용자의 트랜잭션을 수정할 수 없습니다.
- 대상 컨트랙트는 waiver의
AllowedTarget목록에 있어야 합니다.
다음 추천
- 제로 가스 트랜잭션 — 데모 중심의 흐름과 영수증에서 제로 가스를 검증하는 방법을 확인하세요.
- 자체 호스팅 Gas Waiver — 호스팅된 API 없이 자체 waiver를 실행하세요.
- Gas waiver protocol — 전체 래퍼 트랜잭션 사양 및 거버넌스 모델.
- Stable SDK — 타입이 지정된 클라이언트를 사용해 사용자 트랜잭션에 서명한 후 Waiver Server에 제출하세요.

