发票支付
本指南将介绍如何使用 ERC-3009 在链上结算发票,其中确定性随机数来源于发票元数据。该随机数将每次支付与发票关联起来,并防止重复支付。
您将构建什么
一个完整的发票生命周期:买方在链下签署 ERC-3009 授权,供应商在链上提交,对账将生成的 AuthorizationUsed 事件通过确定性随机数与发票匹配。
演示
step 1. Invoice issued
number: INV-2026-001234
amount: 5000 USDT0
dueDate: 2026-04-30
step 2. Buyer signs authorization (off-chain, no gas)
nonce: 0xa1b2...c3d4 (from invoice metadata)
signature: 0xf0e9...1234
step 3. Vendor submits transferWithAuthorization
tx: 0x8f3a...2d41
amount: 5000 USDT0 transferred to vendor
step 4. Reconciliation
AuthorizationUsed(nonce=0xa1b2...) → invoice INV-2026-001234
Transfer event verified for correct amount and parties
ERP: marked PAID at block 1284371概述
买方:─── Buyer ───────────────────────────────────────────
nonce = getInvoiceNonce(invoice)
authorization = { from: buyer, to: vendor, value: amount, nonce, ... }
signature = signTypedData(authorization)
// Option A: Buyer submits the transaction directly.
usdt0.transferWithAuthorization(authorization, signature)
// Option B: Buyer sends {authorization, signature} to the vendor.
// The vendor (or a facilitator) submits on the buyer's behalf.─── Vendor ──────────────────────────────────────────
// If Option B: submit transferWithAuthorization using the buyer's signature
// Reconcile via AuthorizationUsed event
on AuthorizationUsed(authorizer, nonce):
invoice = nonceToInvoice.get(nonce)
transferLog = receipt.logs.find(Transfer matching invoice.buyer, invoice.vendor, invoice.amount)
if transferLog:
erpSystem.markPaid(invoice.id, txHash, settledAt)配置
// config.ts
import { ethers } from "ethers";
export const STABLE_TESTNET_RPC = "https://rpc.testnet.stable.xyz";
export const CHAIN_ID = 2201;
export const USDT0_ADDRESS = "0x78Cf24370174180738C5B8E352B6D14c83a6c9A9";
export const provider = new ethers.JsonRpcProvider(STABLE_TESTNET_RPC);
export const EIP712_DOMAIN = {
name: "USDT0",
version: "1",
chainId: CHAIN_ID,
verifyingContract: USDT0_ADDRESS,
};
export const TRANSFER_WITH_AUTHORIZATION_TYPE = {
TransferWithAuthorization: [
{ name: "from", type: "address" },
{ name: "to", type: "address" },
{ name: "value", type: "uint256" },
{ name: "validAfter", type: "uint256" },
{ name: "validBefore", type: "uint256" },
{ name: "nonce", type: "bytes32" },
],
};
export interface Invoice {
number: string; // e.g. "INV-2026-001234"
vendor: string; // vendor wallet address
buyer: string; // buyer wallet address
amount: bigint; // amount in USDT0 atomic units (6 decimals)
dueDate: number; // Unix timestamp
}步骤 1:生成一个确定性随机数
买方和供应商都可以从发票元数据中独立计算出相同的随机数。无需外部注册表。
// nonce.ts
import { ethers } from "ethers";
import { Invoice } from "./config";
export function getInvoiceNonce(invoice: Invoice): string {
return ethers.solidityPackedKeccak256(
["string", "address", "address", "uint256", "uint256"],
[
invoice.number,
invoice.vendor,
invoice.buyer,
invoice.amount,
invoice.dueDate,
]
);
}
// Example
const invoice: Invoice = {
number: "INV-2026-001234",
vendor: "0xVendorAddress",
buyer: "0xBuyerAddress",
amount: ethers.parseUnits("5000", 6), // 5,000 USDT0
dueDate: Math.floor(new Date("2026-04-30").getTime() / 1000),
};
const nonce = getInvoiceNonce(invoice);
// Same input always produces the same nonce.
// This nonce is consumed on-chain upon payment, preventing double payment.步骤 2:签署授权(买方)
买方使用步骤 1 中的确定性随机数签署 ERC-3009 transferWithAuthorization。
// sign-invoice.ts
import { ethers } from "ethers";
import {
provider,
EIP712_DOMAIN,
TRANSFER_WITH_AUTHORIZATION_TYPE,
Invoice,
} from "./config";
import { getInvoiceNonce } from "./nonce";
const buyerWallet = new ethers.Wallet(process.env.BUYER_KEY!, provider);
async function signInvoiceAuthorization(invoice: Invoice) {
const nonce = getInvoiceNonce(invoice);
const gracePeriod = 30 * 24 * 60 * 60; // 30 days after due date
const authorization = {
from: invoice.buyer,
to: invoice.vendor,
value: invoice.amount,
validAfter: 0,
validBefore: invoice.dueDate + gracePeriod,
nonce,
};
const signature = await buyerWallet.signTypedData(
EIP712_DOMAIN,
TRANSFER_WITH_AUTHORIZATION_TYPE,
authorization
);
return { authorization, signature };
}步骤 3:提交交易
根据提交者的不同,有两种选择。
选项 A:买方提交
买方直接提交 transferWithAuthorization 交易并支付 gas。当买方控制支付的执行时间和方式时使用此方法,例如当买方的会计系统需要与内部审批流程关联的交易哈希时。
// pay.ts
import { ethers } from "ethers";
import { provider, USDT0_ADDRESS } from "./config";
const buyerWallet = new ethers.Wallet(process.env.BUYER_KEY!, provider);
const usdt0 = new ethers.Contract(
USDT0_ADDRESS,
[
"function transferWithAuthorization(address from, address to, uint256 value, uint256 validAfter, uint256 validBefore, bytes32 nonce, uint8 v, bytes32 r, bytes32 s)",
],
buyerWallet,
);
async function payInvoice(
authorization: { from: string; to: string; value: bigint; validAfter: number; validBefore: number; nonce: string },
signature: string,
) {
const { v, r, s } = ethers.Signature.from(signature);
const tx = await usdt0.transferWithAuthorization(
authorization.from,
authorization.to,
authorization.value,
authorization.validAfter,
authorization.validBefore,
authorization.nonce,
v, r, s,
);
const receipt = await tx.wait(1);
console.log("Invoice paid, tx:", receipt.hash);
// The nonce is now consumed; the same invoice cannot be paid twice.
return { txHash: receipt.hash, blockNumber: receipt.blockNumber };
}选项 B:供应商提交
买方通过 API、电子邮件或任何渠道将 {authorization, signature} 发送给供应商。供应商(或促成者)代表买方提交交易,因此买方无需管理 Gas。当供应商需要在同一请求流程中进行同步确认时使用此方法。
// settle.ts
import { ethers } from "ethers";
import { provider, USDT0_ADDRESS } from "./config";
const vendorWallet = new ethers.Wallet(process.env.VENDOR_KEY!, provider);
const usdt0 = new ethers.Contract(
USDT0_ADDRESS,
[
"function transferWithAuthorization(address from, address to, uint256 value, uint256 validAfter, uint256 validBefore, bytes32 nonce, uint8 v, bytes32 r, bytes32 s)",
],
vendorWallet,
);
async function settleInvoice(
authorization: { from: string; to: string; value: bigint; validAfter: number; validBefore: number; nonce: string },
signature: string,
) {
const { v, r, s } = ethers.Signature.from(signature);
const tx = await usdt0.transferWithAuthorization(
authorization.from,
authorization.to,
authorization.value,
authorization.validAfter,
authorization.validBefore,
authorization.nonce,
v, r, s,
);
const receipt = await tx.wait(1);
console.log("Invoice settled, tx:", receipt.hash);
return { txHash: receipt.hash, blockNumber: receipt.blockNumber };
}步骤 4:通过链上事件进行对账(供应商)
无论谁提交了交易,每次发票支付都会发出一个 AuthorizationUsed 事件,其中包含确定性随机数。供应商监听此事件,并通过随机数将其与待处理发票进行匹配。由于随机数来源于发票元数据,因此匹配是精确的。
// reconcile.ts
import { ethers } from "ethers";
import { provider, USDT0_ADDRESS, Invoice } from "./config";
import { getInvoiceNonce } from "./nonce";
const usdt0 = new ethers.Contract(
USDT0_ADDRESS,
[
"event AuthorizationUsed(address indexed authorizer, bytes32 indexed nonce)",
"event Transfer(address indexed from, address indexed to, uint256 value)",
],
provider,
);
// Build a lookup map: nonce -> invoice.
// In production, this comes from your invoice database.
const invoices: Invoice[] = [
{
number: "INV-2026-001234",
vendor: "0xVendorAddress",
buyer: "0xBuyerAddress",
amount: ethers.parseUnits("5000", 6),
dueDate: Math.floor(new Date("2026-04-30").getTime() / 1000),
},
];
const nonceToInvoice = new Map<string, Invoice>();
for (const inv of invoices) {
nonceToInvoice.set(getInvoiceNonce(inv), inv);
}
usdt0.on("AuthorizationUsed", async (authorizer: string, nonce: string, event: any) => {
const invoice = nonceToInvoice.get(nonce);
if (!invoice) return; // not one of our invoices
const receipt = await event.getTransactionReceipt();
const transferLog = receipt.logs
.map((log: any) => {
try { return usdt0.interface.parseLog(log); } catch { return null; }
})
.find(
(parsed: any) =>
parsed?.name === "Transfer" &&
parsed.args[0].toLowerCase() === invoice.buyer.toLowerCase() &&
parsed.args[1].toLowerCase() === invoice.vendor.toLowerCase() &&
parsed.args[2] === invoice.amount
);
if (!transferLog) {
console.error("No matching Transfer event for invoice:", invoice.number);
return;
}
// All checks passed
console.log(`Invoice ${invoice.number} PAID`);
console.log(" tx:", receipt.hash);
console.log(" settled at block:", receipt.blockNumber);
// In production: update your ERP/accounting system here
// erpSystem.markPaid(invoice.number, receipt.hash, receipt.blockNumber);
});
console.log("Listening for invoice settlements...");npx tsx reconcile.tsListening for invoice settlements...
Invoice INV-2026-001234 PAID
tx: 0x8f3a...2d41
settled at block: 1284371处理失败的支付
提交的 transferWithAuthorization 可能会因多种原因而回滚。检测并向供应商或买方报告每个原因,以便可以重试或关闭发票。
| 回滚原因 | 原因 | 恢复 |
|---|---|---|
FiatTokenV2: invalid signature | 签名与授权字段不匹配。 | 要求买方使用未更改的发票数据重新签名。 |
FiatTokenV2: authorization is used or canceled | 随机数已被使用(重复提交)或买方已取消。 | 将发票标记为已支付;通过随机数查找原始交易。 |
FiatTokenV2: authorization is not yet valid | 在 validAfter 之前提交。 | 等待 validAfter 或签发新的授权。 |
FiatTokenV2: authorization is expired | 在 validBefore 之后提交。 | 签发新的授权,并延长有效期。 |
FiatTokenV2: transfer amount exceeds balance | 买方的 USDT0 余额不足。 | 通知买方充值钱包,然后重试同一签名。 |
在重试之前捕获回滚并对其进行分类。
// retry.ts
import { ethers } from "ethers";
async function submitWithRetry(
submit: () => Promise<ethers.ContractTransactionResponse>,
): Promise<string> {
try {
const tx = await submit();
const receipt = await tx.wait(1);
return receipt!.hash;
} catch (err: any) {
const reason = err?.info?.error?.message || err?.reason || err?.message || "";
if (reason.includes("authorization is used or canceled")) {
// Lookup the original tx by AuthorizationUsed event; mark invoice paid.
throw new Error("ALREADY_PAID");
}
if (reason.includes("authorization is expired")) {
throw new Error("AUTHORIZATION_EXPIRED");
}
if (reason.includes("invalid signature")) {
throw new Error("INVALID_SIGNATURE");
}
if (reason.includes("transfer amount exceeds balance")) {
throw new Error("INSUFFICIENT_BALANCE");
}
throw err;
}
}接下来推荐
- 发票结算概念:了解确定性随机数对账模型。
- ERC-3009:回顾此流程背后的签名授权标准。
- 启用免 Gas 交易:结合 Gas Waiver 以消除结算路径中的 Gas。

