import { WEEKLY_COVENANT_SECONDS, buildCookie, createSessionToken, getSoulwallStore, json } from './_session.js'; import { getDonationIntentByExternalId, getDonationIntentById, getSupabaseConfig, getSupabaseUser, markDonationIntentSucceeded, supabaseFetch, upsertSoulwallSession } from './_supabase.js'; import { getGivethProject, normalizeTxHash, verifyGivethDonation } from './_giveth.js'; const CORS_HEADERS = { 'access-control-allow-origin': '*', 'access-control-allow-methods': 'POST, OPTIONS', 'access-control-allow-headers': 'content-type, authorization' }; function corsJson(data, status = 200, headers = {}) { return json(data, status, { ...CORS_HEADERS, ...headers }); } function cleanText(value, maxLength = 220) { return String(value || '').trim().slice(0, maxLength); } async function recordLedgerEvent(env, { txHash, projectSlug = '', projectName = '', amountUsd = null, amountNative = null, currency = 'USD', network = 'polygon', provider = 'giveth', accessExpiresAt = '' }) { const config = getSupabaseConfig(env); if (!config || !txHash || env.SOULWALL_TELEMETRY_ENABLED === '0') return null; const row = { event_id: cleanText(txHash, 180), client_id: 'soulwall-gateway', integration_origin: 'https://soulwall.org', provider: cleanText(provider, 80), project_slug: cleanText(projectSlug, 160), project_name: cleanText(projectName, 220), tx_hash: cleanText(txHash, 180), currency: cleanText(currency, 16).toUpperCase(), amount_usd: Number.isFinite(Number(amountUsd)) ? Number(amountUsd) : null, amount_native: Number.isFinite(Number(amountNative)) ? Number(amountNative) : null, network: cleanText(network, 64), status: 'confirmed', access_expires_at: cleanText(accessExpiresAt, 64) || null, confirmed_at: new Date().toISOString(), updated_at: new Date().toISOString(), raw_event: { source: 'soulwall-giveth-verify', proofRecognized: true, txHash } }; try { const rows = await supabaseFetch(config, '/rest/v1/soulwall_ledger?on_conflict=event_id', { method: 'POST', headers: { prefer: 'resolution=merge-duplicates,return=representation' }, body: JSON.stringify(row) }); return Array.isArray(rows) ? rows[0] || null : null; } catch (error) { return null; } } async function grantGivethAccess(env, { intent, txHash, email, userId }) { if (intent?.status === 'succeeded' && intent?.access_expires_at) { return { duplicate: true, accessExpiresAt: intent.access_expires_at }; } const accessExpiresAt = new Date(Date.now() + WEEKLY_COVENANT_SECONDS * 1000).toISOString(); const profile = await upsertSoulwallSession(env, { email, userId, expiresAt: accessExpiresAt, source: 'giveth', transactionId: txHash }); if (!profile) return { error: 'supabase_profile_update_failed' }; await markDonationIntentSucceeded(env, { uniqueIntentId: intent.unique_intent_id, transactionId: txHash, accessExpiresAt, provider: 'giveth', externalId: txHash }); return { accessExpiresAt }; } async function grantAnonymousGivethAccess(env, txHash) { const store = getSoulwallStore(env); if (!store) { return { error: 'auth_required', message: 'Sign in by email before verifying a Giveth donation.' }; } const consumedKey = `giveth_tx:${txHash}`; const consumed = await store.get(consumedKey); if (consumed) { return { error: 'giveth_tx_already_claimed', message: 'This transaction has already been used for a SoulWall session.' }; } const expSeconds = Math.floor(Date.now() / 1000) + WEEKLY_COVENANT_SECONDS; const session = await createSessionToken(env, 'giveth', expSeconds); if (!session?.token) { return { error: 'session_token_unavailable', message: 'SoulWall could not mint a browser session token.' }; } await store.put(consumedKey, JSON.stringify({ claimedAt: new Date().toISOString() }), { expirationTtl: WEEKLY_COVENANT_SECONDS }); return { accessExpiresAt: new Date(expSeconds * 1000).toISOString(), cookie: buildCookie(session.token, WEEKLY_COVENANT_SECONDS) }; } export async function onRequestOptions() { return new Response(null, { status: 204, headers: CORS_HEADERS }); } export async function onRequestPost({ env, request }) { let body = {}; try { body = await request.json(); } catch (error) { return corsJson({ error: 'invalid_json' }, 400); } const txHash = normalizeTxHash(body.txHash || body.transactionHash || ''); const projectSlug = String(body.projectSlug || body.nonprofitSlug || '').trim(); const intentId = String(body.partnerDonationId || body.intentId || '').trim(); if (!txHash) { return corsJson({ error: 'missing_required_fields', required: ['txHash'], received: { txHash: Boolean(txHash), projectSlug: Boolean(projectSlug), partnerDonationId: Boolean(intentId) } }, 400); } const user = await getSupabaseUser(env, request); const existingExternal = await getDonationIntentByExternalId(env, txHash); if (existingExternal?.status === 'succeeded' && existingExternal?.access_expires_at) { const expiresAtMs = Date.parse(existingExternal.access_expires_at); const nowMs = Date.now(); const stillOpen = Number.isFinite(expiresAtMs) && expiresAtMs > nowMs; const existingProjectSlug = existingExternal.nonprofit_slug || ''; const existingProject = getGivethProject(env, existingProjectSlug); const refreshedProof = existingProjectSlug ? await verifyGivethDonation(env, { txHash, projectSlug: existingProjectSlug }) : null; const refreshedProject = refreshedProof?.ok ? refreshedProof.project : existingProject; await recordLedgerEvent(env, { txHash, projectSlug: refreshedProject?.slug || existingProjectSlug, projectName: refreshedProject?.name || '', amountUsd: refreshedProof?.ok ? refreshedProof.amountUsd : null, amountNative: refreshedProof?.ok ? refreshedProof.amountPol : null, currency: refreshedProof?.amountPol ? 'POL' : 'USD', provider: existingExternal.provider || 'giveth', accessExpiresAt: existingExternal.access_expires_at }); if (stillOpen) { const expSeconds = Math.floor(expiresAtMs / 1000); const session = await createSessionToken(env, 'giveth-duplicate', expSeconds); const headers = session?.token ? { 'set-cookie': buildCookie(session.token, Math.max(1, Math.floor((expiresAtMs - nowMs) / 1000))) } : {}; return corsJson({ accepted: true, duplicate: true, proofRecognized: true, unlocked: true, provider: 'giveth', projectSlug: refreshedProject?.slug || existingProjectSlug, projectName: refreshedProject?.name || '', amountPol: refreshedProof?.ok ? refreshedProof.amountPol : null, amountUsd: refreshedProof?.ok ? refreshedProof.amountUsd : null, message: 'This confirmed Giveth transaction already opened SoulWall. Restoring the active session.', transactionId: txHash, accessExpiresAt: existingExternal.access_expires_at }, 200, headers); } return corsJson({ accepted: true, duplicate: true, proofRecognized: true, unlocked: false, provider: 'giveth', projectSlug: refreshedProject?.slug || existingProjectSlug, projectName: refreshedProject?.name || '', amountPol: refreshedProof?.ok ? refreshedProof.amountPol : null, amountUsd: refreshedProof?.ok ? refreshedProof.amountUsd : null, demoState: 'recognized_expired', message: 'This Giveth transaction was recognized by SoulWall earlier. Its access window has expired, but the proof is real and can appear in the public ledger.', transactionId: txHash, accessExpiresAt: existingExternal.access_expires_at }, 200); } let intent = null; if (user?.email && user?.id) { if (!intentId) { return corsJson({ error: 'missing_required_fields', required: ['partnerDonationId'] }, 400); } intent = await getDonationIntentById(env, intentId); if (!intent?.unique_intent_id || intent.user_id !== user.id || intent.email !== user.email) { return corsJson({ error: 'giveth_intent_not_found' }, 404); } } const verified = await verifyGivethDonation(env, { txHash, projectSlug }); if (!verified.ok) { return corsJson({ accepted: true, unlocked: false, provider: 'giveth', error: verified.error, message: verified.message || 'Giveth donation is not confirmed yet.', details: verified.details || [] }, 202); } await recordLedgerEvent(env, { txHash, projectSlug: verified.project?.slug || projectSlug, projectName: verified.project?.name || '', amountUsd: verified.amountUsd, amountNative: verified.amountPol, currency: verified.amountPol ? 'POL' : 'USD', provider: 'giveth' }); if (!user?.email || !user?.id) { const anonymousResult = await grantAnonymousGivethAccess(env, txHash); if (anonymousResult.error) { if (anonymousResult.error === 'giveth_tx_already_claimed') { return corsJson({ accepted: true, duplicate: true, proofRecognized: true, unlocked: false, provider: 'giveth', projectSlug: verified.project?.slug || projectSlug, projectName: verified.project?.name || '', amountPol: verified.amountPol, amountUsd: verified.amountUsd, transactionId: txHash, demoState: 'recognized_consumed', message: 'This donation proof is already recorded by SoulWall. The product demo is showing the prior-use state rather than treating the donation as failed.' }, 200); } return corsJson({ error: anonymousResult.error, message: anonymousResult.message }, anonymousResult.error === 'auth_required' ? 401 : 409); } return corsJson({ accepted: true, unlocked: true, provider: 'giveth', projectSlug: verified.project?.slug || projectSlug, projectName: verified.project?.name || '', amountPol: verified.amountPol, amountUsd: verified.amountUsd, transactionId: txHash, accessExpiresAt: anonymousResult.accessExpiresAt }, 200, { 'set-cookie': anonymousResult.cookie }); } const result = await grantGivethAccess(env, { intent, txHash, email: user.email, userId: user.id }); if (result.error) return corsJson({ error: result.error }, 503); return corsJson({ accepted: true, unlocked: !result.duplicate, duplicate: Boolean(result.duplicate), provider: 'giveth', projectSlug: verified.project?.slug || projectSlug, projectName: verified.project?.name || '', amountPol: verified.amountPol, amountUsd: verified.amountUsd, transactionId: txHash, accessExpiresAt: result.accessExpiresAt }); }