From a960ddab5cda5f4db75d639cbb315bb5018e73d1 Mon Sep 17 00:00:00 2001 From: Turadg Aleahmad Date: Mon, 5 Dec 2022 13:43:20 -0800 Subject: [PATCH 01/26] build: priceAggregatorChainlink --- packages/zoe/scripts/build-bundles.js | 4 ++++ packages/zoe/src/contracts/priceAggregatorChainlink.js | 4 ++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/zoe/scripts/build-bundles.js b/packages/zoe/scripts/build-bundles.js index 8b96921d756..e46be0e0af3 100644 --- a/packages/zoe/scripts/build-bundles.js +++ b/packages/zoe/scripts/build-bundles.js @@ -11,6 +11,10 @@ const sourceToBundle = [ '../src/contracts/priceAggregator.js', '../bundles/bundle-priceAggregator.js', ], + [ + '../src/contracts/priceAggregatorChainlink.js', + '../bundles/bundle-priceAggregatorChainlink.js', + ], ]; await createBundles(sourceToBundle, dirname); diff --git a/packages/zoe/src/contracts/priceAggregatorChainlink.js b/packages/zoe/src/contracts/priceAggregatorChainlink.js index 65f1756bfe9..547d26eb16d 100644 --- a/packages/zoe/src/contracts/priceAggregatorChainlink.js +++ b/packages/zoe/src/contracts/priceAggregatorChainlink.js @@ -10,9 +10,9 @@ import { calculateMedian, natSafeMath, makeOnewayPriceAuthorityKit, -} from '../contractSupport'; +} from '../contractSupport/index.js'; -import '../../tools/types'; +import '../../tools/types.js'; /** * @typedef {{ roundId: number | undefined, data: string }} Result From ed950f5498e7773c5bd433b76725c8f093e01d48 Mon Sep 17 00:00:00 2001 From: Turadg Aleahmad Date: Mon, 5 Dec 2022 14:37:39 -0800 Subject: [PATCH 02/26] feat(oracle): support continuing invitation --- packages/zoe/src/contracts/priceAggregator.js | 8 +-- .../src/contracts/priceAggregatorChainlink.js | 71 ++++++++++++++++--- .../test-priceAggregatorChainlink.js | 6 +- 3 files changed, 71 insertions(+), 14 deletions(-) diff --git a/packages/zoe/src/contracts/priceAggregator.js b/packages/zoe/src/contracts/priceAggregator.js index 14082ed121c..269ddd4212a 100644 --- a/packages/zoe/src/contracts/priceAggregator.js +++ b/packages/zoe/src/contracts/priceAggregator.js @@ -32,7 +32,7 @@ import '@agoric/ertp/src/types-ambient.js'; export const INVITATION_MAKERS_DESC = 'oracle invitation'; -/** @typedef {ParsableNumber | Ratio} Result */ +/** @typedef {ParsableNumber | Ratio} Price */ /** * This contract aggregates price values from a set of oracles and provides a @@ -425,7 +425,7 @@ const start = async (zcf, privateArgs) => { * @param {object} param1 * @param {Notifier} [param1.notifier] optional notifier that produces oracle price submissions * @param {number} [param1.scaleValueOut] - * @returns {Promise<{admin: OracleAdmin, invitationMakers: {makePushPriceInvitation: (price: ParsableNumber) => Promise>} }>} + * @returns {Promise<{admin: OracleAdmin, invitationMakers: {makePushPriceInvitation: (price: ParsableNumber) => Promise>} }>} */ const offerHandler = async ( seat, @@ -483,7 +483,7 @@ const start = async (zcf, privateArgs) => { /** * @param {Instance | string} [oracleInstance] * @param {OracleQuery} [query] - * @returns {Promise>} + * @returns {Promise>} */ initOracle: async (oracleInstance, query) => { /** @type {OracleKey} */ @@ -526,7 +526,7 @@ const start = async (zcf, privateArgs) => { record.lastSample = ratio; }; - /** @type {OracleAdmin} */ + /** @type {OracleAdmin} */ const oracleAdmin = Far('OracleAdmin', { async delete() { assert(records.has(record), 'Oracle record is already deleted'); diff --git a/packages/zoe/src/contracts/priceAggregatorChainlink.js b/packages/zoe/src/contracts/priceAggregatorChainlink.js index 547d26eb16d..587896c8f18 100644 --- a/packages/zoe/src/contracts/priceAggregatorChainlink.js +++ b/packages/zoe/src/contracts/priceAggregatorChainlink.js @@ -13,9 +13,11 @@ import { } from '../contractSupport/index.js'; import '../../tools/types.js'; +import { assertParsableNumber } from '../contractSupport/ratio.js'; +import { INVITATION_MAKERS_DESC } from './priceAggregator'; /** - * @typedef {{ roundId: number | undefined, data: string }} Result + * @typedef {{ roundId: number | undefined, data: string }} PriceRound * `data` is a string encoded integer (Number.MAX_SAFE_INTEGER) */ @@ -34,7 +36,9 @@ const { add, subtract, multiply, floorDivide, ceilDivide, isGTE } = natSafeMath; * timeout: number, * minSubmissionValue: number, * maxSubmissionValue: number, - * unitAmountIn?: Amount, + * brandIn: Brand<'nat'>, + * brandOut: Brand<'nat'>, + * unitAmountIn: Amount<'nat'>, * }>} zcf * @param {object} root0 * @param {ERef>} [root0.quoteMint] @@ -45,7 +49,8 @@ const start = async ( ) => { const { timer, - brands: { In: brandIn, Out: brandOut }, + brandIn, + brandOut, maxSubmissionCount, minSubmissionCount, restartDelay, @@ -55,6 +60,11 @@ const start = async ( unitAmountIn = AmountMath.make(brandIn, 1n), } = zcf.getTerms(); + // TODO helper like: assertDefined({ brandIn, brandOut, timer, unitAmountIn }) + assert(brandIn, 'missing brandIn'); + assert(brandOut, 'missing brandOut'); + assert(timer, 'missing timer'); + assert(unitAmountIn, 'missing unitAmountIn'); const unitIn = AmountMath.getValue(brandIn, unitAmountIn); @@ -592,13 +602,56 @@ const start = async ( }; /** - * @type {Omit & { - * initOracle: (instance) => Promise>, + * @type {Omit & { + * initOracle: (instance) => Promise>, * getRoundData(_roundId: BigInt): Promise, * oracleRoundState(_oracle: OracleKey, _queriedRoundId: BigInt): Promise * }} */ const creatorFacet = Far('PriceAggregatorChainlinkCreatorFacet', { + /** + * An "oracle invitation" is an invitation to be able to submit data to + * include in the priceAggregator's results. + * + * The offer result from this invitation is a OracleAdmin, which can be used + * directly to manage the price submissions as well as to terminate the + * relationship. + * + * @param {Instance | string} [oracleKey] + */ + makeOracleInvitation: async oracleKey => { + /** + * If custom arguments are supplied to the `zoe.offer` call, they can + * indicate an OraclePriceSubmission notifier and a corresponding + * `shiftValueOut` that should be adapted as part of the priceAuthority's + * reported data. + * + * @param {ZCFSeat} seat + * @returns {Promise<{admin: OracleAdmin, invitationMakers: {makePushPriceInvitation: (result: PriceRound) => Promise>} }>} + */ + const offerHandler = async seat => { + const admin = await creatorFacet.initOracle(oracleKey); + const invitationMakers = Far('invitation makers', { + /** @param {PriceRound} result */ + makePushPriceInvitation(result) { + assertParsableNumber(result.data); + return zcf.makeInvitation(cSeat => { + cSeat.exit(); + admin.pushResult(result); + }, 'PushPrice'); + }, + }); + seat.exit(); + + return harden({ + admin, + + invitationMakers, + }); + }; + + return zcf.makeInvitation(offerHandler, INVITATION_MAKERS_DESC); + }, deleteOracle: async oracleKey => { const records = keyToRecords.get(oracleKey); for (const record of records) { @@ -639,7 +692,7 @@ const start = async ( } records.add(record); - /** @param {Result} result */ + /** @param {PriceRound} result */ const pushResult = async ({ roundId: _roundIdRaw = undefined, data: _submissionRaw, @@ -689,8 +742,8 @@ const start = async ( // Obtain the oracle's publicFacet. assert(records.has(record), 'Oracle record is already deleted'); - /** @type {OracleAdmin} */ - const oracleAdmin = { + /** @type {OracleAdmin} */ + const oracleAdmin = Far('OracleAdmin', { async delete() { assert(records.has(record), 'Oracle record is already deleted'); @@ -718,7 +771,7 @@ const start = async ( data: _submissionRaw, }).catch(console.error); }, - }; + }); return harden(oracleAdmin); }, diff --git a/packages/zoe/test/unitTests/contracts/test-priceAggregatorChainlink.js b/packages/zoe/test/unitTests/contracts/test-priceAggregatorChainlink.js index cf3f9b8088b..7e07023d1f1 100644 --- a/packages/zoe/test/unitTests/contracts/test-priceAggregatorChainlink.js +++ b/packages/zoe/test/unitTests/contracts/test-priceAggregatorChainlink.js @@ -118,8 +118,12 @@ test.before( const aggregator = await E(zoe).startInstance( aggregatorInstallation, - { In: link.issuer, Out: usd.issuer }, + // TODO back to this way of specifying brands + // { In: link.issuer, Out: usd.issuer }, + undefined, { + brandIn: link.issuer, + brandOut: usd.issuer, timer, maxSubmissionCount, minSubmissionCount, From e1f7488e52388b013343d6a6d0dd03ce5b2c9932 Mon Sep 17 00:00:00 2001 From: Turadg Aleahmad Date: Mon, 5 Dec 2022 14:38:25 -0800 Subject: [PATCH 03/26] feat(oracle!): roundId in price results --- packages/agoric-cli/src/commands/oracle.js | 36 ++++++++++++++++++- .../agoric-cli/test/agops-oracle-smoketest.sh | 2 +- packages/zoe/src/contracts/priceAggregator.js | 2 ++ 3 files changed, 38 insertions(+), 2 deletions(-) diff --git a/packages/agoric-cli/src/commands/oracle.js b/packages/agoric-cli/src/commands/oracle.js index 86166d6b2ff..d0f66d6f3c9 100644 --- a/packages/agoric-cli/src/commands/oracle.js +++ b/packages/agoric-cli/src/commands/oracle.js @@ -96,7 +96,7 @@ export const makeOracleCommand = async logger => { oracle .command('pushPrice') - .description('add a current price sample') + .description('add a current price sample to a priceAggregator') .option('--offerId [number]', 'Offer id', Number, Date.now()) .requiredOption( '--oracleAdminAcceptOfferId [number]', @@ -128,6 +128,40 @@ export const makeOracleCommand = async logger => { console.warn('Now execute the prepared offer'); }); + oracle + .command('pushPriceRound') + .description('add a price for a round to a priceAggregatorChainlink') + .option('--offerId [number]', 'Offer id', Number, Date.now()) + .requiredOption( + '--oracleAdminAcceptOfferId [number]', + 'offer that had continuing invitation result', + Number, + ) + .requiredOption('--price [number]', 'price (format TODO)', String) + .requiredOption('--roundId [number]', 'round', Number) + .action(async function () { + // @ts-expect-error this implicit any + const opts = this.opts(); + + /** @type {import('../lib/psm.js').OfferSpec} */ + const offer = { + id: Number(opts.offerId), + invitationSpec: { + source: 'continuing', + previousOffer: opts.oracleAdminAcceptOfferId, + invitationMakerName: 'makePushPriceInvitation', + invitationArgs: harden([{ data: opts.price, roundId: opts.roundId }]), + }, + proposal: {}, + }; + + outputAction({ + method: 'executeOffer', + offer, + }); + + console.warn('Now execute the prepared offer'); + }); oracle .command('query') .description('return current aggregated (median) price') diff --git a/packages/agoric-cli/test/agops-oracle-smoketest.sh b/packages/agoric-cli/test/agops-oracle-smoketest.sh index f24176b3fe1..ab318b76b5f 100644 --- a/packages/agoric-cli/test/agops-oracle-smoketest.sh +++ b/packages/agoric-cli/test/agops-oracle-smoketest.sh @@ -37,7 +37,7 @@ ORACLE_OFFER_ID=$(jq ".body | fromjson | .offer.id" <"$ORACLE_OFFER") # Use invitation result, with continuing invitationMakers to propose a vote PROPOSAL_OFFER=$(mktemp -t agops.XXX) -bin/agops oracle pushPrice --price 1.01 --oracleAdminAcceptOfferId "$ORACLE_OFFER_ID" >|"$PROPOSAL_OFFER" +bin/agops oracle pushPriceRound --price 1.01 --roundId 1 --oracleAdminAcceptOfferId "$ORACLE_OFFER_ID" >|"$PROPOSAL_OFFER" jq ".body | fromjson" <"$PROPOSAL_OFFER" agoric wallet send --from "$WALLET" --offer "$PROPOSAL_OFFER" diff --git a/packages/zoe/src/contracts/priceAggregator.js b/packages/zoe/src/contracts/priceAggregator.js index 269ddd4212a..18564c27778 100644 --- a/packages/zoe/src/contracts/priceAggregator.js +++ b/packages/zoe/src/contracts/priceAggregator.js @@ -35,6 +35,8 @@ export const INVITATION_MAKERS_DESC = 'oracle invitation'; /** @typedef {ParsableNumber | Ratio} Price */ /** + * @deprecated use priceAggregatorChainlink + * * This contract aggregates price values from a set of oracles and provides a * PriceAuthority for their median. This naive method is game-able and so this module * is a stub until we complete what is now in `priceAggregatorChainlink.js`. From a03a92dc34a8de97465106a2e75f160ad59334c6 Mon Sep 17 00:00:00 2001 From: Turadg Aleahmad Date: Tue, 6 Dec 2022 15:23:30 -0800 Subject: [PATCH 04/26] test: cleanup --- .../src/contracts/priceAggregatorChainlink.js | 2 +- .../test-priceAggregatorChainlink.js | 26 ++++--------------- 2 files changed, 6 insertions(+), 22 deletions(-) diff --git a/packages/zoe/src/contracts/priceAggregatorChainlink.js b/packages/zoe/src/contracts/priceAggregatorChainlink.js index 587896c8f18..fe091adc73b 100644 --- a/packages/zoe/src/contracts/priceAggregatorChainlink.js +++ b/packages/zoe/src/contracts/priceAggregatorChainlink.js @@ -38,7 +38,7 @@ const { add, subtract, multiply, floorDivide, ceilDivide, isGTE } = natSafeMath; * maxSubmissionValue: number, * brandIn: Brand<'nat'>, * brandOut: Brand<'nat'>, - * unitAmountIn: Amount<'nat'>, + * unitAmountIn?: Amount<'nat'>, * }>} zcf * @param {object} root0 * @param {ERef>} [root0.quoteMint] diff --git a/packages/zoe/test/unitTests/contracts/test-priceAggregatorChainlink.js b/packages/zoe/test/unitTests/contracts/test-priceAggregatorChainlink.js index 7e07023d1f1..73beff60dc9 100644 --- a/packages/zoe/test/unitTests/contracts/test-priceAggregatorChainlink.js +++ b/packages/zoe/test/unitTests/contracts/test-priceAggregatorChainlink.js @@ -15,7 +15,7 @@ import buildManualTimer from '../../../tools/manualTimer.js'; import '../../../src/contracts/exported.js'; -/** @type {import('ava').TestFn} */ +/** @type {import('ava').TestFn} */ const test = unknownTest; /** @@ -28,6 +28,7 @@ const test = unknownTest; /** * @typedef {object} TestContext * @property {ZoeService} zoe + * @property {(...args) => } makeChainlinkAggregator * @property {MakeFakePriceOracle} makeFakePriceOracle * @property {(POLL_INTERVAL: bigint) => Promise} makeMedianAggregator * @property {Amount} feeAmount @@ -64,6 +65,7 @@ test.before( /** @type {Installation} */ const oracleInstallation = await E(zoe).installBundleID('b1-oracle'); vatAdminState.installBundle('b1-aggregator', aggregatorBundle); + /** @type {Installation} */ const aggregatorInstallation = await E(zoe).installBundleID( 'b1-aggregator', ); @@ -110,7 +112,6 @@ test.before( minSubmissionCount, restartDelay, timeout, - description, minSubmissionValue, maxSubmissionValue, ) => { @@ -118,18 +119,15 @@ test.before( const aggregator = await E(zoe).startInstance( aggregatorInstallation, - // TODO back to this way of specifying brands - // { In: link.issuer, Out: usd.issuer }, undefined, { - brandIn: link.issuer, - brandOut: usd.issuer, timer, + brandIn: link.brand, + brandOut: usd.brand, maxSubmissionCount, minSubmissionCount, restartDelay, timeout, - description, minSubmissionValue, maxSubmissionValue, }, @@ -149,7 +147,6 @@ test('basic', async t => { const minSubmissionCount = 2; const restartDelay = 5; const timeout = 10; - const description = 'Chainlink oracles'; const minSubmissionValue = 100; const maxSubmissionValue = 10000; @@ -158,7 +155,6 @@ test('basic', async t => { minSubmissionCount, restartDelay, timeout, - description, minSubmissionValue, maxSubmissionValue, ); @@ -226,7 +222,6 @@ test('timeout', async t => { const minSubmissionCount = 2; const restartDelay = 2; const timeout = 5; - const description = 'Chainlink oracles'; const minSubmissionValue = 100; const maxSubmissionValue = 10000; @@ -235,7 +230,6 @@ test('timeout', async t => { minSubmissionCount, restartDelay, timeout, - description, minSubmissionValue, maxSubmissionValue, ); @@ -296,7 +290,6 @@ test('issue check', async t => { const minSubmissionCount = 2; const restartDelay = 2; const timeout = 5; - const description = 'Chainlink oracles'; const minSubmissionValue = 100; const maxSubmissionValue = 10000; @@ -305,7 +298,6 @@ test('issue check', async t => { minSubmissionCount, restartDelay, timeout, - description, minSubmissionValue, maxSubmissionValue, ); @@ -354,7 +346,6 @@ test('supersede', async t => { const minSubmissionCount = 2; const restartDelay = 1; const timeout = 5; - const description = 'Chainlink oracles'; const minSubmissionValue = 100; const maxSubmissionValue = 10000; @@ -363,7 +354,6 @@ test('supersede', async t => { minSubmissionCount, restartDelay, timeout, - description, minSubmissionValue, maxSubmissionValue, ); @@ -420,7 +410,6 @@ test('interleaved', async t => { const minSubmissionCount = 3; // requires ALL the oracles for consensus in this case const restartDelay = 1; const timeout = 5; - const description = 'Chainlink oracles'; const minSubmissionValue = 100; const maxSubmissionValue = 10000; @@ -429,7 +418,6 @@ test('interleaved', async t => { minSubmissionCount, restartDelay, timeout, - description, minSubmissionValue, maxSubmissionValue, ); @@ -558,7 +546,6 @@ test('larger', async t => { const minSubmissionCount = 3; const restartDelay = 1; const timeout = 5; - const description = 'Chainlink oracles'; const minSubmissionValue = 100; const maxSubmissionValue = 10000; @@ -567,7 +554,6 @@ test('larger', async t => { minSubmissionCount, restartDelay, timeout, - description, minSubmissionValue, maxSubmissionValue, ); @@ -641,7 +627,6 @@ test('suggest', async t => { const minSubmissionCount = 3; const restartDelay = 1; const timeout = 5; - const description = 'Chainlink oracles'; const minSubmissionValue = 100; const maxSubmissionValue = 10000; @@ -650,7 +635,6 @@ test('suggest', async t => { minSubmissionCount, restartDelay, timeout, - description, minSubmissionValue, maxSubmissionValue, ); From d63ddb23539e0b65c6d89ab221b9e049bf1217c9 Mon Sep 17 00:00:00 2001 From: Turadg Aleahmad Date: Tue, 6 Dec 2022 15:38:39 -0800 Subject: [PATCH 05/26] test: context type --- .../src/contracts/priceAggregatorChainlink.js | 4 +- .../test-priceAggregatorChainlink.js | 255 ++++++++---------- 2 files changed, 120 insertions(+), 139 deletions(-) diff --git a/packages/zoe/src/contracts/priceAggregatorChainlink.js b/packages/zoe/src/contracts/priceAggregatorChainlink.js index fe091adc73b..a13f007ff0b 100644 --- a/packages/zoe/src/contracts/priceAggregatorChainlink.js +++ b/packages/zoe/src/contracts/priceAggregatorChainlink.js @@ -604,7 +604,7 @@ const start = async ( /** * @type {Omit & { * initOracle: (instance) => Promise>, - * getRoundData(_roundId: BigInt): Promise, + * getRoundData(_roundId: bigint | number): Promise, * oracleRoundState(_oracle: OracleKey, _queriedRoundId: BigInt): Promise * }} */ @@ -792,7 +792,7 @@ const start = async ( * timed out. answeredInRound is equal to roundId when the round didn't time out * and was completed regularly. * - * @param {bigint} _roundIdRaw + * @param {bigint | number} _roundIdRaw */ async getRoundData(_roundIdRaw) { const roundId = Nat(_roundIdRaw); diff --git a/packages/zoe/test/unitTests/contracts/test-priceAggregatorChainlink.js b/packages/zoe/test/unitTests/contracts/test-priceAggregatorChainlink.js index 73beff60dc9..25b78cc756a 100644 --- a/packages/zoe/test/unitTests/contracts/test-priceAggregatorChainlink.js +++ b/packages/zoe/test/unitTests/contracts/test-priceAggregatorChainlink.js @@ -15,130 +15,111 @@ import buildManualTimer from '../../../tools/manualTimer.js'; import '../../../src/contracts/exported.js'; -/** @type {import('ava').TestFn} */ +/** @type {import('ava').TestFn>>} */ const test = unknownTest; -/** - * @callback MakeFakePriceOracle - * @param {ExecutionContext} t - * @param {bigint} [valueOut] - * @returns {Promise} - */ - -/** - * @typedef {object} TestContext - * @property {ZoeService} zoe - * @property {(...args) => } makeChainlinkAggregator - * @property {MakeFakePriceOracle} makeFakePriceOracle - * @property {(POLL_INTERVAL: bigint) => Promise} makeMedianAggregator - * @property {Amount} feeAmount - * @property {IssuerKit} link - * - * @typedef {import('ava').ExecutionContext} ExecutionContext - */ - const filename = new URL(import.meta.url).pathname; const dirname = path.dirname(filename); const oraclePath = `${dirname}/../../../src/contracts/oracle.js`; const aggregatorPath = `${dirname}/../../../src/contracts/priceAggregatorChainlink.js`; -test.before( - // comment to maintain formatting for git blame - 'setup aggregator and oracles', - async ot => { - // Outside of tests, we should use the long-lived Zoe on the - // testnet. In this test, we must create a new Zoe. - const { admin, vatAdminState } = makeFakeVatAdmin(); - const { zoeService: zoe } = makeZoeKit(admin); - - // Pack the contracts. - const oracleBundle = await bundleSource(oraclePath); - const aggregatorBundle = await bundleSource(aggregatorPath); - - // Install the contract on Zoe, getting an installation. We can - // use this installation to look up the code we installed. Outside - // of tests, we can also send the installation to someone - // else, and they can use it to create a new contract instance - // using the same code. - vatAdminState.installBundle('b1-oracle', oracleBundle); - /** @type {Installation} */ - const oracleInstallation = await E(zoe).installBundleID('b1-oracle'); - vatAdminState.installBundle('b1-aggregator', aggregatorBundle); - /** @type {Installation} */ - const aggregatorInstallation = await E(zoe).installBundleID( - 'b1-aggregator', +const makeContext = async () => { + // Outside of tests, we should use the long-lived Zoe on the + // testnet. In this test, we must create a new Zoe. + const { admin, vatAdminState } = makeFakeVatAdmin(); + const { zoeService: zoe } = makeZoeKit(admin); + + // Pack the contracts. + const oracleBundle = await bundleSource(oraclePath); + const aggregatorBundle = await bundleSource(aggregatorPath); + + // Install the contract on Zoe, getting an installation. We can + // use this installation to look up the code we installed. Outside + // of tests, we can also send the installation to someone + // else, and they can use it to create a new contract instance + // using the same code. + vatAdminState.installBundle('b1-oracle', oracleBundle); + /** @type {Installation} */ + const oracleInstallation = await E(zoe).installBundleID('b1-oracle'); + vatAdminState.installBundle('b1-aggregator', aggregatorBundle); + /** @type {Installation} */ + const aggregatorInstallation = await E(zoe).installBundleID('b1-aggregator'); + + const link = makeIssuerKit('$LINK', AssetKind.NAT); + const usd = makeIssuerKit('$USD', AssetKind.NAT); + + /** + * @param {bigint} [valueOut] + * @returns {Promise} + */ + const makeFakePriceOracle = async valueOut => { + /** @type {OracleHandler} */ + const oracleHandler = Far('OracleHandler', { + async onQuery({ increment }, _fee) { + assert(valueOut); + assert(increment); + valueOut += increment; + return harden({ + reply: `${valueOut}`, + requiredFee: AmountMath.makeEmpty(link.brand), + }); + }, + onError(query, reason) { + console.error('query', query, 'failed with', reason); + }, + onReply(_query, _reply) {}, + }); + + const startResult = await E(zoe).startInstance( + oracleInstallation, + { Fee: link.issuer }, + { oracleDescription: 'myOracle' }, + ); + const creatorFacet = await E(startResult.creatorFacet).initialize({ + oracleHandler, + }); + + return harden({ + ...startResult, + creatorFacet, + }); + }; + + const makeChainlinkAggregator = async ( + maxSubmissionCount, + minSubmissionCount, + restartDelay, + timeout, + minSubmissionValue, + maxSubmissionValue, + ) => { + const timer = buildManualTimer(() => {}); + + const aggregator = await E(zoe).startInstance( + aggregatorInstallation, + undefined, + { + timer, + brandIn: link.brand, + brandOut: usd.brand, + maxSubmissionCount, + minSubmissionCount, + restartDelay, + timeout, + minSubmissionValue, + maxSubmissionValue, + }, ); + return aggregator; + }; + + return { makeChainlinkAggregator, makeFakePriceOracle, zoe }; +}; - const link = makeIssuerKit('$LINK', AssetKind.NAT); - const usd = makeIssuerKit('$USD', AssetKind.NAT); - - /** @type {MakeFakePriceOracle} */ - const makeFakePriceOracle = async (t, valueOut = undefined) => { - /** @type {OracleHandler} */ - const oracleHandler = Far('OracleHandler', { - async onQuery({ increment }, _fee) { - assert(valueOut); - assert(increment); - valueOut += increment; - return harden({ - reply: `${valueOut}`, - requiredFee: AmountMath.makeEmpty(link.brand), - }); - }, - onError(query, reason) { - console.error('query', query, 'failed with', reason); - }, - onReply(_query, _reply) {}, - }); - - const startResult = await E(zoe).startInstance( - oracleInstallation, - { Fee: link.issuer }, - { oracleDescription: 'myOracle' }, - ); - const creatorFacet = await E(startResult.creatorFacet).initialize({ - oracleHandler, - }); - - return harden({ - ...startResult, - creatorFacet, - }); - }; - - const makeChainlinkAggregator = async ( - maxSubmissionCount, - minSubmissionCount, - restartDelay, - timeout, - minSubmissionValue, - maxSubmissionValue, - ) => { - const timer = buildManualTimer(() => {}); - - const aggregator = await E(zoe).startInstance( - aggregatorInstallation, - undefined, - { - timer, - brandIn: link.brand, - brandOut: usd.brand, - maxSubmissionCount, - minSubmissionCount, - restartDelay, - timeout, - minSubmissionValue, - maxSubmissionValue, - }, - ); - return aggregator; - }; - ot.context.zoe = zoe; - ot.context.makeFakePriceOracle = makeFakePriceOracle; - ot.context.makeChainlinkAggregator = makeChainlinkAggregator; - }, -); +test.before('setup aggregator and oracles', async t => { + t.context = await makeContext(); +}); test('basic', async t => { const { makeFakePriceOracle, zoe } = t.context; @@ -160,9 +141,9 @@ test('basic', async t => { ); const { timer: oracleTimer } = await E(zoe).getTerms(aggregator.instance); - const priceOracleA = await makeFakePriceOracle(t); - const priceOracleB = await makeFakePriceOracle(t); - const priceOracleC = await makeFakePriceOracle(t); + const priceOracleA = await makeFakePriceOracle(); + const priceOracleB = await makeFakePriceOracle(); + const priceOracleC = await makeFakePriceOracle(); const pricePushAdminA = await E(aggregator.creatorFacet).initOracle( priceOracleA.instance, @@ -235,9 +216,9 @@ test('timeout', async t => { ); const { timer: oracleTimer } = await E(zoe).getTerms(aggregator.instance); - const priceOracleA = await makeFakePriceOracle(t); - const priceOracleB = await makeFakePriceOracle(t); - const priceOracleC = await makeFakePriceOracle(t); + const priceOracleA = await makeFakePriceOracle(); + const priceOracleB = await makeFakePriceOracle(); + const priceOracleC = await makeFakePriceOracle(); const pricePushAdminA = await E(aggregator.creatorFacet).initOracle( priceOracleA.instance, @@ -303,9 +284,9 @@ test('issue check', async t => { ); const { timer: oracleTimer } = await E(zoe).getTerms(aggregator.instance); - const priceOracleA = await makeFakePriceOracle(t); - const priceOracleB = await makeFakePriceOracle(t); - const priceOracleC = await makeFakePriceOracle(t); + const priceOracleA = await makeFakePriceOracle(); + const priceOracleB = await makeFakePriceOracle(); + const priceOracleC = await makeFakePriceOracle(); const pricePushAdminA = await E(aggregator.creatorFacet).initOracle( priceOracleA.instance, @@ -359,9 +340,9 @@ test('supersede', async t => { ); const { timer: oracleTimer } = await E(zoe).getTerms(aggregator.instance); - const priceOracleA = await makeFakePriceOracle(t); - const priceOracleB = await makeFakePriceOracle(t); - const priceOracleC = await makeFakePriceOracle(t); + const priceOracleA = await makeFakePriceOracle(); + const priceOracleB = await makeFakePriceOracle(); + const priceOracleC = await makeFakePriceOracle(); const pricePushAdminA = await E(aggregator.creatorFacet).initOracle( priceOracleA.instance, @@ -423,9 +404,9 @@ test('interleaved', async t => { ); const { timer: oracleTimer } = await E(zoe).getTerms(aggregator.instance); - const priceOracleA = await makeFakePriceOracle(t); - const priceOracleB = await makeFakePriceOracle(t); - const priceOracleC = await makeFakePriceOracle(t); + const priceOracleA = await makeFakePriceOracle(); + const priceOracleB = await makeFakePriceOracle(); + const priceOracleC = await makeFakePriceOracle(); const pricePushAdminA = await E(aggregator.creatorFacet).initOracle( priceOracleA.instance, @@ -559,11 +540,11 @@ test('larger', async t => { ); const { timer: oracleTimer } = await E(zoe).getTerms(aggregator.instance); - const priceOracleA = await makeFakePriceOracle(t); - const priceOracleB = await makeFakePriceOracle(t); - const priceOracleC = await makeFakePriceOracle(t); - const priceOracleD = await makeFakePriceOracle(t); - const priceOracleE = await makeFakePriceOracle(t); + const priceOracleA = await makeFakePriceOracle(); + const priceOracleB = await makeFakePriceOracle(); + const priceOracleC = await makeFakePriceOracle(); + const priceOracleD = await makeFakePriceOracle(); + const priceOracleE = await makeFakePriceOracle(); const pricePushAdminA = await E(aggregator.creatorFacet).initOracle( priceOracleA.instance, @@ -640,9 +621,9 @@ test('suggest', async t => { ); const { timer: oracleTimer } = await E(zoe).getTerms(aggregator.instance); - const priceOracleA = await makeFakePriceOracle(t); - const priceOracleB = await makeFakePriceOracle(t); - const priceOracleC = await makeFakePriceOracle(t); + const priceOracleA = await makeFakePriceOracle(); + const priceOracleB = await makeFakePriceOracle(); + const priceOracleC = await makeFakePriceOracle(); const pricePushAdminA = await E(aggregator.creatorFacet).initOracle( priceOracleA.instance, From 28d1564e10364d1a5107b7d671c740f30704443f Mon Sep 17 00:00:00 2001 From: Turadg Aleahmad Date: Tue, 6 Dec 2022 15:41:44 -0800 Subject: [PATCH 06/26] chore(types): cast ManualTimer --- .../contracts/test-priceAggregatorChainlink.js | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/packages/zoe/test/unitTests/contracts/test-priceAggregatorChainlink.js b/packages/zoe/test/unitTests/contracts/test-priceAggregatorChainlink.js index 25b78cc756a..86eabb4178a 100644 --- a/packages/zoe/test/unitTests/contracts/test-priceAggregatorChainlink.js +++ b/packages/zoe/test/unitTests/contracts/test-priceAggregatorChainlink.js @@ -139,6 +139,8 @@ test('basic', async t => { minSubmissionValue, maxSubmissionValue, ); + /** @type {{ timer: ManualTimer }} */ + // @ts-expect-error cast const { timer: oracleTimer } = await E(zoe).getTerms(aggregator.instance); const priceOracleA = await makeFakePriceOracle(); @@ -214,6 +216,8 @@ test('timeout', async t => { minSubmissionValue, maxSubmissionValue, ); + /** @type {{ timer: ManualTimer }} */ + // @ts-expect-error cast const { timer: oracleTimer } = await E(zoe).getTerms(aggregator.instance); const priceOracleA = await makeFakePriceOracle(); @@ -282,6 +286,8 @@ test('issue check', async t => { minSubmissionValue, maxSubmissionValue, ); + /** @type {{ timer: ManualTimer }} */ + // @ts-expect-error cast const { timer: oracleTimer } = await E(zoe).getTerms(aggregator.instance); const priceOracleA = await makeFakePriceOracle(); @@ -338,6 +344,8 @@ test('supersede', async t => { minSubmissionValue, maxSubmissionValue, ); + /** @type {{ timer: ManualTimer }} */ + // @ts-expect-error cast const { timer: oracleTimer } = await E(zoe).getTerms(aggregator.instance); const priceOracleA = await makeFakePriceOracle(); @@ -402,6 +410,8 @@ test('interleaved', async t => { minSubmissionValue, maxSubmissionValue, ); + /** @type {{ timer: ManualTimer }} */ + // @ts-expect-error cast const { timer: oracleTimer } = await E(zoe).getTerms(aggregator.instance); const priceOracleA = await makeFakePriceOracle(); @@ -538,6 +548,8 @@ test('larger', async t => { minSubmissionValue, maxSubmissionValue, ); + /** @type {{ timer: ManualTimer }} */ + // @ts-expect-error cast const { timer: oracleTimer } = await E(zoe).getTerms(aggregator.instance); const priceOracleA = await makeFakePriceOracle(); @@ -619,6 +631,8 @@ test('suggest', async t => { minSubmissionValue, maxSubmissionValue, ); + /** @type {{ timer: ManualTimer }} */ + // @ts-expect-error cast const { timer: oracleTimer } = await E(zoe).getTerms(aggregator.instance); const priceOracleA = await makeFakePriceOracle(); From 274a10b55d99487ff741dce22e3a31e31311d60a Mon Sep 17 00:00:00 2001 From: Turadg Aleahmad Date: Tue, 6 Dec 2022 15:48:27 -0800 Subject: [PATCH 07/26] chores(types): RoundData --- .../src/contracts/priceAggregatorChainlink.js | 28 +++++++++++-------- 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/packages/zoe/src/contracts/priceAggregatorChainlink.js b/packages/zoe/src/contracts/priceAggregatorChainlink.js index a13f007ff0b..5e3d59a07bb 100644 --- a/packages/zoe/src/contracts/priceAggregatorChainlink.js +++ b/packages/zoe/src/contracts/priceAggregatorChainlink.js @@ -23,6 +23,20 @@ import { INVITATION_MAKERS_DESC } from './priceAggregator'; const { add, subtract, multiply, floorDivide, ceilDivide, isGTE } = natSafeMath; +/** + * @typedef {object} RoundData + * @property {bigint} roundId the round ID for which data was retrieved + * @property {number} answer the answer for the given round + * @property {Timestamp} startedAt the timestamp when the round was started. This is 0 + * if the round hasn't been started yet. + * @property {Timestamp} updatedAt the timestamp when the round last was updated (i.e. + * answer was last computed) + * @property {bigint} answeredInRound the round ID of the round in which the answer + * was computed. answeredInRound may be smaller than roundId when the round + * timed out. answeredInRound is equal to roundId when the round didn't time out + * and was completed regularly. + */ + /** * PriceAuthority for their median. Unlike the simpler `priceAggregator.js`, this approximates * the *Node Operator Aggregation* logic of [Chainlink price @@ -604,7 +618,7 @@ const start = async ( /** * @type {Omit & { * initOracle: (instance) => Promise>, - * getRoundData(_roundId: bigint | number): Promise, + * getRoundData(_roundId: bigint | number): Promise, * oracleRoundState(_oracle: OracleKey, _queriedRoundId: BigInt): Promise * }} */ @@ -780,19 +794,9 @@ const start = async ( * consumers are encouraged to check * that they're receiving fresh data by inspecting the updatedAt and * answeredInRound return values. - * return is: [roundId, answer, startedAt, updatedAt, answeredInRound], where - * roundId is the round ID for which data was retrieved - * answer is the answer for the given round - * startedAt is the timestamp when the round was started. This is 0 - * if the round hasn't been started yet. - * updatedAt is the timestamp when the round last was updated (i.e. - * answer was last computed) - * answeredInRound is the round ID of the round in which the answer - * was computed. answeredInRound may be smaller than roundId when the round - * timed out. answeredInRound is equal to roundId when the round didn't time out - * and was completed regularly. * * @param {bigint | number} _roundIdRaw + * @returns {Promise} */ async getRoundData(_roundIdRaw) { const roundId = Nat(_roundIdRaw); From a28c8f75026a2f22289298794942385bdc7687e4 Mon Sep 17 00:00:00 2001 From: Turadg Aleahmad Date: Wed, 7 Dec 2022 10:38:23 -0800 Subject: [PATCH 08/26] docs: why not `brands` key on terms --- packages/zoe/src/contracts/priceAggregator.js | 3 +++ packages/zoe/src/contracts/priceAggregatorChainlink.js | 3 +++ 2 files changed, 6 insertions(+) diff --git a/packages/zoe/src/contracts/priceAggregator.js b/packages/zoe/src/contracts/priceAggregator.js index 18564c27778..8b4a5ac52be 100644 --- a/packages/zoe/src/contracts/priceAggregator.js +++ b/packages/zoe/src/contracts/priceAggregator.js @@ -55,6 +55,9 @@ export const INVITATION_MAKERS_DESC = 'oracle invitation'; * }} privateArgs */ const start = async (zcf, privateArgs) => { + // brands come from named terms instead of `brands` key because the latter is + // a StandardTerm that Zoe creates from the `issuerKeywordRecord` argument and + // Oracle brands are inert (without issuers or mints). const { timer, POLL_INTERVAL, brandIn, brandOut, unitAmountIn } = zcf.getTerms(); assertAllDefined({ brandIn, brandOut, POLL_INTERVAL, timer, unitAmountIn }); diff --git a/packages/zoe/src/contracts/priceAggregatorChainlink.js b/packages/zoe/src/contracts/priceAggregatorChainlink.js index 5e3d59a07bb..ad086bd1b9f 100644 --- a/packages/zoe/src/contracts/priceAggregatorChainlink.js +++ b/packages/zoe/src/contracts/priceAggregatorChainlink.js @@ -61,6 +61,9 @@ const start = async ( zcf, { quoteMint = makeIssuerKit('quote', AssetKind.SET).mint } = {}, ) => { + // brands come from named terms instead of `brands` key because the latter is + // a StandardTerm that Zoe creates from the `issuerKeywordRecord` argument and + // Oracle brands are inert (without issuers or mints). const { timer, brandIn, From 2a2e55930d0702d7fe98661e2c3cfa9166cbd75b Mon Sep 17 00:00:00 2001 From: Turadg Aleahmad Date: Wed, 7 Dec 2022 10:52:24 -0800 Subject: [PATCH 09/26] style: remove superflous underscores --- .../src/contracts/priceAggregatorChainlink.js | 255 +++++++++--------- 1 file changed, 129 insertions(+), 126 deletions(-) diff --git a/packages/zoe/src/contracts/priceAggregatorChainlink.js b/packages/zoe/src/contracts/priceAggregatorChainlink.js index ad086bd1b9f..49f6597a45f 100644 --- a/packages/zoe/src/contracts/priceAggregatorChainlink.js +++ b/packages/zoe/src/contracts/priceAggregatorChainlink.js @@ -296,16 +296,16 @@ const start = async ( }); /** - * @param {bigint} _roundId + * @param {bigint} roundId * @param {Timestamp} blockTimestamp */ - const timedOut = (_roundId, blockTimestamp) => { - if (!details.has(_roundId) || !rounds.has(_roundId)) { + const timedOut = (roundId, blockTimestamp) => { + if (!details.has(roundId) || !rounds.has(roundId)) { return false; } - const startedAt = rounds.get(_roundId).startedAt; - const roundTimeout = details.get(_roundId).roundTimeout; + const startedAt = rounds.get(roundId).startedAt; + const roundTimeout = details.get(roundId).roundTimeout; // TODO Better would be to make `roundTimeout` a `RelativeTime` // everywhere, and to rename it to a name that does not // mistakenly imply that it is an absolute time. @@ -322,53 +322,53 @@ const start = async ( }; /** - * @param {bigint} _roundId + * @param {bigint} roundId * @param {Timestamp} blockTimestamp */ - const updateTimedOutRoundInfo = (_roundId, blockTimestamp) => { + const updateTimedOutRoundInfo = (roundId, blockTimestamp) => { // round 0 is non-existent, so we avoid that case -- round 1 is ignored // because we can't copy from round 0 in that case - if (_roundId === 0n || _roundId === 1n) { + if (roundId === 0n || roundId === 1n) { return; } - const roundTimedOut = timedOut(_roundId, blockTimestamp); + const roundTimedOut = timedOut(roundId, blockTimestamp); if (!roundTimedOut) return; - const prevId = subtract(_roundId, 1); + const prevId = subtract(roundId, 1); - const round = rounds.get(_roundId); + const round = rounds.get(roundId); round.answer = rounds.get(prevId).answer; round.answeredInRound = rounds.get(prevId).answeredInRound; round.updatedAt = blockTimestamp; - details.delete(_roundId); + details.delete(roundId); }; /** - * @param {bigint} _roundId + * @param {bigint} roundId */ - const newRound = _roundId => { - return _roundId === add(reportingRoundId, 1); + const newRound = roundId => { + return roundId === add(reportingRoundId, 1); }; /** - * @param {bigint} _roundId + * @param {bigint} roundId * @param {Timestamp} blockTimestamp */ - const initializeNewRound = (_roundId, blockTimestamp) => { - updateTimedOutRoundInfo(subtract(_roundId, 1), blockTimestamp); + const initializeNewRound = (roundId, blockTimestamp) => { + updateTimedOutRoundInfo(subtract(roundId, 1), blockTimestamp); - reportingRoundId = _roundId; + reportingRoundId = roundId; roundStartUpdater.updateState(reportingRoundId); details.init( - _roundId, + roundId, makeRoundDetails([], maxSubmissionCount, minSubmissionCount, timeout), ); rounds.init( - _roundId, + roundId, makeRound( /* answer = */ 0, /* startedAt = */ 0n, @@ -377,91 +377,90 @@ const start = async ( ), ); - rounds.get(_roundId).startedAt = blockTimestamp; + rounds.get(roundId).startedAt = blockTimestamp; }; /** - * @param {bigint} _roundId - * @param {OracleKey} _oracle + * @param {bigint} roundId + * @param {OracleKey} oracleKey * @param {Timestamp} blockTimestamp */ - const oracleInitializeNewRound = (_roundId, _oracle, blockTimestamp) => { - if (!newRound(_roundId)) return; - const lastStarted = oracleStatuses.get(_oracle).lastStartedRound; // cache storage reads - if (_roundId <= add(lastStarted, restartDelay) && lastStarted !== 0n) - return; - initializeNewRound(_roundId, blockTimestamp); + const oracleInitializeNewRound = (roundId, oracleKey, blockTimestamp) => { + if (!newRound(roundId)) return; + const lastStarted = oracleStatuses.get(oracleKey).lastStartedRound; // cache storage reads + if (roundId <= add(lastStarted, restartDelay) && lastStarted !== 0n) return; + initializeNewRound(roundId, blockTimestamp); - oracleStatuses.get(_oracle).lastStartedRound = _roundId; + oracleStatuses.get(oracleKey).lastStartedRound = roundId; }; /** - * @param {bigint} _roundId + * @param {bigint} roundId */ - const acceptingSubmissions = _roundId => { - return details.has(_roundId) && details.get(_roundId).maxSubmissions !== 0; + const acceptingSubmissions = roundId => { + return details.has(roundId) && details.get(roundId).maxSubmissions !== 0; }; /** - * @param {bigint} _submission - * @param {bigint} _roundId - * @param {OracleKey} _oracle + * @param {bigint} submission + * @param {bigint} roundId + * @param {OracleKey} oracleKey */ - const recordSubmission = (_submission, _roundId, _oracle) => { - if (!acceptingSubmissions(_roundId)) { + const recordSubmission = (submission, roundId, oracleKey) => { + if (!acceptingSubmissions(roundId)) { console.error('round not accepting submissions'); return false; } - details.get(_roundId).submissions.push(_submission); - oracleStatuses.get(_oracle).lastReportedRound = _roundId; - oracleStatuses.get(_oracle).latestSubmission = _submission; + details.get(roundId).submissions.push(submission); + oracleStatuses.get(oracleKey).lastReportedRound = roundId; + oracleStatuses.get(oracleKey).latestSubmission = submission; return true; }; /** - * @param {bigint} _roundId + * @param {bigint} roundId * @param {Timestamp} blockTimestamp */ - const updateRoundAnswer = (_roundId, blockTimestamp) => { + const updateRoundAnswer = (roundId, blockTimestamp) => { if ( - details.get(_roundId).submissions.length < - details.get(_roundId).minSubmissions + details.get(roundId).submissions.length < + details.get(roundId).minSubmissions ) { return [false, 0]; } const newAnswer = calculateMedian( details - .get(_roundId) + .get(roundId) .submissions.filter(sample => isNat(sample) && sample > 0n), { add, divide: floorDivide, isGTE }, ); - rounds.get(_roundId).answer = newAnswer; - rounds.get(_roundId).updatedAt = blockTimestamp; - rounds.get(_roundId).answeredInRound = _roundId; + rounds.get(roundId).answer = newAnswer; + rounds.get(roundId).updatedAt = blockTimestamp; + rounds.get(roundId).answeredInRound = roundId; return [true, newAnswer]; }; /** - * @param {bigint} _roundId + * @param {bigint} roundId */ - const deleteRoundDetails = _roundId => { + const deleteRoundDetails = roundId => { if ( - details.get(_roundId).submissions.length < - details.get(_roundId).maxSubmissions + details.get(roundId).submissions.length < + details.get(roundId).maxSubmissions ) return; - details.delete(_roundId); + details.delete(roundId); }; /** - * @param {bigint} _roundId + * @param {bigint} roundId */ - const validRoundId = _roundId => { - return _roundId <= ROUND_MAX; + const validRoundId = roundId => { + return roundId <= ROUND_MAX; }; /** @@ -471,75 +470,75 @@ const start = async ( }; /** - * @param {bigint} _roundId + * @param {bigint} roundId * @param {Timestamp} blockTimestamp */ - const supersedable = (_roundId, blockTimestamp) => { + const supersedable = (roundId, blockTimestamp) => { return ( - rounds.has(_roundId) && - (TimeMath.absValue(rounds.get(_roundId).updatedAt) > 0n || - timedOut(_roundId, blockTimestamp)) + rounds.has(roundId) && + (TimeMath.absValue(rounds.get(roundId).updatedAt) > 0n || + timedOut(roundId, blockTimestamp)) ); }; /** - * @param {bigint} _roundId - * @param {bigint} _rrId + * @param {bigint} roundId + * @param {bigint} rrId reporting round ID */ - const previousAndCurrentUnanswered = (_roundId, _rrId) => { - return add(_roundId, 1) === _rrId && rounds.get(_rrId).updatedAt === 0n; + const previousAndCurrentUnanswered = (roundId, rrId) => { + return add(roundId, 1) === rrId && rounds.get(rrId).updatedAt === 0n; }; /** - * @param {OracleKey} _oracle - * @param {bigint} _roundId + * @param {OracleKey} oracleKey + * @param {bigint} roundId * @param {Timestamp} blockTimestamp */ - const validateOracleRound = (_oracle, _roundId, blockTimestamp) => { + const validateOracleRound = (oracleKey, roundId, blockTimestamp) => { // cache storage reads - const startingRound = oracleStatuses.get(_oracle).startingRound; + const startingRound = oracleStatuses.get(oracleKey).startingRound; const rrId = reportingRoundId; let canSupersede = true; - if (_roundId > 1n) { - canSupersede = supersedable(subtract(_roundId, 1), blockTimestamp); + if (roundId > 1n) { + canSupersede = supersedable(subtract(roundId, 1), blockTimestamp); } if (startingRound === 0n) return 'not enabled oracle'; - if (startingRound > _roundId) return 'not yet enabled oracle'; - if (oracleStatuses.get(_oracle).endingRound < _roundId) + if (startingRound > roundId) return 'not yet enabled oracle'; + if (oracleStatuses.get(oracleKey).endingRound < roundId) return 'no longer allowed oracle'; - if (oracleStatuses.get(_oracle).lastReportedRound >= _roundId) + if (oracleStatuses.get(oracleKey).lastReportedRound >= roundId) return 'cannot report on previous rounds'; if ( - _roundId !== rrId && - _roundId !== add(rrId, 1) && - !previousAndCurrentUnanswered(_roundId, rrId) + roundId !== rrId && + roundId !== add(rrId, 1) && + !previousAndCurrentUnanswered(roundId, rrId) ) return 'invalid round to report'; - if (_roundId !== 1n && !canSupersede) + if (roundId !== 1n && !canSupersede) return 'previous round not supersedable'; return ''; }; /** - * @param {OracleKey} _oracle - * @param {bigint} _roundId + * @param {OracleKey} oracleKey + * @param {bigint} roundId */ - const delayed = (_oracle, _roundId) => { - const lastStarted = oracleStatuses.get(_oracle).lastStartedRound; - return _roundId > add(lastStarted, restartDelay) || lastStarted === 0n; + const delayed = (oracleKey, roundId) => { + const lastStarted = oracleStatuses.get(oracleKey).lastStartedRound; + return roundId > add(lastStarted, restartDelay) || lastStarted === 0n; }; /** * a method to provide all current info oracleStatuses need. Intended only * only to be callable by oracleStatuses. Not for use by contracts to read state. * - * @param {OracleKey} _oracle + * @param {OracleKey} oracleKey * @param {Timestamp} blockTimestamp */ - const oracleRoundStateSuggestRound = (_oracle, blockTimestamp) => { - const oracle = oracleStatuses.get(_oracle); + const oracleRoundStateSuggestRound = (oracleKey, blockTimestamp) => { + const oracle = oracleStatuses.get(oracleKey); const shouldSupersede = oracle.lastReportedRound === reportingRoundId || @@ -553,7 +552,7 @@ const start = async ( let eligibleToSubmit; if (canSupersede && shouldSupersede) { roundId = add(reportingRoundId, 1); - eligibleToSubmit = delayed(_oracle, roundId); + eligibleToSubmit = delayed(oracleKey, roundId); } else { roundId = reportingRoundId; eligibleToSubmit = acceptingSubmissions(roundId); @@ -571,7 +570,7 @@ const start = async ( roundTimeout = 0; } - const error = validateOracleRound(_oracle, roundId, blockTimestamp); + const error = validateOracleRound(oracleKey, roundId, blockTimestamp); if (error.length !== 0) { eligibleToSubmit = false; } @@ -587,31 +586,35 @@ const start = async ( }; /** - * @param {OracleKey} _oracle - * @param {bigint} _queriedRoundId + * @param {OracleKey} oracleKey + * @param {bigint} queriedRoundId * @param {Timestamp} blockTimestamp */ const eligibleForSpecificRound = ( - _oracle, - _queriedRoundId, + oracleKey, + queriedRoundId, blockTimestamp, ) => { - const error = validateOracleRound(_oracle, _queriedRoundId, blockTimestamp); - if (TimeMath.absValue(rounds.get(_queriedRoundId).startedAt) > 0n) { - return acceptingSubmissions(_queriedRoundId) && error.length === 0; + const error = validateOracleRound( + oracleKey, + queriedRoundId, + blockTimestamp, + ); + if (TimeMath.absValue(rounds.get(queriedRoundId).startedAt) > 0n) { + return acceptingSubmissions(queriedRoundId) && error.length === 0; } else { - return delayed(_oracle, _queriedRoundId) && error.length === 0; + return delayed(oracleKey, queriedRoundId) && error.length === 0; } }; /** - * @param {OracleKey} _oracle + * @param {OracleKey} oracleKey */ - const getStartingRound = _oracle => { + const getStartingRound = oracleKey => { const currentRound = reportingRoundId; if ( currentRound !== 0n && - currentRound === oracleStatuses.get(_oracle).endingRound + currentRound === oracleStatuses.get(oracleKey).endingRound ) { return currentRound; } @@ -621,8 +624,8 @@ const start = async ( /** * @type {Omit & { * initOracle: (instance) => Promise>, - * getRoundData(_roundId: bigint | number): Promise, - * oracleRoundState(_oracle: OracleKey, _queriedRoundId: BigInt): Promise + * getRoundData(roundId: bigint | number): Promise, + * oracleRoundState(oracleKey: OracleKey, queriedRoundId: BigInt): Promise * }} */ const creatorFacet = Far('PriceAggregatorChainlinkCreatorFacet', { @@ -711,14 +714,14 @@ const start = async ( /** @param {PriceRound} result */ const pushResult = async ({ - roundId: _roundIdRaw = undefined, - data: _submissionRaw, + roundId: roundIdRaw = undefined, + data: submissionRaw, }) => { - const parsedSubmission = Nat(parseInt(_submissionRaw, 10)); + const parsedSubmission = Nat(parseInt(submissionRaw, 10)); const blockTimestamp = await E(timer).getCurrentTimestamp(); let roundId; - if (_roundIdRaw === undefined) { + if (roundIdRaw === undefined) { const suggestedRound = oracleRoundStateSuggestRound( oracleKey, blockTimestamp, @@ -727,7 +730,7 @@ const start = async ( ? suggestedRound.queriedRoundId : add(suggestedRound.queriedRoundId, 1); } else { - roundId = Nat(_roundIdRaw); + roundId = Nat(roundIdRaw); } const error = validateOracleRound(oracleKey, roundId, blockTimestamp); @@ -778,14 +781,14 @@ const start = async ( } }, async pushResult({ - roundId: _roundIdRaw = undefined, - data: _submissionRaw, + roundId: roundIdRaw = undefined, + data: submissionRaw, }) { // Sample of NaN, 0, or negative numbers get culled in // the median calculation. pushResult({ - roundId: _roundIdRaw, - data: _submissionRaw, + roundId: roundIdRaw, + data: submissionRaw, }).catch(console.error); }, }); @@ -798,11 +801,11 @@ const start = async ( * that they're receiving fresh data by inspecting the updatedAt and * answeredInRound return values. * - * @param {bigint | number} _roundIdRaw + * @param {bigint | number} roundIdRaw * @returns {Promise} */ - async getRoundData(_roundIdRaw) { - const roundId = Nat(_roundIdRaw); + async getRoundData(roundIdRaw) { + const roundId = Nat(roundIdRaw); assert(rounds.has(roundId), V3_NO_DATA_ERROR); @@ -823,28 +826,28 @@ const start = async ( * a method to provide all current info oracleStatuses need. Intended only * only to be callable by oracleStatuses. Not for use by contracts to read state. * - * @param {OracleKey} _oracle - * @param {bigint} _queriedRoundId + * @param {OracleKey} oracleKey + * @param {bigint} queriedRoundId */ - async oracleRoundState(_oracle, _queriedRoundId) { + async oracleRoundState(oracleKey, queriedRoundId) { const blockTimestamp = await E(timer).getCurrentTimestamp(); - if (_queriedRoundId > 0) { - const round = rounds.get(_queriedRoundId); - const detail = details.get(_queriedRoundId); + if (queriedRoundId > 0) { + const round = rounds.get(queriedRoundId); + const detail = details.get(queriedRoundId); return { eligibleForSpecificRound: eligibleForSpecificRound( - _oracle, - _queriedRoundId, + oracleKey, + queriedRoundId, blockTimestamp, ), - queriedRoundId: _queriedRoundId, - oracleStatus: oracleStatuses.get(_oracle).latestSubmission, + queriedRoundId, + oracleStatus: oracleStatuses.get(oracleKey).latestSubmission, startedAt: round.startedAt, roundTimeout: detail.roundTimeout, oracleCount: oracleCount(), }; } else { - return oracleRoundStateSuggestRound(_oracle, blockTimestamp); + return oracleRoundStateSuggestRound(oracleKey, blockTimestamp); } }, }); From 9f6fd6bf74c13553d8e990e36cb2b23d88aab425 Mon Sep 17 00:00:00 2001 From: Turadg Aleahmad Date: Wed, 7 Dec 2022 11:58:27 -0800 Subject: [PATCH 10/26] refactor: clarity --- .../src/contracts/priceAggregatorChainlink.js | 30 ++++++++++++------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/packages/zoe/src/contracts/priceAggregatorChainlink.js b/packages/zoe/src/contracts/priceAggregatorChainlink.js index 49f6597a45f..c938652fb98 100644 --- a/packages/zoe/src/contracts/priceAggregatorChainlink.js +++ b/packages/zoe/src/contracts/priceAggregatorChainlink.js @@ -5,7 +5,7 @@ import { makeNotifierKit } from '@agoric/notifier'; import { makeLegacyMap } from '@agoric/store'; import { Nat, isNat } from '@agoric/nat'; import { TimeMath } from '@agoric/swingset-vat/src/vats/timer/timeMath.js'; - +import { Fail } from '@agoric/assert'; import { calculateMedian, natSafeMath, @@ -37,19 +37,27 @@ const { add, subtract, multiply, floorDivide, ceilDivide, isGTE } = natSafeMath; * and was completed regularly. */ +// Partly documented at https://github.com/smartcontractkit/chainlink/blob/b045416ebca769aa69bde2da23b5109abe07a8b5/contracts/src/v0.6/FluxAggregator.sol#L153 +/** + * @typedef {object} ChainlinkConfig + * @property {number} maxSubmissionCount + * @property {number} minSubmissionCount + * @property {RelativeTime} restartDelay the number of rounds an Oracle has to wait before they can initiate a round + * @property {number} minSubmissionValue an immutable check for a lower bound of what + * submission values are accepted from an oracle + * @property {number} maxSubmissionValue an immutable check for an upper bound of what + * submission values are accepted from an oracle + * @property {number} timeout the number of seconds after the previous round that + * allowed to lapse before allowing an oracle to skip an unfinished round + */ + /** * PriceAuthority for their median. Unlike the simpler `priceAggregator.js`, this approximates * the *Node Operator Aggregation* logic of [Chainlink price * feeds](https://blog.chain.link/levels-of-data-aggregation-in-chainlink-price-feeds/). * - * @param {ZCF<{ + * @param {ZCF, * brandOut: Brand<'nat'>, * unitAmountIn?: Amount<'nat'>, @@ -357,6 +365,8 @@ const start = async ( * @param {Timestamp} blockTimestamp */ const initializeNewRound = (roundId, blockTimestamp) => { + newRound(roundId) || Fail`Round ${roundId} already started`; + updateTimedOutRoundInfo(subtract(roundId, 1), blockTimestamp); reportingRoundId = roundId; @@ -385,7 +395,7 @@ const start = async ( * @param {OracleKey} oracleKey * @param {Timestamp} blockTimestamp */ - const oracleInitializeNewRound = (roundId, oracleKey, blockTimestamp) => { + const proposeNewRound = (roundId, oracleKey, blockTimestamp) => { if (!newRound(roundId)) return; const lastStarted = oracleStatuses.get(oracleKey).lastStartedRound; // cache storage reads if (roundId <= add(lastStarted, restartDelay) && lastStarted !== 0n) return; @@ -749,7 +759,7 @@ const start = async ( return; } - oracleInitializeNewRound(roundId, oracleKey, blockTimestamp); + proposeNewRound(roundId, oracleKey, blockTimestamp); const recorded = recordSubmission(parsedSubmission, roundId, oracleKey); if (!recorded) { return; From 642cb3765907755a84562ea34f2e1e98d0330e5e Mon Sep 17 00:00:00 2001 From: Turadg Aleahmad Date: Wed, 7 Dec 2022 11:58:47 -0800 Subject: [PATCH 11/26] test: getRoundStartNotifier --- .../test-priceAggregatorChainlink.js | 56 +++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/packages/zoe/test/unitTests/contracts/test-priceAggregatorChainlink.js b/packages/zoe/test/unitTests/contracts/test-priceAggregatorChainlink.js index 86eabb4178a..5fd155bb9d8 100644 --- a/packages/zoe/test/unitTests/contracts/test-priceAggregatorChainlink.js +++ b/packages/zoe/test/unitTests/contracts/test-priceAggregatorChainlink.js @@ -708,3 +708,59 @@ test('suggest', async t => { t.deepEqual(round3Attempt1.roundId, 3n); t.deepEqual(round3Attempt1.answer, 200n); }); + +test('notifications', async t => { + const { makeFakePriceOracle, zoe } = t.context; + + const maxSubmissionCount = 1000; + const minSubmissionCount = 2; + const restartDelay = 1; // have to alternate to start rounds + const timeout = 10; + const minSubmissionValue = 100; + const maxSubmissionValue = 10000; + + const aggregator = await t.context.makeChainlinkAggregator( + maxSubmissionCount, + minSubmissionCount, + restartDelay, + timeout, + minSubmissionValue, + maxSubmissionValue, + ); + /** @type {{ timer: ManualTimer }} */ + // @ts-expect-error cast + const { timer: oracleTimer } = await E(zoe).getTerms(aggregator.instance); + + const priceOracleA = await makeFakePriceOracle(); + const priceOracleB = await makeFakePriceOracle(); + + const pricePushAdminA = await E(aggregator.creatorFacet).initOracle( + priceOracleA.instance, + ); + const pricePushAdminB = await E(aggregator.creatorFacet).initOracle( + priceOracleB.instance, + ); + + const roundStartNotifier = await E( + aggregator.publicFacet, + ).getRoundStartNotifier(); + + await oracleTimer.tick(); + await E(pricePushAdminA).pushResult({ roundId: 1, data: '100' }); + t.is((await E(roundStartNotifier).getUpdateSince()).value, 1n); + await E(pricePushAdminB).pushResult({ roundId: 1, data: '200' }); + + await E(pricePushAdminA).pushResult({ roundId: 2, data: '1000' }); + // A started last round so fails to start next round + t.is((await E(roundStartNotifier).getUpdateSince()).value, 1n); + // B gets to start it + await E(pricePushAdminB).pushResult({ roundId: 2, data: '1000' }); + // now it's roundId=2 + t.is((await E(roundStartNotifier).getUpdateSince()).value, 2n); + // A joins in + await E(pricePushAdminA).pushResult({ roundId: 2, data: '1000' }); + + // A can start again + await E(pricePushAdminA).pushResult({ roundId: 3, data: '1000' }); + t.is((await E(roundStartNotifier).getUpdateSince()).value, 3n); +}); From 9e2617b02c2be9465b6df328bbef8145a3ab8901 Mon Sep 17 00:00:00 2001 From: Turadg Aleahmad Date: Wed, 7 Dec 2022 14:26:53 -0800 Subject: [PATCH 12/26] feat(rpc): publish latestRound --- .../agoric-cli/test/agops-oracle-smoketest.sh | 4 +- .../src/contracts/priceAggregatorChainlink.js | 39 +++++++---- .../test-priceAggregatorChainlink.js | 66 +++++++++++++++++-- 3 files changed, 88 insertions(+), 21 deletions(-) diff --git a/packages/agoric-cli/test/agops-oracle-smoketest.sh b/packages/agoric-cli/test/agops-oracle-smoketest.sh index ab318b76b5f..482e8a85b4e 100644 --- a/packages/agoric-cli/test/agops-oracle-smoketest.sh +++ b/packages/agoric-cli/test/agops-oracle-smoketest.sh @@ -45,5 +45,5 @@ agoric wallet send --from "$WALLET" --offer "$PROPOSAL_OFFER" echo "Offer $ORACLE_OFFER_ID should have numWantsSatisfied: 1" agoric wallet show --from "$WALLET" -# see new price -agoric follow :published.priceFeed.ATOM-USD_price_feed +# verify that the round started +agoric follow :published.priceFeed.ATOM-USD_price_feed.latestRound diff --git a/packages/zoe/src/contracts/priceAggregatorChainlink.js b/packages/zoe/src/contracts/priceAggregatorChainlink.js index c938652fb98..91e67369777 100644 --- a/packages/zoe/src/contracts/priceAggregatorChainlink.js +++ b/packages/zoe/src/contracts/priceAggregatorChainlink.js @@ -1,7 +1,7 @@ import { AmountMath, AssetKind, makeIssuerKit } from '@agoric/ertp'; import { E } from '@endo/eventual-send'; import { Far } from '@endo/marshal'; -import { makeNotifierKit } from '@agoric/notifier'; +import { makeNotifierKit, makeStoredPublishKit } from '@agoric/notifier'; import { makeLegacyMap } from '@agoric/store'; import { Nat, isNat } from '@agoric/nat'; import { TimeMath } from '@agoric/swingset-vat/src/vats/timer/timeMath.js'; @@ -37,6 +37,10 @@ const { add, subtract, multiply, floorDivide, ceilDivide, isGTE } = natSafeMath; * and was completed regularly. */ +/** + * @typedef {Pick} LatestRound + */ + // Partly documented at https://github.com/smartcontractkit/chainlink/blob/b045416ebca769aa69bde2da23b5109abe07a8b5/contracts/src/v0.6/FluxAggregator.sol#L153 /** * @typedef {object} ChainlinkConfig @@ -62,13 +66,13 @@ const { add, subtract, multiply, floorDivide, ceilDivide, isGTE } = natSafeMath; * brandOut: Brand<'nat'>, * unitAmountIn?: Amount<'nat'>, * }>} zcf - * @param {object} root0 - * @param {ERef>} [root0.quoteMint] + * @param {{ + * marshaller: Marshaller, + * quoteMint?: ERef>, + * storageNode: ERef, + * }} privateArgs */ -const start = async ( - zcf, - { quoteMint = makeIssuerKit('quote', AssetKind.SET).mint } = {}, -) => { +const start = async (zcf, privateArgs) => { // brands come from named terms instead of `brands` key because the latter is // a StandardTerm that Zoe creates from the `issuerKeywordRecord` argument and // Oracle brands are inert (without issuers or mints). @@ -96,6 +100,8 @@ const start = async ( // Get the timer's identity. const timerPresence = await timer; + const quoteMint = + privateArgs.quoteMint || makeIssuerKit('quote', AssetKind.SET).mint; const quoteIssuerRecord = await zcf.saveIssuer( E(quoteMint).getIssuer(), 'Quote', @@ -111,10 +117,14 @@ const start = async ( // --- [begin] Chainlink specific values /** @type {bigint} */ let reportingRoundId = 0n; - const { notifier: roundStartNotifier, updater: roundStartUpdater } = - makeNotifierKit( - // Start with the first round. - add(reportingRoundId, 1), + + const { marshaller, storageNode } = privateArgs; + assertDefined({ marshaller, storageNode }); + /** @type {PublishKit} */ + const { publisher: latestRoundPublisher, subscriber: latestRoundSubscriber } = + makeStoredPublishKit( + E(storageNode).makeChildNode('latestRound'), + marshaller, ); /** @type {LegacyMap>} */ @@ -370,7 +380,10 @@ const start = async ( updateTimedOutRoundInfo(subtract(roundId, 1), blockTimestamp); reportingRoundId = roundId; - roundStartUpdater.updateState(reportingRoundId); + latestRoundPublisher.publish({ + roundId: reportingRoundId, + startedAt: blockTimestamp, + }); details.init( roundId, @@ -867,7 +880,7 @@ const start = async ( return priceAuthority; }, getRoundStartNotifier() { - return roundStartNotifier; + return latestRoundSubscriber; }, }); diff --git a/packages/zoe/test/unitTests/contracts/test-priceAggregatorChainlink.js b/packages/zoe/test/unitTests/contracts/test-priceAggregatorChainlink.js index 5fd155bb9d8..78ae8df0227 100644 --- a/packages/zoe/test/unitTests/contracts/test-priceAggregatorChainlink.js +++ b/packages/zoe/test/unitTests/contracts/test-priceAggregatorChainlink.js @@ -9,6 +9,9 @@ import { E } from '@endo/eventual-send'; import { Far } from '@endo/marshal'; import { makeIssuerKit, AssetKind, AmountMath } from '@agoric/ertp'; +import { makeFakeMarshaller } from '@agoric/notifier/tools/testSupports.js'; +import { makeMockChainStorageRoot } from '@agoric/vats/tools/storage-test-utils.js'; +import { subscribeEach } from '@agoric/notifier'; import { makeFakeVatAdmin } from '../../../tools/fakeVatAdmin.js'; import { makeZoeKit } from '../../../src/zoeService/zoe.js'; import buildManualTimer from '../../../tools/manualTimer.js'; @@ -94,6 +97,11 @@ const makeContext = async () => { minSubmissionValue, maxSubmissionValue, ) => { + // ??? why do we need the Far here and not in VaultFactory tests? + const marshaller = Far('fake marshaller', { ...makeFakeMarshaller() }); + const mockStorageRoot = makeMockChainStorageRoot(); + const storageNode = E(mockStorageRoot).makeChildNode('priceAggregator'); + const timer = buildManualTimer(() => {}); const aggregator = await E(zoe).startInstance( @@ -110,8 +118,12 @@ const makeContext = async () => { minSubmissionValue, maxSubmissionValue, }, + { + marshaller, + storageNode: E(storageNode).makeChildNode('LINK-USD_price_feed'), + }, ); - return aggregator; + return { ...aggregator, mockStorageRoot }; }; return { makeChainlinkAggregator, makeFakePriceOracle, zoe }; @@ -741,26 +753,68 @@ test('notifications', async t => { priceOracleB.instance, ); - const roundStartNotifier = await E( + const latestRoundSubscriber = await E( aggregator.publicFacet, ).getRoundStartNotifier(); + const eachLatestRound = subscribeEach(latestRoundSubscriber)[ + Symbol.asyncIterator + ](); await oracleTimer.tick(); await E(pricePushAdminA).pushResult({ roundId: 1, data: '100' }); - t.is((await E(roundStartNotifier).getUpdateSince()).value, 1n); + t.deepEqual((await eachLatestRound.next()).value, { + roundId: 1n, + startedAt: 1n, + }); + // t.deepEqual( + // aggregator.mockStorageRoot.getBody( + // 'mockChainStorageRoot.priceAggregator.LINK-USD_price_feed', + // ), + // { + // quoteAmount: { + // brand: { iface: 'Alleged: quote brand' }, + // value: [ + // { + // amountIn: { brand: { iface: 'Alleged: $LINK brand' }, value: 1n }, + // amountOut: { + // brand: { iface: 'Alleged: $USD brand' }, + // value: 1020n, + // }, + // timer: { iface: 'Alleged: ManualTimer' }, + // timestamp: 1n, + // }, + // ], + // }, + // quotePayment: { iface: 'Alleged: quote payment' }, + // }, + // ); + await E(pricePushAdminB).pushResult({ roundId: 1, data: '200' }); await E(pricePushAdminA).pushResult({ roundId: 2, data: '1000' }); // A started last round so fails to start next round - t.is((await E(roundStartNotifier).getUpdateSince()).value, 1n); + t.deepEqual( + // subscribe fresh because the iterator won't advance yet + (await latestRoundSubscriber.subscribeAfter()).head.value, + { + roundId: 1n, + startedAt: 1n, + }, + ); // B gets to start it await E(pricePushAdminB).pushResult({ roundId: 2, data: '1000' }); // now it's roundId=2 - t.is((await E(roundStartNotifier).getUpdateSince()).value, 2n); + t.deepEqual((await eachLatestRound.next()).value, { + roundId: 2n, + startedAt: 1n, + }); // A joins in await E(pricePushAdminA).pushResult({ roundId: 2, data: '1000' }); // A can start again await E(pricePushAdminA).pushResult({ roundId: 3, data: '1000' }); - t.is((await E(roundStartNotifier).getUpdateSince()).value, 3n); + t.deepEqual((await eachLatestRound.next()).value, { + roundId: 3n, + startedAt: 1n, + }); }); From 4899d40fcb9a56532d584e0b05b3824f5e25061f Mon Sep 17 00:00:00 2001 From: Turadg Aleahmad Date: Wed, 7 Dec 2022 14:57:18 -0800 Subject: [PATCH 13/26] test: chainlink-style price feed --- .../inter-protocol/scripts/price-feed-core.js | 4 +-- .../src/proposals/price-feed-proposal.js | 12 ++++--- .../test/smartWallet/contexts.js | 11 ++++-- .../smartWallet/test-oracle-integration.js | 32 +++++++---------- .../src/contracts/priceAggregatorChainlink.js | 35 +++++++++++++------ 5 files changed, 54 insertions(+), 40 deletions(-) diff --git a/packages/inter-protocol/scripts/price-feed-core.js b/packages/inter-protocol/scripts/price-feed-core.js index 0251ef641cb..21d6572fb19 100644 --- a/packages/inter-protocol/scripts/price-feed-core.js +++ b/packages/inter-protocol/scripts/price-feed-core.js @@ -57,8 +57,8 @@ export const defaultProposalBuilder = async ( brandOutRef: brandOut && publishRef(brandOut), priceAggregatorRef: publishRef( install( - '@agoric/zoe/src/contracts/priceAggregator.js', - '../bundles/bundle-priceAggregator.js', + '@agoric/zoe/src/contracts/priceAggregatorChainlink.js', + '../bundles/bundle-priceAggregatorChainlink.js', ), ), }, diff --git a/packages/inter-protocol/src/proposals/price-feed-proposal.js b/packages/inter-protocol/src/proposals/price-feed-proposal.js index 761c07af4c1..3b375bbfc33 100644 --- a/packages/inter-protocol/src/proposals/price-feed-proposal.js +++ b/packages/inter-protocol/src/proposals/price-feed-proposal.js @@ -87,7 +87,7 @@ export const ensureOracleBrands = async ( /** * @param {ChainBootstrapSpace} powers - * @param {{options: {priceFeedOptions: {AGORIC_INSTANCE_NAME: string, oracleAddresses: string[], contractTerms: unknown, IN_BRAND_NAME: string, OUT_BRAND_NAME: string}}}} config + * @param {{options: {priceFeedOptions: {AGORIC_INSTANCE_NAME: string, oracleAddresses: string[], contractTerms: import('@agoric/zoe/src/contracts/priceAggregatorChainlink.js').ChainlinkConfig, IN_BRAND_NAME: string, OUT_BRAND_NAME: string}}}} config */ export const createPriceFeed = async ( { @@ -129,7 +129,7 @@ export const createPriceFeed = async ( /** * Values come from economy-template.json, which at this writing had IN:ATOM, OUT:USD * - * @type {[[Brand<'nat'>, Brand<'nat'>], [Installation]]} + * @type {[[Brand<'nat'>, Brand<'nat'>], [Installation]]} */ const [[brandIn, brandOut], [priceAggregator]] = await Promise.all([ reserveThenGetNames(E(agoricNamesAdmin).lookupAdmin('oracleBrand'), [ @@ -142,7 +142,6 @@ export const createPriceFeed = async ( ]); const unitAmountIn = await unitAmount(brandIn); - /** @type {import('@agoric/zoe/src/contracts/priceAggregator.js').PriceAggregatorContract['terms']} */ const terms = await deeplyFulfilledObject( harden({ ...contractTerms, @@ -306,7 +305,12 @@ export const startPriceFeeds = async ( priceFeedOptions: { AGORIC_INSTANCE_NAME: `${inBrandName}-${outBrandName} price feed`, contractTerms: { - POLL_INTERVAL: 1n, + minSubmissionCount: 2, + minSubmissionValue: 1, + maxSubmissionCount: 5, + maxSubmissionValue: 99999, + restartDelay: 1n, + timeout: 10, }, oracleAddresses: demoOracleAddresses, IN_BRAND_NAME: inBrandName, diff --git a/packages/inter-protocol/test/smartWallet/contexts.js b/packages/inter-protocol/test/smartWallet/contexts.js index 92c76aef316..47f5c78051a 100644 --- a/packages/inter-protocol/test/smartWallet/contexts.js +++ b/packages/inter-protocol/test/smartWallet/contexts.js @@ -73,10 +73,10 @@ export const makeDefaultTestContext = async (t, makeSpace) => { 'installation', ); const paBundle = await bundleCache.load( - '../zoe/src/contracts/priceAggregator.js', + '../zoe/src/contracts/priceAggregatorChainlink.js', 'priceAggregator', ); - /** @type {Promise>} */ + /** @type {Promise>} */ const paInstallation = E(zoe).install(paBundle); await E(installAdmin).update('priceAggregator', paInstallation); @@ -87,7 +87,12 @@ export const makeDefaultTestContext = async (t, makeSpace) => { priceFeedOptions: { AGORIC_INSTANCE_NAME: `${inBrandName}-${outBrandName} price feed`, contractTerms: { - POLL_INTERVAL: 1n, + minSubmissionCount: 2, + minSubmissionValue: 1, + maxSubmissionCount: 5, + maxSubmissionValue: 99999, + restartDelay: 1n, + timeout: 10, }, oracleAddresses, IN_BRAND_NAME: inBrandName, diff --git a/packages/inter-protocol/test/smartWallet/test-oracle-integration.js b/packages/inter-protocol/test/smartWallet/test-oracle-integration.js index 419ee2f840e..c7321a7d70e 100644 --- a/packages/inter-protocol/test/smartWallet/test-oracle-integration.js +++ b/packages/inter-protocol/test/smartWallet/test-oracle-integration.js @@ -10,13 +10,12 @@ import { import { eventLoopIteration } from '@agoric/zoe/tools/eventLoopIteration.js'; import { E } from '@endo/far'; -import { INVITATION_MAKERS_DESC } from '@agoric/zoe/src/contracts/priceAggregator.js'; -import buildManualTimer from '@agoric/zoe/tools/manualTimer.js'; -import { AmountMath } from '@agoric/ertp'; import { coalesceUpdates } from '@agoric/smart-wallet/src/utils.js'; +import { INVITATION_MAKERS_DESC } from '@agoric/zoe/src/contracts/priceAggregatorChainlink.js'; +import buildManualTimer from '@agoric/zoe/tools/manualTimer.js'; import { ensureOracleBrands } from '../../src/proposals/price-feed-proposal.js'; -import { makeDefaultTestContext } from './contexts.js'; import { headValue } from '../supports.js'; +import { makeDefaultTestContext } from './contexts.js'; /** * @type {import('ava').TestFn> @@ -87,18 +86,15 @@ test('admin price', async t => { const currentSub = E(wallet).getCurrentSubscriber(); await t.context.simpleCreatePriceFeed([operatorAddress], 'ATOM', 'USD'); - const atomBrand = await E(agoricNames).lookup('oracleBrand', 'ATOM'); - const usdBrand = await E(agoricNames).lookup('oracleBrand', 'USD'); const offersFacet = wallet.getOffersFacet(); + /** @type {import('@agoric/zoe/src/zoeService/utils.js').Instance} */ const priceAggregator = await E(agoricNames).lookup( 'instance', 'ATOM-USD price feed', ); - /** @type {import('@agoric/zoe/src/contracts/priceAggregator.js').PriceAggregatorContract['publicFacet']} */ const paPublicFacet = await E(zoe).getPublicFacet(priceAggregator); - const priceAuthority = await E(paPublicFacet).getPriceAuthority(); /** * get invitation details the way a user would @@ -159,12 +155,15 @@ test('admin price', async t => { // Push a new price result ///////////////////////// + /** @type {import('@agoric/zoe/src/contracts/priceAggregatorChainlink.js').PriceRound} */ + const result = { roundId: 1, data: '123' }; + /** @type {import('@agoric/smart-wallet/src/invitations.js').ContinuingInvitationSpec} */ const proposeInvitationSpec = { source: 'continuing', previousOffer: 44, invitationMakerName: 'makePushPriceInvitation', - invitationArgs: harden([123n]), + invitationArgs: harden([result]), }; /** @type {import('@agoric/smart-wallet/src/offers').OfferSpec} */ @@ -185,17 +184,10 @@ test('admin price', async t => { // trigger an aggregation (POLL_INTERVAL=1n in context) E(manualTimer).tickN(1); - const quote = await priceAuthority.quoteGiven( - AmountMath.make(atomBrand, 1_000n), - usdBrand, - ); + const latestRoundSubscriber = await E(paPublicFacet).getRoundStartNotifier(); - t.deepEqual(quote.quoteAmount.value[0].amountIn, { - brand: atomBrand, - value: 1_000n, - }); - t.deepEqual(quote.quoteAmount.value[0].amountOut, { - brand: usdBrand, - value: 123_000n, + t.deepEqual((await latestRoundSubscriber.subscribeAfter()).head.value, { + roundId: 1n, + startedAt: 0n, }); }); diff --git a/packages/zoe/src/contracts/priceAggregatorChainlink.js b/packages/zoe/src/contracts/priceAggregatorChainlink.js index 91e67369777..9eb18bdd5ac 100644 --- a/packages/zoe/src/contracts/priceAggregatorChainlink.js +++ b/packages/zoe/src/contracts/priceAggregatorChainlink.js @@ -6,6 +6,7 @@ import { makeLegacyMap } from '@agoric/store'; import { Nat, isNat } from '@agoric/nat'; import { TimeMath } from '@agoric/swingset-vat/src/vats/timer/timeMath.js'; import { Fail } from '@agoric/assert'; +import { assertAllDefined } from '@agoric/internal'; import { calculateMedian, natSafeMath, @@ -14,7 +15,9 @@ import { import '../../tools/types.js'; import { assertParsableNumber } from '../contractSupport/ratio.js'; -import { INVITATION_MAKERS_DESC } from './priceAggregator'; +import { INVITATION_MAKERS_DESC } from './priceAggregator.js'; + +export { INVITATION_MAKERS_DESC }; /** * @typedef {{ roundId: number | undefined, data: string }} PriceRound @@ -77,23 +80,31 @@ const start = async (zcf, privateArgs) => { // a StandardTerm that Zoe creates from the `issuerKeywordRecord` argument and // Oracle brands are inert (without issuers or mints). const { - timer, brandIn, brandOut, maxSubmissionCount, + maxSubmissionValue, minSubmissionCount, + minSubmissionValue, restartDelay, timeout, - minSubmissionValue, - maxSubmissionValue, + timer, unitAmountIn = AmountMath.make(brandIn, 1n), } = zcf.getTerms(); - // TODO helper like: assertDefined({ brandIn, brandOut, timer, unitAmountIn }) - assert(brandIn, 'missing brandIn'); - assert(brandOut, 'missing brandOut'); - assert(timer, 'missing timer'); - assert(unitAmountIn, 'missing unitAmountIn'); + + assertAllDefined({ + brandIn, + brandOut, + maxSubmissionCount, + maxSubmissionValue, + minSubmissionCount, + minSubmissionValue, + restartDelay, + timeout, + timer, + unitAmountIn, + }); const unitIn = AmountMath.getValue(brandIn, unitAmountIn); @@ -119,7 +130,7 @@ const start = async (zcf, privateArgs) => { let reportingRoundId = 0n; const { marshaller, storageNode } = privateArgs; - assertDefined({ marshaller, storageNode }); + assertAllDefined({ marshaller, storageNode }); /** @type {PublishKit} */ const { publisher: latestRoundPublisher, subscriber: latestRoundSubscriber } = makeStoredPublishKit( @@ -743,6 +754,8 @@ const start = async (zcf, privateArgs) => { const parsedSubmission = Nat(parseInt(submissionRaw, 10)); const blockTimestamp = await E(timer).getCurrentTimestamp(); + console.log('DEBUG pushResult', { parsedSubmission, blockTimestamp }); + let roundId; if (roundIdRaw === undefined) { const suggestedRound = oracleRoundStateSuggestRound( @@ -758,7 +771,7 @@ const start = async (zcf, privateArgs) => { const error = validateOracleRound(oracleKey, roundId, blockTimestamp); if (!(parsedSubmission >= minSubmissionValue)) { - console.error('value below minSubmissionValue'); + console.error('value below minSubmissionValue', minSubmissionValue); return; } From efe9d1171f12e265f55297072648980baa167202 Mon Sep 17 00:00:00 2001 From: Turadg Aleahmad Date: Thu, 8 Dec 2022 11:48:35 -0800 Subject: [PATCH 14/26] style: organize imports --- packages/zoe/src/contracts/priceAggregator.js | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/packages/zoe/src/contracts/priceAggregator.js b/packages/zoe/src/contracts/priceAggregator.js index 8b4a5ac52be..c5e7350db9e 100644 --- a/packages/zoe/src/contracts/priceAggregator.js +++ b/packages/zoe/src/contracts/priceAggregator.js @@ -1,33 +1,34 @@ /// -import { makeIssuerKit, AssetKind, AmountMath } from '@agoric/ertp'; +import { AmountMath, AssetKind, makeIssuerKit } from '@agoric/ertp'; import { assertAllDefined } from '@agoric/internal'; -import { E } from '@endo/eventual-send'; -import { Far } from '@endo/marshal'; import { makeNotifierKit, makeStoredPublishKit, observeNotifier, } from '@agoric/notifier'; import { makeLegacyMap } from '@agoric/store'; +import { E } from '@endo/eventual-send'; +import { Far } from '@endo/marshal'; + +import '../../tools/types.js'; import { calculateMedian, makeOnewayPriceAuthorityKit, } from '../contractSupport/index.js'; import { + addRatios, + assertParsableNumber, + ceilDivideBy, + floorMultiplyBy, makeRatio, makeRatioFromAmounts, + multiplyRatios, parseRatio, - addRatios, ratioGTE, - floorMultiplyBy, - ceilDivideBy, - multiplyRatios, ratiosSame, - assertParsableNumber, } from '../contractSupport/ratio.js'; -import '../../tools/types.js'; import '@agoric/ertp/src/types-ambient.js'; export const INVITATION_MAKERS_DESC = 'oracle invitation'; From 8f7601c215003a8bb966a1ff7517985a158627ba Mon Sep 17 00:00:00 2001 From: Turadg Aleahmad Date: Thu, 8 Dec 2022 12:35:06 -0800 Subject: [PATCH 15/26] test: refactor to named params --- .../test-priceAggregatorChainlink.js | 188 ++++++------------ 1 file changed, 58 insertions(+), 130 deletions(-) diff --git a/packages/zoe/test/unitTests/contracts/test-priceAggregatorChainlink.js b/packages/zoe/test/unitTests/contracts/test-priceAggregatorChainlink.js index 78ae8df0227..c7c071e9b3c 100644 --- a/packages/zoe/test/unitTests/contracts/test-priceAggregatorChainlink.js +++ b/packages/zoe/test/unitTests/contracts/test-priceAggregatorChainlink.js @@ -18,7 +18,7 @@ import buildManualTimer from '../../../tools/manualTimer.js'; import '../../../src/contracts/exported.js'; -/** @type {import('ava').TestFn>>} */ +/** @type {import('ava').TestFn>>} */ const test = unknownTest; const filename = new URL(import.meta.url).pathname; @@ -27,6 +27,14 @@ const dirname = path.dirname(filename); const oraclePath = `${dirname}/../../../src/contracts/oracle.js`; const aggregatorPath = `${dirname}/../../../src/contracts/priceAggregatorChainlink.js`; +const defaultConfig = { + maxSubmissionCount: 1000, + minSubmissionCount: 2, + restartDelay: 5, + timeout: 10, + minSubmissionValue: 100, + maxSubmissionValue: 10000, +}; const makeContext = async () => { // Outside of tests, we should use the long-lived Zoe on the // testnet. In this test, we must create a new Zoe. @@ -89,14 +97,16 @@ const makeContext = async () => { }); }; - const makeChainlinkAggregator = async ( - maxSubmissionCount, - minSubmissionCount, - restartDelay, - timeout, - minSubmissionValue, - maxSubmissionValue, - ) => { + async function makeChainlinkAggregator(config) { + const { + maxSubmissionCount, + maxSubmissionValue, + minSubmissionCount, + minSubmissionValue, + restartDelay, + timeout, + } = config; + // ??? why do we need the Far here and not in VaultFactory tests? const marshaller = Far('fake marshaller', { ...makeFakeMarshaller() }); const mockStorageRoot = makeMockChainStorageRoot(); @@ -124,7 +134,7 @@ const makeContext = async () => { }, ); return { ...aggregator, mockStorageRoot }; - }; + } return { makeChainlinkAggregator, makeFakePriceOracle, zoe }; }; @@ -136,21 +146,7 @@ test.before('setup aggregator and oracles', async t => { test('basic', async t => { const { makeFakePriceOracle, zoe } = t.context; - const maxSubmissionCount = 1000; - const minSubmissionCount = 2; - const restartDelay = 5; - const timeout = 10; - const minSubmissionValue = 100; - const maxSubmissionValue = 10000; - - const aggregator = await t.context.makeChainlinkAggregator( - maxSubmissionCount, - minSubmissionCount, - restartDelay, - timeout, - minSubmissionValue, - maxSubmissionValue, - ); + const aggregator = await t.context.makeChainlinkAggregator(defaultConfig); /** @type {{ timer: ManualTimer }} */ // @ts-expect-error cast const { timer: oracleTimer } = await E(zoe).getTerms(aggregator.instance); @@ -213,21 +209,11 @@ test('basic', async t => { test('timeout', async t => { const { makeFakePriceOracle, zoe } = t.context; - const maxSubmissionCount = 1000; - const minSubmissionCount = 2; - const restartDelay = 2; - const timeout = 5; - const minSubmissionValue = 100; - const maxSubmissionValue = 10000; - - const aggregator = await t.context.makeChainlinkAggregator( - maxSubmissionCount, - minSubmissionCount, - restartDelay, - timeout, - minSubmissionValue, - maxSubmissionValue, - ); + const aggregator = await t.context.makeChainlinkAggregator({ + ...defaultConfig, + restartDelay: 2, + timeout: 5, + }); /** @type {{ timer: ManualTimer }} */ // @ts-expect-error cast const { timer: oracleTimer } = await E(zoe).getTerms(aggregator.instance); @@ -283,21 +269,10 @@ test('timeout', async t => { test('issue check', async t => { const { makeFakePriceOracle, zoe } = t.context; - const maxSubmissionCount = 1000; - const minSubmissionCount = 2; - const restartDelay = 2; - const timeout = 5; - const minSubmissionValue = 100; - const maxSubmissionValue = 10000; - - const aggregator = await t.context.makeChainlinkAggregator( - maxSubmissionCount, - minSubmissionCount, - restartDelay, - timeout, - minSubmissionValue, - maxSubmissionValue, - ); + const aggregator = await t.context.makeChainlinkAggregator({ + ...defaultConfig, + restartDelay: 2, + }); /** @type {{ timer: ManualTimer }} */ // @ts-expect-error cast const { timer: oracleTimer } = await E(zoe).getTerms(aggregator.instance); @@ -341,21 +316,10 @@ test('issue check', async t => { test('supersede', async t => { const { makeFakePriceOracle, zoe } = t.context; - const maxSubmissionCount = 1000; - const minSubmissionCount = 2; - const restartDelay = 1; - const timeout = 5; - const minSubmissionValue = 100; - const maxSubmissionValue = 10000; - - const aggregator = await t.context.makeChainlinkAggregator( - maxSubmissionCount, - minSubmissionCount, - restartDelay, - timeout, - minSubmissionValue, - maxSubmissionValue, - ); + const aggregator = await t.context.makeChainlinkAggregator({ + ...defaultConfig, + restartDelay: 1, + }); /** @type {{ timer: ManualTimer }} */ // @ts-expect-error cast const { timer: oracleTimer } = await E(zoe).getTerms(aggregator.instance); @@ -407,21 +371,13 @@ test('supersede', async t => { test('interleaved', async t => { const { makeFakePriceOracle, zoe } = t.context; - const maxSubmissionCount = 3; - const minSubmissionCount = 3; // requires ALL the oracles for consensus in this case - const restartDelay = 1; - const timeout = 5; - const minSubmissionValue = 100; - const maxSubmissionValue = 10000; - - const aggregator = await t.context.makeChainlinkAggregator( - maxSubmissionCount, - minSubmissionCount, - restartDelay, - timeout, - minSubmissionValue, - maxSubmissionValue, - ); + const aggregator = await t.context.makeChainlinkAggregator({ + ...defaultConfig, + maxSubmissionCount: 3, + minSubmissionCount: 3, // requires ALL the oracles for consensus in this case + restartDelay: 1, + timeout: 5, + }); /** @type {{ timer: ManualTimer }} */ // @ts-expect-error cast const { timer: oracleTimer } = await E(zoe).getTerms(aggregator.instance); @@ -545,21 +501,12 @@ test('interleaved', async t => { test('larger', async t => { const { makeFakePriceOracle, zoe } = t.context; - const maxSubmissionCount = 1000; - const minSubmissionCount = 3; - const restartDelay = 1; - const timeout = 5; - const minSubmissionValue = 100; - const maxSubmissionValue = 10000; - - const aggregator = await t.context.makeChainlinkAggregator( - maxSubmissionCount, - minSubmissionCount, - restartDelay, - timeout, - minSubmissionValue, - maxSubmissionValue, - ); + const aggregator = await t.context.makeChainlinkAggregator({ + ...defaultConfig, + minSubmissionCount: 3, + restartDelay: 1, + timeout: 5, + }); /** @type {{ timer: ManualTimer }} */ // @ts-expect-error cast const { timer: oracleTimer } = await E(zoe).getTerms(aggregator.instance); @@ -628,21 +575,12 @@ test('larger', async t => { test('suggest', async t => { const { makeFakePriceOracle, zoe } = t.context; - const maxSubmissionCount = 1000; - const minSubmissionCount = 3; - const restartDelay = 1; - const timeout = 5; - const minSubmissionValue = 100; - const maxSubmissionValue = 10000; - - const aggregator = await t.context.makeChainlinkAggregator( - maxSubmissionCount, - minSubmissionCount, - restartDelay, - timeout, - minSubmissionValue, - maxSubmissionValue, - ); + const aggregator = await t.context.makeChainlinkAggregator({ + ...defaultConfig, + minSubmissionCount: 3, + restartDelay: 1, + timeout: 5, + }); /** @type {{ timer: ManualTimer }} */ // @ts-expect-error cast const { timer: oracleTimer } = await E(zoe).getTerms(aggregator.instance); @@ -724,21 +662,11 @@ test('suggest', async t => { test('notifications', async t => { const { makeFakePriceOracle, zoe } = t.context; - const maxSubmissionCount = 1000; - const minSubmissionCount = 2; - const restartDelay = 1; // have to alternate to start rounds - const timeout = 10; - const minSubmissionValue = 100; - const maxSubmissionValue = 10000; - - const aggregator = await t.context.makeChainlinkAggregator( - maxSubmissionCount, - minSubmissionCount, - restartDelay, - timeout, - minSubmissionValue, - maxSubmissionValue, - ); + const aggregator = await t.context.makeChainlinkAggregator({ + ...defaultConfig, + maxSubmissionCount: 1000, + restartDelay: 1, // have to alternate to start rounds + }); /** @type {{ timer: ManualTimer }} */ // @ts-expect-error cast const { timer: oracleTimer } = await E(zoe).getTerms(aggregator.instance); From e3e3984b389a143bccd084773474e27d94745561 Mon Sep 17 00:00:00 2001 From: Turadg Aleahmad Date: Thu, 8 Dec 2022 12:35:15 -0800 Subject: [PATCH 16/26] feat: publish price quotes --- .../src/contracts/priceAggregatorChainlink.js | 24 ++++++++++++++++++- .../test-priceAggregatorChainlink.js | 22 +++++++++++++++++ 2 files changed, 45 insertions(+), 1 deletion(-) diff --git a/packages/zoe/src/contracts/priceAggregatorChainlink.js b/packages/zoe/src/contracts/priceAggregatorChainlink.js index 9eb18bdd5ac..95f5e1b526d 100644 --- a/packages/zoe/src/contracts/priceAggregatorChainlink.js +++ b/packages/zoe/src/contracts/priceAggregatorChainlink.js @@ -1,7 +1,11 @@ import { AmountMath, AssetKind, makeIssuerKit } from '@agoric/ertp'; import { E } from '@endo/eventual-send'; import { Far } from '@endo/marshal'; -import { makeNotifierKit, makeStoredPublishKit } from '@agoric/notifier'; +import { + makeNotifierKit, + makeStoredPublishKit, + observeNotifier, +} from '@agoric/notifier'; import { makeLegacyMap } from '@agoric/store'; import { Nat, isNat } from '@agoric/nat'; import { TimeMath } from '@agoric/swingset-vat/src/vats/timer/timeMath.js'; @@ -131,6 +135,12 @@ const start = async (zcf, privateArgs) => { const { marshaller, storageNode } = privateArgs; assertAllDefined({ marshaller, storageNode }); + + // For publishing priceAuthority values to off-chain storage + /** @type {PublishKit} */ + const { publisher: quotePublisher, subscriber: quoteSubscriber } = + makeStoredPublishKit(storageNode, marshaller); + /** @type {PublishKit} */ const { publisher: latestRoundPublisher, subscriber: latestRoundSubscriber } = makeStoredPublishKit( @@ -324,6 +334,17 @@ const start = async (zcf, privateArgs) => { actualBrandOut: brandOut, }); + // for each new quote from the priceAuthority, publish it to off-chain storage + observeNotifier(priceAuthority.makeQuoteNotifier(unitAmountIn, brandOut), { + updateState: quote => quotePublisher.publish(quote), + fail: reason => { + throw Error(`priceAuthority observer failed: ${reason}`); + }, + finish: done => { + throw Error(`priceAuthority observer died: ${done}`); + }, + }); + /** * @param {bigint} roundId * @param {Timestamp} blockTimestamp @@ -892,6 +913,7 @@ const start = async (zcf, privateArgs) => { getPriceAuthority() { return priceAuthority; }, + getSubscriber: () => quoteSubscriber, getRoundStartNotifier() { return latestRoundSubscriber; }, diff --git a/packages/zoe/test/unitTests/contracts/test-priceAggregatorChainlink.js b/packages/zoe/test/unitTests/contracts/test-priceAggregatorChainlink.js index c7c071e9b3c..32aec41205b 100644 --- a/packages/zoe/test/unitTests/contracts/test-priceAggregatorChainlink.js +++ b/packages/zoe/test/unitTests/contracts/test-priceAggregatorChainlink.js @@ -35,6 +35,17 @@ const defaultConfig = { minSubmissionValue: 100, maxSubmissionValue: 10000, }; + +/** + * + * @param {Promise>} subscriber + */ +export const subscriberSubkey = subscriber => { + return E(subscriber) + .getStoreKey() + .then(storeKey => storeKey.storeSubkey); +}; + const makeContext = async () => { // Outside of tests, we should use the long-lived Zoe on the // testnet. In this test, we must create a new Zoe. @@ -746,3 +757,14 @@ test('notifications', async t => { startedAt: 1n, }); }); + +test('storage keys', async t => { + const { publicFacet } = await t.context.makeChainlinkAggregator( + defaultConfig, + ); + + t.is( + await subscriberSubkey(E(publicFacet).getSubscriber()), + 'fake:mockChainStorageRoot.priceAggregator.LINK-USD_price_feed', + ); +}); From 30d3966755900869dd6332a974077073f201983c Mon Sep 17 00:00:00 2001 From: Turadg Aleahmad Date: Thu, 8 Dec 2022 13:50:12 -0800 Subject: [PATCH 17/26] feat!: publish PriceDescription instead of PriceQuote --- packages/zoe/src/contracts/priceAggregator.js | 5 +- .../src/contracts/priceAggregatorChainlink.js | 23 +++++--- .../contracts/test-priceAggregator.js | 25 +++------ .../test-priceAggregatorChainlink.js | 52 +++++++++++-------- 4 files changed, 56 insertions(+), 49 deletions(-) diff --git a/packages/zoe/src/contracts/priceAggregator.js b/packages/zoe/src/contracts/priceAggregator.js index c5e7350db9e..6e468f971af 100644 --- a/packages/zoe/src/contracts/priceAggregator.js +++ b/packages/zoe/src/contracts/priceAggregator.js @@ -35,6 +35,9 @@ export const INVITATION_MAKERS_DESC = 'oracle invitation'; /** @typedef {ParsableNumber | Ratio} Price */ +/** @type {(quote: PriceQuote) => PriceDescription} */ +export const priceDescriptionFromQuote = quote => quote.quoteAmount.value[0]; + /** * @deprecated use priceAggregatorChainlink * @@ -220,7 +223,7 @@ const start = async (zcf, privateArgs) => { // for each new quote from the priceAuthority, publish it to off-chain storage observeNotifier(priceAuthority.makeQuoteNotifier(unitAmountIn, brandOut), { - updateState: quote => publisher.publish(quote), + updateState: quote => publisher.publish(priceDescriptionFromQuote(quote)), fail: reason => { throw Error(`priceAuthority observer failed: ${reason}`); }, diff --git a/packages/zoe/src/contracts/priceAggregatorChainlink.js b/packages/zoe/src/contracts/priceAggregatorChainlink.js index 95f5e1b526d..d7f188ce99c 100644 --- a/packages/zoe/src/contracts/priceAggregatorChainlink.js +++ b/packages/zoe/src/contracts/priceAggregatorChainlink.js @@ -19,7 +19,10 @@ import { import '../../tools/types.js'; import { assertParsableNumber } from '../contractSupport/ratio.js'; -import { INVITATION_MAKERS_DESC } from './priceAggregator.js'; +import { + INVITATION_MAKERS_DESC, + priceDescriptionFromQuote, +} from './priceAggregator.js'; export { INVITATION_MAKERS_DESC }; @@ -33,7 +36,7 @@ const { add, subtract, multiply, floorDivide, ceilDivide, isGTE } = natSafeMath; /** * @typedef {object} RoundData * @property {bigint} roundId the round ID for which data was retrieved - * @property {number} answer the answer for the given round + * @property {bigint} answer the answer for the given round * @property {Timestamp} startedAt the timestamp when the round was started. This is 0 * if the round hasn't been started yet. * @property {Timestamp} updatedAt the timestamp when the round last was updated (i.e. @@ -137,11 +140,11 @@ const start = async (zcf, privateArgs) => { assertAllDefined({ marshaller, storageNode }); // For publishing priceAuthority values to off-chain storage - /** @type {PublishKit} */ - const { publisher: quotePublisher, subscriber: quoteSubscriber } = + /** @type {StoredPublishKit} */ + const { publisher: pricePublisher, subscriber: quoteSubscriber } = makeStoredPublishKit(storageNode, marshaller); - /** @type {PublishKit} */ + /** @type {StoredPublishKit} */ const { publisher: latestRoundPublisher, subscriber: latestRoundSubscriber } = makeStoredPublishKit( E(storageNode).makeChildNode('latestRound'), @@ -165,7 +168,7 @@ const start = async (zcf, privateArgs) => { // --- [end] Chainlink specific values /** - * @param {number} answer + * @param {bigint} answer * @param {Timestamp} startedAt * @param {Timestamp} updatedAt * @param {bigint} answeredInRound @@ -336,7 +339,8 @@ const start = async (zcf, privateArgs) => { // for each new quote from the priceAuthority, publish it to off-chain storage observeNotifier(priceAuthority.makeQuoteNotifier(unitAmountIn, brandOut), { - updateState: quote => quotePublisher.publish(quote), + updateState: quote => + pricePublisher.publish(priceDescriptionFromQuote(quote)), fail: reason => { throw Error(`priceAuthority observer failed: ${reason}`); }, @@ -425,7 +429,7 @@ const start = async (zcf, privateArgs) => { rounds.init( roundId, makeRound( - /* answer = */ 0, + /* answer = */ 0n, /* startedAt = */ 0n, /* updatedAt = */ 0n, /* answeredInRound = */ 0n, @@ -485,6 +489,7 @@ const start = async (zcf, privateArgs) => { return [false, 0]; } + /** @type {bigint | undefined} */ const newAnswer = calculateMedian( details .get(roundId) @@ -492,6 +497,8 @@ const start = async (zcf, privateArgs) => { { add, divide: floorDivide, isGTE }, ); + assert(newAnswer, 'insufficient samples'); + rounds.get(roundId).answer = newAnswer; rounds.get(roundId).updatedAt = blockTimestamp; rounds.get(roundId).answeredInRound = roundId; diff --git a/packages/zoe/test/unitTests/contracts/test-priceAggregator.js b/packages/zoe/test/unitTests/contracts/test-priceAggregator.js index bbfccb08789..c23d6c061a2 100644 --- a/packages/zoe/test/unitTests/contracts/test-priceAggregator.js +++ b/packages/zoe/test/unitTests/contracts/test-priceAggregator.js @@ -89,9 +89,8 @@ const makePublicationChecker = async (t, aggregatorPublicFacet) => { /** @param {{timestamp: bigint, amountOut: any}} spec */ async nextMatches({ timestamp, amountOut }) { const { value } = await E(publications).next(); - const quoteValue = value.quoteAmount.value[0]; - t.is(quoteValue.timestamp, timestamp, 'wrong timestamp'); - t.is(quoteValue.amountOut.value, amountOut, 'wrong amountOut value'); + t.is(value.timestamp, timestamp, 'wrong timestamp'); + t.is(value.amountOut.value, amountOut, 'wrong amountOut value'); }, }; }; @@ -1096,21 +1095,13 @@ test('storage', async t => { 'mockChainStorageRoot.priceAggregator.ATOM-USD_price_feed', ), { - quoteAmount: { - brand: { iface: 'Alleged: quote brand' }, - value: [ - { - amountIn: { brand: { iface: 'Alleged: $ATOM brand' }, value: 1n }, - amountOut: { - brand: { iface: 'Alleged: $USD brand' }, - value: 1020n, - }, - timer: { iface: 'Alleged: ManualTimer' }, - timestamp: 1n, - }, - ], + amountIn: { brand: { iface: 'Alleged: $ATOM brand' }, value: 1n }, + amountOut: { + brand: { iface: 'Alleged: $USD brand' }, + value: 1020n, }, - quotePayment: { iface: 'Alleged: quote payment' }, + timer: { iface: 'Alleged: ManualTimer' }, + timestamp: 1n, }, ); }); diff --git a/packages/zoe/test/unitTests/contracts/test-priceAggregatorChainlink.js b/packages/zoe/test/unitTests/contracts/test-priceAggregatorChainlink.js index 32aec41205b..3c200b010ab 100644 --- a/packages/zoe/test/unitTests/contracts/test-priceAggregatorChainlink.js +++ b/packages/zoe/test/unitTests/contracts/test-priceAggregatorChainlink.js @@ -705,29 +705,6 @@ test('notifications', async t => { roundId: 1n, startedAt: 1n, }); - // t.deepEqual( - // aggregator.mockStorageRoot.getBody( - // 'mockChainStorageRoot.priceAggregator.LINK-USD_price_feed', - // ), - // { - // quoteAmount: { - // brand: { iface: 'Alleged: quote brand' }, - // value: [ - // { - // amountIn: { brand: { iface: 'Alleged: $LINK brand' }, value: 1n }, - // amountOut: { - // brand: { iface: 'Alleged: $USD brand' }, - // value: 1020n, - // }, - // timer: { iface: 'Alleged: ManualTimer' }, - // timestamp: 1n, - // }, - // ], - // }, - // quotePayment: { iface: 'Alleged: quote payment' }, - // }, - // ); - await E(pricePushAdminB).pushResult({ roundId: 1, data: '200' }); await E(pricePushAdminA).pushResult({ roundId: 2, data: '1000' }); @@ -749,6 +726,13 @@ test('notifications', async t => { }); // A joins in await E(pricePushAdminA).pushResult({ roundId: 2, data: '1000' }); + // writes to storage + t.deepEqual( + aggregator.mockStorageRoot.getBody( + 'mockChainStorageRoot.priceAggregator.LINK-USD_price_feed.latestRound', + ), + { roundId: 2n, startedAt: 1n }, + ); // A can start again await E(pricePushAdminA).pushResult({ roundId: 3, data: '1000' }); @@ -756,6 +740,28 @@ test('notifications', async t => { roundId: 3n, startedAt: 1n, }); + t.deepEqual( + aggregator.mockStorageRoot.getBody( + 'mockChainStorageRoot.priceAggregator.LINK-USD_price_feed', + ), + { + quoteAmount: { + brand: { iface: 'Alleged: quote brand' }, + value: [ + { + amountIn: { brand: { iface: 'Alleged: $LINK brand' }, value: 1n }, + amountOut: { + brand: { iface: 'Alleged: $USD brand' }, + value: 1020n, + }, + timer: { iface: 'Alleged: ManualTimer' }, + timestamp: 1n, + }, + ], + }, + quotePayment: { iface: 'Alleged: quote payment' }, + }, + ); }); test('storage keys', async t => { From d7ad2771717c52b8f292026ba2795c194d811030 Mon Sep 17 00:00:00 2001 From: Turadg Aleahmad Date: Mon, 12 Dec 2022 14:44:54 -0800 Subject: [PATCH 18/26] chore: chainlink aggregator uses bech32 key --- .../src/proposals/price-feed-proposal.js | 6 +- .../src/contracts/priceAggregatorChainlink.js | 129 +++++++++--------- 2 files changed, 67 insertions(+), 68 deletions(-) diff --git a/packages/inter-protocol/src/proposals/price-feed-proposal.js b/packages/inter-protocol/src/proposals/price-feed-proposal.js index 3b375bbfc33..d7ed2677ebc 100644 --- a/packages/inter-protocol/src/proposals/price-feed-proposal.js +++ b/packages/inter-protocol/src/proposals/price-feed-proposal.js @@ -187,11 +187,11 @@ export const createPriceFeed = async ( .then(deleter => E(aggregators).set(terms, { aggregator, deleter })); /** - * Send an invitation to one of the oracles. + * Initialize a new oracle and send an invitation to administer it. * * @param {string} addr */ - const distributeInvitation = async addr => { + const addOracle = async addr => { const invitation = await E(aggregator.creatorFacet).makeOracleInvitation( addr, ); @@ -204,7 +204,7 @@ export const createPriceFeed = async ( }; trace('distributing invitations', oracleAddresses); - await Promise.all(oracleAddresses.map(distributeInvitation)); + await Promise.all(oracleAddresses.map(addOracle)); trace('createPriceFeed complete'); }; diff --git a/packages/zoe/src/contracts/priceAggregatorChainlink.js b/packages/zoe/src/contracts/priceAggregatorChainlink.js index d7f188ce99c..e4a788030e9 100644 --- a/packages/zoe/src/contracts/priceAggregatorChainlink.js +++ b/packages/zoe/src/contracts/priceAggregatorChainlink.js @@ -151,7 +151,7 @@ const start = async (zcf, privateArgs) => { marshaller, ); - /** @type {LegacyMap>} */ + /** @type {LegacyMap>} */ const oracleStatuses = makeLegacyMap('oracleStatus'); /** @type {LegacyMap>} */ @@ -247,12 +247,8 @@ const start = async (zcf, privateArgs) => { * @property {number} lastSample */ - /** - * @typedef {{}} OracleKey - */ - - /** @type {LegacyMap>} */ - const keyToRecords = makeLegacyMap('oracleKey'); + /** @type {LegacyMap>} */ + const keyToRecords = makeLegacyMap('oracleAddr'); /** * @param {object} param0 @@ -441,16 +437,16 @@ const start = async (zcf, privateArgs) => { /** * @param {bigint} roundId - * @param {OracleKey} oracleKey + * @param {string} oracleAddr * @param {Timestamp} blockTimestamp */ - const proposeNewRound = (roundId, oracleKey, blockTimestamp) => { + const proposeNewRound = (roundId, oracleAddr, blockTimestamp) => { if (!newRound(roundId)) return; - const lastStarted = oracleStatuses.get(oracleKey).lastStartedRound; // cache storage reads + const lastStarted = oracleStatuses.get(oracleAddr).lastStartedRound; // cache storage reads if (roundId <= add(lastStarted, restartDelay) && lastStarted !== 0n) return; initializeNewRound(roundId, blockTimestamp); - oracleStatuses.get(oracleKey).lastStartedRound = roundId; + oracleStatuses.get(oracleAddr).lastStartedRound = roundId; }; /** @@ -463,17 +459,17 @@ const start = async (zcf, privateArgs) => { /** * @param {bigint} submission * @param {bigint} roundId - * @param {OracleKey} oracleKey + * @param {string} oracleAddr */ - const recordSubmission = (submission, roundId, oracleKey) => { + const recordSubmission = (submission, roundId, oracleAddr) => { if (!acceptingSubmissions(roundId)) { console.error('round not accepting submissions'); return false; } details.get(roundId).submissions.push(submission); - oracleStatuses.get(oracleKey).lastReportedRound = roundId; - oracleStatuses.get(oracleKey).latestSubmission = submission; + oracleStatuses.get(oracleAddr).lastReportedRound = roundId; + oracleStatuses.get(oracleAddr).latestSubmission = submission; return true; }; @@ -552,13 +548,13 @@ const start = async (zcf, privateArgs) => { }; /** - * @param {OracleKey} oracleKey + * @param {string} oracleAddr * @param {bigint} roundId * @param {Timestamp} blockTimestamp */ - const validateOracleRound = (oracleKey, roundId, blockTimestamp) => { + const validateOracleRound = (oracleAddr, roundId, blockTimestamp) => { // cache storage reads - const startingRound = oracleStatuses.get(oracleKey).startingRound; + const startingRound = oracleStatuses.get(oracleAddr).startingRound; const rrId = reportingRoundId; let canSupersede = true; @@ -568,9 +564,9 @@ const start = async (zcf, privateArgs) => { if (startingRound === 0n) return 'not enabled oracle'; if (startingRound > roundId) return 'not yet enabled oracle'; - if (oracleStatuses.get(oracleKey).endingRound < roundId) + if (oracleStatuses.get(oracleAddr).endingRound < roundId) return 'no longer allowed oracle'; - if (oracleStatuses.get(oracleKey).lastReportedRound >= roundId) + if (oracleStatuses.get(oracleAddr).lastReportedRound >= roundId) return 'cannot report on previous rounds'; if ( roundId !== rrId && @@ -584,11 +580,11 @@ const start = async (zcf, privateArgs) => { }; /** - * @param {OracleKey} oracleKey + * @param {string} oracleAddr * @param {bigint} roundId */ - const delayed = (oracleKey, roundId) => { - const lastStarted = oracleStatuses.get(oracleKey).lastStartedRound; + const delayed = (oracleAddr, roundId) => { + const lastStarted = oracleStatuses.get(oracleAddr).lastStartedRound; return roundId > add(lastStarted, restartDelay) || lastStarted === 0n; }; @@ -596,11 +592,11 @@ const start = async (zcf, privateArgs) => { * a method to provide all current info oracleStatuses need. Intended only * only to be callable by oracleStatuses. Not for use by contracts to read state. * - * @param {OracleKey} oracleKey + * @param {string} oracleAddr * @param {Timestamp} blockTimestamp */ - const oracleRoundStateSuggestRound = (oracleKey, blockTimestamp) => { - const oracle = oracleStatuses.get(oracleKey); + const oracleRoundStateSuggestRound = (oracleAddr, blockTimestamp) => { + const oracle = oracleStatuses.get(oracleAddr); const shouldSupersede = oracle.lastReportedRound === reportingRoundId || @@ -614,7 +610,7 @@ const start = async (zcf, privateArgs) => { let eligibleToSubmit; if (canSupersede && shouldSupersede) { roundId = add(reportingRoundId, 1); - eligibleToSubmit = delayed(oracleKey, roundId); + eligibleToSubmit = delayed(oracleAddr, roundId); } else { roundId = reportingRoundId; eligibleToSubmit = acceptingSubmissions(roundId); @@ -632,7 +628,7 @@ const start = async (zcf, privateArgs) => { roundTimeout = 0; } - const error = validateOracleRound(oracleKey, roundId, blockTimestamp); + const error = validateOracleRound(oracleAddr, roundId, blockTimestamp); if (error.length !== 0) { eligibleToSubmit = false; } @@ -648,35 +644,35 @@ const start = async (zcf, privateArgs) => { }; /** - * @param {OracleKey} oracleKey + * @param {string} oracleAddr * @param {bigint} queriedRoundId * @param {Timestamp} blockTimestamp */ const eligibleForSpecificRound = ( - oracleKey, + oracleAddr, queriedRoundId, blockTimestamp, ) => { const error = validateOracleRound( - oracleKey, + oracleAddr, queriedRoundId, blockTimestamp, ); if (TimeMath.absValue(rounds.get(queriedRoundId).startedAt) > 0n) { return acceptingSubmissions(queriedRoundId) && error.length === 0; } else { - return delayed(oracleKey, queriedRoundId) && error.length === 0; + return delayed(oracleAddr, queriedRoundId) && error.length === 0; } }; /** - * @param {OracleKey} oracleKey + * @param {string} oracleAddr */ - const getStartingRound = oracleKey => { + const getStartingRound = oracleAddr => { const currentRound = reportingRoundId; if ( currentRound !== 0n && - currentRound === oracleStatuses.get(oracleKey).endingRound + currentRound === oracleStatuses.get(oracleAddr).endingRound ) { return currentRound; } @@ -687,7 +683,7 @@ const start = async (zcf, privateArgs) => { * @type {Omit & { * initOracle: (instance) => Promise>, * getRoundData(roundId: bigint | number): Promise, - * oracleRoundState(oracleKey: OracleKey, queriedRoundId: BigInt): Promise + * oracleRoundState(oracleAddr: string, queriedRoundId: BigInt): Promise * }} */ const creatorFacet = Far('PriceAggregatorChainlinkCreatorFacet', { @@ -699,9 +695,9 @@ const start = async (zcf, privateArgs) => { * directly to manage the price submissions as well as to terminate the * relationship. * - * @param {Instance | string} [oracleKey] + * @param {string} oracleAddr */ - makeOracleInvitation: async oracleKey => { + makeOracleInvitation: async oracleAddr => { /** * If custom arguments are supplied to the `zoe.offer` call, they can * indicate an OraclePriceSubmission notifier and a corresponding @@ -712,7 +708,7 @@ const start = async (zcf, privateArgs) => { * @returns {Promise<{admin: OracleAdmin, invitationMakers: {makePushPriceInvitation: (result: PriceRound) => Promise>} }>} */ const offerHandler = async seat => { - const admin = await creatorFacet.initOracle(oracleKey); + const admin = await creatorFacet.initOracle(oracleAddr); const invitationMakers = Far('invitation makers', { /** @param {PriceRound} result */ makePushPriceInvitation(result) { @@ -734,43 +730,42 @@ const start = async (zcf, privateArgs) => { return zcf.makeInvitation(offerHandler, INVITATION_MAKERS_DESC); }, - deleteOracle: async oracleKey => { - const records = keyToRecords.get(oracleKey); + /** @param {string} oracleAddr */ + deleteOracle: async oracleAddr => { + const records = keyToRecords.get(oracleAddr); for (const record of records) { records.delete(record); } - oracleStatuses.delete(oracleKey); + oracleStatuses.delete(oracleAddr); // We should remove the entry entirely, as it is empty. - keyToRecords.delete(oracleKey); + keyToRecords.delete(oracleAddr); }, // unlike the median case, no query argument is passed, since polling behavior is undesired - async initOracle(oracleInstance) { - /** @type {OracleKey} */ - const oracleKey = oracleInstance || Far('oracleKey', {}); - + /** @param {string} oracleAddr */ + async initOracle(oracleAddr) { /** @type {OracleRecord} */ const record = { querier: undefined, lastSample: 0 }; /** @type {Set} */ let records; - if (keyToRecords.has(oracleKey)) { - records = keyToRecords.get(oracleKey); + if (keyToRecords.has(oracleAddr)) { + records = keyToRecords.get(oracleAddr); } else { records = new Set(); - keyToRecords.init(oracleKey, records); + keyToRecords.init(oracleAddr, records); const oracleStatus = makeOracleStatus( - /* startingRound = */ getStartingRound(oracleKey), + /* startingRound = */ getStartingRound(oracleAddr), /* endingRound = */ ROUND_MAX, /* lastReportedRound = */ 0n, /* lastStartedRound = */ 0n, /* latestSubmission = */ 0n, /* index = */ oracleStatuses.getSize(), ); - oracleStatuses.init(oracleKey, oracleStatus); + oracleStatuses.init(oracleAddr, oracleStatus); } records.add(record); @@ -787,7 +782,7 @@ const start = async (zcf, privateArgs) => { let roundId; if (roundIdRaw === undefined) { const suggestedRound = oracleRoundStateSuggestRound( - oracleKey, + oracleAddr, blockTimestamp, ); roundId = suggestedRound.eligibleForSpecificRound @@ -797,7 +792,7 @@ const start = async (zcf, privateArgs) => { roundId = Nat(roundIdRaw); } - const error = validateOracleRound(oracleKey, roundId, blockTimestamp); + const error = validateOracleRound(oracleAddr, roundId, blockTimestamp); if (!(parsedSubmission >= minSubmissionValue)) { console.error('value below minSubmissionValue', minSubmissionValue); return; @@ -813,8 +808,12 @@ const start = async (zcf, privateArgs) => { return; } - proposeNewRound(roundId, oracleKey, blockTimestamp); - const recorded = recordSubmission(parsedSubmission, roundId, oracleKey); + proposeNewRound(roundId, oracleAddr, blockTimestamp); + const recorded = recordSubmission( + parsedSubmission, + roundId, + oracleAddr, + ); if (!recorded) { return; } @@ -832,16 +831,16 @@ const start = async (zcf, privateArgs) => { assert(records.has(record), 'Oracle record is already deleted'); // The actual deletion is synchronous. - oracleStatuses.delete(oracleKey); + oracleStatuses.delete(oracleAddr); records.delete(record); if ( records.size === 0 && - keyToRecords.has(oracleKey) && - keyToRecords.get(oracleKey) === records + keyToRecords.has(oracleAddr) && + keyToRecords.get(oracleAddr) === records ) { // We should remove the entry entirely, as it is empty. - keyToRecords.delete(oracleKey); + keyToRecords.delete(oracleAddr); } }, async pushResult({ @@ -890,28 +889,28 @@ const start = async (zcf, privateArgs) => { * a method to provide all current info oracleStatuses need. Intended only * only to be callable by oracleStatuses. Not for use by contracts to read state. * - * @param {OracleKey} oracleKey + * @param {string} oracleAddr * @param {bigint} queriedRoundId */ - async oracleRoundState(oracleKey, queriedRoundId) { + async oracleRoundState(oracleAddr, queriedRoundId) { const blockTimestamp = await E(timer).getCurrentTimestamp(); if (queriedRoundId > 0) { const round = rounds.get(queriedRoundId); const detail = details.get(queriedRoundId); return { eligibleForSpecificRound: eligibleForSpecificRound( - oracleKey, + oracleAddr, queriedRoundId, blockTimestamp, ), queriedRoundId, - oracleStatus: oracleStatuses.get(oracleKey).latestSubmission, + oracleStatus: oracleStatuses.get(oracleAddr).latestSubmission, startedAt: round.startedAt, roundTimeout: detail.roundTimeout, oracleCount: oracleCount(), }; } else { - return oracleRoundStateSuggestRound(oracleKey, blockTimestamp); + return oracleRoundStateSuggestRound(oracleAddr, blockTimestamp); } }, }); From 5e58585f2f2c5730c81991c99cc67ff35c1440ae Mon Sep 17 00:00:00 2001 From: Turadg Aleahmad Date: Mon, 12 Dec 2022 14:52:24 -0800 Subject: [PATCH 19/26] test: second oracle for smoketest --- .../agoric-cli/test/agops-oracle-smoketest.sh | 29 ++++++++++++++++++- .../cosmic-swingset/economy-template.json | 3 +- .../scripts/start-local-chain.sh | 9 ++++++ 3 files changed, 39 insertions(+), 2 deletions(-) diff --git a/packages/agoric-cli/test/agops-oracle-smoketest.sh b/packages/agoric-cli/test/agops-oracle-smoketest.sh index 482e8a85b4e..401b1cb6d45 100644 --- a/packages/agoric-cli/test/agops-oracle-smoketest.sh +++ b/packages/agoric-cli/test/agops-oracle-smoketest.sh @@ -24,6 +24,11 @@ if [ -z "$WALLET" ]; then fi set -x +# this is in economy-template.json in the oracleAddresses list (agoric1dy0yegdsev4xvce3dx7zrz2ad9pesf5svzud6y) +# to use it run `agd keys oracle2 --interactive` and enter this mnenomic: +# dizzy scale gentle good play scene certain acquire approve alarm retreat recycle inch journey fitness grass minimum learn funny way unlock what buzz upon +WALLET2=oracle2 + # Accept invitation to admin an oracle ORACLE_OFFER=$(mktemp -t agops.XXX) bin/agops oracle accept >|"$ORACLE_OFFER" @@ -33,7 +38,14 @@ agoric wallet send --from "$WALLET" --offer "$ORACLE_OFFER" agoric wallet show --from "$WALLET" ORACLE_OFFER_ID=$(jq ".body | fromjson | .offer.id" <"$ORACLE_OFFER") -### Now we have the continuing invitationMakers saved in the wallet +# repeat for oracle2 +ORACLE_OFFER=$(mktemp -t agops.XXX) +bin/agops oracle accept >|"$ORACLE_OFFER" +jq ".body | fromjson" <"$ORACLE_OFFER" +agoric wallet send --from "$WALLET2" --offer "$ORACLE_OFFER" +ORACLE2_OFFER_ID=$(jq ".body | fromjson | .offer.id" <"$ORACLE_OFFER") + +### Now we have the continuing invitationMakers saved in the wallets # Use invitation result, with continuing invitationMakers to propose a vote PROPOSAL_OFFER=$(mktemp -t agops.XXX) @@ -47,3 +59,18 @@ agoric wallet show --from "$WALLET" # verify that the round started agoric follow :published.priceFeed.ATOM-USD_price_feed.latestRound + +# submit another price in the round from the second oracle +PROPOSAL_OFFER=$(mktemp -t agops.XXX) +bin/agops oracle pushPriceRound --price 2.01 --roundId 1 --oracleAdminAcceptOfferId "$ORACLE2_OFFER_ID" >|"$PROPOSAL_OFFER" +jq ".body | fromjson" <"$PROPOSAL_OFFER" +agoric wallet send --from "$WALLET2" --offer "$PROPOSAL_OFFER" + +# second round, first oracle +PROPOSAL_OFFER=$(mktemp -t agops.XXX) +bin/agops oracle pushPriceRound --price 1.02 --roundId 2 --oracleAdminAcceptOfferId "$ORACLE_OFFER_ID" >|"$PROPOSAL_OFFER" +agoric wallet send --from "$WALLET" --offer "$PROPOSAL_OFFER" +# second round, second oracle +PROPOSAL_OFFER=$(mktemp -t agops.XXX) +bin/agops oracle pushPriceRound --price 2.01 --roundId 2 --oracleAdminAcceptOfferId "$ORACLE2_OFFER_ID" >|"$PROPOSAL_OFFER" +agoric wallet send --from "$WALLET2" --offer "$PROPOSAL_OFFER" diff --git a/packages/cosmic-swingset/economy-template.json b/packages/cosmic-swingset/economy-template.json index 26d0ae284c4..00bf970732d 100644 --- a/packages/cosmic-swingset/economy-template.json +++ b/packages/cosmic-swingset/economy-template.json @@ -110,7 +110,8 @@ { "AGORIC_INSTANCE_NAME": "ATOM-USD price feed", "oracleAddresses": [ - "@PRIMARY_ADDRESS@" + "@PRIMARY_ADDRESS@", + "agoric1dy0yegdsev4xvce3dx7zrz2ad9pesf5svzud6y" ], "IN_BRAND_LOOKUP": [ "agoricNames", diff --git a/packages/inter-protocol/scripts/start-local-chain.sh b/packages/inter-protocol/scripts/start-local-chain.sh index cc55ce9de9c..9518e4a1dc1 100755 --- a/packages/inter-protocol/scripts/start-local-chain.sh +++ b/packages/inter-protocol/scripts/start-local-chain.sh @@ -65,3 +65,12 @@ sleep 15 # verify agoric wallet list agoric wallet show --from "$WALLET" + +echo "Repeating for oracle2 account..." +# this is in economy-template.json in the oracleAddresses list (agoric1dy0yegdsev4xvce3dx7zrz2ad9pesf5svzud6y) +# to use it run `agd keys oracle2 --interactive` and enter this mnenomic: +# dizzy scale gentle good play scene certain acquire approve alarm retreat recycle inch journey fitness grass minimum learn funny way unlock what buzz upon +WALLET2=oracle2 +WALLET2_BECH32=$(agd keys show "$WALLET2" --output json | jq -r .address) +make ACCT_ADDR="$WALLET2_BECH32" FUNDS=20000000ubld,20000000ibc/usdc1234 fund-acct +agoric wallet provision --spend --account "$WALLET2" From 99148990824661bdf447fad6f420882a43688aa1 Mon Sep 17 00:00:00 2001 From: Turadg Aleahmad Date: Mon, 12 Dec 2022 14:52:33 -0800 Subject: [PATCH 20/26] fix(chainlink): publish new quotes --- .../zoe/src/contractSupport/priceAuthority.js | 2 +- .../src/contracts/priceAggregatorChainlink.js | 18 ++++-- .../test-priceAggregatorChainlink.js | 56 ++++++++++++------- 3 files changed, 49 insertions(+), 27 deletions(-) diff --git a/packages/zoe/src/contractSupport/priceAuthority.js b/packages/zoe/src/contractSupport/priceAuthority.js index b65a21bf033..8429a1d5536 100644 --- a/packages/zoe/src/contractSupport/priceAuthority.js +++ b/packages/zoe/src/contractSupport/priceAuthority.js @@ -256,7 +256,7 @@ export function makeOnewayPriceAuthorityKit(opts) { amountIn, amountOut: calcAmountOut(amountIn), })); - assert(quote); + assert(quote, 'createQuote returned falsey'); const value = await quote; return harden({ diff --git a/packages/zoe/src/contracts/priceAggregatorChainlink.js b/packages/zoe/src/contracts/priceAggregatorChainlink.js index e4a788030e9..0350bf85766 100644 --- a/packages/zoe/src/contracts/priceAggregatorChainlink.js +++ b/packages/zoe/src/contracts/priceAggregatorChainlink.js @@ -238,8 +238,13 @@ const start = async (zcf, privateArgs) => { return harden({ quoteAmount, quotePayment }); }; - // FIXME: We throw away the updater but shouldn't. - const { notifier } = makeNotifierKit(); + /** + * This is just a signal that there's a new answer, which is read from `lastValueOutForUnitIn` + * + * @type {NotifierRecord} + */ + const { notifier: answerNotifier, updater: answerUpdator } = + makeNotifierKit(); /** * @typedef {object} OracleRecord @@ -326,7 +331,7 @@ const start = async (zcf, privateArgs) => { const { priceAuthority } = makeOnewayPriceAuthorityKit({ createQuote: makeCreateQuote(), - notifier, + notifier: answerNotifier, quoteIssuer: quoteKit.issuer, timer, actualBrandIn: brandIn, @@ -499,6 +504,9 @@ const start = async (zcf, privateArgs) => { rounds.get(roundId).updatedAt = blockTimestamp; rounds.get(roundId).answeredInRound = roundId; + lastValueOutForUnitIn = newAnswer; + answerUpdator.updateState(undefined); + return [true, newAnswer]; }; @@ -551,6 +559,7 @@ const start = async (zcf, privateArgs) => { * @param {string} oracleAddr * @param {bigint} roundId * @param {Timestamp} blockTimestamp + * @returns {string} error message or '' if no error */ const validateOracleRound = (oracleAddr, roundId, blockTimestamp) => { // cache storage reads @@ -576,6 +585,7 @@ const start = async (zcf, privateArgs) => { return 'invalid round to report'; if (roundId !== 1n && !canSupersede) return 'previous round not supersedable'; + // XXX return a more obvious 'no errors' value, e.g. null return ''; }; @@ -777,8 +787,6 @@ const start = async (zcf, privateArgs) => { const parsedSubmission = Nat(parseInt(submissionRaw, 10)); const blockTimestamp = await E(timer).getCurrentTimestamp(); - console.log('DEBUG pushResult', { parsedSubmission, blockTimestamp }); - let roundId; if (roundIdRaw === undefined) { const suggestedRound = oracleRoundStateSuggestRound( diff --git a/packages/zoe/test/unitTests/contracts/test-priceAggregatorChainlink.js b/packages/zoe/test/unitTests/contracts/test-priceAggregatorChainlink.js index 3c200b010ab..9995508a3f2 100644 --- a/packages/zoe/test/unitTests/contracts/test-priceAggregatorChainlink.js +++ b/packages/zoe/test/unitTests/contracts/test-priceAggregatorChainlink.js @@ -9,7 +9,10 @@ import { E } from '@endo/eventual-send'; import { Far } from '@endo/marshal'; import { makeIssuerKit, AssetKind, AmountMath } from '@agoric/ertp'; -import { makeFakeMarshaller } from '@agoric/notifier/tools/testSupports.js'; +import { + eventLoopIteration, + makeFakeMarshaller, +} from '@agoric/notifier/tools/testSupports.js'; import { makeMockChainStorageRoot } from '@agoric/vats/tools/storage-test-utils.js'; import { subscribeEach } from '@agoric/notifier'; import { makeFakeVatAdmin } from '../../../tools/fakeVatAdmin.js'; @@ -707,6 +710,22 @@ test('notifications', async t => { }); await E(pricePushAdminB).pushResult({ roundId: 1, data: '200' }); + await eventLoopIteration(); + t.deepEqual( + aggregator.mockStorageRoot.getBody( + 'mockChainStorageRoot.priceAggregator.LINK-USD_price_feed', + ), + { + amountIn: { brand: { iface: 'Alleged: $LINK brand' }, value: 1n }, + amountOut: { + brand: { iface: 'Alleged: $USD brand' }, + value: 150n, // AVG(100, 200) + }, + timer: { iface: 'Alleged: ManualTimer' }, + timestamp: 1n, + }, + ); + await E(pricePushAdminA).pushResult({ roundId: 2, data: '1000' }); // A started last round so fails to start next round t.deepEqual( @@ -734,34 +753,29 @@ test('notifications', async t => { { roundId: 2n, startedAt: 1n }, ); - // A can start again - await E(pricePushAdminA).pushResult({ roundId: 3, data: '1000' }); - t.deepEqual((await eachLatestRound.next()).value, { - roundId: 3n, - startedAt: 1n, - }); + await eventLoopIteration(); t.deepEqual( aggregator.mockStorageRoot.getBody( 'mockChainStorageRoot.priceAggregator.LINK-USD_price_feed', ), { - quoteAmount: { - brand: { iface: 'Alleged: quote brand' }, - value: [ - { - amountIn: { brand: { iface: 'Alleged: $LINK brand' }, value: 1n }, - amountOut: { - brand: { iface: 'Alleged: $USD brand' }, - value: 1020n, - }, - timer: { iface: 'Alleged: ManualTimer' }, - timestamp: 1n, - }, - ], + amountIn: { brand: { iface: 'Alleged: $LINK brand' }, value: 1n }, + amountOut: { + brand: { iface: 'Alleged: $USD brand' }, + value: 1000n, // AVG(1000, 1000) }, - quotePayment: { iface: 'Alleged: quote payment' }, + timer: { iface: 'Alleged: ManualTimer' }, + timestamp: 1n, }, ); + + // A can start again + await E(pricePushAdminA).pushResult({ roundId: 3, data: '1000' }); + t.deepEqual((await eachLatestRound.next()).value, { + roundId: 3n, + startedAt: 1n, + }); + // no new price yet publishable }); test('storage keys', async t => { From bb2b503e1c64439316840cc42b43722cb7213e3e Mon Sep 17 00:00:00 2001 From: Turadg Aleahmad Date: Tue, 13 Dec 2022 09:51:01 -0800 Subject: [PATCH 21/26] refactor: clearer error message handling --- .../zoe/src/contracts/priceAggregatorChainlink.js | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/packages/zoe/src/contracts/priceAggregatorChainlink.js b/packages/zoe/src/contracts/priceAggregatorChainlink.js index 0350bf85766..f9005662acb 100644 --- a/packages/zoe/src/contracts/priceAggregatorChainlink.js +++ b/packages/zoe/src/contracts/priceAggregatorChainlink.js @@ -559,7 +559,7 @@ const start = async (zcf, privateArgs) => { * @param {string} oracleAddr * @param {bigint} roundId * @param {Timestamp} blockTimestamp - * @returns {string} error message or '' if no error + * @returns {string?} error message, if there is one */ const validateOracleRound = (oracleAddr, roundId, blockTimestamp) => { // cache storage reads @@ -585,8 +585,7 @@ const start = async (zcf, privateArgs) => { return 'invalid round to report'; if (roundId !== 1n && !canSupersede) return 'previous round not supersedable'; - // XXX return a more obvious 'no errors' value, e.g. null - return ''; + return null; }; /** @@ -639,7 +638,7 @@ const start = async (zcf, privateArgs) => { } const error = validateOracleRound(oracleAddr, roundId, blockTimestamp); - if (error.length !== 0) { + if (error !== null) { eligibleToSubmit = false; } @@ -669,9 +668,9 @@ const start = async (zcf, privateArgs) => { blockTimestamp, ); if (TimeMath.absValue(rounds.get(queriedRoundId).startedAt) > 0n) { - return acceptingSubmissions(queriedRoundId) && error.length === 0; + return acceptingSubmissions(queriedRoundId) && error === null; } else { - return delayed(oracleAddr, queriedRoundId) && error.length === 0; + return delayed(oracleAddr, queriedRoundId) && error === null; } }; @@ -811,7 +810,7 @@ const start = async (zcf, privateArgs) => { return; } - if (!(error.length === 0)) { + if (!(error === null)) { console.error(error); return; } From 443ea1cf2f9a8c9e7504b1c7f1d05b5f70b8f65c Mon Sep 17 00:00:00 2001 From: Turadg Aleahmad Date: Tue, 13 Dec 2022 10:00:08 -0800 Subject: [PATCH 22/26] docs --- packages/agoric-cli/test/agops-oracle-smoketest.sh | 10 ++++++++-- packages/zoe/src/contracts/priceAggregatorChainlink.js | 5 +++++ 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/packages/agoric-cli/test/agops-oracle-smoketest.sh b/packages/agoric-cli/test/agops-oracle-smoketest.sh index 401b1cb6d45..3fdec954f39 100644 --- a/packages/agoric-cli/test/agops-oracle-smoketest.sh +++ b/packages/agoric-cli/test/agops-oracle-smoketest.sh @@ -57,6 +57,9 @@ agoric wallet send --from "$WALLET" --offer "$PROPOSAL_OFFER" echo "Offer $ORACLE_OFFER_ID should have numWantsSatisfied: 1" agoric wallet show --from "$WALLET" +# verify feed publishing +agd query vstorage keys published.priceFeed + # verify that the round started agoric follow :published.priceFeed.ATOM-USD_price_feed.latestRound @@ -68,9 +71,12 @@ agoric wallet send --from "$WALLET2" --offer "$PROPOSAL_OFFER" # second round, first oracle PROPOSAL_OFFER=$(mktemp -t agops.XXX) -bin/agops oracle pushPriceRound --price 1.02 --roundId 2 --oracleAdminAcceptOfferId "$ORACLE_OFFER_ID" >|"$PROPOSAL_OFFER" +bin/agops oracle pushPriceRound --price 11.02 --roundId 2 --oracleAdminAcceptOfferId "$ORACLE_OFFER_ID" >|"$PROPOSAL_OFFER" agoric wallet send --from "$WALLET" --offer "$PROPOSAL_OFFER" # second round, second oracle PROPOSAL_OFFER=$(mktemp -t agops.XXX) -bin/agops oracle pushPriceRound --price 2.01 --roundId 2 --oracleAdminAcceptOfferId "$ORACLE2_OFFER_ID" >|"$PROPOSAL_OFFER" +bin/agops oracle pushPriceRound --price 12.02 --roundId 2 --oracleAdminAcceptOfferId "$ORACLE2_OFFER_ID" >|"$PROPOSAL_OFFER" agoric wallet send --from "$WALLET2" --offer "$PROPOSAL_OFFER" + +# see new price +agoric follow :published.priceFeed.ATOM-USD_price_feed diff --git a/packages/zoe/src/contracts/priceAggregatorChainlink.js b/packages/zoe/src/contracts/priceAggregatorChainlink.js index f9005662acb..dfcfb710e50 100644 --- a/packages/zoe/src/contracts/priceAggregatorChainlink.js +++ b/packages/zoe/src/contracts/priceAggregatorChainlink.js @@ -1,3 +1,7 @@ +/** @file + * Adaptation of Chainlink algorithm to the Agoric platform. + * Modeled on https://github.com/smartcontractkit/chainlink/blob/master/contracts/src/v0.6/FluxAggregator.sol (version?) + */ import { AmountMath, AssetKind, makeIssuerKit } from '@agoric/ertp'; import { E } from '@endo/eventual-send'; import { Far } from '@endo/marshal'; @@ -26,6 +30,7 @@ import { export { INVITATION_MAKERS_DESC }; +// FIXME is MAX_SAFE_INTEGER sufficient? why encoded as string? /** * @typedef {{ roundId: number | undefined, data: string }} PriceRound * `data` is a string encoded integer (Number.MAX_SAFE_INTEGER) From a8c836cb70a033d78199372669f6f95314de4d8f Mon Sep 17 00:00:00 2001 From: Turadg Aleahmad Date: Mon, 19 Dec 2022 15:40:30 -0800 Subject: [PATCH 23/26] chore(chainlink)!: 'data' string to 'unitPrice' bigint --- packages/agoric-cli/src/commands/oracle.js | 6 +- .../agoric-cli/test/agops-oracle-smoketest.sh | 8 +- .../smartWallet/test-oracle-integration.js | 2 +- .../src/contracts/priceAggregatorChainlink.js | 24 ++- .../test-priceAggregatorChainlink.js | 142 +++++++++--------- 5 files changed, 89 insertions(+), 93 deletions(-) diff --git a/packages/agoric-cli/src/commands/oracle.js b/packages/agoric-cli/src/commands/oracle.js index d0f66d6f3c9..d09bf6d1577 100644 --- a/packages/agoric-cli/src/commands/oracle.js +++ b/packages/agoric-cli/src/commands/oracle.js @@ -137,7 +137,7 @@ export const makeOracleCommand = async logger => { 'offer that had continuing invitation result', Number, ) - .requiredOption('--price [number]', 'price (format TODO)', String) + .requiredOption('--price [number]', 'price (per unitAmount)', BigInt) .requiredOption('--roundId [number]', 'round', Number) .action(async function () { // @ts-expect-error this implicit any @@ -150,7 +150,9 @@ export const makeOracleCommand = async logger => { source: 'continuing', previousOffer: opts.oracleAdminAcceptOfferId, invitationMakerName: 'makePushPriceInvitation', - invitationArgs: harden([{ data: opts.price, roundId: opts.roundId }]), + invitationArgs: harden([ + { unitPrice: opts.price, roundId: opts.roundId }, + ]), }, proposal: {}, }; diff --git a/packages/agoric-cli/test/agops-oracle-smoketest.sh b/packages/agoric-cli/test/agops-oracle-smoketest.sh index 3fdec954f39..6a978a5f6d6 100644 --- a/packages/agoric-cli/test/agops-oracle-smoketest.sh +++ b/packages/agoric-cli/test/agops-oracle-smoketest.sh @@ -49,7 +49,7 @@ ORACLE2_OFFER_ID=$(jq ".body | fromjson | .offer.id" <"$ORACLE_OFFER") # Use invitation result, with continuing invitationMakers to propose a vote PROPOSAL_OFFER=$(mktemp -t agops.XXX) -bin/agops oracle pushPriceRound --price 1.01 --roundId 1 --oracleAdminAcceptOfferId "$ORACLE_OFFER_ID" >|"$PROPOSAL_OFFER" +bin/agops oracle pushPriceRound --price 101 --roundId 1 --oracleAdminAcceptOfferId "$ORACLE_OFFER_ID" >|"$PROPOSAL_OFFER" jq ".body | fromjson" <"$PROPOSAL_OFFER" agoric wallet send --from "$WALLET" --offer "$PROPOSAL_OFFER" @@ -65,17 +65,17 @@ agoric follow :published.priceFeed.ATOM-USD_price_feed.latestRound # submit another price in the round from the second oracle PROPOSAL_OFFER=$(mktemp -t agops.XXX) -bin/agops oracle pushPriceRound --price 2.01 --roundId 1 --oracleAdminAcceptOfferId "$ORACLE2_OFFER_ID" >|"$PROPOSAL_OFFER" +bin/agops oracle pushPriceRound --price 201 --roundId 1 --oracleAdminAcceptOfferId "$ORACLE2_OFFER_ID" >|"$PROPOSAL_OFFER" jq ".body | fromjson" <"$PROPOSAL_OFFER" agoric wallet send --from "$WALLET2" --offer "$PROPOSAL_OFFER" # second round, first oracle PROPOSAL_OFFER=$(mktemp -t agops.XXX) -bin/agops oracle pushPriceRound --price 11.02 --roundId 2 --oracleAdminAcceptOfferId "$ORACLE_OFFER_ID" >|"$PROPOSAL_OFFER" +bin/agops oracle pushPriceRound --price 1102 --roundId 2 --oracleAdminAcceptOfferId "$ORACLE_OFFER_ID" >|"$PROPOSAL_OFFER" agoric wallet send --from "$WALLET" --offer "$PROPOSAL_OFFER" # second round, second oracle PROPOSAL_OFFER=$(mktemp -t agops.XXX) -bin/agops oracle pushPriceRound --price 12.02 --roundId 2 --oracleAdminAcceptOfferId "$ORACLE2_OFFER_ID" >|"$PROPOSAL_OFFER" +bin/agops oracle pushPriceRound --price 1202 --roundId 2 --oracleAdminAcceptOfferId "$ORACLE2_OFFER_ID" >|"$PROPOSAL_OFFER" agoric wallet send --from "$WALLET2" --offer "$PROPOSAL_OFFER" # see new price diff --git a/packages/inter-protocol/test/smartWallet/test-oracle-integration.js b/packages/inter-protocol/test/smartWallet/test-oracle-integration.js index c7321a7d70e..a862be1464c 100644 --- a/packages/inter-protocol/test/smartWallet/test-oracle-integration.js +++ b/packages/inter-protocol/test/smartWallet/test-oracle-integration.js @@ -156,7 +156,7 @@ test('admin price', async t => { // Push a new price result ///////////////////////// /** @type {import('@agoric/zoe/src/contracts/priceAggregatorChainlink.js').PriceRound} */ - const result = { roundId: 1, data: '123' }; + const result = { roundId: 1, unitPrice: 123n }; /** @type {import('@agoric/smart-wallet/src/invitations.js').ContinuingInvitationSpec} */ const proposeInvitationSpec = { diff --git a/packages/zoe/src/contracts/priceAggregatorChainlink.js b/packages/zoe/src/contracts/priceAggregatorChainlink.js index dfcfb710e50..c5ac014705e 100644 --- a/packages/zoe/src/contracts/priceAggregatorChainlink.js +++ b/packages/zoe/src/contracts/priceAggregatorChainlink.js @@ -30,10 +30,8 @@ import { export { INVITATION_MAKERS_DESC }; -// FIXME is MAX_SAFE_INTEGER sufficient? why encoded as string? /** - * @typedef {{ roundId: number | undefined, data: string }} PriceRound - * `data` is a string encoded integer (Number.MAX_SAFE_INTEGER) + * @typedef {{ roundId: number | undefined, unitPrice: NatValue }} PriceRound */ const { add, subtract, multiply, floorDivide, ceilDivide, isGTE } = natSafeMath; @@ -726,7 +724,7 @@ const start = async (zcf, privateArgs) => { const invitationMakers = Far('invitation makers', { /** @param {PriceRound} result */ makePushPriceInvitation(result) { - assertParsableNumber(result.data); + assertParsableNumber(result.unitPrice); return zcf.makeInvitation(cSeat => { cSeat.exit(); admin.pushResult(result); @@ -786,9 +784,9 @@ const start = async (zcf, privateArgs) => { /** @param {PriceRound} result */ const pushResult = async ({ roundId: roundIdRaw = undefined, - data: submissionRaw, + unitPrice: valueRaw, }) => { - const parsedSubmission = Nat(parseInt(submissionRaw, 10)); + const value = Nat(valueRaw); const blockTimestamp = await E(timer).getCurrentTimestamp(); let roundId; @@ -805,12 +803,12 @@ const start = async (zcf, privateArgs) => { } const error = validateOracleRound(oracleAddr, roundId, blockTimestamp); - if (!(parsedSubmission >= minSubmissionValue)) { + if (!(value >= minSubmissionValue)) { console.error('value below minSubmissionValue', minSubmissionValue); return; } - if (!(parsedSubmission <= maxSubmissionValue)) { + if (!(value <= maxSubmissionValue)) { console.error('value above maxSubmissionValue'); return; } @@ -821,11 +819,7 @@ const start = async (zcf, privateArgs) => { } proposeNewRound(roundId, oracleAddr, blockTimestamp); - const recorded = recordSubmission( - parsedSubmission, - roundId, - oracleAddr, - ); + const recorded = recordSubmission(value, roundId, oracleAddr); if (!recorded) { return; } @@ -857,13 +851,13 @@ const start = async (zcf, privateArgs) => { }, async pushResult({ roundId: roundIdRaw = undefined, - data: submissionRaw, + unitPrice: submissionRaw, }) { // Sample of NaN, 0, or negative numbers get culled in // the median calculation. pushResult({ roundId: roundIdRaw, - data: submissionRaw, + unitPrice: submissionRaw, }).catch(console.error); }, }); diff --git a/packages/zoe/test/unitTests/contracts/test-priceAggregatorChainlink.js b/packages/zoe/test/unitTests/contracts/test-priceAggregatorChainlink.js index 9995508a3f2..4a81e013e56 100644 --- a/packages/zoe/test/unitTests/contracts/test-priceAggregatorChainlink.js +++ b/packages/zoe/test/unitTests/contracts/test-priceAggregatorChainlink.js @@ -181,9 +181,9 @@ test('basic', async t => { // ----- round 1: basic consensus await oracleTimer.tick(); - await E(pricePushAdminA).pushResult({ roundId: 1, data: '100' }); - await E(pricePushAdminB).pushResult({ roundId: 1, data: '200' }); - await E(pricePushAdminC).pushResult({ roundId: 1, data: '300' }); + await E(pricePushAdminA).pushResult({ roundId: 1, unitPrice: 100n }); + await E(pricePushAdminB).pushResult({ roundId: 1, unitPrice: 200n }); + await E(pricePushAdminC).pushResult({ roundId: 1, unitPrice: 300n }); await oracleTimer.tick(); const round1Attempt1 = await E(aggregator.creatorFacet).getRoundData(1); @@ -195,9 +195,9 @@ test('basic', async t => { // the restartDelay, which means its submission will be IGNORED. this means the median // should ONLY be between the OracleB and C values, which is why it is 25000 await oracleTimer.tick(); - await E(pricePushAdminA).pushResult({ roundId: 2, data: '1000' }); - await E(pricePushAdminB).pushResult({ roundId: 2, data: '2000' }); - await E(pricePushAdminC).pushResult({ roundId: 2, data: '3000' }); + await E(pricePushAdminA).pushResult({ roundId: 2, unitPrice: 1000n }); + await E(pricePushAdminB).pushResult({ roundId: 2, unitPrice: 2000n }); + await E(pricePushAdminC).pushResult({ roundId: 2, unitPrice: 3000n }); await oracleTimer.tick(); const round1Attempt2 = await E(aggregator.creatorFacet).getRoundData(1); @@ -209,9 +209,9 @@ test('basic', async t => { // unlike the previous test, if C initializes, all submissions should be recorded, // which means the median will be the expected 5000 here await oracleTimer.tick(); - await E(pricePushAdminC).pushResult({ roundId: 3, data: '5000' }); - await E(pricePushAdminA).pushResult({ roundId: 3, data: '4000' }); - await E(pricePushAdminB).pushResult({ roundId: 3, data: '6000' }); + await E(pricePushAdminC).pushResult({ roundId: 3, unitPrice: 5000n }); + await E(pricePushAdminA).pushResult({ roundId: 3, unitPrice: 4000n }); + await E(pricePushAdminB).pushResult({ roundId: 3, unitPrice: 6000n }); await oracleTimer.tick(); const round1Attempt3 = await E(aggregator.creatorFacet).getRoundData(1); @@ -248,11 +248,11 @@ test('timeout', async t => { // ----- round 1: basic consensus w/ ticking: should work EXACTLY the same await oracleTimer.tick(); - await E(pricePushAdminA).pushResult({ roundId: 1, data: '100' }); + await E(pricePushAdminA).pushResult({ roundId: 1, unitPrice: 100n }); await oracleTimer.tick(); - await E(pricePushAdminB).pushResult({ roundId: 1, data: '200' }); + await E(pricePushAdminB).pushResult({ roundId: 1, unitPrice: 200n }); await oracleTimer.tick(); - await E(pricePushAdminC).pushResult({ roundId: 1, data: '300' }); + await E(pricePushAdminC).pushResult({ roundId: 1, unitPrice: 300n }); const round1Attempt1 = await E(aggregator.creatorFacet).getRoundData(1); t.deepEqual(round1Attempt1.roundId, 1n); @@ -262,15 +262,15 @@ test('timeout', async t => { // timeout behavior is, if more ticks pass than the timeout param (5 here), the round is // considered "timedOut," at which point, the values are simply copied from the previous round await oracleTimer.tick(); - await E(pricePushAdminB).pushResult({ roundId: 2, data: '2000' }); + await E(pricePushAdminB).pushResult({ roundId: 2, unitPrice: 2000n }); await oracleTimer.tick(); await oracleTimer.tick(); await oracleTimer.tick(); await oracleTimer.tick(); await oracleTimer.tick(); await oracleTimer.tick(); // --- should time out here - await E(pricePushAdminC).pushResult({ roundId: 3, data: '1000' }); - await E(pricePushAdminA).pushResult({ roundId: 3, data: '3000' }); + await E(pricePushAdminC).pushResult({ roundId: 3, unitPrice: 1000n }); + await E(pricePushAdminA).pushResult({ roundId: 3, unitPrice: 3000n }); const round1Attempt2 = await E(aggregator.creatorFacet).getRoundData(1); t.deepEqual(round1Attempt2.answer, 200n); @@ -307,20 +307,20 @@ test('issue check', async t => { // ----- round 1: ignore too low values await oracleTimer.tick(); - await E(pricePushAdminA).pushResult({ roundId: 1, data: '50' }); // should be IGNORED + await E(pricePushAdminA).pushResult({ roundId: 1, unitPrice: 50n }); // should be IGNORED await oracleTimer.tick(); - await E(pricePushAdminB).pushResult({ roundId: 1, data: '200' }); + await E(pricePushAdminB).pushResult({ roundId: 1, unitPrice: 200n }); await oracleTimer.tick(); - await E(pricePushAdminC).pushResult({ roundId: 1, data: '300' }); + await E(pricePushAdminC).pushResult({ roundId: 1, unitPrice: 300n }); const round1Attempt1 = await E(aggregator.creatorFacet).getRoundData(1); t.deepEqual(round1Attempt1.answer, 250n); // ----- round 2: ignore too high values await oracleTimer.tick(); - await E(pricePushAdminB).pushResult({ roundId: 2, data: '20000' }); - await E(pricePushAdminC).pushResult({ roundId: 2, data: '1000' }); - await E(pricePushAdminA).pushResult({ roundId: 2, data: '3000' }); + await E(pricePushAdminB).pushResult({ roundId: 2, unitPrice: 20000n }); + await E(pricePushAdminC).pushResult({ roundId: 2, unitPrice: 1000n }); + await E(pricePushAdminA).pushResult({ roundId: 2, unitPrice: 3000n }); await oracleTimer.tick(); const round2Attempt1 = await E(aggregator.creatorFacet).getRoundData(2); @@ -354,9 +354,9 @@ test('supersede', async t => { // ----- round 1: round 1 is NOT supersedable when 3 submits, meaning it will be ignored await oracleTimer.tick(); - await E(pricePushAdminA).pushResult({ roundId: 1, data: '100' }); - await E(pricePushAdminC).pushResult({ roundId: 2, data: '300' }); - await E(pricePushAdminB).pushResult({ roundId: 1, data: '200' }); + await E(pricePushAdminA).pushResult({ roundId: 1, unitPrice: 100n }); + await E(pricePushAdminC).pushResult({ roundId: 2, unitPrice: 300n }); + await E(pricePushAdminB).pushResult({ roundId: 1, unitPrice: 200n }); await oracleTimer.tick(); const round1Attempt1 = await E(aggregator.creatorFacet).getRoundData(1); @@ -364,8 +364,8 @@ test('supersede', async t => { // ----- round 2: oracle C's value from before should have been IGNORED await oracleTimer.tick(); - await E(pricePushAdminB).pushResult({ roundId: 2, data: '2000' }); - await E(pricePushAdminA).pushResult({ roundId: 2, data: '1000' }); + await E(pricePushAdminB).pushResult({ roundId: 2, unitPrice: 2000n }); + await E(pricePushAdminA).pushResult({ roundId: 2, unitPrice: 1000n }); await oracleTimer.tick(); const round2Attempt1 = await E(aggregator.creatorFacet).getRoundData(2); @@ -373,7 +373,7 @@ test('supersede', async t => { // ----- round 3: oracle C should NOT be able to supersede round 3 await oracleTimer.tick(); - await E(pricePushAdminC).pushResult({ roundId: 4, data: '1000' }); + await E(pricePushAdminC).pushResult({ roundId: 4, unitPrice: 1000n }); try { await E(aggregator.creatorFacet).getRoundData(4); @@ -412,9 +412,9 @@ test('interleaved', async t => { // ----- round 1: we now need unanimous submission for a round for it to have consensus await oracleTimer.tick(); - await E(pricePushAdminA).pushResult({ roundId: 1, data: '100' }); - await E(pricePushAdminC).pushResult({ roundId: 2, data: '300' }); - await E(pricePushAdminB).pushResult({ roundId: 1, data: '200' }); + await E(pricePushAdminA).pushResult({ roundId: 1, unitPrice: 100n }); + await E(pricePushAdminC).pushResult({ roundId: 2, unitPrice: 300n }); + await E(pricePushAdminB).pushResult({ roundId: 1, unitPrice: 200n }); await oracleTimer.tick(); try { @@ -425,17 +425,17 @@ test('interleaved', async t => { // ----- round 2: interleaved round submission -- just making sure this works await oracleTimer.tick(); - await E(pricePushAdminC).pushResult({ roundId: 1, data: '300' }); + await E(pricePushAdminC).pushResult({ roundId: 1, unitPrice: 300n }); await oracleTimer.tick(); - await E(pricePushAdminB).pushResult({ roundId: 2, data: '2000' }); - await E(pricePushAdminA).pushResult({ roundId: 2, data: '1000' }); + await E(pricePushAdminB).pushResult({ roundId: 2, unitPrice: 2000n }); + await E(pricePushAdminA).pushResult({ roundId: 2, unitPrice: 1000n }); await oracleTimer.tick(); - await E(pricePushAdminC).pushResult({ roundId: 3, data: '9000' }); + await E(pricePushAdminC).pushResult({ roundId: 3, unitPrice: 9000n }); await oracleTimer.tick(); - await E(pricePushAdminC).pushResult({ roundId: 2, data: '3000' }); // assumes oracle C is going for a resubmission + await E(pricePushAdminC).pushResult({ roundId: 2, unitPrice: 3000n }); // assumes oracle C is going for a resubmission await oracleTimer.tick(); await oracleTimer.tick(); - await E(pricePushAdminA).pushResult({ roundId: 3, data: '5000' }); + await E(pricePushAdminA).pushResult({ roundId: 3, unitPrice: 5000n }); await oracleTimer.tick(); const round1Attempt2 = await E(aggregator.creatorFacet).getRoundData(1); @@ -458,9 +458,9 @@ test('interleaved', async t => { await oracleTimer.tick(); await oracleTimer.tick(); // round 3 is NOT yet supersedeable (since no value present and not yet timed out), so these should fail - await E(pricePushAdminA).pushResult({ roundId: 4, data: '4000' }); - await E(pricePushAdminB).pushResult({ roundId: 4, data: '5000' }); - await E(pricePushAdminC).pushResult({ roundId: 4, data: '6000' }); + await E(pricePushAdminA).pushResult({ roundId: 4, unitPrice: 4000n }); + await E(pricePushAdminB).pushResult({ roundId: 4, unitPrice: 5000n }); + await E(pricePushAdminC).pushResult({ roundId: 4, unitPrice: 6000n }); await oracleTimer.tick(); // --- round 3 has NOW timed out, meaning it is now supersedable try { @@ -476,9 +476,9 @@ test('interleaved', async t => { } // so NOW we should be able to submit round 4, and round 3 should just be copied from round 2 - await E(pricePushAdminA).pushResult({ roundId: 4, data: '4000' }); - await E(pricePushAdminB).pushResult({ roundId: 4, data: '5000' }); - await E(pricePushAdminC).pushResult({ roundId: 4, data: '6000' }); + await E(pricePushAdminA).pushResult({ roundId: 4, unitPrice: 4000n }); + await E(pricePushAdminB).pushResult({ roundId: 4, unitPrice: 5000n }); + await E(pricePushAdminC).pushResult({ roundId: 4, unitPrice: 6000n }); await oracleTimer.tick(); const round3Attempt3 = await E(aggregator.creatorFacet).getRoundData(3); @@ -488,7 +488,7 @@ test('interleaved', async t => { t.deepEqual(round4Attempt2.answer, 5000n); // ----- round 5: ping-ponging should be possible (although this is an unlikely pernicious case) - await E(pricePushAdminC).pushResult({ roundId: 5, data: '1000' }); + await E(pricePushAdminC).pushResult({ roundId: 5, unitPrice: 1000n }); await oracleTimer.tick(); await oracleTimer.tick(); await oracleTimer.tick(); @@ -496,14 +496,14 @@ test('interleaved', async t => { await oracleTimer.tick(); await oracleTimer.tick(); await oracleTimer.tick(); - await E(pricePushAdminA).pushResult({ roundId: 6, data: '1000' }); + await E(pricePushAdminA).pushResult({ roundId: 6, unitPrice: 1000n }); await oracleTimer.tick(); await oracleTimer.tick(); await oracleTimer.tick(); await oracleTimer.tick(); await oracleTimer.tick(); await oracleTimer.tick(); - await E(pricePushAdminC).pushResult({ roundId: 7, data: '1000' }); + await E(pricePushAdminC).pushResult({ roundId: 7, unitPrice: 1000n }); const round5Attempt1 = await E(aggregator.creatorFacet).getRoundData(5); const round6Attempt1 = await E(aggregator.creatorFacet).getRoundData(6); @@ -549,36 +549,36 @@ test('larger', async t => { // ----- round 1: usual case await oracleTimer.tick(); - await E(pricePushAdminA).pushResult({ roundId: 1, data: '100' }); - await E(pricePushAdminB).pushResult({ roundId: 1, data: '200' }); + await E(pricePushAdminA).pushResult({ roundId: 1, unitPrice: 100n }); + await E(pricePushAdminB).pushResult({ roundId: 1, unitPrice: 200n }); await oracleTimer.tick(); await oracleTimer.tick(); - await E(pricePushAdminC).pushResult({ roundId: 2, data: '1000' }); + await E(pricePushAdminC).pushResult({ roundId: 2, unitPrice: 1000n }); await oracleTimer.tick(); - await E(pricePushAdminD).pushResult({ roundId: 3, data: '3000' }); + await E(pricePushAdminD).pushResult({ roundId: 3, unitPrice: 3000n }); await oracleTimer.tick(); await oracleTimer.tick(); await oracleTimer.tick(); - await E(pricePushAdminE).pushResult({ roundId: 1, data: '300' }); + await E(pricePushAdminE).pushResult({ roundId: 1, unitPrice: 300n }); const round1Attempt1 = await E(aggregator.creatorFacet).getRoundData(1); t.deepEqual(round1Attempt1.answer, 200n); // ----- round 2: ignore late arrival await oracleTimer.tick(); - await E(pricePushAdminB).pushResult({ roundId: 2, data: '600' }); + await E(pricePushAdminB).pushResult({ roundId: 2, unitPrice: 600n }); await oracleTimer.tick(); - await E(pricePushAdminA).pushResult({ roundId: 2, data: '500' }); + await E(pricePushAdminA).pushResult({ roundId: 2, unitPrice: 500n }); await oracleTimer.tick(); - await E(pricePushAdminC).pushResult({ roundId: 3, data: '1000' }); + await E(pricePushAdminC).pushResult({ roundId: 3, unitPrice: 1000n }); await oracleTimer.tick(); - await E(pricePushAdminD).pushResult({ roundId: 1, data: '500' }); + await E(pricePushAdminD).pushResult({ roundId: 1, unitPrice: 500n }); await oracleTimer.tick(); await oracleTimer.tick(); await oracleTimer.tick(); - await E(pricePushAdminC).pushResult({ roundId: 2, data: '1000' }); + await E(pricePushAdminC).pushResult({ roundId: 2, unitPrice: 1000n }); await oracleTimer.tick(); - await E(pricePushAdminC).pushResult({ roundId: 1, data: '700' }); // this should be IGNORED since oracle C has already sent round 2 + await E(pricePushAdminC).pushResult({ roundId: 1, unitPrice: 700n }); // this should be IGNORED since oracle C has already sent round 2 const round1Attempt2 = await E(aggregator.creatorFacet).getRoundData(1); const round2Attempt1 = await E(aggregator.creatorFacet).getRoundData(2); @@ -615,9 +615,9 @@ test('suggest', async t => { // ----- round 1: basic consensus await oracleTimer.tick(); - await E(pricePushAdminA).pushResult({ roundId: 1, data: '100' }); - await E(pricePushAdminB).pushResult({ roundId: 1, data: '200' }); - await E(pricePushAdminC).pushResult({ roundId: 1, data: '300' }); + await E(pricePushAdminA).pushResult({ roundId: 1, unitPrice: 100n }); + await E(pricePushAdminB).pushResult({ roundId: 1, unitPrice: 200n }); + await E(pricePushAdminC).pushResult({ roundId: 1, unitPrice: 300n }); await oracleTimer.tick(); const round1Attempt1 = await E(aggregator.creatorFacet).getRoundData(1); @@ -626,7 +626,7 @@ test('suggest', async t => { // ----- round 2: add a new oracle and confirm the suggested round is correct await oracleTimer.tick(); - await E(pricePushAdminB).pushResult({ roundId: 2, data: '1000' }); + await E(pricePushAdminB).pushResult({ roundId: 2, unitPrice: 1000n }); const oracleCSuggestion = await E(aggregator.creatorFacet).oracleRoundState( priceOracleC.instance, 1n, @@ -646,10 +646,10 @@ test('suggest', async t => { t.deepEqual(oracleBSuggestion.oracleCount, 3); await oracleTimer.tick(); - await E(pricePushAdminA).pushResult({ roundId: 2, data: '2000' }); + await E(pricePushAdminA).pushResult({ roundId: 2, unitPrice: 2000n }); await oracleTimer.tick(); await oracleTimer.tick(); - await E(pricePushAdminC).pushResult({ roundId: 2, data: '3000' }); + await E(pricePushAdminC).pushResult({ roundId: 2, unitPrice: 3000n }); const oracleASuggestion = await E(aggregator.creatorFacet).oracleRoundState( priceOracleA.instance, @@ -661,12 +661,12 @@ test('suggest', async t => { t.deepEqual(oracleASuggestion.startedAt, 0n); // round 3 hasn't yet started, so it should be zeroed // ----- round 3: try using suggested round - await E(pricePushAdminC).pushResult({ roundId: 3, data: '100' }); + await E(pricePushAdminC).pushResult({ roundId: 3, unitPrice: 100n }); await oracleTimer.tick(); - await E(pricePushAdminA).pushResult({ roundId: undefined, data: '200' }); + await E(pricePushAdminA).pushResult({ roundId: undefined, unitPrice: 200n }); await oracleTimer.tick(); await oracleTimer.tick(); - await E(pricePushAdminB).pushResult({ roundId: undefined, data: '300' }); + await E(pricePushAdminB).pushResult({ roundId: undefined, unitPrice: 300n }); const round3Attempt1 = await E(aggregator.creatorFacet).getRoundData(3); t.deepEqual(round3Attempt1.roundId, 3n); @@ -703,12 +703,12 @@ test('notifications', async t => { ](); await oracleTimer.tick(); - await E(pricePushAdminA).pushResult({ roundId: 1, data: '100' }); + await E(pricePushAdminA).pushResult({ roundId: 1, unitPrice: 100n }); t.deepEqual((await eachLatestRound.next()).value, { roundId: 1n, startedAt: 1n, }); - await E(pricePushAdminB).pushResult({ roundId: 1, data: '200' }); + await E(pricePushAdminB).pushResult({ roundId: 1, unitPrice: 200n }); await eventLoopIteration(); t.deepEqual( @@ -726,7 +726,7 @@ test('notifications', async t => { }, ); - await E(pricePushAdminA).pushResult({ roundId: 2, data: '1000' }); + await E(pricePushAdminA).pushResult({ roundId: 2, unitPrice: 1000n }); // A started last round so fails to start next round t.deepEqual( // subscribe fresh because the iterator won't advance yet @@ -737,14 +737,14 @@ test('notifications', async t => { }, ); // B gets to start it - await E(pricePushAdminB).pushResult({ roundId: 2, data: '1000' }); + await E(pricePushAdminB).pushResult({ roundId: 2, unitPrice: 1000n }); // now it's roundId=2 t.deepEqual((await eachLatestRound.next()).value, { roundId: 2n, startedAt: 1n, }); // A joins in - await E(pricePushAdminA).pushResult({ roundId: 2, data: '1000' }); + await E(pricePushAdminA).pushResult({ roundId: 2, unitPrice: 1000n }); // writes to storage t.deepEqual( aggregator.mockStorageRoot.getBody( @@ -770,7 +770,7 @@ test('notifications', async t => { ); // A can start again - await E(pricePushAdminA).pushResult({ roundId: 3, data: '1000' }); + await E(pricePushAdminA).pushResult({ roundId: 3, unitPrice: 1000n }); t.deepEqual((await eachLatestRound.next()).value, { roundId: 3n, startedAt: 1n, From 61662e7d147816df1e1672bc4e86dee697f0aa8c Mon Sep 17 00:00:00 2001 From: Turadg Aleahmad Date: Mon, 19 Dec 2022 16:06:42 -0800 Subject: [PATCH 24/26] refactor(chainlink): simplify pushResult fn --- .../zoe/src/contracts/priceAggregatorChainlink.js | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/packages/zoe/src/contracts/priceAggregatorChainlink.js b/packages/zoe/src/contracts/priceAggregatorChainlink.js index c5ac014705e..850dc4cdb6e 100644 --- a/packages/zoe/src/contracts/priceAggregatorChainlink.js +++ b/packages/zoe/src/contracts/priceAggregatorChainlink.js @@ -849,17 +849,7 @@ const start = async (zcf, privateArgs) => { keyToRecords.delete(oracleAddr); } }, - async pushResult({ - roundId: roundIdRaw = undefined, - unitPrice: submissionRaw, - }) { - // Sample of NaN, 0, or negative numbers get culled in - // the median calculation. - pushResult({ - roundId: roundIdRaw, - unitPrice: submissionRaw, - }).catch(console.error); - }, + pushResult, }); return harden(oracleAdmin); From 21018a18c8b643394797450a2c9840625e192929 Mon Sep 17 00:00:00 2001 From: Turadg Aleahmad Date: Wed, 21 Dec 2022 11:40:49 -0800 Subject: [PATCH 25/26] refactor: rename PushPrice invitationMaker --- packages/agoric-cli/src/commands/oracle.js | 4 ++-- .../test/smartWallet/test-oracle-integration.js | 2 +- packages/zoe/src/contracts/priceAggregator.js | 4 ++-- packages/zoe/src/contracts/priceAggregatorChainlink.js | 4 ++-- packages/zoe/test/unitTests/contracts/test-priceAggregator.js | 4 ++-- 5 files changed, 9 insertions(+), 9 deletions(-) diff --git a/packages/agoric-cli/src/commands/oracle.js b/packages/agoric-cli/src/commands/oracle.js index d09bf6d1577..99009bbda53 100644 --- a/packages/agoric-cli/src/commands/oracle.js +++ b/packages/agoric-cli/src/commands/oracle.js @@ -114,7 +114,7 @@ export const makeOracleCommand = async logger => { invitationSpec: { source: 'continuing', previousOffer: opts.oracleAdminAcceptOfferId, - invitationMakerName: 'makePushPriceInvitation', + invitationMakerName: 'PushPrice', invitationArgs: harden([opts.price]), }, proposal: {}, @@ -149,7 +149,7 @@ export const makeOracleCommand = async logger => { invitationSpec: { source: 'continuing', previousOffer: opts.oracleAdminAcceptOfferId, - invitationMakerName: 'makePushPriceInvitation', + invitationMakerName: 'PushPrice', invitationArgs: harden([ { unitPrice: opts.price, roundId: opts.roundId }, ]), diff --git a/packages/inter-protocol/test/smartWallet/test-oracle-integration.js b/packages/inter-protocol/test/smartWallet/test-oracle-integration.js index a862be1464c..ebfcc76695b 100644 --- a/packages/inter-protocol/test/smartWallet/test-oracle-integration.js +++ b/packages/inter-protocol/test/smartWallet/test-oracle-integration.js @@ -162,7 +162,7 @@ test('admin price', async t => { const proposeInvitationSpec = { source: 'continuing', previousOffer: 44, - invitationMakerName: 'makePushPriceInvitation', + invitationMakerName: 'PushPrice', invitationArgs: harden([result]), }; diff --git a/packages/zoe/src/contracts/priceAggregator.js b/packages/zoe/src/contracts/priceAggregator.js index 6e468f971af..457d9da4a31 100644 --- a/packages/zoe/src/contracts/priceAggregator.js +++ b/packages/zoe/src/contracts/priceAggregator.js @@ -434,7 +434,7 @@ const start = async (zcf, privateArgs) => { * @param {object} param1 * @param {Notifier} [param1.notifier] optional notifier that produces oracle price submissions * @param {number} [param1.scaleValueOut] - * @returns {Promise<{admin: OracleAdmin, invitationMakers: {makePushPriceInvitation: (price: ParsableNumber) => Promise>} }>} + * @returns {Promise<{admin: OracleAdmin, invitationMakers: {PushPrice: (price: ParsableNumber) => Promise>} }>} */ const offerHandler = async ( seat, @@ -443,7 +443,7 @@ const start = async (zcf, privateArgs) => { const admin = await creatorFacet.initOracle(oracleKey); const invitationMakers = Far('invitation makers', { /** @param {ParsableNumber} price */ - makePushPriceInvitation(price) { + PushPrice(price) { assertParsableNumber(price); return zcf.makeInvitation(cSeat => { cSeat.exit(); diff --git a/packages/zoe/src/contracts/priceAggregatorChainlink.js b/packages/zoe/src/contracts/priceAggregatorChainlink.js index 850dc4cdb6e..4195867388c 100644 --- a/packages/zoe/src/contracts/priceAggregatorChainlink.js +++ b/packages/zoe/src/contracts/priceAggregatorChainlink.js @@ -717,13 +717,13 @@ const start = async (zcf, privateArgs) => { * reported data. * * @param {ZCFSeat} seat - * @returns {Promise<{admin: OracleAdmin, invitationMakers: {makePushPriceInvitation: (result: PriceRound) => Promise>} }>} + * @returns {Promise<{admin: OracleAdmin, invitationMakers: {PushPrice: (result: PriceRound) => Promise>} }>} */ const offerHandler = async seat => { const admin = await creatorFacet.initOracle(oracleAddr); const invitationMakers = Far('invitation makers', { /** @param {PriceRound} result */ - makePushPriceInvitation(result) { + PushPrice(result) { assertParsableNumber(result.unitPrice); return zcf.makeInvitation(cSeat => { cSeat.exit(); diff --git a/packages/zoe/test/unitTests/contracts/test-priceAggregator.js b/packages/zoe/test/unitTests/contracts/test-priceAggregator.js index c23d6c061a2..e0b8a668d75 100644 --- a/packages/zoe/test/unitTests/contracts/test-priceAggregator.js +++ b/packages/zoe/test/unitTests/contracts/test-priceAggregator.js @@ -559,7 +559,7 @@ test('oracle continuing invitation', async t => { const or1 = E(zoe).offer(inv1, undefined, undefined, { notifier: oracle1 }); const oracleAdmin1 = E(or1).getOfferResult(); const invitationMakers = await E.get(oracleAdmin1).invitationMakers; - t.true('makePushPriceInvitation' in invitationMakers); + t.true('PushPrice' in invitationMakers); const amountIn = AmountMath.make(brandIn, 1000000n); const makeQuoteValue = (timestamp, valueOut) => [ @@ -575,7 +575,7 @@ test('oracle continuing invitation', async t => { E(aggregator.publicFacet).getPriceAuthority(), ).makeQuoteNotifier(amountIn, brandOut); - const invPrice = await E(invitationMakers).makePushPriceInvitation('1234'); + const invPrice = await E(invitationMakers).PushPrice('1234'); const invPriceResult = await E(zoe).offer(invPrice); t.deepEqual(await E(invPriceResult).numWantsSatisfied(), 1); From 8e61373a0ca8c6afc0b2f27a3568011312624c14 Mon Sep 17 00:00:00 2001 From: Turadg Aleahmad Date: Wed, 21 Dec 2022 11:53:02 -0800 Subject: [PATCH 26/26] chore(chainlink)!: only smart-wallet oracles --- .../src/contracts/priceAggregatorChainlink.js | 17 +- .../test-priceAggregatorChainlink.js | 148 +++++------------- 2 files changed, 45 insertions(+), 120 deletions(-) diff --git a/packages/zoe/src/contracts/priceAggregatorChainlink.js b/packages/zoe/src/contracts/priceAggregatorChainlink.js index 4195867388c..7bd245e6c2b 100644 --- a/packages/zoe/src/contracts/priceAggregatorChainlink.js +++ b/packages/zoe/src/contracts/priceAggregatorChainlink.js @@ -691,13 +691,6 @@ const start = async (zcf, privateArgs) => { return add(currentRound, 1); }; - /** - * @type {Omit & { - * initOracle: (instance) => Promise>, - * getRoundData(roundId: bigint | number): Promise, - * oracleRoundState(oracleAddr: string, queriedRoundId: BigInt): Promise - * }} - */ const creatorFacet = Far('PriceAggregatorChainlinkCreatorFacet', { /** * An "oracle invitation" is an invitation to be able to submit data to @@ -707,7 +700,7 @@ const start = async (zcf, privateArgs) => { * directly to manage the price submissions as well as to terminate the * relationship. * - * @param {string} oracleAddr + * @param {string} oracleAddr Bech32 of oracle operator smart wallet */ makeOracleInvitation: async oracleAddr => { /** @@ -756,8 +749,12 @@ const start = async (zcf, privateArgs) => { }, // unlike the median case, no query argument is passed, since polling behavior is undesired - /** @param {string} oracleAddr */ + /** + * @param {string} oracleAddr Bech32 of oracle operator smart wallet + * @returns {Promise>} + */ async initOracle(oracleAddr) { + assert.typeof(oracleAddr, 'string'); /** @type {OracleRecord} */ const record = { querier: undefined, lastSample: 0 }; @@ -885,7 +882,7 @@ const start = async (zcf, privateArgs) => { * a method to provide all current info oracleStatuses need. Intended only * only to be callable by oracleStatuses. Not for use by contracts to read state. * - * @param {string} oracleAddr + * @param {string} oracleAddr Bech32 of oracle operator smart wallet * @param {bigint} queriedRoundId */ async oracleRoundState(oracleAddr, queriedRoundId) { diff --git a/packages/zoe/test/unitTests/contracts/test-priceAggregatorChainlink.js b/packages/zoe/test/unitTests/contracts/test-priceAggregatorChainlink.js index 4a81e013e56..77d09fa8838 100644 --- a/packages/zoe/test/unitTests/contracts/test-priceAggregatorChainlink.js +++ b/packages/zoe/test/unitTests/contracts/test-priceAggregatorChainlink.js @@ -7,7 +7,7 @@ import bundleSource from '@endo/bundle-source'; import { E } from '@endo/eventual-send'; import { Far } from '@endo/marshal'; -import { makeIssuerKit, AssetKind, AmountMath } from '@agoric/ertp'; +import { makeIssuerKit, AssetKind } from '@agoric/ertp'; import { eventLoopIteration, @@ -65,8 +65,6 @@ const makeContext = async () => { // else, and they can use it to create a new contract instance // using the same code. vatAdminState.installBundle('b1-oracle', oracleBundle); - /** @type {Installation} */ - const oracleInstallation = await E(zoe).installBundleID('b1-oracle'); vatAdminState.installBundle('b1-aggregator', aggregatorBundle); /** @type {Installation} */ const aggregatorInstallation = await E(zoe).installBundleID('b1-aggregator'); @@ -74,43 +72,6 @@ const makeContext = async () => { const link = makeIssuerKit('$LINK', AssetKind.NAT); const usd = makeIssuerKit('$USD', AssetKind.NAT); - /** - * @param {bigint} [valueOut] - * @returns {Promise} - */ - const makeFakePriceOracle = async valueOut => { - /** @type {OracleHandler} */ - const oracleHandler = Far('OracleHandler', { - async onQuery({ increment }, _fee) { - assert(valueOut); - assert(increment); - valueOut += increment; - return harden({ - reply: `${valueOut}`, - requiredFee: AmountMath.makeEmpty(link.brand), - }); - }, - onError(query, reason) { - console.error('query', query, 'failed with', reason); - }, - onReply(_query, _reply) {}, - }); - - const startResult = await E(zoe).startInstance( - oracleInstallation, - { Fee: link.issuer }, - { oracleDescription: 'myOracle' }, - ); - const creatorFacet = await E(startResult.creatorFacet).initialize({ - oracleHandler, - }); - - return harden({ - ...startResult, - creatorFacet, - }); - }; - async function makeChainlinkAggregator(config) { const { maxSubmissionCount, @@ -150,7 +111,7 @@ const makeContext = async () => { return { ...aggregator, mockStorageRoot }; } - return { makeChainlinkAggregator, makeFakePriceOracle, zoe }; + return { makeChainlinkAggregator, zoe }; }; test.before('setup aggregator and oracles', async t => { @@ -158,25 +119,21 @@ test.before('setup aggregator and oracles', async t => { }); test('basic', async t => { - const { makeFakePriceOracle, zoe } = t.context; + const { zoe } = t.context; const aggregator = await t.context.makeChainlinkAggregator(defaultConfig); /** @type {{ timer: ManualTimer }} */ // @ts-expect-error cast const { timer: oracleTimer } = await E(zoe).getTerms(aggregator.instance); - const priceOracleA = await makeFakePriceOracle(); - const priceOracleB = await makeFakePriceOracle(); - const priceOracleC = await makeFakePriceOracle(); - const pricePushAdminA = await E(aggregator.creatorFacet).initOracle( - priceOracleA.instance, + 'agorice1priceOracleA', ); const pricePushAdminB = await E(aggregator.creatorFacet).initOracle( - priceOracleB.instance, + 'agorice1priceOracleB', ); const pricePushAdminC = await E(aggregator.creatorFacet).initOracle( - priceOracleC.instance, + 'agorice1priceOracleC', ); // ----- round 1: basic consensus @@ -221,7 +178,7 @@ test('basic', async t => { }); test('timeout', async t => { - const { makeFakePriceOracle, zoe } = t.context; + const { zoe } = t.context; const aggregator = await t.context.makeChainlinkAggregator({ ...defaultConfig, @@ -232,18 +189,14 @@ test('timeout', async t => { // @ts-expect-error cast const { timer: oracleTimer } = await E(zoe).getTerms(aggregator.instance); - const priceOracleA = await makeFakePriceOracle(); - const priceOracleB = await makeFakePriceOracle(); - const priceOracleC = await makeFakePriceOracle(); - const pricePushAdminA = await E(aggregator.creatorFacet).initOracle( - priceOracleA.instance, + 'agorice1priceOracleA', ); const pricePushAdminB = await E(aggregator.creatorFacet).initOracle( - priceOracleB.instance, + 'agorice1priceOracleB', ); const pricePushAdminC = await E(aggregator.creatorFacet).initOracle( - priceOracleC.instance, + 'agorice1priceOracleC', ); // ----- round 1: basic consensus w/ ticking: should work EXACTLY the same @@ -281,7 +234,7 @@ test('timeout', async t => { }); test('issue check', async t => { - const { makeFakePriceOracle, zoe } = t.context; + const { zoe } = t.context; const aggregator = await t.context.makeChainlinkAggregator({ ...defaultConfig, @@ -291,18 +244,14 @@ test('issue check', async t => { // @ts-expect-error cast const { timer: oracleTimer } = await E(zoe).getTerms(aggregator.instance); - const priceOracleA = await makeFakePriceOracle(); - const priceOracleB = await makeFakePriceOracle(); - const priceOracleC = await makeFakePriceOracle(); - const pricePushAdminA = await E(aggregator.creatorFacet).initOracle( - priceOracleA.instance, + 'agorice1priceOracleA', ); const pricePushAdminB = await E(aggregator.creatorFacet).initOracle( - priceOracleB.instance, + 'agorice1priceOracleB', ); const pricePushAdminC = await E(aggregator.creatorFacet).initOracle( - priceOracleC.instance, + 'agorice1priceOracleC', ); // ----- round 1: ignore too low values @@ -328,7 +277,7 @@ test('issue check', async t => { }); test('supersede', async t => { - const { makeFakePriceOracle, zoe } = t.context; + const { zoe } = t.context; const aggregator = await t.context.makeChainlinkAggregator({ ...defaultConfig, @@ -338,18 +287,14 @@ test('supersede', async t => { // @ts-expect-error cast const { timer: oracleTimer } = await E(zoe).getTerms(aggregator.instance); - const priceOracleA = await makeFakePriceOracle(); - const priceOracleB = await makeFakePriceOracle(); - const priceOracleC = await makeFakePriceOracle(); - const pricePushAdminA = await E(aggregator.creatorFacet).initOracle( - priceOracleA.instance, + 'agorice1priceOracleA', ); const pricePushAdminB = await E(aggregator.creatorFacet).initOracle( - priceOracleB.instance, + 'agorice1priceOracleB', ); const pricePushAdminC = await E(aggregator.creatorFacet).initOracle( - priceOracleC.instance, + 'agorice1priceOracleC', ); // ----- round 1: round 1 is NOT supersedable when 3 submits, meaning it will be ignored @@ -383,7 +328,7 @@ test('supersede', async t => { }); test('interleaved', async t => { - const { makeFakePriceOracle, zoe } = t.context; + const { zoe } = t.context; const aggregator = await t.context.makeChainlinkAggregator({ ...defaultConfig, @@ -396,18 +341,14 @@ test('interleaved', async t => { // @ts-expect-error cast const { timer: oracleTimer } = await E(zoe).getTerms(aggregator.instance); - const priceOracleA = await makeFakePriceOracle(); - const priceOracleB = await makeFakePriceOracle(); - const priceOracleC = await makeFakePriceOracle(); - const pricePushAdminA = await E(aggregator.creatorFacet).initOracle( - priceOracleA.instance, + 'agorice1priceOracleA', ); const pricePushAdminB = await E(aggregator.creatorFacet).initOracle( - priceOracleB.instance, + 'agorice1priceOracleB', ); const pricePushAdminC = await E(aggregator.creatorFacet).initOracle( - priceOracleC.instance, + 'agorice1priceOracleC', ); // ----- round 1: we now need unanimous submission for a round for it to have consensus @@ -513,7 +454,7 @@ test('interleaved', async t => { }); test('larger', async t => { - const { makeFakePriceOracle, zoe } = t.context; + const { zoe } = t.context; const aggregator = await t.context.makeChainlinkAggregator({ ...defaultConfig, @@ -525,26 +466,20 @@ test('larger', async t => { // @ts-expect-error cast const { timer: oracleTimer } = await E(zoe).getTerms(aggregator.instance); - const priceOracleA = await makeFakePriceOracle(); - const priceOracleB = await makeFakePriceOracle(); - const priceOracleC = await makeFakePriceOracle(); - const priceOracleD = await makeFakePriceOracle(); - const priceOracleE = await makeFakePriceOracle(); - const pricePushAdminA = await E(aggregator.creatorFacet).initOracle( - priceOracleA.instance, + 'agorice1priceOracleA', ); const pricePushAdminB = await E(aggregator.creatorFacet).initOracle( - priceOracleB.instance, + 'agorice1priceOracleB', ); const pricePushAdminC = await E(aggregator.creatorFacet).initOracle( - priceOracleC.instance, + 'agorice1priceOracleC', ); const pricePushAdminD = await E(aggregator.creatorFacet).initOracle( - priceOracleD.instance, + 'agorice1priceOracleD', ); const pricePushAdminE = await E(aggregator.creatorFacet).initOracle( - priceOracleE.instance, + 'agorice1priceOracleE', ); // ----- round 1: usual case @@ -587,7 +522,7 @@ test('larger', async t => { }); test('suggest', async t => { - const { makeFakePriceOracle, zoe } = t.context; + const { zoe } = t.context; const aggregator = await t.context.makeChainlinkAggregator({ ...defaultConfig, @@ -599,18 +534,14 @@ test('suggest', async t => { // @ts-expect-error cast const { timer: oracleTimer } = await E(zoe).getTerms(aggregator.instance); - const priceOracleA = await makeFakePriceOracle(); - const priceOracleB = await makeFakePriceOracle(); - const priceOracleC = await makeFakePriceOracle(); - const pricePushAdminA = await E(aggregator.creatorFacet).initOracle( - priceOracleA.instance, + 'agorice1priceOracleA', ); const pricePushAdminB = await E(aggregator.creatorFacet).initOracle( - priceOracleB.instance, + 'agorice1priceOracleB', ); const pricePushAdminC = await E(aggregator.creatorFacet).initOracle( - priceOracleC.instance, + 'agorice1priceOracleC', ); // ----- round 1: basic consensus @@ -628,7 +559,7 @@ test('suggest', async t => { await oracleTimer.tick(); await E(pricePushAdminB).pushResult({ roundId: 2, unitPrice: 1000n }); const oracleCSuggestion = await E(aggregator.creatorFacet).oracleRoundState( - priceOracleC.instance, + 'agorice1priceOracleC', 1n, ); @@ -637,7 +568,7 @@ test('suggest', async t => { t.deepEqual(oracleCSuggestion.oracleCount, 3); const oracleBSuggestion = await E(aggregator.creatorFacet).oracleRoundState( - priceOracleB.instance, + 'agorice1priceOracleB', 0n, ); @@ -652,7 +583,7 @@ test('suggest', async t => { await E(pricePushAdminC).pushResult({ roundId: 2, unitPrice: 3000n }); const oracleASuggestion = await E(aggregator.creatorFacet).oracleRoundState( - priceOracleA.instance, + 'agorice1priceOracleA', 0n, ); @@ -674,7 +605,7 @@ test('suggest', async t => { }); test('notifications', async t => { - const { makeFakePriceOracle, zoe } = t.context; + const { zoe } = t.context; const aggregator = await t.context.makeChainlinkAggregator({ ...defaultConfig, @@ -685,14 +616,11 @@ test('notifications', async t => { // @ts-expect-error cast const { timer: oracleTimer } = await E(zoe).getTerms(aggregator.instance); - const priceOracleA = await makeFakePriceOracle(); - const priceOracleB = await makeFakePriceOracle(); - const pricePushAdminA = await E(aggregator.creatorFacet).initOracle( - priceOracleA.instance, + 'agorice1priceOracleA', ); const pricePushAdminB = await E(aggregator.creatorFacet).initOracle( - priceOracleB.instance, + 'agorice1priceOracleB', ); const latestRoundSubscriber = await E(