// Network registry (LinkSpin, 2026-09-15): who is who across the Crypto Team Build Network // properties, keyed by email (and by main wallet once one exists). InstantAdPay writes it as // members join, name themselves, link a wallet and bind a sponsor; LinkSpin reads it at sign-in // and wallet link so a member's sponsor line carries over. Nothing here moves money: each // contract still binds its own sponsor at first activation. Dual-mode store like the rest of // the engine (MySQL when NETWORK_DB_URL is set, JSON on the volume otherwise). In the test area // the JSON file is seeded from an export of the live InstantAdPay accounts. const fs = require('fs'); const path = require('path'); let DATA_DIR = null; const norm = e => String(e || '').trim().toLowerCase(); const J = { db: null, FILE: () => path.join(DATA_DIR, 'registry.json'), load() { try { this.db = JSON.parse(fs.readFileSync(this.FILE(), 'utf8')); } catch (e) { this.db = { v: 1, byEmail: {} }; } if (!this.db.byEmail) this.db.byEmail = {}; }, save() { try { fs.writeFileSync(this.FILE(), JSON.stringify(this.db)); } catch (e) {} }, async get(email) { if (!this.db) this.load(); return this.db.byEmail[norm(email)] || null; }, async byWallet(addr) { if (!this.db) this.load(); const a = norm(addr); return Object.values(this.db.byEmail).find(r => norm(r.wallet) === a) || null; }, async byUsername(u) { if (!this.db) this.load(); const x = norm(u); return Object.values(this.db.byEmail).find(r => norm(r.username) === x) || null; }, async upsert(rec) { if (!this.db) this.load(); const e = norm(rec.email); if (!e) return null; const cur = this.db.byEmail[e] || { email: e, created: Date.now() }; for (const k of ['username', 'wallet', 'sponsorEmail', 'firstProperty', 'joinedAt']) if (rec[k] != null && rec[k] !== '') cur[k] = k === 'wallet' || k === 'sponsorEmail' ? norm(rec[k]) : rec[k]; cur.updated = Date.now(); this.db.byEmail[e] = cur; this.save(); return cur; }, async all() { if (!this.db) this.load(); return Object.values(this.db.byEmail); }, async count() { if (!this.db) this.load(); return Object.keys(this.db.byEmail).length; } }; function init(opts) { DATA_DIR = opts.dataDir; } async function get(email) { return J.get(email); } async function byWallet(addr) { return J.byWallet(addr); } async function byUsername(u) { return J.byUsername(u); } async function upsert(rec) { return J.upsert(rec); } async function all() { return J.all(); } async function count() { return J.count(); } // the sponsor chain above an email, nearest first, bounded (a loop in bad data must not hang a request) async function chainUp(email, max = 25) { const out = []; let cur = await get(email); const seen = new Set([norm(email)]); while (cur && cur.sponsorEmail && out.length < max) { const e = norm(cur.sponsorEmail); if (seen.has(e)) break; seen.add(e); const sp = await get(e); if (!sp) break; out.push(sp); cur = sp; } return out; } // bulk load from an InstantAdPay export: [{email, username, wallet, sponsorEmail, joinedAt}] async function importRows(rows, property) { let n = 0; for (const r of rows || []) { if (await upsert(Object.assign({ firstProperty: property || 'instantadpay' }, r))) n++; } return n; } module.exports = { init, get, byWallet, byUsername, upsert, all, count, chainUp, importRows };