From f29eab893dc87bc1b480d234ec005cbe063286d1 Mon Sep 17 00:00:00 2001 From: Chris Hibbert Date: Thu, 4 May 2023 14:19:41 -0700 Subject: [PATCH] feat(auction): auction publish all bids off-chain --- .../inter-protocol/src/auction/auctionBook.js | 57 +++++++++++++- .../inter-protocol/src/auction/auctioneer.js | 11 ++- .../inter-protocol/src/auction/offerBook.js | 20 +++++ .../snapshots/test-auctionContract.js.md | 33 +++++++- .../snapshots/test-auctionContract.js.snap | Bin 1693 -> 1928 bytes .../test/auction/test-auctionBook.js | 74 +++++++++++++++--- .../test/auction/test-auctionContract.js | 59 +++++++++++++- 7 files changed, 233 insertions(+), 21 deletions(-) diff --git a/packages/inter-protocol/src/auction/auctionBook.js b/packages/inter-protocol/src/auction/auctionBook.js index 4f9f7ee3df3..b466ca3651f 100644 --- a/packages/inter-protocol/src/auction/auctionBook.js +++ b/packages/inter-protocol/src/auction/auctionBook.js @@ -101,6 +101,29 @@ export const makeOfferSpecShape = (bidBrand, collateralBrand) => { * @property {Amount<'nat'> | null} collateralAvailable The amount of collateral remaining */ +/** + * @typedef {object} ScaledBidData + * + * @property {Ratio} bidScaling + * @property {Amount<'nat'>} wanted + * @property {Boolean} exitAfterBuy + */ + +/** + * @typedef {object} PricedBidData + * + * @property {Ratio} price + * @property {Amount<'nat'>} wanted + * @property {Boolean} exitAfterBuy + */ + +/** + * @typedef {object} BidDataNotification + * + * @property {Array} scaledBids + * @property {Array} pricedBids + */ + /** * @param {Baggage} baggage * @param {ZCF} zcf @@ -120,6 +143,7 @@ export const prepareAuctionBook = (baggage, zcf, makeRecorderKit) => { priceAuthority: M.any(), updatingOracleQuote: M.any(), bookDataKit: M.any(), + bidDataKit: M.any(), priceBook: M.any(), scaledBidBook: M.any(), startCollateral: M.any(), @@ -137,9 +161,9 @@ export const prepareAuctionBook = (baggage, zcf, makeRecorderKit) => { * @param {Brand<'nat'>} bidBrand * @param {Brand<'nat'>} collateralBrand * @param {PriceAuthority} pAuthority - * @param {StorageNode} node + * @param {Array} nodes */ - (bidBrand, collateralBrand, pAuthority, node) => { + (bidBrand, collateralBrand, pAuthority, nodes) => { assertAllDefined({ bidBrand, collateralBrand, pAuthority }); const zeroBid = makeEmpty(bidBrand); const zeroRatio = makeRatioFromAmounts( @@ -166,12 +190,19 @@ export const prepareAuctionBook = (baggage, zcf, makeRecorderKit) => { collateralBrand, ); + const [scheduleNode, bidsNode] = nodes; const bookDataKit = makeRecorderKit( - node, + scheduleNode, /** @type {import('@agoric/zoe/src/contractSupport/recorder.js').TypedMatcher} */ ( M.any() ), ); + const bidDataKit = makeRecorderKit( + bidsNode, + /** @type {import('@agoric/zoe/src/contractSupport/recorder.js').TypedMatcher} */ ( + M.any() + ), + ); return { collateralBrand, @@ -185,6 +216,7 @@ export const prepareAuctionBook = (baggage, zcf, makeRecorderKit) => { updatingOracleQuote: zeroRatio, bookDataKit, + bidDataKit, priceBook, scaledBidBook, @@ -381,6 +413,7 @@ export const prepareAuctionBook = (baggage, zcf, makeRecorderKit) => { } else { trace('added Offer ', price, stillWant.value); priceBook.add(seat, price, stillWant, exitAfterBuy); + helper.publishBidData(); } helper.publishBookData(); @@ -435,10 +468,20 @@ export const prepareAuctionBook = (baggage, zcf, makeRecorderKit) => { seat.exit(); } else { scaledBidBook.add(seat, bidScaling, stillWant, exitAfterBuy); + helper.publishBidData(); } helper.publishBookData(); }, + publishBidData() { + const { state } = this; + // XXX should this be compressed somewhat? lots of redundant brands. + state.bidDataKit.recorder.write({ + scaledBids: state.scaledBidBook.publishOffers(), + // @ts-expect-error how to convince TS these ratios are non-null? + pricedBids: state.priceBook.publishOffers(), + }); + }, publishBookData() { const { state } = this; @@ -602,6 +645,7 @@ export const prepareAuctionBook = (baggage, zcf, makeRecorderKit) => { } facets.helper.publishBookData(); + facets.helper.publishBidData(); }, getCurrentPrice() { return this.state.curAuctionPrice; @@ -691,18 +735,22 @@ export const prepareAuctionBook = (baggage, zcf, makeRecorderKit) => { getDataUpdates() { return this.state.bookDataKit.subscriber; }, + getBidDataUpdates() { + return this.state.bidDataKit.subscriber; + }, getPublicTopics() { return { bookData: makeRecorderTopic( 'Auction schedule', this.state.bookDataKit, ), + bids: makeRecorderTopic('Auction Bids', this.state.bidDataKit), }; }, }, }, { - finish: ({ state }) => { + finish: ({ state, facets }) => { const { collateralBrand, bidBrand, priceAuthority } = state; assertAllDefined({ collateralBrand, bidBrand, priceAuthority }); void E.when( @@ -735,6 +783,7 @@ export const prepareAuctionBook = (baggage, zcf, makeRecorderKit) => { }); }, ); + facets.helper.publishBidData(); }, stateShape: AuctionBookStateShape, }, diff --git a/packages/inter-protocol/src/auction/auctioneer.js b/packages/inter-protocol/src/auction/auctioneer.js index 5962458b0ac..1147b1112a7 100644 --- a/packages/inter-protocol/src/auction/auctioneer.js +++ b/packages/inter-protocol/src/auction/auctioneer.js @@ -653,6 +653,9 @@ export const start = async (zcf, privateArgs, baggage) => { getBookDataUpdates(brand) { return books.get(brand).getDataUpdates(); }, + getBidDataUpdates(brand) { + return books.get(brand).getBidDataUpdates(); + }, getPublicTopics(brand) { if (brand) { return books.get(brand).getPublicTopics(); @@ -681,13 +684,17 @@ export const start = async (zcf, privateArgs, baggage) => { const bookId = `book${bookCounter}`; bookCounter += 1; - const bNode = await E(privateArgs.storageNode).makeChildNode(bookId); + const bNode = E(privateArgs.storageNode).makeChildNode(bookId); + const nodes = await Promise.all([ + E(bNode).makeChildNode('schedule'), + E(bNode).makeChildNode('bids'), + ]); const newBook = await makeAuctionBook( brands.Bid, brand, priceAuthority, - bNode, + nodes, ); // These three store.init() calls succeed or fail atomically diff --git a/packages/inter-protocol/src/auction/offerBook.js b/packages/inter-protocol/src/auction/offerBook.js index 2f8adb60075..3920b4e5bc5 100644 --- a/packages/inter-protocol/src/auction/offerBook.js +++ b/packages/inter-protocol/src/auction/offerBook.js @@ -93,6 +93,16 @@ export const prepareScaledBidBook = baggage => const { records } = this.state; return [...records.entries(M.gte(toBidScalingComparator(bidScaling)))]; }, + publishOffers() { + const { records } = this.state; + return [...records.values()].map(r => { + return harden({ + bidScaling: r.bidScaling, + wanted: r.wanted, + exitAfterBuy: r.exitAfterBuy, + }); + }); + }, hasOrders() { const { records } = this.state; return records.getSize() > 0; @@ -186,6 +196,16 @@ export const preparePriceBook = baggage => const { records } = this.state; return [...records.entries(M.gte(toPartialOfferKey(price)))]; }, + publishOffers() { + const { records } = this.state; + return [...records.values()].map(r => { + return harden({ + price: r.price, + wanted: r.wanted, + exitAfterBuy: r.exitAfterBuy, + }); + }); + }, hasOrders() { const { records } = this.state; return records.getSize() > 0; diff --git a/packages/inter-protocol/test/auction/snapshots/test-auctionContract.js.md b/packages/inter-protocol/test/auction/snapshots/test-auctionContract.js.md index af1821265f8..ad4b8d172b7 100644 --- a/packages/inter-protocol/test/auction/snapshots/test-auctionContract.js.md +++ b/packages/inter-protocol/test/auction/snapshots/test-auctionContract.js.md @@ -13,7 +13,38 @@ Generated by [AVA](https://avajs.dev). [ [ - 'published.auction.book0', + 'published.auction.book0.bids', + { + pricedBids: [ + { + exitAfterBuy: false, + price: { + denominator: { + brand: { + iface: 'Alleged: Collateral brand', + }, + value: 200n, + }, + numerator: { + brand: { + iface: 'Alleged: Bid brand', + }, + value: 250n, + }, + }, + wanted: { + brand: { + iface: 'Alleged: Collateral brand', + }, + value: 200n, + }, + }, + ], + scaledBids: [], + }, + ], + [ + 'published.auction.book0.schedule', { collateralAvailable: { brand: { diff --git a/packages/inter-protocol/test/auction/snapshots/test-auctionContract.js.snap b/packages/inter-protocol/test/auction/snapshots/test-auctionContract.js.snap index ff3d24e68e8c1614a86aa3ae6c74ee8f4e938035..f1233516ad3d6caa25306b66f956cf9b25c1d878 100644 GIT binary patch literal 1928 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 makeManualPriceAuthority({ @@ -69,12 +71,10 @@ const assembleAuctionBook = async basics => { const makeAuctionBook = prepareAuctionBook(baggage, zcf, makeRecorderKit); const mockChainStorage = makeMockChainStorageRoot(); - const book = await makeAuctionBook( - moolaKit.brand, - simoleanKit.brand, - pa, - mockChainStorage.makeChildNode('thisBook'), - ); + const book = await makeAuctionBook(moolaKit.brand, simoleanKit.brand, pa, [ + mockChainStorage.makeChildNode('schedule'), + mockChainStorage.makeChildNode('bids'), + ]); return { pa, book }; }; @@ -126,15 +126,24 @@ test('simple addOffer', async t => { ); const { pa, book } = await assembleAuctionBook(basics); pa.setPrice(makeRatioFromAmounts(moola(11n), simoleans(10n))); + const bidTracker = await subscriptionTracker( + t, + subscribeEach(book.getBidDataUpdates()), + ); + await bidTracker.assertInitial({ + pricedBids: [], + scaledBids: [], + }); await eventLoopIteration(); book.addAssets(AmountMath.make(simoleanKit.brand, 123n), donorSeat); book.lockOraclePriceForRound(); book.setStartingRate(makeRatio(50n, moolaKit.brand, 100n)); + const tenFor100 = makeRatioFromAmounts(moola(10n), simoleans(100n)); book.addOffer( harden({ - offerPrice: makeRatioFromAmounts(moola(10n), simoleans(100n)), + offerPrice: tenFor100, maxBuy: simoleans(50n), }), zcfSeat, @@ -143,6 +152,15 @@ test('simple addOffer', async t => { t.true(book.hasOrders()); book.exitAllSeats(); + await bidTracker.assertChange({ + pricedBids: { + 0: { + price: tenFor100, + exitAfterBuy: false, + wanted: simoleans(50n), + }, + }, + }); t.false(book.hasOrders()); }); @@ -151,6 +169,14 @@ test('getOffers to a price limit', async t => { const basics = await setupBasics(); const { moolaKit, moola, simoleanKit, simoleans, zcf, zoe } = basics; const { pa, book } = await assembleAuctionBook(basics); + const bidTracker = await subscriptionTracker( + t, + subscribeEach(book.getBidDataUpdates()), + ); + await bidTracker.assertInitial({ + pricedBids: [], + scaledBids: [], + }); const donorSeat = await makeSeatWithAssets( zoe, @@ -174,9 +200,10 @@ test('getOffers to a price limit', async t => { book.lockOraclePriceForRound(); book.setStartingRate(makeRatio(50n, moolaKit.brand, 100n)); + const tenPercent = makeRatioFromAmounts(moola(10n), moola(100n)); book.addOffer( harden({ - offerBidScaling: makeRatioFromAmounts(moola(10n), moola(100n)), + offerBidScaling: tenPercent, maxBuy: simoleans(50n), }), zcfSeat, @@ -184,6 +211,15 @@ test('getOffers to a price limit', async t => { ); t.true(book.hasOrders()); + await bidTracker.assertChange({ + scaledBids: { + 0: { + bidScaling: tenPercent, + exitAfterBuy: false, + wanted: simoleans(50n), + }, + }, + }); book.exitAllSeats(); t.false(book.hasOrders()); @@ -250,6 +286,14 @@ test('getOffers w/discount', async t => { book.lockOraclePriceForRound(); book.setStartingRate(makeRatio(50n, moolaKit.brand, 100n)); + const bidTracker = await subscriptionTracker( + t, + subscribeEach(book.getBidDataUpdates()), + ); + await bidTracker.assertInitial({ + pricedBids: [], + scaledBids: [], + }); const zcfSeat = await makeSeatWithAssets( zoe, @@ -259,14 +303,24 @@ test('getOffers w/discount', async t => { moolaKit, ); + const tenPercent = makeRatioFromAmounts(moola(10n), moola(100n)); book.addOffer( harden({ - offerBidScaling: makeRatioFromAmounts(moola(10n), moola(100n)), + offerBidScaling: tenPercent, maxBuy: simoleans(50n), }), zcfSeat, true, ); + await bidTracker.assertChange({ + scaledBids: { + 0: { + bidScaling: tenPercent, + exitAfterBuy: false, + wanted: simoleans(50n), + }, + }, + }); t.true(book.hasOrders()); }); diff --git a/packages/inter-protocol/test/auction/test-auctionContract.js b/packages/inter-protocol/test/auction/test-auctionContract.js index e42b8db4f3c..7ac5353a44e 100644 --- a/packages/inter-protocol/test/auction/test-auctionContract.js +++ b/packages/inter-protocol/test/auction/test-auctionContract.js @@ -281,6 +281,11 @@ const makeAuctionDriver = async (t, params = defaultParams) => { subscriptionTracker(t, subscribeEach(subscription)), ); }, + getBidTracker(brand) { + return E.when(E(publicFacet).getBidDataUpdates(brand), subscription => + subscriptionTracker(t, subscribeEach(subscription)), + ); + }, getReserveBalance(keyword) { const reserveCF = E.get(reserveKit).creatorFacet; return E.get(E(reserveCF).getAllocations())[keyword]; @@ -1112,6 +1117,10 @@ test.serial('multiple collaterals', async t => { asset, asset.make(500n), ); + const collatBidTracker = await driver.getBidTracker(collateral.brand); + await collatBidTracker.assertInitial({ pricedBids: [], scaledBids: [] }); + const assetBidTracker = await driver.getBidTracker(asset.brand); + await assetBidTracker.assertInitial({ pricedBids: [], scaledBids: [] }); t.is(await E(collatLiqSeat).getOfferResult(), 'deposited'); t.is(await E(assetLiqSeat).getOfferResult(), 'deposited'); @@ -1124,36 +1133,76 @@ test.serial('multiple collaterals', async t => { ); // offers 290 for up to 300 at 1.1 * .875, so will trigger at the first discount + const price = makeRatioFromAmounts(bid.make(950n), collateral.make(1000n)); const bidderSeat1C = await driver.bidForCollateralSeat( bid.make(265n), collateral.make(300n), - makeRatioFromAmounts(bid.make(950n), collateral.make(1000n)), + price, ); t.is(await E(bidderSeat1C).getOfferResult(), 'Your bid has been accepted'); + collatBidTracker.assertChange({ + pricedBids: { + 0: { + exitAfterBuy: false, + wanted: collateral.make(300n), + price, + }, + }, + }); // offers up to 500 for 2000 at 1.1 * 75%, so will trigger at second discount step + const scale2C = makeRatioFromAmounts(bid.make(75n), bid.make(100n)); const bidderSeat2C = await driver.bidForCollateralSeat( bid.make(500n), collateral.make(2000n), - makeRatioFromAmounts(bid.make(75n), bid.make(100n)), + scale2C, ); t.is(await E(bidderSeat2C).getOfferResult(), 'Your bid has been accepted'); + collatBidTracker.assertChange({ + scaledBids: { + 0: { + exitAfterBuy: false, + wanted: collateral.make(2000n), + bidScaling: scale2C, + }, + }, + }); // offers 50 for 200 at .25 * 50% discount, so triggered at third step + const scale1A = makeRatioFromAmounts(bid.make(50n), bid.make(100n)); const bidderSeat1A = await driver.bidForCollateralSeat( bid.make(23n), asset.make(200n), - makeRatioFromAmounts(bid.make(50n), bid.make(100n)), + scale1A, ); t.is(await E(bidderSeat1A).getOfferResult(), 'Your bid has been accepted'); + assetBidTracker.assertChange({ + scaledBids: { + 0: { + exitAfterBuy: false, + wanted: asset.make(200n), + bidScaling: scale1A, + }, + }, + }); // offers 100 for 300 at .25 * 33%, so triggered at fourth step + const price2A = makeRatioFromAmounts(bid.make(100n), asset.make(1000n)); const bidderSeat2A = await driver.bidForCollateralSeat( bid.make(19n), asset.make(300n), - makeRatioFromAmounts(bid.make(100n), asset.make(1000n)), + price2A, ); t.is(await E(bidderSeat2A).getOfferResult(), 'Your bid has been accepted'); + assetBidTracker.assertChange({ + pricedBids: { + 0: { + exitAfterBuy: false, + wanted: asset.make(300n), + price: price2A, + }, + }, + }); const schedules = await driver.getSchedule(); t.is(schedules.nextAuctionSchedule?.startTime.absValue, 170n); @@ -1161,6 +1210,8 @@ test.serial('multiple collaterals', async t => { await driver.advanceTo(150n); await driver.advanceTo(170n, 'wait'); await driver.advanceTo(175n); + assetBidTracker.assertChange({}); + collatBidTracker.assertChange({}); t.true(await E(bidderSeat1C).hasExited());