学习 P2P 支付
本指南将带领您在 Stable 上构建一个 P2P 支付应用程序。该应用程序处理完整的支付生命周期:发送方直接转移 USDT0,接收方实时检测到入账,双方都可以查询交易历史。与任何钱包或支付界面(无论是移动应用、网页结账还是后端服务)的架构相同。
没有中间件,没有中介。有关概念性概述,请参见 P2P 支付。要跳过 ABI 工作并快速实现可用的 transfer,请使用 Stable SDK。
您将构建什么
构成最小支付应用程序的五个脚本:
wallet.ts: 创建或恢复钱包。getBalance.ts: 查询当前的 USDT0 余额。send.ts: 将 USDT0 发送到另一个地址。receive.ts: 实时监听入账。history.ts: 查询某个地址过去的 Transfer 事件。
演示
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 dotenvadded 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.tsAddress: 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 位小数
}
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...1234Balance: 0.01 USDT03. 发送付款
发送方直接签名并提交转账。在 Stable 上,USDT0 是原生资产,因此简单的价值转账是最便宜的路径(21,000 gas)。这与任何支付应用程序中“发送”的代码路径相同。
// 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.001Payment sent: 0x8f3a...2d41
Payment settled4. 实时接收付款
接收方监听入账 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...5678Watching for incoming payments to 0xBob...5678
Payment received:
from: 0xAlice...1234
amount: 0.001 USDT0
tx: 0x8f3a...2d415. 查询交易历史
查询过去的 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...1234sent 0.001 USDT0 0xBob...5678 0x8f3a...2d41
received 0.01 USDT0 0xFaucet... 0x22b1...3f09接下来推荐
- 订阅和收集:基于拉取的定期订阅与 EIP-7702 委托。
- 使用发票支付:使用 ERC-3009 和确定性随机数结算发票。
- 发送您的第一个 USDT0:参考基本的原生与 ERC-20 转账流程。

