UNPKG

@agoric/zoe

Version:

Zoe: the Smart Contract Framework for Offer Enforcement

166 lines (142 loc) 5.24 kB
import { Fail } from '@endo/errors'; import { E } from '@endo/eventual-send'; import { mustMatch } from '@endo/patterns'; import { Far } from '@endo/marshal'; import { TimeMath, RelativeTimeShape } from '@agoric/time'; // Eventually will be importable from '@agoric/zoe-contract-support' import { defaultAcceptanceMsg, assertIssuerKeywords, assertProposalShape, } from '../../contractSupport/index.js'; import * as secondPriceLogic from './secondPriceLogic.js'; import * as firstPriceLogic from './firstPriceLogic.js'; import { assertBidSeat } from './assertBidSeat.js'; const FIRST_PRICE = 'first-price'; const SECOND_PRICE = 'second-price'; /** * NOT TO BE USED IN PRODUCTION CODE. BIDS ARE PUBLIC. An auction * contract in which the seller offers an Asset for sale, and states a * minimum price. The auction closes at the deadline specified by the * timeAuthority, bidDuration, winnerPriceOption parameters in the terms provided by * the creator of the contract instance. * Winner price option can be `first-price` or `second-price`, default to `second-price`. * * startInstance() specifies the issuers and the terms. An invitation * for the seller is returned as the creatorInvitation. The seller's * offer should look like { give: { Asset: asset }, want: { Ask: * minimumBidAmount}} The asset can be non-fungible, but the Ask * amount should be of a fungible brand. The bidder invitations can be * made by calling makeBidInvitation on the object returned from the * seller's offer. Each bidder can submit an offer: { give: { Bid: * null } want: { Asset: null } }. * * @param {ZCF<{ * timeAuthority: import('@agoric/time').TimerService, * winnerPriceOption?: FIRST_PRICE | SECOND_PRICE, * bidDuration: bigint, * }>} zcf */ const start = zcf => { const { timeAuthority, winnerPriceOption = SECOND_PRICE, bidDuration, } = zcf.getTerms(); mustMatch(bidDuration, RelativeTimeShape); winnerPriceOption === FIRST_PRICE || winnerPriceOption === SECOND_PRICE || Fail`Only first and second price auctions are supported`; let sellSeat; let isTimerStarted = false; let isClosed = false; let closesAfter = null; const bidSeats = []; const priceLogic = winnerPriceOption === FIRST_PRICE ? firstPriceLogic : secondPriceLogic; // seller will use 'Asset' and 'Ask'. buyer will use 'Asset' and 'Bid' assertIssuerKeywords(zcf, harden(['Asset', 'Ask'])); const startWakeupTimerIfNeeded = async () => { if (isTimerStarted) { return; } // XXX toggle flag before `await` to avoid race-condition of 2 consecutive bids isTimerStarted = true; const currentTs = await E(timeAuthority).getCurrentTimestamp(); closesAfter = TimeMath.addAbsRel(currentTs, bidDuration); E(timeAuthority) .setWakeup( closesAfter, Far('wakeObj', { wake: () => { isClosed = true; priceLogic.calcWinnerAndClose(zcf, sellSeat, bidSeats); }, }), ) .catch(err => { console.error( `Could not schedule the close of the auction at the 'closesAfter' deadline ${closesAfter} using this timer ${timeAuthority}`, ); console.error(err); throw err; }); }; const getCurrentBids = () => { if (winnerPriceOption !== FIRST_PRICE) { return 'Bids are hidden for "second price" auctions'; } const bidBrand = sellSeat.getProposal().want.Ask.brand; return bidSeats.map(seat => seat.getAmountAllocated('Bid', bidBrand)); }; const getSessionDetails = () => { const sellerProposal = sellSeat.getProposal(); return harden({ auctionedAssets: sellerProposal.give.Asset, minimumBid: sellerProposal.want.Ask, winnerPriceOption, closesAfter, bidDuration, timeAuthority, bids: getCurrentBids(), }); }; const makeBidInvitation = () => { /** @type {OfferHandler} */ const performBid = seat => { assert(!isClosed, 'Auction session is closed, no more bidding'); assertProposalShape(seat, { give: { Bid: null }, want: { Asset: null }, }); assertBidSeat(zcf, sellSeat, seat); void startWakeupTimerIfNeeded(); bidSeats.push(seat); return defaultAcceptanceMsg; }; const customDetails = getSessionDetails(); return zcf.makeInvitation(performBid, 'bid', customDetails); }; const sell = seat => { assertProposalShape(seat, { give: { Asset: null }, want: { Ask: null }, // The auction is not over until the deadline according to the // provided timer. The seller cannot exit beforehand. exit: { waived: null }, }); // Save the seat for when the auction closes. sellSeat = seat; // The bid invitations can only be sent out after the assets to be // auctioned are escrowed. return Far('offerResult', { makeBidInvitation, getSessionDetails }); }; const publicFacet = Far('auctioneerPublicFacet', { getCurrentBids, getSessionDetails, }); const creatorInvitation = zcf.makeInvitation(sell, 'sellAssets'); return harden({ creatorInvitation, publicFacet }); }; harden(start); export { start, FIRST_PRICE, SECOND_PRICE };