diff --git a/templates/portal_invoice_detail.html b/templates/portal_invoice_detail.html index 2191a1d..3d885a8 100644 --- a/templates/portal_invoice_detail.html +++ b/templates/portal_invoice_detail.html @@ -568,22 +568,48 @@ Reference: ${invoiceRef}`; if (statusEl) statusEl.textContent = "Submitting transaction hash to portal…"; - const resp = await fetch(`/portal/invoice/${invoiceId}/submit-crypto-tx`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - payment_id: paymentId, - asset: asset, - tx_hash: txHash - }) - }); - - const data = await resp.json(); - if (!resp.ok || !data.ok) { - throw new Error((data && (data.detail || data.error)) || "Portal rejected tx hash"); + async function submitTxHashWithRetry() { + const maxAttempts = 12; // about 60 seconds total + const waitMs = 5000; + + for (let attempt = 1; attempt <= maxAttempts; attempt++) { + if (statusEl) { + statusEl.textContent = `Checking RPC for transaction… attempt ${attempt}/${maxAttempts}`; + } + + const resp = await fetch(`/portal/invoice/${invoiceId}/submit-crypto-tx`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + payment_id: paymentId, + asset: asset, + tx_hash: txHash + }) + }); + + const data = await resp.json(); + + if (resp.ok && data.ok) { + window.location.href = data.redirect_url; + return; + } + + const detail = String((data && (data.detail || data.error)) || ""); + const retryable = detail.toLowerCase().includes("not found on rpc"); + + if (!retryable || attempt === maxAttempts) { + throw new Error(detail || "Portal rejected tx hash"); + } + + if (statusEl) { + statusEl.textContent = `Transaction sent. Waiting for RPC to see it… retrying in ${Math.floor(waitMs / 1000)}s`; + } + + await new Promise(resolve => setTimeout(resolve, waitMs)); + } } - window.location.href = data.redirect_url; + await submitTxHashWithRetry(); } catch (err) { if (statusEl) statusEl.textContent = String(err.message || err); walletButton.disabled = false;