diff --git a/packages/zoe/exported.js b/packages/zoe/exported.js index a07c1a3bda2..b2edb022ab8 100644 --- a/packages/zoe/exported.js +++ b/packages/zoe/exported.js @@ -2,5 +2,7 @@ import './src/contractFacet/types'; import './src/zoeService/types'; import './src/contractSupport/types'; import './src/types'; +import './tools/types'; +import '@agoric/notifier/exports'; import '@agoric/ertp/exported'; import '@agoric/store/exported'; diff --git a/packages/zoe/tools/priceAuthorityRegistry.js b/packages/zoe/tools/priceAuthorityRegistry.js new file mode 100644 index 00000000000..2efb4cdb99c --- /dev/null +++ b/packages/zoe/tools/priceAuthorityRegistry.js @@ -0,0 +1,139 @@ +// @ts-check + +import { E } from '@agoric/eventual-send'; +import { makeStore } from '@agoric/store'; +import { assert, details } from '@agoric/assert'; + +import '../exported'; + +/** + * @typedef {Object} Deleter + * @property {() => void} delete + */ + +/** + * @typedef {Object} PriceAuthorityRegistryAdmin + * @property {(pa: ERef, assetBrand: Brand, priceBrand: Brand) + * => Deleter} registerPriceAuthority Add a unique price authority for a given + * asset/price pair + */ + +/** + * @typedef {Object} PriceAuthorityRegistry A price authority that is a facade + * for other backing price authorities registered for a given asset and price + * brand + * @property {PriceAuthority} priceAuthority + * @property {PriceAuthorityRegistryAdmin} adminFacet + */ + +/** + * @returns {PriceAuthorityRegistry} + */ +export const makePriceAuthorityRegistry = () => { + /** + * @typedef {Object} PriceAuthorityRecord A record indicating a registered + * price authority + * @property {ERef} priceAuthority + */ + + /** @type {Store>} */ + const assetToPriceStore = makeStore('assetBrand'); + + /** + * @param {Brand} assetBrand + * @param {Brand} priceBrand + */ + const lookup = (assetBrand, priceBrand) => { + const priceStore = assetToPriceStore.get(assetBrand); + return priceStore.get(priceBrand); + }; + + /** + * This PriceAuthority is just a wrapper for multiple registered + * PriceAuthorities. + * + * @type {PriceAuthority} + */ + const priceAuthority = { + async getQuoteIssuer(assetBrand, priceBrand) { + const record = lookup(assetBrand, priceBrand); + return E(record.priceAuthority).getQuoteIssuer(assetBrand, priceBrand); + }, + async getInputPrice(amountIn, brandOut) { + const record = lookup(amountIn.brand, brandOut); + return E(record.priceAuthority).getInputPrice(amountIn, brandOut); + }, + async getOutputPrice(amountOut, brandIn) { + const record = lookup(brandIn, amountOut.brand); + return E(record.priceAuthority).getOutputPrice(amountOut, brandIn); + }, + async getPriceNotifier(assetBrand, priceBrand) { + const record = lookup(assetBrand, priceBrand); + return E(record.priceAuthority).getPriceNotifier(assetBrand, priceBrand); + }, + async priceAtTime(timer, deadline, assetAmount, priceBrand) { + const record = lookup(assetAmount.brand, priceBrand); + return E(record.priceAuthority).priceAtTime( + timer, + deadline, + assetAmount, + priceBrand, + ); + }, + async priceWhenLT(assetAmount, priceLimit) { + const record = lookup(assetAmount.brand, priceLimit.brand); + return E(record.priceAuthority).priceWhenLT(assetAmount, priceLimit); + }, + async priceWhenLTE(assetAmount, priceLimit) { + const record = lookup(assetAmount.brand, priceLimit.brand); + return E(record.priceAuthority).priceWhenLTE(assetAmount, priceLimit); + }, + async priceWhenGTE(assetAmount, priceLimit) { + const record = lookup(assetAmount.brand, priceLimit.brand); + return E(record.priceAuthority).priceWhenGT(assetAmount, priceLimit); + }, + async priceWhenGT(assetAmount, priceLimit) { + const record = lookup(assetAmount.brand, priceLimit.brand); + return E(record.priceAuthority).priceWhenGT(assetAmount, priceLimit); + }, + }; + + /** @type {PriceAuthorityRegistryAdmin} */ + const adminFacet = { + registerPriceAuthority(pa, assetBrand, priceBrand) { + /** @type {Store} */ + let priceStore; + if (assetToPriceStore.has(assetBrand)) { + priceStore = assetToPriceStore.get(assetBrand); + } else { + priceStore = makeStore('priceBrand'); + assetToPriceStore.init(assetBrand, priceStore); + } + + // Put a box around the authority so that we can be ensured the deleter + // won't delete the wrong thing. + const record = { + priceAuthority: pa, + }; + + // Set up the record. + priceStore.init(priceBrand, harden(record)); + + return harden({ + delete() { + assert.equal( + priceStore.has(priceBrand) && priceStore.get(priceBrand), + record, + details`Price authority already dropped`, + ); + priceStore.delete(priceBrand); + }, + }); + }, + }; + + return harden({ + priceAuthority, + adminFacet, + }); +}; diff --git a/packages/zoe/tools/types.js b/packages/zoe/tools/types.js new file mode 100644 index 00000000000..bb053696180 --- /dev/null +++ b/packages/zoe/tools/types.js @@ -0,0 +1,42 @@ +/** + * @typedef {Object} PriceQuote + * @property {Payment} quotePayment The quote wrapped as a payment + * @property {Amount} quoteAmount Amount of `quotePayment` (`quoteIssuer.getAmountOf(quotePayment)`) + */ + +/** + * @typedef {Object} PriceQuoteValue An individual quote's value + * @property {Amount} assetAmount The amount of the asset being quoted + * @property {Amount} price The quoted price for the `assetAmount` + * @property {TimerService} timer The service that gave the `timestamp` + * @property {number} timestamp A timestamp for the quote according to `timer` + * @property {any=} conditions Additional conditions for the quote + */ + +/** + * @typedef {Object} PriceAuthority An object that mints PriceQuotes and handles + * triggers and notifiers for changes in the price + * @property {(assetBrand: Brand, priceBrand: Brand) => ERef} + * getQuoteIssuer Get the ERTP issuer of PriceQuotes + * @property {(amountIn: Amount, brandOut: Brand) => Promise} + * getInputPrice calculate the amount of brandOut that will be returned if the + * amountIn is sold at the current price + * @property {(amountOut: Amount, brandIn: Brand) => Promise} + * getOutputPrice calculate the amount of brandIn that is required in order to + * get amountOut using the current price + * @property {(assetBrand: Brand, priceBrand: Brand) => ERef>} + * getPriceNotifier + * @property {(timer: TimerService, deadline: number, assetAmount: Amount, + * priceBrand: Brand) => Promise} priceAtTime Resolves after + * `deadline` passes on `timer` with the price of `assetAmount` at that time + * @property {(assetAmount: Amount, priceLimit: Amount) => Promise} + * priceWhenGT Resolve when the price of `assetAmount` exceeds `priceLimit` + * @property {(assetAmount: Amount, priceLimit: Amount) => Promise} + * priceWhenGTE Resolve when the price of `assetAmount` reaches or exceeds + * `priceLimit` + * @property {(assetAmount: Amount, priceLimit: Amount) => Promise} + * priceWhenLTE Resolve when the price of `assetAmount` reaches or drops below + * `priceLimit` + * @property {(assetAmount: Amount, priceLimit: Amount) => Promise} + * priceWhenLT Resolve when the price of `assetAmount` drops below `priceLimit` + */