require('bare-process/global') const EventEmitter = require('bare-events') const b4a = require('b4a') const { assertNonEmpty } = require('../../_shared/lib/errors.js') const { initModuleSwarm, gossipSend } = require('../../_shared/p2p-bare.js') const PROTOCOL = 'credit-ledger/v1' class HyperP2PCreditLedger extends EventEmitter { constructor (opts = {}) { super() this.topic = opts.topic || null this.keyPair = opts.keyPair || require('hypercore-crypto').keyPair() this.peerHex = b4a.toString(this.keyPair.publicKey, 'hex') this._accounts = new Map() this._stats = { credits: 0, debits: 0, gossipIn: 0, gossipOut: 0 } this.swarm = null this._peerMsgs = null } openAccount (accountId, initial = 0) { assertNonEmpty(accountId, 'accountId') if (this._accounts.has(accountId)) throw new Error('account exists') const acct = { id: accountId, balance: initial, updatedAt: Date.now() } this._accounts.set(accountId, acct) this._sync(accountId, 'open', acct.balance) this.emit('account', acct) return acct } credit (accountId, amount, reason = '') { return this._apply(accountId, Math.abs(amount), 'credit', reason) } debit (accountId, amount, reason = '') { return this._apply(accountId, -Math.abs(amount), 'debit', reason) } transfer (fromId, toId, amount) { assertNonEmpty(fromId, 'fromId') assertNonEmpty(toId, 'toId') if (amount <= 0) throw new Error('amount must be positive') const from = this._accounts.get(fromId) const to = this._accounts.get(toId) if (!from || !to) throw new Error('unknown account') if (from.balance < amount) throw new Error('insufficient balance') this.debit(fromId, amount, `transfer to ${toId}`) this.credit(toId, amount, `transfer from ${fromId}`) return { from: fromId, to: toId, amount, at: Date.now() } } balance (accountId) { const a = this._accounts.get(accountId) return a ? a.balance : null } listAccounts () { return [...this._accounts.keys()] } hasAccount (accountId) { return this._accounts.has(accountId) } totalSupply () { let sum = 0 for (const acct of this._accounts.values()) sum += acct.balance return sum } topBalances (limit = 5) { return [...this._accounts.values()] .sort((a, b) => b.balance - a.balance) .slice(0, Math.max(0, limit | 0)) .map((a) => ({ id: a.id, balance: a.balance })) } accountSnapshot (accountId) { const a = this._accounts.get(accountId) return a ? { id: a.id, balance: a.balance, updatedAt: a.updatedAt } : null } _apply (accountId, delta, kind, reason) { assertNonEmpty(accountId, 'accountId') const acct = this._accounts.get(accountId) if (!acct) throw new Error('unknown account') acct.balance += delta acct.updatedAt = Date.now() if (kind === 'credit') this._stats.credits++ else this._stats.debits++ this._sync(accountId, kind, acct.balance, { delta, reason, peer: this.peerHex }) this.emit(kind, { accountId, balance: acct.balance, delta, reason }) return acct } _sync (accountId, kind, balance, extra = {}) { if (!this._peerMsgs) return gossipSend(this, { type: 'ledger-entry', accountId, kind, balance, at: Date.now(), ...extra }) this._stats.gossipOut++ } _onGossip (data) { if (!data || data.type !== 'ledger-entry') return this._stats.gossipIn++ let acct = this._accounts.get(data.accountId) if (!acct && data.kind === 'open') { acct = { id: data.accountId, balance: data.balance, updatedAt: data.at } this._accounts.set(data.accountId, acct) } else if (acct) { acct.balance = data.balance acct.updatedAt = data.at } this.emit('remote-entry', data) } getStats () { return { ...this._stats, accounts: this._accounts.size, protocol: PROTOCOL } } async ready () { if (this.swarm || !this.topic) return this await initModuleSwarm(this, { keyPair: this.keyPair, topic: this.topic, protocol: PROTOCOL, onmessage: (d) => this._onGossip(d) }) return this } async close () { if (this.swarm) await this.swarm.destroy().catch(() => {}) this.swarm = null this._accounts.clear() } } module.exports = { HyperP2PCreditLedger, PROTOCOL }