Are you an LLM? Read llms.txt for a summary of the docs, or llms-full.txt for the full context.
Skip to content

P2P 결제 학습

이 가이드는 Stable에서 P2P 결제 애플리케이션을 구축하는 과정을 안내합니다. 이 앱은 전체 결제 수명 주기를 처리합니다. 즉, 발신자는 USDT0을 직접 전송하고, 수신자는 실시간으로 들어오는 결제를 감지하며, 양측 모두 자신의 거래 내역을 쿼리할 수 있습니다. 모바일 앱, 웹 결제, 백엔드 서비스 등 모든 지갑 또는 결제 인터페이스와 동일한 아키텍처입니다.

미들웨어, 중개자 없음. 개념적 개요는 P2P 결제를 참조하십시오. ABI 작업을 건너뛰고 몇 줄 안에 작동하는 transfer를 사용하려면 Stable SDK를 사용하십시오.

구축할 내용

최소한의 결제 앱을 구성하는 다섯 가지 스크립트:

  • wallet.ts: 지갑을 생성하거나 복원합니다.
  • getBalance.ts: 현재 USDT0 잔액을 쿼리합니다.
  • send.ts: 다른 주소로 USDT0을 보냅니다.
  • receive.ts: 실시간으로 들어오는 결제를 모니터링합니다.
  • history.ts: 주소에 대한 과거 전송 이벤트를 쿼리합니다.

데모

step 1. Alice creates wallet → address: 0xAlice...
step 2. Alice's balance: 0.01 USDT0

step 3. Alice sends 0.001 USDT0 to Bob
        tx:             0x8f3a...2d41
        gas fee:        0.000021 USDT0
        Alice balance:  0.008979 USDT0

step 4. Bob receives payment (real-time event)
        from:   0xAlice...
        amount: 0.001 USDT0
        tx:     0x8f3a...2d41

전제 조건

  • Node.js 20 이상.
  • 테스트넷 USDT0이 있는 프라이빗 키 (지갑 자금 조달을 위해 빠른 시작 참조).

프로젝트 설정

mkdir stable-p2p && cd stable-p2p
npm init -y && npm install ethers dotenv
added 2 packages, audited 3 packages in 1s

모든 스크립트에서 공유되는 config.ts 생성:

// config.ts
import { ethers } from "ethers";
import "dotenv/config";
 
export const STABLE_RPC = "https://rpc.testnet.stable.xyz";
export const STABLE_WS = "wss://rpc.testnet.stable.xyz";
export const USDT0_ADDRESS = "0x78Cf24370174180738C5B8E352B6D14c83a6c9A9";
 
export const provider = new ethers.JsonRpcProvider(STABLE_RPC);

1. 지갑 생성 또는 복원

지갑은 시드 구문에서 파생된 키 쌍입니다. 새 사용자를 위해 지갑을 생성하고 구문을 반환하여 백업할 수 있도록 합니다. 기존 사용자는 동일한 구문에서 지갑을 복원합니다.

// wallet.ts
import { ethers } from "ethers";
import { provider } from "./config";
 
/** 새 사용자를 위한 새 지갑을 생성합니다. */
export function createWallet() {
  const wallet = ethers.Wallet.createRandom(provider);
  return {
    wallet,
    address: wallet.address,
    seedPhrase: wallet.mnemonic!.phrase, // 백업을 위해 사용자에게 표시
  };
}
 
/** 시드 구문에서 지갑을 복원합니다 (기존 사용자). */
export function restoreWallet(seedPhrase: string) {
  const wallet = ethers.Wallet.fromPhrase(seedPhrase, provider);
  return { wallet, address: wallet.address };
}
 
if (import.meta.url === `file://${process.argv[1]}`) {
  const { address, seedPhrase } = createWallet();
  console.log("Address:    ", address);
  console.log("Seed phrase:", seedPhrase);
}
npx tsx wallet.ts
Address:     0xAlice...1234
Seed phrase: liberty shoot ... (12 words)

2. 잔액 확인

USDT0은 Stable의 기본 자산이므로 잔액 쿼리는 이더리움의 ETH와 똑같이 작동합니다. 기본 잔액은 18자리 소수이며, 표시를 위해 formatEther를 사용합니다.

// getBalance.ts
import { ethers } from "ethers";
import { provider } from "./config";
 
export async function getBalance(address: string) {
  const balance = await provider.getBalance(address);
  return ethers.formatEther(balance); // 18 decimals
}
 
if (import.meta.url === `file://${process.argv[1]}`) {
  const address = process.argv[2];
  const balance = await getBalance(address);
  console.log("Balance:", balance, "USDT0");
}
npx tsx getBalance.ts 0xAlice...1234
Balance: 0.01 USDT0

3. 결제 전송

보내는 사람은 직접 이체를 서명하고 제출합니다. Stable에서 USDT0은 기본 자산이므로 간단한 가치 이체가 가장 저렴한 경로입니다 (21,000 가스). 이는 모든 결제 앱의 "보내기"와 동일한 코드 경로입니다.

// send.ts
import { ethers } from "ethers";
import { provider } from "./config";
 
export async function sendPayment(
  senderKey: string,
  recipient: string,
  amount: string // 예시: 0.001 USDT0의 경우 "0.001"
) {
  const wallet = new ethers.Wallet(senderKey, provider);
  const block = await provider.getBlock("latest");
  const baseFee = block!.baseFeePerGas!;
 
  const tx = await wallet.sendTransaction({
    to: recipient,
    value: ethers.parseEther(amount),
    maxFeePerGas: baseFee * 2n,
    maxPriorityFeePerGas: 0n, // Stable에서는 항상 0
  });
 
  console.log("Payment sent:", tx.hash);
 
  const receipt = await tx.wait(1);
  if (receipt!.status === 1) console.log("Payment settled");
  return tx.hash;
}
 
if (import.meta.url === `file://${process.argv[1]}`) {
  const [, , recipient, amount] = process.argv;
  await sendPayment(process.env.PRIVATE_KEY!, recipient, amount);
}
npx tsx send.ts 0xBob...5678 0.001
Payment sent: 0x8f3a...2d41
Payment settled

4. 실시간으로 결제 수신

수신자는 들어오는 Transfer 이벤트를 수신합니다. 이는 기존 결제 앱의 푸시 알림과 동일합니다. Stable에서는 단일 슬롯 완결성 덕분에 수신자는 결제를 거의 즉시 확인합니다.

// receive.ts
import { ethers } from "ethers";
import { STABLE_WS, USDT0_ADDRESS } from "./config";
 
const wsProvider = new ethers.WebSocketProvider(STABLE_WS);
const usdt0 = new ethers.Contract(
  USDT0_ADDRESS,
  ["event Transfer(address indexed from, address indexed to, uint256 value)"],
  wsProvider
);
 
export function watchIncomingPayments(address: string) {
  const filter = usdt0.filters.Transfer(null, address);
 
  usdt0.on(filter, (from: string, to: string, value: bigint, event: any) => {
    console.log("Payment received:");
    console.log("  from:  ", from);
    console.log("  amount:", ethers.formatUnits(value, 6), "USDT0");
    console.log("  tx:    ", event.log.transactionHash);
  });
 
  console.log("Watching for incoming payments to", address);
}
 
if (import.meta.url === `file://${process.argv[1]}`) {
  watchIncomingPayments(process.argv[2]);
}
npx tsx receive.ts 0xBob...5678
Watching for incoming payments to 0xBob...5678
Payment received:
  from:   0xAlice...1234
  amount: 0.001 USDT0
  tx:     0x8f3a...2d41

5. 거래 내역 쿼리

과거 Transfer 이벤트를 쿼리하여 은행 명세서 또는 모든 결제 앱의 거래 목록과 같은 거래 내역 보기를 구축합니다.

// history.ts
import { ethers } from "ethers";
import { provider, USDT0_ADDRESS } from "./config";
 
const usdt0 = new ethers.Contract(
  USDT0_ADDRESS,
  ["event Transfer(address indexed from, address indexed to, uint256 value)"],
  provider
);
 
export async function getTransactionHistory(address: string, fromBlock?: number) {
  if (fromBlock === undefined) {
    const latest = await provider.getBlockNumber();
    fromBlock = Math.max(0, latest - 10_000);
  }
 
  const [sentEvents, receivedEvents] = await Promise.all([
    usdt0.queryFilter(usdt0.filters.Transfer(address, null), fromBlock),
    usdt0.queryFilter(usdt0.filters.Transfer(null, address), fromBlock),
  ]);
 
  return [
    ...sentEvents.map((e: any) => ({
      type: "sent" as const,
      counterparty: e.args[1],
      amount: ethers.formatUnits(e.args[2], 6),
      txHash: e.transactionHash,
      block: e.blockNumber,
    })),
    ...receivedEvents.map((e: any) => ({
      type: "received" as const,
      counterparty: e.args[0],
      amount: ethers.formatUnits(e.args[2], 6),
      txHash: e.transactionHash,
      block: e.blockNumber,
    })),
  ].sort((a, b) => b.block - a.block);
}
 
if (import.meta.url === `file://${process.argv[1]}`) {
  const history = await getTransactionHistory(process.argv[2]);
  for (const tx of history) {
    console.log(`${tx.type}  ${tx.amount} USDT0  ${tx.counterparty}  ${tx.txHash}`);
  }
}
npx tsx history.ts 0xAlice...1234
sent      0.001 USDT0  0xBob...5678    0x8f3a...2d41
received  0.01  USDT0  0xFaucet...     0x22b1...3f09

다음 권장 사항