Linked positions (Qualified Start): extra wallets on one account, credits pooled, buy-from picker
- accounts/db: positions table + JSON store (add/list/owner/remove; main-wallet and cross-account guards) - auth verify: asPosition links a second wallet without touching the session; position wallets can't mint a session - /api/my/positions (chain-refreshed member ids, buyer counts, credits) + remove - credits pooled across main + positions on /api/me, dashboard, campaigns; campaign charged to the best-funded position - My line: own positions listed on level 1 as 'You · position N' - Buy pane: Qualified Start card + Add a position flow + Buy-from picker with connected-wallet guard - Wallet pane: Your positions card; chatbot answer updated Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
+63
-2
@@ -49,6 +49,7 @@ const J = {
|
||||
try { this.db = JSON.parse(fs.readFileSync(this.FILE(), 'utf8')); } catch (e) {}
|
||||
if (!this.db || !this.db.v) this.db = { v: 2, byEmail: {}, byAddress: {}, byCode: {}, joins: 0 };
|
||||
if (!this.db.byCode) this.db.byCode = {};
|
||||
if (!this.db.positions) this.db.positions = {};
|
||||
for (const a of Object.values(this.db.byEmail)) {
|
||||
if (!a.code) { a.code = newCode(c => this.db.byCode[c]); this.db.byCode[a.code] = a.email; }
|
||||
else if (!this.db.byCode[a.code]) this.db.byCode[a.code] = a.email;
|
||||
@@ -160,6 +161,7 @@ const J = {
|
||||
if (acct.address && acct.address !== a) return { error: 'This account is already linked to wallet '
|
||||
+ acct.address.slice(0, 6) + '…' + acct.address.slice(-4) + '. Connect that wallet instead.' };
|
||||
if (this.db.byAddress[a] && this.db.byAddress[a] !== e) return { error: 'That wallet is already linked to a different account.' };
|
||||
if (this.db.positions[a]) return { error: 'That wallet is already a linked position' + (this.db.positions[a].email === e ? ' on this account.' : ' on a different account.') };
|
||||
acct.address = a;
|
||||
this.db.byAddress[a] = e;
|
||||
this.save();
|
||||
@@ -172,7 +174,32 @@ const J = {
|
||||
acct.sponsorRef = ref; this.save();
|
||||
return { ok: true, account: pub(acct) };
|
||||
},
|
||||
async count() { return Object.keys(this.db.byEmail).length; }
|
||||
async count() { return Object.keys(this.db.byEmail).length; },
|
||||
// ---- linked positions (extra wallets on one account) ----
|
||||
async positions(e) {
|
||||
return Object.entries(this.db.positions).filter(([, p]) => p.email === e)
|
||||
.map(([address, p]) => ({ address, email: p.email, memberId: p.memberId || 0, created: p.created }))
|
||||
.sort((x, y) => x.created - y.created);
|
||||
},
|
||||
async addPosition(e, a) {
|
||||
const acct = this.db.byEmail[e];
|
||||
if (!acct) return { error: 'No such account.' };
|
||||
if (!acct.address) return { error: 'Link your main wallet first.' };
|
||||
if (acct.address === a) return { error: 'That is your main wallet. Switch to a different account in your wallet app, then try again.' };
|
||||
if (this.db.byAddress[a]) return { error: 'That wallet is already the main wallet of another account.' };
|
||||
const cur = this.db.positions[a];
|
||||
if (cur && cur.email !== e) return { error: 'That wallet is already a position on a different account.' };
|
||||
if (!cur) { this.db.positions[a] = { email: e, memberId: 0, created: Date.now() }; this.save(); }
|
||||
return { ok: true, address: a, created: !cur };
|
||||
},
|
||||
async setPositionMember(a, id) { const p = this.db.positions[a]; if (p && p.memberId !== id) { p.memberId = id; this.save(); } },
|
||||
async positionOwner(a) { const p = this.db.positions[a]; return p ? { address: a, email: p.email, memberId: p.memberId || 0 } : null; },
|
||||
async removePosition(e, a) {
|
||||
const p = this.db.positions[a];
|
||||
if (!p || p.email !== e) return { error: 'No such position.' };
|
||||
if (p.memberId) return { error: 'That position is already registered on-chain and cannot be unlinked.' };
|
||||
delete this.db.positions[a]; this.save(); return { ok: true };
|
||||
},
|
||||
};
|
||||
|
||||
// ---- MySQL mode ----
|
||||
@@ -278,6 +305,8 @@ const D = {
|
||||
if (!cur) return { error: 'No such account.' };
|
||||
if (cur.address && cur.address !== a) return { error: 'This account is already linked to wallet '
|
||||
+ cur.address.slice(0, 6) + '…' + cur.address.slice(-4) + '. Connect that wallet instead.' };
|
||||
const pos = (await db.q('SELECT email FROM positions WHERE address=?', [a]))[0];
|
||||
if (pos) return { error: 'That wallet is already a linked position' + (pos.email === e ? ' on this account.' : ' on a different account.') };
|
||||
try { await db.q('UPDATE accounts SET address=? WHERE email=?', [a, e]); }
|
||||
catch (err) {
|
||||
if (err.code === 'ER_DUP_ENTRY') return { error: 'That wallet is already linked to a different account.' };
|
||||
@@ -291,7 +320,34 @@ const D = {
|
||||
if (!r.affectedRows) return { error: 'No such account.' };
|
||||
return { ok: true, account: await this.byEmail(e) };
|
||||
},
|
||||
async count() { const r = await db.q('SELECT COUNT(*) n FROM accounts'); return Number(r[0].n); }
|
||||
async count() { const r = await db.q('SELECT COUNT(*) n FROM accounts'); return Number(r[0].n); },
|
||||
// ---- linked positions (extra wallets on one account) ----
|
||||
async positions(e) {
|
||||
const rows = await db.q('SELECT * FROM positions WHERE email=? ORDER BY created', [e]);
|
||||
return rows.map(r => ({ address: r.address, email: r.email, memberId: r.member_id || 0, created: Number(r.created) }));
|
||||
},
|
||||
async addPosition(e, a) {
|
||||
const acct = await this.byEmail(e);
|
||||
if (!acct) return { error: 'No such account.' };
|
||||
if (!acct.address) return { error: 'Link your main wallet first.' };
|
||||
if (acct.address === a) return { error: 'That is your main wallet. Switch to a different account in your wallet app, then try again.' };
|
||||
if (await this.byAddress(a)) return { error: 'That wallet is already the main wallet of another account.' };
|
||||
const cur = (await db.q('SELECT * FROM positions WHERE address=?', [a]))[0];
|
||||
if (cur && cur.email !== e) return { error: 'That wallet is already a position on a different account.' };
|
||||
if (!cur) await db.q('INSERT INTO positions (address,email,member_id,created) VALUES (?,?,0,?)', [a, e, Date.now()]);
|
||||
return { ok: true, address: a, created: !cur };
|
||||
},
|
||||
async setPositionMember(a, id) { await db.q('UPDATE positions SET member_id=? WHERE address=? AND member_id<>?', [id, a, id]); },
|
||||
async positionOwner(a) {
|
||||
const r = (await db.q('SELECT * FROM positions WHERE address=?', [a]))[0];
|
||||
return r ? { address: r.address, email: r.email, memberId: r.member_id || 0 } : null;
|
||||
},
|
||||
async removePosition(e, a) {
|
||||
const r = (await db.q('SELECT * FROM positions WHERE address=? AND email=?', [a, e]))[0];
|
||||
if (!r) return { error: 'No such position.' };
|
||||
if (r.member_id) return { error: 'That position is already registered on-chain and cannot be unlinked.' };
|
||||
await db.q('DELETE FROM positions WHERE address=?', [a]); return { ok: true };
|
||||
},
|
||||
};
|
||||
|
||||
const impl = () => db.enabled() ? D : J;
|
||||
@@ -392,4 +448,9 @@ module.exports = { init, signup, login, ensure, byEmail, byAddress, byCode, byUs
|
||||
getMutes: e => impl().getMutes(String(e || '').toLowerCase()),
|
||||
setMute: (o, t, m) => impl().setMute(String(o || '').toLowerCase(), String(t || '').toLowerCase(), m),
|
||||
sponsorOf, isDownlineOf, getChatSettings,
|
||||
positions: e => impl().positions(normEmail(e)),
|
||||
addPosition: (e, a) => { const x = normAddr(a); return /^0x[0-9a-f]{40}$/.test(x) ? impl().addPosition(normEmail(e), x) : Promise.resolve({ error: 'Bad wallet address.' }); },
|
||||
setPositionMember: (a, id) => impl().setPositionMember(normAddr(a), Number(id) || 0),
|
||||
positionOwner: a => impl().positionOwner(normAddr(a)),
|
||||
removePosition: (e, a) => impl().removePosition(normEmail(e), normAddr(a)),
|
||||
byMemberId: id => impl().byMemberId(id) };
|
||||
|
||||
Reference in New Issue
Block a user