feat: instant payment detection via Freedom.st SSE on backend and frontend

This commit is contained in:
2026-03-18 23:06:57 +04:00
parent c9130243e8
commit 7d1a350f86
2 changed files with 264 additions and 37 deletions
+66 -2
View File
@@ -127,6 +127,7 @@
<script>
const PAY_API = '<%= it.payApi || "" %>';
const SOCK_API = 'https://sock-v1.freedom.st';
let selectedPlan = null;
let selectedCoin = null;
@@ -135,6 +136,7 @@
let paymentId = null;
let pollInterval = null;
let countdownInterval = null;
let sseAbort = null;
// Fetch available coins on load
(async () => {
@@ -234,8 +236,65 @@
updateCountdown(expiresAt);
countdownInterval = setInterval(() => updateCountdown(expiresAt), 1000);
// Start polling
pollInterval = setInterval(() => pollStatus(), 5000);
// Start SSE stream for instant tx detection on the client
watchAddress(data.coin, data.address);
// Poll as fallback (for confirmations and in case SSE drops)
pollInterval = setInterval(() => pollStatus(), 10000);
}
/** Subscribe to Freedom.st SSE filtered by our payment address for instant detection. */
function watchAddress(coin, address) {
if (sseAbort) sseAbort.abort();
sseAbort = new AbortController();
const query = { chain: coin, "data.out.script.address": address };
const q = btoa(JSON.stringify(query));
const url = `${SOCK_API}/sse?q=${q}`;
(async function connect() {
while (!sseAbort.signal.aborted) {
try {
const res = await fetch(url, { signal: sseAbort.signal });
if (!res.ok || !res.body) { await new Promise(r => setTimeout(r, 3000)); continue; }
const reader = res.body.getReader();
const decoder = new TextDecoder();
let buffer = '';
while (!sseAbort.signal.aborted) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split('\n');
buffer = lines.pop() ?? '';
for (const line of lines) {
if (!line.startsWith('data: ')) continue;
try {
const event = JSON.parse(line.slice(6));
onTxDetected(event);
} catch {}
}
}
} catch (e) {
if (sseAbort.signal.aborted) return;
}
if (!sseAbort.signal.aborted) await new Promise(r => setTimeout(r, 3000));
}
})();
}
function onTxDetected(event) {
// Transaction matched our address filter — payment detected instantly
console.log('TX detected via SSE:', event);
const statusEl = document.getElementById('pay-status');
statusEl.innerHTML = `
<span class="w-2 h-2 rounded-full bg-blue-500 animate-pulse"></span>
<span class="text-blue-400">Transaction detected, waiting for confirmation...</span>
`;
// Immediately poll the pay API to get authoritative status update
pollStatus();
}
function updateCountdown(expiresAt) {
@@ -261,12 +320,14 @@
} else if (data.status === 'paid') {
clearInterval(pollInterval);
clearInterval(countdownInterval);
if (sseAbort) { sseAbort.abort(); sseAbort = null; }
document.getElementById('pay-status-section').classList.add('hidden');
document.getElementById('pay-success').classList.remove('hidden');
setTimeout(() => { window.location.href = '/dashboard/settings'; }, 3000);
} else if (data.status === 'expired') {
clearInterval(pollInterval);
clearInterval(countdownInterval);
if (sseAbort) { sseAbort.abort(); sseAbort = null; }
document.getElementById('pay-status-section').classList.add('hidden');
document.getElementById('pay-expired').classList.remove('hidden');
}
@@ -282,6 +343,9 @@
}
function resetCheckout() {
if (sseAbort) { sseAbort.abort(); sseAbort = null; }
if (pollInterval) { clearInterval(pollInterval); pollInterval = null; }
if (countdownInterval) { clearInterval(countdownInterval); countdownInterval = null; }
document.getElementById('step-select').classList.remove('hidden');
document.getElementById('step-pay').classList.add('hidden');
document.getElementById('pay-success').classList.add('hidden');