StableOps
概念

確認

理解 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。

各鏈確認數要求

確認數時間說明
Base2 塊~4 秒OP rollup,回滾風險低
Optimism2 塊~4 秒OP rollup,回滾風險低
Ethereum6 塊~1.2 分鐘交易所標準
Arbitrum1 塊~0.3 秒OP rollup
Polygon20 塊~40 秒重組風險較高
BNB Chain7 塊~21 秒PoSA + BEP-126 Fast Finality
TRON1 塊~3 秒DPOS
Solana1 slot~0.4 秒使用 confirmed slot

Solana 與 Solana Devnet 目前使用同一組閾值:1 slot 進入 confirmed,32 slots 進入 finalized

風險

  • 理論上仍可能深度重組
  • 實際極罕見(< 0.01%)

適用

  • 小額(< $100)
  • 數字商品交付
  • 帳戶積分
  • 對時延敏感的履約

FINALIZED(達到最終確認深度)

交易達到 StableOps 設定的最終確認深度。StableOps 將 FINALIZED 視為付款單終態, 並建議生產履約等待該狀態;但這個基於深度的產品閾值不等同於協議級或 L1 finality 保證。

可靠度:很高

下表時間同樣只是按目前預設閾值和近似出塊時間推導的估算值;真實耗時會隨網路狀態 和 RPC 行為波動。

各鏈最終確認深度

深度時間
Base18 塊~36 秒
Optimism18 塊~36 秒
Ethereum64 塊~13 分鐘
Arbitrum20 塊~5 秒
Polygon128 塊~4.3 分鐘
BNB Chain21 塊~63 秒
TRON19 塊~1 分鐘
Solana32 slots~13 秒

風險:仍有很低的鏈、RPC 與運維殘餘風險;高價值或不可逆履約應結合自身風控。

適用

  • 大額(> $100)
  • 實物發貨
  • 不可逆操作(帳戶升級、訂閱)
  • 生產環境所有履約推薦用 finalized

REVERTED(交易回滾)

交易被回滾,可能因為鏈重組或執行失敗。

原因

  • 鏈重組:競爭鏈變長
  • 執行失敗:合約執行失敗
  • Gas 不足:交易耗盡 Gas
  • Receipt 丟失:交易從鏈上消失

頻率:極罕見(< 0.01%)

發生後

  1. StableOps 檢測到重組或失敗
  2. 訂單狀態變為 REVERTED
  3. 派發 payment.reverted
  4. 地址釋放回地址池(變回 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 (被孤立)

各鏈重組頻率

重組深度頻率備註
Ethereum1–2 塊每天通常無害
Ethereum> 3 塊罕見需要調查
Base1 塊偶發L2 重組少
Optimism1 塊偶發L2 重組少
Polygon1–5 塊較常見重組風險高
BNB Chain1–3 塊偶發PoSA,深重組罕見
TRON1 塊罕見DPOS
Solana1–2 slots偶發快速確認

StableOps 如何處理重組

  1. 持續監聽:每次確認都複查 receipt;支援 blockHash 的鏈會額外比對區塊 hash
  2. 檢測重組blockHash 不一致,或 receipt 丟失 / 失敗 → 視為回滾
  3. 狀態更新:訂單切到 REVERTED
  4. 派發 Webhook:傳送 payment.reverted
  5. 釋放地址:地址釋放回地址池(變回 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'
  }
}

下一步

這篇文件怎麼樣?

最後更新

本頁內容