Skip to content

Commit

Permalink
Merge pull request #7023 from Agoric/dc-pa-initial
Browse files Browse the repository at this point in the history
refactor(scaledPriceAuthority): initialPrice by composition
  • Loading branch information
mergify[bot] authored Feb 17, 2023
2 parents 1d9c7b7 + e7eda6f commit 0de845d
Show file tree
Hide file tree
Showing 3 changed files with 167 additions and 74 deletions.
112 changes: 112 additions & 0 deletions packages/zoe/src/contractSupport/priceAuthorityInitial.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
// @ts-check
import { E } from '@endo/far';
import { Far } from '@endo/marshal';

import { makeNotifier } from '@agoric/notifier';
import { AmountMath } from '@agoric/ertp';

import { multiplyBy } from './ratio.js';
import { mintQuote } from './priceAuthorityTransform.js';

/** @template T @typedef {import('@endo/eventual-send').EOnly<T>} EOnly */

/**
* Override `makeQuoteNotifier`, `quoteGiven` to provide an initial price
* in case one is not yet available from the source.
*
* Mainly for testing vaults without waiting for oracle operators to PushPrice.
*
* @param {Ratio} priceOutPerIn
* @param {PriceAuthority} priceAuthority
* @param {ERef<Mint<'set'>>} quoteMint
* @param {Brand<'nat'>} brandIn
* @param {Brand<'nat'>} brandOut
* @returns {PriceAuthority}
*/
export const makeInitialTransform = (
priceOutPerIn,
priceAuthority,
quoteMint,
brandIn,
brandOut,
) => {
assert.equal(priceOutPerIn.numerator.brand, brandOut);
assert.equal(priceOutPerIn.denominator.brand, brandIn);
let initialMode = true;

const quoteBrandP = E(E(quoteMint).getIssuer()).getBrand();
const timerP = E(priceAuthority).getTimerService(brandIn, brandOut);

/**
* @param {Amount<'nat'>} amountIn
* @param {Amount<'nat'>} amountOut
* @returns {Promise<PriceQuote>}
*/
const mintCurrentQuote = async (amountIn, amountOut) => {
const [quoteBrand, timer, timestamp] = await Promise.all([
quoteBrandP,
timerP,
E(timerP).getCurrentTimestamp(),
]);

return mintQuote(
quoteBrand,
amountIn,
amountOut,
timer,
timestamp,
quoteMint,
);
};

/** @type {PriceAuthority['makeQuoteNotifier']} */
const makeQuoteNotifier = (amountIn, bOut) => {
AmountMath.coerce(brandIn, amountIn);
assert.equal(bOut, brandOut);

const notifier = E(priceAuthority).makeQuoteNotifier(amountIn, brandOut);

const initialUpdateP = mintCurrentQuote(
priceOutPerIn.denominator,
priceOutPerIn.numerator,
).then(value => harden({ value, updateCount: 0n }));

// Wrap our underlying notifier.
const prefixedNotifier = harden({
async getUpdateSince(updateCount = -1n) {
if (initialMode && updateCount === -1n) {
return initialUpdateP;
}

initialMode = false;
return E(notifier).getUpdateSince(updateCount);
},
});

/** @type {Notifier<PriceQuote>} */
const farNotifier = Far('QuoteNotifier', {
...makeNotifier(prefixedNotifier),
// TODO stop exposing baseNotifier methods directly.
...prefixedNotifier,
});
return farNotifier;
};

/** @type {PriceAuthority['quoteGiven']} */
const quoteGiven = async (amountIn, bOut) => {
AmountMath.coerce(brandIn, amountIn);
assert.equal(bOut, brandOut);

const quoteP = E(priceAuthority).quoteGiven(amountIn, brandOut);
quoteP.then(() => (initialMode = false));
return initialMode
? mintCurrentQuote(amountIn, multiplyBy(amountIn, priceOutPerIn))
: quoteP;
};

return Far('PriceAuthority', {
...priceAuthority,
makeQuoteNotifier,
quoteGiven,
});
};
113 changes: 41 additions & 72 deletions packages/zoe/src/contractSupport/priceAuthorityTransform.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,38 @@ import { Far } from '@endo/marshal';
import { assert, details as X } from '@agoric/assert';
import { AmountMath, AssetKind, makeIssuerKit } from '@agoric/ertp';
import { makeNotifier } from '@agoric/notifier';
import { multiplyBy } from './ratio.js';

/** @template T @typedef {import('@endo/eventual-send').EOnly<T>} EOnly */

/**
*
* @param {Brand<'set'>} quoteBrand
* @param {Amount<'nat'>} amountIn
* @param {Amount<'nat'>} amountOut
* @param {ERef<import('@agoric/time/src/types').TimerService>} timer
* @param {import('@agoric/time').Timestamp} timestamp
* @param {ERef<Mint<'set'>>} quoteMint
* @returns {Promise<PriceQuote>}
*/
export const mintQuote = async (
quoteBrand,
amountIn,
amountOut,
timer,
timestamp,
quoteMint,
) => {
const quoteAmount = {
brand: quoteBrand,
value: [{ amountIn, amountOut, timer, timestamp }],
};
const quotePayment = await E(quoteMint).mintPayment({
brand: quoteBrand,
value: [quoteAmount],
});
return harden({ quoteAmount, quotePayment });
};

/**
* @param {object} opts
* @param {ERef<Mint<'set'>>} [opts.quoteMint]
Expand All @@ -15,7 +43,6 @@ import { multiplyBy } from './ratio.js';
* @param {Brand<'nat'>} opts.sourceBrandOut
* @param {Brand<'nat'>} [opts.actualBrandIn]
* @param {Brand<'nat'>} [opts.actualBrandOut]
* @param {Ratio} [opts.initialPrice]
* @param {(amountIn: Amount<'nat'>) => Amount<'nat'>} [opts.makeSourceAmountIn]
* @param {(amountOut: Amount<'nat'>) => Amount<'nat'>} [opts.makeSourceAmountOut]
* @param {(sourceAmountIn: Amount<'nat'>) => Amount<'nat'>} [opts.transformSourceAmountIn]
Expand All @@ -28,17 +55,11 @@ export const makePriceAuthorityTransform = async ({
sourceBrandOut,
actualBrandIn = sourceBrandIn,
actualBrandOut = sourceBrandOut,
initialPrice,
makeSourceAmountIn = x => x,
makeSourceAmountOut = x => x,
transformSourceAmountIn = x => x,
transformSourceAmountOut = x => x,
}) => {
if (initialPrice) {
assert.equal(initialPrice.numerator.brand, actualBrandOut);
assert.equal(initialPrice.denominator.brand, actualBrandIn);
}

const quoteIssuer = E(quoteMint).getIssuer();
const quoteBrand = await E(quoteIssuer).getBrand();

Expand All @@ -61,41 +82,6 @@ export const makePriceAuthorityTransform = async ({
);
};

/**
* @param {Amount<'nat'>} amountIn
* @param {Amount<'nat'>} amountOut
* @returns {Promise<PriceQuote>}
*/
const oneQuote = async (amountIn, amountOut) => {
const timerP = E(sourcePriceAuthority).getTimerService(
sourceBrandIn,
sourceBrandOut,
);
const [timer, timestamp] = await Promise.all([
timerP,
E(timerP).getCurrentTimestamp(),
]);

const quoteAmount = harden({
brand: quoteBrand,
value: [
{
amountIn,
amountOut,
timer,
timestamp,
},
],
});
const quotePayment = await E(quoteMint).mintPayment({
brand: quoteBrand,
value: [quoteAmount],
});
return harden({ quoteAmount, quotePayment });
};
const initialQuoteP =
initialPrice && oneQuote(initialPrice.denominator, initialPrice.numerator);

/**
* @param {PriceQuote} sourceQuote
* @returns {Promise<PriceQuote>}
Expand Down Expand Up @@ -124,19 +110,17 @@ export const makePriceAuthorityTransform = async ({
timer,
timestamp,
} = sourceQuoteValue[0];

const amountIn = transformSourceAmountIn(sourceAmountIn);
const amountOut = transformSourceAmountOut(sourceAmountOut);

const quoteAmount = {
brand: quoteBrand,
value: [{ amountIn, amountOut, timer, timestamp }],
};
const quotePayment = await E(quoteMint).mintPayment({
brand: quoteBrand,
value: [quoteAmount],
});
return harden({ quoteAmount, quotePayment });
return mintQuote(
quoteBrand,
amountIn,
amountOut,
timer,
timestamp,
quoteMint,
);
};

/**
Expand Down Expand Up @@ -205,7 +189,7 @@ export const makePriceAuthorityTransform = async ({
return mutableQuoteWhenOut;
};

/** @type {EOnly<PriceAuthority>} */
/** @type {PriceAuthority} */
const priceAuthority = Far('PriceAuthority', {
getQuoteIssuer(brandIn, brandOut) {
assertBrands(brandIn, brandOut);
Expand All @@ -229,13 +213,7 @@ export const makePriceAuthorityTransform = async ({

// Wrap our underlying notifier with scaled quotes.
const scaledBaseNotifier = harden({
async getUpdateSince(updateCount = -1n) {
if (initialPrice && initialQuoteP && updateCount === -1n) {
return initialQuoteP.then(value =>
harden({ value, updateCount: 0n }),
);
}

async getUpdateSince(updateCount = undefined) {
// We use the same updateCount as our underlying notifier.
const record = await E(notifier).getUpdateSince(updateCount);

Expand All @@ -259,20 +237,11 @@ export const makePriceAuthorityTransform = async ({
AmountMath.coerce(actualBrandIn, amountIn);
assertBrands(amountIn.brand, brandOut);

const sourceQuoteP = E(sourcePriceAuthority).quoteGiven(
const sourceQuote = await E(sourcePriceAuthority).quoteGiven(
makeSourceAmountIn(amountIn),
sourceBrandOut,
);
sourceQuoteP.then(() => {
initialPrice = undefined;
});
if (initialPrice) {
const price = initialPrice;
initialPrice = undefined;
return oneQuote(amountIn, multiplyBy(amountIn, price));
}

return sourceQuoteP.then(scaleQuote);
return scaleQuote(sourceQuote);
},
async quoteWanted(brandIn, amountOut) {
AmountMath.coerce(actualBrandOut, amountOut);
Expand Down
16 changes: 14 additions & 2 deletions packages/zoe/src/contracts/scaledPriceAuthority.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
floorMultiplyBy,
} from '../contractSupport/index.js';
import { makePriceAuthorityTransform } from '../contractSupport/priceAuthorityTransform.js';
import { makeInitialTransform } from '../contractSupport/priceAuthorityInitial.js';

/**
* @typedef {object} ScaledPriceAuthorityOpts
Expand Down Expand Up @@ -48,7 +49,6 @@ export const start = async (
sourceBrandOut,
actualBrandIn,
actualBrandOut,
initialPrice,
// It's hard to make a good guess as to the best rounding strategy for this
// transformation, but we make sure that the amount in is generous and the
// amount out is conservative.
Expand All @@ -58,8 +58,20 @@ export const start = async (
transformSourceAmountOut: amountOut => floorDivideBy(amountOut, scaleOut),
});

const withInitial = initialPrice
? priceAuthority.then(pa =>
makeInitialTransform(
initialPrice,
pa,
quoteMint,
actualBrandIn,
actualBrandOut,
),
)
: priceAuthority;

const publicFacet = Far('publicFacet', {
getPriceAuthority: () => priceAuthority,
getPriceAuthority: () => withInitial,
});
return harden({ publicFacet });
};

0 comments on commit 0de845d

Please sign in to comment.