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 = 'auction-gossip/v1' class HyperP2PAuctionGossip 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._auctions = new Map() this._stats = { opened: 0, bids: 0, closed: 0, gossipIn: 0, gossipOut: 0 } this.swarm = null this._peerMsgs = null } openAuction (auctionId, meta = {}) { assertNonEmpty(auctionId, 'auctionId') if (this._auctions.has(auctionId)) throw new Error('auction already open') const auction = { id: auctionId, meta, status: 'open', bids: [], openedAt: Date.now(), openedBy: this.peerHex } this._auctions.set(auctionId, auction) this._stats.opened++ this._gossip({ type: 'auction-open', auction }) this.emit('open', auction) return auction } placeBid (auctionId, amount, meta = {}) { assertNonEmpty(auctionId, 'auctionId') if (typeof amount !== 'number' || amount <= 0) throw new Error('amount must be a positive number') const auction = this._auctions.get(auctionId) if (!auction || auction.status !== 'open') throw new Error('auction not open') const bid = { amount, bidder: this.peerHex, meta, at: Date.now() } auction.bids.push(bid) auction.bids.sort((a, b) => b.amount - a.amount) this._stats.bids++ this._gossip({ type: 'auction-bid', auctionId, bid }) this.emit('bid', { auctionId, bid }) return bid } closeAuction (auctionId) { const auction = this._auctions.get(auctionId) if (!auction) return null auction.status = 'closed' auction.closedAt = Date.now() auction.winner = auction.bids[0] || null this._stats.closed++ this._gossip({ type: 'auction-close', auctionId, winner: auction.winner, closedAt: auction.closedAt }) this.emit('close', auction) return auction } getAuction (auctionId) { return this._auctions.get(auctionId) || null } listOpen () { return [...this._auctions.values()].filter((a) => a.status === 'open') } _gossip (payload) { if (!this._peerMsgs) return gossipSend(this, payload) this._stats.gossipOut++ } _onGossip (data) { if (!data || !data.type) return this._stats.gossipIn++ if (data.type === 'auction-open' && data.auction) { this._auctions.set(data.auction.id, data.auction) this.emit('remote-open', data.auction) } if (data.type === 'auction-bid' && data.auctionId && data.bid) { const a = this._auctions.get(data.auctionId) if (a && a.status === 'open') { a.bids.push(data.bid) a.bids.sort((x, y) => y.amount - x.amount) this.emit('remote-bid', data) } } if (data.type === 'auction-close' && data.auctionId) { const a = this._auctions.get(data.auctionId) if (a) { a.status = 'closed' a.winner = data.winner a.closedAt = data.closedAt this.emit('remote-close', a) } } } getStats () { return { ...this._stats, auctions: this._auctions.size, open: this.listOpen().length, 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._auctions.clear() } } module.exports = { HyperP2PAuctionGossip, PROTOCOL }