From cd7ebd86b1f37655b9213786ab6828dd6c7c098a Mon Sep 17 00:00:00 2001 From: Chris Hibbert Date: Thu, 5 Nov 2020 23:19:03 -0800 Subject: [PATCH] feat: convert the fakePriceAuthority to a PlayerPiano model (#1985) * feat: convert the fakePriceAuthority to a PlayerPiano model It repeatedly runs through the list of prices provided at setup. This will be used to create a price oracle for the test net. tests updated * fix(fakePriceAuthority): remove some assumptions * chore: fix typo * chore: remove premature inversePriceAuthority.js * feat: add getPriceNotifier, more parameters to fakePriceAuthority * fix: calculate priceOutQuote using priceInQuote * chore: revert bad commit * test: update test-callSpread for playerPiano fakePriceAuthority * chore: clean up unintentional changes * test: update for new fakePriceAuthority * chore: fix tests * chore: lint-fix Co-authored-by: Michael FIG Co-authored-by: Kate Sills --- packages/ERTP/src/localAmountMath.js | 2 +- .../lib/ag-solo/vats/vat-priceAuthority.js | 21 ++ packages/zoe/src/contracts/loan/types.js | 15 ++ packages/zoe/test/fakePriceAuthority.js | 156 ------------- .../contracts/loan/test-addCollateral.js | 19 +- .../unitTests/contracts/loan/test-borrow.js | 32 +-- .../unitTests/contracts/loan/test-loan-e2e.js | 19 +- .../contracts/test-automaticRefund.js | 1 - .../contracts/test-brokenContract.js | 3 - .../unitTests/contracts/test-callSpread.js | 112 +++++---- .../unitTests/contracts/test-escrowToVote.js | 4 - .../unitTests/contracts/test-sellTickets.js | 6 - .../test/unitTests/contracts/test-useObj.js | 1 - .../test/unitTests/test-fakePriceAuthority.js | 131 +++++------ packages/zoe/tools/fakePriceAuthority.js | 215 ++++++++++++++++++ packages/zoe/tools/priceAuthorityRegistry.js | 4 +- packages/zoe/tools/types.js | 11 +- 17 files changed, 405 insertions(+), 347 deletions(-) delete mode 100644 packages/zoe/test/fakePriceAuthority.js create mode 100644 packages/zoe/tools/fakePriceAuthority.js diff --git a/packages/ERTP/src/localAmountMath.js b/packages/ERTP/src/localAmountMath.js index b16a2effe60..7775f7eed18 100644 --- a/packages/ERTP/src/localAmountMath.js +++ b/packages/ERTP/src/localAmountMath.js @@ -2,7 +2,7 @@ import { E } from '@agoric/eventual-send'; import { makeAmountMath } from './amountMath'; /** - * @param {Issuer} issuer + * @param {ERef} issuer * @returns {Promise} */ const makeLocalAmountMath = async issuer => { diff --git a/packages/cosmic-swingset/lib/ag-solo/vats/vat-priceAuthority.js b/packages/cosmic-swingset/lib/ag-solo/vats/vat-priceAuthority.js index fb921011132..1e8198c1ac8 100644 --- a/packages/cosmic-swingset/lib/ag-solo/vats/vat-priceAuthority.js +++ b/packages/cosmic-swingset/lib/ag-solo/vats/vat-priceAuthority.js @@ -1,7 +1,28 @@ import { makePriceAuthorityRegistry } from '@agoric/zoe/tools/priceAuthorityRegistry'; +import { makeFakePriceAuthority } from '@agoric/zoe/tools/fakePriceAuthority'; +import { makeLocalAmountMath } from '@agoric/ertp'; export function buildRootObject(_vatPowers) { return harden({ makePriceAuthority: makePriceAuthorityRegistry, + async makeFakePriceAuthority( + issuerIn, + issuerOut, + priceList, + timer, + quoteInterval = undefined, + ) { + const [mathIn, mathOut] = await Promise.all([ + makeLocalAmountMath(issuerIn), + makeLocalAmountMath(issuerOut), + ]); + return makeFakePriceAuthority( + mathIn, + mathOut, + priceList, + timer, + quoteInterval, + ); + }, }); } diff --git a/packages/zoe/src/contracts/loan/types.js b/packages/zoe/src/contracts/loan/types.js index 83ebb32b8fa..a3627a80755 100644 --- a/packages/zoe/src/contracts/loan/types.js +++ b/packages/zoe/src/contracts/loan/types.js @@ -158,6 +158,21 @@ * Used to tell the contract when a period has occurred * * @property {number} interestRate + * + * @property {ContractFacet} zcf + * @property {ConfigMinusGetDebt} configMinusGetDebt + */ + +/** + * @typedef {Object} ConfigMinusGetDebt + * @property {ZCFSeat} collateralSeat + * @property {PromiseRecord} liquidationPromiseKit + * @property {number} [mmr] + * @property {InstanceHandle} autoswapInstance + * @property {PriceAuthority} priceAuthority + * @property {AsyncIterable} periodAsyncIterable + * @property {number} interestRate + * @property {ZCFSeat} lenderSeat */ /** diff --git a/packages/zoe/test/fakePriceAuthority.js b/packages/zoe/test/fakePriceAuthority.js deleted file mode 100644 index acecbc3eb6e..00000000000 --- a/packages/zoe/test/fakePriceAuthority.js +++ /dev/null @@ -1,156 +0,0 @@ -// ts-check - -import '../exported'; - -import { makeIssuerKit, MathKind } from '@agoric/ertp'; -import { makePromiseKit } from '@agoric/promise-kit'; -import { makeNotifierKit } from '@agoric/notifier'; - -import { E } from '@agoric/eventual-send'; -import { natSafeMath } from '../src/contractSupport'; - -// TODO: multiple price Schedules for different goods, or for moving the price -// in different directions? - -export function makeFakePriceAuthority(amountMaths, priceSchedule, timer) { - const comparisonQueue = []; - let comparisonQueueScheduled; - - const { - mint: quoteMint, - issuer: quoteIssuer, - amountMath: quote, - } = makeIssuerKit('quote', MathKind.SET); - - function priceFromSchedule(targetTime) { - let freshestPrice = 0; - let freshestTime = -1; - for (const tick of priceSchedule) { - if (tick.time > freshestTime && tick.time <= targetTime) { - freshestTime = tick.time; - freshestPrice = tick.price; - } - } - return freshestPrice; - } - - function priceInQuote(currentTime, amountIn, brandOut) { - const mathOut = amountMaths.get(brandOut.getAllegedName()); - const price = priceFromSchedule(currentTime); - const quoteAmount = quote.make( - harden([ - { - amountIn, - amountOut: mathOut.make(price * amountIn.value), - timer, - timestamp: currentTime, - }, - ]), - ); - return harden({ - quotePayment: quoteMint.mintPayment(quoteAmount), - quoteAmount, - }); - } - - function priceOutQuote(currentTime, brandIn, amountOut) { - const mathIn = amountMaths.get(brandIn.getAllegedName()); - const mathOut = amountMaths.get(amountOut.brand.getAllegedName()); - const desiredValue = mathOut.getValue(amountOut); - const price = priceFromSchedule(currentTime); - const quoteAmount = quote.make( - harden([ - { - amountIn: mathIn.make(natSafeMath.floorDivide(desiredValue, price)), - amountOut, - timer, - timestamp: currentTime, - }, - ]), - ); - return harden({ - quotePayment: quoteMint.mintPayment(quoteAmount), - quoteAmount, - }); - } - - function startTimer() { - if (comparisonQueueScheduled) { - return; - } - - comparisonQueueScheduled = true; - const repeater = E(timer).createRepeater(0, 1); - const handler = harden({ - wake: t => { - for (const req of comparisonQueue) { - const priceQuote = priceInQuote(t, req.amountIn, req.brandOut); - const { amountOut: quotedOut } = priceQuote.quoteAmount.value[0]; - if (req.operator(req.math, quotedOut)) { - req.resolve(priceQuote); - comparisonQueue.splice(comparisonQueue.indexOf(req), 1); - } - } - }, - }); - E(repeater).schedule(handler); - } - - function resolveQuoteWhen(operator, amountIn, amountOutLimit) { - const promiseKit = makePromiseKit(); - startTimer(); - comparisonQueue.push({ - operator, - math: amountMaths.get(amountOutLimit.brand.getAllegedName()), - amountIn, - brandOut: amountOutLimit.brand, - resolve: promiseKit.resolve, - }); - return promiseKit.promise; - } - - /** @type {PriceAuthority} */ - const priceAuthority = { - getQuoteIssuer: () => quoteIssuer, - getTimerService: () => timer, - // TODO(hibbert): getPriceNotifier - quoteAtTime: (timeStamp, amountIn, brandOut) => { - const { promise, resolve } = makePromiseKit(); - E(timer).setWakeup( - timeStamp, - harden({ - wake: time => { - return resolve(priceInQuote(time, amountIn, brandOut)); - }, - }), - ); - return promise; - }, - quoteGiven: async (amountIn, brandOut) => - priceInQuote(timer.getCurrentTimestamp(), amountIn, brandOut), - quoteWanted: async (brandIn, amountOut) => - priceOutQuote(timer.getCurrentTimestamp(), brandIn, amountOut), - quoteWhenGTE: (amountIn, amountOutLimit) => { - const compareGTE = (math, amount) => math.isGTE(amount, amountOutLimit); - return resolveQuoteWhen(compareGTE, amountIn, amountOutLimit); - }, - quoteWhenGT: (amountIn, amountOutLimit) => { - const compareGT = (math, amount) => !math.isGTE(amountOutLimit, amount); - return resolveQuoteWhen(compareGT, amountIn, amountOutLimit); - }, - quoteWhenLTE: (amountIn, amountOutLimit) => { - const compareLTE = (math, amount) => math.isGTE(amountOutLimit, amount); - return resolveQuoteWhen(compareLTE, amountIn, amountOutLimit); - }, - quoteWhenLT: (amountIn, amountOutLimit) => { - const compareLT = (math, amount) => !math.isGTE(amount, amountOutLimit); - return resolveQuoteWhen(compareLT, amountIn, amountOutLimit); - }, - // TODO: implement - getPriceNotifier: () => { - const { notifier } = makeNotifierKit(); - return notifier; - }, - }; - return priceAuthority; -} diff --git a/packages/zoe/test/unitTests/contracts/loan/test-addCollateral.js b/packages/zoe/test/unitTests/contracts/loan/test-addCollateral.js index 2206929ac84..40917374ddd 100644 --- a/packages/zoe/test/unitTests/contracts/loan/test-addCollateral.js +++ b/packages/zoe/test/unitTests/contracts/loan/test-addCollateral.js @@ -8,7 +8,7 @@ import '@agoric/install-ses'; import test from 'ava'; import { makeAddCollateralInvitation } from '../../../../src/contracts/loan/addCollateral'; -import { makeFakePriceAuthority } from '../../../fakePriceAuthority'; +import { makeFakePriceAuthority } from '../../../../tools/fakePriceAuthority'; import buildManualTimer from '../../../../tools/manualTimer'; import { @@ -35,21 +35,14 @@ test('makeAddCollateralInvitation', async t => { const { zcfSeat: lenderSeat } = await zcf.makeEmptySeatKit(); - const amountMaths = new Map(); - amountMaths.set( - collateralKit.brand.getAllegedName(), - collateralKit.amountMath, - ); - amountMaths.set(loanKit.brand.getAllegedName(), loanKit.amountMath); - - const priceSchedule = {}; const timer = buildManualTimer(console.log); - const priceAuthority = makeFakePriceAuthority( - amountMaths, - priceSchedule, + const priceAuthority = makeFakePriceAuthority({ + mathIn: collateralKit.amountMath, + mathOut: loanKit.amountMath, + priceList: [], timer, - ); + }); const autoswapInstance = {}; const getDebt = () => loanKit.amountMath.make(100); diff --git a/packages/zoe/test/unitTests/contracts/loan/test-borrow.js b/packages/zoe/test/unitTests/contracts/loan/test-borrow.js index 6eea4c7ff80..0c256f02b7b 100644 --- a/packages/zoe/test/unitTests/contracts/loan/test-borrow.js +++ b/packages/zoe/test/unitTests/contracts/loan/test-borrow.js @@ -1,4 +1,4 @@ -// ts-check +// @ts-check import '../../../../exported'; @@ -22,7 +22,7 @@ import { makeAutoswapInstance, } from './helpers'; -import { makeFakePriceAuthority } from '../../../fakePriceAuthority'; +import { makeFakePriceAuthority } from '../../../../tools/fakePriceAuthority'; import buildManualTimer from '../../../../tools/manualTimer'; import { makeBorrowInvitation } from '../../../../src/contracts/loan/borrow'; @@ -41,24 +41,16 @@ const setupBorrow = async (maxLoanValue = 100) => { ); const mmr = 150; - const amountMaths = new Map(); - amountMaths.set( - collateralKit.brand.getAllegedName(), - collateralKit.amountMath, - ); - amountMaths.set(loanKit.brand.getAllegedName(), loanKit.amountMath); - - const priceSchedule = [ - { time: 0, price: 2 }, - { time: 1, price: 1 }, - ]; + const priceList = [2, 1, 1, 1]; const timer = buildManualTimer(console.log); - const priceAuthority = makeFakePriceAuthority( - amountMaths, - priceSchedule, + const priceAuthority = await makeFakePriceAuthority({ + mathIn: collateralKit.amountMath, + mathOut: loanKit.amountMath, + priceList, timer, - ); + }); + await timer.tick(); const initialLiquidityKeywordRecord = { Central: loanKit.amountMath.make(10000), @@ -192,7 +184,7 @@ test('borrow getLiquidationPromise', async t => { const collateralGiven = collateralKit.amountMath.make(100); - const quoteIssuer = await E(priceAuthority).getQuoteIssuer( + const quoteIssuer = E(priceAuthority).getQuoteIssuer( collateralKit.brand, loanKit.brand, ); @@ -214,7 +206,7 @@ test('borrow getLiquidationPromise', async t => { amountIn: collateralGiven, amountOut: loanKit.amountMath.make(100), timer, - timestamp: 1, + timestamp: 2, }, ]), ), @@ -274,7 +266,7 @@ test('borrow, then addCollateral, then getLiquidationPromise', async t => { amountIn: collateralGiven, amountOut: loanKit.amountMath.make(103), timer, - timestamp: 2, + timestamp: 3, }, ]), ), diff --git a/packages/zoe/test/unitTests/contracts/loan/test-loan-e2e.js b/packages/zoe/test/unitTests/contracts/loan/test-loan-e2e.js index 23dfdfe6106..92ec05abdf0 100644 --- a/packages/zoe/test/unitTests/contracts/loan/test-loan-e2e.js +++ b/packages/zoe/test/unitTests/contracts/loan/test-loan-e2e.js @@ -12,7 +12,7 @@ import { makeSubscriptionKit } from '@agoric/notifier'; import { checkDetails, checkPayout } from './helpers'; import { setup } from '../../setupBasicMints'; -import { makeFakePriceAuthority } from '../../../fakePriceAuthority'; +import { makeFakePriceAuthority } from '../../../../tools/fakePriceAuthority'; import buildManualTimer from '../../../../tools/manualTimer'; const loanRoot = `${__dirname}/../../../../src/contracts/loan/`; @@ -47,21 +47,14 @@ test('loan - lend - exit before borrow', async t => { Loan: loanKit.issuer, }); - const amountMaths = new Map(); - amountMaths.set( - collateralKit.brand.getAllegedName(), - collateralKit.amountMath, - ); - amountMaths.set(loanKit.brand.getAllegedName(), loanKit.amountMath); - - const priceSchedule = {}; const timer = buildManualTimer(console.log); - const priceAuthority = makeFakePriceAuthority( - amountMaths, - priceSchedule, + const priceAuthority = makeFakePriceAuthority({ + mathIn: collateralKit.amountMath, + mathOut: loanKit.amountMath, + priceList: [], timer, - ); + }); const { subscription: periodAsyncIterable } = makeSubscriptionKit(); diff --git a/packages/zoe/test/unitTests/contracts/test-automaticRefund.js b/packages/zoe/test/unitTests/contracts/test-automaticRefund.js index dc0fbb4784c..bc81202baa2 100644 --- a/packages/zoe/test/unitTests/contracts/test-automaticRefund.js +++ b/packages/zoe/test/unitTests/contracts/test-automaticRefund.js @@ -339,7 +339,6 @@ test('zoe - alice tries to complete after completion has already occurred', asyn await E(aliceSeat).getOfferResult(); - console.log('EXPECTED ERROR: seat has been exited >>>'); await t.throwsAsync(() => E(aliceSeat).tryExit(), { message: /seat has been exited/, }); diff --git a/packages/zoe/test/unitTests/contracts/test-brokenContract.js b/packages/zoe/test/unitTests/contracts/test-brokenContract.js index c69eafba1e4..2a257739d40 100644 --- a/packages/zoe/test/unitTests/contracts/test-brokenContract.js +++ b/packages/zoe/test/unitTests/contracts/test-brokenContract.js @@ -24,9 +24,6 @@ test('zoe - brokenAutomaticRefund', async t => { // Alice tries to create an instance, but the contract is badly // written. - console.log( - 'EXPECTED ERROR: The contract did not correctly return a creatorInvitation', - ); await t.throwsAsync( () => zoe.startInstance(installation, issuerKeywordRecord), { message: 'The contract did not correctly return a creatorInvitation' }, diff --git a/packages/zoe/test/unitTests/contracts/test-callSpread.js b/packages/zoe/test/unitTests/contracts/test-callSpread.js index cea2a6d7883..b1f48b33d8c 100644 --- a/packages/zoe/test/unitTests/contracts/test-callSpread.js +++ b/packages/zoe/test/unitTests/contracts/test-callSpread.js @@ -9,11 +9,19 @@ import buildManualTimer from '../../../tools/manualTimer'; import { setup } from '../setupBasicMints'; import { installationPFromSource } from '../installFromSource'; import { assertPayoutDeposit, assertPayoutAmount } from '../../zoeTestHelpers'; -import { makeFakePriceAuthority } from '../../fakePriceAuthority'; +import { makeFakePriceAuthority } from '../../../tools/fakePriceAuthority'; const callSpread = `${__dirname}/../../../src/contracts/callSpread`; const simpleExchange = `${__dirname}/../../../src/contracts/simpleExchange`; +const makeTestPriceAuthority = (amountMaths, priceList, timer) => + makeFakePriceAuthority({ + mathIn: amountMaths.get('simoleans'), + mathOut: amountMaths.get('moola'), + priceList, + timer, + }); + // Underlying is in Simoleans. Collateral, strikePrice and Payout are in bucks. // Value is in Moola. The price oracle takes an amount in Underlying, and // gives the value in Moola. @@ -28,6 +36,7 @@ test('callSpread below Strike1', async t => { bucks, zoe, amountMaths, + brands, } = setup(); const installation = await installationPFromSource(zoe, callSpread); @@ -42,15 +51,10 @@ test('callSpread below Strike1', async t => { // Setup Carol const carolBucksPurse = bucksIssuer.makeEmptyPurse(); - const manualTimer = buildManualTimer(console.log, 1); - const priceAuthority = makeFakePriceAuthority( + const manualTimer = buildManualTimer(console.log, 0); + const priceAuthority = makeTestPriceAuthority( amountMaths, - [ - { time: 0, price: 20 }, - { time: 1, price: 35 }, - { time: 2, price: 15 }, - { time: 3, price: 28 }, - ], + [54, 20, 35, 15, 28], manualTimer, ); // underlying is 2 Simoleans, strike range is 30-50 (doubled) @@ -69,7 +73,10 @@ test('callSpread below Strike1', async t => { Underlying: simoleanIssuer, Collateral: bucksIssuer, Strike: moolaIssuer, - Quote: priceAuthority.getQuoteIssuer(), + Quote: await E(priceAuthority).getQuoteIssuer( + brands.get('simoleans'), + brands.get('moola'), + ), }); const { creatorInvitation } = await zoe.startInstance( installation, @@ -109,8 +116,9 @@ test('callSpread below Strike1', async t => { bucks(300), ); - await manualTimer.tick(); - await manualTimer.tick(); + await E(manualTimer).tick(); + await E(manualTimer).tick(); + await E(manualTimer).tick(); await Promise.all([bobDeposit, carolDeposit]); }); @@ -127,6 +135,7 @@ test('callSpread above Strike2', async t => { bucks, zoe, amountMaths, + brands, } = setup(); const installation = await installationPFromSource(zoe, callSpread); @@ -141,13 +150,10 @@ test('callSpread above Strike2', async t => { // Setup Carol const carolBucksPurse = bucksIssuer.makeEmptyPurse(); - const manualTimer = buildManualTimer(console.log, 1); - const priceAuthority = makeFakePriceAuthority( + const manualTimer = buildManualTimer(console.log, 0); + const priceAuthority = makeTestPriceAuthority( amountMaths, - [ - { time: 0, price: 20 }, - { time: 3, price: 55 }, - ], + [20, 55], manualTimer, ); // underlying is 2 Simoleans, strike range is 30-50 (doubled) @@ -166,7 +172,10 @@ test('callSpread above Strike2', async t => { Underlying: simoleanIssuer, Collateral: bucksIssuer, Strike: moolaIssuer, - Quote: priceAuthority.getQuoteIssuer(), + Quote: await E(priceAuthority).getQuoteIssuer( + brands.get('simoleans'), + brands.get('moola'), + ), }); const { creatorInvitation } = await zoe.startInstance( @@ -212,8 +221,9 @@ test('callSpread above Strike2', async t => { bucks(0), ); - await manualTimer.tick(); - await manualTimer.tick(); + await E(manualTimer).tick(); + await E(manualTimer).tick(); + await E(manualTimer).tick(); await Promise.all([bobDeposit, carolDeposit]); }); @@ -230,6 +240,7 @@ test('callSpread, mid-strike', async t => { bucks, zoe, amountMaths, + brands, } = setup(); const installation = await installationPFromSource(zoe, callSpread); @@ -244,13 +255,10 @@ test('callSpread, mid-strike', async t => { // Setup Carol const carolBucksPurse = bucksIssuer.makeEmptyPurse(); - const manualTimer = buildManualTimer(console.log, 1); - const priceAuthority = makeFakePriceAuthority( + const manualTimer = buildManualTimer(console.log, 0); + const priceAuthority = makeTestPriceAuthority( amountMaths, - [ - { time: 0, price: 20 }, - { time: 3, price: 45 }, - ], + [20, 45], manualTimer, ); // underlying is 2 Simoleans, strike range is 30-50 (doubled) @@ -268,7 +276,10 @@ test('callSpread, mid-strike', async t => { Underlying: simoleanIssuer, Collateral: bucksIssuer, Strike: moolaIssuer, - Quote: priceAuthority.getQuoteIssuer(), + Quote: await E(priceAuthority).getQuoteIssuer( + brands.get('simoleans'), + brands.get('moola'), + ), }); const { creatorInvitation } = await zoe.startInstance( @@ -314,8 +325,9 @@ test('callSpread, mid-strike', async t => { bucks(75), ); - await manualTimer.tick(); - await manualTimer.tick(); + await E(manualTimer).tick(); + await E(manualTimer).tick(); + await E(manualTimer).tick(); await Promise.all([bobDeposit, carolDeposit]); }); @@ -332,6 +344,7 @@ test('callSpread, late exercise', async t => { bucks, zoe, amountMaths, + brands, } = setup(); const installation = await installationPFromSource(zoe, callSpread); @@ -346,13 +359,10 @@ test('callSpread, late exercise', async t => { // Setup Carol const carolBucksPurse = bucksIssuer.makeEmptyPurse(); - const manualTimer = buildManualTimer(console.log, 1); - const priceAuthority = makeFakePriceAuthority( + const manualTimer = buildManualTimer(console.log, 0); + const priceAuthority = makeTestPriceAuthority( amountMaths, - [ - { time: 0, price: 20 }, - { time: 3, price: 45 }, - ], + [20, 45], manualTimer, ); // underlying is 2 Simoleans, strike range is 30-50 (doubled) @@ -371,7 +381,10 @@ test('callSpread, late exercise', async t => { Underlying: simoleanIssuer, Collateral: bucksIssuer, Strike: moolaIssuer, - Quote: priceAuthority.getQuoteIssuer(), + Quote: await E(priceAuthority).getQuoteIssuer( + brands.get('simoleans'), + brands.get('moola'), + ), }); const { creatorInvitation } = await zoe.startInstance( installation, @@ -409,8 +422,9 @@ test('callSpread, late exercise', async t => { bucks(225), ); - await manualTimer.tick(); - await manualTimer.tick(); + await E(manualTimer).tick(); + await E(manualTimer).tick(); + await E(manualTimer).tick(); const carolOptionSeat = await zoe.offer(carolShortOption); const carolPayout = await carolOptionSeat.getPayout('Collateral'); @@ -434,6 +448,7 @@ test('callSpread, sell options', async t => { bucks, zoe, amountMaths, + brands, } = setup(); const installation = await installationPFromSource(zoe, callSpread); const invitationIssuer = await E(zoe).getInvitationIssuer(); @@ -452,13 +467,10 @@ test('callSpread, sell options', async t => { const carolBucksPurse = bucksIssuer.makeEmptyPurse(); const carolBucksPayment = bucksMint.mintPayment(bucks(100)); - const manualTimer = buildManualTimer(console.log, 1); - const priceAuthority = makeFakePriceAuthority( + const manualTimer = buildManualTimer(console.log, 0); + const priceAuthority = makeTestPriceAuthority( amountMaths, - [ - { time: 0, price: 20 }, - { time: 3, price: 45 }, - ], + [20, 45], manualTimer, ); // underlying is 2 Simoleans, strike range is 30-50 (doubled) @@ -477,7 +489,10 @@ test('callSpread, sell options', async t => { Underlying: simoleanIssuer, Collateral: bucksIssuer, Strike: moolaIssuer, - Quote: priceAuthority.getQuoteIssuer(), + Quote: await E(priceAuthority).getQuoteIssuer( + brands.get('simoleans'), + brands.get('moola'), + ), }); const { creatorInvitation } = await zoe.startInstance( installation, @@ -600,7 +615,8 @@ test('callSpread, sell options', async t => { bucks(75), ); - await manualTimer.tick(); - await manualTimer.tick(); + await E(manualTimer).tick(); + await E(manualTimer).tick(); + await E(manualTimer).tick(); await Promise.all([aliceLong, aliceShort, bobDeposit, carolDeposit]); }); diff --git a/packages/zoe/test/unitTests/contracts/test-escrowToVote.js b/packages/zoe/test/unitTests/contracts/test-escrowToVote.js index 8cd6e8fbc04..138f6176455 100644 --- a/packages/zoe/test/unitTests/contracts/test-escrowToVote.js +++ b/packages/zoe/test/unitTests/contracts/test-escrowToVote.js @@ -73,7 +73,6 @@ test('zoe - escrowToVote', async t => { `voter1 gets everything she escrowed back`, ); - console.log('EXPECTED ERROR ->>>'); t.throws( () => voter.vote('NO'), { message: /the voter seat has exited/ }, @@ -96,7 +95,6 @@ test('zoe - escrowToVote', async t => { const seat = await E(zoe).offer(invitation, proposal, payments); const voter = await E(seat).getOfferResult(); - console.log('EXPECTED ERROR ->>>'); await t.throwsAsync( () => E(voter).vote('NOT A VALID ANSWER'), { message: /the answer "NOT A VALID ANSWER" was not 'YES' or 'NO'/ }, @@ -120,7 +118,6 @@ test('zoe - escrowToVote', async t => { `voter2 gets everything she escrowed back`, ); - console.log('EXPECTED ERROR ->>>'); t.throws( () => voter.vote('NO'), { message: /the voter seat has exited/ }, @@ -163,7 +160,6 @@ test('zoe - escrowToVote', async t => { `voter3 gets everything she escrowed back`, ); - console.log('EXPECTED ERROR ->>>'); t.throws( () => voter.vote('NO'), { message: /the voter seat has exited/ }, diff --git a/packages/zoe/test/unitTests/contracts/test-sellTickets.js b/packages/zoe/test/unitTests/contracts/test-sellTickets.js index 46c01103b9c..b8ca4d0c08b 100644 --- a/packages/zoe/test/unitTests/contracts/test-sellTickets.js +++ b/packages/zoe/test/unitTests/contracts/test-sellTickets.js @@ -307,9 +307,6 @@ test(`mint and sell opera tickets`, async t => { }), ); - console.log( - 'EXPECTED ERROR: Some of the wanted items were not available for sale >>>', - ); await t.throwsAsync( seat.getOfferResult(), { message: /Some of the wanted items were not available for sale/ }, @@ -381,9 +378,6 @@ test(`mint and sell opera tickets`, async t => { }), ); - console.log( - 'EXPECTED ERROR: More money is required to buy these items >>> ', - ); await t.throwsAsync( seat.getOfferResult(), { message: /More money.*is required to buy these items/ }, diff --git a/packages/zoe/test/unitTests/contracts/test-useObj.js b/packages/zoe/test/unitTests/contracts/test-useObj.js index 88eebf8182c..68d072ded7c 100644 --- a/packages/zoe/test/unitTests/contracts/test-useObj.js +++ b/packages/zoe/test/unitTests/contracts/test-useObj.js @@ -64,7 +64,6 @@ test('zoe - useObj', async t => { `alice gets everything she escrowed back`, ); - console.log('EXPECTED ERROR ->>>'); t.throws( () => useObj.colorPixels('purple'), { message: /the escrowing offer is no longer active/ }, diff --git a/packages/zoe/test/unitTests/test-fakePriceAuthority.js b/packages/zoe/test/unitTests/test-fakePriceAuthority.js index 08ff244e00c..d97630c0acc 100644 --- a/packages/zoe/test/unitTests/test-fakePriceAuthority.js +++ b/packages/zoe/test/unitTests/test-fakePriceAuthority.js @@ -1,3 +1,4 @@ +// @ts-check // eslint-disable-next-line import/no-extraneous-dependencies import '@agoric/install-ses'; // eslint-disable-next-line import/no-extraneous-dependencies @@ -6,18 +7,23 @@ import { E } from '@agoric/eventual-send'; import buildManualTimer from '../../tools/manualTimer'; import { setup } from './setupBasicMints'; -import { makeFakePriceAuthority } from '../fakePriceAuthority'; +import { makeFakePriceAuthority } from '../../tools/fakePriceAuthority'; + +const makeTestPriceAuthority = (amountMaths, priceList, timer) => + makeFakePriceAuthority({ + mathIn: amountMaths.get('moola'), + mathOut: amountMaths.get('bucks'), + priceList, + timer, + }); test('priceAuthority quoteAtTime', async t => { const { moola, bucks, amountMaths, brands } = setup(); const bucksBrand = brands.get('bucks'); const manualTimer = buildManualTimer(console.log, 0); - const priceAuthority = makeFakePriceAuthority( + const priceAuthority = await makeTestPriceAuthority( amountMaths, - [ - { time: 0, price: 20 }, - { time: 3, price: 55 }, - ], + [20, 55], manualTimer, ); @@ -33,9 +39,10 @@ test('priceAuthority quoteAtTime', async t => { t.is(3, quote.quoteAmount.value[0].timestamp); }); - await manualTimer.tick(); - await manualTimer.tick(); - await manualTimer.tick(); + await E(manualTimer).tick(); + await E(manualTimer).tick(); + await E(manualTimer).tick(); + await E(manualTimer).tick(); await done; }); @@ -43,16 +50,13 @@ test('priceAuthority quoteGiven', async t => { const { moola, amountMaths, brands, bucks } = setup(); const bucksBrand = brands.get('bucks'); const manualTimer = buildManualTimer(console.log, 0); - const priceAuthority = makeFakePriceAuthority( + const priceAuthority = await makeTestPriceAuthority( amountMaths, - [ - { time: 0, price: 20 }, - { time: 3, price: 55 }, - ], + [20, 55], manualTimer, ); - await manualTimer.tick(); + await E(manualTimer).tick(); const quote = await E(priceAuthority).quoteGiven(moola(37), bucksBrand); const quoteAmount = quote.quoteAmount.value[0]; t.is(1, quoteAmount.timestamp); @@ -63,16 +67,13 @@ test('priceAuthority quoteWanted', async t => { const { moola, bucks, amountMaths, brands } = setup(); const moolaBrand = brands.get('moola'); const manualTimer = buildManualTimer(console.log, 0); - const priceAuthority = makeFakePriceAuthority( + const priceAuthority = await makeTestPriceAuthority( amountMaths, - [ - { time: 0, price: 20 }, - { time: 3, price: 55 }, - ], + [20, 55], manualTimer, ); - await manualTimer.tick(); + await E(manualTimer).tick(); const quote = await E(priceAuthority).quoteWanted(moolaBrand, bucks(400)); const quoteAmount = quote.quoteAmount.value[0]; t.is(1, quoteAmount.timestamp); @@ -85,16 +86,13 @@ test('priceAuthority paired quotes', async t => { const moolaBrand = brands.get('moola'); const bucksBrand = brands.get('bucks'); const manualTimer = buildManualTimer(console.log, 0); - const priceAuthority = makeFakePriceAuthority( + const priceAuthority = await makeTestPriceAuthority( amountMaths, - [ - { time: 0, price: 20 }, - { time: 3, price: 55 }, - ], + [20, 55], manualTimer, ); - await manualTimer.tick(); + await E(manualTimer).tick(); const quoteOut = await E(priceAuthority).quoteWanted(moolaBrand, bucks(400)); const quoteOutAmount = quoteOut.quoteAmount.value[0]; @@ -112,14 +110,9 @@ test('priceAuthority paired quotes', async t => { test('priceAuthority quoteWhenGTE', async t => { const { moola, bucks, amountMaths } = setup(); const manualTimer = buildManualTimer(console.log, 0); - const priceAuthority = makeFakePriceAuthority( + const priceAuthority = await makeTestPriceAuthority( amountMaths, - [ - { time: 0, price: 20 }, - { time: 3, price: 30 }, - { time: 4, price: 25 }, - { time: 5, price: 40 }, - ], + [20, 30, 25, 40], manualTimer, ); @@ -127,29 +120,25 @@ test('priceAuthority quoteWhenGTE', async t => { .quoteWhenGTE(moola(1), bucks(40)) .then(quote => { const quoteInAmount = quote.quoteAmount.value[0]; - t.is(5, manualTimer.getCurrentTimestamp()); - t.is(5, quoteInAmount.timestamp); + t.is(4, manualTimer.getCurrentTimestamp()); + t.is(4, quoteInAmount.timestamp); t.deepEqual(bucks(40), quoteInAmount.amountOut); t.deepEqual(moola(1), quoteInAmount.amountIn); }); - await manualTimer.tick(); - await manualTimer.tick(); - await manualTimer.tick(); - await manualTimer.tick(); - await manualTimer.tick(); + await E(manualTimer).tick(); + await E(manualTimer).tick(); + await E(manualTimer).tick(); + await E(manualTimer).tick(); + await E(manualTimer).tick(); }); test('priceAuthority quoteWhenLT', async t => { const { moola, bucks, amountMaths } = setup(); const manualTimer = buildManualTimer(console.log, 0); - const priceAuthority = makeFakePriceAuthority( + const priceAuthority = await makeTestPriceAuthority( amountMaths, - [ - { time: 0, price: 40 }, - { time: 3, price: 30 }, - { time: 4, price: 29 }, - ], + [40, 30, 29], manualTimer, ); @@ -157,28 +146,24 @@ test('priceAuthority quoteWhenLT', async t => { .quoteWhenLT(moola(1), bucks(30)) .then(quote => { const quoteInAmount = quote.quoteAmount.value[0]; - t.is(4, manualTimer.getCurrentTimestamp()); - t.is(4, quoteInAmount.timestamp); + t.is(3, manualTimer.getCurrentTimestamp()); + t.is(3, quoteInAmount.timestamp); t.deepEqual(bucks(29), quoteInAmount.amountOut); t.deepEqual(moola(1), quoteInAmount.amountIn); }); - await manualTimer.tick(); - await manualTimer.tick(); - await manualTimer.tick(); - await manualTimer.tick(); + await E(manualTimer).tick(); + await E(manualTimer).tick(); + await E(manualTimer).tick(); + await E(manualTimer).tick(); }); test('priceAuthority quoteWhenGT', async t => { const { moola, bucks, amountMaths } = setup(); const manualTimer = buildManualTimer(console.log, 0); - const priceAuthority = makeFakePriceAuthority( + const priceAuthority = await makeTestPriceAuthority( amountMaths, - [ - { time: 0, price: 40 }, - { time: 3, price: 30 }, - { time: 4, price: 41 }, - ], + [40, 30, 41], manualTimer, ); @@ -186,28 +171,24 @@ test('priceAuthority quoteWhenGT', async t => { .quoteWhenGT(moola(1), bucks(40)) .then(quote => { const quoteInAmount = quote.quoteAmount.value[0]; - t.is(4, manualTimer.getCurrentTimestamp()); - t.is(4, quoteInAmount.timestamp); + t.is(3, manualTimer.getCurrentTimestamp()); + t.is(3, quoteInAmount.timestamp); t.deepEqual(bucks(41), quoteInAmount.amountOut); t.deepEqual(moola(1), quoteInAmount.amountIn); }); - await manualTimer.tick(); - await manualTimer.tick(); - await manualTimer.tick(); - await manualTimer.tick(); + await E(manualTimer).tick(); + await E(manualTimer).tick(); + await E(manualTimer).tick(); + await E(manualTimer).tick(); }); test('priceAuthority quoteWhenLTE', async t => { const { moola, bucks, amountMaths } = setup(); const manualTimer = buildManualTimer(console.log, 0); - const priceAuthority = makeFakePriceAuthority( + const priceAuthority = await makeTestPriceAuthority( amountMaths, - [ - { time: 0, price: 40 }, - { time: 3, price: 26 }, - { time: 4, price: 25 }, - ], + [40, 26, 50, 25], manualTimer, ); @@ -221,8 +202,8 @@ test('priceAuthority quoteWhenLTE', async t => { t.deepEqual(moola(1), quoteInAmount.amountIn); }); - await manualTimer.tick(); - await manualTimer.tick(); - await manualTimer.tick(); - await manualTimer.tick(); + await E(manualTimer).tick(); + await E(manualTimer).tick(); + await E(manualTimer).tick(); + await E(manualTimer).tick(); }); diff --git a/packages/zoe/tools/fakePriceAuthority.js b/packages/zoe/tools/fakePriceAuthority.js new file mode 100644 index 00000000000..92f293a2385 --- /dev/null +++ b/packages/zoe/tools/fakePriceAuthority.js @@ -0,0 +1,215 @@ +// @ts-check +import { makeIssuerKit, MathKind, makeLocalAmountMath } from '@agoric/ertp'; +import { makePromiseKit } from '@agoric/promise-kit'; +import { makeNotifierKit } from '@agoric/notifier'; +import { E } from '@agoric/eventual-send'; +import { assert, details } from '@agoric/assert'; + +import { natSafeMath } from '../src/contractSupport'; + +import './types'; +import '../exported'; + +/** + * @typedef {Object} FakePriceAuthorityOptions + * @property {AmountMath} mathIn + * @property {AmountMath} mathOut + * @property {Array} priceList + * @property {ERef} timer + * @property {RelativeTime} [quoteInterval] + * @property {ERef} [quoteMint] + * @property {Amount} [unitAmountIn] + */ + +/** + * TODO: multiple price Schedules for different goods, or for moving the price + * in different directions? + * + * @param {FakePriceAuthorityOptions} options + * @returns {Promise} + */ +export async function makeFakePriceAuthority(options) { + const { + mathIn, + mathOut, + priceList, + timer, + unitAmountIn = mathIn.make(1), + quoteInterval = 1, + quoteMint = makeIssuerKit('quote', MathKind.SET).mint, + } = options; + + const unitValueIn = mathIn.getValue(unitAmountIn); + + const comparisonQueue = []; + + let currentPriceIndex = 0; + + function currentPrice() { + return priceList[currentPriceIndex % priceList.length]; + } + + /** + * @param {Brand} brandIn + * @param {Brand} brandOut + */ + const assertBrands = (brandIn, brandOut) => { + assert.equal( + brandIn, + mathIn.getBrand(), + details`${brandIn} is not an expected input brand`, + ); + assert.equal( + brandOut, + mathOut.getBrand(), + details`${brandOut} is not an expected output brand`, + ); + }; + + const quoteIssuer = E(quoteMint).getIssuer(); + const quoteMath = await makeLocalAmountMath(quoteIssuer); + + /** @type {NotifierRecord} */ + const { notifier, updater } = makeNotifierKit(); + + /** + * + * @param {Amount} amountIn + * @param {Brand} brandOut + * @param {Timestamp} quoteTime + * @returns {PriceQuote} + */ + function priceInQuote(amountIn, brandOut, quoteTime) { + assertBrands(amountIn.brand, brandOut); + const quoteAmount = quoteMath.make( + harden([ + { + amountIn, + amountOut: mathOut.make( + natSafeMath.floorDivide( + currentPrice() * amountIn.value, + unitValueIn, + ), + ), + timer, + timestamp: quoteTime, + }, + ]), + ); + const quote = harden({ + quotePayment: E(quoteMint).mintPayment(quoteAmount), + quoteAmount, + }); + updater.updateState(quote); + return quote; + } + + /** + * @param {Brand} brandIn + * @param {Amount} amountOut + * @param {Timestamp} quoteTime + * @returns {PriceQuote} + */ + function priceOutQuote(brandIn, amountOut, quoteTime) { + assertBrands(brandIn, amountOut.brand); + const desiredValue = mathOut.getValue(amountOut); + const price = currentPrice(); + // FIXME: Use natSafeMath.ceilDivide to calculate valueIn. + const valueIn = Math.ceil((desiredValue * unitValueIn) / price); + return priceInQuote(mathIn.make(valueIn), amountOut.brand, quoteTime); + } + + async function startTimer() { + let firstTime = true; + const handler = harden({ + wake: async t => { + if (firstTime) { + firstTime = false; + } else { + currentPriceIndex += 1; + } + for (const req of comparisonQueue) { + // eslint-disable-next-line no-await-in-loop + const priceQuote = priceInQuote(req.amountIn, req.brandOut, t); + const { amountOut: quotedOut } = priceQuote.quoteAmount.value[0]; + if (req.operator(req.math, quotedOut)) { + req.resolve(priceQuote); + comparisonQueue.splice(comparisonQueue.indexOf(req), 1); + } + } + }, + }); + const repeater = E(timer).createRepeater(0, quoteInterval); + return E(repeater).schedule(handler); + } + await startTimer(); + + function resolveQuoteWhen(operator, amountIn, amountOutLimit) { + assertBrands(amountIn.brand, amountOutLimit.brand); + const promiseKit = makePromiseKit(); + comparisonQueue.push({ + operator, + math: mathOut, + amountIn, + brandOut: amountOutLimit.brand, + resolve: promiseKit.resolve, + }); + return promiseKit.promise; + } + + /** @type {PriceAuthority} */ + const priceAuthority = { + getQuoteIssuer: (brandIn, brandOut) => { + assertBrands(brandIn, brandOut); + return quoteIssuer; + }, + getTimerService: (brandIn, brandOut) => { + assertBrands(brandIn, brandOut); + return timer; + }, + getQuoteNotifier: async (brandIn, brandOut) => { + assertBrands(brandIn, brandOut); + return notifier; + }, + quoteAtTime: (timeStamp, amountIn, brandOut) => { + assertBrands(amountIn.brand, brandOut); + const { promise, resolve } = makePromiseKit(); + E(timer).setWakeup( + timeStamp, + harden({ + wake: time => { + return resolve(priceInQuote(amountIn, brandOut, time)); + }, + }), + ); + return promise; + }, + quoteGiven: async (amountIn, brandOut) => { + assertBrands(amountIn.brand, brandOut); + const timestamp = await E(timer).getCurrentTimestamp(); + return priceInQuote(amountIn, brandOut, timestamp); + }, + quoteWanted: async (brandIn, amountOut) => { + assertBrands(brandIn, amountOut.brand); + const timestamp = await E(timer).getCurrentTimestamp(); + return priceOutQuote(brandIn, amountOut, timestamp); + }, + quoteWhenGTE: (amountIn, amountOutLimit) => { + const compareGTE = (math, amount) => math.isGTE(amount, amountOutLimit); + return resolveQuoteWhen(compareGTE, amountIn, amountOutLimit); + }, + quoteWhenGT: (amountIn, amountOutLimit) => { + const compareGT = (math, amount) => !math.isGTE(amountOutLimit, amount); + return resolveQuoteWhen(compareGT, amountIn, amountOutLimit); + }, + quoteWhenLTE: (amountIn, amountOutLimit) => { + const compareLTE = (math, amount) => math.isGTE(amountOutLimit, amount); + return resolveQuoteWhen(compareLTE, amountIn, amountOutLimit); + }, + quoteWhenLT: (amountIn, amountOutLimit) => { + const compareLT = (math, amount) => !math.isGTE(amount, amountOutLimit); + return resolveQuoteWhen(compareLT, amountIn, amountOutLimit); + }, + }; + return priceAuthority; +} diff --git a/packages/zoe/tools/priceAuthorityRegistry.js b/packages/zoe/tools/priceAuthorityRegistry.js index 4b8a4c88f32..4cbffec339c 100644 --- a/packages/zoe/tools/priceAuthorityRegistry.js +++ b/packages/zoe/tools/priceAuthorityRegistry.js @@ -91,8 +91,8 @@ export const makePriceAuthorityRegistry = () => { async quoteWanted(brandIn, amountOut) { return E(paFor(brandIn, amountOut.brand)).quoteWanted(brandIn, amountOut); }, - async getPriceNotifier(brandIn, brandOut) { - return E(paFor(brandIn, brandOut)).getPriceNotifier(brandIn, brandOut); + async getQuoteNotifier(brandIn, brandOut) { + return E(paFor(brandIn, brandOut)).getQuoteNotifier(brandIn, brandOut); }, async quoteAtTime(deadline, amountIn, brandOut) { return E(paFor(amountIn.brand, brandOut)).quoteAtTime( diff --git a/packages/zoe/tools/types.js b/packages/zoe/tools/types.js index bdb25265975..fdfb93b0e2e 100644 --- a/packages/zoe/tools/types.js +++ b/packages/zoe/tools/types.js @@ -46,7 +46,7 @@ /** * @typedef {Object} PriceQuote * @property {Amount} quoteAmount Amount whose value is a PriceQuoteValue - * @property {Payment} quotePayment The `quoteAmount` wrapped as a payment + * @property {ERef} quotePayment The `quoteAmount` wrapped as a payment */ /** @@ -99,11 +99,14 @@ * brandIn/brandOut pair * * @property {(brandIn: Brand, brandOut: Brand) => ERef>} - * getPriceNotifier + * getQuoteNotifier be notified of the latest PriceQuotes for a given + * brandIn/brandOut pair. Note that these are not necessarily all for a + * constant amountIn (or amountOut), though some authorities may do that. The + * fact that they are raw quotes means that a PriceAuthority can implement + * quotes for both fungible and non-fungible brands. * * @property {(deadline: Timestamp, amountIn: Amount, brandOut: Brand) => - * Promise} - * quoteAtTime Resolves after `deadline` passes on the + * Promise} quoteAtTime Resolves after `deadline` passes on the * priceAuthority's timerService with the price quote of `amountIn` at that time * * @property {(amountIn: Amount, brandOut: Brand) => Promise}