This commit is contained in:
Raven Scott
2026-05-20 22:56:32 -04:00
parent b8c335adee
commit dd384eb944
321 changed files with 7484 additions and 3793 deletions
@@ -1,28 +1,39 @@
# hyper-p2p-auction-gossip
**Status:** scaffold (`0.0.0-scaffold`) · **Protocol:** `auction-gossip/v1` · **Wave:** 8
Distributed auction room over Hyperswarm gossip: open auctions, place bids, close with winner.
Auction gossip rounds.
**Category:** Applications (economy)
## Holepunch references (inspiration only)
**Composes with:** `hyper-p2p-decentralized-oracle`, `hyper-p2p-credit-ledger`
- `hyper-p2p-gossip-mesh`
**Protocol:** `auction-gossip/v1`
> This module composes on Hyperswarm/Hypercore — it does **not** re-implement upstream packages.
## When to use
## Composes with
P2P marketplaces that need bid visibility across peers on a shared topic.
- `hyper-p2p-decentralized-oracle`
## When not to use
## Planned API
Centralized auction houses or single-node simulations without `topic`.
- `constructor(opts)` — topic, optional keyPair
- `getStats()` — scaffold counters
- `ready()` — no-op until implemented
- Domain methods — throw `not implemented: scaffold` until Wave 8+ pass
## Quick start
## Layout
```js
const { HyperP2PAuctionGossip } = require('hyper-p2p-auction-gossip')
const auction = new HyperP2PAuctionGossip({ topic: 'my-auction-topic' })
await auction.ready()
auction.openAuction('lot-1', { item: 'vintage-bike' })
auction.placeBid('lot-1', 42)
await auction.close()
```
`modules/applications-economy/hyper-p2p-auction-gossip/`
## Docs
See [`modules/_shared/MODULE_SYSTEM.md`](../../_shared/MODULE_SYSTEM.md).
- [docs/api.md](docs/api.md)
- [docs/architecture.md](docs/architecture.md)
## Test
```bash
npm install && npm test
```
@@ -1,23 +1,43 @@
# hyper-p2p-auction-gossip API
# API: hyper-p2p-auction-gossip
**Status:** scaffold · **Protocol:** `auction-gossip/v1`
**Protocol:** `auction-gossip/v1`
**Export:** `{ HyperP2PAuctionGossip, PROTOCOL }`
## Class `HyperP2PAuctionGossip`
## Constructor
Scaffold stub — methods throw `not implemented: scaffold` until promoted to production tier.
`new HyperP2PAuctionGossip(opts?)`
### `constructor(opts?)`
| Option | Type | Description |
|--------|------|-------------|
| `topic` | string \| buffer | Hyperswarm topic (optional for local-only) |
| `keyPair` | keyPair | Override identity |
### `getStats()`
## Methods
Returns `{ created, errors, protocol, tier: 'scaffold' }`.
| Method | Returns | Description |
|--------|---------|-------------|
| `openAuction(auctionId, meta?)` | auction | Create open auction |
| `placeBid(auctionId, amount, meta?)` | bid | Append bid (sorted desc) |
| `closeAuction(auctionId)` | auction \| null | Close; sets `winner` to highest bid |
| `getAuction(auctionId)` | auction \| null | Lookup |
| `listOpen()` | auction[] | Active auctions |
| `ready()` | Promise\<this\> | Join swarm when `topic` set |
| `close()` | Promise\<void\> | Teardown |
| `getStats()` | object | Counters + protocol |
### `ready()`
## Events
Resolves immediately (no-op).
| Event | Payload |
|-------|---------|
| `open` | auction |
| `bid` | `{ auctionId, bid }` |
| `close` | auction |
| `remote-open` | auction |
| `remote-bid` | gossip payload |
| `remote-close` | auction |
## Wire (planned)
## Errors
| Message | Direction | Notes |
|---------|-----------|-------|
| TBD | gossip | Defined in implementation pass |
- `auction already open` / `auction not open`
- `amount must be a positive number`
- Validation via `assertNonEmpty` on ids
@@ -1,15 +1,21 @@
# hyper-p2p-auction-gossip architecture
# Architecture: hyper-p2p-auction-gossip
**Tier:** scaffold · **Category:** `applications-economy`
**Category:** `applications-economy` · **Protocol:** `auction-gossip/v1`
## Role
Auction gossip rounds.
Gossip-synchronized auction state: each peer keeps a map of auctions and bid lists; highest bid wins on close.
## Wire messages
| type | Fields |
|------|--------|
| `auction-open` | `auction` (id, meta, status, bids, openedBy, openedAt) |
| `auction-bid` | `auctionId`, `bid` (amount, bidder, meta, at) |
| `auction-close` | `auctionId`, `winner`, `closedAt` |
Uses `../../_shared/p2p-bare.js` (`initModuleSwarm`, `gossipSend`).
## Composition
Uses `../../_shared/p2p-bare.js` for Hyperswarm + Protomux when implemented. Does **not** duplicate Holepunch core storage/transport.
## Holepunch boundary
Inspiration: n/a
Pairs with `hyper-p2p-credit-ledger` for settlement and `hyper-p2p-marketplace-listing` for item catalog.
@@ -3,6 +3,9 @@ const { HyperP2PAuctionGossip } = require('../index.js')
async function main () {
const m = new HyperP2PAuctionGossip()
console.log('[scaffold]', m.getStats())
m.openAuction('demo', { item: 'test' })
m.placeBid('demo', 5)
console.log('[auction-gossip]', m.closeAuction('demo'))
await m.close()
}
main().catch(console.error)
@@ -1,7 +1,8 @@
require('bare-process/global')
const EventEmitter = require('bare-events')
const b4a = require('b4a')
const { assertNonEmpty } = require('../../_shared/lib/errors.js')
const { attachGossip, sendGossip } = require('../../_shared/storage-gossip-base.js')
const { initModuleSwarm, gossipSend } = require('../../_shared/p2p-bare.js')
const PROTOCOL = 'auction-gossip/v1'
@@ -10,50 +11,126 @@ class HyperP2PAuctionGossip extends EventEmitter {
super()
this.topic = opts.topic || null
this.keyPair = opts.keyPair || require('hypercore-crypto').keyPair()
this._store = new Map()
this._stats = { ops: 0, gossipIn: 0, gossipOut: 0 }
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
}
put (key, value) {
assertNonEmpty(key, 'key')
this._store.set(key, value)
this._stats.ops++
sendGossip(this, { type: 'auction-gossip-sync', key, value })
this.emit('update', { key, value })
return true
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
}
get (key) { return this._store.get(key) }
delete (key) {
const ok = this._store.delete(key)
if (ok) sendGossip(this, { type: 'auction-gossip-sync', key, value: null })
return ok
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
}
entries () { return [...this._store.entries()] }
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
}
_onGossip (d) {
if (!d || d.type !== 'auction-gossip-sync') return
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 (d.key !== undefined) {
if (d.value === null) this._store.delete(d.key)
else this._store.set(d.key, d.value)
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, size: this._store.size, protocol: PROTOCOL } }
getStats () {
return {
...this._stats,
auctions: this._auctions.size,
open: this.listOpen().length,
protocol: PROTOCOL
}
}
async ready () {
if (this.swarm || !this.topic) return this
await attachGossip(this, { keyPair: this.keyPair, topic: this.topic, protocol: PROTOCOL, onmessage: (d) => this._onGossip(d) })
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()
}
}
@@ -4,23 +4,36 @@ const { HyperP2PAuctionGossip, PROTOCOL } = require('../index.js')
test('exports', (t) => {
t.ok(HyperP2PAuctionGossip)
t.ok(PROTOCOL)
t.is(PROTOCOL, 'auction-gossip/v1')
})
test('basic operation', async (t) => {
test('open bid close', async (t) => {
const m = new HyperP2PAuctionGossip()
m.put('k', 1); t.is(m.get('k'), 1)
m.openAuction('a1', { item: 'widget' })
m.placeBid('a1', 10)
m.placeBid('a1', 20)
const closed = m.closeAuction('a1')
t.is(closed.winner.amount, 20)
await m.close()
})
test('validation', async (t) => {
const m = new HyperP2PAuctionGossip()
try { m.put(null, 1) } catch (e) { t.ok(e) }
try { m.placeBid('missing', 1) } catch (e) { t.ok(e) }
await m.close()
})
test('remote bid via gossip', async (t) => {
const m = new HyperP2PAuctionGossip()
m.openAuction('a1')
m._onGossip({ type: 'auction-bid', auctionId: 'a1', bid: { amount: 5, bidder: 'remote', at: 1 } })
t.is(m.getAuction('a1').bids.length, 1)
await m.close()
})
test('getStats', async (t) => {
const m = new HyperP2PAuctionGossip()
t.ok(m.getStats().protocol)
m.openAuction('x')
t.is(m.getStats().opened, 1)
await m.close()
})