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;