'use strict'; // Holding tank (Marty, 2026-09-11): free members who arrived with no sponsor wait // here, and a member who has bought their own $20+ package can adopt one: // first come, at most two open adoptions at a time, an adoption falls back into // the tank after 7 days if the person never linked a wallet or bought, and a // person can be adopted twice at most before they stay wherever they are. // Members can also release one of their own free referrals into the tank // (pay it forward). The contract binds sponsor at first purchase, so every // hand-off here is a site record until then. const fs = require('fs'); const path = require('path'); const db = require('./db'); const CAP_OPEN = 2; // open adoptions per adopter const TTL_MS = 7 * 86400000; // an adoption's window to convert const MAX_ADOPTIONS = 2; // per adoptee, lifetime const MIN_OWN_BUY_CENTS = 2000; // adopter must have bought a $20+ package themselves let DATA_DIR = '.', accounts, chain, messages, mailer, siteUrl = 'https://instantadpay.com'; const J = { db: { v: 1, nextId: 1, adoptions: [] }, FILE: () => path.join(DATA_DIR, 'tank.json'), load() { try { this.db = Object.assign(this.db, JSON.parse(fs.readFileSync(this.FILE(), 'utf8'))); } catch (e) {} }, save() { try { fs.writeFileSync(this.FILE(), JSON.stringify(this.db)); } catch (e) {} }, async add(a) { const row = Object.assign({ id: this.db.nextId++ }, a); this.db.adoptions.push(row); this.save(); return row; }, async open(adopter) { return this.db.adoptions.filter(x => x.status === 'open' && (!adopter || x.adopter === adopter)); }, async countFor(adoptee) { return this.db.adoptions.filter(x => x.adoptee === adoptee && x.status !== 'released').length; }, async setStatus(id, status) { const x = this.db.adoptions.find(r => r.id === id); if (x) { x.status = status; x.closed = Date.now(); this.save(); } }, async recent(n) { return this.db.adoptions.slice(-(n || 100)).reverse(); } }; const D = { async add(a) { const r = await db.q('INSERT INTO adoptions (adoptee,adopter,ts,expires,status,note) VALUES (?,?,?,?,?,?)', [a.adoptee, a.adopter, a.ts, a.expires, a.status, a.note || null]); return Object.assign({ id: r.insertId }, a); }, async open(adopter) { const rows = adopter ? await db.q("SELECT * FROM adoptions WHERE status='open' AND adopter=?", [adopter]) : await db.q("SELECT * FROM adoptions WHERE status='open'"); return rows.map(rowA); }, async countFor(adoptee) { const r = await db.q("SELECT COUNT(*) n FROM adoptions WHERE adoptee=? AND status<>'released'", [adoptee]); return Number(r[0].n); }, async setStatus(id, status) { await db.q('UPDATE adoptions SET status=?, closed=? WHERE id=?', [status, Date.now(), Number(id)]); }, async recent(n) { return (await db.q('SELECT * FROM adoptions ORDER BY id DESC LIMIT ?', [Number(n) || 100])).map(rowA); } }; const rowA = r => ({ id: r.id, adoptee: r.adoptee, adopter: r.adopter, ts: Number(r.ts), expires: Number(r.expires), status: r.status, note: r.note || null, closed: r.closed ? Number(r.closed) : null }); const impl = () => db.enabled() ? D : J; function init(opts) { DATA_DIR = opts.dataDir; accounts = opts.accounts; chain = opts.chain; messages = opts.messages; mailer = opts.mailer; if (opts.site) siteUrl = opts.site; J.load(); } const mask = e => String(e || '').replace(/^(.).*(@.*)$/, '$1***$2'); const nameOf = a => a.username ? '@' + a.username : (a.memberId ? 'member #' + a.memberId : mask(a.email)); const inTank = a => !a.sponsorRef && !a.memberId; // arrived with no sponsor (or fell back), still free // waiting list: newest sign-in first so a live one is easy to spot async function waiting() { const all = await accounts.listAll(2000); return all.filter(inTank).map(a => ({ email: a.email, name: nameOf(a), username: a.username || null, joined: a.created, lastSeen: a.lastSeen || 0, wallet: false })) .sort((a, b) => (b.lastSeen || b.joined) - (a.lastSeen || a.joined)); } // has this member bought a $20+ package themselves? (the qualifying-for-yourself buy) function hasOwnBuy(memberId) { if (!memberId) return false; for (const ev of chain.recentEvents(200000)) if (ev.type === 'Purchase' && ev.buyerId === memberId && Number(ev.priceCents) >= MIN_OWN_BUY_CENTS) return true; return false; } async function eligibility(email) { const a = await accounts.byEmail(email); if (!a) return { ok: false, reason: 'Sign in first.' }; if (!a.memberId) return { ok: false, reason: 'Switch on payouts and buy your first $20 package to adopt from the tank.' }; if (!hasOwnBuy(a.memberId)) return { ok: false, reason: 'Buy your own $20 or more package first. Adopting is for members who have made that move themselves.' }; const open = await impl().open(a.email); if (open.length >= CAP_OPEN) return { ok: false, reason: 'You have ' + CAP_OPEN + ' open adoptions. Help one of them link a wallet or buy, and a slot frees up.', full: true }; return { ok: true, account: a, open }; } async function view(email) { const e = String(email || '').toLowerCase(); const el = await eligibility(e); const mine = []; for (const ad of await impl().open(e)) { const a = await accounts.byEmail(ad.adoptee); mine.push({ id: ad.id, name: a ? nameOf(a) : mask(ad.adoptee), email: ad.adoptee, ts: ad.ts, expires: ad.expires, lastSeen: a ? (a.lastSeen || 0) : 0, wallet: !!(a && a.address), address: (a && a.address) || null, bought: !!(a && a.memberId) }); } return { eligible: el.ok, reason: el.ok ? '' : el.reason, cap: CAP_OPEN, ttlDays: TTL_MS / 86400000, waiting: await waiting(), mine }; } async function adopt(adopterEmail, who, note) { const e = String(adopterEmail || '').toLowerCase(); const el = await eligibility(e); if (!el.ok) return { error: el.reason }; const me = el.account; const key = String(who || '').trim().toLowerCase().replace(/^@/, ''); const list = await waiting(); const target = list.find(w => (w.username && w.username.toLowerCase() === key) || w.email === key); if (!target) return { error: 'That member is no longer in the tank.' }; if (target.email === e) return { error: 'That is you.' }; if (await impl().countFor(target.email) >= MAX_ADOPTIONS) return { error: 'That member has been adopted twice already and stays where they are.' }; const token = me.username || me.code || String(me.memberId); const r = await accounts.setSponsorRef(target.email, token); if (r.error) return r; const now = Date.now(); const text = String(note || '').trim().slice(0, 600) || ('Hi, I am ' + nameOf(me) + '. You joined InstantAdPay without a sponsor, so I picked you up from the holding tank. I will walk you through the first three steps whenever you are ready. Reply here.'); const ad = await impl().add({ adoptee: target.email, adopter: e, ts: now, expires: now + TTL_MS, status: 'open', note: text }); try { await messages.sendChat(me.memberId || 0, e, target.email, text); } catch (err) {} if (mailer && mailer.hasKey()) { try { await mailer.send(target.email, nameOf(me) + ' is now your sponsor on InstantAdPay', 'You joined InstantAdPay without a sponsor. ' + nameOf(me) + ' has picked you up from the holding tank and is your sponsor now, which means a real person to walk you through the first steps.\n\nTheir message:\n\n' + text + '\n\nReply in your member area: ' + siteUrl + '/my#messages\n\nInstantAdPay'); } catch (err) {} } return { ok: true, adoption: ad, name: target.name }; } // pay it forward: give one of your own free referrals to the tank async function release(ownerEmail, directEmail) { const o = String(ownerEmail || '').toLowerCase(), d = String(directEmail || '').toLowerCase(); const owner = await accounts.byEmail(o), direct = await accounts.byEmail(d); if (!owner || !direct) return { error: 'No such member.' }; const toks = [owner.code, owner.username, owner.memberId ? String(owner.memberId) : null].filter(Boolean).map(String); if (!toks.includes(String(direct.sponsorRef || ''))) return { error: 'That member is not in your line.' }; if (direct.memberId) return { error: 'That member has already bought; on-chain sponsorship cannot move.' }; const r = await accounts.setSponsorRef(d, ''); if (r.error) return r; // an open adoption of this person closes as released (it no longer counts toward their two) let closed = 0; for (const ad of await impl().open(null)) if (ad.adoptee === d) { await impl().setStatus(ad.id, 'released'); closed++; } if (!closed) await impl().add({ adoptee: d, adopter: o, ts: Date.now(), expires: Date.now(), status: 'released', note: 'released to the tank' }); return { ok: true }; } // open adoptions past their window: converted ones close as done; the rest fall // back into the tank unless the person has been adopted twice already async function sweep() { const now = Date.now(); let done = 0, back = 0, kept = 0; for (const ad of await impl().open(null)) { if (ad.expires > now) continue; const a = await accounts.byEmail(ad.adoptee); if (!a) { await impl().setStatus(ad.id, 'expired'); continue; } if (a.address || a.memberId) { await impl().setStatus(ad.id, 'done'); done++; continue; } if (await impl().countFor(ad.adoptee) >= MAX_ADOPTIONS) { await impl().setStatus(ad.id, 'expired'); kept++; continue; } await accounts.setSponsorRef(ad.adoptee, ''); await impl().setStatus(ad.id, 'expired'); back++; } return { done, back, kept }; } // pay-it-forward gift record: the sponsor already sent POL wallet-to-wallet; we // only log the hash and tell the recipient. Recipient must be in the giver's line. async function recordGift(fromEmail, toEmail, tx, pol) { const f = String(fromEmail || '').toLowerCase(), t = String(toEmail || '').toLowerCase(); const giver = await accounts.byEmail(f), to = await accounts.byEmail(t); if (!giver || !to) return { error: 'No such member.' }; const toks = [giver.code, giver.username, giver.memberId ? String(giver.memberId) : null].filter(Boolean).map(String); const adopted = (await impl().open(f)).some(ad => ad.adoptee === t); if (!toks.includes(String(to.sponsorRef || '')) && !adopted) return { error: 'That member is not in your line.' }; if (!/^0x[0-9a-fA-F]{64}$/.test(String(tx || ''))) return { error: 'Transaction hash missing.' }; const amount = Number(pol) || 0; const row = { adoptee: t, adopter: f, ts: Date.now(), expires: Date.now(), status: 'gift', note: 'PIF ' + amount + ' POL ' + tx }; await impl().add(row); const cc = chain.getConfig ? chain.getConfig() : {}; const link = ((cc.explorer || 'https://polygonscan.com').replace(/\/+$/, '')) + '/tx/' + tx; const text = nameOf(giver) + ' just sent ' + amount + ' POL to your wallet so you can buy your first package. It is already there: open Buy packages when you are ready. Proof: ' + link; try { await messages.sendChat(giver.memberId || 0, f, t, text); } catch (e) {} if (mailer && mailer.hasKey()) { try { await mailer.send(t, nameOf(giver) + ' sent you POL for your first InstantAdPay package', text + '\n\n' + siteUrl + '/my#buy'); } catch (e) {} } return { ok: true }; } async function adminView() { const recent = await impl().recent(200); const names = {}; for (const ad of recent) for (const em of [ad.adoptee, ad.adopter]) if (!(em in names)) { const a = await accounts.byEmail(em); names[em] = a ? nameOf(a) : em; } return { waiting: await waiting(), adoptions: recent.map(ad => Object.assign({}, ad, { adopteeName: names[ad.adoptee], adopterName: names[ad.adopter] })), cap: CAP_OPEN, ttlDays: TTL_MS / 86400000 }; } module.exports = { init, view, adopt, release, sweep, adminView, waiting, hasOwnBuy, recordGift, CAP_OPEN, TTL_MS };