Files
modules/applications-economy/hyper-p2p-auction-gossip/index.js
T
Raven ScottandCursor 44b8a80907 Expand economy, autobase, network, observability, and scheduling modules.
Add auction withdraw/cancel, marketplace search helpers, update gossip history, autobase fork/lease/indexer/view APIs, link-probe and circuit-loom utilities, trace trees, pheromone ranking, and scheduling/measurement helpers with tests.

Co-authored-by: Cursor <[email protected]>
2026-05-21 01:09:29 -04:00

175 lines
5.1 KiB
JavaScript

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')
}
listAuctionIds () { return [...this._auctions.keys()] }
highestBid (auctionId) {
const a = this._auctions.get(auctionId)
return a && a.bids.length ? a.bids[0] : null
}
withdrawBid (auctionId, bidder = null) {
const a = this._auctions.get(auctionId)
if (!a || a.status !== 'open') return 0
const who = bidder || this.peerHex
const before = a.bids.length
a.bids = a.bids.filter((b) => b.bidder !== who)
a.bids.sort((x, y) => y.amount - x.amount)
const removed = before - a.bids.length
if (removed) {
this._gossip({ type: 'auction-withdraw', auctionId, bidder: who })
this.emit('withdraw', { auctionId, bidder: who, removed })
}
return removed
}
cancelAuction (auctionId) {
const a = this._auctions.get(auctionId)
if (!a || a.status !== 'open') return false
this._auctions.delete(auctionId)
this._gossip({ type: 'auction-cancel', auctionId })
this.emit('cancel', { auctionId })
return true
}
_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)
}
}
if (data.type === 'auction-withdraw' && data.auctionId) {
this.withdrawBid(data.auctionId, data.bidder)
}
if (data.type === 'auction-cancel' && data.auctionId) {
this._auctions.delete(data.auctionId)
}
}
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 }