確認
理解 StableOps 如何讓支付依次進入 detected、confirmed、finalized 或 reverted 狀態,瞭解各階段的確認閾值、區塊重組風險和業務含義,並據此選擇安全履約時機、處理狀態回退以及為不同鏈設定適當的最終性策略。
鏈上確認(confirmation)是交易逐步變得不可逆的過程。StableOps 把支付追蹤劃分成四個 階段,在速度與安全之間做平衡。
總覽
鏈上發起一筆支付後,它會經過幾個階段才算最終化:
DETECTED → CONFIRMED → FINALIZED
↓ ↓
REVERTED REVERTED每個階段代表不同的可靠度:
- DETECTED:在可查詢的鏈上區塊裡看到交易
- CONFIRMED:達到該鏈所需的確認數
- FINALIZED:達到 StableOps 設定的最終確認深度
- REVERTED:因重組或失敗被回滾
為什麼需要確認
區塊鏈是分散式系統,多個節點競爭出塊,這導致:
- 鏈重組(reorg):競爭鏈超過目前鏈,最近的區塊被替換
- 交易失敗:合約執行失敗、Gas 不足等
- 雙花嘗試:惡意方試圖回滾交易
「確認」就是把交易埋到足夠深,使反轉在算力上幾乎不可能。
各確認階段
DETECTED
交易已經進入 StableOps 可掃描並能匹配到付款單的鏈上區塊。StableOps 不會僅憑 mempool 資料推進訂單狀態。
對多數鏈來說,這基本等價於“第一次在區塊裡看到交易”。但在個別網路上,為了避開
RPC 日誌索引滯後,StableOps 可能會刻意落後鏈頭少量區塊掃描,因此 detected
更準確的含義是“已經在鏈上看到”,而不是“嚴格等於 0 確認”。
可靠度:低(5–10%)
風險:
- 交易可能被 RBF 替換
- 區塊可能在重組中被孤立
- 合約執行可能失敗
做什麼:
- UI 切到「已收到,確認中…」
- 訂單標記為「待確認」
- 此時不要履約
CONFIRMED(達到鏈特定確認數)
交易已經達到該鏈所需確認數。
可靠度:高(95–99%)
下表時間是按 StableOps 目前預設閾值和近似出塊時間推匯出的估算值,不是協議保證, 也不應被視為 SLA。
各鏈確認數要求:
| 鏈 | 確認數 | 時間 | 說明 |
|---|---|---|---|
| Base | 2 塊 | ~4 秒 | OP rollup,回滾風險低 |
| Optimism | 2 塊 | ~4 秒 | OP rollup,回滾風險低 |
| Ethereum | 6 塊 | ~1.2 分鐘 | 交易所標準 |
| Arbitrum | 1 塊 | ~0.3 秒 | OP rollup |
| Polygon | 20 塊 | ~40 秒 | 重組風險較高 |
| BNB Chain | 7 塊 | ~21 秒 | PoSA + BEP-126 Fast Finality |
| TRON | 1 塊 | ~3 秒 | DPOS |
| Solana | 1 slot | ~0.4 秒 | 使用 confirmed slot |
Solana 與 Solana Devnet 目前使用同一組閾值:1 slot 進入 confirmed,32 slots 進入
finalized。
風險:
- 理論上仍可能深度重組
- 實際極罕見(< 0.01%)
適用:
- 小額(< $100)
- 數字商品交付
- 帳戶積分
- 對時延敏感的履約
FINALIZED(達到最終確認深度)
交易達到 StableOps 設定的最終確認深度。StableOps 將 FINALIZED 視為付款單終態,
並建議生產履約等待該狀態;但這個基於深度的產品閾值不等同於協議級或 L1 finality 保證。
可靠度:很高
下表時間同樣只是按目前預設閾值和近似出塊時間推導的估算值;真實耗時會隨網路狀態 和 RPC 行為波動。
各鏈最終確認深度:
| 鏈 | 深度 | 時間 |
|---|---|---|
| Base | 18 塊 | ~36 秒 |
| Optimism | 18 塊 | ~36 秒 |
| Ethereum | 64 塊 | ~13 分鐘 |
| Arbitrum | 20 塊 | ~5 秒 |
| Polygon | 128 塊 | ~4.3 分鐘 |
| BNB Chain | 21 塊 | ~63 秒 |
| TRON | 19 塊 | ~1 分鐘 |
| Solana | 32 slots | ~13 秒 |
風險:仍有很低的鏈、RPC 與運維殘餘風險;高價值或不可逆履約應結合自身風控。
適用:
- 大額(> $100)
- 實物發貨
- 不可逆操作(帳戶升級、訂閱)
- 生產環境所有履約推薦用 finalized
REVERTED(交易回滾)
交易被回滾,可能因為鏈重組或執行失敗。
原因:
- 鏈重組:競爭鏈變長
- 執行失敗:合約執行失敗
- Gas 不足:交易耗盡 Gas
- Receipt 丟失:交易從鏈上消失
頻率:極罕見(< 0.01%)
發生後:
- StableOps 檢測到重組或失敗
- 訂單狀態變為
REVERTED - 派發
payment.reverted - 地址釋放回地址池(變回
AVAILABLE供複用)
完整 payload 結構請參見 Payment 事件 API 參考。
處理方式:
app.post('/webhooks/stableops', async (req, res) => {
const event = req.body
if (event.type === 'payment.reverted') {
const orderId = event.data.payment_order_id
// 1. 沖銷已經做的履約
await reverseOrderFulfillment(orderId)
// 2. 通知客戶
await sendEmail(customer, '支付失敗,請重試')
// 3. 更新資料庫
await db.orders.update({
id: orderId,
status: 'payment_failed',
reason: event.data.reason,
})
// 4. 可選:建立新訂單
const newOrder = await stableops.paymentOrders.create({
merchantOrderId: `${orderId}_retry`,
amount: originalAmount,
expiresAt: new Date(Date.now() + 30 * 60 * 1000).toISOString(),
// ...
})
}
res.sendStatus(200)
})選擇合適的確認級別
決策矩陣
| 金額 | 型別 | 推薦階段 | 原因 |
|---|---|---|---|
| < $10 | 數字商品 | CONFIRMED | 快、風險低 |
| $10 – $100 | 數字商品 | CONFIRMED | 平衡速度與安全 |
| $100 – $1,000 | 數字商品 | FINALIZED | 需要更高安全 |
| > $1,000 | 任意 | FINALIZED | 最高安全 |
| 任意 | 實物 | FINALIZED | 發貨不可逆 |
| 任意 | 帳戶升級 | FINALIZED | 操作不可逆 |
| 任意 | 訂閱 | FINALIZED | 週期性扣費 |
風險偏好
低容忍(金融服務、高價值商品):
- 一律等 FINALIZED
- 絕不在 DETECTED 履約
- 額外加風控
中等容忍(電商、SaaS):
-
$100 等 FINALIZED
- < $100 用 CONFIRMED
- DETECTED 只用於重新整理 UI
高容忍(遊戲、低價值數字商品):
- 大多數交易用 CONFIRMED
- 帳戶變更用 FINALIZED
- 接受偶發回滾
監控確認數
即時推送
StableOps 持續監聽鏈上,確認數每次跨閾值都會推送 Webhook:
// 一筆支付的事件時間線
12:00:00 - payment.detected
12:00:12 - payment.confirmed
12:13:00 - payment.finalized輪詢替代方案
如果你傾向輪詢:
const checkPaymentStatus = async (orderId: string) => {
const order = await stableops.paymentOrders.retrieve(orderId)
switch (order.status) {
case 'detected':
console.log('已收到,等待確認...')
break
case 'confirmed':
console.log('已確認,等待 finality...')
break
case 'finalized':
console.log('已 finalize,可安全履約')
await fulfillOrder(order)
break
case 'reverted':
console.log('已回滾,處理失敗流程')
await handleRevert(order)
break
}
}
// 每 5 秒輪詢
const interval = setInterval(() => checkPaymentStatus(orderId), 5000)鏈重組
什麼是重組
競爭鏈超過目前鏈,導致最近的區塊被替換:
重組前:
Block 100 → Block 101 → Block 102 (你的交易)
↘ Block 102' (競爭鏈)
重組後:
Block 100 → Block 101 → Block 102' → Block 103'
✗ Block 102 (被孤立)各鏈重組頻率
| 鏈 | 重組深度 | 頻率 | 備註 |
|---|---|---|---|
| Ethereum | 1–2 塊 | 每天 | 通常無害 |
| Ethereum | > 3 塊 | 罕見 | 需要調查 |
| Base | 1 塊 | 偶發 | L2 重組少 |
| Optimism | 1 塊 | 偶發 | L2 重組少 |
| Polygon | 1–5 塊 | 較常見 | 重組風險高 |
| BNB Chain | 1–3 塊 | 偶發 | PoSA,深重組罕見 |
| TRON | 1 塊 | 罕見 | DPOS |
| Solana | 1–2 slots | 偶發 | 快速確認 |
StableOps 如何處理重組
- 持續監聽:每次確認都複查 receipt;支援
blockHash的鏈會額外比對區塊 hash - 檢測重組:
blockHash不一致,或 receipt 丟失 / 失敗 → 視為回滾 - 狀態更新:訂單切到
REVERTED - 派發 Webhook:傳送
payment.reverted - 釋放地址:地址釋放回地址池(變回
AVAILABLE)
重組防護
StableOps 多層防護:
- Block hash 校驗:比對儲存的
blockHash與目前鏈(TRON / Solana 沒有使用該欄位) - Receipt 校驗:確認 receipt 仍然存在
- 確認數計數:只統計 canonical chain
- 最終確認深度:等待達到各鏈設定的最終確認深度
最佳實踐
1. 關鍵履約用 FINALIZED
// ✅ 正確:等 finality
app.post('/webhooks/stableops', async (req, res) => {
const event = req.body
if (event.type === 'payment.finalized') {
await fulfillOrder(event.data.payment_order_id)
}
res.sendStatus(200)
})
// ❌ 錯誤:在 detected 就履約
app.post('/webhooks/stableops', async (req, res) => {
const event = req.body
if (event.type === 'payment.detected') {
await fulfillOrder(event.data.payment_order_id) // 危險!
}
res.sendStatus(200)
})2. 處理所有狀態
const handlePaymentWebhook = async (event: WebhookEvent) => {
switch (event.type) {
case 'payment.detected':
await updateUI('已收到,確認中…')
break
case 'payment.confirmed':
await updateUI('已確認,最終化中…')
break
case 'payment.finalized':
await fulfillOrder(event.data.payment_order_id)
break
case 'payment.reverted':
await handleRevert(event.data.payment_order_id)
break
}
}3. 向使用者展示進度
const PaymentStatus = ({ order }) => {
switch (order.status) {
case 'detected':
return (
<div>
<Spinner />
<p>已收到,確認中…</p>
</div>
)
case 'confirmed':
return (
<div>
<Spinner />
<p>已確認,最終化中…</p>
</div>
)
case 'finalized':
return (
<div>
<CheckIcon />
<p>支付完成!訂單正在處理。</p>
</div>
)
}
}4. 詳盡日誌
app.post('/webhooks/stableops', async (req, res) => {
const event = req.body
await db.webhookLogs.create({
type: event.type,
orderId: event.data.payment_order_id,
fromStatus: event.data.from_status,
toStatus: event.data.to_status,
timestamp: new Date(),
})
// 業務處理...
res.sendStatus(200)
})5. 測試重組場景
describe('支付重組處理', () => {
it('應在重組時沖銷履約', async () => {
// 1. 建立訂單
const order = await createOrder()
// 2. 模擬 detected
await handleWebhook({ type: 'payment.detected', data: order })
// 3. 模擬 confirmed
await handleWebhook({ type: 'payment.confirmed', data: order })
// 4. 模擬回滾
await handleWebhook({ type: 'payment.reverted', data: order })
// 5. 驗證已沖銷
const dbOrder = await db.orders.findOne({ id: order.id })
expect(dbOrder.status).toBe('payment_failed')
})
})常見模式
分級履約
不同確認階段提供不同權益:
app.post('/webhooks/stableops', async (req, res) => {
const event = req.body
const orderId = event.data.payment_order_id
switch (event.type) {
case 'payment.confirmed':
// 臨時權益
await grantTrialAccess(orderId)
break
case 'payment.finalized':
// 完整權益
await grantFullAccess(orderId)
break
case 'payment.reverted':
// 全部撤銷
await revokeAccess(orderId)
break
}
res.sendStatus(200)
})按金額條件履約
不同金額取不同確認級別:
const shouldFulfill = (order: PaymentOrder): boolean => {
const amount = parseFloat(order.amount)
if (amount < 100) {
// 小額:confirmed 即可
return order.status === 'confirmed' || order.status === 'finalized'
} else {
// 大額:等 finalized
return order.status === 'finalized'
}
}下一步
這篇文件怎麼樣?
最後更新