LinkSpin test area: InstantAdPay engine fork rebranded, network registry, sponsor carry-over with engine activation and claim window, rotator with /r/ redirects, link-domain mini-sites

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
martbost
2026-09-15 16:16:52 -05:00
commit 010e8d7ffc
130 changed files with 20096 additions and 0 deletions
+55
View File
@@ -0,0 +1,55 @@
// 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 };