穩定幣支付如何安全退款:從申請到鏈上確認
穩定幣退款必須作為新的鏈上轉帳單獨處理。本文講解 StableOps 如何校驗最終訂單和剩餘額度,由商戶從原收款地址簽署退款交易,再核對目標地址、資產、金額與最終深度,並透過 Webhook、失敗重試和對帳避免重複退款與帳務遺漏。
銀行卡支付通常可以透過支付服務商發起退款。穩定幣轉帳則不同。鏈上轉帳一旦確認,不能在原交易上執行撤銷。商戶必須從自己控制的錢包發起一筆新的轉帳,把約定金額髮送到經過核驗的退款地址。
這意味著退款不是付款訂單的反向狀態,也不是把訂單從 finalized 改回未支付。它是一項獨立的資金操作,需要自己的許可權、狀態、交易證據、Webhook 和對帳記錄。
StableOps 的非託管退款流程把這條邊界固定下來:商戶控制原收款地址的簽名錢包,StableOps 記錄退款請求,校驗可退款額度,並根據商戶提交的交易雜湊核對鏈上結果。平台不持有商戶私鑰,也不會替商戶傳送退款交易。
退款是一筆新的鏈上轉帳
一筆穩定幣退款至少包含兩條記錄:
原始支付
付款人錢包 -> 商戶收款地址
payment.finalized
退款
原始收款地址 -> 客戶提供的退款地址
refund.confirmed原始訂單說明客戶應付什麼。退款記錄說明商戶決定退回什麼,以及這筆新的鏈上交易是否真的完成。兩者需要相互關聯,但不能互相覆蓋。
| 記錄 | 作用 | 不應承擔的職責 |
|---|---|---|
| 付款訂單 | 儲存原始金額、鏈、資產、收款地址和支付生命週期 | 不負責儲存退款交易雜湊 |
| 退款記錄 | 儲存退款金額、目標地址、狀態和交易證據 | 不把原訂單改寫成未支付 |
| 商戶帳本 | 記錄銷售、退款、人工入帳和手續費 | 不用錢包餘額差額推斷客戶身份 |
| 鏈上記錄 | 證明實際發生的轉帳與規範狀態 | 不知道客戶訂單和退款政策 |
這也是為什麼穩定幣支付對帳需要分別統計收款、退款、人工入帳和資金調撥。錢包餘額只是一項存量,不能代替事件記錄。
只有最終完成的訂單才能退款
退款前的第一道邊界是訂單狀態。一個處於 detected 或 confirmed 的訂單仍可能因為回執失敗或鏈重組變成 reverted。這時不能把尚未穩定的付款當成可退款資金。
StableOps 只允許 finalized 的付款訂單建立退款請求。這樣可以避免以下錯誤:
- 付款後來回滾,商戶卻已經把同一筆金額退給客戶。
- 退款處理與原始支付確認同時進行,形成重複資金流出。
- 客服根據錢包頁面或客戶提供的交易雜湊,繞過最終性檢查直接退款。
訂單進入 finalized 只表示原始收款達到了平台的最終性邊界。它不會自動發起退款,也不會替商戶判斷退款原因。退款原因、客戶身份、客服審批和資金來源仍屬於商戶自己的業務流程。
這套退款介面不適用於少付、多付、發錯網路或過期後到帳,因為這些轉帳沒有讓原訂單進入 finalized。商戶應根據實際資金落地的鏈與地址,使用自己的資金工具處理,並把結果記入異常帳本。具體流程見少付、多付或發錯網路。
有關確認數與最終性的區別,請閱讀穩定幣支付確認數如何設計。
退款狀態應該表示可觀察的事實
退款狀態不能只記錄“客服點過按鈕”。每個狀態都應對應一項可以被系統或鏈上證據證明的事實。
requested -> submitted -> confirmed
| |
canceled failed -> submitted| 狀態 | 含義 | 允許的下一步 |
|---|---|---|
requested | 退款金額與目標地址已經登記,尚未提交鏈上交易 | 簽名並提交交易,或取消請求 |
submitted | 商戶已提交交易雜湊,系統正在核對回執和轉帳內容 | 等待確認,或在失敗後重新提交 |
confirmed | 交易成功,且鏈、資產、來源、目標和金額全部匹配 | 進入完成與對帳流程 |
failed | 交易回執失敗,或交易內容不符合退款指令 | 修正問題後重新提交 |
canceled | 尚未提交的退款請求已取消 | 建立新的退款請求 |
submitted 不等於客戶已經收到錢。交易雜湊可能不存在,回執可能失敗,交易也可能包含錯誤的代幣轉帳。只有 confirmed 才能作為退款完成事件。
退款事件包括 refund.requested、refund.submitted、refund.confirmed 和 refund.failed。你的系統應對這些事件驗籤、去重,並把真正的客戶權益或帳務變化做成冪等操作。處理 Webhook 的通用模式見穩定幣支付 Webhook:如何防止重複履約。
退款額度必須在併發下保持正確
一筆訂單可能發生多次退款。例如,商戶先退回一部分,再退回剩餘部分。系統不能只檢查每一筆退款是否小於原訂單金額,還要確保所有未完成退款的合計不超過訂單金額。
可退款餘額可以這樣表示:
可退款餘額 = 原始訂單金額 -
requested、submitted、confirmed 退款金額之和failed 不佔用退款額度。失敗退款重新提交時,StableOps 會再次鎖定原付款訂單並核算剩餘額度。如果其他退款已經佔用了額度,本次重新提交會被拒絕,避免舊退款和新退款合計超過原始收款。
兩個客服視窗如果同時為同一訂單發起退款,普通的“讀取餘額,再寫入退款”會產生競態:兩次請求都讀到相同的餘額,最後合計超過原始收款。
安全的實現需要在同一資料庫事務中完成以下操作:
- 鎖定付款訂單。
- 確認訂單屬於目前組織和環境,且狀態為
finalized。 - 找到訂單對應的已匹配支付事件。
- 按該事件的鏈和資產換算金額。
- 統計仍佔用額度的退款。
- 確認本次退款不超過剩餘金額。
- 寫入退款記錄和
refund.requested事件。
其中金額應使用代幣的最小單位整數進行比較。不要用 JavaScript 浮點數計算穩定幣金額,也不要因為顯示格式相同就忽略不同鏈上的代幣精度。
退款地址不能盲目使用付款來源地址
最危險的快捷做法是把原始交易的 from 地址直接當成退款地址。它在很多場景下並不代表真實客戶:
- 交易所可能從熱錢包統一提幣。
- 智慧合約錢包可能透過中繼地址發起交易。
- 代理服務可能代替客戶廣播交易。
- 客戶可能要求退到另一枚明確歸屬自己的地址。
退款時應讓客戶提供目標地址,並完成以下核驗:
- 地址屬於原始支付使用的同一條鏈。
- 地址格式與地址型別符合該鏈的規則。
- 目標地址不是商戶誤操作時容易混淆的收款地址。
- 客戶、組織和資金風險檢查滿足商戶政策。
- 退款金額、資產和手續費承擔方式已經得到審批。
StableOps 建立退款請求時會按原始付款鏈校驗目標地址格式。對於 EVM 鏈,地址隨後按既定規則歸一化。對於 TRON 和 Solana 等採用不同地址格式的網路,系統會保留原始大小寫。格式校驗不能證明地址屬於客戶,地址歸屬和風險審批仍由商戶負責。
退款請求與簽名交易要分開
非託管退款最重要的產品邊界,是把“申請退款”和“簽署交易”拆成兩個動作。
第一步由後端建立退款請求。請求繫結已最終完成的付款訂單、退款金額、原支付事件、鏈、資產、原收款地址和目標地址。建立請求不會移動資金。以下程式碼只能在商戶伺服器端執行,不能把 API 金鑰放進瀏覽器或移動端應用。
async function parseStableOpsResponse(response: Response) {
if (!response.ok) {
const detail = await response.text()
throw new Error(`StableOps API 請求失敗(${response.status}):${detail}`)
}
return response.json()
}
const response = await fetch(`${API_URL}/v1/refunds`, {
method: 'POST',
headers: {
Authorization: `Bearer ${API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
payment_order_id: 'po_123',
amount: '10.00',
destination_address: '0xCustomerRefundAddress',
}),
})
const refund = await parseStableOpsResponse(response)
// refund.status === 'requested'第二步由商戶使用原收款地址簽署並廣播交易。交易成功提交後,商戶把交易雜湊登記到退款記錄:
const submittedResponse = await fetch(`${API_URL}/v1/refunds/${refund.id}/submit`, {
method: 'POST',
headers: {
Authorization: `Bearer ${API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ tx_hash: txHash }),
})
const submitted = await parseStableOpsResponse(submittedResponse)
// submitted.status === 'submitted'這兩個介面的職責不同:
| 動作 | 誰控制 | 是否移動資金 |
|---|---|---|
| 建立退款請求 | 商戶業務後端 | 否 |
| 簽署並廣播交易 | 控制原收款地址的商戶錢包或資金服務 | 是,提交到區塊鏈 |
| 登記交易雜湊 | 商戶後端 | 否,提供待核對的證據 |
| 確認退款 | StableOps 鏈上對帳 | 否,更新退款事實 |
StableOps 的介面路徑分別是 POST /v1/refunds、POST /v1/refunds/:id/submit 和 POST /v1/refunds/:id/cancel。欄位與回應結構見建立非託管退款請求和登記商戶簽名的退款交易。生產環境應把簽名許可權放在資金審批邊界之後,不要讓面向客戶的請求直接獲得熱錢包簽名能力。
確認退款時要核對完整轉帳事實
只登記一個交易雜湊不足以證明退款完成。退款對帳至少應驗證:
| 欄位 | 核對內容 |
|---|---|
| 交易雜湊 | 使用退款記錄中的原始付款鏈查詢該交易 |
| 回執 | 交易執行成功,不是失敗回執 |
| 最終深度 | 目前回執所在區塊達到該鏈設定的最終深度 |
| 來源地址 | 代幣轉帳來自原始支付的收款地址 |
| 目標地址 | 代幣轉帳目標等於退款請求中的地址 |
| 資產 | 代幣合約或鑄幣地址等於原始支付資產 |
| 金額 | 最小單位金額等於退款請求金額 |
對於一個包含多條代幣轉帳的交易,不能因為交易整體成功就把它認定為退款成功。系統必須找到與退款請求完全匹配的那條轉帳記錄。
如果回執暫時不存在或狀態未知,退款會保持 submitted 並等待下一次核對。回執失敗,或者達到最終深度後仍找不到鏈、資產、來源、目標和金額完全匹配的轉帳時,退款才會進入 failed。不要把錯誤交易標記成已確認,也不要在查明實際資金去向前重複發起交易。
Webhook 處理仍然需要兩層冪等
退款事件可能因為網路超時、接收方故障或人工重放而重複投遞。接收端應保留原始請求體,先完成簽名驗證,再按 X-Event-Id 去重。
同時,事件去重不能替代業務冪等。比如,兩個不同的 refund.confirmed 事件不應讓同一張內部退款單產生兩次帳務沖銷。建議同時建立兩道保護:
- 事件收件箱按事件 ID 唯一儲存。
- 內部退款記錄按退款 ID 或業務退款編號冪等更新。
只有在資料庫事務中完成事件記錄和帳務任務寫入後,接收端才應返回成功。外部退款通知、訂單狀態更新和客戶郵件可以放入可重試的發件箱,不要把這些副作用直接放在 HTTP 請求的最後一步。
下面是業務層的簡化判斷:
function applyRefundConfirmed(refundId: string, eventId: string) {
// 事務內完成:
// 1. eventId 不存在時寫入事件收件箱
// 2. refundId 尚未完成時記入退款帳務
// 3. 生成一次性的通知或對帳任務
// 4. 重複事件不再次沖銷
}不要使用瀏覽器回跳、客戶提供的交易雜湊或未驗籤事件直接完成退款帳務。它們可以幫助客服排查,但不能替代伺服器端的鏈上核對。
常見異常應該如何處理
退款流程需要把失敗原因和原始收款記錄放在一起,方便客服、工程和財務共同調查。
| 異常 | 正確處理 |
|---|---|
原訂單不是 finalized | 等待最終性,或根據業務政策取消退款申請 |
| 退款金額超過剩餘額度 | 拒絕請求,檢查已有的 requested、submitted 和 confirmed 退款 |
| 地址屬於錯誤網路 | 取消未提交請求,重新收集同一網路上的目標地址 |
| 交易回執失敗 | 標記 failed,保留失敗原因,修正後重新提交 |
| 交易雜湊沒有預期轉帳 | 標記 failed,不要把交易當作成功退款 |
| 暫時查不到回執 | 保持 submitted,等待後續輪詢,不要立即重複廣播交易 |
| 客戶要求退款到新地址 | 重新執行地址驗證和審批,不直接覆蓋原退款記錄 |
| 退款已確認但客戶未看到餘額 | 提供交易雜湊和區塊瀏覽器連結,先確認客戶檢視的是正確網路和資產 |
付款回滾不是退款失敗。payment.reverted 表示原始收款在規範鏈上不再成立,不能假設商戶仍持有一筆需要退回的資金。有關這類邊界,請閱讀少付、多付或發錯網路和付款回滾常見問題。
把退款納入日常對帳
每筆退款至少應能追溯到以下物件:
商戶業務訂單
-> StableOps 付款訂單
-> 原始 payment.finalized 事件
-> 退款記錄
-> 退款交易雜湊
-> refund.confirmed 事件日常對帳可以檢查:
- 已最終完成的收款是否與商戶帳本中的收入一致。
- 每筆退款是否關聯一個合法的原始支付事件。
confirmed退款是否都有成功的鏈上轉帳。requested或submitted退款是否長時間沒有進展。- 退款金額合計是否超過訂單金額。
- 退款手續費是否按照商戶政策記帳。
- 退款交易是否發生在正確的鏈和資產上。
月末關帳時,不要只匯出已確認退款。還應保留失敗、取消和仍在處理中的退款,記錄處理人、審批時間、失敗原因以及相關交易引用。穩定幣支付對帳介紹瞭如何把訂單、支付事件和鏈上轉帳連線起來。
上線前核對清單
- 只有
finalized付款訂單可以建立退款請求。 - 退款額度按訂單序列核算,未完成退款會佔用額度。
- 金額按對應資產的最小單位比較,不使用浮點數。
- 退款目標地址由客戶明確提供,並完成鏈、格式和風險核驗。
- 建立退款請求與原收款地址的鏈上簽名使用不同的許可權邊界。
- 提交交易雜湊後,伺服器端核對回執、最終深度、來源、目標、資產和金額。
- 只有完整轉帳事實匹配時,退款才進入
confirmed。 -
refund.*Webhook 已完成原始請求體驗籤、事件去重和業務冪等。 - 失敗和取消不會覆蓋原始支付記錄。
- 每筆退款都能關聯商戶訂單、付款訂單、原始支付事件和退款交易雜湊。
- 沙盒環境至少測試部分退款、併發退款、錯誤交易和重複 Webhook。
穩定幣退款的核心不是增加一個“退款”按鈕,而是建立一條可審計的資金流。原始收款保持不可改寫,退款作為獨立交易經過審批、簽名、核對和通知。這樣即使鏈上交易失敗、Webhook 重試或客服重複操作,商戶仍能清楚回答三件事:原來收到了什麼,決定退回什麼,鏈上最終發生了什麼。
相關文章
穩定幣支付對帳需要連線業務訂單、平台支付事件與鏈上轉帳。本文講解如何設計穩定外部索引鍵和可追溯狀態,發現遺漏或重複的 Webhook,核對金額、資產、網路與交易雜湊,並用安全指令碼執行每日異常檢查和月度財務對帳。
穩定幣少付、多付、發錯網路、發錯資產或過期後到帳,都不應讓訂單被悄悄完成。本文說明如何從鏈上事件識別各類支付不匹配,為人工處理保留證據,設計退款或補款流程,並透過精確付款指令和狀態約束降低再次發生的機率。
穩定幣轉帳並不等於支付完成。本文從付款訂單狀態機、鏈與資產精確匹配、確認和重組處理、介面冪等、Webhook 事件去重及可恢復履約出發,說明如何把鏈上轉帳轉化為可安全進入生產環境、能夠審計並支援異常恢復的支付事件。
本文講解交易平台如何用唯一地址、鏈上最終確定、Webhook 冪等處理與每日對帳建置加密貨幣充值監控,在支援客戶選擇多條網路的同時安全增加內部餘額,妥善處理過期、遲到與異常轉帳,並降低重複記帳、錯誤歸屬和鏈重組造成的真實資金損失。