Documentation Index
Fetch the complete documentation index at: https://docs.stablenet.network/llms.txt
Use this file to discover all available pages before exploring further.
수수료 지불자 계정이 트랜잭션 발신자 대신 가스 비용을 부담하는 이중 서명 흐름을 구현합니다.
배우는 내용
이 튜토리얼을 완료하면 다음을 할 수 있습니다:
- ethers.js로 수수료 위임 트랜잭션(타입
0x16) 빌드 및 서명
- 부분 서명된 트랜잭션에 수수료 지불자 서명 추가
- 완성된 트랜잭션을 브로드캐스트하고 가스가 수수료 지불자에게 청구됐는지 확인
수수료 위임 작동 방식
StableNet의 수수료 위임은 트랜잭션 타입 0x16을 사용해 트랜잭션 서명과 가스 납부를 분리합니다.
| 역할 | 서명 | 납부 |
|---|
발신자 (From) | 트랜잭션 의도 (v, r, s) | 전송 가치만 |
수수료 지불자 (FeePayer) | 가스 동의 (fv, fr, fs) | gasLimit × gasPrice |
두 서명 모두 필수입니다. 노드는 트랜잭션을 멤풀에 수락하기 전에 양쪽 서명을 모두 검증합니다. 실행 후 미사용 가스는 발신자가 아닌 수수료 지불자에게 환불됩니다.
수수료 위임은 Applepie 포크 활성화가 필요합니다. 포크 활성화 전에 제출된 타입 0x16 트랜잭션은 즉시 거부됩니다.
사전 준비
- Node.js ≥ 18 및
npm install ethers (v6)
- 테스트넷 계정 두 개: 발신자 지갑, 수수료 지불자 지갑
- 두 계정 모두 Faucet에서 테스트넷 WKRC 받기
트랜잭션 빌드 및 서명
발신자가 트랜잭션을 빌드하고, feePayer를 수수료 지불자 주소로 설정한 후 서명합니다.
import { ethers } from "ethers";
const SENDER_PRIVATE_KEY = process.env.SENDER_PRIVATE_KEY!;
const FEE_PAYER_PRIVATE_KEY = process.env.FEE_PAYER_PRIVATE_KEY!;
const provider = new ethers.JsonRpcProvider("https://api.test.stablenet.network");
const senderWallet = new ethers.Wallet(SENDER_PRIVATE_KEY, provider);
const feePayerWallet = new ethers.Wallet(FEE_PAYER_PRIVATE_KEY, provider);
// 1. 미서명 트랜잭션 빌드 (타입 0x16)
const unsignedTx = {
type: 0x16,
chainId: 8283,
nonce: await provider.getTransactionCount(senderWallet.address),
to: "0xRecipientAddress",
value: ethers.parseEther("1.0"),
gas: 30400n,
maxFeePerGas: ethers.parseUnits("80000", "gwei"),
maxPriorityFeePerGas: ethers.parseUnits("27600", "gwei"),
feePayer: feePayerWallet.address,
input: "0x",
accessList: [],
};
// 2. 발신자 서명
const senderSig = await senderWallet.signTransaction(unsignedTx);
수수료 지불자 서명 추가
수수료 지불자가 부분 서명된 트랜잭션을 받아 내용을 확인한 후 서명을 추가합니다.
// 3. 수수료 지불자 확인 및 서명
const decoded = ethers.Transaction.from(senderSig);
// 서명 전 트랜잭션 확인 — 수수료 지불자는 가스 납부를 승인하는 것입니다
console.log("to:", decoded.to);
console.log("value:", ethers.formatEther(decoded.value), "WKRC");
const feePayerSig = await feePayerWallet.signTransaction(decoded);
수수료 지불자는 서명 전에 to, value, data, gas를 반드시 확인하세요 — 의도치 않은 작업에 가스를 납부하는 것을 방지합니다.
완성된 트랜잭션 브로드캐스트
// 4. 완전히 서명된 트랜잭션 브로드캐스트
const txResponse = await provider.broadcastTransaction(feePayerSig);
console.log("tx hash:", txResponse.hash);
const receipt = await txResponse.wait();
console.log("Confirmed in block", receipt.blockNumber);
console.log("Gas used:", receipt.gasUsed.toString());
tx hash: 0xabc...
Confirmed in block 1234567
Gas used: 21000
수수료 지불자에게 가스가 청구됐는지 확인
수수료 지불자의 잔액을 트랜잭션 전후로 확인해 발신자가 아닌 수수료 지불자 계정에서 가스가 차감됐는지 검증합니다.
const feePayerBalance = await provider.getBalance(feePayerWallet.address);
console.log("Fee payer balance:", ethers.formatEther(feePayerBalance), "WKRC");
Explorer에서 트랜잭션 해시를 검색하면(explorer.stablenet.network) 트랜잭션 상세 정보의 feePayer 필드에서 가스를 납부한 계정을 확인할 수 있습니다.
핵심 불변 조건
tx.origin은 항상 발신자입니다. msg.sender 또는 tx.origin을 확인하는 기존 컨트랙트는 수정 없이 동작합니다.
- 수수료 지불자의 잔액은 가치 전송에 사용될 수 없습니다 — 가스만 위험에 노출됩니다.
- 미사용 가스는 수수료 지불자에게 자동으로 환불됩니다.
- EVM 실행 내부에서는 수수료 위임이 사용됐는지 감지할 수 없습니다 — 스마트 컨트랙트는 수수료 위임 여부를 알 수 없습니다.
다음 단계
수수료 위임 사용 사례
수수료 위임으로 가스리스 dApp 흐름을 구현합니다.
RPC API 레퍼런스
personal_signRawFeeDelegateTransaction 및 관련 메서드.
Foundry로 배포하기
수수료 지불자가 상호작용할 컨트랙트를 배포합니다.
이벤트 구독하기
수수료 위임 트랜잭션이 발생시키는 이벤트를 구독합니다.