const DEFAULT_GIVETH_PROJECTS = [ { slug: 'Save-the-Children-Philippines', name: 'Save the Children Philippines', url: 'https://giveth.io/project/Save-the-Children-Philippines', network: 'Polygon', currency: 'POL' }, { slug: 'Save-the-Children-International', name: 'Save the Children International', url: 'https://giveth.io/project/Save-the-Children-International', network: 'Polygon', currency: 'POL' }, { slug: 'scholarship-programme-for-underprivileged-children', name: 'Scholarship Programme For Underprivileged Children', url: 'https://giveth.io/project/scholarship-programme-for-underprivileged-children', network: 'Polygon', currency: 'POL', recipientAddress: '0x37aB975aB79eAB2d6849e28339E33963143aae6E' } ]; const DEFAULT_GIVETH_SUBGRAPH_URL = 'https://api.thegraph.com/subgraphs/name/giveth/giveth-polygon'; const DEFAULT_POLYGON_BLOCKSCOUT_URL = 'https://polygon.blockscout.com/api/v2'; const DEFAULT_POLYGON_RPC_URL = 'https://1rpc.io/matic'; const SOULWALL_TX_MAX_AGE_MS = 24 * 60 * 60 * 1000; function normalizeSlug(value) { return String(value || '').trim(); } function normalizeTxHash(value) { const txHash = String(value || '').trim(); return /^0x[a-fA-F0-9]{64}$/.test(txHash) ? txHash.toLowerCase() : ''; } function normalizeAddress(value) { const address = String(value || '').trim(); return /^0x[a-fA-F0-9]{40}$/.test(address) ? address.toLowerCase() : ''; } function parseProjectsJson(value) { if (!value) return []; try { const parsed = JSON.parse(value); return Array.isArray(parsed) ? parsed : []; } catch (error) { return []; } } function normalizeProject(project) { const slug = normalizeSlug(project.slug); const url = project.url || `https://giveth.io/project/${encodeURIComponent(slug)}`; return { provider: 'giveth', id: slug, slug, name: project.name || slug.replace(/-/g, ' '), description: project.description || 'Official Giveth project. Open the project page, donate on Polygon, then paste the transaction hash for verification.', url, donateUrl: project.donateUrl || `${url}#donate`, logoUrl: project.logoUrl || project.imageUrl || '', network: project.network || 'Polygon', currency: project.currency || 'POL', recipientAddress: normalizeAddress(project.recipientAddress || project.recipient || project.walletAddress || '') }; } function getGivethProjects(env) { const envProjects = parseProjectsJson(env.GIVETH_PROJECTS_JSON); const projects = envProjects.length ? envProjects : DEFAULT_GIVETH_PROJECTS; return projects.map(normalizeProject).filter((project) => project.slug); } function getGivethProject(env, slug) { const normalized = normalizeSlug(slug); return getGivethProjects(env).find((project) => project.slug === normalized) || null; } function getMetaContent(html, property) { const escaped = property.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); const pattern = new RegExp(`'); } async function withGivethPageMeta(project) { try { const response = await fetch(project.url, { headers: { accept: 'text/html' } }); if (!response.ok) return project; const html = await response.text(); const title = decodeHtmlEntities(getMetaContent(html, 'og:title')); const description = decodeHtmlEntities(getMetaContent(html, 'og:description')); const image = decodeHtmlEntities(getMetaContent(html, 'og:image')); return { ...project, name: title || project.name, description: description || project.description, logoUrl: image || project.logoUrl }; } catch (error) { return project; } } function getGivethSubgraphUrl(env) { return env.GIVETH_SUBGRAPH_URL || DEFAULT_GIVETH_SUBGRAPH_URL; } function getPolygonBlockscoutUrl(env) { return String(env.POLYGON_BLOCKSCOUT_URL || DEFAULT_POLYGON_BLOCKSCOUT_URL).replace(/\/$/, ''); } function getPolygonRpcUrl(env) { return String(env.POLYGON_RPC_URL || DEFAULT_POLYGON_RPC_URL).replace(/\/$/, ''); } function coerceTimestampMs(value) { if (value == null || value === '') return NaN; if (typeof value === 'number') { if (!Number.isFinite(value) || value <= 0) return NaN; return value > 1e12 ? value : value * 1000; } const text = String(value).trim(); if (!text) return NaN; if (/^\d+$/.test(text)) { const numeric = Number(text); return numeric > 1e12 ? numeric : numeric * 1000; } const parsed = Date.parse(text); return Number.isFinite(parsed) ? parsed : NaN; } function getPolygonTransactionTimestampMs(payload = {}) { const candidates = [ payload.timestamp, payload?.timestamp?.value, payload?.timestamp?.datetime, payload.block_timestamp, payload.created_at, payload.inserted_at, payload?.block?.timestamp, payload?.confirmation_duration?.from ]; for (const candidate of candidates) { const parsed = coerceTimestampMs(candidate); if (Number.isFinite(parsed)) return parsed; } return NaN; } async function fetchPolygonTransactionSummary(env, txHash) { try { const response = await fetch(`${getPolygonBlockscoutUrl(env)}/transactions/${txHash}`, { headers: { accept: 'application/json' } }); const payload = await response.json().catch(() => ({})); if (!response.ok) { return { ok: false, error: 'polygon_receipt_unavailable', message: `Polygon receipt lookup failed: ${response.status}` }; } return { ok: true, payload, status: String(payload.status || payload.result || '').toLowerCase(), toAddress: normalizeAddress(payload.to?.hash || payload.to || ''), valuePol: weiToPol(payload.value), timestampMs: getPolygonTransactionTimestampMs(payload) }; } catch (error) { return { ok: false, error: 'polygon_receipt_lookup_failed', message: error.message || 'Polygon receipt lookup failed.' }; } } async function polygonRpc(env, method, params = []) { const response = await fetch(getPolygonRpcUrl(env), { method: 'POST', headers: { 'content-type': 'application/json', accept: 'application/json' }, body: JSON.stringify({ jsonrpc: '2.0', id: 1, method, params }) }); const payload = await response.json().catch(() => ({})); if (!response.ok || payload.error) { throw new Error(payload?.error?.message || `Polygon RPC ${method} failed: ${response.status}`); } return payload.result; } function hexToNumber(value) { const text = String(value || '').trim(); if (!/^0x[0-9a-f]+$/i.test(text)) return NaN; return Number.parseInt(text, 16); } function weiHexToPol(value) { try { const wei = BigInt(String(value || '0x0')); const whole = wei / 1000000000000000000n; const fraction = wei % 1000000000000000000n; return Number(`${whole}.${fraction.toString().padStart(18, '0').slice(0, 8)}`); } catch (error) { return 0; } } async function fetchPolygonTransactionSummaryViaRpc(env, txHash) { try { const tx = await polygonRpc(env, 'eth_getTransactionByHash', [txHash]); if (!tx?.hash) { return { ok: false, error: 'polygon_receipt_unavailable', message: 'SoulWall could not find that transaction hash on Polygon. Double-check that you pasted the final Polygon tx hash.' }; } const receipt = await polygonRpc(env, 'eth_getTransactionReceipt', [txHash]); if (!receipt?.transactionHash) { return { ok: false, error: 'polygon_receipt_pending', message: 'SoulWall found the transaction on Polygon, but the receipt is not available yet.' }; } const block = tx.blockNumber ? await polygonRpc(env, 'eth_getBlockByNumber', [tx.blockNumber, false]) : null; return { ok: true, payload: { tx, receipt, block }, status: String(receipt.status || '').toLowerCase() === '0x1' ? 'success' : 'error', toAddress: normalizeAddress(tx.to || receipt.to || ''), valuePol: weiHexToPol(tx.value), timestampMs: Number.isFinite(hexToNumber(block?.timestamp)) ? hexToNumber(block.timestamp) * 1000 : NaN }; } catch (error) { return { ok: false, error: 'polygon_rpc_lookup_failed', message: error.message || 'Polygon RPC lookup failed.' }; } } async function resolvePolygonTransactionSummary(env, txHash) { const explorerSummary = await fetchPolygonTransactionSummary(env, txHash); if (explorerSummary.ok) return explorerSummary; const notFoundLike = ['polygon_receipt_unavailable', 'polygon_receipt_lookup_failed'].includes(explorerSummary.error); if (!notFoundLike) return explorerSummary; const rpcSummary = await fetchPolygonTransactionSummaryViaRpc(env, txHash); if (rpcSummary.ok) return rpcSummary; return rpcSummary.error === 'polygon_receipt_unavailable' ? explorerSummary : rpcSummary; } function validateSoulwallTxFreshness(summary) { if (!summary?.ok) { return { ok: false, error: summary?.error || 'polygon_receipt_lookup_failed', message: summary?.message || 'Polygon receipt lookup failed.' }; } if (!Number.isFinite(summary.timestampMs)) { return { ok: false, error: 'polygon_tx_timestamp_unavailable', message: 'SoulWall could not read the Polygon transaction timestamp yet. Try again in a moment.' }; } const ageMs = Date.now() - summary.timestampMs; if (ageMs > SOULWALL_TX_MAX_AGE_MS) { return { ok: false, error: 'polygon_tx_too_old', message: 'This Polygon transaction is older than SoulWall\'s 24-hour verification window. Make a fresh donation and paste that new tx hash here.' }; } return { ok: true }; } function getStatus(value) { return String(value || '').trim().toLowerCase(); } function isConfirmedStatus(value) { const status = getStatus(value); return !status || ['confirmed', 'success', 'succeeded', 'completed', 'done'].includes(status); } function getDonationProjectSlug(donation) { return normalizeSlug( donation.projectSlug || donation.project_slug || donation.project?.slug || donation.project?.projectSlug || donation.project?.project_slug || donation.project?.id || '' ); } function getDonationAmountUsd(donation) { const candidates = [ donation.amountUsd, donation.amountUSD, donation.usdValue, donation.valueUsd, donation.amountInUsd, donation.priceUsd, donation.amount_in_usd ]; for (const candidate of candidates) { const number = Number(candidate); if (Number.isFinite(number) && number > 0) return number; } return 0; } function weiToPol(weiValue) { try { const wei = BigInt(String(weiValue || '0')); const whole = wei / 1000000000000000000n; const fraction = wei % 1000000000000000000n; return Number(`${whole}.${fraction.toString().padStart(18, '0').slice(0, 8)}`); } catch (error) { return 0; } } function normalizeDonationRows(payload) { const data = payload?.data || payload || {}; const roots = [ data.donations, data.donationByTransactionHash, data.donation, data.payments, data.transactions ]; return roots.flatMap((root) => { if (!root) return []; return Array.isArray(root) ? root : [root]; }); } async function queryGivethSubgraph(env, query, variables) { const response = await fetch(getGivethSubgraphUrl(env), { method: 'POST', headers: { 'content-type': 'application/json', accept: 'application/json' }, body: JSON.stringify({ query, variables }) }); const payload = await response.json().catch(() => ({})); if (!response.ok || payload.errors?.length) { const detail = payload.errors?.[0]?.message || `Giveth subgraph failed: ${response.status}`; throw new Error(detail); } return normalizeDonationRows(payload); } async function fetchGivethDonationRows(env, txHash) { const queries = [ `query DonationByTransactionHash($txHash: String!) { donations(where: { transactionHash: $txHash }) { id transactionHash status amountUsd amountUSD usdValue projectSlug project { slug title } } }`, `query DonationByTxHash($txHash: String!) { donations(where: { txHash: $txHash }) { id txHash status amountUsd amountUSD usdValue projectSlug project { slug title } } }`, `query DonationByHash($txHash: String!) { donations(where: { transactionId: $txHash }) { id transactionId status amountUsd amountUSD usdValue projectSlug project { slug title } } }` ]; const errors = []; for (const query of queries) { try { const rows = await queryGivethSubgraph(env, query, { txHash }); if (rows.length) return rows; } catch (error) { errors.push(error.message); } } return { rows: [], errors }; } async function verifyPolygonNativeTransfer(env, { txHash, project, transactionSummary = null }) { if (!project.recipientAddress) { return { ok: false, error: 'project_recipient_not_configured', message: 'The donation may already be submitted. SoulWall is waiting for Giveth confirmation for this project, because direct chain fallback proof is not configured yet.' }; } const summary = transactionSummary || await fetchPolygonTransactionSummary(env, txHash); if (!summary.ok) { return { ok: false, error: summary.error, message: summary.message }; } if (!['ok', 'success', 'succeeded'].includes(summary.status)) { return { ok: false, error: 'polygon_tx_not_successful', message: 'The Polygon transaction is visible, but it is not marked successful yet.' }; } if (summary.toAddress !== project.recipientAddress) { return { ok: false, error: 'polygon_recipient_mismatch', message: 'The Polygon transaction is confirmed, but it does not match the selected charity project recipient.', details: [`expected ${project.recipientAddress}`, `received ${summary.toAddress || 'unknown'}`] }; } if (summary.valuePol <= 0) { return { ok: false, error: 'polygon_empty_transfer', message: 'The Polygon transaction is confirmed, but no POL transfer value was found.' }; } return { ok: true, project, donation: { id: txHash, source: 'polygon-blockscout', status: summary.status, amountPol: summary.valuePol, projectSlug: project.slug, recipientAddress: project.recipientAddress }, amountPol: summary.valuePol, txHash }; } async function verifyGivethDonation(env, { txHash, projectSlug }) { const hash = normalizeTxHash(txHash); const project = getGivethProject(env, projectSlug); if (!hash) return { ok: false, error: 'invalid_tx_hash' }; if (!project) return { ok: false, error: 'unknown_giveth_project' }; const transactionSummary = await resolvePolygonTransactionSummary(env, hash); const freshness = validateSoulwallTxFreshness(transactionSummary); if (!freshness.ok) { return freshness; } const result = await fetchGivethDonationRows(env, hash); const rows = Array.isArray(result) ? result : result.rows; const errors = Array.isArray(result) ? [] : result.errors; for (const donation of rows) { const slug = getDonationProjectSlug(donation); const amountUsd = getDonationAmountUsd(donation); const status = donation.status || donation.state || donation.confirmationStatus || ''; if (slug === project.slug && isConfirmedStatus(status)) { return { ok: true, project, donation, amountUsd, txHash: hash }; } } const chainFallback = await verifyPolygonNativeTransfer(env, { txHash: hash, project, transactionSummary }); if (chainFallback.ok) return chainFallback; return { ok: false, error: 'giveth_donation_not_confirmed', message: chainFallback.message || 'No confirmed Giveth donation matched this transaction hash and project slug yet.', details: [ ...errors.slice(0, 3), chainFallback.error ].filter(Boolean) }; } export { getGivethProject, getGivethProjects, normalizeTxHash, verifyGivethDonation, withGivethPageMeta };