// Site-side member records for InstantAdPay. // The chain is the source of truth for money, credits, and qualification; // this module holds only what the chain doesn't: free members who haven't // touched the chain yet, sponsor attribution before first purchase (spec ยง4), // display handles, and join stats. Wiping this file = the clean reset between // the Amoy dress rehearsal and mainnet launch. const fs = require('fs'); const path = require('path'); let DATA_DIR = null; const FILE = () => path.join(DATA_DIR, 'accounts.json'); let db = { v: 1, byAddress: {}, joins: 0 }; function load() { try { db = JSON.parse(fs.readFileSync(FILE(), 'utf8')); } catch (e) {} if (!db || db.v !== 1) db = { v: 1, byAddress: {}, joins: 0 }; } function save() { try { const tmp = FILE() + '.tmp'; fs.writeFileSync(tmp, JSON.stringify(db)); fs.renameSync(tmp, FILE()); } catch (e) { console.error('accounts save failed', e.message); } } function init(opts) { DATA_DIR = opts.dataDir; load(); } function get(address) { return db.byAddress[(address || '').toLowerCase()] || null; } function upsert(address, fields) { const a = (address || '').toLowerCase(); if (!/^0x[0-9a-f]{40}$/.test(a)) return null; const cur = db.byAddress[a] || { created: Date.now() }; db.byAddress[a] = Object.assign(cur, fields || {}); save(); return db.byAddress[a]; } // Sponsor attribution: first touch wins, written on-chain at the member's // first purchase/activation and permanent from then on. function attributeSponsor(address, sponsorId) { const a = (address || '').toLowerCase(); const cur = get(a); if (cur && cur.sponsorId) return cur.sponsorId; // first touch already set const id = Number(sponsorId) || 0; upsert(a, { sponsorId: id }); db.joins += 1; save(); return id; } function count() { return Object.keys(db.byAddress).length; } module.exports = { init, get, upsert, attributeSponsor, count };