메인 콘텐츠로 건너뛰기

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로 배포하기

수수료 지불자가 상호작용할 컨트랙트를 배포합니다.

이벤트 구독하기

수수료 위임 트랜잭션이 발생시키는 이벤트를 구독합니다.