diff --git a/frontend/components/account/AccountCenter.tsx b/frontend/components/account/AccountCenter.tsx index a123b58f..eee36a1e 100644 --- a/frontend/components/account/AccountCenter.tsx +++ b/frontend/components/account/AccountCenter.tsx @@ -151,6 +151,10 @@ type ProviderSelection = { mode: ProviderMode; }; +type ConnectBindOptions = { + openOverlayAfterBind?: boolean; +}; + const WALLETCONNECT_PROJECT_ID = String( process.env.NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID || "", ).trim(); @@ -988,12 +992,15 @@ export function AccountCenter() { } }; - const connectAndBindWallet = async (mode: ProviderMode = "auto") => { + const connectAndBindWallet = async ( + mode: ProviderMode = "auto", + options: ConnectBindOptions = {}, + ): Promise => { setPaymentError(""); setPaymentInfo(""); if (!isAuthenticated) { setPaymentError("请先登录后再绑定钱包。"); - return; + return false; } setPaymentBusy(true); @@ -1009,7 +1016,7 @@ export function AccountCenter() { } catch (tokenErr) { setPaymentError(normalizePaymentError(tokenErr).message); setPaymentBusy(false); - return; + return false; } const authHeaders: Record = { "Content-Type": "application/json", @@ -1028,9 +1035,12 @@ export function AccountCenter() { if (existingWallet) { setWalletAddress(address); setSelectedWallet(address); - setPaymentInfo(`${walletLabel} 已绑定: ${shortAddress(address)}`); + setPaymentInfo( + `${walletLabel} 已绑定: ${shortAddress(address)}。现在可点击“立即订阅并激活服务”。`, + ); + if (options.openOverlayAfterBind) setShowOverlay(true); setPaymentBusy(false); - return; + return true; } setWalletAddress(address); @@ -1063,12 +1073,17 @@ export function AccountCenter() { throw new Error(`verify failed: ${raw}`); } - setPaymentInfo(`${walletLabel} 绑定成功: ${shortAddress(address)}`); + setPaymentInfo( + `${walletLabel} 绑定成功: ${shortAddress(address)}。现在可点击“立即订阅并激活服务”。`, + ); setProviderMode(providerSelection.mode); + if (options.openOverlayAfterBind) setShowOverlay(true); await loadPaymentSnapshot(); + return true; } catch (error) { setPaymentInfo(""); setPaymentError(normalizePaymentError(error).message); + return false; } finally { setPaymentBusy(false); } @@ -1272,8 +1287,19 @@ export function AccountCenter() { ); if (!confirmRes.ok) { const raw = (await confirmRes.text()).slice(0, 350); - setPaymentInfo(`交易已提交: ${shortAddress(txHashNorm)},等待确认中。`); - throw new Error(`confirm pending: ${raw}`); + const lowerRaw = raw.toLowerCase(); + const maybePending = + (confirmRes.status === 404 && + !lowerRaw.includes("payment intent not found")) || + confirmRes.status === 408 || + (confirmRes.status === 409 && + (lowerRaw.includes("confirmations not enough") || + lowerRaw.includes("tx indexed partially"))); + if (maybePending) { + setPaymentInfo(`交易已提交: ${shortAddress(txHashNorm)},等待确认中。`); + throw new Error(`confirm pending: ${raw}`); + } + throw new Error(`confirm failed: ${raw}`); } setPaymentInfo(`支付确认成功,交易: ${shortAddress(txHashNorm)}`); @@ -1309,7 +1335,13 @@ export function AccountCenter() { return; } if (!hasPayingWallet) { - await connectAndBindWallet(providerMode); + setPaymentInfo("请先完成钱包绑定,正在拉起绑定流程..."); + const bound = await connectAndBindWallet(providerMode, { + openOverlayAfterBind: true, + }); + if (!bound) return; + setPaymentInfo("钱包已绑定,正在创建订单并发起支付..."); + await createIntentAndPay(); return; } await createIntentAndPay(); diff --git a/src/payments/contract_checkout.py b/src/payments/contract_checkout.py index 9037d5cd..b9be1142 100644 --- a/src/payments/contract_checkout.py +++ b/src/payments/contract_checkout.py @@ -1559,12 +1559,27 @@ class PaymentContractCheckoutService: raise PaymentCheckoutError(503, "cannot connect payment rpc") if int(w3.eth.chain_id) != int(self.chain_id): raise PaymentCheckoutError(503, "payment rpc chain mismatch") + # Wait for receipt first to avoid transient RPC lag on eth_getTransaction. + receipt = self._wait_receipt(tx_hash_text) + if int(receipt.get("status") or 0) != 1: + raise PaymentCheckoutError(400, "tx reverted") + try: tx = w3.eth.get_transaction(tx_hash_text) except Exception: - raise PaymentCheckoutError(404, "tx not found on chain") - tx_to = _normalize_address(tx.get("to")) - tx_from = _normalize_address(tx.get("from")) + tx = None + + tx_get = getattr(tx, "get", None) + tx_to_raw = tx_get("to") if callable(tx_get) else None + tx_from_raw = tx_get("from") if callable(tx_get) else None + tx_to = _normalize_address( + tx_to_raw or receipt.get("to") + ) + tx_from = _normalize_address( + tx_from_raw or receipt.get("from") + ) + if not tx_to or not tx_from: + raise PaymentCheckoutError(409, "tx indexed partially; retry confirm") if tx_to != intent.receiver_address: raise PaymentCheckoutError( 400, @@ -1578,9 +1593,6 @@ class PaymentContractCheckoutService: ) else: self._require_user_wallet(user_id, tx_from) - receipt = self._wait_receipt(tx_hash_text) - if int(receipt.get("status") or 0) != 1: - raise PaymentCheckoutError(400, "tx reverted") block_number = int(receipt.get("blockNumber") or 0) latest_block = int(w3.eth.block_number) confirmations = max(0, latest_block - block_number + 1) if block_number else 0 @@ -1631,7 +1643,7 @@ class PaymentContractCheckoutService: "block_number": block_number, "status": "confirmed", "raw_receipt": json.loads(Web3.to_json(receipt)), - "raw_tx": json.loads(Web3.to_json(tx)), + "raw_tx": json.loads(Web3.to_json(tx)) if tx is not None else None, "updated_at": now_iso, }, prefer="resolution=merge-duplicates,return=representation",