StableOps
指南

Next.js

按照本指南在 Next.js App Router 中整合 StableOps:在伺服器端建立冪等付款訂單,使用錢包 SDK 發起鏈上轉帳,接收並驗證簽名 Webhook,再以可靠的狀態查詢完成履約;同時覆蓋金鑰隔離、錯誤處理和本地測試。

本指南使用 App Router 和目前的 TypeScript SDK。

想透過 StableOps 託管結帳頁在 Next.js 中接收 USDC?閱讀 部落格教程,並檢視可一鍵部署到 Vercel 的 完整示例專案。

安裝

pnpm add @stableops/api-sdk

設定

STABLEOPS_API_KEY=sk_sandbox_...
STABLEOPS_WEBHOOK_SECRET=whsec_...

建立 lib/stableops.ts:

import { StableOps } from '@stableops/api-sdk'

export const stableops = new StableOps({
  apiKey: process.env.STABLEOPS_API_KEY!,
})

本指南使用沙盒金鑰和 Base Sepolia 測試網。請先在沙盒環境匯入 base-sepolia 收款地址,並準備測試 USDC。上線時需同時切換為生產金鑰、主網資產和生產地址池,不能只修改金鑰。

以下示例省略了業務鑑權。生產環境必須驗證目前使用者及訂單歸屬,從伺服器端商品或帳單記錄讀取應付金額,不得直接信任表單或請求體中的金額;查詢與取消訂單也必須檢查許可權。

建立訂單

建立 app/actions/payment.ts:

'use server'

import { revalidatePath } from 'next/cache'
import { stableops } from '@/lib/stableops'

export async function createPaymentOrder(formData: FormData) {
  const amount = String(formData.get('amount') ?? '')
  const merchantOrderId = String(formData.get('orderId') ?? '')

  const order = await stableops.paymentOrders.create(
    {
      merchantOrderId,
      amount,
      acceptedAssets: [{ chain: 'base-sepolia', asset: 'USDC' }],
      expiresAt: new Date(Date.now() + 30 * 60 * 1000).toISOString(),
      metadata: { source: 'nextjs_checkout' },
    },
    { idempotencyKey: merchantOrderId },
  )

  revalidatePath('/payments')
  return order
}

把返回的一條候選支付指令展示給使用者:

{
  order.paymentInstructions[0] ? (
    <div>
      在 {order.paymentInstructions[0].chain} 上向以下地址傳送 {order.amount}{' '}
      {order.paymentInstructions[0].asset}:<code>{order.paymentInstructions[0].address}</code>
    </div>
  ) : null
}

驗證 Webhook

建立 app/api/webhooks/stableops/route.ts:

import { SIGNATURE_HEADER, verifySignature } from '@stableops/api-sdk/webhooks'

const WEBHOOK_SECRET = process.env.STABLEOPS_WEBHOOK_SECRET!

export async function POST(req: Request) {
  const rawBody = await req.text()
  const result = verifySignature({
    secrets: [WEBHOOK_SECRET],
    header: req.headers.get(SIGNATURE_HEADER) ?? undefined,
    rawBody,
  })

  if (!result.ok) {
    return new Response(`invalid signature: ${result.reason}`, { status: 400 })
  }

  const event = JSON.parse(rawBody) as {
    type: string
    data: { payment_order_id?: string }
  }

  switch (event.type) {
    case 'payment.detected':
      break
    case 'payment.confirmed':
      break
    case 'payment.finalized':
      // 做完自家冪等檢查後再履約。
      break
    case 'payment.expired':
    case 'payment.reverted':
      break
  }

  return new Response('ok')
}

示例中的分支僅演示事件路由,不能直接用作生產履約處理。返回成功前應可靠儲存事件及待處理任務;業務更新與事件處理完成標記應在同一事務中提交,或採用可恢復的處理狀態。具體見 Webhook 可靠處理。

這篇文件怎麼樣?

最後更新

本頁內容