|
|
|
@ -517,108 +517,155 @@ Reference: ${invoiceRef}`; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
const walletButton = document.getElementById("walletPayButton"); |
|
|
|
const walletButton = document.getElementById("walletPayButton"); |
|
|
|
if (walletButton) { |
|
|
|
|
|
|
|
walletButton.addEventListener("click", async function() { |
|
|
|
|
|
|
|
const statusEl = document.getElementById("walletStatusText"); |
|
|
|
|
|
|
|
const originalText = walletButton.textContent; |
|
|
|
|
|
|
|
walletButton.disabled = true; |
|
|
|
|
|
|
|
if (statusEl) statusEl.textContent = "Opening wallet…"; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
try { |
|
|
|
function pendingTxStorageKey(invoiceId, paymentId) { |
|
|
|
if (!window.ethereum) { |
|
|
|
return `otb_pending_tx_${invoiceId}_${paymentId}`; |
|
|
|
throw new Error("No wallet detected in this browser. Use 'Open in MetaMask Mobile' or copy the payment details."); |
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
const invoiceId = walletButton.dataset.invoiceId; |
|
|
|
async function submitTxHash(invoiceId, paymentId, asset, txHash) { |
|
|
|
const paymentId = walletButton.dataset.paymentId; |
|
|
|
const res = await fetch(`/portal/invoice/${invoiceId}/submit-crypto-tx`, { |
|
|
|
const asset = walletButton.dataset.asset; |
|
|
|
method: "POST", |
|
|
|
const chainId = walletButton.dataset.chainId; |
|
|
|
headers: { |
|
|
|
const assetType = walletButton.dataset.assetType; |
|
|
|
"Content-Type": "application/json", |
|
|
|
const to = walletButton.dataset.to; |
|
|
|
"Accept": "application/json" |
|
|
|
const amount = walletButton.dataset.amount; |
|
|
|
}, |
|
|
|
const decimals = Number(walletButton.dataset.decimals || "18"); |
|
|
|
body: JSON.stringify({ |
|
|
|
const tokenContract = walletButton.dataset.tokenContract || ""; |
|
|
|
payment_id: paymentId, |
|
|
|
|
|
|
|
asset: asset, |
|
|
|
|
|
|
|
tx_hash: txHash |
|
|
|
|
|
|
|
}) |
|
|
|
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
const accounts = await window.ethereum.request({ method: "eth_requestAccounts" }); |
|
|
|
let data = {}; |
|
|
|
const from = Array.isArray(accounts) && accounts.length ? accounts[0] : ""; |
|
|
|
try { |
|
|
|
if (!from) { |
|
|
|
data = await res.json(); |
|
|
|
throw new Error("Wallet did not return an account address."); |
|
|
|
} catch (err) { |
|
|
|
|
|
|
|
data = { ok: false, error: "invalid_json_response" }; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
await switchChain(chainId); |
|
|
|
if (!res.ok || !data.ok) { |
|
|
|
|
|
|
|
throw new Error(data.error || `submit_failed_http_${res.status}`); |
|
|
|
let txHash; |
|
|
|
|
|
|
|
if (assetType === "native") { |
|
|
|
|
|
|
|
txHash = await window.ethereum.request({ |
|
|
|
|
|
|
|
method: "eth_sendTransaction", |
|
|
|
|
|
|
|
params: [{ |
|
|
|
|
|
|
|
from: from, |
|
|
|
|
|
|
|
to: to, |
|
|
|
|
|
|
|
value: toHexBigIntFromDecimal(amount, decimals) |
|
|
|
|
|
|
|
}] |
|
|
|
|
|
|
|
}); |
|
|
|
|
|
|
|
} else { |
|
|
|
|
|
|
|
txHash = await window.ethereum.request({ |
|
|
|
|
|
|
|
method: "eth_sendTransaction", |
|
|
|
|
|
|
|
params: [{ |
|
|
|
|
|
|
|
from: from, |
|
|
|
|
|
|
|
to: tokenContract, |
|
|
|
|
|
|
|
data: erc20TransferData(to, amount, decimals) |
|
|
|
|
|
|
|
}] |
|
|
|
|
|
|
|
}); |
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
if (statusEl) statusEl.textContent = "Submitting transaction hash to portal…"; |
|
|
|
return data; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
async function submitTxHashWithRetry() { |
|
|
|
async function tryRecoverPendingTxFromStorage() { |
|
|
|
const maxAttempts = 12; // about 60 seconds total |
|
|
|
const invoiceId = "{{ invoice.id }}"; |
|
|
|
const waitMs = 5000; |
|
|
|
const paymentId = "{{ pending_crypto_payment.id if pending_crypto_payment else '' }}"; |
|
|
|
|
|
|
|
const asset = "{{ selected_crypto_option.symbol if selected_crypto_option else '' }}"; |
|
|
|
|
|
|
|
|
|
|
|
for (let attempt = 1; attempt <= maxAttempts; attempt++) { |
|
|
|
if (!invoiceId || !paymentId || !asset) return; |
|
|
|
if (statusEl) { |
|
|
|
{% if pending_crypto_payment and pending_crypto_payment.txid %} |
|
|
|
statusEl.textContent = `Checking RPC for transaction… attempt ${attempt}/${maxAttempts}`; |
|
|
|
return; |
|
|
|
} |
|
|
|
{% endif %} |
|
|
|
|
|
|
|
|
|
|
|
const resp = await fetch(`/portal/invoice/${invoiceId}/submit-crypto-tx`, { |
|
|
|
const key = pendingTxStorageKey(invoiceId, paymentId); |
|
|
|
method: "POST", |
|
|
|
const savedTx = localStorage.getItem(key); |
|
|
|
headers: { "Content-Type": "application/json" }, |
|
|
|
if (!savedTx || !savedTx.startsWith("0x")) return; |
|
|
|
body: JSON.stringify({ |
|
|
|
|
|
|
|
payment_id: paymentId, |
|
|
|
|
|
|
|
asset: asset, |
|
|
|
|
|
|
|
tx_hash: txHash |
|
|
|
|
|
|
|
}) |
|
|
|
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const data = await resp.json(); |
|
|
|
const walletStatus = document.getElementById("walletStatusText"); |
|
|
|
|
|
|
|
try { |
|
|
|
|
|
|
|
if (walletStatus) walletStatus.textContent = "Retrying saved transaction submission..."; |
|
|
|
|
|
|
|
await submitTxHash(invoiceId, paymentId, asset, savedTx); |
|
|
|
|
|
|
|
localStorage.removeItem(key); |
|
|
|
|
|
|
|
const url = new URL(window.location.href); |
|
|
|
|
|
|
|
url.searchParams.set("pay", "crypto"); |
|
|
|
|
|
|
|
url.searchParams.set("asset", asset); |
|
|
|
|
|
|
|
url.searchParams.set("payment_id", paymentId); |
|
|
|
|
|
|
|
window.location.href = url.toString(); |
|
|
|
|
|
|
|
} catch (err) { |
|
|
|
|
|
|
|
if (walletStatus) walletStatus.textContent = `Saved tx retry failed: ${err.message}`; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
if (resp.ok && data.ok) { |
|
|
|
if (walletButton) { |
|
|
|
window.location.href = data.redirect_url; |
|
|
|
walletButton.addEventListener("click", async function() { |
|
|
|
|
|
|
|
const walletStatus = document.getElementById("walletStatusText"); |
|
|
|
|
|
|
|
const invoiceId = this.dataset.invoiceId; |
|
|
|
|
|
|
|
const paymentId = this.dataset.paymentId; |
|
|
|
|
|
|
|
const asset = this.dataset.asset; |
|
|
|
|
|
|
|
const chainId = this.dataset.chainId; |
|
|
|
|
|
|
|
const assetType = this.dataset.assetType; |
|
|
|
|
|
|
|
const to = this.dataset.to; |
|
|
|
|
|
|
|
const amount = this.dataset.amount; |
|
|
|
|
|
|
|
const decimals = Number(this.dataset.decimals || "18"); |
|
|
|
|
|
|
|
const tokenContract = this.dataset.tokenContract || ""; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const setStatus = (msg) => { |
|
|
|
|
|
|
|
if (walletStatus) walletStatus.textContent = msg; |
|
|
|
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (!window.ethereum || !window.ethereum.request) { |
|
|
|
|
|
|
|
setStatus("No browser wallet detected. Use MetaMask/Rabby or MetaMask Mobile."); |
|
|
|
return; |
|
|
|
return; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
const detail = String((data && (data.detail || data.error)) || ""); |
|
|
|
try { |
|
|
|
const retryable = detail.toLowerCase().includes("not found on rpc"); |
|
|
|
this.disabled = true; |
|
|
|
|
|
|
|
setStatus("Opening wallet..."); |
|
|
|
|
|
|
|
|
|
|
|
if (!retryable || attempt === maxAttempts) { |
|
|
|
await window.ethereum.request({ method: "eth_requestAccounts" }); |
|
|
|
throw new Error(detail || "Portal rejected tx hash"); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (statusEl) { |
|
|
|
if (chainId && chainId !== "None" && chainId !== "") { |
|
|
|
statusEl.textContent = `Transaction sent. Waiting for RPC to see it… retrying in ${Math.floor(waitMs / 1000)}s`; |
|
|
|
try { |
|
|
|
|
|
|
|
await switchChain(Number(chainId)); |
|
|
|
|
|
|
|
} catch (err) { |
|
|
|
|
|
|
|
setStatus(`Chain switch failed: ${err.message || err}`); |
|
|
|
|
|
|
|
this.disabled = false; |
|
|
|
|
|
|
|
return; |
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
await new Promise(resolve => setTimeout(resolve, waitMs)); |
|
|
|
let txParams; |
|
|
|
|
|
|
|
if (assetType === "token" && tokenContract) { |
|
|
|
|
|
|
|
txParams = { |
|
|
|
|
|
|
|
from: (await window.ethereum.request({ method: "eth_accounts" }))[0], |
|
|
|
|
|
|
|
to: tokenContract, |
|
|
|
|
|
|
|
data: erc20TransferData(to, amount, decimals), |
|
|
|
|
|
|
|
value: "0x0" |
|
|
|
|
|
|
|
}; |
|
|
|
|
|
|
|
} else { |
|
|
|
|
|
|
|
txParams = { |
|
|
|
|
|
|
|
from: (await window.ethereum.request({ method: "eth_accounts" }))[0], |
|
|
|
|
|
|
|
to: to, |
|
|
|
|
|
|
|
value: toHexBigIntFromDecimal(amount, decimals) |
|
|
|
|
|
|
|
}; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
setStatus("Waiting for wallet confirmation..."); |
|
|
|
|
|
|
|
const txHash = await window.ethereum.request({ |
|
|
|
|
|
|
|
method: "eth_sendTransaction", |
|
|
|
|
|
|
|
params: [txParams] |
|
|
|
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (!txHash || !String(txHash).startsWith("0x")) { |
|
|
|
|
|
|
|
throw new Error("wallet did not return a tx hash"); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
await submitTxHashWithRetry(); |
|
|
|
const storageKey = pendingTxStorageKey(invoiceId, paymentId); |
|
|
|
|
|
|
|
localStorage.setItem(storageKey, String(txHash)); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
setStatus(`Wallet submitted tx: ${txHash}. Sending to billing server...`); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
await submitTxHash(invoiceId, paymentId, asset, txHash); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
localStorage.removeItem(storageKey); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
setStatus("Transaction submitted. Reloading into processing view..."); |
|
|
|
|
|
|
|
const url = new URL(window.location.href); |
|
|
|
|
|
|
|
url.searchParams.set("pay", "crypto"); |
|
|
|
|
|
|
|
url.searchParams.set("asset", asset); |
|
|
|
|
|
|
|
url.searchParams.set("payment_id", paymentId); |
|
|
|
|
|
|
|
window.location.href = url.toString(); |
|
|
|
} catch (err) { |
|
|
|
} catch (err) { |
|
|
|
if (statusEl) statusEl.textContent = String(err.message || err); |
|
|
|
setStatus(`Wallet submit failed: ${err.message || err}`); |
|
|
|
walletButton.disabled = false; |
|
|
|
this.disabled = false; |
|
|
|
walletButton.textContent = originalText; |
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
}); |
|
|
|
}); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
tryRecoverPendingTxFromStorage(); |
|
|
|
})(); |
|
|
|
})(); |
|
|
|
</script> |
|
|
|
</script> |
|
|
|
|
|
|
|
|
|
|
|
|